metering 0.17.0

German energy metering domain library — DST-correct Europe/Berlin calendar arithmetic, gas m3→kWh_Hs conversion, billing-period aggregation, Ersatzwertbildung, a robust validation engine, Bundesland holiday calendar and SLP/RLM/iMSys classification. Zero I/O, no async, no clock, exact decimal quantities.
Documentation

⚡ metering

Crates.io Docs.rs CI MSRV License

German energy metering domain library for Rust. Europe/Berlin calendar arithmetic, Zählerstandsgang → Lastgang, gas m³→kWh_Hs, Ersatzwertbildung, a robust validation engine, EN 50160, §14a Modul 3 tariff registers, virtual meters (§42b EnWG) 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

cargo add metering

# ...or with serde for every public type:
cargo add metering --features serde

MSRV: Rust 1.94 (edition 2024), pinned in rust-toolchain.toml and verified by a CI lane.


🚀 Quick start

use metering::{AggregationConfig, MeterInterval, QualityFlag, aggregate};
use rust_decimal::dec;
use time::macros::datetime;

let intervals = vec![MeterInterval {
    from: datetime!(2026-06-01 0:00 UTC),
    to:   datetime!(2026-06-01 0:15 UTC),
    value: dec!(2.345),
    quality: QualityFlag::Measured,
    obis_code: Some("1-0:1.8.0".parse().unwrap()),
}];

let period = aggregate(&intervals, &AggregationConfig::rlm());
println!("Arbeitsmenge:    {} kWh", period.arbeitsmenge);
println!("Spitzenleistung: {:?} kW", period.spitzenleistung_kw);
println!("...reached at:   {:?}", period.spitzenleistung_at);
println!("Coverage:        {:.1} %", period.coverage_pct);

🗓️ 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 metering::{IntervalResolution, calendar};
use time::macros::{date, datetime};

assert_eq!(calendar::intervals_in_day(date!(2026 - 03 - 29), IntervalResolution::QuarterHour), Some(92));
assert_eq!(calendar::intervals_in_day(date!(2026 - 10 - 25), IntervalResolution::QuarterHour), Some(100));

// March 2026 holds 2 972 quarter-hours, not 31 × 96 = 2 976.
assert_eq!(calendar::intervals_in_month(date!(2026 - 03 - 01), IntervalResolution::QuarterHour), Some(2_972));

// A German day starts at 23:00 UTC in winter, 22:00 UTC in summer.
assert_eq!(calendar::day_start_utc(date!(2026 - 01 - 15)), datetime!(2026-01-14 23:00 UTC));
assert_eq!(calendar::day_start_utc(date!(2026 - 07 - 15)), datetime!(2026-07-14 22:00 UTC));

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.

→ Time and the calendar

The whole pipeline, end to end

cargo run --example pipeline

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; DST-correct interval counts; Bundesland holidays →
Readings Zählerstandsgang → Lastgang, register rollover, meter exchange →
Validation Order-independent rules V01–V12, 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, 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, f64 only 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. ObisCode and IntervalResolution each 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.

→ Design constraints


⚖️ 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.

→ Regulatory basis

Nothing here is legal advice.


🧪 Testing

just ci     # everything CI runs, in CI order
just test   # cargo test --all-features
just purity # no clock, no I/O, no unsafe outside comments
just site   # serve the documentation site locally

Beyond the unit tests:

  • tests/berlin_calendar.rs — DST interval counts against the tz database
  • tests/string_canonicalisation.rs — proptest: stability, totality, idempotence, injectivity of every string form
  • tests/serde_representation.rs — every wire tag pinned literally
  • tests/proptest_validation.rs — validation invariants under random input
  • tests/regulatory_showcase.rs — worked examples from the published sources
  • tests/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.