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
37 changes: 37 additions & 0 deletions switchbot/adv_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from .adv_parsers.lock import (
process_lock2,
process_locklite,
process_lockvision,
process_wolock,
process_wolock_pro,
)
Expand Down Expand Up @@ -755,6 +756,42 @@ class SwitchbotSupportedType(TypedDict):
"func": process_keypad_vision_pro,
"manufacturer_id": 2409,
},
b"\x00\x11\x69\x09": {
"modelName": SwitchbotModel.LOCK_VISION_PRO,
"modelFriendlyName": "Lock Vision Pro",
"func": process_lockvision,
"manufacturer_id": 2409,
},
b"\x01\x11\x69\x09": {
"modelName": SwitchbotModel.LOCK_VISION_PRO,
"modelFriendlyName": "Lock Vision Pro",
"func": process_lockvision,
"manufacturer_id": 2409,
},
b"\x00\x11\x69\x08": {
"modelName": SwitchbotModel.LOCK_VISION,
"modelFriendlyName": "Lock Vision",
"func": process_locklite,
"manufacturer_id": 2409,
},
b"\x01\x11\x69\x08": {
"modelName": SwitchbotModel.LOCK_VISION,
"modelFriendlyName": "Lock Vision",
"func": process_locklite,
"manufacturer_id": 2409,
},
b"\x00\x10\xff\x90": {
"modelName": SwitchbotModel.LOCK_PRO_WIFI,
"modelFriendlyName": "Lock Pro Wifi",
"func": process_wolock_pro,
"manufacturer_id": 2409,
},
b"\x01\x10\xff\x90": {
"modelName": SwitchbotModel.LOCK_PRO_WIFI,
"modelFriendlyName": "Lock Pro Wifi",
"func": process_wolock_pro,
"manufacturer_id": 2409,
},
}

_SWITCHBOT_MODEL_TO_CHAR: defaultdict[SwitchbotModel, list[str | bytes]] = defaultdict(
Expand Down
16 changes: 16 additions & 0 deletions switchbot/adv_parsers/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,19 @@ def process_lock2(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool
}

return common_data | lock2_data


def process_lockvision(
data: bytes | None, mfr_data: bytes | None
) -> dict[str, bool | int]:
"""Support for lockvision process data."""
common_data = parse_common_data(mfr_data)
if not common_data:
return {}

lockvision_data = {
"power_alarm": bool(mfr_data[11] & 0b00010000),
"battery_status": mfr_data[11] & 0b00000111,
}

return common_data | lockvision_data
3 changes: 3 additions & 0 deletions switchbot/const/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ class SwitchbotModel(StrEnum):
ART_FRAME = "Art Frame"
KEYPAD_VISION = "Keypad Vision"
KEYPAD_VISION_PRO = "Keypad Vision Pro"
LOCK_VISION_PRO = "Lock Vision Pro"
LOCK_VISION = "Lock Vision"
LOCK_PRO_WIFI = "Lock Pro Wifi"


__all__ = [
Expand Down
3 changes: 3 additions & 0 deletions switchbot/devices/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ def _extract_region(userinfo: dict[str, Any]) -> str:
"W1128000": SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
"W1111000": SwitchbotModel.CLIMATE_PANEL,
"W1130000": SwitchbotModel.ART_FRAME,
"W1141001": SwitchbotModel.LOCK_VISION_PRO,
"W1141000": SwitchbotModel.LOCK_VISION,
"W1114000": SwitchbotModel.LOCK_PRO_WIFI,
}

REQ_HEADER = "570f"
Expand Down
22 changes: 20 additions & 2 deletions switchbot/devices/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,45 @@
SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0f4f8101",
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4f8104",
SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4f8107",
SwitchbotModel.LOCK_VISION_PRO: f"{COMMAND_HEADER}0f4f8102",
SwitchbotModel.LOCK_VISION: f"{COMMAND_HEADER}0f4f8102",
SwitchbotModel.LOCK_PRO_WIFI: f"{COMMAND_HEADER}0f4f810a",
}
COMMAND_UNLOCK = {
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e01011080",
SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0f4e01011080",
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e0101000080",
SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4e0101000080",
SwitchbotModel.LOCK_VISION_PRO: f"{COMMAND_HEADER}0f4e0101000080",
SwitchbotModel.LOCK_VISION: f"{COMMAND_HEADER}0f4e0101000080",
SwitchbotModel.LOCK_PRO_WIFI: f"{COMMAND_HEADER}0f4e0101000080",
}
COMMAND_UNLOCK_WITHOUT_UNLATCH = {
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e010110a0",
SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0f4e010110a0",
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e01010000a0",
SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4e01010000a0",
SwitchbotModel.LOCK_VISION_PRO: f"{COMMAND_HEADER}0f4e01010000a0",
SwitchbotModel.LOCK_VISION: f"{COMMAND_HEADER}0f4e01010000a0",
SwitchbotModel.LOCK_PRO_WIFI: f"{COMMAND_HEADER}0f4e01010000a0",
}
COMMAND_LOCK = {
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e01011000",
SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0f4e01011000",
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e0101000000",
SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0f4e0101000000",
SwitchbotModel.LOCK_VISION_PRO: f"{COMMAND_HEADER}0f4e0101000000",
SwitchbotModel.LOCK_VISION: f"{COMMAND_HEADER}0f4e0101000000",
SwitchbotModel.LOCK_PRO_WIFI: f"{COMMAND_HEADER}0f4e0101000000",
}
COMMAND_ENABLE_NOTIFICATIONS = {
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0e01001e00008101",
SwitchbotModel.LOCK_LITE: f"{COMMAND_HEADER}0e01001e00008101",
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0e01001e00008104",
SwitchbotModel.LOCK_ULTRA: f"{COMMAND_HEADER}0e01001e00008107",
SwitchbotModel.LOCK_VISION_PRO: f"{COMMAND_HEADER}0e01001e00008102",
SwitchbotModel.LOCK_VISION: f"{COMMAND_HEADER}0e01001e00008102",
SwitchbotModel.LOCK_PRO_WIFI: f"{COMMAND_HEADER}0e01001e00008104",
}
COMMAND_DISABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e00"

