2021-01-14 08:09:08 +00:00
|
|
|
"""The dhcp integration."""
|
2024-03-08 13:15:26 +00:00
|
|
|
|
2022-02-15 17:02:52 +00:00
|
|
|
from __future__ import annotations
|
2021-01-14 08:09:08 +00:00
|
|
|
|
2022-03-01 04:49:44 +00:00
|
|
|
import asyncio
|
2024-02-09 04:23:42 +00:00
|
|
|
from collections.abc import Callable
|
2021-11-23 12:35:53 +00:00
|
|
|
from dataclasses import dataclass
|
2021-03-28 19:47:28 +00:00
|
|
|
from datetime import timedelta
|
2022-08-23 14:35:20 +00:00
|
|
|
from fnmatch import translate
|
|
|
|
from functools import lru_cache
|
2024-02-04 22:50:08 +00:00
|
|
|
import itertools
|
2021-01-14 08:09:08 +00:00
|
|
|
import logging
|
2022-08-23 14:35:20 +00:00
|
|
|
import re
|
2024-02-09 04:23:42 +00:00
|
|
|
from typing import Any, Final
|
2021-01-14 08:09:08 +00:00
|
|
|
|
2024-02-09 04:23:42 +00:00
|
|
|
import aiodhcpwatcher
|
2021-03-28 19:47:28 +00:00
|
|
|
from aiodiscover import DiscoverHosts
|
|
|
|
from aiodiscover.discovery import (
|
|
|
|
HOSTNAME as DISCOVERY_HOSTNAME,
|
|
|
|
IP_ADDRESS as DISCOVERY_IP_ADDRESS,
|
|
|
|
MAC_ADDRESS as DISCOVERY_MAC_ADDRESS,
|
|
|
|
)
|
2023-12-21 19:36:09 +00:00
|
|
|
from cached_ipaddress import cached_ip_addresses
|
2021-01-14 08:09:08 +00:00
|
|
|
|
2021-10-13 15:37:14 +00:00
|
|
|
from homeassistant import config_entries
|
2022-09-13 00:50:44 +00:00
|
|
|
from homeassistant.components.device_tracker import (
|
2021-01-16 00:01:37 +00:00
|
|
|
ATTR_HOST_NAME,
|
|
|
|
ATTR_IP,
|
|
|
|
ATTR_MAC,
|
|
|
|
ATTR_SOURCE_TYPE,
|
2022-02-19 15:01:34 +00:00
|
|
|
CONNECTED_DEVICE_REGISTERED,
|
2021-01-16 00:01:37 +00:00
|
|
|
DOMAIN as DEVICE_TRACKER_DOMAIN,
|
2022-07-31 13:51:04 +00:00
|
|
|
SourceType,
|
2021-01-16 00:01:37 +00:00
|
|
|
)
|
|
|
|
from homeassistant.const import (
|
|
|
|
EVENT_HOMEASSISTANT_STARTED,
|
|
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
|
|
STATE_HOME,
|
|
|
|
)
|
2024-04-04 21:48:36 +00:00
|
|
|
from homeassistant.core import (
|
|
|
|
Event,
|
|
|
|
EventStateChangedData,
|
|
|
|
HomeAssistant,
|
|
|
|
State,
|
|
|
|
callback,
|
|
|
|
)
|
2021-11-23 12:35:53 +00:00
|
|
|
from homeassistant.data_entry_flow import BaseServiceInfo
|
2023-05-29 19:00:08 +00:00
|
|
|
from homeassistant.helpers import config_validation as cv, discovery_flow
|
2022-02-15 17:02:52 +00:00
|
|
|
from homeassistant.helpers.device_registry import (
|
|
|
|
CONNECTION_NETWORK_MAC,
|
|
|
|
DeviceRegistry,
|
|
|
|
async_get,
|
|
|
|
format_mac,
|
|
|
|
)
|
2022-02-19 15:01:34 +00:00
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
2021-03-28 19:47:28 +00:00
|
|
|
from homeassistant.helpers.event import (
|
|
|
|
async_track_state_added_domain,
|
|
|
|
async_track_time_interval,
|
|
|
|
)
|
2024-03-08 18:35:17 +00:00
|
|
|
from homeassistant.helpers.typing import ConfigType
|
2022-03-01 04:49:44 +00:00
|
|
|
from homeassistant.loader import DHCPMatcher, async_get_dhcp
|
2021-01-14 08:09:08 +00:00
|
|
|
|
2023-05-29 19:00:08 +00:00
|
|
|
from .const import DOMAIN
|
|
|
|
|
|
|
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
|
|
|
|
2021-01-14 08:09:08 +00:00
|
|
|
FILTER = "udp and (port 67 or 68)"
|
2021-11-16 11:19:50 +00:00
|
|
|
HOSTNAME: Final = "hostname"
|
|
|
|
MAC_ADDRESS: Final = "macaddress"
|
|
|
|
IP_ADDRESS: Final = "ip"
|
2022-02-15 17:02:52 +00:00
|
|
|
REGISTERED_DEVICES: Final = "registered_devices"
|
2021-03-28 19:47:28 +00:00
|
|
|
SCAN_INTERVAL = timedelta(minutes=60)
|
2021-01-14 08:09:08 +00:00
|
|
|
|
2022-02-15 17:02:52 +00:00
|
|
|
|
2021-01-14 08:09:08 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2023-04-04 10:44:59 +00:00
|
|
|
@dataclass(slots=True)
|
2021-11-23 12:35:53 +00:00
|
|
|
class DhcpServiceInfo(BaseServiceInfo):
|
2021-11-16 11:19:50 +00:00
|
|
|
"""Prepared info from dhcp entries."""
|
|
|
|
|
2021-11-25 23:13:27 +00:00
|
|
|
ip: str
|
2021-11-16 11:19:50 +00:00
|
|
|
hostname: str
|
|
|
|
macaddress: str
|
|
|
|
|
|
|
|
|
2024-02-04 22:50:08 +00:00
|
|
|
@dataclass(slots=True)
|
|
|
|
class DhcpMatchers:
|
|
|
|
"""Prepared info from dhcp entries."""
|
|
|
|
|
|
|
|
registered_devices_domains: set[str]
|
|
|
|
no_oui_matchers: dict[str, list[DHCPMatcher]]
|
|
|
|
oui_matchers: dict[str, list[DHCPMatcher]]
|
|
|
|
|
|
|
|
|
|
|
|
def async_index_integration_matchers(
|
|
|
|
integration_matchers: list[DHCPMatcher],
|
|
|
|
) -> DhcpMatchers:
|
|
|
|
"""Index the integration matchers.
|
|
|
|
|
|
|
|
We have three types of matchers:
|
|
|
|
|
|
|
|
1. Registered devices
|
|
|
|
2. Devices with no OUI - index by first char of lower() hostname
|
|
|
|
3. Devices with OUI - index by OUI
|
|
|
|
"""
|
|
|
|
registered_devices_domains: set[str] = set()
|
|
|
|
no_oui_matchers: dict[str, list[DHCPMatcher]] = {}
|
|
|
|
oui_matchers: dict[str, list[DHCPMatcher]] = {}
|
|
|
|
for matcher in integration_matchers:
|
|
|
|
domain = matcher["domain"]
|
|
|
|
if REGISTERED_DEVICES in matcher:
|
|
|
|
registered_devices_domains.add(domain)
|
|
|
|
continue
|
|
|
|
|
|
|
|
if mac_address := matcher.get(MAC_ADDRESS):
|
|
|
|
oui_matchers.setdefault(mac_address[:6], []).append(matcher)
|
|
|
|
continue
|
|
|
|
|
|
|
|
if hostname := matcher.get(HOSTNAME):
|
|
|
|
first_char = hostname[0].lower()
|
|
|
|
no_oui_matchers.setdefault(first_char, []).append(matcher)
|
|
|
|
|
|
|
|
return DhcpMatchers(
|
|
|
|
registered_devices_domains=registered_devices_domains,
|
|
|
|
no_oui_matchers=no_oui_matchers,
|
|
|
|
oui_matchers=oui_matchers,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-08-18 11:22:05 +00:00
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
2021-01-14 08:09:08 +00:00
|
|
|
"""Set up the dhcp component."""
|
2022-02-19 15:01:34 +00:00
|
|
|
watchers: list[WatcherBase] = []
|
|
|
|
address_data: dict[str, dict[str, str]] = {}
|
2024-02-04 22:50:08 +00:00
|
|
|
integration_matchers = async_index_integration_matchers(await async_get_dhcp(hass))
|
2022-02-19 15:01:34 +00:00
|
|
|
# For the passive classes we need to start listening
|
|
|
|
# for state changes and connect the dispatchers before
|
|
|
|
# everything else starts up or we will miss events
|
2024-03-15 00:16:19 +00:00
|
|
|
device_watcher = DeviceTrackerWatcher(hass, address_data, integration_matchers)
|
|
|
|
device_watcher.async_start()
|
|
|
|
watchers.append(device_watcher)
|
|
|
|
|
|
|
|
device_tracker_registered_watcher = DeviceTrackerRegisteredWatcher(
|
|
|
|
hass, address_data, integration_matchers
|
|
|
|
)
|
|
|
|
device_tracker_registered_watcher.async_start()
|
|
|
|
watchers.append(device_tracker_registered_watcher)
|
2021-01-14 08:09:08 +00:00
|
|
|
|
2024-02-09 04:23:42 +00:00
|
|
|
async def _async_initialize(event: Event) -> None:
|
|
|
|
await aiodhcpwatcher.async_init()
|
|
|
|
|
2024-03-15 00:16:19 +00:00
|
|
|
network_watcher = NetworkWatcher(hass, address_data, integration_matchers)
|
|
|
|
network_watcher.async_start()
|
|
|
|
watchers.append(network_watcher)
|
|
|
|
|
|
|
|
dhcp_watcher = DHCPWatcher(hass, address_data, integration_matchers)
|
|
|
|
await dhcp_watcher.async_start()
|
|
|
|
watchers.append(dhcp_watcher)
|
2021-01-14 08:09:08 +00:00
|
|
|
|
2024-02-09 04:23:42 +00:00
|
|
|
@callback
|
|
|
|
def _async_stop(event: Event) -> None:
|
2021-01-16 00:01:37 +00:00
|
|
|
for watcher in watchers:
|
2024-02-09 04:23:42 +00:00
|
|
|
watcher.async_stop()
|
2021-01-16 00:01:37 +00:00
|
|
|
|
2024-03-18 14:16:20 +00:00
|
|
|
hass.bus.async_listen_once(
|
|
|
|
EVENT_HOMEASSISTANT_STOP, _async_stop, run_immediately=True
|
|
|
|
)
|
2021-01-14 08:09:08 +00:00
|
|
|
|
2024-03-18 14:16:20 +00:00
|
|
|
hass.bus.async_listen_once(
|
|
|
|
EVENT_HOMEASSISTANT_STARTED, _async_initialize, run_immediately=True
|
|
|
|
)
|
2021-01-14 08:09:08 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
2024-03-15 00:16:19 +00:00
|
|
|
class WatcherBase:
|
2021-01-16 00:01:37 +00:00
|
|
|
"""Base class for dhcp and device tracker watching."""
|
2021-01-14 08:09:08 +00:00
|
|
|
|
2022-03-01 04:49:44 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
hass: HomeAssistant,
|
|
|
|
address_data: dict[str, dict[str, str]],
|
2024-02-04 22:50:08 +00:00
|
|
|
integration_matchers: DhcpMatchers,
|
2022-03-01 04:49:44 +00:00
|
|
|
) -> None:
|
2021-01-14 08:09:08 +00:00
|
|
|
"""Initialize class."""
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
self.hass = hass
|
|
|
|
self._integration_matchers = integration_matchers
|
2021-01-16 00:01:37 +00:00
|
|
|
self._address_data = address_data
|
2024-02-09 04:23:42 +00:00
|
|
|
self._unsub: Callable[[], None] | None = None
|
2021-01-14 08:09:08 +00:00
|
|
|
|
2024-02-09 04:23:42 +00:00
|
|
|
@callback
|
|
|
|
def async_stop(self) -> None:
|
|
|
|
"""Stop scanning for new devices on the network."""
|
|
|
|
if self._unsub:
|
|
|
|
self._unsub()
|
|
|
|
self._unsub = None
|
2022-02-19 15:01:34 +00:00
|
|
|
|
2021-10-13 15:37:14 +00:00
|
|
|
@callback
|
2022-02-26 18:37:24 +00:00
|
|
|
def async_process_client(
|
2024-02-14 01:12:38 +00:00
|
|
|
self, ip_address: str, hostname: str, unformatted_mac_address: str
|
2022-02-26 18:37:24 +00:00
|
|
|
) -> None:
|
2021-01-16 00:01:37 +00:00
|
|
|
"""Process a client."""
|
2023-12-21 19:36:09 +00:00
|
|
|
if (made_ip_address := cached_ip_addresses(ip_address)) is None:
|
|
|
|
# Ignore invalid addresses
|
|
|
|
_LOGGER.debug("Ignoring invalid IP Address: %s", ip_address)
|
|
|
|
return
|
2021-03-30 13:41:12 +00:00
|
|
|
|
|
|
|
if (
|
2023-09-20 09:54:24 +00:00
|
|
|
made_ip_address.is_link_local
|
|
|
|
or made_ip_address.is_loopback
|
|
|
|
or made_ip_address.is_unspecified
|
2021-03-30 13:41:12 +00:00
|
|
|
):
|
|
|
|
# Ignore self assigned addresses, loopback, invalid
|
2021-01-17 09:35:02 +00:00
|
|
|
return
|
|
|
|
|
2024-02-14 01:12:38 +00:00
|
|
|
formatted_mac = format_mac(unformatted_mac_address)
|
|
|
|
# Historically, the MAC address was formatted without colons
|
|
|
|
# and since all consumers of this data are expecting it to be
|
|
|
|
# formatted without colons we will continue to do so
|
|
|
|
mac_address = formatted_mac.replace(":", "")
|
|
|
|
|
2021-01-14 08:09:08 +00:00
|
|
|
data = self._address_data.get(ip_address)
|
2021-03-28 19:47:28 +00:00
|
|
|
if (
|
|
|
|
data
|
|
|
|
and data[MAC_ADDRESS] == mac_address
|
|
|
|
and data[HOSTNAME].startswith(hostname)
|
|
|
|
):
|
2021-01-14 08:09:08 +00:00
|
|
|
# If the address data is the same no need
|
|
|
|
# to process it
|
|
|
|
return
|
|
|
|
|
2021-10-13 15:37:14 +00:00
|
|
|
data = {MAC_ADDRESS: mac_address, HOSTNAME: hostname}
|
|
|
|
self._address_data[ip_address] = data
|
2021-01-14 08:09:08 +00:00
|
|
|
|
2022-03-01 04:49:44 +00:00
|
|
|
lowercase_hostname = hostname.lower()
|
|
|
|
uppercase_mac = mac_address.upper()
|
2021-01-14 08:09:08 +00:00
|
|
|
|
|
|
|
_LOGGER.debug(
|
|
|
|
"Processing updated address data for %s: mac=%s hostname=%s",
|
|
|
|
ip_address,
|
|
|
|
uppercase_mac,
|
|
|
|
lowercase_hostname,
|
|
|
|
)
|
|
|
|
|
2024-02-04 22:50:08 +00:00
|
|
|
matched_domains: set[str] = set()
|
|
|
|
matchers = self._integration_matchers
|
|
|
|
registered_devices_domains = matchers.registered_devices_domains
|
2022-02-15 17:02:52 +00:00
|
|
|
|
|
|
|
dev_reg: DeviceRegistry = async_get(self.hass)
|
|
|
|
if device := dev_reg.async_get_device(
|
2024-02-14 01:12:38 +00:00
|
|
|
connections={(CONNECTION_NETWORK_MAC, formatted_mac)}
|
2022-02-15 17:02:52 +00:00
|
|
|
):
|
|
|
|
for entry_id in device.config_entries:
|
2024-02-04 22:50:08 +00:00
|
|
|
if (
|
|
|
|
entry := self.hass.config_entries.async_get_entry(entry_id)
|
|
|
|
) and entry.domain in registered_devices_domains:
|
|
|
|
matched_domains.add(entry.domain)
|
|
|
|
|
|
|
|
oui = uppercase_mac[:6]
|
|
|
|
lowercase_hostname_first_char = (
|
|
|
|
lowercase_hostname[0] if len(lowercase_hostname) else ""
|
|
|
|
)
|
|
|
|
for matcher in itertools.chain(
|
|
|
|
matchers.no_oui_matchers.get(lowercase_hostname_first_char, ()),
|
|
|
|
matchers.oui_matchers.get(oui, ()),
|
|
|
|
):
|
2022-03-01 04:49:44 +00:00
|
|
|
domain = matcher["domain"]
|
|
|
|
if (
|
|
|
|
matcher_hostname := matcher.get(HOSTNAME)
|
2022-08-23 14:35:20 +00:00
|
|
|
) is not None and not _memorized_fnmatch(
|
|
|
|
lowercase_hostname, matcher_hostname
|
|
|
|
):
|
2021-01-14 08:09:08 +00:00
|
|
|
continue
|
|
|
|
|
2022-03-01 04:49:44 +00:00
|
|
|
_LOGGER.debug("Matched %s against %s", data, matcher)
|
|
|
|
matched_domains.add(domain)
|
2022-02-15 17:02:52 +00:00
|
|
|
|
|
|
|
for domain in matched_domains:
|
2021-10-13 15:37:14 +00:00
|
|
|
discovery_flow.async_create_flow(
|
|
|
|
self.hass,
|
2022-02-15 17:02:52 +00:00
|
|
|
domain,
|
2021-10-13 15:37:14 +00:00
|
|
|
{"source": config_entries.SOURCE_DHCP},
|
2021-11-16 11:19:50 +00:00
|
|
|
DhcpServiceInfo(
|
|
|
|
ip=ip_address,
|
|
|
|
hostname=lowercase_hostname,
|
2022-03-01 04:49:44 +00:00
|
|
|
macaddress=mac_address,
|
2021-11-16 11:19:50 +00:00
|
|
|
),
|
2021-01-14 08:09:08 +00:00
|
|
|
)
|
|
|
|
|
2021-01-16 00:01:37 +00:00
|
|
|
|
2021-03-28 19:47:28 +00:00
|
|
|
class NetworkWatcher(WatcherBase):
|
|
|
|
"""Class to query ptr records routers."""
|
|
|
|
|
2022-03-01 04:49:44 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
hass: HomeAssistant,
|
|
|
|
address_data: dict[str, dict[str, str]],
|
2024-02-04 22:50:08 +00:00
|
|
|
integration_matchers: DhcpMatchers,
|
2022-03-01 04:49:44 +00:00
|
|
|
) -> None:
|
2021-03-28 19:47:28 +00:00
|
|
|
"""Initialize class."""
|
|
|
|
super().__init__(hass, address_data, integration_matchers)
|
2022-03-01 04:49:44 +00:00
|
|
|
self._discover_hosts: DiscoverHosts | None = None
|
|
|
|
self._discover_task: asyncio.Task | None = None
|
2021-03-28 19:47:28 +00:00
|
|
|
|
2024-02-09 04:23:42 +00:00
|
|
|
@callback
|
|
|
|
def async_stop(self) -> None:
|
2021-03-28 19:47:28 +00:00
|
|
|
"""Stop scanning for new devices on the network."""
|
2024-02-09 04:23:42 +00:00
|
|
|
super().async_stop()
|
2021-03-28 19:47:28 +00:00
|
|
|
if self._discover_task:
|
|
|
|
self._discover_task.cancel()
|
|
|
|
self._discover_task = None
|
|
|
|
|
2024-02-09 04:23:42 +00:00
|
|
|
@callback
|
|
|
|
def async_start(self) -> None:
|
2021-03-28 19:47:28 +00:00
|
|
|
"""Start scanning for new devices on the network."""
|
|
|
|
self._discover_hosts = DiscoverHosts()
|
|
|
|
self._unsub = async_track_time_interval(
|
2023-04-05 14:58:02 +00:00
|
|
|
self.hass,
|
|
|
|
self.async_start_discover,
|
|
|
|
SCAN_INTERVAL,
|
|
|
|
name="DHCP network watcher",
|
2021-03-28 19:47:28 +00:00
|
|
|
)
|
|
|
|
self.async_start_discover()
|
|
|
|
|
|
|
|
@callback
|
2022-03-01 04:49:44 +00:00
|
|
|
def async_start_discover(self, *_: Any) -> None:
|
2021-03-28 19:47:28 +00:00
|
|
|
"""Start a new discovery task if one is not running."""
|
|
|
|
if self._discover_task and not self._discover_task.done():
|
|
|
|
return
|
2024-03-08 04:37:43 +00:00
|
|
|
self._discover_task = self.hass.async_create_background_task(
|
|
|
|
self.async_discover(), name="dhcp discovery", eager_start=True
|
|
|
|
)
|
2021-03-28 19:47:28 +00:00
|
|
|
|
2022-03-01 04:49:44 +00:00
|
|
|
async def async_discover(self) -> None:
|
2021-03-28 19:47:28 +00:00
|
|
|
"""Process discovery."""
|
2022-03-01 04:49:44 +00:00
|
|
|
assert self._discover_hosts is not None
|
2021-03-28 19:47:28 +00:00
|
|
|
for host in await self._discover_hosts.async_discover():
|
2021-10-13 15:37:14 +00:00
|
|
|
self.async_process_client(
|
2021-03-28 19:47:28 +00:00
|
|
|
host[DISCOVERY_IP_ADDRESS],
|
|
|
|
host[DISCOVERY_HOSTNAME],
|
2024-02-14 01:12:38 +00:00
|
|
|
host[DISCOVERY_MAC_ADDRESS],
|
2021-03-28 19:47:28 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-01-16 00:01:37 +00:00
|
|
|
class DeviceTrackerWatcher(WatcherBase):
|
|
|
|
"""Class to watch dhcp data from routers."""
|
|
|
|
|
2024-02-09 04:23:42 +00:00
|
|
|
@callback
|
|
|
|
def async_start(self) -> None:
|
2021-01-16 00:01:37 +00:00
|
|
|
"""Stop watching for new device trackers."""
|
|
|
|
self._unsub = async_track_state_added_domain(
|
|
|
|
self.hass, [DEVICE_TRACKER_DOMAIN], self._async_process_device_event
|
|
|
|
)
|
|
|
|
for state in self.hass.states.async_all(DEVICE_TRACKER_DOMAIN):
|
|
|
|
self._async_process_device_state(state)
|
|
|
|
|
|
|
|
@callback
|
2024-03-08 18:35:17 +00:00
|
|
|
def _async_process_device_event(self, event: Event[EventStateChangedData]) -> None:
|
2021-01-16 00:01:37 +00:00
|
|
|
"""Process a device tracker state change event."""
|
2022-01-19 08:00:09 +00:00
|
|
|
self._async_process_device_state(event.data["new_state"])
|
2021-01-16 00:01:37 +00:00
|
|
|
|
|
|
|
@callback
|
2023-07-24 07:11:41 +00:00
|
|
|
def _async_process_device_state(self, state: State | None) -> None:
|
2021-01-16 00:01:37 +00:00
|
|
|
"""Process a device tracker state."""
|
2023-07-24 07:11:41 +00:00
|
|
|
if state is None or state.state != STATE_HOME:
|
2021-01-16 00:01:37 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
attributes = state.attributes
|
|
|
|
|
2022-07-31 13:51:04 +00:00
|
|
|
if attributes.get(ATTR_SOURCE_TYPE) != SourceType.ROUTER:
|
2021-01-16 00:01:37 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
ip_address = attributes.get(ATTR_IP)
|
2021-07-12 15:25:16 +00:00
|
|
|
hostname = attributes.get(ATTR_HOST_NAME, "")
|
2021-01-16 00:01:37 +00:00
|
|
|
mac_address = attributes.get(ATTR_MAC)
|
|
|
|
|
2021-07-12 15:25:16 +00:00
|
|
|
if ip_address is None or mac_address is None:
|
2021-01-16 00:01:37 +00:00
|
|
|
return
|
|
|
|
|
2024-02-14 01:12:38 +00:00
|
|
|
self.async_process_client(ip_address, hostname, mac_address)
|
2021-01-16 00:01:37 +00:00
|
|
|
|
|
|
|
|
2022-02-19 15:01:34 +00:00
|
|
|
class DeviceTrackerRegisteredWatcher(WatcherBase):
|
|
|
|
"""Class to watch data from device tracker registrations."""
|
|
|
|
|
2024-02-09 04:23:42 +00:00
|
|
|
@callback
|
|
|
|
def async_start(self) -> None:
|
2022-02-19 15:01:34 +00:00
|
|
|
"""Stop watching for device tracker registrations."""
|
|
|
|
self._unsub = async_dispatcher_connect(
|
2022-02-26 18:37:24 +00:00
|
|
|
self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_data
|
2022-02-19 15:01:34 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
@callback
|
2022-02-26 18:37:24 +00:00
|
|
|
def _async_process_device_data(self, data: dict[str, str | None]) -> None:
|
2022-02-19 15:01:34 +00:00
|
|
|
"""Process a device tracker state."""
|
2022-02-26 18:37:24 +00:00
|
|
|
ip_address = data[ATTR_IP]
|
|
|
|
hostname = data[ATTR_HOST_NAME] or ""
|
|
|
|
mac_address = data[ATTR_MAC]
|
2022-02-19 15:01:34 +00:00
|
|
|
|
|
|
|
if ip_address is None or mac_address is None:
|
|
|
|
return
|
|
|
|
|
2024-02-14 01:12:38 +00:00
|
|
|
self.async_process_client(ip_address, hostname, mac_address)
|
2022-02-19 15:01:34 +00:00
|
|
|
|
|
|
|
|
2021-01-19 19:49:49 +00:00
|
|
|
class DHCPWatcher(WatcherBase):
|
2021-01-16 00:01:37 +00:00
|
|
|
"""Class to watch dhcp requests."""
|
|
|
|
|
2024-02-09 04:23:42 +00:00
|
|
|
@callback
|
|
|
|
def _async_process_dhcp_request(self, response: aiodhcpwatcher.DHCPRequest) -> None:
|
|
|
|
"""Process a dhcp request."""
|
|
|
|
self.async_process_client(
|
2024-02-14 01:12:38 +00:00
|
|
|
response.ip_address, response.hostname, response.mac_address
|
2021-01-21 07:26:58 +00:00
|
|
|
)
|
2021-03-08 23:15:22 +00:00
|
|
|
|
2024-03-15 00:16:19 +00:00
|
|
|
async def async_start(self) -> None:
|
2024-02-09 04:23:42 +00:00
|
|
|
"""Start watching for dhcp packets."""
|
2024-03-15 00:16:19 +00:00
|
|
|
self._unsub = await aiodhcpwatcher.async_start(self._async_process_dhcp_request)
|
2021-01-14 08:09:08 +00:00
|
|
|
|
|
|
|
|
2022-08-23 14:35:20 +00:00
|
|
|
@lru_cache(maxsize=4096, typed=True)
|
|
|
|
def _compile_fnmatch(pattern: str) -> re.Pattern:
|
|
|
|
"""Compile a fnmatch pattern."""
|
|
|
|
return re.compile(translate(pattern))
|
|
|
|
|
|
|
|
|
|
|
|
@lru_cache(maxsize=1024, typed=True)
|
|
|
|
def _memorized_fnmatch(name: str, pattern: str) -> bool:
|
|
|
|
"""Memorized version of fnmatch that has a larger lru_cache.
|
|
|
|
|
|
|
|
The default version of fnmatch only has a lru_cache of 256 entries.
|
|
|
|
With many devices we quickly reach that limit and end up compiling
|
|
|
|
the same pattern over and over again.
|
|
|
|
|
|
|
|
DHCP has its own memorized fnmatch with its own lru_cache
|
|
|
|
since the data is going to be relatively the same
|
|
|
|
since the devices will not change frequently
|
|
|
|
"""
|
|
|
|
return bool(_compile_fnmatch(pattern).match(name))
|