From 597089beccf7761e87c7851d1658edfd982b9888 Mon Sep 17 00:00:00 2001 From: jrester <31157644+jrester@users.noreply.github.com> Date: Sat, 24 Feb 2024 14:56:44 +0100 Subject: [PATCH] fix #65: handle disabled battery packs (#66) --- CHANGELOG | 1 + README.md | 17 +++++++++++++ tesla_powerwall/const.py | 1 + tesla_powerwall/responses.py | 35 +++++++++++++++++++------- tests/unit/fixtures/system_status.json | 26 +++++++++++++++++++ tests/unit/test_powerwall.py | 9 ++++++- 6 files changed, 79 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 817dfee..20414e7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ ## [WIP] - Use yarl for URL parsing by @bdraco (https://github.com/jrester/tesla_powerwall/pull/62) +- Correctly handle disabled battery packs (https://github.com/jrester/tesla_powerwall/pull/66/) ## [0.5.1] diff --git a/README.md b/README.md index f72a0a5..7fe2acd 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ await powerwall.get_capacity() Get information about the battery packs that are installed: +Assuming that the battery is operational, you can retrive a number of values about each battery: ```python batteries = await powerwall.get_batteries() #=> [, ] @@ -200,6 +201,22 @@ batteries[0].i_out #=> -7.4 batteries[0].grid_state #=> GridState.COMPLIANT +batteries[0].disabled_reasons +#=> [] + +``` + +If a battery is disabled it's `grid_state` will be `GridState.DISABLED` and some values will be `None`. The variable `disabled_reasons` might contain more information why the battery is disabled: +```python +... +batteries[1].grid_state +#=> GridState.DISABLED +batteries[1].disabled_reasons +#=> ["DisabledExcessiveVoltageDrop"] +batteries[1].p_out +#=> None +batteries[1].energy_charged +#=> None ``` ### Powerwall Status diff --git a/tesla_powerwall/const.py b/tesla_powerwall/const.py index 896dab4..f2259a2 100644 --- a/tesla_powerwall/const.py +++ b/tesla_powerwall/const.py @@ -32,6 +32,7 @@ class IslandMode(Enum): class GridState(Enum): + DISABLED = "Disabled" COMPLIANT = "Grid_Compliant" QUALIFYING = "Grid_Qualifying" UNCOMPLIANT = "Grid_Uncompliant" diff --git a/tesla_powerwall/responses.py b/tesla_powerwall/responses.py index b72236a..3d50d72 100644 --- a/tesla_powerwall/responses.py +++ b/tesla_powerwall/responses.py @@ -272,22 +272,38 @@ def from_dict(src: dict) -> "SolarResponse": @dataclass class BatteryResponse(ResponseBase): + """ + A battery pack as part of the system_status response. + """ + part_number: str serial_number: str - energy_charged: int - energy_discharged: int + wobble_detected: bool energy_remaining: int capacity: int - wobble_detected: bool - p_out: int - q_out: int - v_out: float - f_out: float - i_out: float + # Values might be None if this battery is in GridState.DISABLED + energy_charged: Optional[int] + energy_discharged: Optional[int] + p_out: Optional[int] + q_out: Optional[int] + v_out: Optional[float] + f_out: Optional[float] + i_out: Optional[float] grid_state: GridState + disabled_reasons: List[str] @staticmethod def from_dict(src: dict) -> "BatteryResponse": + # Check if the battery is disabled. A battery is considered disabled if: + # - there is at least one disabled reason present in the response, + # - or the pinv_grid_state is empty + disabled_reasons = src["disabled_reasons"] + raw_grid_state = src["pinv_grid_state"] + grid_state = ( + GridState.DISABLED + if len(disabled_reasons) > 0 or len(raw_grid_state) == 0 + else GridState(raw_grid_state) + ) return BatteryResponse( src, part_number=src["PackagePartNumber"], @@ -302,5 +318,6 @@ def from_dict(src: dict) -> "BatteryResponse": v_out=src["v_out"], f_out=src["f_out"], i_out=src["i_out"], - grid_state=GridState(src["pinv_grid_state"]), + grid_state=grid_state, + disabled_reasons=disabled_reasons, ) diff --git a/tests/unit/fixtures/system_status.json b/tests/unit/fixtures/system_status.json index 7a1ed61..64eee63 100644 --- a/tests/unit/fixtures/system_status.json +++ b/tests/unit/fixtures/system_status.json @@ -50,6 +50,32 @@ "version": "67f943cb05d12d", "vf_mode": false, "wobble_detected": false + }, + { + "OpSeqState": "Standby", + "PackagePartNumber": "XXX-E", + "PackageSerialNumber": "XXX", + "Type": "", + "backup_ready": false, + "charge_power_clamped": false, + "disabled_reasons": [ + "DisabledExcessiveVoltageDrop" + ], + "energy_charged": null, + "energy_discharged": null, + "f_out": null, + "i_out": null, + "nominal_energy_remaining": 0, + "nominal_full_pack_energy": 14714, + "off_grid": false, + "p_out": null, + "pinv_grid_state": "", + "pinv_state": "", + "q_out": null, + "v_out": null, + "version": "eb113390162784", + "vf_mode": false, + "wobble_detected": false } ], "battery_target_power": -3646.2544361664613, diff --git a/tests/unit/test_powerwall.py b/tests/unit/test_powerwall.py index 88bccee..63ab158 100644 --- a/tests/unit/test_powerwall.py +++ b/tests/unit/test_powerwall.py @@ -242,7 +242,7 @@ async def test_system_status(self): self.add_response("system_status", body=SYSTEM_STATUS_RESPONSE) batteries = await self.powerwall.get_batteries() - self.assertEqual(len(batteries), 2) + self.assertEqual(len(batteries), 3) self.assertEqual(batteries[0].part_number, "XXX-G") self.assertEqual(batteries[0].serial_number, "TGXXX") self.assertEqual(batteries[0].energy_remaining, 7378) @@ -256,6 +256,13 @@ async def test_system_status(self): self.assertEqual(batteries[0].q_out, 30) self.assertEqual(batteries[0].v_out, 226.60000000000002) self.assertEqual(batteries[0].grid_state, GridState.COMPLIANT) + self.assertEqual(batteries[2].grid_state, GridState.DISABLED) + self.assertEqual(batteries[2].p_out, None) + self.assertEqual(batteries[2].i_out, None) + self.assertEqual(batteries[2].energy_charged, None) + self.assertEqual( + batteries[2].disabled_reasons, ["DisabledExcessiveVoltageDrop"] + ) self.aresponses.assert_plan_strictly_followed() async def test_islanding_mode_offgrid(self):