From ecab016a73eee8d018a4b8617eb2c2680dbc284d Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Thu, 15 Sep 2016 08:06:26 -0700 Subject: Initial commit --- LICENSE | 22 +++++++ README.md | 36 +++++++++++ broadlink/__init__.py | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 32 ++++++++++ 4 files changed, 262 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100755 broadlink/__init__.py create mode 100644 setup.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000000..d8c801656bdf --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Mike Ryan +Copyright (c) 2016 Matthew Garrett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 000000000000..ce9509068bfc --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +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) devices are supported and only one device per network will be used. There is currently no support for the cloud API. + +Example use +----------- + +Discover an available device on the local network: +``` +import broadlink + +device = broadlink.rm2() +device.discover() +``` + +Obtain the authentication key required for further communication: +``` +device.auth() +``` + +Enter learning mode: +``` +device.enter_learning() +``` + +Obtain an IR packet while in learning mode: +``` +ir_packet = device.check_data() +``` +(This will return None if the device does not have a packet to return) + +Send an IR packet: +``` +device.send_data(ir_packet) +``` diff --git a/broadlink/__init__.py b/broadlink/__init__.py new file mode 100755 index 000000000000..c981d6b3eaa4 --- /dev/null +++ b/broadlink/__init__.py @@ -0,0 +1,172 @@ +#!/usr/bin/python + +from datetime import datetime +from socket import * +from Crypto.Cipher import AES +import time +import random + +class rm2: + def __init__(self): + 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]) + s = socket(AF_INET, SOCK_DGRAM) + s.connect(('8.8.8.8', 0)) # connecting to a UDP address doesn't send packets + local_ip_address = s.getsockname()[0] + + self.address = local_ip_address.split('.') + self.id = bytearray([0, 0, 0, 0]) + + def discover(self): + self.cs = socket(AF_INET, SOCK_DGRAM) + self.cs.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + self.cs.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + self.cs.bind(('',0)) + self.port = self.cs.getsockname()[1] + + packet = bytearray(0x30) + + year = datetime.now().year + + packet[0x08] = 0xf9 + packet[0x09] = 0xff + packet[0x0a] = 0xff + packet[0x0b] = 0xff + 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(self.address[0]) + packet[0x19] = int(self.address[1]) + packet[0x1a] = int(self.address[2]) + packet[0x1b] = int(self.address[3]) + packet[0x1c] = self.port & 0xff + packet[0x1d] = self.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 + + self.cs.sendto(packet, ('255.255.255.255', 80)) + response = self.cs.recvfrom(1024) + responsepacket = bytearray(response[0]) + self.host = response[1] + self.mac = responsepacket[0x3a:0x40] + + 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] = 'T' + payload[0x31] = 'e' + payload[0x32] = 's' + payload[0x33] = 't' + payload[0x34] = ' ' + payload[0x35] = ' ' + payload[0x36] = '1' + + response = self.send_packet(0x65, payload) + + enc_payload = response[0x38:] + + aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) + payload = aes.decrypt(str(response[0x38:])) + + self.id = payload[0x00:0x04] + self.key = payload[0x04:0x14] + + def send_packet(self, command, payload): + 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] + + checksum = 0xbeaf + for i in range(len(payload)): + checksum += payload[i] + checksum = checksum & 0xffff + + aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) + payload = aes.encrypt(str(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 + + self.cs.sendto(packet, self.host) + response = self.cs.recvfrom(1024) + return response[0] + + 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 check_data(self): + packet = bytearray(16) + packet[0] = 4 + response = self.send_packet(0x6a, packet) + err = ord(response[0x22]) | (ord(response[0x23]) << 8) + if err == 0: + aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) + payload = aes.decrypt(str(response[0x38:])) + return payload[0x04:] diff --git a/setup.py b/setup.py new file mode 100644 index 000000000000..4ac0917f38bb --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import re +from setuptools import setup, find_packages +import sys +import warnings + +dynamic_requires = [] + +version = 0.1 + +setup( + name='broadlink', + version=0.1, + author='Matthew Garrett', + author_email='mjg59@srcf.ucam.org', + url='http://github.com/mjg59/python-broadlink', + packages=find_packages(), + scripts=[], + license=open('LICENSE').read(), + 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', + ], + include_package_data=True, + zip_safe=False, +) -- cgit 1.4.1 From 6936bc750ee00e495556eee6a28f240020bec945 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Mon, 10 Oct 2016 01:09:06 -0700 Subject: Attempt to handle timezones The timezone should be embedded in the discovery packet, so make a better attempt to do that. --- broadlink/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index c981d6b3eaa4..2891052708a3 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -25,14 +25,21 @@ class rm2: self.cs.bind(('',0)) self.port = self.cs.getsockname()[1] + timezone = time.timezone/-3600 packet = bytearray(0x30) year = datetime.now().year - packet[0x08] = 0xf9 - packet[0x09] = 0xff - packet[0x0a] = 0xff - packet[0x0b] = 0xff + 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 -- cgit 1.4.1 From 60afb5164eb72c3734a297ed18d4c24127ef48e8 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Wed, 12 Oct 2016 05:37:44 -0700 Subject: Document packet format --- protocol.md | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 protocol.md diff --git a/protocol.md b/protocol.md new file mode 100644 index 000000000000..ff776eff1583 --- /dev/null +++ b/protocol.md @@ -0,0 +1,128 @@ +Broadlink RM2 network protocol +============================== + +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. + +Network discovery +----------------- + +To discover Broadlink devices on the local network, send a 48 byte packet with the following contents: + +| Offset | Contents | +|---------|----------| +|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)| +|0x12|Current day in month| +|0x13|Current month| +|0x19-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| +|0x22-0x25|00| +|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. + +Checksum +-------- + +Construct the packet and set checksum bytes to zero. Add each byte to the starting value of 0xbeaf, wrapping after 0xffff. + +Command packet format +--------------------- + +The command packet header is 56 bytes long with the following format: + +|Offset|Contents| +|------|--------| +|0x00|0x5a| +|0x01|0xa5| +|0x02|0xaa| +|0x03|0x55| +|0x04|0x5a| +|0x05|0xa5| +|0x06|0xaa| +|0x07|0x55| +|0x08-0x1f|00| +|0x20-0x21|Checksum of full packet as a little-endian 16 bit integer| +|0x22-0x23|00| +|0x24|0x2a| +|0x25|0x27| +|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 +|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: + +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 +4. Set the checksum initialisation value to 0xbeaf and calculate the checksum of the entire packet. Set 0x20-0x21 to this value. + +Authorisation +------------- + +You must obtain an authorisation key from the device before you can communicate. To do so, generate an 80 byte packet with the following contents: + +|Offset|Contents| +|------|--------| +|0x00-0x03|00| +|0x04-0x12|A 15-digit value that represents this device. Broadlink's implementation uses the IMEI.| +|0x13|01| +|0x14-0x2c|00| +|0x2d|0x01| +|0x30-0x7f|NULL-terminated ASCII string containing the device name| + +Send this payload with a command value of 0x0065. The response packet will contain an encrypted payload from byte 0x38 onwards. Decrypt this using the default key and IV. The format of the decrypted payload is: + +|Offset|Contents| +|------|--------| +|0x00-0x03|Device ID| +|0x04-0x13|Device encryption key| + +All further command packets must use this encryption key and device ID. + +Entering learning mode +---------------------- + +Send the following 16 byte payload with a command value of 0x006a: + +|Offset|Contents| +|------|--------| +|0x00|0x03| +|0x01-0x0f|0x00| + +Reading back data from learning mode +------------------------------------ + +Send the following 16 byte payload with a command value of 0x006a: + +|Offset|Contents| +|------|--------| +|0x00|0x04| +|0x01-0x0f|0x00| + +Byte 0x22 of the response contains a little-endian 16 bit error code. If this is 0, a code has been obtained. Bytes 0x38 and onward of the response are encrypted. Decrypt them. Bytes 0x04 and onward of the decrypted payload contain the captured data. + +Sending data +------------ + +Send the following payload with a command byte of 0x006a + +|Offset|Contents| +|------|--------| +|0x00|0x02| +|0x01-0x03|0x00| +|0x04-end|data| -- cgit 1.4.1 From ef77bc7ea8e3b7f54515220256703a8bd2f6cca5 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Wed, 12 Oct 2016 05:42:07 -0700 Subject: Add some todos in the protocol --- protocol.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/protocol.md b/protocol.md index ff776eff1583..c91c4fbdef91 100644 --- a/protocol.md +++ b/protocol.md @@ -126,3 +126,11 @@ Send the following payload with a command byte of 0x006a |0x00|0x02| |0x01-0x03|0x00| |0x04-end|data| + + +Todo +---- + +* Support for other devices using the Broadlink protocol (various smart home devices) +* Verify whether RF learning uses the same information. The first 4 bytes of the response packet may indicate the type of response that was learned, and the first 4 bytes of the payload may provide that back to the Broadlink on sending. Alternatively, this information may be embedded in the response data already. +* Figure out what the format of the data packets actually is. \ No newline at end of file -- cgit 1.4.1 From 69afd4ce52b47e488e15562b1c04edbffe5f58b0 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Mon, 24 Oct 2016 20:41:27 -0700 Subject: Add support for sensor data Add a check_temperature() function that'll work for both the RM2 and the A1 sensor platform, and a check_sensors() function that returns the full set of sensor data for the A1 as a dict. --- broadlink/__init__.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 2891052708a3..63ad76b38677 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -168,10 +168,65 @@ class rm2: packet[0] = 3 self.send_packet(0x6a, packet) + def check_sensors(self): + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6a, packet) + err = ord(response[0x22]) | (ord(response[0x23]) << 8) + if err == 0: + data = {} + aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) + payload = aes.decrypt(str(response[0x38:])) + 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]) + 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' + air_quality = ord(payload[0x0a]) + 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' + noise = ord(payload[0xc]) + 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_temperature(self): + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6a, packet) + err = ord(response[0x22]) | (ord(response[0x23]) << 8) + if err == 0: + aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) + payload = aes.decrypt(str(response[0x38:])) + temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 + return temp + def check_data(self): packet = bytearray(16) packet[0] = 4 - response = self.send_packet(0x6a, packet) + response = self.send_packet(0x6a, packet) err = ord(response[0x22]) | (ord(response[0x23]) << 8) if err == 0: aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) -- cgit 1.4.1 From 989009e21f235b1bc162010ea14c24c034476eda Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 30 Oct 2016 14:16:40 -0700 Subject: Add support for multiple devices and update documentation Add support for discovering more than one device on the network, and update the documentation to describe that. --- README.md | 25 ++++++--- broadlink/__init__.py | 150 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 107 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index ce9509068bfc..6a6f01847ca0 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,45 @@ 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) devices are supported and only one device per network will be used. 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, 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. Example use ----------- -Discover an available device on the local network: +Discover available devices on the local network: ``` import broadlink -device = broadlink.rm2() -device.discover() +devices = broadlink.discover(timeout=5) ``` Obtain the authentication key required for further communication: ``` -device.auth() +devices[0].auth() ``` Enter learning mode: ``` -device.enter_learning() +devices[0].enter_learning() ``` Obtain an IR packet while in learning mode: ``` -ir_packet = device.check_data() +ir_packet = devices[0].check_data() ``` (This will return None if the device does not have a packet to return) Send an IR packet: ``` -device.send_data(ir_packet) +devices[0].send_data(ir_packet) +``` + +Obtain temperature data from an RM2: +``` +devices[0].check_temperature() +``` + +Obtain sensor data from an A1: +``` +data = devices[0].check_sensors() ``` diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 63ad76b38677..edd5f24bfede 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -1,74 +1,95 @@ #!/usr/bin/python from datetime import datetime -from socket import * from Crypto.Cipher import AES import time import random +import socket -class rm2: - def __init__(self): - 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]) - s = socket(AF_INET, SOCK_DGRAM) - s.connect(('8.8.8.8', 0)) # connecting to a UDP address doesn't send packets - local_ip_address = s.getsockname()[0] +def discover(timeout=None): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 0)) # 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(('',0)) + port = cs.getsockname()[1] + starttime = time.time() - self.address = local_ip_address.split('.') - self.id = bytearray([0, 0, 0, 0]) + devices = [] - def discover(self): - self.cs = socket(AF_INET, SOCK_DGRAM) - self.cs.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) - self.cs.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) - self.cs.bind(('',0)) - self.port = self.cs.getsockname()[1] - - timezone = 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(self.address[0]) - packet[0x19] = int(self.address[1]) - packet[0x1a] = int(self.address[2]) - packet[0x1b] = int(self.address[3]) - packet[0x1c] = self.port & 0xff - packet[0x1d] = self.port >> 8 - packet[0x26] = 6 - checksum = 0xbeaf + timezone = time.timezone/-3600 + packet = bytearray(0x30) - for i in range(len(packet)): - checksum += packet[i] - checksum = checksum & 0xffff - packet[0x20] = checksum & 0xff - packet[0x21] = checksum >> 8 + year = datetime.now().year - self.cs.sendto(packet, ('255.255.255.255', 80)) - response = self.cs.recvfrom(1024) + 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]) - self.host = response[1] - self.mac = responsepacket[0x3a:0x40] + host = response[1] + mac = responsepacket[0x3a:0x40] + return device(host=host, mac=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] + mac = responsepacket[0x3a:0x40] + devices.append(device(host=host, mac=mac)) + +class device: + def __init__(self, host, mac): + self.host = host + self.mac = mac + 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)) def auth(self): payload = bytearray(0x50) @@ -102,7 +123,7 @@ class rm2: enc_payload = response[0x38:] aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) - payload = aes.decrypt(str(response[0x38:])) + payload = aes.decrypt(str(enc_payload)) self.id = payload[0x00:0x04] self.key = payload[0x04:0x14] @@ -232,3 +253,12 @@ class rm2: aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) payload = aes.decrypt(str(response[0x38:])) return payload[0x04:] + +class rm2(device): + def __init__ (self): + device.__init__(self, None, None) + + def discover(self): + dev = discover() + self.host = dev.host + self.mac = dev.mac -- cgit 1.4.1 From 925742768d6e84558fcb99b30ca448103392aeff Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 30 Oct 2016 14:17:44 -0700 Subject: Update documentation to cover RF packets RF packets behave identically to IR packets, so update the docs to match --- README.md | 4 ++-- protocol.md | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6a6f01847ca0..a92fb77ff230 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ Enter learning mode: devices[0].enter_learning() ``` -Obtain an IR packet while in learning mode: +Obtain an IR or RF packet while in learning mode: ``` ir_packet = devices[0].check_data() ``` (This will return None if the device does not have a packet to return) -Send an IR packet: +Send an IR or RF packet: ``` devices[0].send_data(ir_packet) ``` diff --git a/protocol.md b/protocol.md index c91c4fbdef91..1888a19d5337 100644 --- a/protocol.md +++ b/protocol.md @@ -132,5 +132,4 @@ Todo ---- * Support for other devices using the Broadlink protocol (various smart home devices) -* Verify whether RF learning uses the same information. The first 4 bytes of the response packet may indicate the type of response that was learned, and the first 4 bytes of the payload may provide that back to the Broadlink on sending. Alternatively, this information may be embedded in the response data already. * Figure out what the format of the data packets actually is. \ No newline at end of file -- cgit 1.4.1 From bd1c44488d1bc419ca98fe8a2bbdf9d299b9773d Mon Sep 17 00:00:00 2001 From: Kelvin Law Date: Wed, 2 Nov 2016 20:34:46 -0700 Subject: Fix for [Errno 49] Can't assign requested address Fixes #3 on macOS --- broadlink/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index edd5f24bfede..8e38c06df63f 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -8,7 +8,7 @@ import socket def discover(timeout=None): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(('8.8.8.8', 0)) # connecting to a UDP address doesn't send packets + 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) @@ -140,9 +140,9 @@ class device: packet[0x07] = 0x55 packet[0x24] = 0x2a packet[0x25] = 0x27 - packet[0x26] = command - packet[0x28] = self.count & 0xff - packet[0x29] = self.count >> 8 + 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] @@ -167,7 +167,7 @@ class device: for i in range(len(payload)): packet.append(payload[i]) - + checksum = 0xbeaf for i in range(len(packet)): checksum += packet[i] @@ -182,13 +182,13 @@ class device: def send_data(self, data): packet = bytearray([0x02, 0x00, 0x00, 0x00]) packet += data - self.send_packet(0x6a, packet) + self.send_packet(0x6a, packet) def enter_learning(self): packet = bytearray(16) packet[0] = 3 - self.send_packet(0x6a, packet) - + self.send_packet(0x6a, packet) + def check_sensors(self): packet = bytearray(16) packet[0] = 1 -- cgit 1.4.1 From 4705fdf011b7f5ec8b8c30269451bb0a020c6bf2 Mon Sep 17 00:00:00 2001 From: Marcus Stewart Hughes Date: Wed, 9 Nov 2016 22:01:33 +0000 Subject: each send_packet() needs a unique count Increment count instead of randomly generating on each send_packet() as per @mjg59's suggestion. Tested and working as expected. --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 8e38c06df63f..cd9d1599f144 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -129,6 +129,7 @@ class device: self.key = payload[0x04:0x14] def send_packet(self, command, payload): + self.count = (self.count + 1) & 0xffff packet = bytearray(0x38) packet[0x00] = 0x5a packet[0x01] = 0xa5 -- cgit 1.4.1 From c174e40f5903fdbc219ba09bc90256d83ebf1c22 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 13 Nov 2016 14:29:07 +1300 Subject: Add experimental support for smartplugs This adds a set_power() call that should work for the SP2 - unsure whether it will work for the SP3. --- README.md | 5 +++++ broadlink/__init__.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/README.md b/README.md index a92fb77ff230..3de1fa92e7ef 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,8 @@ Obtain sensor data from an A1: ``` data = devices[0].check_sensors() ``` + +Set power state on an SP2/SP3 (0 for off, 1 for on): +``` +devices[0].set_power(1) +``` \ No newline at end of file diff --git a/broadlink/__init__.py b/broadlink/__init__.py index cd9d1599f144..2f3966c4762f 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -180,6 +180,12 @@ class device: response = self.cs.recvfrom(1024) return response[0] + def set_power(self, state): + packet = bytearray(8) + packet[0] = 2 + packet[4] = state + self.send_packet(0x6a, packet) + def send_data(self, data): packet = bytearray([0x02, 0x00, 0x00, 0x00]) packet += data -- cgit 1.4.1 From d490c5b71eaa9fa19f711648a92359678717682f Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Fri, 18 Nov 2016 09:02:59 +1300 Subject: Add automatic determination of device type Look at the device ID and figure out what type of device it is, and provide appropriate classes for each that only support the functionality available on that device. --- broadlink/__init__.py | 100 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 17 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 2f3966c4762f..570ae153f9c1 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -75,8 +75,48 @@ def discover(timeout=None): return devices responsepacket = bytearray(response[0]) host = response[1] + devtype = responsepacket[0x34] | responsepacket[0x35] << 8 mac = responsepacket[0x3a:0x40] - devices.append(device(host=host, mac=mac)) + if devtype == 0: # SP1 + devices.append(sp1(host=host, mac=mac)) + if devtype == 0x2711: # SP2 + devices.append(sp2(host=host, mac=mac)) + if devtype == 0x2719 or devtype == 0x7919 or devtype == 0x271a or devtype == 0x791a: # Honeywell SP2 + devices.append(sp2(host=host, mac=mac)) + if devtype == 0x2720: # SPMini + devices.append(sp2(host=host, mac=mac)) + elif devtype == 0x753e: # SP3 + devices.append(sp2(host=host, mac=mac)) + elif devtype == 0x2728: # SPMini2 + devices.append(sp2(host=host, mac=mac)) + elif devtype == 0x2733 or devtype == 0x273e: # OEM branded SPMini + devices.append(sp2(host=host, mac=mac)) + elif devtype >= 0x7530 and devtype <= 0x7918: # OEM branded SPMini2 + devices.append(sp2(host=host, mac=mac)) + elif devtype == 0x2736: # SPMiniPlus + devices.append(sp2(host=host, mac=mac)) + elif devtype == 0x2712: # RM2 + devices.append(rm(host=host, mac=mac)) + elif devtype == 0x2737: # RM Mini + devices.append(rm(host=host, mac=mac)) + elif devtype == 0x273d: # RM Pro Phicomm + devices.append(rm(host=host, mac=mac)) + elif devtype == 0x2783: # RM2 Home Plus + devices.append(rm(host=host, mac=mac)) + elif devtype == 0x277c: # RM2 Home Plus GDT + devices.append(rm(host=host, mac=mac)) + elif devtype == 0x272a: # RM2 Pro Plus + devices.append(rm(host=host, mac=mac)) + elif devtype == 0x2787: # RM2 Pro Plus2 + devices.append(rm(host=host, mac=mac)) + elif devtype == 0x278b: # RM2 Pro Plus BL + devices.append(rm(host=host, mac=mac)) + elif devtype == 0x278f: # RM Mini Shate + devices.append(rm(host=host, mac=mac)) + elif devtype == 0x2714: # A1 + devices.append(a1(host=host, mac=mac)) + else: + devices.append(device(host=host, mac=mac)) class device: def __init__(self, host, mac): @@ -180,21 +220,31 @@ class device: response = self.cs.recvfrom(1024) return response[0] + +class sp1(device): + def __init__ (self, host, mac): + device.__init__(self, host, mac) + + def set_power(self, state): + packet = bytearray(4) + packet[0] = state + self.send_packet(0x66, packet) + + +class sp2(device): + def __init__ (self, host, mac): + device.__init__(self, host, mac) + def set_power(self, state): packet = bytearray(8) packet[0] = 2 packet[4] = state self.send_packet(0x6a, packet) - 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) +class a1(device): + def __init__ (self, host, mac): + device.__init__(self, host, mac) def check_sensors(self): packet = bytearray(16) @@ -240,28 +290,44 @@ class device: data['noise'] = 'unknown' return data - def check_temperature(self): + +class rm(device): + def __init__ (self, host, mac): + device.__init__(self, host, mac) + + def check_data(self): packet = bytearray(16) - packet[0] = 1 + packet[0] = 4 response = self.send_packet(0x6a, packet) err = ord(response[0x22]) | (ord(response[0x23]) << 8) if err == 0: aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) payload = aes.decrypt(str(response[0x38:])) - temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 - return temp + return payload[0x04:] - def check_data(self): + 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] = 4 + packet[0] = 3 + self.send_packet(0x6a, packet) + + def check_temperature(self): + packet = bytearray(16) + packet[0] = 1 response = self.send_packet(0x6a, packet) err = ord(response[0x22]) | (ord(response[0x23]) << 8) if err == 0: aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) payload = aes.decrypt(str(response[0x38:])) - return payload[0x04:] + temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 + return temp -class rm2(device): +# For legay compatibility - don't use this +class rm2(rm): def __init__ (self): device.__init__(self, None, None) -- cgit 1.4.1 From 652f52eb039a0ecd3fb157558d88374aabe013fe Mon Sep 17 00:00:00 2001 From: Przemek Wiech Date: Fri, 18 Nov 2016 00:19:02 +0100 Subject: Updated SmartPlug commands - set_power() and check_power() --- README.md | 11 ++++++++--- broadlink/__init__.py | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3de1fa92e7ef..47a4691f51bd 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,12 @@ Obtain sensor data from an A1: data = devices[0].check_sensors() ``` -Set power state on an SP2/SP3 (0 for off, 1 for on): +Set power state on a SmartPlug SP2/SP3: +``` +devices[0].set_power(True) +``` + +Check power state on a SmartPlug: +``` +state = devices[0].check_power() ``` -devices[0].set_power(1) -``` \ No newline at end of file diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 2f3966c4762f..45c3d06011e0 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -181,11 +181,23 @@ class device: return response[0] def set_power(self, state): - packet = bytearray(8) + """Sets the power state of the smart plug.""" + packet = bytearray(16) packet[0] = 2 - packet[4] = state + packet[4] = 1 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 = ord(response[0x22]) | (ord(response[0x23]) << 8) + if err == 0: + aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) + payload = aes.decrypt(str(response[0x38:])) + return bool(ord(payload[0x4])) + def send_data(self, data): packet = bytearray([0x02, 0x00, 0x00, 0x00]) packet += data -- cgit 1.4.1 From c68fcea5e6bd561a21a90d40318b3d8b955d4ff8 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 19 Nov 2016 14:22:08 -0800 Subject: Fix single device discovery The automatic device type detection was only implemented when discovering with a timeout. Refactor to do it for the single-device discover() case. --- broadlink/__init__.py | 87 +++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 570ae153f9c1..5d5dd4c3fbf2 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -6,6 +6,48 @@ import time import random import socket +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) + elif devtype == 0x753e: # SP3 + 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 + return sp2(host=host, mac=mac) + elif devtype >= 0x7530 and devtype <= 0x7918: # OEM branded SPMini2 + return sp2(host=host, mac=mac) + elif devtype == 0x2736: # SPMiniPlus + return sp2(host=host, mac=mac) + elif devtype == 0x2712: # RM2 + return rm(host=host, mac=mac) + elif devtype == 0x2737: # RM Mini + return rm(host=host, mac=mac) + elif devtype == 0x273d: # RM Pro Phicomm + return rm(host=host, mac=mac) + elif devtype == 0x2783: # RM2 Home Plus + return rm(host=host, mac=mac) + elif devtype == 0x277c: # RM2 Home Plus GDT + return rm(host=host, mac=mac) + elif devtype == 0x272a: # RM2 Pro Plus + return rm(host=host, mac=mac) + elif devtype == 0x2787: # RM2 Pro Plus2 + return rm(host=host, mac=mac) + elif devtype == 0x278b: # RM2 Pro Plus BL + return rm(host=host, mac=mac) + elif devtype == 0x278f: # RM Mini Shate + return rm(host=host, mac=mac) + elif devtype == 0x2714: # A1 + return a1(host=host, mac=mac) + else: + return device(host=host, mac=mac) + def discover(timeout=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 @@ -65,7 +107,8 @@ def discover(timeout=None): responsepacket = bytearray(response[0]) host = response[1] mac = responsepacket[0x3a:0x40] - return device(host=host, mac=mac) + devtype = responsepacket[0x34] | responsepacket[0x35] << 8 + return gendevice(devtype, host, mac) else: while (time.time() - starttime) < timeout: cs.settimeout(timeout - (time.time() - starttime)) @@ -77,46 +120,8 @@ def discover(timeout=None): host = response[1] devtype = responsepacket[0x34] | responsepacket[0x35] << 8 mac = responsepacket[0x3a:0x40] - if devtype == 0: # SP1 - devices.append(sp1(host=host, mac=mac)) - if devtype == 0x2711: # SP2 - devices.append(sp2(host=host, mac=mac)) - if devtype == 0x2719 or devtype == 0x7919 or devtype == 0x271a or devtype == 0x791a: # Honeywell SP2 - devices.append(sp2(host=host, mac=mac)) - if devtype == 0x2720: # SPMini - devices.append(sp2(host=host, mac=mac)) - elif devtype == 0x753e: # SP3 - devices.append(sp2(host=host, mac=mac)) - elif devtype == 0x2728: # SPMini2 - devices.append(sp2(host=host, mac=mac)) - elif devtype == 0x2733 or devtype == 0x273e: # OEM branded SPMini - devices.append(sp2(host=host, mac=mac)) - elif devtype >= 0x7530 and devtype <= 0x7918: # OEM branded SPMini2 - devices.append(sp2(host=host, mac=mac)) - elif devtype == 0x2736: # SPMiniPlus - devices.append(sp2(host=host, mac=mac)) - elif devtype == 0x2712: # RM2 - devices.append(rm(host=host, mac=mac)) - elif devtype == 0x2737: # RM Mini - devices.append(rm(host=host, mac=mac)) - elif devtype == 0x273d: # RM Pro Phicomm - devices.append(rm(host=host, mac=mac)) - elif devtype == 0x2783: # RM2 Home Plus - devices.append(rm(host=host, mac=mac)) - elif devtype == 0x277c: # RM2 Home Plus GDT - devices.append(rm(host=host, mac=mac)) - elif devtype == 0x272a: # RM2 Pro Plus - devices.append(rm(host=host, mac=mac)) - elif devtype == 0x2787: # RM2 Pro Plus2 - devices.append(rm(host=host, mac=mac)) - elif devtype == 0x278b: # RM2 Pro Plus BL - devices.append(rm(host=host, mac=mac)) - elif devtype == 0x278f: # RM Mini Shate - devices.append(rm(host=host, mac=mac)) - elif devtype == 0x2714: # A1 - devices.append(a1(host=host, mac=mac)) - else: - devices.append(device(host=host, mac=mac)) + dev = gendevice(devtype, host, mac) + devices.append(dev) class device: def __init__(self, host, mac): -- cgit 1.4.1 From d066513d024101b0ae0a2683ac38dd0663e27e19 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 20 Nov 2016 11:16:28 -0800 Subject: Add packet retransmission and timeout UDP doesn't guarantee delivery, so reattempt packet transmission if we don't get a response and timeout if we still don't have anything after (by default) 10 seconds. --- broadlink/__init__.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 942b017ffcf6..12cba918a2a7 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -124,9 +124,10 @@ def discover(timeout=None): devices.append(dev) class device: - def __init__(self, host, mac): + def __init__(self, host, mac, timeout=10): self.host = host self.mac = mac + 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]) @@ -221,8 +222,17 @@ class device: packet[0x20] = checksum & 0xff packet[0x21] = checksum >> 8 - self.cs.sendto(packet, self.host) - response = self.cs.recvfrom(1024) + starttime = time.time() + while True: + try: + self.cs.sendto(packet, self.host) + self.cs.settimeout(1) + response = self.cs.recvfrom(1024) + break + except socket.timeout: + if (time.time() - starttime) < self.timeout: + pass + raise return response[0] -- cgit 1.4.1 From a5d05c95b35a2dff3832dd3eda6ed587a6a4c5d4 Mon Sep 17 00:00:00 2001 From: Alejandro M Date: Thu, 24 Nov 2016 22:25:14 +0100 Subject: Python 3 compatibility --- broadlink/__init__.py | 64 +++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 942b017ffcf6..1935596ab668 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -62,7 +62,7 @@ def discover(timeout=None): devices = [] - timezone = time.timezone/-3600 + timezone = int(time.timezone/-3600) packet = bytearray(0x30) year = datetime.now().year @@ -155,20 +155,20 @@ class device: payload[0x12] = 0x31 payload[0x1e] = 0x01 payload[0x2d] = 0x01 - payload[0x30] = 'T' - payload[0x31] = 'e' - payload[0x32] = 's' - payload[0x33] = 't' - payload[0x34] = ' ' - payload[0x35] = ' ' - payload[0x36] = '1' + 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) enc_payload = response[0x38:] - aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) - payload = aes.decrypt(str(enc_payload)) + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + payload = aes.decrypt(bytes(enc_payload)) self.id = payload[0x00:0x04] self.key = payload[0x04:0x14] @@ -205,8 +205,8 @@ class device: checksum += payload[i] checksum = checksum & 0xffff - aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) - payload = aes.encrypt(str(payload)) + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + payload = aes.encrypt(bytes(payload)) packet[0x34] = checksum & 0xff packet[0x35] = checksum >> 8 @@ -223,7 +223,7 @@ class device: self.cs.sendto(packet, self.host) response = self.cs.recvfrom(1024) - return response[0] + return bytes(response[0]) class sp1(device): @@ -252,11 +252,11 @@ class sp2(device): packet = bytearray(16) packet[0] = 1 response = self.send_packet(0x6a, packet) - err = ord(response[0x22]) | (ord(response[0x23]) << 8) + err = response[0x22] | (response[0x23] << 8) if err == 0: - aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) - payload = aes.decrypt(str(response[0x38:])) - return bool(ord(payload[0x4])) + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + payload = aes.decrypt(bytes(response[0x38:])) + return bool(payload[0x4]) class a1(device): def __init__ (self, host, mac): @@ -266,14 +266,14 @@ class a1(device): packet = bytearray(16) packet[0] = 1 response = self.send_packet(0x6a, packet) - err = ord(response[0x22]) | (ord(response[0x23]) << 8) + err = response[0x22] | (response[0x23] << 8) if err == 0: data = {} - aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) - payload = aes.decrypt(str(response[0x38:])) - 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]) + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + payload = aes.decrypt(bytes(response[0x38:])) + data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 + data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 + light = payload[0x8] if light == 0: data['light'] = 'dark' elif light == 1: @@ -284,7 +284,7 @@ class a1(device): data['light'] = 'bright' else: data['light'] = 'unknown' - air_quality = ord(payload[0x0a]) + air_quality = payload[0x0a] if air_quality == 0: data['air_quality'] = 'excellent' elif air_quality == 1: @@ -295,7 +295,7 @@ class a1(device): data['air_quality'] = 'bad' else: data['air_quality'] = 'unknown' - noise = ord(payload[0xc]) + noise = payload[0xc] if noise == 0: data['noise'] = 'quiet' elif noise == 1: @@ -315,10 +315,10 @@ class rm(device): packet = bytearray(16) packet[0] = 4 response = self.send_packet(0x6a, packet) - err = ord(response[0x22]) | (ord(response[0x23]) << 8) + err = response[0x22] | (response[0x23] << 8) if err == 0: - aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) - payload = aes.decrypt(str(response[0x38:])) + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + payload = aes.decrypt(bytes(response[0x38:])) return payload[0x04:] def send_data(self, data): @@ -335,11 +335,11 @@ class rm(device): packet = bytearray(16) packet[0] = 1 response = self.send_packet(0x6a, packet) - err = ord(response[0x22]) | (ord(response[0x23]) << 8) + err = response[0x22] | (response[0x23] << 8) if err == 0: - aes = AES.new(str(self.key), AES.MODE_CBC, str(self.iv)) - payload = aes.decrypt(str(response[0x38:])) - temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + payload = aes.decrypt(bytes(response[0x38:])) + temp = (payload[0x4] * 10 + payload[0x5]) / 10.0 return temp # For legay compatibility - don't use this -- cgit 1.4.1 From c157860f0d54c9e1f2e8ffb7024ea0c5e26e3d1a Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 3 Dec 2016 14:16:59 -0800 Subject: Fix setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 4ac0917f38bb..25f5d5d260e5 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ setup( url='http://github.com/mjg59/python-broadlink', packages=find_packages(), scripts=[], - license=open('LICENSE').read(), description='Python API for controlling Broadlink IR controllers', classifiers=[ 'Development Status :: 4 - Beta', -- cgit 1.4.1 From 31cd07716573a31c5add48a65d4939e7e1076883 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 3 Dec 2016 14:22:20 -0800 Subject: Expose the type of the discovered device Allow clients to identify what kind of device has been discovered --- broadlink/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 12769d13591c..7c3ba81bcb3c 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -123,6 +123,7 @@ def discover(timeout=None): dev = gendevice(devtype, host, mac) devices.append(dev) + class device: def __init__(self, host, mac, timeout=10): self.host = host @@ -136,6 +137,7 @@ class device: 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" def auth(self): payload = bytearray(0x50) @@ -174,6 +176,9 @@ class device: self.id = payload[0x00:0x04] self.key = payload[0x04:0x14] + def get_type(self): + return self.type + def send_packet(self, command, payload): self.count = (self.count + 1) & 0xffff packet = bytearray(0x38) @@ -239,6 +244,7 @@ class device: class sp1(device): def __init__ (self, host, mac): device.__init__(self, host, mac) + self.type = "SP1" def set_power(self, state): packet = bytearray(4) @@ -249,6 +255,7 @@ class sp1(device): class sp2(device): def __init__ (self, host, mac): device.__init__(self, host, mac) + self.type = "SP2" def set_power(self, state): """Sets the power state of the smart plug.""" @@ -271,6 +278,7 @@ class sp2(device): class a1(device): def __init__ (self, host, mac): device.__init__(self, host, mac) + self.type = "A1" def check_sensors(self): packet = bytearray(16) @@ -320,6 +328,7 @@ class a1(device): class rm(device): def __init__ (self, host, mac): device.__init__(self, host, mac) + self.type = "RM2" def check_data(self): packet = bytearray(16) -- cgit 1.4.1 From d10a43531c423f87f2cb7170646c32120476431a Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 3 Dec 2016 16:40:58 -0800 Subject: Fix python 2 compatibility Ugly but appears to work --- broadlink/__init__.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 7c3ba81bcb3c..94446fc2ff74 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -238,7 +238,7 @@ class device: if (time.time() - starttime) < self.timeout: pass raise - return bytes(response[0]) + return bytearray(response[0]) class sp1(device): @@ -289,9 +289,18 @@ class a1(device): data = {} aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) payload = aes.decrypt(bytes(response[0x38:])) - data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 - data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 - light = payload[0x8] + 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: @@ -302,7 +311,6 @@ class a1(device): data['light'] = 'bright' else: data['light'] = 'unknown' - air_quality = payload[0x0a] if air_quality == 0: data['air_quality'] = 'excellent' elif air_quality == 1: @@ -313,7 +321,6 @@ class a1(device): data['air_quality'] = 'bad' else: data['air_quality'] = 'unknown' - noise = payload[0xc] if noise == 0: data['noise'] = 'quiet' elif noise == 1: @@ -358,7 +365,10 @@ class rm(device): if err == 0: aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) payload = aes.decrypt(bytes(response[0x38:])) - temp = (payload[0x4] * 10 + payload[0x5]) / 10.0 + 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 # For legay compatibility - don't use this -- cgit 1.4.1 From 63d184fc8ceb44362e321ed13c3084dfe82e0f8f Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 3 Dec 2016 16:41:10 -0800 Subject: Provide raw sensor data Provide raw versions of the sensor data for interpretation by clients --- broadlink/__init__.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 94446fc2ff74..f4bb1069caa1 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -331,6 +331,29 @@ class a1(device): 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 = {} + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + payload = aes.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 + class rm(device): def __init__ (self, host, mac): -- cgit 1.4.1 From 83f1c3fc93344c0264223fbf37ac0198626e006b Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 3 Dec 2016 17:02:07 -0800 Subject: Bump version --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 25f5d5d260e5..560271fe300a 100644 --- a/setup.py +++ b/setup.py @@ -8,11 +8,11 @@ import warnings dynamic_requires = [] -version = 0.1 +version = 0.2 setup( name='broadlink', - version=0.1, + version=0.2, author='Matthew Garrett', author_email='mjg59@srcf.ucam.org', url='http://github.com/mjg59/python-broadlink', -- cgit 1.4.1 From 43133b5f31bc7c7111cc9cda8f5b515b61093ad9 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Sun, 18 Dec 2016 19:24:06 +0100 Subject: Create requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000000..f7d3e237ac67 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pycrypto==2.6.1 -- cgit 1.4.1 From 5c658b686d95053cf66c85e59f9c3071f0a0241d Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Sun, 18 Dec 2016 19:38:08 +0100 Subject: Update setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 560271fe300a..8b0eeec31d8d 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ setup( url='http://github.com/mjg59/python-broadlink', packages=find_packages(), scripts=[], + install_requires=['pycrypto==2.6.1'], description='Python API for controlling Broadlink IR controllers', classifiers=[ 'Development Status :: 4 - Beta', -- cgit 1.4.1 From 72cf09f9e42a09b2fd806f3d5f08fb4b05ae4aab Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Tue, 20 Dec 2016 15:59:32 +0200 Subject: allow to specify network interface for discover method --- broadlink/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) mode change 100755 => 100644 broadlink/__init__.py diff --git a/broadlink/__init__.py b/broadlink/__init__.py old mode 100755 new mode 100644 index f4bb1069caa1..ffbfd3ad2b1e --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -48,15 +48,16 @@ def gendevice(devtype, host, mac): else: return device(host=host, mac=mac) -def discover(timeout=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] +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(('',0)) + cs.bind((local_ip_address,0)) port = cs.getsockname()[1] starttime = time.time() -- cgit 1.4.1 From 1296c3da483f37b677c7811c182132af7f5aa77e Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Thu, 22 Dec 2016 09:51:38 +0100 Subject: Make communication thread safe --- broadlink/__init__.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index f4bb1069caa1..1ec3c329eae0 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -5,6 +5,7 @@ from Crypto.Cipher import AES import time import random import socket +import threading def gendevice(devtype, host, mac): if devtype == 0: # SP1 @@ -138,6 +139,7 @@ class device: self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) self.cs.bind(('',0)) self.type = "Unknown" + self.lock = threading.Lock() def auth(self): payload = bytearray(0x50) @@ -228,16 +230,17 @@ class device: packet[0x21] = checksum >> 8 starttime = time.time() - while True: - try: - self.cs.sendto(packet, self.host) - self.cs.settimeout(1) - response = self.cs.recvfrom(1024) - break - except socket.timeout: - if (time.time() - starttime) < self.timeout: - pass - raise + with self.lock: + while True: + try: + self.cs.sendto(packet, self.host) + self.cs.settimeout(1) + response = self.cs.recvfrom(1024) + break + except socket.timeout: + if (time.time() - starttime) < self.timeout: + pass + raise return bytearray(response[0]) -- cgit 1.4.1 From d519623dd4507d3648bd361b4507171b98d330df Mon Sep 17 00:00:00 2001 From: Victor Ferrer Date: Sun, 4 Dec 2016 01:50:46 +0100 Subject: Add new device support: Broadlink MP1 Smart power strip --- .gitignore | 1 + README.md | 10 +++++++++ broadlink/__init__.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000000..0d20b6487c61 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/README.md b/README.md index 47a4691f51bd..3d7598f5d88c 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,13 @@ Check power state on a SmartPlug: ``` state = devices[0].check_power() ``` + +Set power state for S1 on a SmartPowerStrip MP1: +``` +devices[0].set_power(1, True) +``` + +Check power state on a SmartPowerStrip: +``` +state = devices[0].check_power() +``` \ No newline at end of file diff --git a/broadlink/__init__.py b/broadlink/__init__.py index f4bb1069caa1..1ebaf9e50f1a 100755 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -45,6 +45,8 @@ 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 + return mp1(host=host, mac=mac) else: return device(host=host, mac=mac) @@ -241,6 +243,65 @@ class device: return bytearray(response[0]) +class mp1(device): + def __init__ (self, host, mac): + device.__init__(self, host, mac) + 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(self): + """Returns the power state of the smart power strip.""" + 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: + aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) + payload = aes.decrypt(bytes(response[0x38:])) + if type(payload[0x4]) == int: + state = payload[0x0e] + else: + state = ord(payload[0x0e]) + 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): device.__init__(self, host, mac) -- cgit 1.4.1 From b30fc0d1c196320751d196d9b1d62843163b197d Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Tue, 20 Dec 2016 08:27:12 +0100 Subject: Ensure discover() returns devices and not None There's a small race condition in the discover() method. If recv() returns before the socket timeout is reached, but we hit the top of the loop after the timeout is reached, we'll return None rather than the set of devices. --- broadlink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 37a7fcbec26c..12d8b30d518f 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -126,6 +126,7 @@ def discover(timeout=None, local_ip_address=None): mac = responsepacket[0x3a:0x40] dev = gendevice(devtype, host, mac) devices.append(dev) + return devices class device: -- cgit 1.4.1 From 39e170c9e0dedd109f90aae2bafb3f0ee83fc8b1 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Mon, 26 Dec 2016 20:02:04 +0100 Subject: validate the authorization --- broadlink/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 12d8b30d518f..bfa30527a459 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -179,8 +179,16 @@ class device: aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) payload = aes.decrypt(bytes(enc_payload)) + if not payload: + return False + + key = payload[0x04:0x14] + if len(key) % 16 != 0: + return False + self.id = payload[0x00:0x04] - self.key = payload[0x04:0x14] + self.key = key + return True def get_type(self): return self.type -- cgit 1.4.1 From 63e097d73fa43d4f2e1abcaea103af9007ee1532 Mon Sep 17 00:00:00 2001 From: Víctor Ferrer García Date: Tue, 3 Jan 2017 06:31:04 +0100 Subject: check_power_raw included for mp1 devices Export raw power state for MP1 devices --- broadlink/__init__.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index bfa30527a459..d5cc8d28caea 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -286,8 +286,8 @@ class mp1(device): sid_mask = 0x01 << (sid - 1) return self.set_power_mask(sid_mask, state) - def check_power(self): - """Returns the power state of the smart power strip.""" + 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 @@ -307,12 +307,17 @@ class mp1(device): state = payload[0x0e] else: state = ord(payload[0x0e]) - data = {} - data['s1'] = bool(state & 0x01) - data['s2'] = bool(state & 0x02) - data['s3'] = bool(state & 0x04) - data['s4'] = bool(state & 0x08) - return data + 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): -- cgit 1.4.1 From 7a852b20840aab131ba59aaf85f5e67831b7ccc3 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Thu, 29 Dec 2016 14:27:07 -0600 Subject: Bump version to 0.3 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8b0eeec31d8d..a09222a0ce6a 100644 --- a/setup.py +++ b/setup.py @@ -8,11 +8,11 @@ import warnings dynamic_requires = [] -version = 0.2 +version = 0.3 setup( name='broadlink', - version=0.2, + version=0.3, author='Matthew Garrett', author_email='mjg59@srcf.ucam.org', url='http://github.com/mjg59/python-broadlink', -- cgit 1.4.1 From 4f03ffb963d65b65e2d80b5fdcf24b2f02d13e14 Mon Sep 17 00:00:00 2001 From: Thomas Järvstrand Date: Sat, 22 Apr 2017 21:34:37 +0200 Subject: 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 d5cc8d28caea..a6f2c36d6b69 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 @@ class device: 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 @@ class device: 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 @@ class device: 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 @@ class mp1(device): 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 @@ class sp2(device): 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 @@ class a1(device): 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 @@ class a1(device): 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 @@ class rm(device): 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 @@ class rm(device): 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 a09222a0ce6a..912cd301ac34 100644 --- a/setup.py +++ b/setup.py @@ -6,19 +6,23 @@ from setuptools import setup, find_packages 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', -- cgit 1.4.1 From d346476e6397be9f428c38ac7fe567e91ba3dab1 Mon Sep 17 00:00:00 2001 From: Dimitrij Date: Sat, 22 Apr 2017 22:36:50 +0300 Subject: 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 a6f2c36d6b69..495e07c99f0b 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -374,7 +374,11 @@ class sp2(device): 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): -- cgit 1.4.1 From 19851ed423733e90f4e71f8c585fcf3b1b537b49 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Sat, 22 Apr 2017 21:38:02 +0200 Subject: 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 495e07c99f0b..f875f5ba9013 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -274,9 +274,8 @@ class device: 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]) -- cgit 1.4.1 From 2388808f413f6be0f478c8f7e807d21d1bdbfe76 Mon Sep 17 00:00:00 2001 From: wind-rider Date: Sat, 4 Feb 2017 14:43:06 +0100 Subject: 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 1888a19d5337..7a9e23412841 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. -- cgit 1.4.1 From 3c44422b607ddd09a6a48016e88aabb826ec72d5 Mon Sep 17 00:00:00 2001 From: wind-rider Date: Sat, 4 Feb 2017 14:43:50 +0100 Subject: Update protocol.md --- protocol.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/protocol.md b/protocol.md index 7a9e23412841..387a6e79da2f 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| -- cgit 1.4.1 From 846cc353665154d7f38623154abe6cada0bf6a45 Mon Sep 17 00:00:00 2001 From: tobiaswaldvogel Date: Sat, 22 Apr 2017 21:41:30 +0200 Subject: 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 387a6e79da2f..fc58c32e55c5 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 -- cgit 1.4.1 From 76dd4cfc70fa95ae703c680998757be180deed36 Mon Sep 17 00:00:00 2001 From: Ivan Martinez Date: Tue, 10 Jan 2017 22:55:02 -0200 Subject: 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 000000000000..0dc6db66170b --- /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 000000000000..84d96dfec668 --- /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) -- cgit 1.4.1 From de7c5c2a2af68b88aff982d7a1fec5b3dcfd7d3b Mon Sep 17 00:00:00 2001 From: "Ivan F. Martinez" Date: Tue, 10 Jan 2017 22:57:35 -0200 Subject: 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 000000000000..47b45f4c97d3 --- /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 +``` -- cgit 1.4.1 From 5195856200587cc087e7af52db55e1e559e083bf Mon Sep 17 00:00:00 2001 From: Ivan Martinez Date: Sun, 16 Apr 2017 11:34:31 -0300 Subject: 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 0dc6db66170b..c99a5260479b 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 + -- cgit 1.4.1 From d989c27d36aa2b3b2da93be6edb8171221a88096 Mon Sep 17 00:00:00 2001 From: Aydaen Lynch Date: Sat, 22 Apr 2017 15:48:02 -0400 Subject: 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 3d7598f5d88c..1305ea0f5a7b 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 f875f5ba9013..68ad55990752 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -503,3 +503,40 @@ class rm2(rm): 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 fc58c32e55c5..e9ac99b13c3d 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. + -- cgit 1.4.1 From babb3f83d375bd69e6d5048c69c2051141b5b674 Mon Sep 17 00:00:00 2001 From: Robert Sullivan Date: Tue, 25 Apr 2017 15:33:00 +0100 Subject: 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 68ad55990752..3fe28a81c920 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 912cd301ac34..ed627fe13b73 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ import warnings 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 -- cgit 1.4.1 From f7e30344c5748e37924c8a4f28522c2bc99a52ee Mon Sep 17 00:00:00 2001 From: kost Date: Sun, 7 May 2017 20:32:52 +0200 Subject: 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 3fe28a81c920..e757f90005a8 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -245,6 +245,11 @@ class device: 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)) -- cgit 1.4.1 From 2e2c8ef1a15175cf311f757a52e7c561f834f68b Mon Sep 17 00:00:00 2001 From: mob41 Date: Wed, 14 Jun 2017 10:30:27 +0800 Subject: [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 e757f90005a8..1b65345b47b1 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -248,7 +248,7 @@ class device: # 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 ed627fe13b73..1971c92b2f0d 100644 --- a/setup.py +++ b/setup.py @@ -12,11 +12,11 @@ try: 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', -- cgit 1.4.1 From 3d4789305e64a842c2b1a86d4b62ade3fa38d2a2 Mon Sep 17 00:00:00 2001 From: Julian Pastarmov Date: Sat, 25 Nov 2017 21:04:10 +0100 Subject: 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 84d96dfec668..13c65e7a7f14 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) -- cgit 1.4.1 From 4f902342e70fd2528c976abd9a4478aba24de861 Mon Sep 17 00:00:00 2001 From: Bengt Martensson Date: Sat, 25 Nov 2017 21:06:12 +0100 Subject: 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 c99a5260479b..f5575b565980 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: -- cgit 1.4.1 From 1d7fba3d06af33b6af3d51b87a5c66c32751433d Mon Sep 17 00:00:00 2001 From: hackers365 Date: Sat, 17 Jun 2017 17:24:18 +0800 Subject: 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 1b65345b47b1..f17eabb3798f 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) -- cgit 1.4.1 From 626b459b5b13931008fdaaf3b6261dd8bc9c2e33 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Sat, 25 Nov 2017 22:08:58 +0200 Subject: SP3S support (#117) --- broadlink/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index f17eabb3798f..eb945a7d96fe 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 -- cgit 1.4.1 From 2160488da23f54adc6dfc8728167385e2c59eb45 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Sun, 16 Jul 2017 02:30:22 +0300 Subject: sp2.get_energy --- broadlink/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index eb945a7d96fe..ee74b8843fd0 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -386,6 +386,16 @@ class sp2(device): 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) -- cgit 1.4.1 From 71f320638ea59dce4d8dcc8a707912a19005afe2 Mon Sep 17 00:00:00 2001 From: Michael Kaiser Date: Sat, 25 Nov 2017 21:14:12 +0100 Subject: 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 e9ac99b13c3d..2e388d749963 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 -- cgit 1.4.1 From c85f6ac213c5bf12749ab3460700e051ad194125 Mon Sep 17 00:00:00 2001 From: blapid Date: Sat, 25 Nov 2017 22:14:34 +0200 Subject: 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 f5575b565980..3acf168a920d 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 13c65e7a7f14..4a7438dfb7a4 100755 --- a/cli/broadlink_discovery +++ b/cli/broadlink_discovery @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import broadlink import time -- cgit 1.4.1 From b8cf8d073e409fafef50ad0fb5435cb23d016bb2 Mon Sep 17 00:00:00 2001 From: jazzina Date: Sat, 25 Nov 2017 12:20:46 -0800 Subject: 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 ee74b8843fd0..ea4cbc5fab34 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -11,6 +11,7 @@ import random 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 @@ class device: 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 @@ class rm2(rm): 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): -- cgit 1.4.1 From 8bc67af6d73c75587d40f6dbfa3b975c7d69a8a4 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 25 Nov 2017 12:21:48 -0800 Subject: Bump version --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1971c92b2f0d..f8d1f4cab807 100644 --- a/setup.py +++ b/setup.py @@ -12,11 +12,11 @@ try: 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', -- cgit 1.4.1 From dd6eb8767e7a7f1f54aa1e7a7b57309bc4fc5fba Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 25 Nov 2017 12:38:57 -0800 Subject: 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 1305ea0f5a7b..48708827ce36 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 ea4cbc5fab34..a4c8a4841aff 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -501,6 +501,33 @@ class rm(device): 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 -- cgit 1.4.1 From 9ff6fa817b6d4314fcc9b805e480290f3a1ba20e Mon Sep 17 00:00:00 2001 From: Sergey Bogatyrets Date: Sun, 26 Nov 2017 22:10:19 +0300 Subject: 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 3acf168a920d..e9e63d5718aa 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..." - -- cgit 1.4.1 From 82172f54ab722fb9de998d0ae4c24337e41d5787 Mon Sep 17 00:00:00 2001 From: Aleksandr Smirnov Date: Mon, 25 Dec 2017 02:34:37 +0200 Subject: 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 ea4cbc5fab34..dbabce0b3494 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 @@ class S1C(device): 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): -- cgit 1.4.1 From dd0e9083172d1ee54eec167d342edb3b157aea4c Mon Sep 17 00:00:00 2001 From: Valter Vicente Date: Mon, 25 Dec 2017 00:35:09 +0000 Subject: 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 e9e63d5718aa..097a7afbdae7 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 * ==' -- cgit 1.4.1 From 4e33ef446520275e0a9426dc8182dcc3af276da3 Mon Sep 17 00:00:00 2001 From: Brent Avery Date: Tue, 30 Jan 2018 07:45:21 +1100 Subject: 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 a4c8a4841aff..8a193d556101 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -503,7 +503,12 @@ class rm(device): 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 @@ class rm(device): 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 3acf168a920d..dca0a3d6b639 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 -- cgit 1.4.1 From 51ff890c7ca7f5569497b47db6497e908a94267a Mon Sep 17 00:00:00 2001 From: Dominik Lakatoš Date: Thu, 8 Feb 2018 13:47:28 +0100 Subject: 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 dbabce0b3494..50d9f940119c 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -397,7 +397,10 @@ class sp2(device): 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 -- cgit 1.4.1 From 38aa6dc37e28d7a5ab1c91b120baa76dcb8dd754 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Feb 2018 23:56:16 -0800 Subject: 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 f7d3e237ac67..dc83835dc08c 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 f8d1f4cab807..c5c7e267d024 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ try: 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 -- cgit 1.4.1 From d186332b965c32a7f6defd4eb340b46901d99c82 Mon Sep 17 00:00:00 2001 From: Steven Barthen Date: Sat, 10 Mar 2018 21:10:33 +0900 Subject: 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 dbabce0b3494..f85d4e14932d 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 -- cgit 1.4.1 From 26109aac67dc39b1c4dff3d1e971d463fe221847 Mon Sep 17 00:00:00 2001 From: marconfus Date: Sun, 18 Mar 2018 22:54:17 +0100 Subject: 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 592acf4f86d0..e9c1307d8104 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -167,7 +167,7 @@ class device: 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: -- cgit 1.4.1 From 21d96bd140359a3b360bd728b963effc6b747c4f Mon Sep 17 00:00:00 2001 From: Marcin Koperski Date: Sun, 18 Mar 2018 22:55:03 +0100 Subject: 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 097a7afbdae7..8d055f7019b8 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 *' -- cgit 1.4.1 From 8754493951b887e850d93100898d6883443b124f Mon Sep 17 00:00:00 2001 From: Nightreaver Date: Mon, 19 Mar 2018 06:58:15 +0900 Subject: 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 e9c1307d8104..979e608007b9 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -15,63 +15,63 @@ import codecs 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 @@ class device: 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 @@ class mp1(device): 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 @@ class sp1(device): 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 @@ class sp2(device): 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 @@ class a1(device): 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 @@ class rm(device): # 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 @@ class S1C(device): 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 4a7438dfb7a4..385f1932467f 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 "" -- cgit 1.4.1 From 39cc64efcea723cdc5e97220bd345535f558bb8f Mon Sep 17 00:00:00 2001 From: Peter Windridge Date: Sun, 18 Mar 2018 22:03:26 +0000 Subject: 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 979e608007b9..2af62aabd0cf 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 @@ class device: self.id = payload[0x00:0x04] self.key = key + return True def get_type(self): @@ -530,7 +536,8 @@ class rm(device): 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 @@ class rm2(rm): 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 @@ class dooya(device): 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 c5c7e267d024..b982a11a0147 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,9 @@ try: except ImportError as e: dynamic_requires = ['pycryptodome==3.4.11'] +# For Hysen thermostatic heating controller +dynamic_requires.append('PyCRC') + version = 0.6 setup( -- cgit 1.4.1 From 33a2e4ae54e996d37c814494f5da629fb5428f96 Mon Sep 17 00:00:00 2001 From: Nightreaver Date: Mon, 19 Mar 2018 07:03:46 +0900 Subject: 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 2af62aabd0cf..910184a025dd 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -391,7 +391,20 @@ class sp2(device): """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 @@ class sp2(device): 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 8d055f7019b8..4c0c5d7a723f 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) -- cgit 1.4.1 From 8d3c9f1e9f7ad19f1c054ca3278670c8c9ea0eb1 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 18 Mar 2018 14:51:34 -0700 Subject: Version 0.7 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index b982a11a0147..a79afb5558f7 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ except ImportError as e: # 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', -- cgit 1.4.1 From 8b593beff968caf47bf19e703e8fc3b1df3d5e6a Mon Sep 17 00:00:00 2001 From: clach04 Date: Sun, 18 Mar 2018 15:12:20 -0700 Subject: 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 910184a025dd..6a6636d1a3ef 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -182,11 +182,11 @@ class device: 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 @@ class device: # 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)): -- cgit 1.4.1 From 9286d9a1d9d133d9897becf3898025d822a7d841 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 18 Mar 2018 15:12:47 -0700 Subject: Version 0.8 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a79afb5558f7..a9382c109f4c 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ except ImportError as e: # 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', -- cgit 1.4.1 From 53b213ea85e0b7c9052f90ba4e5c3289097e10e9 Mon Sep 17 00:00:00 2001 From: Michael Still Date: Mon, 19 Mar 2018 18:19:51 +1100 Subject: 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 1305ea0f5a7b..6cc123696139 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 +``` -- cgit 1.4.1 From e9748ff0b4f36da2722ef0f84b812069812634af Mon Sep 17 00:00:00 2001 From: Aleksey Bogomolov Date: Sat, 24 Mar 2018 19:05:44 +0300 Subject: 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 6a6636d1a3ef..48e936ba0edd 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 @@ class hysen(device): # 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) -- cgit 1.4.1 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: Update __init__.py --- broadlink/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/broadlink/__init__.py b/broadlink/__init__.py index 6a6636d1a3ef..5dc29192c75e 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 -- cgit 1.4.1 From 90af3b743719e84ed158448534267857a46fa794 Mon Sep 17 00:00:00 2001 From: lordneon Date: Sun, 15 Apr 2018 10:19:05 +0000 Subject: 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 a3fadf5dd93e..a5415c23dc7a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -417,10 +417,16 @@ class sp2(device): 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 @@ class sp2(device): 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): -- cgit 1.4.1 From e724aec007680efbe07f871cd76070c1abeb1712 Mon Sep 17 00:00:00 2001 From: lordneon Date: Sun, 15 Apr 2018 10:19:05 +0000 Subject: 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 a3fadf5dd93e..a5415c23dc7a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -417,10 +417,16 @@ class sp2(device): 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 @@ class sp2(device): 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): -- cgit 1.4.1 From b2c4bed94a3fb942797bba6c46717edf8187be1f Mon Sep 17 00:00:00 2001 From: Tocho Tochev Date: Thu, 19 Apr 2018 23:45:50 +0300 Subject: 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 6cc123696139..74473b8e3063 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 47b45f4c97d3..a04b65548701 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 4c0c5d7a723f..4b0e81f67efd 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() -- cgit 1.4.1 From 766b7b00fb1cec868e3d5fca66f1aada208959ce Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Tue, 24 Apr 2018 08:33:05 -0700 Subject: Version 0.9 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a9382c109f4c..23fb5de1fc54 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ except ImportError as e: # 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', -- cgit 1.4.1 From 56b2ac36e5a2359272f4af8a49cfaf3e1891733a Mon Sep 17 00:00:00 2001 From: Mayeul Cantan Date: Mon, 30 Apr 2018 23:06:19 +0200 Subject: 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 a5415c23dc7a..9dccc8d4b2d0 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -13,69 +13,50 @@ import sys 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: -- cgit 1.4.1 From 71fbb2bcdec24f05360b1aa2967d676f313d6ba4 Mon Sep 17 00:00:00 2001 From: Faustino Aguilar Date: Sat, 14 Jul 2018 19:01:49 -0500 Subject: 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 4b0e81f67efd..54f02a0787ba 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]) -- cgit 1.4.1 From 8cfa02035381233c148096a319a577e9b3715c6f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Aug 2018 11:01:42 +0200 Subject: Update vulnerable pycryptodome --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 23fb5de1fc54..f4482eff56d1 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ try: 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') -- cgit 1.4.1 From cd969a83f93c780d7c5d18a5ec072c1aeafd0270 Mon Sep 17 00:00:00 2001 From: clach04 Date: Thu, 30 Aug 2018 22:43:42 -0700 Subject: readme typo --- cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/README.md b/cli/README.md index a04b65548701..5d7b3be19302 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 -- cgit 1.4.1 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: 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 9dccc8d4b2d0..02a8e2a8e40f 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -399,12 +399,12 @@ class sp2(device): 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 @@ class sp2(device): 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 -- cgit 1.4.1 From bb23c84c90110984fdcd743e60ffd9a31785fd96 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 2 Nov 2018 11:57:07 +0100 Subject: Update requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dc83835dc08c..9b20c33eb44b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pycryptodome==3.4.11 +pycryptodome==3.6.6 -- cgit 1.4.1 From 694b4423b50cc55f9a48d14a41f1615799342c43 Mon Sep 17 00:00:00 2001 From: Sergey Prilukin Date: Sun, 25 Nov 2018 23:58:33 +0200 Subject: 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 dca0a3d6b639..1ff45f1d9b3c 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) -- cgit 1.4.1 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: 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 02a8e2a8e40f..14c1a3bfa805 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 -- cgit 1.4.1 From c393cf6079ccaa24afd61a8e81c051251198f73b Mon Sep 17 00:00:00 2001 From: Pim van den Berg Date: Wed, 16 Jan 2019 09:34:20 +0100 Subject: 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 54f02a0787ba..a4d9a20d24d7 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 *') -- cgit 1.4.1 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: 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 f4482eff56d1..a9af7823b256 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ except ImportError as e: # 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', -- cgit 1.4.1 From 77f11c8d49255d6f41c35f0aefb87d648af66cfd Mon Sep 17 00:00:00 2001 From: gpenverne Date: Fri, 10 May 2019 20:19:09 +0200 Subject: 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 14c1a3bfa805..3d544147c8ec 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 @@ class hysen(device): 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 @@ class hysen(device): # 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 @@ class hysen(device): 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 @@ class hysen(device): # 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 @@ class hysen(device): 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 @@ class hysen(device): # 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) -- cgit 1.4.1 From 852cbc24734bf3f7ac03080aa37374276f7a2856 Mon Sep 17 00:00:00 2001 From: Bartosz Fenski Date: Sat, 18 May 2019 09:07:25 +0200 Subject: 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 a4d9a20d24d7..9317cd197c21 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: -- cgit 1.4.1 From 0db317962348907096a29bb8d3b85c6c1d0ee35e Mon Sep 17 00:00:00 2001 From: ninstein Date: Thu, 7 Feb 2019 04:31:47 +0800 Subject: 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 3d544147c8ec..98b6ba5958ae 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 -- cgit 1.4.1 From 508b0563eda70a67a04df3986c5ea95723d1c31d Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Sat, 18 May 2019 17:11:30 +0200 Subject: 0.10 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a9af7823b256..d85875e201e9 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ except ImportError as e: # 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', -- cgit 1.4.1 From c9a1c106a74ba0dc0b565c39ddf37939a8310935 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Sat, 18 May 2019 17:13:33 +0200 Subject: Update setup.py --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d85875e201e9..00169f44619a 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,11 @@ except ImportError as e: # 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', -- cgit 1.4.1 From a75f98720ec22e9857ef815f594952f34ed5485c Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Sun, 19 May 2019 17:54:14 +0200 Subject: 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 271a1860bfc9..582f64d7fe82 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 @@ except ImportError as e: 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 @@ S1C_SENSORS_TYPES = { 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)) -- cgit 1.4.1 From b8fdf337c9a60d74437bf88fa07134e651f7c0fa Mon Sep 17 00:00:00 2001 From: AnilDaoud Date: Wed, 22 May 2019 13:27:34 +0800 Subject: 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 582f64d7fe82..c3b2cecd1a7d 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) -- cgit 1.4.1 From ef66997953cb2beb05e05b81096b04a79d0f59ec Mon Sep 17 00:00:00 2001 From: AnilDaoud Date: Wed, 22 May 2019 13:28:02 +0800 Subject: 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 385f1932467f..2bb8d998d93f 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)) -- cgit 1.4.1 From 45e26fda237441777d76f5b7362c84ce9f757c89 Mon Sep 17 00:00:00 2001 From: AnilDaoud Date: Thu, 23 May 2019 21:13:14 +0800 Subject: 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 1c3e231d69e5..1a8fe5703468 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: -- cgit 1.4.1 From 38a40c5a19c05039e5c92ced1f553971ccd70d9e Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Mon, 27 May 2019 20:57:32 +0200 Subject: 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 c3b2cecd1a7d..7a385fc1556e 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 @@ class device: 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 @@ class device: 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 @@ class device: return False self.id = payload[0x00:0x04] - self.key = key + self.update_aes(key) return True @@ -278,7 +289,7 @@ class device: 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 @@ class device: 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 @@ class hysen(device): # 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 @@ class S1C(device): 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 9b20c33eb44b..09f445bfd823 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 00169f44619a..0a31dbbb07e1 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') -- cgit 1.4.1 From c94838f56140338455b8cc4011d14c24551da50e Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Mon, 27 May 2019 20:57:57 +0200 Subject: 0.11 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0a31dbbb07e1..d2886298e91c 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ except ImportError: # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') -version = '0.10' +version = '0.11' setup( name='broadlink', -- cgit 1.4.1 From 9ec2da834b2703e1b07bb40452c0b8c148509f60 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Mon, 10 Jun 2019 09:12:50 +0200 Subject: 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 7a385fc1556e..4f1cc823e668 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -357,6 +357,8 @@ class mp1(device): 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) -- cgit 1.4.1 From b6fd48db5d60d374eb4642b87757be56d42a83f7 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Mon, 10 Jun 2019 09:13:18 +0200 Subject: 0.11.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d2886298e91c..bc6495797bdf 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ except ImportError: # For Hysen thermostatic heating controller dynamic_requires.append('PyCRC') -version = '0.11' +version = '0.11.1' setup( name='broadlink', -- cgit 1.4.1 From 9d9b49c3db96a8e92c3b267aa07f764eff659e2b Mon Sep 17 00:00:00 2001 From: Johnson Chin Date: Tue, 25 Jun 2019 02:06:50 +0800 Subject: 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 4f1cc823e668..1517868784f1 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 ], -- cgit 1.4.1 From 1cea255dce6b966c1c7745a7a3327569735ed18e Mon Sep 17 00:00:00 2001 From: carlos-alarcon Date: Mon, 29 Jul 2019 08:39:50 +0200 Subject: 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 2bb8d998d93f..74f36ed51fa5 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)) + -- cgit 1.4.1 From 11c5981793f4a9b23ebceaf0ac2efdd74187c192 Mon Sep 17 00:00:00 2001 From: Leonardo Brondani Schenkel Date: Fri, 16 Aug 2019 11:13:53 +0200 Subject: 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 1517868784f1..8b08ffc5fee2 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) -- cgit 1.4.1 From 2e5361bd8ee470b35884538914a2e3bc579ec1c5 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Wed, 2 Oct 2019 09:25:27 +0300 Subject: 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 8b08ffc5fee2..558305374a13 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -7,11 +7,8 @@ import threading 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 @@ class device: 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 1a8fe5703468..976b5373aa18 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 74f36ed51fa5..1edbafcbecc6 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 bc6495797bdf..ca48ea29b33c 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 @@ setup( 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', -- cgit 1.4.1 From 56e444cacf67c1bfb1337060bb2d39870dcbfe3f Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Wed, 2 Oct 2019 09:26:01 +0300 Subject: 0.12.0 (#285) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ca48ea29b33c..25cc0fc52b30 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', -- cgit 1.4.1 From cbb1d67df81cb27a213430f6b9924b386aa4c1f7 Mon Sep 17 00:00:00 2001 From: Liran BG Date: Wed, 9 Oct 2019 16:44:40 +0300 Subject: 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 976b5373aa18..e608544364a6 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: -- cgit 1.4.1 From 660c2269e4e9f999ad1f383b7aa128511f7ec431 Mon Sep 17 00:00:00 2001 From: Barnaby Gray Date: Sun, 13 Oct 2019 22:12:13 +0100 Subject: 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 558305374a13..be0ca7edec55 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 @@ class mp1(device): 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: 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 be0ca7edec55..3190a7b7446c 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -358,7 +358,11 @@ class bg1(device): 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) -- cgit 1.4.1 From 8e7446f4108db748e82a68e41b254e80b01bd51b Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Wed, 4 Mar 2020 22:24:09 +0100 Subject: 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 3190a7b7446c..1c622301e31a 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -8,6 +8,7 @@ import struct 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 @@ class device: # 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 @@ class bg1(device): 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 -- cgit 1.4.1 From ec4df3966553250ec0973d50442f024aa0989468 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Wed, 4 Mar 2020 22:24:33 +0100 Subject: 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 1c622301e31a..41c8010cd6e2 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 -- cgit 1.4.1 From e84becd05b3bb879899bb7a8492e1441af3b491e Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Wed, 4 Mar 2020 22:25:00 +0100 Subject: 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 41c8010cd6e2..4c0ea4b13719 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 1edbafcbecc6..1c6b80b1483e 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("###########################################") -- cgit 1.4.1 From 654db0935bf41b164bb6a1caf84d155270201dd3 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Wed, 4 Mar 2020 22:26:23 +0100 Subject: 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 4c0ea4b13719..e2207ec42d08 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 -- cgit 1.4.1 From e151a14a7bea0084dcb1bb1de0ca30210b822c66 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Wed, 4 Mar 2020 22:27:22 +0100 Subject: 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 e608544364a6..2de44aa4357a 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: -- cgit 1.4.1 From af95fa2446b7d8bf804c827e7bf4a8a9e67b4026 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Wed, 4 Mar 2020 22:27:55 +0100 Subject: 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 5d7b3be19302..7e229e3eb557 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 2de44aa4357a..5045c5c1082f 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) -- cgit 1.4.1 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: 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 e2207ec42d08..4bf94473a255 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 @@ class device: 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 @@ class device: 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 @@ class device: # 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): -- cgit 1.4.1 From 84e53c8d2b31aa3c31c6e147dd02cf7044bd8765 Mon Sep 17 00:00:00 2001 From: hakana Date: Mon, 16 Mar 2020 19:56:06 +0100 Subject: 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 4bf94473a255..3a99b03ab72d 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 -- cgit 1.4.1 From 4a3950a7c50f574530d9085782d2cff1c3df5c4f Mon Sep 17 00:00:00 2001 From: hakana Date: Mon, 23 Mar 2020 21:23:05 +0100 Subject: 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 3a99b03ab72d..627389bd9462 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -635,21 +635,23 @@ class rm(device): 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 @@ class rm4(rm): 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): -- cgit 1.4.1 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: 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 627389bd9462..a8010574e365 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -14,7 +14,7 @@ from cryptography.hazmat.backends import default_backend 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 @@ class device: 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 @@ class mp1(device): 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 @@ class bg1(device): 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 @@ class sp1(device): 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 @@ class sp2(device): 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 @@ class a1(device): 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 @@ class rm(device): 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 @@ class rm2(rm): 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 @@ class S1C(device): 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): -- cgit 1.4.1 From 0bd58c6f598fe7239246ad9d61508febea625423 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Tue, 24 Mar 2020 16:09:53 +0100 Subject: 0.13.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 25cc0fc52b30..6183611dedb4 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', -- cgit 1.4.1 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: 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 a8010574e365..9fdee5f3546d 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 -- cgit 1.4.1 From 2bc7b06c69487b7750792e1f913905aecd46da3c Mon Sep 17 00:00:00 2001 From: csabavirag Date: Sun, 5 Apr 2020 19:14:47 +0200 Subject: 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 9fdee5f3546d..c9e581fd6c78 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 @@ class dooya(device): 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) -- cgit 1.4.1 From a6827caaf7d796145649d3de69ee5e5bb2f43e66 Mon Sep 17 00:00:00 2001 From: majuss Date: Fri, 10 Apr 2020 09:23:15 +0200 Subject: 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 c9e581fd6c78..49309c21f12f 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -701,10 +701,44 @@ class hysen(device): # 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 @@ class hysen(device): 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 6183611dedb4..b7fe6b6f221e 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( 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', -- cgit 1.4.1 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: Adding new supported devices (#325) --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d5e0154db221..8faba2be75fc 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. -- cgit 1.4.1 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: 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 49309c21f12f..ede6c3129ae6 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -265,7 +265,7 @@ class device: # 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 -- cgit 1.4.1 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: 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 ede6c3129ae6..1a3873ed4fe6 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 -- cgit 1.4.1 From 2f1ac5c9d32dfbfc8ea43023c070cc8b8dc9d8d0 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Thu, 16 Apr 2020 17:01:30 +0200 Subject: 0.13.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b7fe6b6f221e..9afeef02f92b 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', -- cgit 1.4.1 From ed87b8d1de4f5f7ba45dc45bf77c2deeb474a828 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Sun, 19 Apr 2020 08:49:55 +0200 Subject: 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 1a3873ed4fe6..b8f191fb47dd 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -739,7 +739,7 @@ class hysen(device): 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 @@ class hysen(device): 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] -- cgit 1.4.1 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: 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 b8f191fb47dd..3a78b03b8971 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 -- cgit 1.4.1 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: 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 3a78b03b8971..5253d7cfdab8 100644 --- a/broadlink/__init__.py +++ b/broadlink/__init__.py @@ -8,7 +8,6 @@ import struct 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 @@ class device: 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 @@ class device: 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 @@ class bg1(device): 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 @@ class lb1(device): 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 -- cgit 1.4.1 From 17968ef4d40e7cb484909b3ddad33d7795b80091 Mon Sep 17 00:00:00 2001 From: Daniel Høyer Iversen Date: Tue, 21 Apr 2020 07:50:23 +0200 Subject: 0.13.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9afeef02f92b..778f495fb58a 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', -- cgit 1.4.1