fix(fw): rebuild electricity price sensor on epex_spot 4.x with full fee model + future-price chart #218

Merged
dominik.polakovics merged 1 commit from afk/217 into main 2026-07-05 18:24:09 +02:00

Closes #217

What changed

hosts/fw/modules/home-assistant/electricity.nix:

  • sensor.electricity_price (entity id unchanged) is now a modern template: sensor sourced from sensor.epex_spot_data_market_price (the epex_spot 4.x entity). State = (market_price + fees_net) × 1.20, guarded by an availability template so a missing source makes the entity unavailable instead of raising TemplateError on every render.
  • Fee model as named let constants, one per line of the June 2026 Spotty invoice (Σ ≈ 0.1010 net €/kWh), VAT 20% as the final multiplier. The 2027 Elektrizitätsabgabe revert (0.0010 → 0.015) is a one-line edit.
  • sensor.electricity_price_forecast exposes a forecast attribute: [{start, price}, …] consumer prices for the current + all future intervals from the source's data attribute (start_time/end_time/price_per_kwh, resolution-agnostic — works for hourly and 15-min). The entity is excluded from the recorder via recorder.exclude.entities.
  • apexcharts-card (v2.2.3 from nixos-26.05, verified) installed declaratively via services.home-assistant.customLovelaceModules — auto-registers as a Lovelace resource.

Deviation from the acceptance criteria (deliberate)

The criteria asked for the forecast attribute on sensor.electricity_price itself, excluded from the recorder. That is not implementable on HA 2026.5.4 — verified from the 2026.5.4 source: the recorder's exclude config is entity-level only (entities/entity_globs/domains/event_types), and the template integration has no recorder.py platform and no unrecorded-attributes config option. The brief said "recorder.exclude config or template attribute exclusion — whichever HA 2026.5 supports for attributes"; it supports neither.

Options were: (a) put the attribute on sensor.electricity_price and exclude that whole entity — loses its state history and long-term statistics; (b) let the attribute be recorded — ~96 attribute rows/day × up to ~192 entries, the exact DB bloat the issue wants to avoid; or (c) a dedicated sensor.electricity_price_forecast carrying the attribute, wholly recorder-excluded. I picked (c): zero DB bloat and sensor.electricity_price keeps recording clean price history. If you prefer letter-perfect (a), it's a small diff — move the attributes.forecast block over and swap the excluded entity id.

Template validation

Both Jinja templates were validated against a simulated HA environment (HA-style strip + literal_eval, stubbed state_attr/has_value/as_datetime/now, fake 15-min data with past/current/future entries): state math is exact, past intervals are dropped, current interval is kept, output is a native list of {start, price} dicts, and an unavailable source degrades to [] / unavailable without errors.

ApexCharts card YAML (paste into a dashboard via UI, storage mode)

type: custom:apexcharts-card
header:
  show: true
  title: Electricity price (incl. grid fees + 20% VAT)
  show_states: true
  colorize_states: true
graph_span: 36h
span:
  start: hour
now:
  show: true
  label: now
yaxis:
  - min: 0
    decimals: 2
series:
  - entity: sensor.electricity_price
    name: Current price
    unit: EUR/kWh
    show:
      in_header: true
      in_chart: false
  - entity: sensor.electricity_price_forecast
    name: Consumer price
    unit: EUR/kWh
    type: column
    show:
      in_header: false
      legend_value: false
    data_generator: |
      return entity.attributes.forecast.map((e) => [new Date(e.start).getTime(), e.price]);
    color_threshold:
      - value: 0
        color: '#2ecc71'
      - value: 0.22
        color: '#f1c40f'
      - value: 0.28
        color: '#e67e22'
      - value: 0.35
        color: '#e74c3c'
  # Optional overlay: raw spot price, visualizes the fee/tax wedge
  - entity: sensor.epex_spot_data_market_price
    name: Raw spot (net)
    unit: EUR/kWh
    type: line
    curve: stepline
    show:
      in_header: false
      legend_value: false
    data_generator: |
      return entity.attributes.data.map((e) => [new Date(e.start_time).getTime(), e.price_per_kwh]);

Notes: the forecast only carries current + future intervals, so the chart starts "now" (span.start: hour); tomorrow's bars appear after day-ahead publication (~14:00). The raw-spot overlay reads the source sensor's own data attribute, so it also shows today's past hours.

