PySwitchbot 2.1.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. {pyswitchbot-2.1.0/PySwitchbot.egg-info → pyswitchbot-2.3.0}/PKG-INFO +46 -22
  2. pyswitchbot-2.1.0/PKG-INFO → pyswitchbot-2.3.0/README.md +27 -35
  3. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/pyproject.toml +73 -0
  4. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/__init__.py +18 -1
  5. pyswitchbot-2.3.0/switchbot/__version__.py +1 -0
  6. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parser.py +94 -3
  7. pyswitchbot-2.3.0/switchbot/adv_parsers/_sensor_th.py +36 -0
  8. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/air_purifier.py +1 -1
  9. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/art_frame.py +1 -1
  10. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/blind_tilt.py +2 -2
  11. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/bot.py +1 -1
  12. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/bulb.py +1 -1
  13. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/ceiling_light.py +1 -1
  14. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/climate_panel.py +1 -1
  15. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/contact.py +8 -3
  16. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/curtain.py +3 -3
  17. pyswitchbot-2.3.0/switchbot/adv_parsers/fan.py +62 -0
  18. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/hub2.py +1 -1
  19. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/hub3.py +1 -1
  20. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/hubmini_matter.py +1 -1
  21. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/humidifier.py +2 -2
  22. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/keypad.py +1 -1
  23. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/keypad_vision.py +3 -3
  24. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/leak.py +1 -1
  25. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/light_strip.py +30 -2
  26. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/lock.py +4 -4
  27. pyswitchbot-2.3.0/switchbot/adv_parsers/meter.py +44 -0
  28. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/motion.py +2 -1
  29. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/plug.py +1 -1
  30. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/presence_sensor.py +1 -1
  31. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/remote.py +1 -1
  32. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/roller_shade.py +2 -2
  33. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +1 -1
  34. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/vacuum.py +2 -2
  35. pyswitchbot-2.3.0/switchbot/adv_parsers/weather_station.py +40 -0
  36. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/const/__init__.py +18 -1
  37. pyswitchbot-2.3.0/switchbot/const/fan.py +62 -0
  38. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/const/light.py +12 -0
  39. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/air_purifier.py +1 -26
  40. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/art_frame.py +2 -26
  41. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/device.py +20 -5
  42. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/evaporative_humidifier.py +2 -27
  43. pyswitchbot-2.3.0/switchbot/devices/fan.py +307 -0
  44. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/keypad_vision.py +1 -1
  45. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/light_strip.py +196 -51
  46. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/lock.py +7 -16
  47. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/relay_switch.py +28 -48
  48. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/roller_shade.py +55 -17
  49. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/smart_thermostat_radiator.py +13 -26
  50. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/discovery.py +2 -4
  51. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/utils.py +1 -1
  52. pyswitchbot-2.1.0/MANIFEST.in +0 -1
  53. pyswitchbot-2.1.0/PySwitchbot.egg-info/SOURCES.txt +0 -110
  54. pyswitchbot-2.1.0/PySwitchbot.egg-info/dependency_links.txt +0 -1
  55. pyswitchbot-2.1.0/PySwitchbot.egg-info/requires.txt +0 -5
  56. pyswitchbot-2.1.0/PySwitchbot.egg-info/top_level.txt +0 -1
  57. pyswitchbot-2.1.0/README.md +0 -110
  58. pyswitchbot-2.1.0/setup.cfg +0 -4
  59. pyswitchbot-2.1.0/setup.py +0 -40
  60. pyswitchbot-2.1.0/switchbot/adv_parsers/fan.py +0 -33
  61. pyswitchbot-2.1.0/switchbot/adv_parsers/meter.py +0 -56
  62. pyswitchbot-2.1.0/switchbot/const/fan.py +0 -14
  63. pyswitchbot-2.1.0/switchbot/devices/fan.py +0 -103
  64. pyswitchbot-2.1.0/tests/test_adv_parser.py +0 -4444
  65. pyswitchbot-2.1.0/tests/test_air_purifier.py +0 -484
  66. pyswitchbot-2.1.0/tests/test_art_frame.py +0 -221
  67. pyswitchbot-2.1.0/tests/test_base_cover.py +0 -151
  68. pyswitchbot-2.1.0/tests/test_blind_tilt.py +0 -240
  69. pyswitchbot-2.1.0/tests/test_bulb.py +0 -241
  70. pyswitchbot-2.1.0/tests/test_ceiling_light.py +0 -194
  71. pyswitchbot-2.1.0/tests/test_colormode_imports.py +0 -88
  72. pyswitchbot-2.1.0/tests/test_curtain.py +0 -425
  73. pyswitchbot-2.1.0/tests/test_device.py +0 -416
  74. pyswitchbot-2.1.0/tests/test_discovery_callback.py +0 -142
  75. pyswitchbot-2.1.0/tests/test_encrypted_device.py +0 -548
  76. pyswitchbot-2.1.0/tests/test_evaporative_humidifier.py +0 -358
  77. pyswitchbot-2.1.0/tests/test_fan.py +0 -177
  78. pyswitchbot-2.1.0/tests/test_helpers.py +0 -72
  79. pyswitchbot-2.1.0/tests/test_hub2.py +0 -18
  80. pyswitchbot-2.1.0/tests/test_hub3.py +0 -13
  81. pyswitchbot-2.1.0/tests/test_keypad_vision.py +0 -259
  82. pyswitchbot-2.1.0/tests/test_lock.py +0 -881
  83. pyswitchbot-2.1.0/tests/test_meter_pro.py +0 -249
  84. pyswitchbot-2.1.0/tests/test_relay_switch.py +0 -503
  85. pyswitchbot-2.1.0/tests/test_roller_shade.py +0 -223
  86. pyswitchbot-2.1.0/tests/test_smart_thermostat_radiator.py +0 -239
  87. pyswitchbot-2.1.0/tests/test_strip_light.py +0 -398
  88. pyswitchbot-2.1.0/tests/test_utils.py +0 -41
  89. pyswitchbot-2.1.0/tests/test_vacuum.py +0 -145
  90. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/LICENSE +0 -0
  91. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/__init__.py +0 -0
  92. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  93. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/api_config.py +0 -0
  94. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/const/air_purifier.py +0 -0
  95. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/const/climate.py +0 -0
  96. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/const/evaporative_humidifier.py +0 -0
  97. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/const/hub2.py +0 -0
  98. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/const/hub3.py +0 -0
  99. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/const/lock.py +0 -0
  100. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/const/presence_sensor.py +0 -0
  101. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/__init__.py +0 -0
  102. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/base_cover.py +0 -0
  103. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/base_light.py +0 -0
  104. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/blind_tilt.py +0 -0
  105. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/bot.py +0 -0
  106. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/bulb.py +0 -0
  107. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/ceiling_light.py +0 -0
  108. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/contact.py +0 -0
  109. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/curtain.py +0 -0
  110. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/humidifier.py +0 -0
  111. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/keypad.py +0 -0
  112. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/meter.py +0 -0
  113. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/meter_pro.py +0 -0
  114. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/motion.py +0 -0
  115. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/plug.py +0 -0
  116. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/devices/vacuum.py +0 -0
  117. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/enum.py +0 -0
  118. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/helpers.py +0 -0
  119. {pyswitchbot-2.1.0 → pyswitchbot-2.3.0}/switchbot/models.py +0 -0
