@@ -3621,6 +3621,74 @@ def adjust_export_immediate(self, target_soc, freeze=False):
36213621 if not self .call_service_template ("discharge_stop_service" , service_data_stop , domain = "discharge" ):
36223622 self .call_service_template ("charge_stop_service" , service_data_stop , domain = "discharge" )
36233623
3624+ def write_and_poll_gateway_charge_window (self , new_start , new_end ):
3625+ """Verify a complete Hub slot together, or return None for another provider.
3626+
3627+ A start equal to the old end cannot be sent alone while charging is enabled:
3628+ it temporarily disables the slot and the Hub correctly refuses it. Sending
3629+ both bounds also handles swapping the bounds of a midnight-spanning slot.
3630+ """
3631+ from ha import run_async
3632+
3633+ entities = [self .base .get_arg (name , indirect = False , index = self .id ) for name in ("charge_start_time" , "charge_end_time" )]
3634+ owners = [getattr (self .base , "dashboard_index_app" , {}).get (entity ) for entity in entities ]
3635+ if "gateway" not in owners :
3636+ return None
3637+ components = getattr (self .base , "components" , None )
3638+ gateway = components .get_component ("gateway" ) if components else None
3639+ names = ("charge_start_time" , "charge_end_time" )
3640+ target = (new_start , new_end )
3641+ if gateway is not None and gateway .charge_window_uses_staging (* entities ):
3642+ return None # Coordinator slot staging must precede its enable write.
3643+ if owners != ["gateway" , "gateway" ] or gateway is None :
3644+ self .base .record_status (f"Warn: Inverter { self .id } Hub charge window has inconsistent entity ownership" , had_errors = True )
3645+ return False
3646+ if not all (self .check_write_entity ("write_and_poll_gateway_charge_window" , name , entity , value ) for name , entity , value in zip (names , entities , target )):
3647+ return False
3648+
3649+ def read_pair ():
3650+ """Read both endpoint entities without substituting desired values."""
3651+ return tuple (self .base .get_state_wrapper (entity , refresh = True ) for entity in entities )
3652+
3653+ before = read_pair ()
3654+ ledger = self .base .control_ledger
3655+ if ledger is not None :
3656+ for name , entity , value , wanted in zip (names , entities , before , target ):
3657+ self ._ledger_observe (ledger , name , entity , value )
3658+ if value != wanted :
3659+ ledger .note_write_attempt (entity )
3660+ key = "|" .join (entities )
3661+ verified = before == target
3662+ for _retry in range (0 if verified else self ._write_attempts ("charge_window" , key , target )):
3663+ try :
3664+ published = run_async (gateway .update_charge_window (* entities , * target ))
3665+ except Exception as error :
3666+ self .base .log (f"Warn: Inverter { self .id } Hub charge window publication failed: { error } " )
3667+ break
3668+ if published is not True :
3669+ break
3670+ self ._poll_after_write (entities [0 ], lambda state : state == new_start and self .base .get_state_wrapper (entities [1 ], refresh = True ) == new_end )
3671+ verified = read_pair () == target
3672+ if verified :
3673+ self .count_register_writes += 1
3674+ self .registers_moved += sum (old != wanted for old , wanted in zip (before , target ))
3675+ break
3676+ self ._write_backoff_result ("charge_window" , key , target , verified )
3677+ if ledger is not None :
3678+ for name , entity , value in zip (names , entities , target ):
3679+ if verified :
3680+ if before == target :
3681+ ledger .record_ownership_from_read (entity , name , value , now = time .time (), generation = self ._ledger_generation (entity ))
3682+ else :
3683+ ledger .record_write (entity , name , value , now = time .time (), generation = self ._ledger_generation (entity ))
3684+ else :
3685+ ledger .clear (entity )
3686+ if not verified :
3687+ message = f"Warn: Inverter { self .id } write to Hub charge window failed: wanted { target } , got { read_pair ()} "
3688+ self .base .log (message )
3689+ self .base .record_status (message , had_errors = True )
3690+ return verified
3691+
36243692 def adjust_charge_window (self , charge_start_time , charge_end_time , minutes_now ):
36253693 """
36263694 Adjust the charging window times (start and end) in GivTCP
@@ -3692,8 +3760,14 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now):
36923760 # Some inverters have an idle time setting
36933761 self .adjust_idle_time (charge_start = new_start , charge_end = new_end )
36943762
3763+ gateway_window = None
3764+ if new_start != old_start or new_end != old_end :
3765+ gateway_window = self .write_and_poll_gateway_charge_window (new_start , new_end )
3766+ if gateway_window is False :
3767+ return
3768+
36953769 # Disable charging if required to avoid a blip while the start/end entities are written
3696- if not in_new_window and ((new_start != old_start ) or (new_end != old_end )) and self .inv_has_charge_enable_time :
3770+ if gateway_window is None and not in_new_window and ((new_start != old_start ) or (new_end != old_end )) and self .inv_has_charge_enable_time :
36973771 self .disable_charge_window (notify = False )
36983772 have_disabled = True
36993773
@@ -3707,7 +3781,7 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now):
37073781 # Registers this cycle's writes actually moved, for commit_needed() below
37083782 moved_before = self .registers_moved
37093783
3710- if new_start != old_start or (self .inv_charge_time_format in ["H M" , "H:M-H:M" ]):
3784+ if gateway_window is None and ( new_start != old_start or (self .inv_charge_time_format in ["H M" , "H:M-H:M" ]) ):
37113785 if "charge_start_time" in self .base .args :
37123786 # Always write to this as it is the GE default
37133787 entity_id_start = self .base .get_arg ("charge_start_time" , indirect = False , index = self .id )
@@ -3725,7 +3799,7 @@ def adjust_charge_window(self, charge_start_time, charge_end_time, minutes_now):
37253799 self .log ("Warn: Inverter {} unable write charge window start as neither REST or charge_start_time are set" .format (self .id ))
37263800
37273801 # Program end slot
3728- if new_end != old_end or (self .inv_charge_time_format in ["H M" , "H:M-H:M" ]):
3802+ if gateway_window is None and ( new_end != old_end or (self .inv_charge_time_format in ["H M" , "H:M-H:M" ]) ):
37293803 if "charge_end_time" in self .base .args :
37303804 # Always write to this as it is the GE default
37313805 entity_id_end = self .base .get_arg ("charge_end_time" , indirect = False , index = self .id )
0 commit comments