Post-merge verification (human follow-ups per the brief)

  • After bento pulls (~5 min) and HA restarts: confirm sensor.electricity_price tracks (market + 0.101) × 1.2 and the TemplateError ... float got invalid input 'unavailable' no longer recurs in the hass container journal (journalctl -D /var/lib/nixos-containers/hass/var/log/journal on fw, per ADR-0005).
  • Confirm actual interval resolution (hourly vs 15-min) in Developer Tools → States; the mapping works for either.
  • Paste the card YAML above onto a dashboard; delete the orphaned sensor.epex_spot_data_price / sensor.epex_spot_data_net_price registry entries (cosmetic).

Pre-commit dry-build for fw: green.

Closes #217 ## What changed `hosts/fw/modules/home-assistant/electricity.nix`: - **`sensor.electricity_price`** (entity id unchanged) is now a **modern `template:` sensor** sourced from `sensor.epex_spot_data_market_price` (the epex_spot 4.x entity). State = `(market_price + fees_net) × 1.20`, guarded by an `availability` template so a missing source makes the entity *unavailable* instead of raising `TemplateError` on every render. - **Fee model** as named `let` constants, one per line of the June 2026 Spotty invoice (Σ ≈ 0.1010 net €/kWh), VAT 20% as the final multiplier. The 2027 Elektrizitätsabgabe revert (0.0010 → 0.015) is a one-line edit. - **`sensor.electricity_price_forecast`** exposes a `forecast` attribute: `[{start, price}, …]` consumer prices for the current + all future intervals from the source's `data` attribute (`start_time`/`end_time`/`price_per_kwh`, resolution-agnostic — works for hourly and 15-min). The entity is excluded from the recorder via `recorder.exclude.entities`. - **`apexcharts-card`** (v2.2.3 from nixos-26.05, verified) installed declaratively via `services.home-assistant.customLovelaceModules` — auto-registers as a Lovelace resource. ## Deviation from the acceptance criteria (deliberate) The criteria asked for the `forecast` attribute **on `sensor.electricity_price` itself**, excluded from the recorder. That is not implementable on HA 2026.5.4 — verified from the 2026.5.4 source: the recorder's `exclude` config is entity-level only (`entities`/`entity_globs`/`domains`/`event_types`), and the template integration has no `recorder.py` platform and no unrecorded-attributes config option. The brief said "recorder.exclude config or template attribute exclusion — whichever HA 2026.5 supports for attributes"; it supports **neither**. Options were: (a) put the attribute on `sensor.electricity_price` and exclude that whole entity — loses its state history and long-term statistics; (b) let the attribute be recorded — ~96 attribute rows/day × up to ~192 entries, the exact DB bloat the issue wants to avoid; or (c) a dedicated `sensor.electricity_price_forecast` carrying the attribute, wholly recorder-excluded. I picked **(c)**: zero DB bloat *and* `sensor.electricity_price` keeps recording clean price history. If you prefer letter-perfect (a), it's a small diff — move the `attributes.forecast` block over and swap the excluded entity id. ## Template validation Both Jinja templates were validated against a simulated HA environment (HA-style strip + literal_eval, stubbed `state_attr`/`has_value`/`as_datetime`/`now`, fake 15-min `data` with past/current/future entries): state math is exact, past intervals are dropped, current interval is kept, output is a native list of `{start, price}` dicts, and an unavailable source degrades to `[]` / unavailable without errors. ## ApexCharts card YAML (paste into a dashboard via UI, storage mode) ```yaml type: custom:apexcharts-card header: show: true title: Electricity price (incl. grid fees + 20% VAT) show_states: true colorize_states: true graph_span: 36h span: start: hour now: show: true label: now yaxis: - min: 0 decimals: 2 series: - entity: sensor.electricity_price name: Current price unit: EUR/kWh show: in_header: true in_chart: false - entity: sensor.electricity_price_forecast name: Consumer price unit: EUR/kWh type: column show: in_header: false legend_value: false data_generator: | return entity.attributes.forecast.map((e) => [new Date(e.start).getTime(), e.price]); color_threshold: - value: 0 color: '#2ecc71' - value: 0.22 color: '#f1c40f' - value: 0.28 color: '#e67e22' - value: 0.35 color: '#e74c3c' # Optional overlay: raw spot price, visualizes the fee/tax wedge - entity: sensor.epex_spot_data_market_price name: Raw spot (net) unit: EUR/kWh type: line curve: stepline show: in_header: false legend_value: false data_generator: | return entity.attributes.data.map((e) => [new Date(e.start_time).getTime(), e.price_per_kwh]); ``` Notes: the forecast only carries current + future intervals, so the chart starts "now" (`span.start: hour`); tomorrow's bars appear after day-ahead publication (~14:00). The raw-spot overlay reads the source sensor's own `data` attribute, so it also shows today's past hours. ## Post-merge verification (human follow-ups per the brief) - After bento pulls (~5 min) and HA restarts: confirm `sensor.electricity_price` tracks `(market + 0.101) × 1.2` and the `TemplateError ... float got invalid input 'unavailable'` no longer recurs in the hass container journal (`journalctl -D /var/lib/nixos-containers/hass/var/log/journal` on fw, per ADR-0005). - Confirm actual interval resolution (hourly vs 15-min) in Developer Tools → States; the mapping works for either. - Paste the card YAML above onto a dashboard; delete the orphaned `sensor.epex_spot_data_price` / `sensor.epex_spot_data_net_price` registry entries (cosmetic). Pre-commit dry-build for fw: green.
The epex_spot 4.x rename orphaned sensor.epex_spot_data_price, leaving
sensor.electricity_price erroring on every render (float on
'unavailable' raises since HA 2021.10 without a default). The old
formula also never applied 20% VAT.

