From e9f01be09031bca1cf278800386537f50eca4ced Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jun 2024 10:51:28 +0200 Subject: [PATCH] Add coordinator to Aladdin Connect (#118781) --- .../components/aladdin_connect/__init__.py | 12 ++- .../components/aladdin_connect/coordinator.py | 38 ++++++++++ .../components/aladdin_connect/cover.py | 73 +++++++------------ .../components/aladdin_connect/entity.py | 27 +++++++ .../components/aladdin_connect/sensor.py | 50 +++++-------- 5 files changed, 118 insertions(+), 82 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/coordinator.py create mode 100644 homeassistant/components/aladdin_connect/entity.py diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index dcd26c6cd04..6317cf8358e 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from genie_partner_sdk.client import AladdinConnectClient + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -12,10 +14,11 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from .api import AsyncConfigEntryAuth +from .coordinator import AladdinConnectCoordinator PLATFORMS: list[Platform] = [Platform.COVER] -type AladdinConnectConfigEntry = ConfigEntry[AsyncConfigEntryAuth] +type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator] async def async_setup_entry( @@ -25,8 +28,13 @@ async def async_setup_entry( implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) + auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session) + coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth)) - entry.runtime_data = AsyncConfigEntryAuth(async_get_clientsession(hass), session) + await coordinator.async_setup() + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/aladdin_connect/coordinator.py b/homeassistant/components/aladdin_connect/coordinator.py new file mode 100644 index 00000000000..d9af0da9450 --- /dev/null +++ b/homeassistant/components/aladdin_connect/coordinator.py @@ -0,0 +1,38 @@ +"""Define an object to coordinate fetching Aladdin Connect data.""" + +from datetime import timedelta +import logging + +from genie_partner_sdk.client import AladdinConnectClient +from genie_partner_sdk.model import GarageDoor + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class AladdinConnectCoordinator(DataUpdateCoordinator[None]): + """Aladdin Connect Data Update Coordinator.""" + + def __init__(self, hass: HomeAssistant, acc: AladdinConnectClient) -> None: + """Initialize.""" + super().__init__( + hass, + logger=_LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=15), + ) + self.acc = acc + self.doors: list[GarageDoor] = [] + + async def async_setup(self) -> None: + """Fetch initial data.""" + self.doors = await self.acc.get_doors() + + async def _async_update_data(self) -> None: + """Fetch data from API endpoint.""" + for door in self.doors: + await self.acc.update_door(door.device_id, door.door_number) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 54f0ab32db9..29629593c75 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,9 +1,7 @@ """Cover Entity for Genie Garage Door.""" -from datetime import timedelta from typing import Any -from genie_partner_sdk.client import AladdinConnectClient from genie_partner_sdk.model import GarageDoor from homeassistant.components.cover import ( @@ -11,52 +9,36 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import api +from . import AladdinConnectConfigEntry, AladdinConnectCoordinator from .const import DOMAIN - -SCAN_INTERVAL = timedelta(seconds=15) +from .entity import AladdinConnectEntity async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AladdinConnectConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Aladdin Connect platform.""" - session: api.AsyncConfigEntryAuth = config_entry.runtime_data - acc = AladdinConnectClient(session) - doors = await acc.get_doors() - if doors is None: - raise PlatformNotReady("Error from Aladdin Connect getting doors") - device_registry = dr.async_get(hass) - doors_to_add = [] - for door in doors: - existing = device_registry.async_get(door.unique_id) - if existing is None: - doors_to_add.append(door) + coordinator = config_entry.runtime_data - async_add_entities( - (AladdinDevice(acc, door, config_entry) for door in doors_to_add), - ) - remove_stale_devices(hass, config_entry, doors) + async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors) + remove_stale_devices(hass, config_entry) def remove_stale_devices( - hass: HomeAssistant, config_entry: ConfigEntry, devices: list[GarageDoor] + hass: HomeAssistant, config_entry: AladdinConnectConfigEntry ) -> None: """Remove stale devices from device registry.""" device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ) - all_device_ids = {door.unique_id for door in devices} + all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors} for device_entry in device_entries: device_id: str | None = None @@ -75,45 +57,38 @@ def remove_stale_devices( ) -class AladdinDevice(CoverEntity): +class AladdinDevice(AladdinConnectEntity, CoverEntity): """Representation of Aladdin Connect cover.""" _attr_device_class = CoverDeviceClass.GARAGE _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - _attr_has_entity_name = True _attr_name = None def __init__( - self, acc: AladdinConnectClient, device: GarageDoor, entry: ConfigEntry + self, coordinator: AladdinConnectCoordinator, device: GarageDoor ) -> None: """Initialize the Aladdin Connect cover.""" - self._acc = acc - self._device_id = device.device_id - self._number = device.door_number - - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device.unique_id)}, - name=device.name, - manufacturer="Overhead Door", - ) + super().__init__(coordinator, device) self._attr_unique_id = device.unique_id async def async_open_cover(self, **kwargs: Any) -> None: """Issue open command to cover.""" - await self._acc.open_door(self._device_id, self._number) + await self.coordinator.acc.open_door( + self._device.device_id, self._device.door_number + ) async def async_close_cover(self, **kwargs: Any) -> None: """Issue close command to cover.""" - await self._acc.close_door(self._device_id, self._number) - - async def async_update(self) -> None: - """Update status of cover.""" - await self._acc.update_door(self._device_id, self._number) + await self.coordinator.acc.close_door( + self._device.device_id, self._device.door_number + ) @property def is_closed(self) -> bool | None: """Update is closed attribute.""" - value = self._acc.get_door_status(self._device_id, self._number) + value = self.coordinator.acc.get_door_status( + self._device.device_id, self._device.door_number + ) if value is None: return None return bool(value == "closed") @@ -121,7 +96,9 @@ class AladdinDevice(CoverEntity): @property def is_closing(self) -> bool | None: """Update is closing attribute.""" - value = self._acc.get_door_status(self._device_id, self._number) + value = self.coordinator.acc.get_door_status( + self._device.device_id, self._device.door_number + ) if value is None: return None return bool(value == "closing") @@ -129,7 +106,9 @@ class AladdinDevice(CoverEntity): @property def is_opening(self) -> bool | None: """Update is opening attribute.""" - value = self._acc.get_door_status(self._device_id, self._number) + value = self.coordinator.acc.get_door_status( + self._device.device_id, self._device.door_number + ) if value is None: return None return bool(value == "opening") diff --git a/homeassistant/components/aladdin_connect/entity.py b/homeassistant/components/aladdin_connect/entity.py new file mode 100644 index 00000000000..8d9eeefcdfb --- /dev/null +++ b/homeassistant/components/aladdin_connect/entity.py @@ -0,0 +1,27 @@ +"""Defines a base Aladdin Connect entity.""" + +from genie_partner_sdk.model import GarageDoor + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import AladdinConnectCoordinator + + +class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]): + """Defines a base Aladdin Connect entity.""" + + _attr_has_entity_name = True + + def __init__( + self, coordinator: AladdinConnectCoordinator, device: GarageDoor + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._device = device + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.unique_id)}, + name=device.name, + manufacturer="Overhead Door", + ) diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index f9ed2a6aeeb..2bd0168a500 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import cast from genie_partner_sdk.client import AladdinConnectClient from genie_partner_sdk.model import GarageDoor @@ -15,21 +14,19 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import api -from .const import DOMAIN +from . import AladdinConnectConfigEntry, AladdinConnectCoordinator +from .entity import AladdinConnectEntity @dataclass(frozen=True, kw_only=True) class AccSensorEntityDescription(SensorEntityDescription): """Describes AladdinConnect sensor entity.""" - value_fn: Callable + value_fn: Callable[[AladdinConnectClient, str, int], float | None] SENSORS: tuple[AccSensorEntityDescription, ...] = ( @@ -45,52 +42,39 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = ( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: AladdinConnectConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Aladdin Connect sensor devices.""" + coordinator = entry.runtime_data - session: api.AsyncConfigEntryAuth = hass.data[DOMAIN][entry.entry_id] - acc = AladdinConnectClient(session) - - entities = [] - doors = await acc.get_doors() - - for door in doors: - entities.extend( - [AladdinConnectSensor(acc, door, description) for description in SENSORS] - ) - - async_add_entities(entities) + async_add_entities( + AladdinConnectSensor(coordinator, door, description) + for description in SENSORS + for door in coordinator.doors + ) -class AladdinConnectSensor(SensorEntity): +class AladdinConnectSensor(AladdinConnectEntity, SensorEntity): """A sensor implementation for Aladdin Connect devices.""" entity_description: AccSensorEntityDescription - _attr_has_entity_name = True def __init__( self, - acc: AladdinConnectClient, + coordinator: AladdinConnectCoordinator, device: GarageDoor, description: AccSensorEntityDescription, ) -> None: """Initialize a sensor for an Aladdin Connect device.""" - self._device_id = device.device_id - self._number = device.door_number - self._acc = acc + super().__init__(coordinator, device) self.entity_description = description self._attr_unique_id = f"{device.unique_id}-{description.key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device.unique_id)}, - name=device.name, - manufacturer="Overhead Door", - ) @property def native_value(self) -> float | None: """Return the state of the sensor.""" - return cast( - float, - self.entity_description.value_fn(self._acc, self._device_id, self._number), + return self.entity_description.value_fn( + self.coordinator.acc, self._device.device_id, self._device.door_number )