diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index ad0446543db..ac09c908927 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -3,19 +3,17 @@ from __future__ import annotations from abc import ABC, abstractmethod import asyncio -from collections.abc import Callable, Iterable -import contextlib +from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta from fnmatch import translate from functools import lru_cache import itertools import logging -import os import re -import threading -from typing import TYPE_CHECKING, Any, Final, cast +from typing import Any, Final +import aiodhcpwatcher from aiodiscover import DiscoverHosts from aiodiscover.discovery import ( HOSTNAME as DISCOVERY_HOSTNAME, @@ -23,8 +21,6 @@ from aiodiscover.discovery import ( MAC_ADDRESS as DISCOVERY_MAC_ADDRESS, ) 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.components.device_tracker import ( @@ -61,20 +57,13 @@ from homeassistant.loader import DHCPMatcher, async_get_dhcp from .const import DOMAIN -if TYPE_CHECKING: - from scapy.packet import Packet - from scapy.sendrecv import AsyncSniffer - CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) FILTER = "udp and (port 67 or 68)" -REQUESTED_ADDR = "requested_addr" -MESSAGE_TYPE = "message-type" HOSTNAME: Final = "hostname" MAC_ADDRESS: Final = "macaddress" IP_ADDRESS: Final = "ip" REGISTERED_DEVICES: Final = "registered_devices" -DHCP_REQUEST = 3 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 for passive_cls in (DeviceTrackerRegisteredWatcher, DeviceTrackerWatcher): passive_watcher = passive_cls(hass, address_data, integration_matchers) - await passive_watcher.async_start() + passive_watcher.async_start() 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): active_watcher = active_cls(hass, address_data, integration_matchers) - await active_watcher.async_start() + active_watcher.async_start() watchers.append(active_watcher) - async def _async_stop(event: Event) -> None: + @callback + def _async_stop(event: Event) -> None: 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_STARTED, _initialize) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_initialize) return True @@ -178,21 +170,20 @@ class WatcherBase(ABC): self.hass = hass self._integration_matchers = integration_matchers 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 - async def async_stop(self) -> None: - """Stop the watcher.""" - - @abstractmethod - async def async_start(self) -> None: + @callback + def async_start(self) -> None: """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 def async_process_client( self, ip_address: str, hostname: str, mac_address: str @@ -291,20 +282,19 @@ class NetworkWatcher(WatcherBase): ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) - self._unsub: Callable[[], None] | None = None self._discover_hosts: DiscoverHosts | 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.""" - if self._unsub: - self._unsub() - self._unsub = None + super().async_stop() if self._discover_task: self._discover_task.cancel() self._discover_task = None - async def async_start(self) -> None: + @callback + def async_start(self) -> None: """Start scanning for new devices on the network.""" self._discover_hosts = DiscoverHosts() self._unsub = async_track_time_interval( @@ -336,23 +326,8 @@ class NetworkWatcher(WatcherBase): class DeviceTrackerWatcher(WatcherBase): """Class to watch dhcp data from routers.""" - def __init__( - self, - 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: + @callback + def async_start(self) -> None: """Stop watching for new device trackers.""" self._unsub = async_track_state_added_domain( self.hass, [DEVICE_TRACKER_DOMAIN], self._async_process_device_event @@ -391,23 +366,8 @@ class DeviceTrackerWatcher(WatcherBase): class DeviceTrackerRegisteredWatcher(WatcherBase): """Class to watch data from device tracker registrations.""" - def __init__( - self, - 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: + @callback + def async_start(self) -> None: """Stop watching for device tracker registrations.""" self._unsub = async_dispatcher_connect( self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_data @@ -429,114 +389,17 @@ class DeviceTrackerRegisteredWatcher(WatcherBase): class DHCPWatcher(WatcherBase): """Class to watch dhcp requests.""" - def __init__( - self, - hass: HomeAssistant, - address_data: dict[str, dict[str, str]], - integration_matchers: DhcpMatchers, - ) -> 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, + @callback + def _async_process_dhcp_request(self, response: aiodhcpwatcher.DHCPRequest) -> None: + """Process a dhcp request.""" + self.async_process_client( + response.ip_address, response.hostname, _format_mac(response.mac_address) ) - def _handle_dhcp_packet(packet: Packet) -> None: - """Process a dhcp packet.""" - if DHCP not in packet: - return - - 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} + @callback + def async_start(self) -> None: + """Start watching for dhcp packets.""" + self._unsub = aiodhcpwatcher.start(self._async_process_dhcp_request) def _format_mac(mac_address: str) -> str: @@ -544,33 +407,6 @@ def _format_mac(mac_address: str) -> str: 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) def _compile_fnmatch(pattern: str) -> re.Pattern: """Compile a fnmatch pattern.""" diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 70469a93678..142aab52cc8 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -5,10 +5,16 @@ "documentation": "https://www.home-assistant.io/integrations/dhcp", "integration_type": "system", "iot_class": "local_push", - "loggers": ["aiodiscover", "dnspython", "pyroute2", "scapy"], + "loggers": [ + "aiodiscover", + "aiodhcpwatcher", + "dnspython", + "pyroute2", + "scapy" + ], "quality_scale": "internal", "requirements": [ - "scapy==2.5.0", + "aiodhcpwatcher==0.8.0", "aiodiscover==1.6.1", "cached_ipaddress==0.3.0" ] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index addf71bd68f..2ebac8f2bcc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,5 +1,6 @@ # Automatically generated by gen_requirements_all.py, do not edit +aiodhcpwatcher==0.8.0 aiodiscover==1.6.1 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 @@ -51,7 +52,6 @@ PyTurboJPEG==1.7.1 pyudev==0.23.2 PyYAML==6.0.1 requests==2.31.0 -scapy==2.5.0 SQLAlchemy==2.0.25 typing-extensions>=4.9.0,<5.0 ulid-transform==0.9.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2e454e37f23..e723cc09e23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,6 +220,9 @@ aiobotocore==2.9.1 # homeassistant.components.comelit aiocomelit==0.8.3 +# homeassistant.components.dhcp +aiodhcpwatcher==0.8.0 + # homeassistant.components.dhcp aiodiscover==1.6.1 @@ -2485,9 +2488,6 @@ samsungtvws[async,encrypted]==2.6.0 # homeassistant.components.satel_integra satel-integra==0.3.7 -# homeassistant.components.dhcp -scapy==2.5.0 - # homeassistant.components.screenlogic screenlogicpy==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddea8936102..54cbb09a5b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -199,6 +199,9 @@ aiobotocore==2.9.1 # homeassistant.components.comelit aiocomelit==0.8.3 +# homeassistant.components.dhcp +aiodhcpwatcher==0.8.0 + # homeassistant.components.dhcp aiodiscover==1.6.1 @@ -1895,9 +1898,6 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv samsungtvws[async,encrypted]==2.6.0 -# homeassistant.components.dhcp -scapy==2.5.0 - # homeassistant.components.screenlogic screenlogicpy==0.10.0 diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 18d213a7029..487435ef3f5 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -3,10 +3,14 @@ from collections.abc import Awaitable, Callable import datetime import threading from typing import Any, cast -from unittest.mock import MagicMock, patch +from unittest.mock import patch +import aiodhcpwatcher import pytest -from scapy import arch # noqa: F401 +from scapy import ( + arch, # noqa: F401 + interfaces, +) from scapy.error import Scapy_Exception from scapy.layers.dhcp import DHCP from scapy.layers.l2 import Ether @@ -140,29 +144,18 @@ async def _async_get_handle_dhcp_packet( {}, 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): - nonlocal async_handle_dhcp_packet - callback = kwargs["prn"] + def _async_handle_dhcp_request(request: aiodhcpwatcher.DHCPRequest) -> None: + dhcp_watcher._async_process_dhcp_request(request) - async def _async_handle_dhcp_packet(packet): - await hass.async_add_executor_job(callback, packet) + handler = aiodhcpwatcher.make_packet_handler(_async_handle_dhcp_request) - async_handle_dhcp_packet = _async_handle_dhcp_packet - return MagicMock() + async def _async_handle_dhcp_packet(packet): + handler(packet) - with patch( - "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) + return cast("Callable[[Any], Awaitable[None]]", _async_handle_dhcp_packet) 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() - with patch("scapy.sendrecv.AsyncSniffer.start") as start_call, patch( - "homeassistant.components.dhcp._verify_l2socket_setup", - ), patch("scapy.arch.common.compile_filter"), patch( + with patch.object( + interfaces, + "resolve_iface", + ) as resolve_iface_call, patch("scapy.arch.common.compile_filter"), patch( "homeassistant.components.dhcp.DiscoverHosts.async_discover" ): 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) await hass.async_block_till_done() - start_call.assert_called_once() + resolve_iface_call.assert_called_once() async def test_setup_fails_as_root( @@ -569,8 +563,9 @@ async def test_setup_fails_as_root( wait_event = threading.Event() - with patch("os.geteuid", return_value=0), patch( - "homeassistant.components.dhcp._verify_l2socket_setup", + with patch("os.geteuid", return_value=0), patch.object( + interfaces, + "resolve_iface", side_effect=Scapy_Exception, ), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -595,7 +590,10 @@ async def test_setup_fails_non_root( await hass.async_block_till_done() 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, ), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"): 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() - with patch("homeassistant.components.dhcp._verify_l2socket_setup"), patch( + with patch( "scapy.arch.common.compile_filter", 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" ): 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() assert compile_filter.called - assert not async_sniffer.called + assert not resolve_iface_call.called assert ( "Cannot watch for dhcp packets without a functional packet filter" 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 device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() 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() async_dispatcher_send( hass, @@ -718,7 +719,7 @@ async def test_device_tracker_registered(hass: HomeAssistant) -> None: hostname="connect", macaddress="b8b7f16db533", ) - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() 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() async_dispatcher_send( hass, @@ -748,7 +749,7 @@ async def test_device_tracker_registered_hostname_none(hass: HomeAssistant) -> N await hass.async_block_till_done() assert len(mock_init.mock_calls) == 0 - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() 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() hass.states.async_set( "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 device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() 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() hass.states.async_set( "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 device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() 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*"}], ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() hass.states.async_set( "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 device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() 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*"}], ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() hass.states.async_set( "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 device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() 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*"}], ) - await device_tracker_watcher.async_start() + device_tracker_watcher.async_start() await hass.async_block_till_done() hass.states.async_set( "device_tracker.august_connect", @@ -919,7 +920,7 @@ async def test_device_tracker_invalid_ip_address( }, ) await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() 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 device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() 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 device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() 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 device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() 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() 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)) await hass.async_block_till_done() - await device_tracker_watcher.async_stop() + device_tracker_watcher.async_stop() await hass.async_block_till_done() assert len(mock_init.mock_calls) == 1