Rebuild it as a modern template: sensor sourced from
sensor.epex_spot_data_market_price, with the Spotty fee model as named
constants (one per invoice line) and VAT as the final multiplier. Add a
recorder-excluded sensor.electricity_price_forecast carrying consumer
prices for all remaining intervals, and install apexcharts-card via
customLovelaceModules for the future-price chart.

Closes #217
Author
Owner

This was generated by AI while landing a PR.

Land-PR audit — verdict: PASS

Checked:

  • State: open, not draft, no conflicts with main.
  • Verification signal relied on: the repo's commit-time gate (pre-commit dry-build of fw, per CLAUDE.md), which the PR reports green. Not re-run. The diff changes no derivation src/hash, so the gate's eval-only blind spot does not apply — the referenced nixpkgs attr home-assistant-custom-lovelace-modules.apexcharts-card is itself eval-checked.
  • Conventions: Conventional Commit title with host scope (fix(fw):) ✓; no secrets edits, no system.stateVersion change.
  • AFK contract: head afk/217 carries a working Closes #217 ✓.
  • Diff review vs issue #217 intent: source entity, fee constants (Σ = 0.1010 net, invoice-commented, 2027 Abgabe revert is one line), VAT as final multiplier, modern template: syntax, entity id sensor.electricity_price preserved, availability guard replaces the old TemplateError failure mode. The triage-resolved price key (price_per_kwh, ISO start_time/end_time) matches the forecast mapping exactly. Nix float splicing verified: toString renders 0.101000 / 1.200000 — valid Jinja literals.

Noted deviation (deliberate, accepted): the forecast attribute lives on a dedicated sensor.electricity_price_forecast (wholly recorder-excluded) rather than on sensor.electricity_price, because HA 2026.5 supports neither attribute-level recorder exclusion nor template unrecorded-attributes. Option (c) preserves price history with zero DB bloat and was explicitly justified in the PR body.

> *This was generated by AI while landing a PR.* ## Land-PR audit — verdict: PASS **Checked:** - State: open, not draft, no conflicts with `main`. - Verification signal relied on: the repo's commit-time gate (pre-commit dry-build of fw, per CLAUDE.md), which the PR reports green. Not re-run. The diff changes no derivation `src`/hash, so the gate's eval-only blind spot does not apply — the referenced nixpkgs attr `home-assistant-custom-lovelace-modules.apexcharts-card` is itself eval-checked. - Conventions: Conventional Commit title with host scope (`fix(fw):`) ✓; no secrets edits, no `system.stateVersion` change. - AFK contract: head `afk/217` carries a working `Closes #217` ✓. - Diff review vs issue #217 intent: source entity, fee constants (Σ = 0.1010 net, invoice-commented, 2027 Abgabe revert is one line), VAT as final multiplier, modern `template:` syntax, entity id `sensor.electricity_price` preserved, availability guard replaces the old TemplateError failure mode. The triage-resolved price key (`price_per_kwh`, ISO `start_time`/`end_time`) matches the forecast mapping exactly. Nix float splicing verified: `toString` renders `0.101000` / `1.200000` — valid Jinja literals. **Noted deviation (deliberate, accepted):** the `forecast` attribute lives on a dedicated `sensor.electricity_price_forecast` (wholly recorder-excluded) rather than on `sensor.electricity_price`, because HA 2026.5 supports neither attribute-level recorder exclusion nor template unrecorded-attributes. Option (c) preserves price history with zero DB bloat and was explicitly justified in the PR body.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos!218
No description provided.