Expand Down Expand Up @@ -74,6 +89,9 @@ def __init__(
SwitchbotModel.LOCK_PRO,
SwitchbotModel.LOCK_LITE,
SwitchbotModel.LOCK_ULTRA,
SwitchbotModel.LOCK_VISION_PRO,
SwitchbotModel.LOCK_VISION,
SwitchbotModel.LOCK_PRO_WIFI,
):
raise ValueError("initializing SwitchbotLock with a non-lock model")
self._notifications_enabled: bool = False
Expand Down Expand Up @@ -236,15 +254,15 @@ def _update_lock_status(self, data: bytearray) -> None:

@staticmethod
def _parse_lock_data(data: bytes, model: SwitchbotModel) -> dict[str, Any]:
if model == SwitchbotModel.LOCK:
if model in {SwitchbotModel.LOCK, SwitchbotModel.LOCK_VISION_PRO}:
return {
"calibration": bool(data[0] & 0b10000000),
"status": LockStatus((data[0] & 0b01110000) >> 4),
"door_open": bool(data[0] & 0b00000100),
"unclosed_alarm": bool(data[1] & 0b00100000),
"unlocked_alarm": bool(data[1] & 0b00010000),
}
if model == SwitchbotModel.LOCK_LITE:
if model in {SwitchbotModel.LOCK_LITE, SwitchbotModel.LOCK_VISION}:
return {
"calibration": bool(data[0] & 0b10000000),
"status": LockStatus((data[0] & 0b01110000) >> 4),
Expand Down
160 changes: 160 additions & 0 deletions tests/test_adv_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2733,6 +2733,74 @@ def test_hub3_with_empty_data() -> None:
"Lock Ultra",
SwitchbotModel.LOCK_ULTRA,
),
AdvTestCase(
b"\xb0\xe9\xfe\x6f\xc9\xa6\x0a\x00\x00\x2e\x00\x0c\x00\x00\x00\x00",
b"\x00\x80.\x00\x11i\x08",
{
"sequence_number": 10,
"battery": 46,
"calibration": False,
"status": LockStatus.LOCKED,
"update_from_secondary_lock": False,
"double_lock_mode": False,
"unlocked_alarm": False,
"night_latch": False,
},
b"\x00\x11\x69\x08",
"Lock Vision",
SwitchbotModel.LOCK_VISION,
),
AdvTestCase(
b"\xb0\xe9\xfe\xe6\x1aq\x03\x00\x003\x00\x0c\x00\x00\x00\x00",
b"\x00\x803\x00\x11i\t",
{
"sequence_number": 3,
"battery": 51,
"calibration": False,
"status": LockStatus.LOCKED,
"update_from_secondary_lock": False,
"door_open": False,
"door_open_from_secondary_lock": False,
"double_lock_mode": False,
"is_secondary_lock": False,
"manual_unlock_linkage": False,
"unclosed_alarm": False,
"unlocked_alarm": False,
"auto_lock_paused": False,
"night_latch": False,
"power_alarm": False,
"battery_status": 4,
},
b"\x00\x11\x69\x09",
"Lock Vision Pro",
SwitchbotModel.LOCK_VISION_PRO,
),
AdvTestCase(
b"\xb0\xe9\xfe\xb0^\xfe\x0e\x02\x00=\x00\x08",
b"\x00\x80=\x00\x10\xff\x90",
{
"sequence_number": 14,
"battery": 61,
"calibration": False,
"status": LockStatus.LOCKED,
"update_from_secondary_lock": False,
"door_open": False,
"door_open_from_secondary_lock": False,
"double_lock_mode": False,
"is_secondary_lock": False,
"left_battery_compartment_alarm": 0,
"right_battery_compartment_alarm": 0,
"low_temperature_alarm": False,
"manual_unlock_linkage": False,
"unclosed_alarm": False,
"unlocked_alarm": False,
"auto_lock_paused": False,
"night_latch": False,
},
b"\x00\x10\xff\x90",
"Lock Pro Wifi",
SwitchbotModel.LOCK_PRO_WIFI,
),
],
)
def test_lock_active(test_case: AdvTestCase) -> None:
Expand Down Expand Up @@ -2851,6 +2919,74 @@ def test_lock_active(test_case: AdvTestCase) -> None:
"Lock Ultra",
SwitchbotModel.LOCK_ULTRA,
),
AdvTestCase(
b"\xb0\xe9\xfe\x6f\xc9\xa6\x04\x00\x00\x2e\x00\x0c\x00\x00\x00\x00",
b"\x00\x80.\x01\x11i\x08",
{
"sequence_number": 4,
"battery": None,
"calibration": False,
"status": LockStatus.LOCKED,
"update_from_secondary_lock": False,
"double_lock_mode": False,
"unlocked_alarm": False,
"night_latch": False,
},
b"\x00\x11\x69\x08",
"Lock Vision",
SwitchbotModel.LOCK_VISION,
),
AdvTestCase(
b"\xb0\xe9\xfe\xe6\x1aq\x04\x00\x003\x00\x0c\x00\x00\x00\x00",
b"\x00\x803\x01\x11i\t",
{
"sequence_number": 4,
"battery": 51,
"calibration": False,
"status": LockStatus.LOCKED,
"update_from_secondary_lock": False,
"door_open": False,
"door_open_from_secondary_lock": False,
"double_lock_mode": False,
"is_secondary_lock": False,
"manual_unlock_linkage": False,
"unclosed_alarm": False,
"unlocked_alarm": False,
"auto_lock_paused": False,
"night_latch": False,
"power_alarm": False,
"battery_status": 4,
},
b"\x00\x11\x69\x09",
"Lock Vision Pro",
SwitchbotModel.LOCK_VISION_PRO,
),
AdvTestCase(
b"\xb0\xe9\xfe\xb0^\xfe\x0e\x02\x00=\x00\x08",
b"\x00\x80=\x00\x10\xff\x90",
{
"sequence_number": 14,
"battery": 61,
"calibration": False,
"status": LockStatus.LOCKED,
"update_from_secondary_lock": False,
"door_open": False,
"door_open_from_secondary_lock": False,
"double_lock_mode": False,
"is_secondary_lock": False,
"left_battery_compartment_alarm": 0,
"right_battery_compartment_alarm": 0,
"low_temperature_alarm": False,
"manual_unlock_linkage": False,
"unclosed_alarm": False,
"unlocked_alarm": False,
"auto_lock_paused": False,
"night_latch": False,
},
b"\x00\x10\xff\x90",
"Lock Pro Wifi",
SwitchbotModel.LOCK_PRO_WIFI,
),
],
)
def test_lock_passive(test_case: AdvTestCase) -> None:
Expand Down Expand Up @@ -2912,6 +3048,30 @@ def test_lock_passive(test_case: AdvTestCase) -> None:
"Lock Ultra",
SwitchbotModel.LOCK_ULTRA,
),
AdvTestCase(
None,
b"\x00\x80\x2e\x00\x11\x69\x08",
{},
b"\x00\x11\x69\x08",
"Lock Vision",
SwitchbotModel.LOCK_VISION,
),
AdvTestCase(
None,
b"\x00\x803\x00\x11i\t",
{},
b"\x00\x11\x69\x09",
"Lock Vision Pro",
SwitchbotModel.LOCK_VISION_PRO,
),
AdvTestCase(
None,
b"\x00\x80=\x00\x10\xff\x90",
{},
b"\x00\x10\xff\x90",
"Lock Pro Wifi",
SwitchbotModel.LOCK_PRO_WIFI,
),
],
)
def test_lock_with_empty_data(test_case: AdvTestCase) -> None:
Expand Down
Loading