mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Implement a polling fallback for USB monitor (#130918)
This commit is contained in:
parent
762bc7b8d1
commit
9331b1572c
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Coroutine, Sequence
|
||||
import dataclasses
|
||||
from datetime import datetime, timedelta
|
||||
import fnmatch
|
||||
from functools import partial
|
||||
import logging
|
||||
@ -33,6 +34,7 @@ from homeassistant.helpers.deprecation import (
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.service_info.usb import UsbServiceInfo as _UsbServiceInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import USBMatcher, async_get_usb
|
||||
@ -46,6 +48,7 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
POLLING_MONITOR_SCAN_PERIOD = timedelta(seconds=5)
|
||||
REQUEST_SCAN_COOLDOWN = 10 # 10 second cooldown
|
||||
|
||||
__all__ = [
|
||||
@ -229,7 +232,9 @@ class USBDiscovery:
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up USB Discovery."""
|
||||
await self._async_start_monitor()
|
||||
if await self._async_supports_monitoring():
|
||||
await self._async_start_monitor()
|
||||
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self.async_start)
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
|
||||
|
||||
@ -243,26 +248,54 @@ class USBDiscovery:
|
||||
if self._request_debouncer:
|
||||
self._request_debouncer.async_shutdown()
|
||||
|
||||
async def _async_start_monitor(self) -> None:
|
||||
"""Start monitoring hardware with pyudev."""
|
||||
if not sys.platform.startswith("linux"):
|
||||
return
|
||||
async def _async_supports_monitoring(self) -> bool:
|
||||
info = await system_info.async_get_system_info(self.hass)
|
||||
if info.get("docker"):
|
||||
return
|
||||
return not info.get("docker")
|
||||
|
||||
async def _async_start_monitor(self) -> None:
|
||||
"""Start monitoring hardware."""
|
||||
if not await self._async_start_monitor_udev():
|
||||
_LOGGER.info(
|
||||
"Falling back to periodic filesystem polling for development, libudev "
|
||||
"is not present"
|
||||
)
|
||||
self._async_start_monitor_polling()
|
||||
|
||||
@hass_callback
|
||||
def _async_start_monitor_polling(self) -> None:
|
||||
"""Start monitoring hardware with polling (for development only!)."""
|
||||
|
||||
async def _scan(event_time: datetime) -> None:
|
||||
await self._async_scan_serial()
|
||||
|
||||
stop_callback = async_track_time_interval(
|
||||
self.hass, _scan, POLLING_MONITOR_SCAN_PERIOD
|
||||
)
|
||||
|
||||
@hass_callback
|
||||
def _stop_polling(event: Event) -> None:
|
||||
stop_callback()
|
||||
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_polling)
|
||||
|
||||
async def _async_start_monitor_udev(self) -> bool:
|
||||
"""Start monitoring hardware with pyudev. Returns True if successful."""
|
||||
if not sys.platform.startswith("linux"):
|
||||
return False
|
||||
|
||||
if not (
|
||||
observer := await self.hass.async_add_executor_job(
|
||||
self._get_monitor_observer
|
||||
)
|
||||
):
|
||||
return
|
||||
return False
|
||||
|
||||
def _stop_observer(event: Event) -> None:
|
||||
observer.stop()
|
||||
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_observer)
|
||||
self.observer_active = True
|
||||
return True
|
||||
|
||||
def _get_monitor_observer(self) -> MonitorObserver | None:
|
||||
"""Get the monitor observer.
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""Tests for the USB Discovery integration."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, call, patch, sentinel
|
||||
|
||||
@ -59,10 +60,6 @@ def mock_venv():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not sys.platform.startswith("linux"),
|
||||
reason="Only works on linux",
|
||||
)
|
||||
async def test_observer_discovery(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, venv
|
||||
) -> None:
|
||||
@ -93,6 +90,7 @@ async def test_observer_discovery(
|
||||
return mock_observer
|
||||
|
||||
with (
|
||||
patch("sys.platform", "linux"),
|
||||
patch("pyudev.Context"),
|
||||
patch("pyudev.MonitorObserver", new=_create_mock_monitor_observer),
|
||||
patch("pyudev.Monitor.filter_by"),
|
||||
@ -115,10 +113,65 @@ async def test_observer_discovery(
|
||||
assert mock_observer.mock_calls == [call.start(), call.__bool__(), call.stop()]
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not sys.platform.startswith("linux"),
|
||||
reason="Only works on linux",
|
||||
)
|
||||
async def test_polling_discovery(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, venv
|
||||
) -> None:
|
||||
"""Test that polling can discover a device without raising an exception."""
|
||||
new_usb = [{"domain": "test1", "vid": "3039"}]
|
||||
mock_comports_found_device = asyncio.Event()
|
||||
|
||||
def get_comports() -> list:
|
||||
nonlocal mock_comports
|
||||
|
||||
# Only "find" a device after a few invocations
|
||||
if len(mock_comports.mock_calls) < 5:
|
||||
return []
|
||||
|
||||
mock_comports_found_device.set()
|
||||
return [
|
||||
MagicMock(
|
||||
device=slae_sh_device.device,
|
||||
vid=12345,
|
||||
pid=12345,
|
||||
serial_number=slae_sh_device.serial_number,
|
||||
manufacturer=slae_sh_device.manufacturer,
|
||||
description=slae_sh_device.description,
|
||||
)
|
||||
]
|
||||
|
||||
with (
|
||||
patch("sys.platform", "linux"),
|
||||
patch(
|
||||
"homeassistant.components.usb.USBDiscovery._get_monitor_observer",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.usb.POLLING_MONITOR_SCAN_PERIOD",
|
||||
timedelta(seconds=0.01),
|
||||
),
|
||||
patch("homeassistant.components.usb.async_get_usb", return_value=new_usb),
|
||||
patch(
|
||||
"homeassistant.components.usb.comports", side_effect=get_comports
|
||||
) as mock_comports,
|
||||
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
|
||||
):
|
||||
assert await async_setup_component(hass, "usb", {"usb": {}})
|
||||
await hass.async_block_till_done()
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Wait until a new device is discovered after a few polling attempts
|
||||
assert len(mock_config_flow.mock_calls) == 0
|
||||
await mock_comports_found_device.wait()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert len(mock_config_flow.mock_calls) == 1
|
||||
assert mock_config_flow.mock_calls[0][1][0] == "test1"
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_removal_by_observer_before_started(
|
||||
hass: HomeAssistant, operating_system
|
||||
) -> None:
|
||||
@ -671,10 +724,6 @@ async def test_non_matching_discovered_by_scanner_after_started(
|
||||
assert len(mock_config_flow.mock_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not sys.platform.startswith("linux"),
|
||||
reason="Only works on linux",
|
||||
)
|
||||
async def test_observer_on_wsl_fallback_without_throwing_exception(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, venv
|
||||
) -> None:
|
||||
@ -713,10 +762,6 @@ async def test_observer_on_wsl_fallback_without_throwing_exception(
|
||||
assert mock_config_flow.mock_calls[0][1][0] == "test1"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not sys.platform.startswith("linux"),
|
||||
reason="Only works on linux",
|
||||
)
|
||||
async def test_not_discovered_by_observer_before_started_on_docker(
|
||||
hass: HomeAssistant, docker
|
||||
) -> None:
|
||||
|
Loading…
x
Reference in New Issue
Block a user