PySwitchbot 0.64.1__tar.gz → 0.65.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 (94) hide show
  1. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/PKG-INFO +1 -1
  2. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/PySwitchbot.egg-info/SOURCES.txt +1 -0
  4. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/setup.py +1 -1
  5. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/__init__.py +8 -1
  6. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parser.py +19 -6
  7. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/hub2.py +2 -1
  8. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/hub3.py +2 -1
  9. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/hubmini_matter.py +3 -1
  10. pyswitchbot-0.65.0/switchbot/adv_parsers/humidifier.py +107 -0
  11. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/meter.py +3 -1
  12. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/plug.py +3 -1
  13. pyswitchbot-0.65.0/switchbot/adv_parsers/relay_switch.py +48 -0
  14. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/const/__init__.py +10 -0
  15. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/const/evaporative_humidifier.py +14 -0
  16. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/device.py +10 -6
  17. pyswitchbot-0.65.0/switchbot/devices/evaporative_humidifier.py +256 -0
  18. pyswitchbot-0.65.0/switchbot/devices/relay_switch.py +300 -0
  19. pyswitchbot-0.65.0/switchbot/helpers.py +75 -0
  20. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/tests/test_adv_parser.py +470 -70
  21. pyswitchbot-0.65.0/tests/test_evaporative_humidifier.py +357 -0
  22. pyswitchbot-0.65.0/tests/test_helpers.py +72 -0
  23. pyswitchbot-0.65.0/tests/test_relay_switch.py +453 -0
  24. pyswitchbot-0.64.1/switchbot/adv_parsers/humidifier.py +0 -93
  25. pyswitchbot-0.64.1/switchbot/adv_parsers/relay_switch.py +0 -32
  26. pyswitchbot-0.64.1/switchbot/devices/evaporative_humidifier.py +0 -212
  27. pyswitchbot-0.64.1/switchbot/devices/relay_switch.py +0 -136
  28. pyswitchbot-0.64.1/switchbot/helpers.py +0 -17
  29. pyswitchbot-0.64.1/tests/test_evaporative_humidifier.py +0 -202
  30. pyswitchbot-0.64.1/tests/test_relay_switch.py +0 -73
  31. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/LICENSE +0 -0
  32. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/MANIFEST.in +0 -0
  33. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  34. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/PySwitchbot.egg-info/requires.txt +0 -0
  35. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  36. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/README.md +0 -0
  37. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/pyproject.toml +0 -0
  38. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/setup.cfg +0 -0
  39. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/__init__.py +0 -0
  40. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  41. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  42. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/bot.py +0 -0
  43. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/bulb.py +0 -0
  44. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  45. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/contact.py +0 -0
  46. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/curtain.py +0 -0
  47. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/fan.py +0 -0
  48. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/keypad.py +0 -0
  49. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/leak.py +0 -0
  50. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/light_strip.py +0 -0
  51. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/lock.py +0 -0
  52. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/motion.py +0 -0
  53. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/remote.py +0 -0
  54. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  55. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/adv_parsers/vacuum.py +0 -0
  56. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/api_config.py +0 -0
  57. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/const/air_purifier.py +0 -0
  58. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/const/fan.py +0 -0
  59. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/const/hub2.py +0 -0
  60. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/const/hub3.py +0 -0
  61. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/const/lock.py +0 -0
  62. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/__init__.py +0 -0
  63. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/air_purifier.py +0 -0
  64. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/base_cover.py +0 -0
  65. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/base_light.py +0 -0
  66. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/blind_tilt.py +0 -0
  67. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/bot.py +0 -0
  68. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/bulb.py +0 -0
  69. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/ceiling_light.py +0 -0
  70. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/contact.py +0 -0
  71. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/curtain.py +0 -0
  72. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/fan.py +0 -0
  73. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/humidifier.py +0 -0
  74. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/keypad.py +0 -0
  75. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/light_strip.py +0 -0
  76. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/lock.py +0 -0
  77. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/meter.py +0 -0
  78. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/motion.py +0 -0
  79. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/plug.py +0 -0
  80. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/roller_shade.py +0 -0
  81. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/devices/vacuum.py +0 -0
  82. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/discovery.py +0 -0
  83. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/enum.py +0 -0
  84. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/switchbot/models.py +0 -0
  85. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/tests/test_air_purifier.py +0 -0
  86. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/tests/test_base_cover.py +0 -0
  87. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/tests/test_blind_tilt.py +0 -0
  88. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/tests/test_curtain.py +0 -0
  89. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/tests/test_fan.py +0 -0
  90. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/tests/test_hub2.py +0 -0
  91. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/tests/test_hub3.py +0 -0
  92. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/tests/test_lock.py +0 -0
  93. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/tests/test_roller_shade.py +0 -0
  94. {pyswitchbot-0.64.1 → pyswitchbot-0.65.0}/tests/test_vacuum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 0.64.1
