PySwitchbot 0.51.0__tar.gz → 0.54.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 (59) hide show
  1. pyswitchbot-0.54.0/PKG-INFO +59 -0
  2. pyswitchbot-0.54.0/PySwitchbot.egg-info/PKG-INFO +59 -0
  3. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/PySwitchbot.egg-info/SOURCES.txt +6 -1
  4. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/README.md +1 -1
  5. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/setup.py +9 -2
  6. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/__init__.py +3 -0
  7. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parser.py +23 -1
  8. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/blind_tilt.py +1 -0
  9. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/bot.py +1 -0
  10. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/bulb.py +1 -0
  11. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/ceiling_light.py +1 -0
  12. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/contact.py +1 -0
  13. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/curtain.py +1 -0
  14. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/humidifier.py +1 -0
  15. pyswitchbot-0.54.0/switchbot/adv_parsers/keypad.py +22 -0
  16. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/light_strip.py +1 -0
  17. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/lock.py +1 -0
  18. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/meter.py +1 -0
  19. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/motion.py +1 -0
  20. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/plug.py +1 -0
  21. pyswitchbot-0.54.0/switchbot/adv_parsers/relay_switch.py +32 -0
  22. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/const.py +4 -0
  23. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/base_cover.py +1 -0
  24. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/blind_tilt.py +1 -0
  25. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/bot.py +1 -0
  26. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/curtain.py +1 -0
  27. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/device.py +1 -0
  28. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/humidifier.py +1 -0
  29. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/lock.py +1 -0
  30. pyswitchbot-0.54.0/switchbot/devices/motion.py +1 -0
  31. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/plug.py +1 -0
  32. pyswitchbot-0.54.0/switchbot/devices/relay_switch.py +185 -0
  33. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/discovery.py +4 -0
  34. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/enum.py +1 -0
  35. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/models.py +1 -0
  36. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/tests/test_adv_parser.py +90 -37
  37. pyswitchbot-0.54.0/tests/test_relay_switch.py +60 -0
  38. pyswitchbot-0.51.0/PKG-INFO +0 -20
  39. pyswitchbot-0.51.0/PySwitchbot.egg-info/PKG-INFO +0 -20
  40. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/LICENSE +0 -0
  41. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/MANIFEST.in +0 -0
  42. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  43. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/PySwitchbot.egg-info/requires.txt +0 -0
  44. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  45. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/setup.cfg +0 -0
  46. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/__init__.py +0 -0
  47. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/hub2.py +0 -0
  48. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/api_config.py +0 -0
  49. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/__init__.py +0 -0
  50. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/base_light.py +0 -0
  51. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/bulb.py +0 -0
  52. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/ceiling_light.py +0 -0
  53. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/contact.py +0 -0
  54. /pyswitchbot-0.51.0/switchbot/devices/meter.py → /pyswitchbot-0.54.0/switchbot/devices/keypad.py +0 -0
  55. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/light_strip.py +0 -0
  56. /pyswitchbot-0.51.0/switchbot/devices/motion.py → /pyswitchbot-0.54.0/switchbot/devices/meter.py +0 -0
  57. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/tests/test_base_cover.py +0 -0
  58. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/tests/test_blind_tilt.py +0 -0
  59. {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/tests/test_curtain.py +0 -0
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.1
2
+ Name: PySwitchbot
3
+ Version: 0.54.0
4
+ Summary: A library to communicate with Switchbot
5
+ Home-page: https://github.com/sblibs/pySwitchbot/
6
+ Author: Daniel Hjelseth Hoyer
7
+ License: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Other Environment
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Topic :: Home Automation
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: aiohttp>=3.9.5
18
+ Requires-Dist: bleak>=0.19.0
19
+ Requires-Dist: bleak-retry-connector>=3.4.0
20
+ Requires-Dist: cryptography>=39.0.0
21
+ Requires-Dist: pyOpenSSL>=23.0.0
22
+
23
+ # pySwitchbot [![Build Status](https://travis-ci.org/sblibs/pySwitchbot.svg?branch=master)](https://travis-ci.org/sblibs/pySwitchbot)
24
+
25
+ Library to control Switchbot IoT devices https://www.switch-bot.com/bot
26
+
27
+ ## Obtaining locks encryption key
28
+
29
+ Using the script `scripts/get_encryption_key.py` you can manually obtain locks encryption key.
30
+
31
+ Usage:
32
+
33
+ ```shell
34
+ $ python3 get_encryption_key.py MAC USERNAME
35
+ Key ID: xx
36
+ Encryption key: xxxxxxxxxxxxxxxx
37
+ ```
38
+
39
+ Where `MAC` is MAC address of the lock and `USERNAME` is your SwitchBot account username, after that script will ask for your password.
40
+ If authentication succeeds then script should output your key id and encryption key.
41
+
42
+ Examples:
43
+
44
+ - WoLock
45
+
46
+ ```python
47
+ import asyncio
48
+ from switchbot.discovery import GetSwitchbotDevices
49
+ from switchbot.devices import lock
50
+
51
+
52
+ async def main():
53
+ wolock = await GetSwitchbotDevices().get_locks()
54
+ await lock.SwitchbotLock(wolock['32C0F607-18B8-xxxx-xxxx-xxxxxxxxxx'].device, "key-id", "encryption-key").get_lock_status()
55
+
56
+
57
+ asyncio.run(main())
58
+
59
+ ```
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.1
2
+ Name: PySwitchbot
3
+ Version: 0.54.0
4
+ Summary: A library to communicate with Switchbot
5
+ Home-page: https://github.com/sblibs/pySwitchbot/
6
+ Author: Daniel Hjelseth Hoyer
7
+ License: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Other Environment
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Topic :: Home Automation
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: aiohttp>=3.9.5
18
+ Requires-Dist: bleak>=0.19.0
19
+ Requires-Dist: bleak-retry-connector>=3.4.0
20
+ Requires-Dist: cryptography>=39.0.0
21
+ Requires-Dist: pyOpenSSL>=23.0.0
22
+
23
+ # pySwitchbot [![Build Status](https://travis-ci.org/sblibs/pySwitchbot.svg?branch=master)](https://travis-ci.org/sblibs/pySwitchbot)
24
+
25
+ Library to control Switchbot IoT devices https://www.switch-bot.com/bot
26
+
27
+ ## Obtaining locks encryption key
28
+
29
+ Using the script `scripts/get_encryption_key.py` you can manually obtain locks encryption key.
30
+
31
+ Usage:
32
+
33
+ ```shell
34
+ $ python3 get_encryption_key.py MAC USERNAME
35
+ Key ID: xx
36
+ Encryption key: xxxxxxxxxxxxxxxx
37
+ ```
38
+
39
+ Where `MAC` is MAC address of the lock and `USERNAME` is your SwitchBot account username, after that script will ask for your password.
40
+ If authentication succeeds then script should output your key id and encryption key.
41
+
42
+ Examples:
43
+
44
+ - WoLock
45
+
46
+ ```python
47
+ import asyncio
48
+ from switchbot.discovery import GetSwitchbotDevices
49
+ from switchbot.devices import lock
50
+
51
+
52
+ async def main():
53
+ wolock = await GetSwitchbotDevices().get_locks()
54
+ await lock.SwitchbotLock(wolock['32C0F607-18B8-xxxx-xxxx-xxxxxxxxxx'].device, "key-id", "encryption-key").get_lock_status()
55
+
56
+
57
+ asyncio.run(main())
58
+
59
+ ```
@@ -23,11 +23,13 @@ switchbot/adv_parsers/contact.py
23
23
  switchbot/adv_parsers/curtain.py
24
24
  switchbot/adv_parsers/hub2.py
25
25
  switchbot/adv_parsers/humidifier.py
26
+ switchbot/adv_parsers/keypad.py
26
27
  switchbot/adv_parsers/light_strip.py
27
28
  switchbot/adv_parsers/lock.py
28
29
  switchbot/adv_parsers/meter.py
29
30
  switchbot/adv_parsers/motion.py
30
31
  switchbot/adv_parsers/plug.py
32
+ switchbot/adv_parsers/relay_switch.py
31
33
  switchbot/devices/__init__.py
32
34
  switchbot/devices/base_cover.py
33
35
  switchbot/devices/base_light.py
@@ -39,12 +41,15 @@ switchbot/devices/contact.py
39
41
  switchbot/devices/curtain.py
40
42
  switchbot/devices/device.py
41
43
  switchbot/devices/humidifier.py
44
+ switchbot/devices/keypad.py
42
45
  switchbot/devices/light_strip.py
43
46
  switchbot/devices/lock.py
44
47
  switchbot/devices/meter.py
45
48
  switchbot/devices/motion.py
46
49
  switchbot/devices/plug.py
50
+ switchbot/devices/relay_switch.py
47
51
  tests/test_adv_parser.py
48
52
  tests/test_base_cover.py
49
53
  tests/test_blind_tilt.py
50
- tests/test_curtain.py
54
+ tests/test_curtain.py
55
+ tests/test_relay_switch.py
@@ -1,4 +1,4 @@
1
- # pySwitchbot [![Build Status](https://travis-ci.org/Danielhiversen/pySwitchbot.svg?branch=master)](https://travis-ci.org/Danielhiversen/pySwitchbot)
1
+ # pySwitchbot [![Build Status](https://travis-ci.org/sblibs/pySwitchbot.svg?branch=master)](https://travis-ci.org/sblibs/pySwitchbot)
2
2
 
3
3
  Library to control Switchbot IoT devices https://www.switch-bot.com/bot
4
4
 
@@ -1,5 +1,10 @@
1
+ from pathlib import Path
2
+
1
3
  from setuptools import setup
2
4
 
5
+ this_directory = Path(__file__).parent
6
+ long_description = (this_directory / "README.md").read_text()
7
+
3
8
  setup(
4
9
  name="PySwitchbot",
5
10
  packages=["switchbot", "switchbot.devices", "switchbot.adv_parsers"],
@@ -10,10 +15,12 @@ setup(
10
15
  "cryptography>=39.0.0",
11
16
  "pyOpenSSL>=23.0.0",
12
17
  ],
13
- version="0.51.0",
18
+ version="0.54.0",
14
19
  description="A library to communicate with Switchbot",
20
+ long_description=long_description,
21
+ long_description_content_type="text/markdown",
15
22
  author="Daniel Hjelseth Hoyer",
16
- url="https://github.com/Danielhiversen/pySwitchbot/",
23
+ url="https://github.com/sblibs/pySwitchbot/",
17
24
  license="MIT",
18
25
  classifiers=[
19
26
  "Development Status :: 3 - Alpha",
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from bleak_retry_connector import (
@@ -26,6 +27,7 @@ from .devices.humidifier import SwitchbotHumidifier
26
27
  from .devices.light_strip import SwitchbotLightStrip
27
28
  from .devices.lock import SwitchbotLock
28
29
  from .devices.plug import SwitchbotPlugMini
30
+ from .devices.relay_switch import SwitchbotRelaySwitch
29
31
  from .discovery import GetSwitchbotDevices
30
32
  from .models import SwitchBotAdvertisement
31
33
 
@@ -54,4 +56,5 @@ __all__ = [
54
56
  "SwitchbotModel",
55
57
  "SwitchbotLock",
56
58
  "SwitchbotBlindTilt",
59
+ "SwitchbotRelaySwitch",
57
60
  ]
@@ -18,11 +18,16 @@ from .adv_parsers.contact import process_wocontact
18
18
  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
+ from .adv_parsers.keypad import process_wokeypad
21
22
  from .adv_parsers.light_strip import process_wostrip
22
23
  from .adv_parsers.lock import process_wolock, process_wolock_pro
23
24
  from .adv_parsers.meter import process_wosensorth, process_wosensorth_c
24
25
  from .adv_parsers.motion import process_wopresence
25
26
  from .adv_parsers.plug import process_woplugmini
27
+ from .adv_parsers.relay_switch import (
28
+ process_worelay_switch_1,
29
+ process_worelay_switch_1pm,
30
+ )
26
31
  from .const import SwitchbotModel
27
32
  from .models import SwitchBotAdvertisement
28
33
 
@@ -69,7 +74,6 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
69
74
  "modelFriendlyName": "Light Strip",
70
75
  "func": process_wostrip,
71
76
  "manufacturer_id": 2409,
72
- "manufacturer_data_length": 16,
73
77
  },
74
78
  "{": {
75
79
  "modelName": SwitchbotModel.CURTAIN,
@@ -174,6 +178,24 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
174
178
  "func": process_woblindtilt,
175
179
  "manufacturer_id": 2409,
176
180
  },
181
+ "y": {
182
+ "modelName": SwitchbotModel.KEYPAD,
183
+ "modelFriendlyName": "Keypad",
184
+ "func": process_wokeypad,
185
+ "manufacturer_id": 2409,
186
+ },
187
+ "<": {
188
+ "modelName": SwitchbotModel.RELAY_SWITCH_1PM,
189
+ "modelFriendlyName": "Relay Switch 1PM",
190
+ "func": process_worelay_switch_1pm,
191
+ "manufacturer_id": 2409,
192
+ },
193
+ ";": {
194
+ "modelName": SwitchbotModel.RELAY_SWITCH_1,
195
+ "modelFriendlyName": "Relay Switch 1",
196
+ "func": process_worelay_switch_1,
197
+ "manufacturer_id": 2409,
198
+ },
177
199
  }
178
200
 
179
201
  _SWITCHBOT_MODEL_TO_CHAR = {
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Bulb parser."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Ceiling Light adv parser."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
@@ -1,4 +1,5 @@
1
1
  """Contact sensor parser."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Humidifier adv parser."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
@@ -0,0 +1,22 @@
1
+ """Keypad parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ _LOGGER = logging.getLogger(__name__)
8
+
9
+
10
+ def process_wokeypad(
11
+ data: bytes | None,
12
+ mfr_data: bytes | None,
13
+ ) -> dict[str, bool | int | None]:
14
+ """Process woKeypad services data."""
15
+ if data is None or mfr_data is None:
16
+ return {"battery": None, "attempt_state": None}
17
+
18
+ _LOGGER.debug("mfr_data: %s", mfr_data.hex())
19
+ if data:
20
+ _LOGGER.debug("data: %s", data.hex())
21
+
22
+ return {"battery": data[2] & 0b01111111, "attempt_state": mfr_data[6]}
@@ -1,4 +1,5 @@
1
1
  """Light strip adv parser."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Lock parser."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
@@ -1,4 +1,5 @@
1
1
  """Meter parser."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import struct
@@ -1,4 +1,5 @@
1
1
  """Motion sensor parser."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
 
@@ -0,0 +1,32 @@
1
+ """Relay Switch adv parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def process_worelay_switch_1pm(
7
+ data: bytes | None, mfr_data: bytes | None
8
+ ) -> dict[str, bool | int]:
9
+ """Process WoStrip services data."""
10
+ if mfr_data is None:
11
+ return {}
12
+ return {
13
+ "switchMode": True, # for compatibility, useless
14
+ "sequence_number": mfr_data[6],
15
+ "isOn": bool(mfr_data[7] & 0b10000000),
16
+ "power": ((mfr_data[10] << 8) + mfr_data[11]) / 10,
17
+ "voltage": 0,
18
+ "current": 0,
19
+ }
20
+
21
+
22
+ def process_worelay_switch_1(
23
+ data: bytes | None, mfr_data: bytes | None
24
+ ) -> dict[str, bool | int]:
25
+ """Process WoStrip services data."""
26
+ if mfr_data is None:
27
+ return {}
28
+ return {
29
+ "switchMode": True, # for compatibility, useless
30
+ "sequence_number": mfr_data[6],
31
+ "isOn": bool(mfr_data[7] & 0b10000000),
32
+ }
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from enum import Enum
@@ -52,6 +53,9 @@ class SwitchbotModel(StrEnum):
52
53
  LOCK_PRO = "WoLockPro"
53
54
  BLIND_TILT = "WoBlindTilt"
54
55
  HUB2 = "WoHub2"
56
+ KEYPAD = "WoKeypad"
57
+ RELAY_SWITCH_1PM = "Relay Switch 1PM"
58
+ RELAY_SWITCH_1 = "Relay Switch 1"
55
59
 
56
60
 
57
61
  class LockStatus(Enum):
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import asyncio
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import time
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot Lock."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import asyncio
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import time
@@ -0,0 +1,185 @@
1
+ import asyncio
2
+ import logging
3
+ import time
4
+ from typing import Any
5
+
6
+ from bleak.backends.device import BLEDevice
7
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
8
+
9
+ from ..const import SwitchbotModel
10
+ from ..models import SwitchBotAdvertisement
11
+ from .device import SwitchbotDevice
12
+
13
+ _LOGGER = logging.getLogger(__name__)
14
+
15
+ COMMAND_HEADER = "57"
16
+ COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
17
+ COMMAND_TURN_OFF = f"{COMMAND_HEADER}0f70010000"
18
+ COMMAND_TURN_ON = f"{COMMAND_HEADER}0f70010100"
19
+ COMMAND_TOGGLE = f"{COMMAND_HEADER}0f70010200"
20
+ COMMAND_GET_VOLTAGE_AND_CURRENT = f"{COMMAND_HEADER}0f7106000000"
21
+ PASSIVE_POLL_INTERVAL = 10 * 60
22
+
23
+
24
+ class SwitchbotRelaySwitch(SwitchbotDevice):
25
+ """Representation of a Switchbot relay switch 1pm."""
26
+
27
+ def __init__(
28
+ self,
29
+ device: BLEDevice,
30
+ key_id: str,
31
+ encryption_key: str,
32
+ interface: int = 0,
33
+ model: SwitchbotModel = SwitchbotModel.RELAY_SWITCH_1PM,
34
+ **kwargs: Any,
35
+ ) -> None:
36
+ if len(key_id) == 0:
37
+ raise ValueError("key_id is missing")
38
+ elif len(key_id) != 2:
39
+ raise ValueError("key_id is invalid")
40
+ if len(encryption_key) == 0:
41
+ raise ValueError("encryption_key is missing")
42
+ elif len(encryption_key) != 32:
43
+ raise ValueError("encryption_key is invalid")
44
+ self._iv = None
45
+ self._cipher = None
46
+ self._key_id = key_id
47
+ self._encryption_key = bytearray.fromhex(encryption_key)
48
+ self._model: SwitchbotModel = model
49
+ self._force_next_update = False
50
+ super().__init__(device, None, interface, **kwargs)
51
+
52
+ def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
53
+ """Update device data from advertisement."""
54
+ # Obtain voltage and current through command.
55
+ adv_data = advertisement.data["data"]
56
+ if previous_voltage := self._get_adv_value("voltage"):
57
+ adv_data["voltage"] = previous_voltage
58
+ if previous_current := self._get_adv_value("current"):
59
+ adv_data["current"] = previous_current
60
+ current_state = self._get_adv_value("sequence_number")
61
+ super().update_from_advertisement(advertisement)
62
+ new_state = self._get_adv_value("sequence_number")
63
+ _LOGGER.debug(
64
+ "%s: update advertisement: %s (seq before: %s) (seq after: %s)",
65
+ self.name,
66
+ advertisement,
67
+ current_state,
68
+ new_state,
69
+ )
70
+ if current_state != new_state:
71
+ self._force_next_update = True
72
+
73
+ async def update(self, interface: int | None = None) -> None:
74
+ """Update state of device."""
75
+ if info := await self.get_voltage_and_current():
76
+ self._last_full_update = time.monotonic()
77
+ self._update_parsed_data(info)
78
+ self._fire_callbacks()
79
+
80
+ async def get_voltage_and_current(self) -> dict[str, Any] | None:
81
+ """Get voltage and current because advtisement don't have these"""
82
+ result = await self._send_command(COMMAND_GET_VOLTAGE_AND_CURRENT)
83
+ ok = self._check_command_result(result, 0, {1})
84
+ if ok:
85
+ return {
86
+ "voltage": ((result[9] << 8) + result[10]) / 10,
87
+ "current": (result[11] << 8) + result[12],
88
+ }
89
+ return None
90
+
91
+ def poll_needed(self, seconds_since_last_poll: float | None) -> bool:
92
+ """Return if device needs polling."""
93
+ if self._force_next_update:
94
+ self._force_next_update = False
95
+ return True
96
+ if (
97
+ seconds_since_last_poll is not None
98
+ and seconds_since_last_poll < PASSIVE_POLL_INTERVAL
99
+ ):
100
+ return False
101
+ time_since_last_full_update = time.monotonic() - self._last_full_update
102
+ if time_since_last_full_update < PASSIVE_POLL_INTERVAL:
103
+ return False
104
+ return True
105
+
106
+ async def turn_on(self) -> bool:
107
+ """Turn device on."""
108
+ result = await self._send_command(COMMAND_TURN_ON)
109
+ ok = self._check_command_result(result, 0, {1})
110
+ if ok:
111
+ self._override_state({"isOn": True})
112
+ self._fire_callbacks()
113
+ return ok
114
+
115
+ async def turn_off(self) -> bool:
116
+ """Turn device off."""
117
+ result = await self._send_command(COMMAND_TURN_OFF)
118
+ ok = self._check_command_result(result, 0, {1})
119
+ if ok:
120
+ self._override_state({"isOn": False})
121
+ self._fire_callbacks()
122
+ return ok
123
+
124
+ async def async_toggle(self, **kwargs) -> bool:
125
+ """Toggle device."""
126
+ result = await self._send_command(COMMAND_TOGGLE)
127
+ status = self._check_command_result(result, 0, {1})
128
+ return status
129
+
130
+ def is_on(self) -> bool | None:
131
+ """Return switch state from cache."""
132
+ return self._get_adv_value("isOn")
133
+
134
+ async def _send_command(
135
+ self, key: str, retry: int | None = None, encrypt: bool = True
136
+ ) -> bytes | None:
137
+ if not encrypt:
138
+ return await super()._send_command(key[:2] + "000000" + key[2:], retry)
139
+
140
+ result = await self._ensure_encryption_initialized()
141
+ if not result:
142
+ return None
143
+
144
+ encrypted = (
145
+ key[:2] + self._key_id + self._iv[0:2].hex() + self._encrypt(key[2:])
146
+ )
147
+ result = await super()._send_command(encrypted, retry)
148
+ return result[:1] + self._decrypt(result[4:])
149
+
150
+ async def _ensure_encryption_initialized(self) -> bool:
151
+ if self._iv is not None:
152
+ return True
153
+
154
+ result = await self._send_command(
155
+ COMMAND_GET_CK_IV + self._key_id, encrypt=False
156
+ )
157
+ ok = self._check_command_result(result, 0, {1})
158
+ if ok:
159
+ self._iv = result[4:]
160
+
161
+ return ok
162
+
163
+ async def _execute_disconnect(self) -> None:
164
+ await super()._execute_disconnect()
165
+ self._iv = None
166
+ self._cipher = None
167
+
168
+ def _get_cipher(self) -> Cipher:
169
+ if self._cipher is None:
170
+ self._cipher = Cipher(
171
+ algorithms.AES128(self._encryption_key), modes.CTR(self._iv)
172
+ )
173
+ return self._cipher
174
+
175
+ def _encrypt(self, data: str) -> str:
176
+ if len(data) == 0:
177
+ return ""
178
+ encryptor = self._get_cipher().encryptor()
179
+ return (encryptor.update(bytearray.fromhex(data)) + encryptor.finalize()).hex()
180
+
181
+ def _decrypt(self, data: bytearray) -> bytes:
182
+ if len(data) == 0:
183
+ return b""
184
+ decryptor = self._get_cipher().decryptor()
185
+ return decryptor.update(data) + decryptor.finalize()
@@ -124,6 +124,10 @@ class GetSwitchbotDevices:
124
124
  lock_pros = await self._get_devices_by_model("$")
125
125
  return {**locks, **lock_pros}
126
126
 
127
+ async def get_keypads(self) -> dict[str, SwitchBotAdvertisement]:
128
+ """Return all WoKeypad/Keypad devices with services data."""
129
+ return await self._get_devices_by_model("y")
130
+
127
131
  async def get_device_data(
128
132
  self, address: str
129
133
  ) -> dict[str, SwitchBotAdvertisement] | None:
@@ -1,4 +1,5 @@
1
1
  """Enum backports from standard lib."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from enum import Enum
@@ -1,4 +1,5 @@
1
1
  """Library to handle connection with Switchbot."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from dataclasses import dataclass
@@ -807,43 +807,6 @@ def test_bulb_active():
807
807
  )
808
808
 
809
809
 
810
- def test_lightstrip_passive():
811
- """Test parsing lightstrip as passive."""
812
- ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
813
- adv_data = generate_advertisement_data(
814
- manufacturer_data={
815
- 2409: b"`U\xf9(\xe5\x96\x00d\x02\xb0\x00\x00\x00\x00\x00\x00"
816
- },
817
- service_data={},
818
- tx_power=-127,
819
- rssi=-50,
820
- )
821
- result = parse_advertisement_data(ble_device, adv_data)
822
- assert result == SwitchBotAdvertisement(
823
- address="aa:bb:cc:dd:ee:ff",
824
- data={
825
- "data": {
826
- "brightness": 100,
827
- "color_mode": 2,
828
- "delay": False,
829
- "isOn": False,
830
- "loop_index": 0,
831
- "preset": False,
832
- "sequence_number": 0,
833
- "speed": 48,
834
- },
835
- "isEncrypted": False,
836
- "model": "r",
837
- "modelFriendlyName": "Light Strip",
838
- "modelName": SwitchbotModel.LIGHT_STRIP,
839
- "rawAdvData": None,
840
- },
841
- device=ble_device,
842
- rssi=-50,
843
- active=False,
844
- )
845
-
846
-
847
810
  def test_wosensor_passive_and_active():
848
811
  """Test parsing wosensor as passive with active data as well."""
849
812
  ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
@@ -1684,3 +1647,93 @@ def test_meter_pro_c_passive() -> None:
1684
1647
  rssi=-67,
1685
1648
  active=False,
1686
1649
  )
1650
+
1651
+
1652
+ def test_parse_advertisement_data_keypad():
1653
+ """Test parse_advertisement_data for the keypad."""
1654
+ ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
1655
+ adv_data = generate_advertisement_data(
1656
+ manufacturer_data={2409: b"\xeb\x13\x02\xe6#\x0f\x8fd\x00\x00\x00\x00"},
1657
+ service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"y\x00d"},
1658
+ rssi=-67,
1659
+ )
1660
+ result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.KEYPAD)
1661
+ assert result == SwitchBotAdvertisement(
1662
+ address="aa:bb:cc:dd:ee:ff",
1663
+ data={
1664
+ "data": {"attempt_state": 143, "battery": 100},
1665
+ "isEncrypted": False,
1666
+ "model": "y",
1667
+ "modelFriendlyName": "Keypad",
1668
+ "modelName": SwitchbotModel.KEYPAD,
1669
+ "rawAdvData": b"y\x00d",
1670
+ },
1671
+ device=ble_device,
1672
+ rssi=-67,
1673
+ active=True,
1674
+ )
1675
+
1676
+
1677
+ def test_parse_advertisement_data_relay_switch_1pm():
1678
+ """Test parse_advertisement_data for the keypad."""
1679
+ ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
1680
+ adv_data = generate_advertisement_data(
1681
+ manufacturer_data={2409: b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00"},
1682
+ service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"<\x00\x00\x00"},
1683
+ rssi=-67,
1684
+ )
1685
+ result = parse_advertisement_data(
1686
+ ble_device, adv_data, SwitchbotModel.RELAY_SWITCH_1PM
1687
+ )
1688
+ assert result == SwitchBotAdvertisement(
1689
+ address="aa:bb:cc:dd:ee:ff",
1690
+ data={
1691
+ "data": {
1692
+ "switchMode": True,
1693
+ "sequence_number": 71,
1694
+ "isOn": True,
1695
+ "power": 4.9,
1696
+ "voltage": 0,
1697
+ "current": 0,
1698
+ },
1699
+ "isEncrypted": False,
1700
+ "model": "<",
1701
+ "modelFriendlyName": "Relay Switch 1PM",
1702
+ "modelName": SwitchbotModel.RELAY_SWITCH_1PM,
1703
+ "rawAdvData": b"<\x00\x00\x00",
1704
+ },
1705
+ device=ble_device,
1706
+ rssi=-67,
1707
+ active=True,
1708
+ )
1709
+
1710
+
1711
+ def test_parse_advertisement_data_relay_switch_1():
1712
+ """Test parse_advertisement_data for the keypad."""
1713
+ ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
1714
+ adv_data = generate_advertisement_data(
1715
+ manufacturer_data={2409: b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00"},
1716
+ service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b";\x00\x00\x00"},
1717
+ rssi=-67,
1718
+ )
1719
+ result = parse_advertisement_data(
1720
+ ble_device, adv_data, SwitchbotModel.RELAY_SWITCH_1
1721
+ )
1722
+ assert result == SwitchBotAdvertisement(
1723
+ address="aa:bb:cc:dd:ee:ff",
1724
+ data={
1725
+ "data": {
1726
+ "switchMode": True,
1727
+ "sequence_number": 71,
1728
+ "isOn": True,
1729
+ },
1730
+ "isEncrypted": False,
1731
+ "model": ";",
1732
+ "modelFriendlyName": "Relay Switch 1",
1733
+ "modelName": SwitchbotModel.RELAY_SWITCH_1,
1734
+ "rawAdvData": b";\x00\x00\x00",
1735
+ },
1736
+ device=ble_device,
1737
+ rssi=-67,
1738
+ active=True,
1739
+ )
@@ -0,0 +1,60 @@
1
+ from unittest.mock import AsyncMock
2
+
3
+ import pytest
4
+ from bleak.backends.device import BLEDevice
5
+
6
+ from switchbot import SwitchBotAdvertisement, SwitchbotModel
7
+ from switchbot.devices import relay_switch
8
+
9
+ from .test_adv_parser import generate_ble_device
10
+
11
+
12
+ def create_device_for_command_testing(calibration=True, reverse_mode=False):
13
+ ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
14
+ relay_switch_device = relay_switch.SwitchbotRelaySwitch(
15
+ ble_device, "ff", "ffffffffffffffffffffffffffffffff"
16
+ )
17
+ relay_switch_device.update_from_advertisement(make_advertisement_data(ble_device))
18
+ return relay_switch_device
19
+
20
+
21
+ def make_advertisement_data(ble_device: BLEDevice):
22
+ """Set advertisement data with defaults."""
23
+
24
+ return SwitchBotAdvertisement(
25
+ address="aa:bb:cc:dd:ee:ff",
26
+ data={
27
+ "rawAdvData": b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00",
28
+ "data": {
29
+ "switchMode": True,
30
+ "sequence_number": 71,
31
+ "isOn": True,
32
+ "power": 4.9,
33
+ "voltage": 0,
34
+ "current": 0,
35
+ },
36
+ "isEncrypted": False,
37
+ "model": "<",
38
+ "modelFriendlyName": "Relay Switch 1PM",
39
+ "modelName": SwitchbotModel.RELAY_SWITCH_1PM,
40
+ },
41
+ device=ble_device,
42
+ rssi=-80,
43
+ active=True,
44
+ )
45
+
46
+
47
+ @pytest.mark.asyncio
48
+ async def test_turn_on():
49
+ relay_switch_device = create_device_for_command_testing()
50
+ relay_switch_device._send_command = AsyncMock(return_value=b"\x01")
51
+ await relay_switch_device.turn_on()
52
+ assert relay_switch_device.is_on() is True
53
+
54
+
55
+ @pytest.mark.asyncio
56
+ async def test_trun_off():
57
+ relay_switch_device = create_device_for_command_testing()
58
+ relay_switch_device._send_command = AsyncMock(return_value=b"\x01")
59
+ await relay_switch_device.turn_off()
60
+ assert relay_switch_device.is_on() is False
@@ -1,20 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: PySwitchbot
3
- Version: 0.51.0
4
- Summary: A library to communicate with Switchbot
5
- Home-page: https://github.com/Danielhiversen/pySwitchbot/
6
- Author: Daniel Hjelseth Hoyer
7
- License: MIT
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Environment :: Other Environment
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Operating System :: OS Independent
12
- Classifier: Programming Language :: Python
13
- Classifier: Topic :: Home Automation
14
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
- License-File: LICENSE
16
- Requires-Dist: aiohttp>=3.9.5
17
- Requires-Dist: bleak>=0.19.0
18
- Requires-Dist: bleak-retry-connector>=3.4.0
19
- Requires-Dist: cryptography>=39.0.0
20
- Requires-Dist: pyOpenSSL>=23.0.0
@@ -1,20 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: PySwitchbot
3
- Version: 0.51.0
4
- Summary: A library to communicate with Switchbot
5
- Home-page: https://github.com/Danielhiversen/pySwitchbot/
6
- Author: Daniel Hjelseth Hoyer
7
- License: MIT
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Environment :: Other Environment
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Operating System :: OS Independent
12
- Classifier: Programming Language :: Python
13
- Classifier: Topic :: Home Automation
14
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
- License-File: LICENSE
16
- Requires-Dist: aiohttp>=3.9.5
17
- Requires-Dist: bleak>=0.19.0
18
- Requires-Dist: bleak-retry-connector>=3.4.0
19
- Requires-Dist: cryptography>=39.0.0
20
- Requires-Dist: pyOpenSSL>=23.0.0
File without changes
File without changes
File without changes