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.
Files changed (45) hide show
  1. {pyswitchbot-2.2.0.dist-info → pyswitchbot-2.3.0.dist-info}/METADATA +46 -22
  2. pyswitchbot-2.3.0.dist-info/RECORD +81 -0
  3. {pyswitchbot-2.2.0.dist-info → pyswitchbot-2.3.0.dist-info}/WHEEL +1 -2
  4. switchbot/__init__.py +2 -0
  5. switchbot/__version__.py +1 -0
  6. switchbot/adv_parser.py +13 -0
  7. switchbot/adv_parsers/air_purifier.py +1 -1
  8. switchbot/adv_parsers/art_frame.py +1 -1
  9. switchbot/adv_parsers/blind_tilt.py +2 -2
  10. switchbot/adv_parsers/bot.py +1 -1
  11. switchbot/adv_parsers/bulb.py +1 -1
  12. switchbot/adv_parsers/ceiling_light.py +1 -1
  13. switchbot/adv_parsers/climate_panel.py +1 -1
  14. switchbot/adv_parsers/contact.py +8 -3
  15. switchbot/adv_parsers/curtain.py +3 -3
  16. switchbot/adv_parsers/fan.py +11 -4
  17. switchbot/adv_parsers/hub2.py +1 -1
  18. switchbot/adv_parsers/hub3.py +1 -1
  19. switchbot/adv_parsers/hubmini_matter.py +1 -1
  20. switchbot/adv_parsers/humidifier.py +2 -2
  21. switchbot/adv_parsers/keypad.py +1 -1
  22. switchbot/adv_parsers/keypad_vision.py +3 -3
  23. switchbot/adv_parsers/leak.py +1 -1
  24. switchbot/adv_parsers/light_strip.py +16 -3
  25. switchbot/adv_parsers/lock.py +4 -4
  26. switchbot/adv_parsers/meter.py +10 -4
  27. switchbot/adv_parsers/motion.py +2 -1
  28. switchbot/adv_parsers/plug.py +1 -1
  29. switchbot/adv_parsers/presence_sensor.py +1 -1
  30. switchbot/adv_parsers/remote.py +1 -1
  31. switchbot/adv_parsers/roller_shade.py +2 -2
  32. switchbot/adv_parsers/smart_thermostat_radiator.py +1 -1
  33. switchbot/adv_parsers/vacuum.py +2 -2
  34. switchbot/const/__init__.py +1 -0
  35. switchbot/const/light.py +11 -0
  36. switchbot/devices/device.py +3 -2
  37. switchbot/devices/fan.py +82 -1
  38. switchbot/devices/light_strip.py +138 -2
  39. switchbot/devices/relay_switch.py +24 -0
  40. switchbot/devices/smart_thermostat_radiator.py +12 -0
  41. switchbot/discovery.py +2 -4
  42. switchbot/utils.py +1 -1
  43. pyswitchbot-2.2.0.dist-info/RECORD +0 -81
  44. pyswitchbot-2.2.0.dist-info/top_level.txt +0 -1
  45. {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.2.0
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-Python: >=3.11
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 [![codecov](https://codecov.io/gh/sblibs/pySwitchbot/graph/badge.svg?token=TI027U5ISQ)](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, ENCRYPTION_KEY, model=LOCK_MODEL
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, ENCRYPTION_KEY, model=LOCK_MODEL
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,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: poetry-core 2.4.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
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",
@@ -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
 
@@ -9,7 +9,7 @@ def process_art_frame(
9
9
  data: bytes | None, mfr_data: bytes | None
10
10
  ) -> dict[str, bool | int | str]:
11
11
  """Process Art Frame data."""
12
- if mfr_data is None:
12
+ if mfr_data is None or len(mfr_data) < 10:
13
13
  return {}
14
14
 
15
15
  _seq_num = mfr_data[6]
@@ -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,
@@ -8,7 +8,7 @@ def process_wohand(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool
8
8
  if data is None and mfr_data is None:
9
9
  return {}
10
10
 
11
- if data is None:
11
+ if data is None or len(data) < 3:
12
12
  return {
13
13
  "switchMode": None,
14
14
  "isOn": None,
@@ -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],
@@ -9,7 +9,7 @@ def process_climate_panel(
9
9
  data: bytes | None, mfr_data: bytes | None
10
10
  ) -> dict[str, bool | int | str]:
11
11
  """Process Climate Panel data."""
12
- if mfr_data is None:
12
+ if mfr_data is None or len(mfr_data) < 16:
13
13
  return {}
14
14
 
15
15
  seq_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
- battery = data[2] & 0b01111111 if data else None
14
- tested = bool(data[1] & 0b10000000) if data else None
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 mfr_data and len(mfr_data) >= 13:
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, mode_map: dict[int, str]
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
- return {
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
 
@@ -13,7 +13,7 @@ def process_hubmini_matter(
13
13
  """Process Hubmini matter sensor manufacturer data."""
14
14
  temp_data = None
15
15
 
16
- if mfr_data:
16
+ if mfr_data and len(mfr_data) >= 16:
17
17
  temp_data = mfr_data[13:16]
18
18
 
19
19
  if not temp_data:
@@ -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 data is None or len(data) < 3 or mfr_data is None or len(mfr_data) < 2:
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
+ }
@@ -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())
@@ -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
- _wosensorth_data["co2"] = CO2_UNPACK(co2_data)[0]
41
+ co2 = CO2_UNPACK(co2_data)[0]
42
+ if co2 <= CO2_MAX_PPM:
43
+ _wosensorth_data["co2"] = co2
38
44
  return _wosensorth_data
@@ -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
@@ -9,7 +9,7 @@ def process_woplugmini(
9
9
  data: bytes | None, mfr_data: bytes | None
10
10
  ) -> dict[str, bool | int]:
11
11
  """Process plug mini."""
12
- if mfr_data is None:
12
+ if mfr_data is None or len(mfr_data) < 12:
13
13
  return {}
14
14
  return {
15
15
  "switchMode": True,
@@ -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]
@@ -11,7 +11,7 @@ def process_woremote(
11
11
  data: bytes | None, mfr_data: bytes | None
12
12
  ) -> dict[str, int | None]:
13
13
  """Process WoRemote adv data."""
14
- if data is None:
14
+ if data is None or len(data) < 3:
15
15
  return {
16
16
  "battery": None,
17
17
  }
@@ -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]
@@ -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]
@@ -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
@@ -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.exception("No advertisement data to update")
854
- return None
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 90° maps to byte 0x5F (95), not 0x5A (90),
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")
@@ -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 ColorMode, RGBICStripLightColorMode, StripLightColorMode
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
- # TODO: Find new UUIDs to filter on. For example, see
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
- # no : included
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