PySwitchbot 2.2.0__tar.gz → 2.3.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-2.2.0/PySwitchbot.egg-info → pyswitchbot-2.3.0}/PKG-INFO +46 -22
- pyswitchbot-2.2.0/PKG-INFO → pyswitchbot-2.3.0/README.md +27 -35
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/pyproject.toml +73 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/__init__.py +2 -0
- pyswitchbot-2.3.0/switchbot/__version__.py +1 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parser.py +13 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/air_purifier.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/art_frame.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/blind_tilt.py +2 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/bot.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/bulb.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/ceiling_light.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/climate_panel.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/contact.py +8 -3
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/curtain.py +3 -3
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/fan.py +11 -4
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/hub2.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/hub3.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/hubmini_matter.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/humidifier.py +2 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/keypad.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/keypad_vision.py +3 -3
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/leak.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/light_strip.py +16 -3
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/lock.py +4 -4
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/meter.py +10 -4
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/motion.py +2 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/plug.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/presence_sensor.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/remote.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/roller_shade.py +2 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/vacuum.py +2 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/const/__init__.py +1 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/const/light.py +11 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/device.py +3 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/fan.py +82 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/light_strip.py +138 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/relay_switch.py +24 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/smart_thermostat_radiator.py +12 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/discovery.py +2 -4
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/utils.py +1 -1
- pyswitchbot-2.2.0/MANIFEST.in +0 -1
- pyswitchbot-2.2.0/PySwitchbot.egg-info/SOURCES.txt +0 -112
- pyswitchbot-2.2.0/PySwitchbot.egg-info/dependency_links.txt +0 -1
- pyswitchbot-2.2.0/PySwitchbot.egg-info/requires.txt +0 -5
- pyswitchbot-2.2.0/PySwitchbot.egg-info/top_level.txt +0 -1
- pyswitchbot-2.2.0/README.md +0 -110
- pyswitchbot-2.2.0/setup.cfg +0 -4
- pyswitchbot-2.2.0/setup.py +0 -40
- pyswitchbot-2.2.0/tests/test_adv_parser.py +0 -4754
- pyswitchbot-2.2.0/tests/test_air_purifier.py +0 -466
- pyswitchbot-2.2.0/tests/test_art_frame.py +0 -200
- pyswitchbot-2.2.0/tests/test_base_cover.py +0 -151
- pyswitchbot-2.2.0/tests/test_blind_tilt.py +0 -240
- pyswitchbot-2.2.0/tests/test_bulb.py +0 -241
- pyswitchbot-2.2.0/tests/test_ceiling_light.py +0 -194
- pyswitchbot-2.2.0/tests/test_colormode_imports.py +0 -88
- pyswitchbot-2.2.0/tests/test_curtain.py +0 -425
- pyswitchbot-2.2.0/tests/test_device.py +0 -416
- pyswitchbot-2.2.0/tests/test_discovery_callback.py +0 -142
- pyswitchbot-2.2.0/tests/test_encrypted_device.py +0 -593
- pyswitchbot-2.2.0/tests/test_evaporative_humidifier.py +0 -341
- pyswitchbot-2.2.0/tests/test_fan.py +0 -621
- pyswitchbot-2.2.0/tests/test_helpers.py +0 -72
- pyswitchbot-2.2.0/tests/test_hub2.py +0 -18
- pyswitchbot-2.2.0/tests/test_hub3.py +0 -13
- pyswitchbot-2.2.0/tests/test_keypad_vision.py +0 -259
- pyswitchbot-2.2.0/tests/test_lock.py +0 -864
- pyswitchbot-2.2.0/tests/test_meter_pro.py +0 -249
- pyswitchbot-2.2.0/tests/test_relay_switch.py +0 -482
- pyswitchbot-2.2.0/tests/test_roller_shade.py +0 -383
- pyswitchbot-2.2.0/tests/test_smart_thermostat_radiator.py +0 -221
- pyswitchbot-2.2.0/tests/test_strip_light.py +0 -475
- pyswitchbot-2.2.0/tests/test_utils.py +0 -41
- pyswitchbot-2.2.0/tests/test_vacuum.py +0 -145
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/LICENSE +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/_sensor_th.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/weather_station.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/const/air_purifier.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/const/climate.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/const/fan.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/const/hub3.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/const/presence_sensor.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/air_purifier.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/art_frame.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/keypad_vision.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/meter_pro.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/devices/vacuum.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/enum.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.3.0}/switchbot/models.py +0 -0
|
@@ -1,35 +1,33 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PySwitchbot
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: A library to communicate with Switchbot
|
|
5
|
-
Home-page: https://github.com/sblibs/pySwitchbot/
|
|
6
|
-
Author: Daniel Hjelseth Hoyer
|
|
7
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Daniel Hjelseth Hoyer
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
8
9
|
Classifier: Development Status :: 3 - Alpha
|
|
9
10
|
Classifier: Environment :: Other Environment
|
|
10
11
|
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
13
|
Classifier: Operating System :: OS Independent
|
|
12
14
|
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
20
|
Classifier: Topic :: Home Automation
|
|
14
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
-
Requires-
|
|
22
|
+
Requires-Dist: aiohttp (>=3.9.5)
|
|
23
|
+
Requires-Dist: bleak (>=0.19.0)
|
|
24
|
+
Requires-Dist: bleak-retry-connector (>=3.4.0)
|
|
25
|
+
Requires-Dist: cryptography (>=39.0.0)
|
|
26
|
+
Requires-Dist: pyOpenSSL (>=23.0.0)
|
|
27
|
+
Project-URL: Bug Tracker, https://github.com/sblibs/pySwitchbot/issues
|
|
28
|
+
Project-URL: Changelog, https://github.com/sblibs/pySwitchbot/blob/main/CHANGELOG.md
|
|
29
|
+
Project-URL: Repository, https://github.com/sblibs/pySwitchbot/
|
|
16
30
|
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
31
|
|
|
34
32
|
# pySwitchbot [](https://codecov.io/gh/sblibs/pySwitchbot)
|
|
35
33
|
|
|
@@ -58,6 +56,31 @@ Encryption key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
|
58
56
|
Where `MAC` is MAC address of the lock and `USERNAME` is your SwitchBot account username, after that script will ask for your password.
|
|
59
57
|
If authentication succeeds then script should output your key id and encryption key.
|
|
60
58
|
|
|
59
|
+
### Troubleshooting key retrieval
|
|
60
|
+
|
|
61
|
+
Key retrieval talks to SwitchBot's account API with your username and
|
|
62
|
+
password. The most common failures are account-side, not bugs in this library:
|
|
63
|
+
|
|
64
|
+
- **`Authentication failed: ...`** — the username/password were rejected. This
|
|
65
|
+
happens when:
|
|
66
|
+
- Two-factor authentication (2FA) is enabled on the account. The API login
|
|
67
|
+
used here does not support a verification code, so 2FA accounts cannot
|
|
68
|
+
retrieve keys this way — temporarily disable 2FA, fetch the key, then
|
|
69
|
+
re-enable it.
|
|
70
|
+
- The account was created via "Sign in with Apple"/Google and has no
|
|
71
|
+
password set. Set a password in the SwitchBot app first, or use an
|
|
72
|
+
email/password account.
|
|
73
|
+
- The username is an email but the account is registered to a phone number
|
|
74
|
+
(or vice versa). Use the exact identifier you log in with.
|
|
75
|
+
- **`Failed to retrieve encryption key from SwitchBot Account: ...`** —
|
|
76
|
+
authentication succeeded but the key could not be read. Usually the account
|
|
77
|
+
is not the device **owner**: keys are only returned to the owning account,
|
|
78
|
+
not to shared/family members. Retrieve the key from the owner account, or
|
|
79
|
+
transfer ownership in the app.
|
|
80
|
+
|
|
81
|
+
The key only needs to be fetched once; store the `key_id` and encryption key
|
|
82
|
+
and reuse them — there is no need to call the script on every connection.
|
|
83
|
+
|
|
61
84
|
## Examples:
|
|
62
85
|
|
|
63
86
|
#### WoLock (Lock-Pro)
|
|
@@ -79,7 +102,7 @@ LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
|
|
|
79
102
|
async def main():
|
|
80
103
|
wolock = await GetSwitchbotDevices().get_locks()
|
|
81
104
|
await lock.SwitchbotLock(
|
|
82
|
-
wolock[BLE_MAC].device, KEY_ID,
|
|
105
|
+
wolock[BLE_MAC].device, KEY_ID, ENC_KEY, model=LOCK_MODEL
|
|
83
106
|
).unlock()
|
|
84
107
|
|
|
85
108
|
|
|
@@ -103,7 +126,7 @@ LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
|
|
|
103
126
|
async def main():
|
|
104
127
|
wolock = await GetSwitchbotDevices().get_locks()
|
|
105
128
|
await lock.SwitchbotLock(
|
|
106
|
-
wolock[BLE_MAC].device, KEY_ID,
|
|
129
|
+
wolock[BLE_MAC].device, KEY_ID, ENC_KEY, model=LOCK_MODEL
|
|
107
130
|
).lock()
|
|
108
131
|
|
|
109
132
|
|
|
@@ -141,3 +164,4 @@ async def main():
|
|
|
141
164
|
if __name__ == "__main__":
|
|
142
165
|
asyncio.run(main())
|
|
143
166
|
```
|
|
167
|
+
|
|
@@ -1,36 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: PySwitchbot
|
|
3
|
-
Version: 2.2.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
1
|
# pySwitchbot [](https://codecov.io/gh/sblibs/pySwitchbot)
|
|
35
2
|
|
|
36
3
|
Library to control Switchbot IoT devices https://www.switch-bot.com/
|
|
@@ -58,6 +25,31 @@ Encryption key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
|
58
25
|
Where `MAC` is MAC address of the lock and `USERNAME` is your SwitchBot account username, after that script will ask for your password.
|
|
59
26
|
If authentication succeeds then script should output your key id and encryption key.
|
|
60
27
|
|
|
28
|
+
### Troubleshooting key retrieval
|
|
29
|
+
|
|
30
|
+
Key retrieval talks to SwitchBot's account API with your username and
|
|
31
|
+
password. The most common failures are account-side, not bugs in this library:
|
|
32
|
+
|
|
33
|
+
- **`Authentication failed: ...`** — the username/password were rejected. This
|
|
34
|
+
happens when:
|
|
35
|
+
- Two-factor authentication (2FA) is enabled on the account. The API login
|
|
36
|
+
used here does not support a verification code, so 2FA accounts cannot
|
|
37
|
+
retrieve keys this way — temporarily disable 2FA, fetch the key, then
|
|
38
|
+
re-enable it.
|
|
39
|
+
- The account was created via "Sign in with Apple"/Google and has no
|
|
40
|
+
password set. Set a password in the SwitchBot app first, or use an
|
|
41
|
+
email/password account.
|
|
42
|
+
- The username is an email but the account is registered to a phone number
|
|
43
|
+
(or vice versa). Use the exact identifier you log in with.
|
|
44
|
+
- **`Failed to retrieve encryption key from SwitchBot Account: ...`** —
|
|
45
|
+
authentication succeeded but the key could not be read. Usually the account
|
|
46
|
+
is not the device **owner**: keys are only returned to the owning account,
|
|
47
|
+
not to shared/family members. Retrieve the key from the owner account, or
|
|
48
|
+
transfer ownership in the app.
|
|
49
|
+
|
|
50
|
+
The key only needs to be fetched once; store the `key_id` and encryption key
|
|
51
|
+
and reuse them — there is no need to call the script on every connection.
|
|
52
|
+
|
|
61
53
|
## Examples:
|
|
62
54
|
|
|
63
55
|
#### WoLock (Lock-Pro)
|
|
@@ -79,7 +71,7 @@ LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
|
|
|
79
71
|
async def main():
|
|
80
72
|
wolock = await GetSwitchbotDevices().get_locks()
|
|
81
73
|
await lock.SwitchbotLock(
|
|
82
|
-
wolock[BLE_MAC].device, KEY_ID,
|
|
74
|
+
wolock[BLE_MAC].device, KEY_ID, ENC_KEY, model=LOCK_MODEL
|
|
83
75
|
).unlock()
|
|
84
76
|
|
|
85
77
|
|
|
@@ -103,7 +95,7 @@ LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
|
|
|
103
95
|
async def main():
|
|
104
96
|
wolock = await GetSwitchbotDevices().get_locks()
|
|
105
97
|
await lock.SwitchbotLock(
|
|
106
|
-
wolock[BLE_MAC].device, KEY_ID,
|
|
98
|
+
wolock[BLE_MAC].device, KEY_ID, ENC_KEY, model=LOCK_MODEL
|
|
107
99
|
).lock()
|
|
108
100
|
|
|
109
101
|
|
|
@@ -1,8 +1,64 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "PySwitchbot"
|
|
3
|
+
version = "2.3.0"
|
|
4
|
+
description = "A library to communicate with Switchbot"
|
|
5
|
+
authors = ["Daniel Hjelseth Hoyer"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
repository = "https://github.com/sblibs/pySwitchbot/"
|
|
9
|
+
packages = [
|
|
10
|
+
{ include = "switchbot" },
|
|
11
|
+
]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Environment :: Other Environment",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python",
|
|
18
|
+
"Topic :: Home Automation",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.poetry.urls]
|
|
23
|
+
"Bug Tracker" = "https://github.com/sblibs/pySwitchbot/issues"
|
|
24
|
+
"Changelog" = "https://github.com/sblibs/pySwitchbot/blob/main/CHANGELOG.md"
|
|
25
|
+
|
|
26
|
+
[tool.poetry.dependencies]
|
|
27
|
+
python = ">=3.11,<4.0"
|
|
28
|
+
aiohttp = ">=3.9.5"
|
|
29
|
+
bleak = ">=0.19.0"
|
|
30
|
+
bleak-retry-connector = ">=3.4.0"
|
|
31
|
+
cryptography = ">=39.0.0"
|
|
32
|
+
pyOpenSSL = ">=23.0.0"
|
|
33
|
+
|
|
34
|
+
[tool.poetry.group.dev.dependencies]
|
|
35
|
+
pytest = ">=7,<10"
|
|
36
|
+
pytest-cov = ">=3,<8"
|
|
37
|
+
pytest-asyncio = ">=0.19,<1.4"
|
|
38
|
+
|
|
39
|
+
[tool.semantic_release]
|
|
40
|
+
branch = "main"
|
|
41
|
+
# Existing release tags have no "v" prefix (e.g. 2.2.0), unlike PSR's default
|
|
42
|
+
# "v{version}"; without this PSR finds no prior release and bumps from 0.0.0.
|
|
43
|
+
tag_format = "{version}"
|
|
44
|
+
version_toml = ["pyproject.toml:tool.poetry.version"]
|
|
45
|
+
version_variables = ["switchbot/__version__.py:__version__"]
|
|
46
|
+
build_command = "pip install poetry && poetry build"
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
addopts = "--cov=switchbot --cov-report=term-missing"
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ["poetry-core>=1.0.0"]
|
|
53
|
+
build-backend = "poetry.core.masonry.api"
|
|
54
|
+
|
|
1
55
|
[tool.ruff]
|
|
2
56
|
target-version = "py311"
|
|
3
57
|
line-length = 88
|
|
4
58
|
|
|
5
59
|
[tool.ruff.lint]
|
|
60
|
+
preview = true
|
|
61
|
+
explicit-preview-rules = true # opt in ONLY explicitly-listed preview rules
|
|
6
62
|
ignore = [
|
|
7
63
|
"S101", # use of assert
|
|
8
64
|
"D203", # 1 blank line required before class docstring
|
|
@@ -34,15 +90,30 @@ ignore = [
|
|
|
34
90
|
]
|
|
35
91
|
select = [
|
|
36
92
|
"ASYNC", # async rules
|
|
93
|
+
"A", # flake8-builtins
|
|
37
94
|
"B", # flake8-bugbear
|
|
95
|
+
"BLE", # flake8-blind-except
|
|
38
96
|
"D", # flake8-docstrings
|
|
39
97
|
"C4", # flake8-comprehensions
|
|
98
|
+
"C90", # mccabe complexity
|
|
99
|
+
"DTZ", # flake8-datetimez
|
|
100
|
+
"ERA", # eradicate
|
|
101
|
+
"EXE", # flake8-executable
|
|
40
102
|
"S", # flake8-bandit
|
|
41
103
|
"F", # pyflake
|
|
104
|
+
"FA", # flake8-future-annotations
|
|
105
|
+
"FIX", # flake8-fixme
|
|
106
|
+
"FURB", # refurb
|
|
107
|
+
"FURB118", # reimplemented-operator (preview) - unneeded lambdas
|
|
42
108
|
"E", # pycodestyle
|
|
43
109
|
"W", # pycodestyle
|
|
44
110
|
"UP", # pyupgrade
|
|
45
111
|
"I", # isort
|
|
112
|
+
"ICN", # flake8-import-conventions
|
|
113
|
+
"INP", # flake8-no-pep420
|
|
114
|
+
"ISC", # flake8-implicit-str-concat
|
|
115
|
+
"LOG", # flake8-logging
|
|
116
|
+
"Q", # flake8-quotes
|
|
46
117
|
"RUF", # ruff specific
|
|
47
118
|
"FLY", # flynt
|
|
48
119
|
"G", # flake8-logging-format ,
|
|
@@ -60,8 +131,10 @@ select = [
|
|
|
60
131
|
"SLOT", # flake8-slots
|
|
61
132
|
"T100", # Trace found: {name} used
|
|
62
133
|
"T20", # flake8-print
|
|
134
|
+
"TD", # flake8-todos
|
|
63
135
|
"TID", # Tidy imports
|
|
64
136
|
"TRY", # tryceratops
|
|
137
|
+
"YTT", # flake8-2020
|
|
65
138
|
]
|
|
66
139
|
|
|
67
140
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -57,6 +57,7 @@ from .devices.light_strip import (
|
|
|
57
57
|
SwitchbotPermanentOutdoorLight,
|
|
58
58
|
SwitchbotRgbicLight,
|
|
59
59
|
SwitchbotRgbicNeonLight,
|
|
60
|
+
SwitchbotRgbicwwCeilingLight,
|
|
60
61
|
SwitchbotStripLight3,
|
|
61
62
|
)
|
|
62
63
|
from .devices.lock import SwitchbotLock
|
|
@@ -126,6 +127,7 @@ __all__ = [
|
|
|
126
127
|
"SwitchbotRelaySwitch2PM",
|
|
127
128
|
"SwitchbotRgbicLight",
|
|
128
129
|
"SwitchbotRgbicNeonLight",
|
|
130
|
+
"SwitchbotRgbicwwCeilingLight",
|
|
129
131
|
"SwitchbotRollerShade",
|
|
130
132
|
"SwitchbotSmartThermostatRadiator",
|
|
131
133
|
"SwitchbotStandingFan",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.3.0"
|
|
@@ -32,6 +32,7 @@ from .adv_parsers.light_strip import (
|
|
|
32
32
|
process_candle_warmer_lamp,
|
|
33
33
|
process_light,
|
|
34
34
|
process_rgbic_light,
|
|
35
|
+
process_rgbicww_ceiling_light,
|
|
35
36
|
process_wostrip,
|
|
36
37
|
)
|
|
37
38
|
from .adv_parsers.lock import (
|
|
@@ -677,6 +678,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
|
|
|
677
678
|
"func": process_rgbic_light,
|
|
678
679
|
"manufacturer_id": 2409,
|
|
679
680
|
},
|
|
681
|
+
b"\x00\x11\xbb\x10": {
|
|
682
|
+
"modelName": SwitchbotModel.RGBICWW_CEILING_LIGHT,
|
|
683
|
+
"modelFriendlyName": "RGBICWW Ceiling Light",
|
|
684
|
+
"func": process_rgbicww_ceiling_light,
|
|
685
|
+
"manufacturer_id": 2409,
|
|
686
|
+
},
|
|
687
|
+
b"\x01\x11\xbb\x10": {
|
|
688
|
+
"modelName": SwitchbotModel.RGBICWW_CEILING_LIGHT,
|
|
689
|
+
"modelFriendlyName": "RGBICWW Ceiling Light",
|
|
690
|
+
"func": process_rgbicww_ceiling_light,
|
|
691
|
+
"manufacturer_id": 2409,
|
|
692
|
+
},
|
|
680
693
|
b"\x00\x10\xd0\xb7": {
|
|
681
694
|
"modelName": SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
|
|
682
695
|
"modelFriendlyName": "Permanent Outdoor Light",
|
|
@@ -11,7 +11,7 @@ def process_air_purifier(
|
|
|
11
11
|
data: bytes | None, mfr_data: bytes | None
|
|
12
12
|
) -> dict[str, bool | int]:
|
|
13
13
|
"""Process air purifier services data."""
|
|
14
|
-
if mfr_data is None:
|
|
14
|
+
if mfr_data is None or len(mfr_data) < 14:
|
|
15
15
|
return {}
|
|
16
16
|
device_data = mfr_data[6:]
|
|
17
17
|
|
|
@@ -7,7 +7,7 @@ def process_woblindtilt(
|
|
|
7
7
|
data: bytes | None, mfr_data: bytes | None, reverse: bool = False
|
|
8
8
|
) -> dict[str, bool | int]:
|
|
9
9
|
"""Process woBlindTilt services data."""
|
|
10
|
-
if mfr_data is None:
|
|
10
|
+
if mfr_data is None or len(mfr_data) < 10:
|
|
11
11
|
return {}
|
|
12
12
|
|
|
13
13
|
device_data = mfr_data[6:]
|
|
@@ -19,7 +19,7 @@ def process_woblindtilt(
|
|
|
19
19
|
|
|
20
20
|
return {
|
|
21
21
|
"calibration": _calibrated,
|
|
22
|
-
"battery": data[2] & 0b01111111 if data else None,
|
|
22
|
+
"battery": data[2] & 0b01111111 if data and len(data) >= 3 else None,
|
|
23
23
|
"inMotion": _in_motion,
|
|
24
24
|
"tilt": (100 - _tilt) if reverse else _tilt,
|
|
25
25
|
"lightLevel": _light_level,
|
|
@@ -7,7 +7,7 @@ def process_color_bulb(
|
|
|
7
7
|
data: bytes | None, mfr_data: bytes | None
|
|
8
8
|
) -> dict[str, bool | int]:
|
|
9
9
|
"""Process WoBulb services data."""
|
|
10
|
-
if mfr_data is None:
|
|
10
|
+
if mfr_data is None or len(mfr_data) < 11:
|
|
11
11
|
return {}
|
|
12
12
|
return {
|
|
13
13
|
"sequence_number": mfr_data[6],
|
|
@@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
15
15
|
|
|
16
16
|
def process_woceiling(data: bytes, mfr_data: bytes | None) -> dict[str, bool | int]:
|
|
17
17
|
"""Process WoCeiling services data."""
|
|
18
|
-
if mfr_data is None:
|
|
18
|
+
if mfr_data is None or len(mfr_data) < 11:
|
|
19
19
|
return {}
|
|
20
20
|
return {
|
|
21
21
|
"sequence_number": mfr_data[6],
|
|
@@ -10,10 +10,15 @@ def process_wocontact(
|
|
|
10
10
|
if data is None and mfr_data is None:
|
|
11
11
|
return {}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
has_full_mfr = mfr_data is not None and len(mfr_data) >= 13
|
|
14
|
+
has_full_data = data is not None and len(data) >= 9
|
|
15
|
+
if not has_full_mfr and not has_full_data:
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
battery = data[2] & 0b01111111 if data and len(data) >= 3 else None
|
|
19
|
+
tested = bool(data[1] & 0b10000000) if data and len(data) >= 2 else None
|
|
15
20
|
|
|
16
|
-
if
|
|
21
|
+
if has_full_mfr:
|
|
17
22
|
motion_detected = bool(mfr_data[7] & 0b10000000)
|
|
18
23
|
contact_open = bool(mfr_data[7] & 0b00010000)
|
|
19
24
|
contact_timeout = bool(mfr_data[7] & 0b00100000)
|
|
@@ -12,8 +12,8 @@ def process_wocurtain(
|
|
|
12
12
|
battery_data = mfr_data[12]
|
|
13
13
|
elif mfr_data and len(mfr_data) >= 11:
|
|
14
14
|
device_data = mfr_data[8:11]
|
|
15
|
-
battery_data = data[2] if data else None
|
|
16
|
-
elif data:
|
|
15
|
+
battery_data = data[2] if data and len(data) >= 3 else None
|
|
16
|
+
elif data and len(data) >= 6:
|
|
17
17
|
device_data = data[3:6]
|
|
18
18
|
battery_data = data[2]
|
|
19
19
|
else:
|
|
@@ -25,7 +25,7 @@ def process_wocurtain(
|
|
|
25
25
|
_device_chain = device_data[1] & 0b00000111
|
|
26
26
|
|
|
27
27
|
return {
|
|
28
|
-
"calibration": bool(data[1] & 0b01000000) if data else None,
|
|
28
|
+
"calibration": bool(data[1] & 0b01000000) if data and len(data) >= 2 else None,
|
|
29
29
|
"battery": battery_data & 0b01111111 if battery_data is not None else None,
|
|
30
30
|
"inMotion": _in_motion,
|
|
31
31
|
"position": (100 - _position) if reverse else _position,
|
|
@@ -11,10 +11,13 @@ _STANDING_FAN_MODE_MAP: dict[int, str] = {
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _parse_fan(
|
|
14
|
-
mfr_data: bytes | None,
|
|
14
|
+
mfr_data: bytes | None,
|
|
15
|
+
mode_map: dict[int, str],
|
|
16
|
+
*,
|
|
17
|
+
with_charging: bool = False,
|
|
15
18
|
) -> dict[str, bool | int | str | None]:
|
|
16
19
|
"""Shared fan advertisement parse, parameterized on the mode map."""
|
|
17
|
-
if mfr_data is None:
|
|
20
|
+
if mfr_data is None or len(mfr_data) < 10:
|
|
18
21
|
return {}
|
|
19
22
|
|
|
20
23
|
device_data = mfr_data[6:]
|
|
@@ -28,7 +31,7 @@ def _parse_fan(
|
|
|
28
31
|
_battery = device_data[2] & 0b01111111
|
|
29
32
|
_speed = device_data[3] & 0b01111111
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
result: dict[str, bool | int | str | None] = {
|
|
32
35
|
"sequence_number": _seq_num,
|
|
33
36
|
"isOn": _isOn,
|
|
34
37
|
"mode": mode_map.get(_mode),
|
|
@@ -39,6 +42,10 @@ def _parse_fan(
|
|
|
39
42
|
"battery": _battery,
|
|
40
43
|
"speed": _speed,
|
|
41
44
|
}
|
|
45
|
+
if with_charging:
|
|
46
|
+
# Bit 7 of the battery byte is the charging flag (Standing Fan only).
|
|
47
|
+
result["charging"] = bool(device_data[2] & 0b10000000)
|
|
48
|
+
return result
|
|
42
49
|
|
|
43
50
|
|
|
44
51
|
def process_fan(
|
|
@@ -52,4 +59,4 @@ def process_standing_fan(
|
|
|
52
59
|
data: bytes | None, mfr_data: bytes | None
|
|
53
60
|
) -> dict[str, bool | int | str | None]:
|
|
54
61
|
"""Process Standing Fan services data (modes 1-5; adds CUSTOM_NATURAL)."""
|
|
55
|
-
return _parse_fan(mfr_data, _STANDING_FAN_MODE_MAP)
|
|
62
|
+
return _parse_fan(mfr_data, _STANDING_FAN_MODE_MAP, with_charging=True)
|
|
@@ -12,7 +12,7 @@ def process_wohub2(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]
|
|
|
12
12
|
"""Process woHub2 sensor manufacturer data."""
|
|
13
13
|
temp_data = None
|
|
14
14
|
|
|
15
|
-
if mfr_data:
|
|
15
|
+
if mfr_data and len(mfr_data) >= 16:
|
|
16
16
|
status = mfr_data[12]
|
|
17
17
|
temp_data = mfr_data[13:16]
|
|
18
18
|
|
|
@@ -10,7 +10,7 @@ from ..helpers import celsius_to_fahrenheit
|
|
|
10
10
|
|
|
11
11
|
def process_hub3(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]:
|
|
12
12
|
"""Process hub3 sensor manufacturer data."""
|
|
13
|
-
if mfr_data is None:
|
|
13
|
+
if mfr_data is None or len(mfr_data) < 17:
|
|
14
14
|
return {}
|
|
15
15
|
device_data = mfr_data[6:]
|
|
16
16
|
|
|
@@ -46,7 +46,7 @@ def process_wohumidifier(
|
|
|
46
46
|
data: bytes | None, mfr_data: bytes | None
|
|
47
47
|
) -> dict[str, bool | int]:
|
|
48
48
|
"""Process WoHumi services data."""
|
|
49
|
-
if data is None:
|
|
49
|
+
if data is None or len(data) < 5:
|
|
50
50
|
return {
|
|
51
51
|
"isOn": None,
|
|
52
52
|
"level": None,
|
|
@@ -64,7 +64,7 @@ def process_evaporative_humidifier(
|
|
|
64
64
|
data: bytes | None, mfr_data: bytes | None
|
|
65
65
|
) -> dict[str, bool | int]:
|
|
66
66
|
"""Process WoHumi services data."""
|
|
67
|
-
if mfr_data is None:
|
|
67
|
+
if mfr_data is None or len(mfr_data) < 17:
|
|
68
68
|
return {}
|
|
69
69
|
|
|
70
70
|
seq_number = mfr_data[6]
|
|
@@ -12,7 +12,7 @@ def process_wokeypad(
|
|
|
12
12
|
mfr_data: bytes | None,
|
|
13
13
|
) -> dict[str, bool | int | None]:
|
|
14
14
|
"""Process woKeypad services data."""
|
|
15
|
-
if data is None or mfr_data is None:
|
|
15
|
+
if data is None or mfr_data is None or len(data) < 3 or len(mfr_data) < 7:
|
|
16
16
|
return {"battery": None, "attempt_state": None}
|
|
17
17
|
|
|
18
18
|
_LOGGER.debug("mfr_data: %s", mfr_data.hex())
|
|
@@ -7,7 +7,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
7
7
|
|
|
8
8
|
def process_common_mfr_data(mfr_data: bytes | None) -> dict[str, bool | int]:
|
|
9
9
|
"""Process common Keypad Vision (Pro) manufacturer data."""
|
|
10
|
-
if mfr_data is None:
|
|
10
|
+
if mfr_data is None or len(mfr_data) < 13:
|
|
11
11
|
return {}
|
|
12
12
|
|
|
13
13
|
sequence_number = mfr_data[6]
|
|
@@ -41,7 +41,7 @@ def process_keypad_vision(
|
|
|
41
41
|
"""Process Keypad Vision data."""
|
|
42
42
|
result = process_common_mfr_data(mfr_data)
|
|
43
43
|
|
|
44
|
-
if not result:
|
|
44
|
+
if not result or len(mfr_data) < 14:
|
|
45
45
|
return {}
|
|
46
46
|
|
|
47
47
|
pir_triggered_level = mfr_data[13] & 0x03
|
|
@@ -63,7 +63,7 @@ def process_keypad_vision_pro(
|
|
|
63
63
|
"""Process Keypad Vision Pro data."""
|
|
64
64
|
result = process_common_mfr_data(mfr_data)
|
|
65
65
|
|
|
66
|
-
if not result:
|
|
66
|
+
if not result or len(mfr_data) < 14:
|
|
67
67
|
return {}
|
|
68
68
|
|
|
69
69
|
radar_triggered_level = mfr_data[13] & 0x03
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
def process_leak(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool | int]:
|
|
5
5
|
"""Process SwitchBot Water Leak Detector advertisement data."""
|
|
6
|
-
if
|
|
6
|
+
if mfr_data is None or len(mfr_data) < 9:
|
|
7
7
|
return {}
|
|
8
8
|
|
|
9
9
|
water_leak_detected = None
|
|
@@ -9,7 +9,7 @@ def process_wostrip(
|
|
|
9
9
|
data: bytes | None, mfr_data: bytes | None
|
|
10
10
|
) -> dict[str, bool | int]:
|
|
11
11
|
"""Process WoStrip services data."""
|
|
12
|
-
if mfr_data is None:
|
|
12
|
+
if mfr_data is None or len(mfr_data) < 9:
|
|
13
13
|
return {}
|
|
14
14
|
return {
|
|
15
15
|
"sequence_number": mfr_data[6],
|
|
@@ -25,7 +25,7 @@ def process_candle_warmer_lamp(
|
|
|
25
25
|
data: bytes | None, mfr_data: bytes | None
|
|
26
26
|
) -> dict[str, bool | int]:
|
|
27
27
|
"""Process Candle Warmer Lamp services data."""
|
|
28
|
-
if mfr_data is None:
|
|
28
|
+
if mfr_data is None or len(mfr_data) < 9:
|
|
29
29
|
return {}
|
|
30
30
|
return {
|
|
31
31
|
"sequence_number": mfr_data[6],
|
|
@@ -41,7 +41,7 @@ def process_light(
|
|
|
41
41
|
) -> dict[str, bool | int]:
|
|
42
42
|
"""Support for strip light 3 and floor lamp."""
|
|
43
43
|
common_data = process_wostrip(data, mfr_data)
|
|
44
|
-
if not common_data:
|
|
44
|
+
if not common_data or len(mfr_data) < cw_offset + 2:
|
|
45
45
|
return {}
|
|
46
46
|
|
|
47
47
|
light_data = {"cw": _UNPACK_UINT16_BE(mfr_data, cw_offset)[0]}
|
|
@@ -54,3 +54,16 @@ def process_rgbic_light(
|
|
|
54
54
|
) -> dict[str, bool | int]:
|
|
55
55
|
"""Support for RGBIC lights."""
|
|
56
56
|
return process_light(data, mfr_data, cw_offset=10)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def process_rgbicww_ceiling_light(
|
|
60
|
+
data: bytes | None, mfr_data: bytes | None
|
|
61
|
+
) -> dict[str, bool | int]:
|
|
62
|
+
"""Support for RGBICWW Ceiling Light (white + color sub-lights)."""
|
|
63
|
+
common_data = process_light(data, mfr_data, cw_offset=10)
|
|
64
|
+
if not common_data or len(mfr_data) < 14:
|
|
65
|
+
return {}
|
|
66
|
+
return common_data | {
|
|
67
|
+
"main_isOn": bool(mfr_data[13] & 0b10000000),
|
|
68
|
+
"main_brightness": mfr_data[13] & 0b01111111,
|
|
69
|
+
}
|