From 4f03ffb963d65b65e2d80b5fdcf24b2f02d13e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4rvstrand?= Date: Sat, 22 Apr 2017 21:34:37 +0200 Subject: [PATCH 001/261] Add support for pure python AES implementation (#78) --- broadlink/__init__.py | 56 ++++++++++++++++++++++++++++--------------- setup.py | 10 +++++--- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index d5cc8d28..a6f2c36d 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,10 +1,15 @@ #!/usr/bin/python from datetime import datetime -from Crypto.Cipher import AES +try: + from Crypto.Cipher import AES +except ImportError, e: + import pyaes + import time import random import socket +import sys import threading def gendevice(devtype, host, mac): @@ -145,6 +150,29 @@ def __init__(self, host, mac, timeout=10): self.type = "Unknown" self.lock = threading.Lock() + if 'pyaes' in sys.modules: + self.encrypt = self.encrypt_pyaes + self.decrypt = self.decrypt_pyaes + else: + self.encrypt = self.encrypt_pycrypto + self.decrypt = self.decrypt_pycrypto + + def encrypt_pyaes(self, payload): + aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv)) + return "".join([aes.encrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) + + def decrypt_pyaes(self, payload): + aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv)) + return "".join([aes.decrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) + + def encrypt_pycrypto(self, payload): + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + return aes.encrypt(bytes(payload)) + + def decrypt_pycrypto(self, payload): + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + return aes.decrypt(bytes(payload)) + def auth(self): payload = bytearray(0x50) payload[0x04] = 0x31 @@ -174,10 +202,7 @@ def auth(self): response = self.send_packet(0x65, payload) - enc_payload = response[0x38:] - - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - payload = aes.decrypt(bytes(enc_payload)) + payload = self.decrypt(response[0x38:]) if not payload: return False @@ -225,8 +250,7 @@ def send_packet(self, command, payload): checksum += payload[i] checksum = checksum & 0xffff - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - payload = aes.encrypt(bytes(payload)) + payload = self.encrypt(payload) packet[0x34] = checksum & 0xff packet[0x35] = checksum >> 8 @@ -301,8 +325,7 @@ def check_power_raw(self): response = self.send_packet(0x6a, packet) err = response[0x22] | (response[0x23] << 8) if err == 0: - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - payload = aes.decrypt(bytes(response[0x38:])) + payload = self.decrypt(bytes(response[0x38:])) if type(payload[0x4]) == int: state = payload[0x0e] else: @@ -350,8 +373,7 @@ def check_power(self): response = self.send_packet(0x6a, packet) err = response[0x22] | (response[0x23] << 8) if err == 0: - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - payload = aes.decrypt(bytes(response[0x38:])) + payload = self.decrypt(bytes(response[0x38:])) return bool(payload[0x4]) class a1(device): @@ -366,8 +388,7 @@ def check_sensors(self): err = response[0x22] | (response[0x23] << 8) if err == 0: data = {} - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - payload = aes.decrypt(bytes(response[0x38:])) + payload = self.decrypt(bytes(response[0x38:])) if type(payload[0x4]) == int: data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 @@ -417,8 +438,7 @@ def check_sensors_raw(self): err = response[0x22] | (response[0x23] << 8) if err == 0: data = {} - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - payload = aes.decrypt(bytes(response[0x38:])) + payload = self.decrypt(bytes(response[0x38:])) if type(payload[0x4]) == int: data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 @@ -445,8 +465,7 @@ def check_data(self): response = self.send_packet(0x6a, packet) err = response[0x22] | (response[0x23] << 8) if err == 0: - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - payload = aes.decrypt(bytes(response[0x38:])) + payload = self.decrypt(bytes(response[0x38:])) return payload[0x04:] def send_data(self, data): @@ -465,8 +484,7 @@ def check_temperature(self): response = self.send_packet(0x6a, packet) err = response[0x22] | (response[0x23] << 8) if err == 0: - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - payload = aes.decrypt(bytes(response[0x38:])) + payload = self.decrypt(bytes(response[0x38:])) if type(payload[0x4]) == int: temp = (payload[0x4] * 10 + payload[0x5]) / 10.0 else: diff --git a/setup.py b/setup.py index a09222a0..912cd301 100644 --- a/setup.py +++ b/setup.py @@ -6,19 +6,23 @@ import sys import warnings -dynamic_requires = [] +try: + import pyaes + dynamic_requires = ["pyaes==1.6.0"] +except ImportError, e: + dynamic_requires = ['pycrypto==2.6.1'] version = 0.3 setup( name='broadlink', - version=0.3, + version=0.4, author='Matthew Garrett', author_email='mjg59@srcf.ucam.org', url='http://github.com/mjg59/python-broadlink', packages=find_packages(), scripts=[], - install_requires=['pycrypto==2.6.1'], + install_requires=dynamic_requires, description='Python API for controlling Broadlink IR controllers', classifiers=[ 'Development Status :: 4 - Beta', From d346476e6397be9f428c38ac7fe567e91ba3dab1 Mon Sep 17 00:00:00 2001 From: Dimitrij Date: Sat, 22 Apr 2017 22:36:50 +0300 Subject: [PATCH 002/261] fix check_power for SP2/SP3 (#72) Fixed up to avoid merge conflict with #78 --- broadlink/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a6f2c36d..495e07c9 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -374,7 +374,11 @@ def check_power(self): err = response[0x22] | (response[0x23] << 8) if err == 0: payload = self.decrypt(bytes(response[0x38:])) - return bool(payload[0x4]) + if type(payload[0x4]) == int: + state = bool(payload[0x4]) + else: + state = bool(ord(payload[0x4])) + return state class a1(device): def __init__ (self, host, mac): From 19851ed423733e90f4e71f8c585fcf3b1b537b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 22 Apr 2017 21:38:02 +0200 Subject: [PATCH 003/261] bug fix in timeout (#70) --- broadlink/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 495e07c9..f875f5ba 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -274,9 +274,8 @@ def send_packet(self, command, payload): response = self.cs.recvfrom(1024) break except socket.timeout: - if (time.time() - starttime) < self.timeout: - pass - raise + if (time.time() - starttime) > self.timeout: + raise return bytearray(response[0]) From 2388808f413f6be0f478c8f7e807d21d1bdbfe76 Mon Sep 17 00:00:00 2001 From: wind-rider Date: Sat, 4 Feb 2017 14:43:06 +0100 Subject: [PATCH 004/261] Add device type mapping to protocol --- protocol.md | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/protocol.md b/protocol.md index 1888a19d..7a9e2341 100644 --- a/protocol.md +++ b/protocol.md @@ -6,6 +6,11 @@ Encryption Packets include AES-based encryption in CBC mode. The initial key is 0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02. The IV is 0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58. +Checksum +-------- + +Construct the packet and set checksum bytes to zero. Add each byte to the starting value of 0xbeaf, wrapping after 0xffff. + Network discovery ----------------- @@ -30,12 +35,38 @@ To discover Broadlink devices on the local network, send a 48 byte packet with t |0x26|06| |0x27-0x2f|00| -Send this packet as a UDP broadcast to 255.255.255.255 on port 80. Bytes 0x3a-0x40 of any unicast response will contain the MAC address of the target device. +Send this packet as a UDP broadcast to 255.255.255.255 on port 80. -Checksum --------- +Response (any unicast response): +| Offset | Contents | +|---------|----------| +|0x34-0x35|Device type as a little-endian 16 bit integer (see device type mapping)| +|0x3a-0x40|MAC address of the target device| + +Device type mapping: +| Device type in response packet | Device type | Treat as | +|---------|----------|----------| +|0|SP1|SP1| +|0x2711|SP2|SP2| +|0x2719 or 0x7919 or 0x271a or 0x791a|Honeywell SP2|SP2| +|0x2720|SPMini|SP2| +|0x753e|SP3|SP2| +|0x2728|SPMini2|SP2 +|0x2733 or 0x273e|OEM branded SPMini|SP2| +|>= 0x7530 and <= 0x7918|OEM branded SPMini2|SP2| +|0x2736|SPMiniPlus|SP2| +|0x2712|RM2|RM| +|0x2737|RM Mini / RM3 Mini Blackbean|RM| +|0x273d|RM Pro Phicomm|RM| +|0x2783|RM2 Home Plus|RM| +|0x277c|RM2 Home Plus GDT|RM| +|0x272a|RM2 Pro Plus|RM| +|0x2787|RM2 Pro Plus2|RM| +|0x278b|RM2 Pro Plus BL|RM| +|0x278f|RM Mini Shate|RM| +|0x2714|A1|A1| +|0x4EB5|MP1|MP1| -Construct the packet and set checksum bytes to zero. Add each byte to the starting value of 0xbeaf, wrapping after 0xffff. Command packet format --------------------- @@ -132,4 +163,4 @@ Todo ---- * Support for other devices using the Broadlink protocol (various smart home devices) -* Figure out what the format of the data packets actually is. \ No newline at end of file +* Figure out what the format of the data packets actually is. From 3c44422b607ddd09a6a48016e88aabb826ec72d5 Mon Sep 17 00:00:00 2001 From: wind-rider Date: Sat, 4 Feb 2017 14:43:50 +0100 Subject: [PATCH 005/261] Update protocol.md --- protocol.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/protocol.md b/protocol.md index 7a9e2341..387a6e79 100644 --- a/protocol.md +++ b/protocol.md @@ -38,12 +38,14 @@ To discover Broadlink devices on the local network, send a 48 byte packet with t Send this packet as a UDP broadcast to 255.255.255.255 on port 80. Response (any unicast response): + | Offset | Contents | |---------|----------| |0x34-0x35|Device type as a little-endian 16 bit integer (see device type mapping)| |0x3a-0x40|MAC address of the target device| Device type mapping: + | Device type in response packet | Device type | Treat as | |---------|----------|----------| |0|SP1|SP1| From 846cc353665154d7f38623154abe6cada0bf6a45 Mon Sep 17 00:00:00 2001 From: tobiaswaldvogel Date: Sat, 22 Apr 2017 21:41:30 +0200 Subject: [PATCH 006/261] More details about the data structure for IR and RF pulses (#58) * Added more details to payload structure --- protocol.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/protocol.md b/protocol.md index 387a6e79..fc58c32e 100644 --- a/protocol.md +++ b/protocol.md @@ -158,7 +158,20 @@ Send the following payload with a command byte of 0x006a |------|--------| |0x00|0x02| |0x01-0x03|0x00| -|0x04-end|data| +|0x04|0x26 = IR, 0xb2 for RF 433Mhz, 0xd7 for RF 315Mhz| +|0x05|repeat count, (0 = no repeat, 1 send twice, .....)| +|0x06-0x07|Length of the following data in little endian| +|0x08 ....|Pulse lengths in 32,84ms units (ms * 269 / 8192 works very well)| +|....|0x0d 0x05 at the end for IR only| + +Each value is represented by one byte. If the length exceeds one byte +then it is stored big endian with a leading 0. + +Example: The header for my Optoma projector is 8920 4450 +8920 * 269 / 8192 = 0x124 +4450 * 269 / 8192 = 0x92 + +So the data starts with `0x00 0x1 0x24 0x92 ....` Todo From 76dd4cfc70fa95ae703c680998757be180deed36 Mon Sep 17 00:00:00 2001 From: Ivan Martinez Date: Tue, 10 Jan 2017 22:55:02 -0200 Subject: [PATCH 007/261] command line programs to control broadlink devices --- cli/broadlink_cli | 59 +++++++++++++++++++++++++++++++++++++++++ cli/broadlink_discovery | 25 +++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100755 cli/broadlink_cli create mode 100755 cli/broadlink_discovery diff --git a/cli/broadlink_cli b/cli/broadlink_cli new file mode 100755 index 00000000..0dc6db66 --- /dev/null +++ b/cli/broadlink_cli @@ -0,0 +1,59 @@ +#!/usr/bin/python + +import broadlink +import sys +import argparse +import time + +def auto_int(x): + return int(x, 0) + +parser = argparse.ArgumentParser(fromfile_prefix_chars='@'); +parser.add_argument("--device", help="device definition as 'type host mac'") +parser.add_argument("--type", type=auto_int, default=0x2712, help="type of device") +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("--send", help="send command") +parser.add_argument("--learn",action="store_true", help="learn command") +parser.add_argument("--learnfile", help="learn command and save to specified file") +args = parser.parse_args() + +if args.device: + values = args.device.split(); + type = int(values[0],0) + host = values[1] + mac = bytearray.fromhex(values[2]) +else: + type = args.type + host = args.host + mac = bytearray.fromhex(args.mac) + + +dev = broadlink.gendevice(type, (host, 80), mac) +dev.auth() +if args.temperature: + print dev.check_temperature() +if args.send: + data = bytearray.fromhex(args.send) + dev.send_data(data) +if args.learn or args.learnfile: + dev.enter_learning() + data = None + print "Learning..." + timeout = 30 + while (data is None) and (timeout > 0): + time.sleep(2) + timeout -= 2 + data = dev.check_data() + if data: + learned = ''.join(format(x, '02x') for x in bytearray(data)) + if args.learn: + print learned + if args.learnfile: + print "Saving to {}".format(args.learnfile) + with open(args.learnfile, "w") as text_file: + text_file.write(learned) + else: + print "No data received..." + \ No newline at end of file diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery new file mode 100755 index 00000000..84d96dfe --- /dev/null +++ b/cli/broadlink_discovery @@ -0,0 +1,25 @@ +#!/usr/bin/python + +import broadlink +import time +import argparse + +parser = argparse.ArgumentParser(fromfile_prefix_chars='@'); +parser.add_argument("--timeout", type=int, default=5, help="timeout to wait for receiving discovery responses") +args = parser.parse_args() + +print "discover" +devices = broadlink.discover(timeout=args.timeout) +#print devices +for device in devices: + if device.auth(): + print "###########################################" +# print device + print device.type + print "# broadlink_cli --type 0x2712 --host {} --mac {}".format(device.host[0], ''.join(format(x, '02x') for x in device.mac)) + print "Device file data (to be used with --device @filename in broadlink_cli) : " + print "0x2712 {} {}".format(device.host[0], ''.join(format(x, '02x') for x in device.mac)) + print "temperature = {}".format(device.check_temperature()) + print "" + else: + print "Error authenticating with device : {}".format(device.host) From de7c5c2a2af68b88aff982d7a1fec5b3dcfd7d3b Mon Sep 17 00:00:00 2001 From: "Ivan F. Martinez" Date: Tue, 10 Jan 2017 22:57:35 -0200 Subject: [PATCH 008/261] basic documentation for cli programs --- cli/README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 cli/README.md diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000..47b45f4c --- /dev/null +++ b/cli/README.md @@ -0,0 +1,75 @@ +Command line interface for python-broadlink +=========================================== + +This is a command line interface for broadlink python library + +Tested with BroadLink RMPRO / RM2 + + +Requirements +------------ +You should have the broadlink python installed, this can be made in many linux distributions using : +``` +sudo pip install broadlink +``` + +Instalation +----------- +Just copy this files + + +Programs +-------- + + +* 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 + + +device specification formats +---------------------------- + +Using separate parameters for each information: +``` +broadlink_cli --type 0x2712 --host 1.1.1.1 --mac aaaaaaaaaa --temp +``` + +Using all parameters as a single argument: +``` +broadlink_cli --device "0x2712 1.1.1.1 aaaaaaaaaa" --temp +``` + +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 + +Sample usage +------------ + +Learn commands : +``` +# 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 +``` + + +Send command : +``` +broadlink_cli --device @BEDROOM.device --send @LG-TV.power +broadlink_cli --device @BEDROOM.device --send ....datafromlearncommand... +``` + +Get Temperature : +``` +broadlink_cli --device @BEDROOM.device --temperature +``` From 5195856200587cc087e7af52db55e1e559e083bf Mon Sep 17 00:00:00 2001 From: Ivan Martinez Date: Sun, 16 Apr 2017 11:34:31 -0300 Subject: [PATCH 009/261] included sensors option --- cli/broadlink_cli | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 0dc6db66..c99a5260 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -15,6 +15,7 @@ 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("--send", 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("--learnfile", help="learn command and save to specified file") args = parser.parse_args() @@ -34,6 +35,14 @@ dev = broadlink.gendevice(type, (host, 80), mac) dev.auth() if args.temperature: print dev.check_temperature() +if args.sensors: + try: + data = dev.check_sensors() + except: + data = {} + data['temperature'] = dev.check_temperature() + for key in data: + print "{} {}".format(key, data[key]) if args.send: data = bytearray.fromhex(args.send) dev.send_data(data) @@ -56,4 +65,4 @@ if args.learn or args.learnfile: text_file.write(learned) else: print "No data received..." - \ No newline at end of file + From d989c27d36aa2b3b2da93be6edb8171221a88096 Mon Sep 17 00:00:00 2001 From: Aydaen Lynch Date: Sat, 22 Apr 2017 15:48:02 -0400 Subject: [PATCH 010/261] Add in AP Mode device setup for new Broadlink devices. (#53) --- README.md | 14 ++++++++++++++ broadlink/__init__.py | 37 +++++++++++++++++++++++++++++++++++++ protocol.md | 21 +++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/README.md b/README.md index 3d7598f5..1305ea0f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,20 @@ A simple Python API for controlling IR controllers from [Broadlink](http://www.i 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. +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) +``` + Discover available devices on the local network: ``` import broadlink diff --git a/broadlink/__init__.py b/broadlink/__init__.py index f875f5ba..68ad5599 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -503,3 +503,40 @@ def discover(self): dev = discover() self.host = dev.host self.mac = dev.mac + +# 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, password, security_mode): + # Security mode options are (0 - none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) + payload = bytearray(0x88) + payload[0x26] = 0x14 # This seems to always be set to 14 + # Add the SSID to the payload + ssid_start = 68 + ssid_length = 0 + for letter in ssid: + payload[(ssid_start + ssid_length)] = ord(letter) + ssid_length += 1 + # Add the WiFi password to the payload + pass_start = 100 + pass_length = 0 + for letter in password: + payload[(pass_start + pass_length)] = ord(letter) + pass_length += 1 + + payload[0x84] = ssid_length # Character length of SSID + payload[0x85] = pass_length # Character length of password + payload[0x86] = security_mode # Type of encryption (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2) + + checksum = 0xbeaf + for i in range(len(payload)): + checksum += payload[i] + checksum = checksum & 0xffff + + payload[0x20] = checksum & 0xff # Checksum 1 position + payload[0x21] = checksum >> 8 # Checksum 2 position + + sock = socket.socket(socket.AF_INET, # Internet + socket.SOCK_DGRAM) # 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)) \ No newline at end of file diff --git a/protocol.md b/protocol.md index fc58c32e..e9ac99b1 100644 --- a/protocol.md +++ b/protocol.md @@ -11,6 +11,25 @@ Checksum Construct the packet and set checksum bytes to zero. Add each byte to the starting value of 0xbeaf, wrapping after 0xffff. +New device setup +---------------- + +To setup a new Broadlink device while in AP Mode a 136 byte packet needs to be sent to the device as follows: + +| Offset | Contents | +|---------|----------| +|0x00-0x19|00| +|0x20-0x21|Checksum as a little-endian 16 bit integer| +|0x26|14 (Always 14)| +|0x44-0x63|SSID Name (zero padding is appended)| +|0x64-0x83|Password (zero padding is appended)| +|0x84|Character length of SSID| +|0x85|Character length of password| +|0x86|Wireless security mode (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2)| +|0x87-88|00| + +Send this packet as a UDP broadcast to 255.255.255.255 on port 80. + Network discovery ----------------- @@ -179,3 +198,5 @@ Todo * Support for other devices using the Broadlink protocol (various smart home devices) * Figure out what the format of the data packets actually is. +* Deal with the response after AP Mode WiFi network setup. + From babb3f83d375bd69e6d5048c69c2051141b5b674 Mon Sep 17 00:00:00 2001 From: Robert Sullivan Date: Tue, 25 Apr 2017 15:33:00 +0100 Subject: [PATCH 011/261] Python3.5 syntax changes --- broadlink/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 68ad5599..3fe28a81 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -3,7 +3,7 @@ from datetime import datetime try: from Crypto.Cipher import AES -except ImportError, e: +except ImportError as e: import pyaes import time diff --git a/setup.py b/setup.py index 912cd301..ed627fe1 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ try: import pyaes dynamic_requires = ["pyaes==1.6.0"] -except ImportError, e: +except ImportError as e: dynamic_requires = ['pycrypto==2.6.1'] version = 0.3 From f7e30344c5748e37924c8a4f28522c2bc99a52ee Mon Sep 17 00:00:00 2001 From: kost Date: Sun, 7 May 2017 20:32:52 +0200 Subject: [PATCH 012/261] pad the payload for AES encryption (16) (#92) --- broadlink/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 3fe28a81..e757f900 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -245,6 +245,11 @@ def send_packet(self, command, payload): packet[0x32] = self.id[2] packet[0x33] = self.id[3] + # pad the payload for AES encryption + if len(payload)>0: + numpad=(len(payload)//16+1)*16 + payload=payload.ljust(numpad,"\x00") + checksum = 0xbeaf for i in range(len(payload)): checksum += payload[i] @@ -539,4 +544,4 @@ def setup(ssid, password, security_mode): socket.SOCK_DGRAM) # 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)) \ No newline at end of file + sock.sendto(payload, ('255.255.255.255', 80)) From 2e2c8ef1a15175cf311f757a52e7c561f834f68b Mon Sep 17 00:00:00 2001 From: mob41 Date: Wed, 14 Jun 2017 10:30:27 +0800 Subject: [PATCH 013/261] [Critical] Fixes invalid byte string (b) for padding (#97, #107) (#108) * Fixes invalid byte string (b) for padding (#97, #107) * Change version to 0.5 --- broadlink/__init__.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index e757f900..1b65345b 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -248,7 +248,7 @@ def send_packet(self, command, payload): # pad the payload for AES encryption if len(payload)>0: numpad=(len(payload)//16+1)*16 - payload=payload.ljust(numpad,"\x00") + payload=payload.ljust(numpad,b"\x00") checksum = 0xbeaf for i in range(len(payload)): diff --git a/setup.py b/setup.py index ed627fe1..1971c92b 100644 --- a/setup.py +++ b/setup.py @@ -12,11 +12,11 @@ except ImportError as e: dynamic_requires = ['pycrypto==2.6.1'] -version = 0.3 +version = 0.5 setup( name='broadlink', - version=0.4, + version=0.5, author='Matthew Garrett', author_email='mjg59@srcf.ucam.org', url='http://github.com/mjg59/python-broadlink', From 3d4789305e64a842c2b1a86d4b62ade3fa38d2a2 Mon Sep 17 00:00:00 2001 From: Julian Pastarmov Date: Sat, 25 Nov 2017 21:04:10 +0100 Subject: [PATCH 014/261] Only get temperature for devices that support it. (#102) Calling check_temperature is only possible on the RM family of devices. The program used to crash if other types of devices were discovered (A1, MP1 etc.). --- cli/broadlink_discovery | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery index 84d96dfe..13c65e7a 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -19,7 +19,8 @@ for device in devices: print "# broadlink_cli --type 0x2712 --host {} --mac {}".format(device.host[0], ''.join(format(x, '02x') for x in device.mac)) print "Device file data (to be used with --device @filename in broadlink_cli) : " print "0x2712 {} {}".format(device.host[0], ''.join(format(x, '02x') for x in device.mac)) - print "temperature = {}".format(device.check_temperature()) + if hasattr(device, 'check_temperature'): + print "temperature = {}".format(device.check_temperature()) print "" else: print "Error authenticating with device : {}".format(device.host) From 4f902342e70fd2528c976abd9a4478aba24de861 Mon Sep 17 00:00:00 2001 From: Bengt Martensson Date: Sat, 25 Nov 2017 21:06:12 +0100 Subject: [PATCH 015/261] New options for broadlink:cli: --convert and --durations. (#105) Now takes several data arguments. --- cli/broadlink_cli | 81 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index c99a5260..f5575b56 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -5,19 +5,76 @@ import sys import argparse import time +TICK = 32.84 +IR_TOKEN = 0x26 + + def auto_int(x): return int(x, 0) + +def to_microseconds(bytes): + result = [] + # print bytes[0] # 0x26 = 38for IR + length = bytes[2] + 256 * bytes[3] # presently ignored + 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_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 + + parser = argparse.ArgumentParser(fromfile_prefix_chars='@'); parser.add_argument("--device", help="device definition as 'type host mac'") parser.add_argument("--type", type=auto_int, default=0x2712, help="type of device") 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("--send", 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("--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("--learnfile", help="learn command and save to specified file") +parser.add_argument("--durations", action="store_true", help="use durations in micro seconds instead of the Broadlink format") +parser.add_argument("--convert", action="store_true", help="convert input data to durations") +parser.add_argument("data", nargs='*', help="Data to send or convert") args = parser.parse_args() if args.device: @@ -25,14 +82,19 @@ if args.device: type = int(values[0],0) host = values[1] mac = bytearray.fromhex(values[2]) -else: +elif args.mac: type = args.type host = args.host mac = bytearray.fromhex(args.mac) +if args.host: + dev = broadlink.gendevice(type, (host, 80), mac) + dev.auth() -dev = broadlink.gendevice(type, (host, 80), mac) -dev.auth() +if args.convert: + data = bytearray.fromhex(''.join(args.data)) + durations = to_microseconds(data) + print format_durations(durations) if args.temperature: print dev.check_temperature() if args.sensors: @@ -44,7 +106,8 @@ if args.sensors: for key in data: print "{} {}".format(key, data[key]) if args.send: - data = bytearray.fromhex(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: dev.enter_learning() @@ -56,7 +119,9 @@ if args.learn or args.learnfile: timeout -= 2 data = dev.check_data() if data: - learned = ''.join(format(x, '02x') for x in bytearray(data)) + 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 if args.learnfile: From 1d7fba3d06af33b6af3d51b87a5c66c32751433d Mon Sep 17 00:00:00 2001 From: hackers365 Date: Sat, 17 Jun 2017 17:24:18 +0800 Subject: [PATCH 016/261] add honyar mp1 devtype 0x4ef7 --- broadlink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 1b65345b..f17eabb3 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -51,7 +51,7 @@ def gendevice(devtype, host, mac): return rm(host=host, mac=mac) elif devtype == 0x2714: # A1 return a1(host=host, mac=mac) - elif devtype == 0x4EB5: # MP1 + elif devtype == 0x4EB5 or devtype == 0x4EF7: # MP1: 0x4eb5, honyar oem mp1: 0x4ef7 return mp1(host=host, mac=mac) else: return device(host=host, mac=mac) From 626b459b5b13931008fdaaf3b6261dd8bc9c2e33 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Sat, 25 Nov 2017 22:08:58 +0200 Subject: [PATCH 017/261] SP3S support (#117) --- broadlink/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index f17eabb3..eb945a7d 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -23,6 +23,8 @@ def gendevice(devtype, host, mac): return sp2(host=host, mac=mac) elif devtype == 0x753e: # SP3 return sp2(host=host, mac=mac) + elif devtype == 0x947a or devtype == 0x9479: # SP3S + return sp2(host=host, mac=mac) elif devtype == 0x2728: # SPMini2 return sp2(host=host, mac=mac) elif devtype == 0x2733 or devtype == 0x273e: # OEM branded SPMini From 2160488da23f54adc6dfc8728167385e2c59eb45 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Sun, 16 Jul 2017 02:30:22 +0300 Subject: [PATCH 018/261] sp2.get_energy --- broadlink/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index eb945a7d..ee74b884 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -386,6 +386,16 @@ def check_power(self): state = bool(ord(payload[0x4])) return state + def get_energy(self): + packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err == 0: + payload = self.decrypt(bytes(response[0x38:])) + energy = int(hex(ord(payload[7]) * 256 + ord(payload[6]))[2:]) + int(hex(ord(payload[5]))[2:])/100.0 + return energy + + class a1(device): def __init__ (self, host, mac): device.__init__(self, host, mac) From 71f320638ea59dce4d8dcc8a707912a19005afe2 Mon Sep 17 00:00:00 2001 From: Michael Kaiser Date: Sat, 25 Nov 2017 21:14:12 +0100 Subject: [PATCH 019/261] Fix protocol description (#125) * Fix discovery packet format description * fix MAC address offset * Command packet format description * Data description --- protocol.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/protocol.md b/protocol.md index e9ac99b1..2e388d74 100644 --- a/protocol.md +++ b/protocol.md @@ -40,13 +40,14 @@ To discover Broadlink devices on the local network, send a 48 byte packet with t |0x00-0x07|00| |0x08-0x0b|Current offset from GMT as a little-endian 32 bit integer| |0x0c-0x0d|Current year as a little-endian 16 bit integer| -|0x0e|Current number of minutes past the hour| -|0x0f|Current number of hours past midnight| -|0x10|Current number of years past the century| -|0x11|Current day of the week (Monday = 0, Tuesday = 1, etc)| +|0x0e|Current number of seconds past the minute| +|0x0f|Current number of minutes past the hour| +|0x10|Current number of hours past midnight| +|0x11|Current day of the week (Monday = 1, Tuesday = 2, etc)| |0x12|Current day in month| |0x13|Current month| -|0x19-0x1b|Local IP address| +|0x14-0x17|00| +|0x18-0x1b|Local IP address| |0x1c-0x1d|Source port as a little-endian 16 bit integer| |0x1e-0x1f|00| |0x20-0x21|Checksum as a little-endian 16 bit integer| @@ -61,7 +62,7 @@ Response (any unicast response): | Offset | Contents | |---------|----------| |0x34-0x35|Device type as a little-endian 16 bit integer (see device type mapping)| -|0x3a-0x40|MAC address of the target device| +|0x3a-0x3f|MAC address of the target device| Device type mapping: @@ -107,20 +108,19 @@ The command packet header is 56 bytes long with the following format: |0x08-0x1f|00| |0x20-0x21|Checksum of full packet as a little-endian 16 bit integer| |0x22-0x23|00| -|0x24|0x2a| -|0x25|0x27| +|0x24-0x25|Device type as a little-endian 16 bit integer| |0x26-0x27|Command code as a little-endian 16 bit integer| |0x28-0x29|Packet count as a little-endian 16 bit integer| |0x2a-0x2f|Local MAC address| |0x30-0x33|Local device ID (obtained during authentication, 00 before authentication)| -|0x34-0x35|Checksum of packet header as a little-endian 16 bit integer +|0x34-0x35|Checksum of unencrypted payload as a little-endian 16 bit integer |0x36-0x37|00| -The payload is appended immediately after this. The checksum at 0x34 is calculated *before* the payload is appended, and covers only the header. The checksum at 0x20 is calculated *after* the payload is appended, and covers the entire packet (including the checksum at 0x34). Therefore: +The payload is appended immediately after this. The checksum at 0x20 is calculated *after* the payload is appended, and covers the entire packet (including the checksum at 0x34). Therefore: 1. Generate packet header with checksum values set to 0 -2. Set the checksum initialisation value to 0xbeaf and calculate the checksum of the packet header. Set 0x34-0x35 to this value. -3. Append the payload +2. Set the checksum initialisation value to 0xbeaf and calculate the checksum of the unencrypted payload. Set 0x34-0x35 to this value. +3. Encrypt and append the payload 4. Set the checksum initialisation value to 0xbeaf and calculate the checksum of the entire packet. Set 0x20-0x21 to this value. Authorisation @@ -180,7 +180,7 @@ Send the following payload with a command byte of 0x006a |0x04|0x26 = IR, 0xb2 for RF 433Mhz, 0xd7 for RF 315Mhz| |0x05|repeat count, (0 = no repeat, 1 send twice, .....)| |0x06-0x07|Length of the following data in little endian| -|0x08 ....|Pulse lengths in 32,84ms units (ms * 269 / 8192 works very well)| +|0x08 ....|Pulse lengths in 2^-15 s units (µs * 269 / 8192 works very well)| |....|0x0d 0x05 at the end for IR only| Each value is represented by one byte. If the length exceeds one byte From c85f6ac213c5bf12749ab3460700e051ad194125 Mon Sep 17 00:00:00 2001 From: blapid Date: Sat, 25 Nov 2017 22:14:34 +0200 Subject: [PATCH 020/261] Cli venv support (#127) * Update broadlink_cli * Update broadlink_discovery --- cli/broadlink_cli | 2 +- cli/broadlink_discovery | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index f5575b56..3acf168a 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import broadlink import sys diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery index 13c65e7a..4a7438df 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import broadlink import time From b8cf8d073e409fafef50ad0fb5435cb23d016bb2 Mon Sep 17 00:00:00 2001 From: jazzina Date: Sat, 25 Nov 2017 12:20:46 -0800 Subject: [PATCH 021/261] Add support for SmartOne Alarm Kit --- broadlink/__init__.py | 62 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index ee74b884..ea4cbc5f 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -11,6 +11,7 @@ import socket import sys import threading +import codecs def gendevice(devtype, host, mac): if devtype == 0: # SP1 @@ -55,6 +56,8 @@ def gendevice(devtype, host, mac): return a1(host=host, mac=mac) elif devtype == 0x4EB5 or devtype == 0x4EF7: # MP1: 0x4eb5, honyar oem mp1: 0x4ef7 return mp1(host=host, mac=mac) + elif devtype == 0x2722: # S1 (SmartOne Alarm Kit) + return S1C(host=host, mac=mac) else: return device(host=host, mac=mac) @@ -278,7 +281,7 @@ def send_packet(self, command, payload): try: self.cs.sendto(packet, self.host) self.cs.settimeout(1) - response = self.cs.recvfrom(1024) + response = self.cs.recvfrom(2048) break except socket.timeout: if (time.time() - starttime) > self.timeout: @@ -521,6 +524,63 @@ def discover(self): self.host = dev.host self.mac = dev.mac + +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 +} + + +class S1C(device): + """ + Its VERY VERY VERY DIRTY IMPLEMENTATION of S1C + """ + def __init__(self, *a, **kw): + device.__init__(self, *a, **kw) + self.type = 'S1C' + + def get_sensors_status(self): + packet = bytearray(16) + packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err == 0: + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + + payload = aes.decrypt(bytes(response[0x38:])) + if payload: + head = payload[:4] + count = payload[0x4] #need to fix for python 2.x + sensors = payload[0x6:] + sensors_a = [bytearray(sensors[i * 83:(i + 1) * 83]) for i in range(len(sensors) // 83)] + + sens_res = [] + for sens in sensors_a: + status = ord(chr(sens[0])) + _name = str(bytes(sens[4:26]).decode()) + _order = ord(chr(sens[1])) + _type = ord(chr(sens[3])) + _serial = bytes(codecs.encode(sens[26:30],"hex")).decode() + + type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown') + + r = { + 'status': status, + 'name': _name.strip('\x00'), + 'type': type_str, + 'order': _order, + 'serial': _serial, + } + if r['serial'] != '00000000': + sens_res.append(r) + result = { + 'count': count, + 'sensors': sens_res + } + return result + + # 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, password, security_mode): From 8bc67af6d73c75587d40f6dbfa3b975c7d69a8a4 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 25 Nov 2017 12:21:48 -0800 Subject: [PATCH 022/261] Bump version --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1971c92b..f8d1f4ca 100644 --- a/setup.py +++ b/setup.py @@ -12,11 +12,11 @@ except ImportError as e: dynamic_requires = ['pycrypto==2.6.1'] -version = 0.5 +version = 0.6 setup( name='broadlink', - version=0.5, + version=0.6, author='Matthew Garrett', author_email='mjg59@srcf.ucam.org', url='http://github.com/mjg59/python-broadlink', From dd6eb8767e7a7f1f54aa1e7a7b57309bc4fc5fba Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 25 Nov 2017 12:38:57 -0800 Subject: [PATCH 023/261] Experimental RF code --- README.md | 39 ++++++++++++++++++++++++++++++++++++++- broadlink/__init__.py | 27 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1305ea0f..48708827 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,23 @@ Enter learning mode: devices[0].enter_learning() ``` +Sweep RF frequencies: +``` +devices[0].sweep_frequency() +``` + +Check whether a frequency has been found: +``` +found = devices[0].check_frequency() +``` +(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) + Obtain an IR or RF packet while in learning mode: ``` ir_packet = devices[0].check_data() @@ -76,4 +93,24 @@ devices[0].set_power(1, True) Check power state on a SmartPowerStrip: ``` state = devices[0].check_power() -``` \ No newline at end of file +``` + +Learning RF packets +------------------- + +timeout = 10 +devices[0].sweep_frequency() +# Hold down the rf button +for i in range(0, timeout): + found = devices[0].check_frequency() + if found == True: + break + time.sleep(1) +# Tap the rf button +for i in range(0, timeout): + found = devices[0].find_rf_packet() + if found == True: + break + time.sleep(1) +# Obtain the code +code = devices[0].check_data() diff --git a/broadlink/__init__.py b/broadlink/__init__.py index ea4cbc5f..a4c8a484 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -501,6 +501,33 @@ def enter_learning(self): packet[0] = 3 self.send_packet(0x6a, packet) + def sweep_frequency(self): + packet = bytearray(16) + packet[0] = 0x19; + self.send_packet(0x6a, packet) + + def check_frequency(self): + packet = bytearray(16) + packet[0] = 0x1a + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err == 0: + payload = self.decrypt(bytes(response[0x38:])) + if payload[0x04] == 1: + return True + return False + + def find_rf_packet(self): + packet = bytearray(16) + packet[0] = 0x1b + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err == 0: + payload = self.decrypt(bytes(response[0x38:])) + if payload[0x04] == 1: + return True + return False + def check_temperature(self): packet = bytearray(16) packet[0] = 1 From 9ff6fa817b6d4314fcc9b805e480290f3a1ba20e Mon Sep 17 00:00:00 2001 From: Sergey Bogatyrets Date: Sun, 26 Nov 2017 22:10:19 +0300 Subject: [PATCH 024/261] Checking device arg too for defining dev instance (#129) --- cli/broadlink_cli | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 3acf168a..e9e63d57 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -87,7 +87,7 @@ elif args.mac: host = args.host mac = bytearray.fromhex(args.mac) -if args.host: +if args.host or args.device: dev = broadlink.gendevice(type, (host, 80), mac) dev.auth() @@ -123,11 +123,10 @@ if args.learn or args.learnfile: if args.durations \ else ''.join(format(x, '02x') for x in bytearray(data)) if args.learn: - print learned + print learned if args.learnfile: print "Saving to {}".format(args.learnfile) with open(args.learnfile, "w") as text_file: text_file.write(learned) else: print "No data received..." - From 82172f54ab722fb9de998d0ae4c24337e41d5787 Mon Sep 17 00:00:00 2001 From: Aleksandr Smirnov Date: Mon, 25 Dec 2017 02:34:37 +0200 Subject: [PATCH 025/261] support for dooya curtain motor (#134) --- broadlink/__init__.py | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index ea4cbc5f..dbabce0b 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -58,6 +58,8 @@ def gendevice(devtype, host, mac): return mp1(host=host, mac=mac) elif devtype == 0x2722: # S1 (SmartOne Alarm Kit) return S1C(host=host, mac=mac) + elif devtype == 0x4E4D: # Dooya DT360E (DOOYA_CURTAIN_V2) + return dooya(host=host, mac=mac) else: return device(host=host, mac=mac) @@ -581,6 +583,52 @@ def get_sensors_status(self): return result +class dooya(device): + def __init__ (self, host, mac): + device.__init__(self, host, mac) + self.type = "Dooya DT360E" + + def _send(self, magic1, magic2): + packet = bytearray(16) + packet[0] = 0x09 + packet[2] = 0xbb + packet[3] = magic1 + packet[4] = magic2 + packet[9] = 0xfa + packet[10] = 0x44 + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err == 0: + payload = self.decrypt(bytes(response[0x38:])) + return ord(payload[4]) + + def open(self): + return self._send(0x01, 0x00) + + def close(self): + return self._send(0x02, 0x00) + + def stop(self): + return self._send(0x03, 0x00) + + def get_percentage(self): + return self._send(0x06, 0x5d) + + def set_percentage_and_wait(self, new_percentage): + current = self.get_percentage() + if current > new_percentage: + self.close() + while current is not None and current > new_percentage: + time.sleep(0.2) + current = self.get_percentage() + + elif current < new_percentage: + self.open() + while current is not None and current < new_percentage: + time.sleep(0.2) + current = self.get_percentage() + self.stop() + # 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, password, security_mode): From dd0e9083172d1ee54eec167d342edb3b157aea4c Mon Sep 17 00:00:00 2001 From: Valter Vicente Date: Mon, 25 Dec 2017 00:35:09 +0000 Subject: [PATCH 026/261] New flags: check power state and turn device on & off (#132) Added new flags so it is possible to check device's current power state and to turn device on & off from CLI --- cli/broadlink_cli | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index e9e63d57..097a7afb 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -68,6 +68,9 @@ 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("--check", action="store_true", help="check current power state") +parser.add_argument("--turnon", action="store_true", help="turn on device") +parser.add_argument("--turnoff", action="store_true", help="turn off device") 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") @@ -130,3 +133,20 @@ if args.learn or args.learnfile: text_file.write(learned) else: print "No data received..." +if args.check: + if dev.check_power(): + print '* ON *' + else: + print '* OFF *' +if args.turnon: + dev.set_power(True) + if dev.check_power(): + print '== Turned * ON * ==' + else: + print '!! Still OFF !!' +if args.turnoff: + dev.set_power(False) + if dev.check_power(): + print '!! Still ON !!' + else: + print '== Turned * OFF * ==' From 4e33ef446520275e0a9426dc8182dcc3af276da3 Mon Sep 17 00:00:00 2001 From: Brent Avery Date: Tue, 30 Jan 2018 07:45:21 +1100 Subject: [PATCH 027/261] Get rf scan learning working in CLI tool (#87) --- broadlink/__init__.py | 9 ++++-- cli/broadlink_cli | 70 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a4c8a484..8a193d55 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -503,7 +503,12 @@ def enter_learning(self): def sweep_frequency(self): packet = bytearray(16) - packet[0] = 0x19; + packet[0] = 0x19 + self.send_packet(0x6a, packet) + + def cancel_sweep_frequency(self): + packet = bytearray(16) + packet[0] = 0x1e self.send_packet(0x6a, packet) def check_frequency(self): @@ -541,7 +546,7 @@ def check_temperature(self): temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 return temp -# For legay compatibility - don't use this +# For legacy compatibility - don't use this class rm2(rm): def __init__ (self): device.__init__(self, None, None) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 3acf168a..dca0a3d6 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -1,7 +1,6 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import broadlink -import sys import argparse import time @@ -71,6 +70,7 @@ parser.add_argument("--temperature",action="store_true", help="request temperatu 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("--learnfile", help="learn command and save to specified file") parser.add_argument("--durations", action="store_true", help="use durations in micro seconds instead of the Broadlink format") parser.add_argument("--convert", action="store_true", help="convert input data to durations") @@ -78,8 +78,8 @@ parser.add_argument("data", nargs='*', help="Data to send or convert") args = parser.parse_args() if args.device: - values = args.device.split(); - type = int(values[0],0) + values = args.device.split() + type = int(values[0], 0) host = values[1] mac = bytearray.fromhex(values[2]) elif args.mac: @@ -87,16 +87,16 @@ elif args.mac: host = args.host mac = bytearray.fromhex(args.mac) -if args.host: +if args.host or host is not None: dev = broadlink.gendevice(type, (host, 80), mac) dev.auth() if args.convert: data = bytearray.fromhex(''.join(args.data)) durations = to_microseconds(data) - print format_durations(durations) + print(format_durations(durations)) if args.temperature: - print dev.check_temperature() + print(dev.check_temperature()) if args.sensors: try: data = dev.check_sensors() @@ -104,7 +104,7 @@ if args.sensors: data = {} data['temperature'] = dev.check_temperature() for key in data: - print "{} {}".format(key, data[key]) + 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)) @@ -112,7 +112,7 @@ if args.send: if args.learn or args.learnfile: dev.enter_learning() data = None - print "Learning..." + print("Learning...") timeout = 30 while (data is None) and (timeout > 0): time.sleep(2) @@ -123,11 +123,55 @@ if args.learn or args.learnfile: if args.durations \ else ''.join(format(x, '02x') for x in bytearray(data)) if args.learn: - print learned + print(learned) if args.learnfile: - print "Saving to {}".format(args.learnfile) + print("Saving to {}".format(args.learnfile)) with open(args.learnfile, "w") as text_file: text_file.write(learned) else: - print "No data received..." - + print("No data received...") +if args.rfscanlearn: + dev.sweep_frequency() + print("Learning RF Frequency, press and hold the button to learn...") + + timeout = 20 + + while (not dev.check_frequency()) and (timeout > 0): + time.sleep(1) + timeout -= 1 + + if timeout <= 0: + print("RF Frequency not found") + dev.cancel_sweep_frequency() + exit(1) + + print("Found RF Frequency - 1 of 2!") + print("You can now let go of the button") + + input("Press any key to continue...") + + print("To complete learning, single press the button you want to learn") + + dev.find_rf_packet() + + data = None + timeout = 20 + + while (data is None) and (timeout > 0): + time.sleep(1) + timeout -= 1 + data = dev.check_data() + + if data: + print("Found RF Frequency - 2 of 2!") + learned = format_durations(to_microseconds(bytearray(data))) \ + if args.durations \ + else ''.join(format(x, '02x') for x in bytearray(data)) + if args.learn | args.rfscanlearn: + print(learned) + if args.learnfile: + print("Saving to {}".format(args.learnfile)) + with open(args.learnfile, "w") as text_file: + text_file.write(learned) + else: + print("No data received...") \ No newline at end of file From 51ff890c7ca7f5569497b47db6497e908a94267a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Lakato=C5=A1?= Date: Thu, 8 Feb 2018 13:47:28 +0100 Subject: [PATCH 028/261] modified get_energy() to support python3 --- broadlink/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index dbabce0b..50d9f940 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -397,7 +397,10 @@ def get_energy(self): err = response[0x22] | (response[0x23] << 8) if err == 0: payload = self.decrypt(bytes(response[0x38:])) - energy = int(hex(ord(payload[7]) * 256 + ord(payload[6]))[2:]) + int(hex(ord(payload[5]))[2:])/100.0 + if type(payload[0x07]) == int: + energy = int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:])/100.0 + else: + energy = int(hex(ord(payload[0x07]) * 256 + ord(payload[0x06]))[2:]) + int(hex(ord(payload[0x05]))[2:])/100.0 return energy From 38aa6dc37e28d7a5ab1c91b120baa76dcb8dd754 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Feb 2018 23:56:16 -0800 Subject: [PATCH 029/261] Depend on pycryptodome instead of pycrypto --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index f7d3e237..dc83835d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pycrypto==2.6.1 +pycryptodome==3.4.11 diff --git a/setup.py b/setup.py index f8d1f4ca..c5c7e267 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ import pyaes dynamic_requires = ["pyaes==1.6.0"] except ImportError as e: - dynamic_requires = ['pycrypto==2.6.1'] + dynamic_requires = ['pycryptodome==3.4.11'] version = 0.6 From d186332b965c32a7f6defd4eb340b46901d99c82 Mon Sep 17 00:00:00 2001 From: Steven Barthen Date: Sat, 10 Mar 2018 21:10:33 +0900 Subject: [PATCH 030/261] add new rmpro models - Pro Plus 3, Pro Plus 300, Pro Plus HYC, Pro Plus R1, Pro PP --- broadlink/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index dbabce0b..f85d4e14 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -48,8 +48,18 @@ def gendevice(devtype, host, mac): return rm(host=host, mac=mac) elif devtype == 0x2787: # RM2 Pro Plus2 return rm(host=host, mac=mac) + elif devtype == 0x279d: # RM2 Pro Plus3 + return rm(host=host, mac=mac) + elif devtype == 0x27a9: # RM2 Pro Plus_300 + return rm(host=host, mac=mac) elif devtype == 0x278b: # RM2 Pro Plus BL return rm(host=host, mac=mac) + elif devtype == 0x2797: # RM2 Pro Plus HYC + return rm(host=host, mac=mac) + elif devtype == 0x27a1: # RM2 Pro Plus R1 + return rm(host=host, mac=mac) + elif devtype == 0x27a6: # RM2 Pro PP + return rm(host=host, mac=mac) elif devtype == 0x278f: # RM Mini Shate return rm(host=host, mac=mac) elif devtype == 0x2714: # A1 From 26109aac67dc39b1c4dff3d1e971d463fe221847 Mon Sep 17 00:00:00 2001 From: marconfus Date: Sun, 18 Mar 2018 22:54:17 +0100 Subject: [PATCH 031/261] Fix for environments where Crypto and pyaes are installed. (#151) If both Crypto and pyaes are installed 'pyaes' is in sys.modules(), but as it is not imported (see top) it's not available. Fix for #128 --- broadlink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 592acf4f..e9c1307d 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -167,7 +167,7 @@ def __init__(self, host, mac, timeout=10): self.type = "Unknown" self.lock = threading.Lock() - if 'pyaes' in sys.modules: + if 'pyaes' in globals(): self.encrypt = self.encrypt_pyaes self.decrypt = self.decrypt_pyaes else: From 21d96bd140359a3b360bd728b963effc6b747c4f Mon Sep 17 00:00:00 2001 From: Marcin Koperski Date: Sun, 18 Mar 2018 22:55:03 +0100 Subject: [PATCH 032/261] Add option to switch state (#160) Find lacking for an option in one line to switch a state of SP2 switches --- cli/broadlink_cli | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 097a7afb..8d055f70 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -71,6 +71,7 @@ parser.add_argument("--temperature",action="store_true", help="request temperatu parser.add_argument("--check", action="store_true", help="check current power state") parser.add_argument("--turnon", action="store_true", help="turn on device") parser.add_argument("--turnoff", action="store_true", help="turn off device") +parser.add_argument("--switch", action="store_true", help="switch state from on to off and off to 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") @@ -150,3 +151,10 @@ if args.turnoff: print '!! Still ON !!' else: print '== Turned * OFF * ==' +if args.switch: + if dev.check_power(): + dev.set_power(False) + print '* Switch to OFF *' + else: + dev.set_power(True) + print '* Switch to ON *' From 8754493951b887e850d93100898d6883443b124f Mon Sep 17 00:00:00 2001 From: Nightreaver Date: Mon, 19 Mar 2018 06:58:15 +0900 Subject: [PATCH 033/261] broadlink_discovery returns wrong devtype (#157) * few item in gendevice use `if` instead of `elif` * passing `devtype` back to device change `cli/broadlink_discovery` to display proper devtype --- broadlink/__init__.py | 93 +++++++++++++++++++++-------------------- cli/broadlink_discovery | 8 ++-- 2 files changed, 50 insertions(+), 51 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index e9c1307d..979e6080 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -15,63 +15,63 @@ def gendevice(devtype, host, mac): if devtype == 0: # SP1 - return sp1(host=host, mac=mac) - if devtype == 0x2711: # SP2 - return sp2(host=host, mac=mac) - if devtype == 0x2719 or devtype == 0x7919 or devtype == 0x271a or devtype == 0x791a: # Honeywell SP2 - return sp2(host=host, mac=mac) - if devtype == 0x2720: # SPMini - return sp2(host=host, mac=mac) + return sp1(host=host, mac=mac, devtype=devtype) + elif devtype == 0x2711: # SP2 + return sp2(host=host, mac=mac, devtype=devtype) + elif devtype == 0x2719 or devtype == 0x7919 or devtype == 0x271a or devtype == 0x791a: # Honeywell SP2 + return sp2(host=host, mac=mac, devtype=devtype) + elif devtype == 0x2720: # SPMini + return sp2(host=host, mac=mac, devtype=devtype) elif devtype == 0x753e: # SP3 - return sp2(host=host, mac=mac) + return sp2(host=host, mac=mac, devtype=devtype) elif devtype == 0x947a or devtype == 0x9479: # SP3S - return sp2(host=host, mac=mac) + return sp2(host=host, mac=mac, devtype=devtype) elif devtype == 0x2728: # SPMini2 - return sp2(host=host, mac=mac) + return sp2(host=host, mac=mac, devtype=devtype) elif devtype == 0x2733 or devtype == 0x273e: # OEM branded SPMini - return sp2(host=host, mac=mac) + return sp2(host=host, mac=mac, devtype=devtype) elif devtype >= 0x7530 and devtype <= 0x7918: # OEM branded SPMini2 - return sp2(host=host, mac=mac) + return sp2(host=host, mac=mac, devtype=devtype) elif devtype == 0x2736: # SPMiniPlus - return sp2(host=host, mac=mac) + return sp2(host=host, mac=mac, devtype=devtype) elif devtype == 0x2712: # RM2 - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x2737: # RM Mini - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x273d: # RM Pro Phicomm - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x2783: # RM2 Home Plus - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x277c: # RM2 Home Plus GDT - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x272a: # RM2 Pro Plus - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x2787: # RM2 Pro Plus2 - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x279d: # RM2 Pro Plus3 - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x27a9: # RM2 Pro Plus_300 - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x278b: # RM2 Pro Plus BL - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x2797: # RM2 Pro Plus HYC - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x27a1: # RM2 Pro Plus R1 - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x27a6: # RM2 Pro PP - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x278f: # RM Mini Shate - return rm(host=host, mac=mac) + return rm(host=host, mac=mac, devtype=devtype) elif devtype == 0x2714: # A1 - return a1(host=host, mac=mac) + return a1(host=host, mac=mac, devtype=devtype) elif devtype == 0x4EB5 or devtype == 0x4EF7: # MP1: 0x4eb5, honyar oem mp1: 0x4ef7 - return mp1(host=host, mac=mac) + return mp1(host=host, mac=mac, devtype=devtype) elif devtype == 0x2722: # S1 (SmartOne Alarm Kit) - return S1C(host=host, mac=mac) + return S1C(host=host, mac=mac, devtype=devtype) elif devtype == 0x4E4D: # Dooya DT360E (DOOYA_CURTAIN_V2) - return dooya(host=host, mac=mac) + return dooya(host=host, mac=mac, devtype=devtype) else: - return device(host=host, mac=mac) + return device(host=host, mac=mac, devtype=devtype) def discover(timeout=None, local_ip_address=None): if local_ip_address is None: @@ -152,9 +152,10 @@ def discover(timeout=None, local_ip_address=None): class device: - def __init__(self, host, mac, timeout=10): + def __init__(self, host, mac, devtype, timeout=10): self.host = host self.mac = mac + self.devtype = devtype self.timeout = timeout self.count = random.randrange(0xffff) self.key = bytearray([0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) @@ -302,8 +303,8 @@ def send_packet(self, command, payload): class mp1(device): - def __init__ (self, host, mac): - device.__init__(self, host, mac) + def __init__ (self, host, mac, devtype): + device.__init__(self, host, mac, devtype) self.type = "MP1" def set_power_mask(self, sid_mask, state): @@ -365,8 +366,8 @@ def check_power(self): class sp1(device): - def __init__ (self, host, mac): - device.__init__(self, host, mac) + def __init__ (self, host, mac, devtype): + device.__init__(self, host, mac, devtype) self.type = "SP1" def set_power(self, state): @@ -376,8 +377,8 @@ def set_power(self, state): class sp2(device): - def __init__ (self, host, mac): - device.__init__(self, host, mac) + def __init__ (self, host, mac, devtype): + device.__init__(self, host, mac, devtype) self.type = "SP2" def set_power(self, state): @@ -415,8 +416,8 @@ def get_energy(self): class a1(device): - def __init__ (self, host, mac): - device.__init__(self, host, mac) + def __init__ (self, host, mac, devtype): + device.__init__(self, host, mac, devtype) self.type = "A1" def check_sensors(self): @@ -493,8 +494,8 @@ def check_sensors_raw(self): class rm(device): - def __init__ (self, host, mac): - device.__init__(self, host, mac) + def __init__ (self, host, mac, devtype): + device.__init__(self, host, mac, devtype) self.type = "RM2" def check_data(self): @@ -532,7 +533,7 @@ def check_temperature(self): # For legay compatibility - don't use this class rm2(rm): def __init__ (self): - device.__init__(self, None, None) + device.__init__(self, None, None, None) def discover(self): dev = discover() @@ -597,8 +598,8 @@ def get_sensors_status(self): class dooya(device): - def __init__ (self, host, mac): - device.__init__(self, host, mac) + def __init__ (self, host, mac, devtype): + device.__init__(self, host, mac, devtype) self.type = "Dooya DT360E" def _send(self, magic1, magic2): diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery index 4a7438df..385f1932 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -8,17 +8,15 @@ parser = argparse.ArgumentParser(fromfile_prefix_chars='@'); parser.add_argument("--timeout", type=int, default=5, help="timeout to wait for receiving discovery responses") args = parser.parse_args() -print "discover" +print "Discovering..." devices = broadlink.discover(timeout=args.timeout) -#print devices for device in devices: if device.auth(): print "###########################################" -# print device print device.type - print "# broadlink_cli --type 0x2712 --host {} --mac {}".format(device.host[0], ''.join(format(x, '02x') for x in device.mac)) + print "# broadlink_cli --type {} --host {} --mac {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac)) print "Device file data (to be used with --device @filename in broadlink_cli) : " - print "0x2712 {} {}".format(device.host[0], ''.join(format(x, '02x') for x in device.mac)) + print "{} {} {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac)) if hasattr(device, 'check_temperature'): print "temperature = {}".format(device.check_temperature()) print "" From 39cc64efcea723cdc5e97220bd345535f558bb8f Mon Sep 17 00:00:00 2001 From: Peter Windridge Date: Sun, 18 Mar 2018 22:03:26 +0000 Subject: [PATCH 034/261] Basic support for Hysen Heating Controller (dev type 0x4ead) (#138) * Initial support for Hysen heating controller device. Only gets current temperature. * Add switch_to_auto() to put the controller in (pre-programmed) timed mode * Add set_temp() to manually set temperature. Now requires PyCRC (payload needs modbus CRC16) * Remove test script * Get current timer schedule * Get much more data from device * Add PyCRC to install_requires setup.py * Rewrite based on better understanding. Allow setting schedule and changing 'loop mode' * Add set_time function * Support advanced settings and perform CRC check on responses * Explain remaining unknowns for Hyson thermostat The room_temp_adj (or simply 'adj') only applies to the room_temp. It's limited to -5.0..+5.0, but uses a 2 byte data type. This leads to the assumption that external_temp could also use this data type, maybe for showing temperatures below 0 - but I cannot test this currently. Maybe I have to place it near a fridge to confirm. * Fix get_temp and add get_external_temp for Hysen Again: maybe payload[17] also belongs to the external temperature... * remove comment about first 2 bytes and raise error if CRC check on response fails * Remove comment about guessed meaning of unknown Just confirmed, that lowest outside temp is 0. So it seems to only need 1 byte, as room temp does. * add ability to toogle hysen device power Turn display power on/off * Update set_power() to support remote_lock for Hysen Sorry, there was still one thing missing: set/unset remote_lock. I captured again and changed the set_power accordingly. * fix comments --- broadlink/__init__.py | 173 +++++++++++++++++++++++++++++++++++++++++- setup.py | 3 + 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 979e6080..2af62aab 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -66,6 +66,8 @@ def gendevice(devtype, host, mac): return a1(host=host, mac=mac, devtype=devtype) elif devtype == 0x4EB5 or devtype == 0x4EF7: # MP1: 0x4eb5, honyar oem mp1: 0x4ef7 return mp1(host=host, mac=mac, devtype=devtype) + elif devtype == 0x4EAD: # Hysen controller + return hysen(host=host, mac=mac) elif devtype == 0x2722: # S1 (SmartOne Alarm Kit) return S1C(host=host, mac=mac, devtype=devtype) elif devtype == 0x4E4D: # Dooya DT360E (DOOYA_CURTAIN_V2) @@ -134,6 +136,8 @@ def discover(timeout=None, local_ip_address=None): host = response[1] mac = responsepacket[0x3a:0x40] devtype = responsepacket[0x34] | responsepacket[0x35] << 8 + + return gendevice(devtype, host, mac) else: while (time.time() - starttime) < timeout: @@ -151,6 +155,7 @@ def discover(timeout=None, local_ip_address=None): return devices + class device: def __init__(self, host, mac, devtype, timeout=10): self.host = host @@ -231,6 +236,7 @@ def auth(self): self.id = payload[0x00:0x04] self.key = key + return True def get_type(self): @@ -530,7 +536,8 @@ def check_temperature(self): temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 return temp -# For legay compatibility - don't use this + +# For legacy compatibility - don't use this class rm2(rm): def __init__ (self): device.__init__(self, None, None, None) @@ -541,6 +548,169 @@ def discover(self): self.mac = dev.mac +class hysen(device): + def __init__ (self, host, mac, devtype): + device.__init__(self, host, mac, devtype) + self.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): + + from PyCRC.CRC16 import CRC16 + crc = CRC16(modbus_flag=True).calculate(bytes(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) + + # send to device + response = self.send_packet(0x6a, request_payload) + + # check for error + err = response[0x22] | (response[0x23] << 8) + if err: + raise ValueError('broadlink_response_error',err) + + response_payload = bytearray(self.decrypt(bytes(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): + raise ValueError('hysen_response_error','first byte of response is not length') + crc = CRC16(modbus_flag=True).calculate(bytes(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] + else: + raise ValueError('hysen_response_error','CRC check on response failed') + + + # Get current room temperature in degrees celsius + def get_temp(self): + payload = self.send_request(bytearray([0x01,0x03,0x00,0x00,0x00,0x08])) + return payload[0x05] / 2.0 + + # Get current external temperature in degrees celsius + def get_external_temp(self): + payload = self.send_request(bytearray([0x01,0x03,0x00,0x00,0x00,0x08])) + return payload[18] / 2.0 + + # Get full status (including timer schedule) + def get_full_status(self): + payload = self.send_request(bytearray([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['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['fre'] = payload[15] + data['poweron'] = payload[16] + data['unknown'] = payload[17] + data['external_temp'] = (payload[18] & 255)/2.0 + data['hour'] = payload[19] + data['min'] = payload[20] + data['sec'] = payload[21] + data['dayofweek'] = payload[22] + + weekday = [] + for i in range(0, 6): + weekday.append({'start_hour':payload[2*i + 23], 'start_minute':payload[2*i + 24],'temp':payload[i + 39]/2.0}) + + data['weekday'] = weekday + weekend = [] + for i in range(6, 8): + weekend.append({'start_hour':payload[2*i + 23], 'start_minute':payload[2*i + 24],'temp':payload[i + 39]/2.0}) + + data['weekend'] = weekend + return data + + # Change controller mode + # auto_mode = 1 for auto (scheduled/timed) mode, 0 for manual mode. + # 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 + # The sensor command is currently experimental + def set_mode(self, auto_mode, loop_mode,sensor=0): + mode_byte = ( (loop_mode + 1) << 4) + auto_mode + # print 'Mode byte: 0x'+ format(mode_byte, '02x') + self.send_request(bytearray([0x01,0x06,0x00,0x02,mode_byte,sensor])) + + def set_advanced(self, loop_mode, sensor, osv, dif, svh, svl, adj, fre, poweron): + input_payload = bytearray([0x01,0x10,0x00,0x02,0x00,0x05,0x0a, loop_mode, sensor, osv, dif, svh, svl, (int(adj*2)>>8 & 0xff), (int(adj*2) & 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. + def switch_to_auto(self): + self.set_mode(auto_mode=1, loop_mode=0) + + def switch_to_manual(self): + self.set_mode(auto_mode=0, loop_mode=0) + + # Set temperature for manual mode (also activates manual mode if currently in automatic) + def set_temp(self, temp): + self.send_request(bytearray([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=1, remote_lock=0): + self.send_request(bytearray([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, minute, second, day): + self.send_request(bytearray([0x01,0x10,0x00,0x08,0x00,0x02,0x04, hour, minute, second, day ])) + + # Set timer schedule + # Format is the same as you get from get_full_status. + # weekday is a list (ordered) of 6 dicts like: + # {'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,weekend): + # Begin with some magic values ... + input_payload = bytearray([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'] ) + + # weekend times + for i in range(0, 2): + input_payload.append( weekend[i]['start_hour'] ) + input_payload.append( weekend[i]['start_minute'] ) + + # weekday temperatures + for i in range(0, 6): + input_payload.append( int(weekday[i]['temp'] * 2) ) + + # weekend temperatures + for i in range(0, 2): + input_payload.append( int(weekend[i]['temp'] * 2) ) + + self.send_request(input_payload) + + S1C_SENSORS_TYPES = { 0x31: 'Door Sensor', # 49 as hex 0x91: 'Key Fob', # 145 as hex, as serial on fob corpse @@ -643,6 +813,7 @@ def set_percentage_and_wait(self, new_percentage): current = self.get_percentage() self.stop() + # 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, password, security_mode): diff --git a/setup.py b/setup.py index c5c7e267..b982a11a 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,9 @@ except ImportError as e: dynamic_requires = ['pycryptodome==3.4.11'] +# For Hysen thermostatic heating controller +dynamic_requires.append('PyCRC') + version = 0.6 setup( From 33a2e4ae54e996d37c814494f5da629fb5428f96 Mon Sep 17 00:00:00 2001 From: Nightreaver Date: Mon, 19 Mar 2018 07:03:46 +0900 Subject: [PATCH 035/261] implemented method to toggle nightlight on some SP3 devices (#159) * implemented method to toggle nightlight on some SP3 devices * implement nightlight feature to cli * check_power/check_nightligh fixes for py2.7 --- broadlink/__init__.py | 35 +++++++++++++++++++++++++++++++---- cli/broadlink_cli | 20 ++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 2af62aab..910184a0 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -391,7 +391,20 @@ def set_power(self, state): """Sets the power state of the smart plug.""" packet = bytearray(16) packet[0] = 2 - packet[4] = 1 if state else 0 + if self.check_nightlight(): + packet[4] = 3 if state else 2 + else: + packet[4] = 1 if state else 0 + self.send_packet(0x6a, packet) + + def set_nightlight(self, state): + """Sets the night light state of the smart plug""" + 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 self.send_packet(0x6a, packet) def check_power(self): @@ -402,10 +415,24 @@ def check_power(self): err = response[0x22] | (response[0x23] << 8) if err == 0: payload = self.decrypt(bytes(response[0x38:])) - if type(payload[0x4]) == int: - state = bool(payload[0x4]) + if ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3: + state = True + else: + state = False + return state + + def check_nightlight(self): + """Returns the power state of the smart plug.""" + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err == 0: + payload = self.decrypt(bytes(response[0x38:])) + if ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3: + state = True else: - state = bool(ord(payload[0x4])) + state = False return state def get_energy(self): diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 8d055f70..4c0c5d7a 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -69,8 +69,11 @@ 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("--check", action="store_true", help="check current power state") +parser.add_argument("--checknl", action="store_true", help="check current nightlight state") parser.add_argument("--turnon", action="store_true", help="turn on device") parser.add_argument("--turnoff", action="store_true", help="turn off device") +parser.add_argument("--turnnlon", action="store_true", help="turn on nightlight on the device") +parser.add_argument("--turnnloff", action="store_true", help="turn off nightlight on the device") parser.add_argument("--switch", action="store_true", help="switch state from on to off and off to on") parser.add_argument("--send", action="store_true", help="send command") parser.add_argument("--sensors", action="store_true", help="check all sensors") @@ -139,6 +142,11 @@ if args.check: print '* ON *' else: print '* OFF *' +if args.checknl: + if dev.check_nightlight(): + print '* ON *' + else: + print '* OFF *' if args.turnon: dev.set_power(True) if dev.check_power(): @@ -151,6 +159,18 @@ if args.turnoff: print '!! Still ON !!' else: print '== Turned * OFF * ==' +if args.turnnlon: + dev.set_nightlight(True) + if dev.check_nightlight(): + print '== Turned * ON * ==' + else: + print '!! Still OFF !!' +if args.turnnloff: + dev.set_nightlight(False) + if dev.check_nightlight(): + print '!! Still ON !!' + else: + print '== Turned * OFF * ==' if args.switch: if dev.check_power(): dev.set_power(False) From 8d3c9f1e9f7ad19f1c054ca3278670c8c9ea0eb1 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 18 Mar 2018 14:51:34 -0700 Subject: [PATCH 036/261] Version 0.7 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index b982a11a..a79afb55 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') -version = 0.6 +version = 0.7 setup( name='broadlink', - version=0.6, + version=0.7, author='Matthew Garrett', author_email='mjg59@srcf.ucam.org', url='http://github.com/mjg59/python-broadlink', From 8b593beff968caf47bf19e703e8fc3b1df3d5e6a Mon Sep 17 00:00:00 2001 From: clach04 Date: Sun, 18 Mar 2018 15:12:20 -0700 Subject: [PATCH 037/261] Restore Python 3 support (#95) Use explicit byte literals. AES encryption padding broken with commit f7e30344c5748e37924c8a4f28522c2bc99a52ee under Python 3. Python 3 support when using pyaes (instead of Crypto.Cipher.AES) now works. --- broadlink/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 910184a0..6a6636d1 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -182,11 +182,11 @@ def __init__(self, host, mac, devtype, timeout=10): def encrypt_pyaes(self, payload): aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv)) - return "".join([aes.encrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) + return b"".join([aes.encrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) def decrypt_pyaes(self, payload): aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv)) - return "".join([aes.decrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) + return b"".join([aes.decrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) def encrypt_pycrypto(self, payload): aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) @@ -272,7 +272,7 @@ def send_packet(self, command, payload): # pad the payload for AES encryption if len(payload)>0: numpad=(len(payload)//16+1)*16 - payload=payload.ljust(numpad,b"\x00") + payload=payload.ljust(numpad, b"\x00") checksum = 0xbeaf for i in range(len(payload)): From 9286d9a1d9d133d9897becf3898025d822a7d841 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 18 Mar 2018 15:12:47 -0700 Subject: [PATCH 038/261] Version 0.8 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a79afb55..a9382c10 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') -version = 0.7 +version = 0.8 setup( name='broadlink', - version=0.7, + version=0.8, author='Matthew Garrett', author_email='mjg59@srcf.ucam.org', url='http://github.com/mjg59/python-broadlink', From 53b213ea85e0b7c9052f90ba4e5c3289097e10e9 Mon Sep 17 00:00:00 2001 From: Michael Still Date: Mon, 19 Mar 2018 18:19:51 +1100 Subject: [PATCH 039/261] Add the RM3 mini to the supported device list. I picked one of these up cheap on ebay and it totally works. --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1305ea0f..6cc12369 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ Python control for Broadlink RM2 IR controllers =============================================== -A simple Python API for controlling IR controllers from [Broadlink](http://www.ibroadlink.com/rm/). At present, only RM Pro (referred to as RM2 in the codebase) and A1 sensor platform devices are supported. There is currently no support for the cloud API. +A simple Python API for controlling IR controllers from [Broadlink](http://www.ibroadlink.com/rm/). At present, the following devices are currently supported: + +* RM Pro (referred to as RM2 in the codebase) +* A1 sensor platform devices are supported +* RM3 mini IR blaster + +There is currently no support for the cloud API. Example use ----------- @@ -76,4 +82,4 @@ devices[0].set_power(1, True) Check power state on a SmartPowerStrip: ``` state = devices[0].check_power() -``` \ No newline at end of file +``` From e9748ff0b4f36da2722ef0f84b812069812634af Mon Sep 17 00:00:00 2001 From: Aleksey Bogomolov Date: Sat, 24 Mar 2018 19:05:44 +0300 Subject: [PATCH 040/261] fixed Hysen thermostat ctor & added comments --- broadlink/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 6a6636d1..48e936ba 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -67,7 +67,7 @@ def gendevice(devtype, host, mac): elif devtype == 0x4EB5 or devtype == 0x4EF7: # MP1: 0x4eb5, honyar oem mp1: 0x4ef7 return mp1(host=host, mac=mac, devtype=devtype) elif devtype == 0x4EAD: # Hysen controller - return hysen(host=host, mac=mac) + return hysen(host=host, mac=mac, devtype=devtype) elif devtype == 0x2722: # S1 (SmartOne Alarm Kit) return S1C(host=host, mac=mac, devtype=devtype) elif devtype == 0x4E4D: # Dooya DT360E (DOOYA_CURTAIN_V2) @@ -682,6 +682,15 @@ def set_mode(self, auto_mode, loop_mode,sensor=0): # print 'Mode byte: 0x'+ format(mode_byte, '02x') self.send_request(bytearray([0x01,0x06,0x00,0x02,mode_byte,sensor])) + # Advanced settings + # Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor, 2 for internal control temperature, external limit temperature. Factory default: 0. + # Set temperature range for external sensor (OSV) osv = 5..99. Factory default: 42C + # 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 + # 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 def set_advanced(self, loop_mode, sensor, osv, dif, svh, svl, adj, fre, poweron): input_payload = bytearray([0x01,0x10,0x00,0x02,0x00,0x05,0x0a, loop_mode, sensor, osv, dif, svh, svl, (int(adj*2)>>8 & 0xff), (int(adj*2) & 0xff), fre, poweron]) self.send_request(input_payload) From 832a40c278c91cd0409dcc71fa2976bea37a25a4 Mon Sep 17 00:00:00 2001 From: ooo89 <37841599+ooo89@users.noreply.github.com> Date: Wed, 28 Mar 2018 16:56:36 +0200 Subject: [PATCH 041/261] Update __init__.py --- broadlink/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 6a6636d1..5dc29192 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -24,6 +24,8 @@ def gendevice(devtype, host, mac): return sp2(host=host, mac=mac, devtype=devtype) elif devtype == 0x753e: # SP3 return sp2(host=host, mac=mac, devtype=devtype) + elif devtype == 0x7D00: # OEM branded SP3 + return sp2(host=host, mac=mac, devtype=devtype) elif devtype == 0x947a or devtype == 0x9479: # SP3S return sp2(host=host, mac=mac, devtype=devtype) elif devtype == 0x2728: # SPMini2 From 90af3b743719e84ed158448534267857a46fa794 Mon Sep 17 00:00:00 2001 From: lordneon Date: Sun, 15 Apr 2018 10:19:05 +0000 Subject: [PATCH 042/261] Fixed a bug within the SP2 class. check_power and check_nightlight did not check to see if the payload was already an int before calling ord. --- broadlink/__init__.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a3fadf5d..a5415c23 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -417,10 +417,16 @@ def check_power(self): err = response[0x22] | (response[0x23] << 8) if err == 0: payload = self.decrypt(bytes(response[0x38:])) - if ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3: - state = True + if type(payload[0x4]) == int: + if payload[0x4] == 1 or payload[0x4] == 3: + state = True + else: + state = False else: - state = False + if ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3: + state = True + else: + state = False return state def check_nightlight(self): @@ -431,10 +437,16 @@ def check_nightlight(self): err = response[0x22] | (response[0x23] << 8) if err == 0: payload = self.decrypt(bytes(response[0x38:])) - if ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3: - state = True + if type(payload[0x4]) == int: + if payload[0x4] == 2 or payload[0x4] == 3: + state = True + else: + state = False else: - state = False + if ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3: + state = True + else: + state = False return state def get_energy(self): From e724aec007680efbe07f871cd76070c1abeb1712 Mon Sep 17 00:00:00 2001 From: lordneon Date: Sun, 15 Apr 2018 10:19:05 +0000 Subject: [PATCH 043/261] Fixed a bug within the SP2 class. check_power and check_nightlight did not check to see if the payload was already an int before calling ord. --- broadlink/__init__.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a3fadf5d..a5415c23 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -417,10 +417,16 @@ def check_power(self): err = response[0x22] | (response[0x23] << 8) if err == 0: payload = self.decrypt(bytes(response[0x38:])) - if ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3: - state = True + if type(payload[0x4]) == int: + if payload[0x4] == 1 or payload[0x4] == 3: + state = True + else: + state = False else: - state = False + if ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3: + state = True + else: + state = False return state def check_nightlight(self): @@ -431,10 +437,16 @@ def check_nightlight(self): err = response[0x22] | (response[0x23] << 8) if err == 0: payload = self.decrypt(bytes(response[0x38:])) - if ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3: - state = True + if type(payload[0x4]) == int: + if payload[0x4] == 2 or payload[0x4] == 3: + state = True + else: + state = False else: - state = False + if ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3: + state = True + else: + state = False return state def get_energy(self): From b2c4bed94a3fb942797bba6c46717edf8187be1f Mon Sep 17 00:00:00 2001 From: Tocho Tochev Date: Thu, 19 Apr 2018 23:45:50 +0300 Subject: [PATCH 044/261] Add energy option to broadlink_cli --- README.md | 5 +++++ cli/README.md | 5 +++++ cli/broadlink_cli | 3 +++ 3 files changed, 13 insertions(+) diff --git a/README.md b/README.md index 6cc12369..74473b8e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,11 @@ Check power state on a SmartPlug: state = devices[0].check_power() ``` +Check energy consumption on a SmartPlug: +``` +state = devices[0].get_energy() +``` + Set power state for S1 on a SmartPowerStrip MP1: ``` devices[0].set_power(1, True) diff --git a/cli/README.md b/cli/README.md index 47b45f4c..a04b6554 100644 --- a/cli/README.md +++ b/cli/README.md @@ -73,3 +73,8 @@ Get Temperature : ``` broadlink_cli --device @BEDROOM.device --temperature ``` + +Get Energy Consumption (For a SmartPlug) : +``` +broadlink_cli --device @BEDROOM.device --energy +``` diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 4c0c5d7a..4b0e81f6 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -68,6 +68,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("--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") parser.add_argument("--turnon", action="store_true", help="turn on device") @@ -104,6 +105,8 @@ if args.convert: print format_durations(durations) if args.temperature: print dev.check_temperature() +if args.energy: + print dev.get_energy() if args.sensors: try: data = dev.check_sensors() From 766b7b00fb1cec868e3d5fca66f1aada208959ce Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Tue, 24 Apr 2018 08:33:05 -0700 Subject: [PATCH 045/261] Version 0.9 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a9382c10..23fb5de1 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') -version = 0.8 +version = 0.9 setup( name='broadlink', - version=0.8, + version=0.9, author='Matthew Garrett', author_email='mjg59@srcf.ucam.org', url='http://github.com/mjg59/python-broadlink', From 56b2ac36e5a2359272f4af8a49cfaf3e1891733a Mon Sep 17 00:00:00 2001 From: Mayeul Cantan Date: Mon, 30 Apr 2018 23:06:19 +0200 Subject: [PATCH 046/261] Refactor the device list It is now more readable, which should make it easier to parse the code and add new devices. --- broadlink/__init__.py | 103 +++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 61 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a5415c23..9dccc8d4 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -13,69 +13,50 @@ import threading import codecs + def gendevice(devtype, host, mac): - if devtype == 0: # SP1 - return sp1(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2711: # SP2 - return sp2(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2719 or devtype == 0x7919 or devtype == 0x271a or devtype == 0x791a: # Honeywell SP2 - return sp2(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2720: # SPMini - return sp2(host=host, mac=mac, devtype=devtype) - elif devtype == 0x753e: # SP3 - return sp2(host=host, mac=mac, devtype=devtype) - elif devtype == 0x7D00: # OEM branded SP3 - return sp2(host=host, mac=mac, devtype=devtype) - elif devtype == 0x947a or devtype == 0x9479: # SP3S - return sp2(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2728: # SPMini2 - return sp2(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2733 or devtype == 0x273e: # OEM branded SPMini - return sp2(host=host, mac=mac, devtype=devtype) - elif devtype >= 0x7530 and devtype <= 0x7918: # OEM branded SPMini2 - return sp2(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2736: # SPMiniPlus - return sp2(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2712: # RM2 - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2737: # RM Mini - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x273d: # RM Pro Phicomm - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2783: # RM2 Home Plus - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x277c: # RM2 Home Plus GDT - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x272a: # RM2 Pro Plus - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2787: # RM2 Pro Plus2 - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x279d: # RM2 Pro Plus3 - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x27a9: # RM2 Pro Plus_300 - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x278b: # RM2 Pro Plus BL - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2797: # RM2 Pro Plus HYC - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x27a1: # RM2 Pro Plus R1 - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x27a6: # RM2 Pro PP - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x278f: # RM Mini Shate - return rm(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2714: # A1 - return a1(host=host, mac=mac, devtype=devtype) - elif devtype == 0x4EB5 or devtype == 0x4EF7: # MP1: 0x4eb5, honyar oem mp1: 0x4ef7 - return mp1(host=host, mac=mac, devtype=devtype) - elif devtype == 0x4EAD: # Hysen controller - return hysen(host=host, mac=mac, devtype=devtype) - elif devtype == 0x2722: # S1 (SmartOne Alarm Kit) - return S1C(host=host, mac=mac, devtype=devtype) - elif devtype == 0x4E4D: # Dooya DT360E (DOOYA_CURTAIN_V2) - return dooya(host=host, mac=mac, devtype=devtype) - else: + devices = { + sp1: [0], + sp2: [0x2711, # SP2 + 0x2719, 0x7919, 0x271a, 0x791a, # Honeywell SP2 + 0x2720, # SPMini + 0x753e, # SP3 + 0x7D00, # OEM branded SP3 + 0x947a, 0x9479, # SP3S + 0x2728, # SPMini2 + 0x2733, 0x273e, # OEM branded SPMini + 0x7530, 0x7918, # OEM branded SPMini2 + 0x2736 # SPMiniPlus + ], + rm: [0x2712, # RM2 + 0x2737, # RM Mini + 0x273d, # RM Pro Phicomm + 0x2783, # RM2 Home Plus + 0x277c, # RM2 Home Plus GDT + 0x272a, # RM2 Pro Plus + 0x2787, # RM2 Pro Plus2 + 0x279d, # RM2 Pro Plus3 + 0x27a9, # RM2 Pro Plus_300 + 0x278b, # RM2 Pro Plus BL + 0x2797, # RM2 Pro Plus HYC + 0x27a1, # RM2 Pro Plus R1 + 0x27a6, # RM2 Pro PP + 0x278f # RM Mini Shate + ], + a1: [0x2714], # A1 + mp1: [0x4EB5, # MP1 + 0x4EF7 # Honyar oem mp1 + ], + hysen: [0x4EAD], # Hysen controller + S1C: [0x2722], # S1 (SmartOne Alarm Kit) + dooya: [0x4E4D] # Dooya DT360E (DOOYA_CURTAIN_V2) + } + + # Look for the class associated to devtype in devices + [deviceClass] = [dev for dev in devices if devtype in devices[dev]] or [None] + if deviceClass is None: return device(host=host, mac=mac, devtype=devtype) + return deviceClass(host=host, mac=mac, devtype=devtype) def discover(timeout=None, local_ip_address=None): if local_ip_address is None: From 71fbb2bcdec24f05360b1aa2967d676f313d6ba4 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Sat, 14 Jul 2018 19:01:49 -0500 Subject: [PATCH 047/261] Remove trailing semicolons --- cli/broadlink_cli | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 4b0e81f6..54f02a07 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -62,7 +62,7 @@ def parse_durations(str): return result -parser = argparse.ArgumentParser(fromfile_prefix_chars='@'); +parser = argparse.ArgumentParser(fromfile_prefix_chars='@') parser.add_argument("--device", help="device definition as 'type host mac'") parser.add_argument("--type", type=auto_int, default=0x2712, help="type of device") parser.add_argument("--host", help="host address") @@ -86,7 +86,7 @@ parser.add_argument("data", nargs='*', help="Data to send or convert") args = parser.parse_args() if args.device: - values = args.device.split(); + values = args.device.split() type = int(values[0],0) host = values[1] mac = bytearray.fromhex(values[2]) From 8cfa02035381233c148096a319a577e9b3715c6f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Aug 2018 11:01:42 +0200 Subject: [PATCH 048/261] Update vulnerable pycryptodome --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 23fb5de1..f4482eff 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ import pyaes dynamic_requires = ["pyaes==1.6.0"] except ImportError as e: - dynamic_requires = ['pycryptodome==3.4.11'] + dynamic_requires = ['pycryptodome==3.6.6'] # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') From cd969a83f93c780d7c5d18a5ec072c1aeafd0270 Mon Sep 17 00:00:00 2001 From: clach04 Date: Thu, 30 Aug 2018 22:43:42 -0700 Subject: [PATCH 049/261] readme typo --- cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/README.md b/cli/README.md index a04b6554..5d7b3be1 100644 --- a/cli/README.md +++ b/cli/README.md @@ -13,7 +13,7 @@ You should have the broadlink python installed, this can be made in many linux d sudo pip install broadlink ``` -Instalation +Installation ----------- Just copy this files From 73fc5fc4b97f3d62fee7b25a62622cd20bd586de Mon Sep 17 00:00:00 2001 From: Nick Dimov <3619341+dimovnike@users.noreply.github.com> Date: Sat, 27 Oct 2018 16:15:47 +0300 Subject: [PATCH 050/261] Support for a new SP2 device. It returns different codes for check and checknl. --- broadlink/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 9dccc8d4..02a8e2a8 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -399,12 +399,12 @@ def check_power(self): if err == 0: payload = self.decrypt(bytes(response[0x38:])) if type(payload[0x4]) == int: - if payload[0x4] == 1 or payload[0x4] == 3: + if payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD: state = True else: state = False else: - if ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3: + if ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFD: state = True else: state = False @@ -419,12 +419,12 @@ def check_nightlight(self): if err == 0: payload = self.decrypt(bytes(response[0x38:])) if type(payload[0x4]) == int: - if payload[0x4] == 2 or payload[0x4] == 3: + if payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF: state = True else: state = False else: - if ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3: + if ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFF: state = True else: state = False From bb23c84c90110984fdcd743e60ffd9a31785fd96 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 2 Nov 2018 11:57:07 +0100 Subject: [PATCH 051/261] Update requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dc83835d..9b20c33e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pycryptodome==3.4.11 +pycryptodome==3.6.6 From 694b4423b50cc55f9a48d14a41f1615799342c43 Mon Sep 17 00:00:00 2001 From: Sergey Prilukin Date: Sun, 25 Nov 2018 23:58:33 +0200 Subject: [PATCH 052/261] fixed --learnfile argument usage, so it works for both IR and RF --- cli/broadlink_cli | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index dca0a3d6..1ff45f1d 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -61,7 +61,7 @@ def parse_durations(str): return result -parser = argparse.ArgumentParser(fromfile_prefix_chars='@'); +parser = argparse.ArgumentParser(fromfile_prefix_chars='@') parser.add_argument("--device", help="device definition as 'type host mac'") parser.add_argument("--type", type=auto_int, default=0x2712, help="type of device") parser.add_argument("--host", help="host address") @@ -70,8 +70,8 @@ parser.add_argument("--temperature",action="store_true", help="request temperatu 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("--learnfile", help="save learned command to a specified file") parser.add_argument("--rfscanlearn", action="store_true", help="rf scan learning") -parser.add_argument("--learnfile", help="learn command and save to specified file") parser.add_argument("--durations", action="store_true", help="use durations in micro seconds instead of the Broadlink format") parser.add_argument("--convert", action="store_true", help="convert input data to durations") parser.add_argument("data", nargs='*', help="Data to send or convert") @@ -109,7 +109,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: +if args.learn: dev.enter_learning() data = None print("Learning...") @@ -122,9 +122,9 @@ if args.learn or args.learnfile: learned = format_durations(to_microseconds(bytearray(data))) \ if args.durations \ else ''.join(format(x, '02x') for x in bytearray(data)) - if args.learn: + if args.learnfile is None: print(learned) - if args.learnfile: + if args.learnfile is not None: print("Saving to {}".format(args.learnfile)) with open(args.learnfile, "w") as text_file: text_file.write(learned) @@ -148,7 +148,7 @@ if args.rfscanlearn: print("Found RF Frequency - 1 of 2!") print("You can now let go of the button") - input("Press any key to continue...") + input("Press enter to continue...") print("To complete learning, single press the button you want to learn") @@ -167,9 +167,9 @@ if args.rfscanlearn: learned = format_durations(to_microseconds(bytearray(data))) \ if args.durations \ else ''.join(format(x, '02x') for x in bytearray(data)) - if args.learn | args.rfscanlearn: + if args.learnfile is None: print(learned) - if args.learnfile: + if args.learnfile is not None: print("Saving to {}".format(args.learnfile)) with open(args.learnfile, "w") as text_file: text_file.write(learned) From 9996c252f702760df218382713e05b34cf0ead1e Mon Sep 17 00:00:00 2001 From: BAN <32472039+GGBBB2000@users.noreply.github.com> Date: Tue, 8 Jan 2019 16:47:31 +0900 Subject: [PATCH 053/261] Update __init__.py --- broadlink/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 02a8e2a8..14c1a3bf 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -41,7 +41,8 @@ def gendevice(devtype, host, mac): 0x2797, # RM2 Pro Plus HYC 0x27a1, # RM2 Pro Plus R1 0x27a6, # RM2 Pro PP - 0x278f # RM Mini Shate + 0x278f, # RM Mini Shate + 0x27c2 # RM Mini 3 ], a1: [0x2714], # A1 mp1: [0x4EB5, # MP1 From c393cf6079ccaa24afd61a8e81c051251198f73b Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Wed, 16 Jan 2019 09:34:20 +0100 Subject: [PATCH 054/261] broadlink_cli: python3 support --- cli/broadlink_cli | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 54f02a07..a4d9a20d 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -102,11 +102,11 @@ if args.host or args.device: if args.convert: data = bytearray.fromhex(''.join(args.data)) durations = to_microseconds(data) - print format_durations(durations) + print(format_durations(durations)) if args.temperature: - print dev.check_temperature() + print(dev.check_temperature()) if args.energy: - print dev.get_energy() + print(dev.get_energy()) if args.sensors: try: data = dev.check_sensors() @@ -114,7 +114,7 @@ if args.sensors: data = {} data['temperature'] = dev.check_temperature() for key in data: - print "{} {}".format(key, data[key]) + 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)) @@ -122,7 +122,7 @@ if args.send: if args.learn or args.learnfile: dev.enter_learning() data = None - print "Learning..." + print("Learning...") timeout = 30 while (data is None) and (timeout > 0): time.sleep(2) @@ -133,51 +133,51 @@ if args.learn or args.learnfile: if args.durations \ else ''.join(format(x, '02x') for x in bytearray(data)) if args.learn: - print learned + print(learned) if args.learnfile: - print "Saving to {}".format(args.learnfile) + print("Saving to {}".format(args.learnfile)) with open(args.learnfile, "w") as text_file: text_file.write(learned) else: - print "No data received..." + print("No data received...") if args.check: if dev.check_power(): - print '* ON *' + print('* ON *') else: - print '* OFF *' + print('* OFF *') if args.checknl: if dev.check_nightlight(): - print '* ON *' + print('* ON *') else: - print '* OFF *' + print('* OFF *') if args.turnon: dev.set_power(True) if dev.check_power(): - print '== Turned * ON * ==' + print('== Turned * ON * ==') else: - print '!! Still OFF !!' + print('!! Still OFF !!') if args.turnoff: dev.set_power(False) if dev.check_power(): - print '!! Still ON !!' + print('!! Still ON !!') else: - print '== Turned * OFF * ==' + print('== Turned * OFF * ==') if args.turnnlon: dev.set_nightlight(True) if dev.check_nightlight(): - print '== Turned * ON * ==' + print('== Turned * ON * ==') else: - print '!! Still OFF !!' + print('!! Still OFF !!') if args.turnnloff: dev.set_nightlight(False) if dev.check_nightlight(): - print '!! Still ON !!' + print('!! Still ON !!') else: - print '== Turned * OFF * ==' + print('== Turned * OFF * ==') if args.switch: if dev.check_power(): dev.set_power(False) - print '* Switch to OFF *' + print('* Switch to OFF *') else: dev.set_power(True) - print '* Switch to ON *' + print('* Switch to ON *') From 550a01bbab41a94341dc89d6de72b8aa5812a5a8 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 17 May 2019 08:54:05 -0400 Subject: [PATCH 055/261] Bump PyPI package version The latest build of this package is still listing pycryptodome 3.4.11 as a dependency. Please trigger a new push so that these changes are reflected there. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f4482eff..a9af7823 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') -version = 0.9 +version = 0.91 setup( name='broadlink', - version=0.9, + version=0.91, author='Matthew Garrett', author_email='mjg59@srcf.ucam.org', url='http://github.com/mjg59/python-broadlink', From 77f11c8d49255d6f41c35f0aefb87d648af66cfd Mon Sep 17 00:00:00 2001 From: gpenverne Date: Fri, 10 May 2019 20:19:09 +0200 Subject: [PATCH 056/261] Allow string mac address in constructor --- broadlink/__init__.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 14c1a3bf..3d544147 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -143,7 +143,7 @@ def discover(timeout=None, local_ip_address=None): class device: def __init__(self, host, mac, devtype, timeout=10): self.host = host - self.mac = mac + self.mac = mac.encode() if isinstance(mac, str) else mac self.devtype = devtype self.timeout = timeout self.count = random.randrange(0xffff) @@ -577,19 +577,19 @@ def __init__ (self, host, mac, devtype): self.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]) + # 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): - + from PyCRC.CRC16 import CRC16 crc = CRC16(modbus_flag=True).calculate(bytes(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) @@ -599,9 +599,9 @@ def send_request(self,input_payload): # check for error err = response[0x22] | (response[0x23] << 8) - if err: + if err: raise ValueError('broadlink_response_error',err) - + response_payload = bytearray(self.decrypt(bytes(response[0x38:]))) # experimental check on CRC in response (first 2 bytes are len, and trailing bytes are crc) @@ -611,9 +611,9 @@ def send_request(self,input_payload): crc = CRC16(modbus_flag=True).calculate(bytes(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] - else: + else: raise ValueError('hysen_response_error','CRC check on response failed') - + # Get current room temperature in degrees celsius def get_temp(self): @@ -627,7 +627,7 @@ def get_external_temp(self): # Get full status (including timer schedule) def get_full_status(self): - payload = self.send_request(bytearray([0x01,0x03,0x00,0x00,0x00,0x16])) + payload = self.send_request(bytearray([0x01,0x03,0x00,0x00,0x00,0x16])) data = {} data['remote_lock'] = payload[3] & 1 data['power'] = payload[4] & 1 @@ -653,11 +653,11 @@ def get_full_status(self): data['min'] = payload[20] data['sec'] = payload[21] data['dayofweek'] = payload[22] - + weekday = [] for i in range(0, 6): weekday.append({'start_hour':payload[2*i + 23], 'start_minute':payload[2*i + 24],'temp':payload[i + 39]/2.0}) - + data['weekday'] = weekday weekend = [] for i in range(6, 8): @@ -694,7 +694,7 @@ def set_advanced(self, loop_mode, sensor, osv, dif, svh, svl, adj, fre, poweron) # For backwards compatibility only. Prefer calling set_mode directly. Note this function invokes loop_mode=0 and sensor=0. def switch_to_auto(self): self.set_mode(auto_mode=1, loop_mode=0) - + def switch_to_manual(self): self.set_mode(auto_mode=0, loop_mode=0) From 852cbc24734bf3f7ac03080aa37374276f7a2856 Mon Sep 17 00:00:00 2001 From: Bartosz Fenski Date: Sat, 18 May 2019 09:07:25 +0200 Subject: [PATCH 057/261] adding base64 output (#239) base64 output useful when working with Home-Assistant --- cli/broadlink_cli | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index a4d9a20d..9317cd19 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -134,6 +134,7 @@ if args.learn or args.learnfile: else ''.join(format(x, '02x') for x in bytearray(data)) if args.learn: print(learned) + print("Base64: " + base64.b64encode(learned.decode("hex"))) if args.learnfile: print("Saving to {}".format(args.learnfile)) with open(args.learnfile, "w") as text_file: From 0db317962348907096a29bb8d3b85c6c1d0ee35e Mon Sep 17 00:00:00 2001 From: ninstein Date: Thu, 7 Feb 2019 04:31:47 +0800 Subject: [PATCH 058/261] Update __init__.py add new device: TMall OEM SPMini3 --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 3d544147..98b6ba59 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -26,6 +26,7 @@ def gendevice(devtype, host, mac): 0x2728, # SPMini2 0x2733, 0x273e, # OEM branded SPMini 0x7530, 0x7918, # OEM branded SPMini2 + 0x7D0D, # TMall OEM SPMini3 0x2736 # SPMiniPlus ], rm: [0x2712, # RM2 From 508b0563eda70a67a04df3986c5ea95723d1c31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 18 May 2019 17:11:30 +0200 Subject: [PATCH 059/261] 0.10 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a9af7823..d85875e2 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') -version = 0.91 +version = 0.10 setup( name='broadlink', - version=0.91, + version=0.10, author='Matthew Garrett', author_email='mjg59@srcf.ucam.org', url='http://github.com/mjg59/python-broadlink', From c9a1c106a74ba0dc0b565c39ddf37939a8310935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 18 May 2019 17:13:33 +0200 Subject: [PATCH 060/261] Update setup.py --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d85875e2..00169f44 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') -version = 0.10 +version = '0.10' setup( name='broadlink', - version=0.10, + version=version, author='Matthew Garrett', author_email='mjg59@srcf.ucam.org', url='http://github.com/mjg59/python-broadlink', From a75f98720ec22e9857ef815f594952f34ed5485c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 19 May 2019 17:54:14 +0200 Subject: [PATCH 061/261] code clean up (#243) --- broadlink/__init__.py | 1664 +++++++++++++++++++++-------------------- 1 file changed, 834 insertions(+), 830 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 271a1860..582f64d7 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,6 +1,7 @@ #!/usr/bin/python from datetime import datetime + try: from Crypto.Cipher import AES except ImportError as e: @@ -9,771 +10,771 @@ import time import random import socket -import sys import threading import codecs def gendevice(devtype, host, mac): - devices = { - sp1: [0], - sp2: [0x2711, # SP2 - 0x2719, 0x7919, 0x271a, 0x791a, # Honeywell SP2 - 0x2720, # SPMini - 0x753e, # SP3 - 0x7D00, # OEM branded SP3 - 0x947a, 0x9479, # SP3S - 0x2728, # SPMini2 - 0x2733, 0x273e, # OEM branded SPMini - 0x7530, 0x7918, # OEM branded SPMini2 - 0x7D0D, # TMall OEM SPMini3 - 0x2736 # SPMiniPlus - ], - rm: [0x2712, # RM2 - 0x2737, # RM Mini - 0x273d, # RM Pro Phicomm - 0x2783, # RM2 Home Plus - 0x277c, # RM2 Home Plus GDT - 0x272a, # RM2 Pro Plus - 0x2787, # RM2 Pro Plus2 - 0x279d, # RM2 Pro Plus3 - 0x27a9, # RM2 Pro Plus_300 - 0x278b, # RM2 Pro Plus BL - 0x2797, # RM2 Pro Plus HYC - 0x27a1, # RM2 Pro Plus R1 - 0x27a6, # RM2 Pro PP - 0x278f, # RM Mini Shate - 0x27c2 # RM Mini 3 - ], - a1: [0x2714], # A1 - mp1: [0x4EB5, # MP1 - 0x4EF7 # Honyar oem mp1 - ], - hysen: [0x4EAD], # Hysen controller - S1C: [0x2722], # S1 (SmartOne Alarm Kit) - dooya: [0x4E4D] # Dooya DT360E (DOOYA_CURTAIN_V2) - } - - # Look for the class associated to devtype in devices - [deviceClass] = [dev for dev in devices if devtype in devices[dev]] or [None] - if deviceClass is None: - return device(host=host, mac=mac, devtype=devtype) - return deviceClass(host=host, mac=mac, devtype=devtype) + devices = { + sp1: [0], + sp2: [0x2711, # SP2 + 0x2719, 0x7919, 0x271a, 0x791a, # Honeywell SP2 + 0x2720, # SPMini + 0x753e, # SP3 + 0x7D00, # OEM branded SP3 + 0x947a, 0x9479, # SP3S + 0x2728, # SPMini2 + 0x2733, 0x273e, # OEM branded SPMini + 0x7530, 0x7918, # OEM branded SPMini2 + 0x7D0D, # TMall OEM SPMini3 + 0x2736 # SPMiniPlus + ], + rm: [0x2712, # RM2 + 0x2737, # RM Mini + 0x273d, # RM Pro Phicomm + 0x2783, # RM2 Home Plus + 0x277c, # RM2 Home Plus GDT + 0x272a, # RM2 Pro Plus + 0x2787, # RM2 Pro Plus2 + 0x279d, # RM2 Pro Plus3 + 0x27a9, # RM2 Pro Plus_300 + 0x278b, # RM2 Pro Plus BL + 0x2797, # RM2 Pro Plus HYC + 0x27a1, # RM2 Pro Plus R1 + 0x27a6, # RM2 Pro PP + 0x278f, # RM Mini Shate + 0x27c2 # RM Mini 3 + ], + a1: [0x2714], # A1 + mp1: [0x4EB5, # MP1 + 0x4EF7 # Honyar oem mp1 + ], + hysen: [0x4EAD], # Hysen controller + S1C: [0x2722], # S1 (SmartOne Alarm Kit) + dooya: [0x4E4D] # Dooya DT360E (DOOYA_CURTAIN_V2) + } + + # Look for the class associated to devtype in devices + [deviceClass] = [dev for dev in devices if devtype in devices[dev]] or [None] + if deviceClass is None: + return device(host=host, mac=mac, devtype=devtype) + return deviceClass(host=host, mac=mac, devtype=devtype) + def discover(timeout=None, local_ip_address=None): - if local_ip_address is None: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets - local_ip_address = s.getsockname()[0] - address = local_ip_address.split('.') - cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - cs.bind((local_ip_address,0)) - port = cs.getsockname()[1] - starttime = time.time() - - devices = [] - - timezone = int(time.timezone/-3600) - packet = bytearray(0x30) - - year = datetime.now().year - - 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 - 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 - packet[0x18] = int(address[0]) - packet[0x19] = int(address[1]) - packet[0x1a] = int(address[2]) - packet[0x1b] = int(address[3]) - packet[0x1c] = port & 0xff - packet[0x1d] = port >> 8 - packet[0x26] = 6 - checksum = 0xbeaf - - for i in range(len(packet)): - checksum += packet[i] - checksum = checksum & 0xffff - packet[0x20] = checksum & 0xff - packet[0x21] = checksum >> 8 - - cs.sendto(packet, ('255.255.255.255', 80)) - if timeout is None: - response = cs.recvfrom(1024) - responsepacket = bytearray(response[0]) - host = response[1] - mac = responsepacket[0x3a:0x40] - devtype = responsepacket[0x34] | responsepacket[0x35] << 8 - - - return gendevice(devtype, host, mac) - else: - while (time.time() - starttime) < timeout: - cs.settimeout(timeout - (time.time() - starttime)) - try: - response = cs.recvfrom(1024) - except socket.timeout: - return devices - responsepacket = bytearray(response[0]) - host = response[1] - devtype = responsepacket[0x34] | responsepacket[0x35] << 8 - mac = responsepacket[0x3a:0x40] - dev = gendevice(devtype, host, mac) - devices.append(dev) - return devices + if local_ip_address is None: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets + local_ip_address = s.getsockname()[0] + address = local_ip_address.split('.') + cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + cs.bind((local_ip_address, 0)) + port = cs.getsockname()[1] + starttime = time.time() + devices = [] + timezone = int(time.timezone / -3600) + packet = bytearray(0x30) -class device: - def __init__(self, host, mac, devtype, timeout=10): - self.host = host - self.mac = mac.encode() if isinstance(mac, str) else mac - self.devtype = devtype - self.timeout = timeout - self.count = random.randrange(0xffff) - self.key = bytearray([0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) - self.iv = bytearray([0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) - self.id = bytearray([0, 0, 0, 0]) - self.cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - self.cs.bind(('',0)) - self.type = "Unknown" - self.lock = threading.Lock() - - if 'pyaes' in globals(): - self.encrypt = self.encrypt_pyaes - self.decrypt = self.decrypt_pyaes - else: - self.encrypt = self.encrypt_pycrypto - self.decrypt = self.decrypt_pycrypto - - def encrypt_pyaes(self, payload): - aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv)) - return b"".join([aes.encrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) - - def decrypt_pyaes(self, payload): - aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv)) - return b"".join([aes.decrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) - - def encrypt_pycrypto(self, payload): - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - return aes.encrypt(bytes(payload)) - - def decrypt_pycrypto(self, payload): - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - return aes.decrypt(bytes(payload)) - - def auth(self): - 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[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') - - response = self.send_packet(0x65, payload) - - payload = self.decrypt(response[0x38:]) - - if not payload: - return False - - key = payload[0x04:0x14] - if len(key) % 16 != 0: - return False - - self.id = payload[0x00:0x04] - self.key = key - - return True - - def get_type(self): - return self.type - - def send_packet(self, command, payload): - self.count = (self.count + 1) & 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] = 0x2a - packet[0x25] = 0x27 - packet[0x26] = command - packet[0x28] = self.count & 0xff - packet[0x29] = self.count >> 8 - packet[0x2a] = self.mac[0] - packet[0x2b] = self.mac[1] - packet[0x2c] = self.mac[2] - packet[0x2d] = self.mac[3] - packet[0x2e] = self.mac[4] - packet[0x2f] = self.mac[5] - packet[0x30] = self.id[0] - packet[0x31] = self.id[1] - packet[0x32] = self.id[2] - packet[0x33] = self.id[3] - - # pad the payload for AES encryption - if len(payload)>0: - numpad=(len(payload)//16+1)*16 - payload=payload.ljust(numpad, b"\x00") + year = datetime.now().year + 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 + 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 + packet[0x18] = int(address[0]) + packet[0x19] = int(address[1]) + packet[0x1a] = int(address[2]) + packet[0x1b] = int(address[3]) + packet[0x1c] = port & 0xff + packet[0x1d] = port >> 8 + packet[0x26] = 6 checksum = 0xbeaf - for i in range(len(payload)): - checksum += payload[i] - checksum = checksum & 0xffff - - payload = self.encrypt(payload) - packet[0x34] = checksum & 0xff - packet[0x35] = checksum >> 8 - - for i in range(len(payload)): - packet.append(payload[i]) - - checksum = 0xbeaf for i in range(len(packet)): - checksum += packet[i] - checksum = checksum & 0xffff + checksum += packet[i] + checksum = checksum & 0xffff packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 - starttime = time.time() - with self.lock: - while True: + cs.sendto(packet, ('255.255.255.255', 80)) + if timeout is None: + response = cs.recvfrom(1024) + responsepacket = bytearray(response[0]) + host = response[1] + mac = responsepacket[0x3a:0x40] + devtype = responsepacket[0x34] | responsepacket[0x35] << 8 + + return gendevice(devtype, host, mac) + + while (time.time() - starttime) < timeout: + cs.settimeout(timeout - (time.time() - starttime)) try: - self.cs.sendto(packet, self.host) - self.cs.settimeout(1) - response = self.cs.recvfrom(2048) - break + response = cs.recvfrom(1024) except socket.timeout: - if (time.time() - starttime) > self.timeout: - raise - return bytearray(response[0]) + return devices + responsepacket = bytearray(response[0]) + host = response[1] + devtype = responsepacket[0x34] | responsepacket[0x35] << 8 + mac = responsepacket[0x3a:0x40] + dev = gendevice(devtype, host, mac) + devices.append(dev) + return devices + + +class device: + def __init__(self, host, mac, devtype, timeout=10): + self.host = host + self.mac = mac.encode() if isinstance(mac, str) else mac + self.devtype = devtype + self.timeout = timeout + self.count = random.randrange(0xffff) + self.key = bytearray( + [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) + self.iv = bytearray( + [0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) + self.id = bytearray([0, 0, 0, 0]) + self.cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + self.cs.bind(('', 0)) + self.type = "Unknown" + self.lock = threading.Lock() + + if 'pyaes' in globals(): + self.encrypt = self.encrypt_pyaes + self.decrypt = self.decrypt_pyaes + else: + self.encrypt = self.encrypt_pycrypto + self.decrypt = self.decrypt_pycrypto + + def encrypt_pyaes(self, payload): + aes = pyaes.AESModeOfOperationCBC(self.key, iv=bytes(self.iv)) + return b"".join([aes.encrypt(bytes(payload[i:i + 16])) for i in range(0, len(payload), 16)]) + + def decrypt_pyaes(self, payload): + aes = pyaes.AESModeOfOperationCBC(self.key, iv=bytes(self.iv)) + return b"".join([aes.decrypt(bytes(payload[i:i + 16])) for i in range(0, len(payload), 16)]) + + def encrypt_pycrypto(self, payload): + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + return aes.encrypt(bytes(payload)) + + def decrypt_pycrypto(self, payload): + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + return aes.decrypt(bytes(payload)) + + def auth(self): + 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[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') + + response = self.send_packet(0x65, payload) + + payload = self.decrypt(response[0x38:]) + + if not payload: + return False + + key = payload[0x04:0x14] + if len(key) % 16 != 0: + return False + + self.id = payload[0x00:0x04] + self.key = key + + return True + + def get_type(self): + return self.type + + def send_packet(self, command, payload): + self.count = (self.count + 1) & 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] = 0x2a + packet[0x25] = 0x27 + packet[0x26] = command + packet[0x28] = self.count & 0xff + packet[0x29] = self.count >> 8 + packet[0x2a] = self.mac[0] + packet[0x2b] = self.mac[1] + packet[0x2c] = self.mac[2] + packet[0x2d] = self.mac[3] + packet[0x2e] = self.mac[4] + packet[0x2f] = self.mac[5] + packet[0x30] = self.id[0] + packet[0x31] = self.id[1] + packet[0x32] = self.id[2] + packet[0x33] = self.id[3] + + # pad the payload for AES encryption + if payload: + numpad = (len(payload) // 16 + 1) * 16 + payload = payload.ljust(numpad, b"\x00") + + checksum = 0xbeaf + for i in range(len(payload)): + checksum += payload[i] + checksum = checksum & 0xffff + + payload = self.encrypt(payload) + + packet[0x34] = checksum & 0xff + packet[0x35] = checksum >> 8 + + for i in range(len(payload)): + packet.append(payload[i]) + + checksum = 0xbeaf + for i in range(len(packet)): + checksum += packet[i] + checksum = checksum & 0xffff + packet[0x20] = checksum & 0xff + packet[0x21] = checksum >> 8 + + starttime = time.time() + with self.lock: + while True: + try: + self.cs.sendto(packet, self.host) + self.cs.settimeout(1) + response = self.cs.recvfrom(2048) + break + except socket.timeout: + if (time.time() - starttime) > self.timeout: + raise + return bytearray(response[0]) class mp1(device): - def __init__ (self, host, mac, devtype): - device.__init__(self, host, mac, devtype) - self.type = "MP1" - - def set_power_mask(self, sid_mask, state): - """Sets the power state of the smart power strip.""" - - 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 state else sid_mask) - packet[0x07] = 0xc0 - packet[0x08] = 0x02 - packet[0x0a] = 0x03 - packet[0x0d] = sid_mask - packet[0x0e] = sid_mask if state else 0 - - response = self.send_packet(0x6a, packet) - - err = response[0x22] | (response[0x23] << 8) - - def set_power(self, sid, state): - """Sets the power state of the smart power strip.""" - sid_mask = 0x01 << (sid - 1) - return self.set_power_mask(sid_mask, state) - - def check_power_raw(self): - """Returns the power state of the smart power strip 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) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - payload = self.decrypt(bytes(response[0x38:])) - if type(payload[0x4]) == int: - state = payload[0x0e] - else: - state = ord(payload[0x0e]) - return state - - def check_power(self): - """Returns the power state of the smart power strip.""" - state = self.check_power_raw() - data = {} - data['s1'] = bool(state & 0x01) - data['s2'] = bool(state & 0x02) - data['s3'] = bool(state & 0x04) - data['s4'] = bool(state & 0x08) - return data + def __init__(self, host, mac, devtype): + device.__init__(self, host, mac, devtype) + self.type = "MP1" + + def set_power_mask(self, sid_mask, state): + """Sets the power state of the smart power strip.""" + + 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 state else sid_mask) + packet[0x07] = 0xc0 + packet[0x08] = 0x02 + packet[0x0a] = 0x03 + packet[0x0d] = sid_mask + packet[0x0e] = sid_mask if state else 0 + + self.send_packet(0x6a, packet) + + def set_power(self, sid, state): + """Sets the power state of the smart power strip.""" + sid_mask = 0x01 << (sid - 1) + return self.set_power_mask(sid_mask, state) + + def check_power_raw(self): + """Returns the power state of the smart power strip 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) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return None + payload = self.decrypt(bytes(response[0x38:])) + if isinstance(payload[0x4], int): + state = payload[0x0e] + else: + state = ord(payload[0x0e]) + return state + + def check_power(self): + """Returns the power state of the smart power strip.""" + state = self.check_power_raw() + data = {} + data['s1'] = bool(state & 0x01) + data['s2'] = bool(state & 0x02) + data['s3'] = bool(state & 0x04) + data['s4'] = bool(state & 0x08) + return data class sp1(device): - def __init__ (self, host, mac, devtype): - device.__init__(self, host, mac, devtype) - self.type = "SP1" + def __init__(self, host, mac, devtype): + device.__init__(self, host, mac, devtype) + self.type = "SP1" - def set_power(self, state): - packet = bytearray(4) - packet[0] = state - self.send_packet(0x66, packet) + def set_power(self, state): + packet = bytearray(4) + packet[0] = state + self.send_packet(0x66, packet) class sp2(device): - def __init__ (self, host, mac, devtype): - device.__init__(self, host, mac, devtype) - self.type = "SP2" - - def set_power(self, state): - """Sets the power state of the smart plug.""" - 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 - self.send_packet(0x6a, packet) - - def set_nightlight(self, state): - """Sets the night light state of the smart plug""" - 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 - self.send_packet(0x6a, packet) - - def check_power(self): - """Returns the power state of the smart plug.""" - packet = bytearray(16) - packet[0] = 1 - response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - payload = self.decrypt(bytes(response[0x38:])) - if type(payload[0x4]) == int: - if payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD: - state = True - else: - state = False - else: - if ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFD: - state = True + def __init__(self, host, mac, devtype): + device.__init__(self, host, mac, devtype) + self.type = "SP2" + + def set_power(self, state): + """Sets the power state of the smart plug.""" + packet = bytearray(16) + packet[0] = 2 + if self.check_nightlight(): + packet[4] = 3 if state else 2 else: - state = False - return state - - def check_nightlight(self): - """Returns the power state of the smart plug.""" - packet = bytearray(16) - packet[0] = 1 - response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - payload = self.decrypt(bytes(response[0x38:])) - if type(payload[0x4]) == int: - if payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF: - state = True + packet[4] = 1 if state else 0 + self.send_packet(0x6a, packet) + + def set_nightlight(self, state): + """Sets the night light state of the smart plug""" + packet = bytearray(16) + packet[0] = 2 + if self.check_power(): + packet[4] = 3 if state else 1 else: - state = False - else: - if ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFF: - state = True + packet[4] = 2 if state else 0 + self.send_packet(0x6a, packet) + + def check_power(self): + """Returns the power state of the smart plug.""" + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return None + payload = self.decrypt(bytes(response[0x38:])) + if isinstance(payload[0x4], int): + return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD) + return bool(ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFD) + + def check_nightlight(self): + """Returns the power state of the smart plug.""" + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return None + payload = self.decrypt(bytes(response[0x38:])) + if isinstance(payload[0x4], int): + return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) + return bool(ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFF) + + def get_energy(self): + packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return None + payload = self.decrypt(bytes(response[0x38:])) + if isinstance(payload[0x7], int): + energy = int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0 else: - state = False - return state - - def get_energy(self): - packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) - response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - payload = self.decrypt(bytes(response[0x38:])) - if type(payload[0x07]) == int: - energy = int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:])/100.0 - else: - energy = int(hex(ord(payload[0x07]) * 256 + ord(payload[0x06]))[2:]) + int(hex(ord(payload[0x05]))[2:])/100.0 - return energy + energy = int(hex(ord(payload[0x07]) * 256 + ord(payload[0x06]))[2:]) + int( + hex(ord(payload[0x05]))[2:]) / 100.0 + return energy class a1(device): - def __init__ (self, host, mac, devtype): - device.__init__(self, host, mac, devtype) - self.type = "A1" - - def check_sensors(self): - packet = bytearray(16) - packet[0] = 1 - response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - data = {} - payload = self.decrypt(bytes(response[0x38:])) - if type(payload[0x4]) == int: - data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 - data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 - light = payload[0x8] - air_quality = payload[0x0a] - noise = payload[0xc] - else: - data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 - data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0 - light = ord(payload[0x8]) - air_quality = ord(payload[0x0a]) - noise = ord(payload[0xc]) - if light == 0: - data['light'] = 'dark' - elif light == 1: - data['light'] = 'dim' - elif light == 2: - data['light'] = 'normal' - elif light == 3: - data['light'] = 'bright' - else: - data['light'] = 'unknown' - if air_quality == 0: - data['air_quality'] = 'excellent' - elif air_quality == 1: - data['air_quality'] = 'good' - elif air_quality == 2: - data['air_quality'] = 'normal' - elif air_quality == 3: - data['air_quality'] = 'bad' - else: - data['air_quality'] = 'unknown' - if noise == 0: - data['noise'] = 'quiet' - elif noise == 1: - data['noise'] = 'normal' - elif noise == 2: - data['noise'] = 'noisy' - else: - data['noise'] = 'unknown' - return data - - def check_sensors_raw(self): - packet = bytearray(16) - packet[0] = 1 - response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - data = {} - payload = self.decrypt(bytes(response[0x38:])) - if type(payload[0x4]) == int: - data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 - data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 - data['light'] = payload[0x8] - data['air_quality'] = payload[0x0a] - data['noise'] = payload[0xc] - else: - data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 - data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0 - data['light'] = ord(payload[0x8]) - data['air_quality'] = ord(payload[0x0a]) - data['noise'] = ord(payload[0xc]) - return data + def __init__(self, host, mac, devtype): + device.__init__(self, host, mac, devtype) + self.type = "A1" + + def check_sensors(self): + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return None + data = {} + payload = self.decrypt(bytes(response[0x38:])) + if isinstance(payload[0x4], int): + data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 + data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 + light = payload[0x8] + air_quality = payload[0x0a] + noise = payload[0xc] + else: + data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 + data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0 + light = ord(payload[0x8]) + air_quality = ord(payload[0x0a]) + noise = ord(payload[0xc]) + if light == 0: + data['light'] = 'dark' + elif light == 1: + data['light'] = 'dim' + elif light == 2: + data['light'] = 'normal' + elif light == 3: + data['light'] = 'bright' + else: + data['light'] = 'unknown' + if air_quality == 0: + data['air_quality'] = 'excellent' + elif air_quality == 1: + data['air_quality'] = 'good' + elif air_quality == 2: + data['air_quality'] = 'normal' + elif air_quality == 3: + data['air_quality'] = 'bad' + else: + data['air_quality'] = 'unknown' + if noise == 0: + data['noise'] = 'quiet' + elif noise == 1: + data['noise'] = 'normal' + elif noise == 2: + data['noise'] = 'noisy' + else: + data['noise'] = 'unknown' + return data + + def check_sensors_raw(self): + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return None + data = {} + payload = self.decrypt(bytes(response[0x38:])) + if isinstance(payload[0x4], int): + data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 + data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 + data['light'] = payload[0x8] + data['air_quality'] = payload[0x0a] + data['noise'] = payload[0xc] + else: + data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 + data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0 + data['light'] = ord(payload[0x8]) + data['air_quality'] = ord(payload[0x0a]) + data['noise'] = ord(payload[0xc]) + return data class rm(device): - def __init__ (self, host, mac, devtype): - device.__init__(self, host, mac, devtype) - self.type = "RM2" - - def check_data(self): - packet = bytearray(16) - packet[0] = 4 - response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - payload = self.decrypt(bytes(response[0x38:])) - return payload[0x04:] - - def send_data(self, data): - packet = bytearray([0x02, 0x00, 0x00, 0x00]) - packet += data - self.send_packet(0x6a, packet) - - def enter_learning(self): - packet = bytearray(16) - packet[0] = 3 - self.send_packet(0x6a, packet) - - def sweep_frequency(self): - packet = bytearray(16) - packet[0] = 0x19 - self.send_packet(0x6a, packet) - - def cancel_sweep_frequency(self): - packet = bytearray(16) - packet[0] = 0x1e - self.send_packet(0x6a, packet) - - def check_frequency(self): - packet = bytearray(16) - packet[0] = 0x1a - response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - payload = self.decrypt(bytes(response[0x38:])) - if payload[0x04] == 1: - return True - return False - - def find_rf_packet(self): - packet = bytearray(16) - packet[0] = 0x1b - response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - payload = self.decrypt(bytes(response[0x38:])) - if payload[0x04] == 1: - return True - return False - - def check_temperature(self): - packet = bytearray(16) - packet[0] = 1 - response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - payload = self.decrypt(bytes(response[0x38:])) - if type(payload[0x4]) == int: - temp = (payload[0x4] * 10 + payload[0x5]) / 10.0 - else: - temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 - return temp + def __init__(self, host, mac, devtype): + device.__init__(self, host, mac, devtype) + self.type = "RM2" + + def check_data(self): + packet = bytearray(16) + packet[0] = 4 + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return None + payload = self.decrypt(bytes(response[0x38:])) + return payload[0x04:] + + def send_data(self, data): + packet = bytearray([0x02, 0x00, 0x00, 0x00]) + packet += data + self.send_packet(0x6a, packet) + + def enter_learning(self): + packet = bytearray(16) + packet[0] = 3 + self.send_packet(0x6a, packet) + + def sweep_frequency(self): + packet = bytearray(16) + packet[0] = 0x19 + self.send_packet(0x6a, packet) + + def cancel_sweep_frequency(self): + packet = bytearray(16) + packet[0] = 0x1e + self.send_packet(0x6a, packet) + + def check_frequency(self): + packet = bytearray(16) + packet[0] = 0x1a + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return False + payload = self.decrypt(bytes(response[0x38:])) + if payload[0x04] == 1: + return True + return False + + def find_rf_packet(self): + packet = bytearray(16) + packet[0] = 0x1b + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return False + payload = self.decrypt(bytes(response[0x38:])) + if payload[0x04] == 1: + return True + return False + + def check_temperature(self): + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return False + payload = self.decrypt(bytes(response[0x38:])) + if isinstance(payload[0x4], int): + temp = (payload[0x4] * 10 + payload[0x5]) / 10.0 + else: + temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 + return temp # For legacy compatibility - don't use this class rm2(rm): - def __init__ (self): - device.__init__(self, None, None, None) + def __init__(self): + device.__init__(self, None, None, None) - def discover(self): - dev = discover() - self.host = dev.host - self.mac = dev.mac + def discover(self): + dev = discover() + self.host = dev.host + self.mac = dev.mac class hysen(device): - def __init__ (self, host, mac, devtype): - device.__init__(self, host, mac, devtype) - self.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): - - from PyCRC.CRC16 import CRC16 - crc = CRC16(modbus_flag=True).calculate(bytes(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) - - # send to device - response = self.send_packet(0x6a, request_payload) - - # check for error - err = response[0x22] | (response[0x23] << 8) - if err: - raise ValueError('broadlink_response_error',err) - - response_payload = bytearray(self.decrypt(bytes(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): - raise ValueError('hysen_response_error','first byte of response is not length') - crc = CRC16(modbus_flag=True).calculate(bytes(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] - else: - raise ValueError('hysen_response_error','CRC check on response failed') - - - # Get current room temperature in degrees celsius - def get_temp(self): - payload = self.send_request(bytearray([0x01,0x03,0x00,0x00,0x00,0x08])) - return payload[0x05] / 2.0 - - # Get current external temperature in degrees celsius - def get_external_temp(self): - payload = self.send_request(bytearray([0x01,0x03,0x00,0x00,0x00,0x08])) - return payload[18] / 2.0 - - # Get full status (including timer schedule) - def get_full_status(self): - payload = self.send_request(bytearray([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['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['fre'] = payload[15] - data['poweron'] = payload[16] - data['unknown'] = payload[17] - data['external_temp'] = (payload[18] & 255)/2.0 - data['hour'] = payload[19] - data['min'] = payload[20] - data['sec'] = payload[21] - data['dayofweek'] = payload[22] - - weekday = [] - for i in range(0, 6): - weekday.append({'start_hour':payload[2*i + 23], 'start_minute':payload[2*i + 24],'temp':payload[i + 39]/2.0}) - - data['weekday'] = weekday - weekend = [] - for i in range(6, 8): - weekend.append({'start_hour':payload[2*i + 23], 'start_minute':payload[2*i + 24],'temp':payload[i + 39]/2.0}) - - data['weekend'] = weekend - return data - - # Change controller mode - # auto_mode = 1 for auto (scheduled/timed) mode, 0 for manual mode. - # 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 - # The sensor command is currently experimental - def set_mode(self, auto_mode, loop_mode,sensor=0): - mode_byte = ( (loop_mode + 1) << 4) + auto_mode - # print 'Mode byte: 0x'+ format(mode_byte, '02x') - self.send_request(bytearray([0x01,0x06,0x00,0x02,mode_byte,sensor])) - - # Advanced settings - # Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor, 2 for internal control temperature, external limit temperature. Factory default: 0. - # Set temperature range for external sensor (OSV) osv = 5..99. Factory default: 42C - # 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 - # 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 - def set_advanced(self, loop_mode, sensor, osv, dif, svh, svl, adj, fre, poweron): - input_payload = bytearray([0x01,0x10,0x00,0x02,0x00,0x05,0x0a, loop_mode, sensor, osv, dif, svh, svl, (int(adj*2)>>8 & 0xff), (int(adj*2) & 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. - def switch_to_auto(self): - self.set_mode(auto_mode=1, loop_mode=0) - - def switch_to_manual(self): - self.set_mode(auto_mode=0, loop_mode=0) - - # Set temperature for manual mode (also activates manual mode if currently in automatic) - def set_temp(self, temp): - self.send_request(bytearray([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=1, remote_lock=0): - self.send_request(bytearray([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, minute, second, day): - self.send_request(bytearray([0x01,0x10,0x00,0x08,0x00,0x02,0x04, hour, minute, second, day ])) - - # Set timer schedule - # Format is the same as you get from get_full_status. - # weekday is a list (ordered) of 6 dicts like: - # {'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,weekend): - # Begin with some magic values ... - input_payload = bytearray([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'] ) - - # weekend times - for i in range(0, 2): - input_payload.append( weekend[i]['start_hour'] ) - input_payload.append( weekend[i]['start_minute'] ) - - # weekday temperatures - for i in range(0, 6): - input_payload.append( int(weekday[i]['temp'] * 2) ) - - # weekend temperatures - for i in range(0, 2): - input_payload.append( int(weekend[i]['temp'] * 2) ) - - self.send_request(input_payload) + def __init__(self, host, mac, devtype): + device.__init__(self, host, mac, devtype) + self.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): + + from PyCRC.CRC16 import CRC16 + crc = CRC16(modbus_flag=True).calculate(bytes(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) + + # send to device + response = self.send_packet(0x6a, request_payload) + + # check for error + err = response[0x22] | (response[0x23] << 8) + if err: + raise ValueError('broadlink_response_error', err) + + response_payload = bytearray(self.decrypt(bytes(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): + raise ValueError('hysen_response_error', 'first byte of response is not length') + crc = CRC16(modbus_flag=True).calculate(bytes(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') + + # Get current room temperature in degrees celsius + def get_temp(self): + payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])) + return payload[0x05] / 2.0 + + # Get current external temperature in degrees celsius + def get_external_temp(self): + payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])) + return payload[18] / 2.0 + + # Get full status (including timer schedule) + def get_full_status(self): + payload = self.send_request(bytearray([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['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['fre'] = payload[15] + data['poweron'] = payload[16] + data['unknown'] = payload[17] + data['external_temp'] = (payload[18] & 255) / 2.0 + data['hour'] = payload[19] + data['min'] = payload[20] + data['sec'] = payload[21] + data['dayofweek'] = payload[22] + + weekday = [] + for i in range(0, 6): + weekday.append( + {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0}) + + data['weekday'] = weekday + weekend = [] + for i in range(6, 8): + weekend.append( + {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0}) + + data['weekend'] = weekend + return data + + # Change controller mode + # auto_mode = 1 for auto (scheduled/timed) mode, 0 for manual mode. + # 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 + # The sensor command is currently experimental + def set_mode(self, auto_mode, loop_mode, sensor=0): + mode_byte = ((loop_mode + 1) << 4) + auto_mode + # print 'Mode byte: 0x'+ format(mode_byte, '02x') + self.send_request(bytearray([0x01, 0x06, 0x00, 0x02, mode_byte, sensor])) + + # Advanced settings + # Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor, + # 2 for internal control temperature, external limit temperature. Factory default: 0. + # Set temperature range for external sensor (OSV) osv = 5..99. Factory default: 42C + # 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 + # 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 + def set_advanced(self, loop_mode, sensor, osv, dif, svh, svl, adj, fre, poweron): + input_payload = bytearray([0x01, 0x10, 0x00, 0x02, 0x00, 0x05, 0x0a, loop_mode, sensor, osv, dif, svh, svl, + (int(adj * 2) >> 8 & 0xff), (int(adj * 2) & 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. + def switch_to_auto(self): + self.set_mode(auto_mode=1, loop_mode=0) + + def switch_to_manual(self): + self.set_mode(auto_mode=0, loop_mode=0) + + # Set temperature for manual mode (also activates manual mode if currently in automatic) + def set_temp(self, temp): + self.send_request(bytearray([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=1, remote_lock=0): + self.send_request(bytearray([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, minute, second, day): + self.send_request(bytearray([0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day])) + + # Set timer schedule + # Format is the same as you get from get_full_status. + # weekday is a list (ordered) of 6 dicts like: + # {'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, weekend): + # Begin with some magic values ... + input_payload = bytearray([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']) + + # weekend times + for i in range(0, 2): + input_payload.append(weekend[i]['start_hour']) + input_payload.append(weekend[i]['start_minute']) + + # weekday temperatures + for i in range(0, 6): + input_payload.append(int(weekday[i]['temp'] * 2)) + + # weekend temperatures + for i in range(0, 2): + input_payload.append(int(weekend[i]['temp'] * 2)) + + self.send_request(input_payload) S1C_SENSORS_TYPES = { @@ -784,134 +785,137 @@ def set_schedule(self,weekday,weekend): class S1C(device): - """ - Its VERY VERY VERY DIRTY IMPLEMENTATION of S1C - """ - def __init__(self, *a, **kw): - device.__init__(self, *a, **kw) - self.type = 'S1C' - - def get_sensors_status(self): - packet = bytearray(16) - packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors - response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - - payload = aes.decrypt(bytes(response[0x38:])) - if payload: - head = payload[:4] - count = payload[0x4] #need to fix for python 2.x + """ + Its VERY VERY VERY DIRTY IMPLEMENTATION of S1C + """ + + def __init__(self, *a, **kw): + device.__init__(self, *a, **kw) + self.type = 'S1C' + + def get_sensors_status(self): + packet = bytearray(16) + packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return None + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + + payload = aes.decrypt(bytes(response[0x38:])) + if not payload: + return None + count = payload[0x4] sensors = payload[0x6:] sensors_a = [bytearray(sensors[i * 83:(i + 1) * 83]) for i in range(len(sensors) // 83)] sens_res = [] for sens in sensors_a: - status = ord(chr(sens[0])) - _name = str(bytes(sens[4:26]).decode()) - _order = ord(chr(sens[1])) - _type = ord(chr(sens[3])) - _serial = bytes(codecs.encode(sens[26:30],"hex")).decode() - - type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown') - - r = { - 'status': status, - 'name': _name.strip('\x00'), - 'type': type_str, - 'order': _order, - 'serial': _serial, - } - if r['serial'] != '00000000': - sens_res.append(r) + status = ord(chr(sens[0])) + _name = str(bytes(sens[4:26]).decode()) + _order = ord(chr(sens[1])) + _type = ord(chr(sens[3])) + _serial = bytes(codecs.encode(sens[26:30], "hex")).decode() + + type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown') + + r = { + 'status': status, + 'name': _name.strip('\x00'), + 'type': type_str, + 'order': _order, + 'serial': _serial, + } + if r['serial'] != '00000000': + sens_res.append(r) result = { - 'count': count, - 'sensors': sens_res + 'count': count, + 'sensors': sens_res } return result class dooya(device): - def __init__ (self, host, mac, devtype): - device.__init__(self, host, mac, devtype) - self.type = "Dooya DT360E" - - def _send(self, magic1, magic2): - packet = bytearray(16) - packet[0] = 0x09 - packet[2] = 0xbb - packet[3] = magic1 - packet[4] = magic2 - packet[9] = 0xfa - packet[10] = 0x44 - response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err == 0: - payload = self.decrypt(bytes(response[0x38:])) - return ord(payload[4]) - - def open(self): - return self._send(0x01, 0x00) - - def close(self): - return self._send(0x02, 0x00) - - def stop(self): - return self._send(0x03, 0x00) - - def get_percentage(self): - return self._send(0x06, 0x5d) - - def set_percentage_and_wait(self, new_percentage): - current = self.get_percentage() - if current > new_percentage: - self.close() - while current is not None and current > new_percentage: - time.sleep(0.2) + def __init__(self, host, mac, devtype): + device.__init__(self, host, mac, devtype) + self.type = "Dooya DT360E" + + def _send(self, magic1, magic2): + packet = bytearray(16) + packet[0] = 0x09 + packet[2] = 0xbb + packet[3] = magic1 + packet[4] = magic2 + packet[9] = 0xfa + packet[10] = 0x44 + response = self.send_packet(0x6a, packet) + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return None + payload = self.decrypt(bytes(response[0x38:])) + return ord(payload[4]) + + def open(self): + return self._send(0x01, 0x00) + + def close(self): + return self._send(0x02, 0x00) + + def stop(self): + return self._send(0x03, 0x00) + + def get_percentage(self): + return self._send(0x06, 0x5d) + + def set_percentage_and_wait(self, new_percentage): current = self.get_percentage() + if current > new_percentage: + self.close() + while current is not None and current > new_percentage: + time.sleep(0.2) + current = self.get_percentage() - elif current < new_percentage: - self.open() - while current is not None and current < new_percentage: - time.sleep(0.2) - current = self.get_percentage() - self.stop() + elif current < new_percentage: + self.open() + while current is not None and current < new_percentage: + time.sleep(0.2) + current = self.get_percentage() + self.stop() # 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, password, security_mode): - # Security mode options are (0 - none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) - payload = bytearray(0x88) - payload[0x26] = 0x14 # This seems to always be set to 14 - # Add the SSID to the payload - ssid_start = 68 - ssid_length = 0 - for letter in ssid: - payload[(ssid_start + ssid_length)] = ord(letter) - ssid_length += 1 - # Add the WiFi password to the payload - pass_start = 100 - pass_length = 0 - for letter in password: - payload[(pass_start + pass_length)] = ord(letter) - pass_length += 1 - - payload[0x84] = ssid_length # Character length of SSID - payload[0x85] = pass_length # Character length of password - payload[0x86] = security_mode # Type of encryption (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2) - - checksum = 0xbeaf - for i in range(len(payload)): - checksum += payload[i] - checksum = checksum & 0xffff + # Security mode options are (0 - none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) + payload = bytearray(0x88) + payload[0x26] = 0x14 # This seems to always be set to 14 + # Add the SSID to the payload + ssid_start = 68 + ssid_length = 0 + for letter in ssid: + payload[(ssid_start + ssid_length)] = ord(letter) + ssid_length += 1 + # Add the WiFi password to the payload + pass_start = 100 + pass_length = 0 + for letter in password: + payload[(pass_start + pass_length)] = ord(letter) + pass_length += 1 + + payload[0x84] = ssid_length # Character length of SSID + payload[0x85] = pass_length # Character length of password + payload[0x86] = security_mode # Type of encryption (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2) + + checksum = 0xbeaf + for i in range(len(payload)): + checksum += payload[i] + checksum = checksum & 0xffff - payload[0x20] = checksum & 0xff # Checksum 1 position - payload[0x21] = checksum >> 8 # Checksum 2 position + payload[0x20] = checksum & 0xff # Checksum 1 position + payload[0x21] = checksum >> 8 # Checksum 2 position - sock = socket.socket(socket.AF_INET, # Internet - socket.SOCK_DGRAM) # 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 = socket.socket(socket.AF_INET, # Internet + socket.SOCK_DGRAM) # 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)) From b8fdf337c9a60d74437bf88fa07134e651f7c0fa Mon Sep 17 00:00:00 2001 From: AnilDaoud Date: Wed, 22 May 2019 13:27:34 +0800 Subject: [PATCH 062/261] cleaner local ip address lookup (#244) --- broadlink/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 582f64d7..c3b2cecd 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -63,9 +63,7 @@ def gendevice(devtype, host, mac): def discover(timeout=None, local_ip_address=None): if local_ip_address is None: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets - local_ip_address = s.getsockname()[0] + local_ip_address = socket.gethostbyname(socket.gethostname()) address = local_ip_address.split('.') cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) From ef66997953cb2beb05e05b81096b04a79d0f59ec Mon Sep 17 00:00:00 2001 From: AnilDaoud Date: Wed, 22 May 2019 13:28:02 +0800 Subject: [PATCH 063/261] 2to3 broadlink_discovery (#245) --- cli/broadlink_discovery | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery index 385f1932..2bb8d998 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -8,17 +8,17 @@ parser = argparse.ArgumentParser(fromfile_prefix_chars='@'); parser.add_argument("--timeout", type=int, default=5, help="timeout to wait for receiving discovery responses") args = parser.parse_args() -print "Discovering..." +print("Discovering...") devices = broadlink.discover(timeout=args.timeout) for device in devices: if device.auth(): - print "###########################################" - print device.type - print "# broadlink_cli --type {} --host {} --mac {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac)) - print "Device file data (to be used with --device @filename in broadlink_cli) : " - print "{} {} {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac)) + print("###########################################") + print(device.type) + print("# broadlink_cli --type {} --host {} --mac {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac))) + print("Device file data (to be used with --device @filename in broadlink_cli) : ") + print("{} {} {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac))) if hasattr(device, 'check_temperature'): - print "temperature = {}".format(device.check_temperature()) - print "" + print("temperature = {}".format(device.check_temperature())) + print("") else: - print "Error authenticating with device : {}".format(device.host) + print("Error authenticating with device : {}".format(device.host)) From 45e26fda237441777d76f5b7362c84ce9f757c89 Mon Sep 17 00:00:00 2001 From: AnilDaoud Date: Thu, 23 May 2019 21:13:14 +0800 Subject: [PATCH 064/261] fixed learnfile switch and python3 compatibility (#247) --- cli/broadlink_cli | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 1c3e231d..1a8fe570 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -4,6 +4,8 @@ import broadlink import sys import argparse import time +import base64 +import codecs TICK = 32.84 IR_TOKEN = 0x26 @@ -120,7 +122,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: +if args.learn or args.learnfile: dev.enter_learning() data = None print("Learning...") @@ -135,7 +137,8 @@ if args.learn: else ''.join(format(x, '02x') for x in bytearray(data)) if args.learn: print(learned) - print("Base64: " + base64.b64encode(learned.decode("hex"))) + decode_hex = codecs.getdecoder("hex_codec") + print("Base64: " + str(base64.b64encode(decode_hex(learned)[0]))) if args.learnfile: print("Saving to {}".format(args.learnfile)) with open(args.learnfile, "w") as text_file: From 38a40c5a19c05039e5c92ced1f553971ccd70d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Mon, 27 May 2019 20:57:32 +0200 Subject: [PATCH 065/261] Use cryptography instead of pycryptodome (#246) * Use cryptography instad of pycryptodome * Use cryptography instad of pycryptodome --- broadlink/__init__.py | 75 ++++++++++++++++++++++++------------------- requirements.txt | 2 +- setup.py | 9 +++--- 3 files changed, 48 insertions(+), 38 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index c3b2cecd..7a385fc1 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,18 +1,18 @@ #!/usr/bin/python +import codecs +import random +import socket +import threading +import time from datetime import datetime try: - from Crypto.Cipher import AES -except ImportError as e: + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.backends import default_backend +except ImportError: import pyaes -import time -import random -import socket -import threading -import codecs - def gendevice(devtype, host, mac): devices = { @@ -55,10 +55,10 @@ def gendevice(devtype, host, mac): } # Look for the class associated to devtype in devices - [deviceClass] = [dev for dev in devices if devtype in devices[dev]] or [None] - if deviceClass is None: + [device_class] = [dev for dev in devices if devtype in devices[dev]] or [None] + if device_class is None: return device(host=host, mac=mac, devtype=devtype) - return deviceClass(host=host, mac=mac, devtype=devtype) + return device_class(host=host, mac=mac, devtype=devtype) def discover(timeout=None, local_ip_address=None): @@ -145,8 +145,6 @@ def __init__(self, host, mac, devtype, timeout=10): self.devtype = devtype self.timeout = timeout self.count = random.randrange(0xffff) - self.key = bytearray( - [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) self.iv = bytearray( [0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) self.id = bytearray([0, 0, 0, 0]) @@ -160,25 +158,38 @@ def __init__(self, host, mac, devtype, timeout=10): if 'pyaes' in globals(): self.encrypt = self.encrypt_pyaes self.decrypt = self.decrypt_pyaes + self.update_aes = self.update_aes_pyaes + else: - self.encrypt = self.encrypt_pycrypto - self.decrypt = self.decrypt_pycrypto + self.encrypt = self.encrypt_crypto + self.decrypt = self.decrypt_crypto + self.update_aes = self.update_aes_crypto + + self.aes = None + key = bytearray( + [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) + self.update_aes(key) + + def update_aes_pyaes(self, key): + self.aes = pyaes.AESModeOfOperationCBC(key, iv=bytes(self.iv)) def encrypt_pyaes(self, payload): - aes = pyaes.AESModeOfOperationCBC(self.key, iv=bytes(self.iv)) - return b"".join([aes.encrypt(bytes(payload[i:i + 16])) for i in range(0, len(payload), 16)]) + return b"".join([self.aes.encrypt(bytes(payload[i:i + 16])) for i in range(0, len(payload), 16)]) def decrypt_pyaes(self, payload): - aes = pyaes.AESModeOfOperationCBC(self.key, iv=bytes(self.iv)) - return b"".join([aes.decrypt(bytes(payload[i:i + 16])) for i in range(0, len(payload), 16)]) + return b"".join([self.aes.decrypt(bytes(payload[i:i + 16])) for i in range(0, len(payload), 16)]) + + def update_aes_crypto(self, key): + self.aes = Cipher(algorithms.AES(key), modes.CBC(self.iv), + backend=default_backend()) - def encrypt_pycrypto(self, payload): - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - return aes.encrypt(bytes(payload)) + def encrypt_crypto(self, payload): + encryptor = self.aes.encryptor() + return encryptor.update(payload) + encryptor.finalize() - def decrypt_pycrypto(self, payload): - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - return aes.decrypt(bytes(payload)) + def decrypt_crypto(self, payload): + decryptor = self.aes.decryptor() + return decryptor.update(payload) + decryptor.finalize() def auth(self): payload = bytearray(0x50) @@ -219,7 +230,7 @@ def auth(self): return False self.id = payload[0x00:0x04] - self.key = key + self.update_aes(key) return True @@ -278,7 +289,7 @@ def send_packet(self, command, payload): packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 - starttime = time.time() + start_time = time.time() with self.lock: while True: try: @@ -287,7 +298,7 @@ def send_packet(self, command, payload): response = self.cs.recvfrom(2048) break except socket.timeout: - if (time.time() - starttime) > self.timeout: + if (time.time() - start_time) > self.timeout: raise return bytearray(response[0]) @@ -702,7 +713,6 @@ def get_full_status(self): # The sensor command is currently experimental def set_mode(self, auto_mode, loop_mode, sensor=0): mode_byte = ((loop_mode + 1) << 4) + auto_mode - # print 'Mode byte: 0x'+ format(mode_byte, '02x') self.send_request(bytearray([0x01, 0x06, 0x00, 0x02, mode_byte, sensor])) # Advanced settings @@ -787,8 +797,8 @@ class S1C(device): Its VERY VERY VERY DIRTY IMPLEMENTATION of S1C """ - def __init__(self, *a, **kw): - device.__init__(self, *a, **kw) + def __init__(self, host, mac, devtype): + device.__init__(self, host, mac, devtype) self.type = 'S1C' def get_sensors_status(self): @@ -798,9 +808,8 @@ def get_sensors_status(self): err = response[0x22] | (response[0x23] << 8) if err != 0: return None - aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) - payload = aes.decrypt(bytes(response[0x38:])) + payload = self.decrypt(bytes(response[0x38:])) if not payload: return None count = payload[0x4] diff --git a/requirements.txt b/requirements.txt index 9b20c33e..09f445bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pycryptodome==3.6.6 +cryptography==2.6.1 diff --git a/setup.py b/setup.py index 00169f44..0a31dbbb 100644 --- a/setup.py +++ b/setup.py @@ -2,15 +2,16 @@ # -*- coding: utf-8 -*- import re -from setuptools import setup, find_packages import sys import warnings +from setuptools import setup, find_packages + try: - import pyaes + import cryptography + dynamic_requires = ['cryptography>=2.1.1'] +except ImportError: dynamic_requires = ["pyaes==1.6.0"] -except ImportError as e: - dynamic_requires = ['pycryptodome==3.6.6'] # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') From c94838f56140338455b8cc4011d14c24551da50e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Mon, 27 May 2019 20:57:57 +0200 Subject: [PATCH 066/261] 0.11 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0a31dbbb..d2886298 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') -version = '0.10' +version = '0.11' setup( name='broadlink', From 9ec2da834b2703e1b07bb40452c0b8c148509f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Mon, 10 Jun 2019 09:12:50 +0200 Subject: [PATCH 067/261] handle none power raw (#250) --- broadlink/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 7a385fc1..4f1cc823 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -357,6 +357,8 @@ def check_power_raw(self): def check_power(self): """Returns the power state of the smart power strip.""" 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) From b6fd48db5d60d374eb4642b87757be56d42a83f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Mon, 10 Jun 2019 09:13:18 +0200 Subject: [PATCH 068/261] 0.11.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d2886298..bc649579 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') -version = '0.11' +version = '0.11.1' setup( name='broadlink', From 9d9b49c3db96a8e92c3b267aa07f764eff659e2b Mon Sep 17 00:00:00 2001 From: Johnson Chin Date: Tue, 25 Jun 2019 02:06:50 +0800 Subject: [PATCH 069/261] SP2: Add support for OEM SP2 (#251) --- broadlink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 4f1cc823..15178687 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -25,7 +25,7 @@ def gendevice(devtype, host, mac): 0x947a, 0x9479, # SP3S 0x2728, # SPMini2 0x2733, 0x273e, # OEM branded SPMini - 0x7530, 0x7918, # OEM branded SPMini2 + 0x7530, 0x7546, 0x7918, # OEM branded SPMini2 0x7D0D, # TMall OEM SPMini3 0x2736 # SPMiniPlus ], From 1cea255dce6b966c1c7745a7a3327569735ed18e Mon Sep 17 00:00:00 2001 From: carlos-alarcon Date: Mon, 29 Jul 2019 08:39:50 +0200 Subject: [PATCH 070/261] Allow to specify local_ip_address when discovering (#272) --- cli/broadlink_discovery | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery index 2bb8d998..74f36ed5 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -6,10 +6,11 @@ import argparse 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("--ip", default=None, help="ip address to use in the discovery") args = parser.parse_args() print("Discovering...") -devices = broadlink.discover(timeout=args.timeout) +devices = broadlink.discover(timeout=args.timeout, local_ip_address=args.ip) for device in devices: if device.auth(): print("###########################################") @@ -22,3 +23,4 @@ for device in devices: print("") else: print("Error authenticating with device : {}".format(device.host)) + From 11c5981793f4a9b23ebceaf0ac2efdd74187c192 Mon Sep 17 00:00:00 2001 From: Leonardo Brondani Schenkel Date: Fri, 16 Aug 2019 11:13:53 +0200 Subject: [PATCH 071/261] Use old IP address lookup logic as fallback (#275) On some machines, resolving the local hostname results in a loopback IP address (127.0.0.0/8). This breaks discovery. In these situations, fall back to the old IP address lookup logic that was removed on commit 790edb9. --- broadlink/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 15178687..8b08ffc5 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -64,6 +64,10 @@ def gendevice(devtype, host, mac): def discover(timeout=None, local_ip_address=None): if local_ip_address is None: local_ip_address = socket.gethostbyname(socket.gethostname()) + if local_ip_address.startswith('127.'): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets + local_ip_address = s.getsockname()[0] address = local_ip_address.split('.') cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) From 2e5361bd8ee470b35884538914a2e3bc579ec1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 2 Oct 2019 09:25:27 +0300 Subject: [PATCH 072/261] Remove support for pyaes (#281) * remove support for pyaes * remove support for pyaes * remove support for pyaes --- broadlink/__init__.py | 38 ++++++++------------------------------ cli/broadlink_cli | 22 +++++++++++----------- cli/broadlink_discovery | 10 +++++----- setup.py | 13 +------------ 4 files changed, 25 insertions(+), 58 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 8b08ffc5..55830537 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -7,11 +7,8 @@ import time from datetime import datetime -try: - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.backends import default_backend -except ImportError: - import pyaes +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes def gendevice(devtype, host, mac): @@ -65,9 +62,9 @@ def discover(timeout=None, local_ip_address=None): if local_ip_address is None: local_ip_address = socket.gethostbyname(socket.gethostname()) if local_ip_address.startswith('127.'): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets - local_ip_address = s.getsockname()[0] + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets + local_ip_address = s.getsockname()[0] address = local_ip_address.split('.') cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -159,39 +156,20 @@ def __init__(self, host, mac, devtype, timeout=10): self.type = "Unknown" self.lock = threading.Lock() - if 'pyaes' in globals(): - self.encrypt = self.encrypt_pyaes - self.decrypt = self.decrypt_pyaes - self.update_aes = self.update_aes_pyaes - - else: - self.encrypt = self.encrypt_crypto - self.decrypt = self.decrypt_crypto - self.update_aes = self.update_aes_crypto - self.aes = None key = bytearray( [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) self.update_aes(key) - def update_aes_pyaes(self, key): - self.aes = pyaes.AESModeOfOperationCBC(key, iv=bytes(self.iv)) - - def encrypt_pyaes(self, payload): - return b"".join([self.aes.encrypt(bytes(payload[i:i + 16])) for i in range(0, len(payload), 16)]) - - def decrypt_pyaes(self, payload): - return b"".join([self.aes.decrypt(bytes(payload[i:i + 16])) for i in range(0, len(payload), 16)]) - - def update_aes_crypto(self, key): + def update_aes(self, key): self.aes = Cipher(algorithms.AES(key), modes.CBC(self.iv), backend=default_backend()) - def encrypt_crypto(self, payload): + def encrypt(self, payload): encryptor = self.aes.encryptor() return encryptor.update(payload) + encryptor.finalize() - def decrypt_crypto(self, payload): + def decrypt(self, payload): decryptor = self.aes.decryptor() return decryptor.update(payload) + decryptor.finalize() diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 1a8fe570..976b5373 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -1,11 +1,11 @@ #!/usr/bin/env python3 -import broadlink -import sys import argparse -import time import base64 import codecs +import time + +import broadlink TICK = 32.84 IR_TOKEN = 0x26 @@ -18,7 +18,6 @@ def auto_int(x): def to_microseconds(bytes): result = [] # print bytes[0] # 0x26 = 38for IR - length = bytes[2] + 256 * bytes[3] # presently ignored index = 4 while index < len(bytes): chunk = bytes[index] @@ -27,7 +26,7 @@ def to_microseconds(bytes): chunk = bytes[index] chunk = 256 * chunk + bytes[index + 1] index += 2 - result.append(int(round(chunk*TICK))) + result.append(int(round(chunk * TICK))) if chunk == 0x0d05: break return result @@ -40,7 +39,7 @@ def durations_to_broadlink(durations): result.append(len(durations) % 256) result.append(len(durations) / 256) for dur in durations: - num = int(round(dur/TICK)) + num = int(round(dur / TICK)) if num > 255: result.append(0) result.append(num / 256) @@ -69,8 +68,8 @@ parser.add_argument("--device", help="device definition as 'type host mac'") parser.add_argument("--type", type=auto_int, default=0x2712, help="type of device") 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("--energy",action="store_true", help="request energy consumption from device") +parser.add_argument("--temperature", action="store_true", help="request temperature 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") parser.add_argument("--turnon", action="store_true", help="turn on device") @@ -83,14 +82,15 @@ 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("--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") +parser.add_argument("--durations", action="store_true", + help="use durations in micro seconds instead of the Broadlink format") parser.add_argument("--convert", action="store_true", help="convert input data to durations") parser.add_argument("data", nargs='*', help="Data to send or convert") args = parser.parse_args() if args.device: values = args.device.split() - type = int(values[0],0) + type = int(values[0], 0) host = values[1] mac = bytearray.fromhex(values[2]) elif args.mac: @@ -115,7 +115,7 @@ if args.sensors: data = dev.check_sensors() except: data = {} - data['temperature'] = dev.check_temperature() + data['temperature'] = dev.check_temperature() for key in data: print("{} {}".format(key, data[key])) if args.send: diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery index 74f36ed5..1edbafcb 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -1,10 +1,10 @@ #!/usr/bin/env python -import broadlink -import time import argparse -parser = argparse.ArgumentParser(fromfile_prefix_chars='@'); +import broadlink + +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("--ip", default=None, help="ip address to use in the discovery") args = parser.parse_args() @@ -15,7 +15,8 @@ for device in devices: if device.auth(): print("###########################################") print(device.type) - print("# broadlink_cli --type {} --host {} --mac {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac))) + print("# broadlink_cli --type {} --host {} --mac {}".format(hex(device.devtype), device.host[0], + ''.join(format(x, '02x') for x in device.mac))) print("Device file data (to be used with --device @filename in broadlink_cli) : ") print("{} {} {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac))) if hasattr(device, 'check_temperature'): @@ -23,4 +24,3 @@ for device in devices: print("") else: print("Error authenticating with device : {}".format(device.host)) - diff --git a/setup.py b/setup.py index bc649579..ca48ea29 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import re -import sys -import warnings from setuptools import setup, find_packages -try: - import cryptography - dynamic_requires = ['cryptography>=2.1.1'] -except ImportError: - dynamic_requires = ["pyaes==1.6.0"] - -# For Hysen thermostatic heating controller -dynamic_requires.append('PyCRC') version = '0.11.1' @@ -26,7 +15,7 @@ url='http://github.com/mjg59/python-broadlink', packages=find_packages(), scripts=[], - install_requires=dynamic_requires, + install_requires=['cryptography>=2.1.1', 'PyCRC'], description='Python API for controlling Broadlink IR controllers', classifiers=[ 'Development Status :: 4 - Beta', From 56e444cacf67c1bfb1337060bb2d39870dcbfe3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 2 Oct 2019 09:26:01 +0300 Subject: [PATCH 073/261] 0.12.0 (#285) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ca48ea29..25cc0fc5 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.11.1' +version = '0.12.0' setup( name='broadlink', From cbb1d67df81cb27a213430f6b9924b386aa4c1f7 Mon Sep 17 00:00:00 2001 From: Liran BG Date: Wed, 9 Oct 2019 16:44:40 +0300 Subject: [PATCH 074/261] Print base64 RF learned command output (#286) * Print base64 RF learned command output Would be helpful for hassio fans, they can easily copy paste the output into their automation configuration file. * Update broadlink_cli --- cli/broadlink_cli | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 976b5373..e6085443 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -225,6 +225,8 @@ if args.rfscanlearn: 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])).decode('utf8'))) if args.learnfile is not None: print("Saving to {}".format(args.learnfile)) with open(args.learnfile, "w") as text_file: From 660c2269e4e9f999ad1f383b7aa128511f7ec431 Mon Sep 17 00:00:00 2001 From: Barnaby Gray Date: Sun, 13 Oct 2019 22:12:13 +0100 Subject: [PATCH 075/261] Add support for BG Electrical Smart Sockets --- broadlink/__init__.py | 70 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 55830537..be0ca7ed 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,8 +1,10 @@ #!/usr/bin/python import codecs +import json import random import socket +import struct import threading import time from datetime import datetime @@ -48,7 +50,8 @@ def gendevice(devtype, host, mac): ], hysen: [0x4EAD], # Hysen controller S1C: [0x2722], # S1 (SmartOne Alarm Kit) - dooya: [0x4E4D] # Dooya DT360E (DOOYA_CURTAIN_V2) + dooya: [0x4E4D], # Dooya DT360E (DOOYA_CURTAIN_V2) + bg1: [0x51E3] # BG Electrical Smart Power Socket } # Look for the class associated to devtype in devices @@ -349,6 +352,71 @@ def check_power(self): return data +class bg1(device): + def __init__(self, host, mac, devtype): + device.__init__(self, host, mac, devtype) + self.type = "BG1" + + def get_state(self): + """Get state of device""" + packet = self._encode(1, b'{}') + response = self.send_packet(0x6a, packet) + return self._decode(response) + + def set_state(self, pwr=None, pwr1=None, pwr2=None, maxworktime=None, maxworktime1=None, maxworktime2=None, idcbrightness=None): + data = {} + if pwr is not None: + data['pwr'] = int(bool(pwr)) + if pwr1 is not None: + data['pwr1'] = int(bool(pwr1)) + if pwr2 is not None: + data['pwr2'] = int(bool(pwr2)) + if maxworktime is not None: + data['maxworktime'] = maxworktime + if maxworktime1 is not None: + data['maxworktime1'] = maxworktime1 + if maxworktime2 is not None: + data['maxworktime2'] = maxworktime2 + if idcbrightness is not None: + data['idcbrightness'] = idcbrightness + js = json.dumps(data).encode('utf8') + packet = self._encode(2, js) + response = self.send_packet(0x6a, packet) + return self._decode(response) + + def _encode(self, flag, js): + # 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) + struct.pack_into('> 8 + + return packet + + def _decode(self, response): + err = response[0x22] | (response[0x23] << 8) + if err != 0: + return None + + payload = self.decrypt(bytes(response[0x38:])) + js_len = struct.unpack_from(' Date: Wed, 16 Oct 2019 09:58:02 +0100 Subject: [PATCH 076/261] Add detail on return value for get_state() --- broadlink/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index be0ca7ed..3190a7b7 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -358,7 +358,11 @@ def __init__(self, host, mac, devtype): self.type = "BG1" def get_state(self): - """Get state of device""" + """Get state of device. + + Returns: + dict: Dictionary of current state + eg. `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}`""" packet = self._encode(1, b'{}') response = self.send_packet(0x6a, packet) return self._decode(response) From 8e7446f4108db748e82a68e41b254e80b01bd51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 4 Mar 2020 22:24:09 +0100 Subject: [PATCH 077/261] Fix CBC mode padding and use adler32 for checksums (#315) * Fix CBC mode padding and use adler32 for checksums * Change line order This change comes to improve the readability of the code. * Use zero-padding for CBC mode --- broadlink/__init__.py | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 3190a7b7..1c622301 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -8,6 +8,7 @@ import threading import time from datetime import datetime +from zlib import adler32 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -109,11 +110,8 @@ def discover(timeout=None, local_ip_address=None): packet[0x1c] = port & 0xff packet[0x1d] = port >> 8 packet[0x26] = 6 - checksum = 0xbeaf - for i in range(len(packet)): - checksum += packet[i] - checksum = checksum & 0xffff + checksum = adler32(packet, 0xbeaf) & 0xffff packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 @@ -251,26 +249,17 @@ def send_packet(self, command, payload): # pad the payload for AES encryption if payload: - numpad = (len(payload) // 16 + 1) * 16 - payload = payload.ljust(numpad, b"\x00") - - checksum = 0xbeaf - for i in range(len(payload)): - checksum += payload[i] - checksum = checksum & 0xffff - - payload = self.encrypt(payload) - + payload += bytearray(((len(payload)-1)//16+1)*16 - len(payload)) + + checksum = adler32(payload, 0xbeaf) & 0xffff packet[0x34] = checksum & 0xff packet[0x35] = checksum >> 8 + payload = self.encrypt(payload) for i in range(len(payload)): packet.append(payload[i]) - checksum = 0xbeaf - for i in range(len(packet)): - checksum += packet[i] - checksum = checksum & 0xffff + checksum = adler32(packet, 0xbeaf) & 0xffff packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 @@ -403,9 +392,7 @@ def _encode(self, flag, js): for i in range(len(js)): packet.append(js[i]) - checksum = 0xc0ad - for c in packet[0x08:]: - checksum = (checksum + c) & 0xffff + checksum = adler32(packet[0x08:], 0xc0ad) & 0xffff packet[0x06] = checksum & 0xff packet[0x07] = checksum >> 8 @@ -969,11 +956,7 @@ def setup(ssid, password, security_mode): payload[0x85] = pass_length # Character length of password payload[0x86] = security_mode # Type of encryption (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2) - checksum = 0xbeaf - for i in range(len(payload)): - checksum += payload[i] - checksum = checksum & 0xffff - + checksum = adler32(payload, 0xbeaf) & 0xffff payload[0x20] = checksum & 0xff # Checksum 1 position payload[0x21] = checksum >> 8 # Checksum 2 position From ec4df3966553250ec0973d50442f024aa0989468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 4 Mar 2020 22:24:33 +0100 Subject: [PATCH 078/261] Add 27de RM Mini 3 (C) (#314) I have a 0x27de RM Mini 3, as inspired by https://github.com/lprhodes/broadlinkjs-rm/blob/master/index.js, I added the identification and python-broadlink would handle it properly. --- broadlink/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 1c622301..41c8010c 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -43,7 +43,8 @@ def gendevice(devtype, host, mac): 0x27a1, # RM2 Pro Plus R1 0x27a6, # RM2 Pro PP 0x278f, # RM Mini Shate - 0x27c2 # RM Mini 3 + 0x27c2, # RM Mini 3 + 0x27de # RM Mini 3 (C) ], a1: [0x2714], # A1 mp1: [0x4EB5, # MP1 From e84becd05b3bb879899bb7a8492e1441af3b491e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 4 Mar 2020 22:25:00 +0100 Subject: [PATCH 079/261] Add support for specifying destination IP address to use in discovery (#313) Co-authored-by: Kja64 --- broadlink/__init__.py | 4 ++-- cli/broadlink_discovery | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 41c8010c..4c0ea4b1 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -63,7 +63,7 @@ def gendevice(devtype, host, mac): return device_class(host=host, mac=mac, devtype=devtype) -def discover(timeout=None, local_ip_address=None): +def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.255.255'): if local_ip_address is None: local_ip_address = socket.gethostbyname(socket.gethostname()) if local_ip_address.startswith('127.'): @@ -116,7 +116,7 @@ def discover(timeout=None, local_ip_address=None): packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 - cs.sendto(packet, ('255.255.255.255', 80)) + cs.sendto(packet, (discover_ip_address, 80)) if timeout is None: response = cs.recvfrom(1024) responsepacket = bytearray(response[0]) diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery index 1edbafcb..1c6b80b1 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -7,10 +7,11 @@ import broadlink 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("--ip", default=None, help="ip address to use in the discovery") +parser.add_argument("--dst-ip", default=None, help="destination ip address to use in the discovery") args = parser.parse_args() print("Discovering...") -devices = broadlink.discover(timeout=args.timeout, local_ip_address=args.ip) +devices = broadlink.discover(timeout=args.timeout, local_ip_address=args.ip, discover_ip_address=args.dst_ip) for device in devices: if device.auth(): print("###########################################") From 654db0935bf41b164bb6a1caf84d155270201dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 4 Mar 2020 22:26:23 +0100 Subject: [PATCH 080/261] Fix new RM mini3 (#305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix new RM mini3 Some little change * Fix new RM mini3 Some little change Co-authored-by: Daniel Høyer Iversen --- broadlink/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 4c0ea4b1..e2207ec4 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -44,7 +44,8 @@ def gendevice(devtype, host, mac): 0x27a6, # RM2 Pro PP 0x278f, # RM Mini Shate 0x27c2, # RM Mini 3 - 0x27de # RM Mini 3 (C) + 0x27d1, #new RM Mini3 + 0x27de, # RM Mini 3 (C) ], a1: [0x2714], # A1 mp1: [0x4EB5, # MP1 From e151a14a7bea0084dcb1bb1de0ca30210b822c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 4 Mar 2020 22:27:22 +0100 Subject: [PATCH 081/261] decode is not needed for python3 (#298) --- cli/broadlink_cli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index e6085443..2de44aa4 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -226,7 +226,7 @@ if args.rfscanlearn: if args.learnfile is None: print(learned) decode_hex = codecs.getdecoder("hex_codec") - print("Base64: {}".format(str(base64.b64encode(decode_hex(learned)[0])).decode('utf8'))) + print("Base64: {}".format(str(base64.b64encode(decode_hex(learned)[0])))) if args.learnfile is not None: print("Saving to {}".format(args.learnfile)) with open(args.learnfile, "w") as text_file: From af95fa2446b7d8bf804c827e7bf4a8a9e67b4026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 4 Mar 2020 22:27:55 +0100 Subject: [PATCH 082/261] Add --joinwifi option to configure the device with Wifi details (#296) --- cli/README.md | 5 +++++ cli/broadlink_cli | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/cli/README.md b/cli/README.md index 5d7b3be1..7e229e3e 100644 --- a/cli/README.md +++ b/cli/README.md @@ -78,3 +78,8 @@ Get Energy Consumption (For a SmartPlug) : ``` broadlink_cli --device @BEDROOM.device --energy ``` + +Once joined to the Broadlink provisioning Wi-Fi, configure it with your Wi-Fi details: +``` +broadlink_cli --joinwifi MySSID MyWifiPassword +``` diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 2de44aa4..5045c5c1 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -85,6 +85,7 @@ parser.add_argument("--learnfile", help="save learned command to a specified fil parser.add_argument("--durations", action="store_true", help="use durations in micro seconds instead of the Broadlink format") parser.add_argument("--convert", action="store_true", help="convert input data to durations") +parser.add_argument("--joinwifi", nargs=2, help="Args are SSID PASSPHRASE to configure Broadlink device with"); parser.add_argument("data", nargs='*', help="Data to send or convert") args = parser.parse_args() @@ -102,6 +103,9 @@ if args.host or args.device: dev = broadlink.gendevice(type, (host, 80), mac) dev.auth() +if args.joinwifi: + broadlink.setup(args.joinwifi[0], args.joinwifi[1], 4) + if args.convert: data = bytearray.fromhex(''.join(args.data)) durations = to_microseconds(data) From 1a1169f1a9d7b9075e2cebfb10c3ae769237e6d0 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 16 Mar 2020 05:49:41 -0300 Subject: [PATCH 083/261] Add support for 0x5f36 devices and RM4 series (#317) * Add support for 0x5f36 devices This type of device requires a header in the payload. The rest is the same. * Improve request header assignment * Change code sending header I just found out that this device uses a different header for sending codes. This update addresses this issue. * Improve authentication Use the error code to check if the authentication was successful. * Use default value when devtype is None * Use generic remote type if devtype is None * Extend support to RM4 series I just realized that RM4 devices use the same header. I will take the opportunity to extend support to these devices as well. * Add device type 0x62be and create rm4 class The rm4 class will improve code scalability. Just add the RM4 type to this class and it will just work. * Remove comma --- broadlink/__init__.py | 81 ++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index e2207ec4..4bf94473 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -44,9 +44,14 @@ def gendevice(devtype, host, mac): 0x27a6, # RM2 Pro PP 0x278f, # RM Mini Shate 0x27c2, # RM Mini 3 - 0x27d1, #new RM Mini3 - 0x27de, # RM Mini 3 (C) + 0x27d1, # new RM Mini3 + 0x27de # RM Mini 3 (C) ], + rm4: [0x51da, # RM4b + 0x5f36, # RM Mini 3 + 0x610f, # RM4c + 0x62be # RM4c + ], a1: [0x2714], # A1 mp1: [0x4EB5, # MP1 0x4EF7 # Honyar oem mp1 @@ -146,7 +151,7 @@ class device: def __init__(self, host, mac, devtype, timeout=10): self.host = host self.mac = mac.encode() if isinstance(mac, str) else mac - self.devtype = devtype + self.devtype = devtype if devtype is not None else 0x272a self.timeout = timeout self.count = random.randrange(0xffff) self.iv = bytearray( @@ -204,11 +209,11 @@ def auth(self): payload[0x36] = ord('1') response = self.send_packet(0x65, payload) - - payload = self.decrypt(response[0x38:]) - - if not payload: + + if any(response[0x22:0x24]): return False + + payload = self.decrypt(response[0x38:]) key = payload[0x04:0x14] if len(key) % 16 != 0: @@ -233,8 +238,8 @@ def send_packet(self, command, payload): packet[0x05] = 0xa5 packet[0x06] = 0xaa packet[0x07] = 0x55 - packet[0x24] = 0x2a - packet[0x25] = 0x27 + packet[0x24] = self.devtype & 0xff + packet[0x25] = self.devtype >> 8 packet[0x26] = command packet[0x28] = self.count & 0xff packet[0x29] = self.count >> 8 @@ -251,8 +256,8 @@ def send_packet(self, command, payload): # pad the payload for AES encryption if payload: - payload += bytearray(((len(payload)-1)//16+1)*16 - len(payload)) - + payload += bytearray(16 - len(payload)%16) + checksum = adler32(payload, 0xbeaf) & 0xffff packet[0x34] = checksum & 0xff packet[0x35] = checksum >> 8 @@ -571,76 +576,88 @@ class rm(device): def __init__(self, host, mac, devtype): device.__init__(self, host, mac, devtype) self.type = "RM2" + self._request_header = bytes() + self._code_sending_header = bytes() def check_data(self): - packet = bytearray(16) - packet[0] = 4 + packet = bytearray(self._request_header) + packet.append(0x04) response = self.send_packet(0x6a, packet) err = response[0x22] | (response[0x23] << 8) if err != 0: return None payload = self.decrypt(bytes(response[0x38:])) - return payload[0x04:] + return payload[len(self._request_header) + 4:] def send_data(self, data): - packet = bytearray([0x02, 0x00, 0x00, 0x00]) + packet = bytearray(self._code_sending_header) + packet += bytes([0x02, 0x00, 0x00, 0x00]) packet += data self.send_packet(0x6a, packet) def enter_learning(self): - packet = bytearray(16) - packet[0] = 3 + packet = bytearray(self._request_header) + packet.append(0x03) self.send_packet(0x6a, packet) def sweep_frequency(self): - packet = bytearray(16) - packet[0] = 0x19 + packet = bytearray(self._request_header) + packet.append(0x19) self.send_packet(0x6a, packet) def cancel_sweep_frequency(self): - packet = bytearray(16) - packet[0] = 0x1e + packet = bytearray(self._request_header) + packet.append(0x1e) self.send_packet(0x6a, packet) def check_frequency(self): - packet = bytearray(16) - packet[0] = 0x1a + packet = bytearray(self._request_header) + packet.append(0x1a) response = self.send_packet(0x6a, packet) err = response[0x22] | (response[0x23] << 8) if err != 0: return False payload = self.decrypt(bytes(response[0x38:])) - if payload[0x04] == 1: + if payload[len(self._request_header) + 4] == 1: return True return False def find_rf_packet(self): - packet = bytearray(16) - packet[0] = 0x1b + packet = bytearray(self._request_header) + packet.append(0x1b) response = self.send_packet(0x6a, packet) err = response[0x22] | (response[0x23] << 8) if err != 0: return False payload = self.decrypt(bytes(response[0x38:])) - if payload[0x04] == 1: + if payload[len(self._request_header) + 4] == 1: return True return False def check_temperature(self): - packet = bytearray(16) - packet[0] = 1 + packet = bytearray(self._request_header) + packet.append(0x01) response = self.send_packet(0x6a, packet) err = response[0x22] | (response[0x23] << 8) if err != 0: return False payload = self.decrypt(bytes(response[0x38:])) - if isinstance(payload[0x4], int): - temp = (payload[0x4] * 10 + payload[0x5]) / 10.0 + temp_pos = len(self._request_header) + 4 + if isinstance(payload[temp_pos], int): + temp = (payload[temp_pos] * 10 + payload[temp_pos+1]) / 10.0 else: - temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 + temp = (ord(payload[temp_pos]) * 10 + ord(payload[temp_pos+1])) / 10.0 return temp +class rm4(rm): + def __init__(self, host, mac, devtype): + device.__init__(self, host, mac, devtype) + self.type = "RM4" + self._request_header = b'\x04\x00' + self._code_sending_header = b'\xd0\x00' + + # For legacy compatibility - don't use this class rm2(rm): def __init__(self): From 84e53c8d2b31aa3c31c6e147dd02cf7044bd8765 Mon Sep 17 00:00:00 2001 From: hakana Date: Mon, 16 Mar 2020 19:56:06 +0100 Subject: [PATCH 084/261] Add support for RM4 mini with device type 0x610e (#320) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 4bf94473..3a99b03a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -50,6 +50,7 @@ def gendevice(devtype, host, mac): rm4: [0x51da, # RM4b 0x5f36, # RM Mini 3 0x610f, # RM4c + 0x610e, # RM4 mini 0x62be # RM4c ], a1: [0x2714], # A1 From 4a3950a7c50f574530d9085782d2cff1c3df5c4f Mon Sep 17 00:00:00 2001 From: hakana Date: Mon, 23 Mar 2020 21:23:05 +0100 Subject: [PATCH 085/261] Add temperature and humidity for RM4 mini (0x610e) (#321) * Add temperature and humidity reading for RM4 Mini * Change divider for tem and humi (RM4 mini) --- broadlink/__init__.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 3a99b03a..627389bd 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -635,21 +635,23 @@ def find_rf_packet(self): return True return False - def check_temperature(self): + def _read_sensor(self, type, offset, divider): packet = bytearray(self._request_header) - packet.append(0x01) + packet.append(type) response = self.send_packet(0x6a, packet) err = response[0x22] | (response[0x23] << 8) if err != 0: return False payload = self.decrypt(bytes(response[0x38:])) - temp_pos = len(self._request_header) + 4 - if isinstance(payload[temp_pos], int): - temp = (payload[temp_pos] * 10 + payload[temp_pos+1]) / 10.0 + value_pos = len(self._request_header) + offset + if isinstance(payload[value_pos], int): + value = (payload[value_pos] + payload[value_pos+1] / divider) else: - temp = (ord(payload[temp_pos]) * 10 + ord(payload[temp_pos+1])) / 10.0 - return temp + value = (ord(payload[value_pos]) + ord(payload[value_pos+1]) / divider) + return value + def check_temperature(self): + return self._read_sensor( 0x01, 4, 10.0 ) class rm4(rm): def __init__(self, host, mac, devtype): @@ -658,6 +660,17 @@ def __init__(self, host, mac, devtype): self._request_header = b'\x04\x00' self._code_sending_header = b'\xd0\x00' + def check_temperature(self): + return self._read_sensor( 0x24, 4, 100.0 ) + + def check_humidity(self): + return self._read_sensor( 0x24, 6, 100.0 ) + + def check_sensors(self): + return { + 'temperature': self.check_temperature(), + 'humidity': self.check_humidity() + } # For legacy compatibility - don't use this class rm2(rm): From 446496cc24642e5c6ca0dbda6c2f9e3116b9f7ca Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 24 Mar 2020 12:09:31 -0300 Subject: [PATCH 086/261] Obtain device name and cloud byte from HELLO_RESPONSE (#322) --- broadlink/__init__.py | 64 +++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 627389bd..a8010574 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -14,7 +14,7 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -def gendevice(devtype, host, mac): +def gendevice(devtype, host, mac, name=None, cloud=None): devices = { sp1: [0], sp2: [0x2711, # SP2 @@ -66,8 +66,8 @@ def gendevice(devtype, host, mac): # Look for the class associated to devtype in devices [device_class] = [dev for dev in devices if devtype in devices[dev]] or [None] if device_class is None: - return device(host=host, mac=mac, devtype=devtype) - return device_class(host=host, mac=mac, devtype=devtype) + return device(host, mac, devtype, name=name, cloud=cloud) + return device_class(host, mac, devtype, name=name, cloud=cloud) def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.255.255'): @@ -128,10 +128,12 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 response = cs.recvfrom(1024) responsepacket = bytearray(response[0]) host = response[1] - mac = responsepacket[0x3a:0x40] devtype = responsepacket[0x34] | responsepacket[0x35] << 8 - - return gendevice(devtype, host, mac) + mac = responsepacket[0x3a:0x40] + name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8') + cloud = bool(responsepacket[-1]) + device = gendevice(devtype, host, mac, name=name, cloud=cloud) + return device while (time.time() - starttime) < timeout: cs.settimeout(timeout - (time.time() - starttime)) @@ -143,16 +145,20 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 host = response[1] devtype = responsepacket[0x34] | responsepacket[0x35] << 8 mac = responsepacket[0x3a:0x40] - dev = gendevice(devtype, host, mac) - devices.append(dev) + name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8') + cloud = bool(responsepacket[-1]) + device = gendevice(devtype, host, mac, name=name, cloud=cloud) + devices.append(device) return devices class device: - def __init__(self, host, mac, devtype, timeout=10): + def __init__(self, host, mac, devtype, timeout=10, name=None, cloud=None): self.host = host self.mac = mac.encode() if isinstance(mac, str) else mac self.devtype = devtype if devtype is not None else 0x272a + self.name = name + self.cloud = cloud self.timeout = timeout self.count = random.randrange(0xffff) self.iv = bytearray( @@ -286,8 +292,8 @@ def send_packet(self, command, payload): class mp1(device): - def __init__(self, host, mac, devtype): - device.__init__(self, host, mac, devtype) + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) self.type = "MP1" def set_power_mask(self, sid_mask, state): @@ -350,8 +356,8 @@ def check_power(self): class bg1(device): - def __init__(self, host, mac, devtype): - device.__init__(self, host, mac, devtype) + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) self.type = "BG1" def get_state(self): @@ -417,8 +423,8 @@ def _decode(self, response): return state class sp1(device): - def __init__(self, host, mac, devtype): - device.__init__(self, host, mac, devtype) + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) self.type = "SP1" def set_power(self, state): @@ -428,8 +434,8 @@ def set_power(self, state): class sp2(device): - def __init__(self, host, mac, devtype): - device.__init__(self, host, mac, devtype) + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) self.type = "SP2" def set_power(self, state): @@ -494,8 +500,8 @@ def get_energy(self): class a1(device): - def __init__(self, host, mac, devtype): - device.__init__(self, host, mac, devtype) + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) self.type = "A1" def check_sensors(self): @@ -574,8 +580,8 @@ def check_sensors_raw(self): class rm(device): - def __init__(self, host, mac, devtype): - device.__init__(self, host, mac, devtype) + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) self.type = "RM2" self._request_header = bytes() self._code_sending_header = bytes() @@ -654,8 +660,8 @@ def check_temperature(self): return self._read_sensor( 0x01, 4, 10.0 ) class rm4(rm): - def __init__(self, host, mac, devtype): - device.__init__(self, host, mac, devtype) + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) self.type = "RM4" self._request_header = b'\x04\x00' self._code_sending_header = b'\xd0\x00' @@ -684,8 +690,8 @@ def discover(self): class hysen(device): - def __init__(self, host, mac, devtype): - device.__init__(self, host, mac, devtype) + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) self.type = "Hysen heating controller" # Send a request @@ -873,8 +879,8 @@ class S1C(device): Its VERY VERY VERY DIRTY IMPLEMENTATION of S1C """ - def __init__(self, host, mac, devtype): - device.__init__(self, host, mac, devtype) + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) self.type = 'S1C' def get_sensors_status(self): @@ -919,8 +925,8 @@ def get_sensors_status(self): class dooya(device): - def __init__(self, host, mac, devtype): - device.__init__(self, host, mac, devtype) + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) self.type = "Dooya DT360E" def _send(self, magic1, magic2): From 0bd58c6f598fe7239246ad9d61508febea625423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 24 Mar 2020 16:09:53 +0100 Subject: [PATCH 087/261] 0.13.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 25cc0fc5..6183611d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.12.0' +version = '0.13.0' setup( name='broadlink', From 26a4565e58057d9b34b99bf7ac7ffa69e530ab9a Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 5 Apr 2020 14:14:09 -0300 Subject: [PATCH 088/261] Add support for RM4 0x62bc (#331) * Add support for RM4 0x62bc * Fix device name --- broadlink/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a8010574..9fdee5f3 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -49,8 +49,9 @@ def gendevice(devtype, host, mac, name=None, cloud=None): ], rm4: [0x51da, # RM4b 0x5f36, # RM Mini 3 - 0x610f, # RM4c 0x610e, # RM4 mini + 0x610f, # RM4c + 0x62bc, # RM4 mini 0x62be # RM4c ], a1: [0x2714], # A1 From 2bc7b06c69487b7750792e1f913905aecd46da3c Mon Sep 17 00:00:00 2001 From: csabavirag Date: Sun, 5 Apr 2020 19:14:47 +0200 Subject: [PATCH 089/261] Adding support for LB1 (RGB Light Bulb - 0x60e8) (#332) --- broadlink/__init__.py | 62 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 9fdee5f3..c9e581fd 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -61,7 +61,8 @@ def gendevice(devtype, host, mac, name=None, cloud=None): hysen: [0x4EAD], # Hysen controller S1C: [0x2722], # S1 (SmartOne Alarm Kit) dooya: [0x4E4D], # Dooya DT360E (DOOYA_CURTAIN_V2) - bg1: [0x51E3] # BG Electrical Smart Power Socket + bg1: [0x51E3], # BG Electrical Smart Power Socket + lb1 : [0x60c8] # RGB Smart Bulb } # Look for the class associated to devtype in devices @@ -972,6 +973,65 @@ def set_percentage_and_wait(self, new_percentage): current = self.get_percentage() self.stop() +class lb1(device): + 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 } + + def __init__(self, host, mac, devtype): + device.__init__(self, host, mac, devtype) + self.type = "SmartBulb" + + def send_command(self,command, type = 'set'): + packet = bytearray(16+(int(len(command)/16) + 1)*16) + 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 = adler32(packet, 0xbeaf) & 0xffff + + packet[0x00] = (0x0c + len(command)) & 0xff + packet[0x06] = checksum & 0xff # Checksum 1 position + packet[0x07] = checksum >> 8 # Checksum 2 position + + response = self.send_packet(0x6a, packet) + + err = response[0x36] | (response[0x37] << 8) + if err != 0: + return None + payload = self.decrypt(bytes(response[0x38:])) + + responseLength = int(payload[0x0a]) | (int(payload[0x0b]) << 8) + if responseLength > 0: + self.state_dict = json.loads(payload[0x0e:0x0e+responseLength]) + + def set_json(self, jsonstr): + reconvert = json.loads(jsonstr) + if 'bulb_sceneidx' in reconvert.keys(): + reconvert['bulb_sceneidx'] = self.effect_map_dict.get(reconvert['bulb_sceneidx'], 255) + + self.send_command(json.dumps(reconvert)) + return json.dumps(self.state_dict) + + def set_state(self, state): + cmd = '{"pwr":%d}' % (1 if state == "ON" or state == 1 else 0) + self.send_command(cmd) + + def get_state(self): + cmd = "{}" + self.send_command(cmd) + return self.state_dict # 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) From a6827caaf7d796145649d3de69ee5e5bb2f43e66 Mon Sep 17 00:00:00 2001 From: majuss Date: Fri, 10 Apr 2020 09:23:15 +0200 Subject: [PATCH 090/261] Removed PyCRC as dependency; implemented CRC16 directly (#327) * Removed PyCRC as dependency; implemented CRC16 directly * replace crc16 with integrated function * Added self to calculate crc --- broadlink/__init__.py | 40 +++++++++++++++++++++++++++++++++++++--- setup.py | 2 +- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index c9e581fd..49309c21 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -701,10 +701,44 @@ def __init__(self, *args, **kwargs): # 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 calculate_crc16(self, input_data): + from ctypes import c_ushort + 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)) + + try: + is_string = isinstance(input_data, str) + is_bytes = isinstance(input_data, bytes) + + if not is_string and not is_bytes: + raise Exception("Please provide a string or a byte sequence " + "as argument for calculation.") + + crcValue = 0xffff + + for c in input_data: + d = ord(c) if is_string else c + tmp = crcValue ^ d + rotated = c_ushort(crcValue >> 8).value + crcValue = rotated ^ int(crc16_tab[(tmp & 0x00ff)], 0) + + return crcValue + except Exception as e: + print("EXCEPTION(calculate): {}".format(e)) + def send_request(self, input_payload): - from PyCRC.CRC16 import CRC16 - crc = CRC16(modbus_flag=True).calculate(bytes(input_payload)) + crc = calculate_crc16(bytes(input_payload)) # first byte is length, +2 for CRC16 request_payload = bytearray([len(input_payload) + 2, 0x00]) @@ -728,7 +762,7 @@ def send_request(self, input_payload): response_payload_len = response_payload[0] if response_payload_len + 2 > len(response_payload): raise ValueError('hysen_response_error', 'first byte of response is not length') - crc = CRC16(modbus_flag=True).calculate(bytes(response_payload[2:response_payload_len])) + crc = calculate_crc16(bytes(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] diff --git a/setup.py b/setup.py index 6183611d..b7fe6b6f 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', 'PyCRC'], + install_requires=['cryptography>=2.1.1'], description='Python API for controlling Broadlink IR controllers', classifiers=[ 'Development Status :: 4 - Beta', From 3a6d89aff29dd274d0d7c632147a19647a1fe776 Mon Sep 17 00:00:00 2001 From: tiagofreire-pt <41837236+tiagofreire-pt@users.noreply.github.com> Date: Fri, 10 Apr 2020 08:23:37 +0100 Subject: [PATCH 091/261] Adding new supported devices (#325) --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d5e0154d..8faba2be 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ -Python control for Broadlink RM2 IR controllers +Python control for Broadlink RM2, RM3 and RM4 series controllers =============================================== -A simple Python API for controlling IR controllers from [Broadlink](http://www.ibroadlink.com/rm/). At present, the following devices are currently supported: +A simple Python API for controlling IR/RF controllers from [Broadlink](http://www.ibroadlink.com/rm/). At present, the following devices are currently 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 There is currently no support for the cloud API. From a5925063f9a60ffe2ad3ff92bf5ddb30c9463000 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 13 Apr 2020 17:42:51 -0300 Subject: [PATCH 092/261] Fix padding algorithm for CBC mode Due to the lack of a parenthesis, the packets were getting 16 bytes larger than necessary. --- broadlink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 49309c21..ede6c312 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -265,7 +265,7 @@ def send_packet(self, command, payload): # pad the payload for AES encryption if payload: - payload += bytearray(16 - len(payload)%16) + payload += bytearray((16 - len(payload)) % 16) checksum = adler32(payload, 0xbeaf) & 0xffff packet[0x34] = checksum & 0xff From 548aa05da02d7200632b38a7199453f14d2b85a3 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 15 Apr 2020 04:04:01 -0300 Subject: [PATCH 093/261] Extend support to RM4 Pro (0x6026) (#340) --- broadlink/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index ede6c312..1a3873ed 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -49,9 +49,10 @@ def gendevice(devtype, host, mac, name=None, cloud=None): ], rm4: [0x51da, # RM4b 0x5f36, # RM Mini 3 - 0x610e, # RM4 mini + 0x6026, # RM4 Pro + 0x610e, # RM4 Mini 0x610f, # RM4c - 0x62bc, # RM4 mini + 0x62bc, # RM4 Mini 0x62be # RM4c ], a1: [0x2714], # A1 From 2f1ac5c9d32dfbfc8ea43023c070cc8b8dc9d8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 16 Apr 2020 17:01:30 +0200 Subject: [PATCH 094/261] 0.13.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b7fe6b6f..9afeef02 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.13.0' +version = '0.13.1' setup( name='broadlink', From ed87b8d1de4f5f7ba45dc45bf77c2deeb474a828 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sun, 19 Apr 2020 08:49:55 +0200 Subject: [PATCH 095/261] Add missing "self." to calculate_crc16 uses (#344) --- broadlink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 1a3873ed..b8f191fb 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -739,7 +739,7 @@ def calculate_crc16(self, input_data): def send_request(self, input_payload): - crc = calculate_crc16(bytes(input_payload)) + crc = self.calculate_crc16(bytes(input_payload)) # first byte is length, +2 for CRC16 request_payload = bytearray([len(input_payload) + 2, 0x00]) @@ -763,7 +763,7 @@ def send_request(self, input_payload): response_payload_len = response_payload[0] if response_payload_len + 2 > len(response_payload): raise ValueError('hysen_response_error', 'first byte of response is not length') - crc = calculate_crc16(bytes(response_payload[2:response_payload_len])) + crc = self.calculate_crc16(bytes(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] From 65a1b04666026322ba8b1db6a044eb953a45d71a Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 19 Apr 2020 03:50:16 -0300 Subject: [PATCH 096/261] Fix device name (#342) --- broadlink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index b8f191fb..3a78b03b 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -47,7 +47,7 @@ def gendevice(devtype, host, mac, name=None, cloud=None): 0x27d1, # new RM Mini3 0x27de # RM Mini 3 (C) ], - rm4: [0x51da, # RM4b + rm4: [0x51da, # RM4 Mini 0x5f36, # RM Mini 3 0x6026, # RM4 Pro 0x610e, # RM4 Mini From 6cf9292b4498a5a42299cf51a0e41033c87ede4a Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 19 Apr 2020 03:53:09 -0300 Subject: [PATCH 097/261] Revert adler32 (#345) * Revert adler32 * Fix checksum --- broadlink/__init__.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 3a78b03b..5253d7cf 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -8,7 +8,6 @@ import threading import time from datetime import datetime -from zlib import adler32 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -121,8 +120,11 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 packet[0x1c] = port & 0xff packet[0x1d] = port >> 8 packet[0x26] = 6 + + checksum = 0xbeaf + for b in packet: + checksum = (checksum + b) & 0xffff - checksum = adler32(packet, 0xbeaf) & 0xffff packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 @@ -268,7 +270,10 @@ def send_packet(self, command, payload): if payload: payload += bytearray((16 - len(payload)) % 16) - checksum = adler32(payload, 0xbeaf) & 0xffff + checksum = 0xbeaf + for b in payload: + checksum = (checksum + b) & 0xffff + packet[0x34] = checksum & 0xff packet[0x35] = checksum >> 8 @@ -276,7 +281,10 @@ def send_packet(self, command, payload): for i in range(len(payload)): packet.append(payload[i]) - checksum = adler32(packet, 0xbeaf) & 0xffff + checksum = 0xbeaf + for b in packet: + checksum = (checksum + b) & 0xffff + packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 @@ -409,7 +417,10 @@ def _encode(self, flag, js): for i in range(len(js)): packet.append(js[i]) - checksum = adler32(packet[0x08:], 0xc0ad) & 0xffff + checksum = 0xc0ad + for b in packet[0x08:]: + checksum = (checksum + b) & 0xffff + packet[0x06] = checksum & 0xff packet[0x07] = checksum >> 8 @@ -1034,7 +1045,9 @@ def send_command(self,command, type = 'set'): packet[0x0a] = len(command) packet[0x0e:] = map(ord, command) - checksum = adler32(packet, 0xbeaf) & 0xffff + checksum = 0xbeaf + for b in packet: + checksum = (checksum + b) & 0xffff packet[0x00] = (0x0c + len(command)) & 0xff packet[0x06] = checksum & 0xff # Checksum 1 position @@ -1091,7 +1104,10 @@ def setup(ssid, password, security_mode): payload[0x85] = pass_length # Character length of password payload[0x86] = security_mode # Type of encryption (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2) - checksum = adler32(payload, 0xbeaf) & 0xffff + checksum = 0xbeaf + for b in payload: + checksum = (checksum + b) & 0xffff + payload[0x20] = checksum & 0xff # Checksum 1 position payload[0x21] = checksum >> 8 # Checksum 2 position From 17968ef4d40e7cb484909b3ddad33d7795b80091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 21 Apr 2020 07:50:23 +0200 Subject: [PATCH 098/261] 0.13.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9afeef02..778f495f 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.13.1' +version = '0.13.2' setup( name='broadlink', From 3e8b008ef7bbcb9b87dc324c2acfff476ccb0330 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 25 Apr 2020 06:40:48 -0300 Subject: [PATCH 099/261] Close sockets after using them (#350) --- broadlink/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 5253d7cf..2910e16a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -138,6 +138,7 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8') cloud = bool(responsepacket[-1]) device = gendevice(devtype, host, mac, name=name, cloud=cloud) + cs.close() return device while (time.time() - starttime) < timeout: @@ -145,6 +146,7 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 try: response = cs.recvfrom(1024) except socket.timeout: + cs.close() return devices responsepacket = bytearray(response[0]) host = response[1] @@ -154,6 +156,7 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 cloud = bool(responsepacket[-1]) device = gendevice(devtype, host, mac, name=name, cloud=cloud) devices.append(device) + cs.close() return devices @@ -169,10 +172,6 @@ def __init__(self, host, mac, devtype, timeout=10, name=None, cloud=None): self.iv = bytearray( [0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) self.id = bytearray([0, 0, 0, 0]) - self.cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - self.cs.bind(('', 0)) self.type = "Unknown" self.lock = threading.Lock() @@ -290,15 +289,20 @@ def send_packet(self, command, payload): start_time = time.time() with self.lock: + cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + while True: try: - self.cs.sendto(packet, self.host) - self.cs.settimeout(1) - response = self.cs.recvfrom(2048) + cs.sendto(packet, self.host) + cs.settimeout(1) + response = cs.recvfrom(2048) break except socket.timeout: if (time.time() - start_time) > self.timeout: raise + finally: + cs.close() return bytearray(response[0]) @@ -1116,3 +1120,4 @@ def setup(ssid, password, security_mode): 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.close() From f3d0427133072f919768d5af81df7ba93bffcf07 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 25 Apr 2020 06:41:04 -0300 Subject: [PATCH 100/261] Extend support to RM4c Mini (0x6070) (#348) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 2910e16a..7f6f3770 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -49,6 +49,7 @@ def gendevice(devtype, host, mac, name=None, cloud=None): rm4: [0x51da, # RM4 Mini 0x5f36, # RM Mini 3 0x6026, # RM4 Pro + 0x6070, # RM4c Mini 0x610e, # RM4 Mini 0x610f, # RM4c 0x62bc, # RM4 Mini From 5e444988e1d41427171273241c6a88955fbf1601 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 2 May 2020 08:51:59 -0300 Subject: [PATCH 101/261] Fix 0x62be device name (#354) --- broadlink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 7f6f3770..40a4cd52 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -53,7 +53,7 @@ def gendevice(devtype, host, mac, name=None, cloud=None): 0x610e, # RM4 Mini 0x610f, # RM4c 0x62bc, # RM4 Mini - 0x62be # RM4c + 0x62be # RM4c Mini ], a1: [0x2714], # A1 mp1: [0x4EB5, # MP1 From 3216b069f0a13fb08295b303219710b6cafbf599 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 7 May 2020 02:59:21 -0300 Subject: [PATCH 102/261] Raise exceptions for errors (#356) * Raise exceptions for errors * Fix lb1 error check * Fix typo * Improve code readability * Fix exception --- broadlink/__init__.py | 99 +++++++++++++++-------------------------- broadlink/exceptions.py | 97 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 64 deletions(-) create mode 100644 broadlink/exceptions.py diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 40a4cd52..67d86d9d 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -12,6 +12,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from .exceptions import check_error, exception def gendevice(devtype, host, mac, name=None, cloud=None): devices = { @@ -221,10 +222,7 @@ def auth(self): payload[0x36] = ord('1') response = self.send_packet(0x65, payload) - - if any(response[0x22:0x24]): - return False - + check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) key = payload[0x04:0x14] @@ -301,7 +299,7 @@ def send_packet(self, command, payload): break except socket.timeout: if (time.time() - start_time) > self.timeout: - raise + raise exception(0xfffd) finally: cs.close() return bytearray(response[0]) @@ -328,7 +326,8 @@ def set_power_mask(self, sid_mask, state): packet[0x0d] = sid_mask packet[0x0e] = sid_mask if state else 0 - self.send_packet(0x6a, packet) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) def set_power(self, sid, state): """Sets the power state of the smart power strip.""" @@ -348,9 +347,7 @@ def check_power_raw(self): packet[0x08] = 0x01 response = self.send_packet(0x6a, packet) - err = response[0x22] | (response[0x23] << 8) - if err != 0: - return None + check_error(response[0x22:0x24]) payload = self.decrypt(bytes(response[0x38:])) if isinstance(payload[0x4], int): state = payload[0x0e] @@ -384,6 +381,7 @@ def get_state(self): eg. `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}`""" packet = self._encode(1, b'{}') response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) return self._decode(response) def set_state(self, pwr=None, pwr1=None, pwr2=None, maxworktime=None, maxworktime1=None, maxworktime2=None, idcbrightness=None): @@ -405,6 +403,7 @@ def set_state(self, pwr=None, pwr1=None, pwr2=None, maxworktime=None, maxworktim js = json.dumps(data).encode('utf8') packet = self._encode(2, js) response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) return self._decode(response) def _encode(self, flag, js): @@ -432,10 +431,6 @@ def _encode(self, flag, js): return packet def _decode(self, response): - err = response[0x22] | (response[0x23] << 8) - if err != 0: - return None - payload = self.decrypt(bytes(response[0x38:])) js_len = struct.unpack_from('> 8 # Checksum 2 position response = self.send_packet(0x6a, packet) - - err = response[0x36] | (response[0x37] << 8) - if err != 0: - return None + check_error(response[0x36:0x38]) payload = self.decrypt(bytes(response[0x38:])) responseLength = int(payload[0x0a]) | (int(payload[0x0b]) << 8) diff --git a/broadlink/exceptions.py b/broadlink/exceptions.py new file mode 100644 index 00000000..daf1e00f --- /dev/null +++ b/broadlink/exceptions.py @@ -0,0 +1,97 @@ +"""Exceptions for Broadlink devices.""" + + +class BroadlinkException(Exception): + """Common base class for all Broadlink exceptions.""" + pass + + +class AuthenticationError(BroadlinkException): + """Authentication error.""" + pass + + +class AuthorizationError(BroadlinkException): + """Authorization error.""" + pass + + +class CommandNotSupportedError(BroadlinkException): + """Command not supported error.""" + pass + + +class ConnectionClosedError(BroadlinkException): + """Connection closed error.""" + pass + + +class DataValidationError(BroadlinkException): + """Data validation error.""" + pass + + +class DeviceOfflineError(BroadlinkException): + """Device offline error.""" + pass + + +class ReadError(BroadlinkException): + """Read error.""" + pass + + +class SendError(BroadlinkException): + """Send error.""" + pass + + +class SSIDNotFoundError(BroadlinkException): + """SSID not found error.""" + pass + + +class StorageError(BroadlinkException): + """Storage error.""" + pass + + +class UnknownError(BroadlinkException): + """Unknown error.""" + pass + + +class WriteError(BroadlinkException): + """Write error.""" + pass + + +FIRMWARE_ERRORS = { + 0xffff: (AuthenticationError, "Authentication failed"), + 0xfffe: (ConnectionClosedError, "You have been logged out"), + 0xfffd: (DeviceOfflineError, "The device is offline"), + 0xfffc: (CommandNotSupportedError, "Command not supported"), + 0xfffb: (StorageError, "The device storage is full"), + 0xfffa: (DataValidationError, "Structure is abnormal"), + 0xfff9: (AuthorizationError, "Control key is expired"), + 0xfff8: (SendError, "Send error"), + 0xfff7: (WriteError, "Write error"), + 0xfff6: (ReadError, "Read error"), + 0xfff5: (SSIDNotFoundError, "SSID could not be found in AP configuration"), +} + + +def exception(error_code): + """Return exception corresponding to an error code.""" + try: + exc, msg = FIRMWARE_ERRORS[error_code] + return exc(msg) + except KeyError: + return UnknownError("Unknown error: " + hex(error_code)) + + +def check_error(error): + """Raise exception if an error occurred.""" + error_code = error[0] | (error[1] << 8) + if error_code: + raise exception(error_code) From 934bf7a9690564e3d1e39f6283af3ece4d7a5c15 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 7 May 2020 10:53:55 -0300 Subject: [PATCH 103/261] Close the socket outside the while loop (#359) * Close the socket outside the while loop * Solve conflicts --- broadlink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 67d86d9d..a8af0fa0 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -299,9 +299,9 @@ def send_packet(self, command, payload): break except socket.timeout: if (time.time() - start_time) > self.timeout: + cs.close() raise exception(0xfffd) - finally: - cs.close() + cs.close() return bytearray(response[0]) From a731c9c5a5d03b6894a882c4a8eaf5e0b333296a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 7 May 2020 15:54:19 +0200 Subject: [PATCH 104/261] 0.14.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 778f495f..c18df2a6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.13.2' +version = '0.14.0' setup( name='broadlink', From 7dd0a15a7bff75c46f76f61168baafb787f671c0 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 7 May 2020 13:12:58 -0300 Subject: [PATCH 105/261] Extend support to RM4 Pro (0x61a2) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a8af0fa0..da7a162a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -51,6 +51,7 @@ def gendevice(devtype, host, mac, name=None, cloud=None): 0x5f36, # RM Mini 3 0x6026, # RM4 Pro 0x6070, # RM4c Mini + 0x61a2, # RM4 Pro 0x610e, # RM4 Mini 0x610f, # RM4c 0x62bc, # RM4 Mini From 47999a9b67ab7633362b37b9fdcdd68349c3b973 Mon Sep 17 00:00:00 2001 From: Ameer Dawood Date: Sat, 9 May 2020 22:35:51 +0500 Subject: [PATCH 106/261] Fix error if no dst-ip is provided --- cli/broadlink_discovery | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery index 1c6b80b1..276884c3 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -7,7 +7,7 @@ import broadlink 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("--ip", default=None, help="ip address to use in the discovery") -parser.add_argument("--dst-ip", default=None, help="destination 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") args = parser.parse_args() print("Discovering...") From 7dec8f2355bfa179cb6a0c212263a781ffcd7d97 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Tue, 12 May 2020 16:19:49 -0300 Subject: [PATCH 107/261] Handle ReadError in the CLI (#365) * Handle ReadError * Make it more pythonic * Clean up --- cli/broadlink_cli | 94 ++++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 5045c5c1..ca3f23e7 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -6,8 +6,10 @@ import codecs import time import broadlink +from broadlink.exceptions import ReadError TICK = 32.84 +TIMEOUT = 30 IR_TOKEN = 0x26 @@ -85,7 +87,7 @@ parser.add_argument("--learnfile", help="save learned command to a specified fil parser.add_argument("--durations", action="store_true", help="use durations in micro seconds instead of the Broadlink format") parser.add_argument("--convert", action="store_true", help="convert input data to durations") -parser.add_argument("--joinwifi", nargs=2, help="Args are SSID PASSPHRASE to configure Broadlink device with"); +parser.add_argument("--joinwifi", nargs=2, help="Args are SSID PASSPHRASE to configure Broadlink device with") parser.add_argument("data", nargs='*', help="Data to send or convert") args = parser.parse_args() @@ -128,27 +130,29 @@ if args.send: dev.send_data(data) if args.learn or args.learnfile: dev.enter_learning() - data = None print("Learning...") - timeout = 30 - while (data is None) and (timeout > 0): - time.sleep(2) - timeout -= 2 - data = dev.check_data() - if data: - 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]))) - if args.learnfile: - print("Saving to {}".format(args.learnfile)) - with open(args.learnfile, "w") as text_file: - text_file.write(learned) + for second in range(TIMEOUT): + try: + data = dev.check_data() + except ReadError: + time.sleep(1) + else: + break else: 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]))) + if args.learnfile: + print("Saving to {}".format(args.learnfile)) + with open(args.learnfile, "w") as text_file: + text_file.write(learned) if args.check: if dev.check_power(): print('* ON *') @@ -194,13 +198,11 @@ if args.rfscanlearn: dev.sweep_frequency() print("Learning RF Frequency, press and hold the button to learn...") - timeout = 20 - - while (not dev.check_frequency()) and (timeout > 0): + for second in range(TIMEOUT): time.sleep(1) - timeout -= 1 - - if timeout <= 0: + if dev.check_frequency(): + break + else: print("RF Frequency not found") dev.cancel_sweep_frequency() exit(1) @@ -214,26 +216,26 @@ if args.rfscanlearn: dev.find_rf_packet() - data = None - timeout = 20 - - while (data is None) and (timeout > 0): - time.sleep(1) - timeout -= 1 - data = dev.check_data() - - if data: - print("Found RF Frequency - 2 of 2!") - 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: - print("Saving to {}".format(args.learnfile)) - with open(args.learnfile, "w") as text_file: - text_file.write(learned) + for second in range(TIMEOUT): + try: + data = dev.check_data() + except ReadError: + time.sleep(1) + else: + break else: print("No data received...") + exit(1) + + print("Found RF Frequency - 2 of 2!") + 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: + print("Saving to {}".format(args.learnfile)) + with open(args.learnfile, "w") as text_file: + text_file.write(learned) From 1dfdb065d05ab2e949c13c2b8cfdb999355a7e69 Mon Sep 17 00:00:00 2001 From: Jimmy Johnson Date: Tue, 12 May 2020 12:21:32 -0700 Subject: [PATCH 108/261] Fixing RF setup issue (#362) * Fixing RF setup issue * Addressing PR requests --- cli/broadlink_cli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index ca3f23e7..1abd2ad5 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -128,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: +if args.learn or (args.learnfile and not args.rfscanlearn): dev.enter_learning() print("Learning...") for second in range(TIMEOUT): From dec2b684d5c956a0c2402a3f2bdbf87ae0a4cc3b Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Wed, 20 May 2020 21:00:11 -0300 Subject: [PATCH 109/261] Replace bytes with bytearray --- broadlink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a8af0fa0..74e8e015 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -603,7 +603,7 @@ def check_data(self): def send_data(self, data): packet = bytearray(self._code_sending_header) - packet += bytes([0x02, 0x00, 0x00, 0x00]) + packet += bytearray([0x02, 0x00, 0x00, 0x00]) packet += data response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) From 9cda686d24a7968450051f36f8519891f044441a Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Fri, 22 May 2020 14:55:44 -0300 Subject: [PATCH 110/261] Handle storage error --- cli/broadlink_cli | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index 1abd2ad5..ead3a609 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -6,7 +6,7 @@ import codecs import time import broadlink -from broadlink.exceptions import ReadError +from broadlink.exceptions import ReadError, StorageError TICK = 32.84 TIMEOUT = 30 @@ -131,10 +131,11 @@ if args.send: if args.learn or (args.learnfile and not args.rfscanlearn): dev.enter_learning() print("Learning...") - for second in range(TIMEOUT): + start = time.time() + while time.time() - start < TIMEOUT: try: data = dev.check_data() - except ReadError: + except (ReadError, StorageError): time.sleep(1) else: break @@ -198,7 +199,8 @@ if args.rfscanlearn: dev.sweep_frequency() print("Learning RF Frequency, press and hold the button to learn...") - for second in range(TIMEOUT): + start = time.time() + while time.time() - start < TIMEOUT: time.sleep(1) if dev.check_frequency(): break @@ -216,10 +218,11 @@ if args.rfscanlearn: dev.find_rf_packet() - for second in range(TIMEOUT): + start = time.time() + while time.time() - start < TIMEOUT: try: data = dev.check_data() - except ReadError: + except (ReadError, StorageError): time.sleep(1) else: break From 753db8e25baa9258f6f9fb49b29937b302473391 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Fri, 22 May 2020 15:25:33 -0300 Subject: [PATCH 111/261] Sleep before reading the code --- cli/broadlink_cli | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/broadlink_cli b/cli/broadlink_cli index ead3a609..a2e18833 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -133,10 +133,11 @@ if args.learn or (args.learnfile and not args.rfscanlearn): print("Learning...") start = time.time() while time.time() - start < TIMEOUT: + time.sleep(1) try: data = dev.check_data() except (ReadError, StorageError): - time.sleep(1) + continue else: break else: @@ -220,10 +221,11 @@ if args.rfscanlearn: start = time.time() while time.time() - start < TIMEOUT: + time.sleep(1) try: data = dev.check_data() except (ReadError, StorageError): - time.sleep(1) + continue else: break else: From 76bcfd0bfbf7054c93b188bfbc82d0e0d0efe7db Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 8 Jun 2020 07:19:21 -0300 Subject: [PATCH 112/261] Add method for changing device name (#381) --- broadlink/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 3b17f456..896459f7 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -235,6 +235,12 @@ def auth(self): return True + def change_name(self, name): + packet = bytearray(4) + packet.extend(map(ord, name)) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + def get_type(self): return self.type From 4b90e437a0cacdcdfbb217ceaf1a3e5f3672e610 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 8 Jun 2020 07:20:22 -0300 Subject: [PATCH 113/261] Add method for obtaining the firmware version (#379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add method for obtaining firmware version * Use hex notation for bytes Co-authored-by: Daniel Høyer Iversen --- broadlink/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 896459f7..73f64dab 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -235,6 +235,13 @@ def auth(self): return True + def get_fwversion(self): + packet = bytearray([0x68]) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return payload[0x4] | payload[0x5] << 8 + def change_name(self, name): packet = bytearray(4) packet.extend(map(ord, name)) From d0dcf06e2fe167c74ed55f9441ea4423950a0471 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 8 Jun 2020 07:20:44 -0300 Subject: [PATCH 114/261] Rollback to old address lookup logic (#378) --- broadlink/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 73f64dab..35ce629e 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -77,11 +77,10 @@ def gendevice(devtype, host, mac, name=None, cloud=None): def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.255.255'): if local_ip_address is None: - local_ip_address = socket.gethostbyname(socket.gethostname()) - if local_ip_address.startswith('127.'): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets - local_ip_address = s.getsockname()[0] + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets + local_ip_address = s.getsockname()[0] + address = local_ip_address.split('.') cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) From 47087bfc7f91bd1de43108eed498f6ee91e48731 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 8 Jun 2020 07:21:00 -0300 Subject: [PATCH 115/261] Add support to SP2-CL (0x7544) (#375) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 35ce629e..43117d76 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -26,6 +26,7 @@ def gendevice(devtype, host, mac, name=None, cloud=None): 0x2728, # SPMini2 0x2733, 0x273e, # OEM branded SPMini 0x7530, 0x7546, 0x7918, # OEM branded SPMini2 + 0x7544, # SP2-CL 0x7D0D, # TMall OEM SPMini3 0x2736 # SPMiniPlus ], From 89cd7d29704a391ece3478489cf996dc07958a7a Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 8 Jun 2020 07:21:23 -0300 Subject: [PATCH 116/261] Improve A1, RM and RM4 sensors (#374) * Clean up A1 sensor * Improve communication with RM4 sensors * Add check_sensors() to RM class * Fix parenthesis * Fix noise levels * Fix precision error --- broadlink/__init__.py | 115 +++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 75 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 43117d76..a3b94a85 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -525,79 +525,39 @@ def get_energy(self): class a1(device): + + _SENSORS_AND_LEVELS = ( + ('light', ('dark', 'dim', 'normal', 'bright')), + ('air_quality', ('excellent', 'good', 'normal', 'bad')), + ('noise', ('quiet', 'normal', 'noisy')), + ) + def __init__(self, *args, **kwargs): device.__init__(self, *args, **kwargs) self.type = "A1" def check_sensors(self): - packet = bytearray(16) - packet[0] = 1 - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - data = {} - payload = self.decrypt(bytes(response[0x38:])) - if isinstance(payload[0x4], int): - data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 - data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 - light = payload[0x8] - air_quality = payload[0x0a] - noise = payload[0xc] - else: - data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 - data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0 - light = ord(payload[0x8]) - air_quality = ord(payload[0x0a]) - noise = ord(payload[0xc]) - if light == 0: - data['light'] = 'dark' - elif light == 1: - data['light'] = 'dim' - elif light == 2: - data['light'] = 'normal' - elif light == 3: - data['light'] = 'bright' - else: - data['light'] = 'unknown' - if air_quality == 0: - data['air_quality'] = 'excellent' - elif air_quality == 1: - data['air_quality'] = 'good' - elif air_quality == 2: - data['air_quality'] = 'normal' - elif air_quality == 3: - data['air_quality'] = 'bad' - else: - data['air_quality'] = 'unknown' - if noise == 0: - data['noise'] = 'quiet' - elif noise == 1: - data['noise'] = 'normal' - elif noise == 2: - data['noise'] = 'noisy' - else: - data['noise'] = 'unknown' + data = self.check_sensors_raw() + for sensor, levels in self._SENSORS_AND_LEVELS: + try: + data[sensor] = levels[data[sensor]] + except IndexError: + data[sensor] = 'unknown' return data def check_sensors_raw(self): - packet = bytearray(16) - packet[0] = 1 + packet = bytearray([0x1]) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - data = {} payload = self.decrypt(bytes(response[0x38:])) - if isinstance(payload[0x4], int): - data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 - data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 - data['light'] = payload[0x8] - data['air_quality'] = payload[0x0a] - data['noise'] = payload[0xc] - else: - data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 - data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0 - data['light'] = ord(payload[0x8]) - data['air_quality'] = ord(payload[0x0a]) - data['noise'] = ord(payload[0xc]) - return data + data = bytearray(payload[0x4:]) + return { + 'temperature': data[0x0] + data[0x1] / 10.0, + 'humidity': data[0x2] + data[0x3] / 10.0, + 'light': data[0x4], + 'air_quality': data[0x6], + 'noise': data[0x8], + } class rm(device): @@ -660,21 +620,22 @@ def find_rf_packet(self): return True return False - def _read_sensor(self, type, offset, divider): + def _check_sensors(self, command): packet = bytearray(self._request_header) - packet.append(type) + packet.append(command) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) payload = self.decrypt(bytes(response[0x38:])) - value_pos = len(self._request_header) + offset - if isinstance(payload[value_pos], int): - value = (payload[value_pos] + payload[value_pos+1] / divider) - else: - value = (ord(payload[value_pos]) + ord(payload[value_pos+1]) / divider) - return value + return bytearray(payload[len(self._request_header) + 4:]) def check_temperature(self): - return self._read_sensor( 0x01, 4, 10.0 ) + data = self._check_sensors(0x1) + return data[0x0] + data[0x1] / 10.0 + + def check_sensors(self): + data = self._check_sensors(0x1) + return {'temperature': data[0x0] + data[0x1] / 10.0} + class rm4(rm): def __init__(self, *args, **kwargs): @@ -684,17 +645,21 @@ def __init__(self, *args, **kwargs): self._code_sending_header = b'\xd0\x00' def check_temperature(self): - return self._read_sensor( 0x24, 4, 100.0 ) + data = self._check_sensors(0x24) + return data[0x0] + data[0x1] / 100.0 def check_humidity(self): - return self._read_sensor( 0x24, 6, 100.0 ) + data = self._check_sensors(0x24) + return data[0x2] + data[0x3] / 100.0 def check_sensors(self): + data = self._check_sensors(0x24) return { - 'temperature': self.check_temperature(), - 'humidity': self.check_humidity() + 'temperature': data[0x0] + data[0x1] / 100.0, + 'humidity': data[0x2] + data[0x3] / 100.0 } + # For legacy compatibility - don't use this class rm2(rm): def __init__(self): From bfbacbfffcfb88759e08bb6fc2b03f023b7f9a00 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 8 Jun 2020 07:21:55 -0300 Subject: [PATCH 117/261] Handle storage error in broadlink_discovery (#372) --- cli/broadlink_discovery | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery index 276884c3..8ff02ac5 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -3,6 +3,7 @@ import argparse import broadlink +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") @@ -20,8 +21,10 @@ for device in devices: ''.join(format(x, '02x') for x in device.mac))) print("Device file data (to be used with --device @filename in broadlink_cli) : ") print("{} {} {}".format(hex(device.devtype), device.host[0], ''.join(format(x, '02x') for x in device.mac))) - if hasattr(device, 'check_temperature'): + try: print("temperature = {}".format(device.check_temperature())) + except (AttributeError, StorageError): + pass print("") else: print("Error authenticating with device : {}".format(device.host)) From 8862be5c05f3723da2304d9f0c66e0489b02d792 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 8 Jun 2020 07:22:27 -0300 Subject: [PATCH 118/261] Use little-endian for the MAC address and connection ID (#370) --- broadlink/__init__.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a3b94a85..ccb5840b 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -137,7 +137,7 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 responsepacket = bytearray(response[0]) host = response[1] devtype = responsepacket[0x34] | responsepacket[0x35] << 8 - mac = responsepacket[0x3a:0x40] + mac = responsepacket[0x3f:0x39:-1] name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8') cloud = bool(responsepacket[-1]) device = gendevice(devtype, host, mac, name=name, cloud=cloud) @@ -154,7 +154,7 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 responsepacket = bytearray(response[0]) host = response[1] devtype = responsepacket[0x34] | responsepacket[0x35] << 8 - mac = responsepacket[0x3a:0x40] + mac = responsepacket[0x3f:0x39:-1] name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8') cloud = bool(responsepacket[-1]) device = gendevice(devtype, host, mac, name=name, cloud=cloud) @@ -230,7 +230,7 @@ def auth(self): if len(key) % 16 != 0: return False - self.id = payload[0x00:0x04] + self.id = payload[0x03::-1] self.update_aes(key) return True @@ -267,16 +267,16 @@ def send_packet(self, command, payload): packet[0x26] = command packet[0x28] = self.count & 0xff packet[0x29] = self.count >> 8 - packet[0x2a] = self.mac[0] - packet[0x2b] = self.mac[1] - packet[0x2c] = self.mac[2] - packet[0x2d] = self.mac[3] - packet[0x2e] = self.mac[4] - packet[0x2f] = self.mac[5] - packet[0x30] = self.id[0] - packet[0x31] = self.id[1] - packet[0x32] = self.id[2] - packet[0x33] = self.id[3] + 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 if payload: From e9e14e82d2d11dd85c97f0e94b0035c222a77da6 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 14 Jun 2020 11:37:37 -0300 Subject: [PATCH 119/261] Rename change_name() to set_name() (#382) --- broadlink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index ccb5840b..6406e15a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -242,7 +242,7 @@ def get_fwversion(self): payload = self.decrypt(response[0x38:]) return payload[0x4] | payload[0x5] << 8 - def change_name(self, name): + def set_name(self, name): packet = bytearray(4) packet.extend(map(ord, name)) response = self.send_packet(0x6a, packet) From e41d2409422528de1872333e1d4c9b07f96910c3 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Tue, 16 Jun 2020 18:19:18 -0300 Subject: [PATCH 120/261] Add a method to lock/unlock the device --- broadlink/__init__.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 6406e15a..f54b7b27 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -244,9 +244,21 @@ def get_fwversion(self): def set_name(self, name): packet = bytearray(4) - packet.extend(map(ord, name)) + packet += name.encode('utf-8') + packet += bytearray(0x50 - len(packet)) + packet[0x43] = self.cloud response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) + self.name = name + + def set_lock(self, state): + packet = bytearray(4) + packet += self.name.encode('utf-8') + packet += bytearray(0x50 - len(packet)) + packet[0x43] = state + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + self.cloud = bool(state) def get_type(self): return self.type From f10e4fdef03db702aeee2865e5df467d565b8317 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Tue, 16 Jun 2020 16:19:32 -0300 Subject: [PATCH 121/261] Add models and manufacturers --- broadlink/__init__.py | 138 +++++++++++++++++++++++++----------------- 1 file changed, 82 insertions(+), 56 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index f54b7b27..5ae957ed 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -14,66 +14,90 @@ from .exceptions import check_error, exception + def gendevice(devtype, host, mac, name=None, cloud=None): devices = { - sp1: [0], - sp2: [0x2711, # SP2 - 0x2719, 0x7919, 0x271a, 0x791a, # Honeywell SP2 - 0x2720, # SPMini - 0x753e, # SP3 - 0x7D00, # OEM branded SP3 - 0x947a, 0x9479, # SP3S - 0x2728, # SPMini2 - 0x2733, 0x273e, # OEM branded SPMini - 0x7530, 0x7546, 0x7918, # OEM branded SPMini2 - 0x7544, # SP2-CL - 0x7D0D, # TMall OEM SPMini3 - 0x2736 # SPMiniPlus - ], - rm: [0x2712, # RM2 - 0x2737, # RM Mini - 0x273d, # RM Pro Phicomm - 0x2783, # RM2 Home Plus - 0x277c, # RM2 Home Plus GDT - 0x272a, # RM2 Pro Plus - 0x2787, # RM2 Pro Plus2 - 0x279d, # RM2 Pro Plus3 - 0x27a9, # RM2 Pro Plus_300 - 0x278b, # RM2 Pro Plus BL - 0x2797, # RM2 Pro Plus HYC - 0x27a1, # RM2 Pro Plus R1 - 0x27a6, # RM2 Pro PP - 0x278f, # RM Mini Shate - 0x27c2, # RM Mini 3 - 0x27d1, # new RM Mini3 - 0x27de # RM Mini 3 (C) - ], - rm4: [0x51da, # RM4 Mini - 0x5f36, # RM Mini 3 - 0x6026, # RM4 Pro - 0x6070, # RM4c Mini - 0x61a2, # RM4 Pro - 0x610e, # RM4 Mini - 0x610f, # RM4c - 0x62bc, # RM4 Mini - 0x62be # RM4c Mini - ], - a1: [0x2714], # A1 - mp1: [0x4EB5, # MP1 - 0x4EF7 # Honyar oem mp1 - ], - hysen: [0x4EAD], # Hysen controller - S1C: [0x2722], # S1 (SmartOne Alarm Kit) - dooya: [0x4E4D], # Dooya DT360E (DOOYA_CURTAIN_V2) - bg1: [0x51E3], # BG Electrical Smart Power Socket - lb1 : [0x60c8] # RGB Smart Bulb + 0x0000: (sp1, "SP1", "Broadlink"), + + 0x2711: (sp2, "SP2", "Broadlink"), + 0x2719: (sp2, "SP2-compatible", "Honeywell"), + 0x271a: (sp2, "SP2-compatible", "Honeywell"), + 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)"), + 0x753e: (sp2, "SP mini 3", "Broadlink"), + 0X7544: (sp2, "SP2-CL", "Broadlink"), + 0x7546: (sp2, "SP2-UK/BR/IN", "Broadlink (OEM)"), + 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"), + + 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"), + 0x27d1: (rm, "RM mini 3", "Broadlink"), + 0x27de: (rm, "RM mini 3", "Broadlink"), + + 0x51da: (rm4, "RM4 mini", "Broadlink"), + 0x5f36: (rm4, "RM mini", "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"), + + 0x2714: (a1, "e-Sensor", "Broadlink"), + + 0x4eb5: (mp1, "MP1-1K4S", "Broadlink"), + 0x4ef7: (mp1, "MP1-1K4S", "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"), + + 0x2722: (S1C, "S2KIT", "Broadlink"), + + 0x4ead: (hysen, "HY02B05H", "Hysen"), + + 0x4e4d: (dooya, "DT360E-45/20", "Dooya"), + + 0x51e3: (bg1, "BG800/BG900", "BG Electrical"), } # Look for the class associated to devtype in devices - [device_class] = [dev for dev in devices if devtype in devices[dev]] or [None] - if device_class is None: + try: + dev_class, model, manufacturer = devices[devtype] + except KeyError: return device(host, mac, devtype, name=name, cloud=cloud) - return device_class(host, mac, devtype, name=name, cloud=cloud) + + dev = dev_class(host, mac, devtype, name=name, cloud=cloud) + dev.model = model + dev.manufacturer = manufacturer + return dev def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.255.255'): @@ -170,6 +194,8 @@ def __init__(self, host, mac, devtype, timeout=10, name=None, cloud=None): self.devtype = devtype if devtype is not None else 0x272a self.name = name self.cloud = cloud + self.model = None + self.manufacturer = None self.timeout = timeout self.count = random.randrange(0xffff) self.iv = bytearray( @@ -1000,8 +1026,8 @@ class lb1(device): 'color jumping' : 6, 'multicolor jumping' : 7 } - def __init__(self, host, mac, devtype): - device.__init__(self, host, mac, devtype) + def __init__(self, *args, **kwargs): + device.__init__(self, *args, **kwargs) self.type = "SmartBulb" def send_command(self,command, type = 'set'): From daebd806fd8529b9c29b4d70f82a036f30ea5847 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Tue, 16 Jun 2020 23:15:23 -0300 Subject: [PATCH 122/261] Fix 0x5f36 name --- broadlink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 5ae957ed..9366619f 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -58,7 +58,7 @@ def gendevice(devtype, host, mac, name=None, cloud=None): 0x27de: (rm, "RM mini 3", "Broadlink"), 0x51da: (rm4, "RM4 mini", "Broadlink"), - 0x5f36: (rm4, "RM mini", "Broadlink"), + 0x5f36: (rm4, "RM mini 3", "Broadlink"), 0x6026: (rm4, "RM4 pro", "Broadlink"), 0x6070: (rm4, "RM4C mini", "Broadlink"), 0x610e: (rm4, "RM4 mini", "Broadlink"), From 943706de5cd6f0d18f7ffb7dae9a36ebd5001e2b Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Sat, 18 Jul 2020 21:53:00 -0300 Subject: [PATCH 123/261] Expose device dictionary --- broadlink/__init__.py | 46 ++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 9366619f..ab123ac1 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -15,8 +15,8 @@ from .exceptions import check_error, exception -def gendevice(devtype, host, mac, name=None, cloud=None): - devices = { +def get_devices(): + return { 0x0000: (sp1, "SP1", "Broadlink"), 0x2711: (sp2, "SP2", "Broadlink"), @@ -88,16 +88,24 @@ def gendevice(devtype, host, mac, name=None, cloud=None): 0x51e3: (bg1, "BG800/BG900", "BG Electrical"), } - # Look for the class associated to devtype in devices + +def gendevice(dev_type, host, mac, name=None, cloud=None): + """Generate a device.""" try: - dev_class, model, manufacturer = devices[devtype] - except KeyError: - return device(host, mac, devtype, name=name, cloud=cloud) + dev_class, model, manufacturer = get_devices()[dev_type] - dev = dev_class(host, mac, devtype, name=name, cloud=cloud) - dev.model = model - dev.manufacturer = manufacturer - return dev + except KeyError: + return device(host, mac, dev_type, name=name, cloud=cloud) + + return dev_class( + host, + mac, + dev_type, + name=name, + model=model, + manufacturer=manufacturer, + cloud=cloud, + ) def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.255.255'): @@ -188,15 +196,25 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 class device: - def __init__(self, host, mac, devtype, timeout=10, name=None, cloud=None): + def __init__( + self, + host, + mac, + devtype, + timeout=10, + name=None, + model=None, + manufacturer=None, + cloud=None + ): self.host = host self.mac = mac.encode() if isinstance(mac, str) else mac self.devtype = devtype if devtype is not None else 0x272a + self.timeout = timeout self.name = name + self.model = model + self.manufacturer = manufacturer self.cloud = cloud - self.model = None - self.manufacturer = None - self.timeout = timeout self.count = random.randrange(0xffff) self.iv = bytearray( [0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) From 02ce6ec66161298b047c5f3f35544ad7db39fdfa Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 31 Jul 2020 02:09:46 -0300 Subject: [PATCH 124/261] Add support for SC1 device (#388) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index ab123ac1..493b4d9e 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -31,6 +31,7 @@ def get_devices(): 0x753e: (sp2, "SP mini 3", "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"), From e4ad86278d06e81299ff1e7a0f5a83b8c8b95da3 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 31 Jul 2020 02:10:21 -0300 Subject: [PATCH 125/261] Rename lock byte (#389) --- broadlink/__init__.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 493b4d9e..8c844ae8 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -90,13 +90,13 @@ def get_devices(): } -def gendevice(dev_type, host, mac, name=None, cloud=None): +def gendevice(dev_type, host, mac, name=None, is_locked=None): """Generate a device.""" try: dev_class, model, manufacturer = get_devices()[dev_type] except KeyError: - return device(host, mac, dev_type, name=name, cloud=cloud) + return device(host, mac, dev_type, name=name, is_locked=is_locked) return dev_class( host, @@ -105,7 +105,7 @@ def gendevice(dev_type, host, mac, name=None, cloud=None): name=name, model=model, manufacturer=manufacturer, - cloud=cloud, + is_locked=is_locked, ) @@ -172,8 +172,8 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 devtype = responsepacket[0x34] | responsepacket[0x35] << 8 mac = responsepacket[0x3f:0x39:-1] name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8') - cloud = bool(responsepacket[-1]) - device = gendevice(devtype, host, mac, name=name, cloud=cloud) + is_locked = bool(responsepacket[-1]) + device = gendevice(devtype, host, mac, name=name, is_locked=is_locked) cs.close() return device @@ -189,8 +189,8 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 devtype = responsepacket[0x34] | responsepacket[0x35] << 8 mac = responsepacket[0x3f:0x39:-1] name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8') - cloud = bool(responsepacket[-1]) - device = gendevice(devtype, host, mac, name=name, cloud=cloud) + is_locked = bool(responsepacket[-1]) + device = gendevice(devtype, host, mac, name=name, is_locked=is_locked) devices.append(device) cs.close() return devices @@ -206,7 +206,7 @@ def __init__( name=None, model=None, manufacturer=None, - cloud=None + is_locked=None ): self.host = host self.mac = mac.encode() if isinstance(mac, str) else mac @@ -215,7 +215,7 @@ def __init__( self.name = name self.model = model self.manufacturer = manufacturer - self.cloud = cloud + self.is_locked = is_locked self.count = random.randrange(0xffff) self.iv = bytearray( [0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) @@ -291,7 +291,7 @@ def set_name(self, name): packet = bytearray(4) packet += name.encode('utf-8') packet += bytearray(0x50 - len(packet)) - packet[0x43] = self.cloud + packet[0x43] = self.is_locked response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) self.name = name @@ -303,7 +303,7 @@ def set_lock(self, state): packet[0x43] = state response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - self.cloud = bool(state) + self.is_locked = bool(state) def get_type(self): return self.type From fe4aa5b991e033d865e956d7a96526998e917e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 31 Jul 2020 07:11:51 +0200 Subject: [PATCH 126/261] 0.14.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c18df2a6..ae42b4f2 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.14.0' +version = '0.14.1' setup( name='broadlink', From 816773658868acd638c5a70fea6024ab05bdeb7d Mon Sep 17 00:00:00 2001 From: Daan Klijn Date: Mon, 10 Aug 2020 21:28:09 +0200 Subject: [PATCH 127/261] Allow setting custom discover ip port --- broadlink/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 8c844ae8..8cf74d47 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -109,7 +109,12 @@ def gendevice(dev_type, host, mac, name=None, is_locked=None): ) -def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.255.255'): +def discover( + timeout=None, + local_ip_address=None, + discover_ip_address='255.255.255.255', + discover_ip_port=80 +): if local_ip_address is None: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets @@ -156,7 +161,7 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 packet[0x1c] = port & 0xff packet[0x1d] = port >> 8 packet[0x26] = 6 - + checksum = 0xbeaf for b in packet: checksum = (checksum + b) & 0xffff @@ -164,7 +169,7 @@ def discover(timeout=None, local_ip_address=None, discover_ip_address='255.255.2 packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 - cs.sendto(packet, (discover_ip_address, 80)) + cs.sendto(packet, (discover_ip_address, discover_ip_port)) if timeout is None: response = cs.recvfrom(1024) responsepacket = bytearray(response[0]) @@ -285,7 +290,7 @@ def get_fwversion(self): response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return payload[0x4] | payload[0x5] << 8 + return payload[0x4] | payload[0x5] << 8 def set_name(self, name): packet = bytearray(4) @@ -446,7 +451,7 @@ def __init__(self, *args, **kwargs): def get_state(self): """Get state of device. - + Returns: dict: Dictionary of current state eg. `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}`""" From e59d4a6875542ea911268a60b3604aaf539124a6 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 6 Sep 2020 05:50:57 -0300 Subject: [PATCH 128/261] Fix RM4 command header (#412) --- broadlink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 8cf74d47..61e9471f 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -704,7 +704,7 @@ def __init__(self, *args, **kwargs): device.__init__(self, *args, **kwargs) self.type = "RM4" self._request_header = b'\x04\x00' - self._code_sending_header = b'\xd0\x00' + self._code_sending_header = b'\xda\x00' def check_temperature(self): data = self._check_sensors(0x24) From b4aa15617aef4c24075bc06a99b929b725681838 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 6 Sep 2020 05:51:17 -0300 Subject: [PATCH 129/261] Add support for RM4 mini (0x648d) (#411) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 61e9471f..a100fe3e 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -67,6 +67,7 @@ def get_devices(): 0x61a2: (rm4, "RM4 pro", "Broadlink"), 0x62bc: (rm4, "RM4 mini", "Broadlink"), 0x62be: (rm4, "RM4C mini", "Broadlink"), + 0x648d: (rm4, "RM4 mini", "Broadlink"), 0x2714: (a1, "e-Sensor", "Broadlink"), From cf1e2c3a3004635f24525f3e485d7a47a1106eb5 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 6 Sep 2020 05:51:45 -0300 Subject: [PATCH 130/261] Check the sum and length of received packets (#410) * Use sum() for checksums * Check sum and length of received packets --- broadlink/__init__.py | 48 ++++++--------- broadlink/exceptions.py | 133 +++++++++++++++++++++++++++++++--------- 2 files changed, 123 insertions(+), 58 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a100fe3e..9338ef15 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -163,10 +163,7 @@ def discover( packet[0x1d] = port >> 8 packet[0x26] = 6 - checksum = 0xbeaf - for b in packet: - checksum = (checksum + b) & 0xffff - + checksum = sum(packet, 0xbeaf) & 0xffff packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 @@ -345,10 +342,7 @@ def send_packet(self, command, payload): if payload: payload += bytearray((16 - len(payload)) % 16) - checksum = 0xbeaf - for b in payload: - checksum = (checksum + b) & 0xffff - + checksum = sum(payload, 0xbeaf) & 0xffff packet[0x34] = checksum & 0xff packet[0x35] = checksum >> 8 @@ -356,10 +350,7 @@ def send_packet(self, command, payload): for i in range(len(payload)): packet.append(payload[i]) - checksum = 0xbeaf - for b in packet: - checksum = (checksum + b) & 0xffff - + checksum = sum(packet, 0xbeaf) & 0xffff packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 @@ -372,14 +363,23 @@ def send_packet(self, command, payload): try: cs.sendto(packet, self.host) cs.settimeout(1) - response = cs.recvfrom(2048) + resp, _ = cs.recvfrom(2048) + resp = bytearray(resp) break except socket.timeout: if (time.time() - start_time) > self.timeout: cs.close() - raise exception(0xfffd) + raise exception(-4000) # Network timeout. cs.close() - return bytearray(response[0]) + + if len(resp) < 0x30: + raise exception(-4007) # Length error. + + checksum = resp[0x20] | (resp[0x21] << 8) + if sum(resp, 0xbeaf) - sum(resp[0x20:0x22]) & 0xffff != checksum: + raise exception(-4008) # Checksum error. + + return resp class mp1(device): @@ -498,9 +498,7 @@ def _encode(self, flag, js): for i in range(len(js)): packet.append(js[i]) - checksum = 0xc0ad - for b in packet[0x08:]: - checksum = (checksum + b) & 0xffff + checksum = sum(packet[0x08:], 0xc0ad) & 0xffff packet[0x06] = checksum & 0xff packet[0x07] = checksum >> 8 @@ -1055,8 +1053,9 @@ def __init__(self, *args, **kwargs): device.__init__(self, *args, **kwargs) self.type = "SmartBulb" - def send_command(self,command, type = 'set'): + def send_command(self, command, type='set'): packet = bytearray(16+(int(len(command)/16) + 1)*16) + packet[0x00] = 0x0c + len(command) & 0xff packet[0x02] = 0xa5 packet[0x03] = 0xa5 packet[0x04] = 0x5a @@ -1066,11 +1065,7 @@ def send_command(self,command, type = 'set'): packet[0x0a] = len(command) packet[0x0e:] = map(ord, command) - checksum = 0xbeaf - for b in packet: - checksum = (checksum + b) & 0xffff - - packet[0x00] = (0x0c + len(command)) & 0xff + checksum = sum(packet, 0xbeaf) & 0xffff packet[0x06] = checksum & 0xff # Checksum 1 position packet[0x07] = checksum >> 8 # Checksum 2 position @@ -1122,10 +1117,7 @@ def setup(ssid, password, security_mode): payload[0x85] = pass_length # Character length of password payload[0x86] = security_mode # Type of encryption (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2) - checksum = 0xbeaf - for b in payload: - checksum = (checksum + b) & 0xffff - + checksum = sum(payload, 0xbeaf) & 0xffff payload[0x20] = checksum & 0xff # Checksum 1 position payload[0x21] = checksum >> 8 # Checksum 2 position diff --git a/broadlink/exceptions.py b/broadlink/exceptions.py index daf1e00f..dfa0df90 100644 --- a/broadlink/exceptions.py +++ b/broadlink/exceptions.py @@ -1,97 +1,170 @@ """Exceptions for Broadlink devices.""" +import struct class BroadlinkException(Exception): """Common base class for all Broadlink exceptions.""" + + def __init__(self, *args, **kwargs): + """Initialize the exception.""" + super().__init__(*args, **kwargs) + if len(args) >= 3: + self.errno = args[0] + self.strerror = "%s: %s" % (args[1], args[2]) + elif len(args) == 2: + self.errno = args[0] + self.strerror = args[1] + elif len(args) == 1: + self.errno = None + self.strerror = args[0] + else: + self.errno = None + self.strerror = None + + def __str__(self): + """Return the error message.""" + if self.errno is not None: + err_msg = "[Errno %s] %s" % (self.errno, self.strerror) + elif self.strerror is not None: + err_msg = "%s" % (self.strerror) + else: + err_msg = "" + return err_msg + + +class FirmwareException(BroadlinkException): + """Common base class for all firmware exceptions.""" + pass -class AuthenticationError(BroadlinkException): +class AuthenticationError(FirmwareException): """Authentication error.""" + pass -class AuthorizationError(BroadlinkException): +class AuthorizationError(FirmwareException): """Authorization error.""" + pass -class CommandNotSupportedError(BroadlinkException): +class CommandNotSupportedError(FirmwareException): """Command not supported error.""" + pass -class ConnectionClosedError(BroadlinkException): +class ConnectionClosedError(FirmwareException): """Connection closed error.""" + pass -class DataValidationError(BroadlinkException): +class DataValidationError(FirmwareException): """Data validation error.""" + pass -class DeviceOfflineError(BroadlinkException): +class DeviceOfflineError(FirmwareException): """Device offline error.""" + pass -class ReadError(BroadlinkException): +class ReadError(FirmwareException): """Read error.""" + pass -class SendError(BroadlinkException): +class SendError(FirmwareException): """Send error.""" + pass -class SSIDNotFoundError(BroadlinkException): +class SSIDNotFoundError(FirmwareException): """SSID not found error.""" + pass -class StorageError(BroadlinkException): +class StorageError(FirmwareException): """Storage error.""" + pass -class UnknownError(BroadlinkException): - """Unknown error.""" +class WriteError(FirmwareException): + """Write error.""" + pass -class WriteError(BroadlinkException): - """Write error.""" +class SDKException(BroadlinkException): + """Common base class for all SDK exceptions.""" + + pass + + +class ChecksumError(SDKException): + """Received data packet check error.""" + + pass + + +class LengthError(SDKException): + """Received data packet length error.""" + + pass + + +class NetworkTimeoutError(SDKException): + """Network timeout error.""" + + pass + + +class UnknownError(BroadlinkException): + """Unknown error.""" + pass -FIRMWARE_ERRORS = { - 0xffff: (AuthenticationError, "Authentication failed"), - 0xfffe: (ConnectionClosedError, "You have been logged out"), - 0xfffd: (DeviceOfflineError, "The device is offline"), - 0xfffc: (CommandNotSupportedError, "Command not supported"), - 0xfffb: (StorageError, "The device storage is full"), - 0xfffa: (DataValidationError, "Structure is abnormal"), - 0xfff9: (AuthorizationError, "Control key is expired"), - 0xfff8: (SendError, "Send error"), - 0xfff7: (WriteError, "Write error"), - 0xfff6: (ReadError, "Read error"), - 0xfff5: (SSIDNotFoundError, "SSID could not be found in AP configuration"), +BROADLINK_EXCEPTIONS = { + # Firmware-related errors are generated by the device. + -1: (AuthenticationError, "Authentication failed"), + -2: (ConnectionClosedError, "You have been logged out"), + -3: (DeviceOfflineError, "The device is offline"), + -4: (CommandNotSupportedError, "Command not supported"), + -5: (StorageError, "The device storage is full"), + -6: (DataValidationError, "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"), + # DNASDK related errors are generated by this module. + -4000: (NetworkTimeoutError, "Network timeout"), + -4007: (LengthError, "Received data packet length error"), + -4008: (ChecksumError, "Received data packet check error"), } def exception(error_code): """Return exception corresponding to an error code.""" try: - exc, msg = FIRMWARE_ERRORS[error_code] - return exc(msg) + exc, msg = BROADLINK_EXCEPTIONS[error_code] + return exc(error_code, msg) except KeyError: - return UnknownError("Unknown error: " + hex(error_code)) + return UnknownError(error_code, "Unknown error") def check_error(error): """Raise exception if an error occurred.""" - error_code = error[0] | (error[1] << 8) + error_code = struct.unpack("h", error)[0] if error_code: raise exception(error_code) From 086fd1cd75b9ec68d28b377b03823a9f2933391c Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 6 Sep 2020 05:52:12 -0300 Subject: [PATCH 131/261] Fix setters (#407) --- broadlink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 9338ef15..2ee2e666 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -294,7 +294,7 @@ def set_name(self, name): packet = bytearray(4) packet += name.encode('utf-8') packet += bytearray(0x50 - len(packet)) - packet[0x43] = self.is_locked + packet[0x43] = bool(self.is_locked) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) self.name = name @@ -303,7 +303,7 @@ def set_lock(self, state): packet = bytearray(4) packet += self.name.encode('utf-8') packet += bytearray(0x50 - len(packet)) - packet[0x43] = state + packet[0x43] = bool(state) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) self.is_locked = bool(state) From 5af3a81264e3e109ea54d79b98e70771f8c33cec Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Mon, 14 Sep 2020 22:33:59 -0300 Subject: [PATCH 132/261] Restore VPN support --- broadlink/__init__.py | 7 ++----- broadlink/helpers.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 broadlink/helpers.py diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 2ee2e666..bc3a63de 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -13,6 +13,7 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from .exceptions import check_error, exception +from .helpers import get_local_ip def get_devices(): @@ -116,11 +117,7 @@ def discover( discover_ip_address='255.255.255.255', discover_ip_port=80 ): - if local_ip_address is None: - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: - s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets - local_ip_address = s.getsockname()[0] - + local_ip_address = local_ip_address or get_local_ip() address = local_ip_address.split('.') cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) diff --git a/broadlink/helpers.py b/broadlink/helpers.py new file mode 100644 index 00000000..af0c8c69 --- /dev/null +++ b/broadlink/helpers.py @@ -0,0 +1,18 @@ +"""Helper functions.""" +import socket + + +def get_local_ip() -> str: + """Try to determine the local IP address of the machine.""" + # Useful for VPNs. + try: + local_ip_address = socket.gethostbyname(socket.gethostname()) + if not local_ip_address.startswith('127.'): + return local_ip_address + except OSError: + pass + + # Connecting to UDP address does not send packets. + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(('8.8.8.8', 53)) + return s.getsockname()[0] From 20d1d63fc3b8a96da517d52a58d7497526fdc11b Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Tue, 15 Sep 2020 22:55:37 -0300 Subject: [PATCH 133/261] Create a SDKException for DNS errors --- broadlink/exceptions.py | 21 ++++++++++++--------- broadlink/helpers.py | 6 ++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/broadlink/exceptions.py b/broadlink/exceptions.py index dfa0df90..0b73e55d 100644 --- a/broadlink/exceptions.py +++ b/broadlink/exceptions.py @@ -13,23 +13,19 @@ def __init__(self, *args, **kwargs): self.strerror = "%s: %s" % (args[1], args[2]) elif len(args) == 2: self.errno = args[0] - self.strerror = args[1] + self.strerror = str(args[1]) elif len(args) == 1: self.errno = None - self.strerror = args[0] + self.strerror = str(args[0]) else: self.errno = None - self.strerror = None + self.strerror = "" def __str__(self): """Return the error message.""" if self.errno is not None: - err_msg = "[Errno %s] %s" % (self.errno, self.strerror) - elif self.strerror is not None: - err_msg = "%s" % (self.strerror) - else: - err_msg = "" - return err_msg + return "[Errno %s] %s" % (self.errno, self.strerror) + return self.strerror class FirmwareException(BroadlinkException): @@ -122,6 +118,12 @@ class LengthError(SDKException): pass +class DNSError(SDKException): + """Domain name resolution error.""" + + pass + + class NetworkTimeoutError(SDKException): """Network timeout error.""" @@ -151,6 +153,7 @@ class UnknownError(BroadlinkException): -4000: (NetworkTimeoutError, "Network timeout"), -4007: (LengthError, "Received data packet length error"), -4008: (ChecksumError, "Received data packet check error"), + -4013: (DNSError, "Domain name resolution error"), } diff --git a/broadlink/helpers.py b/broadlink/helpers.py index af0c8c69..40f06e3d 100644 --- a/broadlink/helpers.py +++ b/broadlink/helpers.py @@ -1,6 +1,8 @@ """Helper functions.""" import socket +from .exceptions import exception + def get_local_ip() -> str: """Try to determine the local IP address of the machine.""" @@ -9,8 +11,8 @@ def get_local_ip() -> str: local_ip_address = socket.gethostbyname(socket.gethostname()) if not local_ip_address.startswith('127.'): return local_ip_address - except OSError: - pass + except socket.gaierror: + raise exception(-4013) # DNS Error # Connecting to UDP address does not send packets. with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: From 792f1a1c601469060fc3d46419818b13ff6ca36c Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Wed, 16 Sep 2020 04:00:19 -0300 Subject: [PATCH 134/261] Rename DNSError to DNSLookupError --- broadlink/exceptions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/broadlink/exceptions.py b/broadlink/exceptions.py index 0b73e55d..e00f7d87 100644 --- a/broadlink/exceptions.py +++ b/broadlink/exceptions.py @@ -118,8 +118,8 @@ class LengthError(SDKException): pass -class DNSError(SDKException): - """Domain name resolution error.""" +class DNSLookupError(SDKException): + """Failed to obtain local IP address.""" pass @@ -153,7 +153,7 @@ class UnknownError(BroadlinkException): -4000: (NetworkTimeoutError, "Network timeout"), -4007: (LengthError, "Received data packet length error"), -4008: (ChecksumError, "Received data packet check error"), - -4013: (DNSError, "Domain name resolution error"), + -4013: (DNSLookupError, "Failed to obtain local IP address"), } From 8bf107ab696d3bd72b26a19b2079a59b68b297fd Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Wed, 16 Sep 2020 04:41:28 -0300 Subject: [PATCH 135/261] Add docstrings and annotations --- broadlink/__init__.py | 348 ++++++++++++++++++++++++---------------- broadlink/exceptions.py | 36 ----- 2 files changed, 211 insertions(+), 173 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index bc3a63de..a31895e5 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,5 +1,5 @@ -#!/usr/bin/python - +#!/usr/bin/python3 +"""The python-broadlink library.""" import codecs import json import random @@ -16,10 +16,10 @@ from .helpers import get_local_ip -def get_devices(): +def get_devices() -> dict: + """Return all supported devices.""" return { 0x0000: (sp1, "SP1", "Broadlink"), - 0x2711: (sp2, "SP2", "Broadlink"), 0x2719: (sp2, "SP2-compatible", "Honeywell"), 0x271a: (sp2, "SP2-compatible", "Honeywell"), @@ -40,7 +40,6 @@ def get_devices(): 0x7d0d: (sp2, "SP mini 3", "Broadlink (OEM)"), 0x9479: (sp2, "SP3S-US", "Broadlink"), 0x947a: (sp2, "SP3S-EU", "Broadlink"), - 0x2712: (rm, "RM pro/pro+", "Broadlink"), 0x272a: (rm, "RM pro", "Broadlink"), 0x2737: (rm, "RM mini 3", "Broadlink"), @@ -58,7 +57,6 @@ def get_devices(): 0x27c2: (rm, "RM mini 3", "Broadlink"), 0x27d1: (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"), @@ -69,25 +67,18 @@ def get_devices(): 0x62bc: (rm4, "RM4 mini", "Broadlink"), 0x62be: (rm4, "RM4C mini", "Broadlink"), 0x648d: (rm4, "RM4 mini", "Broadlink"), - 0x2714: (a1, "e-Sensor", "Broadlink"), - 0x4eb5: (mp1, "MP1-1K4S", "Broadlink"), 0x4ef7: (mp1, "MP1-1K4S", "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"), - 0x2722: (S1C, "S2KIT", "Broadlink"), - 0x4ead: (hysen, "HY02B05H", "Hysen"), - 0x4e4d: (dooya, "DT360E-45/20", "Dooya"), - 0x51e3: (bg1, "BG800/BG900", "BG Electrical"), } @@ -116,7 +107,8 @@ def discover( local_ip_address=None, discover_ip_address='255.255.255.255', discover_ip_port=80 -): +) -> list: + """Discover devices connected to the local network.""" local_ip_address = local_ip_address or get_local_ip() address = local_ip_address.split('.') cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -143,6 +135,7 @@ def discover( packet[0x09] = 0 packet[0x0a] = 0 packet[0x0b] = 0 + packet[0x0c] = year & 0xff packet[0x0d] = year >> 8 packet[0x0e] = datetime.now().minute @@ -197,6 +190,8 @@ def discover( class device: + """Controls a Broadlink device.""" + def __init__( self, host, @@ -207,7 +202,8 @@ def __init__( model=None, manufacturer=None, is_locked=None - ): + ) -> None: + """Initialize the controller.""" self.host = host self.mac = mac.encode() if isinstance(mac, str) else mac self.devtype = devtype if devtype is not None else 0x272a @@ -228,19 +224,24 @@ def __init__( [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) self.update_aes(key) - def update_aes(self, key): - self.aes = Cipher(algorithms.AES(key), modes.CBC(self.iv), - backend=default_backend()) + def update_aes(self, key) -> None: + """Update AES.""" + self.aes = Cipher( + algorithms.AES(key), modes.CBC(self.iv), backend=default_backend() + ) - def encrypt(self, payload): + def encrypt(self, payload) -> bytes: + """Encrypt the payload.""" encryptor = self.aes.encryptor() return encryptor.update(payload) + encryptor.finalize() - def decrypt(self, payload): + def decrypt(self, payload) -> bytes: + """Decrypt the payload.""" decryptor = self.aes.decryptor() return decryptor.update(payload) + decryptor.finalize() - def auth(self): + def auth(self) -> bool: + """Authenticate to the device.""" payload = bytearray(0x50) payload[0x04] = 0x31 payload[0x05] = 0x31 @@ -277,17 +278,18 @@ def auth(self): self.id = payload[0x03::-1] self.update_aes(key) - return True - def get_fwversion(self): + def get_fwversion(self) -> int: + """Get firmware version.""" packet = bytearray([0x68]) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return payload[0x4] | payload[0x5] << 8 - def set_name(self, name): + def set_name(self, name) -> None: + """Set device name.""" packet = bytearray(4) packet += name.encode('utf-8') packet += bytearray(0x50 - len(packet)) @@ -296,7 +298,8 @@ def set_name(self, name): check_error(response[0x22:0x24]) self.name = name - def set_lock(self, state): + def set_lock(self, state) -> None: + """Lock/unlock the device.""" packet = bytearray(4) packet += self.name.encode('utf-8') packet += bytearray(0x50 - len(packet)) @@ -305,10 +308,12 @@ def set_lock(self, state): check_error(response[0x22:0x24]) self.is_locked = bool(state) - def get_type(self): + def get_type(self) -> str: + """Return device type.""" return self.type - def send_packet(self, command, payload): + def send_packet(self, command, payload) -> bytearray: + """Send a packet to the device.""" self.count = (self.count + 1) & 0xffff packet = bytearray(0x38) packet[0x00] = 0x5a @@ -380,13 +385,15 @@ def send_packet(self, command, payload): class mp1(device): - def __init__(self, *args, **kwargs): + """Controls a Broadlink MP1.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "MP1" - def set_power_mask(self, sid_mask, state): - """Sets the power state of the smart power strip.""" - + def set_power_mask(self, sid_mask, state) -> None: + """Set the power state of the device.""" packet = bytearray(16) packet[0x00] = 0x0d packet[0x02] = 0xa5 @@ -403,13 +410,13 @@ def set_power_mask(self, sid_mask, state): response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - def set_power(self, sid, state): - """Sets the power state of the smart power strip.""" + def set_power(self, sid, state) -> None: + """Set the power state of the device.""" sid_mask = 0x01 << (sid - 1) - return self.set_power_mask(sid_mask, state) + self.set_power_mask(sid_mask, state) - def check_power_raw(self): - """Returns the power state of the smart power strip in raw format.""" + def check_power_raw(self) -> bool: + """Return the power state of the device in raw format.""" packet = bytearray(16) packet[0x00] = 0x0a packet[0x02] = 0xa5 @@ -423,14 +430,10 @@ def check_power_raw(self): response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) payload = self.decrypt(bytes(response[0x38:])) - if isinstance(payload[0x4], int): - state = payload[0x0e] - else: - state = ord(payload[0x0e]) - return state + return payload[0x0e] - def check_power(self): - """Returns the power state of the smart power strip.""" + 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} @@ -443,22 +446,25 @@ def check_power(self): class bg1(device): - def __init__(self, *args, **kwargs): + """Controls a BG Electrical smart outlet.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "BG1" - def get_state(self): - """Get state of device. + def get_state(self) -> dict: + """Return the power state of the device. - Returns: - dict: Dictionary of current state - eg. `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}`""" + Example: `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}` + """ packet = self._encode(1, b'{}') response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) return self._decode(response) - def set_state(self, pwr=None, pwr1=None, pwr2=None, maxworktime=None, maxworktime1=None, maxworktime2=None, idcbrightness=None): + def set_state(self, pwr=None, pwr1=None, pwr2=None, maxworktime=None, maxworktime1=None, maxworktime2=None, idcbrightness=None) -> dict: + """Set the power state of the device.""" data = {} if pwr is not None: data['pwr'] = int(bool(pwr)) @@ -480,15 +486,16 @@ def set_state(self, pwr=None, pwr1=None, pwr2=None, maxworktime=None, maxworktim check_error(response[0x22:0x24]) return self._decode(response) - def _encode(self, flag, js): - # 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 + def _encode(self, flag, js) -> bytearray: + """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) struct.pack_into('> 8 - return packet - def _decode(self, response): + def _decode(self, response) -> dict: + """Decode a message.""" payload = self.decrypt(bytes(response[0x38:])) js_len = struct.unpack_from(' None: + """Initialize the device.""" device.__init__(self, *args, **kwargs) self.type = "SP1" - def set_power(self, state): + def set_power(self, state) -> None: + """Set the power state of the device.""" packet = bytearray(4) packet[0] = state response = self.send_packet(0x66, packet) @@ -521,12 +532,15 @@ def set_power(self, state): class sp2(device): - def __init__(self, *args, **kwargs): + """Controls a Broadlink SP2.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "SP2" - def set_power(self, state): - """Sets the power state of the smart plug.""" + def set_power(self, state) -> None: + """Set the power state of the device.""" packet = bytearray(16) packet[0] = 2 if self.check_nightlight(): @@ -536,8 +550,8 @@ def set_power(self, state): response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - def set_nightlight(self, state): - """Sets the night light state of the smart plug""" + def set_nightlight(self, state) -> None: + """Set the night light state of the device.""" packet = bytearray(16) packet[0] = 2 if self.check_power(): @@ -547,8 +561,8 @@ def set_nightlight(self, state): response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - def check_power(self): - """Returns the power state of the smart plug.""" + def check_power(self) -> bool: + """Return the power state of the device.""" packet = bytearray(16) packet[0] = 1 response = self.send_packet(0x6a, packet) @@ -558,8 +572,8 @@ def check_power(self): return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD) return bool(ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFD) - def check_nightlight(self): - """Returns the power state of the smart plug.""" + def check_nightlight(self) -> bool: + """Return the state of the night light.""" packet = bytearray(16) packet[0] = 1 response = self.send_packet(0x6a, packet) @@ -569,7 +583,8 @@ def check_nightlight(self): return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) return bool(ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFF) - def get_energy(self): + def get_energy(self) -> int: + """Return the energy state of the device.""" packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) @@ -583,6 +598,7 @@ def get_energy(self): class a1(device): + """Controls a Broadlink A1.""" _SENSORS_AND_LEVELS = ( ('light', ('dark', 'dim', 'normal', 'bright')), @@ -590,11 +606,13 @@ class a1(device): ('noise', ('quiet', 'normal', 'noisy')), ) - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "A1" - def check_sensors(self): + def check_sensors(self) -> dict: + """Return the state of the sensors.""" data = self.check_sensors_raw() for sensor, levels in self._SENSORS_AND_LEVELS: try: @@ -603,7 +621,8 @@ def check_sensors(self): data[sensor] = 'unknown' return data - def check_sensors_raw(self): + def check_sensors_raw(self) -> dict: + """Return the state of the sensors in raw format.""" packet = bytearray([0x1]) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) @@ -619,13 +638,17 @@ def check_sensors_raw(self): class rm(device): - def __init__(self, *args, **kwargs): + """Controls a Broadlink RM.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "RM2" self._request_header = bytes() self._code_sending_header = bytes() - def check_data(self): + def check_data(self) -> bytes: + """Return the last captured code.""" packet = bytearray(self._request_header) packet.append(0x04) response = self.send_packet(0x6a, packet) @@ -633,32 +656,37 @@ def check_data(self): payload = self.decrypt(bytes(response[0x38:])) return payload[len(self._request_header) + 4:] - def send_data(self, data): + def send_data(self, data) -> None: + """Send a code to the device.""" packet = bytearray(self._code_sending_header) packet += bytearray([0x02, 0x00, 0x00, 0x00]) packet += data response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - def enter_learning(self): + def enter_learning(self) -> None: + """Enter infrared learning mode.""" packet = bytearray(self._request_header) packet.append(0x03) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - def sweep_frequency(self): + def sweep_frequency(self) -> None: + """Sweep frequency.""" packet = bytearray(self._request_header) packet.append(0x19) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - def cancel_sweep_frequency(self): + def cancel_sweep_frequency(self) -> None: + """Cancel sweep frequency.""" packet = bytearray(self._request_header) packet.append(0x1e) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - def check_frequency(self): + def check_frequency(self) -> bool: + """Return True if the frequency was identified successfully.""" packet = bytearray(self._request_header) packet.append(0x1a) response = self.send_packet(0x6a, packet) @@ -668,7 +696,8 @@ def check_frequency(self): return True return False - def find_rf_packet(self): + def find_rf_packet(self) -> bool: + """Enter radiofrequency learning mode.""" packet = bytearray(self._request_header) packet.append(0x1b) response = self.send_packet(0x6a, packet) @@ -678,7 +707,8 @@ def find_rf_packet(self): return True return False - def _check_sensors(self, command): + def _check_sensors(self, command) -> bytes: + """Return the state of the sensors in raw format.""" packet = bytearray(self._request_header) packet.append(command) response = self.send_packet(0x6a, packet) @@ -686,31 +716,39 @@ def _check_sensors(self, command): payload = self.decrypt(bytes(response[0x38:])) return bytearray(payload[len(self._request_header) + 4:]) - def check_temperature(self): + def check_temperature(self) -> int: + """Return the temperature.""" data = self._check_sensors(0x1) return data[0x0] + data[0x1] / 10.0 - def check_sensors(self): + def check_sensors(self) -> dict: + """Return the state of the sensors.""" data = self._check_sensors(0x1) return {'temperature': data[0x0] + data[0x1] / 10.0} class rm4(rm): - def __init__(self, *args, **kwargs): + """Controls a Broadlink RM4.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "RM4" self._request_header = b'\x04\x00' self._code_sending_header = b'\xda\x00' - def check_temperature(self): + def check_temperature(self) -> int: + """Return the temperature.""" data = self._check_sensors(0x24) return data[0x0] + data[0x1] / 100.0 - def check_humidity(self): + def check_humidity(self) -> int: + """Return the humidity.""" data = self._check_sensors(0x24) return data[0x2] + data[0x3] / 100.0 - def check_sensors(self): + def check_sensors(self) -> dict: + """Return the state of the sensors.""" data = self._check_sensors(0x24) return { 'temperature': data[0x0] + data[0x1] / 100.0, @@ -730,7 +768,10 @@ def discover(self): class hysen(device): - def __init__(self, *args, **kwargs): + """Controls a Hysen HVAC.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "Hysen heating controller" @@ -740,7 +781,8 @@ def __init__(self, *args, **kwargs): # 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 calculate_crc16(self, input_data): + def calculate_crc16(self, input_data) -> int: + """Calculate CRC-16.""" from ctypes import c_ushort crc16_tab = [] crc16_constant = 0xA001 @@ -774,8 +816,8 @@ def calculate_crc16(self, input_data): except Exception as e: print("EXCEPTION(calculate): {}".format(e)) - def send_request(self, input_payload): - + def send_request(self, input_payload) -> bytes: + """Send a request to the device.""" crc = self.calculate_crc16(bytes(input_payload)) # first byte is length, +2 for CRC16 @@ -801,18 +843,22 @@ def send_request(self, input_payload): return response_payload[2:response_payload_len] raise ValueError('hysen_response_error', 'CRC check on response failed') - # Get current room temperature in degrees celsius - def get_temp(self): + def get_temp(self) -> int: + """Return the room temperature in degrees celsius.""" payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])) return payload[0x05] / 2.0 - # Get current external temperature in degrees celsius - def get_external_temp(self): + def get_external_temp(self) -> int: + """Return the external temperature in degrees celsius.""" payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])) return payload[18] / 2.0 - # Get full status (including timer schedule) - def get_full_status(self): + def get_full_status(self) -> dict: + """Return the state of the device. + + Timer schedule included. + """ + payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x16])) data = {} data['remote_lock'] = payload[3] & 1 @@ -862,7 +908,8 @@ def get_full_status(self): # 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 # The sensor command is currently experimental - def set_mode(self, auto_mode, loop_mode, sensor=0): + def set_mode(self, auto_mode, loop_mode, sensor=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])) @@ -877,31 +924,37 @@ def set_mode(self, auto_mode, loop_mode, sensor=0): # 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 - def set_advanced(self, loop_mode, sensor, osv, dif, svh, svl, adj, fre, poweron): + def set_advanced(self, loop_mode, sensor, osv, dif, svh, svl, adj, fre, poweron) -> None: + """Set advanced options.""" input_payload = bytearray([0x01, 0x10, 0x00, 0x02, 0x00, 0x05, 0x0a, loop_mode, sensor, osv, dif, svh, svl, (int(adj * 2) >> 8 & 0xff), (int(adj * 2) & 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. - def switch_to_auto(self): + def switch_to_auto(self) -> None: + """Switch mode to auto.""" self.set_mode(auto_mode=1, loop_mode=0) - def switch_to_manual(self): + def switch_to_manual(self) -> None: + """Switch mode to manual.""" self.set_mode(auto_mode=0, loop_mode=0) # Set temperature for manual mode (also activates manual mode if currently in automatic) - def set_temp(self, temp): + def set_temp(self, temp) -> None: + """Set the target temperature.""" self.send_request(bytearray([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=1, remote_lock=0): + def set_power(self, power=1, remote_lock=0) -> None: + """Set the power state of the device.""" self.send_request(bytearray([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, minute, second, day): + def set_time(self, hour, minute, second, day) -> None: + """Set the time.""" self.send_request(bytearray([0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day])) # Set timer schedule @@ -910,7 +963,8 @@ def set_time(self, hour, minute, second, day): # {'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, weekend): + def set_schedule(self, weekday, weekend) -> None: + """Set timer schedule.""" # Begin with some magic values ... input_payload = bytearray([0x01, 0x10, 0x00, 0x0a, 0x00, 0x0c, 0x18]) @@ -944,15 +998,15 @@ def set_schedule(self, weekday, weekend): class S1C(device): - """ - Its VERY VERY VERY DIRTY IMPLEMENTATION of S1C - """ + """Controls a Broadlink S1C.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = 'S1C' - def get_sensors_status(self): + 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 response = self.send_packet(0x6a, packet) @@ -991,11 +1045,15 @@ def get_sensors_status(self): class dooya(device): - def __init__(self, *args, **kwargs): + """Controls a Dooya curtain motor.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "Dooya DT360E" - def _send(self, magic1, magic2): + def _send(self, magic1, magic2) -> int: + """Send a packet to the device.""" packet = bytearray(16) packet[0] = 0x09 packet[2] = 0xbb @@ -1008,19 +1066,24 @@ def _send(self, magic1, magic2): payload = self.decrypt(bytes(response[0x38:])) return ord(payload[4]) - def open(self): + def open(self) -> int: + """Open the curtain.""" return self._send(0x01, 0x00) - def close(self): + def close(self) -> int: + """Close the curtain.""" return self._send(0x02, 0x00) - def stop(self): + def stop(self) -> int: + """Stop the curtain.""" return self._send(0x03, 0x00) - def get_percentage(self): + def get_percentage(self) -> int: + """Return the position of the curtain.""" return self._send(0x06, 0x5d) - def set_percentage_and_wait(self, new_percentage): + def set_percentage_and_wait(self, new_percentage) -> None: + """Set the position of the curtain.""" current = self.get_percentage() if current > new_percentage: self.close() @@ -1035,22 +1098,29 @@ def set_percentage_and_wait(self, new_percentage): current = self.get_percentage() self.stop() + 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 } - - def __init__(self, *args, **kwargs): + 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, + } + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "SmartBulb" - def send_command(self, command, type='set'): + def send_command(self, command, type='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 @@ -1074,7 +1144,8 @@ def send_command(self, command, type='set'): if responseLength > 0: self.state_dict = json.loads(payload[0x0e:0x0e+responseLength]) - def set_json(self, jsonstr): + def set_json(self, jsonstr) -> 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) @@ -1082,18 +1153,21 @@ def set_json(self, jsonstr): self.send_command(json.dumps(reconvert)) return json.dumps(self.state_dict) - def set_state(self, state): + def set_state(self, state) -> None: + """Set the state of the device.""" cmd = '{"pwr":%d}' % (1 if state == "ON" or state == 1 else 0) self.send_command(cmd) - def get_state(self): + def get_state(self) -> dict: + """Return the state of the device.""" cmd = "{}" self.send_command(cmd) return self.state_dict # 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, password, security_mode): +def setup(ssid, password, security_mode) -> 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) payload[0x26] = 0x14 # This seems to always be set to 14 diff --git a/broadlink/exceptions.py b/broadlink/exceptions.py index e00f7d87..8e3aa4a9 100644 --- a/broadlink/exceptions.py +++ b/broadlink/exceptions.py @@ -31,110 +31,74 @@ def __str__(self): class FirmwareException(BroadlinkException): """Common base class for all firmware exceptions.""" - pass - class AuthenticationError(FirmwareException): """Authentication error.""" - pass - class AuthorizationError(FirmwareException): """Authorization error.""" - pass - class CommandNotSupportedError(FirmwareException): """Command not supported error.""" - pass - class ConnectionClosedError(FirmwareException): """Connection closed error.""" - pass - class DataValidationError(FirmwareException): """Data validation error.""" - pass - class DeviceOfflineError(FirmwareException): """Device offline error.""" - pass - class ReadError(FirmwareException): """Read error.""" - pass - class SendError(FirmwareException): """Send error.""" - pass - class SSIDNotFoundError(FirmwareException): """SSID not found error.""" - pass - class StorageError(FirmwareException): """Storage error.""" - pass - class WriteError(FirmwareException): """Write error.""" - pass - class SDKException(BroadlinkException): """Common base class for all SDK exceptions.""" - pass - class ChecksumError(SDKException): """Received data packet check error.""" - pass - class LengthError(SDKException): """Received data packet length error.""" - pass - class DNSLookupError(SDKException): """Failed to obtain local IP address.""" - pass - class NetworkTimeoutError(SDKException): """Network timeout error.""" - pass - class UnknownError(BroadlinkException): """Unknown error.""" - pass - BROADLINK_EXCEPTIONS = { # Firmware-related errors are generated by the device. From 76012c6cd44c30cc81b950b6f5f26a5b86291505 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Wed, 16 Sep 2020 21:35:09 -0300 Subject: [PATCH 136/261] Add annotations for parameters --- broadlink/__init__.py | 153 ++++++++++++++++++++---------------------- broadlink/helpers.py | 38 +++++++++++ 2 files changed, 110 insertions(+), 81 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index a31895e5..03e45fc9 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -8,12 +8,13 @@ import threading import time from datetime import datetime +from typing import List, Union, Tuple from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from .exceptions import check_error, exception -from .helpers import get_local_ip +from .helpers import calculate_crc16, get_local_ip def get_devices() -> dict: @@ -83,7 +84,13 @@ def get_devices() -> dict: } -def gendevice(dev_type, host, mac, name=None, is_locked=None): +def gendevice( + dev_type: int, + host: Tuple[str, int], + mac: Union[bytes, str], + name: str = None, + is_locked: bool = None, +): """Generate a device.""" try: dev_class, model, manufacturer = get_devices()[dev_type] @@ -103,10 +110,10 @@ def gendevice(dev_type, host, mac, name=None, is_locked=None): def discover( - timeout=None, - local_ip_address=None, - discover_ip_address='255.255.255.255', - discover_ip_port=80 + timeout: int = None, + local_ip_address: str = None, + discover_ip_address: str = '255.255.255.255', + discover_ip_port: int = 80, ) -> list: """Discover devices connected to the local network.""" local_ip_address = local_ip_address or get_local_ip() @@ -194,14 +201,14 @@ class device: def __init__( self, - host, - mac, - devtype, - timeout=10, - name=None, - model=None, - manufacturer=None, - is_locked=None + host: Tuple[str, int], + mac: Union[bytes, str], + devtype: int, + timeout: int = 10, + name: str = None, + model: str = None, + manufacturer: str = None, + is_locked: bool = None, ) -> None: """Initialize the controller.""" self.host = host @@ -224,18 +231,18 @@ def __init__( [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) self.update_aes(key) - def update_aes(self, key) -> None: + def update_aes(self, key: bytes) -> None: """Update AES.""" self.aes = Cipher( algorithms.AES(key), modes.CBC(self.iv), backend=default_backend() ) - def encrypt(self, payload) -> bytes: + def encrypt(self, payload: bytes) -> bytes: """Encrypt the payload.""" encryptor = self.aes.encryptor() return encryptor.update(payload) + encryptor.finalize() - def decrypt(self, payload) -> bytes: + def decrypt(self, payload: bytes) -> bytes: """Decrypt the payload.""" decryptor = self.aes.decryptor() return decryptor.update(payload) + decryptor.finalize() @@ -288,7 +295,7 @@ def get_fwversion(self) -> int: payload = self.decrypt(response[0x38:]) return payload[0x4] | payload[0x5] << 8 - def set_name(self, name) -> None: + def set_name(self, name: str) -> None: """Set device name.""" packet = bytearray(4) packet += name.encode('utf-8') @@ -298,7 +305,7 @@ def set_name(self, name) -> None: check_error(response[0x22:0x24]) self.name = name - def set_lock(self, state) -> None: + def set_lock(self, state: bool) -> None: """Lock/unlock the device.""" packet = bytearray(4) packet += self.name.encode('utf-8') @@ -312,7 +319,7 @@ def get_type(self) -> str: """Return device type.""" return self.type - def send_packet(self, command, payload) -> bytearray: + def send_packet(self, command: int, payload: bytearray) -> bytearray: """Send a packet to the device.""" self.count = (self.count + 1) & 0xffff packet = bytearray(0x38) @@ -392,7 +399,7 @@ def __init__(self, *args, **kwargs) -> None: device.__init__(self, *args, **kwargs) self.type = "MP1" - def set_power_mask(self, sid_mask, state) -> None: + def set_power_mask(self, sid_mask: int, state: bool) -> None: """Set the power state of the device.""" packet = bytearray(16) packet[0x00] = 0x0d @@ -410,7 +417,7 @@ def set_power_mask(self, sid_mask, state) -> None: response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - def set_power(self, sid, state) -> None: + def set_power(self, sid: int, state: bool) -> None: """Set the power state of the device.""" sid_mask = 0x01 << (sid - 1) self.set_power_mask(sid_mask, state) @@ -463,7 +470,16 @@ def get_state(self) -> dict: check_error(response[0x22:0x24]) return self._decode(response) - def set_state(self, pwr=None, pwr1=None, pwr2=None, maxworktime=None, maxworktime1=None, maxworktime2=None, idcbrightness=None) -> 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, + ) -> dict: """Set the power state of the device.""" data = {} if pwr is not None: @@ -486,7 +502,7 @@ def set_state(self, pwr=None, pwr1=None, pwr2=None, maxworktime=None, maxworktim check_error(response[0x22:0x24]) return self._decode(response) - def _encode(self, flag, js) -> bytearray: + def _encode(self, flag: int, js: str) -> bytearray: """Encode a message.""" # The packet format is: # 0x00-0x01 length @@ -507,7 +523,7 @@ def _encode(self, flag, js) -> bytearray: packet[0x07] = checksum >> 8 return packet - def _decode(self, response) -> dict: + def _decode(self, response: bytes) -> dict: """Decode a message.""" payload = self.decrypt(bytes(response[0x38:])) js_len = struct.unpack_from(' None: device.__init__(self, *args, **kwargs) self.type = "SP1" - def set_power(self, state) -> None: + def set_power(self, state: bool) -> None: """Set the power state of the device.""" packet = bytearray(4) packet[0] = state @@ -539,7 +555,7 @@ def __init__(self, *args, **kwargs) -> None: device.__init__(self, *args, **kwargs) self.type = "SP2" - def set_power(self, state) -> None: + def set_power(self, state: bool) -> None: """Set the power state of the device.""" packet = bytearray(16) packet[0] = 2 @@ -550,7 +566,7 @@ def set_power(self, state) -> None: response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - def set_nightlight(self, state) -> None: + def set_nightlight(self, state: bool) -> None: """Set the night light state of the device.""" packet = bytearray(16) packet[0] = 2 @@ -656,7 +672,7 @@ def check_data(self) -> bytes: payload = self.decrypt(bytes(response[0x38:])) return payload[len(self._request_header) + 4:] - def send_data(self, data) -> None: + def send_data(self, data: bytes) -> None: """Send a code to the device.""" packet = bytearray(self._code_sending_header) packet += bytearray([0x02, 0x00, 0x00, 0x00]) @@ -707,7 +723,7 @@ def find_rf_packet(self) -> bool: return True return False - def _check_sensors(self, command) -> bytes: + def _check_sensors(self, command: int) -> bytes: """Return the state of the sensors in raw format.""" packet = bytearray(self._request_header) packet.append(command) @@ -781,44 +797,9 @@ def __init__(self, *args, **kwargs) -> None: # 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 calculate_crc16(self, input_data) -> int: - """Calculate CRC-16.""" - from ctypes import c_ushort - 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)) - - try: - is_string = isinstance(input_data, str) - is_bytes = isinstance(input_data, bytes) - - if not is_string and not is_bytes: - raise Exception("Please provide a string or a byte sequence " - "as argument for calculation.") - - crcValue = 0xffff - - for c in input_data: - d = ord(c) if is_string else c - tmp = crcValue ^ d - rotated = c_ushort(crcValue >> 8).value - crcValue = rotated ^ int(crc16_tab[(tmp & 0x00ff)], 0) - - return crcValue - except Exception as e: - print("EXCEPTION(calculate): {}".format(e)) - - def send_request(self, input_payload) -> bytes: + def send_request(self, input_payload: bytes) -> bytes: """Send a request to the device.""" - crc = self.calculate_crc16(bytes(input_payload)) + crc = calculate_crc16(bytes(input_payload)) # first byte is length, +2 for CRC16 request_payload = bytearray([len(input_payload) + 2, 0x00]) @@ -837,7 +818,7 @@ def send_request(self, input_payload) -> bytes: response_payload_len = response_payload[0] if response_payload_len + 2 > len(response_payload): raise ValueError('hysen_response_error', 'first byte of response is not length') - crc = self.calculate_crc16(bytes(response_payload[2:response_payload_len])) + crc = calculate_crc16(bytes(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] @@ -858,7 +839,6 @@ def get_full_status(self) -> dict: Timer schedule included. """ - payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x16])) data = {} data['remote_lock'] = payload[3] & 1 @@ -908,7 +888,7 @@ def get_full_status(self) -> dict: # 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 # The sensor command is currently experimental - def set_mode(self, auto_mode, loop_mode, sensor=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(bytearray([0x01, 0x06, 0x00, 0x02, mode_byte, sensor])) @@ -924,7 +904,18 @@ def set_mode(self, auto_mode, loop_mode, sensor=0) -> None: # 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 - def set_advanced(self, loop_mode, sensor, osv, dif, svh, svl, adj, fre, poweron) -> None: + def set_advanced( + self, + loop_mode: int, + sensor: int, + osv: int, + dif: int, + svh: int, + svl: int, + adj: float, + fre: int, + poweron: int, + ) -> None: """Set advanced options.""" input_payload = bytearray([0x01, 0x10, 0x00, 0x02, 0x00, 0x05, 0x0a, loop_mode, sensor, osv, dif, svh, svl, (int(adj * 2) >> 8 & 0xff), (int(adj * 2) & 0xff), fre, poweron]) @@ -941,19 +932,19 @@ def switch_to_manual(self) -> None: self.set_mode(auto_mode=0, loop_mode=0) # Set temperature for manual mode (also activates manual mode if currently in automatic) - def set_temp(self, temp) -> None: + def set_temp(self, temp: float) -> None: """Set the target temperature.""" self.send_request(bytearray([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=1, remote_lock=0) -> None: + 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])) # set time on device # n.b. day=1 is Monday, ..., day=7 is Sunday - def set_time(self, hour, minute, second, day) -> None: + 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])) @@ -963,7 +954,7 @@ def set_time(self, hour, minute, second, day) -> 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, weekend) -> None: + def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: """Set timer schedule.""" # Begin with some magic values ... input_payload = bytearray([0x01, 0x10, 0x00, 0x0a, 0x00, 0x0c, 0x18]) @@ -1052,7 +1043,7 @@ def __init__(self, *args, **kwargs) -> None: device.__init__(self, *args, **kwargs) self.type = "Dooya DT360E" - def _send(self, magic1, magic2) -> int: + def _send(self, magic1: int, magic2: int) -> int: """Send a packet to the device.""" packet = bytearray(16) packet[0] = 0x09 @@ -1082,7 +1073,7 @@ def get_percentage(self) -> int: """Return the position of the curtain.""" return self._send(0x06, 0x5d) - def set_percentage_and_wait(self, new_percentage) -> None: + def set_percentage_and_wait(self, new_percentage: int) -> None: """Set the position of the curtain.""" current = self.get_percentage() if current > new_percentage: @@ -1119,7 +1110,7 @@ def __init__(self, *args, **kwargs) -> None: device.__init__(self, *args, **kwargs) self.type = "SmartBulb" - def send_command(self, command, type='set') -> None: + 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 @@ -1144,7 +1135,7 @@ def send_command(self, command, type='set') -> None: if responseLength > 0: self.state_dict = json.loads(payload[0x0e:0x0e+responseLength]) - def set_json(self, jsonstr) -> str: + 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(): @@ -1153,7 +1144,7 @@ def set_json(self, jsonstr) -> str: self.send_command(json.dumps(reconvert)) return json.dumps(self.state_dict) - def set_state(self, state) -> None: + 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) @@ -1166,7 +1157,7 @@ def get_state(self) -> dict: # 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, password, security_mode) -> None: +def setup(ssid: str, password: str, security_mode: int) -> 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) diff --git a/broadlink/helpers.py b/broadlink/helpers.py index 40f06e3d..22ac12f5 100644 --- a/broadlink/helpers.py +++ b/broadlink/helpers.py @@ -1,4 +1,5 @@ """Helper functions.""" +from ctypes import c_ushort import socket from .exceptions import exception @@ -18,3 +19,40 @@ def get_local_ip() -> str: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.connect(('8.8.8.8', 53)) return s.getsockname()[0] + + +def calculate_crc16(input_data) -> int: + """Calculate CRC-16.""" + 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)) + + try: + is_string = isinstance(input_data, str) + is_bytes = isinstance(input_data, bytes) + + if not is_string and not is_bytes: + raise Exception( + "Please provide a string or a byte sequence " + "as argument for calculation." + ) + + crcValue = 0xFFFF + + for c in input_data: + d = ord(c) if is_string else c + tmp = crcValue ^ d + rotated = c_ushort(crcValue >> 8).value + crcValue = rotated ^ int(crc16_tab[(tmp & 0x00FF)], 0) + + return crcValue + except Exception as e: + print("EXCEPTION(calculate): {}".format(e)) From 08c020e5977842c09bf8f7b72a3eceefbaaac952 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Thu, 17 Sep 2020 00:41:32 -0300 Subject: [PATCH 137/261] Call Mr. Krueger --- broadlink/__init__.py | 975 +----------------------------------------- broadlink/alarm.py | 58 +++ broadlink/climate.py | 203 +++++++++ broadlink/cover.py | 59 +++ broadlink/device.py | 205 +++++++++ broadlink/light.py | 71 +++ broadlink/remote.py | 133 ++++++ broadlink/sensor.py | 42 ++ broadlink/switch.py | 227 ++++++++++ 9 files changed, 1006 insertions(+), 967 deletions(-) create mode 100644 broadlink/alarm.py create mode 100644 broadlink/climate.py create mode 100644 broadlink/cover.py create mode 100644 broadlink/device.py create mode 100644 broadlink/light.py create mode 100644 broadlink/remote.py create mode 100644 broadlink/sensor.py create mode 100644 broadlink/switch.py diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 03e45fc9..c3798dca 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,19 +1,19 @@ #!/usr/bin/python3 """The python-broadlink library.""" -import codecs -import json -import random import socket -import struct -import threading import time from datetime import datetime from typing import List, Union, Tuple -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - +from .alarm import S1C +from .cover import dooya +from .climate import hysen +from .device import device from .exceptions import check_error, exception +from .light import lb1 +from .remote import rm, rm2, rm4 +from .sensor import a1 +from .switch import bg1, mp1, sp1, sp2 from .helpers import calculate_crc16, get_local_ip @@ -196,965 +196,6 @@ def discover( return devices -class device: - """Controls a Broadlink device.""" - - def __init__( - self, - host: Tuple[str, int], - mac: Union[bytes, str], - devtype: int, - timeout: int = 10, - name: str = None, - model: str = None, - manufacturer: str = None, - is_locked: bool = None, - ) -> None: - """Initialize the controller.""" - self.host = host - self.mac = mac.encode() if isinstance(mac, str) else mac - self.devtype = devtype if devtype is not None else 0x272a - self.timeout = timeout - self.name = name - self.model = model - self.manufacturer = manufacturer - self.is_locked = is_locked - self.count = random.randrange(0xffff) - self.iv = bytearray( - [0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) - self.id = bytearray([0, 0, 0, 0]) - self.type = "Unknown" - self.lock = threading.Lock() - - self.aes = None - key = bytearray( - [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) - self.update_aes(key) - - def update_aes(self, key: bytes) -> None: - """Update AES.""" - self.aes = Cipher( - algorithms.AES(key), modes.CBC(self.iv), backend=default_backend() - ) - - def encrypt(self, payload: bytes) -> bytes: - """Encrypt the payload.""" - encryptor = self.aes.encryptor() - return encryptor.update(payload) + encryptor.finalize() - - def decrypt(self, payload: bytes) -> bytes: - """Decrypt the payload.""" - decryptor = self.aes.decryptor() - return decryptor.update(payload) + decryptor.finalize() - - 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[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') - - response = self.send_packet(0x65, payload) - check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) - - key = payload[0x04:0x14] - if len(key) % 16 != 0: - return False - - self.id = payload[0x03::-1] - self.update_aes(key) - return True - - def get_fwversion(self) -> int: - """Get firmware version.""" - packet = bytearray([0x68]) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) - return payload[0x4] | payload[0x5] << 8 - - def set_name(self, name: str) -> None: - """Set device name.""" - packet = bytearray(4) - packet += name.encode('utf-8') - packet += bytearray(0x50 - len(packet)) - packet[0x43] = bool(self.is_locked) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - self.name = name - - def set_lock(self, state: bool) -> None: - """Lock/unlock the device.""" - packet = bytearray(4) - packet += self.name.encode('utf-8') - packet += bytearray(0x50 - len(packet)) - packet[0x43] = bool(state) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - self.is_locked = bool(state) - - def get_type(self) -> str: - """Return device type.""" - return self.type - - def send_packet(self, command: int, payload: bytearray) -> bytearray: - """Send a packet to the device.""" - self.count = (self.count + 1) & 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 - if payload: - payload += bytearray((16 - len(payload)) % 16) - - checksum = sum(payload, 0xbeaf) & 0xffff - packet[0x34] = checksum & 0xff - packet[0x35] = checksum >> 8 - - payload = self.encrypt(payload) - for i in range(len(payload)): - packet.append(payload[i]) - - checksum = sum(packet, 0xbeaf) & 0xffff - packet[0x20] = checksum & 0xff - packet[0x21] = checksum >> 8 - - start_time = time.time() - with self.lock: - cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - while True: - try: - cs.sendto(packet, self.host) - cs.settimeout(1) - resp, _ = cs.recvfrom(2048) - resp = bytearray(resp) - break - except socket.timeout: - if (time.time() - start_time) > self.timeout: - cs.close() - raise exception(-4000) # Network timeout. - cs.close() - - if len(resp) < 0x30: - raise exception(-4007) # Length error. - - checksum = resp[0x20] | (resp[0x21] << 8) - if sum(resp, 0xbeaf) - sum(resp[0x20:0x22]) & 0xffff != checksum: - raise exception(-4008) # Checksum error. - - return resp - - -class mp1(device): - """Controls a Broadlink MP1.""" - - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "MP1" - - def set_power_mask(self, sid_mask: int, state: 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 state else sid_mask) - packet[0x07] = 0xc0 - packet[0x08] = 0x02 - packet[0x0a] = 0x03 - packet[0x0d] = sid_mask - packet[0x0e] = sid_mask if state else 0 - - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - - def set_power(self, sid: int, state: bool) -> None: - """Set the power state of the device.""" - sid_mask = 0x01 << (sid - 1) - self.set_power_mask(sid_mask, state) - - def check_power_raw(self) -> bool: - """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) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - return payload[0x0e] - - 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 - - -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" - - 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, b'{}') - response = self.send_packet(0x6a, packet) - 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.""" - data = {} - if pwr is not None: - data['pwr'] = int(bool(pwr)) - if pwr1 is not None: - data['pwr1'] = int(bool(pwr1)) - if pwr2 is not None: - data['pwr2'] = int(bool(pwr2)) - if maxworktime is not None: - data['maxworktime'] = maxworktime - if maxworktime1 is not None: - data['maxworktime1'] = maxworktime1 - if maxworktime2 is not None: - data['maxworktime2'] = maxworktime2 - if idcbrightness is not None: - data['idcbrightness'] = idcbrightness - js = json.dumps(data).encode('utf8') - packet = self._encode(2, js) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - return self._decode(response) - - def _encode(self, flag: int, js: str) -> bytearray: - """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) - struct.pack_into('> 8 - return packet - - def _decode(self, response: bytes) -> dict: - """Decode a message.""" - payload = self.decrypt(bytes(response[0x38:])) - js_len = struct.unpack_from(' None: - """Initialize the device.""" - device.__init__(self, *args, **kwargs) - self.type = "SP1" - - def set_power(self, state: bool) -> None: - """Set the power state of the device.""" - packet = bytearray(4) - packet[0] = state - response = self.send_packet(0x66, packet) - check_error(response[0x22:0x24]) - - -class sp2(device): - """Controls a Broadlink SP2.""" - - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "SP2" - - def set_power(self, state: 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 - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - - def set_nightlight(self, state: 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 - 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(bytes(response[0x38:])) - if isinstance(payload[0x4], int): - return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD) - return bool(ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFD) - - def check_nightlight(self) -> bool: - """Return the state of the night light.""" - packet = bytearray(16) - packet[0] = 1 - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if isinstance(payload[0x4], int): - return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) - return bool(ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFF) - - def get_energy(self) -> int: - """Return the energy state of the device.""" - packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if isinstance(payload[0x7], int): - energy = int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0 - else: - energy = int(hex(ord(payload[0x07]) * 256 + ord(payload[0x06]))[2:]) + int( - hex(ord(payload[0x05]))[2:]) / 100.0 - return energy - - -class a1(device): - """Controls a Broadlink 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() - for sensor, levels in self._SENSORS_AND_LEVELS: - try: - data[sensor] = levels[data[sensor]] - except IndexError: - data[sensor] = 'unknown' - return data - - def check_sensors_raw(self) -> dict: - """Return the state of the sensors in raw format.""" - packet = bytearray([0x1]) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - data = bytearray(payload[0x4:]) - return { - 'temperature': data[0x0] + data[0x1] / 10.0, - 'humidity': data[0x2] + data[0x3] / 10.0, - 'light': data[0x4], - 'air_quality': data[0x6], - 'noise': data[0x8], - } - - -class rm(device): - """Controls a Broadlink RM.""" - - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "RM2" - self._request_header = bytes() - self._code_sending_header = bytes() - - def check_data(self) -> bytes: - """Return the last captured code.""" - packet = bytearray(self._request_header) - packet.append(0x04) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - return payload[len(self._request_header) + 4:] - - def send_data(self, data: bytes) -> None: - """Send a code to the device.""" - packet = bytearray(self._code_sending_header) - packet += bytearray([0x02, 0x00, 0x00, 0x00]) - packet += data - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - - def enter_learning(self) -> None: - """Enter infrared learning mode.""" - packet = bytearray(self._request_header) - packet.append(0x03) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - - def sweep_frequency(self) -> None: - """Sweep frequency.""" - packet = bytearray(self._request_header) - packet.append(0x19) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - - def cancel_sweep_frequency(self) -> None: - """Cancel sweep frequency.""" - packet = bytearray(self._request_header) - packet.append(0x1e) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - - def check_frequency(self) -> bool: - """Return True if the frequency was identified successfully.""" - packet = bytearray(self._request_header) - packet.append(0x1a) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if payload[len(self._request_header) + 4] == 1: - return True - return False - - def find_rf_packet(self) -> bool: - """Enter radiofrequency learning mode.""" - packet = bytearray(self._request_header) - packet.append(0x1b) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if payload[len(self._request_header) + 4] == 1: - return True - return False - - def _check_sensors(self, command: int) -> bytes: - """Return the state of the sensors in raw format.""" - packet = bytearray(self._request_header) - packet.append(command) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - return bytearray(payload[len(self._request_header) + 4:]) - - def check_temperature(self) -> int: - """Return the temperature.""" - data = self._check_sensors(0x1) - return data[0x0] + data[0x1] / 10.0 - - def check_sensors(self) -> dict: - """Return the state of the sensors.""" - data = self._check_sensors(0x1) - return {'temperature': data[0x0] + data[0x1] / 10.0} - - -class rm4(rm): - """Controls a Broadlink RM4.""" - - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "RM4" - self._request_header = b'\x04\x00' - self._code_sending_header = b'\xda\x00' - - def check_temperature(self) -> int: - """Return the temperature.""" - data = self._check_sensors(0x24) - return data[0x0] + data[0x1] / 100.0 - - def check_humidity(self) -> int: - """Return the humidity.""" - data = self._check_sensors(0x24) - return data[0x2] + data[0x3] / 100.0 - - def check_sensors(self) -> dict: - """Return the state of the sensors.""" - data = self._check_sensors(0x24) - return { - 'temperature': data[0x0] + data[0x1] / 100.0, - 'humidity': data[0x2] + data[0x3] / 100.0 - } - - -# For legacy compatibility - don't use this -class rm2(rm): - def __init__(self): - device.__init__(self, None, None, None) - - def discover(self): - dev = discover() - self.host = dev.host - self.mac = dev.mac - - -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" - - # 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: - """Send a request to the device.""" - crc = calculate_crc16(bytes(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) - - # send to device - response = self.send_packet(0x6a, request_payload) - check_error(response[0x22:0x24]) - response_payload = bytearray(self.decrypt(bytes(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): - raise ValueError('hysen_response_error', 'first byte of response is not length') - crc = calculate_crc16(bytes(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: - """Return the room temperature in degrees celsius.""" - payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])) - return payload[0x05] / 2.0 - - def get_external_temp(self) -> int: - """Return the external temperature in degrees celsius.""" - payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])) - return payload[18] / 2.0 - - def get_full_status(self) -> dict: - """Return the state of the device. - - Timer schedule included. - """ - payload = self.send_request(bytearray([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['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['fre'] = payload[15] - data['poweron'] = payload[16] - data['unknown'] = payload[17] - data['external_temp'] = (payload[18] & 255) / 2.0 - data['hour'] = payload[19] - data['min'] = payload[20] - data['sec'] = payload[21] - data['dayofweek'] = payload[22] - - weekday = [] - for i in range(0, 6): - weekday.append( - {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0}) - - data['weekday'] = weekday - weekend = [] - for i in range(6, 8): - weekend.append( - {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0}) - - data['weekend'] = weekend - return data - - # Change controller mode - # auto_mode = 1 for auto (scheduled/timed) mode, 0 for manual mode. - # 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 - # 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.""" - mode_byte = ((loop_mode + 1) << 4) + auto_mode - self.send_request(bytearray([0x01, 0x06, 0x00, 0x02, mode_byte, sensor])) - - # Advanced settings - # Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor, - # 2 for internal control temperature, external limit temperature. Factory default: 0. - # Set temperature range for external sensor (OSV) osv = 5..99. Factory default: 42C - # 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 - # 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 - def set_advanced( - self, - loop_mode: int, - sensor: int, - osv: int, - dif: int, - svh: int, - svl: int, - adj: float, - fre: int, - poweron: int, - ) -> None: - """Set advanced options.""" - input_payload = bytearray([0x01, 0x10, 0x00, 0x02, 0x00, 0x05, 0x0a, loop_mode, sensor, osv, dif, svh, svl, - (int(adj * 2) >> 8 & 0xff), (int(adj * 2) & 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. - def switch_to_auto(self) -> None: - """Switch mode to auto.""" - self.set_mode(auto_mode=1, loop_mode=0) - - def switch_to_manual(self) -> None: - """Switch mode to manual.""" - self.set_mode(auto_mode=0, loop_mode=0) - - # 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)])) - - # 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])) - - # 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])) - - # Set timer schedule - # Format is the same as you get from get_full_status. - # weekday is a list (ordered) of 6 dicts like: - # {'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: - """Set timer schedule.""" - # Begin with some magic values ... - input_payload = bytearray([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']) - - # weekend times - for i in range(0, 2): - input_payload.append(weekend[i]['start_hour']) - input_payload.append(weekend[i]['start_minute']) - - # weekday temperatures - for i in range(0, 6): - input_payload.append(int(weekday[i]['temp'] * 2)) - - # weekend temperatures - for i in range(0, 2): - input_payload.append(int(weekend[i]['temp'] * 2)) - - self.send_request(input_payload) - - -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 -} - - -class S1C(device): - """Controls a Broadlink S1C.""" - - 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) - packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if not payload: - return None - count = payload[0x4] - sensors = payload[0x6:] - sensors_a = [bytearray(sensors[i * 83:(i + 1) * 83]) for i in range(len(sensors) // 83)] - - sens_res = [] - for sens in sensors_a: - status = ord(chr(sens[0])) - _name = str(bytes(sens[4:26]).decode()) - _order = ord(chr(sens[1])) - _type = ord(chr(sens[3])) - _serial = bytes(codecs.encode(sens[26:30], "hex")).decode() - - type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown') - - r = { - 'status': status, - 'name': _name.strip('\x00'), - 'type': type_str, - 'order': _order, - 'serial': _serial, - } - if r['serial'] != '00000000': - sens_res.append(r) - result = { - 'count': count, - 'sensors': sens_res - } - return result - - -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" - - def _send(self, magic1: int, magic2: int) -> int: - """Send a packet to the device.""" - packet = bytearray(16) - packet[0] = 0x09 - packet[2] = 0xbb - packet[3] = magic1 - packet[4] = magic2 - packet[9] = 0xfa - packet[10] = 0x44 - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - return ord(payload[4]) - - def open(self) -> int: - """Open the curtain.""" - return self._send(0x01, 0x00) - - def close(self) -> int: - """Close the curtain.""" - return self._send(0x02, 0x00) - - def stop(self) -> int: - """Stop the curtain.""" - return self._send(0x03, 0x00) - - def get_percentage(self) -> int: - """Return the position of the curtain.""" - return self._send(0x06, 0x5d) - - def set_percentage_and_wait(self, new_percentage: int) -> None: - """Set the position of the curtain.""" - current = self.get_percentage() - if current > new_percentage: - self.close() - while current is not None and current > new_percentage: - time.sleep(0.2) - current = self.get_percentage() - - elif current < new_percentage: - self.open() - while current is not None and current < new_percentage: - time.sleep(0.2) - current = self.get_percentage() - self.stop() - - -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, - } - - def __init__(self, *args, **kwargs) -> None: - """Initialize the controller.""" - device.__init__(self, *args, **kwargs) - self.type = "SmartBulb" - - 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 - - response = self.send_packet(0x6a, packet) - check_error(response[0x36:0x38]) - payload = self.decrypt(bytes(response[0x38:])) - - responseLength = int(payload[0x0a]) | (int(payload[0x0b]) << 8) - if responseLength > 0: - self.state_dict = json.loads(payload[0x0e:0x0e+responseLength]) - - 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) - - self.send_command(json.dumps(reconvert)) - return json.dumps(self.state_dict) - - 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 get_state(self) -> dict: - """Return the state of the device.""" - cmd = "{}" - self.send_command(cmd) - return self.state_dict - # 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: diff --git a/broadlink/alarm.py b/broadlink/alarm.py new file mode 100644 index 00000000..ea1bc546 --- /dev/null +++ b/broadlink/alarm.py @@ -0,0 +1,58 @@ +import codecs + +from .device import device +from .exceptions import check_error + + +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 +} + + +class S1C(device): + """Controls a Broadlink S1C.""" + + 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) + packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(bytes(response[0x38:])) + if not payload: + return None + count = payload[0x4] + sensors = payload[0x6:] + sensors_a = [bytearray(sensors[i * 83:(i + 1) * 83]) for i in range(len(sensors) // 83)] + + sens_res = [] + for sens in sensors_a: + status = ord(chr(sens[0])) + _name = str(bytes(sens[4:26]).decode()) + _order = ord(chr(sens[1])) + _type = ord(chr(sens[3])) + _serial = bytes(codecs.encode(sens[26:30], "hex")).decode() + + type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown') + + r = { + 'status': status, + 'name': _name.strip('\x00'), + 'type': type_str, + 'order': _order, + 'serial': _serial, + } + if r['serial'] != '00000000': + sens_res.append(r) + result = { + 'count': count, + 'sensors': sens_res + } + return result diff --git a/broadlink/climate.py b/broadlink/climate.py new file mode 100644 index 00000000..427063f5 --- /dev/null +++ b/broadlink/climate.py @@ -0,0 +1,203 @@ +from typing import List + +from .device import device +from .exceptions import check_error +from .helpers import calculate_crc16 + + +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" + + # 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: + """Send a request to the device.""" + crc = calculate_crc16(bytes(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) + + # send to device + response = self.send_packet(0x6a, request_payload) + check_error(response[0x22:0x24]) + response_payload = bytearray(self.decrypt(bytes(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): + raise ValueError('hysen_response_error', 'first byte of response is not length') + crc = calculate_crc16(bytes(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: + """Return the room temperature in degrees celsius.""" + payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])) + return payload[0x05] / 2.0 + + def get_external_temp(self) -> int: + """Return the external temperature in degrees celsius.""" + payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])) + return payload[18] / 2.0 + + def get_full_status(self) -> dict: + """Return the state of the device. + + Timer schedule included. + """ + payload = self.send_request(bytearray([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['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['fre'] = payload[15] + data['poweron'] = payload[16] + data['unknown'] = payload[17] + data['external_temp'] = (payload[18] & 255) / 2.0 + data['hour'] = payload[19] + data['min'] = payload[20] + data['sec'] = payload[21] + data['dayofweek'] = payload[22] + + weekday = [] + for i in range(0, 6): + weekday.append( + {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0}) + + data['weekday'] = weekday + weekend = [] + for i in range(6, 8): + weekend.append( + {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0}) + + data['weekend'] = weekend + return data + + # Change controller mode + # auto_mode = 1 for auto (scheduled/timed) mode, 0 for manual mode. + # 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 + # 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.""" + mode_byte = ((loop_mode + 1) << 4) + auto_mode + self.send_request(bytearray([0x01, 0x06, 0x00, 0x02, mode_byte, sensor])) + + # Advanced settings + # Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor, + # 2 for internal control temperature, external limit temperature. Factory default: 0. + # Set temperature range for external sensor (OSV) osv = 5..99. Factory default: 42C + # 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 + # 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 + def set_advanced( + self, + loop_mode: int, + sensor: int, + osv: int, + dif: int, + svh: int, + svl: int, + adj: float, + fre: int, + poweron: int, + ) -> None: + """Set advanced options.""" + input_payload = bytearray([0x01, 0x10, 0x00, 0x02, 0x00, 0x05, 0x0a, loop_mode, sensor, osv, dif, svh, svl, + (int(adj * 2) >> 8 & 0xff), (int(adj * 2) & 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. + def switch_to_auto(self) -> None: + """Switch mode to auto.""" + self.set_mode(auto_mode=1, loop_mode=0) + + def switch_to_manual(self) -> None: + """Switch mode to manual.""" + self.set_mode(auto_mode=0, loop_mode=0) + + # 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)])) + + # 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])) + + # 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])) + + # Set timer schedule + # Format is the same as you get from get_full_status. + # weekday is a list (ordered) of 6 dicts like: + # {'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: + """Set timer schedule.""" + # Begin with some magic values ... + input_payload = bytearray([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']) + + # weekend times + for i in range(0, 2): + input_payload.append(weekend[i]['start_hour']) + input_payload.append(weekend[i]['start_minute']) + + # weekday temperatures + for i in range(0, 6): + input_payload.append(int(weekday[i]['temp'] * 2)) + + # weekend temperatures + for i in range(0, 2): + input_payload.append(int(weekend[i]['temp'] * 2)) + + self.send_request(input_payload) diff --git a/broadlink/cover.py b/broadlink/cover.py new file mode 100644 index 00000000..485e6c76 --- /dev/null +++ b/broadlink/cover.py @@ -0,0 +1,59 @@ +import time + +from .device import device +from .exceptions import check_error + + +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" + + def _send(self, magic1: int, magic2: int) -> int: + """Send a packet to the device.""" + packet = bytearray(16) + packet[0] = 0x09 + packet[2] = 0xbb + packet[3] = magic1 + packet[4] = magic2 + packet[9] = 0xfa + packet[10] = 0x44 + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(bytes(response[0x38:])) + return ord(payload[4]) + + def open(self) -> int: + """Open the curtain.""" + return self._send(0x01, 0x00) + + def close(self) -> int: + """Close the curtain.""" + return self._send(0x02, 0x00) + + def stop(self) -> int: + """Stop the curtain.""" + return self._send(0x03, 0x00) + + def get_percentage(self) -> int: + """Return the position of the curtain.""" + return self._send(0x06, 0x5d) + + def set_percentage_and_wait(self, new_percentage: int) -> None: + """Set the position of the curtain.""" + current = self.get_percentage() + if current > new_percentage: + self.close() + while current is not None and current > new_percentage: + time.sleep(0.2) + current = self.get_percentage() + + elif current < new_percentage: + self.open() + while current is not None and current < new_percentage: + time.sleep(0.2) + current = self.get_percentage() + self.stop() diff --git a/broadlink/device.py b/broadlink/device.py new file mode 100644 index 00000000..92916f93 --- /dev/null +++ b/broadlink/device.py @@ -0,0 +1,205 @@ +import socket +import threading +import random +import time +from typing import Tuple, Union + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +from .exceptions import check_error, exception + + +class device: + """Controls a Broadlink device.""" + + def __init__( + self, + host: Tuple[str, int], + mac: Union[bytes, str], + devtype: int, + timeout: int = 10, + name: str = None, + model: str = None, + manufacturer: str = None, + is_locked: bool = None, + ) -> None: + """Initialize the controller.""" + self.host = host + self.mac = mac.encode() if isinstance(mac, str) else mac + self.devtype = devtype if devtype is not None else 0x272a + self.timeout = timeout + self.name = name + self.model = model + self.manufacturer = manufacturer + self.is_locked = is_locked + self.count = random.randrange(0xffff) + self.iv = bytearray( + [0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) + self.id = bytearray([0, 0, 0, 0]) + self.type = "Unknown" + self.lock = threading.Lock() + + self.aes = None + key = bytearray( + [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) + self.update_aes(key) + + def update_aes(self, key: bytes) -> None: + """Update AES.""" + self.aes = Cipher( + algorithms.AES(key), modes.CBC(self.iv), backend=default_backend() + ) + + def encrypt(self, payload: bytes) -> bytes: + """Encrypt the payload.""" + encryptor = self.aes.encryptor() + return encryptor.update(payload) + encryptor.finalize() + + def decrypt(self, payload: bytes) -> bytes: + """Decrypt the payload.""" + decryptor = self.aes.decryptor() + return decryptor.update(payload) + decryptor.finalize() + + 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[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') + + response = self.send_packet(0x65, payload) + check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + + key = payload[0x04:0x14] + if len(key) % 16 != 0: + return False + + self.id = payload[0x03::-1] + self.update_aes(key) + return True + + def get_fwversion(self) -> int: + """Get firmware version.""" + packet = bytearray([0x68]) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return payload[0x4] | payload[0x5] << 8 + + def set_name(self, name: str) -> None: + """Set device name.""" + packet = bytearray(4) + packet += name.encode('utf-8') + packet += bytearray(0x50 - len(packet)) + packet[0x43] = bool(self.is_locked) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + self.name = name + + def set_lock(self, state: bool) -> None: + """Lock/unlock the device.""" + packet = bytearray(4) + packet += self.name.encode('utf-8') + packet += bytearray(0x50 - len(packet)) + packet[0x43] = bool(state) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + self.is_locked = bool(state) + + def get_type(self) -> str: + """Return device type.""" + return self.type + + def send_packet(self, command: int, payload: bytearray) -> bytearray: + """Send a packet to the device.""" + self.count = (self.count + 1) & 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 + if payload: + payload += bytearray((16 - len(payload)) % 16) + + checksum = sum(payload, 0xbeaf) & 0xffff + packet[0x34] = checksum & 0xff + packet[0x35] = checksum >> 8 + + payload = self.encrypt(payload) + for i in range(len(payload)): + packet.append(payload[i]) + + checksum = sum(packet, 0xbeaf) & 0xffff + packet[0x20] = checksum & 0xff + packet[0x21] = checksum >> 8 + + start_time = time.time() + with self.lock: + cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + while True: + try: + cs.sendto(packet, self.host) + cs.settimeout(1) + resp, _ = cs.recvfrom(2048) + resp = bytearray(resp) + break + except socket.timeout: + if (time.time() - start_time) > self.timeout: + cs.close() + raise exception(-4000) # Network timeout. + cs.close() + + if len(resp) < 0x30: + raise exception(-4007) # Length error. + + checksum = resp[0x20] | (resp[0x21] << 8) + if sum(resp, 0xbeaf) - sum(resp[0x20:0x22]) & 0xffff != checksum: + raise exception(-4008) # Checksum error. + + return resp diff --git a/broadlink/light.py b/broadlink/light.py new file mode 100644 index 00000000..27d3ec45 --- /dev/null +++ b/broadlink/light.py @@ -0,0 +1,71 @@ +import json +from typing import Union + +from .device import device +from .exceptions import check_error + + +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, + } + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" + device.__init__(self, *args, **kwargs) + self.type = "SmartBulb" + + 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 + + response = self.send_packet(0x6a, packet) + check_error(response[0x36:0x38]) + payload = self.decrypt(bytes(response[0x38:])) + + responseLength = int(payload[0x0a]) | (int(payload[0x0b]) << 8) + if responseLength > 0: + self.state_dict = json.loads(payload[0x0e:0x0e+responseLength]) + + 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) + + self.send_command(json.dumps(reconvert)) + return json.dumps(self.state_dict) + + 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 get_state(self) -> dict: + """Return the state of the device.""" + cmd = "{}" + self.send_command(cmd) + return self.state_dict diff --git a/broadlink/remote.py b/broadlink/remote.py new file mode 100644 index 00000000..236f6c45 --- /dev/null +++ b/broadlink/remote.py @@ -0,0 +1,133 @@ +from .device import device +from .exceptions import check_error + + +class rm(device): + """Controls a Broadlink RM.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" + device.__init__(self, *args, **kwargs) + self.type = "RM2" + self._request_header = bytes() + self._code_sending_header = bytes() + + def check_data(self) -> bytes: + """Return the last captured code.""" + packet = bytearray(self._request_header) + packet.append(0x04) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(bytes(response[0x38:])) + return payload[len(self._request_header) + 4:] + + def send_data(self, data: bytes) -> None: + """Send a code to the device.""" + packet = bytearray(self._code_sending_header) + packet += bytearray([0x02, 0x00, 0x00, 0x00]) + packet += data + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + + def enter_learning(self) -> None: + """Enter infrared learning mode.""" + packet = bytearray(self._request_header) + packet.append(0x03) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + + def sweep_frequency(self) -> None: + """Sweep frequency.""" + packet = bytearray(self._request_header) + packet.append(0x19) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + + def cancel_sweep_frequency(self) -> None: + """Cancel sweep frequency.""" + packet = bytearray(self._request_header) + packet.append(0x1e) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + + def check_frequency(self) -> bool: + """Return True if the frequency was identified successfully.""" + packet = bytearray(self._request_header) + packet.append(0x1a) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(bytes(response[0x38:])) + if payload[len(self._request_header) + 4] == 1: + return True + return False + + def find_rf_packet(self) -> bool: + """Enter radiofrequency learning mode.""" + packet = bytearray(self._request_header) + packet.append(0x1b) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(bytes(response[0x38:])) + if payload[len(self._request_header) + 4] == 1: + return True + return False + + def _check_sensors(self, command: int) -> bytes: + """Return the state of the sensors in raw format.""" + packet = bytearray(self._request_header) + packet.append(command) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(bytes(response[0x38:])) + return bytearray(payload[len(self._request_header) + 4:]) + + def check_temperature(self) -> int: + """Return the temperature.""" + data = self._check_sensors(0x1) + return data[0x0] + data[0x1] / 10.0 + + def check_sensors(self) -> dict: + """Return the state of the sensors.""" + data = self._check_sensors(0x1) + return {'temperature': data[0x0] + data[0x1] / 10.0} + + +class rm4(rm): + """Controls a Broadlink RM4.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" + device.__init__(self, *args, **kwargs) + self.type = "RM4" + self._request_header = b'\x04\x00' + self._code_sending_header = b'\xda\x00' + + def check_temperature(self) -> int: + """Return the temperature.""" + data = self._check_sensors(0x24) + return data[0x0] + data[0x1] / 100.0 + + def check_humidity(self) -> int: + """Return the humidity.""" + data = self._check_sensors(0x24) + return data[0x2] + data[0x3] / 100.0 + + def check_sensors(self) -> dict: + """Return the state of the sensors.""" + data = self._check_sensors(0x24) + return { + 'temperature': data[0x0] + data[0x1] / 100.0, + 'humidity': data[0x2] + data[0x3] / 100.0 + } + + +# For legacy compatibility - don't use this +class rm2(rm): + def __init__(self): + device.__init__(self, None, None, None) + + def discover(self): + dev = discover() + self.host = dev.host + self.mac = dev.mac + diff --git a/broadlink/sensor.py b/broadlink/sensor.py new file mode 100644 index 00000000..19db0e86 --- /dev/null +++ b/broadlink/sensor.py @@ -0,0 +1,42 @@ +from .device import device +from .exceptions import check_error + + +class a1(device): + """Controls a Broadlink 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() + for sensor, levels in self._SENSORS_AND_LEVELS: + try: + data[sensor] = levels[data[sensor]] + except IndexError: + data[sensor] = 'unknown' + return data + + def check_sensors_raw(self) -> dict: + """Return the state of the sensors in raw format.""" + packet = bytearray([0x1]) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(bytes(response[0x38:])) + data = bytearray(payload[0x4:]) + return { + 'temperature': data[0x0] + data[0x1] / 10.0, + 'humidity': data[0x2] + data[0x3] / 10.0, + 'light': data[0x4], + 'air_quality': data[0x6], + 'noise': data[0x8], + } diff --git a/broadlink/switch.py b/broadlink/switch.py new file mode 100644 index 00000000..fdc24d14 --- /dev/null +++ b/broadlink/switch.py @@ -0,0 +1,227 @@ +import json +import struct + +from .device import device +from .exceptions import check_error + + +class mp1(device): + """Controls a Broadlink MP1.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" + device.__init__(self, *args, **kwargs) + self.type = "MP1" + + def set_power_mask(self, sid_mask: int, state: 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 state else sid_mask) + packet[0x07] = 0xc0 + packet[0x08] = 0x02 + packet[0x0a] = 0x03 + packet[0x0d] = sid_mask + packet[0x0e] = sid_mask if state else 0 + + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + + def set_power(self, sid: int, state: bool) -> None: + """Set the power state of the device.""" + sid_mask = 0x01 << (sid - 1) + self.set_power_mask(sid_mask, state) + + def check_power_raw(self) -> bool: + """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) + check_error(response[0x22:0x24]) + payload = self.decrypt(bytes(response[0x38:])) + return payload[0x0e] + + 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 + + +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" + + 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, b'{}') + response = self.send_packet(0x6a, packet) + 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.""" + data = {} + if pwr is not None: + data['pwr'] = int(bool(pwr)) + if pwr1 is not None: + data['pwr1'] = int(bool(pwr1)) + if pwr2 is not None: + data['pwr2'] = int(bool(pwr2)) + if maxworktime is not None: + data['maxworktime'] = maxworktime + if maxworktime1 is not None: + data['maxworktime1'] = maxworktime1 + if maxworktime2 is not None: + data['maxworktime2'] = maxworktime2 + if idcbrightness is not None: + data['idcbrightness'] = idcbrightness + js = json.dumps(data).encode('utf8') + packet = self._encode(2, js) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + return self._decode(response) + + def _encode(self, flag: int, js: str) -> bytearray: + """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) + struct.pack_into('> 8 + return packet + + def _decode(self, response: bytes) -> dict: + """Decode a message.""" + payload = self.decrypt(bytes(response[0x38:])) + js_len = struct.unpack_from(' None: + """Initialize the device.""" + device.__init__(self, *args, **kwargs) + self.type = "SP1" + + def set_power(self, state: bool) -> None: + """Set the power state of the device.""" + packet = bytearray(4) + packet[0] = state + response = self.send_packet(0x66, packet) + check_error(response[0x22:0x24]) + + +class sp2(device): + """Controls a Broadlink SP2.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" + device.__init__(self, *args, **kwargs) + self.type = "SP2" + + def set_power(self, state: 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 + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + + def set_nightlight(self, state: 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 + 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(bytes(response[0x38:])) + if isinstance(payload[0x4], int): + return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD) + return bool(ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFD) + + def check_nightlight(self) -> bool: + """Return the state of the night light.""" + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(bytes(response[0x38:])) + if isinstance(payload[0x4], int): + return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) + return bool(ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFF) + + def get_energy(self) -> int: + """Return the energy state of the device.""" + packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) + response = self.send_packet(0x6a, packet) + check_error(response[0x22:0x24]) + payload = self.decrypt(bytes(response[0x38:])) + if isinstance(payload[0x7], int): + energy = int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0 + else: + energy = int(hex(ord(payload[0x07]) * 256 + ord(payload[0x06]))[2:]) + int( + hex(ord(payload[0x05]))[2:]) / 100.0 + return energy From 487a13d895948feee8646788bd1d6e9cc31db753 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Thu, 17 Sep 2020 03:19:24 -0300 Subject: [PATCH 138/261] Improve annotations --- broadlink/__init__.py | 8 ++++---- broadlink/climate.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index c3798dca..8b0eca90 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -3,7 +3,7 @@ import socket import time from datetime import datetime -from typing import List, Union, Tuple +from typing import Dict, List, Union, Tuple, Type from .alarm import S1C from .cover import dooya @@ -17,7 +17,7 @@ from .helpers import calculate_crc16, get_local_ip -def get_devices() -> dict: +def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: """Return all supported devices.""" return { 0x0000: (sp1, "SP1", "Broadlink"), @@ -90,7 +90,7 @@ def gendevice( mac: Union[bytes, str], name: str = None, is_locked: bool = None, -): +) -> device: """Generate a device.""" try: dev_class, model, manufacturer = get_devices()[dev_type] @@ -114,7 +114,7 @@ def discover( local_ip_address: str = None, discover_ip_address: str = '255.255.255.255', discover_ip_port: int = 80, -) -> list: +) -> List[device]: """Discover devices connected to the local network.""" local_ip_address = local_ip_address or get_local_ip() address = local_ip_address.split('.') diff --git a/broadlink/climate.py b/broadlink/climate.py index 427063f5..353ad593 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -19,7 +19,7 @@ def __init__(self, *args, **kwargs) -> None: # 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, input_payload: bytearray) -> bytes: """Send a request to the device.""" crc = calculate_crc16(bytes(input_payload)) From de38e237ca0d217a6107aa66020486cc1395b5c1 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Sun, 20 Sep 2020 01:34:31 -0300 Subject: [PATCH 139/261] Clean up imports --- broadlink/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 8b0eca90..69e4680a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -6,15 +6,14 @@ from typing import Dict, List, Union, Tuple, Type from .alarm import S1C -from .cover import dooya from .climate import hysen +from .cover import dooya from .device import device -from .exceptions import check_error, exception +from .helpers import get_local_ip from .light import lb1 from .remote import rm, rm2, rm4 from .sensor import a1 from .switch import bg1, mp1, sp1, sp2 -from .helpers import calculate_crc16, get_local_ip def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: From 5ef4124491d6e226d0d55476e894dcc29c79f5d4 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Sun, 20 Sep 2020 06:16:49 -0300 Subject: [PATCH 140/261] Remove legacy byte conversions --- broadlink/alarm.py | 14 ++++++-------- broadlink/climate.py | 8 ++++---- broadlink/cover.py | 4 ++-- broadlink/device.py | 15 ++++++++------- broadlink/helpers.py | 33 ++++++++++----------------------- broadlink/light.py | 2 +- broadlink/remote.py | 8 ++++---- broadlink/sensor.py | 2 +- broadlink/switch.py | 27 +++++++++------------------ 9 files changed, 45 insertions(+), 68 deletions(-) diff --git a/broadlink/alarm.py b/broadlink/alarm.py index ea1bc546..62dbb304 100644 --- a/broadlink/alarm.py +++ b/broadlink/alarm.py @@ -1,5 +1,3 @@ -import codecs - from .device import device from .exceptions import check_error @@ -25,7 +23,7 @@ def get_sensors_status(self) -> dict: packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) + payload = self.decrypt(response[0x38:]) if not payload: return None count = payload[0x4] @@ -34,11 +32,11 @@ def get_sensors_status(self) -> dict: sens_res = [] for sens in sensors_a: - status = ord(chr(sens[0])) - _name = str(bytes(sens[4:26]).decode()) - _order = ord(chr(sens[1])) - _type = ord(chr(sens[3])) - _serial = bytes(codecs.encode(sens[26:30], "hex")).decode() + status = sens[0] + _name = sens[4:26].decode() + _order = sens[1] + _type = sens[3] + _serial = sens[26:30].hex() type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown') diff --git a/broadlink/climate.py b/broadlink/climate.py index 353ad593..3d68eb4b 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -19,9 +19,9 @@ def __init__(self, *args, **kwargs) -> None: # 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: bytearray) -> bytes: + def send_request(self, input_payload: bytes) -> bytes: """Send a request to the device.""" - crc = calculate_crc16(bytes(input_payload)) + crc = calculate_crc16(input_payload) # first byte is length, +2 for CRC16 request_payload = bytearray([len(input_payload) + 2, 0x00]) @@ -34,13 +34,13 @@ def send_request(self, input_payload: bytearray) -> bytes: # send to device response = self.send_packet(0x6a, request_payload) check_error(response[0x22:0x24]) - response_payload = bytearray(self.decrypt(bytes(response[0x38:]))) + response_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): raise ValueError('hysen_response_error', 'first byte of response is not length') - crc = calculate_crc16(bytes(response_payload[2:response_payload_len])) + crc = calculate_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): return response_payload[2:response_payload_len] diff --git a/broadlink/cover.py b/broadlink/cover.py index 485e6c76..b6b3be17 100644 --- a/broadlink/cover.py +++ b/broadlink/cover.py @@ -23,8 +23,8 @@ def _send(self, magic1: int, magic2: int) -> int: packet[10] = 0x44 response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - return ord(payload[4]) + payload = self.decrypt(response[0x38:]) + return payload[4] def open(self) -> int: """Open the curtain.""" diff --git a/broadlink/device.py b/broadlink/device.py index 92916f93..bf29ea3a 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -34,14 +34,14 @@ def __init__( self.manufacturer = manufacturer self.is_locked = is_locked self.count = random.randrange(0xffff) - self.iv = bytearray( + self.iv = bytes( [0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) - self.id = bytearray([0, 0, 0, 0]) + self.id = bytes(4) self.type = "Unknown" self.lock = threading.Lock() self.aes = None - key = bytearray( + key = bytes( [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) self.update_aes(key) @@ -133,7 +133,7 @@ def get_type(self) -> str: """Return device type.""" return self.type - def send_packet(self, command: int, payload: bytearray) -> bytearray: + def send_packet(self, command: int, payload: bytes) -> bytes: """Send a packet to the device.""" self.count = (self.count + 1) & 0xffff packet = bytearray(0x38) @@ -162,8 +162,10 @@ def send_packet(self, command: int, payload: bytearray) -> bytearray: packet[0x33] = self.id[0] # pad the payload for AES encryption - if payload: - payload += bytearray((16 - len(payload)) % 16) + padding = (16 - len(payload)) % 16 + if padding: + payload = bytearray(payload) + payload += bytearray(padding) checksum = sum(payload, 0xbeaf) & 0xffff packet[0x34] = checksum & 0xff @@ -187,7 +189,6 @@ def send_packet(self, command: int, payload: bytearray) -> bytearray: cs.sendto(packet, self.host) cs.settimeout(1) resp, _ = cs.recvfrom(2048) - resp = bytearray(resp) break except socket.timeout: if (time.time() - start_time) > self.timeout: diff --git a/broadlink/helpers.py b/broadlink/helpers.py index 22ac12f5..4b214782 100644 --- a/broadlink/helpers.py +++ b/broadlink/helpers.py @@ -21,8 +21,8 @@ def get_local_ip() -> str: return s.getsockname()[0] -def calculate_crc16(input_data) -> int: - """Calculate CRC-16.""" +def calculate_crc16(input_data: bytes) -> int: + """Calculate the CRC-16 of a byte string.""" crc16_tab = [] crc16_constant = 0xA001 @@ -35,24 +35,11 @@ def calculate_crc16(input_data) -> int: crc = c_ushort(crc >> 1).value crc16_tab.append(hex(crc)) - try: - is_string = isinstance(input_data, str) - is_bytes = isinstance(input_data, bytes) - - if not is_string and not is_bytes: - raise Exception( - "Please provide a string or a byte sequence " - "as argument for calculation." - ) - - crcValue = 0xFFFF - - for c in input_data: - d = ord(c) if is_string else c - tmp = crcValue ^ d - rotated = c_ushort(crcValue >> 8).value - crcValue = rotated ^ int(crc16_tab[(tmp & 0x00FF)], 0) - - return crcValue - except Exception as e: - print("EXCEPTION(calculate): {}".format(e)) + crcValue = 0xFFFF + + for c in input_data: + tmp = crcValue ^ c + rotated = c_ushort(crcValue >> 8).value + crcValue = rotated ^ int(crc16_tab[(tmp & 0x00FF)], 0) + + return crcValue diff --git a/broadlink/light.py b/broadlink/light.py index 27d3ec45..1cac4a5c 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -44,7 +44,7 @@ def send_command(self, command: str, type: str = 'set') -> None: response = self.send_packet(0x6a, packet) check_error(response[0x36:0x38]) - payload = self.decrypt(bytes(response[0x38:])) + payload = self.decrypt(response[0x38:]) responseLength = int(payload[0x0a]) | (int(payload[0x0b]) << 8) if responseLength > 0: diff --git a/broadlink/remote.py b/broadlink/remote.py index 236f6c45..a08c6758 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -18,7 +18,7 @@ def check_data(self) -> bytes: packet.append(0x04) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) + payload = self.decrypt(response[0x38:]) return payload[len(self._request_header) + 4:] def send_data(self, data: bytes) -> None: @@ -56,7 +56,7 @@ def check_frequency(self) -> bool: packet.append(0x1a) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) + payload = self.decrypt(response[0x38:]) if payload[len(self._request_header) + 4] == 1: return True return False @@ -67,7 +67,7 @@ def find_rf_packet(self) -> bool: packet.append(0x1b) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) + payload = self.decrypt(response[0x38:]) if payload[len(self._request_header) + 4] == 1: return True return False @@ -78,7 +78,7 @@ def _check_sensors(self, command: int) -> bytes: packet.append(command) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) + payload = self.decrypt(response[0x38:]) return bytearray(payload[len(self._request_header) + 4:]) def check_temperature(self) -> int: diff --git a/broadlink/sensor.py b/broadlink/sensor.py index 19db0e86..819518b0 100644 --- a/broadlink/sensor.py +++ b/broadlink/sensor.py @@ -31,7 +31,7 @@ def check_sensors_raw(self) -> dict: packet = bytearray([0x1]) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) + payload = self.decrypt(response[0x38:]) data = bytearray(payload[0x4:]) return { 'temperature': data[0x0] + data[0x1] / 10.0, diff --git a/broadlink/switch.py b/broadlink/switch.py index fdc24d14..6b287bdf 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -50,7 +50,7 @@ def check_power_raw(self) -> bool: response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) + payload = self.decrypt(response[0x38:]) return payload[0x0e] def check_power(self) -> dict: @@ -116,7 +116,7 @@ def set_state( check_error(response[0x22:0x24]) return self._decode(response) - def _encode(self, flag: int, js: str) -> bytearray: + def _encode(self, flag: int, js: str) -> bytes: """Encode a message.""" # The packet format is: # 0x00-0x01 length @@ -139,7 +139,7 @@ def _encode(self, flag: int, js: str) -> bytearray: def _decode(self, response: bytes) -> dict: """Decode a message.""" - payload = self.decrypt(bytes(response[0x38:])) + payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(' bool: packet[0] = 1 response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if isinstance(payload[0x4], int): - return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD) - return bool(ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFD) + payload = self.decrypt(response[0x38:]) + return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD) def check_nightlight(self) -> bool: """Return the state of the night light.""" @@ -208,20 +206,13 @@ def check_nightlight(self) -> bool: packet[0] = 1 response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if isinstance(payload[0x4], int): - return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) - return bool(ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFF) + payload = self.decrypt(response[0x38:]) + return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) def get_energy(self) -> int: """Return the energy state of the device.""" packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) response = self.send_packet(0x6a, packet) check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if isinstance(payload[0x7], int): - energy = int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0 - else: - energy = int(hex(ord(payload[0x07]) * 256 + ord(payload[0x06]))[2:]) + int( - hex(ord(payload[0x05]))[2:]) / 100.0 - return energy + payload = self.decrypt(response[0x38:]) + return int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0 From 6ac060f1418192798270526447c823ffbbf76e43 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 18 Sep 2020 20:12:09 -0300 Subject: [PATCH 141/261] Add support for Ankuoo NEO and NEO PRO --- broadlink/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 69e4680a..3afee7ad 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -21,6 +21,8 @@ def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: return { 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"), 0x2720: (sp2, "SP mini", "Broadlink"), From 91ae5952ee5e344def30fcbd6b8fb2489fe16a58 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 19 Sep 2020 19:31:34 -0300 Subject: [PATCH 142/261] Add support for OEM Broadlink SP2-IL (0x7539) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 3afee7ad..4f088d9b 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -31,6 +31,7 @@ def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: 0x2736: (sp2, "SP mini+", "Broadlink"), 0x273e: (sp2, "SP mini", "Broadlink"), 0x7530: (sp2, "SP2", "Broadlink (OEM)"), + 0x7539: (sp2, "SP2-IL", "Broadlink (OEM)"), 0x753e: (sp2, "SP mini 3", "Broadlink"), 0X7544: (sp2, "SP2-CL", "Broadlink"), 0x7546: (sp2, "SP2-UK/BR/IN", "Broadlink (OEM)"), From fc0244e6a42ee9d42b7d86e0c68ec1a094af185c Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 19 Sep 2020 13:27:43 -0300 Subject: [PATCH 143/261] Add support for Broadlink RM mini 3 (0x27d0) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 4f088d9b..560fec76 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -58,6 +58,7 @@ def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: 0x27a6: (rm, "RM plus", "Broadlink"), 0x27a9: (rm, "RM pro+", "Broadlink"), 0x27c2: (rm, "RM mini 3", "Broadlink"), + 0x27d0, (rm, "RM mini 3", "Broadlink"), 0x27d1: (rm, "RM mini 3", "Broadlink"), 0x27de: (rm, "RM mini 3", "Broadlink"), 0x51da: (rm4, "RM4 mini", "Broadlink"), From 05440417beebd443aa819284f3f4309083e987d4 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 19 Sep 2020 13:34:07 -0300 Subject: [PATCH 144/261] Add support for Broadlink RM mini 3 (0x27cd) --- broadlink/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 560fec76..34977b08 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -58,7 +58,8 @@ def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: 0x27a6: (rm, "RM plus", "Broadlink"), 0x27a9: (rm, "RM pro+", "Broadlink"), 0x27c2: (rm, "RM mini 3", "Broadlink"), - 0x27d0, (rm, "RM mini 3", "Broadlink"), + 0x27cd: (rm, "RM mini 3", "Broadlink"), + 0x27d0: (rm, "RM mini 3", "Broadlink"), 0x27d1: (rm, "RM mini 3", "Broadlink"), 0x27de: (rm, "RM mini 3", "Broadlink"), 0x51da: (rm4, "RM4 mini", "Broadlink"), From d17fb228cfcb07eaa3dc84fcb52119605b77dd29 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 20 Sep 2020 21:14:01 -0300 Subject: [PATCH 145/261] Add support for Broadlink RM pro+ (0x27c3) (#422) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 34977b08..7f862eea 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -58,6 +58,7 @@ def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: 0x27a6: (rm, "RM plus", "Broadlink"), 0x27a9: (rm, "RM pro+", "Broadlink"), 0x27c2: (rm, "RM mini 3", "Broadlink"), + 0x27c3: (rm, "RM pro+", "Broadlink"), 0x27cd: (rm, "RM mini 3", "Broadlink"), 0x27d0: (rm, "RM mini 3", "Broadlink"), 0x27d1: (rm, "RM mini 3", "Broadlink"), From 868153bfd1c142f660500244068a2cc065fd7313 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Thu, 17 Sep 2020 17:02:43 -0300 Subject: [PATCH 146/261] Add support for Broadlink MP1-1K3S2U (0x4f1b) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 7f862eea..29e8bd8f 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -76,6 +76,7 @@ def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: 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"), From a4f76bcf36893d756de02f40ad597820ef1e2b0c Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Thu, 17 Sep 2020 19:09:45 -0300 Subject: [PATCH 147/261] Add support for Broadlink MP2 (0x7540) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 29e8bd8f..5cae0e29 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -33,6 +33,7 @@ def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: 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"), From 2ae9b9db12840d0e6658f236468e1e89fa2ac17e Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 20 Sep 2020 21:15:56 -0300 Subject: [PATCH 148/261] Add support for Broadlink RM4 pro (0x649b) (#421) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 5cae0e29..c2811903 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -74,6 +74,7 @@ def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: 0x62bc: (rm4, "RM4 mini", "Broadlink"), 0x62be: (rm4, "RM4C mini", "Broadlink"), 0x648d: (rm4, "RM4 mini", "Broadlink"), + 0x649b: (rm4, "RM4 pro", "Broadlink"), 0x2714: (a1, "e-Sensor", "Broadlink"), 0x4eb5: (mp1, "MP1-1K4S", "Broadlink"), 0x4ef7: (mp1, "MP1-1K4S", "Broadlink (OEM)"), From 6ab23e92614ea89df1fe168a00bcf74d91356ed6 Mon Sep 17 00:00:00 2001 From: sivero Date: Sun, 20 Sep 2020 21:17:10 -0300 Subject: [PATCH 149/261] Update __init__.py (#423) Add support for (0x27cc) RM mini 3 --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index c2811903..7951f8f7 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -60,6 +60,7 @@ def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: 0x27a9: (rm, "RM pro+", "Broadlink"), 0x27c2: (rm, "RM mini 3", "Broadlink"), 0x27c3: (rm, "RM pro+", "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"), From 9248ee6b0c633b9c98d548e2551da4d6786600ed Mon Sep 17 00:00:00 2001 From: Tamarinen Date: Wed, 23 Sep 2020 02:58:07 +0200 Subject: [PATCH 150/261] Add sp4 refactored (#429) * Add SP4 device class * Remove legacy byte conversion * Implement suggested improvements * Apply some love to _encode() and _decode() too. * Remove unnecessary safeguards * Remove unnecessary return values * Add missing periods to the comments. Co-authored-by: Martin Samuelsson --- broadlink/__init__.py | 3 +- broadlink/switch.py | 92 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 7951f8f7..f8ce3393 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -13,7 +13,7 @@ from .light import lb1 from .remote import rm, rm2, rm4 from .sensor import a1 -from .switch import bg1, mp1, sp1, sp2 +from .switch import bg1, mp1, sp1, sp2, sp4 def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: @@ -44,6 +44,7 @@ def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: 0x7d0d: (sp2, "SP mini 3", "Broadlink (OEM)"), 0x9479: (sp2, "SP3S-US", "Broadlink"), 0x947a: (sp2, "SP3S-EU", "Broadlink"), + 0x7579: (sp4, "SP4L-EU", "Broadlink"), 0x2712: (rm, "RM pro/pro+", "Broadlink"), 0x272a: (rm, "RM pro", "Broadlink"), 0x2737: (rm, "RM mini 3", "Broadlink"), diff --git a/broadlink/switch.py b/broadlink/switch.py index 6b287bdf..34614f09 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -216,3 +216,95 @@ def get_energy(self) -> int: check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0 + + +class sp4(device): + """Controls a Broadlink SP4.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize the controller.""" + device.__init__(self, *args, **kwargs) + self.type = "SP4" + + def set_power(self, state: bool) -> None: + """Set the power state of the device.""" + self.set_state(pwr=state) + + def set_nightlight(self, state: bool) -> None: + """Set the night light state of the device.""" + self.set_state(ntlight=state) + + def set_state( + self, + pwr: bool = None, + ntlight: bool = None, + indicator: bool = None, + ntlbrightness: int = None, + maxworktime: int = None, + ) -> dict: + """Set state of device.""" + data = {} + if pwr is not None: + data["pwr"] = pwr + if ntlight is not None: + data["ntlight"] = ntlight + if indicator is not None: + data["indicator"] = indicator + if ntlbrightness is not None: + data["ntlbrightness"] = ntlbrightness + if maxworktime is not None: + data["maxworktime"] = maxworktime + + js = json.dumps(data).encode("utf8") + packet = self._encode(2, js) + response = self.send_packet(0x6A, packet) + return self._decode(response) + + def check_power(self) -> bool: + """Return the power state of the device.""" + state = self.get_state() + return state["pwr"] + + def check_nightlight(self) -> bool: + """Return the night light state of the device.""" + state = self.get_state() + return state["ntlight"] + + def get_state(self) -> dict: + """Get full state of device.""" + packet = self._encode(1, b"{}") + response = self.send_packet(0x6A, packet) + return self._decode(response) + + def _encode(self, flag: int, js: str) -> bytes: + """Encode a message.""" + # SP4 support added by Petter Olofsson + # packet format is: + # 0x00-0x03 header 0xa5a5, 0x5a5a + # 0x04-0x05 "0xbeaf" checksum + # 0x06 flag (1 for read or 2 write?) + # 0x07 unknown (0xb) + # 0x08-0x0b length of json + # 0x0c- json data + packet = bytearray(14) + struct.pack_into( + "> 8 + return packet + + def _decode(self, response: bytes) -> dict: + """Decode a message.""" + check_error(response[0x22:0x24]) + + payload = self.decrypt(response[0x38:]) + js_len = struct.unpack_from(" Date: Wed, 23 Sep 2020 02:43:56 -0300 Subject: [PATCH 151/261] Make bind() optional and implement a generator for device discovery (#427) --- broadlink/__init__.py | 133 ++++++++++++++-------------------------- broadlink/device.py | 105 ++++++++++++++++++++++++++++++- broadlink/exceptions.py | 12 ++-- broadlink/helpers.py | 19 ------ 4 files changed, 155 insertions(+), 114 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index f8ce3393..bc5faeec 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,15 +1,13 @@ #!/usr/bin/python3 """The python-broadlink library.""" import socket -import time -from datetime import datetime from typing import Dict, List, Union, Tuple, Type from .alarm import S1C from .climate import hysen from .cover import dooya -from .device import device -from .helpers import get_local_ip +from .device import device, scan +from .exceptions import exception from .light import lb1 from .remote import rm, rm2, rm4 from .sensor import a1 @@ -95,11 +93,11 @@ def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: def gendevice( - dev_type: int, - host: Tuple[str, int], - mac: Union[bytes, str], - name: str = None, - is_locked: bool = None, + dev_type: int, + host: Tuple[str, int], + mac: Union[bytes, str], + name: str = None, + is_locked: bool = None, ) -> device: """Generate a device.""" try: @@ -119,91 +117,50 @@ def gendevice( ) +def hello( + host: str, + port: int = 80, + timeout: int = 10, + 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)) + except StopIteration: + raise exception(-4000) # Network timeout. + + def discover( - timeout: int = None, + timeout: int = 10, local_ip_address: str = None, discover_ip_address: str = '255.255.255.255', discover_ip_port: int = 80, ) -> List[device]: """Discover devices connected to the local network.""" - local_ip_address = local_ip_address or get_local_ip() - address = local_ip_address.split('.') - cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - cs.bind((local_ip_address, 0)) - port = cs.getsockname()[1] - starttime = time.time() - - devices = [] - - timezone = int(time.timezone / -3600) - packet = bytearray(0x30) - - year = datetime.now().year - - 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 - - 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 - packet[0x18] = int(address[0]) - packet[0x19] = int(address[1]) - packet[0x1a] = int(address[2]) - packet[0x1b] = int(address[3]) - packet[0x1c] = port & 0xff - packet[0x1d] = port >> 8 - packet[0x26] = 6 - - checksum = sum(packet, 0xbeaf) & 0xffff - packet[0x20] = checksum & 0xff - packet[0x21] = checksum >> 8 - - cs.sendto(packet, (discover_ip_address, discover_ip_port)) - if timeout is None: - response = cs.recvfrom(1024) - responsepacket = bytearray(response[0]) - host = response[1] - devtype = responsepacket[0x34] | responsepacket[0x35] << 8 - mac = responsepacket[0x3f:0x39:-1] - name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8') - is_locked = bool(responsepacket[-1]) - device = gendevice(devtype, host, mac, name=name, is_locked=is_locked) - cs.close() - return device - - while (time.time() - starttime) < timeout: - cs.settimeout(timeout - (time.time() - starttime)) - try: - response = cs.recvfrom(1024) - except socket.timeout: - cs.close() - return devices - responsepacket = bytearray(response[0]) - host = response[1] - devtype = responsepacket[0x34] | responsepacket[0x35] << 8 - mac = responsepacket[0x3f:0x39:-1] - name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8') - is_locked = bool(responsepacket[-1]) - device = gendevice(devtype, host, mac, name=name, is_locked=is_locked) - devices.append(device) - cs.close() - return devices + responses = scan( + timeout, local_ip_address, discover_ip_address, discover_ip_port + ) + return [gendevice(*resp) for resp in responses] + + +def xdiscover( + timeout: int = 10, + local_ip_address: str = None, + discover_ip_address: str = '255.255.255.255', + discover_ip_port: int = 80, +) -> Generator[device, None, None]: + """Discover devices connected to the local network. + + This function returns a generator that yields devices instantly. + """ + responses = scan( + timeout, local_ip_address, discover_ip_address, discover_ip_port + ) + for resp in responses: + yield gendevice(*resp) # Setup a new Broadlink device via AP Mode. Review the README to see how to enter AP Mode. diff --git a/broadlink/device.py b/broadlink/device.py index bf29ea3a..e1ad59d7 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -2,13 +2,93 @@ import threading import random import time -from typing import Tuple, Union +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 +HelloResponse = Tuple[int, Tuple[str, int], str, str, bool] + + +def scan( + timeout: int = 10, + local_ip_address: str = None, + discover_ip_address: str = '255.255.255.255', + discover_ip_port: int = 80, +) -> Generator[HelloResponse, None, None]: + """Broadcast a hello message and yield responses.""" + cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + if local_ip_address: + cs.bind((local_ip_address, 0)) + port = cs.getsockname()[1] + else: + local_ip_address = "0.0.0.0" + port = 0 + + address = local_ip_address.split('.') + starttime = time.time() + + timezone = int(time.timezone / -3600) + packet = bytearray(0x30) + + year = datetime.now().year + + 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 + + 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 + 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[0x26] = 6 + + checksum = sum(packet, 0xbeaf) & 0xffff + packet[0x20] = checksum & 0xff + packet[0x21] = checksum >> 8 + + cs.sendto(packet, (discover_ip_address, discover_ip_port)) + + try: + while (time.time() - starttime) < timeout: + cs.settimeout(timeout - (time.time() - starttime)) + try: + response, host = cs.recvfrom(1024) + except socket.timeout: + break + + devtype = response[0x34] | response[0x35] << 8 + mac = bytes(reversed(response[0x3a:0x40])) + name = response[0x40:].split(b'\x00')[0].decode('utf-8') + is_locked = bool(response[-1]) + yield devtype, host, mac, name, is_locked + finally: + cs.close() + class device: """Controls a Broadlink device.""" @@ -101,6 +181,29 @@ def auth(self) -> bool: self.update_aes(key) return True + def hello(self, local_ip_address=None) -> bool: + """Send a hello message to the device. + + Device information is checked before updating name and lock status. + """ + responses = scan( + timeout=self.timeout, + local_ip_address=local_ip_address, + discover_ip_address=self.host[0], + discover_ip_port=self.host[1], + ) + try: + devtype, host, mac, name, is_locked = next(responses) + except StopIteration: + raise exception(-4000) # Network timeout. + + if (devtype, host, mac) != (self.devtype, self.host, self.mac): + raise exception(-2040) # Device information is not intact. + + self.name = name + self.is_locked = is_locked + return True + def get_fwversion(self) -> int: """Get firmware version.""" packet = bytearray([0x68]) diff --git a/broadlink/exceptions.py b/broadlink/exceptions.py index 8e3aa4a9..d0e5bc0f 100644 --- a/broadlink/exceptions.py +++ b/broadlink/exceptions.py @@ -80,6 +80,10 @@ 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.""" @@ -88,10 +92,6 @@ class LengthError(SDKException): """Received data packet length error.""" -class DNSLookupError(SDKException): - """Failed to obtain local IP address.""" - - class NetworkTimeoutError(SDKException): """Network timeout error.""" @@ -113,11 +113,11 @@ class UnknownError(BroadlinkException): -9: (WriteError, "Write error"), -10: (ReadError, "Read error"), -11: (SSIDNotFoundError, "SSID could not be found in AP configuration"), - # DNASDK related errors are generated by this module. + # SDK related errors are generated by this module. + -2040: (DeviceInformationError, "Device information is not intact"), -4000: (NetworkTimeoutError, "Network timeout"), -4007: (LengthError, "Received data packet length error"), -4008: (ChecksumError, "Received data packet check error"), - -4013: (DNSLookupError, "Failed to obtain local IP address"), } diff --git a/broadlink/helpers.py b/broadlink/helpers.py index 4b214782..404feadd 100644 --- a/broadlink/helpers.py +++ b/broadlink/helpers.py @@ -1,24 +1,5 @@ """Helper functions.""" from ctypes import c_ushort -import socket - -from .exceptions import exception - - -def get_local_ip() -> str: - """Try to determine the local IP address of the machine.""" - # Useful for VPNs. - try: - local_ip_address = socket.gethostbyname(socket.gethostname()) - if not local_ip_address.startswith('127.'): - return local_ip_address - except socket.gaierror: - raise exception(-4013) # DNS Error - - # Connecting to UDP address does not send packets. - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: - s.connect(('8.8.8.8', 53)) - return s.getsockname()[0] def calculate_crc16(input_data: bytes) -> int: From 0dc0068d63d2e07cf7524854aff16022b544c911 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 24 Sep 2020 02:36:12 -0300 Subject: [PATCH 152/261] Improve code quality (#428) * Fix lint errors * Remove rm2 class * Rename cs to conn * Add __repr__ to device class * Make get_devices() a dictionary * Clean up alarm kit * Add module doscstrings * Fix MAC address conversion --- broadlink/__init__.py | 158 +++++++++++++++++++++--------------------- broadlink/alarm.py | 56 +++++++-------- broadlink/climate.py | 1 + broadlink/cover.py | 1 + broadlink/device.py | 72 +++++++++++-------- broadlink/light.py | 1 + broadlink/remote.py | 13 +--- broadlink/sensor.py | 1 + broadlink/switch.py | 1 + 9 files changed, 150 insertions(+), 154 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index bc5faeec..7324d37e 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 """The python-broadlink library.""" import socket -from typing import Dict, List, Union, Tuple, Type +from typing import Generator, List, Union, Tuple from .alarm import S1C from .climate import hysen @@ -9,87 +9,85 @@ from .device import device, scan from .exceptions import exception from .light import lb1 -from .remote import rm, rm2, rm4 +from .remote import rm, rm4 from .sensor import a1 from .switch import bg1, mp1, sp1, sp2, sp4 -def get_devices() -> Dict[int, Tuple[Type[device], str, str]]: - """Return all supported devices.""" - return { - 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"), - 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)"), - 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"), - 0x7d00: (sp2, "SP3-EU", "Broadlink (OEM)"), - 0x7d0d: (sp2, "SP mini 3", "Broadlink (OEM)"), - 0x9479: (sp2, "SP3S-US", "Broadlink"), - 0x947a: (sp2, "SP3S-EU", "Broadlink"), - 0x7579: (sp4, "SP4L-EU", "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"), - 0x27cc: (rm, "RM mini 3", "Broadlink"), - 0x27cd: (rm, "RM mini 3", "Broadlink"), - 0x27d0: (rm, "RM mini 3", "Broadlink"), - 0x27d1: (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"), - 0x648d: (rm4, "RM4 mini", "Broadlink"), - 0x649b: (rm4, "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"), - 0x2722: (S1C, "S2KIT", "Broadlink"), - 0x4ead: (hysen, "HY02B05H", "Hysen"), - 0x4e4d: (dooya, "DT360E-45/20", "Dooya"), - 0x51e3: (bg1, "BG800/BG900", "BG Electrical"), - } +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"), + 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)"), + 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"), + 0x7d00: (sp2, "SP3-EU", "Broadlink (OEM)"), + 0x7d0d: (sp2, "SP mini 3", "Broadlink (OEM)"), + 0x9479: (sp2, "SP3S-US", "Broadlink"), + 0x947a: (sp2, "SP3S-EU", "Broadlink"), + 0x7579: (sp4, "SP4L-EU", "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"), + 0x27cc: (rm, "RM mini 3", "Broadlink"), + 0x27cd: (rm, "RM mini 3", "Broadlink"), + 0x27d0: (rm, "RM mini 3", "Broadlink"), + 0x27d1: (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"), + 0x648d: (rm4, "RM4 mini", "Broadlink"), + 0x649b: (rm4, "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"), + 0x2722: (S1C, "S2KIT", "Broadlink"), + 0x4ead: (hysen, "HY02B05H", "Hysen"), + 0x4e4d: (dooya, "DT360E-45/20", "Dooya"), + 0x51e3: (bg1, "BG800/BG900", "BG Electrical"), +} def gendevice( @@ -101,7 +99,7 @@ def gendevice( ) -> device: """Generate a device.""" try: - dev_class, model, manufacturer = get_devices()[dev_type] + dev_class, model, manufacturer = SUPPORTED_TYPES[dev_type] except KeyError: return device(host, mac, dev_type, name=name, is_locked=is_locked) @@ -185,7 +183,7 @@ def setup(ssid: str, password: str, security_mode: int) -> None: payload[0x84] = ssid_length # Character length of SSID payload[0x85] = pass_length # Character length of password - payload[0x86] = security_mode # Type of encryption (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2) + payload[0x86] = security_mode # Type of encryption checksum = sum(payload, 0xbeaf) & 0xffff payload[0x20] = checksum & 0xff # Checksum 1 position diff --git a/broadlink/alarm.py b/broadlink/alarm.py index 62dbb304..faded9d4 100644 --- a/broadlink/alarm.py +++ b/broadlink/alarm.py @@ -1,17 +1,17 @@ +"""Support for alarm kits.""" from .device import device from .exceptions import check_error -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 -} - - class S1C(device): """Controls a Broadlink 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) @@ -27,30 +27,22 @@ def get_sensors_status(self) -> dict: if not payload: return None count = payload[0x4] - sensors = payload[0x6:] - sensors_a = [bytearray(sensors[i * 83:(i + 1) * 83]) for i in range(len(sensors) // 83)] - - sens_res = [] - for sens in sensors_a: - status = sens[0] - _name = sens[4:26].decode() - _order = sens[1] - _type = sens[3] - _serial = sens[26:30].hex() - - type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown') - - r = { - 'status': status, - 'name': _name.strip('\x00'), - 'type': type_str, - 'order': _order, - 'serial': _serial, - } - if r['serial'] != '00000000': - sens_res.append(r) - result = { + sensor_data = payload[0x6:] + sensors = [ + bytearray(sensor_data[i * 83:(i + 1) * 83]) + for i in range(len(sensor_data) // 83) + ] + return { 'count': count, - 'sensors': sens_res + 'sensors': [ + { + 'status': sensor[0], + 'name': sensor[4:26].decode().strip('\x00'), + 'type': self._SENSORS_TYPES.get(sensor[3], 'Unknown'), + 'order': sensor[1], + 'serial': sensor[26:30].hex(), + } + for sensor in sensors + if any(sensor[26:30]) + ] } - return result diff --git a/broadlink/climate.py b/broadlink/climate.py index 3d68eb4b..f0c337e5 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -1,3 +1,4 @@ +"""Support for climate control.""" from typing import List from .device import device diff --git a/broadlink/cover.py b/broadlink/cover.py index b6b3be17..236e747c 100644 --- a/broadlink/cover.py +++ b/broadlink/cover.py @@ -1,3 +1,4 @@ +"""Support for covers.""" import time from .device import device diff --git a/broadlink/device.py b/broadlink/device.py index e1ad59d7..0c2021a6 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -1,3 +1,4 @@ +"""Support for Broadlink devices.""" import socket import threading import random @@ -20,13 +21,13 @@ def scan( discover_ip_port: int = 80, ) -> Generator[HelloResponse, None, None]: """Broadcast a hello message and yield responses.""" - cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + conn.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) if local_ip_address: - cs.bind((local_ip_address, 0)) - port = cs.getsockname()[1] + conn.bind((local_ip_address, 0)) + port = conn.getsockname()[1] else: local_ip_address = "0.0.0.0" port = 0 @@ -71,13 +72,13 @@ def scan( packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 - cs.sendto(packet, (discover_ip_address, discover_ip_port)) + conn.sendto(packet, (discover_ip_address, discover_ip_port)) try: while (time.time() - starttime) < timeout: - cs.settimeout(timeout - (time.time() - starttime)) + conn.settimeout(timeout - (time.time() - starttime)) try: - response, host = cs.recvfrom(1024) + response, host = conn.recvfrom(1024) except socket.timeout: break @@ -87,26 +88,26 @@ def scan( is_locked = bool(response[-1]) yield devtype, host, mac, name, is_locked finally: - cs.close() + conn.close() class device: """Controls a Broadlink device.""" def __init__( - self, - host: Tuple[str, int], - mac: Union[bytes, str], - devtype: int, - timeout: int = 10, - name: str = None, - model: str = None, - manufacturer: str = None, - is_locked: bool = None, + self, + host: Tuple[str, int], + mac: Union[bytes, str], + devtype: int, + timeout: int = 10, + name: str = None, + model: str = None, + manufacturer: str = None, + is_locked: bool = None, ) -> None: """Initialize the controller.""" self.host = host - self.mac = mac.encode() if isinstance(mac, str) else mac + self.mac = bytes.fromhex(mac) if isinstance(mac, str) else mac self.devtype = devtype if devtype is not None else 0x272a self.timeout = timeout self.name = name @@ -114,17 +115,28 @@ def __init__( self.manufacturer = manufacturer self.is_locked = is_locked self.count = random.randrange(0xffff) - self.iv = bytes( - [0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) + self.iv = bytes.fromhex('562e17996d093d28ddb3ba695a2e6f58') self.id = bytes(4) self.type = "Unknown" self.lock = threading.Lock() self.aes = None - key = bytes( - [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) + key = bytes.fromhex('097628343fe99e23765c1513accf8b02') self.update_aes(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), + self.name, + "Locked" if self.is_locked else "Unlocked", + ) + def update_aes(self, key: bytes) -> None: """Update AES.""" self.aes = Cipher( @@ -284,20 +296,20 @@ def send_packet(self, command: int, payload: bytes) -> bytes: start_time = time.time() with self.lock: - cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) while True: try: - cs.sendto(packet, self.host) - cs.settimeout(1) - resp, _ = cs.recvfrom(2048) + conn.sendto(packet, self.host) + conn.settimeout(1) + resp, _ = conn.recvfrom(2048) break except socket.timeout: if (time.time() - start_time) > self.timeout: - cs.close() + conn.close() raise exception(-4000) # Network timeout. - cs.close() + conn.close() if len(resp) < 0x30: raise exception(-4007) # Length error. diff --git a/broadlink/light.py b/broadlink/light.py index 1cac4a5c..cffc77d5 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -1,3 +1,4 @@ +"""Support for lights.""" import json from typing import Union diff --git a/broadlink/remote.py b/broadlink/remote.py index a08c6758..4a427f69 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -1,3 +1,4 @@ +"""Support for universal remotes.""" from .device import device from .exceptions import check_error @@ -119,15 +120,3 @@ def check_sensors(self) -> dict: 'temperature': data[0x0] + data[0x1] / 100.0, 'humidity': data[0x2] + data[0x3] / 100.0 } - - -# For legacy compatibility - don't use this -class rm2(rm): - def __init__(self): - device.__init__(self, None, None, None) - - def discover(self): - dev = discover() - self.host = dev.host - self.mac = dev.mac - diff --git a/broadlink/sensor.py b/broadlink/sensor.py index 819518b0..63c23b4b 100644 --- a/broadlink/sensor.py +++ b/broadlink/sensor.py @@ -1,3 +1,4 @@ +"""Support for sensors.""" from .device import device from .exceptions import check_error diff --git a/broadlink/switch.py b/broadlink/switch.py index 34614f09..b25d0692 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -1,3 +1,4 @@ +"""Support for switches.""" import json import struct From f8be3806c3be4f6062817f2d1d073959482ab6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 26 Sep 2020 08:23:22 +0200 Subject: [PATCH 153/261] Create python-publish.yml (#432) --- .github/workflows/python-publish.yml | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 00000000..b38aee69 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,30 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* From 0190d1e1f4ddee426b485918e7060b27c3a60c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 26 Sep 2020 08:25:13 +0200 Subject: [PATCH 154/261] Delete python-publish.yml --- .github/workflows/python-publish.yml | 30 ---------------------------- 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index b38aee69..00000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,30 +0,0 @@ -# This workflows will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries -name: Upload Python Package - -on: - release: - types: [created] - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* From 99add9e6feea6e47be4f3a58783556d7838b759c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 26 Sep 2020 08:27:45 +0200 Subject: [PATCH 155/261] 0.15.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ae42b4f2..a246205d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.14.1' +version = '0.15.0' setup( name='broadlink', From 16abdf850f1df370219e44d53f8659f5a9e6189a Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 18 Oct 2020 04:47:21 -0300 Subject: [PATCH 156/261] Improve device discovery (#438) This update improves device discovery by broadcasting several hello messages and ignoring repeated responses. --- broadlink/device.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/broadlink/device.py b/broadlink/device.py index 0c2021a6..4475b8c1 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -32,14 +32,9 @@ def scan( local_ip_address = "0.0.0.0" port = 0 - address = local_ip_address.split('.') - starttime = time.time() - - timezone = int(time.timezone / -3600) packet = bytearray(0x30) - year = datetime.now().year - + timezone = int(time.timezone / -3600) if timezone < 0: packet[0x08] = 0xff + timezone - 1 packet[0x09] = 0xff @@ -51,6 +46,7 @@ def scan( packet[0x0a] = 0 packet[0x0b] = 0 + year = datetime.now().year packet[0x0c] = year & 0xff packet[0x0d] = year >> 8 packet[0x0e] = datetime.now().minute @@ -60,6 +56,8 @@ def scan( 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]) @@ -72,21 +70,29 @@ def scan( packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 - conn.sendto(packet, (discover_ip_address, discover_ip_port)) + starttime = time.time() + discovered = [] try: while (time.time() - starttime) < timeout: - conn.settimeout(timeout - (time.time() - starttime)) - try: - response, host = conn.recvfrom(1024) - except socket.timeout: - break - - devtype = response[0x34] | response[0x35] << 8 - mac = bytes(reversed(response[0x3a:0x40])) - name = response[0x40:].split(b'\x00')[0].decode('utf-8') - is_locked = bool(response[-1]) - yield devtype, host, mac, name, is_locked + conn.sendto(packet, (discover_ip_address, discover_ip_port)) + conn.settimeout(1) + + while True: + try: + response, host = conn.recvfrom(1024) + except socket.timeout: + break + + devtype = response[0x34] | response[0x35] << 8 + mac = bytes(reversed(response[0x3a:0x40])) + 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]) + yield devtype, host, mac, name, is_locked finally: conn.close() From 9d0d76338d9e97ce5d8f146b8b8d724e80c8f188 Mon Sep 17 00:00:00 2001 From: Timothy Stewart Date: Sun, 18 Oct 2020 02:47:43 -0500 Subject: [PATCH 157/261] chore(docs): Added additional steps for 0.15.0 if discovery fails (#439) --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 8faba2be..642ccc81 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,24 @@ 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. + + +Using your machine's IP address with `local_ip_address` +``` +import broadlink + +devices = broadlink.discover(timeout=5, local_ip_address=192.168.0.100) +``` + +Using your subnet's broadcast address with `discover_ip_address` + +``` +import broadlink + +devices = broadlink.discover(timeout=5, discover_ip_address=192.168.0.255) +``` + Obtain the authentication key required for further communication: ``` devices[0].auth() From f11d825e6e66a5645584eaf56d0f151482349150 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 18 Oct 2020 04:48:25 -0300 Subject: [PATCH 158/261] Add support for Broadlink SP mini 3 (0x7d11) (#443) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 7324d37e..2100cb40 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -41,6 +41,7 @@ 0x9479: (sp2, "SP3S-US", "Broadlink"), 0x947a: (sp2, "SP3S-EU", "Broadlink"), 0x7579: (sp4, "SP4L-EU", "Broadlink"), + 0x7d11: (sp4, "SP mini 3", "Broadlink (OEM)"), 0x2712: (rm, "RM pro/pro+", "Broadlink"), 0x272a: (rm, "RM pro", "Broadlink"), 0x2737: (rm, "RM mini 3", "Broadlink"), From a31d766d3349e52937355bff59c5989a4d5dec04 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 18 Oct 2020 04:49:42 -0300 Subject: [PATCH 159/261] Add support for Broadlink SP mini 3 (0x7583) (#444) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 2100cb40..cec9c7d2 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -41,6 +41,7 @@ 0x9479: (sp2, "SP3S-US", "Broadlink"), 0x947a: (sp2, "SP3S-EU", "Broadlink"), 0x7579: (sp4, "SP4L-EU", "Broadlink"), + 0x7583: (sp4, "SP mini 3", "Broadlink (OEM)"), 0x7d11: (sp4, "SP mini 3", "Broadlink (OEM)"), 0x2712: (rm, "RM pro/pro+", "Broadlink"), 0x272a: (rm, "RM pro", "Broadlink"), From e1fa54f874d796878e981182d6810c8c5e7b530e Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 18 Oct 2020 04:50:15 -0300 Subject: [PATCH 160/261] Fix SP4 class (#445) --- broadlink/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/broadlink/switch.py b/broadlink/switch.py index b25d0692..9d975cc7 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -246,11 +246,11 @@ def set_state( """Set state of device.""" data = {} if pwr is not None: - data["pwr"] = pwr + data["pwr"] = int(bool(pwr)) if ntlight is not None: - data["ntlight"] = ntlight + data["ntlight"] = int(bool(ntlight)) if indicator is not None: - data["indicator"] = indicator + data["indicator"] = int(bool(indicator)) if ntlbrightness is not None: data["ntlbrightness"] = ntlbrightness if maxworktime is not None: From 0beccda83327ce3de3b2916fcf03eb518a31a35f Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 18 Oct 2020 04:50:31 -0300 Subject: [PATCH 161/261] Add support for RM mini 3 (0x27c7) (#446) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index cec9c7d2..9351e252 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -59,6 +59,7 @@ 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"), From c8a5ea02fdd89a6b89933b8ff7e467111e2e59ac Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 18 Oct 2020 04:51:43 -0300 Subject: [PATCH 162/261] Add support for Broadlink RM4 mini (0x653a) (#451) --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 9351e252..87e6a2c8 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -76,6 +76,7 @@ 0x62be: (rm4, "RM4C mini", "Broadlink"), 0x648d: (rm4, "RM4 mini", "Broadlink"), 0x649b: (rm4, "RM4 pro", "Broadlink"), + 0x653a: (rm4, "RM4 mini", "Broadlink"), 0x2714: (a1, "e-Sensor", "Broadlink"), 0x4eb5: (mp1, "MP1-1K4S", "Broadlink"), 0x4ef7: (mp1, "MP1-1K4S", "Broadlink (OEM)"), From 957baf5447823b9bb84fc317bf8315f4b850b528 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 18 Oct 2020 17:22:03 -0300 Subject: [PATCH 163/261] Add support for Broadlink SP4M-US (0x648b) (#447) --- broadlink/__init__.py | 9 +++--- broadlink/switch.py | 69 +++++++++++++++++++++++++++---------------- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 87e6a2c8..97a1fcef 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -11,7 +11,7 @@ from .light import lb1 from .remote import rm, rm4 from .sensor import a1 -from .switch import bg1, mp1, sp1, sp2, sp4 +from .switch import bg1, mp1, sp1, sp2, sp4l, sp4m SUPPORTED_TYPES = { @@ -40,9 +40,10 @@ 0x7d0d: (sp2, "SP mini 3", "Broadlink (OEM)"), 0x9479: (sp2, "SP3S-US", "Broadlink"), 0x947a: (sp2, "SP3S-EU", "Broadlink"), - 0x7579: (sp4, "SP4L-EU", "Broadlink"), - 0x7583: (sp4, "SP mini 3", "Broadlink (OEM)"), - 0x7d11: (sp4, "SP mini 3", "Broadlink (OEM)"), + 0x648b: (sp4m, "SP4M-US", "Broadlink"), + 0x7579: (sp4l, "SP4L-EU", "Broadlink"), + 0x7583: (sp4l, "SP mini 3", "Broadlink"), + 0x7d11: (sp4l, "SP mini 3", "Broadlink"), 0x2712: (rm, "RM pro/pro+", "Broadlink"), 0x272a: (rm, "RM pro", "Broadlink"), 0x2737: (rm, "RM mini 3", "Broadlink"), diff --git a/broadlink/switch.py b/broadlink/switch.py index 9d975cc7..75521876 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -219,13 +219,13 @@ def get_energy(self) -> int: return int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0 -class sp4(device): - """Controls a Broadlink SP4.""" +class sp4l(device): + """Controls a Broadlink SP4L.""" def __init__(self, *args, **kwargs) -> None: """Initialize the controller.""" device.__init__(self, *args, **kwargs) - self.type = "SP4" + self.type = "SP4L" def set_power(self, state: bool) -> None: """Set the power state of the device.""" @@ -242,6 +242,7 @@ def set_state( indicator: bool = None, ntlbrightness: int = None, maxworktime: int = None, + childlock: bool = None, ) -> dict: """Set state of device.""" data = {} @@ -255,9 +256,10 @@ def set_state( data["ntlbrightness"] = ntlbrightness if maxworktime is not None: data["maxworktime"] = maxworktime + if childlock is not None: + data["childlock"] = int(bool(childlock)) - js = json.dumps(data).encode("utf8") - packet = self._encode(2, js) + packet = self._encode(2, data) response = self.send_packet(0x6A, packet) return self._decode(response) @@ -267,36 +269,25 @@ def check_power(self) -> bool: return state["pwr"] def check_nightlight(self) -> bool: - """Return the night light state of the device.""" + """Return the state of the night light.""" state = self.get_state() return state["ntlight"] def get_state(self) -> dict: """Get full state of device.""" - packet = self._encode(1, b"{}") + packet = self._encode(1, {}) response = self.send_packet(0x6A, packet) return self._decode(response) - def _encode(self, flag: int, js: str) -> bytes: + def _encode(self, flag: int, state: dict) -> bytes: """Encode a message.""" - # SP4 support added by Petter Olofsson - # packet format is: - # 0x00-0x03 header 0xa5a5, 0x5a5a - # 0x04-0x05 "0xbeaf" checksum - # 0x06 flag (1 for read or 2 write?) - # 0x07 unknown (0xb) - # 0x08-0x0b length of json - # 0x0c- json data - packet = bytearray(14) + payload = json.dumps(state, separators=(",", ":")).encode() + packet = bytearray(12) struct.pack_into( - "> 8 return packet @@ -304,8 +295,36 @@ def _encode(self, flag: int, js: str) -> bytes: def _decode(self, response: bytes) -> dict: """Decode a message.""" check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) js_len = struct.unpack_from(" None: + """Initialize the controller.""" + device.__init__(self, *args, **kwargs) + self.type = "SP4M" + + 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) + struct.pack_into('> 8 + return packet + + def _decode(self, response: bytes) -> dict: + """Decode a message.""" + check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + js_len = struct.unpack_from(" Date: Sun, 18 Oct 2020 22:31:56 +0200 Subject: [PATCH 164/261] Update protocol.md (#436) Adjust documentation on transmit format * Trailer is actually 0x00 0x0d 0x05 (ie the extended pulse length format) * Trailer doesn't have to be this value on transmit This is based on work in https://github.com/elupus/irgen, but is just a qualified guess that seem to match reality. --- protocol.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/protocol.md b/protocol.md index 2e388d74..e2825640 100644 --- a/protocol.md +++ b/protocol.md @@ -181,11 +181,13 @@ Send the following payload with a command byte of 0x006a |0x05|repeat count, (0 = no repeat, 1 send twice, .....)| |0x06-0x07|Length of the following data in little endian| |0x08 ....|Pulse lengths in 2^-15 s units (µs * 269 / 8192 works very well)| -|....|0x0d 0x05 at the end for IR only| +|....|For IR codes, the pulse lengths should be paired as ON, OFF| Each value is represented by one byte. If the length exceeds one byte then it is stored big endian with a leading 0. +Captures of IR codes from the device will always end with a constant OFF value of `0x00 0x0d 0x05` but the trailing silence can be anything on transmit. The likely reason for this value is a capped timeout value on detection. The value is about 102 milliseconds. + Example: The header for my Optoma projector is 8920 4450 8920 * 269 / 8192 = 0x124 4450 * 269 / 8192 = 0x92 From 1c5ba74f46e386700b4b82a8b3ea6c9c188da753 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Wed, 21 Oct 2020 14:32:32 -0300 Subject: [PATCH 165/261] Fix shebangs --- broadlink/__init__.py | 2 +- cli/broadlink_cli | 1 - cli/broadlink_discovery | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 97a1fcef..2b1e0d13 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 """The python-broadlink library.""" import socket from typing import Generator, List, Union, Tuple diff --git a/cli/broadlink_cli b/cli/broadlink_cli index a2e18833..31bbb2c6 100755 --- a/cli/broadlink_cli +++ b/cli/broadlink_cli @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - import argparse import base64 import codecs diff --git a/cli/broadlink_discovery b/cli/broadlink_discovery index 8ff02ac5..c098c91e 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -1,5 +1,4 @@ -#!/usr/bin/env python - +#!/usr/bin/env python3 import argparse import broadlink From 6f1f976e2fe5b4497734b01c7ad0c1cf8ff819a0 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Wed, 21 Oct 2020 01:05:45 -0300 Subject: [PATCH 166/261] Update packet count in the authentication step --- broadlink/device.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/device.py b/broadlink/device.py index 4475b8c1..07af618b 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -195,6 +195,7 @@ def auth(self) -> bool: if len(key) % 16 != 0: return False + self.count = int.from_bytes(response[0x28:0x30], "little") self.id = payload[0x03::-1] self.update_aes(key) return True From 79f4ad47fffc85438fb7e39e7b46724b82c88587 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 22 Oct 2020 15:54:30 -0700 Subject: [PATCH 167/261] Readme tweaks --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 642ccc81..8d00fc93 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A simple Python API for controlling IR/RF controllers from [Broadlink](http://ww * A1 sensor platform devices are supported * RM3 mini IR blaster * RM4 and RM4C mini blasters +- SP2/SP3/SP4 smart plugs There is currently no support for the cloud API. @@ -104,7 +105,7 @@ Obtain sensor data from an A1: data = devices[0].check_sensors() ``` -Set power state on a SmartPlug SP2/SP3: +Set power state on a SmartPlug SP2/SP3/SP4: ``` devices[0].set_power(True) ``` From 1d81ebd1d0bea98bc3966e25aab295a117972175 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Tue, 3 Nov 2020 23:15:34 -0300 Subject: [PATCH 168/261] Rename SP4L to SP4 and SP4M to SP4B --- broadlink/__init__.py | 11 ++++++----- broadlink/switch.py | 12 ++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 2b1e0d13..604ed88a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -11,7 +11,7 @@ from .light import lb1 from .remote import rm, rm4 from .sensor import a1 -from .switch import bg1, mp1, sp1, sp2, sp4l, sp4m +from .switch import bg1, mp1, sp1, sp2, sp4, sp4b SUPPORTED_TYPES = { @@ -40,10 +40,11 @@ 0x7d0d: (sp2, "SP mini 3", "Broadlink (OEM)"), 0x9479: (sp2, "SP3S-US", "Broadlink"), 0x947a: (sp2, "SP3S-EU", "Broadlink"), - 0x648b: (sp4m, "SP4M-US", "Broadlink"), - 0x7579: (sp4l, "SP4L-EU", "Broadlink"), - 0x7583: (sp4l, "SP mini 3", "Broadlink"), - 0x7d11: (sp4l, "SP mini 3", "Broadlink"), + 0x756c: (sp4, "SP4M", "Broadlink"), + 0x7579: (sp4, "SP4L-EU", "Broadlink"), + 0x7583: (sp4, "SP mini 3", "Broadlink"), + 0x7d11: (sp4, "SP mini 3", "Broadlink"), + 0x648b: (sp4b, "SP4M-US", "Broadlink"), 0x2712: (rm, "RM pro/pro+", "Broadlink"), 0x272a: (rm, "RM pro", "Broadlink"), 0x2737: (rm, "RM mini 3", "Broadlink"), diff --git a/broadlink/switch.py b/broadlink/switch.py index 75521876..efea03d7 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -219,13 +219,13 @@ def get_energy(self) -> int: return int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0 -class sp4l(device): - """Controls a Broadlink SP4L.""" +class sp4(device): + """Controls a Broadlink SP4.""" def __init__(self, *args, **kwargs) -> None: """Initialize the controller.""" device.__init__(self, *args, **kwargs) - self.type = "SP4L" + self.type = "SP4" def set_power(self, state: bool) -> None: """Set the power state of the device.""" @@ -301,13 +301,13 @@ def _decode(self, response: bytes) -> dict: return state -class sp4m(sp4l): - """Controls a Broadlink SP4M.""" +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 = "SP4M" + self.type = "SP4B" def _encode(self, flag: int, state: dict) -> bytes: """Encode a message.""" From 73dab5c3d5d7c679b221c9d1e05b1dc699bc84b5 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 5 Nov 2020 16:19:03 -0300 Subject: [PATCH 169/261] Overload __str__ in the device class (#468) --- broadlink/device.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/broadlink/device.py b/broadlink/device.py index 07af618b..9392024f 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -143,6 +143,13 @@ def __repr__(self): "Locked" if self.is_locked else "Unlocked", ) + def __str__(self): + return "%s (%s at %s)" % ( + self.name, + self.model or hex(self.devtype), + self.host[0], + ) + def update_aes(self, key: bytes) -> None: """Update AES.""" self.aes = Cipher( From b3542a8865b8b218cec6791d3a427a57b4f63318 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Thu, 5 Nov 2020 19:27:12 -0800 Subject: [PATCH 170/261] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a246205d..81827bc0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages -version = '0.15.0' +version = '0.16.0' setup( name='broadlink', From dfd3372c368163f073beec26abe18e68a517b72a Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 5 Nov 2020 11:00:42 -0800 Subject: [PATCH 171/261] Add GH Actions (#470) Add GitHub actions and run black. --- .github/workflows/flake8.yaml | 32 +++++++ broadlink/__init__.py | 147 ++++++++++++++++---------------- broadlink/alarm.py | 28 +++---- broadlink/climate.py | 124 +++++++++++++++++---------- broadlink/cover.py | 8 +- broadlink/device.py | 154 +++++++++++++++++----------------- broadlink/light.py | 54 ++++++------ broadlink/remote.py | 36 ++++---- broadlink/sensor.py | 20 ++--- broadlink/switch.py | 114 ++++++++++++++----------- setup.py | 22 ++--- 11 files changed, 410 insertions(+), 329 deletions(-) create mode 100644 .github/workflows/flake8.yaml diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml new file mode 100644 index 00000000..2fd67945 --- /dev/null +++ b/.github/workflows/flake8.yaml @@ -0,0 +1,32 @@ +name: Python flake8 + +on: + push: + branches: [ main, master, dev, development ] + pull_request: + branches: [ main, master, dev, development ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 wemake-python-styleguide + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. ignore magic numbers and use double quotes and ignore numbers with zeroes before them. + # and ignore lowercase hex numbers and ignore isort incorrect imports + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=90 --ignore=WPS432,WPS339,WPS341,I --inline-quotes double --statistics diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 604ed88a..74ae1fa6 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -20,88 +20,88 @@ 0x2716: (sp2, "NEO PRO", "Ankuoo"), 0x2717: (sp2, "NEO", "Ankuoo"), 0x2719: (sp2, "SP2-compatible", "Honeywell"), - 0x271a: (sp2, "SP2-compatible", "Honeywell"), + 0x271A: (sp2, "SP2-compatible", "Honeywell"), 0x2720: (sp2, "SP mini", "Broadlink"), 0x2728: (sp2, "SP2-compatible", "URANT"), 0x2733: (sp2, "SP3", "Broadlink"), 0x2736: (sp2, "SP mini+", "Broadlink"), - 0x273e: (sp2, "SP mini", "Broadlink"), + 0x273E: (sp2, "SP mini", "Broadlink"), 0x7530: (sp2, "SP2", "Broadlink (OEM)"), 0x7539: (sp2, "SP2-IL", "Broadlink (OEM)"), - 0x753e: (sp2, "SP mini 3", "Broadlink"), + 0x753E: (sp2, "SP mini 3", "Broadlink"), 0x7540: (sp2, "MP2", "Broadlink"), - 0X7544: (sp2, "SP2-CL", "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"), - 0x7d00: (sp2, "SP3-EU", "Broadlink (OEM)"), - 0x7d0d: (sp2, "SP mini 3", "Broadlink (OEM)"), + 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"), - 0x756c: (sp4, "SP4M", "Broadlink"), + 0x947A: (sp2, "SP3S-EU", "Broadlink"), + 0x756C: (sp4, "SP4M", "Broadlink"), 0x7579: (sp4, "SP4L-EU", "Broadlink"), 0x7583: (sp4, "SP mini 3", "Broadlink"), - 0x7d11: (sp4, "SP mini 3", "Broadlink"), - 0x648b: (sp4b, "SP4M-US", "Broadlink"), + 0x7D11: (sp4, "SP mini 3", "Broadlink"), + 0x648B: (sp4b, "SP4M-US", "Broadlink"), 0x2712: (rm, "RM pro/pro+", "Broadlink"), - 0x272a: (rm, "RM pro", "Broadlink"), + 0x272A: (rm, "RM pro", "Broadlink"), 0x2737: (rm, "RM mini 3", "Broadlink"), - 0x273d: (rm, "RM pro", "Broadlink"), - 0x277c: (rm, "RM home", "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"), + 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"), - 0x27de: (rm, "RM mini 3", "Broadlink"), - 0x51da: (rm4, "RM4 mini", "Broadlink"), - 0x5f36: (rm4, "RM mini 3", "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"), + 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"), - 0x648d: (rm4, "RM4 mini", "Broadlink"), - 0x649b: (rm4, "RM4 pro", "Broadlink"), - 0x653a: (rm4, "RM4 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"), + 0x648D: (rm4, "RM4 mini", "Broadlink"), + 0x649B: (rm4, "RM4 pro", "Broadlink"), + 0x653A: (rm4, "RM4 mini", "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"), + 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"), + 0x504E: (lb1, "LB1", "Broadlink"), + 0x60C7: (lb1, "LB1", "Broadlink"), + 0x60C8: (lb1, "LB1", "Broadlink"), 0x6112: (lb1, "LB1", "Broadlink"), 0x2722: (S1C, "S2KIT", "Broadlink"), - 0x4ead: (hysen, "HY02B05H", "Hysen"), - 0x4e4d: (dooya, "DT360E-45/20", "Dooya"), - 0x51e3: (bg1, "BG800/BG900", "BG Electrical"), + 0x4EAD: (hysen, "HY02B05H", "Hysen"), + 0x4E4D: (dooya, "DT360E-45/20", "Dooya"), + 0x51E3: (bg1, "BG800/BG900", "BG Electrical"), } def gendevice( - dev_type: int, - host: Tuple[str, int], - mac: Union[bytes, str], - name: str = None, - is_locked: bool = None, + dev_type: int, + host: Tuple[str, int], + mac: Union[bytes, str], + name: str = None, + is_locked: bool = None, ) -> device: """Generate a device.""" try: @@ -122,10 +122,10 @@ def gendevice( def hello( - host: str, - port: int = 80, - timeout: int = 10, - local_ip_address: str = None, + host: str, + port: int = 80, + timeout: int = 10, + local_ip_address: str = None, ) -> device: """Direct device discovery. @@ -138,31 +138,27 @@ def hello( def discover( - timeout: int = 10, - local_ip_address: str = None, - discover_ip_address: str = '255.255.255.255', - discover_ip_port: int = 80, + timeout: int = 10, + local_ip_address: str = None, + discover_ip_address: str = "255.255.255.255", + discover_ip_port: int = 80, ) -> 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] def xdiscover( - timeout: int = 10, - local_ip_address: str = None, - discover_ip_address: str = '255.255.255.255', - discover_ip_port: int = 80, + timeout: int = 10, + local_ip_address: str = None, + discover_ip_address: str = "255.255.255.255", + discover_ip_port: int = 80, ) -> Generator[device, None, None]: """Discover devices connected to the local network. 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) @@ -191,13 +187,12 @@ def setup(ssid: str, password: str, security_mode: int) -> None: payload[0x85] = pass_length # Character length of password payload[0x86] = security_mode # Type of encryption - checksum = sum(payload, 0xbeaf) & 0xffff - payload[0x20] = checksum & 0xff # Checksum 1 position + checksum = sum(payload, 0xBEAF) & 0xFFFF + payload[0x20] = checksum & 0xFF # Checksum 1 position payload[0x21] = checksum >> 8 # Checksum 2 position - sock = socket.socket(socket.AF_INET, # Internet - socket.SOCK_DGRAM) # UDP + 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, ("255.255.255.255", 80)) sock.close() diff --git a/broadlink/alarm.py b/broadlink/alarm.py index faded9d4..e73b8fad 100644 --- a/broadlink/alarm.py +++ b/broadlink/alarm.py @@ -7,21 +7,21 @@ class S1C(device): """Controls a Broadlink 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", # 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' + self.type = "S1C" 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 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) if not payload: @@ -29,20 +29,20 @@ def get_sensors_status(self) -> dict: count = payload[0x4] sensor_data = payload[0x6:] sensors = [ - bytearray(sensor_data[i * 83:(i + 1) * 83]) + bytearray(sensor_data[i * 83 : (i + 1) * 83]) for i in range(len(sensor_data) // 83) ] return { - 'count': count, - 'sensors': [ + "count": count, + "sensors": [ { - 'status': sensor[0], - 'name': sensor[4:26].decode().strip('\x00'), - 'type': self._SENSORS_TYPES.get(sensor[3], 'Unknown'), - 'order': sensor[1], - 'serial': sensor[26:30].hex(), + "status": sensor[0], + "name": sensor[4:26].decode().strip("\x00"), + "type": self._SENSORS_TYPES.get(sensor[3], "Unknown"), + "order": sensor[1], + "serial": sensor[26:30].hex(), } for sensor in sensors if any(sensor[26:30]) - ] + ], } diff --git a/broadlink/climate.py b/broadlink/climate.py index f0c337e5..036cc9a8 100644 --- a/broadlink/climate.py +++ b/broadlink/climate.py @@ -33,19 +33,22 @@ def send_request(self, input_payload: bytes) -> bytes: request_payload.append((crc >> 8) & 0xFF) # send to device - response = self.send_packet(0x6a, request_payload) + response = self.send_packet(0x6A, request_payload) 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) response_payload_len = response_payload[0] if response_payload_len + 2 > len(response_payload): - raise ValueError('hysen_response_error', 'first byte of response is not length') + raise ValueError( + "hysen_response_error", "first byte of response is not length" + ) crc = calculate_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): + 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') + raise ValueError("hysen_response_error", "CRC check on response failed") def get_temp(self) -> int: """Return the room temperature in degrees celsius.""" @@ -64,43 +67,53 @@ def get_full_status(self) -> dict: """ payload = self.send_request(bytearray([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['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['fre'] = payload[15] - data['poweron'] = payload[16] - data['unknown'] = payload[17] - data['external_temp'] = (payload[18] & 255) / 2.0 - data['hour'] = payload[19] - data['min'] = payload[20] - data['sec'] = payload[21] - data['dayofweek'] = payload[22] + 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["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["fre"] = payload[15] + data["poweron"] = payload[16] + data["unknown"] = payload[17] + data["external_temp"] = (payload[18] & 255) / 2.0 + data["hour"] = payload[19] + data["min"] = payload[20] + data["sec"] = payload[21] + data["dayofweek"] = payload[22] weekday = [] for i in range(0, 6): weekday.append( - {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0}) - - data['weekday'] = weekday + { + "start_hour": payload[2 * i + 23], + "start_minute": payload[2 * i + 24], + "temp": payload[i + 39] / 2.0, + } + ) + + data["weekday"] = weekday weekend = [] for i in range(6, 8): weekend.append( - {'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0}) - - data['weekend'] = weekend + { + "start_hour": payload[2 * i + 23], + "start_minute": payload[2 * i + 24], + "temp": payload[i + 39] / 2.0, + } + ) + + data["weekend"] = weekend return data # Change controller mode @@ -140,8 +153,27 @@ def set_advanced( poweron: int, ) -> None: """Set advanced options.""" - input_payload = bytearray([0x01, 0x10, 0x00, 0x02, 0x00, 0x05, 0x0a, loop_mode, sensor, osv, dif, svh, svl, - (int(adj * 2) >> 8 & 0xff), (int(adj * 2) & 0xff), fre, poweron]) + input_payload = bytearray( + [ + 0x01, + 0x10, + 0x00, + 0x02, + 0x00, + 0x05, + 0x0A, + loop_mode, + sensor, + osv, + dif, + svh, + svl, + (int(adj * 2) >> 8 & 0xFF), + (int(adj * 2) & 0xFF), + fre, + poweron, + ] + ) self.send_request(input_payload) # For backwards compatibility only. Prefer calling set_mode directly. @@ -169,7 +201,11 @@ def set_power(self, power: int = 1, remote_lock: int = 0) -> None: # 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])) + self.send_request( + bytearray( + [0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day] + ) + ) # Set timer schedule # Format is the same as you get from get_full_status. @@ -180,25 +216,25 @@ def set_time(self, hour: int, minute: int, second: int, day: int) -> None: def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None: """Set timer schedule.""" # Begin with some magic values ... - input_payload = bytearray([0x01, 0x10, 0x00, 0x0a, 0x00, 0x0c, 0x18]) + input_payload = bytearray([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']) + input_payload.append(weekday[i]["start_hour"]) + input_payload.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']) + input_payload.append(weekend[i]["start_hour"]) + input_payload.append(weekend[i]["start_minute"]) # weekday temperatures for i in range(0, 6): - input_payload.append(int(weekday[i]['temp'] * 2)) + input_payload.append(int(weekday[i]["temp"] * 2)) # weekend temperatures for i in range(0, 2): - input_payload.append(int(weekend[i]['temp'] * 2)) + input_payload.append(int(weekend[i]["temp"] * 2)) self.send_request(input_payload) diff --git a/broadlink/cover.py b/broadlink/cover.py index 236e747c..2691fe97 100644 --- a/broadlink/cover.py +++ b/broadlink/cover.py @@ -17,12 +17,12 @@ def _send(self, magic1: int, magic2: int) -> int: """Send a packet to the device.""" packet = bytearray(16) packet[0] = 0x09 - packet[2] = 0xbb + packet[2] = 0xBB packet[3] = magic1 packet[4] = magic2 - packet[9] = 0xfa + packet[9] = 0xFA packet[10] = 0x44 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return payload[4] @@ -41,7 +41,7 @@ def stop(self) -> int: def get_percentage(self) -> int: """Return the position of the curtain.""" - return self._send(0x06, 0x5d) + return self._send(0x06, 0x5D) def set_percentage_and_wait(self, new_percentage: int) -> None: """Set the position of the curtain.""" diff --git a/broadlink/device.py b/broadlink/device.py index 9392024f..e040de79 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -15,10 +15,10 @@ def scan( - timeout: int = 10, - local_ip_address: str = None, - discover_ip_address: str = '255.255.255.255', - discover_ip_port: int = 80, + timeout: int = 10, + local_ip_address: str = None, + discover_ip_address: str = "255.255.255.255", + discover_ip_port: int = 80, ) -> Generator[HelloResponse, None, None]: """Broadcast a hello message and yield responses.""" conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -36,38 +36,38 @@ def scan( timezone = int(time.timezone / -3600) if timezone < 0: - packet[0x08] = 0xff + timezone - 1 - packet[0x09] = 0xff - packet[0x0a] = 0xff - packet[0x0b] = 0xff + 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 + 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 + 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('.') + 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[0x1A] = int(address[1]) + packet[0x1B] = int(address[0]) + packet[0x1C] = port & 0xFF + packet[0x1D] = port >> 8 packet[0x26] = 6 - checksum = sum(packet, 0xbeaf) & 0xffff - packet[0x20] = checksum & 0xff + checksum = sum(packet, 0xBEAF) & 0xFFFF + packet[0x20] = checksum & 0xFF packet[0x21] = checksum >> 8 starttime = time.time() @@ -85,12 +85,12 @@ def scan( break devtype = response[0x34] | response[0x35] << 8 - mac = bytes(reversed(response[0x3a:0x40])) + mac = bytes(reversed(response[0x3A:0x40])) if (host, mac, devtype) in discovered: continue discovered.append((host, mac, devtype)) - name = response[0x40:].split(b'\x00')[0].decode('utf-8') + name = response[0x40:].split(b"\x00")[0].decode("utf-8") is_locked = bool(response[-1]) yield devtype, host, mac, name, is_locked finally: @@ -101,33 +101,33 @@ class device: """Controls a Broadlink device.""" def __init__( - self, - host: Tuple[str, int], - mac: Union[bytes, str], - devtype: int, - timeout: int = 10, - name: str = None, - model: str = None, - manufacturer: str = None, - is_locked: bool = None, + self, + host: Tuple[str, int], + mac: Union[bytes, str], + devtype: int, + timeout: int = 10, + name: str = None, + model: str = None, + manufacturer: str = None, + is_locked: bool = None, ) -> 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 if devtype is not None else 0x272A self.timeout = timeout self.name = name self.model = model self.manufacturer = manufacturer self.is_locked = is_locked - self.count = random.randrange(0xffff) - self.iv = bytes.fromhex('562e17996d093d28ddb3ba695a2e6f58') + self.count = random.randrange(0xFFFF) + self.iv = bytes.fromhex("562e17996d093d28ddb3ba695a2e6f58") self.id = bytes(4) self.type = "Unknown" self.lock = threading.Lock() self.aes = None - key = bytes.fromhex('097628343fe99e23765c1513accf8b02') + key = bytes.fromhex("097628343fe99e23765c1513accf8b02") self.update_aes(key) def __repr__(self): @@ -138,7 +138,7 @@ def __repr__(self): hex(self.devtype), self.host[0], self.host[1], - ':'.join(format(x, '02x') for x in self.mac), + ":".join(format(x, "02x") for x in self.mac), self.name, "Locked" if self.is_locked else "Unlocked", ) @@ -175,24 +175,24 @@ def auth(self) -> bool: 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[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[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[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") response = self.send_packet(0x65, payload) check_error(response[0x22:0x24]) @@ -233,7 +233,7 @@ def hello(self, local_ip_address=None) -> bool: def get_fwversion(self) -> int: """Get firmware version.""" packet = bytearray([0x68]) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return payload[0x4] | payload[0x5] << 8 @@ -241,20 +241,20 @@ def get_fwversion(self) -> int: def set_name(self, name: str) -> None: """Set device name.""" packet = bytearray(4) - packet += name.encode('utf-8') + packet += name.encode("utf-8") packet += bytearray(0x50 - len(packet)) packet[0x43] = bool(self.is_locked) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) self.name = name def set_lock(self, state: bool) -> None: """Lock/unlock the device.""" packet = bytearray(4) - packet += self.name.encode('utf-8') + packet += self.name.encode("utf-8") packet += bytearray(0x50 - len(packet)) packet[0x43] = bool(state) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) self.is_locked = bool(state) @@ -264,27 +264,27 @@ def get_type(self) -> str: def send_packet(self, command: int, payload: bytes) -> bytes: """Send a packet to the device.""" - self.count = (self.count + 1) & 0xffff + self.count = (self.count + 1) & 0xFFFF packet = bytearray(0x38) - packet[0x00] = 0x5a - packet[0x01] = 0xa5 - packet[0x02] = 0xaa + packet[0x00] = 0x5A + packet[0x01] = 0xA5 + packet[0x02] = 0xAA packet[0x03] = 0x55 - packet[0x04] = 0x5a - packet[0x05] = 0xa5 - packet[0x06] = 0xaa + packet[0x04] = 0x5A + packet[0x05] = 0xA5 + packet[0x06] = 0xAA packet[0x07] = 0x55 - packet[0x24] = self.devtype & 0xff + packet[0x24] = self.devtype & 0xFF packet[0x25] = self.devtype >> 8 packet[0x26] = command - packet[0x28] = self.count & 0xff + 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[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] @@ -296,16 +296,16 @@ def send_packet(self, command: int, payload: bytes) -> bytes: payload = bytearray(payload) payload += bytearray(padding) - checksum = sum(payload, 0xbeaf) & 0xffff - packet[0x34] = checksum & 0xff + checksum = sum(payload, 0xBEAF) & 0xFFFF + packet[0x34] = checksum & 0xFF packet[0x35] = checksum >> 8 payload = self.encrypt(payload) for i in range(len(payload)): packet.append(payload[i]) - checksum = sum(packet, 0xbeaf) & 0xffff - packet[0x20] = checksum & 0xff + checksum = sum(packet, 0xBEAF) & 0xFFFF + packet[0x20] = checksum & 0xFF packet[0x21] = checksum >> 8 start_time = time.time() @@ -329,7 +329,7 @@ def send_packet(self, command: int, payload: bytes) -> bytes: raise exception(-4007) # Length error. checksum = resp[0x20] | (resp[0x21] << 8) - if sum(resp, 0xbeaf) - sum(resp[0x20:0x22]) & 0xffff != checksum: + if sum(resp, 0xBEAF) - sum(resp[0x20:0x22]) & 0xFFFF != checksum: raise exception(-4008) # Checksum error. return resp diff --git a/broadlink/light.py b/broadlink/light.py index cffc77d5..adadfab5 100644 --- a/broadlink/light.py +++ b/broadlink/light.py @@ -11,14 +11,14 @@ class lb1(device): 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, + "lovely color": 0, + "flashlight": 1, + "lightning": 2, + "color fading": 3, + "color breathing": 4, + "multicolor breathing": 5, + "color jumping": 6, + "multicolor jumping": 7, } def __init__(self, *args, **kwargs) -> None: @@ -26,36 +26,38 @@ def __init__(self, *args, **kwargs) -> None: device.__init__(self, *args, **kwargs) self.type = "SmartBulb" - def send_command(self, command: str, type: str = 'set') -> None: + 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) + 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 + checksum = sum(packet, 0xBEAF) & 0xFFFF + packet[0x06] = checksum & 0xFF # Checksum 1 position packet[0x07] = checksum >> 8 # Checksum 2 position - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x36:0x38]) payload = self.decrypt(response[0x38:]) - responseLength = int(payload[0x0a]) | (int(payload[0x0b]) << 8) + responseLength = int(payload[0x0A]) | (int(payload[0x0B]) << 8) if responseLength > 0: - self.state_dict = json.loads(payload[0x0e:0x0e+responseLength]) + self.state_dict = json.loads(payload[0x0E : 0x0E + responseLength]) 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) + if "bulb_sceneidx" in reconvert.keys(): + reconvert["bulb_sceneidx"] = self.effect_map_dict.get( + reconvert["bulb_sceneidx"], 255 + ) self.send_command(json.dumps(reconvert)) return json.dumps(self.state_dict) diff --git a/broadlink/remote.py b/broadlink/remote.py index 4a427f69..b6528d68 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -17,45 +17,45 @@ def check_data(self) -> bytes: """Return the last captured code.""" packet = bytearray(self._request_header) packet.append(0x04) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return payload[len(self._request_header) + 4:] + return payload[len(self._request_header) + 4 :] def send_data(self, data: bytes) -> None: """Send a code to the device.""" packet = bytearray(self._code_sending_header) packet += bytearray([0x02, 0x00, 0x00, 0x00]) packet += data - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def enter_learning(self) -> None: """Enter infrared learning mode.""" packet = bytearray(self._request_header) packet.append(0x03) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def sweep_frequency(self) -> None: """Sweep frequency.""" packet = bytearray(self._request_header) packet.append(0x19) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def cancel_sweep_frequency(self) -> None: """Cancel sweep frequency.""" packet = bytearray(self._request_header) - packet.append(0x1e) - response = self.send_packet(0x6a, packet) + packet.append(0x1E) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def check_frequency(self) -> bool: """Return True if the frequency was identified successfully.""" packet = bytearray(self._request_header) - packet.append(0x1a) - response = self.send_packet(0x6a, packet) + packet.append(0x1A) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) if payload[len(self._request_header) + 4] == 1: @@ -65,8 +65,8 @@ def check_frequency(self) -> bool: def find_rf_packet(self) -> bool: """Enter radiofrequency learning mode.""" packet = bytearray(self._request_header) - packet.append(0x1b) - response = self.send_packet(0x6a, packet) + packet.append(0x1B) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) if payload[len(self._request_header) + 4] == 1: @@ -77,10 +77,10 @@ def _check_sensors(self, command: int) -> bytes: """Return the state of the sensors in raw format.""" packet = bytearray(self._request_header) packet.append(command) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return bytearray(payload[len(self._request_header) + 4:]) + return bytearray(payload[len(self._request_header) + 4 :]) def check_temperature(self) -> int: """Return the temperature.""" @@ -90,7 +90,7 @@ def check_temperature(self) -> int: def check_sensors(self) -> dict: """Return the state of the sensors.""" data = self._check_sensors(0x1) - return {'temperature': data[0x0] + data[0x1] / 10.0} + return {"temperature": data[0x0] + data[0x1] / 10.0} class rm4(rm): @@ -100,8 +100,8 @@ def __init__(self, *args, **kwargs) -> None: """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "RM4" - self._request_header = b'\x04\x00' - self._code_sending_header = b'\xda\x00' + self._request_header = b"\x04\x00" + self._code_sending_header = b"\xda\x00" def check_temperature(self) -> int: """Return the temperature.""" @@ -117,6 +117,6 @@ def check_sensors(self) -> dict: """Return the state of the sensors.""" data = self._check_sensors(0x24) return { - 'temperature': data[0x0] + data[0x1] / 100.0, - 'humidity': data[0x2] + data[0x3] / 100.0 + "temperature": data[0x0] + data[0x1] / 100.0, + "humidity": data[0x2] + data[0x3] / 100.0, } diff --git a/broadlink/sensor.py b/broadlink/sensor.py index 63c23b4b..ef7f6f12 100644 --- a/broadlink/sensor.py +++ b/broadlink/sensor.py @@ -7,9 +7,9 @@ class a1(device): """Controls a Broadlink A1.""" _SENSORS_AND_LEVELS = ( - ('light', ('dark', 'dim', 'normal', 'bright')), - ('air_quality', ('excellent', 'good', 'normal', 'bad')), - ('noise', ('quiet', 'normal', 'noisy')), + ("light", ("dark", "dim", "normal", "bright")), + ("air_quality", ("excellent", "good", "normal", "bad")), + ("noise", ("quiet", "normal", "noisy")), ) def __init__(self, *args, **kwargs) -> None: @@ -24,20 +24,20 @@ def check_sensors(self) -> dict: try: data[sensor] = levels[data[sensor]] except IndexError: - data[sensor] = 'unknown' + data[sensor] = "unknown" return data def check_sensors_raw(self) -> dict: """Return the state of the sensors in raw format.""" packet = bytearray([0x1]) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) data = bytearray(payload[0x4:]) return { - 'temperature': data[0x0] + data[0x1] / 10.0, - 'humidity': data[0x2] + data[0x3] / 10.0, - 'light': data[0x4], - 'air_quality': data[0x6], - 'noise': data[0x8], + "temperature": data[0x0] + data[0x1] / 10.0, + "humidity": data[0x2] + data[0x3] / 10.0, + "light": data[0x4], + "air_quality": data[0x6], + "noise": data[0x8], } diff --git a/broadlink/switch.py b/broadlink/switch.py index efea03d7..96225451 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -17,19 +17,19 @@ def __init__(self, *args, **kwargs) -> None: def set_power_mask(self, sid_mask: int, state: 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 state else sid_mask) - packet[0x07] = 0xc0 + packet[0x00] = 0x0D + packet[0x02] = 0xA5 + packet[0x03] = 0xA5 + packet[0x04] = 0x5A + packet[0x05] = 0x5A + packet[0x06] = 0xB2 + ((sid_mask << 1) if state 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[0x0A] = 0x03 + packet[0x0D] = sid_mask + packet[0x0E] = sid_mask if state else 0 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def set_power(self, sid: int, state: bool) -> None: @@ -40,30 +40,30 @@ def set_power(self, sid: int, state: bool) -> None: def check_power_raw(self) -> bool: """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[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) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return payload[0x0e] + return payload[0x0E] 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} + 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) + data["s1"] = bool(state & 0x01) + data["s2"] = bool(state & 0x02) + data["s3"] = bool(state & 0x04) + data["s4"] = bool(state & 0x08) return data @@ -80,8 +80,8 @@ 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'{}') - response = self.send_packet(0x6a, packet) + packet = self._encode(1, b"{}") + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) return self._decode(response) @@ -98,22 +98,22 @@ def set_state( """Set the power state of the device.""" data = {} if pwr is not None: - data['pwr'] = int(bool(pwr)) + data["pwr"] = int(bool(pwr)) if pwr1 is not None: - data['pwr1'] = int(bool(pwr1)) + data["pwr1"] = int(bool(pwr1)) if pwr2 is not None: - data['pwr2'] = int(bool(pwr2)) + data["pwr2"] = int(bool(pwr2)) if maxworktime is not None: - data['maxworktime'] = maxworktime + data["maxworktime"] = maxworktime if maxworktime1 is not None: - data['maxworktime1'] = maxworktime1 + data["maxworktime1"] = maxworktime1 if maxworktime2 is not None: - data['maxworktime2'] = maxworktime2 + data["maxworktime2"] = maxworktime2 if idcbrightness is not None: - data['idcbrightness'] = idcbrightness - js = json.dumps(data).encode('utf8') + data["idcbrightness"] = idcbrightness + js = json.dumps(data).encode("utf8") packet = self._encode(2, js) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) return self._decode(response) @@ -129,20 +129,22 @@ def _encode(self, flag: int, js: str) -> bytes: # 0x0e- json data packet = bytearray(14) length = 4 + 2 + 2 + 4 + len(js) - struct.pack_into('> 8 return packet def _decode(self, response: bytes) -> dict: """Decode a message.""" payload = self.decrypt(response[0x38:]) - js_len = struct.unpack_from(' None: packet[4] = 3 if state else 2 else: packet[4] = 1 if state else 0 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) def set_nightlight(self, state: bool) -> None: @@ -189,14 +191,14 @@ def set_nightlight(self, state: bool) -> None: packet[4] = 3 if state else 1 else: packet[4] = 2 if state else 0 - response = self.send_packet(0x6a, packet) + 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) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD) @@ -205,7 +207,7 @@ def check_nightlight(self) -> bool: """Return the state of the night light.""" packet = bytearray(16) packet[0] = 1 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) @@ -213,14 +215,17 @@ def check_nightlight(self) -> bool: def get_energy(self) -> int: """Return the energy state of the device.""" packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - return int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0 + return ( + int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + + int(hex(payload[0x05])[2:]) / 100.0 + ) class sp4(device): - """Controls a Broadlink SP4.""" + """Controls a Broadlink SP4.""" def __init__(self, *args, **kwargs) -> None: """Initialize the controller.""" @@ -314,7 +319,18 @@ def _encode(self, flag: int, state: dict) -> bytes: payload = json.dumps(state, separators=(",", ":")).encode() packet = bytearray(14) length = 4 + 2 + 2 + 4 + len(payload) - struct.pack_into('=2.1.1'], - description='Python API for controlling Broadlink IR controllers', + install_requires=["cryptography>=2.1.1"], + description="Python API for controlling Broadlink IR controllers", classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", ], include_package_data=True, zip_safe=False, From 1ad468aefe1e778365bff31a444720762585ba79 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 7 Nov 2020 05:02:53 -0300 Subject: [PATCH 172/261] Clean up get_energy() (#471) Amend: Fix sp3s energy monitoring (#504) --- broadlink/switch.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/broadlink/switch.py b/broadlink/switch.py index 96225451..fe693abd 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -212,16 +212,14 @@ def check_nightlight(self) -> bool: payload = self.decrypt(response[0x38:]) return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF) - def get_energy(self) -> int: - """Return the energy state of the device.""" + 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]) payload = self.decrypt(response[0x38:]) - return ( - int(hex(payload[0x07] * 256 + payload[0x06])[2:]) - + int(hex(payload[0x05])[2:]) / 100.0 - ) + energy = payload[0x7:0x4:-1].hex() + return int(energy) / 100 class sp4(device): From bc6719dc2a125ef6979d017f98fe640a9d959b72 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 16 Nov 2020 17:04:17 -0300 Subject: [PATCH 173/261] Add new product ids Add support for Broadlink RM4C mini (0x6539) (#476) Add support for Broadlink RM4 pro (0x653C) (#472) Add support for Broadlink RM4S (0x6364) (#475) Add support for Broadlink MCB1 (0x756F) (#478) Add support for Broadlink MCB1 (0xA56A) (#478) Add support for Broadlink RM mini 3 (0x6508) (#479) Add support for Efergy Ego (0x271D) (#481) Add support for Broadlink SCB1E (0x5115) (#514) Add support for Broadlink SCB1E (0x6113) (#488) Add support for Broadlink SP4L-EU (0x618B) (#489) Add support for Broadlink SP4L-UK (0xA589) (#491) Add support for Broadlink RM3 mini (0x27d3) (#493) Add support for Broadlink SP4L-AU (0x6489) (#498) Add support for BG Electrical AHC/U-01 (0x51E2) (#497) Add support for Broadlink MCB1 (0x6111) (#509) --- broadlink/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 74ae1fa6..11172514 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -21,6 +21,7 @@ 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"), @@ -41,9 +42,18 @@ 0x9479: (sp2, "SP3S-US", "Broadlink"), 0x947A: (sp2, "SP3S-EU", "Broadlink"), 0x756C: (sp4, "SP4M", "Broadlink"), + 0x756F: (sp4, "MCB1", "Broadlink"), 0x7579: (sp4, "SP4L-EU", "Broadlink"), 0x7583: (sp4, "SP mini 3", "Broadlink"), 0x7D11: (sp4, "SP mini 3", "Broadlink"), + 0xA56A: (sp4, "MCB1", "Broadlink"), + 0xA589: (sp4, "SP4L-UK", "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"), 0x2712: (rm, "RM pro/pro+", "Broadlink"), 0x272A: (rm, "RM pro", "Broadlink"), @@ -66,6 +76,7 @@ 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"), @@ -76,9 +87,13 @@ 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"), 0x2714: (a1, "e-Sensor", "Broadlink"), 0x4EB5: (mp1, "MP1-1K4S", "Broadlink"), 0x4EF7: (mp1, "MP1-1K4S", "Broadlink (OEM)"), From bdc5432e2e2ccd5208392a29c88049b7fc072623 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Sun, 29 Nov 2020 13:51:09 -0300 Subject: [PATCH 174/261] Clamp the packet count between 0x8000 and 0xFFFF (#486) --- broadlink/device.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/broadlink/device.py b/broadlink/device.py index e040de79..5582209d 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -120,7 +120,7 @@ def __init__( self.model = model self.manufacturer = manufacturer self.is_locked = is_locked - self.count = random.randrange(0xFFFF) + self.count = random.randint(0x8000, 0xFFFF) self.iv = bytes.fromhex("562e17996d093d28ddb3ba695a2e6f58") self.id = bytes(4) self.type = "Unknown" @@ -202,7 +202,6 @@ def auth(self) -> bool: if len(key) % 16 != 0: return False - self.count = int.from_bytes(response[0x28:0x30], "little") self.id = payload[0x03::-1] self.update_aes(key) return True @@ -264,7 +263,7 @@ def get_type(self) -> str: def send_packet(self, command: int, payload: bytes) -> bytes: """Send a packet to the device.""" - self.count = (self.count + 1) & 0xFFFF + self.count = ((self.count + 1) | 0x8000) & 0xFFFF packet = bytearray(0x38) packet[0x00] = 0x5A packet[0x01] = 0xA5 From 1b3fd1632162ff3c2e38603a5c2d55f1510a428d Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 27 Nov 2020 17:16:52 -0300 Subject: [PATCH 175/261] Fix MP1's check_power_raw() annotation (#485) This method returns an integer. --- broadlink/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlink/switch.py b/broadlink/switch.py index fe693abd..41e2c5b8 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -37,7 +37,7 @@ def set_power(self, sid: int, state: bool) -> None: sid_mask = 0x01 << (sid - 1) self.set_power_mask(sid_mask, state) - def check_power_raw(self) -> bool: + def check_power_raw(self) -> int: """Return the power state of the device in raw format.""" packet = bytearray(16) packet[0x00] = 0x0A From d7cbe304e05385fbae3fb2784227268b5e54b063 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Sat, 9 Jan 2021 19:21:54 -0300 Subject: [PATCH 176/261] Improve RM4 communication (#490) --- broadlink/remote.py | 110 +++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 68 deletions(-) diff --git a/broadlink/remote.py b/broadlink/remote.py index b6528d68..88498b61 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -1,4 +1,6 @@ """Support for universal remotes.""" +import struct + from .device import device from .exceptions import check_error @@ -10,87 +12,53 @@ def __init__(self, *args, **kwargs) -> None: """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "RM2" - self._request_header = bytes() - self._code_sending_header = bytes() + + def _send(self, command: int, data: bytes = b'') -> bytes: + """Send a packet to the device.""" + packet = struct.pack(" bytes: """Return the last captured code.""" - packet = bytearray(self._request_header) - packet.append(0x04) - response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) - return payload[len(self._request_header) + 4 :] + return self._send(0x4) def send_data(self, data: bytes) -> None: """Send a code to the device.""" - packet = bytearray(self._code_sending_header) - packet += bytearray([0x02, 0x00, 0x00, 0x00]) - packet += data - response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + self._send(0x2, data) def enter_learning(self) -> None: """Enter infrared learning mode.""" - packet = bytearray(self._request_header) - packet.append(0x03) - response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + self._send(0x3) def sweep_frequency(self) -> None: """Sweep frequency.""" - packet = bytearray(self._request_header) - packet.append(0x19) - response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + self._send(0x19) def cancel_sweep_frequency(self) -> None: """Cancel sweep frequency.""" - packet = bytearray(self._request_header) - packet.append(0x1E) - response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) + self._send(0x1E) def check_frequency(self) -> bool: """Return True if the frequency was identified successfully.""" - packet = bytearray(self._request_header) - packet.append(0x1A) - response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) - if payload[len(self._request_header) + 4] == 1: - return True - return False + resp = self._send(0x1A) + return resp[0] == 1 def find_rf_packet(self) -> bool: """Enter radiofrequency learning mode.""" - packet = bytearray(self._request_header) - packet.append(0x1B) - response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) - if payload[len(self._request_header) + 4] == 1: - return True - return False - - def _check_sensors(self, command: int) -> bytes: - """Return the state of the sensors in raw format.""" - packet = bytearray(self._request_header) - packet.append(command) - response = self.send_packet(0x6A, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) - return bytearray(payload[len(self._request_header) + 4 :]) - - def check_temperature(self) -> int: + resp = self._send(0x1B) + return resp[0] == 1 + + def check_temperature(self) -> float: """Return the temperature.""" - data = self._check_sensors(0x1) - return data[0x0] + data[0x1] / 10.0 + return self.check_sensors()["temperature"] def check_sensors(self) -> dict: """Return the state of the sensors.""" - data = self._check_sensors(0x1) - return {"temperature": data[0x0] + data[0x1] / 10.0} + resp = self._send(0x1) + return {"temperature": resp[0x0] + resp[0x1] / 10.0} class rm4(rm): @@ -100,23 +68,29 @@ def __init__(self, *args, **kwargs) -> None: """Initialize the controller.""" device.__init__(self, *args, **kwargs) self.type = "RM4" - self._request_header = b"\x04\x00" - self._code_sending_header = b"\xda\x00" - def check_temperature(self) -> int: - """Return the temperature.""" - data = self._check_sensors(0x24) - return data[0x0] + data[0x1] / 100.0 + def _send(self, command: int, data: bytes = b'') -> bytes: + """Send a packet to the device.""" + packet = struct.pack(" bool: + """Enter radiofrequency learning mode.""" + self._send(0x1B) + return True - def check_humidity(self) -> int: + def check_humidity(self) -> float: """Return the humidity.""" - data = self._check_sensors(0x24) - return data[0x2] + data[0x3] / 100.0 + return self.check_sensors()["humidity"] def check_sensors(self) -> dict: """Return the state of the sensors.""" - data = self._check_sensors(0x24) + resp = self._send(0x24) return { - "temperature": data[0x0] + data[0x1] / 100.0, - "humidity": data[0x2] + data[0x3] / 100.0, + "temperature": resp[0x0] + resp[0x1] / 100.0, + "humidity": resp[0x2] + resp[0x3] / 100.0, } From c3bb598e27d27ec1e86f8a83efa3f9a3d1887c17 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Sat, 9 Jan 2021 22:18:56 -0300 Subject: [PATCH 177/261] Timeout improvements (#464) - Only start to count the timer inside the lock. - Improve precision of the timeout option. - Use a context manager for the connection. - Remove SO_REUSEADDR option. Amend: Revert retry_intvl (#506) --- broadlink/device.py | 33 +++++++++++++++++---------------- broadlink/remote.py | 12 +++++++----- broadlink/sensor.py | 13 ++++++++++--- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/broadlink/device.py b/broadlink/device.py index 5582209d..6a0c2dee 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -70,13 +70,14 @@ def scan( packet[0x20] = checksum & 0xFF packet[0x21] = checksum >> 8 - starttime = time.time() + start_time = time.time() discovered = [] try: - while (time.time() - starttime) < timeout: + while (time.time() - start_time) < timeout: + time_left = timeout - (time.time() - start_time) + conn.settimeout(min(1, time_left)) conn.sendto(packet, (discover_ip_address, discover_ip_port)) - conn.settimeout(1) while True: try: @@ -307,22 +308,22 @@ def send_packet(self, command: int, payload: bytes) -> bytes: packet[0x20] = checksum & 0xFF packet[0x21] = checksum >> 8 - start_time = time.time() with self.lock: - conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as conn: + timeout = self.timeout + start_time = time.time() - while True: - try: + while True: + time_left = timeout - (time.time() - start_time) + conn.settimeout(min(1, time_left)) conn.sendto(packet, self.host) - conn.settimeout(1) - resp, _ = conn.recvfrom(2048) - break - except socket.timeout: - if (time.time() - start_time) > self.timeout: - conn.close() - raise exception(-4000) # Network timeout. - conn.close() + + 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. diff --git a/broadlink/remote.py b/broadlink/remote.py index 88498b61..6cf32f27 100644 --- a/broadlink/remote.py +++ b/broadlink/remote.py @@ -58,7 +58,9 @@ def check_temperature(self) -> float: def check_sensors(self) -> dict: """Return the state of the sensors.""" resp = self._send(0x1) - return {"temperature": resp[0x0] + resp[0x1] / 10.0} + temperature = struct.unpack(" float: def check_sensors(self) -> dict: """Return the state of the sensors.""" resp = self._send(0x24) - return { - "temperature": resp[0x0] + resp[0x1] / 100.0, - "humidity": resp[0x2] + resp[0x3] / 100.0, - } + temperature = struct.unpack(" dict: response = self.send_packet(0x6A, packet) check_error(response[0x22:0x24]) payload = self.decrypt(response[0x38:]) - data = bytearray(payload[0x4:]) + data = payload[0x4:] + + temperature = struct.unpack(" Date: Mon, 11 Jan 2021 02:25:50 -0300 Subject: [PATCH 178/261] Filter unsupported features and convert sensor data to float (#495) --- broadlink/switch.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/broadlink/switch.py b/broadlink/switch.py index 41e2c5b8..f7f23084 100644 --- a/broadlink/switch.py +++ b/broadlink/switch.py @@ -312,6 +312,18 @@ def __init__(self, *args, **kwargs) -> None: device.__init__(self, *args, **kwargs) self.type = "SP4B" + def get_state(self) -> dict: + """Get full state of device.""" + state = super().get_state() + + # Convert sensor data to float. Remove keys if sensors are not supported. + sensor_attrs = ["current", "volt", "power", "totalconsum", "overload"] + for attr in sensor_attrs: + value = state.pop(attr, -1) + if value != -1: + state[attr] = value / 1000 + return state + def _encode(self, flag: int, state: dict) -> bytes: """Encode a message.""" payload = json.dumps(state, separators=(",", ":")).encode() From b33bbdbbcf1270fb77f346f0e003273247d3ff2f Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel Date: Mon, 11 Jan 2021 17:51:31 -0300 Subject: [PATCH 179/261] Fix encryption errors (#505) --- broadlink/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/broadlink/device.py b/broadlink/device.py index 6a0c2dee..e6753774 100644 --- a/broadlink/device.py +++ b/broadlink/device.py @@ -154,18 +154,18 @@ def __str__(self): def update_aes(self, key: bytes) -> None: """Update AES.""" self.aes = Cipher( - algorithms.AES(key), modes.CBC(self.iv), backend=default_backend() + algorithms.AES(bytes(key)), modes.CBC(self.iv), backend=default_backend() ) def encrypt(self, payload: bytes) -> bytes: """Encrypt the payload.""" encryptor = self.aes.encryptor() - return encryptor.update(payload) + encryptor.finalize() + return encryptor.update(bytes(payload)) + encryptor.finalize() def decrypt(self, payload: bytes) -> bytes: """Decrypt the payload.""" decryptor = self.aes.decryptor() - return decryptor.update(payload) + decryptor.finalize() + return decryptor.update(bytes(payload)) + decryptor.finalize() def auth(self) -> bool: """Authenticate to the device.""" From 9a04f68212541a400619e3bbb476fa8b0df4ece6 Mon Sep 17 00:00:00 2001 From: jbsky Date: Sat, 23 Jan 2021 04:35:48 +0100 Subject: [PATCH 180/261] 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 181/261] 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 182/261] 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 183/261] 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 184/261] 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 185/261] 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 186/261] 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 187/261] 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 188/261] 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 189/261] 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 190/261] 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 191/261] 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 192/261] 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 193/261] 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 194/261] 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 195/261] 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 196/261] 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 197/261] 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 198/261] 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 199/261] 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 200/261] 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 201/261] 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 202/261] 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 203/261] 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 204/261] 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 205/261] 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 206/261] 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 207/261] 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 208/261] 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 209/261] 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 210/261] 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 211/261] 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 212/261] 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 213/261] 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 214/261] 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 215/261] 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 216/261] 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 217/261] 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 218/261] 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 219/261] 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 220/261] 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 221/261] 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 222/261] 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 223/261] 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 224/261] 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 225/261] 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 226/261] 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 227/261] 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 228/261] 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 229/261] 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 230/261] 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 231/261] 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 232/261] 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 233/261] 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 234/261] 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 235/261] 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 236/261] 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 237/261] 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 238/261] 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 239/261] 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 240/261] 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 241/261] 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 242/261] 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 243/261] 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 244/261] 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 245/261] 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 246/261] 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 247/261] 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 248/261] 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 249/261] 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 250/261] 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 251/261] 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 252/261] 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 253/261] 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 254/261] 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 255/261] 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 256/261] 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 257/261] 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 258/261] 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 259/261] 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 260/261] 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 261/261] 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",