Skip to content

Commit f1e0954

Browse files
authored
Automatically setup hardware integrations when firmware info is published by an integration (home-assistant#154030)
1 parent 3c3b4ef commit f1e0954

File tree

12 files changed

+827
-64
lines changed

12 files changed

+827
-64
lines changed

homeassistant/components/homeassistant_connect_zbt2/config_flow.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@
77

88
from homeassistant.components import usb
99
from homeassistant.components.homeassistant_hardware import firmware_config_flow
10+
from homeassistant.components.homeassistant_hardware.helpers import (
11+
HardwareFirmwareDiscoveryInfo,
12+
)
1013
from homeassistant.components.homeassistant_hardware.util import (
1114
ApplicationType,
1215
FirmwareInfo,
1316
ResetTarget,
1417
)
18+
from homeassistant.components.usb import (
19+
usb_service_info_from_device,
20+
usb_unique_id_from_service_info,
21+
)
1522
from homeassistant.config_entries import (
1623
ConfigEntry,
1724
ConfigEntryBaseFlow,
@@ -123,22 +130,16 @@ def async_get_options_flow(
123130

124131
async def async_step_usb(self, discovery_info: UsbServiceInfo) -> ConfigFlowResult:
125132
"""Handle usb discovery."""
126-
device = discovery_info.device
127-
vid = discovery_info.vid
128-
pid = discovery_info.pid
129-
serial_number = discovery_info.serial_number
130-
manufacturer = discovery_info.manufacturer
131-
description = discovery_info.description
132-
unique_id = f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}"
133-
134-
device = discovery_info.device = await self.hass.async_add_executor_job(
133+
unique_id = usb_unique_id_from_service_info(discovery_info)
134+
135+
discovery_info.device = await self.hass.async_add_executor_job(
135136
usb.get_serial_by_id, discovery_info.device
136137
)
137138

138139
try:
139140
await self.async_set_unique_id(unique_id)
140141
finally:
141-
self._abort_if_unique_id_configured(updates={DEVICE: device})
142+
self._abort_if_unique_id_configured(updates={DEVICE: discovery_info.device})
142143

143144
self._usb_info = discovery_info
144145

@@ -148,6 +149,24 @@ async def async_step_usb(self, discovery_info: UsbServiceInfo) -> ConfigFlowResu
148149

149150
return await self.async_step_confirm()
150151

152+
async def async_step_import(
153+
self, fw_discovery_info: HardwareFirmwareDiscoveryInfo
154+
) -> ConfigFlowResult:
155+
"""Handle import from ZHA/OTBR firmware notification."""
156+
assert fw_discovery_info["usb_device"] is not None
157+
usb_info = usb_service_info_from_device(fw_discovery_info["usb_device"])
158+
unique_id = usb_unique_id_from_service_info(usb_info)
159+
160+
if await self.async_set_unique_id(unique_id, raise_on_progress=False):
161+
self._abort_if_unique_id_configured(updates={DEVICE: usb_info.device})
162+
163+
self._usb_info = usb_info
164+
self._device = usb_info.device
165+
self._hardware_name = HARDWARE_NAME
166+
self._probed_firmware_info = fw_discovery_info["firmware_info"]
167+
168+
return self._async_flow_finished()
169+
151170
def _async_flow_finished(self) -> ConfigFlowResult:
152171
"""Create the config entry."""
153172
assert self._usb_info is not None

homeassistant/components/homeassistant_hardware/const.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
ZHA_DOMAIN = "zha"
2020
OTBR_DOMAIN = "otbr"
2121

22+
HARDWARE_INTEGRATION_DOMAINS = {
23+
"homeassistant_sky_connect",
24+
"homeassistant_connect_zbt2",
25+
"homeassistant_yellow",
26+
}
27+
2228
OTBR_ADDON_NAME = "OpenThread Border Router"
2329
OTBR_ADDON_MANAGER_DATA = "openthread_border_router"
2430
OTBR_ADDON_SLUG = "core_openthread_border_router"

homeassistant/components/homeassistant_hardware/helpers.py

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,33 @@
66
from collections.abc import AsyncIterator, Awaitable, Callable
77
from contextlib import asynccontextmanager
88
import logging
9-
from typing import TYPE_CHECKING, Protocol
9+
from typing import TYPE_CHECKING, Protocol, TypedDict
1010

11-
from homeassistant.config_entries import ConfigEntry
11+
from homeassistant.components.usb import (
12+
USBDevice,
13+
async_get_usb_matchers_for_device,
14+
usb_device_from_path,
15+
)
16+
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
1217
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
1318

1419
from . import DATA_COMPONENT
20+
from .const import HARDWARE_INTEGRATION_DOMAINS
1521

1622
if TYPE_CHECKING:
1723
from .util import FirmwareInfo
1824

25+
1926
_LOGGER = logging.getLogger(__name__)
2027

2128

29+
class HardwareFirmwareDiscoveryInfo(TypedDict):
30+
"""Data for triggering hardware integration discovery via firmware notification."""
31+
32+
usb_device: USBDevice | None
33+
firmware_info: FirmwareInfo
34+
35+
2236
class SyncHardwareFirmwareInfoModule(Protocol):
2337
"""Protocol type for Home Assistant Hardware firmware info platform modules."""
2438

@@ -46,6 +60,23 @@ async def async_get_firmware_info(
4660
)
4761

4862

63+
@hass_callback
64+
def async_get_hardware_domain_for_usb_device(
65+
hass: HomeAssistant, usb_device: USBDevice
66+
) -> str | None:
67+
"""Identify which hardware domain should handle a USB device."""
68+
matched = async_get_usb_matchers_for_device(hass, usb_device)
69+
hw_domains = {match["domain"] for match in matched} & HARDWARE_INTEGRATION_DOMAINS
70+
71+
if not hw_domains:
72+
return None
73+
74+
# We can never have two hardware integrations overlap in discovery
75+
assert len(hw_domains) == 1
76+
77+
return list(hw_domains)[0]
78+
79+
4980
class HardwareInfoDispatcher:
5081
"""Central dispatcher for hardware/firmware information."""
5182

@@ -94,14 +125,56 @@ async def notify_firmware_info(
94125
"Received firmware info notification from %r: %s", domain, firmware_info
95126
)
96127

97-
for callback in self._notification_callbacks.get(firmware_info.device, []):
128+
for callback in list(self._notification_callbacks[firmware_info.device]):
98129
try:
99130
callback(firmware_info)
100131
except Exception:
101132
_LOGGER.exception(
102133
"Error while notifying firmware info listener %s", callback
103134
)
104135

136+
await self._async_trigger_hardware_discovery(firmware_info)
137+
138+
async def _async_trigger_hardware_discovery(
139+
self, firmware_info: FirmwareInfo
140+
) -> None:
141+
"""Trigger hardware integration config flows from firmware info.
142+
143+
Identifies which hardware integration should handle the device based on
144+
USB matchers, then triggers an import flow for only that integration.
145+
"""
146+
147+
usb_device = await self.hass.async_add_executor_job(
148+
usb_device_from_path, firmware_info.device
149+
)
150+
151+
if usb_device is None:
152+
_LOGGER.debug("Cannot find USB for path %s", firmware_info.device)
153+
return
154+
155+
hardware_domain = async_get_hardware_domain_for_usb_device(
156+
self.hass, usb_device
157+
)
158+
159+
if hardware_domain is None:
160+
_LOGGER.debug("No hardware integration found for device %s", usb_device)
161+
return
162+
163+
_LOGGER.debug(
164+
"Triggering %s import flow for device %s",
165+
hardware_domain,
166+
firmware_info.device,
167+
)
168+
169+
await self.hass.config_entries.flow.async_init(
170+
hardware_domain,
171+
context={"source": SOURCE_IMPORT},
172+
data=HardwareFirmwareDiscoveryInfo(
173+
usb_device=usb_device,
174+
firmware_info=firmware_info,
175+
),
176+
)
177+
105178
async def iter_firmware_info(self) -> AsyncIterator[FirmwareInfo]:
106179
"""Iterate over all firmware information for all hardware."""
107180
for domain, fw_info_module in self._providers.items():

homeassistant/components/homeassistant_hardware/manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"name": "Home Assistant Hardware",
44
"after_dependencies": ["hassio"],
55
"codeowners": ["@home-assistant/core"],
6+
"dependencies": ["usb"],
67
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
78
"integration_type": "system",
89
"requirements": [

homeassistant/components/homeassistant_sky_connect/config_flow.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@
1010
firmware_config_flow,
1111
silabs_multiprotocol_addon,
1212
)
13+
from homeassistant.components.homeassistant_hardware.helpers import (
14+
HardwareFirmwareDiscoveryInfo,
15+
)
1316
from homeassistant.components.homeassistant_hardware.util import (
1417
ApplicationType,
1518
FirmwareInfo,
1619
)
20+
from homeassistant.components.usb import (
21+
usb_service_info_from_device,
22+
usb_unique_id_from_service_info,
23+
)
1724
from homeassistant.config_entries import (
1825
ConfigEntry,
1926
ConfigEntryBaseFlow,
@@ -142,32 +149,48 @@ def async_get_options_flow(
142149

143150
async def async_step_usb(self, discovery_info: UsbServiceInfo) -> ConfigFlowResult:
144151
"""Handle usb discovery."""
145-
device = discovery_info.device
146-
vid = discovery_info.vid
147-
pid = discovery_info.pid
148-
serial_number = discovery_info.serial_number
149-
manufacturer = discovery_info.manufacturer
150-
description = discovery_info.description
151-
unique_id = f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}"
152+
unique_id = usb_unique_id_from_service_info(discovery_info)
152153

153154
if await self.async_set_unique_id(unique_id):
154-
self._abort_if_unique_id_configured(updates={DEVICE: device})
155+
self._abort_if_unique_id_configured(updates={DEVICE: discovery_info.device})
155156

156157
discovery_info.device = await self.hass.async_add_executor_job(
157158
usb.get_serial_by_id, discovery_info.device
158159
)
159160

160161
self._usb_info = discovery_info
161162

162-
assert description is not None
163-
self._hw_variant = HardwareVariant.from_usb_product_name(description)
163+
assert discovery_info.description is not None
164+
self._hw_variant = HardwareVariant.from_usb_product_name(
165+
discovery_info.description
166+
)
164167

165168
# Set parent class attributes
166169
self._device = self._usb_info.device
167170
self._hardware_name = self._hw_variant.full_name
168171

169172
return await self.async_step_confirm()
170173

174+
async def async_step_import(
175+
self, fw_discovery_info: HardwareFirmwareDiscoveryInfo
176+
) -> ConfigFlowResult:
177+
"""Handle import from ZHA/OTBR firmware notification."""
178+
assert fw_discovery_info["usb_device"] is not None
179+
usb_info = usb_service_info_from_device(fw_discovery_info["usb_device"])
180+
unique_id = usb_unique_id_from_service_info(usb_info)
181+
182+
if await self.async_set_unique_id(unique_id, raise_on_progress=False):
183+
self._abort_if_unique_id_configured(updates={DEVICE: usb_info.device})
184+
185+
self._usb_info = usb_info
186+
assert usb_info.description is not None
187+
self._hw_variant = HardwareVariant.from_usb_product_name(usb_info.description)
188+
self._device = usb_info.device
189+
self._hardware_name = self._hw_variant.full_name
190+
self._probed_firmware_info = fw_discovery_info["firmware_info"]
191+
192+
return self._async_flow_finished()
193+
171194
def _async_flow_finished(self) -> ConfigFlowResult:
172195
"""Create the config entry."""
173196
assert self._usb_info is not None

homeassistant/components/homeassistant_yellow/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
4141

4242
firmware = ApplicationType(entry.data[FIRMWARE])
4343

44+
# Auto start the multiprotocol addon if it is in use
4445
if firmware is ApplicationType.CPC:
4546
try:
4647
await check_multi_pan_addon(hass)

0 commit comments

Comments
 (0)