Skip to content

Commit

Permalink
Reload config only when necessary (part 4)
Browse files Browse the repository at this point in the history
  • Loading branch information
pnbruckner committed Mar 26, 2024
1 parent f2066b2 commit d029472
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 194 deletions.
45 changes: 17 additions & 28 deletions custom_components/sun2/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
"""Sun2 Binary Sensor."""
from __future__ import annotations

from collections.abc import Iterable
from datetime import datetime
from typing import cast

from homeassistant.components.binary_sensor import (
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

Expand Down Expand Up @@ -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:
Expand All @@ -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
224 changes: 76 additions & 148 deletions custom_components/sun2/helpers.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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:
Expand All @@ -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."""
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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."""
Expand All @@ -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:
Expand All @@ -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)
Loading

0 comments on commit d029472

Please sign in to comment.