PySwitchbot 0.47.2__tar.gz → 0.48.0__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.
Files changed (52) hide show
  1. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/PKG-INFO +1 -1
  2. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/README.md +4 -1
  4. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/setup.py +1 -1
  5. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parser.py +6 -0
  6. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/const.py +1 -0
  7. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/lock.py +33 -10
  8. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/discovery.py +3 -1
  9. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/tests/test_adv_parser.py +69 -0
  10. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/LICENSE +0 -0
  11. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/MANIFEST.in +0 -0
  12. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/PySwitchbot.egg-info/SOURCES.txt +0 -0
  13. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  14. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/PySwitchbot.egg-info/requires.txt +0 -0
  15. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  16. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/setup.cfg +0 -0
  17. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/__init__.py +0 -0
  18. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/__init__.py +0 -0
  19. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  20. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/bot.py +0 -0
  21. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/bulb.py +0 -0
  22. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  23. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/contact.py +0 -0
  24. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/curtain.py +0 -0
  25. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/hub2.py +0 -0
  26. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/humidifier.py +0 -0
  27. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/light_strip.py +0 -0
  28. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/lock.py +0 -0
  29. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/meter.py +0 -0
  30. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/motion.py +0 -0
  31. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/adv_parsers/plug.py +0 -0
  32. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/api_config.py +0 -0
  33. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/__init__.py +0 -0
  34. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/base_cover.py +0 -0
  35. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/base_light.py +0 -0
  36. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/blind_tilt.py +0 -0
  37. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/bot.py +0 -0
  38. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/bulb.py +0 -0
  39. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/ceiling_light.py +0 -0
  40. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/contact.py +0 -0
  41. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/curtain.py +0 -0
  42. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/device.py +0 -0
  43. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/humidifier.py +0 -0
  44. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/light_strip.py +0 -0
  45. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/meter.py +0 -0
  46. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/motion.py +0 -0
  47. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/devices/plug.py +0 -0
  48. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/enum.py +0 -0
  49. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/switchbot/models.py +0 -0
  50. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/tests/test_base_cover.py +0 -0
  51. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/tests/test_blind_tilt.py +0 -0
  52. {pyswitchbot-0.47.2 → pyswitchbot-0.48.0}/tests/test_curtain.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySwitchbot
3
- Version: 0.47.2
3
+ Version: 0.48.0
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/Danielhiversen/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySwitchbot
3
- Version: 0.47.2
3
+ Version: 0.48.0
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/Danielhiversen/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -1,10 +1,13 @@
1
1
  # pySwitchbot [![Build Status](https://travis-ci.org/Danielhiversen/pySwitchbot.svg?branch=master)](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
- * WoLock
22
+ - WoLock
20
23
 
21
24
  ```python
22
25
  import asyncio
@@ -10,7 +10,7 @@ setup(
10
10
  "cryptography>=39.0.0",
11
11
  "pyOpenSSL>=23.0.0",
12
12
  ],
13
- version="0.47.2",
13
+ version="0.48.0",
14
14
  description="A library to communicate with Switchbot",
15
15
  author="Daniel Hjelseth Hoyer",
16
16
  url="https://github.com/Danielhiversen/pySwitchbot/",
@@ -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,
157
+ "manufacturer_id": 2409,
158
+ },
153
159
  "x": {
154
160
  "modelName": SwitchbotModel.BLIND_TILT,
155
161
  "modelFriendlyName": "Blind Tilt",
@@ -47,6 +47,7 @@ class SwitchbotModel(StrEnum):
47
47
  COLOR_BULB = "WoBulb"
48
48
  CEILING_LIGHT = "WoCeiling"
49
49
  LOCK = "WoLock"
50
+ LOCK_PRO = "WoLockPro"
50
51
  BLIND_TILT = "WoBlindTilt"
51
52
  HUB2 = "WoHub2"
52
53
 
@@ -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 = f"{COMMAND_HEADER}0f4f8101"
25
- COMMAND_UNLOCK = f"{COMMAND_HEADER}0f4e01011080"
26
- COMMAND_UNLOCK_WITHOUT_UNLATCH = f"{COMMAND_HEADER}0f4e010110a0"
27
- COMMAND_LOCK = f"{COMMAND_HEADER}0f4e01011000"
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, key_id: str, encryption_key: str
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=device, key_id=key_id, encryption_key=encryption_key
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(key=COMMAND_LOCK_INFO, retry=self._retry_count)
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
- return await self._get_devices_by_model("o")
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