diff --git a/custom_components/aiseg2/coordinator.py b/custom_components/aiseg2/coordinator.py deleted file mode 100644 index 4540fab..0000000 --- a/custom_components/aiseg2/coordinator.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Aiseg.""" - -from asyncio import timeout -from datetime import timedelta -import logging - -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed - -from .aiseg_api import ( - AisegAPI, - AisegDevice, - AisegEntityType, - AisegSensor, - ApiAuthError, - ApiError, - ScrapingError, -) -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class DataContainer: - """Utiliy container to manage integration data.""" - - def __init__(self, data: list[AisegSensor]) -> None: - """Initialize.""" - self.map: dict[str, AisegSensor] = {} - for item in data: - self.map[item.getKey()] = item - - def get(self, key: str) -> AisegSensor: - """Get Entity with key.""" - try: - return self.map.get(key) - except KeyError: - return None - - def set(self, item: AisegSensor) -> None: - """Set Entity to container.""" - self.map[item.getKey()] = item - - def getByType(self, entity_type: AisegEntityType) -> list[AisegSensor]: - """Get all entity with the given type.""" - return filter(lambda item: item.type == entity_type, self.map.values()) - - def __eq__(self, other: object): - """Check equality.""" - if not isinstance(other, DataContainer): - return NotImplemented - if len(self.map.keys()) != len(other.map.keys()): - return False - for key in self.map: - if other.get(key).getValue() != self.get(key).getValue(): - return False - return True - - def __len__(self): - """Get number of entity in the container.""" - return len(self.map.keys()) - - -class AisegPoolingCoordinator(DataUpdateCoordinator[DataContainer]): - """My custom coordinator.""" - - def __init__( - self, hass: HomeAssistant, my_api: AisegAPI, update_interval: int = 30 - ) -> None: - """Initialize my coordinator.""" - super().__init__( - hass, - _LOGGER, - # Name of the data. For logging purposes. - name="Aiseg energy", - # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=update_interval), - # Set always_update to `False` if the data returned from the - # api can be compared via `__eq__` to avoid duplicate updates - # being dispatched to listeners - always_update=False, - ) - self.my_api = my_api - self._device: AisegDevice | None = None - - def getDeviceInfo(self): - """Get device information to link entities.""" - if self._device is not None: - return { - "name": self._device.name, - "identifiers": {(DOMAIN, self._device.device_id)}, - "manufacturer": self._device.manufacturer, - } - return {} - - async def _async_setup(self): - """Set up the coordinator. - - This is the place to set up your coordinator, - or to load data, that only needs to be loaded once. - - This method will be called automatically during - coordinator.async_config_entry_first_refresh. - """ - self._device = await self.my_api.get_device() - - async def _async_update_data(self): - """Fetch data from API endpoint. - - This is the place to pre-process the data to lookup tables - so entities can quickly look up their data. - """ - try: - # Note: asyncio.TimeoutError and aiohttp.ClientError are already - # handled by the data update coordinator. - async with timeout(10): - # Grab active context variables to limit data required to be fetched from API - # Note: using context is not required if there is no need or ability to limit - # data retrieved from API. - listening_idx = set(self.async_contexts()) - if self.data is None or len(self.data) == 0: - data = await self.my_api.fetch_data() - return DataContainer(data) - for key in listening_idx: - await self.data.get(key).update() - return self.data - - except ApiAuthError as err: - # Raising ConfigEntryAuthFailed will cancel future updates - # and start a config flow with SOURCE_REAUTH (async_step_reauth) - raise ConfigEntryAuthFailed from err - except ApiError as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err - except ScrapingError as err: - raise UpdateFailed("Err while parsing API response") from err - except Exception as err: - raise UpdateFailed("Err updating sensor") from err diff --git a/custom_components/aiseg2/manifest.json b/custom_components/aiseg2/manifest.json index b67f703..e015860 100644 --- a/custom_components/aiseg2/manifest.json +++ b/custom_components/aiseg2/manifest.json @@ -11,5 +11,5 @@ "requirements": ["lxml"], "ssdp": [], "zeroconf": [], - "version": "0.0.1" + "version": "0.1.0" } diff --git a/custom_components/aiseg2/sensor.py b/custom_components/aiseg2/sensor.py index f59fa05..34b298e 100644 --- a/custom_components/aiseg2/sensor.py +++ b/custom_components/aiseg2/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timedelta from homeassistant.components.sensor import ( SensorDeviceClass, @@ -10,14 +10,15 @@ SensorStateClass, ) from homeassistant.const import UnitOfEnergy, UnitOfPower -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import async_get_time_zone from . import AisegConfigEntry -from .aiseg_api import AisegEntityType -from .coordinator import AisegPoolingCoordinator +from .aiseg_api import AisegEnergySensor, AisegEntityType, AisegPowerSensor +from .const import DOMAIN + +SCAN_INTERVAL = timedelta(seconds=30) async def async_setup_entry( @@ -27,7 +28,6 @@ async def async_setup_entry( ) -> None: """Config entry example.""" my_api = entry.runtime_data - coordinator = AisegPoolingCoordinator(hass, my_api) # Fetch initial data so we have data when entities subscribe # @@ -38,21 +38,30 @@ async def async_setup_entry( # coordinator.async_refresh() instead # tz = await async_get_time_zone(hass.config.time_zone) - await coordinator.async_config_entry_first_refresh() - device_info = coordinator.getDeviceInfo() + data = await my_api.fetch_data() + device = await my_api.get_device() + if device is not None: + device_info = { + "name": device.name, + "identifiers": {(DOMAIN, device.device_id)}, + "manufacturer": device.manufacturer, + } + else: + device_info = {} + energy_entities = [ - EnergySensor(coordinator, item.getKey(), item.getValue(), device_info, tz) - for item in coordinator.data.getByType(AisegEntityType.ENERGY) + EnergySensor(item, item.getKey(), item.getValue(), device_info, tz) + for item in filter(lambda datum: datum.type == AisegEntityType.ENERGY, data) ] power_entities = [ - PowerSensor(coordinator, item.getKey(), item.getValue(), device_info) - for item in coordinator.data.getByType(AisegEntityType.POWER) + PowerSensor(item, item.getKey(), item.getValue(), device_info) + for item in filter(lambda datum: datum.type == AisegEntityType.POWER, data) ] async_add_entities(energy_entities) async_add_entities(power_entities) -class PowerSensor(CoordinatorEntity, SensorEntity): +class PowerSensor(SensorEntity): """Representation of a Sensor.""" _attr_name = "Power" @@ -62,11 +71,11 @@ class PowerSensor(CoordinatorEntity, SensorEntity): _attr_native_value = 0 def __init__( - self, coordinator: AisegPoolingCoordinator, idx, initial_value, device_info + self, sensor: AisegPowerSensor, idx, initial_value, device_info ) -> None: """Pass coordinator to CoordinatorEntity.""" - super().__init__(coordinator, context=idx) self.idx = idx + self.sensor = sensor self._attr_unique_id = idx self._attr_device_info = device_info self._attr_name = idx @@ -76,15 +85,12 @@ def __init__( def translation_key(self): return self._attr_name - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - item = self.coordinator.data.get(self.idx) - self._attr_native_value = item.getValue() - self.async_write_ha_state() + async def async_update(self) -> None: + """Update sensor value.""" + self._attr_native_value = await self.sensor.update() -class EnergySensor(CoordinatorEntity, SensorEntity): +class EnergySensor(SensorEntity): """Representation of a Sensor.""" _attr_name = "Energy" @@ -101,12 +107,12 @@ def _get_today_start_time(self): ) def __init__( - self, coordinator: AisegPoolingCoordinator, idx, initial_value, device_info, tz + self, sensor: AisegEnergySensor, idx, initial_value, device_info, tz ) -> None: """Pass coordinator to CoordinatorEntity.""" - super().__init__(coordinator, context=idx) self.tz = tz self.idx = idx + self.sensor = sensor self._attr_unique_id = idx self._attr_device_info = device_info self._attr_name = idx @@ -117,10 +123,7 @@ def __init__( def translation_key(self): return self._attr_name - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - item = self.coordinator.data.get(self.idx) - self._attr_native_value = item.getValue() + async def async_update(self) -> None: + """Update sensor value.""" + self._attr_native_value = await self.sensor.update() self._attr_last_reset = self._get_today_start_time() - self.async_write_ha_state() diff --git a/custom_components/aiseg2/switch.py b/custom_components/aiseg2/switch.py index 4f0d8f1..bc4e178 100644 --- a/custom_components/aiseg2/switch.py +++ b/custom_components/aiseg2/switch.py @@ -1,13 +1,16 @@ """Platform for switch integration.""" +from datetime import timedelta + from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import AisegConfigEntry -from .aiseg_api import AisegEntityType -from .coordinator import AisegPoolingCoordinator +from .aiseg_api import AisegEntityType, AisegSwitch +from .const import DOMAIN + +SCAN_INTERVAL = timedelta(seconds=10) async def async_setup_entry( @@ -17,7 +20,6 @@ async def async_setup_entry( ) -> None: """Config entry example.""" my_api = entry.runtime_data - coordinator = AisegPoolingCoordinator(hass, my_api, update_interval=10) # Fetch initial data so we have data when entities subscribe # @@ -27,39 +29,41 @@ async def async_setup_entry( # If you do not want to retry setup on failure, use # coordinator.async_refresh() instead # - await coordinator.async_config_entry_first_refresh() - device_info = coordinator.getDeviceInfo() + data = await my_api.fetch_data() + device = await my_api.get_device() + if device is not None: + device_info = { + "name": device.name, + "identifiers": {(DOMAIN, device.device_id)}, + "manufacturer": device.manufacturer, + } + else: + device_info = {} + switch_entities = [ - NotificationEnableSwitch( - coordinator, item.getKey(), item.getValue(), device_info - ) - for item in coordinator.data.getByType(AisegEntityType.SWITCH) + NotificationEnableSwitch(item, item.getKey(), item.getValue(), device_info) + for item in filter(lambda datum: datum.type == AisegEntityType.SWITCH, data) ] async_add_entities(switch_entities) -class NotificationEnableSwitch(CoordinatorEntity, SwitchEntity): +class NotificationEnableSwitch(SwitchEntity): """Entity to manipulate AiSEG config switch.""" __attr_name = "notification_enabled" __attr_device_class = SwitchDeviceClass.SWITCH - def __init__( - self, coordinator: AisegPoolingCoordinator, idx, initial_value, device_info - ) -> None: + def __init__(self, switch: AisegSwitch, idx, initial_value, device_info) -> None: """Initialize.""" - super().__init__(coordinator, context=idx) self.idx = idx + self.switch = switch self._attr_unique_id = idx self._attr_name = idx self.is_on = initial_value self._attr_device_info = device_info self.is_on = False - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - item = self.coordinator.data.get(self.idx) - self.is_on = item.getValue() - self.async_write_ha_state() + async def async_update(self): + """Update switch state.""" + self.is_on = self.switch.getValue()