@@ -75,6 +75,7 @@ def __init__(self):
7575 self .gateway_device = None
7676 self .ems_slot_warned = set ()
7777 self .validation_parse_warned = set ()
78+ self .model_rate_warned = set ()
7879 self ._discovery_report = None
7980 self ._now_utc_exact = datetime .now (timezone .utc )
8081 self .settings_from_cache = False
@@ -8120,6 +8121,26 @@ def _test_get_max_inverter_rate_from_model(my_predbat):
81208121 ("Plant EMS" , None , None , "Plant EMS => None (no number)" ),
81218122 # No number, but max_charge_rate provided as fallback
81228123 ("Gateway" , 2600 , 2600 , "Gateway => fallback to max_charge_rate" ),
8124+ # 3-phase hybrid carries an integer rating as its own segment after 3HY (GH#5136)
8125+ ("GIV-3HY-11" , 9984 , 11000 , "GIV-3HY-11 => 11kW, not the 9984W battery rate" ),
8126+ ("GIV-3HY-8" , 7987 , 8000 , "GIV-3HY-8 => 8kW" ),
8127+ ("giv-3hy-5" , 4000 , 5000 , "giv-3hy-5 => 5kW (case-insensitive)" ),
8128+ # Integer suffixes that are not ratings keep the fallback
8129+ ("GIV-AIO-GW2" , 2600 , 2600 , "GIV-AIO-GW2 => fallback, GW2 is not a rating" ),
8130+ ("GIV-3HY-G3" , 9984 , 9984 , "GIV-3HY-G3 => fallback, G3 is not a rating" ),
8131+ ("GIV-3HY-110" , 9984 , 9984 , "GIV-3HY-110 => fallback, three digits is not a plausible rating" ),
8132+ # Non-3HY hybrids still use the decimal pass
8133+ ("ALPS HY-6.0-GL" , 3600 , 6000 , "ALPS HY-6.0-GL => 6kW" ),
8134+ # The explicit 3HY segment wins over a later decimal segment
8135+ ("GIV-3HY-11-9.5" , 9984 , 11000 , "GIV-3HY-11-9.5 => 11kW, later decimal does not hide the 3HY rating" ),
8136+ # A 3HY decimal rating is not a whole integer segment and falls through to the decimal pass
8137+ ("GIV-3HY-10.0" , 9984 , 10000 , "GIV-3HY-10.0 => 10kW via the decimal pass" ),
8138+ # Trailing punctuation or whitespace does not defeat the segment anchors
8139+ ("GIV-3HY-11." , 9984 , 11000 , "GIV-3HY-11. => 11kW (trailing period)" ),
8140+ ("GIV-3HY-11 " , 9984 , 11000 , "GIV-3HY-11<space> => 11kW (trailing space)" ),
8141+ ("GIV-HY-8.0-G3-HV." , None , 8000 , "GIV-HY-8.0-G3-HV. => 8kW (trailing period)" ),
8142+ # An explicit null model from GE Cloud falls back rather than raising TypeError
8143+ (None , 3600 , 3600 , "None model => fallback to max_charge_rate" ),
81238144 ]
81248145
81258146 failed = 0
@@ -8132,6 +8153,54 @@ def _test_get_max_inverter_rate_from_model(my_predbat):
81328153 else :
81338154 print ("OK {}: got {}" .format (description , result ))
81348155
8156+ # A hybrid model that cannot be parsed warns once per model, non-hybrids stay silent
8157+ ge_cloud .model_rate_warned = set ()
8158+ ge_cloud .log_messages = []
8159+ ge_cloud .get_max_inverter_rate_from_model ("GIV-HYX" , 2600 )
8160+ ge_cloud .get_max_inverter_rate_from_model ("GIV-HYX" , 2600 )
8161+ ge_cloud .get_max_inverter_rate_from_model ("Gateway" , 2600 )
8162+ ge_cloud .get_max_inverter_rate_from_model ("GIV-3HY-11" , 9984 )
8163+ warnings = [message for message in ge_cloud .log_messages if "Unable to determine inverter rating" in message ]
8164+ if len (warnings ) != 1 or "GIV-HYX" not in warnings [0 ]:
8165+ print ("ERROR expected exactly one fallback warning naming GIV-HYX, got {}" .format (warnings ))
8166+ failed += 1
8167+ else :
8168+ print ("OK unparsed hybrid model warns once" )
8169+
8170+ # With no max_charge_rate the warning must not print "NoneW"
8171+ ge_cloud .model_rate_warned = set ()
8172+ ge_cloud .log_messages = []
8173+ ge_cloud .get_max_inverter_rate_from_model ("GIV-HYZ" , None )
8174+ warnings = [message for message in ge_cloud .log_messages if "Unable to determine inverter rating" in message ]
8175+ if len (warnings ) != 1 or "NoneW" in warnings [0 ]:
8176+ print ("ERROR expected one fallback warning without 'NoneW', got {}" .format (warnings ))
8177+ failed += 1
8178+ else :
8179+ print ("OK fallback warning with no max_charge_rate is readable" )
8180+
8181+ # End to end through publish_info: the 3-phase hybrid publishes its 11kW rating, not the fluctuating battery rate (GH#5136)
8182+ ge_cloud .config_args ["prefix" ] = "predbat"
8183+ ge_cloud .dashboard_items .clear ()
8184+ info_3hy = {"info" : {"battery" : {"nominal_capacity" : 186 , "nominal_voltage" : 51.2 }, "model" : "GIV-3HY-11" , "max_charge_rate" : 9984 }}
8185+ run_async (ge_cloud .publish_info ("dev3hy" , info_3hy ))
8186+ rate = ge_cloud .dashboard_items .get ("sensor.predbat_gecloud_dev3hy_max_inverter_rate" , {}).get ("state" )
8187+ if rate != 11000 :
8188+ print ("ERROR publish_info GIV-3HY-11 expected max_inverter_rate 11000, got {}" .format (rate ))
8189+ failed += 1
8190+ else :
8191+ print ("OK publish_info GIV-3HY-11 publishes 11000" )
8192+
8193+ # An explicit null model in the payload publishes the fallback instead of crashing the refresh
8194+ ge_cloud .dashboard_items .clear ()
8195+ info_null_model = {"info" : {"battery" : {"nominal_capacity" : 186 , "nominal_voltage" : 51.2 }, "model" : None , "max_charge_rate" : 3600 }}
8196+ run_async (ge_cloud .publish_info ("devnull" , info_null_model ))
8197+ rate = ge_cloud .dashboard_items .get ("sensor.predbat_gecloud_devnull_max_inverter_rate" , {}).get ("state" )
8198+ if rate != 3600 :
8199+ print ("ERROR publish_info null model expected max_inverter_rate 3600, got {}" .format (rate ))
8200+ failed += 1
8201+ else :
8202+ print ("OK publish_info null model publishes the fallback" )
8203+
81358204 return 1 if failed else 0
81368205
81378206
0 commit comments