@@ -1,35 +1,33 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 2.1.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
+
@@ -1,36 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: PySwitchbot
3
- Version: 2.1.0
4
- Summary: A library to communicate with Switchbot
5
- Home-page: https://github.com/sblibs/pySwitchbot/
6
- Author: Daniel Hjelseth Hoyer
7
- License: MIT
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Environment :: Other Environment
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Operating System :: OS Independent
12
- Classifier: Programming Language :: Python
13
- Classifier: Topic :: Home Automation
14
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
- Requires-Python: >=3.11
16
- Description-Content-Type: text/markdown
17
- License-File: LICENSE
18
- Requires-Dist: aiohttp>=3.9.5
19
- Requires-Dist: bleak>=0.19.0
20
- Requires-Dist: bleak-retry-connector>=3.4.0
21
- Requires-Dist: cryptography>=39.0.0
22
- Requires-Dist: pyOpenSSL>=23.0.0
23
- Dynamic: author
24
- Dynamic: classifier
25
- Dynamic: description
26
- Dynamic: description-content-type
27
- Dynamic: home-page
28
- Dynamic: license
29
- Dynamic: license-file
30
- Dynamic: requires-dist
31
- Dynamic: requires-python
32
- Dynamic: summary
33
-
34
1
  # pySwitchbot [![codecov](https://codecov.io/gh/sblibs/pySwitchbot/graph/badge.svg?token=TI027U5ISQ)](https://codecov.io/gh/sblibs/pySwitchbot)
