mirror of
https://github.com/esphome/esphome.git
synced 2026-10-10 21:39:06 +00:00
[usb_host] Warn instead of failing when a device duplicates a usb_uart entry (#20373)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from itertools import combinations
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_MANUFACTURER
|
||||
@@ -22,6 +23,8 @@ import esphome.final_validate as fv
|
||||
from esphome.helpers import cpp_u16string_escape
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTO_LOAD = ["bytebuffer"]
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
@@ -71,15 +74,19 @@ def usb_device_schema(
|
||||
)
|
||||
|
||||
|
||||
def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]:
|
||||
def _clients_overlap(first: ConfigType, second: ConfigType) -> bool:
|
||||
# Two entries overlap when no field they both constrain tells them apart
|
||||
for first, second in combinations(configs, 2):
|
||||
for key, wildcard in _FILTER_WILDCARDS.items():
|
||||
a = first.get(key)
|
||||
b = second.get(key)
|
||||
if wildcard not in (a, b) and a != b:
|
||||
break
|
||||
else:
|
||||
for key, wildcard in _FILTER_WILDCARDS.items():
|
||||
a = first.get(key)
|
||||
b = second.get(key)
|
||||
if wildcard not in (a, b) and a != b:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]:
|
||||
for first, second in itertools.combinations(configs, 2):
|
||||
if _clients_overlap(first, second):
|
||||
raise cv.Invalid(
|
||||
f"USB configs overlap: {first[CONF_ID]}, {second[CONF_ID]}"
|
||||
)
|
||||
@@ -87,11 +94,29 @@ def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]:
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
# Every USB client on the bus, whichever component configured it: any two could
|
||||
# otherwise open the same device
|
||||
clients = list(config.get(CONF_DEVICES) or ())
|
||||
clients.extend(fv.full_config.get().get("usb_uart") or ())
|
||||
validate_usb_clients(clients)
|
||||
# Any two overlapping clients could otherwise open the same device
|
||||
devices = config.get(CONF_DEVICES) or []
|
||||
uarts = fv.full_config.get().get("usb_uart") or []
|
||||
validate_usb_clients(devices)
|
||||
validate_usb_clients(uarts)
|
||||
# Remove before 2027.10.0, then pool devices and uarts in one validate_usb_clients call
|
||||
for device, uart in itertools.product(devices, uarts):
|
||||
if not _clients_overlap(device, uart):
|
||||
continue
|
||||
# The device matches nothing the usb_uart entry does not already claim
|
||||
redundant = all(
|
||||
uart.get(key) in (wildcard, device.get(key))
|
||||
for key, wildcard in _FILTER_WILDCARDS.items()
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"usb_host device '%s' matches the same USB device as usb_uart '%s'; %s. "
|
||||
"This will be an error in 2027.10.0",
|
||||
device[CONF_ID],
|
||||
uart[CONF_ID],
|
||||
"remove it from usb_host devices"
|
||||
if redundant
|
||||
else "narrow its filter so it no longer matches that device",
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""Tests for usb_host device matching validation."""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.usb_host import validate_usb_clients
|
||||
from esphome.components.usb_host import _final_validate, validate_usb_clients
|
||||
import esphome.config_validation as cv
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
@@ -109,3 +112,73 @@ def test_every_pair_is_compared_not_just_neighbours() -> None:
|
||||
]
|
||||
with pytest.raises(cv.Invalid, match="a, c"):
|
||||
validate_usb_clients(configs)
|
||||
|
||||
|
||||
def _run_final_validate(
|
||||
devices: list[ConfigType], uarts: list[ConfigType]
|
||||
) -> ConfigType:
|
||||
config = {"id": "usb_host", "devices": devices}
|
||||
token = fv.full_config.set({"usb_host": config, "usb_uart": uarts})
|
||||
try:
|
||||
return _final_validate(config)
|
||||
finally:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_device_duplicating_usb_uart_warns(caplog: pytest.LogCaptureFixture) -> None:
|
||||
devices = [{"id": "device_0", "vid": 0x303A, "pid": 0x4001}]
|
||||
uarts = [{"id": "uart_0", "vid": 0x303A, "pid": 0x4001}]
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_run_final_validate(devices, uarts)
|
||||
assert "'device_0'" in caplog.text
|
||||
assert "'uart_0'" in caplog.text
|
||||
assert "remove it from usb_host devices" in caplog.text
|
||||
|
||||
|
||||
def test_wildcard_device_covering_usb_uart_suggests_narrowing(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
devices = [{"id": "device_0", "vid": 0x303A, "pid": 0}]
|
||||
uarts = [{"id": "uart_0", "vid": 0x303A, "pid": 0x4001}]
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_run_final_validate(devices, uarts)
|
||||
assert "narrow its filter" in caplog.text
|
||||
assert "remove it" not in caplog.text
|
||||
|
||||
|
||||
def test_disjoint_device_and_usb_uart_do_not_warn(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
devices = [{"id": "device_0", "vid": 0x303A, "pid": 0x4002}]
|
||||
uarts = [{"id": "uart_0", "vid": 0x303A, "pid": 0x4001}]
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_run_final_validate(devices, uarts)
|
||||
assert caplog.text == ""
|
||||
|
||||
|
||||
_DUPLICATE_CLIENTS = [
|
||||
{"id": "a", "vid": 0x303A, "pid": 0x4001},
|
||||
{"id": "b", "vid": 0x303A, "pid": 0x4001},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("devices", "uarts"),
|
||||
[([*_DUPLICATE_CLIENTS], []), ([], [*_DUPLICATE_CLIENTS])],
|
||||
ids=["devices", "uarts"],
|
||||
)
|
||||
def test_overlap_within_one_component_is_rejected(
|
||||
devices: list[ConfigType], uarts: list[ConfigType]
|
||||
) -> None:
|
||||
with pytest.raises(cv.Invalid, match="a, b"):
|
||||
_run_final_validate(devices, uarts)
|
||||
|
||||
|
||||
def test_device_inside_broader_usb_uart_suggests_removing(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
devices = [{"id": "device_0", "vid": 0x303A, "pid": 0x4001}]
|
||||
uarts = [{"id": "uart_0", "vid": 0x303A, "pid": 0}]
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_run_final_validate(devices, uarts)
|
||||
assert "remove it from usb_host devices" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user