Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion packages/modules/devices/tesla/tesla/counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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 = [
Expand Down
53 changes: 53 additions & 0 deletions packages/modules/devices/tesla/tesla/tesla_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Loading