Fix battery sign and separate PV from inverter output
Verified against simultaneous SolarEdge app readings (PV 40 W, battery discharge 520 W, house 560 W): battery1_power negative means discharge, and power_ac on this DC-coupled Energy Hub is the inverter's TOTAL AC output - it already contains the battery contribution. The old formula double-counted the battery, so house_load collapsed (44 W shown vs 560 W real) and the PV tile showed battery-inflated totals, which looked like PV and house consumption were swapped. New derived fields: house_load = power_ac + grid, and pv_power = power_ac - battery (true PV production). Dashboard PV tiles and both yield panels now use pv_power / integral(pv_power) - the energy_total lifetime counter also counts battery throughput and is not a PV yield.
This commit is contained in:
4 files changed
+57
-42
No files matched your search
@@ -56,15 +56,18 @@ Import `grafana_dashboard.json` (Dashboards -> New -> Import). Select your
|
|||||||
InfluxDB datasource when prompted (e.g. `InfluxDB-HJK`); the `$serial` variable
|
InfluxDB datasource when prompted (e.g. `InfluxDB-HJK`); the `$serial` variable
|
||||||
is filled from the `serialnumber` tag. InfluxQL only, InfluxDB 1.x.
|
is filled from the `serialnumber` tag. InfluxQL only, InfluxDB 1.x.
|
||||||
|
|
||||||
The daemon also writes derived flow fields (`house_load`, `grid_import`,
|
The daemon also writes derived flow fields (`house_load`, `pv_power`,
|
||||||
`grid_export`, `battery_charge`, `battery_discharge`, `solar_to_house`) so the
|
`grid_import`, `grid_export`, `battery_charge`, `battery_discharge`,
|
||||||
dashboard's consumption-source panels are plain `mean()`/`integral()` queries.
|
`solar_to_house`) so the dashboard's consumption-source panels are plain
|
||||||
|
`mean()`/`integral()` queries.
|
||||||
|
|
||||||
**Sign calibration:** `meter1_power` positive = import and `battery1_power`
|
**Sign conventions (verified against the SolarEdge app):** `meter1_power`
|
||||||
positive = discharge are assumptions observed once; verify on the first sunny
|
positive = export, `battery1_power` negative = discharge, and `power_ac` is
|
||||||
day (export while surplus / charge while battery fills). If inverted, flip
|
the inverter's *total* AC output (PV + battery, DC-coupled Energy Hub) - which
|
||||||
`METER_IMPORT_POSITIVE` / `BATTERY_DISCHARGE_POSITIVE` in
|
is why `pv_power = power_ac - battery` and `house_load = power_ac + grid`.
|
||||||
`solaredge_modbus_mqtt/__init__.py` and restart - all derived fields follow.
|
If a different system disagrees, flip `METER_IMPORT_POSITIVE` /
|
||||||
|
`BATTERY_DISCHARGE_POSITIVE` in `solaredge_modbus_mqtt/__init__.py` and
|
||||||
|
restart - the derived fields follow.
|
||||||
|
|
||||||
|
|
||||||
### Uninstall
|
### Uninstall
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
"type": "influxdb",
|
"type": "influxdb",
|
||||||
"uid": "${DS_INFLUXDB}"
|
"uid": "${DS_INFLUXDB}"
|
||||||
},
|
},
|
||||||
"query": "SELECT last(\"power_ac\") AS \"PV\" FROM \"$measurement\" WHERE serialnumber =~ /^$serial$/ AND $timeFilter",
|
"query": "SELECT last(\"pv_power\") AS \"PV\" FROM \"$measurement\" WHERE serialnumber =~ /^$serial$/ AND $timeFilter",
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
"rawQuery": true,
|
"rawQuery": true,
|
||||||
"resultFormat": "time_series"
|
"resultFormat": "time_series"
|
||||||
@@ -387,7 +387,7 @@
|
|||||||
"type": "influxdb",
|
"type": "influxdb",
|
||||||
"uid": "${DS_INFLUXDB}"
|
"uid": "${DS_INFLUXDB}"
|
||||||
},
|
},
|
||||||
"query": "SELECT spread(\"energy_total\") / 1000 AS \"Ertrag\" FROM \"$measurement\" WHERE serialnumber =~ /^$serial$/ AND $timeFilter",
|
"query": "SELECT integral(\"pv_power\", 1h) / 1000 AS \"Ertrag\" FROM \"$measurement\" WHERE serialnumber =~ /^$serial$/ AND $timeFilter",
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
"rawQuery": true,
|
"rawQuery": true,
|
||||||
"resultFormat": "time_series"
|
"resultFormat": "time_series"
|
||||||
@@ -808,7 +808,7 @@
|
|||||||
"type": "influxdb",
|
"type": "influxdb",
|
||||||
"uid": "${DS_INFLUXDB}"
|
"uid": "${DS_INFLUXDB}"
|
||||||
},
|
},
|
||||||
"query": "SELECT mean(\"power_ac\") AS \"PV\" FROM \"$measurement\" WHERE serialnumber =~ /^$serial$/ AND $timeFilter GROUP BY time($__interval) fill(null)",
|
"query": "SELECT mean(\"pv_power\") AS \"PV\" FROM \"$measurement\" WHERE serialnumber =~ /^$serial$/ AND $timeFilter GROUP BY time($__interval) fill(null)",
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
"rawQuery": true,
|
"rawQuery": true,
|
||||||
"resultFormat": "time_series"
|
"resultFormat": "time_series"
|
||||||
@@ -1177,7 +1177,7 @@
|
|||||||
"type": "influxdb",
|
"type": "influxdb",
|
||||||
"uid": "${DS_INFLUXDB}"
|
"uid": "${DS_INFLUXDB}"
|
||||||
},
|
},
|
||||||
"query": "SELECT non_negative_derivative(max(\"energy_total\"), 1h) / 1000 AS \"Ertrag\" FROM \"$measurement\" WHERE serialnumber =~ /^$serial$/ AND $timeFilter GROUP BY time(1h) fill(null)",
|
"query": "SELECT integral(\"pv_power\", 1h) / 1000 AS \"Ertrag\" FROM \"$measurement\" WHERE serialnumber =~ /^$serial$/ AND $timeFilter GROUP BY time(1h) fill(null)",
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
"rawQuery": true,
|
"rawQuery": true,
|
||||||
"resultFormat": "time_series"
|
"resultFormat": "time_series"
|
||||||
|
|||||||
@@ -86,13 +86,14 @@ storage_mode_map = [
|
|||||||
"Remote Control"
|
"Remote Control"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Observed on the probed system: meter1_power positive = grid EXPORT
|
# Verified against the SolarEdge app (simultaneous readings: PV 40 W, battery
|
||||||
# (verified against a full-battery self-consumption system: sustained +2kW
|
# discharging 520 W, house 560 W): meter1_power positive = grid EXPORT, and
|
||||||
# readings at high PV were exports; a kettle appears as a negative spike).
|
# battery1_power negative = DISCHARGE. Also established: power_ac is the
|
||||||
# battery1_power positive = discharge. If day-one graphs disagree, flip here -
|
# inverter's TOTAL AC output - on this DC-coupled Energy Hub it already
|
||||||
# all derived flow fields (house_load, grid_import, ...) follow.
|
# contains the battery's contribution. If a future system disagrees, flip here
|
||||||
|
# - all derived flow fields follow.
|
||||||
METER_IMPORT_POSITIVE = False
|
METER_IMPORT_POSITIVE = False
|
||||||
BATTERY_DISCHARGE_POSITIVE = True
|
BATTERY_DISCHARGE_POSITIVE = False
|
||||||
|
|
||||||
|
|
||||||
class MyInverter:
|
class MyInverter:
|
||||||
@@ -417,33 +418,37 @@ class SolarData:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _flows(d):
|
def _flows(d):
|
||||||
'''Decompose the house supply into grid/battery/solar components.
|
'''Decompose house supply and PV production.
|
||||||
|
|
||||||
|
power_ac is the inverter's TOTAL AC output and already contains the
|
||||||
|
battery's contribution (DC-coupled Energy Hub), so:
|
||||||
|
pv_power = power_ac - battery (discharge-positive)
|
||||||
|
house_load = power_ac + grid
|
||||||
Computed at ingest because all fields land in one InfluxDB point and
|
Computed at ingest because all fields land in one InfluxDB point and
|
||||||
InfluxQL cannot clip series (max(x, 0)) or split them afterwards -
|
InfluxQL cannot clip series (max(x, 0)) or split them afterwards -
|
||||||
dashboard queries stay plain mean()/integral().
|
dashboard queries stay plain mean()/integral().
|
||||||
'''
|
'''
|
||||||
pv = d.get('power_ac')
|
pv_ac = d.get('power_ac')
|
||||||
grid = d.get('meter1_power')
|
meter = d.get('meter1_power')
|
||||||
batt = d.get('battery1_power')
|
batt_raw = d.get('battery1_power')
|
||||||
if pv is None or grid is None or batt is None:
|
if pv_ac is None or meter is None or batt_raw is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
if not METER_IMPORT_POSITIVE:
|
grid = meter if METER_IMPORT_POSITIVE else -meter # + = import
|
||||||
grid = -grid
|
batt = batt_raw if BATTERY_DISCHARGE_POSITIVE else -batt_raw # + = discharge
|
||||||
if not BATTERY_DISCHARGE_POSITIVE:
|
|
||||||
batt = -batt
|
|
||||||
|
|
||||||
load = pv + grid + batt
|
load = pv_ac + grid
|
||||||
grid_import = max(grid, 0.0)
|
grid_import = max(grid, 0.0)
|
||||||
grid_export = max(-grid, 0.0)
|
grid_export = max(-grid, 0.0)
|
||||||
battery_discharge = max(batt, 0.0)
|
battery_discharge = max(batt, 0.0)
|
||||||
battery_charge = max(-batt, 0.0)
|
battery_charge = max(-batt, 0.0)
|
||||||
|
pv_power = max(pv_ac - batt, 0.0)
|
||||||
solar_to_house = max(load - grid_import - battery_discharge, 0.0)
|
solar_to_house = max(load - grid_import - battery_discharge, 0.0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
# + 0.0 normalizes -0.0 from max(-x, 0.0)
|
# + 0.0 normalizes -0.0 from max(-x, 0.0)
|
||||||
'house_load': round(max(load, 0.0), 4) + 0.0,
|
'house_load': round(max(load, 0.0), 4) + 0.0,
|
||||||
|
'pv_power': round(pv_power, 4) + 0.0,
|
||||||
'grid_import': round(grid_import, 4) + 0.0,
|
'grid_import': round(grid_import, 4) + 0.0,
|
||||||
'grid_export': round(grid_export, 4) + 0.0,
|
'grid_export': round(grid_export, 4) + 0.0,
|
||||||
'battery_discharge': round(battery_discharge, 4) + 0.0,
|
'battery_discharge': round(battery_discharge, 4) + 0.0,
|
||||||
|
|||||||
@@ -209,32 +209,39 @@ class TestSolarData(unittest.TestCase):
|
|||||||
self.assertNotIn('storage_control_mode', solarData.data)
|
self.assertNotIn('storage_control_mode', solarData.data)
|
||||||
self.assertAlmostEqual(597.0, solarData.data['power_ac'])
|
self.assertAlmostEqual(597.0, solarData.data['power_ac'])
|
||||||
|
|
||||||
def testFlowsImport(self):
|
def testFlowsNightDischarge(self):
|
||||||
# kettle: PV 861.2 W, grid meter reads -689.5 (import), battery idle
|
# ground truth from the SolarEdge app: PV 40 W, battery discharging
|
||||||
solarData = SolarData(probe_data(power_ac=8612, m1_power=-6895, b1_power=0.0))
|
# 520 W, house 560 W, grid ~0. power_ac (529) = PV + battery output.
|
||||||
|
solarData = SolarData(probe_data(power_ac=5290, m1_power=0, b1_power=-485.0))
|
||||||
|
|
||||||
self.assertAlmostEqual(1550.7, solarData.data['house_load'])
|
self.assertAlmostEqual(529.0, solarData.data['house_load'])
|
||||||
self.assertAlmostEqual(689.5, solarData.data['grid_import'])
|
self.assertAlmostEqual(44.0, solarData.data['pv_power'])
|
||||||
self.assertAlmostEqual(0.0, solarData.data['grid_export'])
|
self.assertAlmostEqual(485.0, solarData.data['battery_discharge'])
|
||||||
self.assertAlmostEqual(0.0, solarData.data['battery_charge'])
|
self.assertAlmostEqual(0.0, solarData.data['battery_charge'])
|
||||||
self.assertAlmostEqual(0.0, solarData.data['battery_discharge'])
|
self.assertAlmostEqual(0.0, solarData.data['grid_import'])
|
||||||
self.assertAlmostEqual(861.2, solarData.data['solar_to_house'])
|
self.assertAlmostEqual(44.0, solarData.data['solar_to_house'])
|
||||||
|
|
||||||
def testFlowsExport(self):
|
def testFlowsSurplus(self):
|
||||||
# observed afternoon: PV 5000, exporting 2000, charging battery with 1000
|
# PV 5000, charging battery with 1000, exporting 2000, house 2000.
|
||||||
solarData = SolarData(probe_data(power_ac=50000, m1_power=20000, b1_power=-1000.0))
|
# Charging happens on the DC side: power_ac only carries 4000.
|
||||||
|
solarData = SolarData(probe_data(power_ac=40000, m1_power=20000, b1_power=1000.0))
|
||||||
|
|
||||||
self.assertAlmostEqual(2000.0, solarData.data['house_load'])
|
self.assertAlmostEqual(2000.0, solarData.data['house_load'])
|
||||||
|
self.assertAlmostEqual(5000.0, solarData.data['pv_power'])
|
||||||
self.assertAlmostEqual(0.0, solarData.data['grid_import'])
|
self.assertAlmostEqual(0.0, solarData.data['grid_import'])
|
||||||
self.assertAlmostEqual(2000.0, solarData.data['grid_export'])
|
self.assertAlmostEqual(2000.0, solarData.data['grid_export'])
|
||||||
self.assertAlmostEqual(1000.0, solarData.data['battery_charge'])
|
self.assertAlmostEqual(1000.0, solarData.data['battery_charge'])
|
||||||
self.assertAlmostEqual(0.0, solarData.data['battery_discharge'])
|
self.assertAlmostEqual(0.0, solarData.data['battery_discharge'])
|
||||||
self.assertAlmostEqual(2000.0, solarData.data['solar_to_house'])
|
self.assertAlmostEqual(2000.0, solarData.data['solar_to_house'])
|
||||||
|
|
||||||
def testFlowsNightDischarge(self):
|
def testFlowsImport(self):
|
||||||
solarData = SolarData(probe_data(power_ac=0, m1_power=0, b1_power=300.0))
|
# no sun: battery covers 300 W of the house, 200 W imported.
|
||||||
|
# power_ac = 300 (battery output), meter reads -200 (import).
|
||||||
|
solarData = SolarData(probe_data(power_ac=3000, m1_power=-2000, b1_power=-300.0))
|
||||||
|
|
||||||
self.assertAlmostEqual(300.0, solarData.data['house_load'])
|
self.assertAlmostEqual(500.0, solarData.data['house_load'])
|
||||||
|
self.assertAlmostEqual(0.0, solarData.data['pv_power'])
|
||||||
|
self.assertAlmostEqual(200.0, solarData.data['grid_import'])
|
||||||
self.assertAlmostEqual(300.0, solarData.data['battery_discharge'])
|
self.assertAlmostEqual(300.0, solarData.data['battery_discharge'])
|
||||||
self.assertAlmostEqual(0.0, solarData.data['solar_to_house'])
|
self.assertAlmostEqual(0.0, solarData.data['solar_to_house'])
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user