Skip to content

fix: remove per-slot devices when the slot is removed - #1402

Merged
raman325 merged 2 commits into
mainfrom
fix/1399-orphaned-slot-devices
Aug 11, 2026
Merged

fix: remove per-slot devices when the slot is removed#1402
raman325 merged 2 commits into
mainfrom
fix/1399-orphaned-slot-devices

Conversation

@raman325

Copy link
Copy Markdown
Owner

Proposed change

Each slot gets its own device (entity.py, identifiers={(DOMAIN, f"{entry_id}|{slot_num}")}), but removing a slot from config only tore down its entities — async_update_listener's slots_to_remove branch never touched the device registry.

Home Assistant does not clean these up either. device_registry.async_cleanup reaps a device only when it is referenced by neither an entity nor a live config entry; a slot device stays attached to the Lock Code Manager entry, so it is permanently exempt. And with no async_remove_config_entry_device hook, Home Assistant rendered no Delete button — matching the report's "no way to delete it besides completely removing the config entry."

Three changes:

  • Slot removal now removes the slot's device, after the entity removers have run, so registry teardown order matches a normal removal (entities first, then their device).
  • Setup sweeps devices whose slot is no longer configured. Registries already polluted by this bug clean up on the next reload rather than needing manual deletion. It sweeps by identifier rather than diffing against a previous config, so it also catches slots removed while the entry was unloaded.
  • async_remove_config_entry_device exposes the Delete button, and allows it only for slot devices that are not currently configured. Deleting a live slot's device would strand its entities and be undone on the next reload; the entry's own device (bare entry_id, no slot suffix) has to outlive the slots that hang off it, so it is refused too.

The device identifier format moves into build_slot_device_identifier / parse_slot_device_identifier next to the existing build_slot_unique_id. The sweep has to parse identifiers back into slot numbers, so the format can no longer live inline at the three call sites that were each rebuilding it by hand.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (which adds functionality)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Three new tests, one per change: removing a slot retires its device while the surviving slot's and the entry's own devices are untouched; a stale slot device is pruned on reload; and the removal hook refuses a configured slot's device and the entry device but permits an unconfigured one. All three were confirmed to fail with the production changes reverted.

Full suite passes (1248 passed). The 43 errors in tests/providers/zha/test_provider.py are pre-existing on main in my local environment and unrelated to this change.

🤖 Generated with Claude Code

Each slot gets its own device, but dropping a slot from config only tore
down its entities. Home Assistant's registry cleanup reaps a device only
when neither an entity nor a live config entry references it, and a slot
device stays attached to the LCM entry -- so it survived forever, and
with no async_remove_config_entry_device hook there was no Delete button
either. The only escape was deleting the whole config entry.

- Slot removal now removes the slot's device, after its entities.
- Setup sweeps devices whose slot is no longer configured, so registries
  already polluted by this bug clean up on the next reload instead of
  needing manual deletion.
- async_remove_config_entry_device exposes the Delete button, and allows
  it only for slot devices that are not currently configured. Deleting a
  live slot's device would strand its entities and be undone on reload;
  the entry's own device has to outlive the slots that hang off it.

The device identifier format moves into build/parse helpers next to
build_slot_unique_id -- the sweep has to parse identifiers back into slot
numbers, so the format can no longer live inline at three call sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3431bbad1215
Copilot AI lite review requested due to automatic review settings August 11, 2026 01:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added python Pull requests that update Python code bug Something isn't working labels Aug 11, 2026
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.99%. Comparing base (8732dd3) to head (0a48629).
⚠️ Report is 6 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1402      +/-   ##
==========================================
+ Coverage   96.98%   96.99%   +0.01%     
==========================================
  Files          53       53              
  Lines        6495     6533      +38     
  Branches      470      470              
==========================================
+ Hits         6299     6337      +38     
  Misses        196      196              
Flag Coverage Δ
python 97.53% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
custom_components/lock_code_manager/__init__.py 96.15% <100.00%> (+0.26%) ⬆️
custom_components/lock_code_manager/diagnostics.py 96.20% <100.00%> (+0.04%) ⬆️
...stom_components/lock_code_manager/domain/config.py 100.00% <100.00%> (ø)
custom_components/lock_code_manager/entity.py 94.92% <100.00%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

`parse_slot_device_identifier` gated on `str.isdigit()`, which rejects the
negative slot number `build_slot_device_identifier` will happily encode --
the slots YAML schema puts no lower bound on the key, so a user can
configure slot -1 today. Its device then parsed to None, which made the
orphan sweep skip it AND made the removal hook classify it as the entry's
own device and refuse deletion: precisely the stuck, undeletable device
issue #1399 is about, reached by a different route.

Parse with `int()` and verify the round-trip instead. A bare `int()` would
have over-corrected -- it accepts `+1` and `1_0` as aliases the builder
never emits, which would map two identifiers onto one slot.

Tests cover the builder/parser round-trip over negative, zero and large
slots, rejection of non-slot and alias identifiers, and both the sweep and
the removal hook against a negative-slot device. All five fail against the
previous parser.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 35550f166bfd
@raman325
raman325 merged commit 186a20b into main Aug 11, 2026
18 checks passed
@raman325
raman325 deleted the fix/1399-orphaned-slot-devices branch August 11, 2026 03:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ISSUE] Remove old lock codes

2 participants