Repository navigation
[Core] Moving an entity to another device keeps a stale disabled_by: device #81
Description
Problem
When an entity registry entry moves to another device, the entity registry does not re-evaluate a disabled_by: device disabler. An entity that was disabled together with its old device stays disabled_by: device on the new device, even when the new device is enabled.
Nothing clears it later. EntityRegistry.async_device_modified only re-enables device-disabled entities when the device they are on now gets a registry update, and the frontend locks the "Enabled" switch for disabled_by: device (see #79). So the entity stays disabled for no visible reason, until the user happens to change the new device (rename it, change its area, or disable and re-enable it).
Both ways of moving an entity are affected:
- The entity platform, when an entity is added with a
device_infothat resolves to another device:EntityRegistry.async_get_or_createupdatesdevice_idof the existing entry and keeps itsdisabled_by. The platform only handles the opposite direction: an enabled entity added to a disabled device getsdisabled_by: device(entity_platform.pyL1146). EntityRegistry.async_update_entity(entity_id, device_id=...)handles neither direction (_async_update_entityonly adjustsconfig_entrydisablers).
Why it matters now
Child devices (home-assistant/architecture#1414) make moving entities between devices routine: an integration moves them between the main device and its child devices whenever the split changes (a re-interview, a quirk or firmware change, a device losing an endpoint). If the user had disabled a child device, its entities come back on the main device disabled. ZHA (zigpy/zha#947), Z-Wave (home-assistant/core#185985) and Matter (home-assistant/core#186033) all move entities this way. The same stuck disabler showed up while reviewing the Matter PR: after a child device was pruned and the entry reloaded, its outlet switch came back on the enabled node device with disabled_by: device.
Reproduction
On the ZHA child device branch (TheJulianJES/core zigpy-bot/zha-child-devices at TheJulianJES/core@ce12ea0a44a, with zigpy/zha#947 at zigpy/zha@a3bd6ff5), with ZHA's own fix-up (_async_reconcile_child_devices) patched out:
- Join a two-socket Tuya
TS011F, which gets the child devices "Channel 1" and "Channel 2". - Disable the "Channel 1" child device. Its switch becomes
disabled_by: device. - Re-interview the device with the split turned off. The entity platform moves the switch back to the main device, which is enabled, but the switch stays
disabled_by: device.
Expected
When an entity's device_id changes, the entity registry reconciles a device disabler with the new device: clear it if the new device is enabled, and set it if the new device is disabled. This matches what already happens to config_entry disablers when an entity's config entry changes.
Workaround
The ZHA branch fixes the disabler by hand after every move (_async_move_entity in homeassistant/components/zha/helpers.py). Clearing it re-enables the entity, so EntityRegistryDisabledHandler reloads the ZHA config entry 30 seconds later, as for any re-enabled entity.
Related
- [Core] Re-created entity restores
disabled_by: deviceon a device that is not disabled #79: the same stale disabler, restored from a deleted entity entry. - [Core] No core support for removing child devices an integration no longer provides #82: no core support for removing child devices an integration no longer provides, which is where these moves happen.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Size
Area
Activity
zigpy-review-bot commentedon Oct 20, 2026
Also fixed by home-assistant/core#186750:
_async_update_entitynow derives adevice/config_entry/ unsetdisabled_byagain wheneverdevice_idorconfig_entry_idchanges, in both directions. This covers the entity platform path (moving an existing entry inasync_get_or_creategoes through it) as well as directasync_update_entity(device_id=...)calls, and the platform's one-way special case is removed. Covered bytest_update_entity_device_id_disabled_by.