PySwitchbot 0.65.0__tar.gz → 0.66.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 (97) hide show
  1. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/PKG-INFO +1 -1
  2. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
  3. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/PySwitchbot.egg-info/SOURCES.txt +6 -0
  4. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/setup.py +1 -1
  5. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/__init__.py +10 -2
  6. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parser.py +13 -1
  7. pyswitchbot-0.66.0/switchbot/adv_parsers/light_strip.py +32 -0
  8. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/__init__.py +12 -0
  9. pyswitchbot-0.66.0/switchbot/const/light.py +34 -0
  10. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/base_light.py +25 -11
  11. pyswitchbot-0.66.0/switchbot/devices/bulb.py +143 -0
  12. pyswitchbot-0.66.0/switchbot/devices/ceiling_light.py +105 -0
  13. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/device.py +108 -68
  14. pyswitchbot-0.66.0/switchbot/devices/light_strip.py +259 -0
  15. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_adv_parser.py +182 -0
  16. pyswitchbot-0.66.0/tests/test_bulb.py +220 -0
  17. pyswitchbot-0.66.0/tests/test_ceiling_light.py +179 -0
  18. pyswitchbot-0.66.0/tests/test_colormode_imports.py +88 -0
  19. pyswitchbot-0.66.0/tests/test_encrypted_device.py +367 -0
  20. pyswitchbot-0.66.0/tests/test_strip_light.py +302 -0
  21. pyswitchbot-0.65.0/switchbot/adv_parsers/light_strip.py +0 -21
  22. pyswitchbot-0.65.0/switchbot/devices/bulb.py +0 -94
  23. pyswitchbot-0.65.0/switchbot/devices/ceiling_light.py +0 -69
  24. pyswitchbot-0.65.0/switchbot/devices/light_strip.py +0 -84
  25. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/LICENSE +0 -0
  26. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/MANIFEST.in +0 -0
  27. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
  28. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/PySwitchbot.egg-info/requires.txt +0 -0
  29. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/PySwitchbot.egg-info/top_level.txt +0 -0
  30. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/README.md +0 -0
  31. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/pyproject.toml +0 -0
  32. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/setup.cfg +0 -0
  33. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/__init__.py +0 -0
  34. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/air_purifier.py +0 -0
  35. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
  36. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/bot.py +0 -0
  37. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/bulb.py +0 -0
  38. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
  39. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/contact.py +0 -0
  40. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/curtain.py +0 -0
  41. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/fan.py +0 -0
  42. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/hub2.py +0 -0
  43. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/hub3.py +0 -0
  44. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
  45. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/humidifier.py +0 -0
  46. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/keypad.py +0 -0
  47. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/leak.py +0 -0
  48. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/lock.py +0 -0
  49. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/meter.py +0 -0
  50. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/motion.py +0 -0
  51. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/plug.py +0 -0
  52. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/relay_switch.py +0 -0
  53. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/remote.py +0 -0
  54. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/roller_shade.py +0 -0
  55. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/adv_parsers/vacuum.py +0 -0
  56. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/api_config.py +0 -0
  57. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/air_purifier.py +0 -0
  58. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/evaporative_humidifier.py +0 -0
  59. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/fan.py +0 -0
  60. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/hub2.py +0 -0
  61. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/hub3.py +0 -0
  62. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/const/lock.py +0 -0
  63. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/__init__.py +0 -0
  64. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/air_purifier.py +0 -0
  65. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/base_cover.py +0 -0
  66. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/blind_tilt.py +0 -0
  67. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/bot.py +0 -0
  68. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/contact.py +0 -0
  69. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/curtain.py +0 -0
  70. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/evaporative_humidifier.py +0 -0
  71. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/fan.py +0 -0
  72. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/humidifier.py +0 -0
  73. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/keypad.py +0 -0
  74. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/lock.py +0 -0
  75. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/meter.py +0 -0
  76. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/motion.py +0 -0
  77. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/plug.py +0 -0
  78. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/relay_switch.py +0 -0
  79. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/roller_shade.py +0 -0
  80. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/devices/vacuum.py +0 -0
  81. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/discovery.py +0 -0
  82. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/enum.py +0 -0
  83. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/helpers.py +0 -0
  84. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/switchbot/models.py +0 -0
  85. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_air_purifier.py +0 -0
  86. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_base_cover.py +0 -0
  87. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_blind_tilt.py +0 -0
  88. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_curtain.py +0 -0
  89. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_evaporative_humidifier.py +0 -0
  90. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_fan.py +0 -0
  91. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_helpers.py +0 -0
  92. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_hub2.py +0 -0
  93. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_hub3.py +0 -0
  94. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_lock.py +0 -0
  95. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_relay_switch.py +0 -0
  96. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_roller_shade.py +0 -0
  97. {pyswitchbot-0.65.0 → pyswitchbot-0.66.0}/tests/test_vacuum.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.66.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.66.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.66.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,7 +24,7 @@ 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
