Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,7 @@ omit =
homeassistant/components/picotts/tts.py
homeassistant/components/piglow/light.py
homeassistant/components/pilight/*
homeassistant/components/ping/__init__.py
homeassistant/components/ping/const.py
homeassistant/components/ping/binary_sensor.py
homeassistant/components/ping/device_tracker.py
Expand Down
46 changes: 39 additions & 7 deletions homeassistant/components/ping/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
"""The ping component."""
from __future__ import annotations

import logging

from icmplib import SocketPermissionError, ping as icmp_ping

from homeassistant.core import callback
from homeassistant.helpers.reload import async_setup_reload_service

from .const import DEFAULT_START_ID, DOMAIN, MAX_PING_ID, PING_ID, PING_PRIVS, PLATFORMS

_LOGGER = logging.getLogger(__name__)

DOMAIN = "ping"
PLATFORMS = ["binary_sensor"]

PING_ID = "ping_id"
DEFAULT_START_ID = 129
MAX_PING_ID = 65534
async def async_setup(hass, config):
"""Set up the template integration."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
hass.data[DOMAIN] = {
PING_PRIVS: await hass.async_add_executor_job(_can_use_icmp_lib_with_privilege),
PING_ID: DEFAULT_START_ID,
}
return True


@callback
Expand All @@ -16,8 +29,7 @@ def async_get_next_ping_id(hass):

Must be called in async
"""
current_id = hass.data.setdefault(DOMAIN, {}).get(PING_ID, DEFAULT_START_ID)

current_id = hass.data[DOMAIN][PING_ID]
if current_id == MAX_PING_ID:
next_id = DEFAULT_START_ID
else:
Expand All @@ -26,3 +38,23 @@ def async_get_next_ping_id(hass):
hass.data[DOMAIN][PING_ID] = next_id

return next_id


def _can_use_icmp_lib_with_privilege() -> None | bool:
"""Verify we can create a raw socket."""
try:
icmp_ping("127.0.0.1", count=0, timeout=0, privileged=True)
except SocketPermissionError:
try:
icmp_ping("127.0.0.1", count=0, timeout=0, privileged=False)
except SocketPermissionError:
_LOGGER.debug(
"Cannot use icmplib because privileges are insufficient to create the socket"
)
return None
else:
_LOGGER.debug("Using icmplib in privileged=False mode")
return False
else:
_LOGGER.debug("Using icmplib in privileged=True mode")
return True
102 changes: 69 additions & 33 deletions homeassistant/components/ping/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@
import sys
from typing import Any

from icmplib import SocketPermissionError, ping as icmp_ping
from icmplib import NameLookupError, ping as icmp_ping
import voluptuous as vol

from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
PLATFORM_SCHEMA,
BinarySensorEntity,
)
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.const import CONF_HOST, CONF_NAME, STATE_ON
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import setup_reload_service
from homeassistant.helpers.restore_state import RestoreEntity

from . import DOMAIN, PLATFORMS, async_get_next_ping_id
from .const import PING_TIMEOUT
from . import async_get_next_ping_id
from .const import DOMAIN, ICMP_TIMEOUT, PING_PRIVS, PING_TIMEOUT

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -63,32 +63,30 @@
)


def setup_platform(hass, config, add_entities, discovery_info=None) -> None:
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None
) -> None:
"""Set up the Ping Binary sensor."""
setup_reload_service(hass, DOMAIN, PLATFORMS)

host = config[CONF_HOST]
count = config[CONF_PING_COUNT]
name = config.get(CONF_NAME, f"{DEFAULT_NAME} {host}")

try:
# Verify we can create a raw socket, or
# fallback to using a subprocess
icmp_ping("127.0.0.1", count=0, timeout=0)
ping_cls = PingDataICMPLib
except SocketPermissionError:
privileged = hass.data[DOMAIN][PING_PRIVS]
if privileged is None:
ping_cls = PingDataSubProcess
else:
ping_cls = PingDataICMPLib

ping_data = ping_cls(hass, host, count)

add_entities([PingBinarySensor(name, ping_data)], True)
async_add_entities(
[PingBinarySensor(name, ping_cls(hass, host, count, privileged))]
)


class PingBinarySensor(BinarySensorEntity):
class PingBinarySensor(RestoreEntity, BinarySensorEntity):
"""Representation of a Ping Binary sensor."""

def __init__(self, name: str, ping) -> None:
"""Initialize the Ping Binary sensor."""
self._available = False
self._name = name
self._ping = ping

Expand All @@ -97,6 +95,11 @@ def name(self) -> str:
"""Return the name of the device."""
return self._name

@property
def available(self) -> str:
"""Return if we have done the first ping."""
return self._available

@property
def device_class(self) -> str:
"""Return the class of this sensor."""
Expand All @@ -105,7 +108,7 @@ def device_class(self) -> str:
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self._ping.available
return self._ping.is_alive

@property
def extra_state_attributes(self) -> dict[str, Any]:
Expand All @@ -121,6 +124,28 @@ def extra_state_attributes(self) -> dict[str, Any]:
async def async_update(self) -> None:
"""Get the latest data."""
await self._ping.async_update()
self._available = True

async def async_added_to_hass(self):
"""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._available = True

if last_state is None or last_state.state != STATE_ON:
self._ping.data = False
return

attributes = last_state.attributes
self._ping.is_alive = True
self._ping.data = {
"min": attributes[ATTR_ROUND_TRIP_TIME_AVG],
"max": attributes[ATTR_ROUND_TRIP_TIME_MAX],
"avg": attributes[ATTR_ROUND_TRIP_TIME_MDEV],
"mdev": attributes[ATTR_ROUND_TRIP_TIME_MIN],
}


class PingData:
Expand All @@ -132,26 +157,37 @@ def __init__(self, hass, host, count) -> None:
self._ip_address = host
self._count = count
self.data = {}
self.available = False
self.is_alive = False


class PingDataICMPLib(PingData):
"""The Class for handling the data retrieval using icmplib."""

def __init__(self, hass, host, count, privileged) -> None:
"""Initialize the data object."""
super().__init__(hass, host, count)
self._privileged = privileged

async def async_update(self) -> None:
"""Retrieve the latest details from the host."""
_LOGGER.debug("ping address: %s", self._ip_address)
data = await self.hass.async_add_executor_job(
partial(
icmp_ping,
self._ip_address,
count=self._count,
timeout=1,
id=async_get_next_ping_id(self.hass),
try:
data = await self.hass.async_add_executor_job(
partial(
icmp_ping,
self._ip_address,
count=self._count,
timeout=ICMP_TIMEOUT,
id=async_get_next_ping_id(self.hass),
privileged=self._privileged,
)
)
)
self.available = data.is_alive
if not self.available:
except NameLookupError:
self.is_alive = False
return

self.is_alive = data.is_alive
if not self.is_alive:
self.data = False
return

Expand All @@ -166,7 +202,7 @@ async def async_update(self) -> None:
class PingDataSubProcess(PingData):
"""The Class for handling the data retrieval using the ping binary."""

def __init__(self, hass, host, count) -> None:
def __init__(self, hass, host, count, privileged) -> None:
"""Initialize the data object."""
super().__init__(hass, host, count)
if sys.platform == "win32":
Expand Down Expand Up @@ -254,4 +290,4 @@ async def async_ping(self):
async def async_update(self) -> None:
"""Retrieve the latest details from the host."""
self.data = await self.async_ping()
self.available = bool(self.data)
self.is_alive = bool(self.data)
17 changes: 17 additions & 0 deletions homeassistant/components/ping/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
"""Tracks devices by sending a ICMP echo request (ping)."""

# The ping binary and icmplib timeouts are not the same
# timeout. ping is an overall timeout, icmplib is the
# time since the data was sent.

# ping binary
PING_TIMEOUT = 3

# icmplib timeout
ICMP_TIMEOUT = 1

PING_ATTEMPTS_COUNT = 3

DOMAIN = "ping"
PLATFORMS = ["binary_sensor"]

PING_ID = "ping_id"
PING_PRIVS = "ping_privs"
DEFAULT_START_ID = 129
MAX_PING_ID = 65534
Loading