PySwitchbot 2.2.0__tar.gz → 2.4.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.4.0}/PKG-INFO +46 -22
- pyswitchbot-2.2.0/PKG-INFO → pyswitchbot-2.4.0/README.md +27 -35
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/pyproject.toml +73 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/__init__.py +4 -5
- pyswitchbot-2.4.0/switchbot/__version__.py +1 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parser.py +25 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/air_purifier.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/art_frame.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/blind_tilt.py +2 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/bot.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/bulb.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/ceiling_light.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/climate_panel.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/contact.py +8 -3
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/curtain.py +3 -3
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/fan.py +11 -4
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/hub2.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/hub3.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/hubmini_matter.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/humidifier.py +2 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/keypad.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/keypad_vision.py +3 -3
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/leak.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/light_strip.py +16 -3
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/lock.py +4 -4
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/meter.py +10 -4
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/motion.py +2 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/plug.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/presence_sensor.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/remote.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/roller_shade.py +2 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/vacuum.py +2 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/const/__init__.py +2 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/const/light.py +11 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/ceiling_light.py +1 -1
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/device.py +4 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/fan.py +87 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/light_strip.py +138 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/meter_pro.py +6 -2
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/relay_switch.py +24 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/smart_thermostat_radiator.py +12 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/discovery.py +2 -4
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.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.4.0}/LICENSE +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/_sensor_th.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/adv_parsers/weather_station.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/const/air_purifier.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/const/climate.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/const/fan.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/const/hub3.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/const/presence_sensor.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/air_purifier.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/art_frame.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/keypad_vision.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/devices/vacuum.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/enum.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-2.2.0 → pyswitchbot-2.4.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.4.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.4.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.5"
|
|
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,10 +57,11 @@ 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
|
|
63
|
-
from .devices.meter_pro import SwitchbotMeterProCO2
|
|
64
|
+
from .devices.meter_pro import SwitchbotMeterPro, SwitchbotMeterProCO2
|
|
64
65
|
from .devices.plug import SwitchbotPlugMini
|
|
65
66
|
from .devices.relay_switch import (
|
|
66
67
|
SwitchbotGarageDoorOpener,
|
|
@@ -94,7 +95,6 @@ __all__ = [
|
|
|
94
95
|
"StripLightColorMode",
|
|
95
96
|
"SwitchBotAdvertisement",
|
|
96
97
|
"Switchbot",
|
|
97
|
-
"Switchbot",
|
|
98
98
|
"SwitchbotAccountConnectionError",
|
|
99
99
|
"SwitchbotAirPurifier",
|
|
100
100
|
"SwitchbotApiError",
|
|
@@ -115,23 +115,22 @@ __all__ = [
|
|
|
115
115
|
"SwitchbotKeypadVision",
|
|
116
116
|
"SwitchbotLightStrip",
|
|
117
117
|
"SwitchbotLock",
|
|
118
|
+
"SwitchbotMeterPro",
|
|
118
119
|
"SwitchbotMeterProCO2",
|
|
119
120
|
"SwitchbotModel",
|
|
120
|
-
"SwitchbotModel",
|
|
121
121
|
"SwitchbotOperationError",
|
|
122
122
|
"SwitchbotPermanentOutdoorLight",
|
|
123
123
|
"SwitchbotPlugMini",
|
|
124
|
-
"SwitchbotPlugMini",
|
|
125
124
|
"SwitchbotRelaySwitch",
|
|
126
125
|
"SwitchbotRelaySwitch2PM",
|
|
127
126
|
"SwitchbotRgbicLight",
|
|
128
127
|
"SwitchbotRgbicNeonLight",
|
|
128
|
+
"SwitchbotRgbicwwCeilingLight",
|
|
129
129
|
"SwitchbotRollerShade",
|
|
130
130
|
"SwitchbotSmartThermostatRadiator",
|
|
131
131
|
"SwitchbotStandingFan",
|
|
132
132
|
"SwitchbotStripLight3",
|
|
133
133
|
"SwitchbotSupportedType",
|
|
134
|
-
"SwitchbotSupportedType",
|
|
135
134
|
"SwitchbotVacuum",
|
|
136
135
|
"VerticalOscillationAngle",
|
|
137
136
|
"close_stale_connections",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.4.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,30 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
|
|
|
677
678
|
"func": process_rgbic_light,
|
|
678
679
|
"manufacturer_id": 2409,
|
|
679
680
|
},
|
|
681
|
+
b"\x00\x11\xbe\xf8": {
|
|
682
|
+
"modelName": SwitchbotModel.RGBICWW_LIGHT_BARS,
|
|
683
|
+
"modelFriendlyName": "RGBICWW Light Bars",
|
|
684
|
+
"func": process_rgbic_light,
|
|
685
|
+
"manufacturer_id": 2409,
|
|
686
|
+
},
|
|
687
|
+
b"\x01\x11\xbe\xf8": {
|
|
688
|
+
"modelName": SwitchbotModel.RGBICWW_LIGHT_BARS,
|
|
689
|
+
"modelFriendlyName": "RGBICWW Light Bars",
|
|
690
|
+
"func": process_rgbic_light,
|
|
691
|
+
"manufacturer_id": 2409,
|
|
692
|
+
},
|
|
693
|
+
b"\x00\x11\xbb\x10": {
|
|
694
|
+
"modelName": SwitchbotModel.RGBICWW_CEILING_LIGHT,
|
|
695
|
+
"modelFriendlyName": "RGBICWW Ceiling Light",
|
|
696
|
+
"func": process_rgbicww_ceiling_light,
|
|
697
|
+
"manufacturer_id": 2409,
|
|
698
|
+
},
|
|
699
|
+
b"\x01\x11\xbb\x10": {
|
|
700
|
+
"modelName": SwitchbotModel.RGBICWW_CEILING_LIGHT,
|
|
701
|
+
"modelFriendlyName": "RGBICWW Ceiling Light",
|
|
702
|
+
"func": process_rgbicww_ceiling_light,
|
|
703
|
+
"manufacturer_id": 2409,
|
|
704
|
+
},
|
|
680
705
|
b"\x00\x10\xd0\xb7": {
|
|
681
706
|
"modelName": SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
|
|
682
707
|
"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
|