From 32b0bf6b4e02968f5040e5b3966376d67a30d98c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 19 Jan 2024 02:40:36 +1000 Subject: [PATCH] Improve coordinator logic in Tessie to allow sleep (#107988) * Poll status before state * Tests --- .../components/tessie/binary_sensor.py | 4 +-- homeassistant/components/tessie/const.py | 10 +++++- .../components/tessie/coordinator.py | 22 ++++++++----- tests/components/tessie/common.py | 6 ++-- tests/components/tessie/conftest.py | 16 ++++++++- tests/components/tessie/test_coordinator.py | 33 ++++++++++--------- 6 files changed, 60 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 5edbb108568..e4c0d5d5c66 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, TessieStatus +from .const import DOMAIN, TessieState from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity @@ -30,7 +30,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = ( TessieBinarySensorEntityDescription( key="state", device_class=BinarySensorDeviceClass.CONNECTIVITY, - is_on=lambda x: x == TessieStatus.ONLINE, + is_on=lambda x: x == TessieState.ONLINE, ), TessieBinarySensorEntityDescription( key="charge_state_battery_heater_on", diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py index 28981b87e6d..591d4652274 100644 --- a/homeassistant/components/tessie/const.py +++ b/homeassistant/components/tessie/const.py @@ -13,13 +13,21 @@ MODELS = { } -class TessieStatus(StrEnum): +class TessieState(StrEnum): """Tessie status.""" ASLEEP = "asleep" ONLINE = "online" +class TessieStatus(StrEnum): + """Tessie status.""" + + ASLEEP = "asleep" + AWAKE = "awake" + WAITING = "waiting_for_sleep" + + class TessieSeatHeaterOptions(StrEnum): """Tessie seat heater options.""" diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index 75cac088bde..c2106af665f 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -5,7 +5,7 @@ import logging from typing import Any from aiohttp import ClientResponseError -from tessie_api import get_state +from tessie_api import get_state, get_status from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -45,11 +45,21 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def _async_update_data(self) -> dict[str, Any]: """Update vehicle data using Tessie API.""" try: + status = await get_status( + session=self.session, + api_key=self.api_key, + vin=self.vin, + ) + if status["status"] == TessieStatus.ASLEEP: + # Vehicle is asleep, no need to poll for data + self.data["state"] = status["status"] + return self.data + vehicle = await get_state( session=self.session, api_key=self.api_key, vin=self.vin, - use_cache=False, + use_cache=True, ) except ClientResponseError as e: if e.status == HTTPStatus.UNAUTHORIZED: @@ -57,13 +67,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise ConfigEntryAuthFailed from e raise e - if vehicle["state"] == TessieStatus.ONLINE: - # Vehicle is online, all data is fresh - return self._flatten(vehicle) - - # Vehicle is asleep, only update state - self.data["state"] = vehicle["state"] - return self.data + return self._flatten(vehicle) def _flatten( self, data: dict[str, Any], parent: str | None = None diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index ae80526e5d9..ccff7f62b1b 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -6,7 +6,7 @@ from unittest.mock import patch from aiohttp import ClientConnectionError, ClientResponseError from aiohttp.client import RequestInfo -from homeassistant.components.tessie.const import DOMAIN +from homeassistant.components.tessie.const import DOMAIN, TessieStatus from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant @@ -14,7 +14,9 @@ from tests.common import MockConfigEntry, load_json_object_fixture TEST_STATE_OF_ALL_VEHICLES = load_json_object_fixture("vehicles.json", DOMAIN) TEST_VEHICLE_STATE_ONLINE = load_json_object_fixture("online.json", DOMAIN) -TEST_VEHICLE_STATE_ASLEEP = load_json_object_fixture("asleep.json", DOMAIN) +TEST_VEHICLE_STATUS_AWAKE = {"status": TessieStatus.AWAKE} +TEST_VEHICLE_STATUS_ASLEEP = {"status": TessieStatus.ASLEEP} + TEST_RESPONSE = {"result": True} TEST_RESPONSE_ERROR = {"result": False, "reason": "reason why"} diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index c7a344d54c5..02b3d56691e 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -5,7 +5,11 @@ from unittest.mock import patch import pytest -from .common import TEST_STATE_OF_ALL_VEHICLES, TEST_VEHICLE_STATE_ONLINE +from .common import ( + TEST_STATE_OF_ALL_VEHICLES, + TEST_VEHICLE_STATE_ONLINE, + TEST_VEHICLE_STATUS_AWAKE, +) @pytest.fixture @@ -18,6 +22,16 @@ def mock_get_state(): yield mock_get_state +@pytest.fixture +def mock_get_status(): + """Mock get_status function.""" + with patch( + "homeassistant.components.tessie.coordinator.get_status", + return_value=TEST_VEHICLE_STATUS_AWAKE, + ) as mock_get_status: + yield mock_get_status + + @pytest.fixture def mock_get_state_of_all_vehicles(): """Mock get_state_of_all_vehicles function.""" diff --git a/tests/components/tessie/test_coordinator.py b/tests/components/tessie/test_coordinator.py index 311222466fd..65f91c6f33e 100644 --- a/tests/components/tessie/test_coordinator.py +++ b/tests/components/tessie/test_coordinator.py @@ -10,8 +10,7 @@ from .common import ( ERROR_AUTH, ERROR_CONNECTION, ERROR_UNKNOWN, - TEST_VEHICLE_STATE_ASLEEP, - TEST_VEHICLE_STATE_ONLINE, + TEST_VEHICLE_STATUS_ASLEEP, setup_platform, ) @@ -20,59 +19,61 @@ from tests.common import async_fire_time_changed WAIT = timedelta(seconds=TESSIE_SYNC_INTERVAL) -async def test_coordinator_online(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_online( + hass: HomeAssistant, mock_get_state, mock_get_status +) -> None: """Tests that the coordinator handles online vehicles.""" - mock_get_state.return_value = TEST_VEHICLE_STATE_ONLINE await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() + mock_get_status.assert_called_once() mock_get_state.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_ON -async def test_coordinator_asleep(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_asleep(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles asleep vehicles.""" - mock_get_state.return_value = TEST_VEHICLE_STATE_ASLEEP await setup_platform(hass) + mock_get_status.return_value = TEST_VEHICLE_STATUS_ASLEEP async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_OFF -async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles client errors.""" - mock_get_state.side_effect = ERROR_UNKNOWN + mock_get_status.side_effect = ERROR_UNKNOWN await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE -async def test_coordinator_auth(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_auth(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles timeout errors.""" - mock_get_state.side_effect = ERROR_AUTH + mock_get_status.side_effect = ERROR_AUTH await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() -async def test_coordinator_connection(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_connection(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles connection errors.""" - mock_get_state.side_effect = ERROR_CONNECTION + mock_get_status.side_effect = ERROR_CONNECTION await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE