Files
homelab/kubernetes/apps/monitoring/dashboards/power-flow.json
claude-botandClaude Opus 4.8 1c333826ff fix(power-flow): dead sensors, balance-based Home total, dedup history
- Server node: sensor.keller_server_power is dead (UPS retired) -> Wave PM
  Mini server outlet (kg_vorratskeller_wave_pm_mini_server_steckdose)
- Garden node: sensor.eg_garten_teichpumpe_power_2 is dead ->
  sensor.shelly_teichpumpe_power_0 (live, peaks ~77 W)
- Home node/series: floor sums only cover submetered circuits (702 W shown
  vs ~1.1 kW real); Home is now the meter balance PV + Grid + Battery, with
  a new grey 'Rest' node for the non-submetered remainder
- Flow lines: Heat Pump is metered inside Keller (was drawn from EG);
  Garden is on floor Aussenbereich, in no floor sum (was drawn from OG)
- Drop redundant batteryIn target (derived from the signed battery sensor);
  update stale comments from the retired *_absolute sensors
- History: max by (entity) guards against duplicate-scrape residue that
  doubled the summed Home series; sign hints in Battery/Grid legends;
  grid query pinned to entity id instead of friendly_name

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 00:33:05 +02:00

87 lines
13 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"annotations": {"list": []},
"description": "Energy flow visualization showing power sources and consumption by floor",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"liveNow": true,
"panels": [
{
"datasource": {"type": "prometheus", "uid": "prometheus"},
"description": "Live-Energiefluss. Vorzeichen: Batterie + = Entladen / − = Laden (open3e), Netz + = Bezug / − = Einspeisung. Home = PV + Netz + Batterie (Bilanz am Zähler). Die Etagen-Summen (powercalc) decken nur submeterte Kreise ab — 'Rest' ist die Differenz zur Bilanz. Server & Wärmepumpe sind in der Keller-Summe enthalten; die Teichpumpe (Floor Außenbereich) ist in keiner Etage.",
"gridPos": {"h": 18, "w": 24, "x": 0, "y": 0},
"id": 1,
"options": {
"getOption": "const data = context.panel.data;\n// null when the series is missing (dead sensor / no data) -> rendered as 'n/a', not a silent 0\nconst getVal = (refId) => {\n const s = data.series.find(s => s.refId === refId);\n const v = s?.fields[1]?.values[0];\n return (v === undefined || v === null) ? null : v;\n};\nconst num = (v) => v ?? 0;\n\n// Get all values\nconst solar = num(getVal('solar'));\nconst grid = num(getVal('grid'));\nconst battery = num(getVal('batteryOut'));\nconst batterySoc = num(getVal('soc'));\nconst keller = num(getVal('keller'));\nconst eg = num(getVal('eg'));\nconst og = num(getVal('og'));\nconst dg = num(getVal('dg'));\nconst server = getVal('server');\nconst heatpump = getVal('heatpump');\nconst garden = getVal('garden');\n\n// Derived values - vitocharge_vx3_vx3_battery_power is SIGNED: + = discharge, - = charge (open3e);\n// smart meter grid power is SIGNED: + = import, - = export\nconst solarPower = Math.max(0, solar);\nconst gridImport = Math.max(0, grid);\nconst gridExport = Math.max(0, -grid);\nconst batteryDischarge = Math.max(0, battery);\nconst batteryCharge = Math.max(0, -battery);\n// Total household consumption from the meter balance; the floor sums only cover submetered circuits\nconst homeTotal = Math.max(0, solar + grid + battery);\nconst floorsPower = keller + eg + og + dg;\nconst rest = Math.max(0, homeTotal - floorsPower - num(garden));\n\nconst fmt = (w) => Math.abs(w) >= 1000 ? (w/1000).toFixed(1) + ' kW' : Math.round(w) + ' W';\n\n// Chart dimensions\nconst w = context.panel.chart.getWidth();\nconst h = context.panel.chart.getHeight();\n\n// Battery display: show discharge or charge with direction indicator\nconst batteryDisplay = batteryDischarge > 0 ? fmt(batteryDischarge) + ' ↑' : (batteryCharge > 0 ? fmt(batteryCharge) + ' ↓' : '0 W');\nconst gridDisplay = gridImport > 0 ? fmt(gridImport) + ' →' : (gridExport > 0 ? fmt(gridExport) + ' ←' : '0 W');\n\n// Node definitions with percentage positions\nconst nodes = [\n // Sources (left side)\n { name: 'Solar', value: solarPower, x: w * 0.1, y: h * 0.15, color: '#F59E0B', icon: '☀️' },\n { name: 'Grid', value: gridImport > 0 ? gridImport : -gridExport, x: w * 0.1, y: h * 0.5, color: '#3B82F6', icon: '⚡', displayText: gridDisplay },\n { name: 'Battery', value: batteryDischarge > 0 ? batteryDischarge : -batteryCharge, x: w * 0.1, y: h * 0.85, color: '#10B981', icon: '🔋', extra: Math.round(batterySoc) + '%', displayText: batteryDisplay },\n \n // Home (center)\n { name: 'Home', value: homeTotal, x: w * 0.4, y: h * 0.5, color: '#F97316', icon: '🏠', size: 70 },\n \n // Floors (right side - top)\n { name: 'Keller', value: keller, x: w * 0.7, y: h * 0.1, color: '#1F60C4', icon: 'KG' },\n { name: 'EG', value: eg, x: w * 0.7, y: h * 0.3, color: '#37872D', icon: 'EG' },\n { name: 'OG', value: og, x: w * 0.7, y: h * 0.5, color: '#E0B400', icon: 'OG' },\n { name: 'DG', value: dg, x: w * 0.7, y: h * 0.7, color: '#C4162A', icon: 'DG' },\n { name: 'Rest', value: rest, x: w * 0.7, y: h * 0.9, color: '#9CA3AF', icon: '…' },\n \n // Large consumers (right side - bottom)\n { name: 'Server', value: num(server), x: w * 0.9, y: h * 0.2, color: '#8F3BB8', icon: '🖥️', displayText: server === null ? 'n/a' : undefined },\n { name: 'Heat Pump', value: num(heatpump), x: w * 0.9, y: h * 0.45, color: '#5794F2', icon: '🌡️', displayText: heatpump === null ? 'n/a' : undefined },\n { name: 'Garden', value: num(garden), x: w * 0.9, y: h * 0.7, color: '#96D98D', icon: '🌳', displayText: garden === null ? 'n/a' : undefined }\n];\n\n// Build graphic elements\nconst elements = [];\n\n// Helper to draw flow line\nconst drawLine = (x1, y1, x2, y2, value, color) => {\n if (value <= 0) return;\n const width = Math.max(2, Math.min(value / 300, 15));\n elements.push({\n type: 'line',\n shape: { x1, y1, x2, y2 },\n style: { stroke: color, lineWidth: width, opacity: 0.6 }\n });\n};\n\n// Draw flow lines from sources to home\nif (solarPower > 0) drawLine(w*0.1+30, h*0.15, w*0.4-35, h*0.5, solarPower, '#F59E0B');\nif (gridImport > 0) drawLine(w*0.1+30, h*0.5, w*0.4-35, h*0.5, gridImport, '#3B82F6');\nif (gridExport > 0) drawLine(w*0.4-35, h*0.5, w*0.1+30, h*0.5, gridExport, '#22C55E');\nif (batteryDischarge > 0) drawLine(w*0.1+30, h*0.85, w*0.4-35, h*0.5, batteryDischarge, '#10B981');\nif (batteryCharge > 0) drawLine(w*0.4-35, h*0.5, w*0.1+30, h*0.85, batteryCharge, '#60A5FA');\n\n// Draw flow lines from home to floors\nif (keller > 0) drawLine(w*0.4+35, h*0.5, w*0.7-25, h*0.1, keller, '#1F60C4');\nif (eg > 0) drawLine(w*0.4+35, h*0.5, w*0.7-25, h*0.3, eg, '#37872D');\nif (og > 0) drawLine(w*0.4+35, h*0.5, w*0.7-25, h*0.5, og, '#E0B400');\nif (dg > 0) drawLine(w*0.4+35, h*0.5, w*0.7-25, h*0.7, dg, '#C4162A');\nif (rest > 0) drawLine(w*0.4+35, h*0.5, w*0.7-25, h*0.9, rest, '#9CA3AF');\n\n// Sub-consumers: Server & Heat Pump are metered inside Keller; Garden (floor Außenbereich) is in no floor sum -> hangs off Home\nif (num(server) > 0) drawLine(w*0.7+25, h*0.1, w*0.9-25, h*0.2, num(server), '#8F3BB8');\nif (num(heatpump) > 0) drawLine(w*0.7+25, h*0.1, w*0.9-25, h*0.45, num(heatpump), '#5794F2');\nif (num(garden) > 0) drawLine(w*0.4+35, h*0.5, w*0.9-25, h*0.7, num(garden), '#96D98D');\n\n// Draw nodes\nnodes.forEach(node => {\n const size = node.size || 50;\n // Circle\n elements.push({\n type: 'circle',\n shape: { cx: node.x, cy: node.y, r: size/2 },\n style: { fill: '#1a1a2e', stroke: node.color, lineWidth: 4 }\n });\n // Icon\n elements.push({\n type: 'text',\n style: { text: node.icon, x: node.x, y: node.y - 8, fontSize: 18, textAlign: 'center', textVerticalAlign: 'middle' }\n });\n // Value\n let valueText = node.displayText || fmt(Math.abs(node.value));\n if (node.extra) valueText += '\\n' + node.extra;\n elements.push({\n type: 'text',\n style: { text: valueText, x: node.x, y: node.y + 14, fontSize: 11, fill: '#fff', textAlign: 'center', textVerticalAlign: 'middle' }\n });\n // Label\n elements.push({\n type: 'text',\n style: { text: node.name, x: node.x, y: node.y + size/2 + 12, fontSize: 11, fill: '#9CA3AF', textAlign: 'center' }\n });\n});\n\nreturn { backgroundColor: 'transparent', graphic: { elements } };"
},
"targets": [
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "homeassistant_sensor_power_w{entity=\"sensor.vitocharge_vx3_vx3_pv_power\"}", "instant": true, "refId": "solar"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "homeassistant_sensor_power_w{entity=\"sensor.smart_meter_sum_active_instantaneous_power\"}", "instant": true, "refId": "grid"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "homeassistant_sensor_power_w{entity=\"sensor.vitocharge_vx3_vx3_battery_power\"}", "instant": true, "refId": "batteryOut"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "homeassistant_sensor_battery_percent{entity=\"sensor.vitocharge_vx3_vx3_battery_soc\"}", "instant": true, "refId": "soc"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "homeassistant_sensor_power_w{entity=\"sensor.keller_power\"}", "instant": true, "refId": "keller"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "homeassistant_sensor_power_w{entity=\"sensor.erdgeschoss_power\"}", "instant": true, "refId": "eg"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "homeassistant_sensor_power_w{entity=\"sensor.obergeschoss_power\"}", "instant": true, "refId": "og"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "homeassistant_sensor_power_w{entity=\"sensor.dachgeschoss_power\"}", "instant": true, "refId": "dg"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "homeassistant_sensor_power_w{entity=\"sensor.kg_vorratskeller_wave_pm_mini_server_steckdose_electric_consumption_w\"}", "instant": true, "refId": "server"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "homeassistant_sensor_power_w{entity=\"sensor.stromverbrauch_heizungsanlage\"}", "instant": true, "refId": "heatpump"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "homeassistant_sensor_power_w{entity=\"sensor.shelly_teichpumpe_power_0\"}", "instant": true, "refId": "garden"}
],
"title": "Power Flow",
"type": "volkovlabs-echarts-panel"
},
{
"collapsed": false,
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 18},
"id": 100,
"panels": [],
"title": "Power History",
"type": "row"
},
{
"datasource": {"type": "prometheus", "uid": "prometheus"},
"description": "Home = PV + Netz + Batterie (bilanzierte Gesamtlast, alle Werte vorzeichenbehaftet). max by (entity) dedupliziert Alt-Serien aus dem Doppel-Scrape (behoben 2026-07-16).",
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"custom": {"axisCenteredZero": true, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 30, "gradientMode": "opacity", "hideFrom": {"legend": false, "tooltip": false, "viz": false}, "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": {"type": "linear"}, "showPoints": "never", "spanNulls": false, "stacking": {"group": "A", "mode": "none"}, "thresholdsStyle": {"mode": "off"}},
"mappings": [],
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}]},
"unit": "watt"
},
"overrides": [
{"matcher": {"id": "byName", "options": "Solar"}, "properties": [{"id": "color", "value": {"fixedColor": "#F59E0B", "mode": "fixed"}}]},
{"matcher": {"id": "byName", "options": "Battery (+ Entladen / − Laden)"}, "properties": [{"id": "color", "value": {"fixedColor": "#10B981", "mode": "fixed"}}]},
{"matcher": {"id": "byName", "options": "Grid (+ Bezug / − Einspeisung)"}, "properties": [{"id": "color", "value": {"fixedColor": "#3B82F6", "mode": "fixed"}}]},
{"matcher": {"id": "byName", "options": "Home"}, "properties": [{"id": "color", "value": {"fixedColor": "#F97316", "mode": "fixed"}}]}
]
},
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 19},
"id": 40,
"options": {"legend": {"calcs": ["mean", "max", "lastNotNull"], "displayMode": "table", "placement": "right", "showLegend": true}, "tooltip": {"mode": "multi", "sort": "desc"}},
"targets": [
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "max by (entity) (homeassistant_sensor_power_w{entity=\"sensor.vitocharge_vx3_vx3_pv_power\"})", "legendFormat": "Solar", "refId": "A"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "max by (entity) (homeassistant_sensor_power_w{entity=\"sensor.vitocharge_vx3_vx3_battery_power\"})", "legendFormat": "Battery (+ Entladen / − Laden)", "refId": "B"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "max by (entity) (homeassistant_sensor_power_w{entity=\"sensor.smart_meter_sum_active_instantaneous_power\"})", "legendFormat": "Grid (+ Bezug / − Einspeisung)", "refId": "C"},
{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "sum(max by (entity) (homeassistant_sensor_power_w{entity=~\"sensor.vitocharge_vx3_vx3_pv_power|sensor.vitocharge_vx3_vx3_battery_power|sensor.smart_meter_sum_active_instantaneous_power\"}))", "legendFormat": "Home", "refId": "D"}
],
"title": "Power Sources & Consumption",
"type": "timeseries"
}
],
"refresh": "10s",
"schemaVersion": 38,
"style": "dark",
"tags": ["home", "energy", "power", "solar", "flow"],
"templating": {"list": []},
"time": {"from": "now-6h", "to": "now"},
"timepicker": {},
"timezone": "",
"title": "Power Flow",
"uid": "power-flow",
"version": 10,
"weekStart": ""
}