@@ -1079,6 +1079,158 @@ def quantising_call_service(service, **kwargs):
10791079 return failed
10801080
10811081
1082+ def test_mixed_fleet_rate_path_from_own_entry (test_name , ha , inv , prev_rate , percent_entry_set , discharge = False ):
1083+ """
1084+ On a mixed fleet, the percent-vs-watts path is chosen from THIS inverter's own entry, not from
1085+ whether the key exists anywhere in args (#5359).
1086+
1087+ Before the fix, get_current_charge_rate()/get_current_discharge_rate() took the percent branch
1088+ whenever "charge_rate_percent"/"discharge_rate_percent" was in self.base.args at all - so an
1089+ inverter whose own slot in that list is None (another inverter in the fleet set the key, this
1090+ one didn't) still took the percent branch, found nothing for its index, fell back to the 100%
1091+ default and always read back battery_rate_max_raw regardless of what was last written. With the
1092+ fix, an unset entry falls back to the watts-based "charge_rate"/"discharge_rate" entity instead.
1093+ """
1094+ failed = False
1095+ print ("Test: {} prev_rate {} percent_entry_set {} discharge {}" .format (test_name , prev_rate , percent_entry_set , discharge ))
1096+
1097+ percent_arg = "discharge_rate_percent" if discharge else "charge_rate_percent"
1098+ watt_entity = "number.discharge_rate" if discharge else "number.charge_rate"
1099+ percent_entity = "number.discharge_rate_percent" if discharge else "number.charge_rate_percent"
1100+
1101+ saved_percent_arg = inv .base .args .get (percent_arg )
1102+ try :
1103+ # Simulate a mixed fleet: the percent key exists in args (another inverter set it up) but
1104+ # this inverter's own index is unset.
1105+ inv .base .args [percent_arg ] = percent_entity if percent_entry_set else None
1106+ ha .dummy_items [watt_entity ] = prev_rate
1107+ ha .dummy_items [percent_entity ] = 100 # Would read back as rate_max if the percent path were wrongly taken
1108+
1109+ current_rate = inv .get_current_discharge_rate () if discharge else inv .get_current_charge_rate ()
1110+ finally :
1111+ if saved_percent_arg is not None :
1112+ inv .base .args [percent_arg ] = saved_percent_arg
1113+ else :
1114+ inv .base .args .pop (percent_arg , None )
1115+
1116+ if percent_entry_set :
1117+ # This inverter's own entry IS set - percent path, read against the percent entity's 100%
1118+ expect_rate = inv .battery_rate_max_raw
1119+ else :
1120+ # This inverter's own entry is unset - falls back to the watts entity, which holds prev_rate
1121+ expect_rate = prev_rate
1122+
1123+ if current_rate != expect_rate :
1124+ print ("ERROR: {} expected current rate {} got {}" .format (test_name , expect_rate , current_rate ))
1125+ failed = True
1126+
1127+ return failed
1128+
1129+
1130+ def test_mixed_fleet_rate_write_skips_unset_entry (test_name , ha , inv , discharge = False ):
1131+ """
1132+ adjust_charge_rate()/adjust_discharge_rate() must not write (or warn about) a register this
1133+ inverter's own entry does not configure, even when the sibling register's key exists in args
1134+ because another inverter in the fleet uses it (#5359).
1135+
1136+ Before the fix, both adjust functions wrote both "charge_rate"/"discharge_rate" and
1137+ "charge_rate_percent"/"discharge_rate_percent" whenever either key existed ANYWHERE in args,
1138+ regardless of this inverter's own entry - so a script-driven inverter sharing a fleet with a
1139+ percent-controlled one tried to write into its own (unset) percent entry every cycle, which
1140+ write_and_poll_value()'s check_write_entity() logs as "No entity_id for ..." and also pins
1141+ had_errors=True onto the status sensor.
1142+ """
1143+ failed = False
1144+ print ("Test: {} discharge {}" .format (test_name , discharge ))
1145+
1146+ percent_arg = "discharge_rate_percent" if discharge else "charge_rate_percent"
1147+ watt_arg = "discharge_rate" if discharge else "charge_rate"
1148+ watt_entity = "number.discharge_rate" if discharge else "number.charge_rate"
1149+
1150+ saved_percent_arg = inv .base .args .get (percent_arg )
1151+ saved_watt_arg = inv .base .args .get (watt_arg )
1152+ log_messages = []
1153+ orig_log = inv .base .log
1154+
1155+ try :
1156+ # This inverter is watts-only: its own charge_rate/discharge_rate entity is configured, but
1157+ # the percent key exists in args only because a sibling inverter in the fleet set it up -
1158+ # this inverter's own slot in that list is None.
1159+ inv .base .args [watt_arg ] = watt_entity
1160+ inv .base .args [percent_arg ] = None
1161+ inv .rest_data = None
1162+ inv .rest_api = None
1163+ ha .dummy_items [watt_entity ] = 0
1164+ inv .base .log = lambda msg , * args , ** kwargs : log_messages .append (str (msg ))
1165+
1166+ if discharge :
1167+ inv .adjust_discharge_rate (250 )
1168+ else :
1169+ inv .adjust_charge_rate (250 )
1170+ finally :
1171+ inv .base .log = orig_log
1172+ if saved_percent_arg is not None :
1173+ inv .base .args [percent_arg ] = saved_percent_arg
1174+ else :
1175+ inv .base .args .pop (percent_arg , None )
1176+ if saved_watt_arg is not None :
1177+ inv .base .args [watt_arg ] = saved_watt_arg
1178+ else :
1179+ inv .base .args .pop (watt_arg , None )
1180+
1181+ warning_found = any ("No entity_id for" in msg for msg in log_messages )
1182+ if warning_found :
1183+ print ("ERROR: {} wrote/warned about the unset percent entry: {}" .format (test_name , log_messages ))
1184+ failed = True
1185+ if float (ha .get_state (watt_entity )) != 250 :
1186+ print ("ERROR: {} expected watt entity to be written to 250, got {}" .format (test_name , ha .get_state (watt_entity )))
1187+ failed = True
1188+
1189+ return failed
1190+
1191+
1192+ def test_create_missing_arg_does_not_fill_sibling_entries (test_name , my_predbat ):
1193+ """
1194+ create_missing_arg() pads every OTHER inverter's slot with None, not the default (#5359 follow-up).
1195+
1196+ When the key is entirely missing from args, create_missing_arg() used to fill all four slots
1197+ with the same default value, and only the caller's own index was then overwritten with its
1198+ dummy entity. On a mixed fleet where inverter 0 triggers this (e.g. inv_output_charge_control
1199+ != "power") before inverter 1 is even constructed, inverter 1's slot ended up holding a bare
1200+ number rather than None - which the per-entry "is this inverter's entry set" check added for
1201+ #5359 then misreads as a configured entity, since a truthy value is indistinguishable from one.
1202+ """
1203+ failed = False
1204+ print (f"**** Running Test: { test_name } ****" )
1205+
1206+ saved_args = copy .deepcopy (my_predbat .args )
1207+ try :
1208+ my_predbat .args ["num_inverters" ] = 2
1209+ # GS (Solis) drives charge rate via current rather than power, so constructing inverter 0
1210+ # takes the create_missing_arg("charge_rate", ...) branch under test
1211+ my_predbat .args ["inverter_type" ] = ["GS" , "GS" ]
1212+ my_predbat .args .pop ("charge_rate" , None )
1213+ my_predbat .args .pop ("charge_rate_percent" , None )
1214+
1215+ inv0 = Inverter (my_predbat , 0 , quiet = True )
1216+ if inv0 .inv_output_charge_control == "power" :
1217+ print ("ERROR: {} test assumes a non-power charge control to exercise create_missing_arg('charge_rate', ...)" .format (test_name ))
1218+ return 1
1219+
1220+ charge_rate_list = my_predbat .args .get ("charge_rate" )
1221+ if not isinstance (charge_rate_list , list ) or len (charge_rate_list ) < 2 :
1222+ print ("ERROR: {} expected charge_rate to be a per-inverter list covering inverter 1, got {}" .format (test_name , charge_rate_list ))
1223+ failed = True
1224+ elif charge_rate_list [1 ] is not None :
1225+ print ("ERROR: {} expected inverter 1's charge_rate slot to stay None until inverter 1 is constructed, got {}" .format (test_name , charge_rate_list [1 ]))
1226+ failed = True
1227+ finally :
1228+ my_predbat .args .clear ()
1229+ my_predbat .args .update (saved_args )
1230+
1231+ return 1 if failed else 0
1232+
1233+
10821234def test_custom_inverter_def_drops_rate_step (test_name , my_predbat , ha ):
10831235 """
10841236 A custom inverter type is built from a copy of the GE row, but must not inherit GivEnergy's rate step (#5324).
@@ -5504,6 +5656,17 @@ def run_inverter_tests(my_predbat_dummy):
55045656 if failed :
55055657 return failed
55065658
5659+ # #5359: mixed-fleet rate path must be chosen from THIS inverter's own entry, not from
5660+ # whether the percent key exists anywhere in args
5661+ failed |= test_mixed_fleet_rate_path_from_own_entry ("mixed_fleet_charge_rate_entry_set" , ha , inv , 200 , percent_entry_set = True )
5662+ failed |= test_mixed_fleet_rate_path_from_own_entry ("mixed_fleet_charge_rate_entry_unset" , ha , inv , 200 , percent_entry_set = False )
5663+ failed |= test_mixed_fleet_rate_path_from_own_entry ("mixed_fleet_discharge_rate_entry_set" , ha , inv , 250 , percent_entry_set = True , discharge = True )
5664+ failed |= test_mixed_fleet_rate_path_from_own_entry ("mixed_fleet_discharge_rate_entry_unset" , ha , inv , 250 , percent_entry_set = False , discharge = True )
5665+ failed |= test_mixed_fleet_rate_write_skips_unset_entry ("mixed_fleet_charge_rate_write_skips_unset_percent" , ha , inv )
5666+ failed |= test_mixed_fleet_rate_write_skips_unset_entry ("mixed_fleet_discharge_rate_write_skips_unset_percent" , ha , inv , discharge = True )
5667+ if failed :
5668+ return failed
5669+
55075670 # #4415: timed_charge_current/timed_discharge_current must be re-asserted every call, not
55085671 # just when the rate itself changes
55095672 failed |= test_current_reasserted_on_unchanged_rate ("current_reassert_charge" , ha , inv , 0 , 200 )
@@ -5985,6 +6148,7 @@ def run_inverter_tests(my_predbat_dummy):
59856148 failed |= test_export_window_no_source_configured_raises ("export_window_no_source_configured_raises" , my_predbat , dummy_items )
59866149 failed |= test_window_warning_names_components_when_type_unset ("window_warning_names_components_when_type_unset" , my_predbat , dummy_items )
59876150 failed |= test_short_per_inverter_list_gets_its_dummy_entity ("short_per_inverter_list_gets_its_dummy_entity" , my_predbat )
6151+ failed |= test_create_missing_arg_does_not_fill_sibling_entries ("create_missing_arg_does_not_fill_sibling_entries" , my_predbat )
59886152 if failed :
59896153 return failed
59906154
0 commit comments