Skip to content

Commit 6f01f51

Browse files
zerzhangpre-commit-ci[bot]bdraco
authored
Add rgbicww light support for switchbot (#389)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <[email protected]> Co-authored-by: J. Nick Koston <[email protected]>
1 parent 3f3a7a3 commit 6f01f51

File tree

9 files changed

+464
-63
lines changed

9 files changed

+464
-63
lines changed

switchbot/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@
4040
from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
4141
from .devices.fan import SwitchbotFan
4242
from .devices.humidifier import SwitchbotHumidifier
43-
from .devices.light_strip import SwitchbotLightStrip, SwitchbotStripLight3
43+
from .devices.light_strip import (
44+
SwitchbotLightStrip,
45+
SwitchbotRgbicLight,
46+
SwitchbotStripLight3,
47+
)
4448
from .devices.lock import SwitchbotLock
4549
from .devices.plug import SwitchbotPlugMini
4650
from .devices.relay_switch import (
@@ -92,6 +96,7 @@
9296
"SwitchbotPlugMini",
9397
"SwitchbotRelaySwitch",
9498
"SwitchbotRelaySwitch2PM",
99+
"SwitchbotRgbicLight",
95100
"SwitchbotRollerShade",
96101
"SwitchbotStripLight3",
97102
"SwitchbotSupportedType",

switchbot/adv_parser.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
2525
from .adv_parsers.keypad import process_wokeypad
2626
from .adv_parsers.leak import process_leak
27-
from .adv_parsers.light_strip import process_light, process_wostrip
27+
from .adv_parsers.light_strip import process_light, process_rgbic_light, process_wostrip
2828
from .adv_parsers.lock import (
2929
process_lock2,
3030
process_locklite,
@@ -349,6 +349,18 @@ class SwitchbotSupportedType(TypedDict):
349349
"func": process_relay_switch_1pm,
350350
"manufacturer_id": 2409,
351351
},
352+
b"\x00\x10\xd0\xb3": {
353+
"modelName": SwitchbotModel.RGBICWW_STRIP_LIGHT,
354+
"modelFriendlyName": "RGBICWW Strip Light",
355+
"func": process_rgbic_light,
356+
"manufacturer_id": 2409,
357+
},
358+
b"\x00\x10\xd0\xb4": {
359+
"modelName": SwitchbotModel.RGBICWW_FLOOR_LAMP,
360+
"modelFriendlyName": "RGBICWW Floor Lamp",
361+
"func": process_rgbic_light,
362+
"manufacturer_id": 2409,
363+
},
352364
}
353365

354366
_SWITCHBOT_MODEL_TO_CHAR = {

switchbot/adv_parsers/light_strip.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
import struct
5+
from ..helpers import _UNPACK_UINT16_BE
66

77

88
def process_wostrip(
@@ -21,12 +21,21 @@ def process_wostrip(
2121
}
2222

2323

24-
def process_light(data: bytes | None, mfr_data: bytes | None) -> dict[str, bool | int]:
24+
def process_light(
25+
data: bytes | None, mfr_data: bytes | None, cw_offset: int = 16
26+
) -> dict[str, bool | int]:
2527
"""Support for strip light 3 and floor lamp."""
2628
common_data = process_wostrip(data, mfr_data)
2729
if not common_data:
2830
return {}
2931

30-
light_data = {"cw": struct.unpack(">H", mfr_data[16:18])[0]}
32+
light_data = {"cw": _UNPACK_UINT16_BE(mfr_data, cw_offset)[0]}
3133

3234
return common_data | light_data
35+
36+
37+
def process_rgbic_light(
38+
data: bytes | None, mfr_data: bytes | None
39+
) -> dict[str, bool | int]:
40+
"""Support for RGBIC lights."""
41+
return process_light(data, mfr_data, cw_offset=10)

switchbot/const/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ class SwitchbotModel(StrEnum):
9494
STRIP_LIGHT_3 = "Strip Light 3"
9595
FLOOR_LAMP = "Floor Lamp"
9696
PLUG_MINI_EU = "Plug Mini (EU)"
97+
RGBICWW_STRIP_LIGHT = "RGBICWW Strip Light"
98+
RGBICWW_FLOOR_LAMP = "RGBICWW Floor Lamp"
9799

98100

99101
__all__ = [

switchbot/const/light.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,15 @@ class CeilingLightColorMode(Enum):
3131
UNKNOWN = 10
3232

3333

34+
class RGBICStripLightColorMode(Enum):
35+
SEGMENTED = 1
36+
RGB = 2
37+
SCENE = 3
38+
MUSIC = 4
39+
CONTROLLER = 5
40+
COLOR_TEMP = 6
41+
EFFECT = 7
42+
UNKNOWN = 10
43+
44+
3445
DEFAULT_COLOR_TEMP = 4001

switchbot/devices/light_strip.py

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from bleak.backends.device import BLEDevice
66

77
from ..const import SwitchbotModel
8-
from ..const.light import ColorMode, StripLightColorMode
8+
from ..const.light import ColorMode, RGBICStripLightColorMode, StripLightColorMode
99
from .base_light import SwitchbotSequenceBaseLight
1010
from .device import SwitchbotEncryptedDevice
1111

@@ -18,6 +18,15 @@
1818
StripLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
1919
StripLightColorMode.UNKNOWN: ColorMode.OFF,
2020
}
21+
_RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP = {
22+
RGBICStripLightColorMode.SEGMENTED: ColorMode.EFFECT,
23+
RGBICStripLightColorMode.RGB: ColorMode.RGB,
24+
RGBICStripLightColorMode.SCENE: ColorMode.EFFECT,
25+
RGBICStripLightColorMode.MUSIC: ColorMode.EFFECT,
26+
RGBICStripLightColorMode.CONTROLLER: ColorMode.EFFECT,
27+
RGBICStripLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
28+
RGBICStripLightColorMode.UNKNOWN: ColorMode.OFF,
29+
}
2130
LIGHT_STRIP_CONTROL_HEADER = "570F4901"
2231
COMMON_EFFECTS = {
2332
"christmas": [
@@ -97,6 +106,122 @@
97106
"570F490701000503600C2B35040C",
98107
],
99108
}
109+
RGBIC_EFFECTS = {
110+
"romance": [
111+
"570F490D01350100FF10EE",
112+
"570F490D0363",
113+
],
114+
"energy": [
115+
"570F490D01000300ED070F34FF14FFE114",
116+
"570F490D03FA",
117+
],
118+
"heartbeat": [
119+
"570F490D01020400FFDEADFE90FDFF9E3D",
120+
"570F490D01020403FCBAFD",
121+
"570F490D03FA",
122+
],
123+
"party": [
124+
"570F490D01030400FF8A47FF524DFF4DEE",
125+
"570F490D010304034DFF8C",
126+
"570F490D03FA",
127+
],
128+
"dynamic": [
129+
"570F490D010403004DFFFB4DFF4FFFBF4D",
130+
"570F490D03FA",
131+
],
132+
"mystery": [
133+
"570F490D01050300F660F6F6D460C6F660",
134+
"570F490D03FA",
135+
],
136+
"lightning": [
137+
"570F490D01340100FFD700",
138+
"570F490D03FA",
139+
],
140+
"rock": [
141+
"570F490D01090300B0F6606864FCFFBC3D",
142+
"570F490D03FA",
143+
],
144+
"starlight": [
145+
"570F490D010A0100FF8C00",
146+
"570F490D0363",
147+
],
148+
"valentine_day": [
149+
"570F490D010C0300FDE0FFFFCC8AD7FF8A",
150+
"570F490D03FA",
151+
],
152+
"dream": [
153+
"570F490D010E0300A3E5FF73F019FFA8E5",
154+
"570F490D03FA",
155+
],
156+
"alarm": [
157+
"570F490D013E0100FF0000",
158+
"570F490D03FA",
159+
],
160+
"fireworks": [
161+
"570F490D01110300FFAA33FFE233FF5CDF",
162+
"570F490D03FA",
163+
],
164+
"waves": [
165+
"570F490D013D01001E90FF",
166+
"570F490D03FA",
167+
],
168+
"christmas": [
169+
"570F490D01380400DC143C228B22DAA520",
170+
"570F490D0363",
171+
"570F490D0138040332CD32",
172+
"570F490D0363",
173+
],
174+
"rainbow": [
175+
"570F490D01160600FF0000FF7F00FFFF00",
176+
"570F490D03FA",
177+
"570F490D0116060300FF000000FF9400D3",
178+
"570F490D03FA",
179+
],
180+
"game": [
181+
"570F490D011A0400D05CFF668FFFFFEFD5",
182+
"570F490D0363",
183+
"570F490D011A0403FFC55C",
184+
"570F490D0363",
185+
],
186+
"halloween": [
187+
"570F490D01320300FF8C009370DB32CD32",
188+
"570F490D0364",
189+
],
190+
"meditation": [
191+
"570F490D013502001E90FF9370DB",
192+
"570F490D0364",
193+
],
194+
"starlit_sky": [
195+
"570F490D010D010099C8FF",
196+
"570F490D0364",
197+
],
198+
"sleep": [
199+
"570F490D01370300FF8C002E4E3E3E3E5E",
200+
"570F490D0364",
201+
],
202+
"movie": [
203+
"570F490D013602001919704B0082",
204+
"570F490D0364",
205+
],
206+
"sunrise": [
207+
"570F490D013F0200FFD700FF4500",
208+
"570F490D03FA",
209+
"570F490D03FA",
210+
],
211+
"sunset": [
212+
"570F490D01390300FF4500FFA500483D8B",
213+
"570F490D0363",
214+
"570F490D0363",
215+
],
216+
"new_year": [
217+
"570F490D013F0300FF0000FFD700228B22",
218+
"570F490D0364",
219+
],
220+
"cherry_blossom": [
221+
"570F490D01400200FFB3C1FF69B4",
222+
"570F490D0364",
223+
],
224+
}
100225

101226

102227
class SwitchbotLightStrip(SwitchbotSequenceBaseLight):
@@ -177,3 +302,44 @@ async def verify_encryption_key(
177302
def color_modes(self) -> set[ColorMode]:
178303
"""Return the supported color modes."""
179304
return {ColorMode.RGB, ColorMode.COLOR_TEMP}
305+
306+
307+
class SwitchbotRgbicLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
308+
"""Support for Switchbot RGBIC lights."""
309+
310+
_effect_dict = RGBIC_EFFECTS
311+
312+
def __init__(
313+
self,
314+
device: BLEDevice,
315+
key_id: str,
316+
encryption_key: str,
317+
interface: int = 0,
318+
model: SwitchbotModel = SwitchbotModel.RGBICWW_STRIP_LIGHT,
319+
**kwargs: Any,
320+
) -> None:
321+
super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
322+
323+
@classmethod
324+
async def verify_encryption_key(
325+
cls,
326+
device: BLEDevice,
327+
key_id: str,
328+
encryption_key: str,
329+
model: SwitchbotModel = SwitchbotModel.RGBICWW_STRIP_LIGHT,
330+
**kwargs: Any,
331+
) -> bool:
332+
return await super().verify_encryption_key(
333+
device, key_id, encryption_key, model, **kwargs
334+
)
335+
336+
@property
337+
def color_modes(self) -> set[ColorMode]:
338+
"""Return the supported color modes."""
339+
return {ColorMode.RGB, ColorMode.COLOR_TEMP}
340+
341+
@property
342+
def color_mode(self) -> ColorMode:
343+
"""Return the current color mode."""
344+
device_mode = RGBICStripLightColorMode(self._get_adv_value("color_mode") or 10)
345+
return _RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)

tests/__init__.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,72 @@ class AdvTestCase:
1111
model: str | bytes
1212
modelFriendlyName: str
1313
modelName: SwitchbotModel
14+
15+
16+
STRIP_LIGHT_3_INFO = AdvTestCase(
17+
b'\xc0N0\xe0U\x9a\x85\x9e"\xd0\x00\x00\x00\x00\x00\x00\x12\x91\x00',
18+
b"\x00\x00\x00\x00\x10\xd0\xb1",
19+
{
20+
"sequence_number": 133,
21+
"isOn": True,
22+
"brightness": 30,
23+
"delay": False,
24+
"network_state": 2,
25+
"color_mode": 2,
26+
"cw": 4753,
27+
},
28+
b"\x00\x10\xd0\xb1",
29+
"Strip Light 3",
30+
SwitchbotModel.STRIP_LIGHT_3,
31+
)
32+
33+
FLOOR_LAMP_INFO = AdvTestCase(
34+
b'\xa0\x85\xe3e,\x06P\xaa"\xd4\x00\x00\x00\x00\x00\x00\r\x93\x00',
35+
b"\x00\x00\x00\x00\x10\xd0\xb0",
36+
{
37+
"sequence_number": 80,
38+
"isOn": True,
39+
"brightness": 42,
40+
"delay": False,
41+
"network_state": 2,
42+
"color_mode": 2,
43+
"cw": 3475,
44+
},
45+
b"\x00\x10\xd0\xb0",
46+
"Floor Lamp",
47+
SwitchbotModel.FLOOR_LAMP,
48+
)
49+
50+
RGBICWW_STRIP_LIGHT_INFO = AdvTestCase(
51+
b'(7/L\x94\xb2\x0c\x9e"\x00\x11:\x00',
52+
b"\x00\x00\x00\x00\x10\xd0\xb3",
53+
{
54+
"sequence_number": 12,
55+
"isOn": True,
56+
"brightness": 30,
57+
"delay": False,
58+
"network_state": 2,
59+
"color_mode": 2,
60+
"cw": 4410,
61+
},
62+
b"\x00\x10\xd0\xb3",
63+
"Rgbic Strip Light",
64+
SwitchbotModel.RGBICWW_STRIP_LIGHT,
65+
)
66+
67+
RGBICWW_FLOOR_LAMP_INFO = AdvTestCase(
68+
b'\xdc\x06u\xa6\xfb\xb2y\x9e"\x00\x11\xb8\x00',
69+
b"\x00\x00\x00\x00\x10\xd0\xb4",
70+
{
71+
"sequence_number": 121,
72+
"isOn": True,
73+
"brightness": 30,
74+
"delay": False,
75+
"network_state": 2,
76+
"color_mode": 2,
77+
"cw": 4536,
78+
},
79+
b"\x00\x10\xd0\xb4",
80+
"Rgbic Floor Lamp",
81+
SwitchbotModel.RGBICWW_FLOOR_LAMP,
82+
)

0 commit comments

Comments
 (0)