Skip to content

ISSUE: BaseLockProvider.async_unload raises ValueError: list.remove(x): x not in list on reload, leaving the entry half-loaded (v0.5.x) #666

Description

@gpatlanta

Describe the bug

NOTE: This was generated by Claude after resolving issues with keymaster on my environment (2 of 2 issues identified)

Environment

keymaster v0.5.1 (HACS)
Home Assistant 2026.5.4, Z-Wave JS (core)
Lock: Allegion/Schlage BE468 (ZWaveJS provider)
Symptom — reloading the keymaster config entry (Settings → Devices & Services → keymaster → Reload) crashes during unload and leaves the entry stuck in UNLOAD_IN_PROGRESS/SETUP_IN_PROGRESS; all of the lock's entities go unavailable and only a full HA restart recovers them:

ERROR [homeassistant.core] Unable to remove unknown job listener (<Job onetime listen homeassistant_started ... KeymasterCoordinator._create_listeners ...>)
ERROR [homeassistant.config_entries] Error unloading entry Side Door for keymaster
Traceback (most recent call last):
File ".../custom_components/keymaster/init.py", line 258, in async_unload_entry
await kmlock.provider.async_unload()
File ".../custom_components/keymaster/providers/_base.py", line 181, in async_unload
ValueError: list.remove(x): x not in list
Analysis — BaseLockProvider.async_unload() (providers/_base.py) unconditionally calls every stored unsub:

async def async_unload(self) -> None:
# Unsubscribe all listeners
for unsub in self._listeners:
unsub() # line 181
self._listeners.clear()
One of the unsub() callbacks performs a list.remove() on a listener that has already been removed (double-unsubscribe, or it was never registered), raising ValueError. This is the well-known HA listener-lifecycle pattern (cf. home-assistant/core#95823, frenck/spook#760). Because it raises mid-loop, the remaining listeners aren't cleaned up and the entry fails to unload.

Repro: reload the keymaster integration (no special state required).

Suggested fixes

Guard the unsub loop so one bad unsub can't abort cleanup:

for unsub in self.listeners:
try:
unsub()
except ValueError:
pass
self.listeners.clear()
And/or make listener registration/removal idempotent so each unsub is invoked exactly once (and isn't tracked after it's been called).
Impact: this also blocks recovery from other provider issues — the normal "reload to fix" path is unavailable, forcing a full restart. Discovered while trying to reload to recover from companion issue #
_.

Home Assistant version

Home Assistant 2026.5.4, Z-Wave JS (core)

Keymaster version

0.5.1

Home Assistant installation type

Home Assistant Container (Docker)

Lock provider

schlage

Lock make and model

Schlage BE468

Steps to reproduce

Symptom — reloading the keymaster config entry (Settings → Devices & Services → keymaster → Reload) crashes during unload and leaves the entry stuck in UNLOAD_IN_PROGRESS/SETUP_IN_PROGRESS; all of the lock's entities go unavailable and only a full HA restart recovers them:

Expected behavior

no stuck entry

Logs

# Reloading the keymaster entry crashes during unload:

2026-06-22 12:03:32.314 ERROR (MainThread) [homeassistant.config_entries] Error unloading entry Side Door for keymaster
Traceback (most recent call last):
  File "/config/custom_components/keymaster/__init__.py", line 258, in async_unload_entry
    await kmlock.provider.async_unload()
  File "/config/custom_components/keymaster/providers/_base.py", line 181, in async_unload
    unsub()
ValueError: list.remove(x): x not in list

# Companion errors fired at the same instant (the full lines dump the entire
# KeymasterLock object, so truncated here):
2026-06-22 12:03:32.148 ERROR (MainThread) [homeassistant.core] Unable to remove unknown job listener (<Job onetime listen homeassistant_started functools.partial(<bound method KeymasterCoordinator._create_listeners ...>))  ...[truncated]
2026-06-22 12:03:32.314 ERROR (MainThread) [homeassistant.core] Unable to remove unknown job listener (<Job listen zwave_js_notification ... ZWaveJSLockProvider.subscribe_lock_events ...>)  ...[truncated]

# After this, the entry is stuck (UNLOAD_IN_PROGRESS / SETUP_IN_PROGRESS) and all of
# the lock's entities go `unavailable` until a full Home Assistant restart.
Two small notes:

For Issue 1, the line "grepping the entire log set for ZWaveJSProvider returns zero matches" is the strongest single piece of evidence — it directly shows the failure path logs nothing. I confirmed it across all your log rotations (count = 0).
For Issue 2, I filled in unsub() as the line-181 source from the integration's providers/_base.py (the for unsub in self._listeners: unsub() loop) so the traceback is complete and points the maintainer straight at the fix.

Screenshots

No response

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions