| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- from __future__ import annotations
- from typing import Any
- from ..const import SwitchbotModel
- from ..const.light import (
- ColorMode,
- RGBICStripLightColorMode,
- RGBICWWCeilingLightColorMode,
- StripLightColorMode,
- )
- from .base_light import SwitchbotSequenceBaseLight
- from .device import SwitchbotEncryptedDevice, update_after_operation
- # Private mapping from device-specific color modes to original ColorMode enum
- _STRIP_LIGHT_COLOR_MODE_MAP = {
- StripLightColorMode.RGB: ColorMode.RGB,
- StripLightColorMode.SCENE: ColorMode.EFFECT,
- StripLightColorMode.MUSIC: ColorMode.EFFECT,
- StripLightColorMode.CONTROLLER: ColorMode.EFFECT,
- StripLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
- StripLightColorMode.UNKNOWN: ColorMode.OFF,
- }
- _RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP = {
- RGBICStripLightColorMode.SEGMENTED: ColorMode.EFFECT,
- RGBICStripLightColorMode.RGB: ColorMode.RGB,
- RGBICStripLightColorMode.SCENE: ColorMode.EFFECT,
- RGBICStripLightColorMode.MUSIC: ColorMode.EFFECT,
- RGBICStripLightColorMode.CONTROLLER: ColorMode.EFFECT,
- RGBICStripLightColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
- RGBICStripLightColorMode.EFFECT: ColorMode.EFFECT,
- RGBICStripLightColorMode.UNKNOWN: ColorMode.OFF,
- }
- _RGBICWW_CEILING_LIGHT_COLOR_MODE_MAP = {
- RGBICWWCeilingLightColorMode.SEGMENTED: ColorMode.EFFECT,
- RGBICWWCeilingLightColorMode.COLOR: ColorMode.RGB,
- RGBICWWCeilingLightColorMode.SCENE: ColorMode.EFFECT,
- RGBICWWCeilingLightColorMode.MUSIC: ColorMode.EFFECT,
- RGBICWWCeilingLightColorMode.CONTROLLER: ColorMode.EFFECT,
- RGBICWWCeilingLightColorMode.WARMWHITE: ColorMode.COLOR_TEMP,
- RGBICWWCeilingLightColorMode.EFFECT: ColorMode.EFFECT,
- RGBICWWCeilingLightColorMode.UNKNOWN: ColorMode.OFF,
- }
- LIGHT_STRIP_CONTROL_HEADER = "570F4901"
- COMMON_EFFECTS = {
- "christmas": [
- "570F49070200033C01",
- "570F490701000600009902006D0EFF0021",
- "570F490701000603009902006D0EFF0021",
- ],
- "halloween": ["570F49070200053C04", "570F490701000300FF6A009E00ED00EA0F"],
- "sunset": [
- "570F49070200033C3C",
- "570F490701000900FF9000ED8C04DD5800",
- "570F490701000903FF2E008E0B004F0500",
- "570F4907010009063F0010270056140033",
- ],
- "vitality": [
- "570F49070200053C02",
- "570F490701000600C5003FD9530AEC9800",
- "570F490701000603FFDF0000895500468B",
- ],
- "flashing": [
- "570F49070200053C02",
- "570F4907010006000000FF00FF00FF0000",
- "570F490701000603FFFF0000FFFFA020F0",
- ],
- "strobe": ["570F49070200043C02", "570F490701000300FF00E19D70FFFF0515"],
- "fade": [
- "570F49070200043C04",
- "570F490701000500FF5481FF00E19D70FF",
- "570F490701000503FF0515FF7FEB",
- ],
- "smooth": [
- "570F49070200033C02",
- "570F4907010007000036FC00F6FF00ED13",
- "570F490701000703F6FF00FF8300FF0800",
- "570F490701000706FF00E1",
- ],
- "forest": [
- "570F49070200033C06",
- "570F490701000400006400228B223CB371",
- "570F49070100040390EE90",
- ],
- "ocean": [
- "570F49070200033C06",
- "570F4907010007004400FF0061FF007BFF",
- "570F490701000703009DFF00B2FF00CBFF",
- "570F49070100070600E9FF",
- ],
- "autumn": [
- "570F49070200043C05",
- "570F490701000700D10035922D13A16501",
- "570F490701000703AB9100DD8C00F4AA29",
- "570F490701000706E8D000",
- ],
- "cool": [
- "570F49070200043C04",
- "570F490701000600001A63006C9A00468B",
- "570F490701000603009DA50089BE4378B6",
- ],
- "flow": [
- "570F49070200033C02",
- "570F490701000600FF00D8E100FFAA00FF",
- "570F4907010006037F00FF5000FF1900FF",
- ],
- "relax": [
- "570F49070200033C03",
- "570F490701000400FF8C00FF7200FF1D00",
- "570F490701000403FF5500",
- ],
- "modern": [
- "570F49070200043C03",
- "570F49070100060089231A5F8969829E5A",
- "570F490701000603BCB05EEDBE5AFF9D60",
- ],
- "rose": [
- "570F49070200043C04",
- "570F490701000500FF1969BC215F7C0225",
- "570F490701000503600C2B35040C",
- ],
- }
- RGBIC_EFFECTS = {
- "romance": [
- "570F490D01350100FF10EE",
- "570F490D0363",
- ],
- "energy": [
- "570F490D01000300ED070F34FF14FFE114",
- "570F490D03FA",
- ],
- "heartbeat": [
- "570F490D01020400FFDEADFE90FDFF9E3D",
- "570F490D01020403FCBAFD",
- "570F490D03FA",
- ],
- "party": [
- "570F490D01030400FF8A47FF524DFF4DEE",
- "570F490D010304034DFF8C",
- "570F490D03FA",
- ],
- "dynamic": [
- "570F490D010403004DFFFB4DFF4FFFBF4D",
- "570F490D03FA",
- ],
- "mystery": [
- "570F490D01050300F660F6F6D460C6F660",
- "570F490D03FA",
- ],
- "lightning": [
- "570F490D01340100FFD700",
- "570F490D03FA",
- ],
- "rock": [
- "570F490D01090300B0F6606864FCFFBC3D",
- "570F490D03FA",
- ],
- "starlight": [
- "570F490D010A0100FF8C00",
- "570F490D0363",
- ],
- "valentine_day": [
- "570F490D010C0300FDE0FFFFCC8AD7FF8A",
- "570F490D03FA",
- ],
- "dream": [
- "570F490D010E0300A3E5FF73F019FFA8E5",
- "570F490D03FA",
- ],
- "alarm": [
- "570F490D013E0100FF0000",
- "570F490D03FA",
- ],
- "fireworks": [
- "570F490D01110300FFAA33FFE233FF5CDF",
- "570F490D03FA",
- ],
- "waves": [
- "570F490D013D01001E90FF",
- "570F490D03FA",
- ],
- "christmas": [
- "570F490D01380400DC143C228B22DAA520",
- "570F490D0363",
- "570F490D0138040332CD32",
- "570F490D0363",
- ],
- "rainbow": [
- "570F490D01160600FF0000FF7F00FFFF00",
- "570F490D03FA",
- "570F490D0116060300FF000000FF9400D3",
- "570F490D03FA",
- ],
- "game": [
- "570F490D011A0400D05CFF668FFFFFEFD5",
- "570F490D0363",
- "570F490D011A0403FFC55C",
- "570F490D0363",
- ],
- "halloween": [
- "570F490D01320300FF8C009370DB32CD32",
- "570F490D0364",
- ],
- "meditation": [
- "570F490D013502001E90FF9370DB",
- "570F490D0364",
- ],
- "starlit_sky": [
- "570F490D010D010099C8FF",
- "570F490D0364",
- ],
- "sleep": [
- "570F490D01370300FF8C002E4E3E3E3E5E",
- "570F490D0364",
- ],
- "movie": [
- "570F490D013602001919704B0082",
- "570F490D0364",
- ],
- "sunrise": [
- "570F490D013F0200FFD700FF4500",
- "570F490D03FA",
- "570F490D03FA",
- ],
- "sunset": [
- "570F490D01390300FF4500FFA500483D8B",
- "570F490D0363",
- "570F490D0363",
- ],
- "new_year": [
- "570F490D013F0300FF0000FFD700228B22",
- "570F490D0364",
- ],
- "cherry_blossom": [
- "570F490D01400200FFB3C1FF69B4",
- "570F490D0364",
- ],
- }
- class SwitchbotLightStrip(SwitchbotSequenceBaseLight):
- """Representation of a Switchbot light strip."""
- _effect_dict = COMMON_EFFECTS
- _turn_on_command = f"{LIGHT_STRIP_CONTROL_HEADER}01"
- _turn_off_command = f"{LIGHT_STRIP_CONTROL_HEADER}02"
- _set_rgb_command = f"{LIGHT_STRIP_CONTROL_HEADER}12{{}}"
- _set_color_temp_command = f"{LIGHT_STRIP_CONTROL_HEADER}11{{}}"
- _set_brightness_command = f"{LIGHT_STRIP_CONTROL_HEADER}14{{}}"
- _get_basic_info_command = ["570003", "570f4A01"]
- @property
- def color_modes(self) -> set[ColorMode]:
- """Return the supported color modes."""
- return {ColorMode.RGB}
- @property
- def color_mode(self) -> ColorMode:
- """Return the current color mode."""
- device_mode = StripLightColorMode(self._get_adv_value("color_mode") or 10)
- return _STRIP_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
- async def get_basic_info(self) -> dict[str, Any] | None:
- """Get device basic settings."""
- if not (
- res := await self._get_multi_commands_results(self._get_basic_info_command)
- ):
- return None
- _version_info, _data = res
- self._state["r"] = _data[3]
- self._state["g"] = _data[4]
- self._state["b"] = _data[5]
- self._state["cw"] = int.from_bytes(_data[7:9], "big")
- return {
- "isOn": bool(_data[1] & 0b10000000),
- "brightness": _data[2] & 0b01111111,
- "r": self._state["r"],
- "g": self._state["g"],
- "b": self._state["b"],
- "cw": self._state["cw"],
- "color_mode": _data[10] & 0b00001111,
- "firmware": _version_info[2] / 10.0,
- }
- class SwitchbotStripLight3(SwitchbotEncryptedDevice, SwitchbotLightStrip):
- """Support for switchbot strip light3 and floor lamp."""
- _model = SwitchbotModel.STRIP_LIGHT_3
- @property
- def color_modes(self) -> set[ColorMode]:
- """Return the supported color modes."""
- return {ColorMode.RGB, ColorMode.COLOR_TEMP}
- class SwitchbotCandleWarmerLamp(SwitchbotEncryptedDevice, SwitchbotLightStrip):
- """Support for Switchbot Candle Warmer Lamp."""
- _model = SwitchbotModel.CANDLE_WARMER_LAMP
- _effect_dict = {}
- _set_rgb_command = ""
- _set_color_temp_command = ""
- @property
- def color_modes(self) -> set[ColorMode]:
- """Return the supported color modes."""
- return {ColorMode.BRIGHTNESS}
- @property
- def color_mode(self) -> ColorMode:
- """Return the current color mode."""
- return ColorMode.BRIGHTNESS
- async def get_basic_info(self) -> dict[str, Any] | None:
- """Get device basic settings."""
- if not (
- res := await self._get_multi_commands_results(self._get_basic_info_command)
- ):
- return None
- _version_info, _data = res
- return {
- "isOn": bool(_data[1] & 0b10000000),
- "brightness": _data[2] & 0b01111111,
- "firmware": _version_info[2] / 10.0,
- }
- class SwitchbotRgbicLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
- """Support for Switchbot RGBIC lights."""
- _model = SwitchbotModel.RGBICWW_STRIP_LIGHT
- _effect_dict = RGBIC_EFFECTS
- @property
- def color_modes(self) -> set[ColorMode]:
- """Return the supported color modes."""
- return {ColorMode.RGB, ColorMode.COLOR_TEMP}
- @property
- def color_mode(self) -> ColorMode:
- """Return the current color mode."""
- device_mode = RGBICStripLightColorMode(self._get_adv_value("color_mode") or 10)
- return _RGBICWW_STRIP_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
- class SwitchbotPermanentOutdoorLight(SwitchbotRgbicLight):
- """Support for Switchbot Permanent Outdoor Light."""
- _model = SwitchbotModel.PERMANENT_OUTDOOR_LIGHT
- class SwitchbotRgbicNeonLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
- """Support for Switchbot RGBIC Neon lights."""
- _model = SwitchbotModel.RGBIC_NEON_ROPE_LIGHT
- _effect_dict = RGBIC_EFFECTS
- @property
- def color_modes(self) -> set[ColorMode]:
- """Return the supported color modes."""
- return {ColorMode.RGB}
- @property
- def color_mode(self) -> ColorMode:
- """Return the current color mode."""
- return ColorMode.RGB
- class SwitchbotRgbicwwCeilingLight(SwitchbotEncryptedDevice, SwitchbotLightStrip):
- """Support for Switchbot RGBICWW Ceiling Light (warm-white + color sub-lights)."""
- _model = SwitchbotModel.RGBICWW_CEILING_LIGHT
- _effect_dict = RGBIC_EFFECTS
- # Color sub-light commands (sub_cmd 0x12 brightness+RGB, 0x14 brightness)
- _set_brightness_command = f"{LIGHT_STRIP_CONTROL_HEADER}14{{}}"
- _set_rgb_command = f"{LIGHT_STRIP_CONTROL_HEADER}12{{}}"
- # Main (warm-white) sub-light commands (sub_cmd 0x09, 0x10, 0x11)
- _set_main_brightness_command = f"{LIGHT_STRIP_CONTROL_HEADER}09{{}}"
- _set_main_color_temp_command = f"{LIGHT_STRIP_CONTROL_HEADER}10{{}}"
- _set_color_temp_command = f"{LIGHT_STRIP_CONTROL_HEADER}11{{}}"
- # Sub-light power control: 0x49 0x01 <onoff> <selector>.
- # onoff = 0x01 (on)/0x02 (off)/0x03 (toggle); here we always send 0x01 and
- # let the selector drive the individual sub-lights.
- # selector: bit7=1 (specify), bits[3:2]=color state, bits[1:0]=white state;
- # state encoding 00=keep, 01=on, 02=off, 03=toggle.
- _turn_on_main_command = f"{LIGHT_STRIP_CONTROL_HEADER}0181"
- _turn_off_main_command = f"{LIGHT_STRIP_CONTROL_HEADER}0182"
- _turn_on_color_command = f"{LIGHT_STRIP_CONTROL_HEADER}0184"
- _turn_off_color_command = f"{LIGHT_STRIP_CONTROL_HEADER}0188"
- @property
- def color_modes(self) -> set[ColorMode]:
- """Return the supported color modes (color sub-light)."""
- return {ColorMode.RGB, ColorMode.COLOR_TEMP}
- @property
- def color_mode(self) -> ColorMode:
- """Return the current color mode."""
- device_mode = RGBICWWCeilingLightColorMode(
- self._get_adv_value("color_mode") or 10
- )
- return _RGBICWW_CEILING_LIGHT_COLOR_MODE_MAP.get(device_mode, ColorMode.OFF)
- @property
- def is_main_on(self) -> bool | None:
- """Return whether the main (warm-white) sub-light is on."""
- return self._get_adv_value("main_isOn")
- @property
- def main_brightness(self) -> int:
- """Return the main (warm-white) sub-light brightness 0-100."""
- return self._get_adv_value("main_brightness") or 0
- @update_after_operation
- async def turn_on_main(self) -> bool:
- """Turn the main (warm-white) sub-light on."""
- result = await self._send_command(self._turn_on_main_command)
- return self._check_command_result(result, 0, {1})
- @update_after_operation
- async def turn_off_main(self) -> bool:
- """Turn the main (warm-white) sub-light off."""
- result = await self._send_command(self._turn_off_main_command)
- return self._check_command_result(result, 0, {1})
- @update_after_operation
- async def turn_on_color(self) -> bool:
- """Turn the color sub-light on."""
- result = await self._send_command(self._turn_on_color_command)
- return self._check_command_result(result, 0, {1})
- @update_after_operation
- async def turn_off_color(self) -> bool:
- """Turn the color sub-light off."""
- result = await self._send_command(self._turn_off_color_command)
- return self._check_command_result(result, 0, {1})
- @update_after_operation
- async def set_main_brightness(self, brightness: int) -> bool:
- """Set the main (warm-white) sub-light brightness (sub_cmd 0x09)."""
- self._validate_brightness(brightness)
- hex_brightness = f"{brightness:02X}"
- result = await self._send_command(
- self._set_main_brightness_command.format(hex_brightness)
- )
- return self._check_command_result(result, 0, {1})
- @update_after_operation
- async def set_main_color_temp(self, color_temp: int) -> bool:
- """Set the main (warm-white) sub-light color temperature (sub_cmd 0x10)."""
- self._validate_color_temp(color_temp)
- hex_data = f"{color_temp:04X}"
- result = await self._send_command(
- self._set_main_color_temp_command.format(hex_data)
- )
- return self._check_command_result(result, 0, {1})
- async def get_basic_info(self) -> dict[str, Any] | None:
- """
- Read the RGB color (and color temp) over GATT.
- Power, brightness and color mode are taken from the advertisement
- (which tracks them reliably). The device's 0x4A01 status response does
- NOT carry a usable color power state - byte 1 stays 0 even when the
- color sub-light is on - so those fields are deliberately not returned
- here, otherwise update() would clobber the correct advertised values.
- """
- if not (
- res := await self._get_multi_commands_results(self._get_basic_info_command)
- ):
- return None
- _version_info, _data = res
- self._state["r"] = _data[3]
- self._state["g"] = _data[4]
- self._state["b"] = _data[5]
- self._state["cw"] = int.from_bytes(_data[7:9], "big")
- return {
- "r": self._state["r"],
- "g": self._state["g"],
- "b": self._state["b"],
- "firmware": _version_info[2] / 10.0,
- }
|