Refactor volvooncall to (mostly) use DataUpdateCoordinator (#75885)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
y34hbuddy 2022-08-04 13:44:39 -04:00 committed by GitHub
parent 02ad4843b8
commit b5a6ee3c56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 209 additions and 136 deletions

View File

@ -2,8 +2,10 @@
from datetime import timedelta from datetime import timedelta
import logging import logging
import async_timeout
import voluptuous as vol import voluptuous as vol
from volvooncall import Connection from volvooncall import Connection
from volvooncall.dashboard import Instrument
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
@ -17,14 +19,13 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import async_dispatcher_send
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util.dt import utcnow from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
DOMAIN = "volvooncall" DOMAIN = "volvooncall"
@ -32,7 +33,6 @@ DATA_KEY = DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_UPDATE_INTERVAL = timedelta(minutes=1)
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
CONF_SERVICE_URL = "service_url" CONF_SERVICE_URL = "service_url"
@ -92,24 +92,31 @@ RESOURCES = [
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.All(
{ cv.deprecated(CONF_SCAN_INTERVAL),
vol.Required(CONF_USERNAME): cv.string, cv.deprecated(CONF_NAME),
vol.Required(CONF_PASSWORD): cv.string, cv.deprecated(CONF_RESOURCES),
vol.Optional( vol.Schema(
CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL {
): vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default={}): cv.schema_with_slug_keys( vol.Required(CONF_PASSWORD): cv.string,
cv.string vol.Optional(
), CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL
vol.Optional(CONF_RESOURCES): vol.All( ): vol.All(
cv.ensure_list, [vol.In(RESOURCES)] cv.time_period, vol.Clamp(min=DEFAULT_UPDATE_INTERVAL)
), ), # ignored, using DataUpdateCoordinator instead
vol.Optional(CONF_REGION): cv.string, vol.Optional(CONF_NAME, default={}): cv.schema_with_slug_keys(
vol.Optional(CONF_SERVICE_URL): cv.string, cv.string
vol.Optional(CONF_MUTABLE, default=True): cv.boolean, ), # ignored, users can modify names of entities in the UI
vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, vol.Optional(CONF_RESOURCES): vol.All(
} cv.ensure_list, [vol.In(RESOURCES)]
), # ignored, users can disable entities in the UI
vol.Optional(CONF_REGION): cv.string,
vol.Optional(CONF_SERVICE_URL): cv.string,
vol.Optional(CONF_MUTABLE, default=True): cv.boolean,
vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean,
}
),
) )
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
@ -128,34 +135,70 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
region=config[DOMAIN].get(CONF_REGION), region=config[DOMAIN].get(CONF_REGION),
) )
interval = config[DOMAIN][CONF_SCAN_INTERVAL] hass.data[DATA_KEY] = {}
data = hass.data[DATA_KEY] = VolvoData(config) volvo_data = VolvoData(hass, connection, config)
def is_enabled(attr): hass.data[DATA_KEY] = VolvoUpdateCoordinator(hass, volvo_data)
"""Return true if the user has enabled the resource."""
return attr in config[DOMAIN].get(CONF_RESOURCES, [attr])
def discover_vehicle(vehicle): return await volvo_data.update()
class VolvoData:
"""Hold component state."""
def __init__(
self,
hass: HomeAssistant,
connection: Connection,
config: ConfigType,
) -> None:
"""Initialize the component state."""
self.hass = hass
self.vehicles: set[str] = set()
self.instruments: set[Instrument] = set()
self.config = config
self.connection = connection
def instrument(self, vin, component, attr, slug_attr):
"""Return corresponding instrument."""
return next(
instrument
for instrument in self.instruments
if instrument.vehicle.vin == vin
and instrument.component == component
and instrument.attr == attr
and instrument.slug_attr == slug_attr
)
def vehicle_name(self, vehicle):
"""Provide a friendly name for a vehicle."""
if vehicle.registration_number and vehicle.registration_number != "UNKNOWN":
return vehicle.registration_number
if vehicle.vin:
return vehicle.vin
return "Volvo"
def discover_vehicle(self, vehicle):
"""Load relevant platforms.""" """Load relevant platforms."""
data.vehicles.add(vehicle.vin) self.vehicles.add(vehicle.vin)
dashboard = vehicle.dashboard( dashboard = vehicle.dashboard(
mutable=config[DOMAIN][CONF_MUTABLE], mutable=self.config[DOMAIN][CONF_MUTABLE],
scandinavian_miles=config[DOMAIN][CONF_SCANDINAVIAN_MILES], scandinavian_miles=self.config[DOMAIN][CONF_SCANDINAVIAN_MILES],
) )
for instrument in ( for instrument in (
instrument instrument
for instrument in dashboard.instruments for instrument in dashboard.instruments
if instrument.component in PLATFORMS and is_enabled(instrument.slug_attr) if instrument.component in PLATFORMS
): ):
data.instruments.add(instrument) self.instruments.add(instrument)
hass.async_create_task( self.hass.async_create_task(
discovery.async_load_platform( discovery.async_load_platform(
hass, self.hass,
PLATFORMS[instrument.component], PLATFORMS[instrument.component],
DOMAIN, DOMAIN,
( (
@ -164,93 +207,71 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
instrument.attr, instrument.attr,
instrument.slug_attr, instrument.slug_attr,
), ),
config, self.config,
) )
) )
async def update(now): async def update(self):
"""Update status from the online service.""" """Update status from the online service."""
try: if not await self.connection.update(journal=True):
if not await connection.update(journal=True): return False
_LOGGER.warning("Could not query server")
return False
for vehicle in connection.vehicles: for vehicle in self.connection.vehicles:
if vehicle.vin not in data.vehicles: if vehicle.vin not in self.vehicles:
discover_vehicle(vehicle) self.discover_vehicle(vehicle)
async_dispatcher_send(hass, SIGNAL_STATE_UPDATED) # this is currently still needed for device_tracker, which isn't using the update coordinator yet
async_dispatcher_send(self.hass, SIGNAL_STATE_UPDATED)
return True return True
finally:
async_track_point_in_utc_time(hass, update, utcnow() + interval)
_LOGGER.info("Logging in to service")
return await update(utcnow())
class VolvoData: class VolvoUpdateCoordinator(DataUpdateCoordinator):
"""Hold component state.""" """Volvo coordinator."""
def __init__(self, config): def __init__(self, hass: HomeAssistant, volvo_data: VolvoData) -> None:
"""Initialize the component state.""" """Initialize the data update coordinator."""
self.vehicles = set()
self.instruments = set()
self.config = config[DOMAIN]
self.names = self.config.get(CONF_NAME)
def instrument(self, vin, component, attr, slug_attr): super().__init__(
"""Return corresponding instrument.""" hass,
return next( _LOGGER,
( name="volvooncall",
instrument update_interval=DEFAULT_UPDATE_INTERVAL,
for instrument in self.instruments
if instrument.vehicle.vin == vin
and instrument.component == component
and instrument.attr == attr
and instrument.slug_attr == slug_attr
),
None,
) )
def vehicle_name(self, vehicle): self.volvo_data = volvo_data
"""Provide a friendly name for a vehicle."""
if ( async def _async_update_data(self):
vehicle.registration_number and vehicle.registration_number.lower() """Fetch data from API endpoint."""
) in self.names:
return self.names[vehicle.registration_number.lower()] async with async_timeout.timeout(10):
if vehicle.vin and vehicle.vin.lower() in self.names: if not await self.volvo_data.update():
return self.names[vehicle.vin.lower()] raise UpdateFailed("Error communicating with API")
if vehicle.registration_number:
return vehicle.registration_number
if vehicle.vin:
return vehicle.vin
return ""
class VolvoEntity(Entity): class VolvoEntity(CoordinatorEntity):
"""Base class for all VOC entities.""" """Base class for all VOC entities."""
def __init__(self, data, vin, component, attribute, slug_attr): def __init__(
self,
vin: str,
component: str,
attribute: str,
slug_attr: str,
coordinator: VolvoUpdateCoordinator,
) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self.data = data super().__init__(coordinator)
self.vin = vin self.vin = vin
self.component = component self.component = component
self.attribute = attribute self.attribute = attribute
self.slug_attr = slug_attr self.slug_attr = slug_attr
async def async_added_to_hass(self):
"""Register update dispatcher."""
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_STATE_UPDATED, self.async_write_ha_state
)
)
@property @property
def instrument(self): def instrument(self):
"""Return corresponding instrument.""" """Return corresponding instrument."""
return self.data.instrument( return self.coordinator.volvo_data.instrument(
self.vin, self.component, self.attribute, self.slug_attr self.vin, self.component, self.attribute, self.slug_attr
) )
@ -270,18 +291,13 @@ class VolvoEntity(Entity):
@property @property
def _vehicle_name(self): def _vehicle_name(self):
return self.data.vehicle_name(self.vehicle) return self.coordinator.volvo_data.vehicle_name(self.vehicle)
@property @property
def name(self): def name(self):
"""Return full name of the entity.""" """Return full name of the entity."""
return f"{self._vehicle_name} {self._entity_name}" return f"{self._vehicle_name} {self._entity_name}"
@property
def should_poll(self):
"""Return the polling state."""
return False
@property @property
def assumed_state(self): def assumed_state(self):
"""Return true if unable to access real state of entity.""" """Return true if unable to access real state of entity."""

