Prevent lookin polling when push updates are coming in (#64687)
Co-authored-by: Chris Talkington <chris@talkingtontech.com>pull/64742/head
parent
84b483673e
commit
50b2e9d794
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue