PySwitchbot 0.60.1__tar.gz → 0.62.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PKG-INFO +1 -1
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PySwitchbot.egg-info/SOURCES.txt +8 -1
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/pyproject.toml +28 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/setup.py +1 -1
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/__init__.py +6 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parser.py +58 -4
- pyswitchbot-0.62.0/switchbot/adv_parsers/air_purifier.py +52 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/hub2.py +1 -3
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/hubmini_matter.py +1 -2
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/meter.py +1 -3
- pyswitchbot-0.62.0/switchbot/adv_parsers/vacuum.py +61 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/const/__init__.py +23 -2
- pyswitchbot-0.62.0/switchbot/const/air_purifier.py +23 -0
- pyswitchbot-0.62.0/switchbot/devices/air_purifier.py +142 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/blind_tilt.py +1 -2
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/device.py +4 -6
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/evaporative_humidifier.py +1 -1
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/fan.py +1 -2
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/relay_switch.py +2 -5
- pyswitchbot-0.62.0/switchbot/devices/vacuum.py +73 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/enum.py +2 -6
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_adv_parser.py +631 -0
- pyswitchbot-0.62.0/tests/test_air_purifier.py +231 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_base_cover.py +2 -2
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_blind_tilt.py +4 -3
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_curtain.py +3 -3
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_evaporative_humidifier.py +1 -1
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_fan.py +7 -5
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_roller_shade.py +1 -1
- pyswitchbot-0.62.0/tests/test_vacuum.py +135 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/LICENSE +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/MANIFEST.in +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/README.md +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/setup.cfg +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/fan.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/const/fan.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/discovery.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/models.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_relay_switch.py +0 -0
|
@@ -16,6 +16,7 @@ switchbot/enum.py
|
|
|
16
16
|
switchbot/helpers.py
|
|
17
17
|
switchbot/models.py
|
|
18
18
|
switchbot/adv_parsers/__init__.py
|
|
19
|
+
switchbot/adv_parsers/air_purifier.py
|
|
19
20
|
switchbot/adv_parsers/blind_tilt.py
|
|
20
21
|
switchbot/adv_parsers/bot.py
|
|
21
22
|
switchbot/adv_parsers/bulb.py
|
|
@@ -36,12 +37,15 @@ switchbot/adv_parsers/plug.py
|
|
|
36
37
|
switchbot/adv_parsers/relay_switch.py
|
|
37
38
|
switchbot/adv_parsers/remote.py
|
|
38
39
|
switchbot/adv_parsers/roller_shade.py
|
|
40
|
+
switchbot/adv_parsers/vacuum.py
|
|
39
41
|
switchbot/const/__init__.py
|
|
42
|
+
switchbot/const/air_purifier.py
|
|
40
43
|
switchbot/const/evaporative_humidifier.py
|
|
41
44
|
switchbot/const/fan.py
|
|
42
45
|
switchbot/const/hub2.py
|
|
43
46
|
switchbot/const/lock.py
|
|
44
47
|
switchbot/devices/__init__.py
|
|
48
|
+
switchbot/devices/air_purifier.py
|
|
45
49
|
switchbot/devices/base_cover.py
|
|
46
50
|
switchbot/devices/base_light.py
|
|
47
51
|
switchbot/devices/blind_tilt.py
|
|
@@ -62,7 +66,9 @@ switchbot/devices/motion.py
|
|
|
62
66
|
switchbot/devices/plug.py
|
|
63
67
|
switchbot/devices/relay_switch.py
|
|
64
68
|
switchbot/devices/roller_shade.py
|
|
69
|
+
switchbot/devices/vacuum.py
|
|
65
70
|
tests/test_adv_parser.py
|
|
71
|
+
tests/test_air_purifier.py
|
|
66
72
|
tests/test_base_cover.py
|
|
67
73
|
tests/test_blind_tilt.py
|
|
68
74
|
tests/test_curtain.py
|
|
@@ -70,4 +76,5 @@ tests/test_evaporative_humidifier.py
|
|
|
70
76
|
tests/test_fan.py
|
|
71
77
|
tests/test_hub2.py
|
|
72
78
|
tests/test_relay_switch.py
|
|
73
|
-
tests/test_roller_shade.py
|
|
79
|
+
tests/test_roller_shade.py
|
|
80
|
+
tests/test_vacuum.py
|
|
@@ -28,8 +28,13 @@ ignore = [
|
|
|
28
28
|
"UP038", # Use `X | Y` in `isinstance` is slower
|
|
29
29
|
"S603", # check for execution of untrusted input
|
|
30
30
|
"S105", # possible hard coded creds
|
|
31
|
+
"TID252", # not for this lib
|
|
32
|
+
"TRY003", # nice but too many to fix,
|
|
33
|
+
"G201", # too noisy
|
|
34
|
+
"PLR2004", # too many to fix
|
|
31
35
|
]
|
|
32
36
|
select = [
|
|
37
|
+
"ASYNC", # async rules
|
|
33
38
|
"B", # flake8-bugbear
|
|
34
39
|
"D", # flake8-docstrings
|
|
35
40
|
"C4", # flake8-comprehensions
|
|
@@ -40,6 +45,24 @@ select = [
|
|
|
40
45
|
"UP", # pyupgrade
|
|
41
46
|
"I", # isort
|
|
42
47
|
"RUF", # ruff specific
|
|
48
|
+
"FLY", # flynt
|
|
49
|
+
"G", # flake8-logging-format ,
|
|
50
|
+
"PERF", # Perflint
|
|
51
|
+
"PGH", # pygrep-hooks
|
|
52
|
+
"PIE", # flake8-pie
|
|
53
|
+
"PL", # pylint
|
|
54
|
+
"PT", # flake8-pytest-style
|
|
55
|
+
"PTH", # flake8-pathlib
|
|
56
|
+
"PYI", # flake8-pyi
|
|
57
|
+
"RET", # flake8-return
|
|
58
|
+
"RSE", # flake8-raise ,
|
|
59
|
+
"SIM", # flake8-simplify
|
|
60
|
+
"SLF", # flake8-self
|
|
61
|
+
"SLOT", # flake8-slots
|
|
62
|
+
"T100", # Trace found: {name} used
|
|
63
|
+
"T20", # flake8-print
|
|
64
|
+
"TID", # Tidy imports
|
|
65
|
+
"TRY", # tryceratops
|
|
43
66
|
]
|
|
44
67
|
|
|
45
68
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -50,10 +73,15 @@ select = [
|
|
|
50
73
|
"D103",
|
|
51
74
|
"D104",
|
|
52
75
|
"S101",
|
|
76
|
+
"SLF001",
|
|
77
|
+
"PLR2004",
|
|
53
78
|
]
|
|
54
79
|
"setup.py" = ["D100"]
|
|
55
80
|
"conftest.py" = ["D100"]
|
|
56
81
|
"docs/conf.py" = ["D100"]
|
|
82
|
+
"scripts/**/*" = [
|
|
83
|
+
"T201"
|
|
84
|
+
]
|
|
57
85
|
|
|
58
86
|
[tool.ruff.lint.isort]
|
|
59
87
|
known-first-party = ["pySwitchbot", "tests"]
|
|
@@ -10,6 +10,7 @@ from bleak_retry_connector import (
|
|
|
10
10
|
|
|
11
11
|
from .adv_parser import SwitchbotSupportedType, parse_advertisement_data
|
|
12
12
|
from .const import (
|
|
13
|
+
AirPurifierMode,
|
|
13
14
|
FanMode,
|
|
14
15
|
LockStatus,
|
|
15
16
|
SwitchbotAccountConnectionError,
|
|
@@ -17,6 +18,7 @@ from .const import (
|
|
|
17
18
|
SwitchbotAuthenticationError,
|
|
18
19
|
SwitchbotModel,
|
|
19
20
|
)
|
|
21
|
+
from .devices.air_purifier import SwitchbotAirPurifier
|
|
20
22
|
from .devices.base_light import SwitchbotBaseLight
|
|
21
23
|
from .devices.blind_tilt import SwitchbotBlindTilt
|
|
22
24
|
from .devices.bot import Switchbot
|
|
@@ -32,10 +34,12 @@ from .devices.lock import SwitchbotLock
|
|
|
32
34
|
from .devices.plug import SwitchbotPlugMini
|
|
33
35
|
from .devices.relay_switch import SwitchbotRelaySwitch
|
|
34
36
|
from .devices.roller_shade import SwitchbotRollerShade
|
|
37
|
+
from .devices.vacuum import SwitchbotVacuum
|
|
35
38
|
from .discovery import GetSwitchbotDevices
|
|
36
39
|
from .models import SwitchBotAdvertisement
|
|
37
40
|
|
|
38
41
|
__all__ = [
|
|
42
|
+
"AirPurifierMode",
|
|
39
43
|
"ColorMode",
|
|
40
44
|
"FanMode",
|
|
41
45
|
"GetSwitchbotDevices",
|
|
@@ -44,6 +48,7 @@ __all__ = [
|
|
|
44
48
|
"Switchbot",
|
|
45
49
|
"Switchbot",
|
|
46
50
|
"SwitchbotAccountConnectionError",
|
|
51
|
+
"SwitchbotAirPurifier",
|
|
47
52
|
"SwitchbotApiError",
|
|
48
53
|
"SwitchbotAuthenticationError",
|
|
49
54
|
"SwitchbotBaseLight",
|
|
@@ -66,6 +71,7 @@ __all__ = [
|
|
|
66
71
|
"SwitchbotRollerShade",
|
|
67
72
|
"SwitchbotSupportedType",
|
|
68
73
|
"SwitchbotSupportedType",
|
|
74
|
+
"SwitchbotVacuum",
|
|
69
75
|
"close_stale_connections",
|
|
70
76
|
"close_stale_connections_by_address",
|
|
71
77
|
"get_device",
|
|
@@ -10,6 +10,7 @@ from typing import Any, TypedDict
|
|
|
10
10
|
from bleak.backends.device import BLEDevice
|
|
11
11
|
from bleak.backends.scanner import AdvertisementData
|
|
12
12
|
|
|
13
|
+
from .adv_parsers.air_purifier import process_air_purifier
|
|
13
14
|
from .adv_parsers.blind_tilt import process_woblindtilt
|
|
14
15
|
from .adv_parsers.bot import process_wohand
|
|
15
16
|
from .adv_parsers.bulb import process_color_bulb
|
|
@@ -33,6 +34,7 @@ from .adv_parsers.relay_switch import (
|
|
|
33
34
|
)
|
|
34
35
|
from .adv_parsers.remote import process_woremote
|
|
35
36
|
from .adv_parsers.roller_shade import process_worollershade
|
|
37
|
+
from .adv_parsers.vacuum import process_vacuum, process_vacuum_k
|
|
36
38
|
from .const import SwitchbotModel
|
|
37
39
|
from .models import SwitchBotAdvertisement
|
|
38
40
|
|
|
@@ -237,6 +239,60 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
|
|
|
237
239
|
"func": process_fan,
|
|
238
240
|
"manufacturer_id": 2409,
|
|
239
241
|
},
|
|
242
|
+
".": {
|
|
243
|
+
"modelName": SwitchbotModel.K20_VACUUM,
|
|
244
|
+
"modelFriendlyName": "K20 Vacuum",
|
|
245
|
+
"func": process_vacuum,
|
|
246
|
+
"manufacturer_id": 2409,
|
|
247
|
+
},
|
|
248
|
+
"z": {
|
|
249
|
+
"modelName": SwitchbotModel.S10_VACUUM,
|
|
250
|
+
"modelFriendlyName": "S10 Vacuum",
|
|
251
|
+
"func": process_vacuum,
|
|
252
|
+
"manufacturer_id": 2409,
|
|
253
|
+
},
|
|
254
|
+
"3": {
|
|
255
|
+
"modelName": SwitchbotModel.K10_PRO_COMBO_VACUUM,
|
|
256
|
+
"modelFriendlyName": "K10+ Pro Combo Vacuum",
|
|
257
|
+
"func": process_vacuum,
|
|
258
|
+
"manufacturer_id": 2409,
|
|
259
|
+
},
|
|
260
|
+
"}": {
|
|
261
|
+
"modelName": SwitchbotModel.K10_VACUUM,
|
|
262
|
+
"modelFriendlyName": "K10+ Vacuum",
|
|
263
|
+
"func": process_vacuum_k,
|
|
264
|
+
"manufacturer_id": 2409,
|
|
265
|
+
},
|
|
266
|
+
"(": {
|
|
267
|
+
"modelName": SwitchbotModel.K10_PRO_VACUUM,
|
|
268
|
+
"modelFriendlyName": "K10+ Pro Vacuum",
|
|
269
|
+
"func": process_vacuum_k,
|
|
270
|
+
"manufacturer_id": 2409,
|
|
271
|
+
},
|
|
272
|
+
"*": {
|
|
273
|
+
"modelName": SwitchbotModel.AIR_PURIFIER,
|
|
274
|
+
"modelFriendlyName": "Air Purifier",
|
|
275
|
+
"func": process_air_purifier,
|
|
276
|
+
"manufacturer_id": 2409,
|
|
277
|
+
},
|
|
278
|
+
"+": {
|
|
279
|
+
"modelName": SwitchbotModel.AIR_PURIFIER,
|
|
280
|
+
"modelFriendlyName": "Air Purifier",
|
|
281
|
+
"func": process_air_purifier,
|
|
282
|
+
"manufacturer_id": 2409,
|
|
283
|
+
},
|
|
284
|
+
"7": {
|
|
285
|
+
"modelName": SwitchbotModel.AIR_PURIFIER,
|
|
286
|
+
"modelFriendlyName": "Air Purifier",
|
|
287
|
+
"func": process_air_purifier,
|
|
288
|
+
"manufacturer_id": 2409,
|
|
289
|
+
},
|
|
290
|
+
"8": {
|
|
291
|
+
"modelName": SwitchbotModel.AIR_PURIFIER,
|
|
292
|
+
"modelFriendlyName": "Air Purifier",
|
|
293
|
+
"func": process_air_purifier,
|
|
294
|
+
"manufacturer_id": 2409,
|
|
295
|
+
},
|
|
240
296
|
}
|
|
241
297
|
|
|
242
298
|
_SWITCHBOT_MODEL_TO_CHAR = {
|
|
@@ -285,10 +341,8 @@ def parse_advertisement_data(
|
|
|
285
341
|
_mfr_id,
|
|
286
342
|
model,
|
|
287
343
|
)
|
|
288
|
-
except Exception
|
|
289
|
-
_LOGGER.exception(
|
|
290
|
-
"Failed to parse advertisement data: %s: %s", advertisement_data, err
|
|
291
|
-
)
|
|
344
|
+
except Exception: # pylint: disable=broad-except
|
|
345
|
+
_LOGGER.exception("Failed to parse advertisement data: %s", advertisement_data)
|
|
292
346
|
return None
|
|
293
347
|
|
|
294
348
|
if not data:
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Air Purifier adv parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
|
|
7
|
+
from ..const.air_purifier import AirPurifierMode, AirQualityLevel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def process_air_purifier(
|
|
11
|
+
data: bytes | None, mfr_data: bytes | None
|
|
12
|
+
) -> dict[str, bool | int]:
|
|
13
|
+
"""Process air purifier services data."""
|
|
14
|
+
if mfr_data is None:
|
|
15
|
+
return {}
|
|
16
|
+
device_data = mfr_data[6:]
|
|
17
|
+
|
|
18
|
+
_seq_num = device_data[0]
|
|
19
|
+
_isOn = bool(device_data[1] & 0b10000000)
|
|
20
|
+
_mode = device_data[1] & 0b00000111
|
|
21
|
+
_is_aqi_valid = bool(device_data[2] & 0b00000100)
|
|
22
|
+
_child_lock = bool(device_data[2] & 0b00000010)
|
|
23
|
+
_speed = device_data[3] & 0b01111111
|
|
24
|
+
_aqi_level = (device_data[4] & 0b00000110) >> 1
|
|
25
|
+
_aqi_level = AirQualityLevel(_aqi_level).name.lower()
|
|
26
|
+
_work_time = struct.unpack(">H", device_data[5:7])[0]
|
|
27
|
+
_err_code = device_data[7]
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
"isOn": _isOn,
|
|
31
|
+
"mode": get_air_purifier_mode(_mode, _speed),
|
|
32
|
+
"isAqiValid": _is_aqi_valid,
|
|
33
|
+
"child_lock": _child_lock,
|
|
34
|
+
"speed": _speed,
|
|
35
|
+
"aqi_level": _aqi_level,
|
|
36
|
+
"filter element working time": _work_time,
|
|
37
|
+
"err_code": _err_code,
|
|
38
|
+
"sequence_number": _seq_num,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_air_purifier_mode(mode: int, speed: int) -> str | None:
|
|
43
|
+
if mode == 1:
|
|
44
|
+
if 0 <= speed <= 33:
|
|
45
|
+
return "level_1"
|
|
46
|
+
if 34 <= speed <= 66:
|
|
47
|
+
return "level_2"
|
|
48
|
+
return "level_3"
|
|
49
|
+
if 1 < mode <= 4:
|
|
50
|
+
mode += 2
|
|
51
|
+
return AirPurifierMode(mode).name.lower()
|
|
52
|
+
return None
|
|
@@ -30,7 +30,7 @@ def process_wohub2(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]
|
|
|
30
30
|
if _temp_c == 0 and humidity == 0:
|
|
31
31
|
return {}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
return {
|
|
34
34
|
# Data should be flat, but we keep the original structure for now
|
|
35
35
|
"temp": {"c": _temp_c, "f": _temp_f},
|
|
36
36
|
"temperature": _temp_c,
|
|
@@ -40,8 +40,6 @@ def process_wohub2(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]
|
|
|
40
40
|
"illuminance": calculate_light_intensity(light_level),
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
return _wohub2_data
|
|
44
|
-
|
|
45
43
|
|
|
46
44
|
def calculate_light_intensity(light_level: int) -> int:
|
|
47
45
|
"""
|
|
@@ -28,10 +28,9 @@ def process_hubmini_matter(
|
|
|
28
28
|
if _temp_c == 0 and humidity == 0:
|
|
29
29
|
return {}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
return {
|
|
32
32
|
"temp": {"c": _temp_c, "f": _temp_f},
|
|
33
33
|
"temperature": _temp_c,
|
|
34
34
|
"fahrenheit": bool(temp_data[2] & 0b10000000),
|
|
35
35
|
"humidity": humidity,
|
|
36
36
|
}
|
|
37
|
-
return paraser_data
|
|
@@ -35,7 +35,7 @@ def process_wosensorth(data: bytes | None, mfr_data: bytes | None) -> dict[str,
|
|
|
35
35
|
if _temp_c == 0 and humidity == 0 and battery == 0:
|
|
36
36
|
return {}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
return {
|
|
39
39
|
# Data should be flat, but we keep the original structure for now
|
|
40
40
|
"temp": {"c": _temp_c, "f": _temp_f},
|
|
41
41
|
"temperature": _temp_c,
|
|
@@ -44,8 +44,6 @@ def process_wosensorth(data: bytes | None, mfr_data: bytes | None) -> dict[str,
|
|
|
44
44
|
"battery": battery,
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
return _wosensorth_data
|
|
48
|
-
|
|
49
47
|
|
|
50
48
|
def process_wosensorth_c(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]:
|
|
51
49
|
"""Process woSensorTH/Temp sensor services data with CO2."""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Vacuum parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def process_vacuum(
|
|
9
|
+
data: bytes | None, mfr_data: bytes | None
|
|
10
|
+
) -> dict[str, bool | int | str]:
|
|
11
|
+
"""Support for s10, k10+ pro combo, k20 process service data."""
|
|
12
|
+
if mfr_data is None:
|
|
13
|
+
return {}
|
|
14
|
+
|
|
15
|
+
_seq_num = mfr_data[6]
|
|
16
|
+
_soc_version = get_device_fw_version(mfr_data[8:11])
|
|
17
|
+
# Steps at the end of the last network configuration
|
|
18
|
+
_step = mfr_data[11] & 0b00001111
|
|
19
|
+
_mqtt_connected = bool(mfr_data[11] & 0b00010000)
|
|
20
|
+
_battery = mfr_data[12]
|
|
21
|
+
_work_status = mfr_data[13] & 0b00111111
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
"sequence_number": _seq_num,
|
|
25
|
+
"soc_version": _soc_version,
|
|
26
|
+
"step": _step,
|
|
27
|
+
"mqtt_connected": _mqtt_connected,
|
|
28
|
+
"battery": _battery,
|
|
29
|
+
"work_status": _work_status,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_device_fw_version(version_bytes: bytes) -> str | None:
|
|
34
|
+
version1 = version_bytes[0] & 0x0F
|
|
35
|
+
version2 = version_bytes[0] >> 4
|
|
36
|
+
version3 = struct.unpack("<H", version_bytes[1:])[0]
|
|
37
|
+
return f"{version1}.{version2}.{version3:>03d}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def process_vacuum_k(
|
|
41
|
+
data: bytes | None, mfr_data: bytes | None
|
|
42
|
+
) -> dict[str, bool | int | str]:
|
|
43
|
+
"""Support for k10+, k10+ pro process service data."""
|
|
44
|
+
if mfr_data is None:
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
_seq_num = mfr_data[6]
|
|
48
|
+
_dustbin_bound = bool(mfr_data[7] & 0b10000000)
|
|
49
|
+
_dusbin_connected = bool(mfr_data[7] & 0b01000000)
|
|
50
|
+
_network_connected = bool(mfr_data[7] & 0b00100000)
|
|
51
|
+
_work_status = (mfr_data[7] & 0b00010000) >> 4
|
|
52
|
+
_battery = mfr_data[8] & 0b01111111
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
"sequence_number": _seq_num,
|
|
56
|
+
"dustbin_bound": _dustbin_bound,
|
|
57
|
+
"dusbin_connected": _dusbin_connected,
|
|
58
|
+
"network_connected": _network_connected,
|
|
59
|
+
"work_status": _work_status,
|
|
60
|
+
"battery": _battery,
|
|
61
|
+
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from ..enum import StrEnum
|
|
6
|
-
from .
|
|
6
|
+
from .air_purifier import AirPurifierMode
|
|
7
|
+
from .fan import FanMode
|
|
7
8
|
|
|
8
9
|
# Preserve old LockStatus export for backwards compatibility
|
|
9
|
-
from .lock import LockStatus
|
|
10
|
+
from .lock import LockStatus
|
|
10
11
|
|
|
11
12
|
DEFAULT_RETRY_COUNT = 3
|
|
12
13
|
DEFAULT_RETRY_TIMEOUT = 1
|
|
@@ -67,3 +68,23 @@ class SwitchbotModel(StrEnum):
|
|
|
67
68
|
ROLLER_SHADE = "Roller Shade"
|
|
68
69
|
HUBMINI_MATTER = "HubMini Matter"
|
|
69
70
|
CIRCULATOR_FAN = "Circulator Fan"
|
|
71
|
+
K20_VACUUM = "K20 Vacuum"
|
|
72
|
+
S10_VACUUM = "S10 Vacuum"
|
|
73
|
+
K10_VACUUM = "K10+ Vacuum"
|
|
74
|
+
K10_PRO_VACUUM = "K10+ Pro Vacuum"
|
|
75
|
+
K10_PRO_COMBO_VACUUM = "K10+ Pro Combo Vacuum"
|
|
76
|
+
AIR_PURIFIER = "Air Purifier"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"DEFAULT_RETRY_COUNT",
|
|
81
|
+
"DEFAULT_RETRY_TIMEOUT",
|
|
82
|
+
"DEFAULT_SCAN_TIMEOUT",
|
|
83
|
+
"AirPurifierMode",
|
|
84
|
+
"FanMode",
|
|
85
|
+
"LockStatus",
|
|
86
|
+
"SwitchbotAccountConnectionError",
|
|
87
|
+
"SwitchbotApiError",
|
|
88
|
+
"SwitchbotAuthenticationError",
|
|
89
|
+
"SwitchbotModel",
|
|
90
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AirPurifierMode(Enum):
|
|
7
|
+
LEVEL_1 = 1
|
|
8
|
+
LEVEL_2 = 2
|
|
9
|
+
LEVEL_3 = 3
|
|
10
|
+
AUTO = 4
|
|
11
|
+
PET = 5
|
|
12
|
+
SLEEP = 6
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def get_modes(cls) -> list[str]:
|
|
16
|
+
return [mode.name.lower() for mode in cls]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AirQualityLevel(Enum):
|
|
20
|
+
EXCELLENT = 0
|
|
21
|
+
GOOD = 1
|
|
22
|
+
MODERATE = 2
|
|
23
|
+
UNHEALTHY = 3
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Library to handle connection with Switchbot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import struct
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from bleak.backends.device import BLEDevice
|
|
10
|
+
|
|
11
|
+
from ..adv_parsers.air_purifier import get_air_purifier_mode
|
|
12
|
+
from ..const import SwitchbotModel
|
|
13
|
+
from ..const.air_purifier import AirPurifierMode, AirQualityLevel
|
|
14
|
+
from .device import (
|
|
15
|
+
SwitchbotEncryptedDevice,
|
|
16
|
+
SwitchbotSequenceDevice,
|
|
17
|
+
update_after_operation,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
COMMAND_HEAD = "570f4c"
|
|
24
|
+
COMMAND_TURN_OFF = f"{COMMAND_HEAD}010000"
|
|
25
|
+
COMMAND_TURN_ON = f"{COMMAND_HEAD}010100"
|
|
26
|
+
COMMAND_SET_MODE = {
|
|
27
|
+
AirPurifierMode.LEVEL_1.name.lower(): f"{COMMAND_HEAD}01010100",
|
|
28
|
+
AirPurifierMode.LEVEL_2.name.lower(): f"{COMMAND_HEAD}01010132",
|
|
29
|
+
AirPurifierMode.LEVEL_3.name.lower(): f"{COMMAND_HEAD}01010164",
|
|
30
|
+
AirPurifierMode.AUTO.name.lower(): f"{COMMAND_HEAD}01010200",
|
|
31
|
+
AirPurifierMode.PET.name.lower(): f"{COMMAND_HEAD}01010300",
|
|
32
|
+
AirPurifierMode.SLEEP.name.lower(): f"{COMMAND_HEAD}01010400",
|
|
33
|
+
}
|
|
34
|
+
DEVICE_GET_BASIC_SETTINGS_KEY = "570f4d81"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SwitchbotAirPurifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
38
|
+
"""Representation of a Switchbot Air Purifier."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
device: BLEDevice,
|
|
43
|
+
key_id: str,
|
|
44
|
+
encryption_key: str,
|
|
45
|
+
interface: int = 0,
|
|
46
|
+
model: SwitchbotModel = SwitchbotModel.AIR_PURIFIER,
|
|
47
|
+
**kwargs: Any,
|
|
48
|
+
) -> None:
|
|
49
|
+
super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
async def verify_encryption_key(
|
|
53
|
+
cls,
|
|
54
|
+
device: BLEDevice,
|
|
55
|
+
key_id: str,
|
|
56
|
+
encryption_key: str,
|
|
57
|
+
model: SwitchbotModel = SwitchbotModel.AIR_PURIFIER,
|
|
58
|
+
**kwargs: Any,
|
|
59
|
+
) -> bool:
|
|
60
|
+
return await super().verify_encryption_key(
|
|
61
|
+
device, key_id, encryption_key, model, **kwargs
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
65
|
+
"""Get device basic settings."""
|
|
66
|
+
if not (_data := await self._get_basic_info()):
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
_LOGGER.debug("data: %s", _data)
|
|
70
|
+
isOn = bool(_data[2] & 0b10000000)
|
|
71
|
+
version_info = (_data[2] & 0b00110000) >> 4
|
|
72
|
+
_mode = _data[2] & 0b00000111
|
|
73
|
+
isAqiValid = bool(_data[3] & 0b00000100)
|
|
74
|
+
child_lock = bool(_data[3] & 0b00000010)
|
|
75
|
+
_aqi_level = (_data[4] & 0b00000110) >> 1
|
|
76
|
+
aqi_level = AirQualityLevel(_aqi_level).name.lower()
|
|
77
|
+
speed = _data[6] & 0b01111111
|
|
78
|
+
pm25 = struct.unpack("<H", _data[12:14])[0] & 0xFFF
|
|
79
|
+
firmware = _data[15] / 10.0
|
|
80
|
+
mode = get_air_purifier_mode(_mode, speed)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"isOn": isOn,
|
|
84
|
+
"version_info": version_info,
|
|
85
|
+
"mode": mode,
|
|
86
|
+
"isAqiValid": isAqiValid,
|
|
87
|
+
"child_lock": child_lock,
|
|
88
|
+
"aqi_level": aqi_level,
|
|
89
|
+
"speed": speed,
|
|
90
|
+
"pm25": pm25,
|
|
91
|
+
"firmware": firmware,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async def _get_basic_info(self) -> bytes | None:
|
|
95
|
+
"""Return basic info of device."""
|
|
96
|
+
_data = await self._send_command(
|
|
97
|
+
key=DEVICE_GET_BASIC_SETTINGS_KEY, retry=self._retry_count
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if _data in (b"\x07", b"\x00"):
|
|
101
|
+
_LOGGER.error("Unsuccessful, please try again")
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
return _data
|
|
105
|
+
|
|
106
|
+
@update_after_operation
|
|
107
|
+
async def set_preset_mode(self, preset_mode: str) -> bool:
|
|
108
|
+
"""Send command to set air purifier preset_mode."""
|
|
109
|
+
result = await self._send_command(COMMAND_SET_MODE[preset_mode])
|
|
110
|
+
return self._check_command_result(result, 0, {1})
|
|
111
|
+
|
|
112
|
+
@update_after_operation
|
|
113
|
+
async def turn_on(self) -> bool:
|
|
114
|
+
"""Turn on the air purifier."""
|
|
115
|
+
result = await self._send_command(COMMAND_TURN_ON)
|
|
116
|
+
return self._check_command_result(result, 0, {1})
|
|
117
|
+
|
|
118
|
+
@update_after_operation
|
|
119
|
+
async def turn_off(self) -> bool:
|
|
120
|
+
"""Turn off the air purifier."""
|
|
121
|
+
result = await self._send_command(COMMAND_TURN_OFF)
|
|
122
|
+
return self._check_command_result(result, 0, {1})
|
|
123
|
+
|
|
124
|
+
def get_current_percentage(self) -> Any:
|
|
125
|
+
"""Return cached percentage."""
|
|
126
|
+
return self._get_adv_value("speed")
|
|
127
|
+
|
|
128
|
+
def is_on(self) -> bool | None:
|
|
129
|
+
"""Return air purifier state from cache."""
|
|
130
|
+
return self._get_adv_value("isOn")
|
|
131
|
+
|
|
132
|
+
def get_current_aqi_level(self) -> Any:
|
|
133
|
+
"""Return cached aqi level."""
|
|
134
|
+
return self._get_adv_value("aqi_level")
|
|
135
|
+
|
|
136
|
+
def get_current_pm25(self) -> Any:
|
|
137
|
+
"""Return cached pm25."""
|
|
138
|
+
return self._get_adv_value("pm25")
|
|
139
|
+
|
|
140
|
+
def get_current_mode(self) -> Any:
|
|
141
|
+
"""Return cached mode."""
|
|
142
|
+
return self._get_adv_value("mode")
|
|
@@ -100,8 +100,7 @@ class SwitchbotBlindTilt(SwitchbotBaseCover, SwitchbotSequenceDevice):
|
|
|
100
100
|
"""Send close command."""
|
|
101
101
|
if self.get_position() > 50:
|
|
102
102
|
return await self.close_up()
|
|
103
|
-
|
|
104
|
-
return await self.close_down()
|
|
103
|
+
return await self.close_down()
|
|
105
104
|
|
|
106
105
|
def get_position(self) -> Any:
|
|
107
106
|
"""Return cached tilt (0-100) of Blind Tilt."""
|
|
@@ -646,7 +646,7 @@ class SwitchbotBaseDevice:
|
|
|
646
646
|
"""
|
|
647
647
|
if not self._sb_adv_data:
|
|
648
648
|
_LOGGER.exception("No advertisement data to update")
|
|
649
|
-
return
|
|
649
|
+
return None
|
|
650
650
|
old_data = self._sb_adv_data.data.get("data") or {}
|
|
651
651
|
merged_data = _merge_data(old_data, new_data)
|
|
652
652
|
if merged_data == old_data:
|
|
@@ -688,9 +688,7 @@ class SwitchbotBaseDevice:
|
|
|
688
688
|
):
|
|
689
689
|
return False
|
|
690
690
|
time_since_last_full_update = time.monotonic() - self._last_full_update
|
|
691
|
-
|
|
692
|
-
return False
|
|
693
|
-
return True
|
|
691
|
+
return not time_since_last_full_update < PASSIVE_POLL_INTERVAL
|
|
694
692
|
|
|
695
693
|
|
|
696
694
|
class SwitchbotDevice(SwitchbotBaseDevice):
|
|
@@ -723,11 +721,11 @@ class SwitchbotEncryptedDevice(SwitchbotDevice):
|
|
|
723
721
|
"""Switchbot base class constructor for encrypted devices."""
|
|
724
722
|
if len(key_id) == 0:
|
|
725
723
|
raise ValueError("key_id is missing")
|
|
726
|
-
|
|
724
|
+
if len(key_id) != 2:
|
|
727
725
|
raise ValueError("key_id is invalid")
|
|
728
726
|
if len(encryption_key) == 0:
|
|
729
727
|
raise ValueError("encryption_key is missing")
|
|
730
|
-
|
|
728
|
+
if len(encryption_key) != 32:
|
|
731
729
|
raise ValueError("encryption_key is invalid")
|
|
732
730
|
self._key_id = key_id
|
|
733
731
|
self._encryption_key = bytearray.fromhex(encryption_key)
|
|
@@ -117,7 +117,7 @@ class SwitchbotEvaporativeHumidifier(SwitchbotEncryptedDevice):
|
|
|
117
117
|
"""Set device mode."""
|
|
118
118
|
if mode == HumidifierMode.DRYING_FILTER:
|
|
119
119
|
return await self.start_drying_filter()
|
|
120
|
-
|
|
120
|
+
if mode not in MODES_COMMANDS:
|
|
121
121
|
raise ValueError("Invalid mode")
|
|
122
122
|
|
|
123
123
|
command = COMMAND_SET_MODE + MODES_COMMANDS[mode]
|