Skip to content

Commit 3c415f5

Browse files
Merge pull request #5293 from springfall2008/fix/gecloud-3hy-integer-rating-5136
fix(gecloud): parse the integer rating of 3-phase hybrid models (GIV-3HY-11)
2 parents e3b8f7d + 385ddda commit 3c415f5

2 files changed

Lines changed: 94 additions & 1 deletion

File tree

‎apps/predbat/gecloud.py‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,8 @@ def initialize(self, ge_cloud_direct, api_key, automatic, automatic_evc=False, e
613613
self.ems_slot_warned = set()
614614
# (device, key) registers whose option validation text could not be parsed, so the warning is logged once
615615
self.validation_parse_warned = set()
616+
# Hybrid model names whose inverter rating could not be parsed, so the fallback warning is logged once
617+
self.model_rate_warned = set()
616618
self.devices_dict = {}
617619
self.device_list = []
618620
self.ems_device = None
@@ -920,18 +922,40 @@ def get_max_inverter_rate_from_model(self, model, max_charge_rate=None):
920922
- GIV-HY3.6 - 3.6kW inverter
921923
- GIV-HY5.0 - 5.0kW inverter
922924
- GIV-HY-8.0-G3-HV. - 8kW inverter
925+
- GIV-3HY-11 - 11kW 3-phase hybrid (integer rating as its own segment after 3HY)
926+
- GIV-AIO-GW2 - Gateway variant, the trailing 2 is not a rating
923927
- Plant EMS - Not an inverter, should use the individual inverter values
924928
"""
925929

930+
# GE Cloud can send an explicit null model, which would crash every regex below
931+
if not isinstance(model, str):
932+
return max_charge_rate
933+
934+
# Trailing whitespace or a trailing '.' (GIV-HY-8.0-G3-HV.) must not defeat the segment anchors below
935+
model_clean = model.strip().rstrip(".")
936+
937+
# Integer rating only as a whole segment straight after the 3-phase hybrid token (GIV-3HY-11),
938+
# bounded by '-' or end of string so suffixes such as GW2 / G3 are never read as a rating.
939+
# Checked before the decimal pass so a later decimal segment cannot hide the explicit 3HY rating
940+
match = re.search(r"(?:^|-)3HY-(\d{1,2})(?=-|$)", model_clean, re.IGNORECASE)
941+
if match:
942+
return int(match.group(1)) * 1000
943+
926944
# Find all decimal numbers anywhere in the model string (e.g. 3.6, 10.0, 12.0)
927945
# Use the last match so that e.g. GIV-AIO-AC-13.5-12.0 resolves to 12kW not 13.5kW
928-
matches = re.findall(r"\d+\.\d+", model)
946+
matches = re.findall(r"\d+\.\d+", model_clean)
929947
if matches:
930948
try:
931949
max_inverter_rate = int(float(matches[-1]) * 1000)
932950
return max_inverter_rate
933951
except ValueError:
934952
pass
953+
954+
# A hybrid with no parseable rating is using a battery rate as its AC rating, warn once per model
955+
if "HY" in model_clean.upper() and model not in self.model_rate_warned:
956+
fallback = "max charge rate {}W".format(max_charge_rate) if max_charge_rate is not None else "no rating"
957+
self.log("GECloud: Warn: Unable to determine inverter rating from model '{}', using {} instead - set inverter_limit in apps.yaml if this is wrong".format(model, fallback))
958+
self.model_rate_warned.add(model)
935959
return max_charge_rate
936960

937961
async def publish_account(self, account):

‎apps/predbat/tests/test_ge_cloud.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)