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.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",

View file

@ -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."""

View file

@ -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,14 +67,8 @@ 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
def _flatten(
self, data: dict[str, Any], parent: str | None = None
) -> dict[str, Any]:

View file

@ -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"}

View file

@ -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."""

View file

@ -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