mirror of
https://github.com/home-assistant/core.git
synced 2025-07-30 18:58:04 +02:00
fixes
This commit is contained in:
@ -277,6 +277,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
|||||||
self._state_type = state_type
|
self._state_type = state_type
|
||||||
self._on_static_info_update(entity_info)
|
self._on_static_info_update(entity_info)
|
||||||
|
|
||||||
|
device_name = device_info.name
|
||||||
# Determine the device connection based on whether this entity belongs to a sub device
|
# Determine the device connection based on whether this entity belongs to a sub device
|
||||||
if entity_info.device_id:
|
if entity_info.device_id:
|
||||||
# Entity belongs to a sub device
|
# Entity belongs to a sub device
|
||||||
@ -285,6 +286,10 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
|||||||
(DOMAIN, f"{device_info.mac_address}_{entity_info.device_id}")
|
(DOMAIN, f"{device_info.mac_address}_{entity_info.device_id}")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# Use the pre-computed device_id_to_name mapping for O(1) lookup
|
||||||
|
device_name = entry_data.device_id_to_name.get(
|
||||||
|
entity_info.device_id, device_info.name
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Entity belongs to the main device
|
# Entity belongs to the main device
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
@ -292,7 +297,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if entity_info.name:
|
if entity_info.name:
|
||||||
self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}"
|
self.entity_id = f"{domain}.{device_name}_{entity_info.object_id}"
|
||||||
else:
|
else:
|
||||||
# https://github.com/home-assistant/core/issues/132532
|
# https://github.com/home-assistant/core/issues/132532
|
||||||
# If name is not set, ESPHome will use the sanitized friendly name
|
# If name is not set, ESPHome will use the sanitized friendly name
|
||||||
@ -300,7 +305,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
|||||||
# as the entity_id before it is sanitized since the sanitizer
|
# as the entity_id before it is sanitized since the sanitizer
|
||||||
# is not utf-8 aware. In this case, its always going to be
|
# is not utf-8 aware. In this case, its always going to be
|
||||||
# an empty string so we drop the object_id.
|
# an empty string so we drop the object_id.
|
||||||
self.entity_id = f"{domain}.{device_info.name}"
|
self.entity_id = f"{domain}.{device_name}"
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
|
@ -160,6 +160,7 @@ class RuntimeEntryData:
|
|||||||
assist_satellite_set_wake_word_callbacks: list[Callable[[str], None]] = field(
|
assist_satellite_set_wake_word_callbacks: list[Callable[[str], None]] = field(
|
||||||
default_factory=list
|
default_factory=list
|
||||||
)
|
)
|
||||||
|
device_id_to_name: dict[int, str] = field(default_factory=dict)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
|
@ -527,6 +527,11 @@ class ESPHomeManager:
|
|||||||
device_info.name,
|
device_info.name,
|
||||||
device_mac,
|
device_mac,
|
||||||
)
|
)
|
||||||
|
# Build device_id_to_name mapping for efficient lookup
|
||||||
|
entry_data.device_id_to_name = {
|
||||||
|
sub_device.device_id: sub_device.name or device_info.name
|
||||||
|
for sub_device in device_info.devices
|
||||||
|
}
|
||||||
self.device_id = _async_setup_device_registry(hass, entry, entry_data)
|
self.device_id = _async_setup_device_registry(hass, entry, entry_data)
|
||||||
|
|
||||||
entry_data.async_update_device_state()
|
entry_data.async_update_device_state()
|
||||||
|
@ -779,7 +779,7 @@ async def test_entity_assignment_to_sub_device(
|
|||||||
)
|
)
|
||||||
assert sub_device_1 is not None
|
assert sub_device_1 is not None
|
||||||
|
|
||||||
motion_sensor = entity_registry.async_get("binary_sensor.test_motion")
|
motion_sensor = entity_registry.async_get("binary_sensor.motion_sensor_motion")
|
||||||
assert motion_sensor is not None
|
assert motion_sensor is not None
|
||||||
assert motion_sensor.device_id == sub_device_1.id
|
assert motion_sensor.device_id == sub_device_1.id
|
||||||
|
|
||||||
@ -789,14 +789,14 @@ async def test_entity_assignment_to_sub_device(
|
|||||||
)
|
)
|
||||||
assert sub_device_2 is not None
|
assert sub_device_2 is not None
|
||||||
|
|
||||||
door_sensor = entity_registry.async_get("binary_sensor.test_door")
|
door_sensor = entity_registry.async_get("binary_sensor.door_sensor_door")
|
||||||
assert door_sensor is not None
|
assert door_sensor is not None
|
||||||
assert door_sensor.device_id == sub_device_2.id
|
assert door_sensor.device_id == sub_device_2.id
|
||||||
|
|
||||||
# Check states
|
# Check states
|
||||||
assert hass.states.get("binary_sensor.test_main_sensor").state == STATE_ON
|
assert hass.states.get("binary_sensor.test_main_sensor").state == STATE_ON
|
||||||
assert hass.states.get("binary_sensor.test_motion").state == STATE_OFF
|
assert hass.states.get("binary_sensor.motion_sensor_motion").state == STATE_OFF
|
||||||
assert hass.states.get("binary_sensor.test_door").state == STATE_ON
|
assert hass.states.get("binary_sensor.door_sensor_door").state == STATE_ON
|
||||||
|
|
||||||
# Check entity friendly names
|
# Check entity friendly names
|
||||||
# Main device entity should have: "{device_name} {entity_name}"
|
# Main device entity should have: "{device_name} {entity_name}"
|
||||||
@ -804,11 +804,11 @@ async def test_entity_assignment_to_sub_device(
|
|||||||
assert main_sensor_state.attributes[ATTR_FRIENDLY_NAME] == "Test Main Sensor"
|
assert main_sensor_state.attributes[ATTR_FRIENDLY_NAME] == "Test Main Sensor"
|
||||||
|
|
||||||
# Sub device 1 entity should have: "Motion Sensor Motion"
|
# Sub device 1 entity should have: "Motion Sensor Motion"
|
||||||
motion_sensor_state = hass.states.get("binary_sensor.test_motion")
|
motion_sensor_state = hass.states.get("binary_sensor.motion_sensor_motion")
|
||||||
assert motion_sensor_state.attributes[ATTR_FRIENDLY_NAME] == "Motion Sensor Motion"
|
assert motion_sensor_state.attributes[ATTR_FRIENDLY_NAME] == "Motion Sensor Motion"
|
||||||
|
|
||||||
# Sub device 2 entity should have: "Door Sensor Door"
|
# Sub device 2 entity should have: "Door Sensor Door"
|
||||||
door_sensor_state = hass.states.get("binary_sensor.test_door")
|
door_sensor_state = hass.states.get("binary_sensor.door_sensor_door")
|
||||||
assert door_sensor_state.attributes[ATTR_FRIENDLY_NAME] == "Door Sensor Door"
|
assert door_sensor_state.attributes[ATTR_FRIENDLY_NAME] == "Door Sensor Door"
|
||||||
|
|
||||||
|
|
||||||
@ -879,6 +879,7 @@ async def test_entity_friendly_names_with_empty_device_names(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check entity friendly name on sub-device with empty name
|
# Check entity friendly name on sub-device with empty name
|
||||||
|
# Since sub device has empty name, it falls back to main device name "test"
|
||||||
state_1 = hass.states.get("binary_sensor.test_motion")
|
state_1 = hass.states.get("binary_sensor.test_motion")
|
||||||
assert state_1 is not None
|
assert state_1 is not None
|
||||||
# With has_entity_name, friendly name is "{device_name} {entity_name}"
|
# With has_entity_name, friendly name is "{device_name} {entity_name}"
|
||||||
@ -886,13 +887,13 @@ async def test_entity_friendly_names_with_empty_device_names(
|
|||||||
assert state_1.attributes[ATTR_FRIENDLY_NAME] == "Main Device Motion Detected"
|
assert state_1.attributes[ATTR_FRIENDLY_NAME] == "Main Device Motion Detected"
|
||||||
|
|
||||||
# Check entity friendly name on sub-device with valid name
|
# Check entity friendly name on sub-device with valid name
|
||||||
state_2 = hass.states.get("binary_sensor.test_status")
|
state_2 = hass.states.get("binary_sensor.kitchen_light_status")
|
||||||
assert state_2 is not None
|
assert state_2 is not None
|
||||||
# Device has name "Kitchen Light", entity has name "Status"
|
# Device has name "Kitchen Light", entity has name "Status"
|
||||||
assert state_2.attributes[ATTR_FRIENDLY_NAME] == "Kitchen Light Status"
|
assert state_2.attributes[ATTR_FRIENDLY_NAME] == "Kitchen Light Status"
|
||||||
|
|
||||||
# Test entity with empty name on sub-device
|
# Test entity with empty name on sub-device
|
||||||
state_3 = hass.states.get("binary_sensor.test")
|
state_3 = hass.states.get("binary_sensor.kitchen_light")
|
||||||
assert state_3 is not None
|
assert state_3 is not None
|
||||||
# Entity has empty name, so friendly name is just the device name
|
# Entity has empty name, so friendly name is just the device name
|
||||||
assert state_3.attributes[ATTR_FRIENDLY_NAME] == "Kitchen Light"
|
assert state_3.attributes[ATTR_FRIENDLY_NAME] == "Kitchen Light"
|
||||||
@ -1030,3 +1031,134 @@ async def test_entity_switches_between_devices(
|
|||||||
sensor_entity = entity_registry.async_get("binary_sensor.test_sensor")
|
sensor_entity = entity_registry.async_get("binary_sensor.test_sensor")
|
||||||
assert sensor_entity is not None
|
assert sensor_entity is not None
|
||||||
assert sensor_entity.device_id == main_device.id
|
assert sensor_entity.device_id == main_device.id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entity_id_uses_sub_device_name(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
mock_client: APIClient,
|
||||||
|
mock_esphome_device: MockESPHomeDeviceType,
|
||||||
|
) -> None:
|
||||||
|
"""Test that entity_id uses sub device name when entity belongs to sub device."""
|
||||||
|
# Define sub devices
|
||||||
|
sub_devices = [
|
||||||
|
SubDeviceInfo(device_id=11111111, name="motion_sensor", area_id=0),
|
||||||
|
SubDeviceInfo(device_id=22222222, name="door_sensor", area_id=0),
|
||||||
|
]
|
||||||
|
|
||||||
|
device_info = {
|
||||||
|
"devices": sub_devices,
|
||||||
|
"name": "main_device",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create entities that belong to different devices
|
||||||
|
entity_info = [
|
||||||
|
# Entity for main device (device_id=0)
|
||||||
|
BinarySensorInfo(
|
||||||
|
object_id="main_sensor",
|
||||||
|
key=1,
|
||||||
|
name="Main Sensor",
|
||||||
|
unique_id="main_sensor",
|
||||||
|
device_id=0,
|
||||||
|
),
|
||||||
|
# Entity for sub device 1
|
||||||
|
BinarySensorInfo(
|
||||||
|
object_id="motion",
|
||||||
|
key=2,
|
||||||
|
name="Motion",
|
||||||
|
unique_id="motion",
|
||||||
|
device_id=11111111,
|
||||||
|
),
|
||||||
|
# Entity for sub device 2
|
||||||
|
BinarySensorInfo(
|
||||||
|
object_id="door",
|
||||||
|
key=3,
|
||||||
|
name="Door",
|
||||||
|
unique_id="door",
|
||||||
|
device_id=22222222,
|
||||||
|
),
|
||||||
|
# Entity without name on sub device
|
||||||
|
BinarySensorInfo(
|
||||||
|
object_id="sensor_no_name",
|
||||||
|
key=4,
|
||||||
|
name="",
|
||||||
|
unique_id="sensor_no_name",
|
||||||
|
device_id=11111111,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
states = [
|
||||||
|
BinarySensorState(key=1, state=True, missing_state=False),
|
||||||
|
BinarySensorState(key=2, state=False, missing_state=False),
|
||||||
|
BinarySensorState(key=3, state=True, missing_state=False),
|
||||||
|
BinarySensorState(key=4, state=True, missing_state=False),
|
||||||
|
]
|
||||||
|
|
||||||
|
await mock_esphome_device(
|
||||||
|
mock_client=mock_client,
|
||||||
|
device_info=device_info,
|
||||||
|
entity_info=entity_info,
|
||||||
|
states=states,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check entity_id for main device entity
|
||||||
|
# Should be: binary_sensor.{main_device_name}_{object_id}
|
||||||
|
assert hass.states.get("binary_sensor.main_device_main_sensor") is not None
|
||||||
|
|
||||||
|
# Check entity_id for sub device 1 entity
|
||||||
|
# Should be: binary_sensor.{sub_device_name}_{object_id}
|
||||||
|
assert hass.states.get("binary_sensor.motion_sensor_motion") is not None
|
||||||
|
|
||||||
|
# Check entity_id for sub device 2 entity
|
||||||
|
# Should be: binary_sensor.{sub_device_name}_{object_id}
|
||||||
|
assert hass.states.get("binary_sensor.door_sensor_door") is not None
|
||||||
|
|
||||||
|
# Check entity_id for entity without name on sub device
|
||||||
|
# Should be: binary_sensor.{sub_device_name}
|
||||||
|
assert hass.states.get("binary_sensor.motion_sensor") is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entity_id_with_empty_sub_device_name(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
mock_client: APIClient,
|
||||||
|
mock_esphome_device: MockESPHomeDeviceType,
|
||||||
|
) -> None:
|
||||||
|
"""Test entity_id when sub device has empty name (falls back to main device name)."""
|
||||||
|
# Define sub device with empty name
|
||||||
|
sub_devices = [
|
||||||
|
SubDeviceInfo(device_id=11111111, name="", area_id=0), # Empty name
|
||||||
|
]
|
||||||
|
|
||||||
|
device_info = {
|
||||||
|
"devices": sub_devices,
|
||||||
|
"name": "main_device",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create entity on sub device with empty name
|
||||||
|
entity_info = [
|
||||||
|
BinarySensorInfo(
|
||||||
|
object_id="sensor",
|
||||||
|
key=1,
|
||||||
|
name="Sensor",
|
||||||
|
unique_id="sensor",
|
||||||
|
device_id=11111111,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
states = [
|
||||||
|
BinarySensorState(key=1, state=True, missing_state=False),
|
||||||
|
]
|
||||||
|
|
||||||
|
await mock_esphome_device(
|
||||||
|
mock_client=mock_client,
|
||||||
|
device_info=device_info,
|
||||||
|
entity_info=entity_info,
|
||||||
|
states=states,
|
||||||
|
)
|
||||||
|
|
||||||
|
# When sub device has empty name, entity_id should use main device name
|
||||||
|
# Should be: binary_sensor.{main_device_name}_{object_id}
|
||||||
|
assert hass.states.get("binary_sensor.main_device_sensor") is not None
|
||||||
|
Reference in New Issue
Block a user