Migrate integrations to setting via_device_id in DeviceInfo (#177494)

This commit is contained in:
Erik Montnemery
2026-07-29 00:42:49 +02:00
committed by GitHub
parent 1b63f75551
commit 2a1c3fe5f7
19 changed files with 150 additions and 22 deletions
@@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
# Create device now as BeoWebsocket needs a device for
# debug logging, firing events etc.
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.unique_id)},
model=entry.data[CONF_MODEL],
@@ -92,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
serial_number=remote.serial_number,
sw_version=remote.app_version,
manufacturer=MANUFACTURER,
via_device=(DOMAIN, entry.unique_id),
via_device_id=device.id,
)
websocket = BeoWebsocket(hass, entry, client)
@@ -35,7 +35,7 @@ async def async_setup_entry(
# Add a device for the SAM itself.
sam_uid = names.sam_device_uid(entry)
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
sam_device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, sam_uid)},
manufacturer="Bryant",
@@ -55,7 +55,7 @@ async def async_setup_entry(
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, names.system_device_uid(sam_uid, sys_id))},
via_device=(DOMAIN, names.sam_device_uid(entry)),
via_device_id=sam_device.id,
manufacturer="Bryant",
name=f"System {sys_id}",
)
@@ -17,6 +17,7 @@ from homeassistant.components.climate import (
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -45,6 +46,7 @@ async def async_setup_entry(
zone_id = sz[1]
client = config_entry.runtime_data.get(tuple(sz))
climate = BryantEvolutionClimate(
hass,
client,
system_id,
zone_id,
@@ -82,6 +84,7 @@ class BryantEvolutionClimate(ClimateEntity):
def __init__(
self,
hass: HomeAssistant,
client: BryantEvolutionLocalClient,
system_id: int,
zone_id: int,
@@ -94,7 +97,11 @@ class BryantEvolutionClimate(ClimateEntity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
manufacturer="Bryant",
via_device=(DOMAIN, names.system_device_uid(sam_uid, system_id)),
via_device_id=dr.async_get_device_id_by_identifier(
hass,
(DOMAIN, names.system_device_uid(sam_uid, system_id)),
config_entry_id=sam_uid, # This is the config entry id
),
name=f"System {system_id} Zone {zone_id}",
)
+11 -2
View File
@@ -3,6 +3,7 @@
from typing import override
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -53,7 +54,11 @@ class BSBLanCircuitEntity(BSBLanEntity):
identifiers={(DOMAIN, f"{mac}-circuit-{circuit}")},
translation_key="heating_circuit",
translation_placeholders={"circuit": str(circuit)},
via_device=(DOMAIN, mac),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, mac),
config_entry_id=coordinator.config_entry.entry_id,
),
manufacturer=main_info["manufacturer"],
model=main_info.get("model"),
model_id=main_info.get("model_id"),
@@ -101,7 +106,11 @@ class BSBLanWaterHeaterDeviceEntity(BSBLanDualCoordinatorEntity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{mac}-water-heater")},
translation_key="water_heater",
via_device=(DOMAIN, mac),
via_device_id=dr.async_get_device_id_by_identifier(
fast_coordinator.hass,
(DOMAIN, mac),
config_entry_id=fast_coordinator.config_entry.entry_id,
),
manufacturer=main_info["manufacturer"],
model=main_info.get("model"),
model_id=main_info.get("model_id"),
@@ -93,7 +93,11 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
f"{self.config_entry.entry_id}-{object_type}-{object_class.index}",
)
},
via_device=(DOMAIN, self.config_entry.entry_id),
via_device_id=dr.async_get_device_id_by_identifier(
self.hass,
(DOMAIN, self.config_entry.entry_id),
config_entry_id=self.config_entry.entry_id,
),
name=object_class.name,
model=f"{self._device} {object_type}",
manufacturer="Comelit",
+6 -1
View File
@@ -2,6 +2,7 @@
from typing import Any, override
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
@@ -47,5 +48,9 @@ class Control4Entity(CoordinatorEntity[Any]):
manufacturer=self._device_manufacturer,
model=self._device_model,
name=self._device_name,
via_device=(DOMAIN, self._controller_unique_id),
via_device_id=dr.async_get_device_id_by_identifier(
self.hass,
(DOMAIN, self._controller_unique_id),
config_entry_id=self.coordinator.config_entry.entry_id,
),
)
+11 -2
View File
@@ -9,6 +9,7 @@ from pydeconz.models.scene import Scene as PydeconzScene
from pydeconz.models.sensor import SensorBase as PydeconzSensorBase
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
@@ -64,7 +65,11 @@ class DeconzBase[_DeviceT: _DeviceType]:
model=self._device.model_id,
name=self._device.name,
sw_version=self._device.software_version,
via_device=(DOMAIN, self.hub.api.config.bridge_id),
via_device_id=dr.async_get_device_id_by_identifier(
self.hub.hass,
(DOMAIN, self.hub.api.config.bridge_id),
config_entry_id=self.hub.config_entry.entry_id,
),
)
@@ -185,5 +190,9 @@ class DeconzSceneMixin(DeconzDevice[PydeconzScene]):
manufacturer="dresden elektronik",
model="deCONZ group",
name=self.deconz_group.name,
via_device=(DOMAIN, self.hub.api.config.bridge_id),
via_device_id=dr.async_get_device_id_by_identifier(
self.hub.hass,
(DOMAIN, self.hub.api.config.bridge_id),
config_entry_id=self.hub.config_entry.entry_id,
),
)
+6 -1
View File
@@ -27,6 +27,7 @@ from homeassistant.components.light import (
LightEntityFeature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.color import (
@@ -411,7 +412,11 @@ class DeconzGroup(DeconzBaseLight[Group]):
manufacturer="dresden elektronik",
model="deCONZ group",
name=self._device.name,
via_device=(DOMAIN, self.hub.api.config.bridge_id),
via_device_id=dr.async_get_device_id_by_identifier(
self.hub.hass,
(DOMAIN, self.hub.api.config.bridge_id),
config_entry_id=self.hub.config_entry.entry_id,
),
)
@property
@@ -43,6 +43,7 @@ class EgaugeData:
class EgaugeDataCoordinator(DataUpdateCoordinator[EgaugeData]):
"""Class to manage fetching eGauge data."""
config_entry: EgaugeConfigEntry
serial_number: str
hostname: str
+6 -1
View File
@@ -1,5 +1,6 @@
"""Base entity for the eGauge integration."""
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -29,5 +30,9 @@ class EgaugeEntity(CoordinatorEntity[EgaugeDataCoordinator]):
name=register_name,
manufacturer=MANUFACTURER,
model=MODEL,
via_device=(DOMAIN, coordinator.serial_number),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, coordinator.serial_number),
config_entry_id=coordinator.config_entry.entry_id,
),
)
@@ -542,13 +542,18 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
self._devices[dev_mac] = device
# manually register device entry for new connected device
dr.async_get(self.hass).async_get_or_create(
device_registry = dr.async_get(self.hass)
device_registry.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, dev_mac)},
default_manufacturer="FRITZ!",
default_model="FRITZ!Box Tracked device",
default_name=device.hostname,
via_device=(DOMAIN, self.unique_id),
via_device_id=dr.async_get_device_id_by_identifier(
self.hass,
(DOMAIN, self.unique_id),
config_entry_id=self.config_entry.entry_id,
),
)
return True
+5 -1
View File
@@ -259,7 +259,11 @@ class FroniusSolarNet:
"model", inverter["device_type"]["value"]
),
name=inverter.get("custom_name", {}).get("value"),
via_device=(DOMAIN, self.solar_net_device_id),
via_device_id=dr.async_get_device_id_by_identifier(
self.hass,
(DOMAIN, self.solar_net_device_id),
config_entry_id=self.config_entry.entry_id,
),
)
inverter_infos.append(
FroniusDeviceInfo(
@@ -31,7 +31,7 @@ from .sensor import (
)
if TYPE_CHECKING:
from . import FroniusSolarNet
from . import FroniusConfigEntry, FroniusSolarNet
class FroniusCoordinatorBase(
@@ -39,6 +39,7 @@ class FroniusCoordinatorBase(
):
"""Query Fronius endpoint and keep track of seen conditions."""
config_entry: FroniusConfigEntry
default_interval: timedelta
error_interval: timedelta
valid_descriptions: Mapping[Platform, Sequence[FroniusEntityDescription]]
+16 -3
View File
@@ -24,6 +24,7 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -859,7 +860,11 @@ class MeterSensor(_FroniusSensorEntity):
manufacturer=meter_data["manufacturer"]["value"],
model=meter_data["model"]["value"],
name=meter_data["model"]["value"],
via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, coordinator.solar_net.solar_net_device_id),
config_entry_id=coordinator.config_entry.entry_id,
),
)
self._attr_unique_id = f"{meter_uid}-{description.key}"
@@ -883,7 +888,11 @@ class OhmpilotSensor(_FroniusSensorEntity):
model=f"{device_data['model']['value']} {device_data['hardware']['value']}",
name=device_data["model"]["value"],
sw_version=device_data["software"]["value"],
via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, coordinator.solar_net.solar_net_device_id),
config_entry_id=coordinator.config_entry.entry_id,
),
)
self._attr_unique_id = f"{device_data['serial']['value']}-{description.key}"
@@ -925,5 +934,9 @@ class StorageSensor(_FroniusSensorEntity):
manufacturer=storage_data["manufacturer"]["value"],
model=storage_data["model"]["value"],
name=storage_data["model"]["value"],
via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, coordinator.solar_net.solar_net_device_id),
config_entry_id=coordinator.config_entry.entry_id,
),
)
@@ -90,6 +90,10 @@ class ShadeEntity(HDEntity):
manufacturer=MANUFACTURER,
model=self._shade.type_name,
sw_version=self._shade.firmware,
via_device=(DOMAIN, self._device_info.serial_number),
via_device_id=dr.async_get_device_id_by_identifier(
self.coordinator.hass,
(DOMAIN, self._device_info.serial_number),
config_entry_id=self.coordinator.config_entry.entry_id,
),
configuration_url=self._configuration_url,
)
@@ -24,6 +24,8 @@ _LOGGER = logging.getLogger(__name__)
class AqualinkDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""Data coordinator for Aqualink systems."""
config_entry: ConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, system: Any
) -> None:
+6 -1
View File
@@ -4,6 +4,7 @@ from typing import override
from iaqualink.device import AqualinkDevice
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -34,7 +35,11 @@ class AqualinkEntity[AqualinkDeviceT: AqualinkDevice](
self._attr_unique_id = f"{dev.system.serial}_{dev.name}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
via_device=(DOMAIN, dev.system.serial),
via_device_id=dr.async_get_device_id_by_identifier(
coordinator.hass,
(DOMAIN, dev.system.serial),
config_entry_id=coordinator.config_entry.entry_id,
),
manufacturer=dev.manufacturer,
model=dev.model,
name=dev.label,
+21
View File
@@ -3149,6 +3149,27 @@ def async_get(hass: HomeAssistant) -> DeviceRegistry:
raise RuntimeError("Device registry not set up") from ex
@callback
def async_get_device_id_by_identifier(
hass: HomeAssistant, identifier: tuple[str, str], *, config_entry_id: str
) -> str:
"""Get the id of the device with the identifier, owned by the config entry.
Convenience wrapper for linking a device to its via device through
via_device_id. Identifiers are unique within a config entry, so the lookup
cannot be ambiguous.
Raises ValueError if no such device exists.
"""
device = async_get(hass).async_get_device_by_identifier(identifier, config_entry_id)
if device is None:
raise ValueError(
f"There is no device with identifier {identifier} in config entry "
f"{config_entry_id}"
)
return device.id
def async_setup(hass: HomeAssistant) -> None:
"""Set up device registry."""
if DATA_REGISTRY in hass.data:
+28
View File
@@ -2430,6 +2430,34 @@ async def test_async_get_device_by_connection_normalizes(
)
async def test_async_get_device_id_by_identifier(
hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
"""The id lookup returns the device id, and raises when there is no match."""
entry = MockConfigEntry(domain="test")
entry.add_to_hass(hass)
device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id, identifiers={("test", "1")}
)
assert (
dr.async_get_device_id_by_identifier(
hass, ("test", "1"), config_entry_id=entry.entry_id
)
== device.id
)
# A missing device is treated as an error: an unknown identifier or the
# wrong config entry both raise rather than silently returning None.
with pytest.raises(ValueError, match="no device with identifier"):
dr.async_get_device_id_by_identifier(
hass, ("test", "missing"), config_entry_id=entry.entry_id
)
with pytest.raises(ValueError, match="no device with identifier"):
dr.async_get_device_id_by_identifier(
hass, ("test", "1"), config_entry_id="unknown_entry_id"
)
@pytest.mark.parametrize(
("create_kwargs", "lookup_kwargs", "miss_kwargs"),
[