PySwitchbot 0.58.0__tar.gz → 0.59.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.59.0/PKG-INFO +135 -0
- pyswitchbot-0.59.0/PySwitchbot.egg-info/PKG-INFO +135 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/PySwitchbot.egg-info/SOURCES.txt +5 -1
- pyswitchbot-0.59.0/README.md +102 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/setup.py +1 -1
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/__init__.py +2 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parser.py +14 -0
- pyswitchbot-0.59.0/switchbot/adv_parsers/hubmini_matter.py +37 -0
- pyswitchbot-0.59.0/switchbot/adv_parsers/roller_shade.py +29 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/const/__init__.py +2 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/base_cover.py +2 -0
- pyswitchbot-0.59.0/switchbot/devices/roller_shade.py +129 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/discovery.py +8 -1
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/tests/test_adv_parser.py +132 -0
- pyswitchbot-0.59.0/tests/test_roller_shade.py +223 -0
- pyswitchbot-0.58.0/PKG-INFO +0 -70
- pyswitchbot-0.58.0/PySwitchbot.egg-info/PKG-INFO +0 -70
- pyswitchbot-0.58.0/README.md +0 -37
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/LICENSE +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/MANIFEST.in +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/pyproject.toml +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/setup.cfg +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/device.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/devices/relay_switch.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/enum.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/switchbot/models.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/tests/test_curtain.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/tests/test_evaporative_humidifier.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.58.0 → pyswitchbot-0.59.0}/tests/test_relay_switch.py +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PySwitchbot
|
|
3
|
+
Version: 0.59.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
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: aiohttp>=3.9.5
|
|
19
|
+
Requires-Dist: bleak>=0.19.0
|
|
20
|
+
Requires-Dist: bleak-retry-connector>=3.4.0
|
|
21
|
+
Requires-Dist: cryptography>=39.0.0
|
|
22
|
+
Requires-Dist: pyOpenSSL>=23.0.0
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: license
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: requires-dist
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
33
|
+
|
|
34
|
+
# pySwitchbot [](https://travis-ci.org/sblibs/pySwitchbot)
|
|
35
|
+
|
|
36
|
+
Library to control Switchbot IoT devices https://www.switch-bot.com/bot
|
|
37
|
+
|
|
38
|
+
## Obtaining encryption key for Switchbot Locks
|
|
39
|
+
|
|
40
|
+
Using the script `scripts/get_encryption_key.py` you can manually obtain locks encryption key.
|
|
41
|
+
|
|
42
|
+
Usage:
|
|
43
|
+
|
|
44
|
+
```shell
|
|
45
|
+
$ python3 get_encryption_key.py MAC USERNAME
|
|
46
|
+
Key ID: XX
|
|
47
|
+
Encryption key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Where `MAC` is MAC address of the lock and `USERNAME` is your SwitchBot account username, after that script will ask for your password.
|
|
51
|
+
If authentication succeeds then script should output your key id and encryption key.
|
|
52
|
+
|
|
53
|
+
## Examples:
|
|
54
|
+
|
|
55
|
+
#### WoLock (Lock-Pro)
|
|
56
|
+
|
|
57
|
+
Unlock:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import asyncio
|
|
61
|
+
from switchbot.discovery import GetSwitchbotDevices
|
|
62
|
+
from switchbot.devices import lock
|
|
63
|
+
from switchbot.const import SwitchbotModel
|
|
64
|
+
|
|
65
|
+
BLE_MAC="XX:XX:XX:XX:XX:XX" # The MAC of your lock
|
|
66
|
+
KEY_ID="XX" # The key-ID of your encryption-key for your lock
|
|
67
|
+
ENC_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # The encryption-key with key-ID "XX"
|
|
68
|
+
LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def main():
|
|
72
|
+
wolock = await GetSwitchbotDevices().get_locks()
|
|
73
|
+
await lock.SwitchbotLock(
|
|
74
|
+
wolock[BLE_MAC].device, KEY_ID, ENCRYPTION_KEY, model=LOCK_MODEL
|
|
75
|
+
).unlock()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Lock:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import asyncio
|
|
85
|
+
from switchbot.discovery import GetSwitchbotDevices
|
|
86
|
+
from switchbot.devices import lock
|
|
87
|
+
from switchbot.const import SwitchbotModel
|
|
88
|
+
|
|
89
|
+
BLE_MAC="XX:XX:XX:XX:XX:XX" # The MAC of your lock
|
|
90
|
+
KEY_ID="XX" # The key-ID of your encryption-key for your lock
|
|
91
|
+
ENC_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # The encryption-key with key-ID "XX"
|
|
92
|
+
LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
wolock = await GetSwitchbotDevices().get_locks()
|
|
97
|
+
await lock.SwitchbotLock(
|
|
98
|
+
wolock[BLE_MAC].device, KEY_ID, ENCRYPTION_KEY, model=LOCK_MODEL
|
|
99
|
+
).lock()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
asyncio.run(main())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### WoCurtain (Curtain 3)
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import asyncio
|
|
109
|
+
from pprint import pprint
|
|
110
|
+
from switchbot import GetSwitchbotDevices
|
|
111
|
+
from switchbot.devices import curtain
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def main():
|
|
115
|
+
# get the BLE advertisement data of all switchbot devices in the vicinity
|
|
116
|
+
advertisement_data = await GetSwitchbotDevices().discover()
|
|
117
|
+
|
|
118
|
+
for i in advertisement_data.values():
|
|
119
|
+
pprint(i)
|
|
120
|
+
print() # print newline so that devices' data is separated visually
|
|
121
|
+
|
|
122
|
+
# find your device's BLE address by inspecting the above printed debug logs, example below
|
|
123
|
+
ble_address = "9915077C-C6FD-5FF6-27D3-45087898790B"
|
|
124
|
+
# get the BLE device (via its address) and construct a curtain device
|
|
125
|
+
ble_device = advertisement_data[ble_address].device
|
|
126
|
+
curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=False)
|
|
127
|
+
|
|
128
|
+
pprint(await curtain_device.get_device_data())
|
|
129
|
+
pprint(await curtain_device.get_basic_info())
|
|
130
|
+
await curtain_device.set_position(100)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
asyncio.run(main())
|
|
135
|
+
```
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PySwitchbot
|
|
3
|
+
Version: 0.59.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
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: aiohttp>=3.9.5
|
|
19
|
+
Requires-Dist: bleak>=0.19.0
|
|
20
|
+
Requires-Dist: bleak-retry-connector>=3.4.0
|
|
21
|
+
Requires-Dist: cryptography>=39.0.0
|
|
22
|
+
Requires-Dist: pyOpenSSL>=23.0.0
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: license
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: requires-dist
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
33
|
+
|
|
34
|
+
# pySwitchbot [](https://travis-ci.org/sblibs/pySwitchbot)
|
|
35
|
+
|
|
36
|
+
Library to control Switchbot IoT devices https://www.switch-bot.com/bot
|
|
37
|
+
|
|
38
|
+
## Obtaining encryption key for Switchbot Locks
|
|
39
|
+
|
|
40
|
+
Using the script `scripts/get_encryption_key.py` you can manually obtain locks encryption key.
|
|
41
|
+
|
|
42
|
+
Usage:
|
|
43
|
+
|
|
44
|
+
```shell
|
|
45
|
+
$ python3 get_encryption_key.py MAC USERNAME
|
|
46
|
+
Key ID: XX
|
|
47
|
+
Encryption key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Where `MAC` is MAC address of the lock and `USERNAME` is your SwitchBot account username, after that script will ask for your password.
|
|
51
|
+
If authentication succeeds then script should output your key id and encryption key.
|
|
52
|
+
|
|
53
|
+
## Examples:
|
|
54
|
+
|
|
55
|
+
#### WoLock (Lock-Pro)
|
|
56
|
+
|
|
57
|
+
Unlock:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import asyncio
|
|
61
|
+
from switchbot.discovery import GetSwitchbotDevices
|
|
62
|
+
from switchbot.devices import lock
|
|
63
|
+
from switchbot.const import SwitchbotModel
|
|
64
|
+
|
|
65
|
+
BLE_MAC="XX:XX:XX:XX:XX:XX" # The MAC of your lock
|
|
66
|
+
KEY_ID="XX" # The key-ID of your encryption-key for your lock
|
|
67
|
+
ENC_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # The encryption-key with key-ID "XX"
|
|
68
|
+
LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def main():
|
|
72
|
+
wolock = await GetSwitchbotDevices().get_locks()
|
|
73
|
+
await lock.SwitchbotLock(
|
|
74
|
+
wolock[BLE_MAC].device, KEY_ID, ENCRYPTION_KEY, model=LOCK_MODEL
|
|
75
|
+
).unlock()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Lock:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import asyncio
|
|
85
|
+
from switchbot.discovery import GetSwitchbotDevices
|
|
86
|
+
from switchbot.devices import lock
|
|
87
|
+
from switchbot.const import SwitchbotModel
|
|
88
|
+
|
|
89
|
+
BLE_MAC="XX:XX:XX:XX:XX:XX" # The MAC of your lock
|
|
90
|
+
KEY_ID="XX" # The key-ID of your encryption-key for your lock
|
|
91
|
+
ENC_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # The encryption-key with key-ID "XX"
|
|
92
|
+
LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
wolock = await GetSwitchbotDevices().get_locks()
|
|
97
|
+
await lock.SwitchbotLock(
|
|
98
|
+
wolock[BLE_MAC].device, KEY_ID, ENCRYPTION_KEY, model=LOCK_MODEL
|
|
99
|
+
).lock()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
asyncio.run(main())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### WoCurtain (Curtain 3)
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import asyncio
|
|
109
|
+
from pprint import pprint
|
|
110
|
+
from switchbot import GetSwitchbotDevices
|
|
111
|
+
from switchbot.devices import curtain
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def main():
|
|
115
|
+
# get the BLE advertisement data of all switchbot devices in the vicinity
|
|
116
|
+
advertisement_data = await GetSwitchbotDevices().discover()
|
|
117
|
+
|
|
118
|
+
for i in advertisement_data.values():
|
|
119
|
+
pprint(i)
|
|
120
|
+
print() # print newline so that devices' data is separated visually
|
|
121
|
+
|
|
122
|
+
# find your device's BLE address by inspecting the above printed debug logs, example below
|
|
123
|
+
ble_address = "9915077C-C6FD-5FF6-27D3-45087898790B"
|
|
124
|
+
# get the BLE device (via its address) and construct a curtain device
|
|
125
|
+
ble_device = advertisement_data[ble_address].device
|
|
126
|
+
curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=False)
|
|
127
|
+
|
|
128
|
+
pprint(await curtain_device.get_device_data())
|
|
129
|
+
pprint(await curtain_device.get_basic_info())
|
|
130
|
+
await curtain_device.set_position(100)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
asyncio.run(main())
|
|
135
|
+
```
|
|
@@ -23,6 +23,7 @@ switchbot/adv_parsers/ceiling_light.py
|
|
|
23
23
|
switchbot/adv_parsers/contact.py
|
|
24
24
|
switchbot/adv_parsers/curtain.py
|
|
25
25
|
switchbot/adv_parsers/hub2.py
|
|
26
|
+
switchbot/adv_parsers/hubmini_matter.py
|
|
26
27
|
switchbot/adv_parsers/humidifier.py
|
|
27
28
|
switchbot/adv_parsers/keypad.py
|
|
28
29
|
switchbot/adv_parsers/leak.py
|
|
@@ -33,6 +34,7 @@ switchbot/adv_parsers/motion.py
|
|
|
33
34
|
switchbot/adv_parsers/plug.py
|
|
34
35
|
switchbot/adv_parsers/relay_switch.py
|
|
35
36
|
switchbot/adv_parsers/remote.py
|
|
37
|
+
switchbot/adv_parsers/roller_shade.py
|
|
36
38
|
switchbot/const/__init__.py
|
|
37
39
|
switchbot/const/evaporative_humidifier.py
|
|
38
40
|
switchbot/const/hub2.py
|
|
@@ -56,10 +58,12 @@ switchbot/devices/meter.py
|
|
|
56
58
|
switchbot/devices/motion.py
|
|
57
59
|
switchbot/devices/plug.py
|
|
58
60
|
switchbot/devices/relay_switch.py
|
|
61
|
+
switchbot/devices/roller_shade.py
|
|
59
62
|
tests/test_adv_parser.py
|
|
60
63
|
tests/test_base_cover.py
|
|
61
64
|
tests/test_blind_tilt.py
|
|
62
65
|
tests/test_curtain.py
|
|
63
66
|
tests/test_evaporative_humidifier.py
|
|
64
67
|
tests/test_hub2.py
|
|
65
|
-
tests/test_relay_switch.py
|
|
68
|
+
tests/test_relay_switch.py
|
|
69
|
+
tests/test_roller_shade.py
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# pySwitchbot [](https://travis-ci.org/sblibs/pySwitchbot)
|
|
2
|
+
|
|
3
|
+
Library to control Switchbot IoT devices https://www.switch-bot.com/bot
|
|
4
|
+
|
|
5
|
+
## Obtaining encryption key for Switchbot Locks
|
|
6
|
+
|
|
7
|
+
Using the script `scripts/get_encryption_key.py` you can manually obtain locks encryption key.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
|
|
11
|
+
```shell
|
|
12
|
+
$ python3 get_encryption_key.py MAC USERNAME
|
|
13
|
+
Key ID: XX
|
|
14
|
+
Encryption key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Where `MAC` is MAC address of the lock and `USERNAME` is your SwitchBot account username, after that script will ask for your password.
|
|
18
|
+
If authentication succeeds then script should output your key id and encryption key.
|
|
19
|
+
|
|
20
|
+
## Examples:
|
|
21
|
+
|
|
22
|
+
#### WoLock (Lock-Pro)
|
|
23
|
+
|
|
24
|
+
Unlock:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import asyncio
|
|
28
|
+
from switchbot.discovery import GetSwitchbotDevices
|
|
29
|
+
from switchbot.devices import lock
|
|
30
|
+
from switchbot.const import SwitchbotModel
|
|
31
|
+
|
|
32
|
+
BLE_MAC="XX:XX:XX:XX:XX:XX" # The MAC of your lock
|
|
33
|
+
KEY_ID="XX" # The key-ID of your encryption-key for your lock
|
|
34
|
+
ENC_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # The encryption-key with key-ID "XX"
|
|
35
|
+
LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def main():
|
|
39
|
+
wolock = await GetSwitchbotDevices().get_locks()
|
|
40
|
+
await lock.SwitchbotLock(
|
|
41
|
+
wolock[BLE_MAC].device, KEY_ID, ENCRYPTION_KEY, model=LOCK_MODEL
|
|
42
|
+
).unlock()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
asyncio.run(main())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Lock:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import asyncio
|
|
52
|
+
from switchbot.discovery import GetSwitchbotDevices
|
|
53
|
+
from switchbot.devices import lock
|
|
54
|
+
from switchbot.const import SwitchbotModel
|
|
55
|
+
|
|
56
|
+
BLE_MAC="XX:XX:XX:XX:XX:XX" # The MAC of your lock
|
|
57
|
+
KEY_ID="XX" # The key-ID of your encryption-key for your lock
|
|
58
|
+
ENC_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # The encryption-key with key-ID "XX"
|
|
59
|
+
LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def main():
|
|
63
|
+
wolock = await GetSwitchbotDevices().get_locks()
|
|
64
|
+
await lock.SwitchbotLock(
|
|
65
|
+
wolock[BLE_MAC].device, KEY_ID, ENCRYPTION_KEY, model=LOCK_MODEL
|
|
66
|
+
).lock()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### WoCurtain (Curtain 3)
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
from pprint import pprint
|
|
77
|
+
from switchbot import GetSwitchbotDevices
|
|
78
|
+
from switchbot.devices import curtain
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def main():
|
|
82
|
+
# get the BLE advertisement data of all switchbot devices in the vicinity
|
|
83
|
+
advertisement_data = await GetSwitchbotDevices().discover()
|
|
84
|
+
|
|
85
|
+
for i in advertisement_data.values():
|
|
86
|
+
pprint(i)
|
|
87
|
+
print() # print newline so that devices' data is separated visually
|
|
88
|
+
|
|
89
|
+
# find your device's BLE address by inspecting the above printed debug logs, example below
|
|
90
|
+
ble_address = "9915077C-C6FD-5FF6-27D3-45087898790B"
|
|
91
|
+
# get the BLE device (via its address) and construct a curtain device
|
|
92
|
+
ble_device = advertisement_data[ble_address].device
|
|
93
|
+
curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=False)
|
|
94
|
+
|
|
95
|
+
pprint(await curtain_device.get_device_data())
|
|
96
|
+
pprint(await curtain_device.get_basic_info())
|
|
97
|
+
await curtain_device.set_position(100)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
asyncio.run(main())
|
|
102
|
+
```
|
|
@@ -29,6 +29,7 @@ from .devices.light_strip import SwitchbotLightStrip
|
|
|
29
29
|
from .devices.lock import SwitchbotLock
|
|
30
30
|
from .devices.plug import SwitchbotPlugMini
|
|
31
31
|
from .devices.relay_switch import SwitchbotRelaySwitch
|
|
32
|
+
from .devices.roller_shade import SwitchbotRollerShade
|
|
32
33
|
from .discovery import GetSwitchbotDevices
|
|
33
34
|
from .models import SwitchBotAdvertisement
|
|
34
35
|
|
|
@@ -58,6 +59,7 @@ __all__ = [
|
|
|
58
59
|
"SwitchbotPlugMini",
|
|
59
60
|
"SwitchbotPlugMini",
|
|
60
61
|
"SwitchbotRelaySwitch",
|
|
62
|
+
"SwitchbotRollerShade",
|
|
61
63
|
"SwitchbotSupportedType",
|
|
62
64
|
"SwitchbotSupportedType",
|
|
63
65
|
"close_stale_connections",
|
|
@@ -17,6 +17,7 @@ from .adv_parsers.ceiling_light import process_woceiling
|
|
|
17
17
|
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
|
+
from .adv_parsers.hubmini_matter import process_hubmini_matter
|
|
20
21
|
from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
|
|
21
22
|
from .adv_parsers.keypad import process_wokeypad
|
|
22
23
|
from .adv_parsers.leak import process_leak
|
|
@@ -30,6 +31,7 @@ from .adv_parsers.relay_switch import (
|
|
|
30
31
|
process_worelay_switch_1pm,
|
|
31
32
|
)
|
|
32
33
|
from .adv_parsers.remote import process_woremote
|
|
34
|
+
from .adv_parsers.roller_shade import process_worollershade
|
|
33
35
|
from .const import SwitchbotModel
|
|
34
36
|
from .models import SwitchBotAdvertisement
|
|
35
37
|
|
|
@@ -216,6 +218,18 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
|
|
|
216
218
|
"func": process_woremote,
|
|
217
219
|
"manufacturer_id": 89,
|
|
218
220
|
},
|
|
221
|
+
",": {
|
|
222
|
+
"modelName": SwitchbotModel.ROLLER_SHADE,
|
|
223
|
+
"modelFriendlyName": "Roller Shade",
|
|
224
|
+
"func": process_worollershade,
|
|
225
|
+
"manufacturer_id": 2409,
|
|
226
|
+
},
|
|
227
|
+
"%": {
|
|
228
|
+
"modelName": SwitchbotModel.HUBMINI_MATTER,
|
|
229
|
+
"modelFriendlyName": "HubMini Matter",
|
|
230
|
+
"func": process_hubmini_matter,
|
|
231
|
+
"manufacturer_id": 2409,
|
|
232
|
+
},
|
|
219
233
|
}
|
|
220
234
|
|
|
221
235
|
_SWITCHBOT_MODEL_TO_CHAR = {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Hubmini matter parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def process_hubmini_matter(
|
|
9
|
+
data: bytes | None, mfr_data: bytes | None
|
|
10
|
+
) -> dict[str, Any]:
|
|
11
|
+
"""Process Hubmini matter sensor manufacturer data."""
|
|
12
|
+
temp_data = None
|
|
13
|
+
|
|
14
|
+
if mfr_data:
|
|
15
|
+
temp_data = mfr_data[13:16]
|
|
16
|
+
|
|
17
|
+
if not temp_data:
|
|
18
|
+
return {}
|
|
19
|
+
|
|
20
|
+
_temp_sign = 1 if temp_data[1] & 0b10000000 else -1
|
|
21
|
+
_temp_c = _temp_sign * (
|
|
22
|
+
(temp_data[1] & 0b01111111) + ((temp_data[0] & 0b00001111) / 10)
|
|
23
|
+
)
|
|
24
|
+
_temp_f = (_temp_c * 9 / 5) + 32
|
|
25
|
+
_temp_f = (_temp_f * 10) / 10
|
|
26
|
+
humidity = temp_data[2] & 0b01111111
|
|
27
|
+
|
|
28
|
+
if _temp_c == 0 and humidity == 0:
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
paraser_data = {
|
|
32
|
+
"temp": {"c": _temp_c, "f": _temp_f},
|
|
33
|
+
"temperature": _temp_c,
|
|
34
|
+
"fahrenheit": bool(temp_data[2] & 0b10000000),
|
|
35
|
+
"humidity": humidity,
|
|
36
|
+
}
|
|
37
|
+
return paraser_data
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Library to handle connection with Switchbot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def process_worollershade(
|
|
7
|
+
data: bytes | None, mfr_data: bytes | None, reverse: bool = True
|
|
8
|
+
) -> dict[str, bool | int]:
|
|
9
|
+
"""Process woRollerShade services data."""
|
|
10
|
+
if mfr_data is None:
|
|
11
|
+
return {}
|
|
12
|
+
|
|
13
|
+
device_data = mfr_data[6:]
|
|
14
|
+
|
|
15
|
+
_position = max(min(device_data[2] & 0b01111111, 100), 0)
|
|
16
|
+
_calibrated = bool(device_data[2] & 0b10000000)
|
|
17
|
+
_in_motion = bool(device_data[1] & 0b00000110)
|
|
18
|
+
_light_level = (device_data[3] >> 4) & 0b00001111
|
|
19
|
+
_device_chain = device_data[3] & 0b00001111
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"calibration": _calibrated,
|
|
23
|
+
"battery": data[2] & 0b01111111 if data else None,
|
|
24
|
+
"inMotion": _in_motion,
|
|
25
|
+
"position": (100 - _position) if reverse else _position,
|
|
26
|
+
"lightLevel": _light_level,
|
|
27
|
+
"deviceChain": _device_chain,
|
|
28
|
+
"sequence_number": device_data[0],
|
|
29
|
+
}
|
|
@@ -10,6 +10,8 @@ from .device import REQ_HEADER, SwitchbotDevice, update_after_operation
|
|
|
10
10
|
|
|
11
11
|
# Cover keys
|
|
12
12
|
COVER_COMMAND = "4501"
|
|
13
|
+
ROLLERSHADE_COMMAND = "4701"
|
|
14
|
+
CONTROL_SOURCE = "00"
|
|
13
15
|
|
|
14
16
|
# For second element of open and close arrs we should add two bytes i.e. ff00
|
|
15
17
|
# First byte [ff] stands for speed (00 or ff - normal, 01 - slow) *
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Library to handle connection with Switchbot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..models import SwitchBotAdvertisement
|
|
9
|
+
from .base_cover import CONTROL_SOURCE, ROLLERSHADE_COMMAND, SwitchbotBaseCover
|
|
10
|
+
from .device import REQ_HEADER, SwitchbotSequenceDevice, update_after_operation
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
OPEN_KEYS = [
|
|
16
|
+
f"{REQ_HEADER}{ROLLERSHADE_COMMAND}01{CONTROL_SOURCE}0100",
|
|
17
|
+
f"{REQ_HEADER}{ROLLERSHADE_COMMAND}05{CONTROL_SOURCE}0000",
|
|
18
|
+
]
|
|
19
|
+
CLOSE_KEYS = [
|
|
20
|
+
f"{REQ_HEADER}{ROLLERSHADE_COMMAND}01{CONTROL_SOURCE}0164",
|
|
21
|
+
f"{REQ_HEADER}{ROLLERSHADE_COMMAND}05{CONTROL_SOURCE}0064",
|
|
22
|
+
]
|
|
23
|
+
POSITION_KEYS = [
|
|
24
|
+
f"{REQ_HEADER}{ROLLERSHADE_COMMAND}01{CONTROL_SOURCE}01",
|
|
25
|
+
f"{REQ_HEADER}{ROLLERSHADE_COMMAND}05{CONTROL_SOURCE}",
|
|
26
|
+
] # +actual_position
|
|
27
|
+
STOP_KEYS = [f"{REQ_HEADER}{ROLLERSHADE_COMMAND}00{CONTROL_SOURCE}01"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SwitchbotRollerShade(SwitchbotBaseCover, SwitchbotSequenceDevice):
|
|
31
|
+
"""Representation of a Switchbot Roller Shade."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
34
|
+
"""Switchbot roller shade constructor."""
|
|
35
|
+
# The position of the roller shade is saved returned with 0 = open and 100 = closed.
|
|
36
|
+
# the definition of position is the same as in Home Assistant.
|
|
37
|
+
|
|
38
|
+
self._reverse: bool = kwargs.pop("reverse_mode", True)
|
|
39
|
+
super().__init__(self._reverse, *args, **kwargs)
|
|
40
|
+
|
|
41
|
+
def _set_parsed_data(
|
|
42
|
+
self, advertisement: SwitchBotAdvertisement, data: dict[str, Any]
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Set data."""
|
|
45
|
+
in_motion = data["inMotion"]
|
|
46
|
+
previous_position = self._get_adv_value("position")
|
|
47
|
+
new_position = data["position"]
|
|
48
|
+
self._update_motion_direction(in_motion, previous_position, new_position)
|
|
49
|
+
super()._set_parsed_data(advertisement, data)
|
|
50
|
+
|
|
51
|
+
@update_after_operation
|
|
52
|
+
async def open(self, mode: int = 0) -> bool:
|
|
53
|
+
"""Send open command. 0 - performance mode, 1 - unfelt mode."""
|
|
54
|
+
self._is_opening = True
|
|
55
|
+
self._is_closing = False
|
|
56
|
+
return await self._send_multiple_commands(OPEN_KEYS)
|
|
57
|
+
|
|
58
|
+
@update_after_operation
|
|
59
|
+
async def close(self, speed: int = 0) -> bool:
|
|
60
|
+
"""Send close command. 0 - performance mode, 1 - unfelt mode."""
|
|
61
|
+
self._is_closing = True
|
|
62
|
+
self._is_opening = False
|
|
63
|
+
return await self._send_multiple_commands(CLOSE_KEYS)
|
|
64
|
+
|
|
65
|
+
@update_after_operation
|
|
66
|
+
async def stop(self) -> bool:
|
|
67
|
+
"""Send stop command to device."""
|
|
68
|
+
self._is_opening = self._is_closing = False
|
|
69
|
+
return await self._send_multiple_commands(STOP_KEYS)
|
|
70
|
+
|
|
71
|
+
@update_after_operation
|
|
72
|
+
async def set_position(self, position: int, mode: int = 0) -> bool:
|
|
73
|
+
"""Send position command (0-100) to device. 0 - performance mode, 1 - unfelt mode."""
|
|
74
|
+
position = (100 - position) if self._reverse else position
|
|
75
|
+
self._update_motion_direction(True, self._get_adv_value("position"), position)
|
|
76
|
+
return await self._send_multiple_commands(
|
|
77
|
+
[
|
|
78
|
+
f"{POSITION_KEYS[0]}{position:02X}",
|
|
79
|
+
f"{POSITION_KEYS[1]}{mode:02X}{position:02X}",
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def get_position(self) -> Any:
|
|
84
|
+
"""Return cached position (0-100) of Curtain."""
|
|
85
|
+
# To get actual position call update() first.
|
|
86
|
+
return self._get_adv_value("position")
|
|
87
|
+
|
|
88
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
89
|
+
"""Get device basic settings."""
|
|
90
|
+
if not (_data := await self._get_basic_info()):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
_position = max(min(_data[5], 100), 0)
|
|
94
|
+
_direction_adjusted_position = (100 - _position) if self._reverse else _position
|
|
95
|
+
_previous_position = self._get_adv_value("position")
|
|
96
|
+
_in_motion = bool(_data[4] & 0b00000011)
|
|
97
|
+
self._update_motion_direction(
|
|
98
|
+
_in_motion, _previous_position, _direction_adjusted_position
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"battery": _data[1],
|
|
103
|
+
"firmware": _data[2] / 10.0,
|
|
104
|
+
"chainLength": _data[3],
|
|
105
|
+
"openDirection": (
|
|
106
|
+
"clockwise" if _data[4] & 0b10000000 == 128 else "anticlockwise"
|
|
107
|
+
),
|
|
108
|
+
"fault": bool(_data[4] & 0b00010000),
|
|
109
|
+
"solarPanel": bool(_data[4] & 0b00001000),
|
|
110
|
+
"calibration": bool(_data[4] & 0b00000100),
|
|
111
|
+
"calibrated": bool(_data[4] & 0b00000100),
|
|
112
|
+
"inMotion": _in_motion,
|
|
113
|
+
"position": _direction_adjusted_position,
|
|
114
|
+
"timers": _data[6],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
def _update_motion_direction(
|
|
118
|
+
self, in_motion: bool, previous_position: int | None, new_position: int
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Update opening/closing status based on movement."""
|
|
121
|
+
if previous_position is None:
|
|
122
|
+
return
|
|
123
|
+
if in_motion is False:
|
|
124
|
+
self._is_closing = self._is_opening = False
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
if new_position != previous_position:
|
|
128
|
+
self._is_opening = new_position > previous_position
|
|
129
|
+
self._is_closing = new_position < previous_position
|