From 55f871fd0cdf07968c822908fe0a8d9e6db1c956 Mon Sep 17 00:00:00 2001 From: proscar87 <68169114+proscar87@users.noreply.github.com> Date: Sun, 6 Sep 2026 01:18:49 -0600 Subject: [PATCH] Adopt has_entity_name to fix duplicated entity ids on HA 2026.4+ (#1499) * Adopt has_entity_name to fix duplicated entity ids on HA 2026.4+ Since HA core 2026.4 (PR 166246) composes entity names as device name + entity name, and only strips the device prefix when the entity name starts with it. Adaptive Lighting's names ('Adaptive Lighting Sleep Mode: stairs' on device 'Adaptive Lighting: stairs') never match, so new installs get ids like switch.adaptive_lighting_stairs_adaptive_lighting_sleep_mode_stairs. Adopt has_entity_name: the main switch takes the device name ('Adaptive Lighting: '), the simple switches use their role ('Sleep Mode', 'Adapt Brightness', 'Adapt Color'). Unique ids are unchanged, so existing installs keep their entity ids via the registry. Fixes #1459 Co-Authored-By: Claude Fable 5 * Test the new entity ids and that existing ones survive The renamed constants were defined but never asserted, so neither the fresh-install ids nor the registry-preservation claim were covered. Co-Authored-By: Claude Fable 5 * test: keep the apply-service test light on Avoid generating brightness zero in the attribute-change helper, which turns the light off and makes the test depend on the current adaptive brightness. --------- Co-authored-by: proscar87 Co-authored-by: Claude Fable 5 Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 15 ++++--- tests/test_switch.py | 40 +++++++++++++++++-- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 696ae1a2..e5e63b3f 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -852,6 +852,8 @@ def _attributes_have_changed( class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" + _attr_has_entity_name = True + def __init__( self, hass: HomeAssistant, @@ -1003,9 +1005,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the device if any.""" - return f"Adaptive Lighting: {self._name}" + # The main switch takes the device name "Adaptive Lighting: " + return None @property def unique_id(self) -> str: @@ -1024,7 +1027,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): identifiers={ (DOMAIN, self._name), }, - name=self._name, + name=f"Adaptive Lighting: {self._name}", entry_type=DeviceEntryType.SERVICE, ) @@ -1636,6 +1639,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): class SimpleSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" + _attr_has_entity_name = True + def __init__( self, which: str, @@ -1657,8 +1662,8 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): @property def name(self) -> str: - """Return the name of the device if any.""" - return self._name + """Return the name of the entity within its device.""" + return self._which @property def unique_id(self) -> str: diff --git a/tests/test_switch.py b/tests/test_switch.py index 37e9df38..24ead48d 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -155,9 +155,9 @@ ENTITY_LIGHT_2 = "light.light_2" ENTITY_LIGHT_3 = "light.light_3" _SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}" ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}" -ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}" -ENTITY_ADAPT_BRIGHTNESS_SWITCH = f"{_SWITCH_FMT}_adapt_brightness_{DEFAULT_NAME}" -ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}" +ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}_sleep_mode" +ENTITY_ADAPT_BRIGHTNESS_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}_adapt_brightness" +ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}_adapt_color" ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE @@ -1092,7 +1092,7 @@ async def test_apply_service(hass): assert entity_id not in switch.lights def increased_brightness(): - return (light._attr_brightness + 100) % 255 + return max(1, (light._attr_brightness + 100) % 255) def increased_color_temp(): return max( @@ -3254,6 +3254,38 @@ async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): ), f"AL overrode manual brightness {manual_brightness} with {al_brightness}" +async def test_fresh_install_entity_ids(hass): + """Test the entity ids a new install gets with device-relative naming.""" + _, switch = await setup_switch(hass, {}) + + assert switch.entity_id == ENTITY_SWITCH + assert switch.sleep_mode_switch.entity_id == ENTITY_SLEEP_MODE_SWITCH + assert switch.adapt_brightness_switch.entity_id == ENTITY_ADAPT_BRIGHTNESS_SWITCH + assert switch.adapt_color_switch.entity_id == ENTITY_ADAPT_COLOR_SWITCH + + +async def test_existing_entity_ids_are_preserved(hass): + """Test an install predating this change keeps its entity ids. + + The unique ids are unchanged, so the entity registry must keep the + classic `..._sleep_mode_` id instead of renaming the entity. + """ + classic_entity_id = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}" + assert classic_entity_id != ENTITY_SLEEP_MODE_SWITCH + + registry = entity_registry.async_get(hass) + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + f"{DEFAULT_NAME}_sleep_mode", + suggested_object_id=classic_entity_id.split(".", 1)[1], + ) + + _, switch = await setup_switch(hass, {}) + + assert switch.sleep_mode_switch.entity_id == classic_entity_id + + def test_validate_ui_options_win_over_stale_data(): """A UI-configured entry's `options` (from the options flow) must win.