Skip to content

Allow removing entity-less devices; evict telemetry dedup map on entity removal#297

Merged
awolden merged 1 commit into
meshcore-dev:mainfrom
mwolter805:fix/removable-contact-devices-pr
Jun 26, 2026
Merged

Allow removing entity-less devices; evict telemetry dedup map on entity removal#297
awolden merged 1 commit into
meshcore-dev:mainfrom
mwolter805:fix/removable-contact-devices-pr

Conversation

@mwolter805

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #247 / #229. #247 made orphaned repeater/client devices removable from the HA UI and, per review, deliberately refused contact/unknown devices while the contact is still live — to avoid deleting a device while it is producing live data and leaving stale entity references behind. Two gaps remain:

  1. An entity-less device is still refused while the contact is live. Once the user has already removed a device's only entity (e.g. a discovered contact's telemetry sensor), the device page shows "This device has no entities" — but Delete is still refused because the pubkey prefix is a live contact, so HA renders the rejection as an unhelpful [object Object] dialog and the empty shell can't be removed. This is the entity-less half of Remove old devices in HA #229.
  2. The telemetry/GPS dedup maps are never evicted on entity removal. TelemetrySensorManager.discovered_sensors and DeviceTrackerManager.discovered_trackers cache the live entity objects by key. Nothing pops those caches when HA removes an entity (UI delete, device-delete cascade, or platform unload), so the manager keeps updating a deregistered entity and won't recreate it until an HA restart — the same desync already patched in the untracked-contact-discard (coordinator.py) and data-only-demote (services.py) flows; the UI-removal path just never reached either eviction.

Changes

  • __init__.py (async_remove_config_entry_device): insert an early "device has no entities → allow removal" branch, excluding the hub (identifier == entry_id). The existing feat: allow removing orphan repeater and client devices from HA UI #247 guards (hub, subscribed repeater/client, live contact/unknown) are unchanged and still govern populated devices — the new branch is only reached when er.async_entries_for_device(..., include_disabled_entities=True) returns empty.
  • telemetry_sensor.py / device_tracker.py: register async_on_remove(partial(self.discovered_*.pop, key, None)) on each telemetry sensor and GPS tracker at the manager's creation site, so removal pops the dedup key and a later telemetry/GPS event recreates the entity instead of updating a deregistered one.

Tests

  • Unit (tests/test_device_removal.py): 3 added — an emptied contact device is removable even while the contact is live; the hub stays refused even when empty; a populated live contact stays refused (the feat: allow removing orphan repeater and client devices from HA UI #247 guard, preserved). Existing refusal/orphan tests run under an autouse non-empty entity-count default, so feat: allow removing orphan repeater and client devices from HA UI #247's behavior is unchanged.
  • Integration (tests_integration/test_dedup_eviction.py): new regression — drives the real telemetry/GPS manager handlers under real Home Assistant, removes the entity via the entity registry, and asserts the dedup key is evicted (riding the real async_on_remove lifecycle). Fails without the eviction.
  • Also verified on a live HA install: deleting an emptied contact device now succeeds with no error dialog, and no "rejected by integration" line is logged.

Compatibility / scope

  • Additive — reverts nothing from feat: allow removing orphan repeater and client devices from HA UI #247. entity_registry is already imported; async_on_remove is standard HA. No dependency or version bump; the populated-device guards are preserved.
  • A removed entity-less device can reappear if the node telemeters again (clean recreate, by design); permanent "ignore this contact" suppression is out of scope.

Files

  • custom_components/meshcore/__init__.py — entity-less-device removal allowance (hub-excluded).
  • custom_components/meshcore/telemetry_sensor.pyasync_on_remove eviction of discovered_sensors.
  • custom_components/meshcore/device_tracker.pyasync_on_remove eviction of discovered_trackers.
  • tests/test_device_removal.py — empty-device unit coverage.
  • tests_integration/test_dedup_eviction.py — eviction regression (new).

Why:
Follow-up to meshcore-dev#247 / meshcore-dev#229. meshcore-dev#247 made orphaned repeater/client devices
removable and, per review, refused contact/unknown devices while live to
avoid leaving stale entity references. Two gaps remained:

1. A device whose entities were already removed was still refused while
   the contact is live, so an empty device shell could not be deleted
   (HA renders the rejection as an unhelpful "[object Object]" dialog).
2. The telemetry/GPS dedup maps (discovered_sensors /
   discovered_trackers) were never evicted when HA removed an entity, so
   the managers kept updating a deregistered entity and would not
   recreate it until a restart -- the same desync already patched in the
   untracked-contact-discard and data-only-demote flows.

This allows removing a device that has no entities (the hub is excluded,
identifier == entry_id), and registers an async_on_remove eviction on
each telemetry sensor and GPS tracker so removal (UI entity delete,
device-delete cascade, or platform unload) pops the manager's dedup key.
The populated-device guards meshcore-dev#247 added (hub, subscribed repeater/client,
live contact/unknown) are unchanged; the empty-device allowance is
inserted ahead of them and is only reached when the device has no
entities.

Tests:
- Unit (tests/test_device_removal.py): 3 added -- empty contact device
  removable while the contact is live, empty hub still refused, populated
  live contact still refused. Existing refusal/orphan guards retained via
  an autouse non-empty entity-count default (no inversions). Full unit
  tier: 216 passed.
- Integration (tests_integration/test_dedup_eviction.py): new regression
  drives the real telemetry/GPS manager handlers under real Home
  Assistant, removes the entity via the entity registry, and asserts the
  dedup key is evicted -- riding the real async_on_remove lifecycle.
  Teeth-checked: fails without the eviction. Full integration tier: 14
  passed.
- ruff check / ruff format clean on the changed files.
@awolden awolden merged commit a28f701 into meshcore-dev:main Jun 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants