metering

Heat

HeizkostenV quantities: the § 6a monthly consumption information, the § 9 warm-water heat and fuel quantity, and the § 9a estimate with its 25 % limit.

heat::heizkosten computes the HeizkostenV quantities the Verordnung defines precisely. Out: the cost distribution of §§ 7 and 8 (money), and § 9b Gradtagszahlen and the § 6a Abs. 3 weather adjustment (not freely citable technical rules).

ClauseFunctionQuantity
§ 6a Abs. 2verbrauchsinformationthe month's consumption and its two comparisons
§ 9 Abs. 2warm_water_heat_kwh, warm_water_heat_kwh_unmeteredheat attributable to central warm water
§ 9 Abs. 3brennstoffverbrauchthe fuel quantity B of the warm-water system
§ 9averbrauchsschaetzungwhether an estimate is permitted

§ 6a — monthly consumption information

The last month's consumption in kWh, compared with the previous month and the same month of the previous year "soweit diese Daten erhoben worden sind". Months are Berlin calendar months; an interval across a month boundary is refused; a partial month is marked incomplete. Heizkostenverteiler units need a kWh factor, which is an argument. The comparison with a "normierten oder durch Vergleichstests ermittelten Durchschnittsnutzers derselben Nutzerkategorie" is undefined by the Verordnung; supply that figure yourself.

use metering::heat::heizkosten::{Erfassung, verbrauchsinformation};
use metering::prelude::*;
use time::Month;
use time::macros::date;

// Daily heat-meter values, 10 kWh a day, through February and March 2026.
let days = (0..59)
    .map(|i| {
        let d = DayBoundary::Strom.day(date!(2026 - 02 - 01) + time::Duration::days(i)).unwrap();
        MeterInterval::new(d.start(), d.end(), dec!(10), QualityFlag::Measured)
    })
    .collect::<Result<Vec<_>, _>>().unwrap();
let series = Series::new(Resolution::Day, DayBoundary::Strom, days).unwrap();

let info = verbrauchsinformation(&series, Erfassung::Waermemenge, 2026, Month::March).unwrap();
let march = info.monat.unwrap();
assert_eq!(march.kwh, dec!(310));
assert!(march.vollstaendig);
assert_eq!(info.vormonat.unwrap().kwh, dec!(280));
assert!(info.vorjahresmonat.is_none()); // not recorded

§ 9 — warm water

§ 9 Abs. 2 Satz 1 requires a Wärmezähler; the fallbacks:

FunctionFormulaWhen
warm_water_heat_kwhQ = 2,5 × V [m³] × (t_w − 10 °C); t_w < 10 °C refusedmetering the heat "nur mit einem unzumutbar hohen Aufwand"
warm_water_heat_kwh_unmeteredQ = 32 × A [m²]"in Ausnahmefällen"

WarmWaterAdjustments applies the cumulative Satz 6 corrections (Erdgas brennwertbezogen × 1,11; gewerbliche Wärmelieferung ÷ 1,15; monovalente Wärmepumpe × 0,30). § 9 Abs. 3: B = Q ÷ Hi, with the supplier's Heizwert first and the statutory table as fallback.

use metering::heat::heizkosten::{
    Brennstoff, Heizwert, WarmWaterAdjustments, brennstoffverbrauch, warm_water_heat_kwh,
};
use rust_decimal::dec;

// 40 m³ of warm water at 60 °C: 2,5 × 40 × 50.
let q = warm_water_heat_kwh(dec!(40), dec!(60), WarmWaterAdjustments::NONE).unwrap();
assert_eq!(q, dec!(5000));

// From Erdgas H at the statutory fallback of 10 kWh/m³.
let b = brennstoffverbrauch(q, Heizwert::Hilfsweise(Brennstoff::ErdgasH)).unwrap();
assert_eq!(b.menge, dec!(500)); // m³

§ 9a — estimation

On one of three named bases — comparable periods, comparable rooms, the building's average. Above 25 % of the area (or volume) it is refused and the costs follow the fixed-cost key alone; exactly 25 % is admitted — the limit bites when the share "überschreitet" it. The estimate itself is your figure.

use metering::heat::heizkosten::{Schaetzgrundlage, SchaetzungError, verbrauchsschaetzung};
use rust_decimal::dec;

let ok = verbrauchsschaetzung(Schaetzgrundlage::VergleichbareZeitraeume, dec!(4200), dec!(25), dec!(100)).unwrap();
assert_eq!(ok.anteil_prozent, dec!(25));

assert_eq!(
    verbrauchsschaetzung(Schaetzgrundlage::VergleichbareRaeume, dec!(4200), dec!(26), dec!(100)).unwrap_err(),
    SchaetzungError::UeberSchaetzgrenze,
);