mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
Add support for USB discovery (#54904)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
11c6a33594
commit
dc74a52f58
@ -549,6 +549,7 @@ homeassistant/components/upcloud/* @scop
|
|||||||
homeassistant/components/updater/* @home-assistant/core
|
homeassistant/components/updater/* @home-assistant/core
|
||||||
homeassistant/components/upnp/* @StevenLooman @ehendrix23
|
homeassistant/components/upnp/* @StevenLooman @ehendrix23
|
||||||
homeassistant/components/uptimerobot/* @ludeeus
|
homeassistant/components/uptimerobot/* @ludeeus
|
||||||
|
homeassistant/components/usb/* @bdraco
|
||||||
homeassistant/components/usgs_earthquakes_feed/* @exxamalte
|
homeassistant/components/usgs_earthquakes_feed/* @exxamalte
|
||||||
homeassistant/components/utility_meter/* @dgomes
|
homeassistant/components/utility_meter/* @dgomes
|
||||||
homeassistant/components/velbus/* @Cereal2nd @brefra
|
homeassistant/components/velbus/* @Cereal2nd @brefra
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"system_health",
|
"system_health",
|
||||||
"tag",
|
"tag",
|
||||||
"timer",
|
"timer",
|
||||||
|
"usb",
|
||||||
"updater",
|
"updater",
|
||||||
"webhook",
|
"webhook",
|
||||||
"zeroconf",
|
"zeroconf",
|
||||||
|
138
homeassistant/components/usb/__init__.py
Normal file
138
homeassistant/components/usb/__init__.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
"""The USB Discovery integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from serial.tools.list_ports import comports
|
||||||
|
from serial.tools.list_ports_common import ListPortInfo
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
|
||||||
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
from homeassistant.loader import async_get_usb
|
||||||
|
|
||||||
|
from .flow import FlowDispatcher, USBFlow
|
||||||
|
from .models import USBDevice
|
||||||
|
from .utils import usb_device_from_port
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Perodic scanning only happens on non-linux systems
|
||||||
|
SCAN_INTERVAL = datetime.timedelta(minutes=60)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up the USB Discovery integration."""
|
||||||
|
usb = await async_get_usb(hass)
|
||||||
|
usb_discovery = USBDiscovery(hass, FlowDispatcher(hass), usb)
|
||||||
|
await usb_discovery.async_setup()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class USBDiscovery:
|
||||||
|
"""Manage USB Discovery."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
flow_dispatcher: FlowDispatcher,
|
||||||
|
usb: list[dict[str, str]],
|
||||||
|
) -> None:
|
||||||
|
"""Init USB Discovery."""
|
||||||
|
self.hass = hass
|
||||||
|
self.flow_dispatcher = flow_dispatcher
|
||||||
|
self.usb = usb
|
||||||
|
self.seen: set[tuple[str, ...]] = set()
|
||||||
|
|
||||||
|
async def async_setup(self) -> None:
|
||||||
|
"""Set up USB Discovery."""
|
||||||
|
if not await self._async_start_monitor():
|
||||||
|
await self._async_start_scanner()
|
||||||
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self.async_start)
|
||||||
|
|
||||||
|
async def async_start(self, event: Event) -> None:
|
||||||
|
"""Start USB Discovery and run a manual scan."""
|
||||||
|
self.flow_dispatcher.async_start()
|
||||||
|
await self.hass.async_add_executor_job(self.scan_serial)
|
||||||
|
|
||||||
|
async def _async_start_scanner(self) -> None:
|
||||||
|
"""Perodic scan with pyserial when the observer is not available."""
|
||||||
|
stop_track = async_track_time_interval(
|
||||||
|
self.hass, lambda now: self.scan_serial(), SCAN_INTERVAL
|
||||||
|
)
|
||||||
|
self.hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_STOP, callback(lambda event: stop_track())
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_start_monitor(self) -> bool:
|
||||||
|
"""Start monitoring hardware with pyudev."""
|
||||||
|
if not sys.platform.startswith("linux"):
|
||||||
|
return False
|
||||||
|
from pyudev import ( # pylint: disable=import-outside-toplevel
|
||||||
|
Context,
|
||||||
|
Monitor,
|
||||||
|
MonitorObserver,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
context = Context()
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
monitor = Monitor.from_netlink(context)
|
||||||
|
monitor.filter_by(subsystem="tty")
|
||||||
|
observer = MonitorObserver(
|
||||||
|
monitor, callback=self._device_discovered, name="usb-observer"
|
||||||
|
)
|
||||||
|
observer.start()
|
||||||
|
self.hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_STOP, lambda event: observer.stop()
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _device_discovered(self, device):
|
||||||
|
"""Call when the observer discovers a new usb tty device."""
|
||||||
|
if device.action != "add":
|
||||||
|
return
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Discovered Device at path: %s, triggering scan serial",
|
||||||
|
device.device_path,
|
||||||
|
)
|
||||||
|
self.scan_serial()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_process_discovered_usb_device(self, device: USBDevice) -> None:
|
||||||
|
"""Process a USB discovery."""
|
||||||
|
_LOGGER.debug("Discovered USB Device: %s", device)
|
||||||
|
device_tuple = dataclasses.astuple(device)
|
||||||
|
if device_tuple in self.seen:
|
||||||
|
return
|
||||||
|
self.seen.add(device_tuple)
|
||||||
|
for matcher in self.usb:
|
||||||
|
if "vid" in matcher and device.vid != matcher["vid"]:
|
||||||
|
continue
|
||||||
|
if "pid" in matcher and device.pid != matcher["pid"]:
|
||||||
|
continue
|
||||||
|
flow: USBFlow = {
|
||||||
|
"domain": matcher["domain"],
|
||||||
|
"context": {"source": config_entries.SOURCE_USB},
|
||||||
|
"data": dataclasses.asdict(device),
|
||||||
|
}
|
||||||
|
self.flow_dispatcher.async_create(flow)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_process_ports(self, ports: list[ListPortInfo]) -> None:
|
||||||
|
"""Process each discovered port."""
|
||||||
|
for port in ports:
|
||||||
|
if port.vid is None and port.pid is None:
|
||||||
|
continue
|
||||||
|
self._async_process_discovered_usb_device(usb_device_from_port(port))
|
||||||
|
|
||||||
|
def scan_serial(self) -> None:
|
||||||
|
"""Scan serial ports."""
|
||||||
|
self.hass.add_job(self._async_process_ports, comports())
|
3
homeassistant/components/usb/const.py
Normal file
3
homeassistant/components/usb/const.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""Constants for the USB Discovery integration."""
|
||||||
|
|
||||||
|
DOMAIN = "usb"
|
48
homeassistant/components/usb/flow.py
Normal file
48
homeassistant/components/usb/flow.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"""The USB Discovery integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Coroutine
|
||||||
|
from typing import Any, TypedDict
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
|
||||||
|
|
||||||
|
class USBFlow(TypedDict):
|
||||||
|
"""A queued usb discovery flow."""
|
||||||
|
|
||||||
|
domain: str
|
||||||
|
context: dict[str, Any]
|
||||||
|
data: dict
|
||||||
|
|
||||||
|
|
||||||
|
class FlowDispatcher:
|
||||||
|
"""Dispatch discovery flows."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Init the discovery dispatcher."""
|
||||||
|
self.hass = hass
|
||||||
|
self.pending_flows: list[USBFlow] = []
|
||||||
|
self.started = False
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_start(self, *_: Any) -> None:
|
||||||
|
"""Start processing pending flows."""
|
||||||
|
self.started = True
|
||||||
|
for flow in self.pending_flows:
|
||||||
|
self.hass.async_create_task(self._init_flow(flow))
|
||||||
|
self.pending_flows = []
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_create(self, flow: USBFlow) -> None:
|
||||||
|
"""Create and add or queue a flow."""
|
||||||
|
if self.started:
|
||||||
|
self.hass.async_create_task(self._init_flow(flow))
|
||||||
|
else:
|
||||||
|
self.pending_flows.append(flow)
|
||||||
|
|
||||||
|
def _init_flow(self, flow: USBFlow) -> Coroutine[None, None, FlowResult]:
|
||||||
|
"""Create a flow."""
|
||||||
|
return self.hass.config_entries.flow.async_init(
|
||||||
|
flow["domain"], context=flow["context"], data=flow["data"]
|
||||||
|
)
|
12
homeassistant/components/usb/manifest.json
Normal file
12
homeassistant/components/usb/manifest.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"domain": "usb",
|
||||||
|
"name": "USB Discovery",
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/usb",
|
||||||
|
"requirements": [
|
||||||
|
"pyudev==0.22.0",
|
||||||
|
"pyserial==3.5"
|
||||||
|
],
|
||||||
|
"codeowners": ["@bdraco"],
|
||||||
|
"quality_scale": "internal",
|
||||||
|
"iot_class": "local_push"
|
||||||
|
}
|
16
homeassistant/components/usb/models.py
Normal file
16
homeassistant/components/usb/models.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""Models helper class for the usb integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class USBDevice:
|
||||||
|
"""A usb device."""
|
||||||
|
|
||||||
|
device: str
|
||||||
|
vid: str
|
||||||
|
pid: str
|
||||||
|
serial_number: str | None
|
||||||
|
manufacturer: str | None
|
||||||
|
description: str | None
|
18
homeassistant/components/usb/utils.py
Normal file
18
homeassistant/components/usb/utils.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
"""The USB Discovery integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from serial.tools.list_ports_common import ListPortInfo
|
||||||
|
|
||||||
|
from .models import USBDevice
|
||||||
|
|
||||||
|
|
||||||
|
def usb_device_from_port(port: ListPortInfo) -> USBDevice:
|
||||||
|
"""Convert serial ListPortInfo to USBDevice."""
|
||||||
|
return USBDevice(
|
||||||
|
device=port.device,
|
||||||
|
vid=f"{hex(port.vid)[2:]:0>4}".upper(),
|
||||||
|
pid=f"{hex(port.pid)[2:]:0>4}".upper(),
|
||||||
|
serial_number=port.serial_number,
|
||||||
|
manufacturer=port.manufacturer,
|
||||||
|
description=port.description,
|
||||||
|
)
|
@ -40,6 +40,7 @@ SOURCE_IMPORT = "import"
|
|||||||
SOURCE_INTEGRATION_DISCOVERY = "integration_discovery"
|
SOURCE_INTEGRATION_DISCOVERY = "integration_discovery"
|
||||||
SOURCE_MQTT = "mqtt"
|
SOURCE_MQTT = "mqtt"
|
||||||
SOURCE_SSDP = "ssdp"
|
SOURCE_SSDP = "ssdp"
|
||||||
|
SOURCE_USB = "usb"
|
||||||
SOURCE_USER = "user"
|
SOURCE_USER = "user"
|
||||||
SOURCE_ZEROCONF = "zeroconf"
|
SOURCE_ZEROCONF = "zeroconf"
|
||||||
SOURCE_DHCP = "dhcp"
|
SOURCE_DHCP = "dhcp"
|
||||||
@ -103,6 +104,9 @@ DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id"
|
|||||||
DISCOVERY_NOTIFICATION_ID = "config_entry_discovery"
|
DISCOVERY_NOTIFICATION_ID = "config_entry_discovery"
|
||||||
DISCOVERY_SOURCES = (
|
DISCOVERY_SOURCES = (
|
||||||
SOURCE_SSDP,
|
SOURCE_SSDP,
|
||||||
|
SOURCE_USB,
|
||||||
|
SOURCE_DHCP,
|
||||||
|
SOURCE_HOMEKIT,
|
||||||
SOURCE_ZEROCONF,
|
SOURCE_ZEROCONF,
|
||||||
SOURCE_HOMEKIT,
|
SOURCE_HOMEKIT,
|
||||||
SOURCE_DHCP,
|
SOURCE_DHCP,
|
||||||
@ -1372,6 +1376,12 @@ class ConfigFlow(data_entry_flow.FlowHandler):
|
|||||||
"""Handle a flow initialized by DHCP discovery."""
|
"""Handle a flow initialized by DHCP discovery."""
|
||||||
return await self.async_step_discovery(discovery_info)
|
return await self.async_step_discovery(discovery_info)
|
||||||
|
|
||||||
|
async def async_step_usb(
|
||||||
|
self, discovery_info: DiscoveryInfoType
|
||||||
|
) -> data_entry_flow.FlowResult:
|
||||||
|
"""Handle a flow initialized by USB discovery."""
|
||||||
|
return await self.async_step_discovery(discovery_info)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_create_entry( # pylint: disable=arguments-differ
|
def async_create_entry( # pylint: disable=arguments-differ
|
||||||
self,
|
self,
|
||||||
|
8
homeassistant/generated/usb.py
Normal file
8
homeassistant/generated/usb.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""Automatically generated by hassfest.
|
||||||
|
|
||||||
|
To update, run python3 -m script.hassfest
|
||||||
|
"""
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
|
||||||
|
USB = [] # type: ignore
|
@ -26,6 +26,7 @@ from awesomeversion import (
|
|||||||
from homeassistant.generated.dhcp import DHCP
|
from homeassistant.generated.dhcp import DHCP
|
||||||
from homeassistant.generated.mqtt import MQTT
|
from homeassistant.generated.mqtt import MQTT
|
||||||
from homeassistant.generated.ssdp import SSDP
|
from homeassistant.generated.ssdp import SSDP
|
||||||
|
from homeassistant.generated.usb import USB
|
||||||
from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF
|
from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF
|
||||||
from homeassistant.util.async_ import gather_with_concurrency
|
from homeassistant.util.async_ import gather_with_concurrency
|
||||||
|
|
||||||
@ -81,6 +82,7 @@ class Manifest(TypedDict, total=False):
|
|||||||
ssdp: list[dict[str, str]]
|
ssdp: list[dict[str, str]]
|
||||||
zeroconf: list[str | dict[str, str]]
|
zeroconf: list[str | dict[str, str]]
|
||||||
dhcp: list[dict[str, str]]
|
dhcp: list[dict[str, str]]
|
||||||
|
usb: list[dict[str, str]]
|
||||||
homekit: dict[str, list[str]]
|
homekit: dict[str, list[str]]
|
||||||
is_built_in: bool
|
is_built_in: bool
|
||||||
version: str
|
version: str
|
||||||
@ -219,6 +221,20 @@ async def async_get_dhcp(hass: HomeAssistant) -> list[dict[str, str]]:
|
|||||||
return dhcp
|
return dhcp
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_usb(hass: HomeAssistant) -> list[dict[str, str]]:
|
||||||
|
"""Return cached list of usb types."""
|
||||||
|
usb: list[dict[str, str]] = USB.copy()
|
||||||
|
|
||||||
|
integrations = await async_get_custom_components(hass)
|
||||||
|
for integration in integrations.values():
|
||||||
|
if not integration.usb:
|
||||||
|
continue
|
||||||
|
for entry in integration.usb:
|
||||||
|
usb.append({"domain": integration.domain, **entry})
|
||||||
|
|
||||||
|
return usb
|
||||||
|
|
||||||
|
|
||||||
async def async_get_homekit(hass: HomeAssistant) -> dict[str, str]:
|
async def async_get_homekit(hass: HomeAssistant) -> dict[str, str]:
|
||||||
"""Return cached list of homekit models."""
|
"""Return cached list of homekit models."""
|
||||||
|
|
||||||
@ -423,6 +439,11 @@ class Integration:
|
|||||||
"""Return Integration dhcp entries."""
|
"""Return Integration dhcp entries."""
|
||||||
return self.manifest.get("dhcp")
|
return self.manifest.get("dhcp")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def usb(self) -> list[dict[str, str]] | None:
|
||||||
|
"""Return Integration usb entries."""
|
||||||
|
return self.manifest.get("usb")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def homekit(self) -> dict[str, list[str]] | None:
|
def homekit(self) -> dict[str, list[str]] | None:
|
||||||
"""Return Integration homekit entries."""
|
"""Return Integration homekit entries."""
|
||||||
|
@ -23,7 +23,9 @@ jinja2==3.0.1
|
|||||||
paho-mqtt==1.5.1
|
paho-mqtt==1.5.1
|
||||||
pillow==8.2.0
|
pillow==8.2.0
|
||||||
pip>=8.0.3,<20.3
|
pip>=8.0.3,<20.3
|
||||||
|
pyserial==3.5
|
||||||
python-slugify==4.0.1
|
python-slugify==4.0.1
|
||||||
|
pyudev==0.22.0
|
||||||
pyyaml==5.4.1
|
pyyaml==5.4.1
|
||||||
requests==2.25.1
|
requests==2.25.1
|
||||||
ruamel.yaml==0.15.100
|
ruamel.yaml==0.15.100
|
||||||
|
@ -1755,6 +1755,7 @@ pysensibo==1.0.3
|
|||||||
pyserial-asyncio==0.5
|
pyserial-asyncio==0.5
|
||||||
|
|
||||||
# homeassistant.components.acer_projector
|
# homeassistant.components.acer_projector
|
||||||
|
# homeassistant.components.usb
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
|
|
||||||
@ -1957,6 +1958,9 @@ pytradfri[async]==7.0.6
|
|||||||
# homeassistant.components.trafikverket_weatherstation
|
# homeassistant.components.trafikverket_weatherstation
|
||||||
pytrafikverket==0.1.6.2
|
pytrafikverket==0.1.6.2
|
||||||
|
|
||||||
|
# homeassistant.components.usb
|
||||||
|
pyudev==0.22.0
|
||||||
|
|
||||||
# homeassistant.components.uptimerobot
|
# homeassistant.components.uptimerobot
|
||||||
pyuptimerobot==21.8.2
|
pyuptimerobot==21.8.2
|
||||||
|
|
||||||
|
@ -1011,6 +1011,7 @@ pyruckus==0.12
|
|||||||
pyserial-asyncio==0.5
|
pyserial-asyncio==0.5
|
||||||
|
|
||||||
# homeassistant.components.acer_projector
|
# homeassistant.components.acer_projector
|
||||||
|
# homeassistant.components.usb
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
|
|
||||||
@ -1095,6 +1096,9 @@ pytraccar==0.9.0
|
|||||||
# homeassistant.components.tradfri
|
# homeassistant.components.tradfri
|
||||||
pytradfri[async]==7.0.6
|
pytradfri[async]==7.0.6
|
||||||
|
|
||||||
|
# homeassistant.components.usb
|
||||||
|
pyudev==0.22.0
|
||||||
|
|
||||||
# homeassistant.components.uptimerobot
|
# homeassistant.components.uptimerobot
|
||||||
pyuptimerobot==21.8.2
|
pyuptimerobot==21.8.2
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ from . import (
|
|||||||
services,
|
services,
|
||||||
ssdp,
|
ssdp,
|
||||||
translations,
|
translations,
|
||||||
|
usb,
|
||||||
zeroconf,
|
zeroconf,
|
||||||
)
|
)
|
||||||
from .model import Config, Integration
|
from .model import Config, Integration
|
||||||
@ -34,6 +35,7 @@ INTEGRATION_PLUGINS = [
|
|||||||
translations,
|
translations,
|
||||||
zeroconf,
|
zeroconf,
|
||||||
dhcp,
|
dhcp,
|
||||||
|
usb,
|
||||||
]
|
]
|
||||||
HASS_PLUGINS = [
|
HASS_PLUGINS = [
|
||||||
coverage,
|
coverage,
|
||||||
|
@ -41,6 +41,7 @@ def validate_integration(config: Config, integration: Integration):
|
|||||||
or "async_step_ssdp" in config_flow
|
or "async_step_ssdp" in config_flow
|
||||||
or "async_step_zeroconf" in config_flow
|
or "async_step_zeroconf" in config_flow
|
||||||
or "async_step_dhcp" in config_flow
|
or "async_step_dhcp" in config_flow
|
||||||
|
or "async_step_usb" in config_flow
|
||||||
)
|
)
|
||||||
|
|
||||||
if not needs_unique_id:
|
if not needs_unique_id:
|
||||||
|
@ -205,6 +205,14 @@ MANIFEST_SCHEMA = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
vol.Optional("usb"): [
|
||||||
|
vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional("vid"): vol.All(str, verify_uppercase),
|
||||||
|
vol.Optional("pid"): vol.All(str, verify_uppercase),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
vol.Required("documentation"): vol.All(
|
vol.Required("documentation"): vol.All(
|
||||||
vol.Url(), documentation_url # pylint: disable=no-value-for-parameter
|
vol.Url(), documentation_url # pylint: disable=no-value-for-parameter
|
||||||
),
|
),
|
||||||
|
64
script/hassfest/usb.py
Normal file
64
script/hassfest/usb.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
"""Generate usb file."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .model import Config, Integration
|
||||||
|
|
||||||
|
BASE = """
|
||||||
|
\"\"\"Automatically generated by hassfest.
|
||||||
|
|
||||||
|
To update, run python3 -m script.hassfest
|
||||||
|
\"\"\"
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
|
||||||
|
USB = {} # type: ignore
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_and_validate(integrations: list[dict[str, str]]) -> str:
|
||||||
|
"""Validate and generate usb data."""
|
||||||
|
match_list = []
|
||||||
|
|
||||||
|
for domain in sorted(integrations):
|
||||||
|
integration = integrations[domain]
|
||||||
|
|
||||||
|
if not integration.manifest or not integration.config_flow:
|
||||||
|
continue
|
||||||
|
|
||||||
|
match_types = integration.manifest.get("usb", [])
|
||||||
|
|
||||||
|
if not match_types:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for entry in match_types:
|
||||||
|
match_list.append({"domain": domain, **entry})
|
||||||
|
|
||||||
|
return BASE.format(json.dumps(match_list, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||||
|
"""Validate usb file."""
|
||||||
|
usb_path = config.root / "homeassistant/generated/usb.py"
|
||||||
|
config.cache["usb"] = content = generate_and_validate(integrations)
|
||||||
|
|
||||||
|
if config.specific_integrations:
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(str(usb_path)) as fp:
|
||||||
|
current = fp.read().strip()
|
||||||
|
if current != content:
|
||||||
|
config.add_error(
|
||||||
|
"usb",
|
||||||
|
"File usb.py is not up to date. Run python3 -m script.hassfest",
|
||||||
|
fixable=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def generate(integrations: dict[str, Integration], config: Config) -> None:
|
||||||
|
"""Generate usb file."""
|
||||||
|
usb_path = config.root / "homeassistant/generated/usb.py"
|
||||||
|
with open(str(usb_path), "w") as fp:
|
||||||
|
fp.write(f"{config.cache['usb']}\n")
|
29
tests/components/usb/__init__.py
Normal file
29
tests/components/usb/__init__.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""Tests for the USB Discovery integration."""
|
||||||
|
|
||||||
|
|
||||||
|
from homeassistant.components.usb.models import USBDevice
|
||||||
|
|
||||||
|
conbee_device = USBDevice(
|
||||||
|
device="/dev/cu.usbmodemDE24338801",
|
||||||
|
vid="1CF1",
|
||||||
|
pid="0030",
|
||||||
|
serial_number="DE2433880",
|
||||||
|
manufacturer="dresden elektronik ingenieurtechnik GmbH",
|
||||||
|
description="ConBee II",
|
||||||
|
)
|
||||||
|
slae_sh_device = USBDevice(
|
||||||
|
device="/dev/cu.usbserial-110",
|
||||||
|
vid="10C4",
|
||||||
|
pid="EA60",
|
||||||
|
serial_number="00_12_4B_00_22_98_88_7F",
|
||||||
|
manufacturer="Silicon Labs",
|
||||||
|
description="slae.sh cc2652rb stick - slaesh's iot stuff",
|
||||||
|
)
|
||||||
|
electro_lama_device = USBDevice(
|
||||||
|
device="/dev/cu.usbserial-110",
|
||||||
|
vid="1A86",
|
||||||
|
pid="7523",
|
||||||
|
serial_number=None,
|
||||||
|
manufacturer=None,
|
||||||
|
description="USB2.0-Serial",
|
||||||
|
)
|
273
tests/components/usb/test_init.py
Normal file
273
tests/components/usb/test_init.py
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
"""Tests for the USB Discovery integration."""
|
||||||
|
import datetime
|
||||||
|
import sys
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
from . import slae_sh_device
|
||||||
|
|
||||||
|
from tests.common import async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not sys.platform.startswith("linux"),
|
||||||
|
reason="Only works on linux",
|
||||||
|
)
|
||||||
|
async def test_discovered_by_observer_before_started(hass):
|
||||||
|
"""Test a device is discovered by the observer before started."""
|
||||||
|
|
||||||
|
async def _mock_monitor_observer_callback(callback):
|
||||||
|
await hass.async_add_executor_job(
|
||||||
|
callback, MagicMock(action="add", device_path="/dev/new")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_mock_monitor_observer(monitor, callback, name):
|
||||||
|
hass.async_create_task(_mock_monitor_observer_callback(callback))
|
||||||
|
return MagicMock()
|
||||||
|
|
||||||
|
new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}]
|
||||||
|
|
||||||
|
mock_comports = [
|
||||||
|
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(
|
||||||
|
"homeassistant.components.usb.async_get_usb", return_value=new_usb
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.usb.comports", return_value=mock_comports
|
||||||
|
), patch(
|
||||||
|
"pyudev.MonitorObserver", new=_create_mock_monitor_observer
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, "usb", {"usb": {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with patch("homeassistant.components.usb.comports", return_value=[]), patch.object(
|
||||||
|
hass.config_entries.flow, "async_init"
|
||||||
|
) as mock_config_flow:
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_config_flow.mock_calls) == 1
|
||||||
|
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_removal_by_observer_before_started(hass):
|
||||||
|
"""Test a device is removed by the observer before started."""
|
||||||
|
|
||||||
|
async def _mock_monitor_observer_callback(callback):
|
||||||
|
await hass.async_add_executor_job(
|
||||||
|
callback, MagicMock(action="remove", device_path="/dev/new")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_mock_monitor_observer(monitor, callback, name):
|
||||||
|
hass.async_create_task(_mock_monitor_observer_callback(callback))
|
||||||
|
return MagicMock()
|
||||||
|
|
||||||
|
new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}]
|
||||||
|
|
||||||
|
mock_comports = [
|
||||||
|
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(
|
||||||
|
"homeassistant.components.usb.async_get_usb", return_value=new_usb
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.usb.comports", return_value=mock_comports
|
||||||
|
), patch(
|
||||||
|
"pyudev.MonitorObserver", new=_create_mock_monitor_observer
|
||||||
|
), 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()
|
||||||
|
|
||||||
|
with patch("homeassistant.components.usb.comports", return_value=[]):
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_config_flow.mock_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovered_by_scanner_after_started(hass):
|
||||||
|
"""Test a device is discovered by the scanner after the started event."""
|
||||||
|
new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}]
|
||||||
|
|
||||||
|
mock_comports = [
|
||||||
|
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("pyudev.Context", side_effect=ImportError), patch(
|
||||||
|
"homeassistant.components.usb.async_get_usb", return_value=new_usb
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.usb.comports", return_value=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()
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_config_flow.mock_calls) == 1
|
||||||
|
assert mock_config_flow.mock_calls[0][1][0] == "test1"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovered_by_scanner_after_started_match_vid_only(hass):
|
||||||
|
"""Test a device is discovered by the scanner after the started event only matching vid."""
|
||||||
|
new_usb = [{"domain": "test1", "vid": "3039"}]
|
||||||
|
|
||||||
|
mock_comports = [
|
||||||
|
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("pyudev.Context", side_effect=ImportError), patch(
|
||||||
|
"homeassistant.components.usb.async_get_usb", return_value=new_usb
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.usb.comports", return_value=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()
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_config_flow.mock_calls) == 1
|
||||||
|
assert mock_config_flow.mock_calls[0][1][0] == "test1"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovered_by_scanner_after_started_match_vid_wrong_pid(hass):
|
||||||
|
"""Test a device is discovered by the scanner after the started event only matching vid but wrong pid."""
|
||||||
|
new_usb = [{"domain": "test1", "vid": "3039", "pid": "9999"}]
|
||||||
|
|
||||||
|
mock_comports = [
|
||||||
|
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("pyudev.Context", side_effect=ImportError), patch(
|
||||||
|
"homeassistant.components.usb.async_get_usb", return_value=new_usb
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.usb.comports", return_value=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()
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_config_flow.mock_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovered_by_scanner_after_started_no_vid_pid(hass):
|
||||||
|
"""Test a device is discovered by the scanner after the started event with no vid or pid."""
|
||||||
|
new_usb = [{"domain": "test1", "vid": "3039", "pid": "9999"}]
|
||||||
|
|
||||||
|
mock_comports = [
|
||||||
|
MagicMock(
|
||||||
|
device=slae_sh_device.device,
|
||||||
|
vid=None,
|
||||||
|
pid=None,
|
||||||
|
serial_number=slae_sh_device.serial_number,
|
||||||
|
manufacturer=slae_sh_device.manufacturer,
|
||||||
|
description=slae_sh_device.description,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch("pyudev.Context", side_effect=ImportError), patch(
|
||||||
|
"homeassistant.components.usb.async_get_usb", return_value=new_usb
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.usb.comports", return_value=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()
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_config_flow.mock_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_non_matching_discovered_by_scanner_after_started(hass):
|
||||||
|
"""Test a device is discovered by the scanner after the started event that does not match."""
|
||||||
|
new_usb = [{"domain": "test1", "vid": "4444", "pid": "4444"}]
|
||||||
|
|
||||||
|
mock_comports = [
|
||||||
|
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("pyudev.Context", side_effect=ImportError), patch(
|
||||||
|
"homeassistant.components.usb.async_get_usb", return_value=new_usb
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.usb.comports", return_value=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()
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=1))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_config_flow.mock_calls) == 0
|
@ -2359,6 +2359,7 @@ async def test_async_setup_update_entry(hass):
|
|||||||
(
|
(
|
||||||
config_entries.SOURCE_DISCOVERY,
|
config_entries.SOURCE_DISCOVERY,
|
||||||
config_entries.SOURCE_SSDP,
|
config_entries.SOURCE_SSDP,
|
||||||
|
config_entries.SOURCE_USB,
|
||||||
config_entries.SOURCE_HOMEKIT,
|
config_entries.SOURCE_HOMEKIT,
|
||||||
config_entries.SOURCE_DHCP,
|
config_entries.SOURCE_DHCP,
|
||||||
config_entries.SOURCE_ZEROCONF,
|
config_entries.SOURCE_ZEROCONF,
|
||||||
|
@ -192,6 +192,12 @@ def test_integration_properties(hass):
|
|||||||
{"hostname": "tesla_*", "macaddress": "044EAF*"},
|
{"hostname": "tesla_*", "macaddress": "044EAF*"},
|
||||||
{"hostname": "tesla_*", "macaddress": "98ED5C*"},
|
{"hostname": "tesla_*", "macaddress": "98ED5C*"},
|
||||||
],
|
],
|
||||||
|
"usb": [
|
||||||
|
{"vid": "10C4", "pid": "EA60"},
|
||||||
|
{"vid": "1CF1", "pid": "0030"},
|
||||||
|
{"vid": "1A86", "pid": "7523"},
|
||||||
|
{"vid": "10C4", "pid": "8A2A"},
|
||||||
|
],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Royal Philips Electronics",
|
"manufacturer": "Royal Philips Electronics",
|
||||||
@ -216,6 +222,12 @@ def test_integration_properties(hass):
|
|||||||
{"hostname": "tesla_*", "macaddress": "044EAF*"},
|
{"hostname": "tesla_*", "macaddress": "044EAF*"},
|
||||||
{"hostname": "tesla_*", "macaddress": "98ED5C*"},
|
{"hostname": "tesla_*", "macaddress": "98ED5C*"},
|
||||||
]
|
]
|
||||||
|
assert integration.usb == [
|
||||||
|
{"vid": "10C4", "pid": "EA60"},
|
||||||
|
{"vid": "1CF1", "pid": "0030"},
|
||||||
|
{"vid": "1A86", "pid": "7523"},
|
||||||
|
{"vid": "10C4", "pid": "8A2A"},
|
||||||
|
]
|
||||||
assert integration.ssdp == [
|
assert integration.ssdp == [
|
||||||
{
|
{
|
||||||
"manufacturer": "Royal Philips Electronics",
|
"manufacturer": "Royal Philips Electronics",
|
||||||
@ -248,6 +260,7 @@ def test_integration_properties(hass):
|
|||||||
assert integration.homekit is None
|
assert integration.homekit is None
|
||||||
assert integration.zeroconf is None
|
assert integration.zeroconf is None
|
||||||
assert integration.dhcp is None
|
assert integration.dhcp is None
|
||||||
|
assert integration.usb is None
|
||||||
assert integration.ssdp is None
|
assert integration.ssdp is None
|
||||||
assert integration.mqtt is None
|
assert integration.mqtt is None
|
||||||
assert integration.version is None
|
assert integration.version is None
|
||||||
@ -268,6 +281,7 @@ def test_integration_properties(hass):
|
|||||||
assert integration.homekit is None
|
assert integration.homekit is None
|
||||||
assert integration.zeroconf == [{"type": "_hue._tcp.local.", "name": "hue*"}]
|
assert integration.zeroconf == [{"type": "_hue._tcp.local.", "name": "hue*"}]
|
||||||
assert integration.dhcp is None
|
assert integration.dhcp is None
|
||||||
|
assert integration.usb is None
|
||||||
assert integration.ssdp is None
|
assert integration.ssdp is None
|
||||||
|
|
||||||
|
|
||||||
@ -342,6 +356,28 @@ def _get_test_integration_with_dhcp_matcher(hass, name, config_flow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_test_integration_with_usb_matcher(hass, name, config_flow):
|
||||||
|
"""Return a generated test integration with a usb matcher."""
|
||||||
|
return loader.Integration(
|
||||||
|
hass,
|
||||||
|
f"homeassistant.components.{name}",
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"domain": name,
|
||||||
|
"config_flow": config_flow,
|
||||||
|
"dependencies": [],
|
||||||
|
"requirements": [],
|
||||||
|
"usb": [
|
||||||
|
{"vid": "10C4", "pid": "EA60"},
|
||||||
|
{"vid": "1CF1", "pid": "0030"},
|
||||||
|
{"vid": "1A86", "pid": "7523"},
|
||||||
|
{"vid": "10C4", "pid": "8A2A"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_get_custom_components(hass, enable_custom_integrations):
|
async def test_get_custom_components(hass, enable_custom_integrations):
|
||||||
"""Verify that custom components are cached."""
|
"""Verify that custom components are cached."""
|
||||||
test_1_integration = _get_test_integration(hass, "test_1", False)
|
test_1_integration = _get_test_integration(hass, "test_1", False)
|
||||||
@ -411,6 +447,24 @@ async def test_get_dhcp(hass):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_usb(hass):
|
||||||
|
"""Verify that custom components with usb matchers are found."""
|
||||||
|
test_1_integration = _get_test_integration_with_usb_matcher(hass, "test_1", True)
|
||||||
|
|
||||||
|
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
|
||||||
|
mock_get.return_value = {
|
||||||
|
"test_1": test_1_integration,
|
||||||
|
}
|
||||||
|
usb = await loader.async_get_usb(hass)
|
||||||
|
usb_for_domain = [entry for entry in usb if entry["domain"] == "test_1"]
|
||||||
|
assert usb_for_domain == [
|
||||||
|
{"domain": "test_1", "vid": "10C4", "pid": "EA60"},
|
||||||
|
{"domain": "test_1", "vid": "1CF1", "pid": "0030"},
|
||||||
|
{"domain": "test_1", "vid": "1A86", "pid": "7523"},
|
||||||
|
{"domain": "test_1", "vid": "10C4", "pid": "8A2A"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def test_get_homekit(hass):
|
async def test_get_homekit(hass):
|
||||||
"""Verify that custom components with homekit are found."""
|
"""Verify that custom components with homekit are found."""
|
||||||
test_1_integration = _get_test_integration(hass, "test_1", True)
|
test_1_integration = _get_test_integration(hass, "test_1", True)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user