Skip to content

Commit

Permalink
Add basic support for lock pro (#241)
Browse files Browse the repository at this point in the history
Co-authored-by: J. Nick Koston <nick@koston.org>
  • Loading branch information
szclsya and bdraco committed Jun 18, 2024
1 parent 93ecb3a commit 48398d0
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 11 deletions.
6 changes: 6 additions & 0 deletions switchbot/adv_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ class SwitchbotSupportedType(TypedDict):
"func": process_wolock,
"manufacturer_id": 2409,
},
"$": {
"modelName": SwitchbotModel.LOCK_PRO,
"modelFriendlyName": "Lock Pro",
"func": process_wolock,
"manufacturer_id": 2409,
},
"x": {
"modelName": SwitchbotModel.BLIND_TILT,
"modelFriendlyName": "Blind Tilt",
Expand Down
1 change: 1 addition & 0 deletions switchbot/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class SwitchbotModel(StrEnum):
COLOR_BULB = "WoBulb"
CEILING_LIGHT = "WoCeiling"
LOCK = "WoLock"
LOCK_PRO = "WoLockPro"
BLIND_TILT = "WoBlindTilt"
HUB2 = "WoHub2"

Expand Down
43 changes: 33 additions & 10 deletions switchbot/devices/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,28 @@
SwitchbotAccountConnectionError,
SwitchbotApiError,
SwitchbotAuthenticationError,
SwitchbotModel,
)
from .device import SwitchbotDevice, SwitchbotOperationError

COMMAND_HEADER = "57"
COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
COMMAND_LOCK_INFO = f"{COMMAND_HEADER}0f4f8101"
COMMAND_UNLOCK = f"{COMMAND_HEADER}0f4e01011080"
COMMAND_UNLOCK_WITHOUT_UNLATCH = f"{COMMAND_HEADER}0f4e010110a0"
COMMAND_LOCK = f"{COMMAND_HEADER}0f4e01011000"
COMMAND_LOCK_INFO = {
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4f8101",
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4f8102",
}
COMMAND_UNLOCK = {
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e01011080",
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e0101000080",
}
COMMAND_UNLOCK_WITHOUT_UNLATCH = {
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e010110a0",
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e01010000a0",
}
COMMAND_LOCK = {
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e01011000",
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e0101000000",
}
COMMAND_ENABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e01001e00008101"
COMMAND_DISABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e00"

