Skip to content

§14a EnWG grid-fee bundle

Primer on the two §14a EnWG modules that the Heartbeat API reports on the heartbeat-prices endpoint, with the exact field mapping the SDK exposes.

Regulatory background

§14a Energiewirtschaftsgesetz (EnWG) governs how operators of controllable consumption devices (heat pumps, EV chargers, batteries over 4.2 kW) are compensated by the Distribution-System Operator for accepting network-side grid interventions. Since BK6-22-300 (BNetzA decision of November 2023, binding from 2024), the DSO must offer one of three modules to every §14a-registered site. The vendor splits the observed data accordingly:

Module Compensation mechanism SDK coverage
Modul 1 Flat annual reduction of the grid fee, fixed €/year fully mapped
Modul 2 Percentage reduction of the controllable device's grid fee not seen in captures
Modul 3 Time-variable grid fee (HT / NT), savings vs. a baseline fully mapped

Modul 2 could ship later if we observe it; the current wrapper handles its absence by returning None across all Modul-2-shaped fields that would be inferred from it.

Modul 1: flat annual reduction

Fields on HeartbeatPriceWindow:

Attribute Type Semantics
module1_provisioning_date str \| None ISO date when the Modul 1 bundle was provisioned. None until the site opts in.
module1_active_days int \| None Days counted as active in this window. Currently mirrors the window length.
module1_savings_per_year_eur float \| None Annual Modul-1 savings projected by the backend, constant across windows.
module1_total_savings_eur float \| None Window-accumulated savings. Backend formula: savings_per_year × active_days / 365.

A non-participating account carries None on all four across every window. Opting in flips all four to populated values on the next backend tick.

Modul 3: time-variable grid fees

Modul 3 ties the controllable device's grid fee to time-of-use tariffs (HT, Hochtarif / NT, Niedertarif). The backend computes savings by comparing what the site actually paid under HT/NT to what it would have paid under the uniform baseline tariff.

Fields on HeartbeatPriceWindow:

Attribute Type Semantics
comparison_grid_fee_eur_per_kwh float \| None The baseline (uniform) grid-fee tariff used for the comparison, in EUR/kWh.
comparison_grid_fees_total_eur float \| None What the site would have paid under the baseline tariff in this window.
variable_grid_fees_total_eur float \| None What the site actually paid under the HT/NT tariff.
module3_total_savings_eur float \| None Net Modul-3 savings for the window. Backend formula: comparison_grid_fees_total − variable_grid_fees_total.
enwg14a_total_savings_eur float \| None Combined Modul-1 + Modul-3 savings. Carries the §14a envelope that the UI surfaces to the user.

All five are None until the site opts into Modul 3. The flip usually coincides with the DSO's rollout milestones.

Convenience method

For code that just needs the Modul-3 savings for a given window:

from onekommafive import HeartbeatPrices


prices: HeartbeatPrices = await system.get_heartbeat_prices()
monthly = prices.module3_savings(window="month")
yearly = prices.module3_savings(window="year")

Valid window names: day, week, month, halfYear, year. Returns None for accounts not on Modul 3; raises ValueError on an unknown window name.

For the full breakdown, iterate the windows directly:

for name in ("day", "week", "month", "halfYear", "year"):
    win = getattr(prices, name)
    if win.module3_total_savings_eur is None:
        continue
    print(
        f"{name:>8}: "
        f"baseline={win.comparison_grid_fees_total_eur:.2f} € "
        f"actual={win.variable_grid_fees_total_eur:.2f} € "
        f"savings={win.module3_total_savings_eur:.2f} €"
    )

CLI

1k5 heartbeat-prices prints a §14a section after the main table when at least one window carries populated Modul-1 or Modul-3 values. On a non-participating account the section is suppressed, so the output stays clean.

  • Full response reference: API.md § heartbeat-prices.
  • Daily observatory diff that first flagged Modul-3 (2026-10-09): tracked out-of-tree in the sibling repo.
  • BNetzA decision text: BK6-22-300 (search the BNetzA portal).