Implement Review Feedback for Tessie (#105937)

This commit is contained in:
Brett Adams 2023-12-18 11:33:26 +10:00 committed by GitHub
parent aac02d7b84
commit a88335272a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 28 additions and 48 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 from .const import DOMAIN, TessieStatus
from .coordinator import TessieDataUpdateCoordinator from .coordinator import TessieDataUpdateCoordinator
from .entity import TessieEntity from .entity import TessieEntity
@ -27,6 +27,11 @@ class TessieBinarySensorEntityDescription(BinarySensorEntityDescription):
DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = ( DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = (
TessieBinarySensorEntityDescription(
key="state",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
is_on=lambda x: x == TessieStatus.ONLINE,
),
TessieBinarySensorEntityDescription( TessieBinarySensorEntityDescription(
key="charge_state_battery_heater_on", key="charge_state_battery_heater_on",
device_class=BinarySensorDeviceClass.HEAT, device_class=BinarySensorDeviceClass.HEAT,

View File

@ -18,6 +18,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
TESSIE_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) TESSIE_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
DESCRIPTION_PLACEHOLDERS = {
"url": "[my.tessie.com/settings/api](https://my.tessie.com/settings/api)"
}
class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@ -57,6 +60,7 @@ class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=TESSIE_SCHEMA, data_schema=TESSIE_SCHEMA,
description_placeholders=DESCRIPTION_PLACEHOLDERS,
errors=errors, errors=errors,
) )
@ -98,5 +102,6 @@ class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form( return self.async_show_form(
step_id="reauth_confirm", step_id="reauth_confirm",
data_schema=TESSIE_SCHEMA, data_schema=TESSIE_SCHEMA,
description_placeholders=DESCRIPTION_PLACEHOLDERS,
errors=errors, errors=errors,
) )

View File

@ -18,4 +18,3 @@ class TessieStatus(StrEnum):
ASLEEP = "asleep" ASLEEP = "asleep"
ONLINE = "online" ONLINE = "online"
OFFLINE = "offline"

View File

@ -20,7 +20,7 @@ TESSIE_SYNC_INTERVAL = 10
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class TessieDataUpdateCoordinator(DataUpdateCoordinator): class TessieDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching data from the Tessie API.""" """Class to manage fetching data from the Tessie API."""
def __init__( def __init__(
@ -35,16 +35,15 @@ class TessieDataUpdateCoordinator(DataUpdateCoordinator):
hass, hass,
_LOGGER, _LOGGER,
name="Tessie", name="Tessie",
update_method=self.async_update_data,
update_interval=timedelta(seconds=TESSIE_SYNC_INTERVAL), update_interval=timedelta(seconds=TESSIE_SYNC_INTERVAL),
) )
self.api_key = api_key self.api_key = api_key
self.vin = vin self.vin = vin
self.session = async_get_clientsession(hass) self.session = async_get_clientsession(hass)
self.data = self._flattern(data) self.data = self._flatten(data)
self.did_first_update = False self.did_first_update = False
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:
vehicle = await get_state( vehicle = await get_state(
@ -54,10 +53,6 @@ class TessieDataUpdateCoordinator(DataUpdateCoordinator):
use_cache=self.did_first_update, use_cache=self.did_first_update,
) )
except ClientResponseError as e: except ClientResponseError as e:
if e.status == HTTPStatus.REQUEST_TIMEOUT:
# Vehicle is offline, only update state and dont throw error
self.data["state"] = TessieStatus.OFFLINE
return self.data
if e.status == HTTPStatus.UNAUTHORIZED: if e.status == HTTPStatus.UNAUTHORIZED:
# Auth Token is no longer valid # Auth Token is no longer valid
raise ConfigEntryAuthFailed from e raise ConfigEntryAuthFailed from e
@ -66,22 +61,22 @@ class TessieDataUpdateCoordinator(DataUpdateCoordinator):
self.did_first_update = True self.did_first_update = True
if vehicle["state"] == TessieStatus.ONLINE: if vehicle["state"] == TessieStatus.ONLINE:
# Vehicle is online, all data is fresh # Vehicle is online, all data is fresh
return self._flattern(vehicle) return self._flatten(vehicle)
# Vehicle is asleep, only update state # Vehicle is asleep, only update state
self.data["state"] = vehicle["state"] self.data["state"] = vehicle["state"]
return self.data return self.data
def _flattern( def _flatten(
self, data: dict[str, Any], parent: str | None = None self, data: dict[str, Any], parent: str | None = None
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Flattern the data structure.""" """Flatten the data structure."""
result = {} result = {}
for key, value in data.items(): for key, value in data.items():
if parent: if parent:
key = f"{parent}_{key}" key = f"{parent}_{key}"
if isinstance(value, dict): if isinstance(value, dict):
result.update(self._flattern(value, key)) result.update(self._flatten(value, key))
else: else:
result[key] = value result[key] = value
return result return result

View File

@ -27,7 +27,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from .const import DOMAIN, TessieStatus from .const import DOMAIN
from .coordinator import TessieDataUpdateCoordinator from .coordinator import TessieDataUpdateCoordinator
from .entity import TessieEntity from .entity import TessieEntity
@ -40,11 +40,6 @@ class TessieSensorEntityDescription(SensorEntityDescription):
DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
TessieSensorEntityDescription(
key="state",
options=[status.value for status in TessieStatus],
device_class=SensorDeviceClass.ENUM,
),
TessieSensorEntityDescription( TessieSensorEntityDescription(
key="charge_state_usable_battery_level", key="charge_state_usable_battery_level",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,

View File

@ -10,7 +10,7 @@
"data": { "data": {
"access_token": "[%key:common::config_flow::data::access_token%]" "access_token": "[%key:common::config_flow::data::access_token%]"
}, },
"description": "Enter your access token from [my.tessie.com/settings/api](https://my.tessie.com/settings/api)." "description": "Enter your access token from {url}."
}, },
"reauth_confirm": { "reauth_confirm": {
"data": { "data": {
@ -23,14 +23,6 @@
}, },
"entity": { "entity": {
"sensor": { "sensor": {
"state": {
"name": "Status",
"state": {
"online": "Online",
"asleep": "Asleep",
"offline": "Offline"
}
},
"charge_state_usable_battery_level": { "charge_state_usable_battery_level": {
"name": "Battery level" "name": "Battery level"
}, },
@ -96,6 +88,9 @@
} }
}, },
"binary_sensor": { "binary_sensor": {
"state": {
"name": "Status"
},
"charge_state_battery_heater_on": { "charge_state_battery_heater_on": {
"name": "Battery heater" "name": "Battery heater"
}, },

View File

@ -2,15 +2,13 @@
from datetime import timedelta from datetime import timedelta
from homeassistant.components.tessie.coordinator import TESSIE_SYNC_INTERVAL from homeassistant.components.tessie.coordinator import TESSIE_SYNC_INTERVAL
from homeassistant.components.tessie.sensor import TessieStatus from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .common import ( from .common import (
ERROR_AUTH, ERROR_AUTH,
ERROR_CONNECTION, ERROR_CONNECTION,
ERROR_TIMEOUT,
ERROR_UNKNOWN, ERROR_UNKNOWN,
TEST_VEHICLE_STATE_ASLEEP, TEST_VEHICLE_STATE_ASLEEP,
TEST_VEHICLE_STATE_ONLINE, TEST_VEHICLE_STATE_ONLINE,
@ -31,7 +29,7 @@ async def test_coordinator_online(hass: HomeAssistant, mock_get_state) -> None:
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_state.assert_called_once()
assert hass.states.get("sensor.test_status").state == TessieStatus.ONLINE 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_state) -> None:
@ -43,7 +41,7 @@ async def test_coordinator_asleep(hass: HomeAssistant, mock_get_state) -> None:
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_state.assert_called_once()
assert hass.states.get("sensor.test_status").state == TessieStatus.ASLEEP 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_state) -> None:
@ -55,19 +53,7 @@ async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_state) -> N
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_state.assert_called_once()
assert hass.states.get("sensor.test_status").state == STATE_UNAVAILABLE assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE
async def test_coordinator_timeout(hass: HomeAssistant, mock_get_state) -> None:
"""Tests that the coordinator handles timeout errors."""
mock_get_state.side_effect = ERROR_TIMEOUT
await setup_platform(hass)
async_fire_time_changed(hass, utcnow() + WAIT)
await hass.async_block_till_done()
mock_get_state.assert_called_once()
assert hass.states.get("sensor.test_status").state == TessieStatus.OFFLINE
async def test_coordinator_auth(hass: HomeAssistant, mock_get_state) -> None: async def test_coordinator_auth(hass: HomeAssistant, mock_get_state) -> None:
@ -89,4 +75,4 @@ async def test_coordinator_connection(hass: HomeAssistant, mock_get_state) -> No
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_state.assert_called_once()
assert hass.states.get("sensor.test_status").state == STATE_UNAVAILABLE assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE