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: <name>'), 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <proscar87@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Bas Nijholt <bas@nijho.lt>
Co-authored-by: Bas Nijholt <basnijholt@gmail.com>
This commit is contained in:
proscar87 2026-09-06 01:18:49 -06:00 committed by GitHub
commit 55f871fd0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 46 additions and 9 deletions

View file

@ -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: <name>"
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:

View file

@ -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_<name>` 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.