Expand All @@ -49,6 +62,7 @@ def __init__(
key_id: str,
encryption_key: str,
interface: int = 0,
model: SwitchbotModel = SwitchbotModel.LOCK,
**kwargs: Any,
) -> None:
if len(key_id) == 0:
Expand All @@ -59,20 +73,27 @@ def __init__(
raise ValueError("encryption_key is missing")
elif len(encryption_key) != 32:
raise ValueError("encryption_key is invalid")
if model not in (SwitchbotModel.LOCK, SwitchbotModel.LOCK_PRO):
raise ValueError("initializing SwitchbotLock with a non-lock model")
self._iv = None
self._cipher = None
self._key_id = key_id
self._encryption_key = bytearray.fromhex(encryption_key)
self._notifications_enabled: bool = False
self._model: SwitchbotModel = model
super().__init__(device, None, interface, **kwargs)

@staticmethod
async def verify_encryption_key(
device: BLEDevice, key_id: str, encryption_key: str
device: BLEDevice,
key_id: str,
encryption_key: str,
model: SwitchbotModel = SwitchbotModel.LOCK,
**kwargs: Any,
) -> bool:
try:
lock = SwitchbotLock(
device=device, key_id=key_id, encryption_key=encryption_key
device, key_id=key_id, encryption_key=encryption_key, model=model
)
except ValueError:
return False
Expand Down Expand Up @@ -183,19 +204,19 @@ async def async_retrieve_encryption_key(
async def lock(self) -> bool:
"""Send lock command."""
return await self._lock_unlock(
COMMAND_LOCK, {LockStatus.LOCKED, LockStatus.LOCKING}
COMMAND_LOCK[self._model], {LockStatus.LOCKED, LockStatus.LOCKING}
)

async def unlock(self) -> bool:
"""Send unlock command. If unlatch feature is enabled in EU firmware, also unlatches door"""
return await self._lock_unlock(
COMMAND_UNLOCK, {LockStatus.UNLOCKED, LockStatus.UNLOCKING}
COMMAND_UNLOCK[self._model], {LockStatus.UNLOCKED, LockStatus.UNLOCKING}
)

async def unlock_without_unlatch(self) -> bool:
"""Send unlock command. This command will not unlatch the door."""
return await self._lock_unlock(
COMMAND_UNLOCK_WITHOUT_UNLATCH,
COMMAND_UNLOCK_WITHOUT_UNLATCH[self._model],
{LockStatus.UNLOCKED, LockStatus.UNLOCKING, LockStatus.NOT_FULLY_LOCKED},
)

Expand Down Expand Up @@ -275,7 +296,9 @@ def is_night_latch_enabled(self) -> bool:

async def _get_lock_info(self) -> bytes | None:
"""Return lock info of device."""
_data = await self._send_command(key=COMMAND_LOCK_INFO, retry=self._retry_count)
_data = await self._send_command(
key=COMMAND_LOCK_INFO[self._model], retry=self._retry_count
)

if not self._check_command_result(_data, 0, COMMAND_RESULT_EXPECTED_VALUES):
_LOGGER.error("Unsuccessful, please try again")
Expand Down
4 changes: 3 additions & 1 deletion switchbot/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ async def get_contactsensors(self) -> dict[str, SwitchBotAdvertisement]:

async def get_locks(self) -> dict[str, SwitchBotAdvertisement]:
"""Return all WoLock/Locks devices with services data."""
return await self._get_devices_by_model("o")
locks = await self._get_devices_by_model("o")
lock_pros = await self._get_devices_by_model("$")
return {**locks, **lock_pros}

async def get_device_data(
self, address: str
Expand Down
69 changes: 69 additions & 0 deletions tests/test_adv_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,75 @@ def test_parsing_lock_passive():
)


def test_parsing_lock_pro_active():
"""Test parsing lock pro with active data."""
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
adv_data = generate_advertisement_data(
manufacturer_data={2409: b"\xc8\xf5,\xd9-V\x07\x82\x00d\x00\x00"},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"$\x80d"},
rssi=-80,
)
result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LOCK_PRO)
assert result == SwitchBotAdvertisement(
address="aa:bb:cc:dd:ee:ff",
data={
"data": {
"battery": 100,
"calibration": True,
"status": LockStatus.LOCKED,
"update_from_secondary_lock": False,
"door_open": False,
"double_lock_mode": False,
"unclosed_alarm": False,
"unlocked_alarm": False,
"auto_lock_paused": False,
"night_latch": False,
},
"model": "$",
"isEncrypted": False,
"modelFriendlyName": "Lock Pro",
"modelName": SwitchbotModel.LOCK_PRO,
"rawAdvData": b"$\x80d",
},
device=ble_device,
rssi=-80,
active=True,
)


def test_parsing_lock_pro_passive():
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
adv_data = generate_advertisement_data(
manufacturer_data={2409: bytes.fromhex("aabbccddeeff208200640000")}, rssi=-67
)
result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LOCK_PRO)
assert result == SwitchBotAdvertisement(
address="aa:bb:cc:dd:ee:ff",
data={
"data": {
"battery": None,
"calibration": True,
"status": LockStatus.LOCKED,
"update_from_secondary_lock": False,
"door_open": False,
"double_lock_mode": False,
"unclosed_alarm": False,
"unlocked_alarm": False,
"auto_lock_paused": False,
"night_latch": False,
},
"model": "$",
"isEncrypted": False,
"modelFriendlyName": "Lock Pro",
"modelName": SwitchbotModel.LOCK_PRO,
"rawAdvData": None,
},
device=ble_device,
rssi=-67,
active=False,
)


def test_parsing_lock_active_old_firmware():
"""Test parsing lock with active data. Old firmware."""
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
Expand Down

0 comments on commit 48398d0

Please sign in to comment.