Ignore static add-on devices when attributing serial ports (#181074)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Balloob Bot
2026-09-02 12:07:10 +00:00
committed by Franck Nijhof
co-authored by Paulus Schoutsen Claude Fable 5.1
parent 2a92f8bb3a
commit 0dbe35ec4a
2 changed files with 47 additions and 10 deletions
+21 -6
View File
@@ -1,6 +1,6 @@
"""Attribution of serial ports to the integrations and apps using them."""
from collections.abc import Mapping, Sequence
from collections.abc import Iterator, Mapping, Sequence
import os
import re
from typing import Any
@@ -156,14 +156,29 @@ async def _async_get_config_entry_consumers(
return consumers
def _iter_option_device_paths(value: Any) -> Iterator[str]:
"""Yield device paths configured anywhere in the options of an app."""
if isinstance(value, str):
if value.startswith("/dev/"):
yield value
elif isinstance(value, Mapping):
for item in value.values():
yield from _iter_option_device_paths(item)
elif isinstance(value, list):
for item in value:
yield from _iter_option_device_paths(item)
@callback
def _async_get_app_consumers(
hass: HomeAssistant,
) -> dict[str, list[SerialPortConsumer]]:
"""Return devices mapped into apps, either statically or through options.
"""Return devices configured in the options of apps.
Supervisor resolves `device(subsystem=tty)` options into real devices, so device
paths that no longer exist are missing and non-serial devices are included.
The `devices` field of an app also lists the static devices of its manifest,
which are mapped into the container whether the app uses them or not, so only
options are evidence of a device being used. Options can refer to devices
that no longer exist or are not serial ports.
"""
if not is_hassio(hass):
return {}
@@ -179,7 +194,7 @@ def _async_get_app_consumers(
if info is None:
continue
for device in info["devices"]:
for device in _iter_option_device_paths(info["options"]):
consumers.setdefault(device, []).append(
SerialPortConsumer(
kind="app",
@@ -224,7 +239,7 @@ async def async_get_serial_port_consumers(
consumers.setdefault(device, []).extend(path_consumers)
for path, path_consumers in app_consumers.items():
# Apps also map non-serial devices, only scanned ports are of interest
# Options can name non-serial devices, only scanned ports are of interest
resolved_path = resolved[path]
if resolved_path not in aliases:
+26 -4
View File
@@ -439,17 +439,26 @@ async def test_app_consumers(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test detecting serial ports mapped into apps."""
"""Test detecting serial ports configured in the options of apps."""
apps_info = {
"core_zwave_js": {
"name": "Z-Wave JS",
"state": "started",
"devices": [TTY_USB0_BY_ID, "/dev/dri/card0"],
"devices": [TTY_USB0],
"options": {"device": TTY_USB0_BY_ID, "gpu": "/dev/dri/card0"},
},
"some_app": {
"name": "Some App",
"state": "stopped",
"devices": [TTY_USB1],
"options": {"serial": [{"port": TTY_USB0}]},
},
# Static devices of the manifest are mapped regardless of being used
"wmbusmeters": {
"name": "Wmbusmeters",
"state": "started",
"devices": [TTY_USB0, TTY_USB1],
"options": {"reset_config": False},
},
"uninstalled_app": None,
}
@@ -478,7 +487,15 @@ async def test_app_consumers(
"domain": None,
"config_entry_id": None,
"slug": "core_zwave_js",
}
},
{
"kind": "app",
"title": "Some App",
"active": False,
"domain": None,
"config_entry_id": None,
"slug": "some_app",
},
],
),
(ESPHOME_PORT, []),
@@ -531,7 +548,12 @@ async def test_multiple_consumers(
entry.add_to_hass(hass)
apps_info = {
"some_app": {"name": "Some App", "state": "started", "devices": [TTY_USB0]}
"some_app": {
"name": "Some App",
"state": "started",
"devices": [TTY_USB0],
"options": {"device": TTY_USB0},
}
}
with (