From d0294728b84560a8e4d1619ac2734a5100228f00 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Tue, 26 Mar 2024 10:34:10 -0500 Subject: [PATCH] Reload config only when necessary (part 4) --- custom_components/sun2/binary_sensor.py | 45 ++--- custom_components/sun2/helpers.py | 224 ++++++++---------------- custom_components/sun2/sensor.py | 29 ++- 3 files changed, 104 insertions(+), 194 deletions(-) diff --git a/custom_components/sun2/binary_sensor.py b/custom_components/sun2/binary_sensor.py index b9c9e31..e547b2c 100644 --- a/custom_components/sun2/binary_sensor.py +++ b/custom_components/sun2/binary_sensor.py @@ -1,6 +1,7 @@ """Sun2 Binary Sensor.""" from __future__ import annotations +from collections.abc import Iterable from datetime import datetime from typing import cast @@ -8,15 +9,13 @@ BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_ELEVATION, CONF_NAME, CONF_UNIQUE_ID, ) -from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.core import CoreState, callback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -228,6 +227,20 @@ def schedule_update(now: datetime) -> None: class Sun2BinarySensorEntrySetup(Sun2EntrySetup): """Binary sensor config entry setup.""" + def _get_entities(self) -> Iterable[Sun2Entity]: + """Return entities to add.""" + for config in self._entry.options.get(CONF_BINARY_SENSORS, []): + unique_id = config[CONF_UNIQUE_ID] + if self._imported: + unique_id = self._uid_prefix + unique_id + self._sun2_entity_params.unique_id = unique_id + threshold = config[CONF_ELEVATION] + yield Sun2ElevationSensor( + self._sun2_entity_params, + self._elevation_name(config.get(CONF_NAME), threshold), + threshold, + ) + def _elevation_name(self, name: str | None, threshold: float | str) -> str: """Return elevation sensor name.""" if name: @@ -240,29 +253,5 @@ def _elevation_name(self, name: str | None, threshold: float | str) -> str: ) return translate(self._hass, "above_pos_elev", {"elevation": str(threshold)}) - def _sensors(self) -> list[Sun2Entity]: - """Return list of entities to add.""" - sensors: list[Sun2Entity] = [] - for config in self._entry.options.get(CONF_BINARY_SENSORS, []): - unique_id = config[CONF_UNIQUE_ID] - if self._imported: - unique_id = self._uid_prefix + unique_id - self._sun2_entity_params.unique_id = unique_id - threshold = config[CONF_ELEVATION] - sensors.append( - Sun2ElevationSensor( - self._sun2_entity_params, - self._elevation_name(config.get(CONF_NAME), threshold), - threshold, - ) - ) - return sensors - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up config entry.""" - await Sun2BinarySensorEntrySetup(hass, entry, async_add_entities)() +async_setup_entry = Sun2BinarySensorEntrySetup.async_setup_entry diff --git a/custom_components/sun2/helpers.py b/custom_components/sun2/helpers.py index fa08488..cccbd19 100644 --- a/custom_components/sun2/helpers.py +++ b/custom_components/sun2/helpers.py @@ -1,8 +1,8 @@ """Sun2 Helpers.""" from __future__ import annotations -from abc import abstractmethod -from collections.abc import Callable, Coroutine, Iterable, Mapping +from abc import ABC, abstractmethod +from collections.abc import Callable, Iterable, Mapping from dataclasses import dataclass, field from datetime import date, datetime, time, timedelta, tzinfo from functools import cached_property, lru_cache @@ -33,7 +33,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.translation import async_get_translations -from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from .const import ( @@ -322,14 +321,6 @@ async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" self._setup_fixed_updating() - def request_astral_data_update(self, astral_data: AstralData) -> None: - """Request update of astral data.""" - cast(ConfigEntry, self.platform.config_entry).async_create_task( - self.hass, - self._update_astral_data_atomic(astral_data), - f"{self.name}: update astral data", - ) - def _cancel_update(self) -> None: """Cancel update.""" if self._unsub_update: @@ -341,17 +332,17 @@ def _update(self, cur_dttm: datetime) -> None: """Update state.""" def _setup_fixed_updating(self) -> None: - """Set up fixed updating.""" + """Set up fixed updating. - async def _update_astral_data_atomic(self, astral_data: AstralData) -> None: - """Update astral data atomically.""" + None by default. Override in subclass if needed. + """ - async def do_update_astral_data() -> None: - """Update astral data.""" - self._update_astral_data(astral_data) + async def update_astral_data(self, astral_data: AstralData) -> None: + """Update astral data. - await self.async_request_call(do_update_astral_data()) - self.async_schedule_update_ha_state(True) + Should be called via Entity.async_request_call. + """ + self._update_astral_data(astral_data) def _update_astral_data(self, astral_data: AstralData) -> None: """Update astral data.""" @@ -394,105 +385,10 @@ def _astral_event( return None -def make_async_setup_entry( - sensors: Callable[ - [HomeAssistant, bool, str, Sun2EntityParams, Iterable[ConfigType | str]], - list[Sun2Entity], - ], - sensor_configs: Callable[[ConfigEntry], Iterable[ConfigType | str]], -) -> Callable[ - [HomeAssistant, ConfigEntry, AddEntitiesCallback], Coroutine[Any, Any, None] -]: - """Make async_setup_entry function.""" - - async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, - ) -> None: - """Set up the sensor platform.""" - s2data = sun2_data(hass) - imported = entry.source == SOURCE_IMPORT - uid_prefix = f"{entry.entry_id}-" - device_info = sun2_dev_info(hass, entry) - config_data = s2data.config_data[entry.entry_id] - if (loc_data := config_data.loc_data) is None: - loc_data = s2data.ha_loc_data - obs_elvs = config_data.obs_elvs - sun2_entity_params = Sun2EntityParams( - device_info, AstralData(loc_data, obs_elvs) - ) - - entities = sensors( - hass, imported, uid_prefix, sun2_entity_params, sensor_configs(entry) - ) - async_add_entities(entities, True) - - def update_entities( - loc_data_: LocData, obs_elvs_: ObsElvs | None = None - ) -> None: - """Update entities with new astral data.""" - nonlocal obs_elvs - - if obs_elvs_ is None: - obs_elvs_ = obs_elvs - else: - obs_elvs = obs_elvs_ - astral_data = AstralData(loc_data_, obs_elvs_) - for entity in entities: - entity.request_astral_data_update(astral_data) - - @callback - def ha_loc_updated() -> None: - """Handle new HA location configuration.""" - update_entities(s2data.ha_loc_data) - - remove_ha_loc_listener: Callable[[], None] | None = None - - def sub_ha_loc_updated() -> None: - """Subscribe to HA location updated signal.""" - nonlocal remove_ha_loc_listener - - if not remove_ha_loc_listener: - remove_ha_loc_listener = async_dispatcher_connect( - hass, SIG_HA_LOC_UPDATED, ha_loc_updated - ) - - def unsub_ha_loc_updated() -> None: - """Unsubscribe to HA location updated signal.""" - nonlocal remove_ha_loc_listener - - if remove_ha_loc_listener: - remove_ha_loc_listener() - remove_ha_loc_listener = None - - @callback - def astral_data_updated(loc_data: LocData | None, obs_elvs: ObsElvs) -> None: - """Handle new astral data.""" - if loc_data is None: - sub_ha_loc_updated() - loc_data = s2data.ha_loc_data - else: - unsub_ha_loc_updated() - update_entities(loc_data, obs_elvs) - - entry.async_on_unload(unsub_ha_loc_updated) - entry.async_on_unload( - async_dispatcher_connect( - hass, - SIG_ASTRAL_DATA_UPDATED.format(entry.entry_id), - astral_data_updated, - ) - ) - - return async_setup_entry - - -class Sun2EntrySetup: - """Config entry setup.""" +class Sun2EntrySetup(ABC): + """Platform config entry setup.""" _remove_ha_loc_listener: Callable[[], None] | None = None - _entities: list[Sun2Entity] def __init__( self, @@ -503,33 +399,25 @@ def __init__( """Initialize.""" self._hass = hass self._entry = entry - self._async_add_entities = async_add_entities entry.async_on_unload(self._unsub_ha_loc_updated) config_data = self._s2data.config_data[entry.entry_id] - if (loc_data := config_data.loc_data) is None: - loc_data = self._s2data.ha_loc_data + loc_data = config_data.loc_data + obs_elvs = config_data.obs_elvs + # These are available to _get_entities method defined in subclass. self._imported = entry.source == SOURCE_IMPORT self._uid_prefix = f"{entry.entry_id}-" - obs_elvs = config_data.obs_elvs self._sun2_entity_params = Sun2EntityParams( sun2_dev_info(hass, entry), - AstralData(loc_data, obs_elvs), + AstralData(self._new_loc_data(loc_data), obs_elvs), ) + self._entities = list(self._get_entities()) + async_add_entities(self._entities, True) self._obs_elvs = obs_elvs - @cached_property - def _s2data(self) -> Sun2Data: - """Return Sun2Data.""" - return sun2_data(self._hass) - - async def __call__(self) -> None: - """Set up config entry.""" - self._entities = self._sensors() - self._async_add_entities(self._entities, True) self._entry.async_on_unload( async_dispatcher_connect( self._hass, @@ -538,19 +426,10 @@ async def __call__(self) -> None: ) ) - @abstractmethod - def _sensors(self) -> list[Sun2Entity]: - """Return list of entities to add.""" - - @callback - def _astral_data_updated(self, loc_data: LocData | None, obs_elvs: ObsElvs) -> None: - """Handle new astral data.""" - if loc_data: - self._unsub_ha_loc_updated() - else: - self._sub_ha_loc_updated() - loc_data = self._s2data.ha_loc_data - self._update_entities(loc_data, obs_elvs) + @cached_property + def _s2data(self) -> Sun2Data: + """Return Sun2Data.""" + return sun2_data(self._hass) def _unsub_ha_loc_updated(self) -> None: """Unsubscribe to HA location updated signal.""" @@ -565,6 +444,31 @@ def _sub_ha_loc_updated(self) -> None: self._hass, SIG_HA_LOC_UPDATED, self._ha_loc_updated ) + def _new_loc_data(self, loc_data: LocData | None) -> LocData: + """Check new location data. + + None -> use HA's configured location. + """ + if loc_data: + self._unsub_ha_loc_updated() + return loc_data + self._sub_ha_loc_updated() + return self._s2data.ha_loc_data + + @abstractmethod + def _get_entities(self) -> Iterable[Sun2Entity]: + """Return entities to add.""" + + @callback + def _astral_data_updated(self, loc_data: LocData | None, obs_elvs: ObsElvs) -> None: + """Handle new astral data.""" + self._update_entities(self._new_loc_data(loc_data), obs_elvs) + + @callback + def _ha_loc_updated(self) -> None: + """Handle new HA location configuration.""" + self._update_entities(self._s2data.ha_loc_data) + def _update_entities( self, loc_data: LocData, obs_elvs: ObsElvs | None = None ) -> None: @@ -575,9 +479,33 @@ def _update_entities( self._obs_elvs = obs_elvs astral_data = AstralData(loc_data, obs_elvs) for entity in self._entities: - entity.request_astral_data_update(astral_data) + self._update_entity(entity, astral_data) - @callback - def _ha_loc_updated(self) -> None: - """Handle new HA location configuration.""" - self._update_entities(self._s2data.ha_loc_data) + def _update_entity(self, entity: Sun2Entity, astral_data: AstralData) -> None: + """Update entity with new astral data.""" + + async def update_entity() -> None: + """Update entity.""" + await entity.async_request_call(entity.update_astral_data(astral_data)) + await entity.async_update_ha_state(True) + + self._entry.async_create_task( + self._hass, update_entity(), f"Update astral data: {entity.name}" + ) + + @classmethod + async def async_setup_entry( + cls, + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Platform async_setup_entry function. + + class Sun2PlatformEntrySetup(Sun2EntrySetup): + def _get_entities(self) -> list[Sun2Entity]: + ... + + async_setup_entry = Sun2PlatformEntrySetup.async_setup_entry + """ + cls(hass, entry, async_add_entities) diff --git a/custom_components/sun2/sensor.py b/custom_components/sun2/sensor.py index ac2f3b2..9ec2027 100644 --- a/custom_components/sun2/sensor.py +++ b/custom_components/sun2/sensor.py @@ -2,10 +2,11 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Generator, Mapping, MutableMapping, Sequence +from collections.abc import Iterable, Mapping, MutableMapping, Sequence from contextlib import suppress from dataclasses import dataclass from datetime import date, datetime, time, timedelta +from itertools import chain from math import ceil, floor from typing import Any, Generic, Optional, TypeVar, Union, cast @@ -19,7 +20,6 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ICON, CONF_ICON, @@ -31,9 +31,8 @@ EVENT_STATE_CHANGED, UnitOfTime, ) -from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, CoreState, Event, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_call_later, async_track_point_in_utc_time, @@ -1168,20 +1167,20 @@ class SensorParams: class Sun2SensorEntrySetup(Sun2EntrySetup): """Binary sensor config entry setup.""" - def _sensors(self) -> list[Sun2Entity]: - """Return list of entities to add.""" - return list(self._basic_sensors()) + list(self._config_sensors()) + def _get_entities(self) -> Iterable[Sun2Entity]: + """Return entities to add.""" + return chain(self._basic_sensors(), self._config_sensors()) - def _basic_sensors(self) -> Generator[Sun2Entity, None, None]: - """Return list of basic entities to add.""" + def _basic_sensors(self) -> Iterable[Sun2Entity]: + """Return basic entities to add.""" for sensor_type, sensor_params in _SENSOR_TYPES.items(): self._sun2_entity_params.unique_id = self._uid_prefix + sensor_type yield sensor_params.cls( self._sun2_entity_params, sensor_type, sensor_params.icon ) - def _config_sensors(self) -> Generator[Sun2Entity, None, None]: - """Return list of configured entities to add.""" + def _config_sensors(self) -> Iterable[Sun2Entity]: + """Return configured entities to add.""" for config in self._entry.options.get(CONF_SENSORS, []): unique_id = config[CONF_UNIQUE_ID] if self._imported: @@ -1236,10 +1235,4 @@ def _time_at_elevation_name( ) -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up config entry.""" - await Sun2SensorEntrySetup(hass, entry, async_add_entities)() +async_setup_entry = Sun2SensorEntrySetup.async_setup_entry