3
+ Version: 0.65.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.64.1
3
+ Version: 0.65.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
@@ -76,6 +76,7 @@ tests/test_blind_tilt.py
76
76
  tests/test_curtain.py
77
77
  tests/test_evaporative_humidifier.py
78
78
  tests/test_fan.py
79
+ tests/test_helpers.py
79
80
  tests/test_hub2.py
80
81
  tests/test_hub3.py
81
82
  tests/test_lock.py
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="0.64.1",
23
+ version="0.65.0",
24
24
  description="A library to communicate with Switchbot",
25
25
  long_description=long_description,
26
26
  long_description_content_type="text/markdown",
@@ -12,6 +12,9 @@ from .adv_parser import SwitchbotSupportedType, parse_advertisement_data
12
12
  from .const import (
13
13
  AirPurifierMode,
14
14
  FanMode,
15
+ HumidifierAction,
16
+ HumidifierMode,
17
+ HumidifierWaterLevel,
15
18
  LockStatus,
16
19
  SwitchbotAccountConnectionError,
17
20
  SwitchbotApiError,
@@ -32,7 +35,7 @@ from .devices.humidifier import SwitchbotHumidifier
32
35
  from .devices.light_strip import SwitchbotLightStrip
33
36
  from .devices.lock import SwitchbotLock
34
37
  from .devices.plug import SwitchbotPlugMini
35
- from .devices.relay_switch import SwitchbotRelaySwitch
38
+ from .devices.relay_switch import SwitchbotRelaySwitch, SwitchbotRelaySwitch2PM
36
39
  from .devices.roller_shade import SwitchbotRollerShade
37
40
  from .devices.vacuum import SwitchbotVacuum
38
41
  from .discovery import GetSwitchbotDevices
@@ -43,6 +46,9 @@ __all__ = [
43
46
  "ColorMode",
44
47
  "FanMode",
45
48
  "GetSwitchbotDevices",
49
+ "HumidifierAction",
50
+ "HumidifierMode",
51
+ "HumidifierWaterLevel",
46
52
  "LockStatus",
47
53
  "SwitchBotAdvertisement",
48
54
  "Switchbot",
@@ -68,6 +74,7 @@ __all__ = [
68
74
  "SwitchbotPlugMini",
69
75
  "SwitchbotPlugMini",
70
76
  "SwitchbotRelaySwitch",
77
+ "SwitchbotRelaySwitch2PM",
71
78
  "SwitchbotRollerShade",
72
79
  "SwitchbotSupportedType",
73
80
  "SwitchbotSupportedType",
@@ -30,8 +30,9 @@ from .adv_parsers.meter import process_wosensorth, process_wosensorth_c
30
30
  from .adv_parsers.motion import process_wopresence
31
31
  from .adv_parsers.plug import process_woplugmini
32
32
  from .adv_parsers.relay_switch import (
33
- process_worelay_switch_1,
34
- process_worelay_switch_1pm,
33
+ process_garage_door_opener,
34
+ process_relay_switch_2pm,
35
+ process_relay_switch_common_data,
35
36
  )
36
37
  from .adv_parsers.remote import process_woremote
37
38
  from .adv_parsers.roller_shade import process_worollershade
@@ -115,13 +116,13 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
115
116
  },
116
117
  "4": {
117
118
  "modelName": SwitchbotModel.METER_PRO,
118
- "modelFriendlyName": "Meter",
119
+ "modelFriendlyName": "Meter Pro",
119
120
  "func": process_wosensorth,
120
121
  "manufacturer_id": 2409,
121
122
  },
122
123
  "5": {
123
124
  "modelName": SwitchbotModel.METER_PRO_C,
124
- "modelFriendlyName": "Meter",
125
+ "modelFriendlyName": "Meter Pro CO2",
125
126
  "func": process_wosensorth_c,
126
127
  "manufacturer_id": 2409,
127
128
  },
@@ -207,13 +208,13 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
207
208
  "<": {
208
209
  "modelName": SwitchbotModel.RELAY_SWITCH_1PM,
209
210
  "modelFriendlyName": "Relay Switch 1PM",
210
- "func": process_worelay_switch_1pm,
211
+ "func": process_relay_switch_common_data,
211
212
  "manufacturer_id": 2409,
212
213
  },
213
214
  ";": {
214
215
  "modelName": SwitchbotModel.RELAY_SWITCH_1,
215
216
  "modelFriendlyName": "Relay Switch 1",
216
- "func": process_worelay_switch_1,
217
+ "func": process_relay_switch_common_data,
217
218
  "manufacturer_id": 2409,
218
219
  },
219
220
  "b": {
@@ -312,6 +313,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
312
313
  "func": process_lock2,
313
314
  "manufacturer_id": 2409,
314
315
  },
316
+ ">": {
317
+ "modelName": SwitchbotModel.GARAGE_DOOR_OPENER,
318
+ "modelFriendlyName": "Garage Door Opener",
319
+ "func": process_garage_door_opener,
320
+ "manufacturer_id": 2409,
321
+ },
322
+ "=": {
323
+ "modelName": SwitchbotModel.RELAY_SWITCH_2PM,
324
+ "modelFriendlyName": "Relay Switch 2PM",
325
+ "func": process_relay_switch_2pm,
326
+ "manufacturer_id": 2409,
327
+ },
315
328
  }
316
329
 
317
330
  _SWITCHBOT_MODEL_TO_CHAR = {
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from typing import Any
6
6
 
7
7
  from ..const.hub2 import LIGHT_INTENSITY_MAP
8
+ from ..helpers import celsius_to_fahrenheit
8
9
 
9
10
 
10
11
  def process_wohub2(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]:
@@ -22,7 +23,7 @@ def process_wohub2(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]
22
23
  _temp_c = _temp_sign * (
23
24
  (temp_data[1] & 0b01111111) + ((temp_data[0] & 0b00001111) / 10)
24
25
  )
25
- _temp_f = (_temp_c * 9 / 5) + 32
26
+ _temp_f = celsius_to_fahrenheit(_temp_c)
26
27
  _temp_f = (_temp_f * 10) / 10
27
28
  humidity = temp_data[2] & 0b01111111
28
29
  light_level = status & 0b11111
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from typing import Any
6
6
 
7
7
  from ..const.hub3 import LIGHT_INTENSITY_MAP
8
+ from ..helpers import celsius_to_fahrenheit
8
9
 
9
10
 
10
11
  def process_hub3(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]:
@@ -26,7 +27,7 @@ def process_hub3(data: bytes | None, mfr_data: bytes | None) -> dict[str, Any]:
26
27
  _temp_c = _temp_sign * (
27
28
  (temp_data[1] & 0b01111111) + ((temp_data[0] & 0b00001111) / 10)
28
29
  )
29
- _temp_f = round(((_temp_c * 9 / 5) + 32), 1)
30
+ _temp_f = round(celsius_to_fahrenheit(_temp_c), 1)
30
31
  humidity = temp_data[2] & 0b01111111
31
32
  motion_detected = bool(device_data[10] & 0b10000000)
32
33
 
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
+ from ..helpers import celsius_to_fahrenheit
8
+
7
9
 
8
10
  def process_hubmini_matter(
9
11
  data: bytes | None, mfr_data: bytes | None
@@ -21,7 +23,7 @@ def process_hubmini_matter(
21
23
  _temp_c = _temp_sign * (
22
24
  (temp_data[1] & 0b01111111) + ((temp_data[0] & 0b00001111) / 10)
23
25
  )
24
- _temp_f = (_temp_c * 9 / 5) + 32
26
+ _temp_f = celsius_to_fahrenheit(_temp_c)
25
27
  _temp_f = (_temp_f * 10) / 10
26
28
  humidity = temp_data[2] & 0b01111111
27
29
 
@@ -0,0 +1,107 @@
1
+ """Humidifier adv parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from datetime import timedelta
7
+
8
+ from ..const.evaporative_humidifier import (
9
+ HumidifierMode,
10
+ HumidifierWaterLevel,
11
+ )
12
+ from ..helpers import celsius_to_fahrenheit
13
+
14
+ _LOGGER = logging.getLogger(__name__)
15
+
16
+ # mfr_data: 943cc68d3d2e
17
+ # data: 650000cd802b6300
18
+ # data: 650000cd802b6300
19
+ # data: 658000c9802b6300
20
+
21
+
22
+ # Low: 658000c5222b6300
23
+ # Med: 658000c5432b6300
24
+ # High: 658000c5642b6300
25
+
26
+
27
+ def calculate_temperature_and_humidity(
28
+ data: bytes, is_meter_binded: bool = True
29
+ ) -> tuple[float | None, float | None, int | None]:
30
+ """Calculate temperature and humidity based on the given flag."""
31
+ if len(data) < 3 or not is_meter_binded:
32
+ return None, None, None
33
+
34
+ humidity = data[0] & 0b01111111
35
+ if humidity > 100:
36
+ return None, None, None
37
+
38
+ _temp_sign = 1 if data[1] & 0b10000000 else -1
39
+ _temp_c = _temp_sign * ((data[1] & 0b01111111) + ((data[2] >> 4) / 10))
40
+ _temp_f = celsius_to_fahrenheit(_temp_c)
41
+
42
+ return _temp_c, _temp_f, humidity
43
+
44
+
45
+ def process_wohumidifier(
46
+ data: bytes | None, mfr_data: bytes | None
47
+ ) -> dict[str, bool | int]:
48
+ """Process WoHumi services data."""
49
+ if data is None:
50
+ return {
51
+ "isOn": None,
52
+ "level": None,
53
+ "switchMode": True,
54
+ }
55
+
56
+ return {
57
+ "isOn": bool(data[1]),
58
+ "level": data[4],
59
+ "switchMode": True,
60
+ }
61
+
62
+
63
+ def process_evaporative_humidifier(
64
+ data: bytes | None, mfr_data: bytes | None
65
+ ) -> dict[str, bool | int]:
66
+ """Process WoHumi services data."""
67
+ if mfr_data is None:
68
+ return {}
69
+
70
+ seq_number = mfr_data[6]
71
+ is_on = bool(mfr_data[7] & 0b10000000)
72
+ mode = HumidifierMode(mfr_data[7] & 0b00001111)
73
+ over_humidify_protection = bool(mfr_data[8] & 0b10000000)
74
+ child_lock = bool(mfr_data[8] & 0b00100000)
75
+ tank_removed = bool(mfr_data[8] & 0b00000100)
76
+ tilted_alert = bool(mfr_data[8] & 0b00000010)
77
+ filter_missing = bool(mfr_data[8] & 0b00000001)
78
+ is_meter_binded = bool(mfr_data[9] & 0b10000000)
79
+
80
+ _temp_c, _temp_f, humidity = calculate_temperature_and_humidity(
81
+ mfr_data[9:12], is_meter_binded
82
+ )
83
+
84
+ water_level = HumidifierWaterLevel(mfr_data[11] & 0b00000011).name.lower()
85
+ filter_run_time = timedelta(
86
+ hours=int.from_bytes(mfr_data[12:14], byteorder="big") & 0xFFF
87
+ )
88
+ target_humidity = mfr_data[16] & 0b01111111
89
+
90
+ return {
91
+ "seq_number": seq_number,
92
+ "isOn": is_on,
93
+ "mode": mode,
94
+ "over_humidify_protection": over_humidify_protection,
95
+ "child_lock": child_lock,
96
+ "tank_removed": tank_removed,
97
+ "tilted_alert": tilted_alert,
98
+ "filter_missing": filter_missing,
99
+ "is_meter_binded": is_meter_binded,
100
+ "humidity": humidity,
101
+ "temperature": _temp_c,
102
+ "temp": {"c": _temp_c, "f": _temp_f},
103
+ "water_level": water_level,
104
+ "filter_run_time": filter_run_time,
105
+ "filter_alert": filter_run_time.days >= 10,
106
+ "target_humidity": target_humidity,
107
+ }
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import struct
6
6
  from typing import Any
7
7
 
8
+ from ..helpers import celsius_to_fahrenheit
9
+
8
10
  CO2_UNPACK = struct.Struct(">H").unpack_from
9
11
 
10
12
 
@@ -28,7 +30,7 @@ def process_wosensorth(data: bytes | None, mfr_data: bytes | None) -> dict[str,
28
30
  _temp_c = _temp_sign * (
29
31
  (temp_data[1] & 0b01111111) + ((temp_data[0] & 0b00001111) / 10)
30
32
  )
31
- _temp_f = (_temp_c * 9 / 5) + 32
33
+ _temp_f = celsius_to_fahrenheit(_temp_c)
32
34
  _temp_f = (_temp_f * 10) / 10
33
35
  humidity = temp_data[2] & 0b01111111
34
36
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from ..helpers import parse_power_data
6
+
5
7
 
6
8
  def process_woplugmini(
7
9
  data: bytes | None, mfr_data: bytes | None
@@ -13,5 +15,5 @@ def process_woplugmini(
13
15
  "switchMode": True,
14
16
  "isOn": mfr_data[7] == 0x80,
15
17
  "wifi_rssi": -mfr_data[9],
16
- "power": (((mfr_data[10] << 8) + mfr_data[11]) & 0x7FFF) / 10, # W
18
+ "power": parse_power_data(mfr_data, 10, 10.0, 0x7FFF), # W
17
19
  }
@@ -0,0 +1,48 @@
1
+ """Relay Switch adv parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def process_relay_switch_common_data(
9
+ data: bytes | None, mfr_data: bytes | None
10
+ ) -> dict[str, Any]:
11
+ """Process relay switch 1 and 1PM common data."""
12
+ if mfr_data is None:
13
+ return {}
14
+ return {
15
+ "switchMode": True, # for compatibility, useless
16
+ "sequence_number": mfr_data[6],
17
+ "isOn": bool(mfr_data[7] & 0b10000000),
18
+ }
19
+
20
+
21
+ def process_garage_door_opener(
22
+ data: bytes | None, mfr_data: bytes | None
23
+ ) -> dict[str, Any]:
24
+ """Process garage door opener services data."""
25
+ if mfr_data is None:
26
+ return {}
27
+ common_data = process_relay_switch_common_data(data, mfr_data)
28
+ common_data["door_open"] = not bool(mfr_data[7] & 0b00100000)
29
+ return common_data
30
+
31
+
32
+ def process_relay_switch_2pm(
33
+ data: bytes | None, mfr_data: bytes | None
34
+ ) -> dict[int, dict[str, Any]]:
35
+ """Process Relay Switch 2PM services data."""
36
+ if mfr_data is None:
37
+ return {}
38
+
39
+ return {
40
+ 1: {
41
+ **process_relay_switch_common_data(data, mfr_data),
42
+ },
43
+ 2: {
44
+ "switchMode": True, # for compatibility, useless
45
+ "sequence_number": mfr_data[6],
46
+ "isOn": bool(mfr_data[7] & 0b01000000),
47
+ },
48
+ }
@@ -4,6 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  from ..enum import StrEnum
6
6
  from .air_purifier import AirPurifierMode
7
+ from .evaporative_humidifier import (
8
+ HumidifierAction,
9
+ HumidifierMode,
10
+ HumidifierWaterLevel,
11
+ )
7
12
  from .fan import FanMode
8
13
 
9
14
  # Preserve old LockStatus export for backwards compatibility
@@ -78,6 +83,8 @@ class SwitchbotModel(StrEnum):
78
83
  HUB3 = "Hub3"
79
84
  LOCK_ULTRA = "Lock Ultra"
80
85
  LOCK_LITE = "Lock Lite"
86
+ GARAGE_DOOR_OPENER = "Garage Door Opener"
87
+ RELAY_SWITCH_2PM = "Relay Switch 2PM"
81
88
 
82
89
 
83
90
  __all__ = [
@@ -86,6 +93,9 @@ __all__ = [
86
93
  "DEFAULT_SCAN_TIMEOUT",
87
94
  "AirPurifierMode",
88
95
  "FanMode",
96
+ "HumidifierAction",
97
+ "HumidifierMode",
98
+ "HumidifierWaterLevel",
89
99
  "LockStatus",
90
100
  "SwitchbotAccountConnectionError",
91
101
  "SwitchbotApiError",
@@ -13,6 +13,10 @@ class HumidifierMode(Enum):
13
13
  AUTO = 7
14
14
  DRYING_FILTER = 8
15
15
 
16
+ @classmethod
17
+ def get_modes(cls) -> list[str]:
18
+ return [mode.name.lower() for mode in cls]
19
+
16
20
 
17
21
  class HumidifierWaterLevel(Enum):
18
22
  EMPTY = 0
@@ -20,6 +24,16 @@ class HumidifierWaterLevel(Enum):
20
24
  MEDIUM = 2
21
25
  HIGH = 3
22
26
 
27
+ @classmethod
28
+ def get_levels(cls) -> list[str]:
29
+ return [level.name.lower() for level in cls]
30
+
31
+
32
+ class HumidifierAction(Enum):
33
+ OFF = 0
34
+ HUMIDIFYING = 1
35
+ DRYING = 2
36
+
23
37
 
24
38
  OVER_HUMIDIFY_PROTECTION_MODES = {
25
39
  HumidifierMode.QUIET,
@@ -116,7 +116,9 @@ def _merge_data(old_data: dict[str, Any], new_data: dict[str, Any]) -> dict[str,
116
116
  """Merge data but only add None keys if they are missing."""
117
117
  merged = old_data.copy()
118
118
  for key, value in new_data.items():
119
- if value is not None or key not in old_data:
119
+ if isinstance(value, dict) and isinstance(old_data.get(key), dict):
120
+ merged[key] = _merge_data(old_data[key], value)
121
+ elif value is not None or key not in old_data:
120
122
  merged[key] = value
121
123
  return merged
122
124
 
@@ -538,7 +540,7 @@ class SwitchbotBaseDevice:
538
540
  self._override_adv_data.update(state)
539
541
  self._update_parsed_data(state)
540
542
 
541
- def _get_adv_value(self, key: str) -> Any:
543
+ def _get_adv_value(self, key: str, channel: int | None = None) -> Any:
542
544
  """Return value from advertisement data."""
543
545
  if self._override_adv_data and key in self._override_adv_data:
544
546
  _LOGGER.debug(
@@ -550,6 +552,8 @@ class SwitchbotBaseDevice:
550
552
  return self._override_adv_data[key]
551
553
  if not self._sb_adv_data:
552
554
  return None
555
+ if channel is not None:
556
+ return self._sb_adv_data.data["data"].get(channel, {}).get(key)
553
557
  return self._sb_adv_data.data["data"].get(key)
554
558
 
555
559
  def get_battery_percent(self) -> Any:
@@ -583,11 +587,11 @@ class SwitchbotBaseDevice:
583
587
 
584
588
  return self._sb_adv_data
585
589
 
586
- async def _get_basic_info(self) -> bytes | None:
590
+ async def _get_basic_info(
591
+ self, cmd: str = DEVICE_GET_BASIC_SETTINGS_KEY
592
+ ) -> bytes | None:
587
593
  """Return basic info of device."""
588
- _data = await self._send_command(
589
- key=DEVICE_GET_BASIC_SETTINGS_KEY, retry=self._retry_count
590
- )
594
+ _data = await self._send_command(key=cmd, retry=self._retry_count)
591
595
 
592
596
  if _data in (b"\x07", b"\x00"):
593
597
  _LOGGER.error("Unsuccessful, please try again")