2020-09-24 07:36:12 +02:00
|
|
|
"""Support for lights."""
|
2021-01-23 04:35:48 +01:00
|
|
|
import enum
|
2020-09-17 05:41:32 +02:00
|
|
|
import json
|
2021-01-23 04:35:48 +01:00
|
|
|
import struct
|
2024-04-17 08:20:13 +02:00
|
|
|
from typing import Optional
|
2020-09-17 05:41:32 +02:00
|
|
|
|
2021-03-31 19:27:05 +02:00
|
|
|
from . import exceptions as e
|
2021-04-03 06:01:38 +02:00
|
|
|
from .device import Device
|
2020-09-17 05:41:32 +02:00
|
|
|
|
|
|
|
|
2021-04-03 06:01:38 +02:00
|
|
|
class lb1(Device):
|
2020-09-17 05:41:32 +02:00
|
|
|
"""Controls a Broadlink LB1."""
|
|
|
|
|
2021-01-28 23:16:25 +01:00
|
|
|
TYPE = "LB1"
|
|
|
|
|
2021-01-23 04:35:48 +01:00
|
|
|
@enum.unique
|
|
|
|
class ColorMode(enum.IntEnum):
|
|
|
|
"""Enumerates color modes."""
|
2021-04-05 19:02:56 +02:00
|
|
|
|
2021-01-23 04:35:48 +01:00
|
|
|
RGB = 0
|
|
|
|
WHITE = 1
|
|
|
|
SCENE = 2
|
2020-09-17 05:41:32 +02:00
|
|
|
|
2021-01-23 04:35:48 +01:00
|
|
|
def get_state(self) -> dict:
|
|
|
|
"""Return the power state of the device.
|
2020-09-17 05:41:32 +02:00
|
|
|
|
2021-01-23 04:35:48 +01:00
|
|
|
Example: `{'red': 128, 'blue': 255, 'green': 128, 'pwr': 1, 'brightness': 75, 'colortemp': 2700, 'hue': 240, 'saturation': 50, 'transitionduration': 1500, 'maxworktime': 0, 'bulb_colormode': 1, 'bulb_scenes': '["@01686464,0,0,0", "#ffffff,10,0,#000000,190,0,0", "2700+100,0,0,0", "#ff0000,500,2500,#00FF00,500,2500,#0000FF,500,2500,0", "@01686464,100,2400,@01686401,100,2400,0", "@01686464,100,2400,@01686401,100,2400,@005a6464,100,2400,@005a6401,100,2400,0", "@01686464,10,0,@00000000,190,0,0", "@01686464,200,0,@005a6464,200,0,0"]', 'bulb_scene': '', 'bulb_sceneidx': 255}`
|
|
|
|
"""
|
|
|
|
packet = self._encode(1, {})
|
2020-11-05 20:00:42 +01:00
|
|
|
response = self.send_packet(0x6A, packet)
|
2021-03-31 19:27:05 +02:00
|
|
|
e.check_error(response[0x22:0x24])
|
2021-01-23 04:35:48 +01:00
|
|
|
return self._decode(response)
|
2020-09-17 05:41:32 +02:00
|
|
|
|
2021-01-23 04:35:48 +01:00
|
|
|
def set_state(
|
|
|
|
self,
|
2024-04-17 08:20:13 +02:00
|
|
|
pwr: Optional[bool] = None,
|
|
|
|
red: Optional[int] = None,
|
|
|
|
blue: Optional[int] = None,
|
|
|
|
green: Optional[int] = None,
|
|
|
|
brightness: Optional[int] = None,
|
|
|
|
colortemp: Optional[int] = None,
|
|
|
|
hue: Optional[int] = None,
|
|
|
|
saturation: Optional[int] = None,
|
|
|
|
transitionduration: Optional[int] = None,
|
|
|
|
maxworktime: Optional[int] = None,
|
|
|
|
bulb_colormode: Optional[int] = None,
|
|
|
|
bulb_scenes: Optional[str] = None,
|
|
|
|
bulb_scene: Optional[str] = None,
|
|
|
|
bulb_sceneidx: Optional[int] = None,
|
2021-01-23 04:35:48 +01:00
|
|
|
) -> dict:
|
|
|
|
"""Set the power state of the device."""
|
|
|
|
state = {}
|
|
|
|
if pwr is not None:
|
|
|
|
state["pwr"] = int(bool(pwr))
|
|
|
|
if red is not None:
|
|
|
|
state["red"] = int(red)
|
|
|
|
if blue is not None:
|
|
|
|
state["blue"] = int(blue)
|
|
|
|
if green is not None:
|
|
|
|
state["green"] = int(green)
|
|
|
|
if brightness is not None:
|
2021-03-25 22:06:16 +01:00
|
|
|
state["brightness"] = int(brightness)
|
2021-01-23 04:35:48 +01:00
|
|
|
if colortemp is not None:
|
2021-03-25 22:06:16 +01:00
|
|
|
state["colortemp"] = int(colortemp)
|
2021-01-23 04:35:48 +01:00
|
|
|
if hue is not None:
|
2021-03-25 22:06:16 +01:00
|
|
|
state["hue"] = int(hue)
|
2021-01-23 04:35:48 +01:00
|
|
|
if saturation is not None:
|
2021-03-25 22:06:16 +01:00
|
|
|
state["saturation"] = int(saturation)
|
2021-01-23 04:35:48 +01:00
|
|
|
if transitionduration is not None:
|
2021-03-25 22:06:16 +01:00
|
|
|
state["transitionduration"] = int(transitionduration)
|
2021-01-23 04:35:48 +01:00
|
|
|
if maxworktime is not None:
|
2021-03-25 22:06:16 +01:00
|
|
|
state["maxworktime"] = int(maxworktime)
|
2021-01-23 04:35:48 +01:00
|
|
|
if bulb_colormode is not None:
|
2021-03-25 22:06:16 +01:00
|
|
|
state["bulb_colormode"] = int(bulb_colormode)
|
2021-01-23 04:35:48 +01:00
|
|
|
if bulb_scenes is not None:
|
2021-03-25 22:06:16 +01:00
|
|
|
state["bulb_scenes"] = str(bulb_scenes)
|
2021-01-23 04:35:48 +01:00
|
|
|
if bulb_scene is not None:
|
2021-03-25 22:06:16 +01:00
|
|
|
state["bulb_scene"] = str(bulb_scene)
|
2021-01-23 04:35:48 +01:00
|
|
|
if bulb_sceneidx is not None:
|
2021-03-25 22:06:16 +01:00
|
|
|
state["bulb_sceneidx"] = int(bulb_sceneidx)
|
2020-09-17 05:41:32 +02:00
|
|
|
|
2021-01-23 04:35:48 +01:00
|
|
|
packet = self._encode(2, state)
|
|
|
|
response = self.send_packet(0x6A, packet)
|
2021-03-31 19:27:05 +02:00
|
|
|
e.check_error(response[0x22:0x24])
|
2021-01-23 04:35:48 +01:00
|
|
|
return self._decode(response)
|
2020-09-17 05:41:32 +02:00
|
|
|
|
2021-04-05 19:02:56 +02:00
|
|
|
def _encode(self, flag: int, state: dict) -> bytes:
|
2021-01-23 04:35:48 +01:00
|
|
|
"""Encode a JSON packet."""
|
|
|
|
# flag: 1 for reading, 2 for writing.
|
|
|
|
packet = bytearray(14)
|
2021-04-05 19:02:56 +02:00
|
|
|
data = json.dumps(state, separators=(",", ":")).encode()
|
|
|
|
p_len = 12 + len(data)
|
2021-01-23 04:35:48 +01:00
|
|
|
struct.pack_into(
|
2021-04-05 19:02:56 +02:00
|
|
|
"<HHHHBBI", packet, 0, p_len, 0xA5A5, 0x5A5A, 0, flag, 0x0B, len(data)
|
2021-01-23 04:35:48 +01:00
|
|
|
)
|
2021-04-05 19:02:56 +02:00
|
|
|
packet.extend(data)
|
|
|
|
checksum = sum(packet[0x02:], 0xBEAF) & 0xFFFF
|
|
|
|
packet[0x06:0x08] = checksum.to_bytes(2, "little")
|
2021-01-23 04:35:48 +01:00
|
|
|
return packet
|
2020-09-17 05:41:32 +02:00
|
|
|
|
2021-04-05 19:02:56 +02:00
|
|
|
def _decode(self, response: bytes) -> dict:
|
2021-01-23 04:35:48 +01:00
|
|
|
"""Decode a JSON packet."""
|
|
|
|
payload = self.decrypt(response[0x38:])
|
|
|
|
js_len = struct.unpack_from("<I", payload, 0xA)[0]
|
2024-04-17 07:49:13 +02:00
|
|
|
state = json.loads(payload[0xE:0xE+js_len])
|
2021-01-23 04:35:48 +01:00
|
|
|
return state
|
2021-03-25 22:06:16 +01:00
|
|
|
|
|
|
|
|
2021-04-30 01:31:30 +02:00
|
|
|
class lb2(Device):
|
|
|
|
"""Controls a Broadlink LB26/LB27."""
|
2021-03-25 22:06:16 +01:00
|
|
|
|
2021-04-30 01:31:30 +02:00
|
|
|
TYPE = "LB2"
|
2021-03-25 22:06:16 +01:00
|
|
|
|
|
|
|
@enum.unique
|
|
|
|
class ColorMode(enum.IntEnum):
|
|
|
|
"""Enumerates color modes."""
|
2021-04-05 19:02:56 +02:00
|
|
|
|
2021-03-25 22:06:16 +01:00
|
|
|
RGB = 0
|
|
|
|
WHITE = 1
|
|
|
|
SCENE = 2
|
|
|
|
|
|
|
|
def get_state(self) -> dict:
|
|
|
|
"""Return the power state of the device.
|
|
|
|
|
|
|
|
Example: `{'red': 128, 'blue': 255, 'green': 128, 'pwr': 1, 'brightness': 75, 'colortemp': 2700, 'hue': 240, 'saturation': 50, 'transitionduration': 1500, 'maxworktime': 0, 'bulb_colormode': 1, 'bulb_scenes': '["@01686464,0,0,0", "#ffffff,10,0,#000000,190,0,0", "2700+100,0,0,0", "#ff0000,500,2500,#00FF00,500,2500,#0000FF,500,2500,0", "@01686464,100,2400,@01686401,100,2400,0", "@01686464,100,2400,@01686401,100,2400,@005a6464,100,2400,@005a6401,100,2400,0", "@01686464,10,0,@00000000,190,0,0", "@01686464,200,0,@005a6464,200,0,0"]', 'bulb_scene': ''}`
|
|
|
|
"""
|
|
|
|
packet = self._encode(1, {})
|
|
|
|
response = self.send_packet(0x6A, packet)
|
2021-03-31 19:27:05 +02:00
|
|
|
e.check_error(response[0x22:0x24])
|
2021-03-25 22:06:16 +01:00
|
|
|
return self._decode(response)
|
|
|
|
|
|
|
|
def set_state(
|
|
|
|
self,
|
2024-04-17 08:20:13 +02:00
|
|
|
pwr: Optional[bool] = None,
|
|
|
|
red: Optional[int] = None,
|
|
|
|
blue: Optional[int] = None,
|
|
|
|
green: Optional[int] = None,
|
|
|
|
brightness: Optional[int] = None,
|
|
|
|
colortemp: Optional[int] = None,
|
|
|
|
hue: Optional[int] = None,
|
|
|
|
saturation: Optional[int] = None,
|
|
|
|
transitionduration: Optional[int] = None,
|
|
|
|
maxworktime: Optional[int] = None,
|
|
|
|
bulb_colormode: Optional[int] = None,
|
|
|
|
bulb_scenes: Optional[str] = None,
|
|
|
|
bulb_scene: Optional[str] = None,
|
2021-03-25 22:06:16 +01:00
|
|
|
) -> dict:
|
|
|
|
"""Set the power state of the device."""
|
|
|
|
state = {}
|
|
|
|
if pwr is not None:
|
|
|
|
state["pwr"] = int(bool(pwr))
|
|
|
|
if red is not None:
|
|
|
|
state["red"] = int(red)
|
|
|
|
if blue is not None:
|
|
|
|
state["blue"] = int(blue)
|
|
|
|
if green is not None:
|
|
|
|
state["green"] = int(green)
|
|
|
|
if brightness is not None:
|
|
|
|
state["brightness"] = int(brightness)
|
|
|
|
if colortemp is not None:
|
|
|
|
state["colortemp"] = int(colortemp)
|
|
|
|
if hue is not None:
|
|
|
|
state["hue"] = int(hue)
|
|
|
|
if saturation is not None:
|
|
|
|
state["saturation"] = int(saturation)
|
|
|
|
if transitionduration is not None:
|
|
|
|
state["transitionduration"] = int(transitionduration)
|
|
|
|
if maxworktime is not None:
|
|
|
|
state["maxworktime"] = int(maxworktime)
|
|
|
|
if bulb_colormode is not None:
|
|
|
|
state["bulb_colormode"] = int(bulb_colormode)
|
|
|
|
if bulb_scenes is not None:
|
|
|
|
state["bulb_scenes"] = str(bulb_scenes)
|
|
|
|
if bulb_scene is not None:
|
|
|
|
state["bulb_scene"] = str(bulb_scene)
|
|
|
|
|
|
|
|
packet = self._encode(2, state)
|
|
|
|
response = self.send_packet(0x6A, packet)
|
2021-03-31 19:27:05 +02:00
|
|
|
e.check_error(response[0x22:0x24])
|
2021-03-25 22:06:16 +01:00
|
|
|
return self._decode(response)
|
|
|
|
|
2021-04-05 19:02:56 +02:00
|
|
|
def _encode(self, flag: int, state: dict) -> bytes:
|
2021-03-25 22:06:16 +01:00
|
|
|
"""Encode a JSON packet."""
|
|
|
|
# flag: 1 for reading, 2 for writing.
|
|
|
|
packet = bytearray(12)
|
2021-04-05 19:02:56 +02:00
|
|
|
data = json.dumps(state, separators=(",", ":")).encode()
|
2024-04-17 07:49:13 +02:00
|
|
|
struct.pack_into(
|
|
|
|
"<HHHBBI", packet, 0, 0xA5A5, 0x5A5A, 0, flag, 0x0B, len(data)
|
|
|
|
)
|
2021-04-05 19:02:56 +02:00
|
|
|
packet.extend(data)
|
|
|
|
checksum = sum(packet, 0xBEAF) & 0xFFFF
|
|
|
|
packet[0x04:0x06] = checksum.to_bytes(2, "little")
|
2021-03-25 22:06:16 +01:00
|
|
|
return packet
|
|
|
|
|
2021-04-05 19:02:56 +02:00
|
|
|
def _decode(self, response: bytes) -> dict:
|
2021-03-25 22:06:16 +01:00
|
|
|
"""Decode a JSON packet."""
|
|
|
|
payload = self.decrypt(response[0x38:])
|
2021-04-05 19:02:56 +02:00
|
|
|
js_len = struct.unpack_from("<I", payload, 0x08)[0]
|
2024-04-17 07:49:13 +02:00
|
|
|
state = json.loads(payload[0x0C:0x0C+js_len])
|
2021-03-25 22:06:16 +01:00
|
|
|
return state
|