From 9a04f68212541a400619e3bbb476fa8b0df4ece6 Mon Sep 17 00:00:00 2001 From: jbsky Date: Sat, 23 Jan 2021 04:35:48 +0100 Subject: [PATCH 01/82] Refactor the LB1 class (#517) * Refactor the LB1 class * General improvements * Enumerate bulb color modes * Clean up encoder Co-authored-by: Felipe Martins Diel --- README.md | 17 ++++++ broadlink/light.py | 140 ++++++++++++++++++++++++++++----------------- 2 files changed, 104 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 8d00fc93..d99d7543 100644 --- a/README.md +++ b/README.md @@ -129,3 +129,20 @@ Check power state on a SmartPowerStrip: ``` state = devices[0].check_power() ``` + +Get state on a bulb +``` +state=devices[0].get_state() +``` + +Set a state on a bulb +``` +devices[0].set_state(pwr=0) +devices[0].set_state(pwr=1) +devices[0].set_state(brightness=75) +devices[0].set_state(bulb_colormode=0) +devices[0].set_state(blue=255) +devices[0].set_state(red=0) +devices[0].set_state(green=128) +devices[0].set_state(bulb_colormode=1) +``` \ No newline at end of file diff --git a/broadlink/light.py b/broadlink/light.py index adadfab5..93633267 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -1,6 +1,8 @@ """Support for lights.""" +import enum import json -from typing import Union +import struct +import typing from .device import device from .exceptions import check_error @@ -9,66 +11,98 @@ class lb1(device): """Controls a Broadlink LB1.""" - state_dict = [] - effect_map_dict = { - "lovely color": 0, - "flashlight": 1, - "lightning": 2, - "color fading": 3, - "color breathing": 4, - "multicolor breathing": 5, - "color jumping": 6, - "multicolor jumping": 7, - } + @enum.unique + class ColorMode(enum.IntEnum): + """Enumerates color modes.""" + RGB = 0 + WHITE = 1 + SCENE = 2 def __init__(self, *args, **kwargs) -> None: """Initialize the controller.""" device.__init__(self, *args, **kwargs) - self.type = "SmartBulb" + self.type = "LB1" - def send_command(self, command: str, type: str = "set") -> None: - """Send a command to the device.""" - packet = bytearray(16 + (int(len(command) / 16) + 1) * 16) - packet[0x00] = 0x0C + len(command) & 0xFF - packet[0x02] = 0xA5 - packet[0x03] = 0xA5 - packet[0x04] = 0x5A - packet[0x05] = 0x5A - packet[0x08] = 0x02 if type == "set" else 0x01 # 0x01 => query, # 0x02 => set - packet[0x09] = 0x0B - packet[0x0A] = len(command) - packet[0x0E:] = map(ord, command) - - checksum = sum(packet, 0xBEAF) & 0xFFFF - packet[0x06] = checksum & 0xFF # Checksum 1 position - packet[0x07] = checksum >> 8 # Checksum 2 position + 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': '', 'bulb_sceneidx': 255}` + """ + packet = self._encode(1, {}) response = self.send_packet(0x6A, packet) - check_error(response[0x36:0x38]) - payload = self.decrypt(response[0x38:]) - - responseLength = int(payload[0x0A]) | (int(payload[0x0B]) << 8) - if responseLength > 0: - self.state_dict = json.loads(payload[0x0E : 0x0E + responseLength]) + check_error(response[0x22:0x24]) + return self._decode(response) - def set_json(self, jsonstr: str) -> str: - """Send a command to the device and return state.""" - reconvert = json.loads(jsonstr) - if "bulb_sceneidx" in reconvert.keys(): - reconvert["bulb_sceneidx"] = self.effect_map_dict.get( - reconvert["bulb_sceneidx"], 255 - ) + def set_state( + self, + pwr: bool = None, + red: int = None, + blue: int = None, + green: int = None, + brightness: int = None, + colortemp: int = None, + hue: int = None, + saturation: int = None, + transitionduration: int = None, + maxworktime: int = None, + bulb_colormode: int = None, + bulb_scenes: str = None, + bulb_scene: str = None, + bulb_sceneidx: int = None, + ) -> 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"] = brightness + if colortemp is not None: + state["colortemp"] = colortemp + if hue is not None: + state["hue"] = hue + if saturation is not None: + state["saturation"] = saturation + if transitionduration is not None: + state["transitionduration"] = transitionduration + if maxworktime is not None: + state["maxworktime"] = maxworktime + if bulb_colormode is not None: + state["bulb_colormode"] = bulb_colormode + if bulb_scenes is not None: + state["bulb_scenes"] = bulb_scenes + if bulb_scene is not None: + state["bulb_scene"] = bulb_scene + if bulb_sceneidx is not None: + state["bulb_sceneidx"] = bulb_sceneidx - self.send_command(json.dumps(reconvert)) - return json.dumps(self.state_dict) + packet = self._encode(2, state) + response = self.send_packet(0x6A, packet) + check_error(response[0x22:0x24]) + return self._decode(response) - def set_state(self, state: Union[str, int]) -> None: - """Set the state of the device.""" - cmd = '{"pwr":%d}' % (1 if state == "ON" or state == 1 else 0) - self.send_command(cmd) + def _encode(self, flag: int, obj: typing.Any) -> bytes: + """Encode a JSON packet.""" + # flag: 1 for reading, 2 for writing. + packet = bytearray(14) + js = json.dumps(obj, separators=[',', ':']).encode() + p_len = 12 + len(js) + struct.pack_into( + " dict: - """Return the state of the device.""" - cmd = "{}" - self.send_command(cmd) - return self.state_dict + def _decode(self, response: bytes) -> typing.Any: + """Decode a JSON packet.""" + payload = self.decrypt(response[0x38:]) + js_len = struct.unpack_from(" Date: Mon, 25 Jan 2021 04:17:31 -0300 Subject: [PATCH 02/82] Update README.md Add new models and fix syntax errors in the instructions. --- README.md | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d99d7543..1bc26f96 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,21 @@ -Python control for Broadlink RM2, RM3 and RM4 series controllers +Python control for Broadlink devices =============================================== -A simple Python API for controlling IR/RF controllers from [Broadlink](http://www.ibroadlink.com/rm/). At present, the following devices are currently supported: +A simple Python API for controlling Broadlink devices. At present, the following devices are supported: -* RM Pro (referred to as RM2 in the codebase) -* A1 sensor platform devices are supported -* RM3 mini IR blaster -* RM4 and RM4C mini blasters -- SP2/SP3/SP4 smart plugs +- Universal remotes: `RM pro`, `RM pro+`, `RM pro plus`, `RM mini 3`, `RM4 pro`, `RM4 mini`, `RM4C mini`, `RM4S` +- Smart plugs: `SP1`, `SP2`, `SP mini`, `SP mini+`, `SP3`, `SP3S`, `SP4L`, `SP4M` +- Power strips: `MP1-1K4S`, `MP1-1K3S2U`, `MP2` +- Control box: `SC1`, `SCB1E`, `MCB1` +- Sensors: `A1` +- Alarm kit: `S1C`, `S2KIT` +- Light bulb: `LB1`, `SB800TD` + +Other devices with Broadlink DNA: +- Smart plugs: `Ankuoo NEO`, `Ankuoo NEO PRO`, `Efergy Ego`, `BG AHC/U-01` +- Outlet: `BG 800`, `BG 900` +- Curtain motor: `Dooya DT360E-45/20` +- Thermostat: `Hysen HY02B05H` There is currently no support for the cloud API. @@ -42,7 +50,7 @@ Using your machine's IP address with `local_ip_address` ``` import broadlink -devices = broadlink.discover(timeout=5, local_ip_address=192.168.0.100) +devices = broadlink.discover(timeout=5, local_ip_address='192.168.0.100') ``` Using your subnet's broadcast address with `discover_ip_address` @@ -50,7 +58,7 @@ Using your subnet's broadcast address with `discover_ip_address` ``` import broadlink -devices = broadlink.discover(timeout=5, discover_ip_address=192.168.0.255) +devices = broadlink.discover(timeout=5, discover_ip_address='192.168.0.255') ``` Obtain the authentication key required for further communication: @@ -145,4 +153,4 @@ devices[0].set_state(blue=255) devices[0].set_state(red=0) devices[0].set_state(green=128) devices[0].set_state(bulb_colormode=1) -``` \ No newline at end of file +``` From 9af3a3c56cc545461fff6bdcdef96f37a0ff60ef Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 25 Jan 2021 17:00:46 -0300 Subject: [PATCH 03/82] Clean up device.py (#523) * Clean up device.send_packet() * Clean up device.auth() * Clean up scan() --- broadlink/device.py | 164 ++++++++++++------------------------------ broadlink/protocol.py | 49 +++++++++++++ 2 files changed, 94 insertions(+), 119 deletions(-) create mode 100644 broadlink/protocol.py diff --git a/broadlink/device.py b/broadlink/device.py index e6753774..9509278e 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -3,13 +3,13 @@ import threading import random import time -from datetime import datetime from typing import Generator, Tuple, Union from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from .exceptions import check_error, exception +from .protocol import Datetime HelloResponse = Tuple[int, Tuple[str, int], str, str, bool] @@ -33,42 +33,13 @@ def scan( port = 0 packet = bytearray(0x30) - - timezone = int(time.timezone / -3600) - if timezone < 0: - packet[0x08] = 0xFF + timezone - 1 - packet[0x09] = 0xFF - packet[0x0A] = 0xFF - packet[0x0B] = 0xFF - else: - packet[0x08] = timezone - packet[0x09] = 0 - packet[0x0A] = 0 - packet[0x0B] = 0 - - year = datetime.now().year - packet[0x0C] = year & 0xFF - packet[0x0D] = year >> 8 - packet[0x0E] = datetime.now().minute - packet[0x0F] = datetime.now().hour - subyear = str(year)[2:] - packet[0x10] = int(subyear) - packet[0x11] = datetime.now().isoweekday() - packet[0x12] = datetime.now().day - packet[0x13] = datetime.now().month - - address = local_ip_address.split(".") - packet[0x18] = int(address[3]) - packet[0x19] = int(address[2]) - packet[0x1A] = int(address[1]) - packet[0x1B] = int(address[0]) - packet[0x1C] = port & 0xFF - packet[0x1D] = port >> 8 + packet[0x08:0x14] = Datetime.pack(Datetime.now()) + packet[0x18:0x1C] = socket.inet_aton(local_ip_address)[::-1] + packet[0x1C:0x1E] = port.to_bytes(2, "little") packet[0x26] = 6 checksum = sum(packet, 0xBEAF) & 0xFFFF - packet[0x20] = checksum & 0xFF - packet[0x21] = checksum >> 8 + packet[0x20:0x22] = checksum.to_bytes(2, "little") start_time = time.time() discovered = [] @@ -81,18 +52,19 @@ def scan( while True: try: - response, host = conn.recvfrom(1024) + resp, host = conn.recvfrom(1024) except socket.timeout: break - devtype = response[0x34] | response[0x35] << 8 - mac = bytes(reversed(response[0x3A:0x40])) + devtype = resp[0x34] | resp[0x35] << 8 + mac = resp[0x3A:0x40][::-1] + if (host, mac, devtype) in discovered: continue discovered.append((host, mac, devtype)) - name = response[0x40:].split(b"\x00")[0].decode("utf-8") - is_locked = bool(response[-1]) + name = resp[0x40:].split(b"\x00")[0].decode() + is_locked = bool(resp[-1]) yield devtype, host, mac, name, is_locked finally: conn.close() @@ -123,7 +95,7 @@ def __init__( self.is_locked = is_locked self.count = random.randint(0x8000, 0xFFFF) self.iv = bytes.fromhex("562e17996d093d28ddb3ba695a2e6f58") - self.id = bytes(4) + self.id = 0 self.type = "Unknown" self.lock = threading.Lock() @@ -170,30 +142,10 @@ def decrypt(self, payload: bytes) -> bytes: def auth(self) -> bool: """Authenticate to the device.""" payload = bytearray(0x50) - payload[0x04] = 0x31 - payload[0x05] = 0x31 - payload[0x06] = 0x31 - payload[0x07] = 0x31 - payload[0x08] = 0x31 - payload[0x09] = 0x31 - payload[0x0A] = 0x31 - payload[0x0B] = 0x31 - payload[0x0C] = 0x31 - payload[0x0D] = 0x31 - payload[0x0E] = 0x31 - payload[0x0F] = 0x31 - payload[0x10] = 0x31 - payload[0x11] = 0x31 - payload[0x12] = 0x31 + payload[0x04:0x14] = [0x31]*16 payload[0x1E] = 0x01 payload[0x2D] = 0x01 - payload[0x30] = ord("T") - payload[0x31] = ord("e") - payload[0x32] = ord("s") - payload[0x33] = ord("t") - payload[0x34] = ord(" ") - payload[0x35] = ord(" ") - payload[0x36] = ord("1") + payload[0x30:0x37] = "Test 1".encode() response = self.send_packet(0x65, payload) check_error(response[0x22:0x24]) @@ -203,7 +155,7 @@ def auth(self) -> bool: if len(key) % 16 != 0: return False - self.id = payload[0x03::-1] + self.id = int.from_bytes(payload[:0x4], "little") self.update_aes(key) return True @@ -262,73 +214,47 @@ def get_type(self) -> str: """Return device type.""" return self.type - def send_packet(self, command: int, payload: bytes) -> bytes: + def send_packet(self, packet_type: int, payload: bytes) -> bytes: """Send a packet to the device.""" self.count = ((self.count + 1) | 0x8000) & 0xFFFF packet = bytearray(0x38) - packet[0x00] = 0x5A - packet[0x01] = 0xA5 - packet[0x02] = 0xAA - packet[0x03] = 0x55 - packet[0x04] = 0x5A - packet[0x05] = 0xA5 - packet[0x06] = 0xAA - packet[0x07] = 0x55 - packet[0x24] = self.devtype & 0xFF - packet[0x25] = self.devtype >> 8 - packet[0x26] = command - packet[0x28] = self.count & 0xFF - packet[0x29] = self.count >> 8 - packet[0x2A] = self.mac[5] - packet[0x2B] = self.mac[4] - packet[0x2C] = self.mac[3] - packet[0x2D] = self.mac[2] - packet[0x2E] = self.mac[1] - packet[0x2F] = self.mac[0] - packet[0x30] = self.id[3] - packet[0x31] = self.id[2] - packet[0x32] = self.id[1] - packet[0x33] = self.id[0] - - # pad the payload for AES encryption - padding = (16 - len(payload)) % 16 - if padding: - payload = bytearray(payload) - payload += bytearray(padding) + packet[0x00:0x08] = bytes.fromhex("5aa5aa555aa5aa55") + packet[0x24:0x26] = self.devtype.to_bytes(2, "little") + packet[0x26:0x28] = packet_type.to_bytes(2, "little") + packet[0x28:0x2a] = self.count.to_bytes(2, "little") + packet[0x2a:0x30] = self.mac[::-1] + packet[0x30:0x34] = self.id.to_bytes(4, "little") - checksum = sum(payload, 0xBEAF) & 0xFFFF - packet[0x34] = checksum & 0xFF - packet[0x35] = checksum >> 8 + p_checksum = sum(payload, 0xBEAF) & 0xFFFF + packet[0x34:0x36] = p_checksum.to_bytes(2, "little") - payload = self.encrypt(payload) - for i in range(len(payload)): - packet.append(payload[i]) + padding = (16 - len(payload)) % 16 + payload = self.encrypt(payload + bytes(padding)) + packet.extend(payload) checksum = sum(packet, 0xBEAF) & 0xFFFF - packet[0x20] = checksum & 0xFF - packet[0x21] = checksum >> 8 - - with self.lock: - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as conn: - timeout = self.timeout - start_time = time.time() - - while True: - time_left = timeout - (time.time() - start_time) - conn.settimeout(min(1, time_left)) - conn.sendto(packet, self.host) - - try: - resp = conn.recvfrom(2048)[0] - break - except socket.timeout: - if (time.time() - start_time) > timeout: - raise exception(-4000) # Network timeout. + packet[0x20:0x22] = checksum.to_bytes(2, "little") + + with self.lock and socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as conn: + timeout = self.timeout + start_time = time.time() + + while True: + time_left = timeout - (time.time() - start_time) + conn.settimeout(min(1, time_left)) + conn.sendto(packet, self.host) + + try: + resp = conn.recvfrom(2048)[0] + break + except socket.timeout: + if (time.time() - start_time) > timeout: + raise exception(-4000) # Network timeout. if len(resp) < 0x30: raise exception(-4007) # Length error. - checksum = resp[0x20] | (resp[0x21] << 8) + checksum = int.from_bytes(resp[0x20:0x22], "little") if sum(resp, 0xBEAF) - sum(resp[0x20:0x22]) & 0xFFFF != checksum: raise exception(-4008) # Checksum error. diff --git a/broadlink/protocol.py b/broadlink/protocol.py new file mode 100644 index 00000000..017125ed --- /dev/null +++ b/broadlink/protocol.py @@ -0,0 +1,49 @@ +import datetime as dt +import time + + +class Datetime: + """Helps to pack and unpack datetime objects for the Broadlink protocol.""" + + @staticmethod + def pack(datetime: dt.datetime) -> bytes: + """Pack the timestamp to be sent over the Broadlink protocol.""" + data = bytearray(12) + utcoffset = int(datetime.utcoffset().total_seconds() / 3600) + data[:0x04] = utcoffset.to_bytes(4, "little", signed=True) + data[0x04:0x06] = datetime.year.to_bytes(2, "little") + data[0x06] = datetime.minute + data[0x07] = datetime.hour + data[0x08] = int(datetime.strftime('%y')) + data[0x09] = datetime.isoweekday() + data[0x0A] = datetime.day + data[0x0B] = datetime.month + return data + + @staticmethod + def unpack(data: bytes) -> dt.datetime: + """Unpack a timestamp received over the Broadlink protocol.""" + utcoffset = int.from_bytes(data[0x00:0x04], "little", signed=True) + year = int.from_bytes(data[0x04:0x06], "little") + minute = data[0x06] + hour = data[0x07] + subyear = data[0x08] + isoweekday = data[0x09] + day = data[0x0A] + month = data[0x0B] + + tz_info = dt.timezone(dt.timedelta(hours=utcoffset)) + datetime = dt.datetime(year, month, day, hour, minute, 0, 0, tz_info) + + if datetime.isoweekday() != isoweekday: + raise ValueError("isoweekday does not match") + if int(datetime.strftime('%y')) != subyear: + raise ValueError("subyear does not match") + + return datetime + + @staticmethod + def now() -> dt.datetime: + """Return the current date and time with timezone info.""" + tz_info = dt.timezone(dt.timedelta(seconds=-time.timezone)) + return dt.datetime.now(tz_info) From 21fa2a20bf6935bc7896432add75a971ef7c3f39 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Wed, 27 Jan 2021 16:39:54 -0300 Subject: [PATCH 04/82] Add a function to send ping packets (#526) Rename 'host' attribute to 'address' (ping) (#528) --- broadlink/__init__.py | 2 +- broadlink/device.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 11172514..1b2f0abd 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -6,7 +6,7 @@ from .alarm import S1C from .climate import hysen from .cover import dooya -from .device import device, scan +from .device import device, ping, scan from .exceptions import exception from .light import lb1 from .remote import rm, rm4 diff --git a/broadlink/device.py b/broadlink/device.py index 9509278e..970ee7ff 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -70,6 +70,20 @@ def scan( conn.close() +def ping(address: str, port: int = 80) -> None: + """Send a ping packet to an address. + + This packet feeds the watchdog timer of firmwares >= v53. + Useful to prevent reboots when the cloud cannot be reached. + It must be sent every 2 minutes in such cases. + """ + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as conn: + conn.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + packet = bytearray(0x30) + packet[0x26] = 1 + conn.sendto(packet, (address, port)) + + class device: """Controls a Broadlink device.""" @@ -182,6 +196,15 @@ def hello(self, local_ip_address=None) -> bool: self.is_locked = is_locked return True + def ping(self) -> None: + """Ping the device. + + This packet feeds the watchdog timer of firmwares >= v53. + Useful to prevent reboots when the cloud cannot be reached. + It must be sent every 2 minutes in such cases. + """ + ping(self.host[0], port=self.host[1]) + def get_fwversion(self) -> int: """Get firmware version.""" packet = bytearray([0x68]) From 586d44493ed426c474b069418121398e41e4af73 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 27 Jan 2021 18:50:45 -0300 Subject: [PATCH 05/82] Change rm.find_rf_packet()'s return value to None (#527) Change rm.find_rf_packet()'s return value to None. If there is no exception, it worked. --- broadlink/remote.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/broadlink/remote.py b/broadlink/remote.py index 6cf32f27..f4b8ffb8 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -46,10 +46,9 @@ def check_frequency(self) -> bool: resp = self._send(0x1A) return resp[0] == 1 - def find_rf_packet(self) -> bool: + def find_rf_packet(self) -> None: """Enter radiofrequency learning mode.""" - resp = self._send(0x1B) - return resp[0] == 1 + self._send(0x1B) def check_temperature(self) -> float: """Return the temperature.""" @@ -80,11 +79,6 @@ def _send(self, command: int, data: bytes = b'') -> bytes: p_len = struct.unpack(" bool: - """Enter radiofrequency learning mode.""" - self._send(0x1B) - return True - def check_humidity(self) -> float: """Return the humidity.""" return self.check_sensors()["humidity"] From e12fd6f115e8daecb4d7827949c8b4cc54e1f47f Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 28 Jan 2021 19:16:25 -0300 Subject: [PATCH 06/82] Make the type a class attribute (#530) --- broadlink/alarm.py | 7 ++----- broadlink/climate.py | 5 +---- broadlink/cover.py | 5 +---- broadlink/device.py | 4 +++- broadlink/light.py | 7 ++----- broadlink/remote.py | 10 ++-------- broadlink/sensor.py | 7 ++----- broadlink/switch.py | 30 ++++++------------------------ 8 files changed, 19 insertions(+), 56 deletions(-) diff --git a/broadlink/alarm.py b/broadlink/alarm.py index e73b8fad..f49b2cf1 100644 --- a/broadlink/alarm.py +++ b/broadlink/alarm.py @@ -6,17 +6,14 @@ class S1C(device): """Controls a Broadlink S1C.""" + TYPE = "S1C" + _SENSORS_TYPES = { 0x31: "Door Sensor", # 49 as hex 0x91: "Key Fob", # 145 as hex, as serial on fob corpse 0x21: "Motion Sensor", # 33 as hex } - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "S1C" - def get_sensors_status(self) -> dict: """Return the state of the sensors.""" packet = bytearray(16) diff --git a/broadlink/climate.py b/broadlink/climate.py index 036cc9a8..b510db9d 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -9,10 +9,7 @@ class hysen(device): """Controls a Hysen HVAC.""" - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "Hysen heating controller" + TYPE = "Hysen heating controller" # Send a request # input_payload should be a bytearray, usually 6 bytes, e.g. bytearray([0x01,0x06,0x00,0x02,0x10,0x00]) diff --git a/broadlink/cover.py b/broadlink/cover.py index 2691fe97..b2dc84a0 100644 --- a/broadlink/cover.py +++ b/broadlink/cover.py @@ -8,10 +8,7 @@ class dooya(device): """Controls a Dooya curtain motor.""" - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "Dooya DT360E" + TYPE = "Dooya DT360E" def _send(self, magic1: int, magic2: int) -> int: """Send a packet to the device.""" diff --git a/broadlink/device.py b/broadlink/device.py index 970ee7ff..4a8bc3a5 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -87,6 +87,8 @@ def ping(address: str, port: int = 80) -> None: class device: """Controls a Broadlink device.""" + TYPE = "Unknown" + def __init__( self, host: Tuple[str, int], @@ -110,7 +112,7 @@ def __init__( self.count = random.randint(0x8000, 0xFFFF) self.iv = bytes.fromhex("562e17996d093d28ddb3ba695a2e6f58") self.id = 0 - self.type = "Unknown" + self.type = self.TYPE # For backwards compatibility. self.lock = threading.Lock() self.aes = None diff --git a/broadlink/light.py b/broadlink/light.py index 93633267..4673e48d 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -11,6 +11,8 @@ class lb1(device): """Controls a Broadlink LB1.""" + TYPE = "LB1" + @enum.unique class ColorMode(enum.IntEnum): """Enumerates color modes.""" @@ -18,11 +20,6 @@ class ColorMode(enum.IntEnum): WHITE = 1 SCENE = 2 - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "LB1" - def get_state(self) -> dict: """Return the power state of the device. diff --git a/broadlink/remote.py b/broadlink/remote.py index f4b8ffb8..f97b755c 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -8,10 +8,7 @@ class rm(device): """Controls a Broadlink RM.""" - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "RM2" + TYPE = "RM2" def _send(self, command: int, data: bytes = b'') -> bytes: """Send a packet to the device.""" @@ -65,10 +62,7 @@ def check_sensors(self) -> dict: class rm4(rm): """Controls a Broadlink RM4.""" - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "RM4" + TYPE = "RM4" def _send(self, command: int, data: bytes = b'') -> bytes: """Send a packet to the device.""" diff --git a/broadlink/sensor.py b/broadlink/sensor.py index 3a257b12..9c5572d2 100644 --- a/broadlink/sensor.py +++ b/broadlink/sensor.py @@ -8,17 +8,14 @@ class a1(device): """Controls a Broadlink A1.""" + TYPE = "A1" + _SENSORS_AND_LEVELS = ( ("light", ("dark", "dim", "normal", "bright")), ("air_quality", ("excellent", "good", "normal", "bad")), ("noise", ("quiet", "normal", "noisy")), ) - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "A1" - def check_sensors(self) -> dict: """Return the state of the sensors.""" data = self.check_sensors_raw() diff --git a/broadlink/switch.py b/broadlink/switch.py index f7f23084..e5e0a1aa 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -9,10 +9,7 @@ class mp1(device): """Controls a Broadlink MP1.""" - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "MP1" + TYPE = "MP1" def set_power_mask(self, sid_mask: int, state: bool) -> None: """Set the power state of the device.""" @@ -70,10 +67,7 @@ def check_power(self) -> dict: class bg1(device): """Controls a BG Electrical smart outlet.""" - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "BG1" + TYPE = "BG1" def get_state(self) -> dict: """Return the power state of the device. @@ -151,10 +145,7 @@ def _decode(self, response: bytes) -> dict: class sp1(device): """Controls a Broadlink SP1.""" - def __init__(self, *args, **kwargs) -> None: - """Initialize the device.""" - device.__init__(self, *args, **kwargs) - self.type = "SP1" + TYPE = "SP1" def set_power(self, state: bool) -> None: """Set the power state of the device.""" @@ -167,10 +158,7 @@ def set_power(self, state: bool) -> None: class sp2(device): """Controls a Broadlink SP2.""" - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "SP2" + TYPE = "SP2" def set_power(self, state: bool) -> None: """Set the power state of the device.""" @@ -225,10 +213,7 @@ def get_energy(self) -> float: class sp4(device): """Controls a Broadlink SP4.""" - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "SP4" + TYPE = "SP4" def set_power(self, state: bool) -> None: """Set the power state of the device.""" @@ -307,10 +292,7 @@ def _decode(self, response: bytes) -> dict: class sp4b(sp4): """Controls a Broadlink SP4 (type B).""" - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "SP4B" + TYPE = "SP4B" def get_state(self) -> dict: """Get full state of device.""" From 008846ba418e5ebebe2fa753f1a230454afa1953 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 4 Feb 2021 17:16:17 -0300 Subject: [PATCH 07/82] Fix index (#533) --- broadlink/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/device.py b/broadlink/device.py index 4a8bc3a5..144ceeba 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -161,7 +161,7 @@ def auth(self) -> bool: payload[0x04:0x14] = [0x31]*16 payload[0x1E] = 0x01 payload[0x2D] = 0x01 - payload[0x30:0x37] = "Test 1".encode() + payload[0x30:0x36] = "Test 1".encode() response = self.send_packet(0x65, payload) check_error(response[0x22:0x24]) From 1b73cfce3a3faea3eb6dc529f44814f0e2ffeaa8 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 16 Feb 2021 16:38:10 -0300 Subject: [PATCH 08/82] Split the sp2 class into smaller classes (#521) --- broadlink/__init__.py | 19 +++++++++---------- broadlink/switch.py | 44 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 1b2f0abd..07131276 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """The python-broadlink library.""" import socket -from typing import Generator, List, Union, Tuple +from typing import Generator, List, Tuple, Union from .alarm import S1C from .climate import hysen @@ -11,21 +11,16 @@ from .light import lb1 from .remote import rm, rm4 from .sensor import a1 -from .switch import bg1, mp1, sp1, sp2, sp4, sp4b - +from .switch import bg1, mp1, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b SUPPORTED_TYPES = { 0x0000: (sp1, "SP1", "Broadlink"), 0x2711: (sp2, "SP2", "Broadlink"), - 0x2716: (sp2, "NEO PRO", "Ankuoo"), 0x2717: (sp2, "NEO", "Ankuoo"), 0x2719: (sp2, "SP2-compatible", "Honeywell"), 0x271A: (sp2, "SP2-compatible", "Honeywell"), - 0x271D: (sp2, "Ego", "Efergy"), 0x2720: (sp2, "SP mini", "Broadlink"), 0x2728: (sp2, "SP2-compatible", "URANT"), - 0x2733: (sp2, "SP3", "Broadlink"), - 0x2736: (sp2, "SP mini+", "Broadlink"), 0x273E: (sp2, "SP mini", "Broadlink"), 0x7530: (sp2, "SP2", "Broadlink (OEM)"), 0x7539: (sp2, "SP2-IL", "Broadlink (OEM)"), @@ -37,10 +32,14 @@ 0x7918: (sp2, "SP2", "Broadlink (OEM)"), 0x7919: (sp2, "SP2-compatible", "Honeywell"), 0x791A: (sp2, "SP2-compatible", "Honeywell"), - 0x7D00: (sp2, "SP3-EU", "Broadlink (OEM)"), 0x7D0D: (sp2, "SP mini 3", "Broadlink (OEM)"), - 0x9479: (sp2, "SP3S-US", "Broadlink"), - 0x947A: (sp2, "SP3S-EU", "Broadlink"), + 0x2716: (sp2s, "NEO PRO", "Ankuoo"), + 0x271D: (sp2s, "Ego", "Efergy"), + 0x2736: (sp2s, "SP mini+", "Broadlink"), + 0x2733: (sp3, "SP3", "Broadlink"), + 0x7D00: (sp3, "SP3-EU", "Broadlink (OEM)"), + 0x9479: (sp3s, "SP3S-US", "Broadlink"), + 0x947A: (sp3s, "SP3S-EU", "Broadlink"), 0x756C: (sp4, "SP4M", "Broadlink"), 0x756F: (sp4, "MCB1", "Broadlink"), 0x7579: (sp4, "SP4L-EU", "Broadlink"), diff --git a/broadlink/switch.py b/broadlink/switch.py index e5e0a1aa..775b7580 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -160,6 +160,44 @@ class sp2(device): TYPE = "SP2" + def set_power(self, state: bool) -> None: + """Set the power state of the device.""" + packet = bytearray(16) + packet[0] = 2 + packet[4] = int(bool(state)) + response = self.send_packet(0x6A, packet) + check_error(response[0x22:0x24]) + + def check_power(self) -> bool: + """Return the power state of the device.""" + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6A, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return bool(payload[0x4]) + + +class sp2s(sp2): + """Controls a Broadlink SP2S.""" + + TYPE = "SP2S" + + def get_energy(self) -> float: + """Return the power consumption in W.""" + packet = bytearray(16) + packet[0] = 4 + response = self.send_packet(0x6A, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return int.from_bytes(payload[0x4:0x7], "little") / 1000 + + +class sp3(device): + """Controls a Broadlink SP3.""" + + TYPE = "SP3" + def set_power(self, state: bool) -> None: """Set the power state of the device.""" packet = bytearray(16) @@ -200,6 +238,12 @@ def check_nightlight(self) -> bool: payload = self.decrypt(response[0x38:]) return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) + +class sp3s(sp2): + """Controls a Broadlink SP3S.""" + + TYPE = "SP3S" + def get_energy(self) -> float: """Return the power consumption in W.""" packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) From 39ee67bb98978c281225a540e6f3b39fe9058d92 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 16 Feb 2021 18:14:11 -0300 Subject: [PATCH 09/82] Split the rm and rm4 classes into smaller classes (#529) --- broadlink/__init__.py | 80 +++++++++++++++++++-------------------- broadlink/remote.py | 88 ++++++++++++++++++++++++++++++------------- 2 files changed, 101 insertions(+), 67 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 07131276..81481f42 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -9,7 +9,7 @@ from .device import device, ping, scan from .exceptions import exception from .light import lb1 -from .remote import rm, rm4 +from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro from .sensor import a1 from .switch import bg1, mp1, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b @@ -54,45 +54,45 @@ 0x618B: (sp4b, "SP4L-EU", "Broadlink"), 0x6489: (sp4b, "SP4L-AU", "Broadlink"), 0x648B: (sp4b, "SP4M-US", "Broadlink"), - 0x2712: (rm, "RM pro/pro+", "Broadlink"), - 0x272A: (rm, "RM pro", "Broadlink"), - 0x2737: (rm, "RM mini 3", "Broadlink"), - 0x273D: (rm, "RM pro", "Broadlink"), - 0x277C: (rm, "RM home", "Broadlink"), - 0x2783: (rm, "RM home", "Broadlink"), - 0x2787: (rm, "RM pro", "Broadlink"), - 0x278B: (rm, "RM plus", "Broadlink"), - 0x278F: (rm, "RM mini", "Broadlink"), - 0x2797: (rm, "RM pro+", "Broadlink"), - 0x279D: (rm, "RM pro+", "Broadlink"), - 0x27A1: (rm, "RM plus", "Broadlink"), - 0x27A6: (rm, "RM plus", "Broadlink"), - 0x27A9: (rm, "RM pro+", "Broadlink"), - 0x27C2: (rm, "RM mini 3", "Broadlink"), - 0x27C3: (rm, "RM pro+", "Broadlink"), - 0x27C7: (rm, "RM mini 3", "Broadlink"), - 0x27CC: (rm, "RM mini 3", "Broadlink"), - 0x27CD: (rm, "RM mini 3", "Broadlink"), - 0x27D0: (rm, "RM mini 3", "Broadlink"), - 0x27D1: (rm, "RM mini 3", "Broadlink"), - 0x27D3: (rm, "RM mini 3", "Broadlink"), - 0x27DE: (rm, "RM mini 3", "Broadlink"), - 0x51DA: (rm4, "RM4 mini", "Broadlink"), - 0x5F36: (rm4, "RM mini 3", "Broadlink"), - 0x6026: (rm4, "RM4 pro", "Broadlink"), - 0x6070: (rm4, "RM4C mini", "Broadlink"), - 0x610E: (rm4, "RM4 mini", "Broadlink"), - 0x610F: (rm4, "RM4C mini", "Broadlink"), - 0x61A2: (rm4, "RM4 pro", "Broadlink"), - 0x62BC: (rm4, "RM4 mini", "Broadlink"), - 0x62BE: (rm4, "RM4C mini", "Broadlink"), - 0x6364: (rm4, "RM4S", "Broadlink"), - 0x648D: (rm4, "RM4 mini", "Broadlink"), - 0x649B: (rm4, "RM4 pro", "Broadlink"), - 0x6508: (rm4, "RM mini 3", "Broadlink"), - 0x6539: (rm4, "RM4C mini", "Broadlink"), - 0x653A: (rm4, "RM4 mini", "Broadlink"), - 0x653C: (rm4, "RM4 pro", "Broadlink"), + 0x2737: (rmmini, "RM mini 3", "Broadlink"), + 0x278F: (rmmini, "RM mini", "Broadlink"), + 0x27C2: (rmmini, "RM mini 3", "Broadlink"), + 0x27C7: (rmmini, "RM mini 3", "Broadlink"), + 0x27CC: (rmmini, "RM mini 3", "Broadlink"), + 0x27CD: (rmmini, "RM mini 3", "Broadlink"), + 0x27D0: (rmmini, "RM mini 3", "Broadlink"), + 0x27D1: (rmmini, "RM mini 3", "Broadlink"), + 0x27D3: (rmmini, "RM mini 3", "Broadlink"), + 0x27DE: (rmmini, "RM mini 3", "Broadlink"), + 0x2712: (rmpro, "RM pro/pro+", "Broadlink"), + 0x272A: (rmpro, "RM pro", "Broadlink"), + 0x273D: (rmpro, "RM pro", "Broadlink"), + 0x277C: (rmpro, "RM home", "Broadlink"), + 0x2783: (rmpro, "RM home", "Broadlink"), + 0x2787: (rmpro, "RM pro", "Broadlink"), + 0x278B: (rmpro, "RM plus", "Broadlink"), + 0x2797: (rmpro, "RM pro+", "Broadlink"), + 0x279D: (rmpro, "RM pro+", "Broadlink"), + 0x27A1: (rmpro, "RM plus", "Broadlink"), + 0x27A6: (rmpro, "RM plus", "Broadlink"), + 0x27A9: (rmpro, "RM pro+", "Broadlink"), + 0x27C3: (rmpro, "RM pro+", "Broadlink"), + 0x5F36: (rmminib, "RM mini 3", "Broadlink"), + 0x6508: (rmminib, "RM mini 3", "Broadlink"), + 0x51DA: (rm4mini, "RM4 mini", "Broadlink"), + 0x6070: (rm4mini, "RM4C mini", "Broadlink"), + 0x610E: (rm4mini, "RM4 mini", "Broadlink"), + 0x610F: (rm4mini, "RM4C mini", "Broadlink"), + 0x62BC: (rm4mini, "RM4 mini", "Broadlink"), + 0x62BE: (rm4mini, "RM4C mini", "Broadlink"), + 0x6364: (rm4mini, "RM4S", "Broadlink"), + 0x648D: (rm4mini, "RM4 mini", "Broadlink"), + 0x6539: (rm4mini, "RM4C mini", "Broadlink"), + 0x653A: (rm4mini, "RM4 mini", "Broadlink"), + 0x6026: (rm4pro, "RM4 pro", "Broadlink"), + 0x61A2: (rm4pro, "RM4 pro", "Broadlink"), + 0x649B: (rm4pro, "RM4 pro", "Broadlink"), + 0x653C: (rm4pro, "RM4 pro", "Broadlink"), 0x2714: (a1, "e-Sensor", "Broadlink"), 0x4EB5: (mp1, "MP1-1K4S", "Broadlink"), 0x4EF7: (mp1, "MP1-1K4S", "Broadlink (OEM)"), diff --git a/broadlink/remote.py b/broadlink/remote.py index f97b755c..7e4f91e2 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -5,10 +5,10 @@ from .exceptions import check_error -class rm(device): - """Controls a Broadlink RM.""" +class rmmini(device): + """Controls a Broadlink RM mini 3.""" - TYPE = "RM2" + TYPE = "RMMINI" def _send(self, command: int, data: bytes = b'') -> bytes: """Send a packet to the device.""" @@ -18,10 +18,6 @@ def _send(self, command: int, data: bytes = b'') -> bytes: payload = self.decrypt(resp[0x38:]) return payload[0x4:] - def check_data(self) -> bytes: - """Return the last captured code.""" - return self._send(0x4) - def send_data(self, data: bytes) -> None: """Send a code to the device.""" self._send(0x2, data) @@ -30,14 +26,20 @@ def enter_learning(self) -> None: """Enter infrared learning mode.""" self._send(0x3) + def check_data(self) -> bytes: + """Return the last captured code.""" + return self._send(0x4) + + +class rmpro(rmmini): + """Controls a Broadlink RM pro.""" + + TYPE = "RMPRO" + def sweep_frequency(self) -> None: """Sweep frequency.""" self._send(0x19) - def cancel_sweep_frequency(self) -> None: - """Cancel sweep frequency.""" - self._send(0x1E) - def check_frequency(self) -> bool: """Return True if the frequency was identified successfully.""" resp = self._send(0x1A) @@ -47,22 +49,25 @@ def find_rf_packet(self) -> None: """Enter radiofrequency learning mode.""" self._send(0x1B) - def check_temperature(self) -> float: - """Return the temperature.""" - return self.check_sensors()["temperature"] + def cancel_sweep_frequency(self) -> None: + """Cancel sweep frequency.""" + self._send(0x1E) def check_sensors(self) -> dict: """Return the state of the sensors.""" resp = self._send(0x1) - temperature = struct.unpack(" float: + """Return the temperature.""" + return self.check_sensors()["temperature"] -class rm4(rm): - """Controls a Broadlink RM4.""" +class rmminib(rmmini): + """Controls a Broadlink RM mini 3 (new firmware).""" - TYPE = "RM4" + TYPE = "RMMINIB" def _send(self, command: int, data: bytes = b'') -> bytes: """Send a packet to the device.""" @@ -73,14 +78,43 @@ def _send(self, command: int, data: bytes = b'') -> bytes: p_len = struct.unpack(" float: - """Return the humidity.""" - return self.check_sensors()["humidity"] + +class rm4mini(rmminib): + """Controls a Broadlink RM4 mini.""" + + TYPE = "RM4MINI" def check_sensors(self) -> dict: """Return the state of the sensors.""" resp = self._send(0x24) - temperature = struct.unpack(" float: + """Return the temperature.""" + return self.check_sensors()["temperature"] + + def check_humidity(self) -> float: + """Return the humidity.""" + return self.check_sensors()["humidity"] + + +class rm4pro(rm4mini, rmpro): + """Controls a Broadlink RM4 pro.""" + + TYPE = "RM4PRO" + + +class rm(rmpro): + """For backwards compatibility.""" + + TYPE = "RM2" + + +class rm4(rm4pro): + """For backwards compatibility.""" + + TYPE = "RM4" From 5dee06c8150c065db8e6377fc96db836cf98ca44 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 17 Feb 2021 00:38:18 -0300 Subject: [PATCH 10/82] Make 0x2711 a sp2s device (#538) --- broadlink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 81481f42..400962cc 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -15,7 +15,6 @@ SUPPORTED_TYPES = { 0x0000: (sp1, "SP1", "Broadlink"), - 0x2711: (sp2, "SP2", "Broadlink"), 0x2717: (sp2, "NEO", "Ankuoo"), 0x2719: (sp2, "SP2-compatible", "Honeywell"), 0x271A: (sp2, "SP2-compatible", "Honeywell"), @@ -33,6 +32,7 @@ 0x7919: (sp2, "SP2-compatible", "Honeywell"), 0x791A: (sp2, "SP2-compatible", "Honeywell"), 0x7D0D: (sp2, "SP mini 3", "Broadlink (OEM)"), + 0x2711: (sp2s, "SP2", "Broadlink"), 0x2716: (sp2s, "NEO PRO", "Ankuoo"), 0x271D: (sp2s, "Ego", "Efergy"), 0x2736: (sp2s, "SP mini+", "Broadlink"), From 20b9eed6bc4159213af69dd90f7ebc642cd356c9 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 11 Mar 2021 01:00:10 -0300 Subject: [PATCH 11/82] Add a method to update device name and lock status (#537) --- broadlink/remote.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/broadlink/remote.py b/broadlink/remote.py index 7e4f91e2..1227daee 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -18,6 +18,12 @@ def _send(self, command: int, data: bytes = b'') -> bytes: payload = self.decrypt(resp[0x38:]) return payload[0x4:] + def update(self) -> None: + """Update device name and lock status.""" + resp = self._send(0x1) + self.name = resp[0x48:].split(b"\x00")[0].decode() + self.is_locked = bool(resp[0x87]) + def send_data(self, data: bytes) -> None: """Send a code to the device.""" self._send(0x2, data) From 90a43835e8dc052535450c78e7cbaac245df02b5 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 11 Mar 2021 01:25:57 -0300 Subject: [PATCH 12/82] Reset connection ID and AES key before sending Client Key Exchange packets (#549) --- broadlink/device.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/broadlink/device.py b/broadlink/device.py index 144ceeba..ffe1228f 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -89,6 +89,9 @@ class device: TYPE = "Unknown" + __INIT_KEY = "097628343fe99e23765c1513accf8b02" + __INIT_VECT = "562e17996d093d28ddb3ba695a2e6f58" + def __init__( self, host: Tuple[str, int], @@ -110,14 +113,13 @@ def __init__( self.manufacturer = manufacturer self.is_locked = is_locked self.count = random.randint(0x8000, 0xFFFF) - self.iv = bytes.fromhex("562e17996d093d28ddb3ba695a2e6f58") + self.iv = bytes.fromhex(self.__INIT_VECT) self.id = 0 self.type = self.TYPE # For backwards compatibility. self.lock = threading.Lock() self.aes = None - key = bytes.fromhex("097628343fe99e23765c1513accf8b02") - self.update_aes(key) + self.update_aes(bytes.fromhex(self.__INIT_KEY)) def __repr__(self): return "<%s: %s %s (%s) at %s:%s | %s | %s | %s>" % ( @@ -157,6 +159,9 @@ def decrypt(self, payload: bytes) -> bytes: def auth(self) -> bool: """Authenticate to the device.""" + self.id = 0 + self.update_aes(bytes.fromhex(self.__INIT_KEY)) + payload = bytearray(0x50) payload[0x04:0x14] = [0x31]*16 payload[0x1E] = 0x01 From a11b7233c9827c0095d0f488cc5036e8fb2f691c Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 11 Mar 2021 02:51:29 -0300 Subject: [PATCH 13/82] Improve repr(device) and str(device) (#550) --- broadlink/device.py | 59 ++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/broadlink/device.py b/broadlink/device.py index ffe1228f..d67ac8ae 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -98,15 +98,15 @@ def __init__( mac: Union[bytes, str], devtype: int, timeout: int = 10, - name: str = None, - model: str = None, - manufacturer: str = None, - is_locked: bool = None, + name: str = "", + model: str = "", + manufacturer: str = "", + is_locked: bool = False, ) -> None: """Initialize the controller.""" self.host = host self.mac = bytes.fromhex(mac) if isinstance(mac, str) else mac - self.devtype = devtype if devtype is not None else 0x272A + self.devtype = devtype self.timeout = timeout self.name = name self.model = model @@ -121,25 +121,40 @@ def __init__( self.aes = None self.update_aes(bytes.fromhex(self.__INIT_KEY)) - def __repr__(self): - return "<%s: %s %s (%s) at %s:%s | %s | %s | %s>" % ( - type(self).__name__, - self.manufacturer, - self.model, - hex(self.devtype), - self.host[0], - self.host[1], - ":".join(format(x, "02x") for x in self.mac), + def __repr__(self) -> str: + """Return a formal representation of the device.""" + return ( + "%s.%s(%s, mac=%r, devtype=%r, timeout=%r, name=%r, " + "model=%r, manufacturer=%r, is_locked=%r)" + ) % ( + self.__class__.__module__, + self.__class__.__qualname__, + self.host, + self.mac, + self.devtype, + self.timeout, self.name, - "Locked" if self.is_locked else "Unlocked", + self.model, + self.manufacturer, + self.is_locked, ) - def __str__(self): - return "%s (%s at %s)" % ( - self.name, - self.model or hex(self.devtype), - self.host[0], - ) + def __str__(self) -> str: + """Return a readable representation of the device.""" + model = [] + if self.manufacturer: + model.append(self.manufacturer) + if self.model: + model.append(self.model) + model.append(hex(self.devtype)) + model = " ".join(model) + + info = [] + info.append(model) + info.append(f"{self.host[0]}:{self.host[1]}") + info.append(":".join(format(x, "02x") for x in self.mac).upper()) + info = " / ".join(info) + return "%s (%s)" % (self.name or "Unknown", info) def update_aes(self, key: bytes) -> None: """Update AES.""" @@ -225,7 +240,7 @@ def set_name(self, name: str) -> None: packet = bytearray(4) packet += name.encode("utf-8") packet += bytearray(0x50 - len(packet)) - packet[0x43] = bool(self.is_locked) + packet[0x43] = self.is_locked response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) self.name = name From 7c0b4d529ff9e824ece9438272b6e356e2684809 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 11 Mar 2021 04:33:14 -0300 Subject: [PATCH 14/82] Improve exceptions (#551) --- broadlink/exceptions.py | 106 +++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/broadlink/exceptions.py b/broadlink/exceptions.py index d0e5bc0f..19fdf403 100644 --- a/broadlink/exceptions.py +++ b/broadlink/exceptions.py @@ -1,19 +1,17 @@ """Exceptions for Broadlink devices.""" +import collections import struct class BroadlinkException(Exception): - """Common base class for all Broadlink exceptions.""" + """Base class common to all Broadlink exceptions.""" def __init__(self, *args, **kwargs): """Initialize the exception.""" super().__init__(*args, **kwargs) - if len(args) >= 3: + if len(args) >= 2: self.errno = args[0] - self.strerror = "%s: %s" % (args[1], args[2]) - elif len(args) == 2: - self.errno = args[0] - self.strerror = str(args[1]) + self.strerror = ": ".join(str(arg) for arg in args[1:]) elif len(args) == 1: self.errno = None self.strerror = str(args[0]) @@ -22,78 +20,90 @@ def __init__(self, *args, **kwargs): self.strerror = "" def __str__(self): - """Return the error message.""" + """Return str(self).""" if self.errno is not None: return "[Errno %s] %s" % (self.errno, self.strerror) return self.strerror + def __eq__(self, other): + """Return self==value.""" + return type(self) == type(other) and self.args == other.args + + def __hash__(self): + """Return hash(self).""" + return hash((type(self), self.args)) + + +class MultipleErrors(BroadlinkException): + """Multiple errors.""" + + def __init__(self, *args, **kwargs): + """Initialize the exception.""" + errors = args[0][:] if args else [] + counter = collections.Counter(errors) + strerror = "Multiple errors occurred: %s" % counter + super().__init__(strerror, **kwargs) + self.errors = errors + + def __repr__(self): + """Return repr(self).""" + return "MultipleErrors(%r)" % self.errors -class FirmwareException(BroadlinkException): - """Common base class for all firmware exceptions.""" + def __str__(self): + """Return str(self).""" + return self.strerror -class AuthenticationError(FirmwareException): +class AuthenticationError(BroadlinkException): """Authentication error.""" -class AuthorizationError(FirmwareException): +class AuthorizationError(BroadlinkException): """Authorization error.""" -class CommandNotSupportedError(FirmwareException): +class CommandNotSupportedError(BroadlinkException): """Command not supported error.""" -class ConnectionClosedError(FirmwareException): +class ConnectionClosedError(BroadlinkException): """Connection closed error.""" -class DataValidationError(FirmwareException): - """Data validation error.""" +class StructureAbnormalError(BroadlinkException): + """Structure abnormal error.""" -class DeviceOfflineError(FirmwareException): +class DeviceOfflineError(BroadlinkException): """Device offline error.""" -class ReadError(FirmwareException): +class ReadError(BroadlinkException): """Read error.""" -class SendError(FirmwareException): +class SendError(BroadlinkException): """Send error.""" -class SSIDNotFoundError(FirmwareException): +class SSIDNotFoundError(BroadlinkException): """SSID not found error.""" -class StorageError(FirmwareException): +class StorageError(BroadlinkException): """Storage error.""" -class WriteError(FirmwareException): +class WriteError(BroadlinkException): """Write error.""" -class SDKException(BroadlinkException): - """Common base class for all SDK exceptions.""" - - -class DeviceInformationError(SDKException): - """Device information is not intact.""" - - -class ChecksumError(SDKException): - """Received data packet check error.""" - - -class LengthError(SDKException): - """Received data packet length error.""" +class NetworkTimeoutError(BroadlinkException): + """Network timeout error.""" -class NetworkTimeoutError(SDKException): - """Network timeout error.""" +class DataValidationError(BroadlinkException): + """Data validation error.""" class UnknownError(BroadlinkException): @@ -107,30 +117,34 @@ class UnknownError(BroadlinkException): -3: (DeviceOfflineError, "The device is offline"), -4: (CommandNotSupportedError, "Command not supported"), -5: (StorageError, "The device storage is full"), - -6: (DataValidationError, "Structure is abnormal"), + -6: (StructureAbnormalError, "Structure is abnormal"), -7: (AuthorizationError, "Control key is expired"), -8: (SendError, "Send error"), -9: (WriteError, "Write error"), -10: (ReadError, "Read error"), -11: (SSIDNotFoundError, "SSID could not be found in AP configuration"), # SDK related errors are generated by this module. - -2040: (DeviceInformationError, "Device information is not intact"), + -2040: (DataValidationError, "Device information is not intact"), -4000: (NetworkTimeoutError, "Network timeout"), - -4007: (LengthError, "Received data packet length error"), - -4008: (ChecksumError, "Received data packet check error"), + -4007: (DataValidationError, "Received data packet length error"), + -4008: (DataValidationError, "Received data packet check error"), + -4009: (DataValidationError, "Received data packet information type error"), + -4010: (DataValidationError, "Received encrypted data packet length error"), + -4011: (DataValidationError, "Received encrypted data packet check error"), + -4012: (AuthorizationError, "Device control ID error"), } -def exception(error_code): +def exception(err_code: int) -> BroadlinkException: """Return exception corresponding to an error code.""" try: - exc, msg = BROADLINK_EXCEPTIONS[error_code] - return exc(error_code, msg) + exc, msg = BROADLINK_EXCEPTIONS[err_code] + return exc(err_code, msg) except KeyError: - return UnknownError(error_code, "Unknown error") + return UnknownError(err_code, "Unknown error") -def check_error(error): +def check_error(error: bytes) -> None: """Raise exception if an error occurred.""" error_code = struct.unpack("h", error)[0] if error_code: From 335399ef2f7b0fa6b1fcda8e42fd17248b9818fd Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 11 Mar 2021 07:36:20 -0300 Subject: [PATCH 15/82] Add new devices to README.md (#552) --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1bc26f96..eab8cba2 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,19 @@ Python control for Broadlink devices A simple Python API for controlling Broadlink devices. At present, the following devices are supported: -- Universal remotes: `RM pro`, `RM pro+`, `RM pro plus`, `RM mini 3`, `RM4 pro`, `RM4 mini`, `RM4C mini`, `RM4S` -- Smart plugs: `SP1`, `SP2`, `SP mini`, `SP mini+`, `SP3`, `SP3S`, `SP4L`, `SP4M` -- Power strips: `MP1-1K4S`, `MP1-1K3S2U`, `MP2` -- Control box: `SC1`, `SCB1E`, `MCB1` -- Sensors: `A1` -- Alarm kit: `S1C`, `S2KIT` -- Light bulb: `LB1`, `SB800TD` +- **Universal remotes**: RM home, RM mini 3, RM plus, RM pro, RM pro+, RM4 mini, RM4 pro, RM4C mini, RM4S +- **Smart plugs**: SP mini, SP mini 3, SP mini+, SP1, SP2, SP2-BR, SP2-CL, SP2-IN, SP2-UK, SP3, SP3-EU, SP3S-EU, SP3S-US, SP4L-AU, SP4L-EU, SP4L-UK, SP4M, SP4M-US +- **Power strips**: MP1-1K3S2U, MP1-1K4S, MP2 +- **Wi-Fi controlled switches**: MCB1, SC1, SCB1E +- **Environment sensors**: A1 +- **Alarm kits**: S2KIT +- **Light bulbs**: LB1, SB800TD Other devices with Broadlink DNA: -- Smart plugs: `Ankuoo NEO`, `Ankuoo NEO PRO`, `Efergy Ego`, `BG AHC/U-01` -- Outlet: `BG 800`, `BG 900` -- Curtain motor: `Dooya DT360E-45/20` -- Thermostat: `Hysen HY02B05H` +- **Smart plugs**: Ankuoo NEO, Ankuoo NEO PRO, BG AHC/U-01, Efergy Ego +- **Outlets**: BG 800, BG 900 +- **Curtain motors**: Dooya DT360E-45/20 +- **Thermostats**: Hysen HY02B05H There is currently no support for the cloud API. From 9eeee0dedab4dd16de0d438ae4b2d35dac054b8d Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:07:56 -0300 Subject: [PATCH 16/82] Bump cryptography from 2.6.1 to 3.2 (#553) --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 09f445bf..2c6c996c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -cryptography==2.6.1 +cryptography==3.2 diff --git a/setup.py b/setup.py index f662cc64..a0a0705f 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ url="http://github.com/mjg59/python-broadlink", packages=find_packages(), scripts=[], - install_requires=["cryptography>=2.1.1"], + install_requires=["cryptography>=3.2"], description="Python API for controlling Broadlink IR controllers", classifiers=[ "Development Status :: 4 - Beta", From 9ff6b2d48e58f005765088cdf3dc5cc553cdb01a Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Thu, 11 Mar 2021 18:10:04 -0300 Subject: [PATCH 17/82] 0.17.0 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a0a0705f..65bc33e0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.16.0' +version = '0.17.0' setup( name="broadlink", @@ -16,7 +16,7 @@ packages=find_packages(), scripts=[], install_requires=["cryptography>=3.2"], - description="Python API for controlling Broadlink IR controllers", + description="Python API for controlling Broadlink devices", classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", From 822b3c326631c1902b5892a83db126291acbf0b6 Mon Sep 17 00:00:00 2001 From: Andrew Berry Date: Sun, 11 Apr 2021 20:41:07 -0400 Subject: [PATCH 18/82] Add a TROUBLESHOOTING doc with WiFi password notes (#563) * Add a TROUBLESHOOTING doc with WiFi password notes * Update TROUBLESHOOTING.md Co-authored-by: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> --- TROUBLESHOOTING.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 TROUBLESHOOTING.md diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 00000000..a20765b2 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,9 @@ +# Troubleshooting + +## Firmware issues + +### AP setup fails with non-alphanumeric passwords + +Some devices ship with firmware that cannot connect to WLANs with non-alphanumeric passwords. To fix this, update the firmware to the latest version. You can also change the password to one with just letters and numbers or create a separate guest network with a simpler password. + +_First seen on Broadlink RM4 pro 0x6026. Already fixed in firmware v52079._ From d45c9d0850554620157f5c5fb775c61272edf3f2 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 14 Mar 2021 12:20:25 -0300 Subject: [PATCH 19/82] Improve the CLI (#555) * Add check humidity option * Rename type to devtype * Remove unnecessary try except clause * Add commands to README.md --- cli/README.md | 113 ++++++++++++++++++++++++++++++++++------------ cli/broadlink_cli | 15 +++--- 2 files changed, 90 insertions(+), 38 deletions(-) diff --git a/cli/README.md b/cli/README.md index 7e229e3e..7b40b4cf 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,37 +1,29 @@ Command line interface for python-broadlink =========================================== -This is a command line interface for broadlink python library - -Tested with BroadLink RMPRO / RM2 +This is a command line interface for the python-broadlink API. Requirements ------------ -You should have the broadlink python installed, this can be made in many linux distributions using : +You need to install the module first: ``` -sudo pip install broadlink +pip3 install broadlink ``` Installation ----------- -Just copy this files +Download "broadlink_cli" and "broadlink_discovery". Programs -------- +* broadlink_discovery: Discover Broadlink devices connected to the local network. - -* broadlink_discovery -used to run the discovery in the network -this program withh show the command line parameters to be used with -broadlink_cli to select broadlink device - -* broadlink_cli -used to send commands and query the broadlink device +* broadlink_cli: Send commands and query the Broadlink device. -device specification formats +Device specification formats ---------------------------- Using separate parameters for each information: @@ -48,38 +40,99 @@ Using file with parameters: ``` broadlink_cli --device @BEDROOM.device --temp ``` -This is prefered as the configuration is stored in file and you can change -just a file to point to a different hardware +This is prefered as the configuration is stored in a file and you can change +it later to point to a different device. -Sample usage ------------- +Example usage +------------- + +### Common commands -Learn commands : +#### Join device to the Wi-Fi network +``` +broadlink_cli --joinwifi SSID PASSWORD +``` + +#### Discover devices connected to the local network +``` +broadlink_discovery +``` + +### Universal remotes + +#### Learn IR code and show at console ``` -# Learn and save to file -broadlink_cli --device @BEDROOM.device --learnfile LG-TV.power -# LEard and show at console broadlink_cli --device @BEDROOM.device --learn ``` +#### Learn RF code and show at console +``` +broadlink_cli --device @BEDROOM.device --rfscanlearn +``` + +#### Learn IR code and save to file +``` +broadlink_cli --device @BEDROOM.device --learnfile LG-TV.power +``` + +#### Learn RF code and save to file +``` +broadlink_cli --device @BEDROOM.device --rfscanlearn --learnfile LG-TV.power +``` -Send command : +#### Send code +``` +broadlink_cli --device @BEDROOM.device --send DATA +``` + +#### Send code from file ``` broadlink_cli --device @BEDROOM.device --send @LG-TV.power -broadlink_cli --device @BEDROOM.device --send ....datafromlearncommand... ``` -Get Temperature : +#### Check temperature ``` broadlink_cli --device @BEDROOM.device --temperature ``` -Get Energy Consumption (For a SmartPlug) : +#### Check humidity ``` -broadlink_cli --device @BEDROOM.device --energy +broadlink_cli --device @BEDROOM.device --temperature +``` + +### Smart plugs + +#### Turn on +``` +broadlink_cli --device @BEDROOM.device --turnon +``` + +#### Turn off +``` +broadlink_cli --device @BEDROOM.device --turnoff ``` -Once joined to the Broadlink provisioning Wi-Fi, configure it with your Wi-Fi details: +#### Turn on nightlight +``` +broadlink_cli --device @BEDROOM.device --turnnlon ``` -broadlink_cli --joinwifi MySSID MyWifiPassword + +#### Turn off nightlight +``` +broadlink_cli --device @BEDROOM.device --turnnloff +``` + +#### Check power state +``` +broadlink_cli --device @BEDROOM.device --check +``` + +#### Check nightlight state +``` +broadlink_cli --device @BEDROOM.device --checknl +``` + +#### Check power consumption +``` +broadlink_cli --device @BEDROOM.device --energy ``` diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 31bbb2c6..36a83e19 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -70,6 +70,7 @@ parser.add_argument("--type", type=auto_int, default=0x2712, help="type of devic parser.add_argument("--host", help="host address") parser.add_argument("--mac", help="mac address (hex reverse), as used by python-broadlink library") parser.add_argument("--temperature", action="store_true", help="request temperature from device") +parser.add_argument("--humidity", action="store_true", help="request humidity from device") parser.add_argument("--energy", action="store_true", help="request energy consumption from device") parser.add_argument("--check", action="store_true", help="check current power state") parser.add_argument("--checknl", action="store_true", help="check current nightlight state") @@ -92,16 +93,16 @@ args = parser.parse_args() if args.device: values = args.device.split() - type = int(values[0], 0) + devtype = int(values[0], 0) host = values[1] mac = bytearray.fromhex(values[2]) elif args.mac: - type = args.type + devtype = args.type host = args.host mac = bytearray.fromhex(args.mac) if args.host or args.device: - dev = broadlink.gendevice(type, (host, 80), mac) + dev = broadlink.gendevice(devtype, (host, 80), mac) dev.auth() if args.joinwifi: @@ -113,14 +114,12 @@ if args.convert: print(format_durations(durations)) if args.temperature: print(dev.check_temperature()) +if args.humidity: + print(dev.check_humidity()) if args.energy: print(dev.get_energy()) if args.sensors: - try: - data = dev.check_sensors() - except: - data = {} - data['temperature'] = dev.check_temperature() + data = dev.check_sensors() for key in data: print("{} {}".format(key, data[key])) if args.send: From de0cebc00f837b9ccd138e3b4bee5e6017ee2b2e Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 24 Mar 2021 11:44:45 -0300 Subject: [PATCH 20/82] Add support for Broadlink SCB2 (0x6494) (#558) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 400962cc..3e749bf0 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -54,6 +54,7 @@ 0x618B: (sp4b, "SP4L-EU", "Broadlink"), 0x6489: (sp4b, "SP4L-AU", "Broadlink"), 0x648B: (sp4b, "SP4M-US", "Broadlink"), + 0x6494: (sp4b, "SCB2", "Broadlink"), 0x2737: (rmmini, "RM mini 3", "Broadlink"), 0x278F: (rmmini, "RM mini", "Broadlink"), 0x27C2: (rmmini, "RM mini 3", "Broadlink"), From f3e4edcad46d889efda16f977caf0b3cc1b32888 Mon Sep 17 00:00:00 2001 From: Johnson Chin Date: Wed, 24 Mar 2021 22:46:39 +0800 Subject: [PATCH 21/82] Add support for Broadlink SP4L-UK (0x7587) (#561) * Add new SP4L-UK type * Switch: SP4 check power and nightlight to return as boolean Co-authored-by: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> --- broadlink/__init__.py | 1 + broadlink/switch.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 3e749bf0..1abc47ed 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -44,6 +44,7 @@ 0x756F: (sp4, "MCB1", "Broadlink"), 0x7579: (sp4, "SP4L-EU", "Broadlink"), 0x7583: (sp4, "SP mini 3", "Broadlink"), + 0x7587: (sp4, "SP4L-UK", "Broadlink"), 0x7D11: (sp4, "SP mini 3", "Broadlink"), 0xA56A: (sp4, "MCB1", "Broadlink"), 0xA589: (sp4, "SP4L-UK", "Broadlink"), diff --git a/broadlink/switch.py b/broadlink/switch.py index 775b7580..2286cd9b 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -298,12 +298,12 @@ def set_state( def check_power(self) -> bool: """Return the power state of the device.""" state = self.get_state() - return state["pwr"] + return bool(state["pwr"]) def check_nightlight(self) -> bool: """Return the state of the night light.""" state = self.get_state() - return state["ntlight"] + return bool(state["ntlight"]) def get_state(self) -> dict: """Get full state of device.""" From 2198400ad694cf174fd6597e2f5b091db5b69f30 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 25 Mar 2021 18:06:16 -0300 Subject: [PATCH 22/82] Add support for Broadlink LB27 R1 (0xA4F4) (#557) * Add support for Broadlink LB27 R1 (0xA4F4) * Improve typing --- broadlink/__init__.py | 3 +- broadlink/light.py | 119 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 1abc47ed..26392e9a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -8,7 +8,7 @@ from .cover import dooya from .device import device, ping, scan from .exceptions import exception -from .light import lb1 +from .light import lb1, lb27r1 from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro from .sensor import a1 from .switch import bg1, mp1, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b @@ -105,6 +105,7 @@ 0x60C7: (lb1, "LB1", "Broadlink"), 0x60C8: (lb1, "LB1", "Broadlink"), 0x6112: (lb1, "LB1", "Broadlink"), + 0xA4F4: (lb27r1, "LB27 R1", "Broadlink"), 0x2722: (S1C, "S2KIT", "Broadlink"), 0x4EAD: (hysen, "HY02B05H", "Hysen"), 0x4E4D: (dooya, "DT360E-45/20", "Dooya"), diff --git a/broadlink/light.py b/broadlink/light.py index 4673e48d..afa60a4b 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -2,7 +2,7 @@ import enum import json import struct -import typing +import typing as t from .device import device from .exceptions import check_error @@ -58,32 +58,32 @@ def set_state( if green is not None: state["green"] = int(green) if brightness is not None: - state["brightness"] = brightness + state["brightness"] = int(brightness) if colortemp is not None: - state["colortemp"] = colortemp + state["colortemp"] = int(colortemp) if hue is not None: - state["hue"] = hue + state["hue"] = int(hue) if saturation is not None: - state["saturation"] = saturation + state["saturation"] = int(saturation) if transitionduration is not None: - state["transitionduration"] = transitionduration + state["transitionduration"] = int(transitionduration) if maxworktime is not None: - state["maxworktime"] = maxworktime + state["maxworktime"] = int(maxworktime) if bulb_colormode is not None: - state["bulb_colormode"] = bulb_colormode + state["bulb_colormode"] = int(bulb_colormode) if bulb_scenes is not None: - state["bulb_scenes"] = bulb_scenes + state["bulb_scenes"] = str(bulb_scenes) if bulb_scene is not None: - state["bulb_scene"] = bulb_scene + state["bulb_scene"] = str(bulb_scene) if bulb_sceneidx is not None: - state["bulb_sceneidx"] = bulb_sceneidx + state["bulb_sceneidx"] = int(bulb_sceneidx) packet = self._encode(2, state) response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) return self._decode(response) - def _encode(self, flag: int, obj: typing.Any) -> bytes: + def _encode(self, flag: int, obj: t.Any) -> bytes: """Encode a JSON packet.""" # flag: 1 for reading, 2 for writing. packet = bytearray(14) @@ -97,9 +97,102 @@ def _encode(self, flag: int, obj: typing.Any) -> bytes: packet[0x6:0x8] = checksum.to_bytes(2, "little") return packet - def _decode(self, response: bytes) -> typing.Any: + def _decode(self, response: bytes) -> t.Any: """Decode a JSON packet.""" payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(" 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) + check_error(response[0x22:0x24]) + return self._decode(response) + + def set_state( + self, + pwr: bool = None, + red: int = None, + blue: int = None, + green: int = None, + brightness: int = None, + colortemp: int = None, + hue: int = None, + saturation: int = None, + transitionduration: int = None, + maxworktime: int = None, + bulb_colormode: int = None, + bulb_scenes: str = None, + bulb_scene: str = None, + ) -> 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) + check_error(response[0x22:0x24]) + return self._decode(response) + + def _encode(self, flag: int, obj: t.Any) -> bytes: + """Encode a JSON packet.""" + # flag: 1 for reading, 2 for writing. + packet = bytearray(12) + js = json.dumps(obj, separators=[',', ':']).encode() + struct.pack_into( + " t.Any: + """Decode a JSON packet.""" + payload = self.decrypt(response[0x38:]) + js_len = struct.unpack_from(" Date: Wed, 31 Mar 2021 14:27:05 -0300 Subject: [PATCH 23/82] Make better use of namespaces (#564) Use namespaces for typing and exceptions. --- broadlink/__init__.py | 14 +++++++------- broadlink/alarm.py | 4 ++-- broadlink/climate.py | 4 ++-- broadlink/cover.py | 4 ++-- broadlink/device.py | 30 +++++++++++++++--------------- broadlink/light.py | 10 +++++----- broadlink/remote.py | 6 +++--- broadlink/sensor.py | 4 ++-- broadlink/switch.py | 32 ++++++++++++++++---------------- 9 files changed, 54 insertions(+), 54 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 26392e9a..cae7b116 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 """The python-broadlink library.""" import socket -from typing import Generator, List, Tuple, Union +import typing as t +from . import exceptions as e from .alarm import S1C from .climate import hysen from .cover import dooya from .device import device, ping, scan -from .exceptions import exception from .light import lb1, lb27r1 from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro from .sensor import a1 @@ -115,8 +115,8 @@ def gendevice( dev_type: int, - host: Tuple[str, int], - mac: Union[bytes, str], + host: t.Tuple[str, int], + mac: t.Union[bytes, str], name: str = None, is_locked: bool = None, ) -> device: @@ -151,7 +151,7 @@ def hello( try: return next(xdiscover(timeout, local_ip_address, host, port)) except StopIteration: - raise exception(-4000) # Network timeout. + raise e.exception(-4000) # Network timeout. def discover( @@ -159,7 +159,7 @@ def discover( local_ip_address: str = None, discover_ip_address: str = "255.255.255.255", discover_ip_port: int = 80, -) -> List[device]: +) -> t.List[device]: """Discover devices connected to the local network.""" responses = scan(timeout, local_ip_address, discover_ip_address, discover_ip_port) return [gendevice(*resp) for resp in responses] @@ -170,7 +170,7 @@ def xdiscover( local_ip_address: str = None, discover_ip_address: str = "255.255.255.255", discover_ip_port: int = 80, -) -> Generator[device, None, None]: +) -> t.Generator[device, None, None]: """Discover devices connected to the local network. This function returns a generator that yields devices instantly. diff --git a/broadlink/alarm.py b/broadlink/alarm.py index f49b2cf1..80301011 100644 --- a/broadlink/alarm.py +++ b/broadlink/alarm.py @@ -1,6 +1,6 @@ """Support for alarm kits.""" +from . import exceptions as e from .device import device -from .exceptions import check_error class S1C(device): @@ -19,7 +19,7 @@ def get_sensors_status(self) -> dict: packet = bytearray(16) packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) if not payload: return None diff --git a/broadlink/climate.py b/broadlink/climate.py index b510db9d..9f64949f 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -1,8 +1,8 @@ """Support for climate control.""" from typing import List +from . import exceptions as e from .device import device -from .exceptions import check_error from .helpers import calculate_crc16 @@ -31,7 +31,7 @@ def send_request(self, input_payload: bytes) -> bytes: # send to device response = self.send_packet(0x6A, request_payload) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) response_payload = self.decrypt(response[0x38:]) # experimental check on CRC in response (first 2 bytes are len, and trailing bytes are crc) diff --git a/broadlink/cover.py b/broadlink/cover.py index b2dc84a0..2d0cf677 100644 --- a/broadlink/cover.py +++ b/broadlink/cover.py @@ -1,8 +1,8 @@ """Support for covers.""" import time +from . import exceptions as e from .device import device -from .exceptions import check_error class dooya(device): @@ -20,7 +20,7 @@ def _send(self, magic1: int, magic2: int) -> int: packet[9] = 0xFA packet[10] = 0x44 response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return payload[4] diff --git a/broadlink/device.py b/broadlink/device.py index d67ac8ae..6cdec0e6 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -3,15 +3,15 @@ import threading import random import time -from typing import Generator, Tuple, Union +import typing as t from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from .exceptions import check_error, exception +from . import exceptions as e from .protocol import Datetime -HelloResponse = Tuple[int, Tuple[str, int], str, str, bool] +HelloResponse = t.Tuple[int, t.Tuple[str, int], str, str, bool] def scan( @@ -19,7 +19,7 @@ def scan( local_ip_address: str = None, discover_ip_address: str = "255.255.255.255", discover_ip_port: int = 80, -) -> Generator[HelloResponse, None, None]: +) -> t.Generator[HelloResponse, None, None]: """Broadcast a hello message and yield responses.""" conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -94,8 +94,8 @@ class device: def __init__( self, - host: Tuple[str, int], - mac: Union[bytes, str], + host: t.Tuple[str, int], + mac: t.Union[bytes, str], devtype: int, timeout: int = 10, name: str = "", @@ -184,7 +184,7 @@ def auth(self) -> bool: payload[0x30:0x36] = "Test 1".encode() response = self.send_packet(0x65, payload) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) key = payload[0x04:0x14] @@ -209,10 +209,10 @@ def hello(self, local_ip_address=None) -> bool: try: devtype, host, mac, name, is_locked = next(responses) except StopIteration: - raise exception(-4000) # Network timeout. + raise e.exception(-4000) # Network timeout. if (devtype, host, mac) != (self.devtype, self.host, self.mac): - raise exception(-2040) # Device information is not intact. + raise e.exception(-2040) # Device information is not intact. self.name = name self.is_locked = is_locked @@ -231,7 +231,7 @@ def get_fwversion(self) -> int: """Get firmware version.""" packet = bytearray([0x68]) response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return payload[0x4] | payload[0x5] << 8 @@ -242,7 +242,7 @@ def set_name(self, name: str) -> None: packet += bytearray(0x50 - len(packet)) packet[0x43] = self.is_locked response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) self.name = name def set_lock(self, state: bool) -> None: @@ -252,7 +252,7 @@ def set_lock(self, state: bool) -> None: packet += bytearray(0x50 - len(packet)) packet[0x43] = bool(state) response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) self.is_locked = bool(state) def get_type(self) -> str: @@ -294,13 +294,13 @@ def send_packet(self, packet_type: int, payload: bytes) -> bytes: break except socket.timeout: if (time.time() - start_time) > timeout: - raise exception(-4000) # Network timeout. + raise e.exception(-4000) # Network timeout. if len(resp) < 0x30: - raise exception(-4007) # Length error. + raise e.exception(-4007) # Length error. checksum = int.from_bytes(resp[0x20:0x22], "little") if sum(resp, 0xBEAF) - sum(resp[0x20:0x22]) & 0xFFFF != checksum: - raise exception(-4008) # Checksum error. + raise e.exception(-4008) # Checksum error. return resp diff --git a/broadlink/light.py b/broadlink/light.py index afa60a4b..68f0233d 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -4,8 +4,8 @@ import struct import typing as t +from . import exceptions as e from .device import device -from .exceptions import check_error class lb1(device): @@ -27,7 +27,7 @@ def get_state(self) -> dict: """ packet = self._encode(1, {}) response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) return self._decode(response) def set_state( @@ -80,7 +80,7 @@ def set_state( packet = self._encode(2, state) response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) return self._decode(response) def _encode(self, flag: int, obj: t.Any) -> bytes: @@ -124,7 +124,7 @@ def get_state(self) -> dict: """ packet = self._encode(1, {}) response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) return self._decode(response) def set_state( @@ -174,7 +174,7 @@ def set_state( packet = self._encode(2, state) response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) return self._decode(response) def _encode(self, flag: int, obj: t.Any) -> bytes: diff --git a/broadlink/remote.py b/broadlink/remote.py index 1227daee..931727e0 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -1,8 +1,8 @@ """Support for universal remotes.""" import struct +from . import exceptions as e from .device import device -from .exceptions import check_error class rmmini(device): @@ -14,7 +14,7 @@ def _send(self, command: int, data: bytes = b'') -> bytes: """Send a packet to the device.""" packet = struct.pack(" bytes: """Send a packet to the device.""" packet = struct.pack(" dict: """Return the state of the sensors in raw format.""" packet = bytearray([0x1]) response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) data = payload[0x4:] diff --git a/broadlink/switch.py b/broadlink/switch.py index 2286cd9b..fb9ad065 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -2,8 +2,8 @@ import json import struct +from . import exceptions as e from .device import device -from .exceptions import check_error class mp1(device): @@ -27,7 +27,7 @@ def set_power_mask(self, sid_mask: int, state: bool) -> None: packet[0x0E] = sid_mask if state else 0 response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) def set_power(self, sid: int, state: bool) -> None: """Set the power state of the device.""" @@ -47,7 +47,7 @@ def check_power_raw(self) -> int: packet[0x08] = 0x01 response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return payload[0x0E] @@ -76,7 +76,7 @@ def get_state(self) -> dict: """ packet = self._encode(1, b"{}") response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) return self._decode(response) def set_state( @@ -108,7 +108,7 @@ def set_state( js = json.dumps(data).encode("utf8") packet = self._encode(2, js) response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) return self._decode(response) def _encode(self, flag: int, js: str) -> bytes: @@ -152,7 +152,7 @@ def set_power(self, state: bool) -> None: packet = bytearray(4) packet[0] = state response = self.send_packet(0x66, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) class sp2(device): @@ -166,14 +166,14 @@ def set_power(self, state: bool) -> None: packet[0] = 2 packet[4] = int(bool(state)) response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) def check_power(self) -> bool: """Return the power state of the device.""" packet = bytearray(16) packet[0] = 1 response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return bool(payload[0x4]) @@ -188,7 +188,7 @@ def get_energy(self) -> float: packet = bytearray(16) packet[0] = 4 response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return int.from_bytes(payload[0x4:0x7], "little") / 1000 @@ -207,7 +207,7 @@ def set_power(self, state: bool) -> None: else: packet[4] = 1 if state else 0 response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) def set_nightlight(self, state: bool) -> None: """Set the night light state of the device.""" @@ -218,14 +218,14 @@ def set_nightlight(self, state: bool) -> None: else: packet[4] = 2 if state else 0 response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) def check_power(self) -> bool: """Return the power state of the device.""" packet = bytearray(16) packet[0] = 1 response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD) @@ -234,7 +234,7 @@ def check_nightlight(self) -> bool: packet = bytearray(16) packet[0] = 1 response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) @@ -248,7 +248,7 @@ def get_energy(self) -> float: """Return the power consumption in W.""" packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) energy = payload[0x7:0x4:-1].hex() return int(energy) / 100 @@ -326,7 +326,7 @@ def _encode(self, flag: int, state: dict) -> bytes: def _decode(self, response: bytes) -> dict: """Decode a message.""" - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(" bytes: def _decode(self, response: bytes) -> dict: """Decode a message.""" - check_error(response[0x22:0x24]) + e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(" Date: Thu, 1 Apr 2021 12:19:16 -0300 Subject: [PATCH 24/82] Improve CRC-16 function (#565) * Rename calculate_crc16 to crc16 * Apply PEP-8 naming conventions * Remove unnecessary import * Accept any sequence type * Remove unnecessary conversions * Expose polynomial and initial value as kwargs * Remove unnecessary bitwise operations * Store the CRC-16 table for performance * Add missing type hints * Update docstring * General improvements --- broadlink/climate.py | 6 +++--- broadlink/helpers.py | 46 +++++++++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 9f64949f..982f105b 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -3,7 +3,7 @@ from . import exceptions as e from .device import device -from .helpers import calculate_crc16 +from .helpers import crc16 class hysen(device): @@ -19,7 +19,7 @@ class hysen(device): def send_request(self, input_payload: bytes) -> bytes: """Send a request to the device.""" - crc = calculate_crc16(input_payload) + crc = crc16(input_payload) # first byte is length, +2 for CRC16 request_payload = bytearray([len(input_payload) + 2, 0x00]) @@ -40,7 +40,7 @@ def send_request(self, input_payload: bytes) -> bytes: raise ValueError( "hysen_response_error", "first byte of response is not length" ) - crc = calculate_crc16(response_payload[2:response_payload_len]) + crc = crc16(response_payload[2:response_payload_len]) if (response_payload[response_payload_len] == crc & 0xFF) and ( response_payload[response_payload_len + 1] == (crc >> 8) & 0xFF ): diff --git a/broadlink/helpers.py b/broadlink/helpers.py index 404feadd..d5d7e804 100644 --- a/broadlink/helpers.py +++ b/broadlink/helpers.py @@ -1,26 +1,32 @@ """Helper functions.""" -from ctypes import c_ushort +import typing as t +_crc16_cache = {} -def calculate_crc16(input_data: bytes) -> int: - """Calculate the CRC-16 of a byte string.""" - crc16_tab = [] - crc16_constant = 0xA001 - for i in range(0, 256): - crc = c_ushort(i).value - for j in range(0, 8): - if crc & 0x0001: - crc = c_ushort(crc >> 1).value ^ crc16_constant - else: - crc = c_ushort(crc >> 1).value - crc16_tab.append(hex(crc)) +def crc16( + sequence: t.Sequence[int], + polynomial: int = 0xA001, # Default: Modbus CRC-16. + init_value: int = 0xFFFF, +) -> int: + """Calculate the CRC-16 of a sequence of integers.""" + global _crc16_cache - crcValue = 0xFFFF + try: + crc_table = _crc16_cache[polynomial] + except KeyError: + crc_table = [] + for dividend in range(0, 256): + remainder = dividend + for _ in range(0, 8): + if remainder & 1: + remainder = remainder >> 1 ^ polynomial + else: + remainder = remainder >> 1 + crc_table.append(remainder) + _crc16_cache[polynomial] = crc_table - for c in input_data: - tmp = crcValue ^ c - rotated = c_ushort(crcValue >> 8).value - crcValue = rotated ^ int(crc16_tab[(tmp & 0x00FF)], 0) - - return crcValue + crc = init_value + for item in sequence: + crc = crc >> 8 ^ crc_table[(crc ^ item) & 0xFF] + return crc From 4e1e6907629f86c6e1f8615fbf2a11cb2863b42b Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 1 Apr 2021 15:02:35 -0300 Subject: [PATCH 25/82] Encapsulate crc16() to avoid global (#566) --- broadlink/climate.py | 6 ++--- broadlink/helpers.py | 58 ++++++++++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 982f105b..af00aeb7 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -3,7 +3,7 @@ from . import exceptions as e from .device import device -from .helpers import crc16 +from .helpers import CRC16 class hysen(device): @@ -19,7 +19,7 @@ class hysen(device): def send_request(self, input_payload: bytes) -> bytes: """Send a request to the device.""" - crc = crc16(input_payload) + crc = CRC16.calculate(input_payload) # first byte is length, +2 for CRC16 request_payload = bytearray([len(input_payload) + 2, 0x00]) @@ -40,7 +40,7 @@ def send_request(self, input_payload: bytes) -> bytes: raise ValueError( "hysen_response_error", "first byte of response is not length" ) - crc = crc16(response_payload[2:response_payload_len]) + crc = CRC16.calculate(response_payload[2:response_payload_len]) if (response_payload[response_payload_len] == crc & 0xFF) and ( response_payload[response_payload_len + 1] == (crc >> 8) & 0xFF ): diff --git a/broadlink/helpers.py b/broadlink/helpers.py index d5d7e804..100c1132 100644 --- a/broadlink/helpers.py +++ b/broadlink/helpers.py @@ -1,32 +1,38 @@ -"""Helper functions.""" +"""Helper functions and classes.""" import typing as t -_crc16_cache = {} +class CRC16: # pylint: disable=R0903 + """Helps with CRC-16 calculation. -def crc16( - sequence: t.Sequence[int], - polynomial: int = 0xA001, # Default: Modbus CRC-16. - init_value: int = 0xFFFF, -) -> int: - """Calculate the CRC-16 of a sequence of integers.""" - global _crc16_cache + CRC tables are cached for performance. + """ - try: - crc_table = _crc16_cache[polynomial] - except KeyError: - crc_table = [] - for dividend in range(0, 256): - remainder = dividend - for _ in range(0, 8): - if remainder & 1: - remainder = remainder >> 1 ^ polynomial - else: - remainder = remainder >> 1 - crc_table.append(remainder) - _crc16_cache[polynomial] = crc_table + _cache = {} - crc = init_value - for item in sequence: - crc = crc >> 8 ^ crc_table[(crc ^ item) & 0xFF] - return crc + @classmethod + def calculate( + cls, + sequence: t.Sequence[int], + polynomial: int = 0xA001, # Modbus CRC-16. + init_value: int = 0xFFFF, + ) -> int: + """Calculate the CRC-16 of a sequence of integers.""" + try: + crc_table = cls._cache[polynomial] + except KeyError: + crc_table = [] + for dividend in range(0, 256): + remainder = dividend + for _ in range(0, 8): + if remainder & 1: + remainder = remainder >> 1 ^ polynomial + else: + remainder = remainder >> 1 + crc_table.append(remainder) + cls._cache[polynomial] = crc_table + + crc = init_value + for item in sequence: + crc = crc >> 8 ^ crc_table[(crc ^ item) & 0xFF] + return crc From 67b674859f6affe1de2cce9e2c16ade22e04d299 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 1 Apr 2021 20:32:36 -0300 Subject: [PATCH 26/82] Segregate CRC16.get_table() from CRC16.calculate() (#567) --- broadlink/helpers.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/broadlink/helpers.py b/broadlink/helpers.py index 100c1132..948fdd0a 100644 --- a/broadlink/helpers.py +++ b/broadlink/helpers.py @@ -2,7 +2,7 @@ import typing as t -class CRC16: # pylint: disable=R0903 +class CRC16: """Helps with CRC-16 calculation. CRC tables are cached for performance. @@ -11,13 +11,8 @@ class CRC16: # pylint: disable=R0903 _cache = {} @classmethod - def calculate( - cls, - sequence: t.Sequence[int], - polynomial: int = 0xA001, # Modbus CRC-16. - init_value: int = 0xFFFF, - ) -> int: - """Calculate the CRC-16 of a sequence of integers.""" + def get_table(cls, polynomial: int): + """Return the CRC-16 table for a polynomial.""" try: crc_table = cls._cache[polynomial] except KeyError: @@ -31,7 +26,17 @@ def calculate( remainder = remainder >> 1 crc_table.append(remainder) cls._cache[polynomial] = crc_table + return crc_table + @classmethod + def calculate( + cls, + sequence: t.Sequence[int], + polynomial: int = 0xA001, # CRC-16-ANSI. + init_value: int = 0xFFFF, + ) -> int: + """Calculate the CRC-16 of a sequence of integers.""" + crc_table = cls.get_table(polynomial) crc = init_value for item in sequence: crc = crc >> 8 ^ crc_table[(crc ^ item) & 0xFF] From 056434ab4650f217c236ca1b5c2a970940573d9d Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 2 Apr 2021 10:37:30 -0300 Subject: [PATCH 27/82] Add support for Broadlink RM4C pro (0x6184) (#568) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index cae7b116..224b259d 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -92,6 +92,7 @@ 0x6539: (rm4mini, "RM4C mini", "Broadlink"), 0x653A: (rm4mini, "RM4 mini", "Broadlink"), 0x6026: (rm4pro, "RM4 pro", "Broadlink"), + 0x6184: (rm4pro, "RM4C pro", "Broadlink"), 0x61A2: (rm4pro, "RM4 pro", "Broadlink"), 0x649B: (rm4pro, "RM4 pro", "Broadlink"), 0x653C: (rm4pro, "RM4 pro", "Broadlink"), From 36b293bf05614a2f8a0f496d51e011bb178a613c Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 2 Apr 2021 13:21:30 -0300 Subject: [PATCH 28/82] Raise exceptions explicitly (#569) --- broadlink/__init__.py | 8 ++++-- broadlink/device.py | 58 +++++++++++++++++++++++++++++++++---------- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 224b259d..1522faf5 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -151,8 +151,12 @@ def hello( """ try: return next(xdiscover(timeout, local_ip_address, host, port)) - except StopIteration: - raise e.exception(-4000) # Network timeout. + except StopIteration as err: + raise e.NetworkTimeoutError( + -4000, + "Network timeout", + f"No response received within {timeout}s", + ) from err def discover( diff --git a/broadlink/device.py b/broadlink/device.py index 6cdec0e6..f6b09b5e 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -207,12 +207,30 @@ def hello(self, local_ip_address=None) -> bool: discover_ip_port=self.host[1], ) try: - devtype, host, mac, name, is_locked = next(responses) - except StopIteration: - raise e.exception(-4000) # Network timeout. - - if (devtype, host, mac) != (self.devtype, self.host, self.mac): - raise e.exception(-2040) # Device information is not intact. + devtype, _, mac, name, is_locked = next(responses) + + except StopIteration as err: + raise e.NetworkTimeoutError( + -4000, + "Network timeout", + f"No response received within {self.timeout}s", + ) from err + + if mac != self.mac: + raise e.DataValidationError( + -2040, + "Device information is not intact", + "The MAC address is different", + f"Expected {self.mac} and received {mac}", + ) + + if devtype != self.devtype: + raise e.DataValidationError( + -2040, + "Device information is not intact", + "The product ID is different", + f"Expected {self.devtype} and received {devtype}", + ) self.name = name self.is_locked = is_locked @@ -292,15 +310,29 @@ def send_packet(self, packet_type: int, payload: bytes) -> bytes: try: resp = conn.recvfrom(2048)[0] break - except socket.timeout: + except socket.timeout as err: if (time.time() - start_time) > timeout: - raise e.exception(-4000) # Network timeout. + raise e.NetworkTimeoutError( + -4000, + "Network timeout", + f"No response received within {timeout}s", + ) from err if len(resp) < 0x30: - raise e.exception(-4007) # Length error. - - checksum = int.from_bytes(resp[0x20:0x22], "little") - if sum(resp, 0xBEAF) - sum(resp[0x20:0x22]) & 0xFFFF != checksum: - raise e.exception(-4008) # Checksum error. + raise e.DataValidationError( + -4007, + "Received data packet length error", + f"Expected at least 48 bytes and received {len(resp)}", + ) + + nom_checksum = int.from_bytes(resp[0x20:0x22], "little") + real_checksum = sum(resp, 0xBEAF) - sum(resp[0x20:0x22]) & 0xFFFF + + if nom_checksum != real_checksum: + raise e.DataValidationError( + -4008, + "Received data packet check error", + f"Expected a checksum of {nom_checksum} and received {real_checksum}", + ) return resp From b77e803864f6def8c00aa48400e98d332c5144fd Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 3 Apr 2021 01:01:38 -0300 Subject: [PATCH 29/82] Use CamelCase for the Device class (#570) --- broadlink/__init__.py | 12 ++++++------ broadlink/alarm.py | 4 ++-- broadlink/climate.py | 4 ++-- broadlink/cover.py | 4 ++-- broadlink/device.py | 2 +- broadlink/light.py | 6 +++--- broadlink/remote.py | 4 ++-- broadlink/sensor.py | 4 ++-- broadlink/switch.py | 14 +++++++------- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 1522faf5..d3bdd90e 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -7,7 +7,7 @@ from .alarm import S1C from .climate import hysen from .cover import dooya -from .device import device, ping, scan +from .device import Device, ping, scan from .light import lb1, lb27r1 from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro from .sensor import a1 @@ -120,13 +120,13 @@ def gendevice( mac: t.Union[bytes, str], name: str = None, is_locked: bool = None, -) -> device: +) -> Device: """Generate a device.""" try: dev_class, model, manufacturer = SUPPORTED_TYPES[dev_type] except KeyError: - return device(host, mac, dev_type, name=name, is_locked=is_locked) + return Device(host, mac, dev_type, name=name, is_locked=is_locked) return dev_class( host, @@ -144,7 +144,7 @@ def hello( port: int = 80, timeout: int = 10, local_ip_address: str = None, -) -> device: +) -> Device: """Direct device discovery. Useful if the device is locked. @@ -164,7 +164,7 @@ def discover( local_ip_address: str = None, discover_ip_address: str = "255.255.255.255", discover_ip_port: int = 80, -) -> t.List[device]: +) -> t.List[Device]: """Discover devices connected to the local network.""" responses = scan(timeout, local_ip_address, discover_ip_address, discover_ip_port) return [gendevice(*resp) for resp in responses] @@ -175,7 +175,7 @@ def xdiscover( local_ip_address: str = None, discover_ip_address: str = "255.255.255.255", discover_ip_port: int = 80, -) -> t.Generator[device, None, None]: +) -> t.Generator[Device, None, None]: """Discover devices connected to the local network. This function returns a generator that yields devices instantly. diff --git a/broadlink/alarm.py b/broadlink/alarm.py index 80301011..dbd14b54 100644 --- a/broadlink/alarm.py +++ b/broadlink/alarm.py @@ -1,9 +1,9 @@ """Support for alarm kits.""" from . import exceptions as e -from .device import device +from .device import Device -class S1C(device): +class S1C(Device): """Controls a Broadlink S1C.""" TYPE = "S1C" diff --git a/broadlink/climate.py b/broadlink/climate.py index af00aeb7..186ad586 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -2,11 +2,11 @@ from typing import List from . import exceptions as e -from .device import device +from .device import Device from .helpers import CRC16 -class hysen(device): +class hysen(Device): """Controls a Hysen HVAC.""" TYPE = "Hysen heating controller" diff --git a/broadlink/cover.py b/broadlink/cover.py index 2d0cf677..c0f08abb 100644 --- a/broadlink/cover.py +++ b/broadlink/cover.py @@ -2,10 +2,10 @@ import time from . import exceptions as e -from .device import device +from .device import Device -class dooya(device): +class dooya(Device): """Controls a Dooya curtain motor.""" TYPE = "Dooya DT360E" diff --git a/broadlink/device.py b/broadlink/device.py index f6b09b5e..cf889d90 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -84,7 +84,7 @@ def ping(address: str, port: int = 80) -> None: conn.sendto(packet, (address, port)) -class device: +class Device: """Controls a Broadlink device.""" TYPE = "Unknown" diff --git a/broadlink/light.py b/broadlink/light.py index 68f0233d..907add27 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -5,10 +5,10 @@ import typing as t from . import exceptions as e -from .device import device +from .device import Device -class lb1(device): +class lb1(Device): """Controls a Broadlink LB1.""" TYPE = "LB1" @@ -105,7 +105,7 @@ def _decode(self, response: bytes) -> t.Any: return state -class lb27r1(device): +class lb27r1(Device): """Controls a Broadlink LB27 R1.""" TYPE = "LB27R1" diff --git a/broadlink/remote.py b/broadlink/remote.py index 931727e0..47710e38 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -2,10 +2,10 @@ import struct from . import exceptions as e -from .device import device +from .device import Device -class rmmini(device): +class rmmini(Device): """Controls a Broadlink RM mini 3.""" TYPE = "RMMINI" diff --git a/broadlink/sensor.py b/broadlink/sensor.py index 5f48a95b..33e7587d 100644 --- a/broadlink/sensor.py +++ b/broadlink/sensor.py @@ -2,10 +2,10 @@ import struct from . import exceptions as e -from .device import device +from .device import Device -class a1(device): +class a1(Device): """Controls a Broadlink A1.""" TYPE = "A1" diff --git a/broadlink/switch.py b/broadlink/switch.py index fb9ad065..1db8cf33 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -3,10 +3,10 @@ import struct from . import exceptions as e -from .device import device +from .device import Device -class mp1(device): +class mp1(Device): """Controls a Broadlink MP1.""" TYPE = "MP1" @@ -64,7 +64,7 @@ def check_power(self) -> dict: return data -class bg1(device): +class bg1(Device): """Controls a BG Electrical smart outlet.""" TYPE = "BG1" @@ -142,7 +142,7 @@ def _decode(self, response: bytes) -> dict: return state -class sp1(device): +class sp1(Device): """Controls a Broadlink SP1.""" TYPE = "SP1" @@ -155,7 +155,7 @@ def set_power(self, state: bool) -> None: e.check_error(response[0x22:0x24]) -class sp2(device): +class sp2(Device): """Controls a Broadlink SP2.""" TYPE = "SP2" @@ -193,7 +193,7 @@ def get_energy(self) -> float: return int.from_bytes(payload[0x4:0x7], "little") / 1000 -class sp3(device): +class sp3(Device): """Controls a Broadlink SP3.""" TYPE = "SP3" @@ -254,7 +254,7 @@ def get_energy(self) -> float: return int(energy) / 100 -class sp4(device): +class sp4(Device): """Controls a Broadlink SP4.""" TYPE = "SP4" From 12fdf01631da799f0f719ae39a1b534b1d75da7a Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 5 Apr 2021 14:02:56 -0300 Subject: [PATCH 30/82] Improve code quality (#572) * Improve typing * Use better names * Clean up switch.py * Remove unused import * Run black * Remove unnecessary comments * Clean up climate.py --- broadlink/__init__.py | 4 +- broadlink/alarm.py | 10 ++- broadlink/climate.py | 113 ++++++++++++++------------------- broadlink/device.py | 42 +++++------- broadlink/helpers.py | 4 +- broadlink/light.py | 39 ++++++------ broadlink/protocol.py | 4 +- broadlink/remote.py | 10 +-- broadlink/switch.py | 144 ++++++++++++++++++------------------------ 9 files changed, 159 insertions(+), 211 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index d3bdd90e..c1c3febf 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -118,8 +118,8 @@ def gendevice( dev_type: int, host: t.Tuple[str, int], mac: t.Union[bytes, str], - name: str = None, - is_locked: bool = None, + name: str = "", + is_locked: bool = False, ) -> Device: """Generate a device.""" try: diff --git a/broadlink/alarm.py b/broadlink/alarm.py index dbd14b54..a9b5e879 100644 --- a/broadlink/alarm.py +++ b/broadlink/alarm.py @@ -9,20 +9,18 @@ class S1C(Device): TYPE = "S1C" _SENSORS_TYPES = { - 0x31: "Door Sensor", # 49 as hex - 0x91: "Key Fob", # 145 as hex, as serial on fob corpse - 0x21: "Motion Sensor", # 33 as hex + 0x31: "Door Sensor", + 0x91: "Key Fob", + 0x21: "Motion Sensor", } def get_sensors_status(self) -> dict: """Return the state of the sensors.""" packet = bytearray(16) - packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors + packet[0] = 0x06 response = self.send_packet(0x6A, packet) e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - if not payload: - return None count = payload[0x4] sensor_data = payload[0x6:] sensors = [ diff --git a/broadlink/climate.py b/broadlink/climate.py index 186ad586..98770cd4 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -1,5 +1,5 @@ """Support for climate control.""" -from typing import List +import typing as t from . import exceptions as e from .device import Device @@ -11,50 +11,38 @@ class hysen(Device): TYPE = "Hysen heating controller" - # Send a request - # input_payload should be a bytearray, usually 6 bytes, e.g. bytearray([0x01,0x06,0x00,0x02,0x10,0x00]) - # Returns decrypted payload - # New behaviour: raises a ValueError if the device response indicates an error or CRC check fails - # The function prepends length (2 bytes) and appends CRC - - def send_request(self, input_payload: bytes) -> bytes: + def send_request(self, request: t.Sequence[int]) -> bytes: """Send a request to the device.""" - crc = CRC16.calculate(input_payload) - - # first byte is length, +2 for CRC16 - request_payload = bytearray([len(input_payload) + 2, 0x00]) - request_payload.extend(input_payload) - - # append CRC - request_payload.append(crc & 0xFF) - request_payload.append((crc >> 8) & 0xFF) + packet = bytearray() + packet.extend((len(request) + 2).to_bytes(2, "little")) + packet.extend(request) + packet.extend(CRC16.calculate(request).to_bytes(2, "little")) - # send to device - response = self.send_packet(0x6A, request_payload) + response = self.send_packet(0x6A, packet) e.check_error(response[0x22:0x24]) - response_payload = self.decrypt(response[0x38:]) + payload = self.decrypt(response[0x38:]) - # experimental check on CRC in response (first 2 bytes are len, and trailing bytes are crc) - response_payload_len = response_payload[0] - if response_payload_len + 2 > len(response_payload): + p_len = int.from_bytes(payload[:0x02], "little") + if p_len + 2 > len(payload): raise ValueError( "hysen_response_error", "first byte of response is not length" ) - crc = CRC16.calculate(response_payload[2:response_payload_len]) - if (response_payload[response_payload_len] == crc & 0xFF) and ( - response_payload[response_payload_len + 1] == (crc >> 8) & 0xFF - ): - return response_payload[2:response_payload_len] - raise ValueError("hysen_response_error", "CRC check on response failed") - - def get_temp(self) -> int: + + nom_crc = int.from_bytes(payload[p_len : p_len + 2], "little") + real_crc = CRC16.calculate(payload[0x02:p_len]) + if nom_crc != real_crc: + raise ValueError("hysen_response_error", "CRC check on response failed") + + return payload[0x02:p_len] + + def get_temp(self) -> float: """Return the room temperature in degrees celsius.""" - payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])) + payload = self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]) return payload[0x05] / 2.0 - def get_external_temp(self) -> int: + def get_external_temp(self) -> float: """Return the external temperature in degrees celsius.""" - payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])) + payload = self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]) return payload[18] / 2.0 def get_full_status(self) -> dict: @@ -62,28 +50,28 @@ def get_full_status(self) -> dict: Timer schedule included. """ - payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x16])) + payload = self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x16]) data = {} data["remote_lock"] = payload[3] & 1 data["power"] = payload[4] & 1 data["active"] = (payload[4] >> 4) & 1 data["temp_manual"] = (payload[4] >> 6) & 1 - data["room_temp"] = (payload[5] & 255) / 2.0 - data["thermostat_temp"] = (payload[6] & 255) / 2.0 - data["auto_mode"] = payload[7] & 15 - data["loop_mode"] = (payload[7] >> 4) & 15 + data["room_temp"] = payload[5] / 2.0 + data["thermostat_temp"] = payload[6] / 2.0 + data["auto_mode"] = payload[7] & 0xF + data["loop_mode"] = payload[7] >> 4 data["sensor"] = payload[8] data["osv"] = payload[9] data["dif"] = payload[10] data["svh"] = payload[11] data["svl"] = payload[12] - data["room_temp_adj"] = ((payload[13] << 8) + payload[14]) / 2.0 - if data["room_temp_adj"] > 32767: - data["room_temp_adj"] = 32767 - data["room_temp_adj"] + data["room_temp_adj"] = ( + int.from_bytes(payload[13:15], "big", signed=True) / 10.0 + ) data["fre"] = payload[15] data["poweron"] = payload[16] data["unknown"] = payload[17] - data["external_temp"] = (payload[18] & 255) / 2.0 + data["external_temp"] = payload[18] / 2.0 data["hour"] = payload[19] data["min"] = payload[20] data["sec"] = payload[21] @@ -124,7 +112,7 @@ def get_full_status(self) -> dict: def set_mode(self, auto_mode: int, loop_mode: int, sensor: int = 0) -> None: """Set the mode of the device.""" mode_byte = ((loop_mode + 1) << 4) + auto_mode - self.send_request(bytearray([0x01, 0x06, 0x00, 0x02, mode_byte, sensor])) + self.send_request([0x01, 0x06, 0x00, 0x02, mode_byte, sensor]) # Advanced settings # Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor, @@ -133,7 +121,7 @@ def set_mode(self, auto_mode: int, loop_mode: int, sensor: int = 0) -> None: # Deadzone for floor temprature (dIF) dif = 1..9. Factory default: 2C # Upper temperature limit for internal sensor (SVH) svh = 5..99. Factory default: 35C # Lower temperature limit for internal sensor (SVL) svl = 5..99. Factory default: 5C - # Actual temperature calibration (AdJ) adj = -0.5. Prescision 0.1C + # Actual temperature calibration (AdJ) adj = -0.5. Precision 0.1C # Anti-freezing function (FrE) fre = 0 for anti-freezing function shut down, # 1 for anti-freezing function open. Factory default: 0 # Power on memory (POn) poweron = 0 for power on memory off, 1 for power on memory on. Factory default: 0 @@ -150,7 +138,7 @@ def set_advanced( poweron: int, ) -> None: """Set advanced options.""" - input_payload = bytearray( + self.send_request( [ 0x01, 0x10, @@ -165,13 +153,12 @@ def set_advanced( dif, svh, svl, - (int(adj * 2) >> 8 & 0xFF), - (int(adj * 2) & 0xFF), + int(adj * 10) >> 8 & 0xFF, + int(adj * 10) & 0xFF, fre, poweron, ] ) - self.send_request(input_payload) # For backwards compatibility only. Prefer calling set_mode directly. # Note this function invokes loop_mode=0 and sensor=0. @@ -186,22 +173,20 @@ def switch_to_manual(self) -> None: # Set temperature for manual mode (also activates manual mode if currently in automatic) def set_temp(self, temp: float) -> None: """Set the target temperature.""" - self.send_request(bytearray([0x01, 0x06, 0x00, 0x01, 0x00, int(temp * 2)])) + self.send_request([0x01, 0x06, 0x00, 0x01, 0x00, int(temp * 2)]) # Set device on(1) or off(0), does not deactivate Wifi connectivity. # Remote lock disables control by buttons on thermostat. def set_power(self, power: int = 1, remote_lock: int = 0) -> None: """Set the power state of the device.""" - self.send_request(bytearray([0x01, 0x06, 0x00, 0x00, remote_lock, power])) + self.send_request([0x01, 0x06, 0x00, 0x00, remote_lock, power]) # set time on device # n.b. day=1 is Monday, ..., day=7 is Sunday def set_time(self, hour: int, minute: int, second: int, day: int) -> None: """Set the time.""" self.send_request( - bytearray( - [0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day] - ) + [0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day] ) # Set timer schedule @@ -210,28 +195,26 @@ def set_time(self, hour: int, minute: int, second: int, day: int) -> None: # {'start_hour':17, 'start_minute':30, 'temp': 22 } # Each one specifies the thermostat temp that will become effective at start_hour:start_minute # weekend is similar but only has 2 (e.g. switch on in morning and off in afternoon) - def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: + def set_schedule(self, weekday: t.List[dict], weekend: t.List[dict]) -> None: """Set timer schedule.""" - # Begin with some magic values ... - input_payload = bytearray([0x01, 0x10, 0x00, 0x0A, 0x00, 0x0C, 0x18]) + request = [0x01, 0x10, 0x00, 0x0A, 0x00, 0x0C, 0x18] - # Now simply append times/temps # weekday times for i in range(0, 6): - input_payload.append(weekday[i]["start_hour"]) - input_payload.append(weekday[i]["start_minute"]) + request.append(weekday[i]["start_hour"]) + request.append(weekday[i]["start_minute"]) # weekend times for i in range(0, 2): - input_payload.append(weekend[i]["start_hour"]) - input_payload.append(weekend[i]["start_minute"]) + request.append(weekend[i]["start_hour"]) + request.append(weekend[i]["start_minute"]) # weekday temperatures for i in range(0, 6): - input_payload.append(int(weekday[i]["temp"] * 2)) + request.append(int(weekday[i]["temp"] * 2)) # weekend temperatures for i in range(0, 2): - input_payload.append(int(weekend[i]["temp"] * 2)) + request.append(int(weekend[i]["temp"] * 2)) - self.send_request(input_payload) + self.send_request(request) diff --git a/broadlink/device.py b/broadlink/device.py index cf889d90..f65f54ba 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -141,20 +141,12 @@ def __repr__(self) -> str: def __str__(self) -> str: """Return a readable representation of the device.""" - model = [] - if self.manufacturer: - model.append(self.manufacturer) - if self.model: - model.append(self.model) - model.append(hex(self.devtype)) - model = " ".join(model) - - info = [] - info.append(model) - info.append(f"{self.host[0]}:{self.host[1]}") - info.append(":".join(format(x, "02x") for x in self.mac).upper()) - info = " / ".join(info) - return "%s (%s)" % (self.name or "Unknown", info) + return "%s (%s / %s:%s / %s)" % ( + self.name or "Unknown", + " ".join(filter(None, [self.manufacturer, self.model, hex(self.devtype)])), + *self.host, + ":".join(format(x, "02X") for x in self.mac), + ) def update_aes(self, key: bytes) -> None: """Update AES.""" @@ -177,22 +169,18 @@ def auth(self) -> bool: self.id = 0 self.update_aes(bytes.fromhex(self.__INIT_KEY)) - payload = bytearray(0x50) - payload[0x04:0x14] = [0x31]*16 - payload[0x1E] = 0x01 - payload[0x2D] = 0x01 - payload[0x30:0x36] = "Test 1".encode() + packet = bytearray(0x50) + packet[0x04:0x14] = [0x31] * 16 + packet[0x1E] = 0x01 + packet[0x2D] = 0x01 + packet[0x30:0x36] = "Test 1".encode() - response = self.send_packet(0x65, payload) + response = self.send_packet(0x65, packet) e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - key = payload[0x04:0x14] - if len(key) % 16 != 0: - return False - self.id = int.from_bytes(payload[:0x4], "little") - self.update_aes(key) + self.update_aes(payload[0x04:0x14]) return True def hello(self, local_ip_address=None) -> bool: @@ -284,8 +272,8 @@ def send_packet(self, packet_type: int, payload: bytes) -> bytes: packet[0x00:0x08] = bytes.fromhex("5aa5aa555aa5aa55") packet[0x24:0x26] = self.devtype.to_bytes(2, "little") packet[0x26:0x28] = packet_type.to_bytes(2, "little") - packet[0x28:0x2a] = self.count.to_bytes(2, "little") - packet[0x2a:0x30] = self.mac[::-1] + packet[0x28:0x2A] = self.count.to_bytes(2, "little") + packet[0x2A:0x30] = self.mac[::-1] packet[0x30:0x34] = self.id.to_bytes(4, "little") p_checksum = sum(payload, 0xBEAF) & 0xFFFF diff --git a/broadlink/helpers.py b/broadlink/helpers.py index 948fdd0a..6ee54991 100644 --- a/broadlink/helpers.py +++ b/broadlink/helpers.py @@ -8,10 +8,10 @@ class CRC16: CRC tables are cached for performance. """ - _cache = {} + _cache: t.Dict[int, t.List[int]] = {} @classmethod - def get_table(cls, polynomial: int): + def get_table(cls, polynomial: int) -> t.List[int]: """Return the CRC-16 table for a polynomial.""" try: crc_table = cls._cache[polynomial] diff --git a/broadlink/light.py b/broadlink/light.py index 907add27..00bce4d0 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -2,7 +2,6 @@ import enum import json import struct -import typing as t from . import exceptions as e from .device import Device @@ -16,6 +15,7 @@ class lb1(Device): @enum.unique class ColorMode(enum.IntEnum): """Enumerates color modes.""" + RGB = 0 WHITE = 1 SCENE = 2 @@ -83,21 +83,21 @@ def set_state( e.check_error(response[0x22:0x24]) return self._decode(response) - def _encode(self, flag: int, obj: t.Any) -> bytes: + def _encode(self, flag: int, state: dict) -> bytes: """Encode a JSON packet.""" # flag: 1 for reading, 2 for writing. packet = bytearray(14) - js = json.dumps(obj, separators=[',', ':']).encode() - p_len = 12 + len(js) + data = json.dumps(state, separators=(",", ":")).encode() + p_len = 12 + len(data) struct.pack_into( - " t.Any: + def _decode(self, response: bytes) -> dict: """Decode a JSON packet.""" payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(" bytes: + def _encode(self, flag: int, state: dict) -> bytes: """Encode a JSON packet.""" # flag: 1 for reading, 2 for writing. packet = bytearray(12) - js = json.dumps(obj, separators=[',', ':']).encode() - struct.pack_into( - " t.Any: + def _decode(self, response: bytes) -> dict: """Decode a JSON packet.""" payload = self.decrypt(response[0x38:]) - js_len = struct.unpack_from(" bytes: data[0x04:0x06] = datetime.year.to_bytes(2, "little") data[0x06] = datetime.minute data[0x07] = datetime.hour - data[0x08] = int(datetime.strftime('%y')) + data[0x08] = int(datetime.strftime("%y")) data[0x09] = datetime.isoweekday() data[0x0A] = datetime.day data[0x0B] = datetime.month @@ -37,7 +37,7 @@ def unpack(data: bytes) -> dt.datetime: if datetime.isoweekday() != isoweekday: raise ValueError("isoweekday does not match") - if int(datetime.strftime('%y')) != subyear: + if int(datetime.strftime("%y")) != subyear: raise ValueError("subyear does not match") return datetime diff --git a/broadlink/remote.py b/broadlink/remote.py index 47710e38..017dac47 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -10,7 +10,7 @@ class rmmini(Device): TYPE = "RMMINI" - def _send(self, command: int, data: bytes = b'') -> bytes: + def _send(self, command: int, data: bytes = b"") -> bytes: """Send a packet to the device.""" packet = struct.pack(" bytes: + def _send(self, command: int, data: bytes = b"") -> bytes: """Send a packet to the device.""" packet = struct.pack(" dict: @@ -96,7 +96,7 @@ def check_sensors(self) -> dict: temp = struct.unpack(" float: diff --git a/broadlink/switch.py b/broadlink/switch.py index 1db8cf33..7e0f16e5 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -11,7 +11,7 @@ class mp1(Device): TYPE = "MP1" - def set_power_mask(self, sid_mask: int, state: bool) -> None: + def set_power_mask(self, sid_mask: int, pwr: bool) -> None: """Set the power state of the device.""" packet = bytearray(16) packet[0x00] = 0x0D @@ -19,20 +19,20 @@ def set_power_mask(self, sid_mask: int, state: bool) -> None: packet[0x03] = 0xA5 packet[0x04] = 0x5A packet[0x05] = 0x5A - packet[0x06] = 0xB2 + ((sid_mask << 1) if state else sid_mask) + packet[0x06] = 0xB2 + ((sid_mask << 1) if pwr else sid_mask) packet[0x07] = 0xC0 packet[0x08] = 0x02 packet[0x0A] = 0x03 packet[0x0D] = sid_mask - packet[0x0E] = sid_mask if state else 0 + packet[0x0E] = sid_mask if pwr else 0 response = self.send_packet(0x6A, packet) e.check_error(response[0x22:0x24]) - def set_power(self, sid: int, state: bool) -> None: + def set_power(self, sid: int, pwr: bool) -> None: """Set the power state of the device.""" sid_mask = 0x01 << (sid - 1) - self.set_power_mask(sid_mask, state) + self.set_power_mask(sid_mask, pwr) def check_power_raw(self) -> int: """Return the power state of the device in raw format.""" @@ -53,15 +53,13 @@ def check_power_raw(self) -> int: def check_power(self) -> dict: """Return the power state of the device.""" - state = self.check_power_raw() - if state is None: - return {"s1": None, "s2": None, "s3": None, "s4": None} - data = {} - data["s1"] = bool(state & 0x01) - data["s2"] = bool(state & 0x02) - data["s3"] = bool(state & 0x04) - data["s4"] = bool(state & 0x08) - return data + data = self.check_power_raw() + return { + "s1": bool(data & 1), + "s2": bool(data & 2), + "s3": bool(data & 4), + "s4": bool(data & 8), + } class bg1(Device): @@ -74,7 +72,7 @@ def get_state(self) -> dict: Example: `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}` """ - packet = self._encode(1, b"{}") + packet = self._encode(1, {}) response = self.send_packet(0x6A, packet) e.check_error(response[0x22:0x24]) return self._decode(response) @@ -90,48 +88,38 @@ def set_state( idcbrightness: int = None, ) -> dict: """Set the power state of the device.""" - data = {} + state = {} if pwr is not None: - data["pwr"] = int(bool(pwr)) + state["pwr"] = int(bool(pwr)) if pwr1 is not None: - data["pwr1"] = int(bool(pwr1)) + state["pwr1"] = int(bool(pwr1)) if pwr2 is not None: - data["pwr2"] = int(bool(pwr2)) + state["pwr2"] = int(bool(pwr2)) if maxworktime is not None: - data["maxworktime"] = maxworktime + state["maxworktime"] = maxworktime if maxworktime1 is not None: - data["maxworktime1"] = maxworktime1 + state["maxworktime1"] = maxworktime1 if maxworktime2 is not None: - data["maxworktime2"] = maxworktime2 + state["maxworktime2"] = maxworktime2 if idcbrightness is not None: - data["idcbrightness"] = idcbrightness - js = json.dumps(data).encode("utf8") - packet = self._encode(2, js) + state["idcbrightness"] = idcbrightness + + packet = self._encode(2, state) response = self.send_packet(0x6A, packet) e.check_error(response[0x22:0x24]) return self._decode(response) - def _encode(self, flag: int, js: str) -> bytes: + def _encode(self, flag: int, state: dict) -> bytes: """Encode a message.""" - # The packet format is: - # 0x00-0x01 length - # 0x02-0x05 header - # 0x06-0x07 00 - # 0x08 flag (1 for read or 2 write?) - # 0x09 unknown (0xb) - # 0x0a-0x0d length of json - # 0x0e- json data packet = bytearray(14) - length = 4 + 2 + 2 + 4 + len(js) + data = json.dumps(state).encode() + length = 12 + len(data) struct.pack_into( - "> 8 + packet.extend(data) + checksum = sum(packet[0x2:], 0xBEAF) & 0xFFFF + packet[0x06:0x08] = checksum.to_bytes(2, "little") return packet def _decode(self, response: bytes) -> dict: @@ -147,10 +135,10 @@ class sp1(Device): TYPE = "SP1" - def set_power(self, state: bool) -> None: + def set_power(self, pwr: bool) -> None: """Set the power state of the device.""" packet = bytearray(4) - packet[0] = state + packet[0] = bool(pwr) response = self.send_packet(0x66, packet) e.check_error(response[0x22:0x24]) @@ -160,11 +148,11 @@ class sp2(Device): TYPE = "SP2" - def set_power(self, state: bool) -> None: + def set_power(self, pwr: bool) -> None: """Set the power state of the device.""" packet = bytearray(16) packet[0] = 2 - packet[4] = int(bool(state)) + packet[4] = bool(pwr) response = self.send_packet(0x6A, packet) e.check_error(response[0x22:0x24]) @@ -198,25 +186,19 @@ class sp3(Device): TYPE = "SP3" - def set_power(self, state: bool) -> None: + def set_power(self, pwr: bool) -> None: """Set the power state of the device.""" packet = bytearray(16) packet[0] = 2 - if self.check_nightlight(): - packet[4] = 3 if state else 2 - else: - packet[4] = 1 if state else 0 + packet[4] = self.check_nightlight() << 1 | bool(pwr) response = self.send_packet(0x6A, packet) e.check_error(response[0x22:0x24]) - def set_nightlight(self, state: bool) -> None: + def set_nightlight(self, ntlight: bool) -> None: """Set the night light state of the device.""" packet = bytearray(16) packet[0] = 2 - if self.check_power(): - packet[4] = 3 if state else 1 - else: - packet[4] = 2 if state else 0 + packet[4] = bool(ntlight) << 1 | self.check_power() response = self.send_packet(0x6A, packet) e.check_error(response[0x22:0x24]) @@ -227,7 +209,7 @@ def check_power(self) -> bool: response = self.send_packet(0x6A, packet) e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD) + return bool(payload[0x4] & 1) def check_nightlight(self) -> bool: """Return the state of the night light.""" @@ -236,7 +218,7 @@ def check_nightlight(self) -> bool: response = self.send_packet(0x6A, packet) e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) + return bool(payload[0x4] & 2) class sp3s(sp2): @@ -259,13 +241,13 @@ class sp4(Device): TYPE = "SP4" - def set_power(self, state: bool) -> None: + def set_power(self, pwr: bool) -> None: """Set the power state of the device.""" - self.set_state(pwr=state) + self.set_state(pwr=pwr) - def set_nightlight(self, state: bool) -> None: + def set_nightlight(self, ntlight: bool) -> None: """Set the night light state of the device.""" - self.set_state(ntlight=state) + self.set_state(ntlight=ntlight) def set_state( self, @@ -277,21 +259,21 @@ def set_state( childlock: bool = None, ) -> dict: """Set state of device.""" - data = {} + state = {} if pwr is not None: - data["pwr"] = int(bool(pwr)) + state["pwr"] = int(bool(pwr)) if ntlight is not None: - data["ntlight"] = int(bool(ntlight)) + state["ntlight"] = int(bool(ntlight)) if indicator is not None: - data["indicator"] = int(bool(indicator)) + state["indicator"] = int(bool(indicator)) if ntlbrightness is not None: - data["ntlbrightness"] = ntlbrightness + state["ntlbrightness"] = ntlbrightness if maxworktime is not None: - data["maxworktime"] = maxworktime + state["maxworktime"] = maxworktime if childlock is not None: - data["childlock"] = int(bool(childlock)) + state["childlock"] = int(bool(childlock)) - packet = self._encode(2, data) + packet = self._encode(2, state) response = self.send_packet(0x6A, packet) return self._decode(response) @@ -313,15 +295,14 @@ def get_state(self) -> dict: def _encode(self, flag: int, state: dict) -> bytes: """Encode a message.""" - payload = json.dumps(state, separators=(",", ":")).encode() packet = bytearray(12) + data = json.dumps(state, separators=(",", ":")).encode() struct.pack_into( - "> 8 + packet[0x04:0x06] = checksum.to_bytes(2, "little") return packet def _decode(self, response: bytes) -> dict: @@ -352,9 +333,9 @@ def get_state(self) -> dict: def _encode(self, flag: int, state: dict) -> bytes: """Encode a message.""" - payload = json.dumps(state, separators=(",", ":")).encode() packet = bytearray(14) - length = 4 + 2 + 2 + 4 + len(payload) + data = json.dumps(state, separators=(",", ":")).encode() + length = 12 + len(data) struct.pack_into( " bytes: 0x0000, flag, 0x0B, - len(payload), + len(data), ) - packet.extend(payload) - checksum = sum(packet[0x8:], 0xC0AD) & 0xFFFF - packet[0x06] = checksum & 0xFF - packet[0x07] = checksum >> 8 + packet.extend(data) + checksum = sum(packet[0x02:], 0xBEAF) & 0xFFFF + packet[0x06:0x08] = checksum.to_bytes(2, "little") return packet def _decode(self, response: bytes) -> dict: From fc5c33ee97113af2fbb64b5f6a498813d07ba13e Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 6 Apr 2021 15:28:12 -0300 Subject: [PATCH 31/82] Use the absolute position to read the lock status (#575) --- broadlink/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/device.py b/broadlink/device.py index f65f54ba..1399628e 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -64,7 +64,7 @@ def scan( discovered.append((host, mac, devtype)) name = resp[0x40:].split(b"\x00")[0].decode() - is_locked = bool(resp[-1]) + is_locked = bool(resp[0x7F]) yield devtype, host, mac, name, is_locked finally: conn.close() From 49322ddaaef351017b1ae25a8b809bb4ea90b736 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 11 Apr 2021 20:42:44 -0300 Subject: [PATCH 32/82] Add support for Broadlink SP4L-CN (0x7568) (#577) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index c1c3febf..1466bf95 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -40,6 +40,7 @@ 0x7D00: (sp3, "SP3-EU", "Broadlink (OEM)"), 0x9479: (sp3s, "SP3S-US", "Broadlink"), 0x947A: (sp3s, "SP3S-EU", "Broadlink"), + 0x7568: (sp4, "SP4L-CN", "Broadlink"), 0x756C: (sp4, "SP4M", "Broadlink"), 0x756F: (sp4, "MCB1", "Broadlink"), 0x7579: (sp4, "SP4L-EU", "Broadlink"), From e1f3b83efd69d153cc682e6aa9b08e1b8eaff5e4 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 23 Apr 2021 17:02:00 -0300 Subject: [PATCH 33/82] Add support for Broadlink SP4L-EU (0xA5D3) (#582) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 1466bf95..73ff30ba 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -49,6 +49,7 @@ 0x7D11: (sp4, "SP mini 3", "Broadlink"), 0xA56A: (sp4, "MCB1", "Broadlink"), 0xA589: (sp4, "SP4L-UK", "Broadlink"), + 0xA5D3: (sp4, "SP4L-EU", "Broadlink"), 0x5115: (sp4b, "SCB1E", "Broadlink"), 0x51E2: (sp4b, "AHC/U-01", "BG Electrical"), 0x6111: (sp4b, "MCB1", "Broadlink"), From d48d1347a3c2ac019b88de83775b821319e27379 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 29 Apr 2021 18:59:58 -0300 Subject: [PATCH 34/82] Move constants to const.py (#584) --- broadlink/__init__.py | 19 ++++++++++--------- broadlink/const.py | 5 +++++ broadlink/device.py | 13 +++++++------ cli/broadlink_cli | 3 ++- cli/broadlink_discovery | 5 +++-- 5 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 broadlink/const.py diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 73ff30ba..693820dd 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -4,6 +4,7 @@ import typing as t from . import exceptions as e +from .const import DEFAULT_BCAST_ADDR, DEFAULT_PORT, DEFAULT_TIMEOUT from .alarm import S1C from .climate import hysen from .cover import dooya @@ -143,8 +144,8 @@ def gendevice( def hello( host: str, - port: int = 80, - timeout: int = 10, + port: int = DEFAULT_PORT, + timeout: int = DEFAULT_TIMEOUT, local_ip_address: str = None, ) -> Device: """Direct device discovery. @@ -162,10 +163,10 @@ def hello( def discover( - timeout: int = 10, + timeout: int = DEFAULT_TIMEOUT, local_ip_address: str = None, - discover_ip_address: str = "255.255.255.255", - discover_ip_port: int = 80, + discover_ip_address: str = DEFAULT_BCAST_ADDR, + discover_ip_port: int = DEFAULT_PORT, ) -> t.List[Device]: """Discover devices connected to the local network.""" responses = scan(timeout, local_ip_address, discover_ip_address, discover_ip_port) @@ -173,10 +174,10 @@ def discover( def xdiscover( - timeout: int = 10, + timeout: int = DEFAULT_TIMEOUT, local_ip_address: str = None, - discover_ip_address: str = "255.255.255.255", - discover_ip_port: int = 80, + discover_ip_address: str = DEFAULT_BCAST_ADDR, + discover_ip_port: int = DEFAULT_PORT, ) -> t.Generator[Device, None, None]: """Discover devices connected to the local network. @@ -218,5 +219,5 @@ def setup(ssid: str, password: str, security_mode: int) -> None: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Internet # UDP sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - sock.sendto(payload, ("255.255.255.255", 80)) + sock.sendto(payload, (DEFAULT_BCAST_ADDR, DEFAULT_PORT)) sock.close() diff --git a/broadlink/const.py b/broadlink/const.py new file mode 100644 index 00000000..19c37f52 --- /dev/null +++ b/broadlink/const.py @@ -0,0 +1,5 @@ +"""Constants.""" +DEFAULT_BCAST_ADDR = "255.255.255.255" +DEFAULT_PORT = 80 +DEFAULT_RETRY_INTVL = 1 +DEFAULT_TIMEOUT = 10 diff --git a/broadlink/device.py b/broadlink/device.py index 1399628e..c1f8fa8f 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -9,16 +9,17 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from . import exceptions as e +from .const import DEFAULT_BCAST_ADDR, DEFAULT_PORT, DEFAULT_RETRY_INTVL, DEFAULT_TIMEOUT from .protocol import Datetime HelloResponse = t.Tuple[int, t.Tuple[str, int], str, str, bool] def scan( - timeout: int = 10, + timeout: int = DEFAULT_TIMEOUT, local_ip_address: str = None, - discover_ip_address: str = "255.255.255.255", - discover_ip_port: int = 80, + discover_ip_address: str = DEFAULT_BCAST_ADDR, + discover_ip_port: int = DEFAULT_PORT, ) -> t.Generator[HelloResponse, None, None]: """Broadcast a hello message and yield responses.""" conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -70,7 +71,7 @@ def scan( conn.close() -def ping(address: str, port: int = 80) -> None: +def ping(address: str, port: int = DEFAULT_PORT) -> None: """Send a ping packet to an address. This packet feeds the watchdog timer of firmwares >= v53. @@ -97,7 +98,7 @@ def __init__( host: t.Tuple[str, int], mac: t.Union[bytes, str], devtype: int, - timeout: int = 10, + timeout: int = DEFAULT_TIMEOUT, name: str = "", model: str = "", manufacturer: str = "", @@ -292,7 +293,7 @@ def send_packet(self, packet_type: int, payload: bytes) -> bytes: while True: time_left = timeout - (time.time() - start_time) - conn.settimeout(min(1, time_left)) + conn.settimeout(min(DEFAULT_RETRY_INTVL, time_left)) conn.sendto(packet, self.host) try: diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 36a83e19..f7a24ade 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -5,6 +5,7 @@ import codecs import time import broadlink +from broadlink.const import DEFAULT_PORT from broadlink.exceptions import ReadError, StorageError TICK = 32.84 @@ -102,7 +103,7 @@ elif args.mac: mac = bytearray.fromhex(args.mac) if args.host or args.device: - dev = broadlink.gendevice(devtype, (host, 80), mac) + dev = broadlink.gendevice(devtype, (host, DEFAULT_PORT), mac) dev.auth() if args.joinwifi: diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery index c098c91e..477e1bd7 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -2,12 +2,13 @@ import argparse import broadlink +from broadlink.const import DEFAULT_BCAST_ADDR, DEFAULT_TIMEOUT from broadlink.exceptions import StorageError parser = argparse.ArgumentParser(fromfile_prefix_chars='@') -parser.add_argument("--timeout", type=int, default=5, help="timeout to wait for receiving discovery responses") +parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="timeout to wait for receiving discovery responses") parser.add_argument("--ip", default=None, help="ip address to use in the discovery") -parser.add_argument("--dst-ip", default="255.255.255.255", help="destination ip address to use in the discovery") +parser.add_argument("--dst-ip", default=DEFAULT_BCAST_ADDR, help="destination ip address to use in the discovery") args = parser.parse_args() print("Discovering...") From 6a54803a361432d10198f6890ea8eec42edc31a2 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 29 Apr 2021 19:51:22 -0300 Subject: [PATCH 35/82] Improve code quality (#585) * Improve docstrings * Fix line-too-long * Disable unidiomatic-typecheck * Move smart plugs to the top * Use constants from const.py * Run black --- broadlink/climate.py | 8 +- broadlink/device.py | 9 +- broadlink/exceptions.py | 1 + broadlink/protocol.py | 1 + broadlink/switch.py | 248 ++++++++++++++++++++-------------------- 5 files changed, 137 insertions(+), 130 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 98770cd4..32c6ffe5 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -1,4 +1,4 @@ -"""Support for climate control.""" +"""Support for HVAC units.""" import typing as t from . import exceptions as e @@ -106,8 +106,8 @@ def get_full_status(self) -> dict: # Manual mode will activate last used temperature. # In typical usage call set_temp to activate manual control and set temp. # loop_mode refers to index in [ "12345,67", "123456,7", "1234567" ] - # E.g. loop_mode = 0 ("12345,67") means Saturday and Sunday follow the "weekend" schedule - # loop_mode = 2 ("1234567") means every day (including Saturday and Sunday) follows the "weekday" schedule + # E.g. loop_mode = 0 ("12345,67") means Saturday and Sunday (weekend schedule) + # loop_mode = 2 ("1234567") means every day, including Saturday and Sunday (weekday schedule) # The sensor command is currently experimental def set_mode(self, auto_mode: int, loop_mode: int, sensor: int = 0) -> None: """Set the mode of the device.""" @@ -124,7 +124,7 @@ def set_mode(self, auto_mode: int, loop_mode: int, sensor: int = 0) -> None: # Actual temperature calibration (AdJ) adj = -0.5. Precision 0.1C # Anti-freezing function (FrE) fre = 0 for anti-freezing function shut down, # 1 for anti-freezing function open. Factory default: 0 - # Power on memory (POn) poweron = 0 for power on memory off, 1 for power on memory on. Factory default: 0 + # Power on memory (POn) poweron = 0 for off, 1 for on. Default: 0 def set_advanced( self, loop_mode: int, diff --git a/broadlink/device.py b/broadlink/device.py index c1f8fa8f..74e916f4 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -9,7 +9,12 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from . import exceptions as e -from .const import DEFAULT_BCAST_ADDR, DEFAULT_PORT, DEFAULT_RETRY_INTVL, DEFAULT_TIMEOUT +from .const import ( + DEFAULT_BCAST_ADDR, + DEFAULT_PORT, + DEFAULT_RETRY_INTVL, + DEFAULT_TIMEOUT, +) from .protocol import Datetime HelloResponse = t.Tuple[int, t.Tuple[str, int], str, str, bool] @@ -48,7 +53,7 @@ def scan( try: while (time.time() - start_time) < timeout: time_left = timeout - (time.time() - start_time) - conn.settimeout(min(1, time_left)) + conn.settimeout(min(DEFAULT_RETRY_INTVL, time_left)) conn.sendto(packet, (discover_ip_address, discover_ip_port)) while True: diff --git a/broadlink/exceptions.py b/broadlink/exceptions.py index 19fdf403..2343ad6e 100644 --- a/broadlink/exceptions.py +++ b/broadlink/exceptions.py @@ -27,6 +27,7 @@ def __str__(self): def __eq__(self, other): """Return self==value.""" + # pylint: disable=unidiomatic-typecheck return type(self) == type(other) and self.args == other.args def __hash__(self): diff --git a/broadlink/protocol.py b/broadlink/protocol.py index effee38c..b5b09d7a 100644 --- a/broadlink/protocol.py +++ b/broadlink/protocol.py @@ -1,3 +1,4 @@ +"""The networking part of the python-broadlink library.""" import datetime as dt import time diff --git a/broadlink/switch.py b/broadlink/switch.py index 7e0f16e5..1079cde0 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -6,130 +6,6 @@ from .device import Device -class mp1(Device): - """Controls a Broadlink MP1.""" - - TYPE = "MP1" - - def set_power_mask(self, sid_mask: int, pwr: bool) -> None: - """Set the power state of the device.""" - packet = bytearray(16) - packet[0x00] = 0x0D - packet[0x02] = 0xA5 - packet[0x03] = 0xA5 - packet[0x04] = 0x5A - packet[0x05] = 0x5A - packet[0x06] = 0xB2 + ((sid_mask << 1) if pwr else sid_mask) - packet[0x07] = 0xC0 - packet[0x08] = 0x02 - packet[0x0A] = 0x03 - packet[0x0D] = sid_mask - packet[0x0E] = sid_mask if pwr else 0 - - response = self.send_packet(0x6A, packet) - e.check_error(response[0x22:0x24]) - - def set_power(self, sid: int, pwr: bool) -> None: - """Set the power state of the device.""" - sid_mask = 0x01 << (sid - 1) - self.set_power_mask(sid_mask, pwr) - - def check_power_raw(self) -> int: - """Return the power state of the device in raw format.""" - packet = bytearray(16) - packet[0x00] = 0x0A - packet[0x02] = 0xA5 - packet[0x03] = 0xA5 - packet[0x04] = 0x5A - packet[0x05] = 0x5A - packet[0x06] = 0xAE - packet[0x07] = 0xC0 - packet[0x08] = 0x01 - - response = self.send_packet(0x6A, packet) - e.check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) - return payload[0x0E] - - def check_power(self) -> dict: - """Return the power state of the device.""" - data = self.check_power_raw() - return { - "s1": bool(data & 1), - "s2": bool(data & 2), - "s3": bool(data & 4), - "s4": bool(data & 8), - } - - -class bg1(Device): - """Controls a BG Electrical smart outlet.""" - - TYPE = "BG1" - - def get_state(self) -> dict: - """Return the power state of the device. - - Example: `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}` - """ - packet = self._encode(1, {}) - response = self.send_packet(0x6A, packet) - e.check_error(response[0x22:0x24]) - return self._decode(response) - - def set_state( - self, - pwr: bool = None, - pwr1: bool = None, - pwr2: bool = None, - maxworktime: int = None, - maxworktime1: int = None, - maxworktime2: int = None, - idcbrightness: int = None, - ) -> dict: - """Set the power state of the device.""" - state = {} - if pwr is not None: - state["pwr"] = int(bool(pwr)) - if pwr1 is not None: - state["pwr1"] = int(bool(pwr1)) - if pwr2 is not None: - state["pwr2"] = int(bool(pwr2)) - if maxworktime is not None: - state["maxworktime"] = maxworktime - if maxworktime1 is not None: - state["maxworktime1"] = maxworktime1 - if maxworktime2 is not None: - state["maxworktime2"] = maxworktime2 - if idcbrightness is not None: - state["idcbrightness"] = idcbrightness - - packet = self._encode(2, state) - response = self.send_packet(0x6A, packet) - e.check_error(response[0x22:0x24]) - return self._decode(response) - - def _encode(self, flag: int, state: dict) -> bytes: - """Encode a message.""" - packet = bytearray(14) - data = json.dumps(state).encode() - length = 12 + len(data) - struct.pack_into( - " dict: - """Decode a message.""" - payload = self.decrypt(response[0x38:]) - js_len = struct.unpack_from(" dict: js_len = struct.unpack_from(" dict: + """Return the power state of the device. + + Example: `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}` + """ + packet = self._encode(1, {}) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response) + + def set_state( + self, + pwr: bool = None, + pwr1: bool = None, + pwr2: bool = None, + maxworktime: int = None, + maxworktime1: int = None, + maxworktime2: int = None, + idcbrightness: int = None, + ) -> dict: + """Set the power state of the device.""" + state = {} + if pwr is not None: + state["pwr"] = int(bool(pwr)) + if pwr1 is not None: + state["pwr1"] = int(bool(pwr1)) + if pwr2 is not None: + state["pwr2"] = int(bool(pwr2)) + if maxworktime is not None: + state["maxworktime"] = maxworktime + if maxworktime1 is not None: + state["maxworktime1"] = maxworktime1 + if maxworktime2 is not None: + state["maxworktime2"] = maxworktime2 + if idcbrightness is not None: + state["idcbrightness"] = idcbrightness + + packet = self._encode(2, state) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response) + + def _encode(self, flag: int, state: dict) -> bytes: + """Encode a message.""" + packet = bytearray(14) + data = json.dumps(state).encode() + length = 12 + len(data) + struct.pack_into( + " dict: + """Decode a message.""" + payload = self.decrypt(response[0x38:]) + js_len = struct.unpack_from(" None: + """Set the power state of the device.""" + packet = bytearray(16) + packet[0x00] = 0x0D + packet[0x02] = 0xA5 + packet[0x03] = 0xA5 + packet[0x04] = 0x5A + packet[0x05] = 0x5A + packet[0x06] = 0xB2 + ((sid_mask << 1) if pwr else sid_mask) + packet[0x07] = 0xC0 + packet[0x08] = 0x02 + packet[0x0A] = 0x03 + packet[0x0D] = sid_mask + packet[0x0E] = sid_mask if pwr else 0 + + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + + def set_power(self, sid: int, pwr: bool) -> None: + """Set the power state of the device.""" + sid_mask = 0x01 << (sid - 1) + self.set_power_mask(sid_mask, pwr) + + def check_power_raw(self) -> int: + """Return the power state of the device in raw format.""" + packet = bytearray(16) + packet[0x00] = 0x0A + packet[0x02] = 0xA5 + packet[0x03] = 0xA5 + packet[0x04] = 0x5A + packet[0x05] = 0x5A + packet[0x06] = 0xAE + packet[0x07] = 0xC0 + packet[0x08] = 0x01 + + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return payload[0x0E] + + def check_power(self) -> dict: + """Return the power state of the device.""" + data = self.check_power_raw() + return { + "s1": bool(data & 1), + "s2": bool(data & 2), + "s3": bool(data & 4), + "s4": bool(data & 8), + } From 2d863bd6c197f01b2c1e99efd5ab67f400536661 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 29 Apr 2021 20:31:30 -0300 Subject: [PATCH 36/82] Rename the lb27r1 class to lb2 (#586) --- broadlink/__init__.py | 4 ++-- broadlink/light.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 693820dd..beb9fb3b 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -9,7 +9,7 @@ from .climate import hysen from .cover import dooya from .device import Device, ping, scan -from .light import lb1, lb27r1 +from .light import lb1, lb2 from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro from .sensor import a1 from .switch import bg1, mp1, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b @@ -109,7 +109,7 @@ 0x60C7: (lb1, "LB1", "Broadlink"), 0x60C8: (lb1, "LB1", "Broadlink"), 0x6112: (lb1, "LB1", "Broadlink"), - 0xA4F4: (lb27r1, "LB27 R1", "Broadlink"), + 0xA4F4: (lb2, "LB27 R1", "Broadlink"), 0x2722: (S1C, "S2KIT", "Broadlink"), 0x4EAD: (hysen, "HY02B05H", "Hysen"), 0x4E4D: (dooya, "DT360E-45/20", "Dooya"), diff --git a/broadlink/light.py b/broadlink/light.py index 00bce4d0..b3e20a19 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -105,10 +105,10 @@ def _decode(self, response: bytes) -> dict: return state -class lb27r1(Device): - """Controls a Broadlink LB27 R1.""" +class lb2(Device): + """Controls a Broadlink LB26/LB27.""" - TYPE = "LB27R1" + TYPE = "LB2" @enum.unique class ColorMode(enum.IntEnum): From b43b296ff302d60bbfdd5144c855c468d43dfd22 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 1 May 2021 15:05:31 -0300 Subject: [PATCH 37/82] Add support for Broadlink RM mini 3 (0x6507) (#589) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index beb9fb3b..02c4b38a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -83,6 +83,7 @@ 0x27A9: (rmpro, "RM pro+", "Broadlink"), 0x27C3: (rmpro, "RM pro+", "Broadlink"), 0x5F36: (rmminib, "RM mini 3", "Broadlink"), + 0x6507: (rmminib, "RM mini 3", "Broadlink"), 0x6508: (rmminib, "RM mini 3", "Broadlink"), 0x51DA: (rm4mini, "RM4 mini", "Broadlink"), 0x6070: (rm4mini, "RM4C mini", "Broadlink"), From c6390924bf07952921636088d21f94ffab55aeb4 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 3 May 2021 16:02:54 -0300 Subject: [PATCH 38/82] Add support for Broadlink SP4L-AU (0x757B) (#590) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 02c4b38a..e1aa2fba 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -45,6 +45,7 @@ 0x756C: (sp4, "SP4M", "Broadlink"), 0x756F: (sp4, "MCB1", "Broadlink"), 0x7579: (sp4, "SP4L-EU", "Broadlink"), + 0x757B: (sp4, "SP4L-AU", "Broadlink"), 0x7583: (sp4, "SP mini 3", "Broadlink"), 0x7587: (sp4, "SP4L-UK", "Broadlink"), 0x7D11: (sp4, "SP mini 3", "Broadlink"), From 1ae12e7d1c18bce9c098b722e70bbc03fcf5968f Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 3 May 2021 17:34:22 -0300 Subject: [PATCH 39/82] Remove local_ip_address option from hello() (#591) --- broadlink/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index e1aa2fba..53df4021 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -148,14 +148,15 @@ def hello( host: str, port: int = DEFAULT_PORT, timeout: int = DEFAULT_TIMEOUT, - local_ip_address: str = None, ) -> Device: """Direct device discovery. Useful if the device is locked. """ try: - return next(xdiscover(timeout, local_ip_address, host, port)) + return next( + xdiscover(timeout=timeout, discover_ip_address=host, discover_ip_port=port) + ) except StopIteration as err: raise e.NetworkTimeoutError( -4000, From bc44166702e46bff4f950c0927190308fd626ad0 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 6 May 2021 14:52:11 -0300 Subject: [PATCH 40/82] Refactor SUPPORTED_TYPES (#592) --- broadlink/__init__.py | 275 ++++++++++++++++++++++++------------------ 1 file changed, 159 insertions(+), 116 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 53df4021..5cbcd830 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -15,107 +15,147 @@ from .switch import bg1, mp1, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b SUPPORTED_TYPES = { - 0x0000: (sp1, "SP1", "Broadlink"), - 0x2717: (sp2, "NEO", "Ankuoo"), - 0x2719: (sp2, "SP2-compatible", "Honeywell"), - 0x271A: (sp2, "SP2-compatible", "Honeywell"), - 0x2720: (sp2, "SP mini", "Broadlink"), - 0x2728: (sp2, "SP2-compatible", "URANT"), - 0x273E: (sp2, "SP mini", "Broadlink"), - 0x7530: (sp2, "SP2", "Broadlink (OEM)"), - 0x7539: (sp2, "SP2-IL", "Broadlink (OEM)"), - 0x753E: (sp2, "SP mini 3", "Broadlink"), - 0x7540: (sp2, "MP2", "Broadlink"), - 0x7544: (sp2, "SP2-CL", "Broadlink"), - 0x7546: (sp2, "SP2-UK/BR/IN", "Broadlink (OEM)"), - 0x7547: (sp2, "SC1", "Broadlink"), - 0x7918: (sp2, "SP2", "Broadlink (OEM)"), - 0x7919: (sp2, "SP2-compatible", "Honeywell"), - 0x791A: (sp2, "SP2-compatible", "Honeywell"), - 0x7D0D: (sp2, "SP mini 3", "Broadlink (OEM)"), - 0x2711: (sp2s, "SP2", "Broadlink"), - 0x2716: (sp2s, "NEO PRO", "Ankuoo"), - 0x271D: (sp2s, "Ego", "Efergy"), - 0x2736: (sp2s, "SP mini+", "Broadlink"), - 0x2733: (sp3, "SP3", "Broadlink"), - 0x7D00: (sp3, "SP3-EU", "Broadlink (OEM)"), - 0x9479: (sp3s, "SP3S-US", "Broadlink"), - 0x947A: (sp3s, "SP3S-EU", "Broadlink"), - 0x7568: (sp4, "SP4L-CN", "Broadlink"), - 0x756C: (sp4, "SP4M", "Broadlink"), - 0x756F: (sp4, "MCB1", "Broadlink"), - 0x7579: (sp4, "SP4L-EU", "Broadlink"), - 0x757B: (sp4, "SP4L-AU", "Broadlink"), - 0x7583: (sp4, "SP mini 3", "Broadlink"), - 0x7587: (sp4, "SP4L-UK", "Broadlink"), - 0x7D11: (sp4, "SP mini 3", "Broadlink"), - 0xA56A: (sp4, "MCB1", "Broadlink"), - 0xA589: (sp4, "SP4L-UK", "Broadlink"), - 0xA5D3: (sp4, "SP4L-EU", "Broadlink"), - 0x5115: (sp4b, "SCB1E", "Broadlink"), - 0x51E2: (sp4b, "AHC/U-01", "BG Electrical"), - 0x6111: (sp4b, "MCB1", "Broadlink"), - 0x6113: (sp4b, "SCB1E", "Broadlink"), - 0x618B: (sp4b, "SP4L-EU", "Broadlink"), - 0x6489: (sp4b, "SP4L-AU", "Broadlink"), - 0x648B: (sp4b, "SP4M-US", "Broadlink"), - 0x6494: (sp4b, "SCB2", "Broadlink"), - 0x2737: (rmmini, "RM mini 3", "Broadlink"), - 0x278F: (rmmini, "RM mini", "Broadlink"), - 0x27C2: (rmmini, "RM mini 3", "Broadlink"), - 0x27C7: (rmmini, "RM mini 3", "Broadlink"), - 0x27CC: (rmmini, "RM mini 3", "Broadlink"), - 0x27CD: (rmmini, "RM mini 3", "Broadlink"), - 0x27D0: (rmmini, "RM mini 3", "Broadlink"), - 0x27D1: (rmmini, "RM mini 3", "Broadlink"), - 0x27D3: (rmmini, "RM mini 3", "Broadlink"), - 0x27DE: (rmmini, "RM mini 3", "Broadlink"), - 0x2712: (rmpro, "RM pro/pro+", "Broadlink"), - 0x272A: (rmpro, "RM pro", "Broadlink"), - 0x273D: (rmpro, "RM pro", "Broadlink"), - 0x277C: (rmpro, "RM home", "Broadlink"), - 0x2783: (rmpro, "RM home", "Broadlink"), - 0x2787: (rmpro, "RM pro", "Broadlink"), - 0x278B: (rmpro, "RM plus", "Broadlink"), - 0x2797: (rmpro, "RM pro+", "Broadlink"), - 0x279D: (rmpro, "RM pro+", "Broadlink"), - 0x27A1: (rmpro, "RM plus", "Broadlink"), - 0x27A6: (rmpro, "RM plus", "Broadlink"), - 0x27A9: (rmpro, "RM pro+", "Broadlink"), - 0x27C3: (rmpro, "RM pro+", "Broadlink"), - 0x5F36: (rmminib, "RM mini 3", "Broadlink"), - 0x6507: (rmminib, "RM mini 3", "Broadlink"), - 0x6508: (rmminib, "RM mini 3", "Broadlink"), - 0x51DA: (rm4mini, "RM4 mini", "Broadlink"), - 0x6070: (rm4mini, "RM4C mini", "Broadlink"), - 0x610E: (rm4mini, "RM4 mini", "Broadlink"), - 0x610F: (rm4mini, "RM4C mini", "Broadlink"), - 0x62BC: (rm4mini, "RM4 mini", "Broadlink"), - 0x62BE: (rm4mini, "RM4C mini", "Broadlink"), - 0x6364: (rm4mini, "RM4S", "Broadlink"), - 0x648D: (rm4mini, "RM4 mini", "Broadlink"), - 0x6539: (rm4mini, "RM4C mini", "Broadlink"), - 0x653A: (rm4mini, "RM4 mini", "Broadlink"), - 0x6026: (rm4pro, "RM4 pro", "Broadlink"), - 0x6184: (rm4pro, "RM4C pro", "Broadlink"), - 0x61A2: (rm4pro, "RM4 pro", "Broadlink"), - 0x649B: (rm4pro, "RM4 pro", "Broadlink"), - 0x653C: (rm4pro, "RM4 pro", "Broadlink"), - 0x2714: (a1, "e-Sensor", "Broadlink"), - 0x4EB5: (mp1, "MP1-1K4S", "Broadlink"), - 0x4EF7: (mp1, "MP1-1K4S", "Broadlink (OEM)"), - 0x4F1B: (mp1, "MP1-1K3S2U", "Broadlink (OEM)"), - 0x4F65: (mp1, "MP1-1K3S2U", "Broadlink"), - 0x5043: (lb1, "SB800TD", "Broadlink (OEM)"), - 0x504E: (lb1, "LB1", "Broadlink"), - 0x60C7: (lb1, "LB1", "Broadlink"), - 0x60C8: (lb1, "LB1", "Broadlink"), - 0x6112: (lb1, "LB1", "Broadlink"), - 0xA4F4: (lb2, "LB27 R1", "Broadlink"), - 0x2722: (S1C, "S2KIT", "Broadlink"), - 0x4EAD: (hysen, "HY02B05H", "Hysen"), - 0x4E4D: (dooya, "DT360E-45/20", "Dooya"), - 0x51E3: (bg1, "BG800/BG900", "BG Electrical"), + sp1: { + 0x0000: ("SP1", "Broadlink"), + }, + sp2: { + 0x2717: ("NEO", "Ankuoo"), + 0x2719: ("SP2-compatible", "Honeywell"), + 0x271A: ("SP2-compatible", "Honeywell"), + 0x2720: ("SP mini", "Broadlink"), + 0x2728: ("SP2-compatible", "URANT"), + 0x273E: ("SP mini", "Broadlink"), + 0x7530: ("SP2", "Broadlink (OEM)"), + 0x7539: ("SP2-IL", "Broadlink (OEM)"), + 0x753E: ("SP mini 3", "Broadlink"), + 0x7540: ("MP2", "Broadlink"), + 0x7544: ("SP2-CL", "Broadlink"), + 0x7546: ("SP2-UK/BR/IN", "Broadlink (OEM)"), + 0x7547: ("SC1", "Broadlink"), + 0x7918: ("SP2", "Broadlink (OEM)"), + 0x7919: ("SP2-compatible", "Honeywell"), + 0x791A: ("SP2-compatible", "Honeywell"), + 0x7D0D: ("SP mini 3", "Broadlink (OEM)"), + }, + sp2s: { + 0x2711: ("SP2", "Broadlink"), + 0x2716: ("NEO PRO", "Ankuoo"), + 0x271D: ("Ego", "Efergy"), + 0x2736: ("SP mini+", "Broadlink"), + }, + sp3: { + 0x2733: ("SP3", "Broadlink"), + 0x7D00: ("SP3-EU", "Broadlink (OEM)"), + }, + sp3s: { + 0x9479: ("SP3S-US", "Broadlink"), + 0x947A: ("SP3S-EU", "Broadlink"), + }, + sp4: { + 0x7568: ("SP4L-CN", "Broadlink"), + 0x756C: ("SP4M", "Broadlink"), + 0x756F: ("MCB1", "Broadlink"), + 0x7579: ("SP4L-EU", "Broadlink"), + 0x757B: ("SP4L-AU", "Broadlink"), + 0x7583: ("SP mini 3", "Broadlink"), + 0x7587: ("SP4L-UK", "Broadlink"), + 0x7D11: ("SP mini 3", "Broadlink"), + 0xA56A: ("MCB1", "Broadlink"), + 0xA589: ("SP4L-UK", "Broadlink"), + 0xA5D3: ("SP4L-EU", "Broadlink"), + }, + sp4b: { + 0x5115: ("SCB1E", "Broadlink"), + 0x51E2: ("AHC/U-01", "BG Electrical"), + 0x6111: ("MCB1", "Broadlink"), + 0x6113: ("SCB1E", "Broadlink"), + 0x618B: ("SP4L-EU", "Broadlink"), + 0x6489: ("SP4L-AU", "Broadlink"), + 0x648B: ("SP4M-US", "Broadlink"), + 0x6494: ("SCB2", "Broadlink"), + }, + rmmini: { + 0x2737: ("RM mini 3", "Broadlink"), + 0x278F: ("RM mini", "Broadlink"), + 0x27C2: ("RM mini 3", "Broadlink"), + 0x27C7: ("RM mini 3", "Broadlink"), + 0x27CC: ("RM mini 3", "Broadlink"), + 0x27CD: ("RM mini 3", "Broadlink"), + 0x27D0: ("RM mini 3", "Broadlink"), + 0x27D1: ("RM mini 3", "Broadlink"), + 0x27D3: ("RM mini 3", "Broadlink"), + 0x27DE: ("RM mini 3", "Broadlink"), + }, + rmpro: { + 0x2712: ("RM pro/pro+", "Broadlink"), + 0x272A: ("RM pro", "Broadlink"), + 0x273D: ("RM pro", "Broadlink"), + 0x277C: ("RM home", "Broadlink"), + 0x2783: ("RM home", "Broadlink"), + 0x2787: ("RM pro", "Broadlink"), + 0x278B: ("RM plus", "Broadlink"), + 0x2797: ("RM pro+", "Broadlink"), + 0x279D: ("RM pro+", "Broadlink"), + 0x27A1: ("RM plus", "Broadlink"), + 0x27A6: ("RM plus", "Broadlink"), + 0x27A9: ("RM pro+", "Broadlink"), + 0x27C3: ("RM pro+", "Broadlink"), + }, + rmminib: { + 0x5F36: ("RM mini 3", "Broadlink"), + 0x6507: ("RM mini 3", "Broadlink"), + 0x6508: ("RM mini 3", "Broadlink"), + }, + rm4mini: { + 0x51DA: ("RM4 mini", "Broadlink"), + 0x6070: ("RM4C mini", "Broadlink"), + 0x610E: ("RM4 mini", "Broadlink"), + 0x610F: ("RM4C mini", "Broadlink"), + 0x62BC: ("RM4 mini", "Broadlink"), + 0x62BE: ("RM4C mini", "Broadlink"), + 0x6364: ("RM4S", "Broadlink"), + 0x648D: ("RM4 mini", "Broadlink"), + 0x6539: ("RM4C mini", "Broadlink"), + 0x653A: ("RM4 mini", "Broadlink"), + }, + rm4pro: { + 0x6026: ("RM4 pro", "Broadlink"), + 0x6184: ("RM4C pro", "Broadlink"), + 0x61A2: ("RM4 pro", "Broadlink"), + 0x649B: ("RM4 pro", "Broadlink"), + 0x653C: ("RM4 pro", "Broadlink"), + }, + a1: { + 0x2714: ("e-Sensor", "Broadlink"), + }, + mp1: { + 0x4EB5: ("MP1-1K4S", "Broadlink"), + 0x4EF7: ("MP1-1K4S", "Broadlink (OEM)"), + 0x4F1B: ("MP1-1K3S2U", "Broadlink (OEM)"), + 0x4F65: ("MP1-1K3S2U", "Broadlink"), + }, + lb1: { + 0x5043: ("SB800TD", "Broadlink (OEM)"), + 0x504E: ("LB1", "Broadlink"), + 0x60C7: ("LB1", "Broadlink"), + 0x60C8: ("LB1", "Broadlink"), + 0x6112: ("LB1", "Broadlink"), + }, + lb2: { + 0xA4F4: ("LB27 R1", "Broadlink"), + }, + S1C: { + 0x2722: ("S2KIT", "Broadlink"), + }, + hysen: { + 0x4EAD: ("HY02B05H", "Hysen"), + }, + dooya: { + 0x4E4D: ("DT360E-45/20", "Dooya"), + }, + bg1: { + 0x51E3: ("BG800/BG900", "BG Electrical"), + }, } @@ -127,21 +167,24 @@ def gendevice( is_locked: bool = False, ) -> Device: """Generate a device.""" - try: - dev_class, model, manufacturer = SUPPORTED_TYPES[dev_type] - - except KeyError: - return Device(host, mac, dev_type, name=name, is_locked=is_locked) - - return dev_class( - host, - mac, - dev_type, - name=name, - model=model, - manufacturer=manufacturer, - is_locked=is_locked, - ) + for dev_cls, products in SUPPORTED_TYPES.items(): + try: + model, manufacturer = products[dev_type] + + except KeyError: + continue + + return dev_cls( + host, + mac, + dev_type, + name=name, + model=model, + manufacturer=manufacturer, + is_locked=is_locked, + ) + + return Device(host, mac, dev_type, name=name, is_locked=is_locked) def hello( From 3f92850a5fea00a72da2e74a8d0dbe7629390bc0 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 11 May 2021 17:25:22 -0300 Subject: [PATCH 41/82] Add support for Broadlink SP4L-EU (0xA56C) (#593) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 5cbcd830..b8aaac3c 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -61,6 +61,7 @@ 0x7587: ("SP4L-UK", "Broadlink"), 0x7D11: ("SP mini 3", "Broadlink"), 0xA56A: ("MCB1", "Broadlink"), + 0xA56C: ("SP4L-EU", "Broadlink"), 0xA589: ("SP4L-UK", "Broadlink"), 0xA5D3: ("SP4L-EU", "Broadlink"), }, From a84a628d1c4d2f0c4f836f66f23b8462df0c9ef8 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 15 May 2021 15:06:38 -0300 Subject: [PATCH 42/82] Add support for Broadlink RM mini 3 (0x27DC) (#594) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index b8aaac3c..b3eee5d0 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -85,6 +85,7 @@ 0x27D0: ("RM mini 3", "Broadlink"), 0x27D1: ("RM mini 3", "Broadlink"), 0x27D3: ("RM mini 3", "Broadlink"), + 0x27DC: ("RM mini 3", "Broadlink"), 0x27DE: ("RM mini 3", "Broadlink"), }, rmpro: { From ca1634575ee582fcb51738165fcf5b2809191526 Mon Sep 17 00:00:00 2001 From: 1UPNuke <65999898+1UPNuke@users.noreply.github.com> Date: Sun, 20 Jun 2021 21:10:47 +0300 Subject: [PATCH 43/82] Add support for Clas Ohlson SL-2 E14 (0x6065) (#600) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index b3eee5d0..b8ea5671 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -139,6 +139,7 @@ lb1: { 0x5043: ("SB800TD", "Broadlink (OEM)"), 0x504E: ("LB1", "Broadlink"), + 0x606E: ("SB500TD", "Broadlink (OEM)"), 0x60C7: ("LB1", "Broadlink"), 0x60C8: ("LB1", "Broadlink"), 0x6112: ("LB1", "Broadlink"), From 84bec957ad616b384aaed435d4d8bc5b2d25a058 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 13 Oct 2021 03:31:19 -0300 Subject: [PATCH 44/82] Fix flake8 tests (#622) --- .github/workflows/flake8.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml index 2fd67945..cb396611 100644 --- a/.github/workflows/flake8.yaml +++ b/.github/workflows/flake8.yaml @@ -21,7 +21,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 wemake-python-styleguide + pip install wheel + pip install flake8 flake8-quotes if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | From 62f81bc281f0d7b9c9cb840abcf7d25533e1fac9 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 17 Oct 2021 05:36:42 -0300 Subject: [PATCH 45/82] Add support for Broadlink SCB1E (0xA56B) (#623) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index b8ea5671..61de0260 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -61,6 +61,7 @@ 0x7587: ("SP4L-UK", "Broadlink"), 0x7D11: ("SP mini 3", "Broadlink"), 0xA56A: ("MCB1", "Broadlink"), + 0xA56B: ("SCB1E", "Broadlink"), 0xA56C: ("SP4L-EU", "Broadlink"), 0xA589: ("SP4L-UK", "Broadlink"), 0xA5D3: ("SP4L-EU", "Broadlink"), From e29170c7547022785e6238b418064aaa9dd1183f Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 17 Oct 2021 06:11:45 -0300 Subject: [PATCH 46/82] Fix indentation of README.md (#624) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index eab8cba2..8b873095 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ Example use Setup a new device on your local wireless network: 1. Put the device into AP Mode - 1. Long press the reset button until the blue LED is blinking quickly. - 2. Long press again until blue LED is blinking slowly. - 3. Manually connect to the WiFi SSID named BroadlinkProv. + 1. Long press the reset button until the blue LED is blinking quickly. + 2. Long press again until blue LED is blinking slowly. + 3. Manually connect to the WiFi SSID named BroadlinkProv. 2. Run setup() and provide your ssid, network password (if secured), and set the security mode - 1. Security mode options are (0 = none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) + 1. Security mode options are (0 = none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) ``` import broadlink From a721087c0746cb5546d4bce092de9d8ad198d572 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 17 Oct 2021 09:20:33 -0300 Subject: [PATCH 47/82] Improve README.md (#625) --- README.md | 215 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 128 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 8b873095..009315d1 100644 --- a/README.md +++ b/README.md @@ -1,150 +1,184 @@ -Python control for Broadlink devices -=============================================== +# python-broadlink -A simple Python API for controlling Broadlink devices. At present, the following devices are supported: +A Python module and CLI for controlling Broadlink devices locally. The following devices are supported: - **Universal remotes**: RM home, RM mini 3, RM plus, RM pro, RM pro+, RM4 mini, RM4 pro, RM4C mini, RM4S -- **Smart plugs**: SP mini, SP mini 3, SP mini+, SP1, SP2, SP2-BR, SP2-CL, SP2-IN, SP2-UK, SP3, SP3-EU, SP3S-EU, SP3S-US, SP4L-AU, SP4L-EU, SP4L-UK, SP4M, SP4M-US +- **Smart plugs**: SP mini, SP mini 3, SP mini+, SP1, SP2, SP2-BR, SP2-CL, SP2-IN, SP2-UK, SP3, SP3-EU, SP3S-EU, SP3S-US, SP4L-AU, SP4L-EU, SP4L-UK, SP4M, SP4M-US, Ankuoo NEO, Ankuoo NEO PRO, Efergy Ego, BG AHC/U-01 +- **Switches**: MCB1, SC1, SCB1E, SCB2 +- **Outlets**: BG 800, BG 900 - **Power strips**: MP1-1K3S2U, MP1-1K4S, MP2 -- **Wi-Fi controlled switches**: MCB1, SC1, SCB1E - **Environment sensors**: A1 -- **Alarm kits**: S2KIT -- **Light bulbs**: LB1, SB800TD - -Other devices with Broadlink DNA: -- **Smart plugs**: Ankuoo NEO, Ankuoo NEO PRO, BG AHC/U-01, Efergy Ego -- **Outlets**: BG 800, BG 900 +- **Alarm kits**: S1C, S2KIT +- **Light bulbs**: LB1, LB2, SB800TD - **Curtain motors**: Dooya DT360E-45/20 - **Thermostats**: Hysen HY02B05H -There is currently no support for the cloud API. +## Installation -Example use ------------ +Use pip3 to install the latest version of this module. -Setup a new device on your local wireless network: - -1. Put the device into AP Mode - 1. Long press the reset button until the blue LED is blinking quickly. - 2. Long press again until blue LED is blinking slowly. - 3. Manually connect to the WiFi SSID named BroadlinkProv. -2. Run setup() and provide your ssid, network password (if secured), and set the security mode - 1. Security mode options are (0 = none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) ``` -import broadlink - -broadlink.setup('myssid', 'mynetworkpass', 3) +pip3 install broadlink ``` -Discover available devices on the local network: +## Basic functions + +First, open Python 3 and import this module. + ``` +python3 +``` +```python3 import broadlink - -devices = broadlink.discover(timeout=5) ``` -You may need to specify `local_ip_address` or `discover_ip_address` if discovery does not return any devices. +Now let's try some functions... +### Setup -Using your machine's IP address with `local_ip_address` -``` -import broadlink +In order to control the device, you need to connect it to your local network. If you have already configured the device with the Broadlink app, this step is not necessary. -devices = broadlink.discover(timeout=5, local_ip_address='192.168.0.100') +1. Put the device into AP Mode. + - Long press the reset button until the blue LED is blinking quickly. + - Long press again until blue LED is blinking slowly. + - Manually connect to the WiFi SSID named BroadlinkProv. +2. Connect the device to your local network with the setup function. +```python3 +broadlink.setup('myssid', 'mynetworkpass', 3) ``` -Using your subnet's broadcast address with `discover_ip_address` +Security mode options are (0 = none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) +### Discovery + +Use this function to discover devices: + +```python3 +devices = broadlink.discover() ``` -import broadlink -devices = broadlink.discover(timeout=5, discover_ip_address='192.168.0.255') +#### Advanced options +You may need to specify `local_ip_address` or `discover_ip_address` if discovery does not return any devices. + +```python3 +devices = broadlink.discover(local_ip_address='192.168.0.100') # IP address of your local machine. ``` -Obtain the authentication key required for further communication: +```python3 +devices = broadlink.discover(discover_ip_address='192.168.0.255') # Broadcast address of your subnet. ``` -devices[0].auth() + +If the device is locked, it may not be discoverable with broadcast. In such cases, you can use the unicast version `broadlink.hello()` for direct discovery: +```python3 +device = broadlink.hello('192.168.0.16') # IP address of your Broadlink device. ``` -Enter learning mode: +If you are a perfomance freak, use `broadlink.xdiscover()` to create devices instantly: +```python3 +for device in broadlink.xdiscover(): + print(device) # Example action. Do whatever you want here. ``` -devices[0].enter_learning() + +### Authentication +After discovering the device, call the `auth()` method to obtain the authentication key required for further communication: +```python3 +device.auth() ``` -Sweep RF frequencies: +The next steps depend on the type of device you want to control. + +## Universal remotes + +### Learning IR codes + +Learning IR codes takes place in three steps. + +1. Enter learning mode: +```python3 +device.enter_learning() ``` -devices[0].sweep_frequency() +2. When the LED blinks, point the remote at the Broadlink device and press the button you want to learn. +3. Get the IR packet. +```python3 +packet = device.check_data() ``` -Cancel sweep RF frequencies: -``` -devices[0].cancel_sweep_frequency() +### Learning RF codes + +Learning IR codes takes place in five steps. + +1. Sweep the frequency: +```python3 +device.sweep_frequency() ``` -Check whether a frequency has been found: +2. When the LED blinks, point the remote at the Broadlink device for the first time and long press the button you want to learn. +3. Enter learning mode: +```python3 +device.find_rf_packet() ``` -found = devices[0].check_frequency() +4. When the LED blinks, point the remote at the Broadlink device for the second time and short press the button you want to learn. +5. Get the RF packet: +```python3 +packet = device.check_data() ``` -(This will return True if the RM has locked onto a frequency, False otherwise) -Attempt to learn an RF packet: -``` -found = devices[0].find_rf_packet() -``` -(This will return True if a packet has been found, False otherwise) +### Canceling learning -Obtain an IR or RF packet while in learning mode: -``` -ir_packet = devices[0].check_data() +You can exit the learning mode in the middle of the process by calling this method: +```python3 +device.cancel_sweep_frequency() ``` -(This will return None if the device does not have a packet to return) -Send an IR or RF packet: -``` -devices[0].send_data(ir_packet) +### Sending IR/RF packets +```python3 +device.send_data(packet) ``` -Obtain temperature data from an RM2: -``` -devices[0].check_temperature() +### Fetching sensor data +```python3 +data = device.check_sensors() ``` -Obtain sensor data from an A1: -``` -data = devices[0].check_sensors() -``` +## Switches -Set power state on a SmartPlug SP2/SP3/SP4: -``` -devices[0].set_power(True) +### Setting power state +```python3 +device.set_power(True) +device.set_power(False) ``` -Check power state on a SmartPlug: -``` -state = devices[0].check_power() +### Checking power state +```python3 +state = device.check_power() ``` -Check energy consumption on a SmartPlug: -``` -state = devices[0].get_energy() +### Checking energy consumption +```python3 +state = device.get_energy() ``` -Set power state for S1 on a SmartPowerStrip MP1: -``` -devices[0].set_power(1, True) -``` +## Power strips -Check power state on a SmartPowerStrip: -``` -state = devices[0].check_power() +### Setting power state +```python3 +device.set_power(1, True) # Example socket. It could be 2 or 3. +device.set_power(1, False) ``` -Get state on a bulb -``` -state=devices[0].get_state() +### Checking power state +```python3 +state = device.check_power() ``` -Set a state on a bulb +## Light bulbs + +### Fetching data +```python3 +state = device.get_state() ``` + +### Setting state attributes +```python3 devices[0].set_state(pwr=0) devices[0].set_state(pwr=1) devices[0].set_state(brightness=75) @@ -154,3 +188,10 @@ devices[0].set_state(red=0) devices[0].set_state(green=128) devices[0].set_state(bulb_colormode=1) ``` + +## Environment sensors + +### Fetching sensor data +```python3 +data = device.check_sensors() +``` \ No newline at end of file From dc3cf509fcfb500cc1758c626b04e466000285cd Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 17 Oct 2021 10:20:41 -0300 Subject: [PATCH 48/82] Add pull request template (#626) --- .github/PULL_REQUEST_TEMPLATE.md | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..de98d3df --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,55 @@ + +## Context + + + +## Proposed change + + + +## Type of change + + +- [ ] Dependency upgrade +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New device +- [ ] New product id (the device is already supported with a different id) +- [ ] New feature (which adds functionality to an existing device) +- [ ] Breaking change (fix/feature causing existing functionality to break) +- [ ] Code quality improvements to existing code or addition of tests +- [ ] Documentation + +## Additional information + + +- This PR fixes issue: fixes # +- This PR is related to: +- Link to documentation pull request: + +## Checklist + + +- [ ] The code change is tested and works locally. +- [ ] The code has been formatted using Black. +- [ ] The code follows the [Zen of Python](https://www.python.org/dev/peps/pep-0020/). +- [ ] I am creating the Pull Request against the correct branch. +- [ ] Documentation added/updated. From 26ee3192d9f887332bb99c2dbe16d4a6ebf6ee9f Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 17 Oct 2021 13:23:05 -0300 Subject: [PATCH 49/82] Change the type and model of the hysen class (#627) --- broadlink/__init__.py | 2 +- broadlink/climate.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 61de0260..fc64e844 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -152,7 +152,7 @@ 0x2722: ("S2KIT", "Broadlink"), }, hysen: { - 0x4EAD: ("HY02B05H", "Hysen"), + 0x4EAD: ("HY02/HY03", "Hysen"), }, dooya: { 0x4E4D: ("DT360E-45/20", "Dooya"), diff --git a/broadlink/climate.py b/broadlink/climate.py index 32c6ffe5..eee5f119 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -7,9 +7,17 @@ class hysen(Device): - """Controls a Hysen HVAC.""" + """Controls a Hysen heating thermostat. - TYPE = "Hysen heating controller" + This device is manufactured by Hysen and sold under different + brands, including Floureon, Beca Energy, Beok and Decdeal. + + Supported models: + - HY02B05H + - HY03WE + """ + + TYPE = "HYS" def send_request(self, request: t.Sequence[int]) -> bytes: """Send a request to the device.""" From 24ef7302bda0f451cf05fad030bd0221afecf779 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 17 Oct 2021 13:56:11 -0300 Subject: [PATCH 50/82] Bump version to 0.18.0 (#621) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 65bc33e0..505386b9 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.17.0' +version = '0.18.0' setup( name="broadlink", From b596984b44e3122713646327e4df157f7adae817 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 18 Oct 2021 13:42:32 -0300 Subject: [PATCH 51/82] Add ip_address option to setup() (#628) * Add ip_address option to setup() * Update README.md --- README.md | 7 +++++++ broadlink/__init__.py | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 009315d1..d66ab643 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,13 @@ broadlink.setup('myssid', 'mynetworkpass', 3) Security mode options are (0 = none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) +#### Advanced options + +You may need to specify a broadcast address if setup is not working. +```python3 +broadlink.setup('myssid', 'mynetworkpass', 3, ip_address='192.168.0.255') +``` + ### Discovery Use this function to discover devices: diff --git a/broadlink/__init__.py b/broadlink/__init__.py index fc64e844..5f715201 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -240,7 +240,12 @@ def xdiscover( # Setup a new Broadlink device via AP Mode. Review the README to see how to enter AP Mode. # Only tested with Broadlink RM3 Mini (Blackbean) -def setup(ssid: str, password: str, security_mode: int) -> None: +def setup( + ssid: str, + password: str, + security_mode: int, + ip_address: str = DEFAULT_BCAST_ADDR, +) -> None: """Set up a new Broadlink device via AP mode.""" # Security mode options are (0 - none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) payload = bytearray(0x88) @@ -269,5 +274,5 @@ def setup(ssid: str, password: str, security_mode: int) -> None: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Internet # UDP sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - sock.sendto(payload, (DEFAULT_BCAST_ADDR, DEFAULT_PORT)) + sock.sendto(payload, (ip_address, DEFAULT_PORT)) sock.close() From 11febb043bf6cb574fcca29beef049f06713be20 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 18 Oct 2021 14:05:39 -0300 Subject: [PATCH 52/82] Improve README.md (#629) --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d66ab643..06b7f9c3 100644 --- a/README.md +++ b/README.md @@ -67,17 +67,19 @@ devices = broadlink.discover() #### Advanced options You may need to specify `local_ip_address` or `discover_ip_address` if discovery does not return any devices. +Using the IP address of your local machine: ```python3 -devices = broadlink.discover(local_ip_address='192.168.0.100') # IP address of your local machine. +devices = broadlink.discover(local_ip_address='192.168.0.100') ``` +Using the broadcast address of your subnet: ```python3 -devices = broadlink.discover(discover_ip_address='192.168.0.255') # Broadcast address of your subnet. +devices = broadlink.discover(discover_ip_address='192.168.0.255') ``` If the device is locked, it may not be discoverable with broadcast. In such cases, you can use the unicast version `broadlink.hello()` for direct discovery: ```python3 -device = broadlink.hello('192.168.0.16') # IP address of your Broadlink device. +device = broadlink.hello('192.168.0.16') ``` If you are a perfomance freak, use `broadlink.xdiscover()` to create devices instantly: @@ -112,7 +114,7 @@ packet = device.check_data() ### Learning RF codes -Learning IR codes takes place in five steps. +Learning RF codes takes place in five steps. 1. Sweep the frequency: ```python3 From 9873af9bc471a5cf12610485ae9dcdb9bc108bb4 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 18 Oct 2021 14:19:41 -0300 Subject: [PATCH 53/82] Standardize ip_address option (#630) --- broadlink/__init__.py | 8 ++++++-- broadlink/device.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 5f715201..560fd53d 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -192,7 +192,7 @@ def gendevice( def hello( - host: str, + ip_address: str, port: int = DEFAULT_PORT, timeout: int = DEFAULT_TIMEOUT, ) -> Device: @@ -202,7 +202,11 @@ def hello( """ try: return next( - xdiscover(timeout=timeout, discover_ip_address=host, discover_ip_port=port) + xdiscover( + timeout=timeout, + discover_ip_address=ip_address, + discover_ip_port=port, + ) ) except StopIteration as err: raise e.NetworkTimeoutError( diff --git a/broadlink/device.py b/broadlink/device.py index 74e916f4..3bc9d8aa 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -76,7 +76,7 @@ def scan( conn.close() -def ping(address: str, port: int = DEFAULT_PORT) -> None: +def ping(ip_address: str, port: int = DEFAULT_PORT) -> None: """Send a ping packet to an address. This packet feeds the watchdog timer of firmwares >= v53. @@ -87,7 +87,7 @@ def ping(address: str, port: int = DEFAULT_PORT) -> None: conn.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) packet = bytearray(0x30) packet[0x26] = 1 - conn.sendto(packet, (address, port)) + conn.sendto(packet, (ip_address, port)) class Device: From f2a582b8f994791b1736b6336296a83b0ab51b4c Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 18 Oct 2021 15:59:47 -0300 Subject: [PATCH 54/82] Add support for Broadlink MP1 with power meter (#631) --- broadlink/__init__.py | 6 ++++-- broadlink/switch.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 560fd53d..d69c91d4 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -12,7 +12,7 @@ from .light import lb1, lb2 from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro from .sensor import a1 -from .switch import bg1, mp1, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b +from .switch import bg1, mp1, mp1s, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b SUPPORTED_TYPES = { sp1: { @@ -133,10 +133,12 @@ }, mp1: { 0x4EB5: ("MP1-1K4S", "Broadlink"), - 0x4EF7: ("MP1-1K4S", "Broadlink (OEM)"), 0x4F1B: ("MP1-1K3S2U", "Broadlink (OEM)"), 0x4F65: ("MP1-1K3S2U", "Broadlink"), }, + mp1s: { + 0x4EF7: ("MP1-1K4S", "Broadlink (OEM)"), + }, lb1: { 0x5043: ("SB800TD", "Broadlink (OEM)"), 0x504E: ("LB1", "Broadlink"), diff --git a/broadlink/switch.py b/broadlink/switch.py index 1079cde0..a2c15be2 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -360,3 +360,18 @@ def check_power(self) -> dict: "s3": bool(data & 4), "s4": bool(data & 8), } + + +class mp1s(mp1): + """Controls a Broadlink MP1S.""" + + TYPE = "MP1S" + + def get_energy(self) -> float: + """Return the power consumption in W.""" + packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + energy = payload[0x7:0x4:-1].hex() + return int(energy) / 100 From bb195043142582c210122b449d7686eb6311a04d Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 18 Oct 2021 18:23:05 -0300 Subject: [PATCH 55/82] Fix instructions for learning RF codes (#632) --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 06b7f9c3..eea48d3f 100644 --- a/README.md +++ b/README.md @@ -114,23 +114,33 @@ packet = device.check_data() ### Learning RF codes -Learning RF codes takes place in five steps. +Learning RF codes takes place in six steps. 1. Sweep the frequency: ```python3 device.sweep_frequency() ``` 2. When the LED blinks, point the remote at the Broadlink device for the first time and long press the button you want to learn. -3. Enter learning mode: +3. Check if the frequency was successfully identified: +```python3 +ok = device.check_frequency() +if ok: + print('Frequency found!') +``` +4. Enter learning mode: ```python3 device.find_rf_packet() ``` -4. When the LED blinks, point the remote at the Broadlink device for the second time and short press the button you want to learn. -5. Get the RF packet: +5. When the LED blinks, point the remote at the Broadlink device for the second time and short press the button you want to learn. +6. Get the RF packet: ```python3 packet = device.check_data() ``` +#### Notes + +Universal remotes with product id 0x2712 use the same method for learning IR and RF codes. They don't need to sweep frequency. Just call `device.enter_learning()` and `device.check_data()`. + ### Canceling learning You can exit the learning mode in the middle of the process by calling this method: From 3bdb6dfb92b7819e835406a9cb499508ee63924b Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 19 Mar 2022 15:28:54 -0300 Subject: [PATCH 56/82] Merge new product ids into master (#667) * Add support for Broadlink LB26 R1 (0x644E) (#636) * Add support for Broadlink LB26 R1 (0x644E) * Add Broadlink LB26 R1 to README.md * Add missing device codes for LB27 R1 Smart Bulbs (#644) These are two missing codes. This closes issue #639 * Add support for Broadlink RM4 pro (0x5213) (#649) * Add support for Broadlink RM4 TV mate (0x5209) (#655) * Move 0x644C and 0x644E to the LB1 class (#666) Co-authored-by: Mathew Verdouw --- README.md | 4 ++-- broadlink/__init__.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 009315d1..90fad7b7 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ A Python module and CLI for controlling Broadlink devices locally. The following devices are supported: -- **Universal remotes**: RM home, RM mini 3, RM plus, RM pro, RM pro+, RM4 mini, RM4 pro, RM4C mini, RM4S +- **Universal remotes**: RM home, RM mini 3, RM plus, RM pro, RM pro+, RM4 mini, RM4 pro, RM4C mini, RM4S, RM4 TV mate - **Smart plugs**: SP mini, SP mini 3, SP mini+, SP1, SP2, SP2-BR, SP2-CL, SP2-IN, SP2-UK, SP3, SP3-EU, SP3S-EU, SP3S-US, SP4L-AU, SP4L-EU, SP4L-UK, SP4M, SP4M-US, Ankuoo NEO, Ankuoo NEO PRO, Efergy Ego, BG AHC/U-01 - **Switches**: MCB1, SC1, SCB1E, SCB2 - **Outlets**: BG 800, BG 900 - **Power strips**: MP1-1K3S2U, MP1-1K4S, MP2 - **Environment sensors**: A1 - **Alarm kits**: S1C, S2KIT -- **Light bulbs**: LB1, LB2, SB800TD +- **Light bulbs**: LB1, LB26 R1, LB27 R1, SB800TD - **Curtain motors**: Dooya DT360E-45/20 - **Thermostats**: Hysen HY02B05H diff --git a/broadlink/__init__.py b/broadlink/__init__.py index fc64e844..db8f3e5f 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -111,6 +111,7 @@ }, rm4mini: { 0x51DA: ("RM4 mini", "Broadlink"), + 0x5209: ("RM4 TV mate", "Broadlink"), 0x6070: ("RM4C mini", "Broadlink"), 0x610E: ("RM4 mini", "Broadlink"), 0x610F: ("RM4C mini", "Broadlink"), @@ -122,6 +123,7 @@ 0x653A: ("RM4 mini", "Broadlink"), }, rm4pro: { + 0x5213: ("RM4 pro", "Broadlink"), 0x6026: ("RM4 pro", "Broadlink"), 0x6184: ("RM4C pro", "Broadlink"), 0x61A2: ("RM4 pro", "Broadlink"), @@ -144,9 +146,12 @@ 0x60C7: ("LB1", "Broadlink"), 0x60C8: ("LB1", "Broadlink"), 0x6112: ("LB1", "Broadlink"), + 0x644C: ("LB27 R1", "Broadlink"), + 0x644E: ("LB26 R1", "Broadlink"), }, lb2: { 0xA4F4: ("LB27 R1", "Broadlink"), + 0xA5F7: ("LB27 R1", "Broadlink"), }, S1C: { 0x2722: ("S2KIT", "Broadlink"), From d870560e6e6a579e3fbd0793890e2dc16c3712fa Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 19 Mar 2022 15:43:00 -0300 Subject: [PATCH 57/82] Bump version to 0.18.1 (#668) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 505386b9..ac01c085 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.18.0' +version = '0.18.1' setup( name="broadlink", From 2b70440786c7b63eb4445676db78a2acd387eaf4 Mon Sep 17 00:00:00 2001 From: Steven Dodd Date: Sat, 19 Mar 2022 22:31:14 +0000 Subject: [PATCH 58/82] Add support for S3 Hub and LC-1 (1,2&3 gang) light switches (#654) * https://github.com/mjg59/python-broadlink/issues/647 * Added get_state(did) and update documentation * Fixed pwr3 set_state * Added get_subdevices() * Cleaned up get_subdevices * Updated S3 documentation * Added device id 0xA59C:("S3", "Broadlink") * Improve logic of get_subdevices() Prevents infinite loop. * Black * Move s3 closer to s1c * Update README.md Co-authored-by: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> --- README.md | 33 ++++++++++++++++- broadlink/__init__.py | 5 +++ broadlink/hub.py | 83 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 broadlink/hub.py diff --git a/README.md b/README.md index 90fad7b7..675dfe0a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A Python module and CLI for controlling Broadlink devices locally. The following - **Light bulbs**: LB1, LB26 R1, LB27 R1, SB800TD - **Curtain motors**: Dooya DT360E-45/20 - **Thermostats**: Hysen HY02B05H +- **Hubs**: S3 ## Installation @@ -194,4 +195,34 @@ devices[0].set_state(bulb_colormode=1) ### Fetching sensor data ```python3 data = device.check_sensors() -``` \ No newline at end of file +``` + +## Hubs + +### Discovering subdevices +```python3 +device.get_subdevices() +``` + +### Fetching data +Use the DID obtained from get_subdevices() for the input parameter to query specific sub-device. + +```python3 +device.get_state(did="00000000000000000000a043b0d06963") +``` + +### Setting state attributes +The parameters depend on the type of subdevice that is being controlled. In this example, we are controlling LC-1 switches: + +#### Turn on +```python3 +device.set_state(did="00000000000000000000a043b0d0783a", pwr=1) +device.set_state(did="00000000000000000000a043b0d0783a", pwr1=1) +device.set_state(did="00000000000000000000a043b0d0783a", pwr2=1) +``` +#### Turn off +```python3 +device.set_state(did="00000000000000000000a043b0d0783a", pwr=0) +device.set_state(did="00000000000000000000a043b0d0783a", pwr1=0) +device.set_state(did="00000000000000000000a043b0d0783a", pwr2=0) +``` diff --git a/broadlink/__init__.py b/broadlink/__init__.py index db8f3e5f..7711f662 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -9,6 +9,7 @@ from .climate import hysen from .cover import dooya from .device import Device, ping, scan +from .hub import s3 from .light import lb1, lb2 from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro from .sensor import a1 @@ -156,6 +157,10 @@ S1C: { 0x2722: ("S2KIT", "Broadlink"), }, + s3: { + 0xA59C:("S3", "Broadlink"), + 0xA64D:("S3", "Broadlink"), + }, hysen: { 0x4EAD: ("HY02/HY03", "Hysen"), }, diff --git a/broadlink/hub.py b/broadlink/hub.py new file mode 100644 index 00000000..07b02e82 --- /dev/null +++ b/broadlink/hub.py @@ -0,0 +1,83 @@ +"""Support for hubs.""" +import struct +import json + +from . import exceptions as e +from .device import Device + + +class s3(Device): + """Controls a Broadlink S3.""" + + TYPE = "S3" + MAX_SUBDEVICES = 8 + + def get_subdevices(self) -> list: + """Return the lit of sub devices.""" + sub_devices = [] + step = 5 + + for index in range(0, self.MAX_SUBDEVICES, step): + state = {"count": step, "index": index} + packet = self._encode(14, state) + resp = self.send_packet(0x6A, packet) + e.check_error(resp[0x22:0x24]) + resp = self._decode(resp) + + sub_devices.extend(resp["list"]) + if len(sub_devices) == resp["total"]: + break + + return sub_devices + + def get_state(self, did: str = None) -> dict: + """Return the power state of the device.""" + state = {} + if did is not None: + state["did"] = did + + packet = self._encode(1, state) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response) + + def set_state( + self, + did: str = None, + pwr1: bool = None, + pwr2: bool = None, + pwr3: bool = None, + ) -> dict: + """Set the power state of the device.""" + state = {} + if did is not None: + state["did"] = did + if pwr1 is not None: + state["pwr1"] = int(bool(pwr1)) + if pwr2 is not None: + state["pwr2"] = int(bool(pwr2)) + if pwr3 is not None: + state["pwr3"] = int(bool(pwr3)) + + packet = self._encode(2, state) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response) + + def _encode(self, flag: int, state: dict) -> bytes: + """Encode a JSON packet.""" + # flag: 1 for reading, 2 for writing. + packet = bytearray(12) + data = json.dumps(state, separators=(",", ":")).encode() + struct.pack_into(" dict: + """Decode a JSON packet.""" + payload = self.decrypt(response[0x38:]) + js_len = struct.unpack_from(" Date: Mon, 23 May 2022 02:35:28 -0300 Subject: [PATCH 59/82] Merge new product ids into master (#686) * Add support for Broadlink RM4 mini (0x5216) (#671) * Add support for Broadlink RM4 pro 0x520B (#673) * Add support for SP4L-UK 0xA569 (#677) * Fixing typo in rm4pro device definitions (#682) * Add support for Bestcon RM4C pro (0x5218) (#683) * Add support for Broadlink RM4 TV mate (0x5212) (#684) * Add support for Broadlink RM4 mini (0x520C) (#685) --- broadlink/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 7711f662..2f66c427 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -61,6 +61,7 @@ 0x7583: ("SP mini 3", "Broadlink"), 0x7587: ("SP4L-UK", "Broadlink"), 0x7D11: ("SP mini 3", "Broadlink"), + 0xA569: ("SP4L-UK", "Broadlink"), 0xA56A: ("MCB1", "Broadlink"), 0xA56B: ("SCB1E", "Broadlink"), 0xA56C: ("SP4L-EU", "Broadlink"), @@ -113,6 +114,9 @@ rm4mini: { 0x51DA: ("RM4 mini", "Broadlink"), 0x5209: ("RM4 TV mate", "Broadlink"), + 0x520C: ("RM4 mini", "Broadlink"), + 0x5212: ("RM4 TV mate", "Broadlink"), + 0x5216: ("RM4 mini", "Broadlink"), 0x6070: ("RM4C mini", "Broadlink"), 0x610E: ("RM4 mini", "Broadlink"), 0x610F: ("RM4C mini", "Broadlink"), @@ -124,7 +128,9 @@ 0x653A: ("RM4 mini", "Broadlink"), }, rm4pro: { + 0x520B: ("RM4 pro", "Broadlink"), 0x5213: ("RM4 pro", "Broadlink"), + 0x5218: ("RM4C pro", "Broadlink"), 0x6026: ("RM4 pro", "Broadlink"), 0x6184: ("RM4C pro", "Broadlink"), 0x61A2: ("RM4 pro", "Broadlink"), From a86e9cbb9ce44c25485226d46541a1cdcff940b7 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 23 May 2022 02:38:55 -0300 Subject: [PATCH 60/82] Bump version to 0.18.2 (#687) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ac01c085..4ca34c42 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.18.1' +version = '0.18.2' setup( name="broadlink", From 47b324505007f580805f96690160c64051716e77 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 20 Nov 2022 15:37:31 -0300 Subject: [PATCH 61/82] Fix Github actions (#727) * Bump checkout to v3 and setup-python to v4 * Remove unused branches * Fix ubuntu at v20.04 --- .github/workflows/flake8.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml index cb396611..aa09a19c 100644 --- a/.github/workflows/flake8.yaml +++ b/.github/workflows/flake8.yaml @@ -2,20 +2,20 @@ name: Python flake8 on: push: - branches: [ main, master, dev, development ] + branches: [ master, dev ] pull_request: - branches: [ main, master, dev, development ] + branches: [ master, dev ] jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: python-version: [3.6, 3.7, 3.8, 3.9] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 9d5339ab8e0495ca63ac40822ec72882f97348f5 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 20 Nov 2022 15:44:34 -0300 Subject: [PATCH 62/82] Merge new product ids into master (#726) * Add support for RM4C mini (0x520D) (#694) * Add support for SP4L-US (0x648C) (#707) * Add support for RM4C mate (0x5211) (#709) * Add support for RM4 mini (0x521C) (#710) * Add support for LB1 (0x644B) (#717) --- broadlink/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 2f66c427..9a9a44cb 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -76,6 +76,7 @@ 0x618B: ("SP4L-EU", "Broadlink"), 0x6489: ("SP4L-AU", "Broadlink"), 0x648B: ("SP4M-US", "Broadlink"), + 0x648C: ("SP4L-US", "Broadlink"), 0x6494: ("SCB2", "Broadlink"), }, rmmini: { @@ -115,8 +116,11 @@ 0x51DA: ("RM4 mini", "Broadlink"), 0x5209: ("RM4 TV mate", "Broadlink"), 0x520C: ("RM4 mini", "Broadlink"), + 0x520D: ("RM4C mini", "Broadlink"), + 0x5211: ("RM4C mate", "Broadlink"), 0x5212: ("RM4 TV mate", "Broadlink"), 0x5216: ("RM4 mini", "Broadlink"), + 0x521C: ("RM4 mini", "Broadlink"), 0x6070: ("RM4C mini", "Broadlink"), 0x610E: ("RM4 mini", "Broadlink"), 0x610F: ("RM4C mini", "Broadlink"), @@ -153,6 +157,7 @@ 0x60C7: ("LB1", "Broadlink"), 0x60C8: ("LB1", "Broadlink"), 0x6112: ("LB1", "Broadlink"), + 0x644B: ("LB1", "Broadlink"), 0x644C: ("LB27 R1", "Broadlink"), 0x644E: ("LB26 R1", "Broadlink"), }, From 3c183eaaef6cbaf9c1154b232116bc130cd2113f Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 20 Nov 2022 15:48:08 -0300 Subject: [PATCH 63/82] Bump version to 0.18.3 (#728) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4ca34c42..3373f870 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.18.2' +version = '0.18.3' setup( name="broadlink", From abcc9aaeed6723de333516f2f0bff79afb6ac372 Mon Sep 17 00:00:00 2001 From: fustom Date: Sun, 22 Jan 2023 05:50:37 +0100 Subject: [PATCH 64/82] Add heating_cooling state to Hysen (#722) --- broadlink/climate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index eee5f119..15aea900 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -64,6 +64,7 @@ def get_full_status(self) -> dict: data["power"] = payload[4] & 1 data["active"] = (payload[4] >> 4) & 1 data["temp_manual"] = (payload[4] >> 6) & 1 + data["heating_cooling"] = (payload[4] >> 7) & 1 data["room_temp"] = payload[5] / 2.0 data["thermostat_temp"] = payload[6] / 2.0 data["auto_mode"] = payload[7] & 0xF @@ -185,9 +186,11 @@ def set_temp(self, temp: float) -> None: # Set device on(1) or off(0), does not deactivate Wifi connectivity. # Remote lock disables control by buttons on thermostat. - def set_power(self, power: int = 1, remote_lock: int = 0) -> None: + # heating_cooling: heating(0) cooling(1) + def set_power(self, power: int = 1, remote_lock: int = 0, heating_cooling: int = 0) -> None: """Set the power state of the device.""" - self.send_request([0x01, 0x06, 0x00, 0x00, remote_lock, power]) + state = (heating_cooling << 7) + power + self.send_request([0x01, 0x06, 0x00, 0x00, remote_lock, state]) # set time on device # n.b. day=1 is Monday, ..., day=7 is Sunday From 634370d8785d2483eb075221d4d30f0bfb3a27d3 Mon Sep 17 00:00:00 2001 From: Ian Munsie Date: Wed, 10 Apr 2024 04:40:00 +1000 Subject: [PATCH 65/82] Add ability to RF scan a specific frequency (#613) * Add ability to RF scan a specific frequency This adds an optional parameter to find_rf_packet(), along with a corresponding --rflearn parameter (defaulting to 433.92) to broadlink_cli that specifies the frequency to tune to, rather than requiring the frequency be found via sweeping. This is almost mandatory for certain types of remotes that do not repeat their signals while the button is held, and saves significant time when the frequency is known in advance or when many buttons are to be captured in a row. Additionally: - A get_frequency() API is added to return the current frequency the device is tuned to. - A check_frequency_ex() API is added to perform functions of both check_frequency() and get_frequency() in a single call. - broadlink_cli --rfscanlearn will now report the current frequency at 1 second intervals during sweeping, and will report the frequency it finally locks on to. * Clean up remote.py * Clean up broadlink_cli * Update conditional * Fix message --------- Co-authored-by: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> --- broadlink/remote.py | 14 ++++++++++---- cli/broadlink_cli | 46 +++++++++++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/broadlink/remote.py b/broadlink/remote.py index 017dac47..f4db3d2f 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -1,5 +1,6 @@ """Support for universal remotes.""" import struct +import typing as t from . import exceptions as e from .device import Device @@ -46,14 +47,19 @@ def sweep_frequency(self) -> None: """Sweep frequency.""" self._send(0x19) - def check_frequency(self) -> bool: + def check_frequency(self) -> t.Tuple[bool, float]: """Return True if the frequency was identified successfully.""" resp = self._send(0x1A) - return resp[0] == 1 + is_found = bool(resp[0]) + frequency = struct.unpack(" None: + def find_rf_packet(self, frequency: float = None) -> None: """Enter radiofrequency learning mode.""" - self._send(0x1B) + payload = bytearray() + if frequency: + payload += struct.pack(" None: """Cancel sweep frequency.""" diff --git a/cli/broadlink_cli b/cli/broadlink_cli index f7a24ade..1083e596 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -83,7 +83,8 @@ parser.add_argument("--switch", action="store_true", help="switch state from on parser.add_argument("--send", action="store_true", help="send command") parser.add_argument("--sensors", action="store_true", help="check all sensors") parser.add_argument("--learn", action="store_true", help="learn command") -parser.add_argument("--rfscanlearn", action="store_true", help="rf scan learning") +parser.add_argument("--rflearn", action="store_true", help="rf scan learning") +parser.add_argument("--frequency", type=float, help="specify radiofrequency for learning") parser.add_argument("--learnfile", help="save learned command to a specified file") parser.add_argument("--durations", action="store_true", help="use durations in micro seconds instead of the Broadlink format") @@ -127,7 +128,7 @@ if args.send: data = durations_to_broadlink(parse_durations(' '.join(args.data))) \ if args.durations else bytearray.fromhex(''.join(args.data)) dev.send_data(data) -if args.learn or (args.learnfile and not args.rfscanlearn): +if args.learn or (args.learnfile and not args.rflearn): dev.enter_learning() print("Learning...") start = time.time() @@ -195,28 +196,33 @@ if args.switch: else: dev.set_power(True) print('* Switch to ON *') -if args.rfscanlearn: - dev.sweep_frequency() - print("Learning RF Frequency, press and hold the button to learn...") - - start = time.time() - while time.time() - start < TIMEOUT: - time.sleep(1) - if dev.check_frequency(): - break +if args.rflearn: + if args.frequency: + frequency = args.frequency + print("Press the button you want to learn, a short press...") else: - print("RF Frequency not found") - dev.cancel_sweep_frequency() - exit(1) + dev.sweep_frequency() + print("Detecting radiofrequency, press and hold the button to learn...") + + start = time.time() + while time.time() - start < TIMEOUT: + time.sleep(1) + locked, frequency = dev.check_frequency() + if locked: + break + else: + print("Radiofrequency not found") + dev.cancel_sweep_frequency() + exit(1) - print("Found RF Frequency - 1 of 2!") - print("You can now let go of the button") + print("Radiofrequency detected: {}MHz".format(frequency)) + print("You can now let go of the button") - input("Press enter to continue...") + input("Press enter to continue...") - print("To complete learning, single press the button you want to learn") + print("Press the button again, now a short press.") - dev.find_rf_packet() + dev.find_rf_packet(frequency) start = time.time() while time.time() - start < TIMEOUT: @@ -231,7 +237,7 @@ if args.rfscanlearn: print("No data received...") exit(1) - print("Found RF Frequency - 2 of 2!") + print("Packet found!") learned = format_durations(to_microseconds(bytearray(data))) \ if args.durations \ else ''.join(format(x, '02x') for x in bytearray(data)) From d7ed9855b98a74006a5c3309391fc95e6ffe5cc1 Mon Sep 17 00:00:00 2001 From: irsl Date: Tue, 9 Apr 2024 21:06:38 +0200 Subject: [PATCH 66/82] Thermostat: get the 1st decimal place (#772) --- broadlink/climate.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) mode change 100644 => 100755 broadlink/climate.py diff --git a/broadlink/climate.py b/broadlink/climate.py old mode 100644 new mode 100755 index 15aea900..9531268a --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -43,15 +43,28 @@ def send_request(self, request: t.Sequence[int]) -> bytes: return payload[0x02:p_len] + def _room_or_ext_temp_logic(self, payload, base_index): + base_temp = payload[base_index] / 2.0 + add_offset = (payload[4] >> 3) & 1 # should offset be added? + offset_raw_value = (payload[17] >> 4) & 3 # offset value + offset = (offset_raw_value + 1) / 10 if add_offset else 0.0 + return base_temp + offset + + def _room_temp_logic(self, payload): + return self._room_or_ext_temp_logic(payload, 5) + + def _ext_temp_logic(self, payload): + return self._room_or_ext_temp_logic(payload, 18) + def get_temp(self) -> float: """Return the room temperature in degrees celsius.""" payload = self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]) - return payload[0x05] / 2.0 + return self._room_temp_logic(payload) def get_external_temp(self) -> float: """Return the external temperature in degrees celsius.""" payload = self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]) - return payload[18] / 2.0 + return self._ext_temp_logic(payload) def get_full_status(self) -> dict: """Return the state of the device. @@ -65,7 +78,7 @@ def get_full_status(self) -> dict: data["active"] = (payload[4] >> 4) & 1 data["temp_manual"] = (payload[4] >> 6) & 1 data["heating_cooling"] = (payload[4] >> 7) & 1 - data["room_temp"] = payload[5] / 2.0 + data["room_temp"] = self._room_temp_logic(payload) data["thermostat_temp"] = payload[6] / 2.0 data["auto_mode"] = payload[7] & 0xF data["loop_mode"] = payload[7] >> 4 @@ -80,7 +93,7 @@ def get_full_status(self) -> dict: data["fre"] = payload[15] data["poweron"] = payload[16] data["unknown"] = payload[17] - data["external_temp"] = payload[18] / 2.0 + data["external_temp"] = self._ext_temp_logic(payload) data["hour"] = payload[19] data["min"] = payload[20] data["sec"] = payload[21] @@ -187,7 +200,9 @@ def set_temp(self, temp: float) -> None: # Set device on(1) or off(0), does not deactivate Wifi connectivity. # Remote lock disables control by buttons on thermostat. # heating_cooling: heating(0) cooling(1) - def set_power(self, power: int = 1, remote_lock: int = 0, heating_cooling: int = 0) -> None: + def set_power( + self, power: int = 1, remote_lock: int = 0, heating_cooling: int = 0 + ) -> None: """Set the power state of the device.""" state = (heating_cooling << 7) + power self.send_request([0x01, 0x06, 0x00, 0x00, remote_lock, state]) From 06c91ae3943423ebc119a4627fd46efb11d68bfa Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:14:04 -0300 Subject: [PATCH 67/82] Remove auxiliary functions from hysen class (#780) --- broadlink/climate.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/broadlink/climate.py b/broadlink/climate.py index 9531268a..c04e65c0 100755 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -43,28 +43,22 @@ def send_request(self, request: t.Sequence[int]) -> bytes: return payload[0x02:p_len] - def _room_or_ext_temp_logic(self, payload, base_index): + def _decode_temp(self, payload, base_index): base_temp = payload[base_index] / 2.0 add_offset = (payload[4] >> 3) & 1 # should offset be added? offset_raw_value = (payload[17] >> 4) & 3 # offset value offset = (offset_raw_value + 1) / 10 if add_offset else 0.0 return base_temp + offset - def _room_temp_logic(self, payload): - return self._room_or_ext_temp_logic(payload, 5) - - def _ext_temp_logic(self, payload): - return self._room_or_ext_temp_logic(payload, 18) - def get_temp(self) -> float: """Return the room temperature in degrees celsius.""" payload = self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]) - return self._room_temp_logic(payload) + return self._decode_temp(payload, 5) def get_external_temp(self) -> float: """Return the external temperature in degrees celsius.""" payload = self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]) - return self._ext_temp_logic(payload) + return self._decode_temp(payload, 18) def get_full_status(self) -> dict: """Return the state of the device. @@ -78,7 +72,7 @@ def get_full_status(self) -> dict: data["active"] = (payload[4] >> 4) & 1 data["temp_manual"] = (payload[4] >> 6) & 1 data["heating_cooling"] = (payload[4] >> 7) & 1 - data["room_temp"] = self._room_temp_logic(payload) + data["room_temp"] = self._decode_temp(payload, 5) data["thermostat_temp"] = payload[6] / 2.0 data["auto_mode"] = payload[7] & 0xF data["loop_mode"] = payload[7] >> 4 @@ -93,7 +87,7 @@ def get_full_status(self) -> dict: data["fre"] = payload[15] data["poweron"] = payload[16] data["unknown"] = payload[17] - data["external_temp"] = self._ext_temp_logic(payload) + data["external_temp"] = self._decode_temp(payload, 18) data["hour"] = payload[19] data["min"] = payload[20] data["sec"] = payload[21] From 66744707f517593de8276ed550cecae31c204c57 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:26:43 -0300 Subject: [PATCH 68/82] Merge new product ids into master (#781) * Add support for Broadlink SP4L-AU (0xA576) (#731) * Add support for Broadlink RM mini 3 (0x27B7) (#751) * Add support for Broadlink LB27 C1 (0x6488) (#752) * Add support for Broadlink SP mini 3 (0x7549) (#753) --- broadlink/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 9a9a44cb..3a60f772 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -33,6 +33,7 @@ 0x7544: ("SP2-CL", "Broadlink"), 0x7546: ("SP2-UK/BR/IN", "Broadlink (OEM)"), 0x7547: ("SC1", "Broadlink"), + 0x7549: ("SP mini 3", "Broadlink (OEM)"), 0x7918: ("SP2", "Broadlink (OEM)"), 0x7919: ("SP2-compatible", "Honeywell"), 0x791A: ("SP2-compatible", "Honeywell"), @@ -65,6 +66,7 @@ 0xA56A: ("MCB1", "Broadlink"), 0xA56B: ("SCB1E", "Broadlink"), 0xA56C: ("SP4L-EU", "Broadlink"), + 0xA576: ("SP4L-AU", "Broadlink"), 0xA589: ("SP4L-UK", "Broadlink"), 0xA5D3: ("SP4L-EU", "Broadlink"), }, @@ -82,6 +84,7 @@ rmmini: { 0x2737: ("RM mini 3", "Broadlink"), 0x278F: ("RM mini", "Broadlink"), + 0x27B7: ("RM mini 3", "Broadlink"), 0x27C2: ("RM mini 3", "Broadlink"), 0x27C7: ("RM mini 3", "Broadlink"), 0x27CC: ("RM mini 3", "Broadlink"), @@ -158,8 +161,9 @@ 0x60C8: ("LB1", "Broadlink"), 0x6112: ("LB1", "Broadlink"), 0x644B: ("LB1", "Broadlink"), - 0x644C: ("LB27 R1", "Broadlink"), + 0x644C: ("LB27 R1", "Broadlink"), 0x644E: ("LB26 R1", "Broadlink"), + 0x6488: ("LB27 C1", "Broadlink"), }, lb2: { 0xA4F4: ("LB27 R1", "Broadlink"), From c6bf96da470d9c70646492d06f2a1c61c11056fb Mon Sep 17 00:00:00 2001 From: Hozoy Date: Wed, 10 Apr 2024 06:23:35 +0800 Subject: [PATCH 69/82] Add mp1s get_status function (#762) --- broadlink/switch.py | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/broadlink/switch.py b/broadlink/switch.py index a2c15be2..e60f124a 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -367,11 +367,40 @@ class mp1s(mp1): TYPE = "MP1S" - def get_energy(self) -> float: - """Return the power consumption in W.""" - packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) + def get_status(self) -> dict: + """ + Return the power state of the device. + voltage in V. + current in A. + power in W. + power consumption in kW·h. + """ + packet = bytearray(16) + packet[0x00] = 0x0E + packet[0x02] = 0xA5 + packet[0x03] = 0xA5 + packet[0x04] = 0x5A + packet[0x05] = 0x5A + packet[0x06] = 0xB2 + packet[0x07] = 0xC0 + packet[0x08] = 0x01 + packet[0x0A] = 0x04 + response = self.send_packet(0x6A, packet) e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - energy = payload[0x7:0x4:-1].hex() - return int(energy) / 100 + payload_str = payload.hex()[4:-6] + + def get_value(start, end, factors): + value = sum(int(payload_str[i-2:i]) * factor for i, + factor in zip(range(start, end, -2), factors)) + return value + + return { + 'voltage': get_value(34, 30, [10, 0.1]), + 'current': get_value(40, 34, [1, 0.01, 0.0001]), + 'power': get_value(46, 40, [100, 1, 0.01]), + 'power_consumption': get_value(54, 46, [10000, 100, 1, 0.01]) + } + + From cacebe7f3c5d1ea0317cf11930350b9352b96bfa Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 9 Apr 2024 19:43:29 -0300 Subject: [PATCH 70/82] Rename MP1S state parameters (#783) * Rename MP1S state parameters * Rename get_status to get_state --- broadlink/__init__.py | 8 ++++---- broadlink/switch.py | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 596a8ec1..a2bd5eac 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -160,7 +160,7 @@ 0x60C8: ("LB1", "Broadlink"), 0x6112: ("LB1", "Broadlink"), 0x644B: ("LB1", "Broadlink"), - 0x644C: ("LB27 R1", "Broadlink"), + 0x644C: ("LB27 R1", "Broadlink"), 0x644E: ("LB26 R1", "Broadlink"), }, lb2: { @@ -170,9 +170,9 @@ S1C: { 0x2722: ("S2KIT", "Broadlink"), }, - s3: { - 0xA59C:("S3", "Broadlink"), - 0xA64D:("S3", "Broadlink"), + s3: { + 0xA59C: ("S3", "Broadlink"), + 0xA64D: ("S3", "Broadlink"), }, hysen: { 0x4EAD: ("HY02/HY03", "Hysen"), diff --git a/broadlink/switch.py b/broadlink/switch.py index e60f124a..1a94de6b 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -367,9 +367,9 @@ class mp1s(mp1): TYPE = "MP1S" - def get_status(self) -> dict: - """ - Return the power state of the device. + def get_state(self) -> dict: + """Return the power state of the device. + voltage in V. current in A. power in W. @@ -392,15 +392,15 @@ def get_status(self) -> dict: payload_str = payload.hex()[4:-6] def get_value(start, end, factors): - value = sum(int(payload_str[i-2:i]) * factor for i, - factor in zip(range(start, end, -2), factors)) + value = sum( + int(payload_str[i - 2 : i]) * factor + for i, factor in zip(range(start, end, -2), factors) + ) return value - + return { - 'voltage': get_value(34, 30, [10, 0.1]), - 'current': get_value(40, 34, [1, 0.01, 0.0001]), - 'power': get_value(46, 40, [100, 1, 0.01]), - 'power_consumption': get_value(54, 46, [10000, 100, 1, 0.01]) + "volt": get_value(34, 30, [10, 0.1]), + "current": get_value(40, 34, [1, 0.01, 0.0001]), + "power": get_value(46, 40, [100, 1, 0.01]), + "totalconsum": get_value(54, 46, [10000, 100, 1, 0.01]), } - - From 821820c61e16e3daaa3f5ced18916d1236cf5587 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 9 Apr 2024 19:56:30 -0300 Subject: [PATCH 71/82] Add support for BG Electrical EHC31 (0x6480) (#784) --- broadlink/__init__.py | 5 +++- broadlink/switch.py | 56 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a2bd5eac..3f63930d 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -13,7 +13,7 @@ from .light import lb1, lb2 from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro from .sensor import a1 -from .switch import bg1, mp1, mp1s, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b +from .switch import bg1, ehc31, mp1, mp1s, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b SUPPORTED_TYPES = { sp1: { @@ -183,6 +183,9 @@ bg1: { 0x51E3: ("BG800/BG900", "BG Electrical"), }, + ehc31: { + 0x6480: ("EHC31", "BG Electrical"), + }, } diff --git a/broadlink/switch.py b/broadlink/switch.py index 1a94de6b..bf0d7438 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -306,6 +306,62 @@ def _decode(self, response: bytes) -> dict: return state +class ehc31(bg1): + """Controls a BG Electrical smart extension lead.""" + + TYPE = "EHC31" + + def set_state( + self, + pwr: bool = None, + pwr1: bool = None, + pwr2: bool = None, + pwr3: bool = None, + maxworktime1: int = None, + maxworktime2: int = None, + maxworktime3: int = None, + idcbrightness: int = None, + childlock: bool = None, + childlock1: bool = None, + childlock2: bool = None, + childlock3: bool = None, + childlock4: bool = None, + ) -> dict: + """Set the power state of the device.""" + state = {} + if pwr is not None: + state["pwr"] = int(bool(pwr)) + if pwr1 is not None: + state["pwr1"] = int(bool(pwr1)) + if pwr2 is not None: + state["pwr2"] = int(bool(pwr2)) + if pwr3 is not None: + state["pwr3"] = int(bool(pwr3)) + if maxworktime1 is not None: + state["maxworktime1"] = maxworktime1 + if maxworktime2 is not None: + state["maxworktime2"] = maxworktime2 + if maxworktime3 is not None: + state["maxworktime3"] = maxworktime3 + if idcbrightness is not None: + state["idcbrightness"] = idcbrightness + if childlock is not None: + state["childlock"] = int(bool(childlock)) + if childlock1 is not None: + state["childlock1"] = int(bool(childlock1)) + if childlock2 is not None: + state["childlock2"] = int(bool(childlock2)) + if childlock3 is not None: + state["childlock3"] = int(bool(childlock3)) + if childlock4 is not None: + state["childlock4"] = int(bool(childlock4)) + + packet = self._encode(2, state) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response) + + class mp1(Device): """Controls a Broadlink MP1.""" From 4766d68289c1bfeab26378d0620646bfca662223 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 9 Apr 2024 20:32:41 -0300 Subject: [PATCH 72/82] Add support for Dooya DT360E (v2) (#785) --- broadlink/__init__.py | 5 +++- broadlink/cover.py | 62 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 3f63930d..3bfff8e9 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -7,7 +7,7 @@ from .const import DEFAULT_BCAST_ADDR, DEFAULT_PORT, DEFAULT_TIMEOUT from .alarm import S1C from .climate import hysen -from .cover import dooya +from .cover import dooya, dooya2 from .device import Device, ping, scan from .hub import s3 from .light import lb1, lb2 @@ -180,6 +180,9 @@ dooya: { 0x4E4D: ("DT360E-45/20", "Dooya"), }, + dooya2: { + 0x4F6E: ("DT360E-45/20", "Dooya"), + }, bg1: { 0x51E3: ("BG800/BG900", "BG Electrical"), }, diff --git a/broadlink/cover.py b/broadlink/cover.py index c0f08abb..1889c5c3 100644 --- a/broadlink/cover.py +++ b/broadlink/cover.py @@ -8,15 +8,15 @@ class dooya(Device): """Controls a Dooya curtain motor.""" - TYPE = "Dooya DT360E" + TYPE = "DT360E" - def _send(self, magic1: int, magic2: int) -> int: + def _send(self, command: int, attribute: int = 0) -> int: """Send a packet to the device.""" packet = bytearray(16) packet[0] = 0x09 packet[2] = 0xBB - packet[3] = magic1 - packet[4] = magic2 + packet[3] = command + packet[4] = attribute packet[9] = 0xFA packet[10] = 0x44 response = self.send_packet(0x6A, packet) @@ -26,15 +26,15 @@ def _send(self, magic1: int, magic2: int) -> int: def open(self) -> int: """Open the curtain.""" - return self._send(0x01, 0x00) + return self._send(0x01) def close(self) -> int: """Close the curtain.""" - return self._send(0x02, 0x00) + return self._send(0x02) def stop(self) -> int: """Stop the curtain.""" - return self._send(0x03, 0x00) + return self._send(0x03) def get_percentage(self) -> int: """Return the position of the curtain.""" @@ -55,3 +55,51 @@ def set_percentage_and_wait(self, new_percentage: int) -> None: time.sleep(0.2) current = self.get_percentage() self.stop() + + +class dooya2(Device): + """Controls a Dooya curtain motor (version 2).""" + + TYPE = "DT360E-2" + + def _send(self, command: int, attribute: int = 0) -> int: + """Send a packet to the device.""" + checksum = 0xC0C4 + command + attribute & 0xFFFF + packet = bytearray(32) + packet[0] = 0x16 + packet[2] = 0xA5 + packet[3] = 0xA5 + packet[4] = 0x5A + packet[5] = 0x5A + packet[6] = checksum & 0xFF + packet[7] = checksum >> 8 + packet[8] = 0x02 + packet[9] = 0x0B + packet[10] = 0x0A + packet[15] = command + packet[16] = attribute + + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return payload[0x11] + + def open(self) -> None: + """Open the curtain.""" + self._send(0x01) + + def close(self) -> None: + """Close the curtain.""" + self._send(0x02) + + def stop(self) -> None: + """Stop the curtain.""" + self._send(0x03) + + def get_percentage(self) -> int: + """Return the position of the curtain.""" + return self._send(0x06) + + def set_percentage(self, new_percentage: int) -> None: + """Set the position of the curtain.""" + self._send(0x09, new_percentage) From 84af992dccc73b03bf179ff5e0790f272ae3323d Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:35:25 -0300 Subject: [PATCH 73/82] Add support for Wistar smart curtain (0x4F6C) (#786) * Add support for Wistar smart curtain (0x4F6C) * Rename wsrc to wser --- broadlink/__init__.py | 5 +- broadlink/cover.py | 138 ++++++++++++++++++++++++++++++++---------- 2 files changed, 110 insertions(+), 33 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 3bfff8e9..070f06db 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -7,7 +7,7 @@ from .const import DEFAULT_BCAST_ADDR, DEFAULT_PORT, DEFAULT_TIMEOUT from .alarm import S1C from .climate import hysen -from .cover import dooya, dooya2 +from .cover import dooya, dooya2, wser from .device import Device, ping, scan from .hub import s3 from .light import lb1, lb2 @@ -183,6 +183,9 @@ dooya2: { 0x4F6E: ("DT360E-45/20", "Dooya"), }, + wser: { + 0x4F6C: ("WSER", "Wistar"), + }, bg1: { 0x51E3: ("BG800/BG900", "BG Electrical"), }, diff --git a/broadlink/cover.py b/broadlink/cover.py index 1889c5c3..23727a86 100644 --- a/broadlink/cover.py +++ b/broadlink/cover.py @@ -13,15 +13,16 @@ class dooya(Device): def _send(self, command: int, attribute: int = 0) -> int: """Send a packet to the device.""" packet = bytearray(16) - packet[0] = 0x09 - packet[2] = 0xBB - packet[3] = command - packet[4] = attribute - packet[9] = 0xFA - packet[10] = 0x44 - response = self.send_packet(0x6A, packet) - e.check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) + packet[0x00] = 0x09 + packet[0x02] = 0xBB + packet[0x03] = command + packet[0x04] = attribute + packet[0x09] = 0xFA + packet[0x0A] = 0x44 + + resp = self.send_packet(0x6A, packet) + e.check_error(resp[0x22:0x24]) + payload = self.decrypt(resp[0x38:]) return payload[4] def open(self) -> int: @@ -62,44 +63,117 @@ class dooya2(Device): TYPE = "DT360E-2" - def _send(self, command: int, attribute: int = 0) -> int: - """Send a packet to the device.""" - checksum = 0xC0C4 + command + attribute & 0xFFFF - packet = bytearray(32) - packet[0] = 0x16 - packet[2] = 0xA5 - packet[3] = 0xA5 - packet[4] = 0x5A - packet[5] = 0x5A + def _send(self, operation: int, data: bytes): + """Send a command to the device.""" + packet = bytearray(14) + packet[0x02] = 0xA5 + packet[0x03] = 0xA5 + packet[0x04] = 0x5A + packet[0x05] = 0x5A + packet[0x08] = operation + packet[0x09] = 0x0B + + data_len = len(data) + packet[0x0A] = data_len & 0xFF + packet[0x0B] = data_len >> 8 + + packet += bytes(data) + + checksum = sum(packet, 0xBEAF) & 0xFFFF packet[6] = checksum & 0xFF packet[7] = checksum >> 8 - packet[8] = 0x02 - packet[9] = 0x0B - packet[10] = 0x0A - packet[15] = command - packet[16] = attribute - response = self.send_packet(0x6A, packet) - e.check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) - return payload[0x11] + packet_len = len(packet) - 2 + packet[0] = packet_len & 0xFF + packet[1] = packet_len >> 8 + + resp = self.send_packet(0x6a, packet) + e.check_error(resp[0x22:0x24]) + payload = self.decrypt(resp[0x38:]) + return payload def open(self) -> None: """Open the curtain.""" - self._send(0x01) + self._send(2, [0x00, 0x01, 0x00]) def close(self) -> None: """Close the curtain.""" - self._send(0x02) + self._send(2, [0x00, 0x02, 0x00]) def stop(self) -> None: """Stop the curtain.""" - self._send(0x03) + self._send(2, [0x00, 0x03, 0x00]) def get_percentage(self) -> int: """Return the position of the curtain.""" - return self._send(0x06) + resp = self._send(1, [0x00, 0x06, 0x00]) + return resp[0x11] def set_percentage(self, new_percentage: int) -> None: """Set the position of the curtain.""" - self._send(0x09, new_percentage) + self._send(2, [0x00, 0x09, new_percentage]) + + +class wser(Device): + """Controls a Wistar curtain motor""" + + TYPE = "WSER" + + def _send(self, operation: int, data: bytes): + """Send a command to the device.""" + packet = bytearray(14) + packet[0x02] = 0xA5 + packet[0x03] = 0xA5 + packet[0x04] = 0x5A + packet[0x05] = 0x5A + packet[0x08] = operation + packet[0x09] = 0x0B + + data_len = len(data) + packet[0x0A] = data_len & 0xFF + packet[0x0B] = data_len >> 8 + + packet += bytes(data) + + checksum = sum(packet, 0xBEAF) & 0xFFFF + packet[6] = checksum & 0xFF + packet[7] = checksum >> 8 + + packet_len = len(packet) - 2 + packet[0] = packet_len & 0xFF + packet[1] = packet_len >> 8 + + resp = self.send_packet(0x6a, packet) + e.check_error(resp[0x22:0x24]) + payload = self.decrypt(resp[0x38:]) + return payload + + def get_position(self) -> int: + """Return the position of the curtain.""" + resp = self._send(1, []) + position = resp[0x0E] + return position + + def open(self) -> int: + """Open the curtain.""" + resp = self._send(2, [0x4a, 0x31, 0xa0]) + position = resp[0x0E] + return position + + def close(self) -> int: + """Close the curtain.""" + resp = self._send(2, [0x61, 0x32, 0xa0]) + position = resp[0x0E] + return position + + def stop(self) -> int: + """Stop the curtain.""" + resp = self._send(2, [0x4c, 0x73, 0xa0]) + position = resp[0x0E] + return position + + def set_position(self, position: int) -> int: + """Set the position of the curtain.""" + resp = self._send(2, [position, 0x70, 0xa0]) + position = resp[0x0E] + return position From 247be74c33b533b42a14be98179d9f8b5293d385 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:51:41 -0300 Subject: [PATCH 74/82] Expose IR/RF conversion functions (#788) * Move IR duration<->Broadlink conversion down from CLI * Fix --learn base64 to not crash with --durations Also remove its b'...' wrapping. * Fix IR/RF conversions --------- Co-authored-by: William Grant --- broadlink/remote.py | 41 +++++++++++++++++ cli/broadlink_cli | 106 ++++++++++++++++---------------------------- 2 files changed, 79 insertions(+), 68 deletions(-) diff --git a/broadlink/remote.py b/broadlink/remote.py index f4db3d2f..89cb71f5 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -6,6 +6,47 @@ from .device import Device +def pulses_to_data(pulses: t.List[int], tick: int = 32.84) -> None: + """Convert a microsecond duration sequence into a Broadlink IR packet.""" + result = bytearray(4) + result[0x00] = 0x26 + + for pulse in pulses: + div, mod = divmod(int(pulse // tick), 256) + if div: + result.append(0) + result.append(div) + result.append(mod) + + data_len = len(result) - 4 + result[0x02] = data_len & 0xFF + result[0x03] = data_len >> 8 + + return result + + +def data_to_pulses(data: bytes, tick: int = 32.84) -> t.List[int]: + """Parse a Broadlink packet into a microsecond duration sequence.""" + result = [] + index = 4 + end = min(256 * data[0x03] + data[0x02] + 4, len(data)) + + while index < end: + chunk = data[index] + index += 1 + + if chunk == 0: + try: + chunk = 256 * data[index] + data[index + 1] + except IndexError: + raise ValueError("Malformed data.") + index += 2 + + result.append(int(chunk * tick)) + + return result + + class rmmini(Device): """Controls a Broadlink RM mini 3.""" diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 1083e596..35317ee4 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -1,68 +1,32 @@ #!/usr/bin/env python3 import argparse import base64 -import codecs import time +import typing as t import broadlink from broadlink.const import DEFAULT_PORT from broadlink.exceptions import ReadError, StorageError +from broadlink.remote import data_to_pulses, pulses_to_data -TICK = 32.84 TIMEOUT = 30 -IR_TOKEN = 0x26 def auto_int(x): return int(x, 0) -def to_microseconds(bytes): - result = [] - # print bytes[0] # 0x26 = 38for IR - index = 4 - while index < len(bytes): - chunk = bytes[index] - index += 1 - if chunk == 0: - chunk = bytes[index] - chunk = 256 * chunk + bytes[index + 1] - index += 2 - result.append(int(round(chunk * TICK))) - if chunk == 0x0d05: - break - return result - - -def durations_to_broadlink(durations): - result = bytearray() - result.append(IR_TOKEN) - result.append(0) - result.append(len(durations) % 256) - result.append(len(durations) / 256) - for dur in durations: - num = int(round(dur / TICK)) - if num > 255: - result.append(0) - result.append(num / 256) - result.append(num % 256) - return result +def format_pulses(pulses: t.List[int]) -> str: + """Format pulses.""" + return " ".join( + f"+{pulse}" if i % 2 == 0 else f"-{pulse}" + for i, pulse in enumerate(pulses) + ) -def format_durations(data): - result = '' - for i in range(0, len(data)): - if len(result) > 0: - result += ' ' - result += ('+' if i % 2 == 0 else '-') + str(data[i]) - return result - - -def parse_durations(str): - result = [] - for s in str.split(): - result.append(abs(int(s))) - return result +def parse_pulses(data: t.List[str]) -> t.List[int]: + """Parse pulses.""" + return [abs(int(s)) for s in data] parser = argparse.ArgumentParser(fromfile_prefix_chars='@') @@ -112,8 +76,8 @@ if args.joinwifi: if args.convert: data = bytearray.fromhex(''.join(args.data)) - durations = to_microseconds(data) - print(format_durations(durations)) + pulses = data_to_pulses(data) + print(format_pulses(pulses)) if args.temperature: print(dev.check_temperature()) if args.humidity: @@ -125,8 +89,11 @@ if args.sensors: for key in data: print("{} {}".format(key, data[key])) if args.send: - data = durations_to_broadlink(parse_durations(' '.join(args.data))) \ - if args.durations else bytearray.fromhex(''.join(args.data)) + data = ( + pulses_to_data(parse_pulses(args.data)) + if args.durations + else bytes.fromhex(''.join(args.data)) + ) dev.send_data(data) if args.learn or (args.learnfile and not args.rflearn): dev.enter_learning() @@ -144,17 +111,19 @@ if args.learn or (args.learnfile and not args.rflearn): print("No data received...") exit(1) - learned = format_durations(to_microseconds(bytearray(data))) \ - if args.durations \ - else ''.join(format(x, '02x') for x in bytearray(data)) - if args.learn: - print(learned) - decode_hex = codecs.getdecoder("hex_codec") - print("Base64: " + str(base64.b64encode(decode_hex(learned)[0]))) + print("Packet found!") + raw_fmt = data.hex() + base64_fmt = base64.b64encode(data).decode('ascii') + pulse_fmt = format_pulses(data_to_pulses(data)) + + print("Raw:", raw_fmt) + print("Base64:", base64_fmt) + print("Pulses:", pulse_fmt) + if args.learnfile: print("Saving to {}".format(args.learnfile)) with open(args.learnfile, "w") as text_file: - text_file.write(learned) + text_file.write(pulse_fmt if args.durations else raw_fmt) if args.check: if dev.check_power(): print('* ON *') @@ -238,14 +207,15 @@ if args.rflearn: exit(1) print("Packet found!") - learned = format_durations(to_microseconds(bytearray(data))) \ - if args.durations \ - else ''.join(format(x, '02x') for x in bytearray(data)) - if args.learnfile is None: - print(learned) - decode_hex = codecs.getdecoder("hex_codec") - print("Base64: {}".format(str(base64.b64encode(decode_hex(learned)[0])))) - if args.learnfile is not None: + raw_fmt = data.hex() + base64_fmt = base64.b64encode(data).decode('ascii') + pulse_fmt = format_pulses(data_to_pulses(data)) + + print("Raw:", raw_fmt) + print("Base64:", base64_fmt) + print("Pulses:", pulse_fmt) + + if args.learnfile: print("Saving to {}".format(args.learnfile)) with open(args.learnfile, "w") as text_file: - text_file.write(learned) + text_file.write(pulse_fmt if args.durations else raw_fmt) From eb0f98a410990022156dad3f58d257141224e88a Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 10 Apr 2024 23:15:46 -0300 Subject: [PATCH 75/82] Fix README.md (#789) --- cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/README.md b/cli/README.md index 7b40b4cf..b7e48dc9 100644 --- a/cli/README.md +++ b/cli/README.md @@ -97,7 +97,7 @@ broadlink_cli --device @BEDROOM.device --temperature #### Check humidity ``` -broadlink_cli --device @BEDROOM.device --temperature +broadlink_cli --device @BEDROOM.device --humidity ``` ### Smart plugs From 24b9d308b6a6a27be452564ad47075edee50651c Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 10 Apr 2024 23:55:41 -0300 Subject: [PATCH 76/82] Fix s3.get_subdevices() (#790) * Fix s3.get_subdevices() * Fix docstring --- broadlink/hub.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/broadlink/hub.py b/broadlink/hub.py index 07b02e82..cb24dc8d 100644 --- a/broadlink/hub.py +++ b/broadlink/hub.py @@ -12,22 +12,34 @@ class s3(Device): TYPE = "S3" MAX_SUBDEVICES = 8 - def get_subdevices(self) -> list: - """Return the lit of sub devices.""" + def get_subdevices(self, step: int = 5) -> list: + """Return a list of sub devices.""" + total = self.MAX_SUBDEVICES sub_devices = [] - step = 5 + seen = set() + index = 0 - for index in range(0, self.MAX_SUBDEVICES, step): + while index < total: state = {"count": step, "index": index} packet = self._encode(14, state) resp = self.send_packet(0x6A, packet) e.check_error(resp[0x22:0x24]) resp = self._decode(resp) - sub_devices.extend(resp["list"]) - if len(sub_devices) == resp["total"]: + for device in resp["list"]: + did = device["did"] + if did in seen: + continue + + seen.add(did) + sub_devices.append(device) + + total = resp["total"] + if len(seen) >= total: break + index += step + return sub_devices def get_state(self, did: str = None) -> dict: From fa44b54d88a73c2a5fdc320d6fd0528587a2e84b Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 12 Apr 2024 02:10:06 -0300 Subject: [PATCH 77/82] Add support for Broadlink A2 (#791) * Add support for Broadlink A2 * Add supported type * Fix bugs * Improve device name --- broadlink/__init__.py | 7 +++-- broadlink/cover.py | 58 ++++++++++++++++++------------------ broadlink/sensor.py | 69 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 90 insertions(+), 44 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 070f06db..1d4ffb2a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -12,7 +12,7 @@ from .hub import s3 from .light import lb1, lb2 from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro -from .sensor import a1 +from .sensor import a1, a2 from .switch import bg1, ehc31, mp1, mp1s, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b SUPPORTED_TYPES = { @@ -142,7 +142,10 @@ 0x653C: ("RM4 pro", "Broadlink"), }, a1: { - 0x2714: ("e-Sensor", "Broadlink"), + 0x2714: ("A1", "Broadlink"), + }, + a2: { + 0x4F60: ("A2", "Broadlink"), }, mp1: { 0x4EB5: ("MP1-1K4S", "Broadlink"), diff --git a/broadlink/cover.py b/broadlink/cover.py index 23727a86..1d8b41ef 100644 --- a/broadlink/cover.py +++ b/broadlink/cover.py @@ -63,31 +63,32 @@ class dooya2(Device): TYPE = "DT360E-2" - def _send(self, operation: int, data: bytes): + def _send(self, operation: int, data: bytes = b""): """Send a command to the device.""" - packet = bytearray(14) + packet = bytearray(12) packet[0x02] = 0xA5 packet[0x03] = 0xA5 packet[0x04] = 0x5A packet[0x05] = 0x5A packet[0x08] = operation packet[0x09] = 0x0B - - data_len = len(data) - packet[0x0A] = data_len & 0xFF - packet[0x0B] = data_len >> 8 - packet += bytes(data) + if data: + data_len = len(data) + packet[0x0A] = data_len & 0xFF + packet[0x0B] = data_len >> 8 + packet += bytes(2) + packet.extend(data) checksum = sum(packet, 0xBEAF) & 0xFFFF - packet[6] = checksum & 0xFF - packet[7] = checksum >> 8 + packet[0x06] = checksum & 0xFF + packet[0x07] = checksum >> 8 packet_len = len(packet) - 2 - packet[0] = packet_len & 0xFF - packet[1] = packet_len >> 8 + packet[0x00] = packet_len & 0xFF + packet[0x01] = packet_len >> 8 - resp = self.send_packet(0x6a, packet) + resp = self.send_packet(0x6A, packet) e.check_error(resp[0x22:0x24]) payload = self.decrypt(resp[0x38:]) return payload @@ -119,31 +120,32 @@ class wser(Device): TYPE = "WSER" - def _send(self, operation: int, data: bytes): + def _send(self, operation: int, data: bytes = b""): """Send a command to the device.""" - packet = bytearray(14) + packet = bytearray(12) packet[0x02] = 0xA5 packet[0x03] = 0xA5 packet[0x04] = 0x5A packet[0x05] = 0x5A packet[0x08] = operation packet[0x09] = 0x0B - - data_len = len(data) - packet[0x0A] = data_len & 0xFF - packet[0x0B] = data_len >> 8 - packet += bytes(data) + if data: + data_len = len(data) + packet[0x0A] = data_len & 0xFF + packet[0x0B] = data_len >> 8 + packet += bytes(2) + packet.extend(data) checksum = sum(packet, 0xBEAF) & 0xFFFF - packet[6] = checksum & 0xFF - packet[7] = checksum >> 8 + packet[0x06] = checksum & 0xFF + packet[0x07] = checksum >> 8 packet_len = len(packet) - 2 - packet[0] = packet_len & 0xFF - packet[1] = packet_len >> 8 + packet[0x00] = packet_len & 0xFF + packet[0x01] = packet_len >> 8 - resp = self.send_packet(0x6a, packet) + resp = self.send_packet(0x6A, packet) e.check_error(resp[0x22:0x24]) payload = self.decrypt(resp[0x38:]) return payload @@ -156,24 +158,24 @@ def get_position(self) -> int: def open(self) -> int: """Open the curtain.""" - resp = self._send(2, [0x4a, 0x31, 0xa0]) + resp = self._send(2, [0x4A, 0x31, 0xA0]) position = resp[0x0E] return position def close(self) -> int: """Close the curtain.""" - resp = self._send(2, [0x61, 0x32, 0xa0]) + resp = self._send(2, [0x61, 0x32, 0xA0]) position = resp[0x0E] return position def stop(self) -> int: """Stop the curtain.""" - resp = self._send(2, [0x4c, 0x73, 0xa0]) + resp = self._send(2, [0x4C, 0x73, 0xA0]) position = resp[0x0E] return position def set_position(self, position: int) -> int: """Set the position of the curtain.""" - resp = self._send(2, [position, 0x70, 0xa0]) + resp = self._send(2, [position, 0x70, 0xA0]) position = resp[0x0E] return position diff --git a/broadlink/sensor.py b/broadlink/sensor.py index 33e7587d..21bae0b9 100644 --- a/broadlink/sensor.py +++ b/broadlink/sensor.py @@ -1,6 +1,4 @@ """Support for sensors.""" -import struct - from . import exceptions as e from .device import Device @@ -29,19 +27,62 @@ def check_sensors(self) -> dict: def check_sensors_raw(self) -> dict: """Return the state of the sensors in raw format.""" packet = bytearray([0x1]) - response = self.send_packet(0x6A, packet) - e.check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) - data = payload[0x4:] + resp = self.send_packet(0x6A, packet) + e.check_error(resp[0x22:0x24]) + data = self.decrypt(resp[0x38:]) + + return { + "temperature": data[0x04] + data[0x05] / 10.0, + "humidity": data[0x06] + data[0x07] / 10.0, + "light": data[0x08], + "air_quality": data[0x0A], + "noise": data[0x0C], + } + + +class a2(Device): + """Controls a Broadlink A2.""" + + TYPE = "A2" - temperature = struct.unpack("> 8 + packet += bytes(2) + packet.extend(data) + + checksum = sum(packet, 0xBEAF) & 0xFFFF + packet[0x06] = checksum & 0xFF + packet[0x07] = checksum >> 8 + + packet_len = len(packet) - 2 + packet[0x00] = packet_len & 0xFF + packet[0x01] = packet_len >> 8 + + resp = self.send_packet(0x6A, packet) + e.check_error(resp[0x22:0x24]) + payload = self.decrypt(resp[0x38:]) + return payload + + def check_sensors_raw(self) -> dict: + """Return the state of the sensors in raw format.""" + data = self._send(1) return { - "temperature": temperature, - "humidity": humidity, - "light": data[0x4], - "air_quality": data[0x6], - "noise": data[0x8], + "temperature": data[0x13] * 256 + data[0x14], + "humidity": data[0x15] * 256 + data[0x16], + "pm10": data[0x0D] * 256 + data[0x0E], + "pm2_5": data[0x0F] * 256 + data[0x10], + "pm1": data[0x11] * 256 + data[0x12], } From 1e115586136acb937853aa0384c7288abd82455b Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 17 Apr 2024 02:06:36 -0300 Subject: [PATCH 78/82] Add support for Tornado 16X SQ air conditioner (0x4E2A) (#520) * Add support for Tornado 16X SQ air conditioner * Make Tornado a generic HVAC class * Better names * Clean up IntEnums * Clean up encoders * Fix indexes * Improve set_state() interface * Enumerate presets * Rename state to power in get_ac_info() * Paint it black * Use CRC16 helper class * Remove log messages * Fix bugs * Return state in set_state() --- broadlink/__init__.py | 13 ++- broadlink/climate.py | 262 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 258 insertions(+), 17 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 1d4ffb2a..8af3e4c6 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -6,7 +6,7 @@ from . import exceptions as e from .const import DEFAULT_BCAST_ADDR, DEFAULT_PORT, DEFAULT_TIMEOUT from .alarm import S1C -from .climate import hysen +from .climate import hvac, hysen from .cover import dooya, dooya2, wser from .device import Device, ping, scan from .hub import s3 @@ -177,6 +177,9 @@ 0xA59C: ("S3", "Broadlink"), 0xA64D: ("S3", "Broadlink"), }, + hvac: { + 0x4E2A: ("HVAC", "Licensed manufacturer"), + }, hysen: { 0x4EAD: ("HY02/HY03", "Hysen"), }, @@ -258,7 +261,9 @@ def discover( discover_ip_port: int = DEFAULT_PORT, ) -> t.List[Device]: """Discover devices connected to the local network.""" - responses = scan(timeout, local_ip_address, discover_ip_address, discover_ip_port) + responses = scan( + timeout, local_ip_address, discover_ip_address, discover_ip_port + ) return [gendevice(*resp) for resp in responses] @@ -272,7 +277,9 @@ def xdiscover( This function returns a generator that yields devices instantly. """ - responses = scan(timeout, local_ip_address, discover_ip_address, discover_ip_port) + responses = scan( + timeout, local_ip_address, discover_ip_address, discover_ip_port + ) for resp in responses: yield gendevice(*resp) diff --git a/broadlink/climate.py b/broadlink/climate.py index c04e65c0..1a0c6006 100755 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -1,5 +1,7 @@ -"""Support for HVAC units.""" -import typing as t +"""Support for climate control.""" +import enum +import struct +from typing import List, Sequence from . import exceptions as e from .device import Device @@ -19,7 +21,7 @@ class hysen(Device): TYPE = "HYS" - def send_request(self, request: t.Sequence[int]) -> bytes: + def send_request(self, request: Sequence[int]) -> bytes: """Send a request to the device.""" packet = bytearray() packet.extend((len(request) + 2).to_bytes(2, "little")) @@ -31,15 +33,15 @@ def send_request(self, request: t.Sequence[int]) -> bytes: payload = self.decrypt(response[0x38:]) p_len = int.from_bytes(payload[:0x02], "little") - if p_len + 2 > len(payload): - raise ValueError( - "hysen_response_error", "first byte of response is not length" - ) - - nom_crc = int.from_bytes(payload[p_len : p_len + 2], "little") + nom_crc = int.from_bytes(payload[p_len:p_len+2], "little") real_crc = CRC16.calculate(payload[0x02:p_len]) + if nom_crc != real_crc: - raise ValueError("hysen_response_error", "CRC check on response failed") + raise e.DataValidationError( + -4008, + "Received data packet check error", + f"Expected a checksum of {nom_crc} and received {real_crc}", + ) return payload[0x02:p_len] @@ -74,7 +76,7 @@ def get_full_status(self) -> dict: data["heating_cooling"] = (payload[4] >> 7) & 1 data["room_temp"] = self._decode_temp(payload, 5) data["thermostat_temp"] = payload[6] / 2.0 - data["auto_mode"] = payload[7] & 0xF + data["auto_mode"] = payload[7] & 0x0F data["loop_mode"] = payload[7] >> 4 data["sensor"] = payload[8] data["osv"] = payload[9] @@ -125,7 +127,9 @@ def get_full_status(self) -> dict: # E.g. loop_mode = 0 ("12345,67") means Saturday and Sunday (weekend schedule) # loop_mode = 2 ("1234567") means every day, including Saturday and Sunday (weekday schedule) # The sensor command is currently experimental - def set_mode(self, auto_mode: int, loop_mode: int, sensor: int = 0) -> None: + def set_mode( + self, auto_mode: int, loop_mode: int, sensor: int = 0 + ) -> None: """Set the mode of the device.""" mode_byte = ((loop_mode + 1) << 4) + auto_mode self.send_request([0x01, 0x06, 0x00, 0x02, mode_byte, sensor]) @@ -206,7 +210,19 @@ def set_power( def set_time(self, hour: int, minute: int, second: int, day: int) -> None: """Set the time.""" self.send_request( - [0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day] + [ + 0x01, + 0x10, + 0x00, + 0x08, + 0x00, + 0x02, + 0x04, + hour, + minute, + second, + day + ] ) # Set timer schedule @@ -215,7 +231,7 @@ def set_time(self, hour: int, minute: int, second: int, day: int) -> None: # {'start_hour':17, 'start_minute':30, 'temp': 22 } # Each one specifies the thermostat temp that will become effective at start_hour:start_minute # weekend is similar but only has 2 (e.g. switch on in morning and off in afternoon) - def set_schedule(self, weekday: t.List[dict], weekend: t.List[dict]) -> None: + def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: """Set timer schedule.""" request = [0x01, 0x10, 0x00, 0x0A, 0x00, 0x0C, 0x18] @@ -238,3 +254,221 @@ def set_schedule(self, weekday: t.List[dict], weekend: t.List[dict]) -> None: request.append(int(weekend[i]["temp"] * 2)) self.send_request(request) + + +class hvac(Device): + """Controls a HVAC. + + Supported models: + - Tornado SMART X SQ series + - Aux ASW-H12U3/JIR1DI-US + - Aux ASW-H36U2/LFR1DI-US + """ + + TYPE = "HVAC" + + @enum.unique + class Mode(enum.IntEnum): + """Enumerates modes.""" + + AUTO = 0 + COOL = 1 + DRY = 2 + HEAT = 3 + FAN = 4 + + @enum.unique + class Speed(enum.IntEnum): + """Enumerates fan speed.""" + + HIGH = 1 + MID = 2 + LOW = 3 + AUTO = 5 + + @enum.unique + class Preset(enum.IntEnum): + """Enumerates presets.""" + + NORMAL = 0 + TURBO = 1 + MUTE = 2 + + @enum.unique + class SwHoriz(enum.IntEnum): + """Enumerates horizontal swing.""" + + ON = 0 + OFF = 7 + + @enum.unique + class SwVert(enum.IntEnum): + """Enumerates vertical swing.""" + + ON = 0 + POS1 = 1 + POS2 = 2 + POS3 = 3 + POS4 = 4 + POS5 = 5 + OFF = 7 + + def _encode(self, data: bytes) -> bytes: + """Encode data for transport.""" + packet = bytearray(10) + p_len = 10 + len(data) + struct.pack_into( + " bytes: + """Decode data from transport.""" + # payload[0x2:0x8] == bytes([0xbb, 0x00, 0x07, 0x00, 0x00, 0x00]) + payload = self.decrypt(response[0x38:]) + p_len = int.from_bytes(payload[:0x02], "little") + nom_crc = int.from_bytes(payload[p_len:p_len+2], "little") + real_crc = CRC16.calculate(payload[0x02:p_len], polynomial=0x9BE4) + + if nom_crc != real_crc: + raise e.DataValidationError( + -4008, + "Received data packet check error", + f"Expected a checksum of {nom_crc} and received {real_crc}", + ) + + d_len = int.from_bytes(payload[0x08:0x0A], "little") + return payload[0x0A:0x0A+d_len] + + def _send(self, command: int, data: bytes = b"") -> bytes: + """Send a command to the unit.""" + prefix = bytes([((command << 4) | 1), 1]) + packet = self._encode(prefix + data) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response)[0x02:] + + def _parse_state(self, data: bytes) -> dict: + """Parse state.""" + state = {} + state["power"] = bool(data[0x08] & 1 << 5) + state["target_temp"] = 8 + (data[0x00] >> 3) + (data[0x04] >> 7) * 0.5 + state["swing_v"] = self.SwVert(data[0x00] & 0b111) + state["swing_h"] = self.SwHoriz(data[0x01] >> 5) + state["mode"] = self.Mode(data[0x05] >> 5) + state["speed"] = self.Speed(data[0x03] >> 5) + state["preset"] = self.Preset(data[0x04] >> 6) + state["sleep"] = bool(data[0x05] & 1 << 2) + state["ifeel"] = bool(data[0x05] & 1 << 3) + state["health"] = bool(data[0x08] & 1 << 1) + state["clean"] = bool(data[0x08] & 1 << 2) + state["display"] = bool(data[0x0A] & 1 << 4) + state["mildew"] = bool(data[0x0A] & 1 << 3) + return state + + def set_state( + self, + power: bool, + target_temp: float, # 16<=target_temp<=32 + mode: Mode, + speed: Speed, + preset: Preset, + swing_h: SwHoriz, + swing_v: SwVert, + sleep: bool, + ifeel: bool, + display: bool, + health: bool, + clean: bool, + mildew: bool, + ) -> dict: + """Set the state of the device.""" + # TODO: decode unknown bits + UNK0 = 0b100 + UNK1 = 0b1101 + UNK2 = 0b101 + + target_temp = round(target_temp * 2) / 2 + + if preset == self.Preset.MUTE: + if mode != self.Mode.FAN: + raise ValueError("mute is only available in fan mode") + speed = self.Speed.LOW + + elif preset == self.Preset.TURBO: + if mode not in {self.Mode.COOL, self.Mode.HEAT}: + raise ValueError("turbo is only available in cooling/heating") + speed = self.Speed.HIGH + + data = bytearray(0x0D) + data[0x00] = (int(target_temp) - 8 << 3) | swing_v + data[0x01] = (swing_h << 5) | UNK0 + data[0x02] = ((target_temp % 1 == 0.5) << 7) | UNK1 + data[0x03] = speed << 5 + data[0x04] = preset << 6 + data[0x05] = mode << 5 | sleep << 2 | ifeel << 3 + data[0x08] = power << 5 | clean << 2 | (health and 0b11) + data[0x0A] = display << 4 | mildew << 3 + data[0x0C] = UNK2 + + resp = self._send(0, data) + return self._parse_state(resp) + + def get_state(self) -> dict: + """Returns a dictionary with the unit's parameters. + + Returns: + dict: + power (bool): + target_temp (float): temperature set point 16 dict: + """Returns dictionary with AC info. + + Returns: + dict: + power (bool): power + ambient_temp (float): ambient temperature + """ + resp = self._send(2) + + if len(resp) < 22: + raise e.DataValidationError( + -4007, + "Received data packet length error", + f"Expected at least 24 bytes and received {len(resp) + 2}", + ) + + ac_info = {} + ac_info["power"] = resp[0x1] & 1 + + ambient_temp = resp[0x05] & 0b11111, resp[0x15] & 0b11111 + if any(ambient_temp): + ac_info["ambient_temp"] = ambient_temp[0] + ambient_temp[1] / 10.0 + + return ac_info From c4979562c8d7af2559263cd03c8132afa47b417d Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 17 Apr 2024 02:49:13 -0300 Subject: [PATCH 79/82] Fix type hints (#794) --- broadlink/__init__.py | 2 +- broadlink/cover.py | 5 +-- broadlink/device.py | 2 +- broadlink/hub.py | 16 +++++----- broadlink/light.py | 62 +++++++++++++++++++------------------ broadlink/remote.py | 12 ++++---- broadlink/sensor.py | 4 ++- broadlink/switch.py | 71 ++++++++++++++++++++++++------------------- 8 files changed, 95 insertions(+), 79 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 8af3e4c6..080e1cd1 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -269,7 +269,7 @@ def discover( def xdiscover( timeout: int = DEFAULT_TIMEOUT, - local_ip_address: str = None, + local_ip_address: str | None = None, discover_ip_address: str = DEFAULT_BCAST_ADDR, discover_ip_port: int = DEFAULT_PORT, ) -> t.Generator[Device, None, None]: diff --git a/broadlink/cover.py b/broadlink/cover.py index 1d8b41ef..75317943 100644 --- a/broadlink/cover.py +++ b/broadlink/cover.py @@ -1,5 +1,6 @@ """Support for covers.""" import time +from typing import Sequence from . import exceptions as e from .device import Device @@ -63,7 +64,7 @@ class dooya2(Device): TYPE = "DT360E-2" - def _send(self, operation: int, data: bytes = b""): + def _send(self, operation: int, data: Sequence = b""): """Send a command to the device.""" packet = bytearray(12) packet[0x02] = 0xA5 @@ -120,7 +121,7 @@ class wser(Device): TYPE = "WSER" - def _send(self, operation: int, data: bytes = b""): + def _send(self, operation: int, data: Sequence = b""): """Send a command to the device.""" packet = bytearray(12) packet[0x02] = 0xA5 diff --git a/broadlink/device.py b/broadlink/device.py index 3bc9d8aa..287b3542 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -22,7 +22,7 @@ def scan( timeout: int = DEFAULT_TIMEOUT, - local_ip_address: str = None, + local_ip_address: str | None = None, discover_ip_address: str = DEFAULT_BCAST_ADDR, discover_ip_port: int = DEFAULT_PORT, ) -> t.Generator[HelloResponse, None, None]: diff --git a/broadlink/hub.py b/broadlink/hub.py index cb24dc8d..38894090 100644 --- a/broadlink/hub.py +++ b/broadlink/hub.py @@ -42,7 +42,7 @@ def get_subdevices(self, step: int = 5) -> list: return sub_devices - def get_state(self, did: str = None) -> dict: + def get_state(self, did: str | None = None) -> dict: """Return the power state of the device.""" state = {} if did is not None: @@ -55,10 +55,10 @@ def get_state(self, did: str = None) -> dict: def set_state( self, - did: str = None, - pwr1: bool = None, - pwr2: bool = None, - pwr3: bool = None, + did: str | None = None, + pwr1: bool | None = None, + pwr2: bool | None = None, + pwr3: bool | None = None, ) -> dict: """Set the power state of the device.""" state = {} @@ -81,7 +81,9 @@ def _encode(self, flag: int, state: dict) -> bytes: # flag: 1 for reading, 2 for writing. packet = bytearray(12) data = json.dumps(state, separators=(",", ":")).encode() - struct.pack_into(" dict: """Decode a JSON packet.""" payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(" dict: def set_state( self, - pwr: bool = None, - red: int = None, - blue: int = None, - green: int = None, - brightness: int = None, - colortemp: int = None, - hue: int = None, - saturation: int = None, - transitionduration: int = None, - maxworktime: int = None, - bulb_colormode: int = None, - bulb_scenes: str = None, - bulb_scene: str = None, - bulb_sceneidx: int = None, + pwr: bool | None = None, + red: int | None = None, + blue: int | None = None, + green: int | None = None, + brightness: int | None = None, + colortemp: int | None = None, + hue: int | None = None, + saturation: int | None = None, + transitionduration: int | None = None, + maxworktime: int | None = None, + bulb_colormode: int | None = None, + bulb_scenes: str | None = None, + bulb_scene: str | None = None, + bulb_sceneidx: int | None = None, ) -> dict: """Set the power state of the device.""" state = {} @@ -101,7 +101,7 @@ def _decode(self, response: bytes) -> dict: """Decode a JSON packet.""" payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(" dict: def set_state( self, - pwr: bool = None, - red: int = None, - blue: int = None, - green: int = None, - brightness: int = None, - colortemp: int = None, - hue: int = None, - saturation: int = None, - transitionduration: int = None, - maxworktime: int = None, - bulb_colormode: int = None, - bulb_scenes: str = None, - bulb_scene: str = None, + pwr: bool | None = None, + red: int | None = None, + blue: int | None = None, + green: int | None = None, + brightness: int | None = None, + colortemp: int | None = None, + hue: int | None = None, + saturation: int | None = None, + transitionduration: int | None = None, + maxworktime: int | None = None, + bulb_colormode: int | None = None, + bulb_scenes: str | None = None, + bulb_scene: str | None = None, ) -> dict: """Set the power state of the device.""" state = {} @@ -183,7 +183,9 @@ def _encode(self, flag: int, state: dict) -> bytes: # flag: 1 for reading, 2 for writing. packet = bytearray(12) data = json.dumps(state, separators=(",", ":")).encode() - struct.pack_into(" dict: """Decode a JSON packet.""" payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(" None: +def pulses_to_data(pulses: t.List[int], tick: float = 32.84) -> bytes: """Convert a microsecond duration sequence into a Broadlink IR packet.""" result = bytearray(4) result[0x00] = 0x26 @@ -25,7 +25,7 @@ def pulses_to_data(pulses: t.List[int], tick: int = 32.84) -> None: return result -def data_to_pulses(data: bytes, tick: int = 32.84) -> t.List[int]: +def data_to_pulses(data: bytes, tick: float = 32.84) -> t.List[int]: """Parse a Broadlink packet into a microsecond duration sequence.""" result = [] index = 4 @@ -38,8 +38,8 @@ def data_to_pulses(data: bytes, tick: int = 32.84) -> t.List[int]: if chunk == 0: try: chunk = 256 * data[index] + data[index + 1] - except IndexError: - raise ValueError("Malformed data.") + except IndexError as err: + raise ValueError("Malformed data.") from err index += 2 result.append(int(chunk * tick)) @@ -95,7 +95,7 @@ def check_frequency(self) -> t.Tuple[bool, float]: frequency = struct.unpack(" None: + def find_rf_packet(self, frequency: float | None = None) -> None: """Enter radiofrequency learning mode.""" payload = bytearray() if frequency: @@ -129,7 +129,7 @@ def _send(self, command: int, data: bytes = b"") -> bytes: e.check_error(resp[0x22:0x24]) payload = self.decrypt(resp[0x38:]) p_len = struct.unpack(" None: def set_state( self, - pwr: bool = None, - ntlight: bool = None, - indicator: bool = None, - ntlbrightness: int = None, - maxworktime: int = None, - childlock: bool = None, + pwr: bool | None = None, + ntlight: bool | None = None, + indicator: bool | None = None, + ntlbrightness: int | None = None, + maxworktime: int | None = None, + childlock: bool | None = None, ) -> dict: """Set state of device.""" state = {} @@ -186,7 +186,7 @@ def _decode(self, response: bytes) -> dict: e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(" dict: e.check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(" dict: def set_state( self, - pwr: bool = None, - pwr1: bool = None, - pwr2: bool = None, - maxworktime: int = None, - maxworktime1: int = None, - maxworktime2: int = None, - idcbrightness: int = None, + pwr: bool | None = None, + pwr1: bool | None = None, + pwr2: bool | None = None, + maxworktime: int | None = None, + maxworktime1: int | None = None, + maxworktime2: int | None = None, + idcbrightness: int | None = None, ) -> dict: """Set the power state of the device.""" state = {} @@ -291,7 +291,16 @@ def _encode(self, flag: int, state: dict) -> bytes: data = json.dumps(state).encode() length = 12 + len(data) struct.pack_into( - " dict: """Decode a message.""" payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(" dict: """Set the power state of the device.""" state = {} @@ -449,7 +458,7 @@ def get_state(self) -> dict: def get_value(start, end, factors): value = sum( - int(payload_str[i - 2 : i]) * factor + int(payload_str[i-2:i]) * factor for i, factor in zip(range(start, end, -2), factors) ) return value From ff4628de1b2d6d783d338e84292ee7ed831e9758 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 17 Apr 2024 02:55:38 -0300 Subject: [PATCH 80/82] Merge new product ids into master (#795) * Add support for Broadlink SP4M-JP (0x756B) (#782) * Add support for Luceco/BG Electrical A60 bulb (0x606D) (#766) * Add support for Luceco EFCF60WSMT (0xA6EF) (#787) * Add support for Broadlink WS4 (#792) * Add support for Broadlink SP4D-US (0xA6F4) (#793) --- broadlink/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 3a60f772..1cf7deb1 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -55,6 +55,7 @@ }, sp4: { 0x7568: ("SP4L-CN", "Broadlink"), + 0x756B: ("SP4M-JP", "Broadlink"), 0x756C: ("SP4M", "Broadlink"), 0x756F: ("MCB1", "Broadlink"), 0x7579: ("SP4L-EU", "Broadlink"), @@ -62,6 +63,7 @@ 0x7583: ("SP mini 3", "Broadlink"), 0x7587: ("SP4L-UK", "Broadlink"), 0x7D11: ("SP mini 3", "Broadlink"), + 0xA4F9: ("WS4", "Broadlink (OEM)"), 0xA569: ("SP4L-UK", "Broadlink"), 0xA56A: ("MCB1", "Broadlink"), 0xA56B: ("SCB1E", "Broadlink"), @@ -69,6 +71,7 @@ 0xA576: ("SP4L-AU", "Broadlink"), 0xA589: ("SP4L-UK", "Broadlink"), 0xA5D3: ("SP4L-EU", "Broadlink"), + 0xA6F4: ("SP4D-US", "Broadlink"), }, sp4b: { 0x5115: ("SCB1E", "Broadlink"), @@ -156,6 +159,7 @@ lb1: { 0x5043: ("SB800TD", "Broadlink (OEM)"), 0x504E: ("LB1", "Broadlink"), + 0x606D: ("SLA22RGB9W81/SLA27RGB9W81", "Luceco"), 0x606E: ("SB500TD", "Broadlink (OEM)"), 0x60C7: ("LB1", "Broadlink"), 0x60C8: ("LB1", "Broadlink"), @@ -168,13 +172,14 @@ lb2: { 0xA4F4: ("LB27 R1", "Broadlink"), 0xA5F7: ("LB27 R1", "Broadlink"), + 0xA6EF: ("EFCF60WSMT", "Luceco"), }, S1C: { 0x2722: ("S2KIT", "Broadlink"), }, s3: { - 0xA59C:("S3", "Broadlink"), - 0xA64D:("S3", "Broadlink"), + 0xA59C: ("S3", "Broadlink"), + 0xA64D: ("S3", "Broadlink"), }, hysen: { 0x4EAD: ("HY02/HY03", "Hysen"), From 0a9acab2b80306166cddbb8d94c09ae788735b66 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 17 Apr 2024 03:20:13 -0300 Subject: [PATCH 81/82] Make type hints compatible with Python 3.6 (#797) --- broadlink/__init__.py | 14 +++++------ broadlink/device.py | 12 +++++----- broadlink/helpers.py | 8 +++---- broadlink/hub.py | 11 +++++---- broadlink/light.py | 55 ++++++++++++++++++++++--------------------- broadlink/remote.py | 10 ++++---- broadlink/switch.py | 53 +++++++++++++++++++++-------------------- cli/broadlink_cli | 6 ++--- 8 files changed, 86 insertions(+), 83 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 98d04d92..d3135501 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """The python-broadlink library.""" import socket -import typing as t +from typing import Generator, List, Optional, Tuple, Union from . import exceptions as e from .const import DEFAULT_BCAST_ADDR, DEFAULT_PORT, DEFAULT_TIMEOUT @@ -212,8 +212,8 @@ def gendevice( dev_type: int, - host: t.Tuple[str, int], - mac: t.Union[bytes, str], + host: Tuple[str, int], + mac: Union[bytes, str], name: str = "", is_locked: bool = False, ) -> Device: @@ -265,10 +265,10 @@ def hello( def discover( timeout: int = DEFAULT_TIMEOUT, - local_ip_address: str = None, + local_ip_address: Optional[str] = None, discover_ip_address: str = DEFAULT_BCAST_ADDR, discover_ip_port: int = DEFAULT_PORT, -) -> t.List[Device]: +) -> List[Device]: """Discover devices connected to the local network.""" responses = scan( timeout, local_ip_address, discover_ip_address, discover_ip_port @@ -278,10 +278,10 @@ def discover( def xdiscover( timeout: int = DEFAULT_TIMEOUT, - local_ip_address: str | None = None, + local_ip_address: Optional[str] = None, discover_ip_address: str = DEFAULT_BCAST_ADDR, discover_ip_port: int = DEFAULT_PORT, -) -> t.Generator[Device, None, None]: +) -> Generator[Device, None, None]: """Discover devices connected to the local network. This function returns a generator that yields devices instantly. diff --git a/broadlink/device.py b/broadlink/device.py index 287b3542..5a10bc01 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -3,7 +3,7 @@ import threading import random import time -import typing as t +from typing import Generator, Optional, Tuple, Union from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -17,15 +17,15 @@ ) from .protocol import Datetime -HelloResponse = t.Tuple[int, t.Tuple[str, int], str, str, bool] +HelloResponse = Tuple[int, Tuple[str, int], str, str, bool] def scan( timeout: int = DEFAULT_TIMEOUT, - local_ip_address: str | None = None, + local_ip_address: Optional[str] = None, discover_ip_address: str = DEFAULT_BCAST_ADDR, discover_ip_port: int = DEFAULT_PORT, -) -> t.Generator[HelloResponse, None, None]: +) -> Generator[HelloResponse, None, None]: """Broadcast a hello message and yield responses.""" conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -100,8 +100,8 @@ class Device: def __init__( self, - host: t.Tuple[str, int], - mac: t.Union[bytes, str], + host: Tuple[str, int], + mac: Union[bytes, str], devtype: int, timeout: int = DEFAULT_TIMEOUT, name: str = "", diff --git a/broadlink/helpers.py b/broadlink/helpers.py index 6ee54991..e7b3d4c9 100644 --- a/broadlink/helpers.py +++ b/broadlink/helpers.py @@ -1,5 +1,5 @@ """Helper functions and classes.""" -import typing as t +from typing import Dict, List, Sequence class CRC16: @@ -8,10 +8,10 @@ class CRC16: CRC tables are cached for performance. """ - _cache: t.Dict[int, t.List[int]] = {} + _cache: Dict[int, List[int]] = {} @classmethod - def get_table(cls, polynomial: int) -> t.List[int]: + def get_table(cls, polynomial: int) -> List[int]: """Return the CRC-16 table for a polynomial.""" try: crc_table = cls._cache[polynomial] @@ -31,7 +31,7 @@ def get_table(cls, polynomial: int) -> t.List[int]: @classmethod def calculate( cls, - sequence: t.Sequence[int], + sequence: Sequence[int], polynomial: int = 0xA001, # CRC-16-ANSI. init_value: int = 0xFFFF, ) -> int: diff --git a/broadlink/hub.py b/broadlink/hub.py index 38894090..0fd4ae53 100644 --- a/broadlink/hub.py +++ b/broadlink/hub.py @@ -1,6 +1,7 @@ """Support for hubs.""" import struct import json +from typing import Optional from . import exceptions as e from .device import Device @@ -42,7 +43,7 @@ def get_subdevices(self, step: int = 5) -> list: return sub_devices - def get_state(self, did: str | None = None) -> dict: + def get_state(self, did: Optional[str] = None) -> dict: """Return the power state of the device.""" state = {} if did is not None: @@ -55,10 +56,10 @@ def get_state(self, did: str | None = None) -> dict: def set_state( self, - did: str | None = None, - pwr1: bool | None = None, - pwr2: bool | None = None, - pwr3: bool | None = None, + did: Optional[str] = None, + pwr1: Optional[bool] = None, + pwr2: Optional[bool] = None, + pwr3: Optional[bool] = None, ) -> dict: """Set the power state of the device.""" state = {} diff --git a/broadlink/light.py b/broadlink/light.py index 0dd0d206..1ae87e8f 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -2,6 +2,7 @@ import enum import json import struct +from typing import Optional from . import exceptions as e from .device import Device @@ -32,20 +33,20 @@ def get_state(self) -> dict: def set_state( self, - pwr: bool | None = None, - red: int | None = None, - blue: int | None = None, - green: int | None = None, - brightness: int | None = None, - colortemp: int | None = None, - hue: int | None = None, - saturation: int | None = None, - transitionduration: int | None = None, - maxworktime: int | None = None, - bulb_colormode: int | None = None, - bulb_scenes: str | None = None, - bulb_scene: str | None = None, - bulb_sceneidx: int | None = None, + 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, ) -> dict: """Set the power state of the device.""" state = {} @@ -130,19 +131,19 @@ def get_state(self) -> dict: def set_state( self, - pwr: bool | None = None, - red: int | None = None, - blue: int | None = None, - green: int | None = None, - brightness: int | None = None, - colortemp: int | None = None, - hue: int | None = None, - saturation: int | None = None, - transitionduration: int | None = None, - maxworktime: int | None = None, - bulb_colormode: int | None = None, - bulb_scenes: str | None = None, - bulb_scene: str | None = None, + 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, ) -> dict: """Set the power state of the device.""" state = {} diff --git a/broadlink/remote.py b/broadlink/remote.py index 64b7c35d..60c54ce2 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -1,12 +1,12 @@ """Support for universal remotes.""" import struct -import typing as t +from typing import List, Optional, Tuple from . import exceptions as e from .device import Device -def pulses_to_data(pulses: t.List[int], tick: float = 32.84) -> bytes: +def pulses_to_data(pulses: List[int], tick: float = 32.84) -> bytes: """Convert a microsecond duration sequence into a Broadlink IR packet.""" result = bytearray(4) result[0x00] = 0x26 @@ -25,7 +25,7 @@ def pulses_to_data(pulses: t.List[int], tick: float = 32.84) -> bytes: return result -def data_to_pulses(data: bytes, tick: float = 32.84) -> t.List[int]: +def data_to_pulses(data: bytes, tick: float = 32.84) -> List[int]: """Parse a Broadlink packet into a microsecond duration sequence.""" result = [] index = 4 @@ -88,14 +88,14 @@ def sweep_frequency(self) -> None: """Sweep frequency.""" self._send(0x19) - def check_frequency(self) -> t.Tuple[bool, float]: + def check_frequency(self) -> Tuple[bool, float]: """Return True if the frequency was identified successfully.""" resp = self._send(0x1A) is_found = bool(resp[0]) frequency = struct.unpack(" None: + def find_rf_packet(self, frequency: Optional[float] = None) -> None: """Enter radiofrequency learning mode.""" payload = bytearray() if frequency: diff --git a/broadlink/switch.py b/broadlink/switch.py index a49cb1c0..8393f6b1 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -1,6 +1,7 @@ """Support for switches.""" import json import struct +from typing import Optional from . import exceptions as e from .device import Device @@ -127,12 +128,12 @@ def set_nightlight(self, ntlight: bool) -> None: def set_state( self, - pwr: bool | None = None, - ntlight: bool | None = None, - indicator: bool | None = None, - ntlbrightness: int | None = None, - maxworktime: int | None = None, - childlock: bool | None = None, + pwr: Optional[bool] = None, + ntlight: Optional[bool] = None, + indicator: Optional[bool] = None, + ntlbrightness: Optional[int] = None, + maxworktime: Optional[int] = None, + childlock: Optional[bool] = None, ) -> dict: """Set state of device.""" state = {} @@ -255,13 +256,13 @@ def get_state(self) -> dict: def set_state( self, - pwr: bool | None = None, - pwr1: bool | None = None, - pwr2: bool | None = None, - maxworktime: int | None = None, - maxworktime1: int | None = None, - maxworktime2: int | None = None, - idcbrightness: int | None = None, + pwr: Optional[bool] = None, + pwr1: Optional[bool] = None, + pwr2: Optional[bool] = None, + maxworktime: Optional[int] = None, + maxworktime1: Optional[int] = None, + maxworktime2: Optional[int] = None, + idcbrightness: Optional[int] = None, ) -> dict: """Set the power state of the device.""" state = {} @@ -322,19 +323,19 @@ class ehc31(bg1): def set_state( self, - pwr: bool | None = None, - pwr1: bool | None = None, - pwr2: bool | None = None, - pwr3: bool | None = None, - maxworktime1: int | None = None, - maxworktime2: int | None = None, - maxworktime3: int | None = None, - idcbrightness: int | None = None, - childlock: bool | None = None, - childlock1: bool | None = None, - childlock2: bool | None = None, - childlock3: bool | None = None, - childlock4: bool | None = None, + pwr: Optional[bool] = None, + pwr1: Optional[bool] = None, + pwr2: Optional[bool] = None, + pwr3: Optional[bool] = None, + maxworktime1: Optional[int] = None, + maxworktime2: Optional[int] = None, + maxworktime3: Optional[int] = None, + idcbrightness: Optional[int] = None, + childlock: Optional[bool] = None, + childlock1: Optional[bool] = None, + childlock2: Optional[bool] = None, + childlock3: Optional[bool] = None, + childlock4: Optional[bool] = None, ) -> dict: """Set the power state of the device.""" state = {} diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 35317ee4..7913e332 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -2,7 +2,7 @@ import argparse import base64 import time -import typing as t +from typing import List import broadlink from broadlink.const import DEFAULT_PORT @@ -16,7 +16,7 @@ def auto_int(x): return int(x, 0) -def format_pulses(pulses: t.List[int]) -> str: +def format_pulses(pulses: List[int]) -> str: """Format pulses.""" return " ".join( f"+{pulse}" if i % 2 == 0 else f"-{pulse}" @@ -24,7 +24,7 @@ def format_pulses(pulses: t.List[int]) -> str: ) -def parse_pulses(data: t.List[str]) -> t.List[int]: +def parse_pulses(data: List[str]) -> List[int]: """Parse pulses.""" return [abs(int(s)) for s in data] From 730853e5faf2cf979596662faf9def2b1f8fee6d Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 17 Apr 2024 03:26:53 -0300 Subject: [PATCH 82/82] Bump version to 0.19.0 (#798) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3373f870..0426f148 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.18.3' +version = '0.19.0' setup( name="broadlink",