fix(heatpump-roi): repair solar ratio in cost chart, honest annual savings, since-install blended price #290

Merged
k.renner merged 1 commits from claude-bot/dash-heatpump-roi into master 2026-07-17 13:16:48 +00:00
Member

Heat Pump ROI — post-rebuild review (PR #271 follow-up)

Fresh-eyes review of the rebuilt dashboard. The InfluxDB migration itself (PR #271) is sound — entity unions, unit normalization and the survivor utility_meters all check out against live data. But the review found one real correctness bug (the cost chart silently ignores solar) and two methodology problems in the financial numbers that drive the investment picture.

Before

before
before-bottom

After

after
after-bottom

Review report

1. CRITICAL — "Historical Cost Comparison" ignored solar entirely — ✅ FIXED

The chained Flux joins renamed the _value columns to _value_j2 / plain _value, not _value_gi / _value_sp as the final map() expected. Both lookups returned null, so solar_ratio was 0 for every row and the whole "Electricity Cost" line was priced at full grid price. Verified live: the joined rows had gi=null, sp=null, line ended at €4,667 = 14,284 kWh × 0.3271 exactly. Real cost ≈ €3.4K — the chart overstated electricity cost by ~€1.1K (37%) and contradicted the "Running Cost" stat on the same dashboard.
Fix: each stream renames its column to a unique name (h/d/gi/ge/sp) before joining. The chart tail now lands at €3.37K, consistent with the stat.

2. MAJOR — blended price included 1.4 years of pre-heat-pump PV history — ✅ FIXED

The self-consumption ratio came from the Vitocharge lifetime counters, which date back to the PV install (May 2023) — 17 months before the heat pump existed. Lifetime ratio: 0.322 → €0.2475/kWh. Ratio from counter deltas since Oct 2024 (verified: gi 22,221 kWh, ge 3,761, sp 16,753): 0.369 → €0.2359/kWh. All cost/savings/ROI stats now use the since-install ratio; the cost chart subtracts the install-date counter baselines so its per-day ratio matches. Effect: running cost €3.54K→€3.37K, savings since install €8.38K→€8.54K.

3. MAJOR — "Projected Annual Savings" used a broken calendar extrapolation — ✅ FIXED

The old formula annualized year-to-date cost with 12 / (month + day/30). Two problems: (a) it overcounts elapsed time by a full month (on Jul 17 it says 7.57 months elapsed; actually 6.55) — in January it would divide by 1.2 instead of 0.5 and wildly misproject; (b) linear calendar extrapolation of a heating load is seasonally biased no matter the divisor. Fix: use the actual trailing-365-day consumption (utility_meter spread, verified live: 7,395 + 993 = 8,388 kWh) × blended price. Retitled "Annual Savings (Trailing 12M)" — a true rolling year with real seasonality, stable in every calendar month. Payback panel updated to match. Values barely move today (€4.70K→€4.67K, 15.5y→15.6y) — but now they're right by construction, not by accident of the date.

4. MINOR — stale legend "Last" values (10,734 / 1,230 / €9.47K) — ✅ FIXED

The counters' friendly_name tag changed on 2026-03-04 (verified in InfluxDB), splitting each entity into two table keys. After keep() the tables merged unsorted, so the legend "Last" calc picked the old era's final row — the Historical panels showed Last = 10,734 kWh while the line correctly ended at 12,577. Fix: group() |> sort(_time) before aggregateWindow in both historical panels. Legends now read 12,577 / 1,707 / €11.9K.

5. MINOR — misleading presentation — ✅ FIXED

  • "Running Cost (Total)" had green/yellow/red thresholds (3000/5000) on a cumulative-since-install figure — it would sit permanently red as a matter of arithmetic. Now neutral single color and retitled "Running Cost (Since Install)", with a description saying it is not an annual figure.
  • The €6,650 oil baseline was hardcoded in six places. Now a dashboard variable oil_baseline next to Investment/prices, so the whole ROI model is tunable from the toolbar.
  • Added descriptions to Total Consumption / Running Cost / Cost This Year.

Reviewed and left as-is

  • SPF gauges (3.2) and SPF-over-time union — agree with the device and the old ViCare values.
  • Total Consumption 14,284 kWh — matches both utility_meters (12,577 + 1,707).
  • Oil baseline model (€6,650/yr prorated linearly since Oct 2024) — reasonable given the 2022–23 order data; now variable-driven.
  • Daily Consumption / Cumulative Recent / Last-7-days panels — verified correct (summer: heating 0, DHW ~3 kWh/day).

⚠️ NOT FIXED / for awareness

  • The oil baseline prorates linearly over the year while the electricity side accrues seasonally, so "Savings Since Install" runs slightly ahead in summer and behind in winter within any year. Neutral over full years; left as-is to keep the model simple.
  • Payback ignores discounting/maintenance/oil-boiler replacement counterfactual — it's a simple payback metric, kept as designed.

Verification

All numbers checked against live InfluxDB via the Grafana datasource API before editing:

  • Counter values: heating 12,577 kWh / DHW 1,707.1 (utility_meters); year counters 4,355 + 594.3; vitocharge pv 20,038,720 Wh, feed-in 6,699.2 kWh, grid 28,088.2 kWh.
  • Since-install spreads: gi 22,221.4 / ge 3,761.4 / sp 16,753.1 → ratio 0.369, blended €0.23593.
  • Trailing-365d consumption: 8,387.9 kWh.
  • Every replacement query executed live before being written into the JSON; expected panel values (0.236 / 3,370 / 1,168 / 4,671 / 15.6 / 8,544 / 11.7%) all reproduced in the AFTER render.
  • Fixed chart query run across both era boundaries (2026-03-04 tag change, 2026-07-02 ViCare→open3e cutover): smooth, no jumps.
# Heat Pump ROI — post-rebuild review (PR #271 follow-up) Fresh-eyes review of the rebuilt dashboard. The InfluxDB migration itself (PR #271) is sound — entity unions, unit normalization and the survivor utility_meters all check out against live data. But the review found **one real correctness bug** (the cost chart silently ignores solar) and **two methodology problems** in the financial numbers that drive the investment picture. ## Before ![before](https://gitea.renner.uno/attachments/f68d8197-fe46-446c-b953-e54af629d07d) ![before-bottom](https://gitea.renner.uno/attachments/35f87e00-be66-4482-a40c-94c942510ded) ## After ![after](https://gitea.renner.uno/attachments/81d4fca0-d4d1-46ca-81e4-5da983bc219c) ![after-bottom](https://gitea.renner.uno/attachments/28030ac3-2c37-49a5-a2e6-f4fe0157fd19) ## Review report ### 1. CRITICAL — "Historical Cost Comparison" ignored solar entirely — ✅ FIXED The chained Flux joins renamed the `_value` columns to `_value_j2` / plain `_value`, **not** `_value_gi` / `_value_sp` as the final `map()` expected. Both lookups returned null, so `solar_ratio` was 0 for **every** row and the whole "Electricity Cost" line was priced at full grid price. Verified live: the joined rows had `gi=null, sp=null`, line ended at €4,667 = 14,284 kWh × 0.3271 exactly. Real cost ≈ €3.4K — the chart overstated electricity cost by ~€1.1K (37%) and contradicted the "Running Cost" stat on the same dashboard. **Fix:** each stream renames its column to a unique name (`h`/`d`/`gi`/`ge`/`sp`) before joining. The chart tail now lands at €3.37K, consistent with the stat. ### 2. MAJOR — blended price included 1.4 years of pre-heat-pump PV history — ✅ FIXED The self-consumption ratio came from the Vitocharge **lifetime** counters, which date back to the PV install (May 2023) — 17 months before the heat pump existed. Lifetime ratio: 0.322 → €0.2475/kWh. Ratio from counter **deltas since Oct 2024** (verified: gi 22,221 kWh, ge 3,761, sp 16,753): **0.369 → €0.2359/kWh**. All cost/savings/ROI stats now use the since-install ratio; the cost chart subtracts the install-date counter baselines so its per-day ratio matches. Effect: running cost €3.54K→€3.37K, savings since install €8.38K→€8.54K. ### 3. MAJOR — "Projected Annual Savings" used a broken calendar extrapolation — ✅ FIXED The old formula annualized year-to-date cost with `12 / (month + day/30)`. Two problems: (a) it overcounts elapsed time by a full month (on Jul 17 it says 7.57 months elapsed; actually 6.55) — in January it would divide by 1.2 instead of 0.5 and wildly misproject; (b) linear calendar extrapolation of a heating load is seasonally biased no matter the divisor. **Fix:** use the actual trailing-365-day consumption (utility_meter spread, verified live: 7,395 + 993 = 8,388 kWh) × blended price. Retitled **"Annual Savings (Trailing 12M)"** — a true rolling year with real seasonality, stable in every calendar month. Payback panel updated to match. Values barely move today (€4.70K→€4.67K, 15.5y→15.6y) — but now they're right by construction, not by accident of the date. ### 4. MINOR — stale legend "Last" values (10,734 / 1,230 / €9.47K) — ✅ FIXED The counters' `friendly_name` tag changed on 2026-03-04 (verified in InfluxDB), splitting each entity into two table keys. After `keep()` the tables merged unsorted, so the legend "Last" calc picked the old era's final row — the Historical panels showed Last = 10,734 kWh while the line correctly ended at 12,577. **Fix:** `group() |> sort(_time)` before `aggregateWindow` in both historical panels. Legends now read 12,577 / 1,707 / €11.9K. ### 5. MINOR — misleading presentation — ✅ FIXED - "Running Cost (Total)" had green/yellow/red thresholds (3000/5000) on a **cumulative-since-install** figure — it would sit permanently red as a matter of arithmetic. Now neutral single color and retitled "Running Cost (Since Install)", with a description saying it is not an annual figure. - The €6,650 oil baseline was hardcoded in six places. Now a dashboard variable **`oil_baseline`** next to Investment/prices, so the whole ROI model is tunable from the toolbar. - Added descriptions to Total Consumption / Running Cost / Cost This Year. ### Reviewed and left as-is - SPF gauges (3.2) and SPF-over-time union — agree with the device and the old ViCare values. - Total Consumption 14,284 kWh — matches both utility_meters (12,577 + 1,707). - Oil baseline model (€6,650/yr prorated linearly since Oct 2024) — reasonable given the 2022–23 order data; now variable-driven. - Daily Consumption / Cumulative Recent / Last-7-days panels — verified correct (summer: heating 0, DHW ~3 kWh/day). ### ⚠️ NOT FIXED / for awareness - The oil baseline prorates **linearly** over the year while the electricity side accrues seasonally, so "Savings Since Install" runs slightly ahead in summer and behind in winter within any year. Neutral over full years; left as-is to keep the model simple. - Payback ignores discounting/maintenance/oil-boiler replacement counterfactual — it's a simple payback metric, kept as designed. ## Verification All numbers checked against live InfluxDB via the Grafana datasource API before editing: - Counter values: heating 12,577 kWh / DHW 1,707.1 (utility_meters); year counters 4,355 + 594.3; vitocharge pv 20,038,720 Wh, feed-in 6,699.2 kWh, grid 28,088.2 kWh. - Since-install spreads: gi 22,221.4 / ge 3,761.4 / sp 16,753.1 → ratio 0.369, blended €0.23593. - Trailing-365d consumption: 8,387.9 kWh. - Every replacement query executed live before being written into the JSON; expected panel values (0.236 / 3,370 / 1,168 / 4,671 / 15.6 / 8,544 / 11.7%) all reproduced in the AFTER render. - Fixed chart query run across both era boundaries (2026-03-04 tag change, 2026-07-02 ViCare→open3e cutover): smooth, no jumps.
claude-bot added 1 commit 2026-07-16 23:22:48 +00:00
- Historical Cost Comparison: the chained joins renamed _value columns to
  _value_j2/_value (not _value_gi/_value_sp as the map expected), so the
  solar ratio was always null->0 and the whole cost line was priced at full
  grid price (EUR 4.67K instead of ~3.4K). Streams now pre-rename columns
  to h/d/gi/ge/sp so the join is unambiguous.
- Blended price now uses counter deltas since heat-pump install (Oct 2024)
  instead of device-lifetime counters that include 1.4 years of PV-only
  history from May 2023 (ratio 0.369 vs 0.322 -> EUR 0.236/kWh vs 0.248).
  Applied consistently to all cost/savings/ROI stats and the cost chart
  (chart subtracts install-date counter baselines).
- Annual savings / payback: replace the calendar-YTD extrapolation
  (12 / (month + day/30) - which overcounts elapsed time by a month and is
  seasonally biased) with the actual trailing-365-day consumption spread.
- Fix stale legend 'Last' values (10734/1230/EUR 9.47K): the counters'
  friendly_name tag changed 2026-03-04, splitting each series into two
  tables; group()+sort() merges them before aggregateWindow.
- New oil_baseline dashboard variable (6650) replaces six hardcoded copies.
- Running Cost: neutral color (was green/yellow/red on a forever-growing
  cumulative), retitled '(Since Install)'; added descriptions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Member

Scrape-residue verification (cross-cutting check): the duplicate-Prometheus-scrape residue that hits long-window dashboards (2 jobs x N pod lifetimes = ~12 series/entity before 2026-07-16) does not affect this dashboard. Verified against the JSON on this branch: all 50 datasource references are influxdb, zero Prometheus targets or expr fields remain (PR #271 removed them all, this PR keeps it that way). The default Oct-2024+ window is therefore safe — InfluxDB series here are only split by the friendly_name tag change, which this PR already handles with group() |> sort(_time).

**Scrape-residue verification (cross-cutting check):** the duplicate-Prometheus-scrape residue that hits long-window dashboards (2 jobs x N pod lifetimes = ~12 series/entity before 2026-07-16) does **not** affect this dashboard. Verified against the JSON on this branch: all 50 datasource references are `influxdb`, zero Prometheus targets or `expr` fields remain (PR #271 removed them all, this PR keeps it that way). The default Oct-2024+ window is therefore safe — InfluxDB series here are only split by the `friendly_name` tag change, which this PR already handles with `group() |> sort(_time)`.
k.renner merged commit 76404f6bd9 into master 2026-07-17 13:16:48 +00:00
Sign in to join this conversation.