metering 0.10.0

German energy metering domain library — interval types, Gas conversion (m3→kWh_Hs), billing period aggregation (Spitzenleistung, HT/NT), SLP/RLM/iMSys classification, Mehr-/Mindermengensaldo, and Hampel-filter quality scoring. Zero I/O, no async, no float money.
Documentation

metering

German energy metering domain library — interval types, gas conversion, billing period aggregation, SLP/RLM/iMSys classification, and Hampel-filter quality scoring.

metering is the pure domain library used by edmd for all metering arithmetic. It has no I/O, no async, and no floating-point money.


Design constraints

Constraint Detail
No I/O All inputs are passed as arguments.
No async Synchronous throughout.
No float money All energy amounts use rust_decimal.
Deterministic Same inputs always produce the same output.

Core types

MeterInterval

A single timestamped energy reading:

pub struct MeterInterval {
    pub period_start: OffsetDateTime,
    pub period_end: OffsetDateTime,
    pub value_kwh: Decimal,
    pub quality: QualityFlag,
}

Sparte

pub enum Sparte { Strom, Gas }

QualityFlag

Measured, Substituted, Estimated, Invalid — mapped to MSCONS/UTILTS Datenqualitätskennzeichen.


Gas conversion: m³ → kWh_Hs

pub fn gas_m3_to_kwh_hs(
    m3: Decimal,
    brennwert_kwh_m3: Decimal,
    zustandszahl: Decimal,
) -> Decimal

Implements the Brennwertkorrektur formula per §24 GasGVV / DVGW G 685:

$$kWh_{Hs} = m^3 \times z \times H_s$$


Billing period aggregation

aggregate computes a BillingPeriodAggregate from a slice of MeterIntervals:

let agg = aggregate(&intervals, &AggregationConfig::rlm_strom())?;
println!("kWh HT: {}", agg.kwh_ht);
println!("kWh NT: {}", agg.kwh_nt);
println!("Spitzenlast: {:?} kW", agg.spitzenleistung_kw);

AggregationConfig presets

Preset Messtyp Split
rlm_strom() RLM Spitzenleistung §2 Nr. 17 MessZV + HT/NT split
slp_strom() SLP HT/NT split (no Spitzenleistung)
rlm_zweitarif() RLM Custom HT/NT window
gas() Gas Single total (m³ → kWh_Hs, no tariff split)

SLP/RLM/iMSys classification

pub fn classify_messtyp(
    malo: &MaloId,
    jahresverbrauch_kwh: Decimal,
    sparte: Sparte,
) -> Messtyp
Messtyp Criteria Basis
Slp < 100 MWh/a (Strom) or < 1.500 MWh/a (Gas) §3 MessZV
Rlm ≥ 100 MWh/a (Strom) or ≥ 1.500 MWh/a (Gas) §4 MessZV
Imsys Pflichteinbau iMSys (§41a EnWG) §41a EnWG

Mehr-/Mindermengensaldo

pub fn compute_imbalance(
    forecast_kwh: Decimal,
    actual_kwh: Decimal,
) -> ImbalanceResult

ImbalanceResult contains delta_kwh and delta_pct. Positive = Mehrmenge (actual > forecast); negative = Mindermenge. Basis: §27 MessZV.


Hampel-filter quality scoring

score_intervals runs a Hampel filter over a time-ordered slice of intervals and returns per-interval QualityGrades:

pub fn score_intervals(
    intervals: &[MeterInterval],
    window_k: usize,   // half-window size (default: 3)
    threshold: f64,    // MAD threshold in sigma units (default: 3.0)
) -> Vec<QualityGrade>

QualityGrade

Grade Meaning Effect in edmd
A Clean — within threshold Billing proceeds normally
B Slightly suspicious Logged; billing proceeds
C Likely outlier de.edmd.reading.quality.warning CloudEvent emitted
F Severe outlier / substituted Billing run blocked; operator alert

hampel_filter is also exposed as a low-level primitive that returns raw outlier indices.


§17 MessZV substitute value generation

When meter readings are missing or faulty, the MSB must supply substitute values before billing. metering implements all four methods from §17 MessZV and BDEW practice.

Quick usage

use metering::{fill_gaps, fill_gaps_with_config, FillGapsConfig, SubstituteMethod};

// Automatic method selection (linear for short gaps, carry-forward for long)
let filled = fill_gaps(&intervals, 900, period_from, period_to);

// Prior-period averaging per §17 Abs. 2 MessZV (same time-slot, prior week)
let prior_week: Vec<MeterInterval> = fetch_prior_week(&malo_id);
let filled = fill_gaps_with_config(
    &intervals, 900, period_from, period_to,
    &FillGapsConfig::prior_period(prior_week),
);

SubstituteMethod variants

Variant When to use BDEW recommendation
LinearInterpolation Short gaps (≤ 3 intervals) with surrounding data Primary for RLM/iMSys
PriorPeriodAverage Longer gaps; same time-slot from prior reference week Biomass, industrial load
ZeroFill Documented plant shutdown — affirmative zero only Outage with evidence
LastValueCarryForward Conservative fallback when no context available SLP, default for longer gaps

FillGapsConfig

pub struct FillGapsConfig {
    pub method: SubstituteMethod,            // default: LinearInterpolation
    pub prior_period_intervals: Vec<MeterInterval>, // for PriorPeriodAverage
    pub short_gap_threshold: usize,          // default: 3 (auto-linear below this)
}

FillGapsConfig::prior_period(prior_week_intervals) and FillGapsConfig::zero_fill() are convenience constructors.

Filled intervals carry quality = QualityFlag::Substituted (billable per §17 MessZV Abs. 1).


Feature flags

Flag Effect
serde Derive Serialize/Deserialize on all public types

Testing

cargo test -p metering --all-features

37 tests covering gas conversion, aggregation (RLM/SLP/Gas), Messtyp classification, imbalance arithmetic, Hampel filter edge cases, and §17 MessZV substitute value generation (all four methods including PriorPeriodAverage).


Regulatory basis

  • §3, §4 MessZV — SLP/RLM classification thresholds
  • §2 Nr. 17 MessZV — Spitzenleistung definition for RLM
  • §17 MessZV — Ersatzwertbildung (substitute value generation)
  • §27 MessZV — Mehr-/Mindermengensaldo
  • §24 GasGVV / DVGW G 685 — Gas Brennwertkorrektur
  • §41a EnWG — 15-Minuten-Lastgang and iMSys Pflichteinbau