mirror of https://github.com/home-assistant/core
Prevent lookin polling when push updates are coming in (#64687)
Co-authored-by: Chris Talkington <chris@talkingtontech.com>
This commit is contained in:
parent
84b483673e
commit
50b2e9d794
|
@ -611,6 +611,7 @@ omit =
|
||||||
homeassistant/components/logi_circle/sensor.py
|
homeassistant/components/logi_circle/sensor.py
|
||||||
homeassistant/components/london_underground/sensor.py
|
homeassistant/components/london_underground/sensor.py
|
||||||
homeassistant/components/lookin/__init__.py
|
homeassistant/components/lookin/__init__.py
|
||||||
|
homeassistant/components/lookin/coordinator.py
|
||||||
homeassistant/components/lookin/entity.py
|
homeassistant/components/lookin/entity.py
|
||||||
homeassistant/components/lookin/models.py
|
homeassistant/components/lookin/models.py
|
||||||
homeassistant/components/lookin/sensor.py
|
homeassistant/components/lookin/sensor.py
|
||||||
|
|
|
@ -18,18 +18,22 @@ from aiolookin import (
|
||||||
)
|
)
|
||||||
from aiolookin.models import UDPCommandType, UDPEvent
|
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.const import CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
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 .const import DOMAIN, PLATFORMS, TYPE_TO_PLATFORM
|
||||||
|
from .coordinator import LookinDataUpdateCoordinator, LookinPushCoordinator
|
||||||
from .models import LookinData
|
from .models import LookinData
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
UDP_LOCK = "udp_lock"
|
||||||
|
UDP_LISTENER = "udp_listener"
|
||||||
|
UDP_SUBSCRIPTIONS = "udp_subscriptions"
|
||||||
|
|
||||||
|
|
||||||
def _async_climate_updater(
|
def _async_climate_updater(
|
||||||
lookin_protocol: LookInHttpProtocol,
|
lookin_protocol: LookInHttpProtocol,
|
||||||
|
@ -55,9 +59,42 @@ def _async_remote_updater(
|
||||||
return _async_update
|
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:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up lookin from a config entry."""
|
"""Set up lookin from a config entry."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
host = entry.data[CONF_HOST]
|
host = entry.data[CONF_HOST]
|
||||||
lookin_protocol = LookInHttpProtocol(
|
lookin_protocol = LookInHttpProtocol(
|
||||||
api_uri=f"http://{host}", session=async_get_clientsession(hass)
|
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:
|
except (asyncio.TimeoutError, aiohttp.ClientError) as ex:
|
||||||
raise ConfigEntryNotReady from ex
|
raise ConfigEntryNotReady from ex
|
||||||
|
|
||||||
meteo_coordinator: DataUpdateCoordinator = DataUpdateCoordinator(
|
push_coordinator = LookinPushCoordinator(entry.title)
|
||||||
|
|
||||||
|
meteo_coordinator: LookinDataUpdateCoordinator = LookinDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
push_coordinator,
|
||||||
name=entry.title,
|
name=entry.title,
|
||||||
update_method=lookin_protocol.get_meteo_sensor,
|
update_method=lookin_protocol.get_meteo_sensor,
|
||||||
update_interval=timedelta(
|
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()
|
await meteo_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
device_coordinators: dict[str, DataUpdateCoordinator] = {}
|
device_coordinators: dict[str, LookinDataUpdateCoordinator] = {}
|
||||||
for remote in devices:
|
for remote in devices:
|
||||||
if (platform := TYPE_TO_PLATFORM.get(remote["Type"])) is None:
|
if (platform := TYPE_TO_PLATFORM.get(remote["Type"])) is None:
|
||||||
continue
|
continue
|
||||||
|
@ -89,9 +128,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
updater = _async_climate_updater(lookin_protocol, uuid)
|
updater = _async_climate_updater(lookin_protocol, uuid)
|
||||||
else:
|
else:
|
||||||
updater = _async_remote_updater(lookin_protocol, uuid)
|
updater = _async_remote_updater(lookin_protocol, uuid)
|
||||||
coordinator = DataUpdateCoordinator(
|
coordinator = LookinDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
push_coordinator,
|
||||||
name=f"{entry.title} {uuid}",
|
name=f"{entry.title} {uuid}",
|
||||||
update_method=updater,
|
update_method=updater,
|
||||||
update_interval=timedelta(
|
update_interval=timedelta(
|
||||||
|
@ -109,16 +148,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
meteo.update_from_value(event.value)
|
meteo.update_from_value(event.value)
|
||||||
meteo_coordinator.async_set_updated_data(meteo)
|
meteo_coordinator.async_set_updated_data(meteo)
|
||||||
|
|
||||||
lookin_udp_subs = LookinUDPSubscriptions()
|
lookin_udp_subs = await async_start_udp_listener(hass)
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
lookin_udp_subs.subscribe_event(
|
lookin_udp_subs.subscribe_event(
|
||||||
lookin_device.id, UDPCommandType.meteo, None, _async_meteo_push_update
|
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[DOMAIN][entry.entry_id] = LookinData(
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = LookinData(
|
|
||||||
lookin_udp_subs=lookin_udp_subs,
|
lookin_udp_subs=lookin_udp_subs,
|
||||||
lookin_device=lookin_device,
|
lookin_device=lookin_device,
|
||||||
meteo_coordinator=meteo_coordinator,
|
meteo_coordinator=meteo_coordinator,
|
||||||
|
@ -136,4 +174,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
await async_stop_udp_listener(hass)
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
|
@ -35,9 +35,9 @@ from homeassistant.const import (
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from .const import DOMAIN, TYPE_TO_PLATFORM
|
from .const import DOMAIN, TYPE_TO_PLATFORM
|
||||||
|
from .coordinator import LookinDataUpdateCoordinator
|
||||||
from .entity import LookinCoordinatorEntity
|
from .entity import LookinCoordinatorEntity
|
||||||
from .models import LookinData
|
from .models import LookinData
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity):
|
||||||
uuid: str,
|
uuid: str,
|
||||||
device: Climate,
|
device: Climate,
|
||||||
lookin_data: LookinData,
|
lookin_data: LookinData,
|
||||||
coordinator: DataUpdateCoordinator,
|
coordinator: LookinDataUpdateCoordinator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init the ConditionerEntity."""
|
"""Init the ConditionerEntity."""
|
||||||
super().__init__(coordinator, uuid, device, lookin_data)
|
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 aiolookin.models import Device, UDPCommandType, UDPEvent
|
||||||
|
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
CoordinatorEntity,
|
|
||||||
DataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import DOMAIN, MODEL_NAMES
|
from .const import DOMAIN, MODEL_NAMES
|
||||||
|
from .coordinator import LookinDataUpdateCoordinator
|
||||||
from .models import LookinData
|
from .models import LookinData
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -55,6 +53,8 @@ class LookinDeviceMixIn:
|
||||||
class LookinDeviceCoordinatorEntity(LookinDeviceMixIn, CoordinatorEntity):
|
class LookinDeviceCoordinatorEntity(LookinDeviceMixIn, CoordinatorEntity):
|
||||||
"""A lookin device entity on the device itself that uses the coordinator."""
|
"""A lookin device entity on the device itself that uses the coordinator."""
|
||||||
|
|
||||||
|
coordinator: LookinDataUpdateCoordinator
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|
||||||
def __init__(self, lookin_data: LookinData) -> None:
|
def __init__(self, lookin_data: LookinData) -> None:
|
||||||
|
@ -85,12 +85,14 @@ class LookinEntityMixIn:
|
||||||
class LookinCoordinatorEntity(LookinDeviceMixIn, LookinEntityMixIn, CoordinatorEntity):
|
class LookinCoordinatorEntity(LookinDeviceMixIn, LookinEntityMixIn, CoordinatorEntity):
|
||||||
"""A lookin device entity for an external device that uses the coordinator."""
|
"""A lookin device entity for an external device that uses the coordinator."""
|
||||||
|
|
||||||
|
coordinator: LookinDataUpdateCoordinator
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
_attr_assumed_state = True
|
_attr_assumed_state = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: DataUpdateCoordinator,
|
coordinator: LookinDataUpdateCoordinator,
|
||||||
uuid: str,
|
uuid: str,
|
||||||
device: Remote | Climate,
|
device: Remote | Climate,
|
||||||
lookin_data: LookinData,
|
lookin_data: LookinData,
|
||||||
|
@ -117,7 +119,7 @@ class LookinPowerEntity(LookinCoordinatorEntity):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: DataUpdateCoordinator,
|
coordinator: LookinDataUpdateCoordinator,
|
||||||
uuid: str,
|
uuid: str,
|
||||||
device: Remote | Climate,
|
device: Remote | Climate,
|
||||||
lookin_data: LookinData,
|
lookin_data: LookinData,
|
||||||
|
@ -137,7 +139,7 @@ class LookinPowerPushRemoteEntity(LookinPowerEntity):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: DataUpdateCoordinator,
|
coordinator: LookinDataUpdateCoordinator,
|
||||||
uuid: str,
|
uuid: str,
|
||||||
device: Remote,
|
device: Remote,
|
||||||
lookin_data: LookinData,
|
lookin_data: LookinData,
|
||||||
|
|
|
@ -21,9 +21,9 @@ from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import STATE_ON, STATE_STANDBY, Platform
|
from homeassistant.const import STATE_ON, STATE_STANDBY, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from .const import DOMAIN, TYPE_TO_PLATFORM
|
from .const import DOMAIN, TYPE_TO_PLATFORM
|
||||||
|
from .coordinator import LookinDataUpdateCoordinator
|
||||||
from .entity import LookinPowerPushRemoteEntity
|
from .entity import LookinPowerPushRemoteEntity
|
||||||
from .models import LookinData
|
from .models import LookinData
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: DataUpdateCoordinator,
|
coordinator: LookinDataUpdateCoordinator,
|
||||||
uuid: str,
|
uuid: str,
|
||||||
device: Remote,
|
device: Remote,
|
||||||
lookin_data: LookinData,
|
lookin_data: LookinData,
|
||||||
|
|
|
@ -6,7 +6,7 @@ from typing import Any
|
||||||
|
|
||||||
from aiolookin import Device, LookInHttpProtocol, LookinUDPSubscriptions
|
from aiolookin import Device, LookInHttpProtocol, LookinUDPSubscriptions
|
||||||
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from .coordinator import LookinDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -15,7 +15,7 @@ class LookinData:
|
||||||
|
|
||||||
lookin_udp_subs: LookinUDPSubscriptions
|
lookin_udp_subs: LookinUDPSubscriptions
|
||||||
lookin_device: Device
|
lookin_device: Device
|
||||||
meteo_coordinator: DataUpdateCoordinator
|
meteo_coordinator: LookinDataUpdateCoordinator
|
||||||
devices: list[dict[str, Any]]
|
devices: list[dict[str, Any]]
|
||||||
lookin_protocol: LookInHttpProtocol
|
lookin_protocol: LookInHttpProtocol
|
||||||
device_coordinators: dict[str, DataUpdateCoordinator]
|
device_coordinators: dict[str, LookinDataUpdateCoordinator]
|
||||||
|
|
Loading…
Reference in New Issue