mirror of
https://github.com/home-assistant/core.git
synced 2026-09-05 21:12:24 +01:00
Report every SwitchBot Cloud lock state (#180448)
This commit is contained in:
@@ -2,15 +2,36 @@
|
||||
|
||||
from typing import Any, override
|
||||
|
||||
from switchbot_api import Device, LockCommands, LockV2Commands, Remote, SwitchBotAPI
|
||||
from switchbot_api import (
|
||||
Device,
|
||||
LockCommands,
|
||||
LockV2Commands,
|
||||
Remote,
|
||||
SwitchBotAPI,
|
||||
SwitchbotCloudDeviceLockState,
|
||||
)
|
||||
|
||||
from homeassistant.components.lock import LockEntity, LockEntityFeature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
# The cloud camel cases these when polled and upper cases them over the
|
||||
# webhook, so look them up by their lower cased value
|
||||
LOCK_STATES_BY_VALUE = {
|
||||
state.value.lower(): state for state in SwitchbotCloudDeviceLockState
|
||||
}
|
||||
|
||||
# A latch bolt or half locked door is secured, both are resting positions the
|
||||
# lock can be commanded into
|
||||
LOCKED_STATES = {
|
||||
SwitchbotCloudDeviceLockState.LOCKED,
|
||||
SwitchbotCloudDeviceLockState.LATCH_BOLT_LOCKED,
|
||||
SwitchbotCloudDeviceLockState.HALF_LOCKED,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -44,7 +65,11 @@ class SwitchBotCloudLock(SwitchBotCloudEntity, LockEntity):
|
||||
def _set_attributes(self) -> None:
|
||||
"""Set attributes from coordinator data."""
|
||||
if coord_data := self.coordinator.data:
|
||||
self._attr_is_locked = coord_data["lockState"].lower() == "locked"
|
||||
state = LOCK_STATES_BY_VALUE.get(coord_data["lockState"].lower())
|
||||
self._attr_is_locked = state in LOCKED_STATES if state else None
|
||||
self._attr_is_locking = state is SwitchbotCloudDeviceLockState.LOCKING
|
||||
self._attr_is_unlocking = state is SwitchbotCloudDeviceLockState.UNLOCKING
|
||||
self._attr_is_jammed = state is SwitchbotCloudDeviceLockState.JAMMED
|
||||
if self.__model not in [
|
||||
"Smart Lock Lite",
|
||||
"Smart Lock Vision",
|
||||
@@ -54,23 +79,34 @@ class SwitchBotCloudLock(SwitchBotCloudEntity, LockEntity):
|
||||
]:
|
||||
self._attr_supported_features = LockEntityFeature.OPEN
|
||||
|
||||
@callback
|
||||
def _write_optimistic_state(self, *, is_locked: bool) -> None:
|
||||
"""Write the state the command asked for, until the cloud reports back.
|
||||
|
||||
The transient states have to go with it: they outrank `is_locked` in
|
||||
`LockEntity.state`, so a lock commanded out of a jam would keep
|
||||
reading jammed until the next poll.
|
||||
"""
|
||||
self._attr_is_locked = is_locked
|
||||
self._attr_is_locking = False
|
||||
self._attr_is_unlocking = False
|
||||
self._attr_is_jammed = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def async_lock(self, **kwargs: Any) -> None:
|
||||
"""Lock the lock."""
|
||||
await self.send_api_command(LockCommands.LOCK)
|
||||
self._attr_is_locked = True
|
||||
self.async_write_ha_state()
|
||||
self._write_optimistic_state(is_locked=True)
|
||||
|
||||
@override
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Unlock the lock."""
|
||||
await self.send_api_command(LockCommands.UNLOCK)
|
||||
self._attr_is_locked = False
|
||||
self.async_write_ha_state()
|
||||
self._write_optimistic_state(is_locked=False)
|
||||
|
||||
@override
|
||||
async def async_open(self, **kwargs: Any) -> None:
|
||||
"""Latch open the lock."""
|
||||
await self.send_api_command(LockV2Commands.DEADBOLT)
|
||||
self._attr_is_locked = False
|
||||
self.async_write_ha_state()
|
||||
self._write_optimistic_state(is_locked=False)
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.const import (
|
||||
SERVICE_LOCK,
|
||||
SERVICE_OPEN,
|
||||
SERVICE_UNLOCK,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -104,3 +105,88 @@ async def test_lock_open(
|
||||
LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: lock_id}, blocking=True
|
||||
)
|
||||
assert hass.states.get(lock_id).state == LockState.UNLOCKED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("lock_state", "expected_state"),
|
||||
[
|
||||
("locked", LockState.LOCKED),
|
||||
("unlocked", LockState.UNLOCKED),
|
||||
("locking", LockState.LOCKING),
|
||||
("unlocking", LockState.UNLOCKING),
|
||||
("jammed", LockState.JAMMED),
|
||||
("latchBoltLocked", LockState.LOCKED),
|
||||
("halfLocked", LockState.LOCKED),
|
||||
# The webhook reports the very same states upper cased
|
||||
("LOCKED", LockState.LOCKED),
|
||||
("JAMMED", LockState.JAMMED),
|
||||
# Anything the cloud adds later is unknown rather than unlocked
|
||||
("somethingNew", STATE_UNKNOWN),
|
||||
],
|
||||
)
|
||||
async def test_lock_states(
|
||||
hass: HomeAssistant,
|
||||
mock_list_devices,
|
||||
mock_get_status,
|
||||
lock_state: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test every lock state the cloud reports."""
|
||||
mock_list_devices.return_value = [
|
||||
Device(
|
||||
version="V1.0",
|
||||
deviceId="lock-id-1",
|
||||
deviceName="lock-1",
|
||||
deviceType="Smart Lock Ultra",
|
||||
hubDeviceId="test-hub-id",
|
||||
),
|
||||
]
|
||||
|
||||
mock_get_status.return_value = {"lockState": lock_state}
|
||||
|
||||
entry = await configure_integration(hass)
|
||||
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert hass.states.get("lock.lock_1").state == expected_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "expected_state"),
|
||||
[
|
||||
(SERVICE_LOCK, LockState.LOCKED),
|
||||
(SERVICE_UNLOCK, LockState.UNLOCKED),
|
||||
(SERVICE_OPEN, LockState.UNLOCKED),
|
||||
],
|
||||
)
|
||||
async def test_command_replaces_a_jam(
|
||||
hass: HomeAssistant,
|
||||
mock_list_devices,
|
||||
mock_get_status,
|
||||
service: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test a command clears the jam instead of leaving it standing."""
|
||||
mock_list_devices.return_value = [
|
||||
Device(
|
||||
version="V1.0",
|
||||
deviceId="lock-id-1",
|
||||
deviceName="lock-1",
|
||||
deviceType="Smart Lock Ultra",
|
||||
hubDeviceId="test-hub-id",
|
||||
),
|
||||
]
|
||||
|
||||
mock_get_status.return_value = {"lockState": "jammed"}
|
||||
|
||||
entry = await configure_integration(hass)
|
||||
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
lock_id = "lock.lock_1"
|
||||
assert hass.states.get(lock_id).state == LockState.JAMMED
|
||||
|
||||
with patch.object(SwitchBotAPI, "send_command"):
|
||||
await hass.services.async_call(
|
||||
LOCK_DOMAIN, service, {ATTR_ENTITY_ID: lock_id}, blocking=True
|
||||
)
|
||||
assert hass.states.get(lock_id).state == expected_state
|
||||
|
||||
Reference in New Issue
Block a user