Convert dhcp watcher to asyncio (#109938)
parent
261f9c5d62
commit
122ac059bc
|
@ -3,19 +3,17 @@ from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable
|
||||||
import contextlib
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from fnmatch import translate
|
from fnmatch import translate
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import threading
|
from typing import Any, Final
|
||||||
from typing import TYPE_CHECKING, Any, Final, cast
|
|
||||||
|
|
||||||
|
import aiodhcpwatcher
|
||||||
from aiodiscover import DiscoverHosts
|
from aiodiscover import DiscoverHosts
|
||||||
from aiodiscover.discovery import (
|
from aiodiscover.discovery import (
|
||||||
HOSTNAME as DISCOVERY_HOSTNAME,
|
HOSTNAME as DISCOVERY_HOSTNAME,
|
||||||
|
@ -23,8 +21,6 @@ from aiodiscover.discovery import (
|
||||||
MAC_ADDRESS as DISCOVERY_MAC_ADDRESS,
|
MAC_ADDRESS as DISCOVERY_MAC_ADDRESS,
|
||||||
)
|
)
|
||||||
from cached_ipaddress import cached_ip_addresses
|
from cached_ipaddress import cached_ip_addresses
|
||||||
from scapy.config import conf
|
|
||||||
from scapy.error import Scapy_Exception
|
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
|
@ -61,20 +57,13 @@ from homeassistant.loader import DHCPMatcher, async_get_dhcp
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from scapy.packet import Packet
|
|
||||||
from scapy.sendrecv import AsyncSniffer
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
FILTER = "udp and (port 67 or 68)"
|
FILTER = "udp and (port 67 or 68)"
|
||||||
REQUESTED_ADDR = "requested_addr"
|
|
||||||
MESSAGE_TYPE = "message-type"
|
|
||||||
HOSTNAME: Final = "hostname"
|
HOSTNAME: Final = "hostname"
|
||||||
MAC_ADDRESS: Final = "macaddress"
|
MAC_ADDRESS: Final = "macaddress"
|
||||||
IP_ADDRESS: Final = "ip"
|
IP_ADDRESS: Final = "ip"
|
||||||
REGISTERED_DEVICES: Final = "registered_devices"
|
REGISTERED_DEVICES: Final = "registered_devices"
|
||||||
DHCP_REQUEST = 3
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=60)
|
SCAN_INTERVAL = timedelta(minutes=60)
|
||||||
|
|
||||||
|
|
||||||
|
@ -144,22 +133,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
# everything else starts up or we will miss events
|
# everything else starts up or we will miss events
|
||||||
for passive_cls in (DeviceTrackerRegisteredWatcher, DeviceTrackerWatcher):
|
for passive_cls in (DeviceTrackerRegisteredWatcher, DeviceTrackerWatcher):
|
||||||
passive_watcher = passive_cls(hass, address_data, integration_matchers)
|
passive_watcher = passive_cls(hass, address_data, integration_matchers)
|
||||||
await passive_watcher.async_start()
|
passive_watcher.async_start()
|
||||||
watchers.append(passive_watcher)
|
watchers.append(passive_watcher)
|
||||||
|
|
||||||
async def _initialize(event: Event) -> None:
|
async def _async_initialize(event: Event) -> None:
|
||||||
|
await aiodhcpwatcher.async_init()
|
||||||
|
|
||||||
for active_cls in (DHCPWatcher, NetworkWatcher):
|
for active_cls in (DHCPWatcher, NetworkWatcher):
|
||||||
active_watcher = active_cls(hass, address_data, integration_matchers)
|
active_watcher = active_cls(hass, address_data, integration_matchers)
|
||||||
await active_watcher.async_start()
|
active_watcher.async_start()
|
||||||
watchers.append(active_watcher)
|
watchers.append(active_watcher)
|
||||||
|
|
||||||
async def _async_stop(event: Event) -> None:
|
@callback
|
||||||
|
def _async_stop(event: Event) -> None:
|
||||||
for watcher in watchers:
|
for watcher in watchers:
|
||||||
await watcher.async_stop()
|
watcher.async_stop()
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop)
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _initialize)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_initialize)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -178,21 +170,20 @@ class WatcherBase(ABC):
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._integration_matchers = integration_matchers
|
self._integration_matchers = integration_matchers
|
||||||
self._address_data = address_data
|
self._address_data = address_data
|
||||||
|
self._unsub: Callable[[], None] | None = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_stop(self) -> None:
|
||||||
|
"""Stop scanning for new devices on the network."""
|
||||||
|
if self._unsub:
|
||||||
|
self._unsub()
|
||||||
|
self._unsub = None
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def async_stop(self) -> None:
|
@callback
|
||||||
"""Stop the watcher."""
|
def async_start(self) -> None:
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def async_start(self) -> None:
|
|
||||||
"""Start the watcher."""
|
"""Start the watcher."""
|
||||||
|
|
||||||
def process_client(self, ip_address: str, hostname: str, mac_address: str) -> None:
|
|
||||||
"""Process a client."""
|
|
||||||
self.hass.loop.call_soon_threadsafe(
|
|
||||||
self.async_process_client, ip_address, hostname, mac_address
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_process_client(
|
def async_process_client(
|
||||||
self, ip_address: str, hostname: str, mac_address: str
|
self, ip_address: str, hostname: str, mac_address: str
|
||||||
|
@ -291,20 +282,19 @@ class NetworkWatcher(WatcherBase):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize class."""
|
"""Initialize class."""
|
||||||
super().__init__(hass, address_data, integration_matchers)
|
super().__init__(hass, address_data, integration_matchers)
|
||||||
self._unsub: Callable[[], None] | None = None
|
|
||||||
self._discover_hosts: DiscoverHosts | None = None
|
self._discover_hosts: DiscoverHosts | None = None
|
||||||
self._discover_task: asyncio.Task | None = None
|
self._discover_task: asyncio.Task | None = None
|
||||||
|
|
||||||
async def async_stop(self) -> None:
|
@callback
|
||||||
|
def async_stop(self) -> None:
|
||||||
"""Stop scanning for new devices on the network."""
|
"""Stop scanning for new devices on the network."""
|
||||||
if self._unsub:
|
super().async_stop()
|
||||||
self._unsub()
|
|
||||||
self._unsub = None
|
|
||||||
if self._discover_task:
|
if self._discover_task:
|
||||||
self._discover_task.cancel()
|
self._discover_task.cancel()
|
||||||
self._discover_task = None
|
self._discover_task = None
|
||||||
|
|
||||||
async def async_start(self) -> None:
|
@callback
|
||||||
|
def async_start(self) -> None:
|
||||||
"""Start scanning for new devices on the network."""
|
"""Start scanning for new devices on the network."""
|
||||||
self._discover_hosts = DiscoverHosts()
|
self._discover_hosts = DiscoverHosts()
|
||||||
self._unsub = async_track_time_interval(
|
self._unsub = async_track_time_interval(
|
||||||
|
@ -336,23 +326,8 @@ class NetworkWatcher(WatcherBase):
|
||||||
class DeviceTrackerWatcher(WatcherBase):
|
class DeviceTrackerWatcher(WatcherBase):
|
||||||
"""Class to watch dhcp data from routers."""
|
"""Class to watch dhcp data from routers."""
|
||||||
|
|
||||||
def __init__(
|
@callback
|
||||||
self,
|
def async_start(self) -> None:
|
||||||
hass: HomeAssistant,
|
|
||||||
address_data: dict[str, dict[str, str]],
|
|
||||||
integration_matchers: DhcpMatchers,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize class."""
|
|
||||||
super().__init__(hass, address_data, integration_matchers)
|
|
||||||
self._unsub: Callable[[], None] | None = None
|
|
||||||
|
|
||||||
async def async_stop(self) -> None:
|
|
||||||
"""Stop watching for new device trackers."""
|
|
||||||
if self._unsub:
|
|
||||||
self._unsub()
|
|
||||||
self._unsub = None
|
|
||||||
|
|
||||||
async def async_start(self) -> None:
|
|
||||||
"""Stop watching for new device trackers."""
|
"""Stop watching for new device trackers."""
|
||||||
self._unsub = async_track_state_added_domain(
|
self._unsub = async_track_state_added_domain(
|
||||||
self.hass, [DEVICE_TRACKER_DOMAIN], self._async_process_device_event
|
self.hass, [DEVICE_TRACKER_DOMAIN], self._async_process_device_event
|
||||||
|
@ -391,23 +366,8 @@ class DeviceTrackerWatcher(WatcherBase):
|
||||||
class DeviceTrackerRegisteredWatcher(WatcherBase):
|
class DeviceTrackerRegisteredWatcher(WatcherBase):
|
||||||
"""Class to watch data from device tracker registrations."""
|
"""Class to watch data from device tracker registrations."""
|
||||||
|
|
||||||
def __init__(
|
@callback
|
||||||
self,
|
def async_start(self) -> None:
|
||||||
hass: HomeAssistant,
|
|
||||||
address_data: dict[str, dict[str, str]],
|
|
||||||
integration_matchers: DhcpMatchers,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize class."""
|
|
||||||
super().__init__(hass, address_data, integration_matchers)
|
|
||||||
self._unsub: Callable[[], None] | None = None
|
|
||||||
|
|
||||||
async def async_stop(self) -> None:
|
|
||||||
"""Stop watching for device tracker registrations."""
|
|
||||||
if self._unsub:
|
|
||||||
self._unsub()
|
|
||||||
self._unsub = None
|
|
||||||
|
|
||||||
async def async_start(self) -> None:
|
|
||||||
"""Stop watching for device tracker registrations."""
|
"""Stop watching for device tracker registrations."""
|
||||||
self._unsub = async_dispatcher_connect(
|
self._unsub = async_dispatcher_connect(
|
||||||
self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_data
|
self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_data
|
||||||
|
@ -429,114 +389,17 @@ class DeviceTrackerRegisteredWatcher(WatcherBase):
|
||||||
class DHCPWatcher(WatcherBase):
|
class DHCPWatcher(WatcherBase):
|
||||||
"""Class to watch dhcp requests."""
|
"""Class to watch dhcp requests."""
|
||||||
|
|
||||||
def __init__(
|
@callback
|
||||||
self,
|
def _async_process_dhcp_request(self, response: aiodhcpwatcher.DHCPRequest) -> None:
|
||||||
hass: HomeAssistant,
|
"""Process a dhcp request."""
|
||||||
address_data: dict[str, dict[str, str]],
|
self.async_process_client(
|
||||||
integration_matchers: DhcpMatchers,
|
response.ip_address, response.hostname, _format_mac(response.mac_address)
|
||||||
) -> None:
|
|
||||||
"""Initialize class."""
|
|
||||||
super().__init__(hass, address_data, integration_matchers)
|
|
||||||
self._sniffer: AsyncSniffer | None = None
|
|
||||||
self._started = threading.Event()
|
|
||||||
|
|
||||||
async def async_stop(self) -> None:
|
|
||||||
"""Stop watching for new device trackers."""
|
|
||||||
await self.hass.async_add_executor_job(self._stop)
|
|
||||||
|
|
||||||
def _stop(self) -> None:
|
|
||||||
"""Stop the thread."""
|
|
||||||
if self._started.is_set():
|
|
||||||
assert self._sniffer is not None
|
|
||||||
self._sniffer.stop()
|
|
||||||
|
|
||||||
async def async_start(self) -> None:
|
|
||||||
"""Start watching for dhcp packets."""
|
|
||||||
await self.hass.async_add_executor_job(self._start)
|
|
||||||
|
|
||||||
def _start(self) -> None:
|
|
||||||
"""Start watching for dhcp packets."""
|
|
||||||
# Local import because importing from scapy has side effects such as opening
|
|
||||||
# sockets
|
|
||||||
from scapy import arch # pylint: disable=import-outside-toplevel # noqa: F401
|
|
||||||
from scapy.layers.dhcp import DHCP # pylint: disable=import-outside-toplevel
|
|
||||||
from scapy.layers.inet import IP # pylint: disable=import-outside-toplevel
|
|
||||||
from scapy.layers.l2 import Ether # pylint: disable=import-outside-toplevel
|
|
||||||
|
|
||||||
#
|
|
||||||
# Importing scapy.sendrecv will cause a scapy resync which will
|
|
||||||
# import scapy.arch.read_routes which will import scapy.sendrecv
|
|
||||||
#
|
|
||||||
# We avoid this circular import by importing arch above to ensure
|
|
||||||
# the module is loaded and avoid the problem
|
|
||||||
#
|
|
||||||
from scapy.sendrecv import ( # pylint: disable=import-outside-toplevel
|
|
||||||
AsyncSniffer,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _handle_dhcp_packet(packet: Packet) -> None:
|
@callback
|
||||||
"""Process a dhcp packet."""
|
def async_start(self) -> None:
|
||||||
if DHCP not in packet:
|
"""Start watching for dhcp packets."""
|
||||||
return
|
self._unsub = aiodhcpwatcher.start(self._async_process_dhcp_request)
|
||||||
|
|
||||||
options_dict = _dhcp_options_as_dict(packet[DHCP].options)
|
|
||||||
if options_dict.get(MESSAGE_TYPE) != DHCP_REQUEST:
|
|
||||||
# Not a DHCP request
|
|
||||||
return
|
|
||||||
|
|
||||||
ip_address = options_dict.get(REQUESTED_ADDR) or cast(str, packet[IP].src)
|
|
||||||
assert isinstance(ip_address, str)
|
|
||||||
hostname = ""
|
|
||||||
if (hostname_bytes := options_dict.get(HOSTNAME)) and isinstance(
|
|
||||||
hostname_bytes, bytes
|
|
||||||
):
|
|
||||||
with contextlib.suppress(AttributeError, UnicodeDecodeError):
|
|
||||||
hostname = hostname_bytes.decode()
|
|
||||||
mac_address = _format_mac(cast(str, packet[Ether].src))
|
|
||||||
|
|
||||||
if ip_address is not None and mac_address is not None:
|
|
||||||
self.process_client(ip_address, hostname, mac_address)
|
|
||||||
|
|
||||||
# disable scapy promiscuous mode as we do not need it
|
|
||||||
conf.sniff_promisc = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
_verify_l2socket_setup(FILTER)
|
|
||||||
except (Scapy_Exception, OSError) as ex:
|
|
||||||
if os.geteuid() == 0:
|
|
||||||
_LOGGER.error("Cannot watch for dhcp packets: %s", ex)
|
|
||||||
else:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Cannot watch for dhcp packets without root or CAP_NET_RAW: %s", ex
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
_verify_working_pcap(FILTER)
|
|
||||||
except (Scapy_Exception, ImportError) as ex:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Cannot watch for dhcp packets without a functional packet filter: %s",
|
|
||||||
ex,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._sniffer = AsyncSniffer(
|
|
||||||
filter=FILTER,
|
|
||||||
started_callback=self._started.set,
|
|
||||||
prn=_handle_dhcp_packet,
|
|
||||||
store=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._sniffer.start()
|
|
||||||
if self._sniffer.thread:
|
|
||||||
self._sniffer.thread.name = self.__class__.__name__
|
|
||||||
|
|
||||||
|
|
||||||
def _dhcp_options_as_dict(
|
|
||||||
dhcp_options: Iterable[tuple[str, int | bytes | None]],
|
|
||||||
) -> dict[str, str | int | bytes | None]:
|
|
||||||
"""Extract data from packet options as a dict."""
|
|
||||||
return {option[0]: option[1] for option in dhcp_options if len(option) >= 2}
|
|
||||||
|
|
||||||
|
|
||||||
def _format_mac(mac_address: str) -> str:
|
def _format_mac(mac_address: str) -> str:
|
||||||
|
@ -544,33 +407,6 @@ def _format_mac(mac_address: str) -> str:
|
||||||
return format_mac(mac_address).replace(":", "")
|
return format_mac(mac_address).replace(":", "")
|
||||||
|
|
||||||
|
|
||||||
def _verify_l2socket_setup(cap_filter: str) -> None:
|
|
||||||
"""Create a socket using the scapy configured l2socket.
|
|
||||||
|
|
||||||
Try to create the socket
|
|
||||||
to see if we have permissions
|
|
||||||
since AsyncSniffer will do it another
|
|
||||||
thread so we will not be able to capture
|
|
||||||
any permission or bind errors.
|
|
||||||
"""
|
|
||||||
conf.L2socket(filter=cap_filter)
|
|
||||||
|
|
||||||
|
|
||||||
def _verify_working_pcap(cap_filter: str) -> None:
|
|
||||||
"""Verify we can create a packet filter.
|
|
||||||
|
|
||||||
If we cannot create a filter we will be listening for
|
|
||||||
all traffic which is too intensive.
|
|
||||||
"""
|
|
||||||
# Local import because importing from scapy has side effects such as opening
|
|
||||||
# sockets
|
|
||||||
from scapy.arch.common import ( # pylint: disable=import-outside-toplevel
|
|
||||||
compile_filter,
|
|
||||||
)
|
|
||||||
|
|
||||||
compile_filter(cap_filter)
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=4096, typed=True)
|
@lru_cache(maxsize=4096, typed=True)
|
||||||
def _compile_fnmatch(pattern: str) -> re.Pattern:
|
def _compile_fnmatch(pattern: str) -> re.Pattern:
|
||||||
"""Compile a fnmatch pattern."""
|
"""Compile a fnmatch pattern."""
|
||||||
|
|
|
@ -5,10 +5,16 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/dhcp",
|
"documentation": "https://www.home-assistant.io/integrations/dhcp",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aiodiscover", "dnspython", "pyroute2", "scapy"],
|
"loggers": [
|
||||||
|
"aiodiscover",
|
||||||
|
"aiodhcpwatcher",
|
||||||
|
"dnspython",
|
||||||
|
"pyroute2",
|
||||||
|
"scapy"
|
||||||
|
],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"scapy==2.5.0",
|
"aiodhcpwatcher==0.8.0",
|
||||||
"aiodiscover==1.6.1",
|
"aiodiscover==1.6.1",
|
||||||
"cached_ipaddress==0.3.0"
|
"cached_ipaddress==0.3.0"
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# Automatically generated by gen_requirements_all.py, do not edit
|
# Automatically generated by gen_requirements_all.py, do not edit
|
||||||
|
|
||||||
|
aiodhcpwatcher==0.8.0
|
||||||
aiodiscover==1.6.1
|
aiodiscover==1.6.1
|
||||||
aiohttp-fast-url-dispatcher==0.3.0
|
aiohttp-fast-url-dispatcher==0.3.0
|
||||||
aiohttp-zlib-ng==0.3.1
|
aiohttp-zlib-ng==0.3.1
|
||||||
|
@ -51,7 +52,6 @@ PyTurboJPEG==1.7.1
|
||||||
pyudev==0.23.2
|
pyudev==0.23.2
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
scapy==2.5.0
|
|
||||||
SQLAlchemy==2.0.25
|
SQLAlchemy==2.0.25
|
||||||
typing-extensions>=4.9.0,<5.0
|
typing-extensions>=4.9.0,<5.0
|
||||||
ulid-transform==0.9.0
|
ulid-transform==0.9.0
|
||||||
|
|
|
@ -220,6 +220,9 @@ aiobotocore==2.9.1
|
||||||
# homeassistant.components.comelit
|
# homeassistant.components.comelit
|
||||||
aiocomelit==0.8.3
|
aiocomelit==0.8.3
|
||||||
|
|
||||||
|
# homeassistant.components.dhcp
|
||||||
|
aiodhcpwatcher==0.8.0
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodiscover==1.6.1
|
aiodiscover==1.6.1
|
||||||
|
|
||||||
|
@ -2485,9 +2488,6 @@ samsungtvws[async,encrypted]==2.6.0
|
||||||
# homeassistant.components.satel_integra
|
# homeassistant.components.satel_integra
|
||||||
satel-integra==0.3.7
|
satel-integra==0.3.7
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
|
||||||
scapy==2.5.0
|
|
||||||
|
|
||||||
# homeassistant.components.screenlogic
|
# homeassistant.components.screenlogic
|
||||||
screenlogicpy==0.10.0
|
screenlogicpy==0.10.0
|
||||||
|
|
||||||
|
|
|
@ -199,6 +199,9 @@ aiobotocore==2.9.1
|
||||||
# homeassistant.components.comelit
|
# homeassistant.components.comelit
|
||||||
aiocomelit==0.8.3
|
aiocomelit==0.8.3
|
||||||
|
|
||||||
|
# homeassistant.components.dhcp
|
||||||
|
aiodhcpwatcher==0.8.0
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodiscover==1.6.1
|
aiodiscover==1.6.1
|
||||||
|
|
||||||
|
@ -1895,9 +1898,6 @@ samsungctl[websocket]==0.7.1
|
||||||
# homeassistant.components.samsungtv
|
# homeassistant.components.samsungtv
|
||||||
samsungtvws[async,encrypted]==2.6.0
|
samsungtvws[async,encrypted]==2.6.0
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
|
||||||
scapy==2.5.0
|
|
||||||
|
|
||||||
# homeassistant.components.screenlogic
|
# homeassistant.components.screenlogic
|
||||||
screenlogicpy==0.10.0
|
screenlogicpy==0.10.0
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,14 @@ from collections.abc import Awaitable, Callable
|
||||||
import datetime
|
import datetime
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import aiodhcpwatcher
|
||||||
import pytest
|
import pytest
|
||||||
from scapy import arch # noqa: F401
|
from scapy import (
|
||||||
|
arch, # noqa: F401
|
||||||
|
interfaces,
|
||||||
|
)
|
||||||
from scapy.error import Scapy_Exception
|
from scapy.error import Scapy_Exception
|
||||||
from scapy.layers.dhcp import DHCP
|
from scapy.layers.dhcp import DHCP
|
||||||
from scapy.layers.l2 import Ether
|
from scapy.layers.l2 import Ether
|
||||||
|
@ -140,29 +144,18 @@ async def _async_get_handle_dhcp_packet(
|
||||||
{},
|
{},
|
||||||
integration_matchers,
|
integration_matchers,
|
||||||
)
|
)
|
||||||
async_handle_dhcp_packet: Callable[[Any], Awaitable[None]] | None = None
|
with patch("aiodhcpwatcher.start"):
|
||||||
|
dhcp_watcher.async_start()
|
||||||
|
|
||||||
def _mock_sniffer(*args, **kwargs):
|
def _async_handle_dhcp_request(request: aiodhcpwatcher.DHCPRequest) -> None:
|
||||||
nonlocal async_handle_dhcp_packet
|
dhcp_watcher._async_process_dhcp_request(request)
|
||||||
callback = kwargs["prn"]
|
|
||||||
|
|
||||||
async def _async_handle_dhcp_packet(packet):
|
handler = aiodhcpwatcher.make_packet_handler(_async_handle_dhcp_request)
|
||||||
await hass.async_add_executor_job(callback, packet)
|
|
||||||
|
|
||||||
async_handle_dhcp_packet = _async_handle_dhcp_packet
|
async def _async_handle_dhcp_packet(packet):
|
||||||
return MagicMock()
|
handler(packet)
|
||||||
|
|
||||||
with patch(
|
return cast("Callable[[Any], Awaitable[None]]", _async_handle_dhcp_packet)
|
||||||
"homeassistant.components.dhcp._verify_l2socket_setup",
|
|
||||||
), patch(
|
|
||||||
"scapy.arch.common.compile_filter",
|
|
||||||
), patch(
|
|
||||||
"scapy.sendrecv.AsyncSniffer",
|
|
||||||
_mock_sniffer,
|
|
||||||
):
|
|
||||||
await dhcp_watcher.async_start()
|
|
||||||
|
|
||||||
return cast("Callable[[Any], Awaitable[None]]", async_handle_dhcp_packet)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_dhcp_match_hostname_and_macaddress(hass: HomeAssistant) -> None:
|
async def test_dhcp_match_hostname_and_macaddress(hass: HomeAssistant) -> None:
|
||||||
|
@ -541,9 +534,10 @@ async def test_setup_and_stop(hass: HomeAssistant) -> None:
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
with patch("scapy.sendrecv.AsyncSniffer.start") as start_call, patch(
|
with patch.object(
|
||||||
"homeassistant.components.dhcp._verify_l2socket_setup",
|
interfaces,
|
||||||
), patch("scapy.arch.common.compile_filter"), patch(
|
"resolve_iface",
|
||||||
|
) as resolve_iface_call, patch("scapy.arch.common.compile_filter"), patch(
|
||||||
"homeassistant.components.dhcp.DiscoverHosts.async_discover"
|
"homeassistant.components.dhcp.DiscoverHosts.async_discover"
|
||||||
):
|
):
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
@ -552,7 +546,7 @@ async def test_setup_and_stop(hass: HomeAssistant) -> None:
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
start_call.assert_called_once()
|
resolve_iface_call.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_fails_as_root(
|
async def test_setup_fails_as_root(
|
||||||
|
@ -569,8 +563,9 @@ async def test_setup_fails_as_root(
|
||||||
|
|
||||||
wait_event = threading.Event()
|
wait_event = threading.Event()
|
||||||
|
|
||||||
with patch("os.geteuid", return_value=0), patch(
|
with patch("os.geteuid", return_value=0), patch.object(
|
||||||
"homeassistant.components.dhcp._verify_l2socket_setup",
|
interfaces,
|
||||||
|
"resolve_iface",
|
||||||
side_effect=Scapy_Exception,
|
side_effect=Scapy_Exception,
|
||||||
), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"):
|
), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"):
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
@ -595,7 +590,10 @@ async def test_setup_fails_non_root(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
with patch("os.geteuid", return_value=10), patch(
|
with patch("os.geteuid", return_value=10), patch(
|
||||||
"homeassistant.components.dhcp._verify_l2socket_setup",
|
"scapy.arch.common.compile_filter"
|
||||||
|
), patch.object(
|
||||||
|
interfaces,
|
||||||
|
"resolve_iface",
|
||||||
side_effect=Scapy_Exception,
|
side_effect=Scapy_Exception,
|
||||||
), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"):
|
), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"):
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
@ -618,10 +616,13 @@ async def test_setup_fails_with_broken_libpcap(
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
with patch("homeassistant.components.dhcp._verify_l2socket_setup"), patch(
|
with patch(
|
||||||
"scapy.arch.common.compile_filter",
|
"scapy.arch.common.compile_filter",
|
||||||
side_effect=ImportError,
|
side_effect=ImportError,
|
||||||
) as compile_filter, patch("scapy.sendrecv.AsyncSniffer") as async_sniffer, patch(
|
) as compile_filter, patch.object(
|
||||||
|
interfaces,
|
||||||
|
"resolve_iface",
|
||||||
|
) as resolve_iface_call, patch(
|
||||||
"homeassistant.components.dhcp.DiscoverHosts.async_discover"
|
"homeassistant.components.dhcp.DiscoverHosts.async_discover"
|
||||||
):
|
):
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
@ -630,7 +631,7 @@ async def test_setup_fails_with_broken_libpcap(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert compile_filter.called
|
assert compile_filter.called
|
||||||
assert not async_sniffer.called
|
assert not resolve_iface_call.called
|
||||||
assert (
|
assert (
|
||||||
"Cannot watch for dhcp packets without a functional packet filter"
|
"Cannot watch for dhcp packets without a functional packet filter"
|
||||||
in caplog.text
|
in caplog.text
|
||||||
|
@ -666,9 +667,9 @@ async def test_device_tracker_hostname_and_macaddress_exists_before_start(
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 1
|
assert len(mock_init.mock_calls) == 1
|
||||||
|
@ -699,7 +700,7 @@ async def test_device_tracker_registered(hass: HomeAssistant) -> None:
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
hass,
|
hass,
|
||||||
|
@ -718,7 +719,7 @@ async def test_device_tracker_registered(hass: HomeAssistant) -> None:
|
||||||
hostname="connect",
|
hostname="connect",
|
||||||
macaddress="b8b7f16db533",
|
macaddress="b8b7f16db533",
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
@ -738,7 +739,7 @@ async def test_device_tracker_registered_hostname_none(hass: HomeAssistant) -> N
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
hass,
|
hass,
|
||||||
|
@ -748,7 +749,7 @@ async def test_device_tracker_registered_hostname_none(hass: HomeAssistant) -> N
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 0
|
assert len(mock_init.mock_calls) == 0
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
@ -771,7 +772,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start(
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"device_tracker.august_connect",
|
"device_tracker.august_connect",
|
||||||
|
@ -784,7 +785,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 1
|
assert len(mock_init.mock_calls) == 1
|
||||||
|
@ -818,7 +819,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_home(
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"device_tracker.august_connect",
|
"device_tracker.august_connect",
|
||||||
|
@ -831,7 +832,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_home(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 0
|
assert len(mock_init.mock_calls) == 0
|
||||||
|
@ -848,7 +849,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_router(
|
||||||
{},
|
{},
|
||||||
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"device_tracker.august_connect",
|
"device_tracker.august_connect",
|
||||||
|
@ -861,7 +862,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_not_router(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 0
|
assert len(mock_init.mock_calls) == 0
|
||||||
|
@ -878,7 +879,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_hostname_missi
|
||||||
{},
|
{},
|
||||||
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"device_tracker.august_connect",
|
"device_tracker.august_connect",
|
||||||
|
@ -890,7 +891,7 @@ async def test_device_tracker_hostname_and_macaddress_after_start_hostname_missi
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 0
|
assert len(mock_init.mock_calls) == 0
|
||||||
|
@ -907,7 +908,7 @@ async def test_device_tracker_invalid_ip_address(
|
||||||
{},
|
{},
|
||||||
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
[{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}],
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"device_tracker.august_connect",
|
"device_tracker.august_connect",
|
||||||
|
@ -919,7 +920,7 @@ async def test_device_tracker_invalid_ip_address(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert "Ignoring invalid IP Address: invalid" in caplog.text
|
assert "Ignoring invalid IP Address: invalid" in caplog.text
|
||||||
|
@ -955,9 +956,9 @@ async def test_device_tracker_ignore_self_assigned_ips_before_start(
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 0
|
assert len(mock_init.mock_calls) == 0
|
||||||
|
@ -988,9 +989,9 @@ async def test_aiodiscover_finds_new_hosts(hass: HomeAssistant) -> None:
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 1
|
assert len(mock_init.mock_calls) == 1
|
||||||
|
@ -1047,9 +1048,9 @@ async def test_aiodiscover_does_not_call_again_on_shorter_hostname(
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 2
|
assert len(mock_init.mock_calls) == 2
|
||||||
|
@ -1092,7 +1093,7 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass: HomeAssistant) -
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await device_tracker_watcher.async_start()
|
device_tracker_watcher.async_start()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 0
|
assert len(mock_init.mock_calls) == 0
|
||||||
|
@ -1109,7 +1110,7 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass: HomeAssistant) -
|
||||||
):
|
):
|
||||||
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=65))
|
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=65))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await device_tracker_watcher.async_stop()
|
device_tracker_watcher.async_stop()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_init.mock_calls) == 1
|
assert len(mock_init.mock_calls) == 1
|
||||||
|
|
Loading…
Reference in New Issue