diff --git a/homeassistant/components/bang_olufsen/__init__.py b/homeassistant/components/bang_olufsen/__init__.py index f55971aed4dc..8f18ea01b8ed 100644 --- a/homeassistant/components/bang_olufsen/__init__.py +++ b/homeassistant/components/bang_olufsen/__init__.py @@ -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) diff --git a/homeassistant/components/bryant_evolution/__init__.py b/homeassistant/components/bryant_evolution/__init__.py index bf8cab983df2..153a85b6a38c 100644 --- a/homeassistant/components/bryant_evolution/__init__.py +++ b/homeassistant/components/bryant_evolution/__init__.py @@ -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}", ) diff --git a/homeassistant/components/bryant_evolution/climate.py b/homeassistant/components/bryant_evolution/climate.py index 7eb52a6d0122..11af1d1bdbaa 100644 --- a/homeassistant/components/bryant_evolution/climate.py +++ b/homeassistant/components/bryant_evolution/climate.py @@ -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}", ) diff --git a/homeassistant/components/bsblan/entity.py b/homeassistant/components/bsblan/entity.py index f7c6a8a773cd..1f138c385be7 100644 --- a/homeassistant/components/bsblan/entity.py +++ b/homeassistant/components/bsblan/entity.py @@ -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"), diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py index bf1f9e719082..4086be646f1c 100644 --- a/homeassistant/components/comelit/coordinator.py +++ b/homeassistant/components/comelit/coordinator.py @@ -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", diff --git a/homeassistant/components/control4/entity.py b/homeassistant/components/control4/entity.py index d7b57330af50..9fe497f18a3a 100644 --- a/homeassistant/components/control4/entity.py +++ b/homeassistant/components/control4/entity.py @@ -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, + ), ) diff --git a/homeassistant/components/deconz/entity.py b/homeassistant/components/deconz/entity.py index b65f91f1c910..32487e3e4e45 100644 --- a/homeassistant/components/deconz/entity.py +++ b/homeassistant/components/deconz/entity.py @@ -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, + ), ) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index df8b73c14537..6092f7ec95ae 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -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 diff --git a/homeassistant/components/egauge/coordinator.py b/homeassistant/components/egauge/coordinator.py index 0f7256451833..e6077a692f2b 100644 --- a/homeassistant/components/egauge/coordinator.py +++ b/homeassistant/components/egauge/coordinator.py @@ -43,6 +43,7 @@ class EgaugeData: class EgaugeDataCoordinator(DataUpdateCoordinator[EgaugeData]): """Class to manage fetching eGauge data.""" + config_entry: EgaugeConfigEntry serial_number: str hostname: str diff --git a/homeassistant/components/egauge/entity.py b/homeassistant/components/egauge/entity.py index cc12606907d3..cf15c6e8abcc 100644 --- a/homeassistant/components/egauge/entity.py +++ b/homeassistant/components/egauge/entity.py @@ -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, + ), ) diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index 9742085f4998..635c7a7cf340 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -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 diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index ea147ee2276e..287bb2c1d037 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -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( diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index 17f70f8e4d4e..6b42975c2c22 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -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]] diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 6bcbde7cd72c..ff2da91152da 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -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, + ), ) diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 1f797bb86f88..fdf9b35cbc00 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -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, ) diff --git a/homeassistant/components/iaqualink/coordinator.py b/homeassistant/components/iaqualink/coordinator.py index 989e1cb18b5c..abd2e0423cb4 100644 --- a/homeassistant/components/iaqualink/coordinator.py +++ b/homeassistant/components/iaqualink/coordinator.py @@ -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: diff --git a/homeassistant/components/iaqualink/entity.py b/homeassistant/components/iaqualink/entity.py index 8213c40780ec..c26d9c0753be 100644 --- a/homeassistant/components/iaqualink/entity.py +++ b/homeassistant/components/iaqualink/entity.py @@ -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, diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index d40ba43b8e16..ecdf7abb8344 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -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: diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index c7e50d2c59df..da97812e00d9 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -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"), [