Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add update coordinator to ping #104148

Merged
merged 3 commits into from
Nov 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 29 additions & 3 deletions homeassistant/components/ping/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
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__)

Expand All @@ -25,13 +27,15 @@ class PingDomainData:
"""Dataclass to store privileged status."""

privileged: bool | None
coordinators: dict[str, PingUpdateCoordinator]


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the ping integration."""

hass.data[DOMAIN] = PingDomainData(
privileged=await _can_use_icmp_lib_with_privilege(),
coordinators={},
)

return True
Expand All @@ -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))

Expand All @@ -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:
Expand Down
71 changes: 14 additions & 57 deletions homeassistant/components/ping/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -13,17 +12,17 @@
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__)

Expand All @@ -32,10 +31,6 @@
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,
Expand Down Expand Up @@ -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

Expand All @@ -120,47 +105,19 @@ def __init__(
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"],
}
53 changes: 53 additions & 0 deletions homeassistant/components/ping/coordinator.py
Original file line number Diff line number Diff line change
@@ -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,
)
38 changes: 8 additions & 30 deletions homeassistant/components/ping/device_tracker.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,16 +18,14 @@
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},
Expand Down Expand Up @@ -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:
Expand All @@ -132,15 +114,11 @@ def source_type(self) -> SourceType:
@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:
"""Return if entity is enabled by default."""
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()