@@ -241,6 +241,7 @@ class GatewayMQTT(ComponentBase):
241241 _slots_without_capacity = frozenset ()
242242 _last_ratings = {} # replaced, never changed in place
243243 _assumed_ratings = frozenset ()
244+ _reserve_before_zero = {} # replaced, never changed in place
244245 gateway_device_id = None
245246
246247 def initialize (self , gateway_device_id = None , mqtt_host = None , mqtt_port = 8883 , mqtt_token = None , gateway_inverter_serial = None , gateway_evc_automatic = False , gateway_evc_control = False , gateway_shared_ct = True , gateway_integrate_power = False , ** kwargs ):
@@ -349,6 +350,7 @@ def initialize(self, gateway_device_id=None, mqtt_host=None, mqtt_port=8883, mqt
349350 self ._slots_without_capacity = frozenset () # slots bound with no battery size sensor, re-bound once the hub reports a size
350351 self ._last_ratings = {} # (entity suffix, rating name) -> the last real value the hub reported
351352 self ._assumed_ratings = frozenset () # entity suffixes of inverters whose maximum rate has never been reported
353+ self ._reserve_before_zero = {} # entity suffix -> the last real reserve, set aside when Predbat wrote a reserve of 0
352354 self ._discovery = {} # upper-case serial -> what the hub last reported it to be (see _update_discovery)
353355 self ._discovery_dirty = False
354356 self ._discovery_save_retry_at = 0
@@ -881,6 +883,8 @@ def _process_telemetry(self, data):
881883 for inv in status .inverters :
882884 if not inv .primary and self ._is_published (inv ):
883885 self ._publish_inverter (inv )
886+ # The integration above ran on the slots as they were before this status changed them
887+ self ._update_integrated_energy (status , rebase = True )
884888 self ._inject_unreported_inverters (status )
885889 self ._inject_shared_site_metering (status )
886890 self ._check_configured_inverters (status )
@@ -1077,7 +1081,7 @@ def _inject_shared_site_metering(self, status):
10771081 for name , wh in today .items ():
10781082 self .dashboard_item (f"sensor.{ pfx } _{ name } _today" , round (wh / 1000.0 , 2 ), attributes = GATEWAY_ATTRIBUTE_TABLE .get (f"{ name } _today" , {}), app = "gateway" )
10791083
1080- def _update_integrated_energy (self , status ):
1084+ def _update_integrated_energy (self , status , rebase = False ):
10811085 """Integrate grid and battery power into today's import, export and load (gateway_integrate_power).
10821086
10831087 Some inverters do not keep usable grid energy counters. GivEnergy AC inverters sharing one
@@ -1097,6 +1101,12 @@ def _update_integrated_energy(self, status):
10971101 Each interval uses the mean of the readings at its two ends. The figures return to zero at
10981102 local midnight. They are kept in memory only, so they also restart from zero when Predbat
10991103 restarts, which Predbat reads the same way as the midnight reset.
1104+
1105+ Args:
1106+ status: A decoded GatewayStatus protobuf message.
1107+ rebase: True when auto-config has just run on a status that was already integrated.
1108+ Nothing is added: its readings become the baseline for the inverters now in the
1109+ slots, so one that has just joined is counted from this status and not the next.
11001110 """
11011111 if not self .gateway_integrate_power :
11021112 return
@@ -1143,7 +1153,7 @@ def _update_integrated_energy(self, status):
11431153 state = {"day" : day , "time" : None , ** zeroed }
11441154 self ._integrated_energy = state
11451155 previous = state ["time" ]
1146- if previous is not None and now <= previous :
1156+ if previous is not None and now <= previous and not rebase :
11471157 # The same status again (a re-inject), or the hub's clock stepping back
11481158 return
11491159
@@ -1158,6 +1168,9 @@ def _update_integrated_energy(self, status):
11581168
11591169 if previous is None :
11601170 state ["pv" ] = {serial : {"base" : wh , "high" : None , "high_time" : 0 } for serial , wh in pv_wh .items ()}
1171+ elif rebase :
1172+ for serial , wh in pv_wh .items ():
1173+ state ["pv" ].setdefault (serial , {"base" : wh , "high" : None , "high_time" : 0 })
11611174 else :
11621175 elapsed = now - previous
11631176 hours = elapsed / 3600.0
@@ -2676,11 +2689,16 @@ async def number_event(self, entity_id, value):
26762689 elif "_charge_rate" in entity_id :
26772690 await self ._send_control (entity_id , "set_charge_rate" , power_w = val , serial = serial )
26782691 elif "_reserve" in entity_id :
2692+ await self ._send_control (entity_id , "set_reserve" , target_soc = val , serial = serial )
26792693 if val == 0 :
26802694 # A reserve of 0% asked for here is real, so the next 0 from the hub is not a gap
2681- # to fill with the last value (see _last_rating)
2682- self ._last_ratings = {key : kept for key , kept in self ._last_ratings .items () if key != (_serial_suffix (serial ), "reserve" )}
2683- await self ._send_control (entity_id , "set_reserve" , target_soc = val , serial = serial )
2695+ # to fill with the last value (see _last_rating). Only once the write has gone
2696+ # out, and the value is set aside in case the hub refuses it (see _process_ack).
2697+ key = (_serial_suffix (serial ), "reserve" )
2698+ self ._reserve_before_zero = {suffix : kept for suffix , kept in self ._reserve_before_zero .items () if suffix != key [0 ]}
2699+ if key in self ._last_ratings :
2700+ self ._reserve_before_zero = {** self ._reserve_before_zero , key [0 ]: self ._last_ratings [key ]}
2701+ self ._last_ratings = {kept_key : kept for kept_key , kept in self ._last_ratings .items () if kept_key != key }
26842702 elif "_target_soc" in entity_id :
26852703 await self ._send_control (entity_id , "set_target_soc" , target_soc = val , serial = serial )
26862704
@@ -2880,6 +2898,7 @@ def _process_ack(self, data):
28802898 apply_cache = ok and entry ["cached" ] is None
28812899 command = entry ["command" ]
28822900 serial = entry ["kwargs" ].get ("serial" )
2901+ zero_reserve = entry ["kwargs" ].get ("target_soc" ) == 0
28832902
28842903 if ok :
28852904 self .log (f"Info: GatewayMQTT: { command } for { serial } acknowledged by hub ({ command_id } ) applied={ ack .get ('applied' )} " )
@@ -2893,6 +2912,13 @@ def _process_ack(self, data):
28932912 self .log (f"Warn: GatewayMQTT: { command } for { serial } refused by hub: replay ({ command_id } already used), { action } " )
28942913 else :
28952914 self .log (f"Warn: GatewayMQTT: { command } for { serial } refused by hub: { error } " )
2915+ if command == "set_reserve" and zero_reserve and serial :
2916+ # The reserve of 0 was not applied, so a 0 from the hub is a gap again and the
2917+ # reserve it had before is still the last real one
2918+ key = (_serial_suffix (serial ), "reserve" )
2919+ if key [0 ] in self ._reserve_before_zero and key not in self ._last_ratings :
2920+ self ._last_ratings = {** self ._last_ratings , key : self ._reserve_before_zero [key [0 ]]}
2921+ restore_status = True
28962922 if restore_status and self ._last_status is not None :
28972923 self ._inject_entities (self ._last_status )
28982924
0 commit comments