⚡ metering
German energy metering domain library for Rust. Europe/Berlin calendar arithmetic (including the 06:00 Gastag), Zählerstandsgang → Lastgang, gas m³→kWh_Hs and the SigLinDe gas SLP, Ersatzwertbildung, a robust validation engine, EN 50160, §14a Modul 3 tariff registers, virtual meters (§42b EnWG), check-digit-validated MaLo-IDs and Jahresprognose.
🧊 Zero I/O · ⏱️ no async · 🕰️ no clock · 🔢 exact decimal quantities
It computes quantities, not money: what leaves this crate is kWh, m³ and kW, which a billing layer then prices.
📖 Documentation & guides · 🦀 API reference
📦 Installation
# ...or with serde for every public type:
MSRV: Rust 1.94 (edition 2024), pinned in
rust-toolchain.toml and verified by a CI lane.
🚀 Quick start
use ;
use dec;
use datetime;
let intervals = vec!;
let period = aggregate;
println!;
println!;
println!;
println!;
🗓️ Why this exists: a German day is not 24 hours
The single most consequential thing the crate gets right.
| Day | Length | Quarter-hours |
|---|---|---|
| ordinary | 24 h | 96 |
| last Sunday in March (spring forward) | 23 h | 92 |
| last Sunday in October (fall back) | 25 h | 100 |
use ;
use ;
assert_eq!;
assert_eq!;
// March 2026 holds 2 972 quarter-hours, not 31 × 96 = 2 976.
assert_eq!;
// A German day starts at 23:00 UTC in winter, 22:00 UTC in summer.
assert_eq!;
assert_eq!;
A completeness check built on a hard-coded 96 raises a false alarm every spring and — worse — hides a genuine four-interval gap every autumn, because 96 of an expected 100 looks complete.
The whole pipeline, end to end
examples/pipeline.rs walks one Liefertag from the
Zählerstandsgang the gateway delivered through to the §14a register split —
differencing, validation, Ersatzwertbildung, aggregation and grading — on the
25-hour autumn DST day, with a wrapped register and a corrupt reading planted in
it. It asserts its own invariants, so CI runs it as a test.
🧭 What's in it
| Area | What it does | Guide |
|---|---|---|
| Calendar | Berlin days, months, years and the 06:00 Gastag; DST-correct interval counts; Bundesland holidays | → |
| Readings | Zählerstandsgang → Lastgang, register rollover, meter exchange | → |
| Identifiers | MaloId with the BDEW check digit verified at the parse; MeloId |
→ |
| Validation | Order-independent rules V01–V12 (V10 retired), Hampel outlier test, A/B/C/F grading | → |
| Ersatzwerte | Four methods, calendar-aware grid, audit trail of what actually ran | → |
| Tariff registers | HT/NT and §14a Modul 3 in one mechanism | → |
| Gas & units | m³→kWh_Hs, G 685 rounding, the SigLinDe gas SLP, exact-rational unit normalisation | → |
| Power quality | EN 50160 as the statistical test it actually is | → |
| Virtual meters | Sum, Residual, GGV allocation (§42b EnWG) | → |
| End to end | The full MSB pipeline as a runnable example | → |
🎯 Scope
This crate computes quantities. Four neighbouring concerns are deliberately out of scope, each with a better home:
| Not here | Why | Where instead |
|---|---|---|
| Money — prices, tariffs, invoices | the output is kWh, m³ and kW | your billing layer |
| EDIFACT / XML market messages | parsing a MSCONS is not arithmetic | mako |
| Fristen — counting Werktage to a deadline | a process-engine concern | your process engine |
| SMGW certificates, device inventory | PKI and asset tracking, not quantities | your device management |
The third is the least obvious. The crate does carry a German statutory holiday calendar, because SLP day typing and tariff-register classification cannot be done without one — but it counts no business days.
🧱 Design constraints
- Determinism. No function reads the system clock, the filesystem or the network. Where an instant is needed it is a parameter, so equal inputs give equal outputs. CI enforces this with a grep over non-test code.
- Exact decimals for quantities,
f64only for statistics. The two meet in one place — the outlier rule converts values to run the Hampel filter — and nothing a float touches is written back into a quantity. - One value, one string.
ObisCodeandIntervalResolutioneach have exactly one canonical spelling, held by a proptest suite. - Serde tags are semver-covered, pinned literally by a test.
- Domain enums are exhaustive; only error enums are
#[non_exhaustive]. - Unknown is not good. Where a quantity cannot be determined the API says
so — an
Option, or an error — rather than returning a benign-looking default.
⚖️ Regulatory basis
Every provision is quoted from the published text and dated, and the library is explicit about the claims it cannot verify — the 2025 SLP dynamisation function is published as an image, G 685's final rounding diverges between Netzbetreiber, VDE-AR-N 4400 is paywalled. Those are parameters, not constants.
Nothing here is legal advice.
🧪 Testing
Beyond the unit tests:
tests/berlin_calendar.rs— DST interval counts against the tz databasetests/string_canonicalisation.rs— proptest: stability, totality, idempotence, injectivity of every string formtests/serde_representation.rs— every wire tag pinned literallytests/proptest_validation.rs— validation invariants under random inputtests/regulatory_showcase.rs— worked examples from the published sourcestests/doc_samples.rs— every code block in this README and on the documentation site, compiled and run
🤝 Contributing
Issues and pull requests welcome. A change that touches a regulated calculation should cite the provision it implements — and if a citation here is wrong, saying so is the most valuable issue you can file.
📄 License
Licensed under either of Apache-2.0 or MIT at your option.