From 0cda291abaf5ee96c258aa93855ccac017404b0f Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 17 Jul 2026 01:20:50 +0200 Subject: [PATCH] fix(heatpump-roi): repair solar ratio in cost chart, honest annual savings, since-install blended price - 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 --- .../monitoring/dashboards/heatpump-roi.json | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/kubernetes/apps/monitoring/dashboards/heatpump-roi.json b/kubernetes/apps/monitoring/dashboards/heatpump-roi.json index 7f548f60..0ae7254c 100644 --- a/kubernetes/apps/monitoring/dashboards/heatpump-roi.json +++ b/kubernetes/apps/monitoring/dashboards/heatpump-roi.json @@ -1,6 +1,6 @@ { "annotations": {"list": []}, - "description": "Heat Pump ROI Analysis - Viessmann Vitocal 250-A. All panels read from InfluxDB (bucket suedstr28), which is authoritative and live. After the 2026-07 ViCare->open3e/MQTT migration the heat-pump/PV entities were renamed; panels query the current entity_ids and, where an entity was renamed, UNION the pre-migration e3_* series with the post-migration series so history stays unbroken.", + "description": "Heat Pump ROI Analysis - Viessmann Vitocal 250-A. All panels read from InfluxDB (bucket suedstr28), which is authoritative and live. After the 2026-07 ViCare->open3e/MQTT migration the heat-pump/PV entities were renamed; panels query the current entity_ids and, where an entity was renamed, UNION the pre-migration e3_* series with the post-migration series so history stays unbroken. The blended electricity price weights solar vs grid by the self-consumption ratio accumulated since the heat-pump install (Oct 2024); annual savings and payback use the actual trailing-12-month consumption.", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, @@ -29,7 +29,7 @@ "gridPos": {"h": 4, "w": 4, "x": 4, "y": 0}, "id": 2, "options": {"colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, - "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "import \"array\"\narray.from(rows: [{_time: now(), _value: 6650.0}])", "refId": "A"}], + "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "import \"array\"\narray.from(rows: [{_time: now(), _value: float(v: \"${oil_baseline}\")}])", "refId": "A"}], "title": "Oil Baseline (Annual)", "description": "Based on 2022-2023 oil orders: ~5,000L at ~1.33 EUR/L average", "type": "stat" @@ -43,9 +43,9 @@ "gridPos": {"h": 4, "w": 4, "x": 8, "y": 0}, "id": 3, "options": {"colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, - "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "from(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" or r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" or r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\")) |> last() |> keep(columns: [\"entity_id\", \"_value\"]) |> pivot(rowKey: [], columnKey: [\"entity_id\"], valueColumn: \"_value\") |> map(fn: (r) => ({_time: now(), self: r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, total: r.vitocharge_vx3_vx3_grid_supplied_energy + r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy})) |> map(fn: (r) => ({_time: r._time, _value: (r.self / r.total) * ${solar_price} + (1.0 - r.self / r.total) * ${grid_price}}))", "refId": "A"}], + "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "import \"array\"\ngi = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_import_from_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nge = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_export_to_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nsp = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_solar_energy_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nselfc = sp[0] - ge[0]\ntotal = gi[0] + selfc\nblended = (selfc / total) * ${solar_price} + (1.0 - selfc / total) * ${grid_price}\narray.from(rows: [{_time: now(), _value: blended}])", "refId": "A"}], "title": "Blended Price", - "description": "Weighted average: solar self-consumption @ EUR ${solar_price}, grid @ EUR ${grid_price}. Self-consumption ratio from Vitocharge PV/grid counters.", + "description": "Weighted average: solar self-consumption @ EUR ${solar_price}, grid @ EUR ${grid_price}. Self-consumption ratio from Vitocharge PV/grid counter deltas since heat-pump install (Oct 2024), unioning pre-migration ViCare with post-migration open3e counters.", "type": "stat" }, { @@ -57,21 +57,23 @@ "gridPos": {"h": 4, "w": 4, "x": 12, "y": 0}, "id": 4, "options": {"colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, - "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "from(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" or r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\")) |> last() |> keep(columns: [\"_value\"]) |> group() |> sum()", "refId": "A"}], + "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "from(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" or r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\")) |> group(columns: [\"entity_id\"]) |> max() |> group() |> sum()", "refId": "A"}], "title": "Total Consumption", + "description": "Cumulative heat-pump electricity since install: sum of the heating + DHW utility_meter counters.", "type": "stat" }, { "datasource": {"type": "influxdb", "uid": "influxdb"}, "fieldConfig": { - "defaults": {"color": {"mode": "thresholds"}, "mappings": [], "thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}, {"color": "yellow", "value": 3000}, {"color": "red", "value": 5000}]}, "unit": "currencyEUR"}, + "defaults": {"color": {"mode": "thresholds"}, "mappings": [], "thresholds": {"mode": "absolute", "steps": [{"color": "yellow", "value": null}]}, "unit": "currencyEUR"}, "overrides": [] }, "gridPos": {"h": 4, "w": 4, "x": 16, "y": 0}, "id": 5, "options": {"colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, - "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "from(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" or r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\" or r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" or r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" or r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\")) |> last() |> keep(columns: [\"entity_id\", \"_value\"]) |> pivot(rowKey: [], columnKey: [\"entity_id\"], valueColumn: \"_value\") |> map(fn: (r) => ({_time: now(), self: r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, total: r.vitocharge_vx3_vx3_grid_supplied_energy + r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, cons: r.e3_vitocal_heating_energy_consumption_total + r.e3_vitocal_dhw_energy_consumption_total})) |> map(fn: (r) => ({_time: r._time, _value: r.cons * ((r.self / r.total) * ${solar_price} + (1.0 - r.self / r.total) * ${grid_price})}))", "refId": "A"}], - "title": "Running Cost (Total)", + "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "import \"array\"\ngi = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_import_from_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nge = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_export_to_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nsp = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_solar_energy_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nselfc = sp[0] - ge[0]\ntotal = gi[0] + selfc\nblended = (selfc / total) * ${solar_price} + (1.0 - selfc / total) * ${grid_price}\ncons = from(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" or r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\")) |> group(columns: [\"entity_id\"]) |> max() |> group() |> sum() |> findColumn(fn: (key) => true, column: \"_value\")\narray.from(rows: [{_time: now(), _value: cons[0] * blended}])", "refId": "A"}], + "title": "Running Cost (Since Install)", + "description": "Total consumption times blended price. Cumulative since Oct 2024, not an annual figure.", "type": "stat" }, { @@ -83,8 +85,9 @@ "gridPos": {"h": 4, "w": 4, "x": 20, "y": 0}, "id": 6, "options": {"colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, - "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "from(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"vitocal_250_a_vitocal_heating_electric_energy_year\" or r.entity_id == \"vitocal_250_a_vitocal_dhw_electric_energy_year\" or r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" or r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" or r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\")) |> last() |> keep(columns: [\"entity_id\", \"_value\"]) |> pivot(rowKey: [], columnKey: [\"entity_id\"], valueColumn: \"_value\") |> map(fn: (r) => ({_time: now(), self: r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, total: r.vitocharge_vx3_vx3_grid_supplied_energy + r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, cons: r.vitocal_250_a_vitocal_heating_electric_energy_year + r.vitocal_250_a_vitocal_dhw_electric_energy_year})) |> map(fn: (r) => ({_time: r._time, _value: r.cons * ((r.self / r.total) * ${solar_price} + (1.0 - r.self / r.total) * ${grid_price})}))", "refId": "A"}], + "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "import \"array\"\ngi = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_import_from_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nge = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_export_to_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nsp = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_solar_energy_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nselfc = sp[0] - ge[0]\ntotal = gi[0] + selfc\nblended = (selfc / total) * ${solar_price} + (1.0 - selfc / total) * ${grid_price}\nyr = from(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"vitocal_250_a_vitocal_heating_electric_energy_year\" or r.entity_id == \"vitocal_250_a_vitocal_dhw_electric_energy_year\")) |> group(columns: [\"entity_id\"]) |> last() |> group() |> sum() |> findColumn(fn: (key) => true, column: \"_value\")\narray.from(rows: [{_time: now(), _value: yr[0] * blended}])", "refId": "A"}], "title": "Cost This Year", + "description": "Device year counters (heating + DHW electric energy) times blended price.", "type": "stat" }, { @@ -135,9 +138,9 @@ "gridPos": {"h": 5, "w": 4, "x": 9, "y": 4}, "id": 10, "options": {"colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, - "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "import \"date\"\nfrom(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"vitocal_250_a_vitocal_heating_electric_energy_year\" or r.entity_id == \"vitocal_250_a_vitocal_dhw_electric_energy_year\" or r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" or r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" or r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\")) |> last() |> keep(columns: [\"entity_id\", \"_value\"]) |> pivot(rowKey: [], columnKey: [\"entity_id\"], valueColumn: \"_value\") |> map(fn: (r) => ({_time: now(), self: r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, total: r.vitocharge_vx3_vx3_grid_supplied_energy + r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, costYearRaw: r.vitocal_250_a_vitocal_heating_electric_energy_year + r.vitocal_250_a_vitocal_dhw_electric_energy_year})) |> map(fn: (r) => ({_time: r._time, _value: 6650.0 - ((r.costYearRaw * ((r.self / r.total) * ${solar_price} + (1.0 - r.self / r.total) * ${grid_price})) * 12.0 / (float(v: date.month(t: now())) + float(v: date.monthDay(t: now())) / 30.0))}))", "refId": "A"}], - "title": "Projected Annual Savings", - "description": "Oil baseline minus projected annual electricity cost", + "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "import \"array\"\ngi = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_import_from_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nge = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_export_to_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nsp = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_solar_energy_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nselfc = sp[0] - ge[0]\ntotal = gi[0] + selfc\nblended = (selfc / total) * ${solar_price} + (1.0 - selfc / total) * ${grid_price}\nhp = from(bucket: \"suedstr28\") |> range(start: -365d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" or r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\")) |> group(columns: [\"entity_id\"]) |> spread() |> group() |> sum() |> findColumn(fn: (key) => true, column: \"_value\")\narray.from(rows: [{_time: now(), _value: float(v: \"${oil_baseline}\") - hp[0] * blended}])", "refId": "A"}], + "title": "Annual Savings (Trailing 12M)", + "description": "Oil baseline minus actual heat-pump electricity cost of the last 365 days (counter deltas x blended price). A true rolling year, immune to seasonal extrapolation bias.", "type": "stat" }, { @@ -149,9 +152,9 @@ "gridPos": {"h": 5, "w": 4, "x": 13, "y": 4}, "id": 11, "options": {"colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, - "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "import \"date\"\nfrom(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"vitocal_250_a_vitocal_heating_electric_energy_year\" or r.entity_id == \"vitocal_250_a_vitocal_dhw_electric_energy_year\" or r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" or r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" or r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\")) |> last() |> keep(columns: [\"entity_id\", \"_value\"]) |> pivot(rowKey: [], columnKey: [\"entity_id\"], valueColumn: \"_value\") |> map(fn: (r) => ({_time: now(), self: r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, total: r.vitocharge_vx3_vx3_grid_supplied_energy + r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, costYearRaw: r.vitocal_250_a_vitocal_heating_electric_energy_year + r.vitocal_250_a_vitocal_dhw_electric_energy_year})) |> map(fn: (r) => ({_time: r._time, _value: float(v: \"${investment_cost}\") / (6650.0 - ((r.costYearRaw * ((r.self / r.total) * ${solar_price} + (1.0 - r.self / r.total) * ${grid_price})) * 12.0 / (float(v: date.month(t: now())) + float(v: date.monthDay(t: now())) / 30.0)))}))", "refId": "A"}], + "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "import \"array\"\ngi = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_import_from_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nge = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_export_to_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nsp = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_solar_energy_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nselfc = sp[0] - ge[0]\ntotal = gi[0] + selfc\nblended = (selfc / total) * ${solar_price} + (1.0 - selfc / total) * ${grid_price}\nhp = from(bucket: \"suedstr28\") |> range(start: -365d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" or r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\")) |> group(columns: [\"entity_id\"]) |> spread() |> group() |> sum() |> findColumn(fn: (key) => true, column: \"_value\")\narray.from(rows: [{_time: now(), _value: float(v: \"${investment_cost}\") / (float(v: \"${oil_baseline}\") - hp[0] * blended)}])", "refId": "A"}], "title": "Payback Period (Years)", - "description": "Investment / Annual Savings", + "description": "Investment / annual savings (trailing 12 months)", "type": "stat" }, { @@ -163,7 +166,7 @@ "gridPos": {"h": 5, "w": 4, "x": 17, "y": 4}, "id": 12, "options": {"colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, - "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "from(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" or r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\" or r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" or r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" or r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\")) |> last() |> keep(columns: [\"entity_id\", \"_value\"]) |> pivot(rowKey: [], columnKey: [\"entity_id\"], valueColumn: \"_value\") |> map(fn: (r) => ({_time: now(), self: r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, total: r.vitocharge_vx3_vx3_grid_supplied_energy + r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, cons: r.e3_vitocal_heating_energy_consumption_total + r.e3_vitocal_dhw_energy_consumption_total})) |> map(fn: (r) => ({_time: r._time, _value: (6650.0 * (float(v: uint(v: now()) - uint(v: 2024-10-01T00:00:00Z)) / 1000000000.0) / 31536000.0) - (r.cons * ((r.self / r.total) * ${solar_price} + (1.0 - r.self / r.total) * ${grid_price}))}))", "refId": "A"}], + "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "import \"array\"\ngi = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_import_from_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nge = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_export_to_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nsp = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_solar_energy_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nselfc = sp[0] - ge[0]\ntotal = gi[0] + selfc\nblended = (selfc / total) * ${solar_price} + (1.0 - selfc / total) * ${grid_price}\ncons = from(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" or r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\")) |> group(columns: [\"entity_id\"]) |> max() |> group() |> sum() |> findColumn(fn: (key) => true, column: \"_value\")\narray.from(rows: [{_time: now(), _value: float(v: \"${oil_baseline}\") * (float(v: uint(v: now()) - uint(v: 2024-10-01T00:00:00Z)) / 1000000000.0) / 31536000.0 - cons[0] * blended}])", "refId": "A"}], "title": "Savings Since Install", "description": "Oil baseline prorated since Oct 2024 minus actual costs", "type": "stat" @@ -177,7 +180,7 @@ "gridPos": {"h": 5, "w": 3, "x": 21, "y": 4}, "id": 40, "options": {"minVizHeight": 75, "minVizWidth": 75, "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "showThresholdLabels": false, "showThresholdMarkers": true}, - "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "from(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" or r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\" or r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" or r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" or r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\")) |> last() |> keep(columns: [\"entity_id\", \"_value\"]) |> pivot(rowKey: [], columnKey: [\"entity_id\"], valueColumn: \"_value\") |> map(fn: (r) => ({_time: now(), self: r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, total: r.vitocharge_vx3_vx3_grid_supplied_energy + r.vitocharge_vx3_vx3_pv_production_total / 1000.0 - r.vitocharge_vx3_vx3_grid_feed_in_energy, cons: r.e3_vitocal_heating_energy_consumption_total + r.e3_vitocal_dhw_energy_consumption_total})) |> map(fn: (r) => ({_time: r._time, _value: ((6650.0 * (float(v: uint(v: now()) - uint(v: 2024-10-01T00:00:00Z)) / 1000000000.0) / 31536000.0) - (r.cons * ((r.self / r.total) * ${solar_price} + (1.0 - r.self / r.total) * ${grid_price}))) / float(v: \"${investment_cost}\") * 100.0}))", "refId": "A"}], + "targets": [{"datasource": {"type": "influxdb", "uid": "influxdb"}, "query": "import \"array\"\ngi = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_import_from_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nge = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_export_to_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nsp = union(tables: [from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_solar_energy_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value})), from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0}))]) |> group() |> spread() |> findColumn(fn: (key) => true, column: \"_value\")\nselfc = sp[0] - ge[0]\ntotal = gi[0] + selfc\nblended = (selfc / total) * ${solar_price} + (1.0 - selfc / total) * ${grid_price}\ncons = from(bucket: \"suedstr28\") |> range(start: -30d) |> filter(fn: (r) => r._field == \"value\" and (r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" or r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\")) |> group(columns: [\"entity_id\"]) |> max() |> group() |> sum() |> findColumn(fn: (key) => true, column: \"_value\")\narray.from(rows: [{_time: now(), _value: (float(v: \"${oil_baseline}\") * (float(v: uint(v: now()) - uint(v: 2024-10-01T00:00:00Z)) / 1000000000.0) / 31536000.0 - cons[0] * blended) / float(v: \"${investment_cost}\") * 100.0}])", "refId": "A"}], "title": "ROI Progress", "description": "Percentage of investment recovered through savings", "type": "gauge" @@ -211,12 +214,12 @@ "targets": [ { "datasource": {"type": "influxdb", "uid": "influxdb"}, - "query": "from(bucket: \"suedstr28\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"kWh\")\n |> filter(fn: (r) => r.entity_id == \"e3_vitocal_heating_energy_consumption_total\")\n |> filter(fn: (r) => r._field == \"value\")\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"Heating Total\"})", + "query": "from(bucket: \"suedstr28\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"kWh\")\n |> filter(fn: (r) => r.entity_id == \"e3_vitocal_heating_energy_consumption_total\")\n |> filter(fn: (r) => r._field == \"value\")\n |> group()\n |> sort(columns: [\"_time\"])\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"Heating Total\"})", "refId": "A" }, { "datasource": {"type": "influxdb", "uid": "influxdb"}, - "query": "from(bucket: \"suedstr28\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"kWh\")\n |> filter(fn: (r) => r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\")\n |> filter(fn: (r) => r._field == \"value\")\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"DHW Total\"})", + "query": "from(bucket: \"suedstr28\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"kWh\")\n |> filter(fn: (r) => r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\")\n |> filter(fn: (r) => r._field == \"value\")\n |> group()\n |> sort(columns: [\"_time\"])\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"DHW Total\"})", "refId": "B" } ], @@ -244,12 +247,12 @@ "targets": [ { "datasource": {"type": "influxdb", "uid": "influxdb"}, - "query": "// Heat pump consumption (survivor utility_meters, continuous)\nheatpump_heating = from(bucket: \"suedstr28\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"kWh\" and r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" and r._field == \"value\")\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n\nheatpump_dhw = from(bucket: \"suedstr28\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"kWh\" and r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\" and r._field == \"value\")\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n\n// Grid import: old ViCare (Wh) UNION new open3e (kWh) -> normalize to kWh\ngrid_import = union(tables: [\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_import_from_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})),\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))])\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n\n// Grid export: old ViCare (Wh) UNION new open3e (kWh) -> normalize to kWh\ngrid_export = union(tables: [\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_export_to_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})),\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))])\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n\n// Solar production: old ViCare (kWh) UNION new open3e (Wh) -> normalize to kWh\nsolar_prod = union(tables: [\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_solar_energy_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value})),\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0}))])\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n\n// Chain joins (Flux only supports 2-way joins)\njoin1 = join(tables: {h: heatpump_heating, d: heatpump_dhw}, on: [\"_time\"])\njoin2 = join(tables: {j1: join1, gi: grid_import}, on: [\"_time\"])\njoin3 = join(tables: {j2: join2, ge: grid_export}, on: [\"_time\"])\njoin(tables: {j3: join3, sp: solar_prod}, on: [\"_time\"])\n |> map(fn: (r) => {\n heatpump = r._value_h + r._value_d\n self_consumption = r._value_sp - r._value_ge\n total_consumption = r._value_gi + self_consumption\n solar_ratio = if total_consumption > 0.0 then self_consumption / total_consumption else 0.0\n return {_time: r._time, _value: heatpump * ((1.0 - solar_ratio) * ${grid_price} + solar_ratio * ${solar_price})}\n })\n |> rename(columns: {_value: \"Electricity Cost\"})", + "query": "// Heat pump consumption (survivor utility_meters, continuous). group()+sort() merges the\n// friendly_name-era table split (tag changed 2026-03) so daily lasts stay time-ordered.\nheatpump_heating = from(bucket: \"suedstr28\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"kWh\" and r.entity_id == \"e3_vitocal_heating_energy_consumption_total\" and r._field == \"value\")\n |> group()\n |> sort(columns: [\"_time\"])\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"h\"})\n\nheatpump_dhw = from(bucket: \"suedstr28\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"kWh\" and r.entity_id == \"e3_vitocal_dhw_energy_consumption_total\" and r._field == \"value\")\n |> group()\n |> sort(columns: [\"_time\"])\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"d\"})\n\n// Grid import: old ViCare (Wh) UNION new open3e (kWh) -> normalize to kWh\ngrid_import = union(tables: [\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_import_from_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})),\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))])\n |> group()\n |> sort(columns: [\"_time\"])\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"gi\"})\n\n// Grid export: old ViCare (Wh) UNION new open3e (kWh) -> normalize to kWh\ngrid_export = union(tables: [\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_export_to_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})),\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))])\n |> group()\n |> sort(columns: [\"_time\"])\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"ge\"})\n\n// Solar production: old ViCare (kWh) UNION new open3e (Wh) -> normalize to kWh\nsolar_prod = union(tables: [\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_solar_energy_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value})),\n from(bucket: \"suedstr28\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0}))])\n |> group()\n |> sort(columns: [\"_time\"])\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"sp\"})\n\n// Counter values at heat-pump install (Oct 2024): subtracted so the self-consumption ratio\n// only counts flows since install, not the PV history from May 2023.\ngiB = union(tables: [\n from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_import_from_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})),\n from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_supplied_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))])\n |> group() |> min() |> findColumn(fn: (key) => true, column: \"_value\")\ngeB = union(tables: [\n from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_energy_export_to_grid\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0})),\n from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_grid_feed_in_energy\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value}))])\n |> group() |> min() |> findColumn(fn: (key) => true, column: \"_value\")\nspB = union(tables: [\n from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"e3_vitocharge_03_solar_energy_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value})),\n from(bucket: \"suedstr28\") |> range(start: 2024-10-01T00:00:00Z) |> filter(fn: (r) => r.entity_id == \"vitocharge_vx3_vx3_pv_production_total\" and r._field == \"value\") |> map(fn: (r) => ({_time: r._time, _value: r._value / 1000.0}))])\n |> group() |> min() |> findColumn(fn: (key) => true, column: \"_value\")\n\n// Chain joins (Flux only supports 2-way joins). Columns are pre-renamed to h/d/gi/ge/sp because\n// join() suffixes colliding _value columns unpredictably across chained joins.\njoin1 = join(tables: {h: heatpump_heating, d: heatpump_dhw}, on: [\"_time\"])\njoin2 = join(tables: {j1: join1, gi: grid_import}, on: [\"_time\"])\njoin3 = join(tables: {j2: join2, ge: grid_export}, on: [\"_time\"])\njoin(tables: {j3: join3, sp: solar_prod}, on: [\"_time\"])\n |> map(fn: (r) => {\n heatpump = r.h + r.d\n self_consumption = (r.sp - spB[0]) - (r.ge - geB[0])\n total_consumption = (r.gi - giB[0]) + self_consumption\n solar_ratio = if total_consumption > 0.0 then self_consumption / total_consumption else 0.0\n return {_time: r._time, _value: heatpump * ((1.0 - solar_ratio) * ${grid_price} + solar_ratio * ${solar_price})}\n })\n |> rename(columns: {_value: \"Electricity Cost\"})", "refId": "A" }, { "datasource": {"type": "influxdb", "uid": "influxdb"}, - "query": "from(bucket: \"suedstr28\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"kWh\")\n |> filter(fn: (r) => r.entity_id == \"e3_vitocal_heating_energy_consumption_total\")\n |> filter(fn: (r) => r._field == \"value\")\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n |> map(fn: (r) => ({_time: r._time, _value: 6650.0 * float(v: uint(v: r._time) - uint(v: 2024-10-01T00:00:00Z)) / 31536000000000000.0}))\n |> rename(columns: {_value: \"Oil Baseline\"})", + "query": "from(bucket: \"suedstr28\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"kWh\")\n |> filter(fn: (r) => r.entity_id == \"e3_vitocal_heating_energy_consumption_total\")\n |> filter(fn: (r) => r._field == \"value\")\n |> group()\n |> sort(columns: [\"_time\"])\n |> aggregateWindow(every: 1d, fn: last, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\"])\n |> map(fn: (r) => ({_time: r._time, _value: float(v: \"${oil_baseline}\") * float(v: uint(v: r._time) - uint(v: 2024-10-01T00:00:00Z)) / 31536000000000000.0}))\n |> rename(columns: {_value: \"Oil Baseline\"})", "refId": "B" } ], @@ -409,6 +412,18 @@ "query": "72968", "type": "custom" }, + { + "current": {"selected": true, "text": "6650", "value": "6650"}, + "description": "Annual oil heating cost baseline: ~5,000L at ~1.33 EUR/L (2022-2023 oil orders)", + "hide": 0, + "label": "Oil Baseline", + "name": "oil_baseline", + "options": [ + {"selected": true, "text": "6650", "value": "6650"} + ], + "query": "6650", + "type": "custom" + }, { "current": {"selected": true, "text": "0.08", "value": "0.08"}, "description": "Electricity price when battery full/solar excess", -- 2.54.0