Prevent lookin polling when push updates are coming in (#64687)

Co-authored-by: Chris Talkington <chris@talkingtontech.com>
This commit is contained in:
J. Nick Koston 2022-01-22 21:19:34 -10:00 committed by GitHub
parent 84b483673e
commit 50b2e9d794
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 154 additions and 26 deletions

View File

@ -611,6 +611,7 @@ omit =
homeassistant/components/logi_circle/sensor.py
homeassistant/components/london_underground/sensor.py
homeassistant/components/lookin/__init__.py
homeassistant/components/lookin/coordinator.py
homeassistant/components/lookin/entity.py
homeassistant/components/lookin/models.py
homeassistant/components/lookin/sensor.py

View File

@ -18,18 +18,22 @@ from aiolookin import (
)
from aiolookin.models import UDPCommandType, UDPEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, PLATFORMS, TYPE_TO_PLATFORM
from .coordinator import LookinDataUpdateCoordinator, LookinPushCoordinator
from .models import LookinData
LOGGER = logging.getLogger(__name__)
UDP_LOCK = "udp_lock"
UDP_LISTENER = "udp_listener"
UDP_SUBSCRIPTIONS = "udp_subscriptions"
def _async_climate_updater(
lookin_protocol: LookInHttpProtocol,
@ -55,9 +59,42 @@ def _async_remote_updater(
return _async_update
async def async_start_udp_listener(hass: HomeAssistant) -> LookinUDPSubscriptions:
"""Start the shared udp listener."""
domain_data = hass.data[DOMAIN]
if UDP_LOCK not in domain_data:
udp_lock = domain_data[UDP_LOCK] = asyncio.Lock()
else:
udp_lock = domain_data[UDP_LOCK]
async with udp_lock:
if UDP_LISTENER not in domain_data:
lookin_udp_subs = domain_data[UDP_SUBSCRIPTIONS] = LookinUDPSubscriptions()
domain_data[UDP_LISTENER] = await start_lookin_udp(lookin_udp_subs, None)
else:
lookin_udp_subs = domain_data[UDP_SUBSCRIPTIONS]
return lookin_udp_subs
async def async_stop_udp_listener(hass: HomeAssistant) -> None:
"""Stop the shared udp listener."""
domain_data = hass.data[DOMAIN]
async with domain_data[UDP_LOCK]:
loaded_entries = [
entry
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.state == ConfigEntryState.LOADED
]
if len(loaded_entries) > 1:
return
domain_data[UDP_LISTENER]()
del domain_data[UDP_LISTENER]
del domain_data[UDP_SUBSCRIPTIONS]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up lookin from a config entry."""
hass.data.setdefault(DOMAIN, {})
host = entry.data[CONF_HOST]
lookin_protocol = LookInHttpProtocol(
api_uri=f"http://{host}", session=async_get_clientsession(hass)
@ -69,9 +106,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except (asyncio.TimeoutError, aiohttp.ClientError) as ex:
raise ConfigEntryNotReady from ex
meteo_coordinator: DataUpdateCoordinator = DataUpdateCoordinator(
push_coordinator = LookinPushCoordinator(entry.title)
meteo_coordinator: LookinDataUpdateCoordinator = LookinDataUpdateCoordinator(
hass,
LOGGER,
push_coordinator,
name=entry.title,
update_method=lookin_protocol.get_meteo_sensor,
update_interval=timedelta(
@ -80,7 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
await meteo_coordinator.async_config_entry_first_refresh()
device_coordinators: dict[str, DataUpdateCoordinator] = {}
device_coordinators: dict[str, LookinDataUpdateCoordinator] = {}
for remote in devices:
if (platform := TYPE_TO_PLATFORM.get(remote["Type"])) is None:
continue
@ -89,9 +128,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
updater = _async_climate_updater(lookin_protocol, uuid)
else:
updater = _async_remote_updater(lookin_protocol, uuid)
coordinator = DataUpdateCoordinator(
coordinator = LookinDataUpdateCoordinator(
hass,
LOGGER,
push_coordinator,
name=f"{entry.title} {uuid}",
update_method=updater,
update_interval=timedelta(
@ -109,16 +148,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
meteo.update_from_value(event.value)
meteo_coordinator.async_set_updated_data(meteo)
lookin_udp_subs = LookinUDPSubscriptions()
lookin_udp_subs = await async_start_udp_listener(hass)
entry.async_on_unload(
lookin_udp_subs.subscribe_event(
lookin_device.id, UDPCommandType.meteo, None, _async_meteo_push_update
)
)
entry.async_on_unload(await start_lookin_udp(lookin_udp_subs, lookin_device.id))
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = LookinData(
hass.data[DOMAIN][entry.entry_id] = LookinData(
lookin_udp_subs=lookin_udp_subs,
lookin_device=lookin_device,
meteo_coordinator=meteo_coordinator,
@ -136,4 +174,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
await async_stop_udp_listener(hass)
return unload_ok

View File

@ -35,9 +35,9 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, TYPE_TO_PLATFORM
from .coordinator import LookinDataUpdateCoordinator
from .entity import LookinCoordinatorEntity
from .models import LookinData
@ -115,7 +115,7 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity):
uuid: str,
device: Climate,
lookin_data: LookinData,
coordinator: DataUpdateCoordinator,
coordinator: LookinDataUpdateCoordinator,
) -> None:
"""Init the ConditionerEntity."""
super().__init__(coordinator, uuid, device, lookin_data)

View File

@ -0,0 +1,85 @@
"""Coordinator for lookin devices."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from datetime import timedelta
import logging
import time
from typing import cast
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
NEVER_TIME = -120.0 # Time that will never match time.monotonic()
ACTIVE_UPDATES_INTERVAL = 3 # Consider active for 3x the update interval
class LookinPushCoordinator:
"""Keep track of when the last push update was."""
def __init__(self, name: str) -> None:
"""Init the push coordininator."""
self.last_update = NEVER_TIME
self.name = name
def update(self) -> None:
"""Remember the last push time."""
self.last_update = time.monotonic()
def active(self, interval: timedelta) -> bool:
"""Check if the last push update was recently."""
time_since_last_update = time.monotonic() - self.last_update
is_active = (
time_since_last_update < interval.total_seconds() * ACTIVE_UPDATES_INTERVAL
)
_LOGGER.debug(
"%s: push updates active: %s (time_since_last_update=%s)",
self.name,
is_active,
time_since_last_update,
)
return is_active
class LookinDataUpdateCoordinator(DataUpdateCoordinator):
"""DataUpdateCoordinator to gather data for a specific lookin devices."""
def __init__(
self,
hass: HomeAssistant,
push_coordinator: LookinPushCoordinator,
name: str,
update_interval: timedelta | None = None,
update_method: Callable[[], Awaitable[dict]] | None = None,
) -> None:
"""Initialize DataUpdateCoordinator to gather data for specific device."""
self.push_coordinator = push_coordinator
super().__init__(
hass,
_LOGGER,
name=name,
update_interval=update_interval,
update_method=update_method,
)
@callback
def async_set_updated_data(self, data: dict) -> None:
"""Manually update data, notify listeners and reset refresh interval, and remember."""
self.push_coordinator.update()
super().async_set_updated_data(data)
async def _async_update_data(self) -> dict:
"""Fetch data only if we have not received a push inside the interval."""
interval = self.update_interval
if (
interval is not None
and self.last_update_success
and self.data
and self.push_coordinator.active(interval)
):
data = self.data
else:
data = await super()._async_update_data()
return cast(dict, data)

View File

@ -9,12 +9,10 @@ from aiolookin import POWER_CMD, POWER_OFF_CMD, POWER_ON_CMD, Climate, Remote
from aiolookin.models import Device, UDPCommandType, UDPEvent
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MODEL_NAMES
from .coordinator import LookinDataUpdateCoordinator
from .models import LookinData
LOGGER = logging.getLogger(__name__)
@ -55,6 +53,8 @@ class LookinDeviceMixIn:
class LookinDeviceCoordinatorEntity(LookinDeviceMixIn, CoordinatorEntity):
"""A lookin device entity on the device itself that uses the coordinator."""
coordinator: LookinDataUpdateCoordinator
_attr_should_poll = False
def __init__(self, lookin_data: LookinData) -> None:
@ -85,12 +85,14 @@ class LookinEntityMixIn:
class LookinCoordinatorEntity(LookinDeviceMixIn, LookinEntityMixIn, CoordinatorEntity):
"""A lookin device entity for an external device that uses the coordinator."""
coordinator: LookinDataUpdateCoordinator
_attr_should_poll = False
_attr_assumed_state = True
def __init__(
self,
coordinator: DataUpdateCoordinator,
coordinator: LookinDataUpdateCoordinator,
uuid: str,
device: Remote | Climate,
lookin_data: LookinData,
@ -117,7 +119,7 @@ class LookinPowerEntity(LookinCoordinatorEntity):
def __init__(
self,
coordinator: DataUpdateCoordinator,
coordinator: LookinDataUpdateCoordinator,
uuid: str,
device: Remote | Climate,
lookin_data: LookinData,
@ -137,7 +139,7 @@ class LookinPowerPushRemoteEntity(LookinPowerEntity):
def __init__(
self,
coordinator: DataUpdateCoordinator,
coordinator: LookinDataUpdateCoordinator,
uuid: str,
device: Remote,
lookin_data: LookinData,

View File

@ -21,9 +21,9 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_ON, STATE_STANDBY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, TYPE_TO_PLATFORM
from .coordinator import LookinDataUpdateCoordinator
from .entity import LookinPowerPushRemoteEntity
from .models import LookinData
@ -80,7 +80,7 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity):
def __init__(
self,
coordinator: DataUpdateCoordinator,
coordinator: LookinDataUpdateCoordinator,
uuid: str,
device: Remote,
lookin_data: LookinData,

View File

@ -6,7 +6,7 @@ from typing import Any
from aiolookin import Device, LookInHttpProtocol, LookinUDPSubscriptions
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .coordinator import LookinDataUpdateCoordinator
@dataclass
@ -15,7 +15,7 @@ class LookinData:
lookin_udp_subs: LookinUDPSubscriptions
lookin_device: Device
meteo_coordinator: DataUpdateCoordinator
meteo_coordinator: LookinDataUpdateCoordinator
devices: list[dict[str, Any]]
lookin_protocol: LookInHttpProtocol
device_coordinators: dict[str, DataUpdateCoordinator]
device_coordinators: dict[str, LookinDataUpdateCoordinator]