PySwitchbot 2.2.0__py3-none-any.whl → 2.3.0__py3-none-any.whl
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.dist-info → pyswitchbot-2.3.0.dist-info}/METADATA +46 -22
- pyswitchbot-2.3.0.dist-info/RECORD +81 -0
- {pyswitchbot-2.2.0.dist-info → pyswitchbot-2.3.0.dist-info}/WHEEL +1 -2
- switchbot/__init__.py +2 -0
- switchbot/__version__.py +1 -0
- switchbot/adv_parser.py +13 -0
- switchbot/adv_parsers/air_purifier.py +1 -1
- switchbot/adv_parsers/art_frame.py +1 -1
- switchbot/adv_parsers/blind_tilt.py +2 -2
- switchbot/adv_parsers/bot.py +1 -1
- switchbot/adv_parsers/bulb.py +1 -1
- switchbot/adv_parsers/ceiling_light.py +1 -1
- switchbot/adv_parsers/climate_panel.py +1 -1
- switchbot/adv_parsers/contact.py +8 -3
- switchbot/adv_parsers/curtain.py +3 -3
- switchbot/adv_parsers/fan.py +11 -4
- switchbot/adv_parsers/hub2.py +1 -1
- switchbot/adv_parsers/hub3.py +1 -1
- switchbot/adv_parsers/hubmini_matter.py +1 -1
- switchbot/adv_parsers/humidifier.py +2 -2
- switchbot/adv_parsers/keypad.py +1 -1
- switchbot/adv_parsers/keypad_vision.py +3 -3
- switchbot/adv_parsers/leak.py +1 -1
- switchbot/adv_parsers/light_strip.py +16 -3
- switchbot/adv_parsers/lock.py +4 -4
- switchbot/adv_parsers/meter.py +10 -4
- switchbot/adv_parsers/motion.py +2 -1
- switchbot/adv_parsers/plug.py +1 -1
- switchbot/adv_parsers/presence_sensor.py +1 -1
- switchbot/adv_parsers/remote.py +1 -1
- switchbot/adv_parsers/roller_shade.py +2 -2
- switchbot/adv_parsers/smart_thermostat_radiator.py +1 -1
- switchbot/adv_parsers/vacuum.py +2 -2
- switchbot/const/__init__.py +1 -0
- switchbot/const/light.py +11 -0
- switchbot/devices/device.py +3 -2
- switchbot/devices/fan.py +82 -1
- switchbot/devices/light_strip.py +138 -2
- switchbot/devices/relay_switch.py +24 -0
- switchbot/devices/smart_thermostat_radiator.py +12 -0
- switchbot/discovery.py +2 -4
- switchbot/utils.py +1 -1
- pyswitchbot-2.2.0.dist-info/RECORD +0 -81
- pyswitchbot-2.2.0.dist-info/top_level.txt +0 -1
- {pyswitchbot-2.2.0.dist-info → pyswitchbot-2.3.0.dist-info}/licenses/LICENSE +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
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
switchbot/__init__.py,sha256=MVFn7-FcK6rEqNk6SVGSMf2bbbSxC_spupxO8d6v-Wk,4156
|
|
2
|
+
switchbot/__version__.py,sha256=CpK8IH_dCUAwg9tqv7zm9FxbBFkxCnED1JUiRe7cftU,22
|
|
3
|
+
switchbot/adv_parser.py,sha256=k4Mpoj7bOlstBllTZJ4ILw5fbWKncvsj8sLDZLSr18M,33635
|
|
4
|
+
switchbot/adv_parsers/__init__.py,sha256=d39GEAICA1RrQiLy1ks49x0d9a-A_yKn9Awkj5za8Uk,46
|
|
5
|
+
switchbot/adv_parsers/_sensor_th.py,sha256=ze182l3_YFHJJv98_vq6qB2twlUTExkxoC1sLZL3TLs,1199
|
|
6
|
+
switchbot/adv_parsers/air_purifier.py,sha256=MU24S-lGNR4kqW-olodqcleHNnkOa5bd7Vj4wYPEymQ,1535
|
|
7
|
+
switchbot/adv_parsers/art_frame.py,sha256=ZSLEAcwq2ZHeneAAouw2KvZqlKDXYtf6iDVuqE86Z2M,981
|
|
8
|
+
switchbot/adv_parsers/blind_tilt.py,sha256=qtDALLJXhfCorpQX2vt-nLTaEg-1bcNsqzLO3w03fHY,868
|
|
9
|
+
switchbot/adv_parsers/bot.py,sha256=BcdXkTlkKohR4LJiVidqaJlATjLr1VeeMKdQ0i1eDW0,654
|
|
10
|
+
switchbot/adv_parsers/bulb.py,sha256=rYRARx8lSo3rLsBcpWJWCem2Kuib5wohvJIkGbEdLIE,655
|
|
11
|
+
switchbot/adv_parsers/ceiling_light.py,sha256=-3p7nbfWpse19nsnl9VQG56V4ZdJmJDPrj_4diCDz4A,690
|
|
12
|
+
switchbot/adv_parsers/climate_panel.py,sha256=egXGTB1t554N4RaO9bhU0LCzTCNKKemoeAa2NrxkwRE,1279
|
|
13
|
+
switchbot/adv_parsers/contact.py,sha256=cQgQuOAVRtc3vvjTaOAOA74VguC3nQYeyc0CufnfENo,1475
|
|
14
|
+
switchbot/adv_parsers/curtain.py,sha256=DNFnrF_uG4YPr7b7pi430ZKOrpalrQ2uwwFpl1BjCqY,1239
|
|
15
|
+
switchbot/adv_parsers/fan.py,sha256=GiKBp8hIL1j6bzZkfsERRP9Bm2uTlnYK-8piRU38Kuc,2102
|
|
16
|
+
switchbot/adv_parsers/hub2.py,sha256=79_onOxjPrVGFWK5d2XV-LtxMUFSQsMzcgaXk96T9a4,1587
|
|
17
|
+
switchbot/adv_parsers/hub3.py,sha256=-PJacqBLazwnxPUt6luK6Dvs-sf7oJ5vax0TwTFA0tA,1905
|
|
18
|
+
switchbot/adv_parsers/hubmini_matter.py,sha256=kzmb8ru-Yv537qGo53jS-vLe4Hjzb7mlmyt8oU7-MmQ,957
|
|
19
|
+
switchbot/adv_parsers/humidifier.py,sha256=6CYQSEjZ4Oq2ybgT8ELa0bUqOzcTD1pBzunuGWABzjs,3109
|
|
20
|
+
switchbot/adv_parsers/keypad.py,sha256=KpYn1orBaqoFTEVUoy1qk6o2FFHgP3KbeX0Dj2o0qgc,586
|
|
21
|
+
switchbot/adv_parsers/keypad_vision.py,sha256=oLfLceU5a6XurrRhbY6uJTX7H8v19bVq_BN_hZvCHJ8,2359
|
|
22
|
+
switchbot/adv_parsers/leak.py,sha256=UQz98zc5c4k5-wOjlNsq5cekHZh6rUQh_FwG8v1ozfg,885
|
|
23
|
+
switchbot/adv_parsers/light_strip.py,sha256=uBMyP0RuMwWZ3DmafTLFGLE7W3dV-UOxNVNjntAt1i8,2150
|
|
24
|
+
switchbot/adv_parsers/lock.py,sha256=UgAvADKm5ygVQBUtVoogh04LaDbT-zwIWMGJlr1bvlc,3386
|
|
25
|
+
switchbot/adv_parsers/meter.py,sha256=OydW8tK8hGV9cjabfypwYfQDWP0PbPjgwVzv2_wa2A4,1325
|
|
26
|
+
switchbot/adv_parsers/motion.py,sha256=v9SEkAWk1VKkMZkdNsa3FlAOVpxEXIBXiEdLzEBsJZs,1263
|
|
27
|
+
switchbot/adv_parsers/plug.py,sha256=hvF0E_Zh7ALS7MRNFu_69ha2TEXzctw3LYPSK8AniDg,509
|
|
28
|
+
switchbot/adv_parsers/presence_sensor.py,sha256=-shJBuB7sf7IVgsEOZvB5O8EXso8rSaXCw2k8UwmeVs,1353
|
|
29
|
+
switchbot/adv_parsers/relay_switch.py,sha256=akP8XTWcu_EmVUfyMQb1wr9xHR0FEuksRR6eGqsp3EI,1836
|
|
30
|
+
switchbot/adv_parsers/remote.py,sha256=21RnWzsb5Z3giJdiFQxUxTlkQ0W5oK6-EUTXaMYaybo,450
|
|
31
|
+
switchbot/adv_parsers/roller_shade.py,sha256=Y1zl5NtUrNeGBweytK0FvPOrwiSEIjh4e2xhEHEcLEY,973
|
|
32
|
+
switchbot/adv_parsers/smart_thermostat_radiator.py,sha256=feaYZL8sGfYDLXpKlIc71VgKTVQWYKUqSXdlOiYZ2V4,1795
|
|
33
|
+
switchbot/adv_parsers/vacuum.py,sha256=3ncf6BxE3UeC5SIIoiujYFdhDLyDjLfqjRwVqv-6Zo0,2108
|
|
34
|
+
switchbot/adv_parsers/weather_station.py,sha256=HOCZ-gef7e6HrOdfzcIx12rV9XURwoCetUQuShyk1R4,1155
|
|
35
|
+
switchbot/api_config.py,sha256=WSsoKkgNORkbqBZDH0U0rsEwc-etpBDTCXLSJnChsPA,263
|
|
36
|
+
switchbot/const/__init__.py,sha256=jeyWrsG85C2DO5i4m7W_xUnGREOmWP-43oW26pCTCEQ,4433
|
|
37
|
+
switchbot/const/air_purifier.py,sha256=eKMMgArZuANoDAWUKXWu0X7DPVR6di2T7i4SfO4Kddk,379
|
|
38
|
+
switchbot/const/climate.py,sha256=4Kuz1nopygRs734KAyC_IgEXmPpRLgeE9JJgyJBclQA,902
|
|
39
|
+
switchbot/const/evaporative_humidifier.py,sha256=Klf-jlGovrDr6DreXEkPsQnYr6Gb9U02EtB0dM7u8Z0,831
|
|
40
|
+
switchbot/const/fan.py,sha256=PbKmjNo4NHA_q-Tto3JBcNr71lzqQEWqk1zx_olN64Y,1250
|
|
41
|
+
switchbot/const/hub2.py,sha256=K1nh-FMn0wxjk3s7kPir_1NMcuR0eNcmVXCiDX2t5xM,445
|
|
42
|
+
switchbot/const/hub3.py,sha256=UQwk_NVimMd1aqS1LOkLrL7PTySZzcr0WYnpk3EFPFk,308
|
|
43
|
+
switchbot/const/light.py,sha256=DsCua_IYYvGy23GmviSB9GkwATPGVYIe6VNF7rZt97U,817
|
|
44
|
+
switchbot/const/lock.py,sha256=wKvq5uctsKpcM6gSzxPrcvmezzRhPZPd5n948I8R4n0,342
|
|
45
|
+
switchbot/const/presence_sensor.py,sha256=X2oCfl0rbdd1-dSrqQFn3NDtEBXbti-87DbWq7NyN3A,135
|
|
46
|
+
switchbot/devices/__init__.py,sha256=_fyzMy8qWC_2Y1BJ4hggrCP1udNqSoU6wFmfOiYpEl0,32
|
|
47
|
+
switchbot/devices/air_purifier.py,sha256=JwT5HHQLvkDdFrDEKFT8sA41bM2G_DGfuOpf-74Smyw,10620
|
|
48
|
+
switchbot/devices/art_frame.py,sha256=JUb7s9ZgnPxfc3K-shxuWItiMorelzldew53PwQHr4A,4361
|
|
49
|
+
switchbot/devices/base_cover.py,sha256=UWNA7eGbJdY80Mu-k41xO4tnyk7FhpYaWRWihGvDM2g,4385
|
|
50
|
+
switchbot/devices/base_light.py,sha256=WWJJdRHfvoSYMRHDSv_RCmo5D6hYoZRxQVYfsOXZ7K0,6590
|
|
51
|
+
switchbot/devices/blind_tilt.py,sha256=tY0JoZhyqU_6PlhjtnIaEeIG3Z-JdZKJ5Jwg_5b6xSQ,5761
|
|
52
|
+
switchbot/devices/bot.py,sha256=Co2IiQa2si0WPs3HFize2CPpj5GVbMT570vMP5YWpaQ,4121
|
|
53
|
+
switchbot/devices/bulb.py,sha256=ISxXck8JsBlFojLB8M5Wfip_z4o6_-VvyFbBvqK6_Bg,2313
|
|
54
|
+
switchbot/devices/ceiling_light.py,sha256=45UsGj_gPQv8tJO40UtkzOBYvE-PZovGEgOYlMGaacc,2645
|
|
55
|
+
switchbot/devices/contact.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
56
|
+
switchbot/devices/curtain.py,sha256=p3qcPWHaSA5ewPONl811hL7swFRca37Omb2LKYoAxgo,7086
|
|
57
|
+
switchbot/devices/device.py,sha256=k0owlB-_bslNp5yJv4swm-HJ4_YlvKLnfxJCLTnoaIA,49533
|
|
58
|
+
switchbot/devices/evaporative_humidifier.py,sha256=Dmpg4oCp02iPBz6gc7z5nobio5rDyLoSLP3Oil6sRJ4,8279
|
|
59
|
+
switchbot/devices/fan.py,sha256=t1PeCF91o1p5FSMXnQYOvZX38_CGsyPaOPguExkDORc,12879
|
|
60
|
+
switchbot/devices/humidifier.py,sha256=oqtRFMSA5ZSaKMEisg1gwHwWQeWBcEt21N5Qg-fqeTc,3433
|
|
61
|
+
switchbot/devices/keypad.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
62
|
+
switchbot/devices/keypad_vision.py,sha256=hhkbzT_uWqGy07pcReNlfAjcU5r27O4HI45xBiPAi1M,5367
|
|
63
|
+
switchbot/devices/light_strip.py,sha256=Wgq4tgsZ8RXnX6SBUetDuV58kYQHrZ5BsSq6YkswNJQ,16271
|
|
64
|
+
switchbot/devices/lock.py,sha256=GNytU2mBoDFIQxHPVS_XEcydhUt2HHa_rdIHLUclGLM,11878
|
|
65
|
+
switchbot/devices/meter.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
66
|
+
switchbot/devices/meter_pro.py,sha256=tUprQ_9MEhJCFGBu1UFrQzndaM5yVFbuQBItboNrULA,6996
|
|
67
|
+
switchbot/devices/motion.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
68
|
+
switchbot/devices/plug.py,sha256=RqOppC3KmjROxukHFLNttEhDg7-hH_IaiTCfXWXwF_k,1397
|
|
69
|
+
switchbot/devices/relay_switch.py,sha256=FH5pAskYIpsclmKXBaSHsjXzyGtsn3WxEt_GZU6FvFU,10525
|
|
70
|
+
switchbot/devices/roller_shade.py,sha256=Lv3DDUgnlxArJY6qQc9HXxJwd7NcPEYmQvWnWn_yWdE,6431
|
|
71
|
+
switchbot/devices/smart_thermostat_radiator.py,sha256=iSdPfns5bsRXs_iYUGmdOSH2NhirCb12p0Zv7OfV71U,6955
|
|
72
|
+
switchbot/devices/vacuum.py,sha256=qzeEaXPNIs8B-FyH7NLnAqXSxd5dB7ioq6tSiajF-LM,2287
|
|
73
|
+
switchbot/discovery.py,sha256=5GZrKkQe1JfDCitM_74YDljUnBGYwjErAZFdT4v969I,6364
|
|
74
|
+
switchbot/enum.py,sha256=0IfWypBw2oe9lWgdRkrD_1FqqeHpakCxtwlSaUlmcPg,125
|
|
75
|
+
switchbot/helpers.py,sha256=DJLvYZ4Dq0dkiGMOzfR--nSTM_8jE9QtB7t570PuGSU,2091
|
|
76
|
+
switchbot/models.py,sha256=xibQOVGHC60FWWc54N6BW46vxFuJUjj6tK1Qw0_eTbI,372
|
|
77
|
+
switchbot/utils.py,sha256=glU1SDBeoXuu9K46nSGvg1YVWGNAGC0bo13tJ-ccDNU,724
|
|
78
|
+
pyswitchbot-2.3.0.dist-info/METADATA,sha256=WDn-qiFAGbd4RlLsSB2AF491TCsh1NRQBn9lwLfSHjY,5794
|
|
79
|
+
pyswitchbot-2.3.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
|
|
80
|
+
pyswitchbot-2.3.0.dist-info/licenses/LICENSE,sha256=_hdUCmoRDzki4fYDJfn-5KpG7-7p6YJ6IsbMfrTXN6Y,1078
|
|
81
|
+
pyswitchbot-2.3.0.dist-info/RECORD,,
|
switchbot/__init__.py
CHANGED
|
@@ -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",
|
switchbot/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.3.0"
|
switchbot/adv_parser.py
CHANGED
|
@@ -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,
|
switchbot/adv_parsers/bot.py
CHANGED
switchbot/adv_parsers/bulb.py
CHANGED
|
@@ -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],
|
switchbot/adv_parsers/contact.py
CHANGED
|
@@ -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)
|
switchbot/adv_parsers/curtain.py
CHANGED
|
@@ -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,
|
switchbot/adv_parsers/fan.py
CHANGED
|
@@ -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)
|
switchbot/adv_parsers/hub2.py
CHANGED
|
@@ -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
|
|
switchbot/adv_parsers/hub3.py
CHANGED
|
@@ -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]
|
switchbot/adv_parsers/keypad.py
CHANGED
|
@@ -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
|
switchbot/adv_parsers/leak.py
CHANGED
|
@@ -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
|
+
}
|
switchbot/adv_parsers/lock.py
CHANGED
|
@@ -12,7 +12,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
12
12
|
def process_wolock(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool | int]:
|
|
13
13
|
"""Support for lock and lock lite process data."""
|
|
14
14
|
common_data = process_locklite(data, mfr_data)
|
|
15
|
-
if not common_data:
|
|
15
|
+
if not common_data or len(mfr_data) < 9:
|
|
16
16
|
return {}
|
|
17
17
|
|
|
18
18
|
common_data["door_open"] = bool(mfr_data[7] & 0b00000100)
|
|
@@ -26,7 +26,7 @@ def process_locklite(
|
|
|
26
26
|
data: bytes | None, mfr_data: bytes | None
|
|
27
27
|
) -> dict[str, bool | int]:
|
|
28
28
|
"""Support for lock lite process data."""
|
|
29
|
-
if mfr_data is None:
|
|
29
|
+
if mfr_data is None or len(mfr_data) < 9:
|
|
30
30
|
return {}
|
|
31
31
|
|
|
32
32
|
_LOGGER.debug("mfr_data: %s", mfr_data.hex())
|
|
@@ -35,7 +35,7 @@ def process_locklite(
|
|
|
35
35
|
|
|
36
36
|
return {
|
|
37
37
|
"sequence_number": mfr_data[6],
|
|
38
|
-
"battery": data[2] & 0b01111111 if data else None,
|
|
38
|
+
"battery": data[2] & 0b01111111 if data and len(data) >= 3 else None,
|
|
39
39
|
"calibration": bool(mfr_data[7] & 0b10000000),
|
|
40
40
|
"status": LockStatus((mfr_data[7] & 0b01110000) >> 4),
|
|
41
41
|
"update_from_secondary_lock": bool(mfr_data[7] & 0b00001000),
|
|
@@ -46,7 +46,7 @@ def process_locklite(
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def parse_common_data(mfr_data: bytes | None) -> dict[str, bool | int]:
|
|
49
|
-
if mfr_data is None:
|
|
49
|
+
if mfr_data is None or len(mfr_data) < 12:
|
|
50
50
|
return {}
|
|
51
51
|
|
|
52
52
|
_LOGGER.debug("mfr_data: %s", mfr_data.hex())
|
switchbot/adv_parsers/meter.py
CHANGED
|
@@ -9,17 +9,21 @@ from ._sensor_th import decode_temp_humidity
|
|
|
9
9
|
|
|
10
10
|
CO2_UNPACK = struct.Struct(">H").unpack_from
|
|
11
11
|
|
|
12
|
+
# Meter Pro CO2 sensor spec range is 400-9999 ppm. Higher values are
|
|
13
|
+
# transient parsing artifacts and surface as huge spikes downstream.
|
|
14
|
+
CO2_MAX_PPM = 9999
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
def process_wosensorth(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]:
|
|
14
18
|
"""Process woSensorTH/Temp sensor services data."""
|
|
15
19
|
temp_data: bytes | None = None
|
|
16
20
|
battery: int | None = None
|
|
17
21
|
|
|
18
|
-
if mfr_data:
|
|
22
|
+
if mfr_data and len(mfr_data) >= 11:
|
|
19
23
|
temp_data = mfr_data[8:11]
|
|
20
24
|
|
|
21
|
-
if data:
|
|
22
|
-
if not temp_data:
|
|
25
|
+
if data and len(data) >= 3:
|
|
26
|
+
if not temp_data and len(data) >= 6:
|
|
23
27
|
temp_data = data[3:6]
|
|
24
28
|
battery = data[2] & 0b01111111
|
|
25
29
|
|
|
@@ -34,5 +38,7 @@ def process_wosensorth_c(data: bytes | None, mfr_data: bytes | None) -> dict[str
|
|
|
34
38
|
_wosensorth_data = process_wosensorth(data, mfr_data)
|
|
35
39
|
if _wosensorth_data and mfr_data and len(mfr_data) >= 15:
|
|
36
40
|
co2_data = mfr_data[13:15]
|
|
37
|
-
|
|
41
|
+
co2 = CO2_UNPACK(co2_data)[0]
|
|
42
|
+
if co2 <= CO2_MAX_PPM:
|
|
43
|
+
_wosensorth_data["co2"] = co2
|
|
38
44
|
return _wosensorth_data
|
switchbot/adv_parsers/motion.py
CHANGED
|
@@ -16,8 +16,9 @@ def process_wopresence(
|
|
|
16
16
|
sense_distance = None
|
|
17
17
|
light_intensity = None
|
|
18
18
|
is_light = None
|
|
19
|
+
motion_detected = None
|
|
19
20
|
|
|
20
|
-
if data:
|
|
21
|
+
if data and len(data) >= 6:
|
|
21
22
|
tested = bool(data[1] & 0b10000000)
|
|
22
23
|
motion_detected = bool(data[1] & 0b01000000)
|
|
23
24
|
battery = data[2] & 0b01111111
|
switchbot/adv_parsers/plug.py
CHANGED
|
@@ -13,7 +13,7 @@ def process_presence_sensor(
|
|
|
13
13
|
data: bytes | None, mfr_data: bytes | None
|
|
14
14
|
) -> dict[str, bool | int | str]:
|
|
15
15
|
"""Process Presence Sensor data."""
|
|
16
|
-
if mfr_data is None:
|
|
16
|
+
if mfr_data is None or len(mfr_data) < 12:
|
|
17
17
|
return {}
|
|
18
18
|
|
|
19
19
|
seq_number = mfr_data[6]
|
switchbot/adv_parsers/remote.py
CHANGED
|
@@ -7,7 +7,7 @@ def process_worollershade(
|
|
|
7
7
|
data: bytes | None, mfr_data: bytes | None, reverse: bool = True
|
|
8
8
|
) -> dict[str, bool | int]:
|
|
9
9
|
"""Process woRollerShade 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:]
|
|
@@ -20,7 +20,7 @@ def process_worollershade(
|
|
|
20
20
|
|
|
21
21
|
return {
|
|
22
22
|
"calibration": _calibrated,
|
|
23
|
-
"battery": data[2] & 0b01111111 if data else None,
|
|
23
|
+
"battery": data[2] & 0b01111111 if data and len(data) >= 3 else None,
|
|
24
24
|
"inMotion": _in_motion,
|
|
25
25
|
"position": (100 - _position) if reverse else _position,
|
|
26
26
|
"lightLevel": _light_level,
|
|
@@ -11,7 +11,7 @@ def process_smart_thermostat_radiator(
|
|
|
11
11
|
data: bytes | None, mfr_data: bytes | None
|
|
12
12
|
) -> dict[str, bool | int | str]:
|
|
13
13
|
"""Process Smart Thermostat Radiator data."""
|
|
14
|
-
if mfr_data is None:
|
|
14
|
+
if mfr_data is None or len(mfr_data) < 13:
|
|
15
15
|
return {}
|
|
16
16
|
|
|
17
17
|
_seq_num = mfr_data[6]
|
switchbot/adv_parsers/vacuum.py
CHANGED
|
@@ -12,7 +12,7 @@ def process_vacuum(
|
|
|
12
12
|
data: bytes | None, mfr_data: bytes | None
|
|
13
13
|
) -> dict[str, bool | int | str]:
|
|
14
14
|
"""Support for s10, k10+ pro combo, k20 process service data."""
|
|
15
|
-
if mfr_data is None:
|
|
15
|
+
if mfr_data is None or len(mfr_data) < 14:
|
|
16
16
|
return {}
|
|
17
17
|
|
|
18
18
|
_seq_num = mfr_data[6]
|
|
@@ -48,7 +48,7 @@ def process_vacuum_k(
|
|
|
48
48
|
data: bytes | None, mfr_data: bytes | None
|
|
49
49
|
) -> dict[str, bool | int | str]:
|
|
50
50
|
"""Support for k10+, k10+ pro process service data."""
|
|
51
|
-
if mfr_data is None:
|
|
51
|
+
if mfr_data is None or len(mfr_data) < 9:
|
|
52
52
|
return {}
|
|
53
53
|
|
|
54
54
|
_seq_num = mfr_data[6]
|
switchbot/const/__init__.py
CHANGED
|
@@ -107,6 +107,7 @@ class SwitchbotModel(StrEnum):
|
|
|
107
107
|
PLUG_MINI_EU = "Plug Mini (EU)"
|
|
108
108
|
RGBICWW_STRIP_LIGHT = "RGBICWW Strip Light"
|
|
109
109
|
RGBICWW_FLOOR_LAMP = "RGBICWW Floor Lamp"
|
|
110
|
+
RGBICWW_CEILING_LIGHT = "RGBICWW Ceiling Light"
|
|
110
111
|
PERMANENT_OUTDOOR_LIGHT = "Permanent Outdoor Light"
|
|
111
112
|
RGBIC_NEON_ROPE_LIGHT = "RGBIC Neon Rope Light"
|
|
112
113
|
RGBIC_NEON_WIRE_ROPE_LIGHT = "RGBIC Neon Wire Rope Light"
|
switchbot/const/light.py
CHANGED
|
@@ -43,4 +43,15 @@ class RGBICStripLightColorMode(Enum):
|
|
|
43
43
|
UNKNOWN = 10
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
class RGBICWWCeilingLightColorMode(Enum):
|
|
47
|
+
SEGMENTED = 1
|
|
48
|
+
COLOR = 2
|
|
49
|
+
SCENE = 3
|
|
50
|
+
MUSIC = 4
|
|
51
|
+
CONTROLLER = 5
|
|
52
|
+
WARMWHITE = 6
|
|
53
|
+
EFFECT = 7
|
|
54
|
+
UNKNOWN = 10
|
|
55
|
+
|
|
56
|
+
|
|
46
57
|
DEFAULT_COLOR_TEMP = 4001
|
switchbot/devices/device.py
CHANGED
|
@@ -102,6 +102,7 @@ API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = {
|
|
|
102
102
|
"W1102001": SwitchbotModel.STRIP_LIGHT_3,
|
|
103
103
|
"W1102003": SwitchbotModel.RGBICWW_STRIP_LIGHT,
|
|
104
104
|
"W1102004": SwitchbotModel.RGBICWW_FLOOR_LAMP,
|
|
105
|
+
"W1162000": SwitchbotModel.RGBICWW_CEILING_LIGHT,
|
|
105
106
|
"W1104000": SwitchbotModel.PLUG_MINI_EU,
|
|
106
107
|
"W1128000": SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
|
|
107
108
|
"W1111000": SwitchbotModel.CLIMATE_PANEL,
|
|
@@ -850,8 +851,8 @@ class SwitchbotBaseDevice:
|
|
|
850
851
|
Returns true if data has changed and False if not.
|
|
851
852
|
"""
|
|
852
853
|
if not self._sb_adv_data:
|
|
853
|
-
_LOGGER.
|
|
854
|
-
return
|
|
854
|
+
_LOGGER.debug("%s: No advertisement data to update", self.name)
|
|
855
|
+
return False
|
|
855
856
|
old_data = self._sb_adv_data.data.get("data") or {}
|
|
856
857
|
merged_data = _merge_data(old_data, new_data)
|
|
857
858
|
if merged_data == old_data:
|
switchbot/devices/fan.py
CHANGED
|
@@ -37,6 +37,11 @@ COMMAND_START_OSCILLATION_ALL_AXES = f"{COMMAND_HEAD}02010101"
|
|
|
37
37
|
COMMAND_STOP_OSCILLATION_ALL_AXES = f"{COMMAND_HEAD}02010202"
|
|
38
38
|
COMMAND_SET_OSCILLATION_PARAMS = f"{COMMAND_HEAD}0202" # +angles
|
|
39
39
|
COMMAND_SET_NIGHT_LIGHT = f"{COMMAND_HEAD}0502" # +state
|
|
40
|
+
# Standing Fan (FAN2) extra controls.
|
|
41
|
+
COMMAND_SET_DISPLAY_LIGHT = f"{COMMAND_HEAD}0501" # +state + FFFF (front LED display)
|
|
42
|
+
COMMAND_SET_SOUND = f"{COMMAND_HEAD}0601" # +level (64 on / 00 off)
|
|
43
|
+
COMMAND_SET_AUTO_RECENTER = f"{COMMAND_HEAD}0205" # +both axes (0101 on / 0202 off)
|
|
44
|
+
COMMAND_SET_CHILD_LOCK = f"{COMMAND_HEAD}07" # +state (01 on / 02 off)
|
|
40
45
|
COMMAND_SET_MODE = {
|
|
41
46
|
FanMode.NORMAL.name.lower(): f"{COMMAND_HEAD}030101ff",
|
|
42
47
|
FanMode.NATURAL.name.lower(): f"{COMMAND_HEAD}030102ff",
|
|
@@ -69,6 +74,10 @@ class SwitchbotFan(SwitchbotSequenceDevice):
|
|
|
69
74
|
return None
|
|
70
75
|
|
|
71
76
|
_LOGGER.debug("data: %s", _data)
|
|
77
|
+
return self._parse_basic_info(_data, _data1)
|
|
78
|
+
|
|
79
|
+
def _parse_basic_info(self, _data: bytes, _data1: bytes) -> dict[str, Any]:
|
|
80
|
+
"""Decode the basic-info connection response into a state dict."""
|
|
72
81
|
battery = _data[2] & 0b01111111
|
|
73
82
|
isOn = bool(_data[3] & 0b10000000)
|
|
74
83
|
oscillating_horizontal = bool(_data[3] & 0b01000000)
|
|
@@ -186,6 +195,22 @@ class SwitchbotStandingFan(SwitchbotFan):
|
|
|
186
195
|
_command_start_oscillation: ClassVar[str] = COMMAND_START_OSCILLATION_ALL_AXES
|
|
187
196
|
_command_stop_oscillation: ClassVar[str] = COMMAND_STOP_OSCILLATION_ALL_AXES
|
|
188
197
|
|
|
198
|
+
def _parse_basic_info(self, _data: bytes, _data1: bytes) -> dict[str, Any]:
|
|
199
|
+
"""Add the Standing-Fan-only fields to the basic-info response."""
|
|
200
|
+
info = super()._parse_basic_info(_data, _data1)
|
|
201
|
+
# Sweep angle as the raw device byte: horizontal is the angle in degrees
|
|
202
|
+
# (30/60/90); vertical encodes 90 as 95 (see VerticalOscillationAngle).
|
|
203
|
+
info["oscillating_horizontal_angle"] = _data[4]
|
|
204
|
+
info["oscillating_vertical_angle"] = _data[6]
|
|
205
|
+
info["charging"] = bool(_data[2] & 0b10000000)
|
|
206
|
+
info["child_lock"] = bool(_data[3] & 0b00000001)
|
|
207
|
+
info["display"] = bool(_data[3] & 0b00000010)
|
|
208
|
+
# bit 4 = horizontal axis, bit 3 = vertical; the app toggles both at once.
|
|
209
|
+
info["auto_recenter"] = bool(_data[3] & 0b00011000)
|
|
210
|
+
if len(_data) > 10:
|
|
211
|
+
info["sound"] = bool(_data[10] & 0b01111111)
|
|
212
|
+
return info
|
|
213
|
+
|
|
189
214
|
@update_after_operation
|
|
190
215
|
async def set_horizontal_oscillation_angle(
|
|
191
216
|
self, angle: HorizontalOscillationAngle | int
|
|
@@ -204,7 +229,7 @@ class SwitchbotStandingFan(SwitchbotFan):
|
|
|
204
229
|
Set vertical oscillation angle (30 / 60 / 90 degrees).
|
|
205
230
|
|
|
206
231
|
The device uses a different byte encoding on the vertical axis than
|
|
207
|
-
on the horizontal one
|
|
232
|
+
on the horizontal one: 90° maps to byte 0x5F (95), not 0x5A (90),
|
|
208
233
|
which the firmware interprets as an axis halt. Use
|
|
209
234
|
`VerticalOscillationAngle` (or the raw byte values 30 / 60 / 95).
|
|
210
235
|
"""
|
|
@@ -221,6 +246,62 @@ class SwitchbotStandingFan(SwitchbotFan):
|
|
|
221
246
|
result = await self._send_command(cmd)
|
|
222
247
|
return self._check_command_result(result, 0, {1})
|
|
223
248
|
|
|
249
|
+
@update_after_operation
|
|
250
|
+
async def set_child_lock(self, enabled: bool) -> bool:
|
|
251
|
+
"""Enable or disable the child lock."""
|
|
252
|
+
cmd = f"{COMMAND_SET_CHILD_LOCK}{'01' if enabled else '02'}"
|
|
253
|
+
result = await self._send_command(cmd)
|
|
254
|
+
return self._check_command_result(result, 0, {1})
|
|
255
|
+
|
|
256
|
+
@update_after_operation
|
|
257
|
+
async def set_display(self, enabled: bool) -> bool:
|
|
258
|
+
"""Turn the front display (LED) on or off."""
|
|
259
|
+
cmd = f"{COMMAND_SET_DISPLAY_LIGHT}{'01' if enabled else '02'}FFFF"
|
|
260
|
+
result = await self._send_command(cmd)
|
|
261
|
+
return self._check_command_result(result, 0, {1})
|
|
262
|
+
|
|
263
|
+
@update_after_operation
|
|
264
|
+
async def set_sound(self, enabled: bool) -> bool:
|
|
265
|
+
"""Turn the key tone (buzzer) on or off."""
|
|
266
|
+
cmd = f"{COMMAND_SET_SOUND}{'64' if enabled else '00'}"
|
|
267
|
+
result = await self._send_command(cmd)
|
|
268
|
+
return self._check_command_result(result, 0, {1})
|
|
269
|
+
|
|
270
|
+
@update_after_operation
|
|
271
|
+
async def set_auto_recenter(self, enabled: bool) -> bool:
|
|
272
|
+
"""Enable or disable auto return-to-center on both axes."""
|
|
273
|
+
cmd = f"{COMMAND_SET_AUTO_RECENTER}{'0101' if enabled else '0202'}"
|
|
274
|
+
result = await self._send_command(cmd)
|
|
275
|
+
return self._check_command_result(result, 0, {1})
|
|
276
|
+
|
|
277
|
+
def get_horizontal_oscillation_angle(self) -> int | None:
|
|
278
|
+
"""Return cached horizontal oscillation angle (raw device byte)."""
|
|
279
|
+
return self._get_adv_value("oscillating_horizontal_angle")
|
|
280
|
+
|
|
281
|
+
def get_vertical_oscillation_angle(self) -> int | None:
|
|
282
|
+
"""Return cached vertical oscillation angle (raw device byte; 90° = 95)."""
|
|
283
|
+
return self._get_adv_value("oscillating_vertical_angle")
|
|
284
|
+
|
|
224
285
|
def get_night_light_state(self) -> int | None:
|
|
225
286
|
"""Return cached night light state."""
|
|
226
287
|
return self._get_adv_value("nightLight")
|
|
288
|
+
|
|
289
|
+
def is_charging(self) -> bool | None:
|
|
290
|
+
"""Return cached charging state."""
|
|
291
|
+
return self._get_adv_value("charging")
|
|
292
|
+
|
|
293
|
+
def get_child_lock(self) -> bool | None:
|
|
294
|
+
"""Return cached child-lock state."""
|
|
295
|
+
return self._get_adv_value("child_lock")
|
|
296
|
+
|
|
297
|
+
def get_display(self) -> bool | None:
|
|
298
|
+
"""Return cached front-display (LED) state."""
|
|
299
|
+
return self._get_adv_value("display")
|
|
300
|
+
|
|
301
|
+
def get_sound(self) -> bool | None:
|
|
302
|
+
"""Return cached key-tone (buzzer) state."""
|
|
303
|
+
return self._get_adv_value("sound")
|
|
304
|
+
|
|
305
|
+
def get_auto_recenter(self) -> bool | None:
|
|
306
|
+
"""Return cached auto-recenter (return-to-center) state."""
|
|
307
|
+
return self._get_adv_value("auto_recenter")
|
switchbot/devices/light_strip.py
CHANGED
|
@@ -3,9 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from ..const import SwitchbotModel
|
|
6
|
-
from ..const.light import
|
|
6
|
+
from ..const.light import (
|
|
7
|
+
ColorMode,
|
|
8
|
+
RGBICStripLightColorMode,
|
|
9
|
+
RGBICWWCeilingLightColorMode,
|
|
10
|
+
StripLightColorMode,
|
|
11
|
+
)
|
|
7
12
|
from .base_light import SwitchbotSequenceBaseLight
|
|
8
|
-
from .device import SwitchbotEncryptedDevice
|
|
13
|
+
from .device import SwitchbotEncryptedDevice, update_after_operation
|
|
9
14
|
|
|
10
15
|
# Private mapping from device-specific color modes to original ColorMode enum
|
|
11
16
|
_STRIP_LIGHT_COLOR_MODE_MAP = {
|
|
@@ -26,6 +31,16 @@ _RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP = {
|
|
|
26
31
|
RGBICStripLightColorMode.EFFECT: ColorMode.EFFECT,
|
|
27
32
|
RGBICStripLightColorMode.UNKNOWN: ColorMode.OFF,
|
|
28
33
|
}
|
|
34
|
+
_RGBICWW_CEILING_LIGHT_COLOR_MODE_MAP = {
|
|
35
|
+
RGBICWWCeilingLightColorMode.SEGMENTED: ColorMode.EFFECT,
|
|
36
|
+
RGBICWWCeilingLightColorMode.COLOR: ColorMode.RGB,
|
|
37
|
+
RGBICWWCeilingLightColorMode.SCENE: ColorMode.EFFECT,
|
|
38
|
+
RGBICWWCeilingLightColorMode.MUSIC: ColorMode.EFFECT,
|
|
39
|
+
RGBICWWCeilingLightColorMode.CONTROLLER: ColorMode.EFFECT,
|
|
40
|
+
RGBICWWCeilingLightColorMode.WARMWHITE: ColorMode.COLOR_TEMP,
|
|
41
|
+
RGBICWWCeilingLightColorMode.EFFECT: ColorMode.EFFECT,
|
|
42
|
+
RGBICWWCeilingLightColorMode.UNKNOWN: ColorMode.OFF,
|
|
43
|
+
}
|
|
29
44
|
LIGHT_STRIP_CONTROL_HEADER = "570F4901"
|
|
30
45
|
COMMON_EFFECTS = {
|
|
31
46
|
"christmas": [
|
|
@@ -352,3 +367,124 @@ class SwitchbotRgbicNeonLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
|
|
|
352
367
|
def color_mode(self) -> ColorMode:
|
|
353
368
|
"""Return the current color mode."""
|
|
354
369
|
return ColorMode.RGB
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class SwitchbotRgbicwwCeilingLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
|
|
373
|
+
"""Support for Switchbot RGBICWW Ceiling Light (warm-white + color sub-lights)."""
|
|
374
|
+
|
|
375
|
+
_model = SwitchbotModel.RGBICWW_CEILING_LIGHT
|
|
376
|
+
_effect_dict = RGBIC_EFFECTS
|
|
377
|
+
|
|
378
|
+
# Color sub-light commands (sub_cmd 0x12 brightness+RGB, 0x14 brightness)
|
|
379
|
+
_set_brightness_command = f"{LIGHT_STRIP_CONTROL_HEADER}14{{}}"
|
|
380
|
+
_set_rgb_command = f"{LIGHT_STRIP_CONTROL_HEADER}12{{}}"
|
|
381
|
+
|
|
382
|
+
# Main (warm-white) sub-light commands (sub_cmd 0x09, 0x10, 0x11)
|
|
383
|
+
_set_main_brightness_command = f"{LIGHT_STRIP_CONTROL_HEADER}09{{}}"
|
|
384
|
+
_set_main_color_temp_command = f"{LIGHT_STRIP_CONTROL_HEADER}10{{}}"
|
|
385
|
+
_set_color_temp_command = f"{LIGHT_STRIP_CONTROL_HEADER}11{{}}"
|
|
386
|
+
|
|
387
|
+
# Sub-light power control: 0x49 0x01 <onoff> <selector>.
|
|
388
|
+
# onoff = 0x01 (on)/0x02 (off)/0x03 (toggle); here we always send 0x01 and
|
|
389
|
+
# let the selector drive the individual sub-lights.
|
|
390
|
+
# selector: bit7=1 (specify), bits[3:2]=color state, bits[1:0]=white state;
|
|
391
|
+
# state encoding 00=keep, 01=on, 02=off, 03=toggle.
|
|
392
|
+
_turn_on_main_command = f"{LIGHT_STRIP_CONTROL_HEADER}0181"
|
|
393
|
+
_turn_off_main_command = f"{LIGHT_STRIP_CONTROL_HEADER}0182"
|
|
394
|
+
_turn_on_color_command = f"{LIGHT_STRIP_CONTROL_HEADER}0184"
|
|
395
|
+
_turn_off_color_command = f"{LIGHT_STRIP_CONTROL_HEADER}0188"
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def color_modes(self) -> set[ColorMode]:
|
|
399
|
+
"""Return the supported color modes (color sub-light)."""
|
|
400
|
+
return {ColorMode.RGB, ColorMode.COLOR_TEMP}
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def color_mode(self) -> ColorMode:
|
|
404
|
+
"""Return the current color mode."""
|
|
405
|
+
device_mode = RGBICWWCeilingLightColorMode(
|
|
406
|
+
self._get_adv_value("color_mode") or 10
|
|
407
|
+
)
|
|
408
|
+
return _RGBICWW_CEILING_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def is_main_on(self) -> bool | None:
|
|
412
|
+
"""Return whether the main (warm-white) sub-light is on."""
|
|
413
|
+
return self._get_adv_value("main_isOn")
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def main_brightness(self) -> int:
|
|
417
|
+
"""Return the main (warm-white) sub-light brightness 0-100."""
|
|
418
|
+
return self._get_adv_value("main_brightness") or 0
|
|
419
|
+
|
|
420
|
+
@update_after_operation
|
|
421
|
+
async def turn_on_main(self) -> bool:
|
|
422
|
+
"""Turn the main (warm-white) sub-light on."""
|
|
423
|
+
result = await self._send_command(self._turn_on_main_command)
|
|
424
|
+
return self._check_command_result(result, 0, {1})
|
|
425
|
+
|
|
426
|
+
@update_after_operation
|
|
427
|
+
async def turn_off_main(self) -> bool:
|
|
428
|
+
"""Turn the main (warm-white) sub-light off."""
|
|
429
|
+
result = await self._send_command(self._turn_off_main_command)
|
|
430
|
+
return self._check_command_result(result, 0, {1})
|
|
431
|
+
|
|
432
|
+
@update_after_operation
|
|
433
|
+
async def turn_on_color(self) -> bool:
|
|
434
|
+
"""Turn the color sub-light on."""
|
|
435
|
+
result = await self._send_command(self._turn_on_color_command)
|
|
436
|
+
return self._check_command_result(result, 0, {1})
|
|
437
|
+
|
|
438
|
+
@update_after_operation
|
|
439
|
+
async def turn_off_color(self) -> bool:
|
|
440
|
+
"""Turn the color sub-light off."""
|
|
441
|
+
result = await self._send_command(self._turn_off_color_command)
|
|
442
|
+
return self._check_command_result(result, 0, {1})
|
|
443
|
+
|
|
444
|
+
@update_after_operation
|
|
445
|
+
async def set_main_brightness(self, brightness: int) -> bool:
|
|
446
|
+
"""Set the main (warm-white) sub-light brightness (sub_cmd 0x09)."""
|
|
447
|
+
self._validate_brightness(brightness)
|
|
448
|
+
hex_brightness = f"{brightness:02X}"
|
|
449
|
+
result = await self._send_command(
|
|
450
|
+
self._set_main_brightness_command.format(hex_brightness)
|
|
451
|
+
)
|
|
452
|
+
return self._check_command_result(result, 0, {1})
|
|
453
|
+
|
|
454
|
+
@update_after_operation
|
|
455
|
+
async def set_main_color_temp(self, color_temp: int) -> bool:
|
|
456
|
+
"""Set the main (warm-white) sub-light color temperature (sub_cmd 0x10)."""
|
|
457
|
+
self._validate_color_temp(color_temp)
|
|
458
|
+
hex_data = f"{color_temp:04X}"
|
|
459
|
+
result = await self._send_command(
|
|
460
|
+
self._set_main_color_temp_command.format(hex_data)
|
|
461
|
+
)
|
|
462
|
+
return self._check_command_result(result, 0, {1})
|
|
463
|
+
|
|
464
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
465
|
+
"""
|
|
466
|
+
Read the RGB color (and color temp) over GATT.
|
|
467
|
+
|
|
468
|
+
Power, brightness and color mode are taken from the advertisement
|
|
469
|
+
(which tracks them reliably). The device's 0x4A01 status response does
|
|
470
|
+
NOT carry a usable color power state - byte 1 stays 0 even when the
|
|
471
|
+
color sub-light is on - so those fields are deliberately not returned
|
|
472
|
+
here, otherwise update() would clobber the correct advertised values.
|
|
473
|
+
"""
|
|
474
|
+
if not (
|
|
475
|
+
res := await self._get_multi_commands_results(self._get_basic_info_command)
|
|
476
|
+
):
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
_version_info, _data = res
|
|
480
|
+
self._state["r"] = _data[3]
|
|
481
|
+
self._state["g"] = _data[4]
|
|
482
|
+
self._state["b"] = _data[5]
|
|
483
|
+
self._state["cw"] = int.from_bytes(_data[7:9], "big")
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
"r": self._state["r"],
|
|
487
|
+
"g": self._state["g"],
|
|
488
|
+
"b": self._state["b"],
|
|
489
|
+
"firmware": _version_info[2] / 10.0,
|
|
490
|
+
}
|
|
@@ -136,6 +136,14 @@ class SwitchbotRelaySwitch(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
|
136
136
|
|
|
137
137
|
if not (_data := await self._get_basic_info(COMMAND_GET_BASIC_INFO)):
|
|
138
138
|
return None
|
|
139
|
+
if len(_data) < 17:
|
|
140
|
+
_LOGGER.warning(
|
|
141
|
+
"%s: Short basic-info response (%d bytes): %s",
|
|
142
|
+
self.name,
|
|
143
|
+
len(_data),
|
|
144
|
+
_data.hex(),
|
|
145
|
+
)
|
|
146
|
+
return None
|
|
139
147
|
if not (
|
|
140
148
|
_channel1_data := await self._get_basic_info(
|
|
141
149
|
COMMAND_GET_CHANNEL1_INFO.format(
|
|
@@ -144,6 +152,14 @@ class SwitchbotRelaySwitch(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
|
144
152
|
)
|
|
145
153
|
):
|
|
146
154
|
return None
|
|
155
|
+
if len(_channel1_data) < 15:
|
|
156
|
+
_LOGGER.warning(
|
|
157
|
+
"%s: Short channel1 response (%d bytes): %s",
|
|
158
|
+
self.name,
|
|
159
|
+
len(_channel1_data),
|
|
160
|
+
_channel1_data.hex(),
|
|
161
|
+
)
|
|
162
|
+
return None
|
|
147
163
|
|
|
148
164
|
_LOGGER.debug(
|
|
149
165
|
"on-off hex: %s, channel1_hex_data: %s", _data.hex(), _channel1_data.hex()
|
|
@@ -223,6 +239,14 @@ class SwitchbotRelaySwitch2PM(SwitchbotRelaySwitch):
|
|
|
223
239
|
)
|
|
224
240
|
):
|
|
225
241
|
return None
|
|
242
|
+
if len(_channel2_data) < 15:
|
|
243
|
+
_LOGGER.warning(
|
|
244
|
+
"%s: Short channel2 response (%d bytes): %s",
|
|
245
|
+
self.name,
|
|
246
|
+
len(_channel2_data),
|
|
247
|
+
_channel2_data.hex(),
|
|
248
|
+
)
|
|
249
|
+
return None
|
|
226
250
|
|
|
227
251
|
_LOGGER.debug("channel2_hex_data: %s", _channel2_data.hex())
|
|
228
252
|
|
|
@@ -41,6 +41,7 @@ MODE_TEMP_RANGE = {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
DEFAULT_TEMP_RANGE = (5.0, 35.0)
|
|
44
|
+
HYSTERESIS_IDLE_THRESHOLD = 0.5
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
class SwitchbotSmartThermostatRadiator(
|
|
@@ -193,4 +194,15 @@ class SwitchbotSmartThermostatRadiator(
|
|
|
193
194
|
"""Return current action from cache."""
|
|
194
195
|
if not self.is_on():
|
|
195
196
|
return ClimateAction.OFF
|
|
197
|
+
|
|
198
|
+
current_temp = self.get_current_temperature()
|
|
199
|
+
target_temp = self.get_target_temperature()
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
current_temp is not None
|
|
203
|
+
and target_temp is not None
|
|
204
|
+
and current_temp >= (target_temp + HYSTERESIS_IDLE_THRESHOLD)
|
|
205
|
+
):
|
|
206
|
+
return ClimateAction.IDLE
|
|
207
|
+
|
|
196
208
|
return ClimateAction.HEATING
|
switchbot/discovery.py
CHANGED
|
@@ -53,7 +53,7 @@ class GetSwitchbotDevices:
|
|
|
53
53
|
devices = None
|
|
54
54
|
devices = bleak.BleakScanner(
|
|
55
55
|
detection_callback=self.detection_callback,
|
|
56
|
-
#
|
|
56
|
+
# Could filter on service UUIDs once new ones are identified; see
|
|
57
57
|
# https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/4ad138bb09f0fbbfa41b152ca327a78c1d0b6ba9/devicetypes/meter.md
|
|
58
58
|
adapter=self._interface,
|
|
59
59
|
)
|
|
@@ -65,9 +65,7 @@ class GetSwitchbotDevices:
|
|
|
65
65
|
|
|
66
66
|
if devices is None:
|
|
67
67
|
if retry < 1:
|
|
68
|
-
_LOGGER.error(
|
|
69
|
-
"Scanning for Switchbot devices failed. Stop trying", exc_info=True
|
|
70
|
-
)
|
|
68
|
+
_LOGGER.error("Scanning for Switchbot devices failed. Stop trying")
|
|
71
69
|
return self._adv_data
|
|
72
70
|
|
|
73
71
|
_LOGGER.warning(
|
switchbot/utils.py
CHANGED
|
@@ -17,7 +17,7 @@ def format_mac_upper(mac: str) -> str:
|
|
|
17
17
|
to_test = to_test.replace(".", "")
|
|
18
18
|
|
|
19
19
|
if len(to_test) == 12:
|
|
20
|
-
#
|
|
20
|
+
# bare 12-char hex, insert colons
|
|
21
21
|
return ":".join(to_test.upper()[i : i + 2] for i in range(0, 12, 2))
|
|
22
22
|
|
|
23
23
|
# Not sure how formatted, return original
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
pyswitchbot-2.2.0.dist-info/licenses/LICENSE,sha256=_hdUCmoRDzki4fYDJfn-5KpG7-7p6YJ6IsbMfrTXN6Y,1078
|
|
2
|
-
switchbot/__init__.py,sha256=FApjrCQXf1Tsg0Qvd6b6KNIjdUdbgZlsmF2Lcb_I0qY,4086
|
|
3
|
-
switchbot/adv_parser.py,sha256=os5xmsISvp8K3xu0TvWFY9stwK-YLSbRUteiw5T7hXg,33146
|
|
4
|
-
switchbot/api_config.py,sha256=WSsoKkgNORkbqBZDH0U0rsEwc-etpBDTCXLSJnChsPA,263
|
|
5
|
-
switchbot/discovery.py,sha256=GvD8r5gkJ11Lj6Ya9-5n37mJNcoMAwXFCtLpC8aSveM,6405
|
|
6
|
-
switchbot/enum.py,sha256=0IfWypBw2oe9lWgdRkrD_1FqqeHpakCxtwlSaUlmcPg,125
|
|
7
|
-
switchbot/helpers.py,sha256=DJLvYZ4Dq0dkiGMOzfR--nSTM_8jE9QtB7t570PuGSU,2091
|
|
8
|
-
switchbot/models.py,sha256=xibQOVGHC60FWWc54N6BW46vxFuJUjj6tK1Qw0_eTbI,372
|
|
9
|
-
switchbot/utils.py,sha256=a_Plftx7z3BY2Lyyb4REnDG2a-kybyCKitzGvspPXT8,706
|
|
10
|
-
switchbot/adv_parsers/__init__.py,sha256=d39GEAICA1RrQiLy1ks49x0d9a-A_yKn9Awkj5za8Uk,46
|
|
11
|
-
switchbot/adv_parsers/_sensor_th.py,sha256=ze182l3_YFHJJv98_vq6qB2twlUTExkxoC1sLZL3TLs,1199
|
|
12
|
-
switchbot/adv_parsers/air_purifier.py,sha256=kOaOtNgZDqoQ3KX9keDtehleAg2ET0nrxGV4mO68YJM,1513
|
|
13
|
-
switchbot/adv_parsers/art_frame.py,sha256=spZFXKFsP5YixH6fjKpUkqSMl6HRmUQsBCjRwxAANHs,959
|
|
14
|
-
switchbot/adv_parsers/blind_tilt.py,sha256=jLgoxKKkBNPhhA1w6s3Vm9yjLBsVU5u7lddwiQ1kVb0,827
|
|
15
|
-
switchbot/adv_parsers/bot.py,sha256=aq0NA4G3tNvgMxpVa8KmNSOqwZYrxrB-Tz73oULkG-U,637
|
|
16
|
-
switchbot/adv_parsers/bulb.py,sha256=rCteaPC7Z1Wz_gXKJLCoxbzZgJPpZNcfs9OywGJErek,633
|
|
17
|
-
switchbot/adv_parsers/ceiling_light.py,sha256=ONrehUpH-e7JB9fB0wKj1o-HkPsk9Y2n7blP5U4px3Q,668
|
|
18
|
-
switchbot/adv_parsers/climate_panel.py,sha256=z9ZiMKkKR8-H_omTO1lSNqgqM8SbnNRasg_FeYuIN-M,1257
|
|
19
|
-
switchbot/adv_parsers/contact.py,sha256=VYAsEmIikeyJUKSYDfpwpDS9BjnaBX4pArIglcEyja8,1271
|
|
20
|
-
switchbot/adv_parsers/curtain.py,sha256=LfALqzwQKTdtlG8ykqTmwvh65elEpIJ4TfUI1sGPaWc,1182
|
|
21
|
-
switchbot/adv_parsers/fan.py,sha256=eOJ2uEllksuw30BuCCMJ2ut23jQehNue44SmEk2F3Vo,1796
|
|
22
|
-
switchbot/adv_parsers/hub2.py,sha256=tKoOp30LCkJvwu-paptuSuvx8JQOxGMWsrmU73JZfus,1563
|
|
23
|
-
switchbot/adv_parsers/hub3.py,sha256=V5VdGS39Pp0G_lqw5Kdfe1X6-K_ryfxlBKpnj3D9sVQ,1883
|
|
24
|
-
switchbot/adv_parsers/hubmini_matter.py,sha256=gNZh2jChGyypN2HbamNZGKI_Bc2U8u491hQE2RnshOM,933
|
|
25
|
-
switchbot/adv_parsers/humidifier.py,sha256=UTZdGETPGdpCn5md50OlszHiNKoTVcbwJeSNW-oyfFs,3070
|
|
26
|
-
switchbot/adv_parsers/keypad.py,sha256=_MkQOmCEMliiM48mmq3wZ3xcUR28WiAe2rPi74COlxo,548
|
|
27
|
-
switchbot/adv_parsers/keypad_vision.py,sha256=fN5V9VgVOn9cv1xs6HzACKNnoXLP1xo38EQKhq3VRJY,2293
|
|
28
|
-
switchbot/adv_parsers/leak.py,sha256=eIvX2N0E0C_XCXGJQCp9L14uFQ76xMp2TLRHLOJQhrc,918
|
|
29
|
-
switchbot/adv_parsers/light_strip.py,sha256=Pyyyq2oWmYHpJkE087VJLSO_og0rMEcX3q74EQoIAbk,1624
|
|
30
|
-
switchbot/adv_parsers/lock.py,sha256=jpXW44JGOc2q0cnkCvH4nklvSd7TyMGjc1bdgA4tAzc,3303
|
|
31
|
-
switchbot/adv_parsers/meter.py,sha256=vQAi2lZJNYzXYhQ4hMcI0VAlUwv_O95VVJuEfpcEf1U,1052
|
|
32
|
-
switchbot/adv_parsers/motion.py,sha256=4bDLPg1u9Y_c_SY_KW2TrNnhOVnLazOa1qT4JFPu_68,1217
|
|
33
|
-
switchbot/adv_parsers/plug.py,sha256=M6Xo8_0R4FuRUNmK4_HI1IkHGRqdvBHta4XDnyOUDVk,487
|
|
34
|
-
switchbot/adv_parsers/presence_sensor.py,sha256=oDHZ_RHcMtV2Rp1N3fMNePwTuM9Q3eH5JEYp1Yt-X5s,1331
|
|
35
|
-
switchbot/adv_parsers/relay_switch.py,sha256=akP8XTWcu_EmVUfyMQb1wr9xHR0FEuksRR6eGqsp3EI,1836
|
|
36
|
-
switchbot/adv_parsers/remote.py,sha256=rQyi9QptFwWNxeqduec0p_3xapAEDjwyC-D1Absfm0w,433
|
|
37
|
-
switchbot/adv_parsers/roller_shade.py,sha256=VXdPkkIGFuhRSJMAuocZ_9fHuIAV0mZqRt7NQdZ-H8Y,932
|
|
38
|
-
switchbot/adv_parsers/smart_thermostat_radiator.py,sha256=-MKrvFM2I5k6RKHqENUo7VnJJyAu2kq1jJiQ_JDQ3WY,1773
|
|
39
|
-
switchbot/adv_parsers/vacuum.py,sha256=vj5EjWdLkApiPsrZoGN1--Xm16QGxn5IKe0bjEet1G4,2065
|
|
40
|
-
switchbot/adv_parsers/weather_station.py,sha256=HOCZ-gef7e6HrOdfzcIx12rV9XURwoCetUQuShyk1R4,1155
|
|
41
|
-
switchbot/const/__init__.py,sha256=c9eMos2QBp0ht7BgA6-YRUbvZvSSgiwhA8G9R6eLDEw,4381
|
|
42
|
-
switchbot/const/air_purifier.py,sha256=eKMMgArZuANoDAWUKXWu0X7DPVR6di2T7i4SfO4Kddk,379
|
|
43
|
-
switchbot/const/climate.py,sha256=4Kuz1nopygRs734KAyC_IgEXmPpRLgeE9JJgyJBclQA,902
|
|
44
|
-
switchbot/const/evaporative_humidifier.py,sha256=Klf-jlGovrDr6DreXEkPsQnYr6Gb9U02EtB0dM7u8Z0,831
|
|
45
|
-
switchbot/const/fan.py,sha256=PbKmjNo4NHA_q-Tto3JBcNr71lzqQEWqk1zx_olN64Y,1250
|
|
46
|
-
switchbot/const/hub2.py,sha256=K1nh-FMn0wxjk3s7kPir_1NMcuR0eNcmVXCiDX2t5xM,445
|
|
47
|
-
switchbot/const/hub3.py,sha256=UQwk_NVimMd1aqS1LOkLrL7PTySZzcr0WYnpk3EFPFk,308
|
|
48
|
-
switchbot/const/light.py,sha256=MRgIUyVyv_vsvrts82TbfWuT6q3rQnP2KURfdB8a7pA,644
|
|
49
|
-
switchbot/const/lock.py,sha256=wKvq5uctsKpcM6gSzxPrcvmezzRhPZPd5n948I8R4n0,342
|
|
50
|
-
switchbot/const/presence_sensor.py,sha256=X2oCfl0rbdd1-dSrqQFn3NDtEBXbti-87DbWq7NyN3A,135
|
|
51
|
-
switchbot/devices/__init__.py,sha256=_fyzMy8qWC_2Y1BJ4hggrCP1udNqSoU6wFmfOiYpEl0,32
|
|
52
|
-
switchbot/devices/air_purifier.py,sha256=JwT5HHQLvkDdFrDEKFT8sA41bM2G_DGfuOpf-74Smyw,10620
|
|
53
|
-
switchbot/devices/art_frame.py,sha256=JUb7s9ZgnPxfc3K-shxuWItiMorelzldew53PwQHr4A,4361
|
|
54
|
-
switchbot/devices/base_cover.py,sha256=UWNA7eGbJdY80Mu-k41xO4tnyk7FhpYaWRWihGvDM2g,4385
|
|
55
|
-
switchbot/devices/base_light.py,sha256=WWJJdRHfvoSYMRHDSv_RCmo5D6hYoZRxQVYfsOXZ7K0,6590
|
|
56
|
-
switchbot/devices/blind_tilt.py,sha256=tY0JoZhyqU_6PlhjtnIaEeIG3Z-JdZKJ5Jwg_5b6xSQ,5761
|
|
57
|
-
switchbot/devices/bot.py,sha256=Co2IiQa2si0WPs3HFize2CPpj5GVbMT570vMP5YWpaQ,4121
|
|
58
|
-
switchbot/devices/bulb.py,sha256=ISxXck8JsBlFojLB8M5Wfip_z4o6_-VvyFbBvqK6_Bg,2313
|
|
59
|
-
switchbot/devices/ceiling_light.py,sha256=45UsGj_gPQv8tJO40UtkzOBYvE-PZovGEgOYlMGaacc,2645
|
|
60
|
-
switchbot/devices/contact.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
61
|
-
switchbot/devices/curtain.py,sha256=p3qcPWHaSA5ewPONl811hL7swFRca37Omb2LKYoAxgo,7086
|
|
62
|
-
switchbot/devices/device.py,sha256=vQzJZQ_cpu2EadLEwzslq--cq2xk7euU0jqlMAhZ0QQ,49467
|
|
63
|
-
switchbot/devices/evaporative_humidifier.py,sha256=Dmpg4oCp02iPBz6gc7z5nobio5rDyLoSLP3Oil6sRJ4,8279
|
|
64
|
-
switchbot/devices/fan.py,sha256=S431LLjGBb9d_B6EB71rTWZDw0sCG6HYtG09phtkaQ8,9008
|
|
65
|
-
switchbot/devices/humidifier.py,sha256=oqtRFMSA5ZSaKMEisg1gwHwWQeWBcEt21N5Qg-fqeTc,3433
|
|
66
|
-
switchbot/devices/keypad.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
67
|
-
switchbot/devices/keypad_vision.py,sha256=hhkbzT_uWqGy07pcReNlfAjcU5r27O4HI45xBiPAi1M,5367
|
|
68
|
-
switchbot/devices/light_strip.py,sha256=-hBEpR9BHG4B5pIxbrGZKRMSgImM1we0ziPqxfqYLa8,10562
|
|
69
|
-
switchbot/devices/lock.py,sha256=GNytU2mBoDFIQxHPVS_XEcydhUt2HHa_rdIHLUclGLM,11878
|
|
70
|
-
switchbot/devices/meter.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
71
|
-
switchbot/devices/meter_pro.py,sha256=tUprQ_9MEhJCFGBu1UFrQzndaM5yVFbuQBItboNrULA,6996
|
|
72
|
-
switchbot/devices/motion.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
73
|
-
switchbot/devices/plug.py,sha256=RqOppC3KmjROxukHFLNttEhDg7-hH_IaiTCfXWXwF_k,1397
|
|
74
|
-
switchbot/devices/relay_switch.py,sha256=uQAUZyptTWQVqfUUIQJAgGrQzty0j0Cmfb5g-A-I3ts,9746
|
|
75
|
-
switchbot/devices/roller_shade.py,sha256=Lv3DDUgnlxArJY6qQc9HXxJwd7NcPEYmQvWnWn_yWdE,6431
|
|
76
|
-
switchbot/devices/smart_thermostat_radiator.py,sha256=m8Ex32O6qqzO08XlZAefAFpIfwkKWI_pIFxqB0qb5Gs,6601
|
|
77
|
-
switchbot/devices/vacuum.py,sha256=qzeEaXPNIs8B-FyH7NLnAqXSxd5dB7ioq6tSiajF-LM,2287
|
|
78
|
-
pyswitchbot-2.2.0.dist-info/METADATA,sha256=EAJbE30uQoO9L1jaUltWOYUGlTRAFVkT5a7o1vDMLYM,4197
|
|
79
|
-
pyswitchbot-2.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
80
|
-
pyswitchbot-2.2.0.dist-info/top_level.txt,sha256=HUeT93OSFPT5mzylEY3uhH7bvqiVvcpGx6XBAELiRxg,10
|
|
81
|
-
pyswitchbot-2.2.0.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
switchbot
|
|
File without changes
|