Home Assistant Unofficial Reference 2024.12.1
helpers.py
Go to the documentation of this file.
1 """Ping classes shared between platforms."""
2 
3 import asyncio
4 from contextlib import suppress
5 import logging
6 import re
7 from typing import TYPE_CHECKING, Any
8 
9 from icmplib import NameLookupError, async_ping
10 
11 from homeassistant.core import HomeAssistant
12 
13 from .const import ICMP_TIMEOUT, PING_TIMEOUT
14 
15 _LOGGER = logging.getLogger(__name__)
16 
17 PING_MATCHER = re.compile(
18  r"(?P<min>\d+.\d+)\/(?P<avg>\d+.\d+)\/(?P<max>\d+.\d+)\/(?P<mdev>\d+.\d+)"
19 )
20 
21 PING_MATCHER_BUSYBOX = re.compile(
22  r"(?P<min>\d+.\d+)\/(?P<avg>\d+.\d+)\/(?P<max>\d+.\d+)"
23 )
24 
25 WIN32_PING_MATCHER = re.compile(r"(?P<min>\d+)ms.+(?P<max>\d+)ms.+(?P<avg>\d+)ms")
26 
27 
28 class PingData:
29  """The base class for handling the data retrieval."""
30 
31  data: dict[str, Any] | None = None
32  is_alive: bool = False
33 
34  def __init__(self, hass: HomeAssistant, host: str, count: int) -> None:
35  """Initialize the data object."""
36  self.hasshass = hass
37  self.ip_addressip_address = host
38  self._count_count = count
39 
40 
42  """The Class for handling the data retrieval using icmplib."""
43 
44  def __init__(
45  self, hass: HomeAssistant, host: str, count: int, privileged: bool | None
46  ) -> None:
47  """Initialize the data object."""
48  super().__init__(hass, host, count)
49  self._privileged_privileged = privileged
50 
51  async def async_update(self) -> None:
52  """Retrieve the latest details from the host."""
53  _LOGGER.debug("ping address: %s", self.ip_addressip_address)
54  try:
55  data = await async_ping(
56  self.ip_addressip_address,
57  count=self._count_count,
58  timeout=ICMP_TIMEOUT,
59  privileged=self._privileged_privileged,
60  )
61  except NameLookupError:
62  _LOGGER.debug("Error resolving host: %s", self.ip_addressip_address)
63  self.is_aliveis_alive = False
64  return
65 
66  _LOGGER.debug(
67  "async_ping returned: reachable=%s sent=%i received=%s",
68  data.is_alive,
69  data.packets_sent,
70  data.packets_received,
71  )
72 
73  self.is_aliveis_alive = data.is_alive
74  if not self.is_aliveis_alive:
75  self.datadata = None
76  return
77 
78  self.datadata = {
79  "min": data.min_rtt,
80  "max": data.max_rtt,
81  "avg": data.avg_rtt,
82  }
83 
84 
86  """The Class for handling the data retrieval using the ping binary."""
87 
88  def __init__(
89  self, hass: HomeAssistant, host: str, count: int, privileged: bool | None
90  ) -> None:
91  """Initialize the data object."""
92  super().__init__(hass, host, count)
93  self._ping_cmd_ping_cmd = [
94  "ping",
95  "-n",
96  "-q",
97  "-c",
98  str(self._count_count),
99  "-W1",
100  self.ip_addressip_address,
101  ]
102 
103  async def async_ping(self) -> dict[str, Any] | None:
104  """Send ICMP echo request and return details if success."""
105  _LOGGER.debug(
106  "Pinging %s with: `%s`", self.ip_addressip_address, " ".join(self._ping_cmd_ping_cmd)
107  )
108 
109  pinger = await asyncio.create_subprocess_exec(
110  *self._ping_cmd_ping_cmd,
111  stdin=None,
112  stdout=asyncio.subprocess.PIPE,
113  stderr=asyncio.subprocess.PIPE,
114  close_fds=False, # required for posix_spawn
115  )
116  try:
117  async with asyncio.timeout(self._count_count + PING_TIMEOUT):
118  out_data, out_error = await pinger.communicate()
119 
120  if out_data:
121  _LOGGER.debug(
122  "Output of command: `%s`, return code: %s:\n%s",
123  " ".join(self._ping_cmd_ping_cmd),
124  pinger.returncode,
125  out_data,
126  )
127  if out_error:
128  _LOGGER.debug(
129  "Error of command: `%s`, return code: %s:\n%s",
130  " ".join(self._ping_cmd_ping_cmd),
131  pinger.returncode,
132  out_error,
133  )
134 
135  if pinger.returncode and pinger.returncode > 1:
136  # returncode of 1 means the host is unreachable
137  _LOGGER.exception(
138  "Error running command: `%s`, return code: %s",
139  " ".join(self._ping_cmd_ping_cmd),
140  pinger.returncode,
141  )
142 
143  if "max/" not in str(out_data):
144  match = PING_MATCHER_BUSYBOX.search(
145  str(out_data).rsplit("\n", maxsplit=1)[-1]
146  )
147  if TYPE_CHECKING:
148  assert match is not None
149  rtt_min, rtt_avg, rtt_max = match.groups()
150  return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max}
151  match = PING_MATCHER.search(str(out_data).rsplit("\n", maxsplit=1)[-1])
152  if TYPE_CHECKING:
153  assert match is not None
154  rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
155  except TimeoutError:
156  _LOGGER.debug(
157  "Timed out running command: `%s`, after: %s",
158  " ".join(self._ping_cmd_ping_cmd),
159  self._count_count + PING_TIMEOUT,
160  )
161 
162  if pinger:
163  with suppress(TypeError):
164  await pinger.kill() # type: ignore[func-returns-value]
165  del pinger
166 
167  return None
168  except AttributeError as err:
169  _LOGGER.debug("Error matching ping output: %s", err)
170  return None
171  return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev}
172 
173  async def async_update(self) -> None:
174  """Retrieve the latest details from the host."""
175  self.datadata = await self.async_pingasync_ping()
176  self.is_aliveis_alive = self.datadata is not None
None __init__(self, HomeAssistant hass, str host, int count, bool|None privileged)
Definition: helpers.py:46
None __init__(self, HomeAssistant hass, str host, int count, bool|None privileged)
Definition: helpers.py:90
None __init__(self, HomeAssistant hass, str host, int count)
Definition: helpers.py:34