Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions custom_components/keymaster/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,9 @@ async def _handle_lock_state_change(
)
self._state_change_autolock_started.add(kmlock.keymaster_config_entry_id)
self.async_schedule_keymaster_notifications([kmlock.keymaster_config_entry_id])
if uses_provider_lock_events:
kmlock.lock_state = LockState.UNLOCKED
self._last_unlock_code_slot[kmlock.keymaster_config_entry_id] = 0
Comment thread
firstof9 marked this conversation as resolved.
elif new_state == LockState.LOCKED:
if old_state != LockState.LOCKED:
if not uses_provider_lock_events:
Expand All @@ -1033,6 +1036,8 @@ async def _handle_lock_state_change(
await kmlock.autolock_timer.cancel()
self._state_change_autolock_started.discard(kmlock.keymaster_config_entry_id)
self.async_schedule_keymaster_notifications([kmlock.keymaster_config_entry_id])
if uses_provider_lock_events:
kmlock.lock_state = LockState.LOCKED
Comment thread
firstof9 marked this conversation as resolved.

async def _handle_door_state_change(
self,
Expand Down
103 changes: 101 additions & 2 deletions tests/test_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1426,7 +1426,7 @@ async def test_handle_lock_state_change_unlocked(self, mock_coordinator, mock_km
) as mock_notify:
await mock_coordinator._handle_lock_state_change(mock_kmlock, event)

assert mock_kmlock.lock_state == LockState.LOCKED
assert mock_kmlock.lock_state == LockState.UNLOCKED
mock_kmlock.autolock_timer.start.assert_called_once_with(duration=300)
mock_coordinator.async_schedule_keymaster_notifications.assert_called_once()
mock_notify.assert_not_called()
Expand Down Expand Up @@ -1459,7 +1459,7 @@ async def test_handle_lock_state_change_locked(self, mock_coordinator, mock_kmlo
) as mock_notify:
await mock_coordinator._handle_lock_state_change(mock_kmlock, event)

assert mock_kmlock.lock_state == LockState.UNLOCKED
assert mock_kmlock.lock_state == LockState.LOCKED
mock_kmlock.autolock_timer.cancel.assert_called_once()
mock_coordinator.async_schedule_keymaster_notifications.assert_called_once()
mock_notify.assert_not_called()
Expand Down Expand Up @@ -3768,3 +3768,102 @@ async def test_create_listeners_with_event(hass: HomeAssistant) -> None:

# The existing listeners in the list should be cleared out since event is not None
assert mock_unsub not in lock.listeners


async def test_handle_provider_lock_event_prioritizes_event_label(hass: HomeAssistant) -> None:
Comment thread
firstof9 marked this conversation as resolved.
"""Test that _handle_provider_lock_event trusts explicit event_label over entity state mismatch."""
coordinator = KeymasterCoordinator(hass)
kmlock = KeymasterLock(
lock_name="Front Door",
lock_entity_id="lock.front_door",
keymaster_config_entry_id="entry_1",
)
# When lock entity state matches tracked state (state_changed is False), trust explicit event_label
kmlock.lock_state = LockState.UNLOCKED
coordinator.kmlocks["entry_1"] = kmlock

# Set lock entity state to unlocked in state machine
hass.states.async_set("lock.front_door", LockState.UNLOCKED)

with patch.object(coordinator, "_lock_unlocked", new_callable=AsyncMock) as mock_unlocked:
await coordinator._handle_provider_lock_event(
kmlock=kmlock,
code_slot_num=1,
event_label="Unlocked via Keypad",
action_code=1,
)
mock_unlocked.assert_called_once_with(
kmlock=kmlock,
code_slot_num=1,
source="event",
event_label="Unlocked via Keypad",
action_code=1,
)


async def test_handle_lock_state_change_syncs_push_provider_lock_state(
Comment thread
firstof9 marked this conversation as resolved.
hass: HomeAssistant,
) -> None:
"""Test that _handle_lock_state_change syncs kmlock.lock_state for push providers."""
coordinator = KeymasterCoordinator(hass)
mock_provider = Mock()
mock_provider.supports_push_updates = True

kmlock = KeymasterLock(
lock_name="Front Door",
lock_entity_id="lock.front_door",
keymaster_config_entry_id="entry_1",
provider=mock_provider,
)
kmlock.lock_state = LockState.LOCKED
coordinator.kmlocks["entry_1"] = kmlock

event_data = {
"entity_id": "lock.front_door",
"old_state": Mock(state=LockState.LOCKED),
"new_state": Mock(state=LockState.UNLOCKED),
}
event = Event("state_changed", data=event_data)

await coordinator._handle_lock_state_change(kmlock, event)
assert kmlock.lock_state == LockState.UNLOCKED
assert coordinator._last_unlock_code_slot["entry_1"] == 0


@pytest.mark.parametrize(
("supports_push", "provider_obj"),
[
(False, Mock(supports_push_updates=False)),
(False, None),
],
)
async def test_handle_lock_state_change_non_push_provider_does_not_sync_state(
hass: HomeAssistant, supports_push: bool, provider_obj: Any
) -> None:
"""Test that _handle_lock_state_change does not sync kmlock.lock_state for non-push providers."""
coordinator = KeymasterCoordinator(hass)
kmlock = KeymasterLock(
lock_name="Front Door",
lock_entity_id="lock.front_door",
keymaster_config_entry_id="entry_1",
provider=provider_obj,
)
kmlock.lock_state = LockState.LOCKED
coordinator.kmlocks["entry_1"] = kmlock

event_data = {
"entity_id": "lock.front_door",
"old_state": Mock(state=LockState.LOCKED),
"new_state": Mock(state=LockState.UNLOCKED),
}
event = Event("state_changed", data=event_data)

with patch.object(coordinator, "_lock_unlocked", new_callable=AsyncMock) as mock_unlocked:
await coordinator._handle_lock_state_change(kmlock, event)
mock_unlocked.assert_called_once_with(
kmlock=kmlock,
source="state_change",
event_label="Manual Unlock",
)
# Lock state should remain LOCKED as it wasn't mutated by state_change for non-push provider
assert kmlock.lock_state == LockState.LOCKED
Loading