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.
- pyswitchbot-0.54.0/PKG-INFO +59 -0
- pyswitchbot-0.54.0/PySwitchbot.egg-info/PKG-INFO +59 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/PySwitchbot.egg-info/SOURCES.txt +6 -1
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/README.md +1 -1
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/setup.py +9 -2
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/__init__.py +3 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parser.py +23 -1
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/blind_tilt.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/bot.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/bulb.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/ceiling_light.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/contact.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/curtain.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/humidifier.py +1 -0
- pyswitchbot-0.54.0/switchbot/adv_parsers/keypad.py +22 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/light_strip.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/lock.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/meter.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/motion.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/plug.py +1 -0
- pyswitchbot-0.54.0/switchbot/adv_parsers/relay_switch.py +32 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/const.py +4 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/base_cover.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/blind_tilt.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/bot.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/curtain.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/device.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/humidifier.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/lock.py +1 -0
- pyswitchbot-0.54.0/switchbot/devices/motion.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/plug.py +1 -0
- pyswitchbot-0.54.0/switchbot/devices/relay_switch.py +185 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/discovery.py +4 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/enum.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/models.py +1 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/tests/test_adv_parser.py +90 -37
- pyswitchbot-0.54.0/tests/test_relay_switch.py +60 -0
- pyswitchbot-0.51.0/PKG-INFO +0 -20
- pyswitchbot-0.51.0/PySwitchbot.egg-info/PKG-INFO +0 -20
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/LICENSE +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/MANIFEST.in +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/setup.cfg +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/contact.py +0 -0
- /pyswitchbot-0.51.0/switchbot/devices/meter.py → /pyswitchbot-0.54.0/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/switchbot/devices/light_strip.py +0 -0
- /pyswitchbot-0.51.0/switchbot/devices/motion.py → /pyswitchbot-0.54.0/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.51.0 → pyswitchbot-0.54.0}/tests/test_blind_tilt.py +0 -0
- {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 [](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 [](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 [](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.
|
|
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/
|
|
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 = {
|
|
@@ -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]}
|
|
@@ -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):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -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:
|
|
@@ -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
|
pyswitchbot-0.51.0/PKG-INFO
DELETED
|
@@ -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
|
|
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
|
/pyswitchbot-0.51.0/switchbot/devices/meter.py → /pyswitchbot-0.54.0/switchbot/devices/keypad.py
RENAMED
|
File without changes
|
|
File without changes
|
/pyswitchbot-0.51.0/switchbot/devices/motion.py → /pyswitchbot-0.54.0/switchbot/devices/meter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|