27
+ from .adv_parsers.light_strip import process_light, process_wostrip
28
28
  from .adv_parsers.lock import process_lock2, process_wolock, process_wolock_pro
29
29
  from .adv_parsers.meter import process_wosensorth, process_wosensorth_c
30
30
  from .adv_parsers.motion import process_wopresence
@@ -325,6 +325,18 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
325
325
  "func": process_relay_switch_2pm,
326
326
  "manufacturer_id": 2409,
327
327
  },
328
+ b"\x00\x10\xd0\xb0": {
329
+ "modelName": SwitchbotModel.FLOOR_LAMP,
330
+ "modelFriendlyName": "Floor Lamp",
331
+ "func": process_light,
332
+ "manufacturer_id": 2409,
333
+ },
334
+ b"\x00\x10\xd0\xb1": {
335
+ "modelName": SwitchbotModel.STRIP_LIGHT_3,
336
+ "modelFriendlyName": "Strip Light 3",
337
+ "func": process_light,
338
+ "manufacturer_id": 2409,
339
+ },
328
340
  }
329
341
 
330
342
  _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
@@ -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
@@ -1,13 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- import time
5
4
  from abc import abstractmethod
6
5
  from typing import Any
7
6
 
8
7
  from ..helpers import create_background_task
9
8
  from ..models import SwitchBotAdvertisement
10
- from .device import ColorMode, SwitchbotDevice
9
+ from .device import SwitchbotDevice
11
10
 
12
11
  _LOGGER = logging.getLogger(__name__)
13
12
 
@@ -43,9 +42,10 @@ class SwitchbotBaseLight(SwitchbotDevice):
43
42
  return self._get_adv_value("brightness") or 0
44
43
 
45
44
  @property
46
- def color_mode(self) -> ColorMode:
45
+ @abstractmethod
46
+ def color_mode(self) -> Any:
47
47
  """Return the current color mode."""
48
- return ColorMode(self._get_adv_value("color_mode") or 0)
48
+ raise NotImplementedError("Subclasses must implement color mode")
49
49
 
50
50
  @property
51
51
  def min_temp(self) -> int:
@@ -57,10 +57,19 @@ class SwitchbotBaseLight(SwitchbotDevice):
57
57
  """Return maximum color temp."""
58
58
  return 6500
59
59
 
60
+ @property
61
+ def get_effect_list(self) -> list[str] | None:
62
+ """Return the list of supported effects."""
63
+ return None
64
+
60
65
  def is_on(self) -> bool | None:
61
66
  """Return bulb state from cache."""
62
67
  return self._get_adv_value("isOn")
63
68
 
69
+ def get_effect(self):
70
+ """Return the current effect."""
71
+ return self._get_adv_value("effect")
72
+
64
73
  @abstractmethod
65
74
  async def turn_on(self) -> bool:
66
75
  """Turn device on."""
@@ -81,13 +90,18 @@ class SwitchbotBaseLight(SwitchbotDevice):
81
90
  async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
82
91
  """Set rgb."""
83
92
 
