diff --git a/packages/modules/devices/tesla/tesla/counter.py b/packages/modules/devices/tesla/tesla/counter.py index 586214e9dd..91606eecd9 100644 --- a/packages/modules/devices/tesla/tesla/counter.py +++ b/packages/modules/devices/tesla/tesla/counter.py @@ -2,6 +2,8 @@ import logging import math +from requests import HTTPError + from modules.common.abstract_device import AbstractCounter from modules.common.component_state import CounterState from modules.common.component_type import ComponentDescriptor @@ -16,6 +18,9 @@ class TeslaCounter(AbstractCounter): def __init__(self, component_config: TeslaCounterSetup) -> None: self.component_config = component_config + # Powerwall 3 / newer gateway firmware no longer provide /api/meters/site (HTTP 404). + # Remember that, so we do not request the endpoint again in every cycle. + self._meters_site_available = True def initialize(self) -> None: self.store = get_component_value_store( @@ -71,9 +76,27 @@ def _calc_currents_and_pf_from_pqu( return currents, pfs + def _get_meters_site(self, client: PowerwallHttpClient): + if not self._meters_site_available: + return None + try: + return client.get_json("/api/meters/site") + except HTTPError as e: + status = getattr(getattr(e, "response", None), "status_code", None) + if status != 404: + raise + self._meters_site_available = False + log.info( + "Tesla gateway does not provide /api/meters/site (HTTP 404, e.g. Powerwall 3). " + "Using total values from /api/meters/aggregates only." + ) + return None + def update(self, client: PowerwallHttpClient, aggregate): try: - meters_site = client.get_json("/api/meters/site") + meters_site = self._get_meters_site(client) + if meters_site is None: + raise KeyError("/api/meters/site not available") cached = meters_site[0]["Cached_readings"] voltages = [ diff --git a/packages/modules/devices/tesla/tesla/tesla_test.py b/packages/modules/devices/tesla/tesla/tesla_test.py index 5b4036118f..947d59e505 100644 --- a/packages/modules/devices/tesla/tesla/tesla_test.py +++ b/packages/modules/devices/tesla/tesla/tesla_test.py @@ -5,6 +5,7 @@ from modules.common.configurable_device import ConfigurableDevice from modules.devices.tesla.tesla import bat +from modules.devices.tesla.tesla import counter from modules.devices.tesla.tesla import device from modules.devices.tesla.tesla.device import Tesla, create_device from modules.common.component_state import BatState @@ -160,3 +161,55 @@ def test_powerwall_update_if_cookie_cached(monkeypatch, requests_mock: requests_ # evaluation assert_battery_state_correct(mock_bat_value_store.set.call_args[0][0]) + + +def setup_counter_component() -> ConfigurableDevice: + device_config = Tesla(configuration=TeslaConfiguration( + ip_address="sample-address", + email="sample@mail.com", + password="some password")) + dev = create_device(device_config) + dev.add_component(counter.component_descriptor.configuration_factory()) + return dev + + +def test_powerwall_counter_falls_back_to_aggregates_if_meters_site_404(monkeypatch, + requests_mock: requests_mock.Mocker): + # Powerwall 3 with Backup Gateway 2 answers /api/meters/site with HTTP 404 + # setup + mock_counter_value_store = Mock() + monkeypatch.setattr(counter, "get_component_value_store", Mock(return_value=mock_counter_value_store)) + requests_mock.get("https://sample-address/api/meters/aggregates", text=sample_aggregates_json) + meters_site_mock = requests_mock.get("https://sample-address/api/meters/site", status_code=404) + monkeypatch.setattr(device, "_authenticate", + Mock(return_value={"AuthCookie": "auth-cookie", "UserRecord": "user-record"})) + dev = setup_counter_component() + + # execution + dev.update() + dev.update() + + # evaluation + state = mock_counter_value_store.set.call_args[0][0] + assert state.power == -21.449996948242188 + assert state.imported == 3276432.6625 + assert state.exported == 1136916.6875 + assert mock_counter_value_store.set.call_count == 2 + # endpoint is not requested again once it is known to be missing + assert meters_site_mock.call_count == 1 + + +def test_powerwall_counter_raises_on_other_http_errors(monkeypatch, requests_mock: requests_mock.Mocker): + # setup + mock_counter_value_store = Mock() + monkeypatch.setattr(counter, "get_component_value_store", Mock(return_value=mock_counter_value_store)) + requests_mock.get("https://sample-address/api/meters/aggregates", text=sample_aggregates_json) + requests_mock.get("https://sample-address/api/meters/site", status_code=500) + monkeypatch.setattr(device, "_authenticate", + Mock(return_value={"AuthCookie": "auth-cookie", "UserRecord": "user-record"})) + + # execution + setup_counter_component().update() + + # evaluation + mock_counter_value_store.set.assert_not_called()