From dfff22b5ceb0130892930e0399dc7228cb7ae822 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 18 Nov 2023 17:07:58 +0100 Subject: [PATCH] Add update coordinator to ping (#104148) * Add update coordinator to ping * Remove config_entry from coordinator * Remove PARALLEL_UPDATES and set to hass.data --- .coveragerc | 1 + homeassistant/components/ping/__init__.py | 32 ++++++++- .../components/ping/binary_sensor.py | 71 ++++--------------- homeassistant/components/ping/coordinator.py | 53 ++++++++++++++ .../components/ping/device_tracker.py | 38 +++------- 5 files changed, 105 insertions(+), 90 deletions(-) create mode 100644 homeassistant/components/ping/coordinator.py diff --git a/.coveragerc b/.coveragerc index 13de6cb29c8..40fcd50f57f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -948,6 +948,7 @@ omit = homeassistant/components/pilight/switch.py homeassistant/components/ping/__init__.py homeassistant/components/ping/binary_sensor.py + homeassistant/components/ping/coordinator.py homeassistant/components/ping/device_tracker.py homeassistant/components/ping/helpers.py homeassistant/components/pioneer/media_player.py diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index 3280173813d..81df1401f91 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -7,12 +7,14 @@ import logging from icmplib import SocketPermissionError, async_ping from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import CONF_PING_COUNT, DOMAIN +from .coordinator import PingUpdateCoordinator +from .helpers import PingDataICMPLib, PingDataSubProcess _LOGGER = logging.getLogger(__name__) @@ -25,6 +27,7 @@ class PingDomainData: """Dataclass to store privileged status.""" privileged: bool | None + coordinators: dict[str, PingUpdateCoordinator] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -32,6 +35,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = PingDomainData( privileged=await _can_use_icmp_lib_with_privilege(), + coordinators={}, ) return True @@ -40,6 +44,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ping (ICMP) from a config entry.""" + data: PingDomainData = hass.data[DOMAIN] + + host: str = entry.options[CONF_HOST] + count: int = int(entry.options[CONF_PING_COUNT]) + ping_cls: type[PingDataICMPLib | PingDataSubProcess] + if data.privileged is None: + ping_cls = PingDataSubProcess + else: + ping_cls = PingDataICMPLib + + coordinator = PingUpdateCoordinator( + hass=hass, ping=ping_cls(hass, host, count, data.privileged) + ) + await coordinator.async_config_entry_first_refresh() + + data.coordinators[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) @@ -53,7 +74,12 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + # drop coordinator for config entry + hass.data[DOMAIN].coordinators.pop(entry.entry_id) + + return unload_ok async def _can_use_icmp_lib_with_privilege() -> None | bool: diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index f81a6b7d22d..97636111586 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -1,7 +1,6 @@ """Tracks the latency of a host by sending ICMP echo requests (ping).""" from __future__ import annotations -from datetime import timedelta import logging from typing import Any @@ -13,17 +12,17 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, STATE_ON +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PingDomainData from .const import CONF_IMPORTED_BY, CONF_PING_COUNT, DEFAULT_PING_COUNT, DOMAIN -from .helpers import PingDataICMPLib, PingDataSubProcess +from .coordinator import PingUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -32,10 +31,6 @@ ATTR_ROUND_TRIP_TIME_MAX = "round_trip_time_max" ATTR_ROUND_TRIP_TIME_MDEV = "round_trip_time_mdev" ATTR_ROUND_TRIP_TIME_MIN = "round_trip_time_min" -SCAN_INTERVAL = timedelta(minutes=5) - -PARALLEL_UPDATES = 50 - PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, @@ -86,31 +81,21 @@ async def async_setup_entry( data: PingDomainData = hass.data[DOMAIN] - host: str = entry.options[CONF_HOST] - count: int = int(entry.options[CONF_PING_COUNT]) - ping_cls: type[PingDataSubProcess | PingDataICMPLib] - if data.privileged is None: - ping_cls = PingDataSubProcess - else: - ping_cls = PingDataICMPLib - - async_add_entities( - [PingBinarySensor(entry, ping_cls(hass, host, count, data.privileged))] - ) + async_add_entities([PingBinarySensor(entry, data.coordinators[entry.entry_id])]) -class PingBinarySensor(RestoreEntity, BinarySensorEntity): +class PingBinarySensor(CoordinatorEntity[PingUpdateCoordinator], BinarySensorEntity): """Representation of a Ping Binary sensor.""" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_available = False def __init__( - self, - config_entry: ConfigEntry, - ping_cls: PingDataSubProcess | PingDataICMPLib, + self, config_entry: ConfigEntry, coordinator: PingUpdateCoordinator ) -> None: """Initialize the Ping Binary sensor.""" + super().__init__(coordinator) + self._attr_name = config_entry.title self._attr_unique_id = config_entry.entry_id @@ -120,47 +105,19 @@ class PingBinarySensor(RestoreEntity, BinarySensorEntity): config_entry.data[CONF_IMPORTED_BY] == "binary_sensor" ) - self._ping = ping_cls - @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self._ping.is_alive + return self.coordinator.data.is_alive @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the ICMP checo request.""" - if self._ping.data is None: + if self.coordinator.data.data is None: return None return { - ATTR_ROUND_TRIP_TIME_AVG: self._ping.data["avg"], - ATTR_ROUND_TRIP_TIME_MAX: self._ping.data["max"], - ATTR_ROUND_TRIP_TIME_MDEV: self._ping.data["mdev"], - ATTR_ROUND_TRIP_TIME_MIN: self._ping.data["min"], - } - - async def async_update(self) -> None: - """Get the latest data.""" - await self._ping.async_update() - self._attr_available = True - - async def async_added_to_hass(self) -> None: - """Restore previous state on restart to avoid blocking startup.""" - await super().async_added_to_hass() - - last_state = await self.async_get_last_state() - if last_state is not None: - self._attr_available = True - - if last_state is None or last_state.state != STATE_ON: - self._ping.data = None - return - - attributes = last_state.attributes - self._ping.is_alive = True - self._ping.data = { - "min": attributes[ATTR_ROUND_TRIP_TIME_MIN], - "max": attributes[ATTR_ROUND_TRIP_TIME_MAX], - "avg": attributes[ATTR_ROUND_TRIP_TIME_AVG], - "mdev": attributes[ATTR_ROUND_TRIP_TIME_MDEV], + ATTR_ROUND_TRIP_TIME_AVG: self.coordinator.data.data["avg"], + ATTR_ROUND_TRIP_TIME_MAX: self.coordinator.data.data["max"], + ATTR_ROUND_TRIP_TIME_MDEV: self.coordinator.data.data["mdev"], + ATTR_ROUND_TRIP_TIME_MIN: self.coordinator.data.data["min"], } diff --git a/homeassistant/components/ping/coordinator.py b/homeassistant/components/ping/coordinator.py new file mode 100644 index 00000000000..dadd105b606 --- /dev/null +++ b/homeassistant/components/ping/coordinator.py @@ -0,0 +1,53 @@ +"""DataUpdateCoordinator for the ping integration.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import logging +from typing import Any + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .helpers import PingDataICMPLib, PingDataSubProcess + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(slots=True, frozen=True) +class PingResult: + """Dataclass returned by the coordinator.""" + + ip_address: str + is_alive: bool + data: dict[str, Any] | None + + +class PingUpdateCoordinator(DataUpdateCoordinator[PingResult]): + """The Ping update coordinator.""" + + ping: PingDataSubProcess | PingDataICMPLib + + def __init__( + self, + hass: HomeAssistant, + ping: PingDataSubProcess | PingDataICMPLib, + ) -> None: + """Initialize the Ping coordinator.""" + self.ping = ping + + super().__init__( + hass, + _LOGGER, + name=f"Ping {ping.ip_address}", + update_interval=timedelta(minutes=5), + ) + + async def _async_update_data(self) -> PingResult: + """Trigger ping check.""" + await self.ping.async_update() + return PingResult( + ip_address=self.ping.ip_address, + is_alive=self.ping.is_alive, + data=self.ping.data, + ) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index af07325db00..ceff1b2e124 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -1,7 +1,6 @@ """Tracks devices by sending a ICMP echo request (ping).""" from __future__ import annotations -from datetime import timedelta import logging import voluptuous as vol @@ -19,16 +18,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PingDomainData from .const import CONF_IMPORTED_BY, CONF_PING_COUNT, DOMAIN -from .helpers import PingDataICMPLib, PingDataSubProcess +from .coordinator import PingUpdateCoordinator _LOGGER = logging.getLogger(__name__) -PARALLEL_UPDATES = 0 -SCAN_INTERVAL = timedelta(minutes=5) - PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOSTS): {cv.slug: cv.string}, @@ -84,40 +81,25 @@ async def async_setup_entry( data: PingDomainData = hass.data[DOMAIN] - host: str = entry.options[CONF_HOST] - count: int = int(entry.options[CONF_PING_COUNT]) - ping_cls: type[PingDataSubProcess | PingDataICMPLib] - if data.privileged is None: - ping_cls = PingDataSubProcess - else: - ping_cls = PingDataICMPLib - - async_add_entities( - [PingDeviceTracker(entry, ping_cls(hass, host, count, data.privileged))] - ) + async_add_entities([PingDeviceTracker(entry, data.coordinators[entry.entry_id])]) -class PingDeviceTracker(ScannerEntity): +class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity): """Representation of a Ping device tracker.""" - ping: PingDataSubProcess | PingDataICMPLib - def __init__( - self, - config_entry: ConfigEntry, - ping_cls: PingDataSubProcess | PingDataICMPLib, + self, config_entry: ConfigEntry, coordinator: PingUpdateCoordinator ) -> None: """Initialize the Ping device tracker.""" - super().__init__() + super().__init__(coordinator) self._attr_name = config_entry.title - self.ping = ping_cls self.config_entry = config_entry @property def ip_address(self) -> str: """Return the primary ip address of the device.""" - return self.ping.ip_address + return self.coordinator.data.ip_address @property def unique_id(self) -> str: @@ -132,7 +114,7 @@ class PingDeviceTracker(ScannerEntity): @property def is_connected(self) -> bool: """Return true if ping returns is_alive.""" - return self.ping.is_alive + return self.coordinator.data.is_alive @property def entity_registry_enabled_default(self) -> bool: @@ -140,7 +122,3 @@ class PingDeviceTracker(ScannerEntity): if CONF_IMPORTED_BY in self.config_entry.data: return bool(self.config_entry.data[CONF_IMPORTED_BY] == "device_tracker") return False - - async def async_update(self) -> None: - """Update the sensor.""" - await self.ping.async_update()