Skip to content

Commit 5d0e06c

Browse files
CIclaude
andcommitted
fix(discovery): Fox reporter guards capacity and matches ExportLimit as automatic_config() does
Addresses the Copilot review on #5184: - Only a positive int or float capacity reaches capacity_watts(), the same test drives_it already applied. A string capacity used to raise inside build_discovery() and lose Fox's whole report; now it drops the rating only. - export_limit is matched case-insensitively, as automatic_config() does when it sets hasExportLimit, so the copied test stays identical to its source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 24b512b commit 5d0e06c

2 files changed

Lines changed: 52 additions & 4 deletions

File tree

‎apps/predbat/fox.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2469,7 +2469,8 @@ def build_discovery(self):
24692469
if has_scheduler:
24702470
capabilities.append("schedule")
24712471
settings = self.device_settings.get(serial, {}) or {}
2472-
if "ExportLimit" in settings:
2472+
# Matched case-insensitively, as automatic_config() does when it sets hasExportLimit
2473+
if any(str(name).lower() == "exportlimit" for name in settings):
24732474
capabilities.append("export_limit")
24742475
flags = []
24752476
if detail.get("thirdPartyGen", False):
@@ -2490,8 +2491,12 @@ def build_discovery(self):
24902491
if firmware:
24912492
info["firmware"] = firmware
24922493

2494+
# capacity_watts() multiplies the raw field, so only a positive number reaches it -
2495+
# the same test drives_it applies below. Anything else drops the rating, not the report.
2496+
capacity = detail.get("capacity", 0)
2497+
capacity_is_rating = isinstance(capacity, (int, float)) and capacity > 0
24932498
ratings = {}
2494-
if detail.get("capacity"):
2499+
if capacity_is_rating:
24952500
ratings["inverter_w"] = self.capacity_watts(detail)
24962501
battery_list = detail.get("batteryList") or []
24972502
summed = [entry for entry in battery_list if isinstance(entry, dict) and "capacity" in entry] if isinstance(battery_list, list) else []
@@ -2504,8 +2509,7 @@ def build_discovery(self):
25042509
# all hold. Duplicated here, not shared, so this observer does not touch that control
25052510
# path; test_fox_build_discovery_sets_inverter_type_only_where_automatic_config_would
25062511
# runs the real automatic_config() to keep the two in step.
2507-
capacity = detail.get("capacity", 0)
2508-
drives_it = has_battery and has_scheduler and isinstance(capacity, (int, float)) and capacity > 0
2512+
drives_it = has_battery and has_scheduler and capacity_is_rating
25092513

25102514
inverters.append(
25112515
inverter_record(

‎apps/predbat/tests/test_fox_api.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7681,6 +7681,48 @@ def test_fox_build_discovery_sets_inverter_type_only_where_automatic_config_woul
76817681
return 0
76827682

76837683

7684+
def test_fox_build_discovery_survives_a_capacity_that_is_not_a_number(my_predbat):
7685+
"""A capacity that is not a positive number drops the rating, not the whole report.
7686+
7687+
capacity_watts() multiplies the raw field by 1000.0, so a non-empty string would raise inside
7688+
build_discovery(); refresh_discovery() would catch that and file nothing for any Fox device.
7689+
The rating is read under the same test drives_it applies: a positive int or float.
7690+
"""
7691+
print("**** test_fox_build_discovery_survives_a_capacity_that_is_not_a_number ****")
7692+
for capacity in ("8", "unknown", -8, None):
7693+
fox = _fox_discovery_api(my_predbat)
7694+
fox.device_detail["BATT001"]["capacity"] = capacity
7695+
7696+
report = fox.build_discovery()
7697+
7698+
by_id = {record["device_id"]: record for record in report["inverters"]}
7699+
assert set(by_id) == {"fox:BATT001", "fox:PVONLY1"}, f"capacity {capacity!r}: every device is still reported"
7700+
battery = by_id["fox:BATT001"]
7701+
assert "inverter_w" not in battery.get("ratings", {}), f"capacity {capacity!r} is not a rating: {battery.get('ratings')}"
7702+
assert "inverter_type" not in battery, f"capacity {capacity!r}: automatic_config() would not drive this device"
7703+
assert sorted(battery["functions"]) == ["battery", "solar"], "the rest of the record is unaffected"
7704+
assert by_id["fox:PVONLY1"]["ratings"]["inverter_w"] == 5000.0, "the other device keeps its rating"
7705+
print("PASS: Fox reports every device when a capacity is not a number")
7706+
return 0
7707+
7708+
7709+
def test_fox_build_discovery_matches_export_limit_as_automatic_config_does(my_predbat):
7710+
"""export_limit is found by the same case-insensitive match automatic_config() uses for hasExportLimit."""
7711+
print("**** test_fox_build_discovery_matches_export_limit_as_automatic_config_does ****")
7712+
for name in ("ExportLimit", "exportlimit", "EXPORTLIMIT"):
7713+
fox = _fox_discovery_api(my_predbat)
7714+
fox.device_settings["BATT001"] = {name: {"value": 12000.0}}
7715+
battery = {record["device_id"]: record for record in fox.build_discovery()["inverters"]}["fox:BATT001"]
7716+
assert "export_limit" in battery["capabilities"], f"setting {name!r}: {battery['capabilities']}"
7717+
7718+
fox = _fox_discovery_api(my_predbat)
7719+
fox.device_settings["BATT001"] = {"WorkMode": {"value": "SelfUse"}}
7720+
battery = {record["device_id"]: record for record in fox.build_discovery()["inverters"]}["fox:BATT001"]
7721+
assert "export_limit" not in battery["capabilities"], "no ExportLimit setting, no capability"
7722+
print("PASS: Fox matches ExportLimit as automatic_config() does")
7723+
return 0
7724+
7725+
76847726
def test_fox_build_discovery_returns_none_before_discovery(my_predbat):
76857727
"""With no devices found yet there is nothing to describe, so nothing is reported."""
76867728
print("**** test_fox_build_discovery_returns_none_before_discovery ****")
@@ -8043,6 +8085,8 @@ def run_fox_api_tests(my_predbat):
80438085
failed |= test_fox_build_discovery_round_trips_through_validate_report(my_predbat)
80448086
failed |= test_fox_build_discovery_battery_ratings_tell_the_aio_bug_from_a_healthy_stack(my_predbat)
80458087
failed |= test_fox_build_discovery_sets_inverter_type_only_where_automatic_config_would(my_predbat)
8088+
failed |= test_fox_build_discovery_survives_a_capacity_that_is_not_a_number(my_predbat)
8089+
failed |= test_fox_build_discovery_matches_export_limit_as_automatic_config_does(my_predbat)
80468090
failed |= test_fox_build_discovery_returns_none_before_discovery(my_predbat)
80478091
failed |= test_fox_run_reports_discovery_and_survives_a_failure(my_predbat)
80488092
failed |= test_fox_run_reports_discovery_when_automatic_config_fails(my_predbat)

0 commit comments

Comments
 (0)