View File

@ -1,12 +1,19 @@
"""Support for VOC.""" """Support for VOC."""
from __future__ import annotations from __future__ import annotations
from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity from contextlib import suppress
import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA,
BinarySensorEntity,
)
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 homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DATA_KEY, VolvoEntity from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator
async def async_setup_platform( async def async_setup_platform(
@ -24,16 +31,25 @@ async def async_setup_platform(
class VolvoSensor(VolvoEntity, BinarySensorEntity): class VolvoSensor(VolvoEntity, BinarySensorEntity):
"""Representation of a Volvo sensor.""" """Representation of a Volvo sensor."""
def __init__(
self,
coordinator: VolvoUpdateCoordinator,
vin: str,
component: str,
attribute: str,
slug_attr: str,
) -> None:
"""Initialize the sensor."""
super().__init__(vin, component, attribute, slug_attr, coordinator)
with suppress(vol.Invalid):
self._attr_device_class = DEVICE_CLASSES_SCHEMA(
self.instrument.device_class
)
@property @property
def is_on(self): def is_on(self) -> bool | None:
"""Return True if the binary sensor is on, but invert for the 'Door lock'.""" """Fetch from update coordinator."""
if self.instrument.attr == "is_locked": if self.instrument.attr == "is_locked":
return not self.instrument.is_on return not self.instrument.is_on
return self.instrument.is_on return self.instrument.is_on
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
if self.instrument.device_class in DEVICE_CLASSES:
return self.instrument.device_class
return None

View File

@ -7,7 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify from homeassistant.util import slugify
from . import DATA_KEY, SIGNAL_STATE_UPDATED from . import DATA_KEY, SIGNAL_STATE_UPDATED, VolvoUpdateCoordinator
async def async_setup_scanner( async def async_setup_scanner(
@ -21,8 +21,12 @@ async def async_setup_scanner(
return False return False
vin, component, attr, slug_attr = discovery_info vin, component, attr, slug_attr = discovery_info
data = hass.data[DATA_KEY] coordinator: VolvoUpdateCoordinator = hass.data[DATA_KEY]
instrument = data.instrument(vin, component, attr, slug_attr) volvo_data = coordinator.volvo_data
instrument = volvo_data.instrument(vin, component, attr, slug_attr)
if instrument is None:
return False
async def see_vehicle() -> None: async def see_vehicle() -> None:
"""Handle the reporting of the vehicle position.""" """Handle the reporting of the vehicle position."""

View File

@ -1,4 +1,5 @@
"""Support for Volvo On Call locks.""" """Support for Volvo On Call locks."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
@ -10,7 +11,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 ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DATA_KEY, VolvoEntity from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator
async def async_setup_platform( async def async_setup_platform(
@ -31,15 +32,28 @@ class VolvoLock(VolvoEntity, LockEntity):
instrument: Lock instrument: Lock
def __init__(
self,
coordinator: VolvoUpdateCoordinator,
vin: str,
component: str,
attribute: str,
slug_attr: str,
) -> None:
"""Initialize the lock."""
super().__init__(vin, component, attribute, slug_attr, coordinator)
@property @property
def is_locked(self) -> bool | None: def is_locked(self) -> bool | None:
"""Return true if lock is locked.""" """Determine if car is locked."""
return self.instrument.is_locked return self.instrument.is_locked
async def async_lock(self, **kwargs: Any) -> None: async def async_lock(self, **kwargs: Any) -> None:
"""Lock the car.""" """Lock the car."""
await self.instrument.lock() await self.instrument.lock()
await self.coordinator.async_request_refresh()
async def async_unlock(self, **kwargs: Any) -> None: async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the car.""" """Unlock the car."""
await self.instrument.unlock() await self.instrument.unlock()
await self.coordinator.async_request_refresh()

View File

@ -2,11 +2,11 @@
from __future__ import annotations from __future__ import annotations
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DATA_KEY, VolvoEntity from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator
async def async_setup_platform( async def async_setup_platform(
@ -24,12 +24,24 @@ async def async_setup_platform(
class VolvoSensor(VolvoEntity, SensorEntity): class VolvoSensor(VolvoEntity, SensorEntity):
"""Representation of a Volvo sensor.""" """Representation of a Volvo sensor."""
@property def __init__(
def native_value(self): self,
"""Return the state.""" coordinator: VolvoUpdateCoordinator,
return self.instrument.state vin: str,
component: str,
attribute: str,
slug_attr: str,
) -> None:
"""Initialize the sensor."""
super().__init__(vin, component, attribute, slug_attr, coordinator)
self._update_value_and_unit()
@property def _update_value_and_unit(self) -> None:
def native_unit_of_measurement(self): self._attr_native_value = self.instrument.state
"""Return the unit of measurement.""" self._attr_native_unit_of_measurement = self.instrument.unit
return self.instrument.unit
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_value_and_unit()
self.async_write_ha_state()

View File

@ -6,7 +6,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 ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DATA_KEY, VolvoEntity from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator
async def async_setup_platform( async def async_setup_platform(
@ -24,17 +24,28 @@ async def async_setup_platform(
class VolvoSwitch(VolvoEntity, SwitchEntity): class VolvoSwitch(VolvoEntity, SwitchEntity):
"""Representation of a Volvo switch.""" """Representation of a Volvo switch."""
def __init__(
self,
coordinator: VolvoUpdateCoordinator,
vin: str,
component: str,
attribute: str,
slug_attr: str,
) -> None:
"""Initialize the switch."""
super().__init__(vin, component, attribute, slug_attr, coordinator)
@property @property
def is_on(self): def is_on(self):
"""Return true if switch is on.""" """Determine if switch is on."""
return self.instrument.state return self.instrument.state
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the switch on.""" """Turn the switch on."""
await self.instrument.turn_on() await self.instrument.turn_on()
self.async_write_ha_state() await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn the switch off.""" """Turn the switch off."""
await self.instrument.turn_off() await self.instrument.turn_off()
self.async_write_ha_state() await self.coordinator.async_request_refresh()