From 4b22f4f0342a93fb950885bc733832cdabcdab19 Mon Sep 17 00:00:00 2001 From: Joerg Date: Sat, 10 Oct 2026 16:13:58 +0200 Subject: [PATCH] More data --- README.md | 16 +++++++++ solaredge_modbus_mqtt/__init__.py | 43 +++++++++++++++++++++++++ solaredge_modbus_mqtt/test_SolarData.py | 38 ++++++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/README.md b/README.md index b0dcd54..745b573 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,22 @@ Register addresses were validated by probing a SE10K-RWB48BFN4 (fw 0004.0025.001 with one battery and one meter. Blocks that fail to read (flaky gateway) are skipped for that cycle; the fields are simply absent from the point. +### Grafana dashboard + +Import `grafana_dashboard.json` (Dashboards -> New -> Import). Select your +InfluxDB datasource when prompted (e.g. `InfluxDB-HJK`); the `$serial` variable +is filled from the `serialnumber` tag. InfluxQL only, InfluxDB 1.x. + +The daemon also writes derived flow fields (`house_load`, `grid_import`, +`grid_export`, `battery_charge`, `battery_discharge`, `solar_to_house`) so the +dashboard's consumption-source panels are plain `mean()`/`integral()` queries. + +**Sign calibration:** `meter1_power` positive = import and `battery1_power` +positive = discharge are assumptions observed once; verify on the first sunny +day (export while surplus / charge while battery fills). If inverted, flip +`METER_IMPORT_POSITIVE` / `BATTERY_DISCHARGE_POSITIVE` in +`solaredge_modbus_mqtt/__init__.py` and restart - all derived fields follow. + ### Uninstall diff --git a/solaredge_modbus_mqtt/__init__.py b/solaredge_modbus_mqtt/__init__.py index 4576cf3..baadf19 100644 --- a/solaredge_modbus_mqtt/__init__.py +++ b/solaredge_modbus_mqtt/__init__.py @@ -86,6 +86,12 @@ storage_mode_map = [ "Remote Control" ] +# Observed on the probed system: meter1_power positive = grid import, +# battery1_power positive = discharge. If day-one graphs disagree, flip here - +# all derived flow fields (house_load, grid_import, ...) follow. +METER_IMPORT_POSITIVE = True +BATTERY_DISCHARGE_POSITIVE = True + class MyInverter: @@ -403,6 +409,43 @@ class SolarData: def __init__(self, data): self.rawData = data self.data = self._parseData(data) + self.data.update(self._flows(self.data)) + + @staticmethod + def _flows(d): + '''Decompose the house supply into grid/battery/solar components. + + Computed at ingest because all fields land in one InfluxDB point and + InfluxQL cannot clip series (max(x, 0)) or split them afterwards - + dashboard queries stay plain mean()/integral(). + ''' + pv = d.get('power_ac') + grid = d.get('meter1_power') + batt = d.get('battery1_power') + if pv is None or grid is None or batt is None: + return {} + + if not METER_IMPORT_POSITIVE: + grid = -grid + if not BATTERY_DISCHARGE_POSITIVE: + batt = -batt + + load = pv + grid + batt + grid_import = max(grid, 0.0) + grid_export = max(-grid, 0.0) + battery_discharge = max(batt, 0.0) + battery_charge = max(-batt, 0.0) + solar_to_house = max(load - grid_import - battery_discharge, 0.0) + + return { + # + 0.0 normalizes -0.0 from max(-x, 0.0) + 'house_load': round(max(load, 0.0), 4) + 0.0, + 'grid_import': round(grid_import, 4) + 0.0, + 'grid_export': round(grid_export, 4) + 0.0, + 'battery_discharge': round(battery_discharge, 4) + 0.0, + 'battery_charge': round(battery_charge, 4) + 0.0, + 'solar_to_house': round(solar_to_house, 4) + 0.0, + } def _parseData(self, data): result = {} diff --git a/solaredge_modbus_mqtt/test_SolarData.py b/solaredge_modbus_mqtt/test_SolarData.py index 39bd175..aa6be67 100644 --- a/solaredge_modbus_mqtt/test_SolarData.py +++ b/solaredge_modbus_mqtt/test_SolarData.py @@ -185,6 +185,44 @@ class TestSolarData(unittest.TestCase): self.assertNotIn('storage_control_mode', solarData.data) self.assertAlmostEqual(597.0, solarData.data['power_ac']) + def testFlowsImport(self): + # probed live values: PV 861.2 W, grid +689.5 (import), battery idle + solarData = SolarData(probe_data(power_ac=8612, m1_power=6895, b1_power=0.0)) + + self.assertAlmostEqual(1550.7, solarData.data['house_load']) + self.assertAlmostEqual(689.5, solarData.data['grid_import']) + self.assertAlmostEqual(0.0, solarData.data['grid_export']) + self.assertAlmostEqual(0.0, solarData.data['battery_charge']) + self.assertAlmostEqual(0.0, solarData.data['battery_discharge']) + self.assertAlmostEqual(861.2, solarData.data['solar_to_house']) + + def testFlowsSurplus(self): + # PV 5000, exporting 2000, charging battery with 1000 + solarData = SolarData(probe_data(power_ac=50000, m1_power=-20000, b1_power=-1000.0)) + + self.assertAlmostEqual(2000.0, solarData.data['house_load']) + self.assertAlmostEqual(0.0, solarData.data['grid_import']) + self.assertAlmostEqual(2000.0, solarData.data['grid_export']) + self.assertAlmostEqual(1000.0, solarData.data['battery_charge']) + self.assertAlmostEqual(0.0, solarData.data['battery_discharge']) + self.assertAlmostEqual(2000.0, solarData.data['solar_to_house']) + + def testFlowsNightDischarge(self): + solarData = SolarData(probe_data(power_ac=0, m1_power=0, b1_power=300.0)) + + self.assertAlmostEqual(300.0, solarData.data['house_load']) + self.assertAlmostEqual(300.0, solarData.data['battery_discharge']) + self.assertAlmostEqual(0.0, solarData.data['solar_to_house']) + + def testFlowsSkippedWhenBlocksMissing(self): + data = probe_data() + for key in [k for k in data if k.startswith('m1_')]: + del data[key] + solarData = SolarData(data) + + self.assertNotIn('house_load', solarData.data) + self.assertNotIn('grid_import', solarData.data) + def testLineProtocol(self): solarData = SolarData(probe_data()) line = to_line_protocol(solarData.data)