mako-emob 0.20.0

NZR-EMob / Modell 2 allocation engine — the virtual Bilanzierungsgebiet for charge-session-exact supplier allocation (BK6-20-160 Anlage 6, BK6-24-267)
Documentation
# mako-emob

**NZR-EMob / Modell 2 — the virtual Bilanzierungsgebiet**

„Bring your own supplier" at a charge point, per BNetzA **BK6-20-160 Anlage 6**
(„NZR-EMob"), the BDEW Anwendungshilfe *Zum Modell 2* V1.3 and Beschluss
**BK6-24-267**. A Ladepunktbetreiber runs a regelzonenweites
Bilanzierungsgebiet of its own and books every charging session into the
Bilanzkreis of the supplier the customer chose — quarter hour by quarter hour,
across DSO borders, with intraday supplier changes.

This crate holds the **allocation engine, its invariants, and the three
Modellwechsel legs**: pure domain, no I/O, no wire rendering. The
decision trees live in `mako_pruefung::emob` and the answer Fristen in
`mako_fristen::antwort::EMOB`, because that is where every other market process
keeps them.

## Five things this crate exists to get right

### 1. The Deltamenge is a term in the identity, not a rounding error

```text
NGZ(t, richtung) = Σ Zuordnungen + Deltamenge        exactly, every ¼ h
```

Anlage 6 §IV.1 obliges the LPB to assign the **whole** Bilanzierungsgebiet every
quarter hour. Whatever no Marktlokation claimed is the Deltamenge, and §IV.2
books it to a Bilanzkreis the LPB names, **at the LPB's own cost**. It settles
in money, so `QuarterHourAllocation` reports it as a field and returns a
`ConservationProof` beside every row rather than asserting it away.

The arithmetic is `metering::allocation::allocate` with each claim capped at
itself — reused rather than reimplemented, because that crate's
`Σ allocated + residual = total` identity *is* this one and its residual *is*
the Deltamenge. One call covers both directions of imbalance:

| Case | Result | Delta |
|---|---|---|
| claims **under** the NGZ | every claim met in full | `NGZ − Σ claims` |
| claims **over** the NGZ | every claim cut back in proportion | zero up to the six-decimal cut, and `Ueberdeckung` is recorded |

Over-claiming is real — local generation behind the Netzanschluss feeds the
charge points — and no published rule resolves it. Proportional cut-back is this
crate's stated default and is recorded on the result, because an operator whose
claims routinely exceed the NGZ has a metering problem, not a rounding one.

### 2. The Anmeldung answer window is seven Werktage

Three Werktage is the window on the other two legs: the LF's answer to a 55240,
and the VNB's answer to an **Abmeldung** (55242 → 55243), which has no LF leg to
wait for. It cannot work for the Anmeldung — the VNB may spend 3 WT sending the
55240 and the LF has 3 WT of its own, so `E_0510` Prüfschritt 1 („Ging innerhalb
der Antwortfrist eine Ablehnung des Lieferanten ein?") is undecidable before the
6. WT. The windows and a `const` assertion that pins the relation live in
`mako_fristen::antwort`.

### 3. Bezug and Einspeisung never net

`Richtung` is part of the allocation key and each direction carries its own
non-negative pool. Netting them inside a quarter hour would let a V2G discharge
cancel a neighbour's draw, and both would vanish from their suppliers'
Bilanzkreise. The Zeitreihentyp the BIKO expects for an Einspeisungs-BK-SZR eMob
is still open, so a deployment holds those rows — but the direction is modelled
regardless, because the alternative is silent netting.

### 4. Two identifiers are deliberately not the market's

`VirtualMaloId::new` **refuses an eleven-digit value**. AWH Kap. 1.6.1 lets the
LPB use its Stromnetzbetreibernummer for Zählpunktbildung und BG-Beantragung and
for nothing else; nothing grants it a MaLo-ID range for the per-vehicle objects
it needs internally. A virtual Marktlokation that looked like a BDEW MaLo-ID
would collide with a real one the day a Netzbetreiber issued it, and the
collision would surface as energy booked to a stranger's Bilanzkreis.

`TokenRef` carries an opaque keyed hash, never an RFID UID or an eMAID. Those
identify a natural person's charging contract across every operator they visit;
the allocation only needs to know which virtual MaLo a session belongs to, which
is a lookup the token registry already performed.

### 5. Silence is not consent

Neither Anlage 6 nor the AWH gives an unanswered leg a default outcome. The
GPKE Beendigung der Zuordnung has one — „Verstreicht die Frist …, gilt dies als
Bestätigung" — and Modell 2 has nothing of the kind, so an expired window puts
the process in `Eskaliert` and waits for an operator. Confirming would move a
Marktlokation between Bilanzierungsgebieten on no one's say-so.

## The three legs

One state machine, three workflow names, because all three run on the **same**
Marktlokation and a process resolves by (business key, workflow name):

| Workflow | Request → Answer | Tree | Frist | Answered by |
|---|---|---|---|---|
| `emob-anmeldung` | 55238 → 55239 | `E_0513``E_0510` | 7 WT | NB (VNB) |
| `emob-zuordnungsende` | 55240 → 55241 | `E_0511` | 3 WT | **LF** |
| `emob-abmeldung` | 55242 → 55243 | `E_0512` | 3 WT | NB (VNB) |

The 55240 leg runs *inside* the Anmeldung's own window, which is why that
window is seven Werktage. An answer carries both its code and the tree that
produced it into `SG4 STS+E01` DE 9013/1131: `A01` means opposite things in
`E_0510` and `E_0511`, so the pair is the smallest unit that means anything.

`mako_emob::EmobModule` registers the six Prüfidentifikatoren; `makod` routes,
adapts and renders them.

## The quarter-hour grid needs no DST special case

A `Viertelstunde` is an instant plus fifteen minutes of real time, and German
local time is offset from UTC by whole hours — so a UTC-aligned quarter hour is
aligned in Europe/Berlin too, and the 92- and 100-slot days are simply days with
fewer or more instants in them. No day is assumed to hold 96 of them.

## Provenance rides on every value

A charge point reporting **clock-aligned meter values** every 900 s (OCPP
`AlignedDataCtrlr`) measures each quarter hour. A **CDR** reports one total and
splitting it assumes constant power, which a tapering charge curve violates —
an estimate wearing the shape of a measurement, whose error lands on whichever
supplier held the slot boundary. `Provenance` is therefore on every allocated
value, and `SessionSplit` reports what the six-decimal cut could not place
instead of dropping it.

## Prüfidentifikatoren

| PID | Message | From → To | Answered by |
|---|---|---|---|
| 55238 | Anmeldung in Modell 2 | NB (LPB) → NB (VNB) | 55239, `E_0513``E_0514``E_0510` |
| 55240 | Beendigung der Zuordnung zur MaLo | NB (VNB) → LF | 55241, `E_0511` |
| 55242 | Abmeldung aus dem Modell 2 | NB (LPB) → NB (VNB) | 55243, `E_0512` |
| 55235 / 55236 | Zuordnung / Beendigung ZP der NGZ zur NZR | verantw. NB → benachb. NB, ÜNB | 55237, `E_0102` / `E_0103` (MaBiS) |
| 13018 | MSCONS Netzgangzeitreihe | NB (VNB) → NB (LPB), ÜNB ||

`A01` is an **Ablehnung** in `E_0510` and a **Zustimmung** in `E_0511` and
`E_0512`. The three run in the same process and a combined VNB+LF deployment
walks all of them, so every lookup is keyed on `(ebd, code)`.

The market role on the wire is always **NB** — the BDEW Rollenmodell defines no
LPB („der LPB kommuniziert aus prozessualer Sicht wie die Rolle NB", AWH
Kap. 1.4). `mako_engine::marktrolle::Marktrolle::Lpb` is a *deployment* role, the
same pattern as `Nmsb`/`Amsb` and `Lfn`/`Lfa`.

## Sources

- **BK6-20-160** (21.12.2020) Anlage 6 „NZR-EMob"; Mitteilung Nr. 4 (03.05.2022)
- **BDEW AWH „Zum Modell 2 …" V1.3** (01.04.2025)
- **BDEW AWH Ergänzung der Marktregeln … (MaBiS)** V1.0 (27.04.2022)
- **BK6-24-267** (15.05.2025), bestandskräftig
- **UTILMD AHB Strom 2.2** Kap. 11; **EBD 4.3** Kap. 17
- **MaBiS** BK6-24-174 Anlage 3, Kap. 3.8 / 3.10 / 5

## Related crates

| Crate | Role |
|---|---|
| [`mako-emob`]https://docs.rs/mako-emob**this crate** | The Modell 2 allocation engine, its invariants and the three Modellwechsel legs |
| [`mako-engine`]https://docs.rs/mako-engine | Event-sourced workflow runtime — `Workflow`, `Process`, `EventStore`, deadlines |
| [`mako-fristen`]https://docs.rs/mako-fristen | *When* an answer is due — Werktage, the MaKo holiday calendar, the per-PID Antwortfristen |
| [`mako-pruefung`]https://docs.rs/mako-pruefung | Where the `E_0510``E_0513` Entscheidungsbäume live (`mako_pruefung::emob`) |
| [`mako-mabis`]https://docs.rs/mako-mabis | The Summenzeitreihen the virtual Bilanzierungsgebiet settles through |
| [`makod`]https://hupe1980.github.io/mako/docs/services/makod/ | Production daemon — routes, adapts and renders these workflows |

Part of **mako**, an open-source Rust platform for German energy market
communication (Marktkommunikation). Full documentation: <https://hupe1980.github.io/mako/>

## License

MIT OR Apache-2.0