2019-04-03 15:40:03 +00:00
|
|
|
"""Tracks devices by sending a ICMP echo request (ping)."""
|
2021-03-31 13:06:49 +00:00
|
|
|
import asyncio
|
2019-12-09 13:29:39 +00:00
|
|
|
from datetime import timedelta
|
2017-01-04 20:06:09 +00:00
|
|
|
import logging
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
|
2021-06-02 16:10:33 +00:00
|
|
|
from icmplib import async_multiping
|
2017-01-04 20:06:09 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2019-12-09 13:29:39 +00:00
|
|
|
from homeassistant import const, util
|
2019-07-31 19:25:30 +00:00
|
|
|
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
2019-05-15 21:43:45 +00:00
|
|
|
from homeassistant.components.device_tracker.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_SCAN_INTERVAL,
|
|
|
|
SCAN_INTERVAL,
|
|
|
|
SOURCE_TYPE_ROUTER,
|
|
|
|
)
|
2019-12-09 13:29:39 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2021-03-31 13:06:49 +00:00
|
|
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
|
|
|
from homeassistant.util.async_ import gather_with_concurrency
|
2020-08-05 10:43:35 +00:00
|
|
|
from homeassistant.util.process import kill_subprocess
|
|
|
|
|
2021-03-31 13:06:49 +00:00
|
|
|
from .const import DOMAIN, ICMP_TIMEOUT, PING_ATTEMPTS_COUNT, PING_PRIVS, PING_TIMEOUT
|
2017-01-04 20:06:09 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2020-08-05 10:43:35 +00:00
|
|
|
PARALLEL_UPDATES = 0
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_PING_COUNT = "count"
|
2021-03-31 13:06:49 +00:00
|
|
|
CONCURRENT_PING_LIMIT = 6
|
2017-01-04 20:06:09 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
|
|
{
|
2020-02-08 14:19:46 +00:00
|
|
|
vol.Required(const.CONF_HOSTS): {cv.slug: cv.string},
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int,
|
|
|
|
}
|
|
|
|
)
|
2017-01-04 20:06:09 +00:00
|
|
|
|
|
|
|
|
2020-08-28 13:50:09 +00:00
|
|
|
class HostSubProcess:
|
2017-01-04 20:06:09 +00:00
|
|
|
"""Host object with ping detection."""
|
|
|
|
|
2021-03-31 13:06:49 +00:00
|
|
|
def __init__(self, ip_address, dev_id, hass, config, privileged):
|
2017-01-04 20:06:09 +00:00
|
|
|
"""Initialize the Host pinger."""
|
|
|
|
self.hass = hass
|
|
|
|
self.ip_address = ip_address
|
|
|
|
self.dev_id = dev_id
|
|
|
|
self._count = config[CONF_PING_COUNT]
|
2019-07-31 19:25:30 +00:00
|
|
|
if sys.platform == "win32":
|
2021-03-31 13:06:49 +00:00
|
|
|
self._ping_cmd = ["ping", "-n", "1", "-w", "1000", ip_address]
|
2017-01-04 20:06:09 +00:00
|
|
|
else:
|
2021-03-31 13:06:49 +00:00
|
|
|
self._ping_cmd = ["ping", "-n", "-q", "-c1", "-W1", ip_address]
|
2017-01-04 20:06:09 +00:00
|
|
|
|
|
|
|
def ping(self):
|
2017-04-11 16:05:27 +00:00
|
|
|
"""Send an ICMP echo request and return True if success."""
|
2021-04-25 00:39:24 +00:00
|
|
|
with subprocess.Popen(
|
2019-07-31 19:25:30 +00:00
|
|
|
self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
|
2021-04-25 00:39:24 +00:00
|
|
|
) as pinger:
|
|
|
|
try:
|
|
|
|
pinger.communicate(timeout=1 + PING_TIMEOUT)
|
|
|
|
return pinger.returncode == 0
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
kill_subprocess(pinger)
|
|
|
|
return False
|
|
|
|
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
return False
|
2017-01-04 20:06:09 +00:00
|
|
|
|
2021-03-31 13:06:49 +00:00
|
|
|
def update(self) -> bool:
|
2017-01-04 20:06:09 +00:00
|
|
|
"""Update device state by sending one or more ping messages."""
|
|
|
|
failed = 0
|
2017-06-08 05:30:51 +00:00
|
|
|
while failed < self._count: # check more times if host is unreachable
|
2017-01-04 20:06:09 +00:00
|
|
|
if self.ping():
|
|
|
|
return True
|
|
|
|
failed += 1
|
|
|
|
|
2017-04-11 16:05:27 +00:00
|
|
|
_LOGGER.debug("No response from %s failed=%d", self.ip_address, failed)
|
2021-03-31 13:06:49 +00:00
|
|
|
return False
|
2017-01-04 20:06:09 +00:00
|
|
|
|
|
|
|
|
2021-03-31 13:06:49 +00:00
|
|
|
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Set up the Host objects and return the update function."""
|
2020-08-28 13:50:09 +00:00
|
|
|
|
2021-03-31 13:06:49 +00:00
|
|
|
privileged = hass.data[DOMAIN][PING_PRIVS]
|
|
|
|
ip_to_dev_id = {ip: dev_id for (dev_id, ip) in config[const.CONF_HOSTS].items()}
|
2019-07-31 19:25:30 +00:00
|
|
|
interval = config.get(
|
|
|
|
CONF_SCAN_INTERVAL,
|
2021-03-31 13:06:49 +00:00
|
|
|
timedelta(seconds=len(ip_to_dev_id) * config[CONF_PING_COUNT]) + SCAN_INTERVAL,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
|
|
|
_LOGGER.debug(
|
|
|
|
"Started ping tracker with interval=%s on hosts: %s",
|
|
|
|
interval,
|
2021-03-31 13:06:49 +00:00
|
|
|
",".join(ip_to_dev_id.keys()),
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2017-12-29 15:18:39 +00:00
|
|
|
|
2021-03-31 13:06:49 +00:00
|
|
|
if privileged is None:
|
|
|
|
hosts = [
|
|
|
|
HostSubProcess(ip, dev_id, hass, config, privileged)
|
|
|
|
for (dev_id, ip) in config[const.CONF_HOSTS].items()
|
|
|
|
]
|
|
|
|
|
|
|
|
async def async_update(now):
|
|
|
|
"""Update all the hosts on every interval time."""
|
|
|
|
results = await gather_with_concurrency(
|
|
|
|
CONCURRENT_PING_LIMIT,
|
2021-07-19 08:46:09 +00:00
|
|
|
*(hass.async_add_executor_job(host.update) for host in hosts),
|
2021-03-31 13:06:49 +00:00
|
|
|
)
|
|
|
|
await asyncio.gather(
|
2021-07-19 08:46:09 +00:00
|
|
|
*(
|
2021-03-31 13:06:49 +00:00
|
|
|
async_see(dev_id=host.dev_id, source_type=SOURCE_TYPE_ROUTER)
|
|
|
|
for idx, host in enumerate(hosts)
|
|
|
|
if results[idx]
|
2021-07-19 08:46:09 +00:00
|
|
|
)
|
2021-03-31 13:06:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
async def async_update(now):
|
|
|
|
"""Update all the hosts on every interval time."""
|
2021-06-02 16:10:33 +00:00
|
|
|
responses = await async_multiping(
|
|
|
|
list(ip_to_dev_id),
|
|
|
|
count=PING_ATTEMPTS_COUNT,
|
|
|
|
timeout=ICMP_TIMEOUT,
|
|
|
|
privileged=privileged,
|
2021-03-31 13:06:49 +00:00
|
|
|
)
|
|
|
|
_LOGGER.debug("Multiping responses: %s", responses)
|
|
|
|
await asyncio.gather(
|
2021-07-19 08:46:09 +00:00
|
|
|
*(
|
2021-03-31 13:06:49 +00:00
|
|
|
async_see(dev_id=dev_id, source_type=SOURCE_TYPE_ROUTER)
|
|
|
|
for idx, dev_id in enumerate(ip_to_dev_id.values())
|
|
|
|
if responses[idx].is_alive
|
2021-07-19 08:46:09 +00:00
|
|
|
)
|
2021-03-31 13:06:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
async def _async_update_interval(now):
|
2017-12-29 15:18:39 +00:00
|
|
|
try:
|
2021-03-31 13:06:49 +00:00
|
|
|
await async_update(now)
|
2017-12-29 15:18:39 +00:00
|
|
|
finally:
|
2021-04-15 06:49:28 +00:00
|
|
|
if not hass.is_stopping:
|
|
|
|
async_track_point_in_utc_time(
|
|
|
|
hass, _async_update_interval, util.dt.utcnow() + interval
|
|
|
|
)
|
2017-12-29 15:18:39 +00:00
|
|
|
|
2021-03-31 13:06:49 +00:00
|
|
|
await _async_update_interval(None)
|
2017-12-29 15:18:39 +00:00
|
|
|
return True
|