From 07f361a404b9aaac0a0c83e53814b9d608472d92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Jun 2025 18:26:09 -0500 Subject: [PATCH] empty name uses device name, use get_base_entity_object_id --- esphome/core/entity_helpers.py | 13 ++++++-- ...ties_not_allowed_on_different_devices.yaml | 30 ++++++++++++++++++- tests/integration/test_duplicate_entities.py | 25 +++++++++++++++- tests/unit_tests/core/test_entity_helpers.py | 11 +++++++ 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 62dc1d7b57..2442fbca4b 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -187,8 +187,17 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy # Get the entity name entity_name = config[CONF_NAME] - # For duplicate detection, just use the sanitized name - name_key = sanitize(snake_case(entity_name)) + # Get device name if entity is on a sub-device + device_name = None + if CONF_DEVICE_ID in config: + device_id_obj = config[CONF_DEVICE_ID] + device_name = device_id_obj.id + + # Calculate what object_id will actually be used + # This handles empty names correctly by using device/friendly names + name_key = get_base_entity_object_id( + entity_name, CORE.friendly_name, device_name + ) # Check for duplicates unique_key = (platform, name_key) diff --git a/tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml b/tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml index 275f36a7b9..f7d017a0ae 100644 --- a/tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml +++ b/tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml @@ -133,7 +133,35 @@ button: name: "Reset Main" on_press: [] # Main device -# Scenario 6: Special characters in names - now with unique names +# Scenario 6: Empty names (should use device names) +select: + - platform: template + name: "" + device_id: controller_1 + options: + - "Option 1" + - "Option 2" + lambda: return {"Option 1"}; + set_action: [] + + - platform: template + name: "" + device_id: controller_2 + options: + - "Option 1" + - "Option 2" + lambda: return {"Option 1"}; + set_action: [] + + - platform: template + name: "" # Main device + options: + - "Option 1" + - "Option 2" + lambda: return {"Option 1"}; + set_action: [] + +# Scenario 7: Special characters in names - now with unique names number: - platform: template name: "Temperature Setpoint! Controller 1" diff --git a/tests/integration/test_duplicate_entities.py b/tests/integration/test_duplicate_entities.py index 88747facb1..b7ee8dd478 100644 --- a/tests/integration/test_duplicate_entities.py +++ b/tests/integration/test_duplicate_entities.py @@ -52,6 +52,7 @@ async def test_duplicate_entities_not_allowed_on_different_devices( switches = [e for e in all_entities if e.__class__.__name__ == "SwitchInfo"] buttons = [e for e in all_entities if e.__class__.__name__ == "ButtonInfo"] numbers = [e for e in all_entities if e.__class__.__name__ == "NumberInfo"] + selects = [e for e in all_entities if e.__class__.__name__ == "SelectInfo"] # Scenario 1: Check that temperature sensors have unique names per device temp_sensors = [s for s in sensors if "Temperature" in s.name] @@ -144,7 +145,28 @@ async def test_duplicate_entities_not_allowed_on_different_devices( f"Reset buttons should have unique names, got {reset_names}" ) - # Scenario 7: Check special characters in number names - now unique + # Scenario 7: Check empty name selects (should use device names) + empty_selects = [s for s in selects if s.name == ""] + assert len(empty_selects) == 3, ( + f"Expected exactly 3 empty name selects, got {len(empty_selects)}" + ) + + # Group by device + c1_selects = [s for s in empty_selects if s.device_id == controller_1.device_id] + c2_selects = [s for s in empty_selects if s.device_id == controller_2.device_id] + + # For main device, device_id is 0 + main_selects = [s for s in empty_selects if s.device_id == 0] + + # Check object IDs for empty name entities - they should use device names + assert len(c1_selects) == 1 and c1_selects[0].object_id == "controller_1" + assert len(c2_selects) == 1 and c2_selects[0].object_id == "controller_2" + assert ( + len(main_selects) == 1 + and main_selects[0].object_id == "duplicate-entities-test" + ) + + # Scenario 8: Check special characters in number names - now unique temp_numbers = [n for n in numbers if "Temperature Setpoint!" in n.name] assert len(temp_numbers) == 2, ( f"Expected exactly 2 temperature setpoint numbers, got {len(temp_numbers)}" @@ -170,6 +192,7 @@ async def test_duplicate_entities_not_allowed_on_different_devices( + len(switches) + len(buttons) + len(numbers) + + len(selects) ) def on_state(state) -> None: diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index a5e44c9b2a..0dcdd84507 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -555,6 +555,17 @@ def test_entity_duplicate_validator_with_devices() -> None: assert validated3 == config3 assert ("sensor", "humidity") in CORE.unique_ids + # Empty names should use device names and be allowed + config4 = {CONF_NAME: "", CONF_DEVICE_ID: device1} + validated4 = validator(config4) + assert validated4 == config4 + assert ("sensor", "device1") in CORE.unique_ids + + config5 = {CONF_NAME: "", CONF_DEVICE_ID: device2} + validated5 = validator(config5) + assert validated5 == config5 + assert ("sensor", "device2") in CORE.unique_ids + def test_duplicate_entity_yaml_validation( yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]