Improve coordinator logic in Tessie to allow sleep (#107988)

* Poll status before state

* Tests
This commit is contained in:
Brett Adams 2024-01-19 02:40:36 +10:00 committed by GitHub
parent cdb798bec0
commit 32b0bf6b4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 60 additions and 31 deletions

View file

@ -14,7 +14,7 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, TessieStatus from .const import DOMAIN, TessieState
from .coordinator import TessieStateUpdateCoordinator from .coordinator import TessieStateUpdateCoordinator
from .entity import TessieEntity from .entity import TessieEntity
@ -30,7 +30,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = (
TessieBinarySensorEntityDescription( TessieBinarySensorEntityDescription(
key="state", key="state",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
is_on=lambda x: x == TessieStatus.ONLINE, is_on=lambda x: x == TessieState.ONLINE,
), ),
TessieBinarySensorEntityDescription( TessieBinarySensorEntityDescription(
key="charge_state_battery_heater_on", key="charge_state_battery_heater_on",

View file

@ -13,13 +13,21 @@ MODELS = {
} }
class TessieStatus(StrEnum): class TessieState(StrEnum):
"""Tessie status.""" """Tessie status."""
ASLEEP = "asleep" ASLEEP = "asleep"
ONLINE = "online" ONLINE = "online"
class TessieStatus(StrEnum):
"""Tessie status."""
ASLEEP = "asleep"
AWAKE = "awake"
WAITING = "waiting_for_sleep"
class TessieSeatHeaterOptions(StrEnum): class TessieSeatHeaterOptions(StrEnum):
"""Tessie seat heater options.""" """Tessie seat heater options."""

View file

@ -5,7 +5,7 @@ import logging
from typing import Any from typing import Any
from aiohttp import ClientResponseError 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.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
@ -45,11 +45,21 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Update vehicle data using Tessie API.""" """Update vehicle data using Tessie API."""
try: 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( vehicle = await get_state(
session=self.session, session=self.session,
api_key=self.api_key, api_key=self.api_key,
vin=self.vin, vin=self.vin,
use_cache=False, use_cache=True,
) )
except ClientResponseError as e: except ClientResponseError as e:
if e.status == HTTPStatus.UNAUTHORIZED: if e.status == HTTPStatus.UNAUTHORIZED:
@ -57,13 +67,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
raise ConfigEntryAuthFailed from e raise ConfigEntryAuthFailed from e
raise e raise e
if vehicle["state"] == TessieStatus.ONLINE: return self._flatten(vehicle)
# 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
def _flatten( def _flatten(
self, data: dict[str, Any], parent: str | None = None self, data: dict[str, Any], parent: str | None = None

View file

@ -6,7 +6,7 @@ from unittest.mock import patch
from aiohttp import ClientConnectionError, ClientResponseError from aiohttp import ClientConnectionError, ClientResponseError
from aiohttp.client import RequestInfo 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.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant 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_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_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 = {"result": True}
TEST_RESPONSE_ERROR = {"result": False, "reason": "reason why"} TEST_RESPONSE_ERROR = {"result": False, "reason": "reason why"}

View file

@ -5,7 +5,11 @@ from unittest.mock import patch
import pytest 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 @pytest.fixture
@ -18,6 +22,16 @@ def mock_get_state():
yield 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 @pytest.fixture
def mock_get_state_of_all_vehicles(): def mock_get_state_of_all_vehicles():
"""Mock get_state_of_all_vehicles function.""" """Mock get_state_of_all_vehicles function."""

View file

@ -10,8 +10,7 @@ from .common import (
ERROR_AUTH, ERROR_AUTH,
ERROR_CONNECTION, ERROR_CONNECTION,
ERROR_UNKNOWN, ERROR_UNKNOWN,
TEST_VEHICLE_STATE_ASLEEP, TEST_VEHICLE_STATUS_ASLEEP,
TEST_VEHICLE_STATE_ONLINE,
setup_platform, setup_platform,
) )
@ -20,59 +19,61 @@ from tests.common import async_fire_time_changed
WAIT = timedelta(seconds=TESSIE_SYNC_INTERVAL) 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.""" """Tests that the coordinator handles online vehicles."""
mock_get_state.return_value = TEST_VEHICLE_STATE_ONLINE
await setup_platform(hass) await setup_platform(hass)
async_fire_time_changed(hass, utcnow() + WAIT) async_fire_time_changed(hass, utcnow() + WAIT)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_get_status.assert_called_once()
mock_get_state.assert_called_once() mock_get_state.assert_called_once()
assert hass.states.get("binary_sensor.test_status").state == STATE_ON 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.""" """Tests that the coordinator handles asleep vehicles."""
mock_get_state.return_value = TEST_VEHICLE_STATE_ASLEEP
await setup_platform(hass) await setup_platform(hass)
mock_get_status.return_value = TEST_VEHICLE_STATUS_ASLEEP
async_fire_time_changed(hass, utcnow() + WAIT) async_fire_time_changed(hass, utcnow() + WAIT)
await hass.async_block_till_done() 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 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.""" """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) await setup_platform(hass)
async_fire_time_changed(hass, utcnow() + WAIT) async_fire_time_changed(hass, utcnow() + WAIT)
await hass.async_block_till_done() 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 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.""" """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) await setup_platform(hass)
async_fire_time_changed(hass, utcnow() + WAIT) async_fire_time_changed(hass, utcnow() + WAIT)
await hass.async_block_till_done() 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.""" """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) await setup_platform(hass)
async_fire_time_changed(hass, utcnow() + WAIT) async_fire_time_changed(hass, utcnow() + WAIT)
await hass.async_block_till_done() 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 assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE