PySwitchbot 0.60.0__tar.gz → 0.61.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.0 → pyswitchbot-0.61.0}/PKG-INFO +1 -1
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/PySwitchbot.egg-info/SOURCES.txt +4 -1
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/pyproject.toml +28 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/setup.py +1 -1
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/__init__.py +2 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parser.py +33 -4
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/fan.py +1 -1
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/hub2.py +1 -3
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/hubmini_matter.py +1 -2
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/meter.py +1 -3
- pyswitchbot-0.61.0/switchbot/adv_parsers/vacuum.py +61 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/const/__init__.py +20 -2
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/const/fan.py +1 -1
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/blind_tilt.py +1 -2
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/device.py +4 -6
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/evaporative_humidifier.py +1 -1
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/fan.py +6 -7
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/relay_switch.py +2 -5
- pyswitchbot-0.61.0/switchbot/devices/vacuum.py +73 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/enum.py +2 -6
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/tests/test_adv_parser.py +448 -2
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/tests/test_base_cover.py +2 -2
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/tests/test_blind_tilt.py +4 -3
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/tests/test_curtain.py +3 -3
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/tests/test_evaporative_humidifier.py +1 -1
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/tests/test_fan.py +13 -11
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/tests/test_roller_shade.py +1 -1
- pyswitchbot-0.61.0/tests/test_vacuum.py +135 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/LICENSE +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/MANIFEST.in +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/README.md +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/setup.cfg +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/discovery.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/switchbot/models.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.60.0 → pyswitchbot-0.61.0}/tests/test_relay_switch.py +0 -0
|
@@ -36,6 +36,7 @@ switchbot/adv_parsers/plug.py
|
|
|
36
36
|
switchbot/adv_parsers/relay_switch.py
|
|
37
37
|
switchbot/adv_parsers/remote.py
|
|
38
38
|
switchbot/adv_parsers/roller_shade.py
|
|
39
|
+
switchbot/adv_parsers/vacuum.py
|
|
39
40
|
switchbot/const/__init__.py
|
|
40
41
|
switchbot/const/evaporative_humidifier.py
|
|
41
42
|
switchbot/const/fan.py
|
|
@@ -62,6 +63,7 @@ switchbot/devices/motion.py
|
|
|
62
63
|
switchbot/devices/plug.py
|
|
63
64
|
switchbot/devices/relay_switch.py
|
|
64
65
|
switchbot/devices/roller_shade.py
|
|
66
|
+
switchbot/devices/vacuum.py
|
|
65
67
|
tests/test_adv_parser.py
|
|
66
68
|
tests/test_base_cover.py
|
|
67
69
|
tests/test_blind_tilt.py
|
|
@@ -70,4 +72,5 @@ tests/test_evaporative_humidifier.py
|
|
|
70
72
|
tests/test_fan.py
|
|
71
73
|
tests/test_hub2.py
|
|
72
74
|
tests/test_relay_switch.py
|
|
73
|
-
tests/test_roller_shade.py
|
|
75
|
+
tests/test_roller_shade.py
|
|
76
|
+
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"]
|
|
@@ -32,6 +32,7 @@ from .devices.lock import SwitchbotLock
|
|
|
32
32
|
from .devices.plug import SwitchbotPlugMini
|
|
33
33
|
from .devices.relay_switch import SwitchbotRelaySwitch
|
|
34
34
|
from .devices.roller_shade import SwitchbotRollerShade
|
|
35
|
+
from .devices.vacuum import SwitchbotVacuum
|
|
35
36
|
from .discovery import GetSwitchbotDevices
|
|
36
37
|
from .models import SwitchBotAdvertisement
|
|
37
38
|
|
|
@@ -66,6 +67,7 @@ __all__ = [
|
|
|
66
67
|
"SwitchbotRollerShade",
|
|
67
68
|
"SwitchbotSupportedType",
|
|
68
69
|
"SwitchbotSupportedType",
|
|
70
|
+
"SwitchbotVacuum",
|
|
69
71
|
"close_stale_connections",
|
|
70
72
|
"close_stale_connections_by_address",
|
|
71
73
|
"get_device",
|
|
@@ -33,6 +33,7 @@ from .adv_parsers.relay_switch import (
|
|
|
33
33
|
)
|
|
34
34
|
from .adv_parsers.remote import process_woremote
|
|
35
35
|
from .adv_parsers.roller_shade import process_worollershade
|
|
36
|
+
from .adv_parsers.vacuum import process_vacuum, process_vacuum_k
|
|
36
37
|
from .const import SwitchbotModel
|
|
37
38
|
from .models import SwitchBotAdvertisement
|
|
38
39
|
|
|
@@ -237,6 +238,36 @@ SUPPORTED_TYPES: dict[str, SwitchbotSupportedType] = {
|
|
|
237
238
|
"func": process_fan,
|
|
238
239
|
"manufacturer_id": 2409,
|
|
239
240
|
},
|
|
241
|
+
".": {
|
|
242
|
+
"modelName": SwitchbotModel.K20_VACUUM,
|
|
243
|
+
"modelFriendlyName": "K20 Vacuum",
|
|
244
|
+
"func": process_vacuum,
|
|
245
|
+
"manufacturer_id": 2409,
|
|
246
|
+
},
|
|
247
|
+
"z": {
|
|
248
|
+
"modelName": SwitchbotModel.S10_VACUUM,
|
|
249
|
+
"modelFriendlyName": "S10 Vacuum",
|
|
250
|
+
"func": process_vacuum,
|
|
251
|
+
"manufacturer_id": 2409,
|
|
252
|
+
},
|
|
253
|
+
"3": {
|
|
254
|
+
"modelName": SwitchbotModel.K10_PRO_COMBO_VACUUM,
|
|
255
|
+
"modelFriendlyName": "K10+ Pro Combo Vacuum",
|
|
256
|
+
"func": process_vacuum,
|
|
257
|
+
"manufacturer_id": 2409,
|
|
258
|
+
},
|
|
259
|
+
"}": {
|
|
260
|
+
"modelName": SwitchbotModel.K10_VACUUM,
|
|
261
|
+
"modelFriendlyName": "K10+ Vacuum",
|
|
262
|
+
"func": process_vacuum_k,
|
|
263
|
+
"manufacturer_id": 2409,
|
|
264
|
+
},
|
|
265
|
+
"(": {
|
|
266
|
+
"modelName": SwitchbotModel.K10_PRO_VACUUM,
|
|
267
|
+
"modelFriendlyName": "K10+ Pro Vacuum",
|
|
268
|
+
"func": process_vacuum_k,
|
|
269
|
+
"manufacturer_id": 2409,
|
|
270
|
+
},
|
|
240
271
|
}
|
|
241
272
|
|
|
242
273
|
_SWITCHBOT_MODEL_TO_CHAR = {
|
|
@@ -285,10 +316,8 @@ def parse_advertisement_data(
|
|
|
285
316
|
_mfr_id,
|
|
286
317
|
model,
|
|
287
318
|
)
|
|
288
|
-
except Exception
|
|
289
|
-
_LOGGER.exception(
|
|
290
|
-
"Failed to parse advertisement data: %s: %s", advertisement_data, err
|
|
291
|
-
)
|
|
319
|
+
except Exception: # pylint: disable=broad-except
|
|
320
|
+
_LOGGER.exception("Failed to parse advertisement data: %s", advertisement_data)
|
|
292
321
|
return None
|
|
293
322
|
|
|
294
323
|
if not data:
|
|
@@ -15,7 +15,7 @@ def process_fan(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool |
|
|
|
15
15
|
_seq_num = device_data[0]
|
|
16
16
|
_isOn = bool(device_data[1] & 0b10000000)
|
|
17
17
|
_mode = (device_data[1] & 0b01110000) >> 4
|
|
18
|
-
_mode = FanMode(_mode).name if 1 <= _mode <= 4 else None
|
|
18
|
+
_mode = FanMode(_mode).name.lower() if 1 <= _mode <= 4 else None
|
|
19
19
|
_nightLight = (device_data[1] & 0b00001100) >> 2
|
|
20
20
|
_oscillate_left_and_right = bool(device_data[1] & 0b00000010)
|
|
21
21
|
_oscillate_up_and_down = bool(device_data[1] & 0b00000001)
|
|
@@ -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,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from ..enum import StrEnum
|
|
6
|
-
from .fan import FanMode
|
|
6
|
+
from .fan import FanMode
|
|
7
7
|
|
|
8
8
|
# Preserve old LockStatus export for backwards compatibility
|
|
9
|
-
from .lock import LockStatus
|
|
9
|
+
from .lock import LockStatus
|
|
10
10
|
|
|
11
11
|
DEFAULT_RETRY_COUNT = 3
|
|
12
12
|
DEFAULT_RETRY_TIMEOUT = 1
|
|
@@ -67,3 +67,21 @@ class SwitchbotModel(StrEnum):
|
|
|
67
67
|
ROLLER_SHADE = "Roller Shade"
|
|
68
68
|
HUBMINI_MATTER = "HubMini Matter"
|
|
69
69
|
CIRCULATOR_FAN = "Circulator Fan"
|
|
70
|
+
K20_VACUUM = "K20 Vacuum"
|
|
71
|
+
S10_VACUUM = "S10 Vacuum"
|
|
72
|
+
K10_VACUUM = "K10+ Vacuum"
|
|
73
|
+
K10_PRO_VACUUM = "K10+ Pro Vacuum"
|
|
74
|
+
K10_PRO_COMBO_VACUUM = "K10+ Pro Combo Vacuum"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"DEFAULT_RETRY_COUNT",
|
|
79
|
+
"DEFAULT_RETRY_TIMEOUT",
|
|
80
|
+
"DEFAULT_SCAN_TIMEOUT",
|
|
81
|
+
"FanMode",
|
|
82
|
+
"LockStatus",
|
|
83
|
+
"SwitchbotAccountConnectionError",
|
|
84
|
+
"SwitchbotApiError",
|
|
85
|
+
"SwitchbotAuthenticationError",
|
|
86
|
+
"SwitchbotModel",
|
|
87
|
+
]
|
|
@@ -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]
|
|
@@ -21,10 +21,10 @@ COMMAND_TURN_OFF = f"{COMMAND_HEAD}0102"
|
|
|
21
21
|
COMMAND_START_OSCILLATION = f"{COMMAND_HEAD}020101ff"
|
|
22
22
|
COMMAND_STOP_OSCILLATION = f"{COMMAND_HEAD}020102ff"
|
|
23
23
|
COMMAND_SET_MODE = {
|
|
24
|
-
FanMode.NORMAL.name: f"{COMMAND_HEAD}030101ff",
|
|
25
|
-
FanMode.NATURAL.name: f"{COMMAND_HEAD}030102ff",
|
|
26
|
-
FanMode.SLEEP.name: f"{COMMAND_HEAD}030103",
|
|
27
|
-
FanMode.BABY.name: f"{COMMAND_HEAD}030104",
|
|
24
|
+
FanMode.NORMAL.name.lower(): f"{COMMAND_HEAD}030101ff",
|
|
25
|
+
FanMode.NATURAL.name.lower(): f"{COMMAND_HEAD}030102ff",
|
|
26
|
+
FanMode.SLEEP.name.lower(): f"{COMMAND_HEAD}030103",
|
|
27
|
+
FanMode.BABY.name.lower(): f"{COMMAND_HEAD}030104",
|
|
28
28
|
}
|
|
29
29
|
COMMAND_SET_PERCENTAGE = f"{COMMAND_HEAD}0302" # +speed
|
|
30
30
|
COMMAND_GET_BASIC_INFO = "570f428102"
|
|
@@ -48,7 +48,7 @@ class SwitchbotFan(SwitchbotSequenceDevice):
|
|
|
48
48
|
isOn = bool(_data[3] & 0b10000000)
|
|
49
49
|
oscillating = bool(_data[3] & 0b01100000)
|
|
50
50
|
_mode = _data[8] & 0b00000111
|
|
51
|
-
mode = FanMode(_mode).name if 1 <= _mode <= 4 else None
|
|
51
|
+
mode = FanMode(_mode).name.lower() if 1 <= _mode <= 4 else None
|
|
52
52
|
speed = _data[9]
|
|
53
53
|
firmware = _data1[2] / 10.0
|
|
54
54
|
|
|
@@ -86,8 +86,7 @@ class SwitchbotFan(SwitchbotSequenceDevice):
|
|
|
86
86
|
"""Send command to set fan oscillation"""
|
|
87
87
|
if oscillating:
|
|
88
88
|
return await self._send_command(COMMAND_START_OSCILLATION)
|
|
89
|
-
|
|
90
|
-
return await self._send_command(COMMAND_STOP_OSCILLATION)
|
|
89
|
+
return await self._send_command(COMMAND_STOP_OSCILLATION)
|
|
91
90
|
|
|
92
91
|
@update_after_operation
|
|
93
92
|
async def turn_on(self) -> bool:
|
|
@@ -106,9 +106,7 @@ class SwitchbotRelaySwitch(SwitchbotEncryptedDevice):
|
|
|
106
106
|
):
|
|
107
107
|
return False
|
|
108
108
|
time_since_last_full_update = time.monotonic() - self._last_full_update
|
|
109
|
-
|
|
110
|
-
return False
|
|
111
|
-
return True
|
|
109
|
+
return not time_since_last_full_update < PASSIVE_POLL_INTERVAL
|
|
112
110
|
|
|
113
111
|
async def turn_on(self) -> bool:
|
|
114
112
|
"""Turn device on."""
|
|
@@ -131,8 +129,7 @@ class SwitchbotRelaySwitch(SwitchbotEncryptedDevice):
|
|
|
131
129
|
async def async_toggle(self, **kwargs) -> bool:
|
|
132
130
|
"""Toggle device."""
|
|
133
131
|
result = await self._send_command(COMMAND_TOGGLE)
|
|
134
|
-
|
|
135
|
-
return status
|
|
132
|
+
return self._check_command_result(result, 0, {1})
|
|
136
133
|
|
|
137
134
|
def is_on(self) -> bool | None:
|
|
138
135
|
"""Return switch state from cache."""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Library to handle connection with Switchbot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .device import SwitchbotSequenceDevice, update_after_operation
|
|
8
|
+
|
|
9
|
+
COMMAND_CLEAN_UP = {
|
|
10
|
+
1: "570F5A00FFFF7001",
|
|
11
|
+
2: "5A400101010126",
|
|
12
|
+
}
|
|
13
|
+
COMMAND_RETURN_DOCK = {
|
|
14
|
+
1: "570F5A00FFFF7002",
|
|
15
|
+
2: "5A400101010225",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SwitchbotVacuum(SwitchbotSequenceDevice):
|
|
20
|
+
"""Representation of a Switchbot Vacuum."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, device, password=None, interface=0, **kwargs):
|
|
23
|
+
super().__init__(device, password, interface, **kwargs)
|
|
24
|
+
|
|
25
|
+
@update_after_operation
|
|
26
|
+
async def clean_up(self, protocol_version: int) -> bool:
|
|
27
|
+
"""Send command to perform a spot clean-up."""
|
|
28
|
+
return await self._send_command(COMMAND_CLEAN_UP[protocol_version])
|
|
29
|
+
|
|
30
|
+
@update_after_operation
|
|
31
|
+
async def return_to_dock(self, protocol_version: int) -> bool:
|
|
32
|
+
"""Send command to return the dock."""
|
|
33
|
+
return await self._send_command(COMMAND_RETURN_DOCK[protocol_version])
|
|
34
|
+
|
|
35
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
36
|
+
"""Only support get the ble version through the command."""
|
|
37
|
+
if not (_data := await self._get_basic_info()):
|
|
38
|
+
return None
|
|
39
|
+
return {
|
|
40
|
+
"firmware": _data[2],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def get_soc_version(self) -> str:
|
|
44
|
+
"""Return device soc version."""
|
|
45
|
+
return self._get_adv_value("soc_version")
|
|
46
|
+
|
|
47
|
+
def get_last_step(self) -> int:
|
|
48
|
+
"""Return device last step after network configuration."""
|
|
49
|
+
return self._get_adv_value("step")
|
|
50
|
+
|
|
51
|
+
def get_mqtt_connnect_status(self) -> bool:
|
|
52
|
+
"""Return device mqtt connect status."""
|
|
53
|
+
return self._get_adv_value("mqtt_connected")
|
|
54
|
+
|
|
55
|
+
def get_battery(self) -> int:
|
|
56
|
+
"""Return device battery."""
|
|
57
|
+
return self._get_adv_value("battery")
|
|
58
|
+
|
|
59
|
+
def get_work_status(self) -> int:
|
|
60
|
+
"""Return device work status."""
|
|
61
|
+
return self._get_adv_value("work_status")
|
|
62
|
+
|
|
63
|
+
def get_dustbin_bound_status(self) -> bool:
|
|
64
|
+
"""Return the dustbin bound status"""
|
|
65
|
+
return self._get_adv_value("dustbin_bound")
|
|
66
|
+
|
|
67
|
+
def get_dustbin_connnected_status(self) -> bool:
|
|
68
|
+
"""Return the dustbin connected status"""
|
|
69
|
+
return self._get_adv_value("dusbin_connected")
|
|
70
|
+
|
|
71
|
+
def get_network_connected_status(self) -> bool:
|
|
72
|
+
"""Return the network connected status"""
|
|
73
|
+
return self._get_adv_value("network_connected")
|
|
@@ -3,17 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from enum import Enum
|
|
6
|
-
from typing import Any,
|
|
7
|
-
|
|
8
|
-
_StrEnumT = TypeVar("_StrEnumT", bound="StrEnum")
|
|
6
|
+
from typing import Any, Self
|
|
9
7
|
|
|
10
8
|
|
|
11
9
|
class StrEnum(str, Enum):
|
|
12
10
|
"""Partial backport of Python 3.11's StrEnum for our basic use cases."""
|
|
13
11
|
|
|
14
|
-
def __new__(
|
|
15
|
-
cls: type[_StrEnumT], value: str, *args: Any, **kwargs: Any
|
|
16
|
-
) -> _StrEnumT:
|
|
12
|
+
def __new__(cls, value: str, *args: Any, **kwargs: Any) -> Self:
|
|
17
13
|
"""Create a new StrEnum instance."""
|
|
18
14
|
if not isinstance(value, str):
|
|
19
15
|
raise TypeError(f"{value!r} is not a string")
|