Skip to content

Commit f017c74

Browse files
authored
fix: sync manager stuck in LOADING for disabled slots (#1085)
1 parent 325bf02 commit f017c74

2 files changed

Lines changed: 82 additions & 7 deletions

File tree

custom_components/lock_code_manager/sync.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,21 +238,38 @@ def _ensure_entities_ready(self) -> bool:
238238
"""Ensure all dependent entities exist with valid states.
239239
240240
The name entity (CONF_NAME) is optional — STATE_UNKNOWN is its
241-
normal state when no name is configured, so we only block on
242-
STATE_UNAVAILABLE for it.
241+
normal state when no name is configured.
242+
243+
When the slot is inactive (active entity is OFF), PIN and code
244+
sensor entities are allowed to be STATE_UNKNOWN since the sync
245+
manager only needs to know the slot is off to proceed (clear or
246+
confirm already cleared). This prevents disabled slots from
247+
being stuck in LOADING forever.
243248
"""
249+
# Collect all states in a single pass
250+
states: dict[str, str | None] = {}
244251
for key in (CONF_PIN, CONF_NAME, ATTR_ACTIVE, ATTR_CODE):
245252
if key not in self._entity_id_map:
246253
return False
247254
state = self._get_entity_state(key)
248255
if state is None or state == STATE_UNAVAILABLE:
249256
_LOGGER.debug("%s: Waiting for %s state", self._log_prefix, key)
250257
return False
251-
# Name is optional — STATE_UNKNOWN is valid (no name configured).
252-
# All other entities must have a definite state.
253-
if key != CONF_NAME and state == STATE_UNKNOWN:
258+
states[key] = state
259+
260+
# Active entity must always have a definite state
261+
if states[ATTR_ACTIVE] == STATE_UNKNOWN:
262+
_LOGGER.debug("%s: Waiting for %s state", self._log_prefix, ATTR_ACTIVE)
263+
return False
264+
265+
# Name is always optional (STATE_UNKNOWN = no name configured)
266+
# PIN and code sensor can be unknown when the slot is inactive
267+
slot_inactive = states[ATTR_ACTIVE] == STATE_OFF
268+
for key in (CONF_PIN, ATTR_CODE):
269+
if states[key] == STATE_UNKNOWN and not slot_inactive:
254270
_LOGGER.debug("%s: Waiting for %s state", self._log_prefix, key)
255271
return False
272+
256273
return True
257274

258275
def _resolve_slot_state(self) -> SlotState | None:

tests/test_sync.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import pytest
99

10-
from homeassistant.const import STATE_OFF, STATE_ON
10+
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
1111
from homeassistant.core import HomeAssistant
1212
from homeassistant.helpers.issue_registry import (
1313
IssueSeverity,
@@ -25,7 +25,7 @@
2525
from custom_components.lock_code_manager.models import SlotCode, SyncState
2626
from custom_components.lock_code_manager.sync import SlotState, SlotSyncManager
2727

28-
from .common import SLOT_1_IN_SYNC_ENTITY
28+
from .common import SLOT_1_ACTIVE_ENTITY, SLOT_1_IN_SYNC_ENTITY, SLOT_1_PIN_ENTITY
2929
from .conftest import async_trigger_sync_tick, get_in_sync_entity_obj
3030

3131

@@ -426,6 +426,64 @@ async def test_loading_to_out_of_sync(
426426
await async_trigger_sync_tick(hass, SLOT_1_IN_SYNC_ENTITY, set_dirty=False)
427427
assert manager._state is SyncState.OUT_OF_SYNC
428428

429+
async def test_disabled_slot_with_unknown_pin_exits_loading(
430+
self,
431+
hass: HomeAssistant,
432+
mock_lock_config_entry,
433+
lock_code_manager_config_entry,
434+
) -> None:
435+
"""Disabled slot with unknown PIN/code transitions out of LOADING."""
436+
entity_obj = get_in_sync_entity_obj(hass, SLOT_1_IN_SYNC_ENTITY)
437+
manager = entity_obj._sync_manager
438+
manager._state = SyncState.LOADING
439+
440+
# Simulate disabled slot: active=off, PIN unknown
441+
hass.states.async_set(SLOT_1_ACTIVE_ENTITY, STATE_OFF)
442+
hass.states.async_set(SLOT_1_PIN_ENTITY, STATE_UNKNOWN)
443+
# Coordinator has slot as EMPTY (no code on lock)
444+
manager._coordinator.data[1] = SlotCode.EMPTY
445+
446+
await async_trigger_sync_tick(hass, SLOT_1_IN_SYNC_ENTITY, set_dirty=False)
447+
448+
# Should transition to IN_SYNC (slot off, code empty = in sync)
449+
assert manager._state is SyncState.IN_SYNC
450+
451+
async def test_active_unknown_stays_in_loading(
452+
self,
453+
hass: HomeAssistant,
454+
mock_lock_config_entry,
455+
lock_code_manager_config_entry,
456+
) -> None:
457+
"""Slot stays in LOADING when active entity is STATE_UNKNOWN."""
458+
entity_obj = get_in_sync_entity_obj(hass, SLOT_1_IN_SYNC_ENTITY)
459+
manager = entity_obj._sync_manager
460+
manager._state = SyncState.LOADING
461+
462+
hass.states.async_set(SLOT_1_ACTIVE_ENTITY, STATE_UNKNOWN)
463+
manager._coordinator.data[1] = SlotCode.EMPTY
464+
465+
await async_trigger_sync_tick(hass, SLOT_1_IN_SYNC_ENTITY, set_dirty=False)
466+
assert manager._state is SyncState.LOADING
467+
468+
async def test_enabled_slot_with_unknown_pin_stays_in_loading(
469+
self,
470+
hass: HomeAssistant,
471+
mock_lock_config_entry,
472+
lock_code_manager_config_entry,
473+
) -> None:
474+
"""Enabled slot with unknown PIN stays in LOADING (needs PIN to sync)."""
475+
entity_obj = get_in_sync_entity_obj(hass, SLOT_1_IN_SYNC_ENTITY)
476+
manager = entity_obj._sync_manager
477+
manager._state = SyncState.LOADING
478+
479+
# Active=on but PIN unknown — can't sync without knowing what PIN to set
480+
hass.states.async_set(SLOT_1_ACTIVE_ENTITY, STATE_ON)
481+
hass.states.async_set(SLOT_1_PIN_ENTITY, STATE_UNKNOWN)
482+
manager._coordinator.data[1] = SlotCode.EMPTY
483+
484+
await async_trigger_sync_tick(hass, SLOT_1_IN_SYNC_ENTITY, set_dirty=False)
485+
assert manager._state is SyncState.LOADING
486+
429487
async def test_loading_to_synced(
430488
self,
431489
hass: HomeAssistant,

0 commit comments

Comments
 (0)