35
2
 
36
3
  Library to control Switchbot IoT devices https://www.switch-bot.com/
@@ -58,6 +25,31 @@ Encryption key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
58
25
  Where `MAC` is MAC address of the lock and `USERNAME` is your SwitchBot account username, after that script will ask for your password.
59
26
  If authentication succeeds then script should output your key id and encryption key.
60
27
 
28
+ ### Troubleshooting key retrieval
29
+
30
+ Key retrieval talks to SwitchBot's account API with your username and
31
+ password. The most common failures are account-side, not bugs in this library:
32
+
33
+ - **`Authentication failed: ...`** — the username/password were rejected. This
34
+ happens when:
35
+ - Two-factor authentication (2FA) is enabled on the account. The API login
36
+ used here does not support a verification code, so 2FA accounts cannot
37
+ retrieve keys this way — temporarily disable 2FA, fetch the key, then
38
+ re-enable it.
39
+ - The account was created via "Sign in with Apple"/Google and has no
40
+ password set. Set a password in the SwitchBot app first, or use an
41
+ email/password account.
42
+ - The username is an email but the account is registered to a phone number
43
+ (or vice versa). Use the exact identifier you log in with.
44
+ - **`Failed to retrieve encryption key from SwitchBot Account: ...`** —
45
+ authentication succeeded but the key could not be read. Usually the account
46
+ is not the device **owner**: keys are only returned to the owning account,
47
+ not to shared/family members. Retrieve the key from the owner account, or
48
+ transfer ownership in the app.
49
+
50
+ The key only needs to be fetched once; store the `key_id` and encryption key
51
+ and reuse them — there is no need to call the script on every connection.
52
+
61
53
  ## Examples:
62
54
 
63
55
  #### WoLock (Lock-Pro)
@@ -79,7 +71,7 @@ LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
79
71
  async def main():
80
72
  wolock = await GetSwitchbotDevices().get_locks()
81
73
  await lock.SwitchbotLock(
82
- wolock[BLE_MAC].device, KEY_ID, ENCRYPTION_KEY, model=LOCK_MODEL
74
+ wolock[BLE_MAC].device, KEY_ID, ENC_KEY, model=LOCK_MODEL
83
75
  ).unlock()
84
76
 
85
77
 
@@ -103,7 +95,7 @@ LOCK_MODEL=SwitchbotModel.LOCK_PRO # Your lock model (here we use the Lock-Pro)
103
95
  async def main():
104
96
  wolock = await GetSwitchbotDevices().get_locks()
105
97
  await lock.SwitchbotLock(
106
- wolock[BLE_MAC].device, KEY_ID, ENCRYPTION_KEY, model=LOCK_MODEL
98
+ wolock[BLE_MAC].device, KEY_ID, ENC_KEY, model=LOCK_MODEL
107
99
  ).lock()
108
100
 
109
101
 
