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.
Files changed (82) hide show
  1. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PKG-INFO +1 -1
  2. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PySwitchbot.egg-info/SOURCES.txt +8 -1
  4. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/pyproject.toml +28 -0
  5. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/setup.py +1 -1
  6. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/__init__.py +6 -0
  7. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parser.py +58 -4
  8. pyswitchbot-0.62.0/switchbot/adv_parsers/air_purifier.py +52 -0
  9. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/hub2.py +1 -3
  10. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/hubmini_matter.py +1 -2
  11. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/meter.py +1 -3
  12. pyswitchbot-0.62.0/switchbot/adv_parsers/vacuum.py +61 -0
  13. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/const/__init__.py +23 -2
  14. pyswitchbot-0.62.0/switchbot/const/air_purifier.py +23 -0
  15. pyswitchbot-0.62.0/switchbot/devices/air_purifier.py +142 -0
  16. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/blind_tilt.py +1 -2
  17. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/device.py +4 -6
  18. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/evaporative_humidifier.py +1 -1
  19. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/fan.py +1 -2
  20. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/relay_switch.py +2 -5
  21. pyswitchbot-0.62.0/switchbot/devices/vacuum.py +73 -0
  22. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/enum.py +2 -6
  23. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_adv_parser.py +631 -0
  24. pyswitchbot-0.62.0/tests/test_air_purifier.py +231 -0
  25. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_base_cover.py +2 -2
  26. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_blind_tilt.py +4 -3
  27. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_curtain.py +3 -3
  28. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_evaporative_humidifier.py +1 -1
  29. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_fan.py +7 -5
  30. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_roller_shade.py +1 -1
  31. pyswitchbot-0.62.0/tests/test_vacuum.py +135 -0
  32. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/LICENSE +0 -0
  33. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/MANIFEST.in +0 -0
  34. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  35. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PySwitchbot.egg-info/requires.txt +0 -0
  36. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  37. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/README.md +0 -0
  38. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/setup.cfg +0 -0
  39. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/__init__.py +0 -0
  40. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  41. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/bot.py +0 -0
  42. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/bulb.py +0 -0
  43. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  44. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/contact.py +0 -0
  45. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/curtain.py +0 -0
  46. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/fan.py +0 -0
  47. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/humidifier.py +0 -0
  48. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/keypad.py +0 -0
  49. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/leak.py +0 -0
  50. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/light_strip.py +0 -0
  51. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/lock.py +0 -0
  52. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/motion.py +0 -0
  53. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/plug.py +0 -0
  54. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  55. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/remote.py +0 -0
  56. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  57. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/api_config.py +0 -0
  58. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/const/evaporative_humidifier.py +0 -0
  59. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/const/fan.py +0 -0
  60. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/const/hub2.py +0 -0
  61. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/const/lock.py +0 -0
  62. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/__init__.py +0 -0
  63. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/base_cover.py +0 -0
  64. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/base_light.py +0 -0
  65. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/bot.py +0 -0
  66. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/bulb.py +0 -0
  67. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/ceiling_light.py +0 -0
  68. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/contact.py +0 -0
  69. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/curtain.py +0 -0
  70. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/humidifier.py +0 -0
  71. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/keypad.py +0 -0
  72. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/light_strip.py +0 -0
  73. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/lock.py +0 -0
  74. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/meter.py +0 -0
  75. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/motion.py +0 -0
  76. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/plug.py +0 -0
  77. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/devices/roller_shade.py +0 -0
  78. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/discovery.py +0 -0
  79. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/helpers.py +0 -0
  80. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/switchbot/models.py +0 -0
  81. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_hub2.py +0 -0
  82. {pyswitchbot-0.60.1 → pyswitchbot-0.62.0}/tests/test_relay_switch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 0.60.1
3
+ Version: 0.62.0
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/sblibs/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 0.60.1
3
+ Version: 0.62.0
4
4
  Summary: A library to communicate with Switchbot
5
5
  Home-page: https://github.com/sblibs/pySwitchbot/
6
6
  Author: Daniel Hjelseth Hoyer
@@ -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"]
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="0.60.1",
23
+ version="0.62.0",
24
24
  description="A library to communicate with Switchbot",
25
25
  long_description=long_description,
26
26
  long_description_content_type="text/markdown",
@@ -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 as err: # pylint: disable=broad-except
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
- _wohub2_data = {
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
- paraser_data = {
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
- _wosensorth_data = {
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 .fan import FanMode as FanMode
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 as 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
- else:
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
- if time_since_last_full_update < PASSIVE_POLL_INTERVAL:
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
- elif len(key_id) != 2:
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
- elif len(encryption_key) != 32:
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
- elif mode not in MODES_COMMANDS:
120
+ if mode not in MODES_COMMANDS:
121
121
  raise ValueError("Invalid mode")
122
122
 
123
123
  command = COMMAND_SET_MODE + MODES_COMMANDS[mode]