PySwitchbot 0.60.1__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.
Files changed (78) hide show
  1. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/PKG-INFO +1 -1
  2. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/PySwitchbot.egg-info/SOURCES.txt +4 -1
  4. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/pyproject.toml +28 -0
  5. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/setup.py +1 -1
  6. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/__init__.py +2 -0
  7. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parser.py +33 -4
  8. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/hub2.py +1 -3
  9. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/hubmini_matter.py +1 -2
  10. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/meter.py +1 -3
  11. pyswitchbot-0.61.0/switchbot/adv_parsers/vacuum.py +61 -0
  12. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/const/__init__.py +20 -2
  13. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/blind_tilt.py +1 -2
  14. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/device.py +4 -6
  15. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/evaporative_humidifier.py +1 -1
  16. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/fan.py +1 -2
  17. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/relay_switch.py +2 -5
  18. pyswitchbot-0.61.0/switchbot/devices/vacuum.py +73 -0
  19. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/enum.py +2 -6
  20. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/tests/test_adv_parser.py +446 -0
  21. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/tests/test_base_cover.py +2 -2
  22. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/tests/test_blind_tilt.py +4 -3
  23. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/tests/test_curtain.py +3 -3
  24. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/tests/test_evaporative_humidifier.py +1 -1
  25. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/tests/test_fan.py +7 -5
  26. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/tests/test_roller_shade.py +1 -1
  27. pyswitchbot-0.61.0/tests/test_vacuum.py +135 -0
  28. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/LICENSE +0 -0
  29. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/MANIFEST.in +0 -0
  30. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  31. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/PySwitchbot.egg-info/requires.txt +0 -0
  32. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  33. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/README.md +0 -0
  34. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/setup.cfg +0 -0
  35. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/__init__.py +0 -0
  36. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  37. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/bot.py +0 -0
  38. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/bulb.py +0 -0
  39. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  40. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/contact.py +0 -0
  41. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/curtain.py +0 -0
  42. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/fan.py +0 -0
  43. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/humidifier.py +0 -0
  44. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/keypad.py +0 -0
  45. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/leak.py +0 -0
  46. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/light_strip.py +0 -0
  47. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/lock.py +0 -0
  48. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/motion.py +0 -0
  49. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/plug.py +0 -0
  50. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  51. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/remote.py +0 -0
  52. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  53. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/api_config.py +0 -0
  54. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/const/evaporative_humidifier.py +0 -0
  55. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/const/fan.py +0 -0
  56. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/const/hub2.py +0 -0
  57. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/const/lock.py +0 -0
  58. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/__init__.py +0 -0
  59. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/base_cover.py +0 -0
  60. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/base_light.py +0 -0
  61. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/bot.py +0 -0
  62. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/bulb.py +0 -0
  63. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/ceiling_light.py +0 -0
  64. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/contact.py +0 -0
  65. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/curtain.py +0 -0
  66. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/humidifier.py +0 -0
  67. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/keypad.py +0 -0
  68. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/light_strip.py +0 -0
  69. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/lock.py +0 -0
  70. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/meter.py +0 -0
  71. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/motion.py +0 -0
  72. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/plug.py +0 -0
  73. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/devices/roller_shade.py +0 -0
  74. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/discovery.py +0 -0
  75. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/helpers.py +0 -0
  76. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/switchbot/models.py +0 -0
  77. {pyswitchbot-0.60.1 → pyswitchbot-0.61.0}/tests/test_hub2.py +0 -0
  78. {pyswitchbot-0.60.1 → pyswitchbot-0.61.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.61.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.61.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
@@ -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"]
@@ -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.61.0",
24
24
  description="A library to communicate with Switchbot",
25
25
  long_description=long_description,
26
26
  long_description_content_type="text/markdown",
@@ -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 as err: # pylint: disable=broad-except
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:
@@ -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,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from ..enum import StrEnum
6
- from .fan import FanMode as FanMode
6
+ from .fan import FanMode
7
7
 
8
8
  # Preserve old LockStatus export for backwards compatibility
9
- from .lock import LockStatus as 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
- 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]
@@ -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
- else:
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
- if time_since_last_full_update < PASSIVE_POLL_INTERVAL:
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
- status = self._check_command_result(result, 0, {1})
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, TypeVar
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")