PySwitchbot 0.47.2__tar.gz → 0.48.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/PKG-INFO +1 -1
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/README.md +4 -1
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/setup.py +1 -1
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parser.py +7 -1
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/lock.py +24 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/const.py +1 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/lock.py +33 -10
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/discovery.py +3 -1
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/tests/test_adv_parser.py +69 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/LICENSE +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/MANIFEST.in +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/PySwitchbot.egg-info/SOURCES.txt +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/setup.cfg +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/__init__.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/device.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/enum.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/switchbot/models.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.47.2 → pyswitchbot-0.48.1}/tests/test_curtain.py +0 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# pySwitchbot [](https://travis-ci.org/Danielhiversen/pySwitchbot)
|
|
2
|
+
|
|
2
3
|
Library to control Switchbot IoT devices https://www.switch-bot.com/bot
|
|
3
4
|
|
|
4
5
|
## Obtaining locks encryption key
|
|
6
|
+
|
|
5
7
|
Using the script `scripts/get_encryption_key.py` you can manually obtain locks encryption key.
|
|
6
8
|
|
|
7
9
|
Usage:
|
|
10
|
+
|
|
8
11
|
```shell
|
|
9
12
|
$ python3 get_encryption_key.py MAC USERNAME
|
|
10
13
|
Key ID: xx
|
|
@@ -16,7 +19,7 @@ If authentication succeeds then script should output your key id and encryption
|
|
|
16
19
|
|
|
17
20
|
Examples:
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
- WoLock
|
|
20
23
|
|
|
21
24
|
```python
|
|
22
25
|
import asyncio
|
|
@@ -19,7 +19,7 @@ from .adv_parsers.curtain import process_wocurtain
|
|
|
19
19
|
from .adv_parsers.hub2 import process_wohub2
|
|
20
20
|
from .adv_parsers.humidifier import process_wohumidifier
|
|
21
21
|
from .adv_parsers.light_strip import process_wostrip
|
|
22
|
-
from .adv_parsers.lock import process_wolock
|
|
22
|
+
from .adv_parsers.lock import process_wolock, process_wolock_pro
|
|
23
23
|
from .adv_parsers.meter import process_wosensorth
|
|
24
24
|
from .adv_parsers.motion import process_wopresence
|
|
25
25
|
from .adv_parsers.plug import process_woplugmini
|
|
@@ -150,6 +150,12 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
|
|
|
150
150
|
"func": process_wolock,
|
|
151
151
|
"manufacturer_id": 2409,
|
|
152
152
|
},
|
|
153
|
+
"$": {
|
|
154
|
+
"modelName": SwitchbotModel.LOCK_PRO,
|
|
155
|
+
"modelFriendlyName": "Lock Pro",
|
|
156
|
+
"func": process_wolock_pro,
|
|
157
|
+
"manufacturer_id": 2409,
|
|
158
|
+
},
|
|
153
159
|
"x": {
|
|
154
160
|
"modelName": SwitchbotModel.BLIND_TILT,
|
|
155
161
|
"modelFriendlyName": "Blind Tilt",
|
|
@@ -29,3 +29,27 @@ def process_wolock(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool
|
|
|
29
29
|
"auto_lock_paused": bool(mfr_data[8] & 0b00000010),
|
|
30
30
|
"night_latch": bool(mfr_data[9] & 0b00000001) if len(mfr_data) > 9 else False,
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def process_wolock_pro(
|
|
35
|
+
data: bytes | None, mfr_data: bytes | None
|
|
36
|
+
) -> dict[str, bool | int]:
|
|
37
|
+
_LOGGER.debug("mfr_data: %s", mfr_data.hex())
|
|
38
|
+
if data:
|
|
39
|
+
_LOGGER.debug("data: %s", data.hex())
|
|
40
|
+
|
|
41
|
+
res = {
|
|
42
|
+
"battery": data[2] & 0b01111111 if data else None,
|
|
43
|
+
"calibration": bool(mfr_data[7] & 0b10000000),
|
|
44
|
+
"status": LockStatus((mfr_data[7] & 0b00111000) >> 3),
|
|
45
|
+
"door_open": bool(mfr_data[8] & 0b01100000),
|
|
46
|
+
# Double lock mode is not supported on Lock Pro
|
|
47
|
+
"update_from_secondary_lock": False,
|
|
48
|
+
"double_lock_mode": False,
|
|
49
|
+
"unclosed_alarm": bool(mfr_data[11] & 0b10000000),
|
|
50
|
+
"unlocked_alarm": bool(mfr_data[11] & 0b01000000),
|
|
51
|
+
"auto_lock_paused": bool(mfr_data[8] & 0b100000),
|
|
52
|
+
"night_latch": bool(mfr_data[9] & 0b00000001),
|
|
53
|
+
}
|
|
54
|
+
_LOGGER.debug(res)
|
|
55
|
+
return res
|
|
@@ -16,15 +16,28 @@ from ..const import (
|
|
|
16
16
|
SwitchbotAccountConnectionError,
|
|
17
17
|
SwitchbotApiError,
|
|
18
18
|
SwitchbotAuthenticationError,
|
|
19
|
+
SwitchbotModel,
|
|
19
20
|
)
|
|
20
21
|
from .device import SwitchbotDevice, SwitchbotOperationError
|
|
21
22
|
|
|
22
23
|
COMMAND_HEADER = "57"
|
|
23
24
|
COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
|
|
24
|
-
COMMAND_LOCK_INFO =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
COMMAND_LOCK_INFO = {
|
|
26
|
+
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4f8101",
|
|
27
|
+
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4f8102",
|
|
28
|
+
}
|
|
29
|
+
COMMAND_UNLOCK = {
|
|
30
|
+
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e01011080",
|
|
31
|
+
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e0101000080",
|
|
32
|
+
}
|
|
33
|
+
COMMAND_UNLOCK_WITHOUT_UNLATCH = {
|
|
34
|
+
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e010110a0",
|
|
35
|
+
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e01010000a0",
|
|
36
|
+
}
|
|
37
|
+
COMMAND_LOCK = {
|
|
38
|
+
SwitchbotModel.LOCK: f"{COMMAND_HEADER}0f4e01011000",
|
|
39
|
+
SwitchbotModel.LOCK_PRO: f"{COMMAND_HEADER}0f4e0101000000",
|
|
40
|
+
}
|
|
28
41
|
COMMAND_ENABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e01001e00008101"
|
|
29
42
|
COMMAND_DISABLE_NOTIFICATIONS = f"{COMMAND_HEADER}0e00"
|
|
30
43
|
|
|
@@ -49,6 +62,7 @@ class SwitchbotLock(SwitchbotDevice):
|
|
|
49
62
|
key_id: str,
|
|
50
63
|
encryption_key: str,
|
|
51
64
|
interface: int = 0,
|
|
65
|
+
model: SwitchbotModel = SwitchbotModel.LOCK,
|
|
52
66
|
**kwargs: Any,
|
|
53
67
|
) -> None:
|
|
54
68
|
if len(key_id) == 0:
|
|
@@ -59,20 +73,27 @@ class SwitchbotLock(SwitchbotDevice):
|
|
|
59
73
|
raise ValueError("encryption_key is missing")
|
|
60
74
|
elif len(encryption_key) != 32:
|
|
61
75
|
raise ValueError("encryption_key is invalid")
|
|
76
|
+
if model not in (SwitchbotModel.LOCK, SwitchbotModel.LOCK_PRO):
|
|
77
|
+
raise ValueError("initializing SwitchbotLock with a non-lock model")
|
|
62
78
|
self._iv = None
|
|
63
79
|
self._cipher = None
|
|
64
80
|
self._key_id = key_id
|
|
65
81
|
self._encryption_key = bytearray.fromhex(encryption_key)
|
|
66
82
|
self._notifications_enabled: bool = False
|
|
83
|
+
self._model: SwitchbotModel = model
|
|
67
84
|
super().__init__(device, None, interface, **kwargs)
|
|
68
85
|
|
|
69
86
|
@staticmethod
|
|
70
87
|
async def verify_encryption_key(
|
|
71
|
-
device: BLEDevice,
|
|
88
|
+
device: BLEDevice,
|
|
89
|
+
key_id: str,
|
|
90
|
+
encryption_key: str,
|
|
91
|
+
model: SwitchbotModel = SwitchbotModel.LOCK,
|
|
92
|
+
**kwargs: Any,
|
|
72
93
|
) -> bool:
|
|
73
94
|
try:
|
|
74
95
|
lock = SwitchbotLock(
|
|
75
|
-
device
|
|
96
|
+
device, key_id=key_id, encryption_key=encryption_key, model=model
|
|
76
97
|
)
|
|
77
98
|
except ValueError:
|
|
78
99
|
return False
|
|
@@ -183,19 +204,19 @@ class SwitchbotLock(SwitchbotDevice):
|
|
|
183
204
|
async def lock(self) -> bool:
|
|
184
205
|
"""Send lock command."""
|
|
185
206
|
return await self._lock_unlock(
|
|
186
|
-
COMMAND_LOCK, {LockStatus.LOCKED, LockStatus.LOCKING}
|
|
207
|
+
COMMAND_LOCK[self._model], {LockStatus.LOCKED, LockStatus.LOCKING}
|
|
187
208
|
)
|
|
188
209
|
|
|
189
210
|
async def unlock(self) -> bool:
|
|
190
211
|
"""Send unlock command. If unlatch feature is enabled in EU firmware, also unlatches door"""
|
|
191
212
|
return await self._lock_unlock(
|
|
192
|
-
COMMAND_UNLOCK, {LockStatus.UNLOCKED, LockStatus.UNLOCKING}
|
|
213
|
+
COMMAND_UNLOCK[self._model], {LockStatus.UNLOCKED, LockStatus.UNLOCKING}
|
|
193
214
|
)
|
|
194
215
|
|
|
195
216
|
async def unlock_without_unlatch(self) -> bool:
|
|
196
217
|
"""Send unlock command. This command will not unlatch the door."""
|
|
197
218
|
return await self._lock_unlock(
|
|
198
|
-
COMMAND_UNLOCK_WITHOUT_UNLATCH,
|
|
219
|
+
COMMAND_UNLOCK_WITHOUT_UNLATCH[self._model],
|
|
199
220
|
{LockStatus.UNLOCKED, LockStatus.UNLOCKING, LockStatus.NOT_FULLY_LOCKED},
|
|
200
221
|
)
|
|
201
222
|
|
|
@@ -275,7 +296,9 @@ class SwitchbotLock(SwitchbotDevice):
|
|
|
275
296
|
|
|
276
297
|
async def _get_lock_info(self) -> bytes | None:
|
|
277
298
|
"""Return lock info of device."""
|
|
278
|
-
_data = await self._send_command(
|
|
299
|
+
_data = await self._send_command(
|
|
300
|
+
key=COMMAND_LOCK_INFO[self._model], retry=self._retry_count
|
|
301
|
+
)
|
|
279
302
|
|
|
280
303
|
if not self._check_command_result(_data, 0, COMMAND_RESULT_EXPECTED_VALUES):
|
|
281
304
|
_LOGGER.error("Unsuccessful, please try again")
|
|
@@ -120,7 +120,9 @@ class GetSwitchbotDevices:
|
|
|
120
120
|
|
|
121
121
|
async def get_locks(self) -> dict[str, SwitchBotAdvertisement]:
|
|
122
122
|
"""Return all WoLock/Locks devices with services data."""
|
|
123
|
-
|
|
123
|
+
locks = await self._get_devices_by_model("o")
|
|
124
|
+
lock_pros = await self._get_devices_by_model("$")
|
|
125
|
+
return {**locks, **lock_pros}
|
|
124
126
|
|
|
125
127
|
async def get_device_data(
|
|
126
128
|
self, address: str
|
|
@@ -1390,6 +1390,75 @@ def test_parsing_lock_passive():
|
|
|
1390
1390
|
)
|
|
1391
1391
|
|
|
1392
1392
|
|
|
1393
|
+
def test_parsing_lock_pro_active():
|
|
1394
|
+
"""Test parsing lock pro with active data."""
|
|
1395
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
1396
|
+
adv_data = generate_advertisement_data(
|
|
1397
|
+
manufacturer_data={2409: b"\xc8\xf5,\xd9-V\x07\x82\x00d\x00\x00"},
|
|
1398
|
+
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"$\x80d"},
|
|
1399
|
+
rssi=-80,
|
|
1400
|
+
)
|
|
1401
|
+
result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LOCK_PRO)
|
|
1402
|
+
assert result == SwitchBotAdvertisement(
|
|
1403
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
1404
|
+
data={
|
|
1405
|
+
"data": {
|
|
1406
|
+
"battery": 100,
|
|
1407
|
+
"calibration": True,
|
|
1408
|
+
"status": LockStatus.LOCKED,
|
|
1409
|
+
"update_from_secondary_lock": False,
|
|
1410
|
+
"door_open": False,
|
|
1411
|
+
"double_lock_mode": False,
|
|
1412
|
+
"unclosed_alarm": False,
|
|
1413
|
+
"unlocked_alarm": False,
|
|
1414
|
+
"auto_lock_paused": False,
|
|
1415
|
+
"night_latch": False,
|
|
1416
|
+
},
|
|
1417
|
+
"model": "$",
|
|
1418
|
+
"isEncrypted": False,
|
|
1419
|
+
"modelFriendlyName": "Lock Pro",
|
|
1420
|
+
"modelName": SwitchbotModel.LOCK_PRO,
|
|
1421
|
+
"rawAdvData": b"$\x80d",
|
|
1422
|
+
},
|
|
1423
|
+
device=ble_device,
|
|
1424
|
+
rssi=-80,
|
|
1425
|
+
active=True,
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def test_parsing_lock_pro_passive():
|
|
1430
|
+
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
1431
|
+
adv_data = generate_advertisement_data(
|
|
1432
|
+
manufacturer_data={2409: bytes.fromhex("aabbccddeeff208200640000")}, rssi=-67
|
|
1433
|
+
)
|
|
1434
|
+
result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LOCK_PRO)
|
|
1435
|
+
assert result == SwitchBotAdvertisement(
|
|
1436
|
+
address="aa:bb:cc:dd:ee:ff",
|
|
1437
|
+
data={
|
|
1438
|
+
"data": {
|
|
1439
|
+
"battery": None,
|
|
1440
|
+
"calibration": True,
|
|
1441
|
+
"status": LockStatus.LOCKED,
|
|
1442
|
+
"update_from_secondary_lock": False,
|
|
1443
|
+
"door_open": False,
|
|
1444
|
+
"double_lock_mode": False,
|
|
1445
|
+
"unclosed_alarm": False,
|
|
1446
|
+
"unlocked_alarm": False,
|
|
1447
|
+
"auto_lock_paused": False,
|
|
1448
|
+
"night_latch": False,
|
|
1449
|
+
},
|
|
1450
|
+
"model": "$",
|
|
1451
|
+
"isEncrypted": False,
|
|
1452
|
+
"modelFriendlyName": "Lock Pro",
|
|
1453
|
+
"modelName": SwitchbotModel.LOCK_PRO,
|
|
1454
|
+
"rawAdvData": None,
|
|
1455
|
+
},
|
|
1456
|
+
device=ble_device,
|
|
1457
|
+
rssi=-67,
|
|
1458
|
+
active=False,
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
|
|
1393
1462
|
def test_parsing_lock_active_old_firmware():
|
|
1394
1463
|
"""Test parsing lock with active data. Old firmware."""
|
|
1395
1464
|
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|