PySwitchbot 0.66.0__tar.gz → 0.68.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 (96) hide show
  1. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/PKG-INFO +1 -1
  2. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/setup.py +1 -1
  4. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/__init__.py +6 -1
  5. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parser.py +7 -2
  6. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/lock.py +17 -4
  7. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/air_purifier.py +3 -14
  8. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/base_light.py +72 -16
  9. pyswitchbot-0.68.0/switchbot/devices/bulb.py +66 -0
  10. pyswitchbot-0.68.0/switchbot/devices/ceiling_light.py +69 -0
  11. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/device.py +34 -2
  12. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/evaporative_humidifier.py +3 -13
  13. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/fan.py +2 -14
  14. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/light_strip.py +41 -121
  15. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/lock.py +30 -12
  16. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/relay_switch.py +6 -15
  17. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/vacuum.py +0 -3
  18. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_adv_parser.py +2 -8
  19. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_bulb.py +46 -15
  20. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_ceiling_light.py +16 -10
  21. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_colormode_imports.py +8 -8
  22. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_fan.py +8 -5
  23. pyswitchbot-0.68.0/tests/test_lock.py +677 -0
  24. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_relay_switch.py +13 -2
  25. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_strip_light.py +74 -17
  26. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_vacuum.py +4 -2
  27. pyswitchbot-0.66.0/switchbot/devices/bulb.py +0 -143
  28. pyswitchbot-0.66.0/switchbot/devices/ceiling_light.py +0 -105
  29. pyswitchbot-0.66.0/tests/test_lock.py +0 -42
  30. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/LICENSE +0 -0
  31. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/MANIFEST.in +0 -0
  32. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/PySwitchbot.egg-info/SOURCES.txt +0 -0
  33. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  34. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/PySwitchbot.egg-info/requires.txt +0 -0
  35. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  36. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/README.md +0 -0
  37. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/pyproject.toml +0 -0
  38. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/setup.cfg +0 -0
  39. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/__init__.py +0 -0
  40. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  41. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  42. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/bot.py +0 -0
  43. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/bulb.py +0 -0
  44. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  45. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/contact.py +0 -0
  46. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/curtain.py +0 -0
  47. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/fan.py +0 -0
  48. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/hub2.py +0 -0
  49. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/hub3.py +0 -0
  50. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  51. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/humidifier.py +0 -0
  52. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/keypad.py +0 -0
  53. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/leak.py +0 -0
  54. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/light_strip.py +0 -0
  55. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/meter.py +0 -0
  56. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/motion.py +0 -0
  57. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/plug.py +0 -0
  58. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  59. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/remote.py +0 -0
  60. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  61. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/adv_parsers/vacuum.py +0 -0
  62. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/api_config.py +0 -0
  63. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/const/__init__.py +0 -0
  64. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/const/air_purifier.py +0 -0
  65. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/const/evaporative_humidifier.py +0 -0
  66. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/const/fan.py +0 -0
  67. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/const/hub2.py +0 -0
  68. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/const/hub3.py +0 -0
  69. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/const/light.py +0 -0
  70. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/const/lock.py +0 -0
  71. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/__init__.py +0 -0
  72. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/base_cover.py +0 -0
  73. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/blind_tilt.py +0 -0
  74. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/bot.py +0 -0
  75. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/contact.py +0 -0
  76. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/curtain.py +0 -0
  77. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/humidifier.py +0 -0
  78. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/keypad.py +0 -0
  79. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/meter.py +0 -0
  80. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/motion.py +0 -0
  81. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/plug.py +0 -0
  82. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/devices/roller_shade.py +0 -0
  83. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/discovery.py +0 -0
  84. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/enum.py +0 -0
  85. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/helpers.py +0 -0
  86. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/switchbot/models.py +0 -0
  87. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_air_purifier.py +0 -0
  88. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_base_cover.py +0 -0
  89. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_blind_tilt.py +0 -0
  90. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_curtain.py +0 -0
  91. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_encrypted_device.py +0 -0
  92. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_evaporative_humidifier.py +0 -0
  93. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_helpers.py +0 -0
  94. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_hub2.py +0 -0
  95. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_hub3.py +0 -0
  96. {pyswitchbot-0.66.0 → pyswitchbot-0.68.0}/tests/test_roller_shade.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySwitchbot
3
- Version: 0.66.0
3
+ Version: 0.68.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.66.0
3
+ Version: 0.68.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
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="0.66.0",
23
+ version="0.68.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,7 +32,11 @@ from .devices.bot import Switchbot
32
32
  from .devices.bulb import SwitchbotBulb
33
33
  from .devices.ceiling_light import SwitchbotCeilingLight
34
34
  from .devices.curtain import SwitchbotCurtain
35
- from .devices.device import SwitchbotDevice, SwitchbotEncryptedDevice
35
+ from .devices.device import (
36
+ SwitchbotDevice,
37
+ SwitchbotEncryptedDevice,
38
+ SwitchbotOperationError,
39
+ )
36
40
  from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
37
41
  from .devices.fan import SwitchbotFan
38
42
  from .devices.humidifier import SwitchbotHumidifier
@@ -78,6 +82,7 @@ __all__ = [
78
82
  "SwitchbotLock",
79
83
  "SwitchbotModel",
80
84
  "SwitchbotModel",
85
+ "SwitchbotOperationError",
81
86
  "SwitchbotPlugMini",
82
87
  "SwitchbotPlugMini",
83
88
  "SwitchbotRelaySwitch",
@@ -25,7 +25,12 @@ from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohu
25
25
  from .adv_parsers.keypad import process_wokeypad
26
26
  from .adv_parsers.leak import process_leak
27
27
  from .adv_parsers.light_strip import process_light, process_wostrip
28
- from .adv_parsers.lock import process_lock2, process_wolock, process_wolock_pro
28
+ from .adv_parsers.lock import (
29
+ process_lock2,
30
+ process_locklite,
31
+ process_wolock,
32
+ process_wolock_pro,
33
+ )
29
34
  from .adv_parsers.meter import process_wosensorth, process_wosensorth_c
30
35
  from .adv_parsers.motion import process_wopresence
31
36
  from .adv_parsers.plug import process_woplugmini
@@ -304,7 +309,7 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
304
309
  "-": {
305
310
  "modelName": SwitchbotModel.LOCK_LITE,
306
311
  "modelFriendlyName": "Lock Lite",
307
- "func": process_wolock,
312
+ "func": process_locklite,
308
313
  "manufacturer_id": 2409,
309
314
  },
310
315
  b"\x00\x10\xa5\xb8": {
@@ -11,6 +11,21 @@ _LOGGER = logging.getLogger(__name__)
11
11
 
12
12
  def process_wolock(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool | int]:
13
13
  """Support for lock and lock lite process data."""
14
+ common_data = process_locklite(data, mfr_data)
15
+ if not common_data:
16
+ return {}
17
+
18
+ common_data["door_open"] = bool(mfr_data[7] & 0b00000100)
19
+ common_data["unclosed_alarm"] = bool(mfr_data[8] & 0b00100000)
20
+ common_data["auto_lock_paused"] = bool(mfr_data[8] & 0b00000010)
21
+
22
+ return common_data
23
+
24
+
25
+ def process_locklite(
26
+ data: bytes | None, mfr_data: bytes | None
27
+ ) -> dict[str, bool | int]:
28
+ """Support for lock lite process data."""
14
29
  if mfr_data is None:
15
30
  return {}
16
31
 
@@ -24,11 +39,8 @@ def process_wolock(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool
24
39
  "calibration": bool(mfr_data[7] & 0b10000000),
25
40
  "status": LockStatus((mfr_data[7] & 0b01110000) >> 4),
26
41
  "update_from_secondary_lock": bool(mfr_data[7] & 0b00001000),
27
- "door_open": bool(mfr_data[7] & 0b00000100),
28
42
  "double_lock_mode": bool(mfr_data[8] & 0b10000000),
29
- "unclosed_alarm": bool(mfr_data[8] & 0b00100000),
30
43
  "unlocked_alarm": bool(mfr_data[8] & 0b00010000),
31
- "auto_lock_paused": bool(mfr_data[8] & 0b00000010),
32
44
  "night_latch": bool(mfr_data[9] & 0b00000001) if len(mfr_data) > 9 else False,
33
45
  }
34
46
 
@@ -37,10 +49,11 @@ def parse_common_data(mfr_data: bytes | None) -> dict[str, bool | int]:
37
49
  if mfr_data is None:
38
50
  return {}
39
51
 
52
+ _LOGGER.debug("mfr_data: %s", mfr_data.hex())
40
53
  return {
41
54
  "sequence_number": mfr_data[6],
42
55
  "calibration": bool(mfr_data[7] & 0b10000000),
43
- "status": LockStatus((mfr_data[7] & 0b01111000) >> 4),
56
+ "status": LockStatus((mfr_data[7] & 0b01111000) >> 3),
44
57
  "update_from_secondary_lock": bool(mfr_data[8] & 0b11000000),
45
58
  "door_open_from_secondary_lock": bool(mfr_data[8] & 0b00100000),
46
59
  "door_open": bool(mfr_data[8] & 0b00010000),
@@ -21,8 +21,6 @@ _LOGGER = logging.getLogger(__name__)
21
21
 
22
22
 
23
23
  COMMAND_HEAD = "570f4c"
24
- COMMAND_TURN_OFF = f"{COMMAND_HEAD}010000"
25
- COMMAND_TURN_ON = f"{COMMAND_HEAD}010100"
26
24
  COMMAND_SET_MODE = {
27
25
  AirPurifierMode.LEVEL_1.name.lower(): f"{COMMAND_HEAD}01010100",
28
26
  AirPurifierMode.LEVEL_2.name.lower(): f"{COMMAND_HEAD}01010132",
@@ -37,6 +35,9 @@ DEVICE_GET_BASIC_SETTINGS_KEY = "570f4d81"
37
35
  class SwitchbotAirPurifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
38
36
  """Representation of a Switchbot Air Purifier."""
39
37
 
38
+ _turn_on_command = f"{COMMAND_HEAD}010100"
39
+ _turn_off_command = f"{COMMAND_HEAD}010000"
40
+
40
41
  def __init__(
41
42
  self,
42
43
  device: BLEDevice,
@@ -109,18 +110,6 @@ class SwitchbotAirPurifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
109
110
  result = await self._send_command(COMMAND_SET_MODE[preset_mode])
110
111
  return self._check_command_result(result, 0, {1})
111
112
 
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
113
  def get_current_percentage(self) -> Any:
125
114
  """Return cached percentage."""
126
115
  return self._get_adv_value("speed")
@@ -6,7 +6,7 @@ from typing import Any
6
6
 
7
7
  from ..helpers import create_background_task
8
8
  from ..models import SwitchBotAdvertisement
9
- from .device import SwitchbotDevice
9
+ from .device import SwitchbotDevice, SwitchbotOperationError, update_after_operation
10
10
 
11
11
  _LOGGER = logging.getLogger(__name__)
12
12
 
@@ -14,14 +14,19 @@ _LOGGER = logging.getLogger(__name__)
14
14
  class SwitchbotBaseLight(SwitchbotDevice):
15
15
  """Representation of a Switchbot light."""
16
16
 
17
+ _effect_dict: dict[str, list[str]] = {}
18
+ _set_brightness_command: str = ""
19
+ _set_color_temp_command: str = ""
20
+ _set_rgb_command: str = ""
21
+
17
22
  def __init__(self, *args: Any, **kwargs: Any) -> None:
18
- """Switchbot bulb constructor."""
23
+ """Switchbot base light constructor."""
19
24
  super().__init__(*args, **kwargs)
20
25
  self._state: dict[str, Any] = {}
21
26
 
22
27
  @property
23
28
  def on(self) -> bool | None:
24
- """Return if bulb is on."""
29
+ """Return if light is on."""
25
30
  return self.is_on()
26
31
 
27
32
  @property
@@ -60,7 +65,7 @@ class SwitchbotBaseLight(SwitchbotDevice):
60
65
  @property
61
66
  def get_effect_list(self) -> list[str] | None:
62
67
  """Return the list of supported effects."""
63
- return None
68
+ return list(self._effect_dict) if self._effect_dict else None
64
69
 
65
70
  def is_on(self) -> bool | None:
66
71
  """Return bulb state from cache."""
@@ -70,25 +75,49 @@ class SwitchbotBaseLight(SwitchbotDevice):
70
75
  """Return the current effect."""
71
76
  return self._get_adv_value("effect")
72
77
 
73
- @abstractmethod
74
- async def turn_on(self) -> bool:
75
- """Turn device on."""
76
-
77
- @abstractmethod
78
- async def turn_off(self) -> bool:
79
- """Turn device off."""
80
-
81
- @abstractmethod
78
+ @update_after_operation
82
79
  async def set_brightness(self, brightness: int) -> bool:
83
80
  """Set brightness."""
81
+ assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
82
+ hex_brightness = f"{brightness:02X}"
83
+ self._check_function_support(self._set_brightness_command)
84
+ result = await self._send_command(
85
+ self._set_brightness_command.format(hex_brightness)
86
+ )
87
+ return self._check_command_result(result, 0, {1})
84
88
 
85
- @abstractmethod
89
+ @update_after_operation
86
90
  async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
87
91
  """Set color temp."""
88
-
89
- @abstractmethod
92
+ assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
93
+ assert 2700 <= color_temp <= 6500, "Color Temp must be between 2700 and 6500"
94
+ hex_data = f"{brightness:02X}{color_temp:04X}"
95
+ self._check_function_support(self._set_color_temp_command)
96
+ result = await self._send_command(self._set_color_temp_command.format(hex_data))
97
+ return self._check_command_result(result, 0, {1})
98
+
99
+ @update_after_operation
90
100
  async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
91
101
  """Set rgb."""
102
+ assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
103
+ assert 0 <= r <= 255, "r must be between 0 and 255"
104
+ assert 0 <= g <= 255, "g must be between 0 and 255"
105
+ assert 0 <= b <= 255, "b must be between 0 and 255"
106
+ self._check_function_support(self._set_rgb_command)
107
+ hex_data = f"{brightness:02X}{r:02X}{g:02X}{b:02X}"
108
+ result = await self._send_command(self._set_rgb_command.format(hex_data))
109
+ return self._check_command_result(result, 0, {1})
110
+
111
+ @update_after_operation
112
+ async def set_effect(self, effect: str) -> bool:
113
+ """Set effect."""
114
+ effect_template = self._effect_dict.get(effect.lower())
115
+ if not effect_template:
116
+ raise SwitchbotOperationError(f"Effect {effect} not supported")
117
+ result = await self._send_multiple_commands(effect_template)
118
+ if result:
119
+ self._override_state({"effect": effect})
120
+ return result
92
121
 
93
122
  async def _send_multiple_commands(self, keys: list[str]) -> bool:
94
123
  """
@@ -103,6 +132,33 @@ class SwitchbotBaseLight(SwitchbotDevice):
103
132
  final_result |= self._check_command_result(result, 0, {1})
104
133
  return final_result
105
134
 
135
+ async def _get_multi_commands_results(
136
+ self, commands: list[str]
137
+ ) -> tuple[bytes, bytes] | None:
138
+ """Check results after sending multiple commands."""
139
+ if not (results := await self._get_basic_info_by_multi_commands(commands)):
140
+ return None
141
+
142
+ _version_info, _data = results[0], results[1]
143
+ _LOGGER.debug(
144
+ "version info: %s, data: %s, address: %s",
145
+ _version_info,
146
+ _data,
147
+ self._device.address,
148
+ )
149
+ return _version_info, _data
150
+
151
+ async def _get_basic_info_by_multi_commands(
152
+ self, commands: list[str]
153
+ ) -> list[bytes] | None:
154
+ """Get device basic settings by sending multiple commands."""
155
+ results = []
156
+ for command in commands:
157
+ if not (result := await self._get_basic_info(command)):
158
+ return None
159
+ results.append(result)
160
+ return results
161
+
106
162
 
107
163
  class SwitchbotSequenceBaseLight(SwitchbotBaseLight):
108
164
  """Representation of a Switchbot light."""
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..const.light import BulbColorMode, ColorMode
6
+ from .base_light import SwitchbotSequenceBaseLight
7
+
8
+ # Private mapping from device-specific color modes to original ColorMode enum
9
+ _BULB_COLOR_MODE_MAP = {
10
+ BulbColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
11
+ BulbColorMode.RGB: ColorMode.RGB,
12
+ BulbColorMode.DYNAMIC: ColorMode.EFFECT,
13
+ BulbColorMode.UNKNOWN: ColorMode.OFF,
14
+ }
15
+ COLOR_BULB_CONTROL_HEADER = "570F4701"
16
+
17
+
18
+ class SwitchbotBulb(SwitchbotSequenceBaseLight):
19
+ """Representation of a Switchbot bulb."""
20
+
21
+ _turn_on_command = f"{COLOR_BULB_CONTROL_HEADER}01"
22
+ _turn_off_command = f"{COLOR_BULB_CONTROL_HEADER}02"
23
+ _set_rgb_command = f"{COLOR_BULB_CONTROL_HEADER}12{{}}"
24
+ _set_color_temp_command = f"{COLOR_BULB_CONTROL_HEADER}13{{}}"
25
+ _set_brightness_command = f"{COLOR_BULB_CONTROL_HEADER}14{{}}"
26
+ _get_basic_info_command = ["570003", "570f4801"]
27
+ _effect_dict = {
28
+ "colorful": ["570F4701010300"],
29
+ "flickering": ["570F4701010301"],
30
+ "breathing": ["570F4701010302"],
31
+ }
32
+
33
+ @property
34
+ def color_modes(self) -> set[ColorMode]:
35
+ """Return the supported color modes."""
36
+ return {ColorMode.RGB, ColorMode.COLOR_TEMP}
37
+
38
+ @property
39
+ def color_mode(self) -> ColorMode:
40
+ """Return the current color mode."""
41
+ device_mode = BulbColorMode(self._get_adv_value("color_mode") or 10)
42
+ return _BULB_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
43
+
44
+ async def get_basic_info(self) -> dict[str, Any] | None:
45
+ """Get device basic settings."""
46
+ if not (
47
+ res := await self._get_multi_commands_results(self._get_basic_info_command)
48
+ ):
49
+ return None
50
+ _version_info, _data = res
51
+
52
+ self._state["r"] = _data[3]
53
+ self._state["g"] = _data[4]
54
+ self._state["b"] = _data[5]
55
+ self._state["cw"] = int.from_bytes(_data[6:8], "big")
56
+
57
+ return {
58
+ "isOn": bool(_data[1] & 0b10000000),
59
+ "brightness": _data[2] & 0b01111111,
60
+ "r": self._state["r"],
61
+ "g": self._state["g"],
62
+ "b": self._state["b"],
63
+ "cw": self._state["cw"],
64
+ "color_mode": _data[10] & 0b00001111,
65
+ "firmware": _version_info[2] / 10.0,
66
+ }
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..const.light import (
6
+ DEFAULT_COLOR_TEMP,
7
+ CeilingLightColorMode,
8
+ ColorMode,
9
+ )
10
+ from .base_light import SwitchbotSequenceBaseLight
11
+ from .device import update_after_operation
12
+
13
+ # Private mapping from device-specific color modes to original ColorMode enum
14
+ _CEILING_LIGHT_COLOR_MODE_MAP = {
15
+ CeilingLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
16
+ CeilingLightColorMode.NIGHT: ColorMode.COLOR_TEMP,
17
+ CeilingLightColorMode.MUSIC: ColorMode.EFFECT,
18
+ CeilingLightColorMode.UNKNOWN: ColorMode.OFF,
19
+ }
20
+ CEILING_LIGHT_CONTROL_HEADER = "570F5401"
21
+
22
+
23
+ class SwitchbotCeilingLight(SwitchbotSequenceBaseLight):
24
+ """Representation of a Switchbot ceiling light."""
25
+
26
+ _turn_on_command = f"{CEILING_LIGHT_CONTROL_HEADER}01FF01FFFF"
27
+ _turn_off_command = f"{CEILING_LIGHT_CONTROL_HEADER}02FF01FFFF"
28
+ _set_brightness_command = f"{CEILING_LIGHT_CONTROL_HEADER}01FF01{{}}"
29
+ _set_color_temp_command = f"{CEILING_LIGHT_CONTROL_HEADER}01FF01{{}}"
30
+ _get_basic_info_command = ["5702", "570f5581"]
31
+
32
+ @property
33
+ def color_modes(self) -> set[ColorMode]:
34
+ """Return the supported color modes."""
35
+ return {ColorMode.COLOR_TEMP}
36
+
37
+ @property
38
+ def color_mode(self) -> ColorMode:
39
+ """Return the current color mode."""
40
+ device_mode = CeilingLightColorMode(self._get_adv_value("color_mode") or 10)
41
+ return _CEILING_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
42
+
43
+ @update_after_operation
44
+ async def set_brightness(self, brightness: int) -> bool:
45
+ """Set brightness."""
46
+ assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
47
+ hex_brightness = f"{brightness:02X}"
48
+ color_temp = self._state.get("cw", DEFAULT_COLOR_TEMP)
49
+ hex_data = f"{hex_brightness}{color_temp:04X}"
50
+ result = await self._send_command(self._set_brightness_command.format(hex_data))
51
+ return self._check_command_result(result, 0, {1})
52
+
53
+ async def get_basic_info(self) -> dict[str, Any] | None:
54
+ """Get device basic settings."""
55
+ if not (
56
+ res := await self._get_multi_commands_results(self._get_basic_info_command)
57
+ ):
58
+ return None
59
+ _version_info, _data = res
60
+
61
+ self._state["cw"] = int.from_bytes(_data[3:5], "big")
62
+
63
+ return {
64
+ "isOn": bool(_data[1] & 0b10000000),
65
+ "color_mode": _data[1] & 0b01000000,
66
+ "brightness": _data[2] & 0b01111111,
67
+ "cw": self._state["cw"],
68
+ "firmware": _version_info[2] / 10.0,
69
+ }
@@ -125,6 +125,10 @@ def _handle_timeout(fut: asyncio.Future[None]) -> None:
125
125
  class SwitchbotBaseDevice:
126
126
  """Base Representation of a Switchbot Device."""
127
127
 
128
+ _turn_on_command: str | None = None
129
+ _turn_off_command: str | None = None
130
+ _press_command: str | None = None
131
+
128
132
  def __init__(
129
133
  self,
130
134
  device: BLEDevice,
@@ -291,7 +295,7 @@ class SwitchbotBaseDevice:
291
295
  """Return RSSI of device."""
292
296
  if self._sb_adv_data:
293
297
  return self._sb_adv_data.rssi
294
- return self._device.rssi
298
+ return -127
295
299
 
296
300
  async def _ensure_connected(self):
297
301
  """Ensure connection to device is established."""
@@ -694,6 +698,34 @@ class SwitchbotBaseDevice:
694
698
  time_since_last_full_update = time.monotonic() - self._last_full_update
695
699
  return not time_since_last_full_update < PASSIVE_POLL_INTERVAL
696
700
 
701
+ def _check_function_support(self, cmd: str | None = None) -> None:
702
+ """Check if the command is supported by the device model."""
703
+ if not cmd:
704
+ raise SwitchbotOperationError(
705
+ f"Current device {self._device.address} does not support this functionality"
706
+ )
707
+
708
+ @update_after_operation
709
+ async def turn_on(self) -> bool:
710
+ """Turn device on."""
711
+ self._check_function_support(self._turn_on_command)
712
+ result = await self._send_command(self._turn_on_command)
713
+ return self._check_command_result(result, 0, {1})
714
+
715
+ @update_after_operation
716
+ async def turn_off(self) -> bool:
717
+ """Turn device off."""
718
+ self._check_function_support(self._turn_off_command)
719
+ result = await self._send_command(self._turn_off_command)
720
+ return self._check_command_result(result, 0, {1})
721
+
722
+ @update_after_operation
723
+ async def press(self) -> bool:
724
+ """Press the device."""
725
+ self._check_function_support(self._press_command)
726
+ result = await self._send_command(self._press_command)
727
+ return self._check_command_result(result, 0, {1})
728
+
697
729
 
698
730
  class SwitchbotDevice(SwitchbotBaseDevice):
699
731
  """
@@ -735,8 +767,8 @@ class SwitchbotEncryptedDevice(SwitchbotDevice):
735
767
  self._encryption_key = bytearray.fromhex(encryption_key)
736
768
  self._iv: bytes | None = None
737
769
  self._cipher: bytes | None = None
738
- self._model = model
739
770
  super().__init__(device, None, interface, **kwargs)
771
+ self._model = model
740
772
 
741
773
  # Old non-async method preserved for backwards compatibility
742
774
  @classmethod
@@ -23,7 +23,6 @@ _LOGGER = logging.getLogger(__name__)
23
23
  COMMAND_HEADER = "57"
24
24
  COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
25
25
  COMMAND_TURN_ON = f"{COMMAND_HEADER}0f430101"
26
- COMMAND_TURN_OFF = f"{COMMAND_HEADER}0f430100"
27
26
  COMMAND_CHILD_LOCK_ON = f"{COMMAND_HEADER}0f430501"
28
27
  COMMAND_CHILD_LOCK_OFF = f"{COMMAND_HEADER}0f430500"
29
28
  COMMAND_AUTO_DRY_ON = f"{COMMAND_HEADER}0f430a01"
@@ -48,6 +47,9 @@ DEVICE_GET_BASIC_SETTINGS_KEY = "570f4481"
48
47
  class SwitchbotEvaporativeHumidifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
49
48
  """Representation of a Switchbot Evaporative Humidifier"""
50
49
 
50
+ _turn_on_command = COMMAND_TURN_ON
51
+ _turn_off_command = f"{COMMAND_HEADER}0f430100"
52
+
51
53
  def __init__(
52
54
  self,
53
55
  device: BLEDevice,
@@ -113,18 +115,6 @@ class SwitchbotEvaporativeHumidifier(SwitchbotSequenceDevice, SwitchbotEncrypted
113
115
  "target_humidity": target_humidity,
114
116
  }
115
117
 
116
- @update_after_operation
117
- async def turn_on(self) -> bool:
118
- """Turn device on."""
119
- result = await self._send_command(COMMAND_TURN_ON)
120
- return self._check_command_result(result, 0, {1})
121
-
122
- @update_after_operation
123
- async def turn_off(self) -> bool:
124
- """Turn device off."""
125
- result = await self._send_command(COMMAND_TURN_OFF)
126
- return self._check_command_result(result, 0, {1})
127
-
128
118
  @update_after_operation
129
119
  async def set_target_humidity(self, target_humidity: int) -> bool:
130
120
  """Set target humidity."""
@@ -16,8 +16,6 @@ _LOGGER = logging.getLogger(__name__)
16
16
 
17
17
 
18
18
  COMMAND_HEAD = "570f41"
19
- COMMAND_TURN_ON = f"{COMMAND_HEAD}0101"
20
- COMMAND_TURN_OFF = f"{COMMAND_HEAD}0102"
21
19
  COMMAND_START_OSCILLATION = f"{COMMAND_HEAD}020101ff"
22
20
  COMMAND_STOP_OSCILLATION = f"{COMMAND_HEAD}020102ff"
23
21
  COMMAND_SET_MODE = {
@@ -33,8 +31,8 @@ COMMAND_GET_BASIC_INFO = "570f428102"
33
31
  class SwitchbotFan(SwitchbotSequenceDevice):
34
32
  """Representation of a Switchbot Circulator Fan."""
35
33
 
36
- def __init__(self, device, password=None, interface=0, **kwargs):
37
- super().__init__(device, password, interface, **kwargs)
34
+ _turn_on_command = f"{COMMAND_HEAD}0101"
35
+ _turn_off_command = f"{COMMAND_HEAD}0102"
38
36
 
39
37
  async def get_basic_info(self) -> dict[str, Any] | None:
40
38
  """Get device basic settings."""
@@ -88,16 +86,6 @@ class SwitchbotFan(SwitchbotSequenceDevice):
88
86
  return await self._send_command(COMMAND_START_OSCILLATION)
89
87
  return await self._send_command(COMMAND_STOP_OSCILLATION)
90
88
 
91
- @update_after_operation
92
- async def turn_on(self) -> bool:
93
- """Turn on the fan."""
94
- return await self._send_command(COMMAND_TURN_ON)
95
-
96
- @update_after_operation
97
- async def turn_off(self) -> bool:
98
- """Turn off the fan."""
99
- return await self._send_command(COMMAND_TURN_OFF)
100
-
101
89
  def get_current_percentage(self) -> Any:
102
90
  """Return cached percentage."""
103
91
  return self._get_adv_value("speed")