@@ -1,8 +1,64 @@
1
+ [tool.poetry]
2
+ name = "PySwitchbot"
3
+ version = "2.3.0"
4
+ description = "A library to communicate with Switchbot"
5
+ authors = ["Daniel Hjelseth Hoyer"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ repository = "https://github.com/sblibs/pySwitchbot/"
9
+ packages = [
10
+ { include = "switchbot" },
11
+ ]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Other Environment",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python",
18
+ "Topic :: Home Automation",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ ]
21
+
22
+ [tool.poetry.urls]
23
+ "Bug Tracker" = "https://github.com/sblibs/pySwitchbot/issues"
24
+ "Changelog" = "https://github.com/sblibs/pySwitchbot/blob/main/CHANGELOG.md"
25
+
26
+ [tool.poetry.dependencies]
27
+ python = ">=3.11,<4.0"
28
+ aiohttp = ">=3.9.5"
29
+ bleak = ">=0.19.0"
30
+ bleak-retry-connector = ">=3.4.0"
31
+ cryptography = ">=39.0.0"
32
+ pyOpenSSL = ">=23.0.0"
33
+
34
+ [tool.poetry.group.dev.dependencies]
35
+ pytest = ">=7,<10"
36
+ pytest-cov = ">=3,<8"
37
+ pytest-asyncio = ">=0.19,<1.4"
38
+
39
+ [tool.semantic_release]
40
+ branch = "main"
41
+ # Existing release tags have no "v" prefix (e.g. 2.2.0), unlike PSR's default
42
+ # "v{version}"; without this PSR finds no prior release and bumps from 0.0.0.
43
+ tag_format = "{version}"
44
+ version_toml = ["pyproject.toml:tool.poetry.version"]
45
+ version_variables = ["switchbot/__version__.py:__version__"]
46
+ build_command = "pip install poetry && poetry build"
47
+
48
+ [tool.pytest.ini_options]
49
+ addopts = "--cov=switchbot --cov-report=term-missing"
50
+
51
+ [build-system]
52
+ requires = ["poetry-core>=1.0.0"]
53
+ build-backend = "poetry.core.masonry.api"
54
+
1
55
  [tool.ruff]
2
56
  target-version = "py311"
3
57
  line-length = 88
4
58
 
5
59
  [tool.ruff.lint]