84
- def poll_needed(self, last_poll_time: float | None) -> bool:
85
- """Return if poll is needed."""
86
- return False
87
-
88
- async def update(self) -> None:
89
- """Update device data."""
90
- self._last_full_update = time.monotonic()
93
+ async def _send_multiple_commands(self, keys: list[str]) -> bool:
94
+ """
95
+ Send multiple commands to device.
96
+
97
+ Since we current have no way to tell which command the device
98
+ needs we send both.
99
+ """
100
+ final_result = False
101
+ for key in keys:
102
+ result = await self._send_command(key)
103
+ final_result |= self._check_command_result(result, 0, {1})
104
+ return final_result
91
105
 
92
106
 
93
107
  class SwitchbotSequenceBaseLight(SwitchbotBaseLight):
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from ..const.light import BulbColorMode, ColorMode
7
+ from .base_light import SwitchbotSequenceBaseLight
8
+ from .device import REQ_HEADER, SwitchbotOperationError, update_after_operation
9
+
10
+ BULB_COMMAND_HEADER = "4701"
11
+ BULB_REQUEST = f"{REQ_HEADER}4801"
12
+
13
+ BULB_COMMAND = f"{REQ_HEADER}{BULB_COMMAND_HEADER}"
14
+ # Bulb keys
15
+ BULB_ON_KEY = f"{BULB_COMMAND}01"
16
+ BULB_OFF_KEY = f"{BULB_COMMAND}02"
17
+ RGB_BRIGHTNESS_KEY = f"{BULB_COMMAND}12"
18
+ CW_BRIGHTNESS_KEY = f"{BULB_COMMAND}13"
19
+ BRIGHTNESS_KEY = f"{BULB_COMMAND}14"
20
+ RGB_KEY = f"{BULB_COMMAND}16"
21
+ CW_KEY = f"{BULB_COMMAND}17"
22
+
23
+ DEVICE_GET_VERSION_KEY = "570003"
24
+ DEVICE_GET_BASIC_SETTINGS_KEY = "570f4801"
25
+
26
+ _LOGGER = logging.getLogger(__name__)
27
+
28
+
29
+ EFFECT_DICT = {
30
+ "Colorful": "570F4701010300",
31
+ "Flickering": "570F4701010301",
32
+ "Breathing": "570F4701010302",
33
+ }
34
+
35
+ # Private mapping from device-specific color modes to original ColorMode enum
36
+ _BULB_COLOR_MODE_MAP = {
37
+ BulbColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
38
+ BulbColorMode.RGB: ColorMode.RGB,
39
+ BulbColorMode.DYNAMIC: ColorMode.EFFECT,
40
+ BulbColorMode.UNKNOWN: ColorMode.OFF,
41
+ }
42
+
43
+
44
+ class SwitchbotBulb(SwitchbotSequenceBaseLight):
45
+ """Representation of a Switchbot bulb."""
46
+
47
+ @property
48
+ def color_modes(self) -> set[ColorMode]:
49
+ """Return the supported color modes."""
50
+ return {ColorMode.RGB, ColorMode.COLOR_TEMP}
51
+
52
+ @property
53
+ def color_mode(self) -> ColorMode:
54
+ """Return the current color mode."""
55
+ device_mode = BulbColorMode(self._get_adv_value("color_mode") or 10)
56
+ return _BULB_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
57
+
58
+ @property
59
+ def get_effect_list(self) -> list[str]:
60
+ """Return the list of supported effects."""
61
+ return list(EFFECT_DICT.keys())
62
+
63
+ @update_after_operation
64
+ async def turn_on(self) -> bool:
65
+ """Turn device on."""
66
+ result = await self._send_command(BULB_ON_KEY)
67
+ return self._check_command_result(result, 0, {1})
68
+
69
+ @update_after_operation
70
+ async def turn_off(self) -> bool:
71
+ """Turn device off."""
72
+ result = await self._send_command(BULB_OFF_KEY)
73
+ return self._check_command_result(result, 0, {1})
74
+
75
+ @update_after_operation
76
+ async def set_brightness(self, brightness: int) -> bool:
77
+ """Set brightness."""
78
+ assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
79
+ result = await self._send_command(f"{BRIGHTNESS_KEY}{brightness:02X}")
80
+ return self._check_command_result(result, 0, {1})
81
+
82
+ @update_after_operation
83
+ async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
84
+ """Set color temp."""
85
+ assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
86
+ assert 2700 <= color_temp <= 6500, "Color Temp must be between 2700 and 6500"
87
+ result = await self._send_command(
88
+ f"{CW_BRIGHTNESS_KEY}{brightness:02X}{color_temp:04X}"
89
+ )
90
+ return self._check_command_result(result, 0, {1})
91
+
92
+ @update_after_operation
93
+ async def set_rgb(self, brightness: int, r: int, g: int, b: int) -> bool:
94
+ """Set rgb."""
95
+ assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
96
+ assert 0 <= r <= 255, "r must be between 0 and 255"
97
+ assert 0 <= g <= 255, "g must be between 0 and 255"
98
+ assert 0 <= b <= 255, "b must be between 0 and 255"
99
+ result = await self._send_command(
100
+ f"{RGB_BRIGHTNESS_KEY}{brightness:02X}{r:02X}{g:02X}{b:02X}"
101
+ )
102
+ return self._check_command_result(result, 0, {1})
103
+
104
+ @update_after_operation
105
+ async def set_effect(self, effect: str) -> bool:
106
+ """Set effect."""
107
+ effect_template = EFFECT_DICT.get(effect)
108
+ if not effect_template:
109
+ raise SwitchbotOperationError(f"Effect {effect} not supported")
110
+ result = await self._send_command(effect_template)
111
+ if result:
112
+ self._override_state({"effect": effect})
113
+ return result
114
+
115
+ async def get_basic_info(self) -> dict[str, Any] | None:
116
+ """Get device basic settings."""
117
+ if not (_data := await self._get_basic_info(DEVICE_GET_BASIC_SETTINGS_KEY)):
118
+ return None
119
+ if not (_version_info := await self._get_basic_info(DEVICE_GET_VERSION_KEY)):
120
+ return None
121
+
122
+ _LOGGER.debug(
123
+ "data: %s, version info: %s, address: %s",
124
+ _data,
125
+ _version_info,
126
+ self._device.address,
127
+ )
128
+
129
+ self._state["r"] = _data[3]
130
+ self._state["g"] = _data[4]
131
+ self._state["b"] = _data[5]
132
+ self._state["cw"] = int.from_bytes(_data[6:8], "big")
133
+
134
+ return {
135
+ "isOn": bool(_data[1] & 0b10000000),
136
+ "brightness": _data[2] & 0b01111111,
137
+ "r": self._state["r"],
138
+ "g": self._state["g"],
139
+ "b": self._state["b"],
140
+ "cw": self._state["cw"],
141
+ "color_mode": _data[10] & 0b00001111,
142
+ "firmware": _version_info[2] / 10.0,
143
+ }
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from ..const.light import (
7
+ DEFAULT_COLOR_TEMP,
8
+ CeilingLightColorMode,
9
+ ColorMode,
10
+ )
11
+ from .base_light import SwitchbotSequenceBaseLight
12
+ from .device import REQ_HEADER, update_after_operation
13
+
14
+ CEILING_LIGHT_COMMAND_HEADER = "5401"
15
+ CEILING_LIGHT_REQUEST = f"{REQ_HEADER}5501"
16
+
17
+ CEILING_LIGHT_COMMAND = f"{REQ_HEADER}{CEILING_LIGHT_COMMAND_HEADER}"
18
+ CEILING_LIGHT_ON_KEY = f"{CEILING_LIGHT_COMMAND}01FF01FFFF"
19
+ CEILING_LIGHT_OFF_KEY = f"{CEILING_LIGHT_COMMAND}02FF01FFFF"
20
+ CW_BRIGHTNESS_KEY = f"{CEILING_LIGHT_COMMAND}010001"
21
+ BRIGHTNESS_KEY = f"{CEILING_LIGHT_COMMAND}01FF01"
22
+
23
+ DEVICE_GET_VERSION_KEY = "5702"
24
+ DEVICE_GET_BASIC_SETTINGS_KEY = "570f5581"
25
+
26
+ _LOGGER = logging.getLogger(__name__)
27
+
28
+ # Private mapping from device-specific color modes to original ColorMode enum
29
+ _CEILING_LIGHT_COLOR_MODE_MAP = {
30
+ CeilingLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
31
+ CeilingLightColorMode.NIGHT: ColorMode.COLOR_TEMP,
32
+ CeilingLightColorMode.MUSIC: ColorMode.EFFECT,
33
+ CeilingLightColorMode.UNKNOWN: ColorMode.OFF,
34
+ }
35
+
36
+
37
+ class SwitchbotCeilingLight(SwitchbotSequenceBaseLight):
38
+ """Representation of a Switchbot ceiling light."""
39
+
40
+ @property
41
+ def color_modes(self) -> set[ColorMode]:
42
+ """Return the supported color modes."""
43
+ return {ColorMode.COLOR_TEMP}
44
+
45
+ @property
46
+ def color_mode(self) -> ColorMode:
47
+ """Return the current color mode."""
48
+ device_mode = CeilingLightColorMode(self._get_adv_value("color_mode") or 10)
49
+ return _CEILING_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
50
+
51
+ @update_after_operation
52
+ async def turn_on(self) -> bool:
53
+ """Turn device on."""
54
+ result = await self._send_command(CEILING_LIGHT_ON_KEY)
55
+ return self._check_command_result(result, 0, {1})
56
+
57
+ @update_after_operation
58
+ async def turn_off(self) -> bool:
59
+ """Turn device off."""
60
+ result = await self._send_command(CEILING_LIGHT_OFF_KEY)
61
+ return self._check_command_result(result, 0, {1})
62
+
63
+ @update_after_operation
64
+ async def set_brightness(self, brightness: int) -> bool:
65
+ """Set brightness."""
66
+ assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
67
+ color_temp = self._state.get("cw", DEFAULT_COLOR_TEMP)
68
+ result = await self._send_command(
69
+ f"{BRIGHTNESS_KEY}{brightness:02X}{color_temp:04X}"
70
+ )
71
+ return self._check_command_result(result, 0, {1})
72
+
73
+ @update_after_operation
74
+ async def set_color_temp(self, brightness: int, color_temp: int) -> bool:
75
+ """Set color temp."""
76
+ assert 0 <= brightness <= 100, "Brightness must be between 0 and 100"
77
+ assert 2700 <= color_temp <= 6500, "Color Temp must be between 2700 and 6500"
78
+ result = await self._send_command(
79
+ f"{CW_BRIGHTNESS_KEY}{brightness:02X}{color_temp:04X}"
80
+ )
81
+ return self._check_command_result(result, 0, {1})
82
+
83
+ async def get_basic_info(self) -> dict[str, Any] | None:
84
+ """Get device basic settings."""
85
+ if not (_data := await self._get_basic_info(DEVICE_GET_BASIC_SETTINGS_KEY)):
86
+ return None
87
+ if not (_version_info := await self._get_basic_info(DEVICE_GET_VERSION_KEY)):
88
+ return None
89
+
90
+ _LOGGER.debug(
91
+ "data: %s, version info: %s, address: %s",
92
+ _data,
93
+ _version_info,
94
+ self._device.address,
95
+ )
96
+
97
+ self._state["cw"] = int.from_bytes(_data[3:5], "big")
98
+
99
+ return {
100
+ "isOn": bool(_data[1] & 0b10000000),
101
+ "color_mode": _data[1] & 0b01000000,
102
+ "brightness": _data[2] & 0b01111111,
103
+ "cw": self._state["cw"],
104
+ "firmware": _version_info[2] / 10.0,
105
+ }