PySwitchbot 0.65.0__tar.gz → 0.67.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 (99) hide show
  1. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/PKG-INFO +1 -1
  2. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/SOURCES.txt +6 -0
  4. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/setup.py +1 -1
  5. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/__init__.py +10 -2
  6. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parser.py +20 -3
  7. pyswitchbot-0.67.0/switchbot/adv_parsers/light_strip.py +32 -0
  8. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/lock.py +17 -4
  9. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/__init__.py +12 -0
  10. pyswitchbot-0.67.0/switchbot/const/light.py +34 -0
  11. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/air_purifier.py +3 -14
  12. pyswitchbot-0.67.0/switchbot/devices/base_light.py +179 -0
  13. pyswitchbot-0.67.0/switchbot/devices/bulb.py +66 -0
  14. pyswitchbot-0.67.0/switchbot/devices/ceiling_light.py +69 -0
  15. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/device.py +133 -69
  16. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/evaporative_humidifier.py +3 -13
  17. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/fan.py +2 -14
  18. pyswitchbot-0.67.0/switchbot/devices/light_strip.py +179 -0
  19. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/lock.py +30 -12
  20. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/relay_switch.py +5 -15
  21. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/vacuum.py +0 -3
  22. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_adv_parser.py +184 -8
  23. pyswitchbot-0.67.0/tests/test_bulb.py +251 -0
  24. pyswitchbot-0.67.0/tests/test_ceiling_light.py +185 -0
  25. pyswitchbot-0.67.0/tests/test_colormode_imports.py +88 -0
  26. pyswitchbot-0.67.0/tests/test_encrypted_device.py +367 -0
  27. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_fan.py +8 -5
  28. pyswitchbot-0.67.0/tests/test_lock.py +677 -0
  29. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_relay_switch.py +2 -2
  30. pyswitchbot-0.67.0/tests/test_strip_light.py +359 -0
  31. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_vacuum.py +4 -2
  32. pyswitchbot-0.65.0/switchbot/adv_parsers/light_strip.py +0 -21
  33. pyswitchbot-0.65.0/switchbot/devices/base_light.py +0 -109
  34. pyswitchbot-0.65.0/switchbot/devices/bulb.py +0 -94
  35. pyswitchbot-0.65.0/switchbot/devices/ceiling_light.py +0 -69
  36. pyswitchbot-0.65.0/switchbot/devices/light_strip.py +0 -84
  37. pyswitchbot-0.65.0/tests/test_lock.py +0 -42
  38. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/LICENSE +0 -0
  39. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/MANIFEST.in +0 -0
  40. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  41. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/requires.txt +0 -0
  42. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  43. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/README.md +0 -0
  44. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/pyproject.toml +0 -0
  45. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/setup.cfg +0 -0
  46. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/__init__.py +0 -0
  47. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  48. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  49. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/bot.py +0 -0
  50. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/bulb.py +0 -0
  51. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  52. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/contact.py +0 -0
  53. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/curtain.py +0 -0
  54. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/fan.py +0 -0
  55. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/hub2.py +0 -0
  56. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/hub3.py +0 -0
  57. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  58. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/humidifier.py +0 -0
  59. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/keypad.py +0 -0
  60. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/leak.py +0 -0
  61. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/meter.py +0 -0
  62. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/motion.py +0 -0
  63. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/plug.py +0 -0
  64. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  65. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/remote.py +0 -0
  66. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  67. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/adv_parsers/vacuum.py +0 -0
  68. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/api_config.py +0 -0
  69. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/air_purifier.py +0 -0
  70. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/evaporative_humidifier.py +0 -0
  71. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/fan.py +0 -0
  72. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/hub2.py +0 -0
  73. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/hub3.py +0 -0
  74. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/const/lock.py +0 -0
  75. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/__init__.py +0 -0
  76. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/base_cover.py +0 -0
  77. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/blind_tilt.py +0 -0
  78. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/bot.py +0 -0
  79. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/contact.py +0 -0
  80. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/curtain.py +0 -0
  81. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/humidifier.py +0 -0
  82. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/keypad.py +0 -0
  83. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/meter.py +0 -0
  84. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/motion.py +0 -0
  85. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/plug.py +0 -0
  86. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/devices/roller_shade.py +0 -0
  87. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/discovery.py +0 -0
  88. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/enum.py +0 -0
  89. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/helpers.py +0 -0
  90. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/switchbot/models.py +0 -0
  91. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_air_purifier.py +0 -0
  92. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_base_cover.py +0 -0
  93. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_blind_tilt.py +0 -0
  94. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_curtain.py +0 -0
  95. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_evaporative_humidifier.py +0 -0
  96. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_helpers.py +0 -0
  97. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_hub2.py +0 -0
  98. {pyswitchbot-0.65.0 → pyswitchbot-0.67.0}/tests/test_hub3.py +0 -0
  99. {pyswitchbot-0.65.0 → pyswitchbot-0.67.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.65.0
3
+ Version: 0.67.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.65.0
3
+ Version: 0.67.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
@@ -45,6 +45,7 @@ switchbot/const/evaporative_humidifier.py
45
45
  switchbot/const/fan.py
46
46
  switchbot/const/hub2.py
47
47
  switchbot/const/hub3.py
48
+ switchbot/const/light.py
48
49
  switchbot/const/lock.py
49
50
  switchbot/devices/__init__.py
50
51
  switchbot/devices/air_purifier.py
@@ -73,7 +74,11 @@ tests/test_adv_parser.py
73
74
  tests/test_air_purifier.py
74
75
  tests/test_base_cover.py
75
76
  tests/test_blind_tilt.py
77
+ tests/test_bulb.py
78
+ tests/test_ceiling_light.py
79
+ tests/test_colormode_imports.py
76
80
  tests/test_curtain.py
81
+ tests/test_encrypted_device.py
77
82
  tests/test_evaporative_humidifier.py
78
83
  tests/test_fan.py
79
84
  tests/test_helpers.py
@@ -82,4 +87,5 @@ tests/test_hub3.py
82
87
  tests/test_lock.py
83
88
  tests/test_relay_switch.py
84
89
  tests/test_roller_shade.py
90
+ tests/test_strip_light.py
85
91
  tests/test_vacuum.py
@@ -20,7 +20,7 @@ setup(
20
20
  "cryptography>=39.0.0",
21
21
  "pyOpenSSL>=23.0.0",
22
22
  ],
23
- version="0.65.0",
23
+ version="0.67.0",
24
24
  description="A library to communicate with Switchbot",
25
25
  long_description=long_description,
26
26
  long_description_content_type="text/markdown",
@@ -11,11 +11,15 @@ from bleak_retry_connector import (
11
11
  from .adv_parser import SwitchbotSupportedType, parse_advertisement_data
12
12
  from .const import (
13
13
  AirPurifierMode,
14
+ BulbColorMode,
15
+ CeilingLightColorMode,
16
+ ColorMode,
14
17
  FanMode,
15
18
  HumidifierAction,
16
19
  HumidifierMode,
17
20
  HumidifierWaterLevel,
18
21
  LockStatus,
22
+ StripLightColorMode,
19
23
  SwitchbotAccountConnectionError,
20
24
  SwitchbotApiError,
21
25
  SwitchbotAuthenticationError,
@@ -28,11 +32,11 @@ from .devices.bot import Switchbot
28
32
  from .devices.bulb import SwitchbotBulb
29
33
  from .devices.ceiling_light import SwitchbotCeilingLight
30
34
  from .devices.curtain import SwitchbotCurtain
31
- from .devices.device import ColorMode, SwitchbotDevice, SwitchbotEncryptedDevice
35
+ from .devices.device import SwitchbotDevice, SwitchbotEncryptedDevice
32
36
  from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
33
37
  from .devices.fan import SwitchbotFan
34
38
  from .devices.humidifier import SwitchbotHumidifier
35
- from .devices.light_strip import SwitchbotLightStrip
39
+ from .devices.light_strip import SwitchbotLightStrip, SwitchbotStripLight3
36
40
  from .devices.lock import SwitchbotLock
37
41
  from .devices.plug import SwitchbotPlugMini
38
42
  from .devices.relay_switch import SwitchbotRelaySwitch, SwitchbotRelaySwitch2PM
@@ -43,6 +47,8 @@ from .models import SwitchBotAdvertisement
43
47
 
44
48
  __all__ = [
45
49
  "AirPurifierMode",
50
+ "BulbColorMode",
51
+ "CeilingLightColorMode",
46
52
  "ColorMode",
47
53
  "FanMode",
48
54
  "GetSwitchbotDevices",
@@ -50,6 +56,7 @@ __all__ = [
50
56
  "HumidifierMode",
51
57
  "HumidifierWaterLevel",
52
58
  "LockStatus",
59
+ "StripLightColorMode",
53
60
  "SwitchBotAdvertisement",
54
61
  "Switchbot",
55
62
  "Switchbot",
@@ -76,6 +83,7 @@ __all__ = [
76
83
  "SwitchbotRelaySwitch",
77
84
  "SwitchbotRelaySwitch2PM",
78
85
  "SwitchbotRollerShade",
86
+ "SwitchbotStripLight3",
79
87
  "SwitchbotSupportedType",
80
88
  "SwitchbotSupportedType",
81
89
  "SwitchbotVacuum",
@@ -24,8 +24,13 @@ from .adv_parsers.hubmini_matter import process_hubmini_matter
24
24
  from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
25
25
  from .adv_parsers.keypad import process_wokeypad
26
26
  from .adv_parsers.leak import process_leak
27
- from .adv_parsers.light_strip import process_wostrip
28
- from .adv_parsers.lock import process_lock2, process_wolock, process_wolock_pro
27
+ from .adv_parsers.light_strip import process_light, process_wostrip
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": {
@@ -325,6 +330,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
325
330
  "func": process_relay_switch_2pm,
326
331
  "manufacturer_id": 2409,
327
332
  },
333
+ b"\x00\x10\xd0\xb0": {
334
+ "modelName": SwitchbotModel.FLOOR_LAMP,
335
+ "modelFriendlyName": "Floor Lamp",
336
+ "func": process_light,
337
+ "manufacturer_id": 2409,
338
+ },
339
+ b"\x00\x10\xd0\xb1": {
340
+ "modelName": SwitchbotModel.STRIP_LIGHT_3,
341
+ "modelFriendlyName": "Strip Light 3",
342
+ "func": process_light,
343
+ "manufacturer_id": 2409,
344
+ },
328
345
  }
329
346
 
330
347
  _SWITCHBOT_MODEL_TO_CHAR = {
@@ -0,0 +1,32 @@
1
+ """Light strip adv parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import struct
6
+
7
+
8
+ def process_wostrip(
9
+ data: bytes | None, mfr_data: bytes | None
10
+ ) -> dict[str, bool | int]:
11
+ """Process WoStrip services data."""
12
+ if mfr_data is None:
13
+ return {}
14
+ return {
15
+ "sequence_number": mfr_data[6],
16
+ "isOn": bool(mfr_data[7] & 0b10000000),
17
+ "brightness": mfr_data[7] & 0b01111111,
18
+ "delay": bool(mfr_data[8] & 0b10000000),
19
+ "network_state": (mfr_data[8] & 0b01110000) >> 4,
20
+ "color_mode": mfr_data[8] & 0b00001111,
21
+ }
22
+
23
+
24
+ def process_light(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool | int]:
25
+ """Support for strip light 3 and floor lamp."""
26
+ common_data = process_wostrip(data, mfr_data)
27
+ if not common_data:
28
+ return {}
29
+
30
+ light_data = {"cw": struct.unpack(">H", mfr_data[16:18])[0]}
31
+
32
+ return common_data | light_data
@@ -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),
@@ -10,6 +10,12 @@ from .evaporative_humidifier import (
10
10
  HumidifierWaterLevel,
11
11
  )
12
12
  from .fan import FanMode
13
+ from .light import (
14
+ BulbColorMode,
15
+ CeilingLightColorMode,
16
+ ColorMode,
17
+ StripLightColorMode,
18
+ )
13
19
 
14
20
  # Preserve old LockStatus export for backwards compatibility
15
21
  from .lock import LockStatus
@@ -85,6 +91,8 @@ class SwitchbotModel(StrEnum):
85
91
  LOCK_LITE = "Lock Lite"
86
92
  GARAGE_DOOR_OPENER = "Garage Door Opener"
87
93
  RELAY_SWITCH_2PM = "Relay Switch 2PM"
94
+ STRIP_LIGHT_3 = "Strip Light 3"
95
+ FLOOR_LAMP = "Floor Lamp"
88
96
 
89
97
 
90
98
  __all__ = [
@@ -92,11 +100,15 @@ __all__ = [
92
100
  "DEFAULT_RETRY_TIMEOUT",
93
101
  "DEFAULT_SCAN_TIMEOUT",
94
102
  "AirPurifierMode",
103
+ "BulbColorMode",
104
+ "CeilingLightColorMode",
105
+ "ColorMode",
95
106
  "FanMode",
96
107
  "HumidifierAction",
97
108
  "HumidifierMode",
98
109
  "HumidifierWaterLevel",
99
110
  "LockStatus",
111
+ "StripLightColorMode",
100
112
  "SwitchbotAccountConnectionError",
101
113
  "SwitchbotApiError",
102
114
  "SwitchbotAuthenticationError",
@@ -0,0 +1,34 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ColorMode(Enum):
5
+ OFF = 0
6
+ COLOR_TEMP = 1
7
+ RGB = 2
8
+ EFFECT = 3
9
+
10
+
11
+ class StripLightColorMode(Enum):
12
+ RGB = 2
13
+ SCENE = 3
14
+ MUSIC = 4
15
+ CONTROLLER = 5
16
+ COLOR_TEMP = 6
17
+ UNKNOWN = 10
18
+
19
+
20
+ class BulbColorMode(Enum):
21
+ COLOR_TEMP = 1
22
+ RGB = 2
23
+ DYNAMIC = 3
24
+ UNKNOWN = 10
25
+
26
+
27
+ class CeilingLightColorMode(Enum):
28
+ COLOR_TEMP = 0
29
+ NIGHT = 1
30
+ MUSIC = 4
31
+ UNKNOWN = 10
32
+
33
+
34
+ DEFAULT_COLOR_TEMP = 4001
@@ -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")
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from abc import abstractmethod
5
+ from typing import Any
6
+
7
+ from ..helpers import create_background_task
8
+ from ..models import SwitchBotAdvertisement
9
+ from .device import SwitchbotDevice, SwitchbotOperationError, update_after_operation
10
+
11
+ _LOGGER = logging.getLogger(__name__)
12
+
13
+
14
+ class SwitchbotBaseLight(SwitchbotDevice):
15
+ """Representation of a Switchbot light."""
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
+
22
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
23
+ """Switchbot base light constructor."""
24
+ super().__init__(*args, **kwargs)
25
+ self._state: dict[str, Any] = {}
26
+
27
+ @property
28
+ def on(self) -> bool | None:
29
+ """Return if light is on."""
30
+ return self.is_on()
31
+
32
+ @property
33
+ def rgb(self) -> tuple[int, int, int] | None:
34
+ """Return the current rgb value."""
35
+ if "r" not in self._state or "g" not in self._state or "b" not in self._state:
36
+ return None
37
+ return self._state["r"], self._state["g"], self._state["b"]
38
+
39
+ @property
40
+ def color_temp(self) -> int | None:
41
+ """Return the current color temp value."""
42
+ return self._state.get("cw") or self.min_temp
43
+
44
+ @property
45
+ def brightness(self) -> int | None:
46
+ """Return the current brightness value."""
47
+ return self._get_adv_value("brightness") or 0
48
+
49
+ @property
50
+ @abstractmethod
51
+ def color_mode(self) -> Any:
52
+ """Return the current color mode."""
53
+ raise NotImplementedError("Subclasses must implement color mode")
54
+
55
+ @property
56
+ def min_temp(self) -> int:
57
+ """Return minimum color temp."""
58
+ return 2700
59
+
60
+ @property
61
+ def max_temp(self) -> int:
62
+ """Return maximum color temp."""
63
+ return 6500
64
+
65
+ @property
66
+ def get_effect_list(self) -> list[str] | None:
67
+ """Return the list of supported effects."""
68
+ return list(self._effect_dict) if self._effect_dict else None
69
+
70
+ def is_on(self) -> bool | None:
71
+ """Return bulb state from cache."""
72
+ return self._get_adv_value("isOn")
73
+
74
+ def get_effect(self):
75
+ """Return the current effect."""
76
+ return self._get_adv_value("effect")
77
+
78
+ @update_after_operation
79
+ async def set_brightness(self, brightness: int) -> bool:
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})
88
+
89
+ @update_after_operation
90
+ async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
91
+ """Set color temp."""
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
100
+ async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
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
121
+
122
+ async def _send_multiple_commands(self, keys: list[str]) -> bool:
123
+ """
124
+ Send multiple commands to device.
125
+
126
+ Since we current have no way to tell which command the device
127
+ needs we send both.
128
+ """
129
+ final_result = False
130
+ for key in keys:
131
+ result = await self._send_command(key)
132
+ final_result |= self._check_command_result(result, 0, {1})
133
+ return final_result
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
+
162
+
163
+ class SwitchbotSequenceBaseLight(SwitchbotBaseLight):
164
+ """Representation of a Switchbot light."""
165
+
166
+ def update_from_advertisement(self, advertisement: SwitchBotAdvertisement) -> None:
167
+ """Update device data from advertisement."""
168
+ current_state = self._get_adv_value("sequence_number")
169
+ super().update_from_advertisement(advertisement)
170
+ new_state = self._get_adv_value("sequence_number")
171
+ _LOGGER.debug(
172
+ "%s: update advertisement: %s (seq before: %s) (seq after: %s)",
173
+ self.name,
174
+ advertisement,
175
+ current_state,
176
+ new_state,
177
+ )
178
+ if current_state != new_state:
179
+ create_background_task(self.update())
@@ -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
+ }