60
+ preview = true
61
+ explicit-preview-rules = true # opt in ONLY explicitly-listed preview rules
6
62
  ignore = [
7
63
  "S101", # use of assert
8
64
  "D203", # 1 blank line required before class docstring
@@ -34,15 +90,30 @@ ignore = [
34
90
  ]
35
91
  select = [
36
92
  "ASYNC", # async rules
93
+ "A", # flake8-builtins
37
94
  "B", # flake8-bugbear
95
+ "BLE", # flake8-blind-except
38
96
  "D", # flake8-docstrings
39
97
  "C4", # flake8-comprehensions
98
+ "C90", # mccabe complexity
99
+ "DTZ", # flake8-datetimez
100
+ "ERA", # eradicate
101
+ "EXE", # flake8-executable
40
102
  "S", # flake8-bandit
41
103
  "F", # pyflake
104
+ "FA", # flake8-future-annotations
105
+ "FIX", # flake8-fixme
106
+ "FURB", # refurb
107
+ "FURB118", # reimplemented-operator (preview) - unneeded lambdas
42
108
  "E", # pycodestyle
43
109
  "W", # pycodestyle
44
110
  "UP", # pyupgrade
45
111
  "I", # isort
112
+ "ICN", # flake8-import-conventions
113
+ "INP", # flake8-no-pep420
114
+ "ISC", # flake8-implicit-str-concat
115
+ "LOG", # flake8-logging
116
+ "Q", # flake8-quotes
46
117
  "RUF", # ruff specific
47
118
  "FLY", # flynt
48
119
  "G", # flake8-logging-format ,
@@ -60,8 +131,10 @@ select = [
60
131
  "SLOT", # flake8-slots
61
132
  "T100", # Trace found: {name} used
62
133
  "T20", # flake8-print
134
+ "TD", # flake8-todos
63
135
  "TID", # Tidy imports
64
136
  "TRY", # tryceratops
137
+ "YTT", # flake8-2020
65
138
  ]
66
139
 
67
140
  [tool.ruff.lint.per-file-ignores]
@@ -18,16 +18,20 @@ from .const import (
18
18
  ClimateMode,
19
19
  ColorMode,
20
20
  FanMode,
21
+ HorizontalOscillationAngle,
21
22
  HumidifierAction,
22
23
  HumidifierMode,
23
24
  HumidifierWaterLevel,
24
25
  LockStatus,
26
+ NightLightState,
25
27
  SmartThermostatRadiatorMode,
28
+ StandingFanMode,
26
29
  StripLightColorMode,
27
30
  SwitchbotAccountConnectionError,
28
31
  SwitchbotApiError,
29
32
  SwitchbotAuthenticationError,
30
33
  SwitchbotModel,
34
+ VerticalOscillationAngle,
31
35
  )
32
36
  from .devices.air_purifier import SwitchbotAirPurifier
33
37
  from .devices.art_frame import SwitchbotArtFrame
@@ -44,12 +48,16 @@ from .devices.device import (
44
48
  fetch_cloud_devices,
45
49
  )
46
50
  from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
47
- from .devices.fan import SwitchbotFan
51
+ from .devices.fan import SwitchbotFan, SwitchbotStandingFan
48
52
  from .devices.humidifier import SwitchbotHumidifier
49
53
  from .devices.keypad_vision import SwitchbotKeypadVision
50
54
  from .devices.light_strip import (
55
+ SwitchbotCandleWarmerLamp,
51
56
  SwitchbotLightStrip,
57
+ SwitchbotPermanentOutdoorLight,
52
58
  SwitchbotRgbicLight,
59
+ SwitchbotRgbicNeonLight,
60
+ SwitchbotRgbicwwCeilingLight,
53
61
  SwitchbotStripLight3,
54
62
  )
55
63
  from .devices.lock import SwitchbotLock
@@ -76,11 +84,14 @@ __all__ = [
76
84
  "ColorMode",
77
85
  "FanMode",
78
86
  "GetSwitchbotDevices",
87
+ "HorizontalOscillationAngle",
79
88
  "HumidifierAction",
80
89
  "HumidifierMode",
81
90
  "HumidifierWaterLevel",
82
91
  "LockStatus",
92
+ "NightLightState",
83
93
  "SmartThermostatRadiatorMode",
94
+ "StandingFanMode",
84
95
  "StripLightColorMode",
85
96
  "SwitchBotAdvertisement",
86
97
  "Switchbot",
@@ -93,6 +104,7 @@ __all__ = [
93
104
  "SwitchbotBaseLight",
94
105
  "SwitchbotBlindTilt",
95
106
  "SwitchbotBulb",
107
+ "SwitchbotCandleWarmerLamp",
96
108
  "SwitchbotCeilingLight",
97
109
  "SwitchbotCurtain",
98
110
  "SwitchbotDevice",
@@ -108,17 +120,22 @@ __all__ = [
108
120
  "SwitchbotModel",
109
121
  "SwitchbotModel",
110
122
  "SwitchbotOperationError",
123
+ "SwitchbotPermanentOutdoorLight",
111
124
  "SwitchbotPlugMini",
112
125
  "SwitchbotPlugMini",
113
126
  "SwitchbotRelaySwitch",
114
127
  "SwitchbotRelaySwitch2PM",
115
128
  "SwitchbotRgbicLight",
129
+ "SwitchbotRgbicNeonLight",
130
+ "SwitchbotRgbicwwCeilingLight",
116
131
  "SwitchbotRollerShade",
117
132
  "SwitchbotSmartThermostatRadiator",
133
+ "SwitchbotStandingFan",
118
134
  "SwitchbotStripLight3",
119
135
  "SwitchbotSupportedType",
120
136
  "SwitchbotSupportedType",
121
137
  "SwitchbotVacuum",
138
+ "VerticalOscillationAngle",
122
139
  "close_stale_connections",
123
140
  "close_stale_connections_by_address",
124
141
  "fetch_cloud_devices",
@@ -0,0 +1 @@
1
+ __version__ = "2.3.0"
@@ -20,7 +20,7 @@ from .adv_parsers.ceiling_light import process_woceiling
20
20
  from .adv_parsers.climate_panel import process_climate_panel
21
21
  from .adv_parsers.contact import process_wocontact
22
22
  from .adv_parsers.curtain import process_wocurtain
23
- from .adv_parsers.fan import process_fan
23
+ from .adv_parsers.fan import process_fan, process_standing_fan
24
24
  from .adv_parsers.hub2 import process_wohub2
25
25
  from .adv_parsers.hub3 import process_hub3
26
26
  from .adv_parsers.hubmini_matter import process_hubmini_matter
@@ -28,7 +28,13 @@ from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohu
28
28
  from .adv_parsers.keypad import process_wokeypad
29
29
  from .adv_parsers.keypad_vision import process_keypad_vision, process_keypad_vision_pro
30
30
  from .adv_parsers.leak import process_leak
31
- from .adv_parsers.light_strip import process_light, process_rgbic_light, process_wostrip
31
+ from .adv_parsers.light_strip import (
32
+ process_candle_warmer_lamp,
33
+ process_light,
34
+ process_rgbic_light,
35
+ process_rgbicww_ceiling_light,
36
+ process_wostrip,
37
+ )
32
38
  from .adv_parsers.lock import (
33
39
  process_lock2,
34
40
  process_locklite,
@@ -49,6 +55,7 @@ from .adv_parsers.remote import process_woremote
49
55
  from .adv_parsers.roller_shade import process_worollershade
50
56
  from .adv_parsers.smart_thermostat_radiator import process_smart_thermostat_radiator
51
57
  from .adv_parsers.vacuum import process_vacuum, process_vacuum_k
58
+ from .adv_parsers.weather_station import process_weather_station
52
59
  from .const import SwitchbotModel
53
60
  from .models import SwitchBotAdvertisement
54
61
  from .utils import format_mac_upper
@@ -69,7 +76,7 @@ class SwitchbotSupportedType(TypedDict):
69
76
 
70
77
  modelName: SwitchbotModel
71
78
  modelFriendlyName: str
72
- func: Callable[[bytes, bytes | None], dict[str, bool | int]]
79
+ func: Callable[[bytes | None, bytes | None], dict[str, bool | int | str | None]]
73
80
  manufacturer_id: int | None
74
81
  manufacturer_data_length: int | None
75
82
 
@@ -611,6 +618,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
611
618
  "func": process_light,
612
619
  "manufacturer_id": 2409,
613
620
  },
621
+ b"\x00\x11\x22\xb8": {
622
+ "modelName": SwitchbotModel.CANDLE_WARMER_LAMP,
623
+ "modelFriendlyName": "Candle Warmer Lamp",
624
+ "func": process_candle_warmer_lamp,
625
+ "manufacturer_id": 2409,
626
+ },
627
+ b"\x01\x11\x22\xb8": {
628
+ "modelName": SwitchbotModel.CANDLE_WARMER_LAMP,
629
+ "modelFriendlyName": "Candle Warmer Lamp",
630
+ "func": process_candle_warmer_lamp,
631
+ "manufacturer_id": 2409,
632
+ },
614
633
  b"\x00\x10\xd0\xb1": {
615
634
  "modelName": SwitchbotModel.STRIP_LIGHT_3,
616
635
  "modelFriendlyName": "Strip Light 3",
@@ -659,6 +678,54 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
659
678
  "func": process_rgbic_light,
660
679
  "manufacturer_id": 2409,
661
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
+ },
693
+ b"\x00\x10\xd0\xb7": {
694
+ "modelName": SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
695
+ "modelFriendlyName": "Permanent Outdoor Light",
696
+ "func": process_rgbic_light,
697
+ "manufacturer_id": 2409,
698
+ },
699
+ b"\x01\x10\xd0\xb7": {
700
+ "modelName": SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
701
+ "modelFriendlyName": "Permanent Outdoor Light",
702
+ "func": process_rgbic_light,
703
+ "manufacturer_id": 2409,
704
+ },
705
+ b"\x00\x10\xd0\xb5": {
706
+ "modelName": SwitchbotModel.RGBIC_NEON_WIRE_ROPE_LIGHT,
707
+ "modelFriendlyName": "RGBIC Neon Wire Rope Light",
708
+ "func": process_wostrip,
709
+ "manufacturer_id": 2409,
710
+ },
711
+ b"\x00\x10\xd0\xb6": {
712
+ "modelName": SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
713
+ "modelFriendlyName": "RGBIC Neon Rope Light",
714
+ "func": process_wostrip,
715
+ "manufacturer_id": 2409,
716
+ },
717
+ b"\x01\x10\xd0\xb5": {
718
+ "modelName": SwitchbotModel.RGBIC_NEON_WIRE_ROPE_LIGHT,
719
+ "modelFriendlyName": "RGBIC Neon Wire Rope Light",
720
+ "func": process_wostrip,
721
+ "manufacturer_id": 2409,
722
+ },
723
+ b"\x01\x10\xd0\xb6": {
724
+ "modelName": SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
725
+ "modelFriendlyName": "RGBIC Neon Rope Light",
726
+ "func": process_wostrip,
727
+ "manufacturer_id": 2409,
728
+ },
662
729
  b"\x00\x10\xfb\xa8": {
663
730
  "modelName": SwitchbotModel.K11_VACUUM,
664
731
  "modelFriendlyName": "K11+ Vacuum",
@@ -791,6 +858,30 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
791
858
  "func": process_wolock_pro,
792
859
  "manufacturer_id": 2409,
793
860
  },
861
+ b"\x00\x11\x07\x60": {
862
+ "modelName": SwitchbotModel.STANDING_FAN,
863
+ "modelFriendlyName": "Standing Fan",
864
+ "func": process_standing_fan,
865
+ "manufacturer_id": 2409,
866
+ },
867
+ b"\x01\x11\x07\x60": {
868
+ "modelName": SwitchbotModel.STANDING_FAN,
869
+ "modelFriendlyName": "Standing Fan",
870
+ "func": process_standing_fan,
871
+ "manufacturer_id": 2409,
872
+ },
873
+ b"\x00\x10\x53\xb0": {
874
+ "modelName": SwitchbotModel.WEATHER_STATION,
875
+ "modelFriendlyName": "Weather Station",
876
+ "func": process_weather_station,
877
+ "manufacturer_id": 2409,
878
+ },
879
+ b"\x01\x10\x53\xb0": {
880
+ "modelName": SwitchbotModel.WEATHER_STATION,
881
+ "modelFriendlyName": "Weather Station",
882
+ "func": process_weather_station,
883
+ "manufacturer_id": 2409,
884
+ },
794
885
  }
795
886
 
796
887
  _SWITCHBOT_MODEL_TO_CHAR: defaultdict[SwitchbotModel, list[str | bytes]] = defaultdict(
@@ -0,0 +1,36 @@
1
+ """Shared temperature/humidity decoding helpers for T/H sensors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..helpers import celsius_to_fahrenheit
8
+
9
+
10
+ def decode_temp_humidity(temp_data: bytes, battery: int | None) -> dict[str, Any]:
11
+ """
12
+ Decode temperature/humidity/fahrenheit-flag from a 3-byte payload.
13
+
14
+ Layout (bytes after company ID, for SwitchBot T/H sensors):
15
+ byte 0: bits[3:0] = temperature decimal (0.1 °C units)
16
+ byte 1: bit[7] = temperature sign (1 = positive), bits[6:0] = integer °C
17
+ byte 2: bit[7] = fahrenheit-display flag, bits[6:0] = humidity %
18
+ """
19
+ _temp_sign = 1 if temp_data[1] & 0b10000000 else -1
20
+ _temp_c = _temp_sign * (
21
+ (temp_data[1] & 0b01111111) + ((temp_data[0] & 0b00001111) / 10)
22
+ )
23
+ _temp_f = celsius_to_fahrenheit(_temp_c)
24
+ _temp_f = (_temp_f * 10) / 10
25
+ humidity = temp_data[2] & 0b01111111
26
+
27
+ if _temp_c == 0 and humidity == 0 and battery == 0:
28
+ return {}
29
+
30
+ return {
31
+ "temp": {"c": _temp_c, "f": _temp_f},
32
+ "temperature": _temp_c,
33
+ "fahrenheit": bool(temp_data[2] & 0b10000000),
34
+ "humidity": humidity,
35
+ "battery": battery,
36
+ }
@@ -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)