diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 37f8e738aee..81dd7d7f1ed 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -244,9 +244,21 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]): self._key = entity_info.key self._state_type = state_type self._on_static_info_update(entity_info) - self._attr_device_info = DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)} - ) + + # Determine the device connection based on whether this entity belongs to a sub device + if entity_info.device_id: + # Entity belongs to a sub device + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, f"{device_info.mac_address}_{entity_info.device_id}") + } + ) + else: + # Entity belongs to the main device + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)} + ) + if entity_info.name: self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}" else: diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index b4af39586d4..84c9b36f838 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -751,6 +751,26 @@ def _async_setup_device_registry( device_info = entry_data.device_info if TYPE_CHECKING: assert device_info is not None + + device_registry = dr.async_get(hass) + # Build sets of valid device identifiers and connections + valid_connections = {(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)} + valid_identifiers = { + (DOMAIN, f"{device_info.mac_address}_{sub_device.device_id}") + for sub_device in device_info.devices + } + + # Remove devices that no longer exist + for device in dr.async_entries_for_config_entry(device_registry, entry.entry_id): + # Skip devices we want to keep + if ( + device.connections & valid_connections + or device.identifiers & valid_identifiers + ): + continue + # Remove everything else + device_registry.async_remove_device(device.id) + sw_version = device_info.esphome_version if device_info.compilation_time: sw_version += f" ({device_info.compilation_time})" @@ -779,11 +799,14 @@ def _async_setup_device_registry( f"{device_info.project_version} (ESPHome {device_info.esphome_version})" ) - suggested_area = None + suggested_area: str | None = None if device_info.suggested_area: suggested_area = device_info.suggested_area + elif device_info.area: + # Use main device area if suggested_area is not set + suggested_area = device_info.area.name - device_registry = dr.async_get(hass) + # Create/update main device device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, configuration_url=configuration_url, @@ -794,6 +817,36 @@ def _async_setup_device_registry( sw_version=sw_version, suggested_area=suggested_area, ) + + # Handle sub devices + # Find available areas from device_info + areas_by_id = {area.area_id: area for area in device_info.areas} + # Create/update sub devices that should exist + for sub_device in device_info.devices: + # Create a unique identifier for this sub device + sub_device_identifier = f"{device_info.mac_address}_{sub_device.device_id}" + + # Determine the area for this sub device + sub_device_suggested_area: str | None = None + if sub_device.area_id and sub_device.area_id in areas_by_id: + sub_device_suggested_area = areas_by_id[sub_device.area_id].name + + sub_device_entry = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, sub_device_identifier)}, + name=sub_device.name, + manufacturer=manufacturer, + model=model, + sw_version=sw_version, + suggested_area=sub_device_suggested_area, + ) + + # Update the sub device to set via_device_id + device_registry.async_update_device( + sub_device_entry.id, + via_device_id=device_entry.id, + ) + return device_entry.id