From 69fb01b21b4df45b89426305b80d455e23612bac Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Sun, 10 Apr 2022 17:35:09 +0300 Subject: [PATCH 1/4] Moving to python-broadlink 0.18.0 --- broadlinkmanager/broadlink/__init__.py | 1328 +++-------------- .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 6403 bytes .../__pycache__/alarm.cpython-36.pyc | Bin 0 -> 1540 bytes .../__pycache__/climate.cpython-36.pyc | Bin 0 -> 5654 bytes .../__pycache__/const.cpython-36.pyc | Bin 0 -> 305 bytes .../__pycache__/cover.cpython-36.pyc | Bin 0 -> 2017 bytes .../__pycache__/device.cpython-36.pyc | Bin 0 -> 9563 bytes .../__pycache__/exceptions.cpython-36.pyc | Bin 0 -> 6215 bytes .../__pycache__/helpers.cpython-36.pyc | Bin 0 -> 1337 bytes .../__pycache__/light.cpython-36.pyc | Bin 0 -> 6393 bytes .../__pycache__/protocol.cpython-36.pyc | Bin 0 -> 1973 bytes .../__pycache__/remote.cpython-36.pyc | Bin 0 -> 5196 bytes .../__pycache__/sensor.cpython-36.pyc | Bin 0 -> 1610 bytes .../__pycache__/switch.cpython-36.pyc | Bin 0 -> 11788 bytes broadlinkmanager/broadlink/alarm.py | 43 + broadlinkmanager/broadlink/climate.py | 228 +++ broadlinkmanager/broadlink/const.py | 5 + broadlinkmanager/broadlink/cover.py | 57 + broadlinkmanager/broadlink/device.py | 332 +++++ broadlinkmanager/broadlink/exceptions.py | 145 +- broadlinkmanager/broadlink/helpers.py | 55 +- broadlinkmanager/broadlink/light.py | 197 +++ broadlinkmanager/broadlink/protocol.py | 50 + broadlinkmanager/broadlink/remote.py | 126 ++ broadlinkmanager/broadlink/sensor.py | 47 + broadlinkmanager/broadlink/switch.py | 362 +++++ broadlinkmanager/broadlinkmanager.py | 292 ++-- broadlinkmanager/calls.php | 154 ++ broadlinkmanager/calls.py | 38 + 29 files changed, 2156 insertions(+), 1303 deletions(-) create mode 100644 broadlinkmanager/broadlink/__pycache__/__init__.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/alarm.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/climate.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/const.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/cover.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/device.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/exceptions.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/helpers.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/light.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/protocol.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/remote.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/sensor.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/__pycache__/switch.cpython-36.pyc create mode 100644 broadlinkmanager/broadlink/alarm.py create mode 100644 broadlinkmanager/broadlink/climate.py create mode 100644 broadlinkmanager/broadlink/const.py create mode 100644 broadlinkmanager/broadlink/cover.py create mode 100644 broadlinkmanager/broadlink/device.py create mode 100644 broadlinkmanager/broadlink/light.py create mode 100644 broadlinkmanager/broadlink/protocol.py create mode 100644 broadlinkmanager/broadlink/remote.py create mode 100644 broadlinkmanager/broadlink/sensor.py create mode 100644 broadlinkmanager/broadlink/switch.py create mode 100644 broadlinkmanager/calls.php create mode 100644 broadlinkmanager/calls.py diff --git a/broadlinkmanager/broadlink/__init__.py b/broadlinkmanager/broadlink/__init__.py index e4e19f6..fc64e84 100644 --- a/broadlinkmanager/broadlink/__init__.py +++ b/broadlinkmanager/broadlink/__init__.py @@ -1,1112 +1,247 @@ -#!/usr/bin/python - -import codecs -import json -import random +#!/usr/bin/env python3 +"""The python-broadlink library.""" import socket -import struct -import threading -import time -from datetime import datetime - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - -from .exceptions import check_error, exception -from .helpers import get_local_ip - - -def get_devices(): - return { - 0x0000: (sp1, "SP1", "Broadlink"), - - 0x2711: (sp2, "SP2", "Broadlink"), - 0x2719: (sp2, "SP2-compatible", "Honeywell"), - 0x271a: (sp2, "SP2-compatible", "Honeywell"), - 0x2720: (sp2, "SP mini", "Broadlink"), - 0x2728: (sp2, "SP2-compatible", "URANT"), - 0x2733: (sp2, "SP3", "Broadlink"), - 0x2736: (sp2, "SP mini+", "Broadlink"), - 0x273e: (sp2, "SP mini", "Broadlink"), - 0x7530: (sp2, "SP2", "Broadlink (OEM)"), - 0x753e: (sp2, "SP mini 3", "Broadlink"), - 0X7544: (sp2, "SP2-CL", "Broadlink"), - 0x7546: (sp2, "SP2-UK/BR/IN", "Broadlink (OEM)"), - 0x7547: (sp2, "SC1", "Broadlink"), - 0x7918: (sp2, "SP2", "Broadlink (OEM)"), - 0x7919: (sp2, "SP2-compatible", "Honeywell"), - 0x791a: (sp2, "SP2-compatible", "Honeywell"), - 0x7d00: (sp2, "SP3-EU", "Broadlink (OEM)"), - 0x7d0d: (sp2, "SP mini 3", "Broadlink (OEM)"), - 0x9479: (sp2, "SP3S-US", "Broadlink"), - 0x947a: (sp2, "SP3S-EU", "Broadlink"), - - 0x2712: (rm, "RM pro/pro+", "Broadlink"), - 0x272a: (rm, "RM pro", "Broadlink"), - 0x2737: (rm, "RM mini 3", "Broadlink"), - 0x273d: (rm, "RM pro", "Broadlink"), - 0x277c: (rm, "RM home", "Broadlink"), - 0x2783: (rm, "RM home", "Broadlink"), - 0x2787: (rm, "RM pro", "Broadlink"), - 0x278b: (rm, "RM plus", "Broadlink"), - 0x278f: (rm, "RM mini", "Broadlink"), - 0x2797: (rm, "RM pro+", "Broadlink"), - 0x279d: (rm, "RM pro+", "Broadlink"), - 0x27a1: (rm, "RM plus", "Broadlink"), - 0x27a6: (rm, "RM plus", "Broadlink"), - 0x27a9: (rm, "RM pro+", "Broadlink"), - 0x27c2: (rm, "RM mini 3", "Broadlink"), - 0x27d1: (rm, "RM mini 3", "Broadlink"), - 0x27de: (rm, "RM mini 3", "Broadlink"), - 0x27c3: (rm, "RM pro+", "Broadlink"), - 0x27c7: (rm, "RM mini 3", "Broadlink"), - 0x27cc: (rm, "RM mini 3", "Broadlink"), - 0x27cd: (rm, "RM mini 3", "Broadlink"), - 0x27D0: (rm, "RM mini 3", "Broadlink"), - 0x27d3: (rm, "RM mini 3", "Broadlink"), - 0x27dc: (rm, "RM mini 3", "Broadlink"), - 0x6507: (rm, "RM mini 3", "Broadlink"), - 0x6508: (rm, "RM mini 3", "Broadlink"), - - 0x6539: (rm4, "RM4C mini", "Broadlink"), - 0x51da: (rm4, "RM4 mini", "Broadlink"), - 0x5f36: (rm4, "RM mini 3", "Broadlink"), - 0x6026: (rm4, "RM4 pro", "Broadlink"), - 0x6070: (rm4, "RM4C mini", "Broadlink"), - 0x610e: (rm4, "RM4 mini", "Broadlink"), - 0x610f: (rm4, "RM4C mini", "Broadlink"), - 0x61a2: (rm4, "RM4 pro", "Broadlink"), - 0x62bc: (rm4, "RM4 mini", "Broadlink"), - 0x62be: (rm4, "RM4C mini", "Broadlink"), - 0x648d: (rm4, "RM4 mini", "Broadlink"), - 0x2714: (a1, "e-Sensor", "Broadlink"), - - 0x4eb5: (mp1, "MP1-1K4S", "Broadlink"), - 0x4ef7: (mp1, "MP1-1K4S", "Broadlink (OEM)"), - 0x4f65: (mp1, "MP1-1K3S2U", "Broadlink"), - - 0x5043: (lb1, "SB800TD", "Broadlink (OEM)"), - 0x504e: (lb1, "LB1", "Broadlink"), - 0x60c7: (lb1, "LB1", "Broadlink"), - 0x60c8: (lb1, "LB1", "Broadlink"), - 0x6112: (lb1, "LB1", "Broadlink"), - - 0x2722: (S1C, "S2KIT", "Broadlink"), - - 0x4ead: (hysen, "HY02B05H", "Hysen"), - - 0x4e4d: (dooya, "DT360E-45/20", "Dooya"), - - 0x51e3: (bg1, "BG800/BG900", "BG Electrical"), - 0x653c: (rm4, "RM4 pro", "Broadlink"), - 0x649b: (rm4, "RM4 pro", "Broadlink"), - 0x653a: (rm4, "RM4 mini", "Broadlink"), - 0x6184: (rm4, "RM4C mini", "Broadlink"), - 0x5209: (rm4, "RM4 TV Mate", "Broadlink") - } +import typing as t + +from . import exceptions as e +from .const import DEFAULT_BCAST_ADDR, DEFAULT_PORT, DEFAULT_TIMEOUT +from .alarm import S1C +from .climate import hysen +from .cover import dooya +from .device import Device, ping, scan +from .light import lb1, lb2 +from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro +from .sensor import a1 +from .switch import bg1, mp1, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b + +SUPPORTED_TYPES = { + sp1: { + 0x0000: ("SP1", "Broadlink"), + }, + sp2: { + 0x2717: ("NEO", "Ankuoo"), + 0x2719: ("SP2-compatible", "Honeywell"), + 0x271A: ("SP2-compatible", "Honeywell"), + 0x2720: ("SP mini", "Broadlink"), + 0x2728: ("SP2-compatible", "URANT"), + 0x273E: ("SP mini", "Broadlink"), + 0x7530: ("SP2", "Broadlink (OEM)"), + 0x7539: ("SP2-IL", "Broadlink (OEM)"), + 0x753E: ("SP mini 3", "Broadlink"), + 0x7540: ("MP2", "Broadlink"), + 0x7544: ("SP2-CL", "Broadlink"), + 0x7546: ("SP2-UK/BR/IN", "Broadlink (OEM)"), + 0x7547: ("SC1", "Broadlink"), + 0x7918: ("SP2", "Broadlink (OEM)"), + 0x7919: ("SP2-compatible", "Honeywell"), + 0x791A: ("SP2-compatible", "Honeywell"), + 0x7D0D: ("SP mini 3", "Broadlink (OEM)"), + }, + sp2s: { + 0x2711: ("SP2", "Broadlink"), + 0x2716: ("NEO PRO", "Ankuoo"), + 0x271D: ("Ego", "Efergy"), + 0x2736: ("SP mini+", "Broadlink"), + }, + sp3: { + 0x2733: ("SP3", "Broadlink"), + 0x7D00: ("SP3-EU", "Broadlink (OEM)"), + }, + sp3s: { + 0x9479: ("SP3S-US", "Broadlink"), + 0x947A: ("SP3S-EU", "Broadlink"), + }, + sp4: { + 0x7568: ("SP4L-CN", "Broadlink"), + 0x756C: ("SP4M", "Broadlink"), + 0x756F: ("MCB1", "Broadlink"), + 0x7579: ("SP4L-EU", "Broadlink"), + 0x757B: ("SP4L-AU", "Broadlink"), + 0x7583: ("SP mini 3", "Broadlink"), + 0x7587: ("SP4L-UK", "Broadlink"), + 0x7D11: ("SP mini 3", "Broadlink"), + 0xA56A: ("MCB1", "Broadlink"), + 0xA56B: ("SCB1E", "Broadlink"), + 0xA56C: ("SP4L-EU", "Broadlink"), + 0xA589: ("SP4L-UK", "Broadlink"), + 0xA5D3: ("SP4L-EU", "Broadlink"), + }, + sp4b: { + 0x5115: ("SCB1E", "Broadlink"), + 0x51E2: ("AHC/U-01", "BG Electrical"), + 0x6111: ("MCB1", "Broadlink"), + 0x6113: ("SCB1E", "Broadlink"), + 0x618B: ("SP4L-EU", "Broadlink"), + 0x6489: ("SP4L-AU", "Broadlink"), + 0x648B: ("SP4M-US", "Broadlink"), + 0x6494: ("SCB2", "Broadlink"), + }, + rmmini: { + 0x2737: ("RM mini 3", "Broadlink"), + 0x278F: ("RM mini", "Broadlink"), + 0x27C2: ("RM mini 3", "Broadlink"), + 0x27C7: ("RM mini 3", "Broadlink"), + 0x27CC: ("RM mini 3", "Broadlink"), + 0x27CD: ("RM mini 3", "Broadlink"), + 0x27D0: ("RM mini 3", "Broadlink"), + 0x27D1: ("RM mini 3", "Broadlink"), + 0x27D3: ("RM mini 3", "Broadlink"), + 0x27DC: ("RM mini 3", "Broadlink"), + 0x27DE: ("RM mini 3", "Broadlink"), + }, + rmpro: { + 0x2712: ("RM pro/pro+", "Broadlink"), + 0x272A: ("RM pro", "Broadlink"), + 0x273D: ("RM pro", "Broadlink"), + 0x277C: ("RM home", "Broadlink"), + 0x2783: ("RM home", "Broadlink"), + 0x2787: ("RM pro", "Broadlink"), + 0x278B: ("RM plus", "Broadlink"), + 0x2797: ("RM pro+", "Broadlink"), + 0x279D: ("RM pro+", "Broadlink"), + 0x27A1: ("RM plus", "Broadlink"), + 0x27A6: ("RM plus", "Broadlink"), + 0x27A9: ("RM pro+", "Broadlink"), + 0x27C3: ("RM pro+", "Broadlink"), + }, + rmminib: { + 0x5F36: ("RM mini 3", "Broadlink"), + 0x6507: ("RM mini 3", "Broadlink"), + 0x6508: ("RM mini 3", "Broadlink"), + }, + rm4mini: { + 0x51DA: ("RM4 mini", "Broadlink"), + 0x6070: ("RM4C mini", "Broadlink"), + 0x610E: ("RM4 mini", "Broadlink"), + 0x610F: ("RM4C mini", "Broadlink"), + 0x62BC: ("RM4 mini", "Broadlink"), + 0x62BE: ("RM4C mini", "Broadlink"), + 0x6364: ("RM4S", "Broadlink"), + 0x648D: ("RM4 mini", "Broadlink"), + 0x6539: ("RM4C mini", "Broadlink"), + 0x653A: ("RM4 mini", "Broadlink"), + }, + rm4pro: { + 0x6026: ("RM4 pro", "Broadlink"), + 0x6184: ("RM4C pro", "Broadlink"), + 0x61A2: ("RM4 pro", "Broadlink"), + 0x649B: ("RM4 pro", "Broadlink"), + 0x653C: ("RM4 pro", "Broadlink"), + }, + a1: { + 0x2714: ("e-Sensor", "Broadlink"), + }, + mp1: { + 0x4EB5: ("MP1-1K4S", "Broadlink"), + 0x4EF7: ("MP1-1K4S", "Broadlink (OEM)"), + 0x4F1B: ("MP1-1K3S2U", "Broadlink (OEM)"), + 0x4F65: ("MP1-1K3S2U", "Broadlink"), + }, + lb1: { + 0x5043: ("SB800TD", "Broadlink (OEM)"), + 0x504E: ("LB1", "Broadlink"), + 0x606E: ("SB500TD", "Broadlink (OEM)"), + 0x60C7: ("LB1", "Broadlink"), + 0x60C8: ("LB1", "Broadlink"), + 0x6112: ("LB1", "Broadlink"), + }, + lb2: { + 0xA4F4: ("LB27 R1", "Broadlink"), + }, + S1C: { + 0x2722: ("S2KIT", "Broadlink"), + }, + hysen: { + 0x4EAD: ("HY02/HY03", "Hysen"), + }, + dooya: { + 0x4E4D: ("DT360E-45/20", "Dooya"), + }, + bg1: { + 0x51E3: ("BG800/BG900", "BG Electrical"), + }, +} -def gendevice(dev_type, host, mac, name=None, is_locked=None): +def gendevice( + dev_type: int, + host: t.Tuple[str, int], + mac: t.Union[bytes, str], + name: str = "", + is_locked: bool = False, +) -> Device: """Generate a device.""" - try: - dev_class, model, manufacturer = get_devices()[dev_type] - - except KeyError: - return device(host, mac, dev_type, name=name, is_locked=is_locked) - - return dev_class( - host, - mac, - dev_type, - name=name, - model=model, - manufacturer=manufacturer, - is_locked=is_locked, - ) - - -def discover( - timeout=None, - local_ip_address=None, - discover_ip_address='255.255.255.255', - discover_ip_port=80 -): - local_ip_address = local_ip_address or get_local_ip() - address = local_ip_address.split('.') - cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - cs.bind((local_ip_address, 0)) - port = cs.getsockname()[1] - starttime = time.time() - - devices = [] - - timezone = int(time.timezone / -3600) - packet = bytearray(0x30) - - year = datetime.now().year - - if timezone < 0: - packet[0x08] = 0xff + timezone - 1 - packet[0x09] = 0xff - packet[0x0a] = 0xff - packet[0x0b] = 0xff - else: - packet[0x08] = timezone - packet[0x09] = 0 - packet[0x0a] = 0 - packet[0x0b] = 0 - packet[0x0c] = year & 0xff - packet[0x0d] = year >> 8 - packet[0x0e] = datetime.now().minute - packet[0x0f] = datetime.now().hour - subyear = str(year)[2:] - packet[0x10] = int(subyear) - packet[0x11] = datetime.now().isoweekday() - packet[0x12] = datetime.now().day - packet[0x13] = datetime.now().month - packet[0x18] = int(address[0]) - packet[0x19] = int(address[1]) - packet[0x1a] = int(address[2]) - packet[0x1b] = int(address[3]) - packet[0x1c] = port & 0xff - packet[0x1d] = port >> 8 - packet[0x26] = 6 - - checksum = sum(packet, 0xbeaf) & 0xffff - packet[0x20] = checksum & 0xff - packet[0x21] = checksum >> 8 - - cs.sendto(packet, (discover_ip_address, discover_ip_port)) - if timeout is None: - response = cs.recvfrom(1024) - responsepacket = bytearray(response[0]) - host = response[1] - devtype = responsepacket[0x34] | responsepacket[0x35] << 8 - mac = responsepacket[0x3f:0x39:-1] - name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8') - is_locked = bool(responsepacket[-1]) - device = gendevice(devtype, host, mac, name=name, is_locked=is_locked) - cs.close() - return device - - while (time.time() - starttime) < timeout: - cs.settimeout(timeout - (time.time() - starttime)) + for dev_cls, products in SUPPORTED_TYPES.items(): try: - response = cs.recvfrom(1024) - except socket.timeout: - cs.close() - return devices - responsepacket = bytearray(response[0]) - host = response[1] - devtype = responsepacket[0x34] | responsepacket[0x35] << 8 - mac = responsepacket[0x3f:0x39:-1] - name = responsepacket[0x40:].split(b'\x00')[0].decode('utf-8') - is_locked = bool(responsepacket[-1]) - device = gendevice(devtype, host, mac, name=name, is_locked=is_locked) - devices.append(device) - cs.close() - return devices - - -class device: - def __init__( - self, - host, - mac, - devtype, - timeout=10, - name=None, - model=None, - manufacturer=None, - is_locked=None - ): - self.host = host - self.mac = mac.encode() if isinstance(mac, str) else mac - self.devtype = devtype if devtype is not None else 0x272a - self.timeout = timeout - self.name = name - self.model = model - self.manufacturer = manufacturer - self.is_locked = is_locked - self.count = random.randrange(0xffff) - self.iv = bytearray( - [0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) - self.id = bytearray([0, 0, 0, 0]) - self.type = "Unknown" - self.lock = threading.Lock() - - self.aes = None - key = bytearray( - [0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) - self.update_aes(key) - - def update_aes(self, key): - self.aes = Cipher(algorithms.AES(key), modes.CBC(self.iv), - backend=default_backend()) - - def encrypt(self, payload): - encryptor = self.aes.encryptor() - return encryptor.update(payload) + encryptor.finalize() - - def decrypt(self, payload): - decryptor = self.aes.decryptor() - return decryptor.update(payload) + decryptor.finalize() - - 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) - check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) - - key = payload[0x04:0x14] - if len(key) % 16 != 0: - return False - - self.id = payload[0x03::-1] - self.update_aes(key) - - return True - - def get_fwversion(self): - packet = bytearray([0x68]) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(response[0x38:]) - return payload[0x4] | payload[0x5] << 8 - - def set_name(self, name): - packet = bytearray(4) - packet += name.encode('utf-8') - packet += bytearray(0x50 - len(packet)) - packet[0x43] = bool(self.is_locked) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - self.name = name - - def set_lock(self, state): - packet = bytearray(4) - packet += self.name.encode('utf-8') - packet += bytearray(0x50 - len(packet)) - packet[0x43] = bool(state) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - self.is_locked = bool(state) - - def get_type(self): - 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] = self.devtype & 0xff - packet[0x25] = self.devtype >> 8 - packet[0x26] = command - packet[0x28] = self.count & 0xff - packet[0x29] = self.count >> 8 - packet[0x2a] = self.mac[5] - packet[0x2b] = self.mac[4] - packet[0x2c] = self.mac[3] - packet[0x2d] = self.mac[2] - packet[0x2e] = self.mac[1] - packet[0x2f] = self.mac[0] - packet[0x30] = self.id[3] - packet[0x31] = self.id[2] - packet[0x32] = self.id[1] - packet[0x33] = self.id[0] - - # pad the payload for AES encryption - if payload: - payload += bytearray((16 - len(payload)) % 16) - - checksum = sum(payload, 0xbeaf) & 0xffff - packet[0x34] = checksum & 0xff - packet[0x35] = checksum >> 8 - - payload = self.encrypt(payload) - for i in range(len(payload)): - packet.append(payload[i]) - - checksum = sum(packet, 0xbeaf) & 0xffff - packet[0x20] = checksum & 0xff - packet[0x21] = checksum >> 8 - - start_time = time.time() - with self.lock: - cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - while True: - try: - cs.sendto(packet, self.host) - cs.settimeout(1) - resp, _ = cs.recvfrom(2048) - resp = bytearray(resp) - break - except socket.timeout: - if (time.time() - start_time) > self.timeout: - cs.close() - raise exception(-4000) # Network timeout. - cs.close() - - if len(resp) < 0x30: - raise exception(-4007) # Length error. - - checksum = resp[0x20] | (resp[0x21] << 8) - if sum(resp, 0xbeaf) - sum(resp[0x20:0x22]) & 0xffff != checksum: - raise exception(-4008) # Checksum error. - - return resp - - -class mp1(device): - def __init__(self, *args, **kwargs): - device.__init__(self, *args, **kwargs) - self.type = "MP1" - - def set_power_mask(self, sid_mask, state): - """Sets the power state of the smart power strip.""" - - packet = bytearray(16) - packet[0x00] = 0x0d - packet[0x02] = 0xa5 - packet[0x03] = 0xa5 - packet[0x04] = 0x5a - packet[0x05] = 0x5a - packet[0x06] = 0xb2 + ((sid_mask << 1) if state else sid_mask) - packet[0x07] = 0xc0 - packet[0x08] = 0x02 - packet[0x0a] = 0x03 - packet[0x0d] = sid_mask - packet[0x0e] = sid_mask if state else 0 - - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - - def set_power(self, sid, 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) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if isinstance(payload[0x4], int): - state = payload[0x0e] - else: - state = ord(payload[0x0e]) - return state - - 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) - data['s3'] = bool(state & 0x04) - data['s4'] = bool(state & 0x08) - return data - - -class bg1(device): - def __init__(self, *args, **kwargs): - device.__init__(self, *args, **kwargs) - self.type = "BG1" - - def get_state(self): - """Get state of device. - Returns: - dict: Dictionary of current state - eg. `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}`""" - packet = self._encode(1, b'{}') - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - return self._decode(response) - - def set_state(self, pwr=None, pwr1=None, pwr2=None, maxworktime=None, maxworktime1=None, maxworktime2=None, idcbrightness=None): - data = {} - if pwr is not None: - data['pwr'] = int(bool(pwr)) - if pwr1 is not None: - data['pwr1'] = int(bool(pwr1)) - if pwr2 is not None: - data['pwr2'] = int(bool(pwr2)) - if maxworktime is not None: - data['maxworktime'] = maxworktime - if maxworktime1 is not None: - data['maxworktime1'] = maxworktime1 - if maxworktime2 is not None: - data['maxworktime2'] = maxworktime2 - if idcbrightness is not None: - data['idcbrightness'] = idcbrightness - js = json.dumps(data).encode('utf8') - packet = self._encode(2, js) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - return self._decode(response) - - def _encode(self, flag, 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): - payload = self.decrypt(bytes(response[0x38:])) - js_len = struct.unpack_from(' Device: + """Direct device discovery. - def send_data(self, data): - packet = bytearray(self._code_sending_header) - packet += bytearray([0x02, 0x00, 0x00, 0x00]) - packet += data - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - - def enter_learning(self): - packet = bytearray(self._request_header) - packet.append(0x03) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - - def sweep_frequency(self): - packet = bytearray(self._request_header) - packet.append(0x19) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - - def cancel_sweep_frequency(self): - packet = bytearray(self._request_header) - packet.append(0x1e) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - - def check_frequency(self): - packet = bytearray(self._request_header) - packet.append(0x1a) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if payload[len(self._request_header) + 4] == 1: - return True - return False - - def find_rf_packet(self): - packet = bytearray(self._request_header) - packet.append(0x1b) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if payload[len(self._request_header) + 4] == 1: - return True - return False - - def _check_sensors(self, command): - packet = bytearray(self._request_header) - packet.append(command) - response = self.send_packet(0x6a, packet) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - return bytearray(payload[len(self._request_header) + 4:]) - - def check_temperature(self): - data = self._check_sensors(0x1) - return data[0x0] + data[0x1] / 10.0 - - def check_sensors(self): - data = self._check_sensors(0x1) - return {'temperature': data[0x0] + data[0x1] / 10.0} - - -class rm4(rm): - def __init__(self, *args, **kwargs): - device.__init__(self, *args, **kwargs) - self.type = "RM4" - self._request_header = b'\x04\x00' - self._code_sending_header = b'\xda\x00' - - def check_temperature(self): - data = self._check_sensors(0x24) - return data[0x0] + data[0x1] / 100.0 - - def check_humidity(self): - data = self._check_sensors(0x24) - return data[0x2] + data[0x3] / 100.0 - - def check_sensors(self): - data = self._check_sensors(0x24) - return { - 'temperature': data[0x0] + data[0x1] / 100.0, - 'humidity': data[0x2] + data[0x3] / 100.0 - } - - -# For legacy compatibility - don't use this -class rm2(rm): - def __init__(self): - device.__init__(self, None, None, None) - - def discover(self): - dev = discover() - self.host = dev.host - self.mac = dev.mac - - -class hysen(device): - def __init__(self, *args, **kwargs): - device.__init__(self, *args, **kwargs) - self.type = "Hysen heating controller" - - # Send a request - # input_payload should be a bytearray, usually 6 bytes, e.g. bytearray([0x01,0x06,0x00,0x02,0x10,0x00]) - # Returns decrypted payload - # New behaviour: raises a ValueError if the device response indicates an error or CRC check fails - # The function prepends length (2 bytes) and appends CRC - - def 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): - - crc = self.calculate_crc16(bytes(input_payload)) - - # first byte is length, +2 for CRC16 - request_payload = bytearray([len(input_payload) + 2, 0x00]) - request_payload.extend(input_payload) - - # append CRC - request_payload.append(crc & 0xFF) - request_payload.append((crc >> 8) & 0xFF) - - # send to device - response = self.send_packet(0x6a, request_payload) - check_error(response[0x22:0x24]) - response_payload = bytearray(self.decrypt(bytes(response[0x38:]))) - - # experimental check on CRC in response (first 2 bytes are len, and trailing bytes are crc) - response_payload_len = response_payload[0] - if response_payload_len + 2 > len(response_payload): - raise ValueError('hysen_response_error', 'first byte of response is not length') - crc = 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] - 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 - 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 = { - 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): + Useful if the device is locked. """ - Its VERY VERY VERY DIRTY IMPLEMENTATION of S1C - """ - - def __init__(self, *args, **kwargs): - device.__init__(self, *args, **kwargs) - 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) - check_error(response[0x22:0x24]) - payload = self.decrypt(bytes(response[0x38:])) - if not payload: - return None - count = payload[0x4] - sensors = payload[0x6:] - sensors_a = [bytearray(sensors[i * 83:(i + 1) * 83]) for i in range(len(sensors) // 83)] - - sens_res = [] - for sens in sensors_a: - status = ord(chr(sens[0])) - _name = str(bytes(sens[4:26]).decode()) - _order = ord(chr(sens[1])) - _type = ord(chr(sens[3])) - _serial = bytes(codecs.encode(sens[26:30], "hex")).decode() - - type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown') - - r = { - 'status': status, - 'name': _name.strip('\x00'), - 'type': type_str, - 'order': _order, - 'serial': _serial, - } - if r['serial'] != '00000000': - sens_res.append(r) - result = { - 'count': count, - 'sensors': sens_res - } - return result - - -class dooya(device): - def __init__(self, *args, **kwargs): - device.__init__(self, *args, **kwargs) - 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) - check_error(response[0x22:0x24]) - 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() - -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, *args, **kwargs): - device.__init__(self, *args, **kwargs) - self.type = "SmartBulb" - - def send_command(self, command, type='set'): - packet = bytearray(16+(int(len(command)/16) + 1)*16) - packet[0x00] = 0x0c + len(command) & 0xff - packet[0x02] = 0xa5 - packet[0x03] = 0xa5 - packet[0x04] = 0x5a - packet[0x05] = 0x5a - packet[0x08] = 0x02 if type == "set" else 0x01 # 0x01 => query, # 0x02 => set - packet[0x09] = 0x0b - packet[0x0a] = len(command) - packet[0x0e:] = map(ord, command) - - checksum = sum(packet, 0xbeaf) & 0xffff - packet[0x06] = checksum & 0xff # Checksum 1 position - packet[0x07] = checksum >> 8 # Checksum 2 position - - response = self.send_packet(0x6a, packet) - check_error(response[0x36:0x38]) - payload = self.decrypt(bytes(response[0x38:])) - - responseLength = int(payload[0x0a]) | (int(payload[0x0b]) << 8) - if responseLength > 0: - self.state_dict = json.loads(payload[0x0e:0x0e+responseLength]) - - def set_json(self, jsonstr): - reconvert = json.loads(jsonstr) - if 'bulb_sceneidx' in reconvert.keys(): - reconvert['bulb_sceneidx'] = self.effect_map_dict.get(reconvert['bulb_sceneidx'], 255) + try: + return next( + xdiscover(timeout=timeout, discover_ip_address=host, discover_ip_port=port) + ) + except StopIteration as err: + raise e.NetworkTimeoutError( + -4000, + "Network timeout", + f"No response received within {timeout}s", + ) from err - 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 discover( + timeout: int = DEFAULT_TIMEOUT, + local_ip_address: str = None, + discover_ip_address: str = DEFAULT_BCAST_ADDR, + discover_ip_port: int = DEFAULT_PORT, +) -> t.List[Device]: + """Discover devices connected to the local network.""" + responses = scan(timeout, local_ip_address, discover_ip_address, discover_ip_port) + return [gendevice(*resp) for resp in responses] + + +def xdiscover( + timeout: int = DEFAULT_TIMEOUT, + local_ip_address: str = None, + discover_ip_address: str = DEFAULT_BCAST_ADDR, + discover_ip_port: int = DEFAULT_PORT, +) -> t.Generator[Device, None, None]: + """Discover devices connected to the local network. + + This function returns a generator that yields devices instantly. + """ + responses = scan(timeout, local_ip_address, discover_ip_address, discover_ip_port) + for resp in responses: + yield gendevice(*resp) - 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) -def setup(ssid, password, security_mode): +def setup(ssid: str, password: str, security_mode: int) -> None: + """Set up a new Broadlink device via AP mode.""" # Security mode options are (0 - none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) payload = bytearray(0x88) payload[0x26] = 0x14 # This seems to always be set to 14 @@ -1125,15 +260,14 @@ def setup(ssid, password, security_mode): payload[0x84] = ssid_length # Character length of SSID payload[0x85] = pass_length # Character length of password - payload[0x86] = security_mode # Type of encryption (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2) + payload[0x86] = security_mode # Type of encryption - checksum = sum(payload, 0xbeaf) & 0xffff - payload[0x20] = checksum & 0xff # Checksum 1 position + checksum = sum(payload, 0xBEAF) & 0xFFFF + payload[0x20] = checksum & 0xFF # Checksum 1 position payload[0x21] = checksum >> 8 # Checksum 2 position - sock = socket.socket(socket.AF_INET, # Internet - socket.SOCK_DGRAM) # UDP + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Internet # UDP sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - sock.sendto(payload, ('255.255.255.255', 80)) + sock.sendto(payload, (DEFAULT_BCAST_ADDR, DEFAULT_PORT)) sock.close() diff --git a/broadlinkmanager/broadlink/__pycache__/__init__.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d8f4230e101813c7b3dec6cd2f56be5fdec0e72 GIT binary patch literal 6403 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 B)oB0# literal 0 HcmV?d00001 diff --git a/broadlinkmanager/broadlink/__pycache__/alarm.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/alarm.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58b16d39d0f988d212e6e538b6894f9af8ac5745 GIT binary patch literal 1540 zcmZuxU2oh(6rB%$?IvjFNn z=Ycu-3|e&?K#-USDp*d(Eanu)Ja@)!>{9Y25sq-riEyR6sHo1dTcLj$nf0 zz85@pCG2@Yg)f40C-y}L_U~HWzjJrCb5|8ctGo)+@N=zFkq?Wr5Inuv+JORMeOXjW z&-}0DH2gvx&o;hMNNHt{czp- z%T<=<)jkZKEz_^UlK;uj0Yf9h&oF3Y_z`755d>{R*N}Muxdw!~Mcarj$%qMONg?+f zvQLG(Lxi`)dhwetSF1R5g%A4N=Ku(%u#4J6B{WPYM=AeU`~HvsUMR9G;c&nRTp&D5kY)?eux`FazaG3Hq$ zWeWK-mP)&poyzPikyS{m!>v=~Usb6G^5j&PyZu89LJs}e2`Y5g*t=5V(4 zE%56P5Rekk#Rd9x1f*v-U(7S6v_jg11vH@RbQ50aS(k2c#=8FO#>)%jFbZstBq(l@ z*lv=Hm6+r>UQdz-lQds>{3L-QO_JC_HT8SQ!x6*GXuMfk?HU1}tJsJK&T1VXpbm35 wql^YzqvsG5`~-SSG?uXb5XX>0ygoHnLi5$jer$uimgGL3z^b{pAmVlYFYOq7hyVZp literal 0 HcmV?d00001 diff --git a/broadlinkmanager/broadlink/__pycache__/climate.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/climate.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9bc5e214d042c165e3ddb46c6fe16bd67700950b GIT binary patch literal 5654 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 SJGhVNOye?ur5Cg2&iprDya4h5 literal 0 HcmV?d00001 diff --git a/broadlinkmanager/broadlink/__pycache__/const.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/const.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70fab033a19717a9d5ad8243dac7ee5935338859 GIT binary patch literal 305 zcmXr!<>k6`Cn%|$fq~&M5W@i@kmUfx#S%awg&~R|g)xdTg(-?Dg*l2jg(Zq5g*BK# zldX!&IX|zsBr&g~Sg(rT$kbF13BL>g>U{|^=_MDC@Y7_y#U3A@lAjzOe@n>4)y*-~ zCnVm<*)cdI-qFP+=oSx5EWke~BuW^@3vvw!ij4R43kmbN#Saq<@$_}|4-Hw#P{aW= z8ch81(9bU@(N9S&(@)PV(N8MMPfW?l%*)PA%u7s9Ez$*AoSj;PA&wBxPX_r#ub}c4 WhfQvNN@-529mw&;ARAa1ArJtI#8PAc literal 0 HcmV?d00001 diff --git a/broadlinkmanager/broadlink/__pycache__/cover.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/cover.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06271296c5012ace178510c1f8b11d07764e216d GIT binary patch literal 2017 zcma)6&2Ah;5bmD;{k6S8K=@IH1O-OGIuHrvU>SrR!O5VkrHGZ#WIWw=#-5#?^o$el zvOaC#9YB5oo`EOe%CW_b)4oDZRP~I>Zb;D1Xuhth?wb1Ps=D3jH2?hT-ocL!A%ByF z#{&9G_=-CKj0_2*%t+~wGLu=UF*1i1B|zJ$J#vN)C0`TfF!w28E_Y_;&}GJe1m5Jz z;55&LEd4_v{a8Havgps@mt=2F0RyU<{4C~qnTV_a&e`RUlbFYNQo;2Jg8VO`*wz^$?M3xjR2 z9`G%;1`%9QHWOkTO|I>Ttdt@x{K(%$i65WJGDgw$0V`Ycv zWCwm)ybeGNk1oD#+Jkq}(sj|-X9FrdI2dD|@WB<6r0=#31l)q|pc!_BIobF&=b6rR zT||9}iPd?Af4v7ZEN`O-W2?XiN?8Ilaic&5VoaKq8z^4cd~YWe1z)z0Uanx@j!_kz zx^$qhOxwHt53enfbzB~|F?1Q6llt# Jp3$al^B;$O(W3wW literal 0 HcmV?d00001 diff --git a/broadlinkmanager/broadlink/__pycache__/device.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/device.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19e77630a779db2df8500db9222c7f1735d5d297 GIT binary patch literal 9563 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 literal 0 HcmV?d00001 diff --git a/broadlinkmanager/broadlink/__pycache__/helpers.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/helpers.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..896a3fe68c6cc2aa4b32672322b51bb94a44ff95 GIT binary patch literal 1337 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 UHeMYbuh;wu6{ez*T`H~fC%z)-*8l(j literal 0 HcmV?d00001 diff --git a/broadlinkmanager/broadlink/__pycache__/remote.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/remote.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dfe86cc8afd53f9eb8306332943a0c5b0fcc1440 GIT binary patch literal 5196 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 literal 0 HcmV?d00001 diff --git a/broadlinkmanager/broadlink/__pycache__/switch.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/switch.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f5476560fcbb396949c969c699a529e804001bd GIT binary patch literal 11788 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 EKbNlwod5s; literal 0 HcmV?d00001 diff --git a/broadlinkmanager/broadlink/alarm.py b/broadlinkmanager/broadlink/alarm.py new file mode 100644 index 0000000..a9b5e87 --- /dev/null +++ b/broadlinkmanager/broadlink/alarm.py @@ -0,0 +1,43 @@ +"""Support for alarm kits.""" +from . import exceptions as e +from .device import Device + + +class S1C(Device): + """Controls a Broadlink S1C.""" + + TYPE = "S1C" + + _SENSORS_TYPES = { + 0x31: "Door Sensor", + 0x91: "Key Fob", + 0x21: "Motion Sensor", + } + + def get_sensors_status(self) -> dict: + """Return the state of the sensors.""" + packet = bytearray(16) + packet[0] = 0x06 + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + count = payload[0x4] + sensor_data = payload[0x6:] + sensors = [ + bytearray(sensor_data[i * 83 : (i + 1) * 83]) + for i in range(len(sensor_data) // 83) + ] + return { + "count": count, + "sensors": [ + { + "status": sensor[0], + "name": sensor[4:26].decode().strip("\x00"), + "type": self._SENSORS_TYPES.get(sensor[3], "Unknown"), + "order": sensor[1], + "serial": sensor[26:30].hex(), + } + for sensor in sensors + if any(sensor[26:30]) + ], + } diff --git a/broadlinkmanager/broadlink/climate.py b/broadlinkmanager/broadlink/climate.py new file mode 100644 index 0000000..eee5f11 --- /dev/null +++ b/broadlinkmanager/broadlink/climate.py @@ -0,0 +1,228 @@ +"""Support for HVAC units.""" +import typing as t + +from . import exceptions as e +from .device import Device +from .helpers import CRC16 + + +class hysen(Device): + """Controls a Hysen heating thermostat. + + This device is manufactured by Hysen and sold under different + brands, including Floureon, Beca Energy, Beok and Decdeal. + + Supported models: + - HY02B05H + - HY03WE + """ + + TYPE = "HYS" + + def send_request(self, request: t.Sequence[int]) -> bytes: + """Send a request to the device.""" + packet = bytearray() + packet.extend((len(request) + 2).to_bytes(2, "little")) + packet.extend(request) + packet.extend(CRC16.calculate(request).to_bytes(2, "little")) + + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + + p_len = int.from_bytes(payload[:0x02], "little") + if p_len + 2 > len(payload): + raise ValueError( + "hysen_response_error", "first byte of response is not length" + ) + + nom_crc = int.from_bytes(payload[p_len : p_len + 2], "little") + real_crc = CRC16.calculate(payload[0x02:p_len]) + if nom_crc != real_crc: + raise ValueError("hysen_response_error", "CRC check on response failed") + + return payload[0x02:p_len] + + def get_temp(self) -> float: + """Return the room temperature in degrees celsius.""" + payload = self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]) + return payload[0x05] / 2.0 + + def get_external_temp(self) -> float: + """Return the external temperature in degrees celsius.""" + payload = self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]) + return payload[18] / 2.0 + + def get_full_status(self) -> dict: + """Return the state of the device. + + Timer schedule included. + """ + payload = self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x16]) + data = {} + data["remote_lock"] = payload[3] & 1 + data["power"] = payload[4] & 1 + data["active"] = (payload[4] >> 4) & 1 + data["temp_manual"] = (payload[4] >> 6) & 1 + data["room_temp"] = payload[5] / 2.0 + data["thermostat_temp"] = payload[6] / 2.0 + data["auto_mode"] = payload[7] & 0xF + data["loop_mode"] = payload[7] >> 4 + data["sensor"] = payload[8] + data["osv"] = payload[9] + data["dif"] = payload[10] + data["svh"] = payload[11] + data["svl"] = payload[12] + data["room_temp_adj"] = ( + int.from_bytes(payload[13:15], "big", signed=True) / 10.0 + ) + data["fre"] = payload[15] + data["poweron"] = payload[16] + data["unknown"] = payload[17] + data["external_temp"] = payload[18] / 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 (weekend schedule) + # loop_mode = 2 ("1234567") means every day, including Saturday and Sunday (weekday schedule) + # The sensor command is currently experimental + def set_mode(self, auto_mode: int, loop_mode: int, sensor: int = 0) -> None: + """Set the mode of the device.""" + mode_byte = ((loop_mode + 1) << 4) + auto_mode + self.send_request([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. Precision 0.1C + # Anti-freezing function (FrE) fre = 0 for anti-freezing function shut down, + # 1 for anti-freezing function open. Factory default: 0 + # Power on memory (POn) poweron = 0 for off, 1 for on. Default: 0 + def set_advanced( + self, + loop_mode: int, + sensor: int, + osv: int, + dif: int, + svh: int, + svl: int, + adj: float, + fre: int, + poweron: int, + ) -> None: + """Set advanced options.""" + self.send_request( + [ + 0x01, + 0x10, + 0x00, + 0x02, + 0x00, + 0x05, + 0x0A, + loop_mode, + sensor, + osv, + dif, + svh, + svl, + int(adj * 10) >> 8 & 0xFF, + int(adj * 10) & 0xFF, + fre, + poweron, + ] + ) + + # For backwards compatibility only. Prefer calling set_mode directly. + # Note this function invokes loop_mode=0 and sensor=0. + def switch_to_auto(self) -> None: + """Switch mode to auto.""" + self.set_mode(auto_mode=1, loop_mode=0) + + def switch_to_manual(self) -> None: + """Switch mode to manual.""" + self.set_mode(auto_mode=0, loop_mode=0) + + # Set temperature for manual mode (also activates manual mode if currently in automatic) + def set_temp(self, temp: float) -> None: + """Set the target temperature.""" + self.send_request([0x01, 0x06, 0x00, 0x01, 0x00, int(temp * 2)]) + + # Set device on(1) or off(0), does not deactivate Wifi connectivity. + # Remote lock disables control by buttons on thermostat. + def set_power(self, power: int = 1, remote_lock: int = 0) -> None: + """Set the power state of the device.""" + self.send_request([0x01, 0x06, 0x00, 0x00, remote_lock, power]) + + # set time on device + # n.b. day=1 is Monday, ..., day=7 is Sunday + def set_time(self, hour: int, minute: int, second: int, day: int) -> None: + """Set the time.""" + self.send_request( + [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: t.List[dict], weekend: t.List[dict]) -> None: + """Set timer schedule.""" + request = [0x01, 0x10, 0x00, 0x0A, 0x00, 0x0C, 0x18] + + # weekday times + for i in range(0, 6): + request.append(weekday[i]["start_hour"]) + request.append(weekday[i]["start_minute"]) + + # weekend times + for i in range(0, 2): + request.append(weekend[i]["start_hour"]) + request.append(weekend[i]["start_minute"]) + + # weekday temperatures + for i in range(0, 6): + request.append(int(weekday[i]["temp"] * 2)) + + # weekend temperatures + for i in range(0, 2): + request.append(int(weekend[i]["temp"] * 2)) + + self.send_request(request) diff --git a/broadlinkmanager/broadlink/const.py b/broadlinkmanager/broadlink/const.py new file mode 100644 index 0000000..19c37f5 --- /dev/null +++ b/broadlinkmanager/broadlink/const.py @@ -0,0 +1,5 @@ +"""Constants.""" +DEFAULT_BCAST_ADDR = "255.255.255.255" +DEFAULT_PORT = 80 +DEFAULT_RETRY_INTVL = 1 +DEFAULT_TIMEOUT = 10 diff --git a/broadlinkmanager/broadlink/cover.py b/broadlinkmanager/broadlink/cover.py new file mode 100644 index 0000000..c0f08ab --- /dev/null +++ b/broadlinkmanager/broadlink/cover.py @@ -0,0 +1,57 @@ +"""Support for covers.""" +import time + +from . import exceptions as e +from .device import Device + + +class dooya(Device): + """Controls a Dooya curtain motor.""" + + TYPE = "Dooya DT360E" + + def _send(self, magic1: int, magic2: int) -> int: + """Send a packet to the device.""" + packet = bytearray(16) + packet[0] = 0x09 + packet[2] = 0xBB + packet[3] = magic1 + packet[4] = magic2 + packet[9] = 0xFA + packet[10] = 0x44 + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return payload[4] + + def open(self) -> int: + """Open the curtain.""" + return self._send(0x01, 0x00) + + def close(self) -> int: + """Close the curtain.""" + return self._send(0x02, 0x00) + + def stop(self) -> int: + """Stop the curtain.""" + return self._send(0x03, 0x00) + + def get_percentage(self) -> int: + """Return the position of the curtain.""" + return self._send(0x06, 0x5D) + + def set_percentage_and_wait(self, new_percentage: int) -> None: + """Set the position of the curtain.""" + current = self.get_percentage() + if current > new_percentage: + self.close() + while current is not None and current > new_percentage: + time.sleep(0.2) + current = self.get_percentage() + + elif current < new_percentage: + self.open() + while current is not None and current < new_percentage: + time.sleep(0.2) + current = self.get_percentage() + self.stop() diff --git a/broadlinkmanager/broadlink/device.py b/broadlinkmanager/broadlink/device.py new file mode 100644 index 0000000..74e916f --- /dev/null +++ b/broadlinkmanager/broadlink/device.py @@ -0,0 +1,332 @@ +"""Support for Broadlink devices.""" +import socket +import threading +import random +import time +import typing as t + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +from . import exceptions as e +from .const import ( + DEFAULT_BCAST_ADDR, + DEFAULT_PORT, + DEFAULT_RETRY_INTVL, + DEFAULT_TIMEOUT, +) +from .protocol import Datetime + +HelloResponse = t.Tuple[int, t.Tuple[str, int], str, str, bool] + + +def scan( + timeout: int = DEFAULT_TIMEOUT, + local_ip_address: str = None, + discover_ip_address: str = DEFAULT_BCAST_ADDR, + discover_ip_port: int = DEFAULT_PORT, +) -> t.Generator[HelloResponse, None, None]: + """Broadcast a hello message and yield responses.""" + conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + conn.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + if local_ip_address: + conn.bind((local_ip_address, 0)) + port = conn.getsockname()[1] + else: + local_ip_address = "0.0.0.0" + port = 0 + + packet = bytearray(0x30) + packet[0x08:0x14] = Datetime.pack(Datetime.now()) + packet[0x18:0x1C] = socket.inet_aton(local_ip_address)[::-1] + packet[0x1C:0x1E] = port.to_bytes(2, "little") + packet[0x26] = 6 + + checksum = sum(packet, 0xBEAF) & 0xFFFF + packet[0x20:0x22] = checksum.to_bytes(2, "little") + + start_time = time.time() + discovered = [] + + try: + while (time.time() - start_time) < timeout: + time_left = timeout - (time.time() - start_time) + conn.settimeout(min(DEFAULT_RETRY_INTVL, time_left)) + conn.sendto(packet, (discover_ip_address, discover_ip_port)) + + while True: + try: + resp, host = conn.recvfrom(1024) + except socket.timeout: + break + + devtype = resp[0x34] | resp[0x35] << 8 + mac = resp[0x3A:0x40][::-1] + + if (host, mac, devtype) in discovered: + continue + discovered.append((host, mac, devtype)) + + name = resp[0x40:].split(b"\x00")[0].decode() + is_locked = bool(resp[0x7F]) + yield devtype, host, mac, name, is_locked + finally: + conn.close() + + +def ping(address: str, port: int = DEFAULT_PORT) -> None: + """Send a ping packet to an address. + + This packet feeds the watchdog timer of firmwares >= v53. + Useful to prevent reboots when the cloud cannot be reached. + It must be sent every 2 minutes in such cases. + """ + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as conn: + conn.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + packet = bytearray(0x30) + packet[0x26] = 1 + conn.sendto(packet, (address, port)) + + +class Device: + """Controls a Broadlink device.""" + + TYPE = "Unknown" + + __INIT_KEY = "097628343fe99e23765c1513accf8b02" + __INIT_VECT = "562e17996d093d28ddb3ba695a2e6f58" + + def __init__( + self, + host: t.Tuple[str, int], + mac: t.Union[bytes, str], + devtype: int, + timeout: int = DEFAULT_TIMEOUT, + name: str = "", + model: str = "", + manufacturer: str = "", + is_locked: bool = False, + ) -> None: + """Initialize the controller.""" + self.host = host + self.mac = bytes.fromhex(mac) if isinstance(mac, str) else mac + self.devtype = devtype + self.timeout = timeout + self.name = name + self.model = model + self.manufacturer = manufacturer + self.is_locked = is_locked + self.count = random.randint(0x8000, 0xFFFF) + self.iv = bytes.fromhex(self.__INIT_VECT) + self.id = 0 + self.type = self.TYPE # For backwards compatibility. + self.lock = threading.Lock() + + self.aes = None + self.update_aes(bytes.fromhex(self.__INIT_KEY)) + + def __repr__(self) -> str: + """Return a formal representation of the device.""" + return ( + "%s.%s(%s, mac=%r, devtype=%r, timeout=%r, name=%r, " + "model=%r, manufacturer=%r, is_locked=%r)" + ) % ( + self.__class__.__module__, + self.__class__.__qualname__, + self.host, + self.mac, + self.devtype, + self.timeout, + self.name, + self.model, + self.manufacturer, + self.is_locked, + ) + + def __str__(self) -> str: + """Return a readable representation of the device.""" + return "%s (%s / %s:%s / %s)" % ( + self.name or "Unknown", + " ".join(filter(None, [self.manufacturer, self.model, hex(self.devtype)])), + *self.host, + ":".join(format(x, "02X") for x in self.mac), + ) + + def update_aes(self, key: bytes) -> None: + """Update AES.""" + self.aes = Cipher( + algorithms.AES(bytes(key)), modes.CBC(self.iv), backend=default_backend() + ) + + def encrypt(self, payload: bytes) -> bytes: + """Encrypt the payload.""" + encryptor = self.aes.encryptor() + return encryptor.update(bytes(payload)) + encryptor.finalize() + + def decrypt(self, payload: bytes) -> bytes: + """Decrypt the payload.""" + decryptor = self.aes.decryptor() + return decryptor.update(bytes(payload)) + decryptor.finalize() + + def auth(self) -> bool: + """Authenticate to the device.""" + self.id = 0 + self.update_aes(bytes.fromhex(self.__INIT_KEY)) + + packet = bytearray(0x50) + packet[0x04:0x14] = [0x31] * 16 + packet[0x1E] = 0x01 + packet[0x2D] = 0x01 + packet[0x30:0x36] = "Test 1".encode() + + response = self.send_packet(0x65, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + + self.id = int.from_bytes(payload[:0x4], "little") + self.update_aes(payload[0x04:0x14]) + return True + + def hello(self, local_ip_address=None) -> bool: + """Send a hello message to the device. + + Device information is checked before updating name and lock status. + """ + responses = scan( + timeout=self.timeout, + local_ip_address=local_ip_address, + discover_ip_address=self.host[0], + discover_ip_port=self.host[1], + ) + try: + devtype, _, mac, name, is_locked = next(responses) + + except StopIteration as err: + raise e.NetworkTimeoutError( + -4000, + "Network timeout", + f"No response received within {self.timeout}s", + ) from err + + if mac != self.mac: + raise e.DataValidationError( + -2040, + "Device information is not intact", + "The MAC address is different", + f"Expected {self.mac} and received {mac}", + ) + + if devtype != self.devtype: + raise e.DataValidationError( + -2040, + "Device information is not intact", + "The product ID is different", + f"Expected {self.devtype} and received {devtype}", + ) + + self.name = name + self.is_locked = is_locked + return True + + def ping(self) -> None: + """Ping the device. + + This packet feeds the watchdog timer of firmwares >= v53. + Useful to prevent reboots when the cloud cannot be reached. + It must be sent every 2 minutes in such cases. + """ + ping(self.host[0], port=self.host[1]) + + def get_fwversion(self) -> int: + """Get firmware version.""" + packet = bytearray([0x68]) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return payload[0x4] | payload[0x5] << 8 + + def set_name(self, name: str) -> None: + """Set device name.""" + packet = bytearray(4) + packet += name.encode("utf-8") + packet += bytearray(0x50 - len(packet)) + packet[0x43] = self.is_locked + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + self.name = name + + def set_lock(self, state: bool) -> None: + """Lock/unlock the device.""" + packet = bytearray(4) + packet += self.name.encode("utf-8") + packet += bytearray(0x50 - len(packet)) + packet[0x43] = bool(state) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + self.is_locked = bool(state) + + def get_type(self) -> str: + """Return device type.""" + return self.type + + def send_packet(self, packet_type: int, payload: bytes) -> bytes: + """Send a packet to the device.""" + self.count = ((self.count + 1) | 0x8000) & 0xFFFF + packet = bytearray(0x38) + packet[0x00:0x08] = bytes.fromhex("5aa5aa555aa5aa55") + packet[0x24:0x26] = self.devtype.to_bytes(2, "little") + packet[0x26:0x28] = packet_type.to_bytes(2, "little") + packet[0x28:0x2A] = self.count.to_bytes(2, "little") + packet[0x2A:0x30] = self.mac[::-1] + packet[0x30:0x34] = self.id.to_bytes(4, "little") + + p_checksum = sum(payload, 0xBEAF) & 0xFFFF + packet[0x34:0x36] = p_checksum.to_bytes(2, "little") + + padding = (16 - len(payload)) % 16 + payload = self.encrypt(payload + bytes(padding)) + packet.extend(payload) + + checksum = sum(packet, 0xBEAF) & 0xFFFF + packet[0x20:0x22] = checksum.to_bytes(2, "little") + + with self.lock and socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as conn: + timeout = self.timeout + start_time = time.time() + + while True: + time_left = timeout - (time.time() - start_time) + conn.settimeout(min(DEFAULT_RETRY_INTVL, time_left)) + conn.sendto(packet, self.host) + + try: + resp = conn.recvfrom(2048)[0] + break + except socket.timeout as err: + if (time.time() - start_time) > timeout: + raise e.NetworkTimeoutError( + -4000, + "Network timeout", + f"No response received within {timeout}s", + ) from err + + if len(resp) < 0x30: + raise e.DataValidationError( + -4007, + "Received data packet length error", + f"Expected at least 48 bytes and received {len(resp)}", + ) + + nom_checksum = int.from_bytes(resp[0x20:0x22], "little") + real_checksum = sum(resp, 0xBEAF) - sum(resp[0x20:0x22]) & 0xFFFF + + if nom_checksum != real_checksum: + raise e.DataValidationError( + -4008, + "Received data packet check error", + f"Expected a checksum of {nom_checksum} and received {real_checksum}", + ) + + return resp diff --git a/broadlinkmanager/broadlink/exceptions.py b/broadlinkmanager/broadlink/exceptions.py index d3f8b4a..2343ad6 100644 --- a/broadlinkmanager/broadlink/exceptions.py +++ b/broadlinkmanager/broadlink/exceptions.py @@ -1,19 +1,17 @@ """Exceptions for Broadlink devices.""" +import collections import struct class BroadlinkException(Exception): - """Common base class for all Broadlink exceptions.""" + """Base class common to all Broadlink exceptions.""" def __init__(self, *args, **kwargs): """Initialize the exception.""" super().__init__(*args, **kwargs) - if len(args) >= 3: + if len(args) >= 2: self.errno = args[0] - self.strerror = "%s: %s" % (args[1], args[2]) - elif len(args) == 2: - self.errno = args[0] - self.strerror = str(args[1]) + self.strerror = ": ".join(str(arg) for arg in args[1:]) elif len(args) == 1: self.errno = None self.strerror = str(args[0]) @@ -22,119 +20,96 @@ def __init__(self, *args, **kwargs): self.strerror = "" def __str__(self): - """Return the error message.""" + """Return str(self).""" if self.errno is not None: return "[Errno %s] %s" % (self.errno, self.strerror) return self.strerror + def __eq__(self, other): + """Return self==value.""" + # pylint: disable=unidiomatic-typecheck + return type(self) == type(other) and self.args == other.args -class FirmwareException(BroadlinkException): - """Common base class for all firmware exceptions.""" - - pass + def __hash__(self): + """Return hash(self).""" + return hash((type(self), self.args)) -class AuthenticationError(FirmwareException): - """Authentication error.""" +class MultipleErrors(BroadlinkException): + """Multiple errors.""" - pass + def __init__(self, *args, **kwargs): + """Initialize the exception.""" + errors = args[0][:] if args else [] + counter = collections.Counter(errors) + strerror = "Multiple errors occurred: %s" % counter + super().__init__(strerror, **kwargs) + self.errors = errors + def __repr__(self): + """Return repr(self).""" + return "MultipleErrors(%r)" % self.errors -class AuthorizationError(FirmwareException): - """Authorization error.""" + def __str__(self): + """Return str(self).""" + return self.strerror - pass +class AuthenticationError(BroadlinkException): + """Authentication error.""" -class CommandNotSupportedError(FirmwareException): - """Command not supported error.""" - pass +class AuthorizationError(BroadlinkException): + """Authorization error.""" -class ConnectionClosedError(FirmwareException): - """Connection closed error.""" +class CommandNotSupportedError(BroadlinkException): + """Command not supported error.""" - pass +class ConnectionClosedError(BroadlinkException): + """Connection closed error.""" -class DataValidationError(FirmwareException): - """Data validation error.""" - pass +class StructureAbnormalError(BroadlinkException): + """Structure abnormal error.""" -class DeviceOfflineError(FirmwareException): +class DeviceOfflineError(BroadlinkException): """Device offline error.""" - pass - -class ReadError(FirmwareException): +class ReadError(BroadlinkException): """Read error.""" - pass - -class SendError(FirmwareException): +class SendError(BroadlinkException): """Send error.""" - pass - -class SSIDNotFoundError(FirmwareException): +class SSIDNotFoundError(BroadlinkException): """SSID not found error.""" - pass - -class StorageError(FirmwareException): +class StorageError(BroadlinkException): """Storage error.""" - pass - -class WriteError(FirmwareException): +class WriteError(BroadlinkException): """Write error.""" - pass - - -class SDKException(BroadlinkException): - """Common base class for all SDK exceptions.""" - - pass - - -class ChecksumError(SDKException): - """Received data packet check error.""" - - pass - -class LengthError(SDKException): - """Received data packet length error.""" - - pass - - -class DNSLookupError(SDKException): - """Failed to obtain local IP address.""" - - pass - - -class NetworkTimeoutError(SDKException): +class NetworkTimeoutError(BroadlinkException): """Network timeout error.""" - pass + +class DataValidationError(BroadlinkException): + """Data validation error.""" class UnknownError(BroadlinkException): """Unknown error.""" - pass - BROADLINK_EXCEPTIONS = { # Firmware-related errors are generated by the device. @@ -143,31 +118,35 @@ class UnknownError(BroadlinkException): -3: (DeviceOfflineError, "The device is offline"), -4: (CommandNotSupportedError, "Command not supported"), -5: (StorageError, "The device storage is full"), - -6: (DataValidationError, "Structure is abnormal"), + -6: (StructureAbnormalError, "Structure is abnormal"), -7: (AuthorizationError, "Control key is expired"), -8: (SendError, "Send error"), -9: (WriteError, "Write error"), -10: (ReadError, "Read error"), -11: (SSIDNotFoundError, "SSID could not be found in AP configuration"), - # DNASDK related errors are generated by this module. + # SDK related errors are generated by this module. + -2040: (DataValidationError, "Device information is not intact"), -4000: (NetworkTimeoutError, "Network timeout"), - -4007: (LengthError, "Received data packet length error"), - -4008: (ChecksumError, "Received data packet check error"), - -4013: (DNSLookupError, "Failed to obtain local IP address"), + -4007: (DataValidationError, "Received data packet length error"), + -4008: (DataValidationError, "Received data packet check error"), + -4009: (DataValidationError, "Received data packet information type error"), + -4010: (DataValidationError, "Received encrypted data packet length error"), + -4011: (DataValidationError, "Received encrypted data packet check error"), + -4012: (AuthorizationError, "Device control ID error"), } -def exception(error_code): +def exception(err_code: int) -> BroadlinkException: """Return exception corresponding to an error code.""" try: - exc, msg = BROADLINK_EXCEPTIONS[error_code] - return exc(error_code, msg) + exc, msg = BROADLINK_EXCEPTIONS[err_code] + return exc(err_code, msg) except KeyError: - return UnknownError(error_code, "Unknown error") + return UnknownError(err_code, "Unknown error") -def check_error(error): +def check_error(error: bytes) -> None: """Raise exception if an error occurred.""" error_code = struct.unpack("h", error)[0] if error_code: - raise exception(error_code) \ No newline at end of file + raise exception(error_code) diff --git a/broadlinkmanager/broadlink/helpers.py b/broadlinkmanager/broadlink/helpers.py index 1ff112a..6ee5499 100644 --- a/broadlinkmanager/broadlink/helpers.py +++ b/broadlinkmanager/broadlink/helpers.py @@ -1,20 +1,43 @@ -"""Helper functions.""" -import socket +"""Helper functions and classes.""" +import typing as t -from .exceptions import exception +class CRC16: + """Helps with CRC-16 calculation. -def get_local_ip() -> str: - """Try to determine the local IP address of the machine.""" - # Useful for VPNs. - try: - local_ip_address = socket.gethostbyname(socket.gethostname()) - if not local_ip_address.startswith('127.'): - return local_ip_address - except socket.gaierror: - raise exception(-4013) # DNS Error + CRC tables are cached for performance. + """ - # Connecting to UDP address does not send packets. - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: - s.connect(('8.8.8.8', 53)) - return s.getsockname()[0] \ No newline at end of file + _cache: t.Dict[int, t.List[int]] = {} + + @classmethod + def get_table(cls, polynomial: int) -> t.List[int]: + """Return the CRC-16 table for a polynomial.""" + try: + crc_table = cls._cache[polynomial] + except KeyError: + crc_table = [] + for dividend in range(0, 256): + remainder = dividend + for _ in range(0, 8): + if remainder & 1: + remainder = remainder >> 1 ^ polynomial + else: + remainder = remainder >> 1 + crc_table.append(remainder) + cls._cache[polynomial] = crc_table + return crc_table + + @classmethod + def calculate( + cls, + sequence: t.Sequence[int], + polynomial: int = 0xA001, # CRC-16-ANSI. + init_value: int = 0xFFFF, + ) -> int: + """Calculate the CRC-16 of a sequence of integers.""" + crc_table = cls.get_table(polynomial) + crc = init_value + for item in sequence: + crc = crc >> 8 ^ crc_table[(crc ^ item) & 0xFF] + return crc diff --git a/broadlinkmanager/broadlink/light.py b/broadlinkmanager/broadlink/light.py new file mode 100644 index 0000000..b3e20a1 --- /dev/null +++ b/broadlinkmanager/broadlink/light.py @@ -0,0 +1,197 @@ +"""Support for lights.""" +import enum +import json +import struct + +from . import exceptions as e +from .device import Device + + +class lb1(Device): + """Controls a Broadlink LB1.""" + + TYPE = "LB1" + + @enum.unique + class ColorMode(enum.IntEnum): + """Enumerates color modes.""" + + RGB = 0 + WHITE = 1 + SCENE = 2 + + def get_state(self) -> dict: + """Return the power state of the device. + + Example: `{'red': 128, 'blue': 255, 'green': 128, 'pwr': 1, 'brightness': 75, 'colortemp': 2700, 'hue': 240, 'saturation': 50, 'transitionduration': 1500, 'maxworktime': 0, 'bulb_colormode': 1, 'bulb_scenes': '["@01686464,0,0,0", "#ffffff,10,0,#000000,190,0,0", "2700+100,0,0,0", "#ff0000,500,2500,#00FF00,500,2500,#0000FF,500,2500,0", "@01686464,100,2400,@01686401,100,2400,0", "@01686464,100,2400,@01686401,100,2400,@005a6464,100,2400,@005a6401,100,2400,0", "@01686464,10,0,@00000000,190,0,0", "@01686464,200,0,@005a6464,200,0,0"]', 'bulb_scene': '', 'bulb_sceneidx': 255}` + """ + packet = self._encode(1, {}) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response) + + def set_state( + self, + pwr: bool = None, + red: int = None, + blue: int = None, + green: int = None, + brightness: int = None, + colortemp: int = None, + hue: int = None, + saturation: int = None, + transitionduration: int = None, + maxworktime: int = None, + bulb_colormode: int = None, + bulb_scenes: str = None, + bulb_scene: str = None, + bulb_sceneidx: int = None, + ) -> dict: + """Set the power state of the device.""" + state = {} + if pwr is not None: + state["pwr"] = int(bool(pwr)) + if red is not None: + state["red"] = int(red) + if blue is not None: + state["blue"] = int(blue) + if green is not None: + state["green"] = int(green) + if brightness is not None: + state["brightness"] = int(brightness) + if colortemp is not None: + state["colortemp"] = int(colortemp) + if hue is not None: + state["hue"] = int(hue) + if saturation is not None: + state["saturation"] = int(saturation) + if transitionduration is not None: + state["transitionduration"] = int(transitionduration) + if maxworktime is not None: + state["maxworktime"] = int(maxworktime) + if bulb_colormode is not None: + state["bulb_colormode"] = int(bulb_colormode) + if bulb_scenes is not None: + state["bulb_scenes"] = str(bulb_scenes) + if bulb_scene is not None: + state["bulb_scene"] = str(bulb_scene) + if bulb_sceneidx is not None: + state["bulb_sceneidx"] = int(bulb_sceneidx) + + packet = self._encode(2, state) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response) + + def _encode(self, flag: int, state: dict) -> bytes: + """Encode a JSON packet.""" + # flag: 1 for reading, 2 for writing. + packet = bytearray(14) + data = json.dumps(state, separators=(",", ":")).encode() + p_len = 12 + len(data) + struct.pack_into( + " dict: + """Decode a JSON packet.""" + payload = self.decrypt(response[0x38:]) + js_len = struct.unpack_from(" dict: + """Return the power state of the device. + + Example: `{'red': 128, 'blue': 255, 'green': 128, 'pwr': 1, 'brightness': 75, 'colortemp': 2700, 'hue': 240, 'saturation': 50, 'transitionduration': 1500, 'maxworktime': 0, 'bulb_colormode': 1, 'bulb_scenes': '["@01686464,0,0,0", "#ffffff,10,0,#000000,190,0,0", "2700+100,0,0,0", "#ff0000,500,2500,#00FF00,500,2500,#0000FF,500,2500,0", "@01686464,100,2400,@01686401,100,2400,0", "@01686464,100,2400,@01686401,100,2400,@005a6464,100,2400,@005a6401,100,2400,0", "@01686464,10,0,@00000000,190,0,0", "@01686464,200,0,@005a6464,200,0,0"]', 'bulb_scene': ''}` + """ + packet = self._encode(1, {}) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response) + + def set_state( + self, + pwr: bool = None, + red: int = None, + blue: int = None, + green: int = None, + brightness: int = None, + colortemp: int = None, + hue: int = None, + saturation: int = None, + transitionduration: int = None, + maxworktime: int = None, + bulb_colormode: int = None, + bulb_scenes: str = None, + bulb_scene: str = None, + ) -> dict: + """Set the power state of the device.""" + state = {} + if pwr is not None: + state["pwr"] = int(bool(pwr)) + if red is not None: + state["red"] = int(red) + if blue is not None: + state["blue"] = int(blue) + if green is not None: + state["green"] = int(green) + if brightness is not None: + state["brightness"] = int(brightness) + if colortemp is not None: + state["colortemp"] = int(colortemp) + if hue is not None: + state["hue"] = int(hue) + if saturation is not None: + state["saturation"] = int(saturation) + if transitionduration is not None: + state["transitionduration"] = int(transitionduration) + if maxworktime is not None: + state["maxworktime"] = int(maxworktime) + if bulb_colormode is not None: + state["bulb_colormode"] = int(bulb_colormode) + if bulb_scenes is not None: + state["bulb_scenes"] = str(bulb_scenes) + if bulb_scene is not None: + state["bulb_scene"] = str(bulb_scene) + + packet = self._encode(2, state) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response) + + def _encode(self, flag: int, state: dict) -> bytes: + """Encode a JSON packet.""" + # flag: 1 for reading, 2 for writing. + packet = bytearray(12) + data = json.dumps(state, separators=(",", ":")).encode() + struct.pack_into(" dict: + """Decode a JSON packet.""" + payload = self.decrypt(response[0x38:]) + js_len = struct.unpack_from(" bytes: + """Pack the timestamp to be sent over the Broadlink protocol.""" + data = bytearray(12) + utcoffset = int(datetime.utcoffset().total_seconds() / 3600) + data[:0x04] = utcoffset.to_bytes(4, "little", signed=True) + data[0x04:0x06] = datetime.year.to_bytes(2, "little") + data[0x06] = datetime.minute + data[0x07] = datetime.hour + data[0x08] = int(datetime.strftime("%y")) + data[0x09] = datetime.isoweekday() + data[0x0A] = datetime.day + data[0x0B] = datetime.month + return data + + @staticmethod + def unpack(data: bytes) -> dt.datetime: + """Unpack a timestamp received over the Broadlink protocol.""" + utcoffset = int.from_bytes(data[0x00:0x04], "little", signed=True) + year = int.from_bytes(data[0x04:0x06], "little") + minute = data[0x06] + hour = data[0x07] + subyear = data[0x08] + isoweekday = data[0x09] + day = data[0x0A] + month = data[0x0B] + + tz_info = dt.timezone(dt.timedelta(hours=utcoffset)) + datetime = dt.datetime(year, month, day, hour, minute, 0, 0, tz_info) + + if datetime.isoweekday() != isoweekday: + raise ValueError("isoweekday does not match") + if int(datetime.strftime("%y")) != subyear: + raise ValueError("subyear does not match") + + return datetime + + @staticmethod + def now() -> dt.datetime: + """Return the current date and time with timezone info.""" + tz_info = dt.timezone(dt.timedelta(seconds=-time.timezone)) + return dt.datetime.now(tz_info) diff --git a/broadlinkmanager/broadlink/remote.py b/broadlinkmanager/broadlink/remote.py new file mode 100644 index 0000000..017dac4 --- /dev/null +++ b/broadlinkmanager/broadlink/remote.py @@ -0,0 +1,126 @@ +"""Support for universal remotes.""" +import struct + +from . import exceptions as e +from .device import Device + + +class rmmini(Device): + """Controls a Broadlink RM mini 3.""" + + TYPE = "RMMINI" + + def _send(self, command: int, data: bytes = b"") -> bytes: + """Send a packet to the device.""" + packet = struct.pack(" None: + """Update device name and lock status.""" + resp = self._send(0x1) + self.name = resp[0x48:].split(b"\x00")[0].decode() + self.is_locked = bool(resp[0x87]) + + def send_data(self, data: bytes) -> None: + """Send a code to the device.""" + self._send(0x2, data) + + def enter_learning(self) -> None: + """Enter infrared learning mode.""" + self._send(0x3) + + def check_data(self) -> bytes: + """Return the last captured code.""" + return self._send(0x4) + + +class rmpro(rmmini): + """Controls a Broadlink RM pro.""" + + TYPE = "RMPRO" + + def sweep_frequency(self) -> None: + """Sweep frequency.""" + self._send(0x19) + + def check_frequency(self) -> bool: + """Return True if the frequency was identified successfully.""" + resp = self._send(0x1A) + return resp[0] == 1 + + def find_rf_packet(self) -> None: + """Enter radiofrequency learning mode.""" + self._send(0x1B) + + def cancel_sweep_frequency(self) -> None: + """Cancel sweep frequency.""" + self._send(0x1E) + + def check_sensors(self) -> dict: + """Return the state of the sensors.""" + resp = self._send(0x1) + temp = struct.unpack(" float: + """Return the temperature.""" + return self.check_sensors()["temperature"] + + +class rmminib(rmmini): + """Controls a Broadlink RM mini 3 (new firmware).""" + + TYPE = "RMMINIB" + + def _send(self, command: int, data: bytes = b"") -> bytes: + """Send a packet to the device.""" + packet = struct.pack(" dict: + """Return the state of the sensors.""" + resp = self._send(0x24) + temp = struct.unpack(" float: + """Return the temperature.""" + return self.check_sensors()["temperature"] + + def check_humidity(self) -> float: + """Return the humidity.""" + return self.check_sensors()["humidity"] + + +class rm4pro(rm4mini, rmpro): + """Controls a Broadlink RM4 pro.""" + + TYPE = "RM4PRO" + + +class rm(rmpro): + """For backwards compatibility.""" + + TYPE = "RM2" + + +class rm4(rm4pro): + """For backwards compatibility.""" + + TYPE = "RM4" diff --git a/broadlinkmanager/broadlink/sensor.py b/broadlinkmanager/broadlink/sensor.py new file mode 100644 index 0000000..33e7587 --- /dev/null +++ b/broadlinkmanager/broadlink/sensor.py @@ -0,0 +1,47 @@ +"""Support for sensors.""" +import struct + +from . import exceptions as e +from .device import Device + + +class a1(Device): + """Controls a Broadlink A1.""" + + TYPE = "A1" + + _SENSORS_AND_LEVELS = ( + ("light", ("dark", "dim", "normal", "bright")), + ("air_quality", ("excellent", "good", "normal", "bad")), + ("noise", ("quiet", "normal", "noisy")), + ) + + def check_sensors(self) -> dict: + """Return the state of the sensors.""" + data = self.check_sensors_raw() + for sensor, levels in self._SENSORS_AND_LEVELS: + try: + data[sensor] = levels[data[sensor]] + except IndexError: + data[sensor] = "unknown" + return data + + def check_sensors_raw(self) -> dict: + """Return the state of the sensors in raw format.""" + packet = bytearray([0x1]) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + data = payload[0x4:] + + temperature = struct.unpack(" None: + """Set the power state of the device.""" + packet = bytearray(4) + packet[0] = bool(pwr) + response = self.send_packet(0x66, packet) + e.check_error(response[0x22:0x24]) + + +class sp2(Device): + """Controls a Broadlink SP2.""" + + TYPE = "SP2" + + def set_power(self, pwr: bool) -> None: + """Set the power state of the device.""" + packet = bytearray(16) + packet[0] = 2 + packet[4] = bool(pwr) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + + def check_power(self) -> bool: + """Return the power state of the device.""" + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return bool(payload[0x4]) + + +class sp2s(sp2): + """Controls a Broadlink SP2S.""" + + TYPE = "SP2S" + + def get_energy(self) -> float: + """Return the power consumption in W.""" + packet = bytearray(16) + packet[0] = 4 + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return int.from_bytes(payload[0x4:0x7], "little") / 1000 + + +class sp3(Device): + """Controls a Broadlink SP3.""" + + TYPE = "SP3" + + def set_power(self, pwr: bool) -> None: + """Set the power state of the device.""" + packet = bytearray(16) + packet[0] = 2 + packet[4] = self.check_nightlight() << 1 | bool(pwr) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + + def set_nightlight(self, ntlight: bool) -> None: + """Set the night light state of the device.""" + packet = bytearray(16) + packet[0] = 2 + packet[4] = bool(ntlight) << 1 | self.check_power() + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + + def check_power(self) -> bool: + """Return the power state of the device.""" + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return bool(payload[0x4] & 1) + + def check_nightlight(self) -> bool: + """Return the state of the night light.""" + packet = bytearray(16) + packet[0] = 1 + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return bool(payload[0x4] & 2) + + +class sp3s(sp2): + """Controls a Broadlink SP3S.""" + + TYPE = "SP3S" + + def get_energy(self) -> float: + """Return the power consumption in W.""" + packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45]) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + energy = payload[0x7:0x4:-1].hex() + return int(energy) / 100 + + +class sp4(Device): + """Controls a Broadlink SP4.""" + + TYPE = "SP4" + + def set_power(self, pwr: bool) -> None: + """Set the power state of the device.""" + self.set_state(pwr=pwr) + + def set_nightlight(self, ntlight: bool) -> None: + """Set the night light state of the device.""" + self.set_state(ntlight=ntlight) + + def set_state( + self, + pwr: bool = None, + ntlight: bool = None, + indicator: bool = None, + ntlbrightness: int = None, + maxworktime: int = None, + childlock: bool = None, + ) -> dict: + """Set state of device.""" + state = {} + if pwr is not None: + state["pwr"] = int(bool(pwr)) + if ntlight is not None: + state["ntlight"] = int(bool(ntlight)) + if indicator is not None: + state["indicator"] = int(bool(indicator)) + if ntlbrightness is not None: + state["ntlbrightness"] = ntlbrightness + if maxworktime is not None: + state["maxworktime"] = maxworktime + if childlock is not None: + state["childlock"] = int(bool(childlock)) + + packet = self._encode(2, state) + response = self.send_packet(0x6A, packet) + return self._decode(response) + + def check_power(self) -> bool: + """Return the power state of the device.""" + state = self.get_state() + return bool(state["pwr"]) + + def check_nightlight(self) -> bool: + """Return the state of the night light.""" + state = self.get_state() + return bool(state["ntlight"]) + + def get_state(self) -> dict: + """Get full state of device.""" + packet = self._encode(1, {}) + response = self.send_packet(0x6A, packet) + return self._decode(response) + + def _encode(self, flag: int, state: dict) -> bytes: + """Encode a message.""" + packet = bytearray(12) + data = json.dumps(state, separators=(",", ":")).encode() + struct.pack_into( + " dict: + """Decode a message.""" + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + js_len = struct.unpack_from(" dict: + """Get full state of device.""" + state = super().get_state() + + # Convert sensor data to float. Remove keys if sensors are not supported. + sensor_attrs = ["current", "volt", "power", "totalconsum", "overload"] + for attr in sensor_attrs: + value = state.pop(attr, -1) + if value != -1: + state[attr] = value / 1000 + return state + + def _encode(self, flag: int, state: dict) -> bytes: + """Encode a message.""" + packet = bytearray(14) + data = json.dumps(state, separators=(",", ":")).encode() + length = 12 + len(data) + struct.pack_into( + " dict: + """Decode a message.""" + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + js_len = struct.unpack_from(" dict: + """Return the power state of the device. + + Example: `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}` + """ + packet = self._encode(1, {}) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response) + + def set_state( + self, + pwr: bool = None, + pwr1: bool = None, + pwr2: bool = None, + maxworktime: int = None, + maxworktime1: int = None, + maxworktime2: int = None, + idcbrightness: int = None, + ) -> dict: + """Set the power state of the device.""" + state = {} + if pwr is not None: + state["pwr"] = int(bool(pwr)) + if pwr1 is not None: + state["pwr1"] = int(bool(pwr1)) + if pwr2 is not None: + state["pwr2"] = int(bool(pwr2)) + if maxworktime is not None: + state["maxworktime"] = maxworktime + if maxworktime1 is not None: + state["maxworktime1"] = maxworktime1 + if maxworktime2 is not None: + state["maxworktime2"] = maxworktime2 + if idcbrightness is not None: + state["idcbrightness"] = idcbrightness + + packet = self._encode(2, state) + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + return self._decode(response) + + def _encode(self, flag: int, state: dict) -> bytes: + """Encode a message.""" + packet = bytearray(14) + data = json.dumps(state).encode() + length = 12 + len(data) + struct.pack_into( + " dict: + """Decode a message.""" + payload = self.decrypt(response[0x38:]) + js_len = struct.unpack_from(" None: + """Set the power state of the device.""" + packet = bytearray(16) + packet[0x00] = 0x0D + packet[0x02] = 0xA5 + packet[0x03] = 0xA5 + packet[0x04] = 0x5A + packet[0x05] = 0x5A + packet[0x06] = 0xB2 + ((sid_mask << 1) if pwr else sid_mask) + packet[0x07] = 0xC0 + packet[0x08] = 0x02 + packet[0x0A] = 0x03 + packet[0x0D] = sid_mask + packet[0x0E] = sid_mask if pwr else 0 + + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + + def set_power(self, sid: int, pwr: bool) -> None: + """Set the power state of the device.""" + sid_mask = 0x01 << (sid - 1) + self.set_power_mask(sid_mask, pwr) + + def check_power_raw(self) -> int: + """Return the power state of the device in raw format.""" + packet = bytearray(16) + packet[0x00] = 0x0A + packet[0x02] = 0xA5 + packet[0x03] = 0xA5 + packet[0x04] = 0x5A + packet[0x05] = 0x5A + packet[0x06] = 0xAE + packet[0x07] = 0xC0 + packet[0x08] = 0x01 + + response = self.send_packet(0x6A, packet) + e.check_error(response[0x22:0x24]) + payload = self.decrypt(response[0x38:]) + return payload[0x0E] + + def check_power(self) -> dict: + """Return the power state of the device.""" + data = self.check_power_raw() + return { + "s1": bool(data & 1), + "s2": bool(data & 2), + "s3": bool(data & 4), + "s4": bool(data & 8), + } diff --git a/broadlinkmanager/broadlinkmanager.py b/broadlinkmanager/broadlinkmanager.py index c9b3ed4..4ce1378 100644 --- a/broadlinkmanager/broadlinkmanager.py +++ b/broadlinkmanager/broadlinkmanager.py @@ -1,9 +1,30 @@ # region Importing -import os, json, subprocess, time, broadlink, argparse, datetime, re, shutil, uvicorn, socket, aiofiles +import os +import json +import subprocess +import time +import broadlink +import argparse +import datetime +import re +import shutil +import uvicorn +import socket +import aiofiles from os import environ, path from json import dumps from broadlink.exceptions import ReadError, StorageError +from broadlink import exceptions as e +from broadlink.const import DEFAULT_BCAST_ADDR, DEFAULT_PORT, DEFAULT_TIMEOUT +from broadlink.alarm import S1C +from broadlink.climate import hysen +from broadlink.cover import dooya +from broadlink.device import Device, ping, scan +from broadlink.light import lb1, lb2 +from broadlink.remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro +from broadlink.sensor import a1 +from broadlink.switch import bg1, mp1, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b from subprocess import call from loguru import logger from fastapi import FastAPI, Request, File, Form, UploadFile @@ -21,40 +42,45 @@ ENABLE_GOOGLE_ANALYTICS = os.getenv("ENABLE_GOOGLE_ANALYTICS") # endregion -#Get Lan IP +# Get Lan IP + + def GetLocalIP(): - p = subprocess.Popen("hostname -I | awk '{print $1}'", stdout=subprocess.PIPE, shell=True) + p = subprocess.Popen( + "hostname -I | awk '{print $1}'", stdout=subprocess.PIPE, shell=True) (output, err) = p.communicate() p_status = p.wait() ip = re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", str(output))[0] logger.debug(ip) return str(ip) + local_ip_address = GetLocalIP() # Get version from version file for dynamic change + + def GetVersionFromFle(): - with open("VERSION","r") as version: + with open("VERSION", "r") as version: v = version.read() return v # Tags metadata for swagger docs tags_metadata = [ - + { "name": "Commands", "description": "Learn / Send RF or IR commands", - - }, - { + + }, + { "name": "Devices", "description": "Scan for devices on the network or load/save from/to file", - - }, - -] + }, + +] # region Parsing Default arguments for descovery @@ -72,7 +98,8 @@ def GetVersionFromFle(): # region Declaring Flask app -app = FastAPI(title="Apprise API", description="Send multi channel notification using single endpoint", version=GetVersionFromFle(), openapi_tags=tags_metadata,contact={"name":"Tomer Klein","email":"tomer.klein@gmail.com","url":"https://github.com/t0mer/broadlinkmanager-docker"}) +app = FastAPI(title="Apprise API", description="Send multi channel notification using single endpoint", version=GetVersionFromFle( +), openapi_tags=tags_metadata, contact={"name": "Tomer Klein", "email": "tomer.klein@gmail.com", "url": "https://github.com/t0mer/broadlinkmanager-docker"}) logger.info("Configuring app") app.mount("/dist", StaticFiles(directory="dist"), name="dist") app.mount("/js", StaticFiles(directory="dist/js"), name="js") @@ -111,8 +138,9 @@ def GetVersionFromFle(): def get_analytics_code(): try: - if ENABLE_GOOGLE_ANALYTICS=="True": - analytics_file_path = os.path.join(app.root_path, 'templates', 'analytics_code.html') + if ENABLE_GOOGLE_ANALYTICS == "True": + analytics_file_path = os.path.join( + app.root_path, 'templates', 'analytics_code.html') f = open(analytics_file_path, "r") content = f.read() f.close() @@ -127,73 +155,110 @@ def get_analytics_code(): analytics_code = get_analytics_code() + def getDeviceName(deviceType): name = { - 0x2711: "SP2", - 0x2719: "Honeywell SP2", - 0x7919: "Honeywell SP2", - 0x271a: "Honeywell SP2", - 0x791a: "Honeywell SP2", - 0x2720: "SPMini", - 0x753e: "SP3", - 0x7D00: "OEM branded SP3", - 0x947a: "SP3S", - 0x9479: "SP3S", - 0x2728: "SPMini2", - 0x2733: "OEM branded SPMini", - 0x273e: "OEM branded SPMini", - 0x7530: "OEM branded SPMini2", - 0x7546: "OEM branded SPMini2", - 0x7918: "OEM branded SPMini2", - 0x7D0D: "TMall OEM SPMini3", - 0x2736: "SPMiniPlus", - 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", - 0x2714: "A1", - 0x4EB5: "MP1", - 0x4EF7: "Honyar oem mp1", - 0x4EAD: "Hysen controller", - 0x2722: "S1 (SmartOne Alarm Kit)", - 0x4E4D: "Dooya DT360E (DOOYA_CURTAIN_V2)", - 0x51da: "RM4 Mini", - 0x5f36: "RM Mini 3", - 0x6026: "RM4 Pro", - 0x6070: "RM4c Mini", - 0x61a2: "RM4 Pro", - 0x610e: "RM4 Mini", - 0x610f: "RM4c", - 0x62bc: "RM4 Mini", - 0x62be: "RM4c Mini", - 0x51E3: "BG Electrical Smart Power Socket", - 0x60c8: "RGB Smart Bulb", - 0x6539: "RM4c Mini", - 0x653a: "RM4 Mini", - 0x653c: "RM4 Pro", - 0x649b: "RM4 Pro", - 0x6184: "RM4C mini", - 0x648d: "RM4 Mini", - 0x5209: "RM4 TV Mate", - 0x27C3: "RM pro+", - 0x27C7: "RM mini 3", - 0x27CC: "RM mini 3", - 0x27D0: "RM mini 3", - 0x27D3: "RM mini 3", - 0x27DC: "RM mini 3", - 0x6507: "RM mini 3", - 0x6508: "RM mini 3", + 0x0000: "SP1 ( Broadlink)", + 0x2717: "NEO ( Ankuoo)", + 0x2719: "SP2-compatible ( Honeywell)", + 0x271A: "SP2-compatible ( Honeywell)", + 0x2720: "SPmini ( Broadlink)", + 0x2728: "SP2-compatible ( URANT)", + 0x273E: "SPmini ( Broadlink)", + 0x7530: "SP2 ( Broadlink(OEM)", + 0x7539: "SP2-IL ( Broadlink(OEM)", + 0x753E: "SPmini3 ( Broadlink)", + 0x7540: "MP2 ( Broadlink)", + 0x7544: "SP2-CL ( Broadlink)", + 0x7546: "SP2-UK/BR/IN ( Broadlink(OEM)", + 0x7547: "SC1 ( Broadlink)", + 0x7918: "SP2 ( Broadlink(OEM)", + 0x7919: "SP2-compatible ( Honeywell)", + 0x791A: "SP2-compatible ( Honeywell)", + 0x7D0D: "SPmini3 ( Broadlink(OEM)", + 0x2711: "SP2 ( Broadlink)", + 0x2716: "NEOPRO ( Ankuoo)", + 0x271D: "Ego ( Efergy)", + 0x2736: "SPmini+ ( Broadlink)", + 0x2733: "SP3 ( Broadlink)", + 0x7D00: "SP3-EU ( Broadlink(OEM)", + 0x9479: "SP3S-US ( Broadlink)", + 0x947A: "SP3S-EU ( Broadlink)", + 0x7568: "SP4L-CN ( Broadlink)", + 0x756C: "SP4M ( Broadlink)", + 0x756F: "MCB1 ( Broadlink)", + 0x7579: "SP4L-EU ( Broadlink)", + 0x757B: "SP4L-AU ( Broadlink)", + 0x7583: "SPmini3 ( Broadlink)", + 0x7587: "SP4L-UK ( Broadlink)", + 0x7D11: "SPmini3 ( Broadlink)", + 0xA56A: "MCB1 ( Broadlink)", + 0xA56B: "SCB1E ( Broadlink)", + 0xA56C: "SP4L-EU ( Broadlink)", + 0xA589: "SP4L-UK ( Broadlink)", + 0xA5D3: "SP4L-EU ( Broadlink)", + 0x5115: "SCB1E ( Broadlink)", + 0x51E2: "AHC/U-01 ( BGElectrical)", + 0x6111: "MCB1 ( Broadlink)", + 0x6113: "SCB1E ( Broadlink)", + 0x618B: "SP4L-EU ( Broadlink)", + 0x6489: "SP4L-AU ( Broadlink)", + 0x648B: "SP4M-US ( Broadlink)", + 0x6494: "SCB2 ( Broadlink)", + 0x2737: "RMmini3 ( Broadlink)", + 0x278F: "RMmini ( Broadlink)", + 0x27C2: "RMmini3 ( Broadlink)", + 0x27C7: "RMmini3 ( Broadlink)", + 0x27CC: "RMmini3 ( Broadlink)", + 0x27CD: "RMmini3 ( Broadlink)", + 0x27D0: "RMmini3 ( Broadlink)", + 0x27D1: "RMmini3 ( Broadlink)", + 0x27D3: "RMmini3 ( Broadlink)", + 0x27DC: "RMmini3 ( Broadlink)", + 0x27DE: "RMmini3 ( Broadlink)", + 0x2712: "RMpro/pro+ ( Broadlink)", + 0x272A: "RMpro ( Broadlink)", + 0x273D: "RMpro ( Broadlink)", + 0x277C: "RMhome ( Broadlink)", + 0x2783: "RMhome ( Broadlink)", + 0x2787: "RMpro ( Broadlink)", + 0x278B: "RMplus ( Broadlink)", + 0x2797: "RMpro+ ( Broadlink)", + 0x279D: "RMpro+ ( Broadlink)", + 0x27A1: "RMplus ( Broadlink)", + 0x27A6: "RMplus ( Broadlink)", + 0x27A9: "RMpro+ ( Broadlink)", + 0x27C3: "RMpro+ ( Broadlink)", + 0x5F36: "RMmini3 ( Broadlink)", + 0x6507: "RMmini3 ( Broadlink)", + 0x6508: "RMmini3 ( Broadlink)", + 0x51DA: "RM4mini ( Broadlink)", + 0x6070: "RM4Cmini ( Broadlink)", + 0x610E: "RM4mini ( Broadlink)", + 0x610F: "RM4Cmini ( Broadlink)", + 0x62BC: "RM4mini ( Broadlink)", + 0x62BE: "RM4Cmini ( Broadlink)", + 0x6364: "RM4S ( Broadlink)", + 0x648D: "RM4mini ( Broadlink)", + 0x6539: "RM4Cmini ( Broadlink)", + 0x653A: "RM4mini ( Broadlink)", + 0x6026: "RM4pro ( Broadlink)", + 0x6184: "RM4Cpro ( Broadlink)", + 0x61A2: "RM4pro ( Broadlink)", + 0x649B: "RM4pro ( Broadlink)", + 0x653C: "RM4pro ( Broadlink)", + 0x2714: "e-Sensor ( Broadlink)", + 0x5043: "SB800TD ( Broadlink(OEM)", + 0x504E: "LB1 ( Broadlink)", + 0x606E: "SB500TD ( Broadlink(OEM)", + 0x60C7: "LB1 ( Broadlink)", + 0x60C8: "LB1 ( Broadlink)", + 0x6112: "LB1 ( Broadlink)", + 0xA4F4: "LB27R1 ( Broadlink)", + 0x2722: "S2KIT ( Broadlink)", + 0x4EAD: "HY02/HY03 ( Hysen)", + 0x4E4D: "DT360E-45/20 ( Dooya)", + 0x51E3: "BG800/BG900 ( BGElectrical)", } return name.get(deviceType, "Not Supported") @@ -202,6 +267,7 @@ def getDeviceName(deviceType): def auto_int(x): return int(x, 0) + def to_microseconds(bytes): result = [] # print bytes[0] # 0x26 = 38for IR @@ -218,6 +284,7 @@ def to_microseconds(bytes): break return result + def durations_to_broadlink(durations): result = bytearray() result.append(IR_TOKEN) @@ -232,6 +299,7 @@ def durations_to_broadlink(durations): result.append(num % 256) return result + def format_durations(data): result = '' for i in range(0, len(data)): @@ -240,24 +308,28 @@ def format_durations(data): result += ('+' if i % 2 == 0 else '-') + str(data[i]) return result + def parse_durations(str): result = [] for s in str.split(): result.append(abs(int(s))) return result + def initDevice(dtype, host, mac): dtypeTmp = dtype if dtypeTmp == '0x6539': - dtypeTmp = '0x610F' + dtypeTmp = '0x610F' _dtype = int(dtypeTmp, 0) _host = host _mac = bytearray.fromhex(mac) return broadlink.gendevice(_dtype, (_host, 80), _mac) + def GetDevicesFilePath(): return os.path.join(app.root_path, 'data', 'devices.json') + def writeXml(_file): root = ET.Element("root") doc = ET.SubElement(root, "doc") @@ -269,37 +341,37 @@ def writeXml(_file): @app.get('/', include_in_schema=False) def devices(request: Request): - return templates.TemplateResponse('index.html', context={'request': request,'analytics':analytics_code, 'version': GetVersionFromFle()}) + return templates.TemplateResponse('index.html', context={'request': request, 'analytics': analytics_code, 'version': GetVersionFromFle()}) @app.get('/generator', include_in_schema=False) def generator(request: Request): - return templates.TemplateResponse('generator.html', context={'request': request,'analytics':analytics_code, 'version': GetVersionFromFle()}) + return templates.TemplateResponse('generator.html', context={'request': request, 'analytics': analytics_code, 'version': GetVersionFromFle()}) @app.get('/livolo', include_in_schema=False) def livolo(request: Request): - return templates.TemplateResponse('livolo.html', context={'request': request,'analytics':analytics_code, 'version': GetVersionFromFle()}) + return templates.TemplateResponse('livolo.html', context={'request': request, 'analytics': analytics_code, 'version': GetVersionFromFle()}) @app.get('/energenie', include_in_schema=False) def energenie(request: Request): - return templates.TemplateResponse('energenie.html', context={'request': request,'analytics':analytics_code, 'version': GetVersionFromFle()}) + return templates.TemplateResponse('energenie.html', context={'request': request, 'analytics': analytics_code, 'version': GetVersionFromFle()}) @app.get('/repeats', include_in_schema=False) def repeats(request: Request): - return templates.TemplateResponse('repeats.html', context={'request': request,'analytics':analytics_code, 'version': GetVersionFromFle()}) + return templates.TemplateResponse('repeats.html', context={'request': request, 'analytics': analytics_code, 'version': GetVersionFromFle()}) @app.get('/convert', include_in_schema=False) def convert(request: Request): - return templates.TemplateResponse('convert.html', context={'request': request,'analytics':analytics_code, 'version': GetVersionFromFle()}) + return templates.TemplateResponse('convert.html', context={'request': request, 'analytics': analytics_code, 'version': GetVersionFromFle()}) @app.get('/about', include_in_schema=False) def about(request: Request): - return templates.TemplateResponse('about.html', context={'request': request,'analytics':analytics_code, 'version': GetVersionFromFle()}) + return templates.TemplateResponse('about.html', context={'request': request, 'analytics': analytics_code, 'version': GetVersionFromFle()}) @app.get('/temperature', tags=["Commands"], summary="Read Temperature") @@ -315,7 +387,7 @@ def temperature(request: Request, mac: str = "", host: str = "", type: str = "") @app.get('/ir/learn', tags=["Commands"], summary="Learn IR code") -def learnir(request: Request, mac: str = "", host: str = "", type: str = "", command: str =""): +def learnir(request: Request, mac: str = "", host: str = "", type: str = "", command: str = ""): logger.info("Learning IR Code for device: " + host) dev = initDevice(type, host, mac) dev.auth() @@ -338,8 +410,10 @@ def learnir(request: Request, mac: str = "", host: str = "", type: str = "", com return JSONResponse('{"data":"' + learned + '","success":1,"message":"IR Data Received"}') # Send IR/RF + + @app.get('/command/send', tags=["Commands"], summary="Send IR/RF Command") -def command(request: Request, mac: str = "", host: str = "", type: str = "", command: str =""): +def command(request: Request, mac: str = "", host: str = "", type: str = "", command: str = ""): logger.info("Sending Command (IR/RF) using device: " + host) dev = initDevice(type, host, mac) logger.info("Sending command: " + command) @@ -355,15 +429,15 @@ def command(request: Request, mac: str = "", host: str = "", type: str = "", com # Learn RF @app.get('/rf/learn', include_in_schema=False) -def sweep(request: Request, mac: str = "", host: str = "", type: str = "", command: str =""): +def sweep(request: Request, mac: str = "", host: str = "", type: str = "", command: str = ""): global _continu_to_sweep global _rf_sweep_message global _rf_sweep_status _continu_to_sweep = False _rf_sweep_message = '' _rf_sweep_status = False - logger.info("Device:" + host + " entering RF learning mode" ) - dev = initDevice(type, host,mac) + logger.info("Device:" + host + " entering RF learning mode") + dev = initDevice(type, host, mac) dev.auth() logger.info("Device:" + host + " is sweeping for frequency") dev.sweep_frequency() @@ -389,9 +463,10 @@ def sweep(request: Request, mac: str = "", host: str = "", type: str = "", comma _rf_sweep_message = "Click The Continue button" _rf_sweep_message = "To complete learning, single press the button you want to learn" - logger.info("To complete learning, single press the button you want to learn") + logger.info( + "To complete learning, single press the button you want to learn") _rf_sweep_status = False - logger.error("Device:" +host + " is searching for RF packets!") + logger.error("Device:" + host + " is searching for RF packets!") dev.find_rf_packet() start = time.time() while time.time() - start < TIMEOUT: @@ -417,6 +492,7 @@ def sweep(request: Request, mac: str = "", host: str = "", type: str = "", comma # Get RF Learning state + @app.get('/rf/status', include_in_schema=False) def rfstatus(request: Request): global _continu_to_sweep @@ -425,6 +501,8 @@ def rfstatus(request: Request): return JSONResponse('{"_continu_to_sweep":"' + str(_continu_to_sweep) + '","_rf_sweep_message":"' + _rf_sweep_message + '","_rf_sweep_status":"' + str(_rf_sweep_status) + '" }') # Continue with RF Scan + + @app.get('/rf/continue', include_in_schema=False) def rfcontinue(request: Request): global _continu_to_sweep @@ -469,17 +547,19 @@ def load_devices_from_file(request: Request): @app.get('/autodiscover', tags=["Devices"]) -def search_for_devices(request: Request,freshscan: str = "1"): +def search_for_devices(request: Request, freshscan: str = "1"): _devices = '' if path.exists(GetDevicesFilePath()) and freshscan != "1": return load_devices_from_file(request) else: logger.info("Searcing for devices...") _devices = '[' - devices = broadlink.discover(timeout=5, local_ip_address=local_ip_address, discover_ip_address="255.255.255.255") + devices = broadlink.discover( + timeout=5, local_ip_address=local_ip_address, discover_ip_address="255.255.255.255") for device in devices: if device.auth(): - logger.info("New device detected: " + getDeviceName(device.devtype) + " (ip: " + device.host[0] + ", mac: " + ''.join(format(x, '02x') for x in device.mac) + ")") + logger.info("New device detected: " + getDeviceName(device.devtype) + " (ip: " + + device.host[0] + ", mac: " + ''.join(format(x, '02x') for x in device.mac) + ")") _devices = _devices + '{"name":"' + \ getDeviceName(device.devtype) + '",' _devices = _devices + '"type":"' + \ @@ -488,7 +568,7 @@ def search_for_devices(request: Request,freshscan: str = "1"): _devices = _devices + '"mac":"' + \ ''.join(format(x, '02x') for x in device.mac) + '"},' - if len(_devices)==1: + if len(_devices) == 1: _devices = _devices + ']' logger.debug("No Devices Found " + str(_devices)) else: @@ -497,32 +577,30 @@ def search_for_devices(request: Request,freshscan: str = "1"): return JSONResponse(_devices) - - @app.get('/device/ping', tags=["Devices"]) -def get_device_status(request: Request, host: str=""): +def get_device_status(request: Request, host: str = ""): try: - if host =="": + if host == "": logger.error("Host must be a valid ip or hostname") return JSONResponse('{"status":"Host must be a valid ip or hostname","success":"0"}') - p = subprocess.Popen("fping -C1 -q "+ host +" 2>&1 | grep -v '-' | wc -l", stdout=subprocess.PIPE, shell=True) + p = subprocess.Popen( + "fping -C1 -q " + host + " 2>&1 | grep -v '-' | wc -l", stdout=subprocess.PIPE, shell=True) logger.debug(host) (output, err) = p.communicate() p_status = p.wait() logger.debug(str(output)) status = re.findall('\d+', str(output))[0] - if status=="1": + if status == "1": return JSONResponse('{"status":"online","success":"1"}') else: return JSONResponse('{"status":"offline","success":"1"}') except Exception as e: - logger.error("Error pinging "+ host + " Error: " + str(e)) + logger.error("Error pinging " + host + " Error: " + str(e)) return JSONResponse('{"status":"Error pinging ' + host + '" ,"success":"0"}') # endregion API Methods - # Start Application if __name__ == '__main__': logger.info("Broadllink Manager is up and running") diff --git a/broadlinkmanager/calls.php b/broadlinkmanager/calls.php new file mode 100644 index 0000000..7e700bd --- /dev/null +++ b/broadlinkmanager/calls.php @@ -0,0 +1,154 @@ + 16) $ord1 = $ord1 - 7; if ($ord2 + > 16) $ord2 = $ord2 - 7; $str_hex[$i] = $ord1 * 16 + $ord2; + } + return $str_hex; + } + +function geturi($host, $post, $headers, $request = 0) { + + $url = "https://".$host.$post; $timeout = 7; $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); if (preg_match("/\bPOST\b/i", $headers[0])) curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, + CURLOPT_HTTPHEADER, $headers); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $timeout); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE); curl_setopt($curl, + CURLOPT_SSL_VERIFYPEER, FALSE); if ($request) curl_setopt($curl, CURLOPT_POSTFIELDS, $request); $result["msg"] = curl_exec($curl); $result["error"] = curl_errno($curl); if ($result["error"]) { + $result["msg"] = curl_error($curl); + } + return $result; + } + + + +function Auth($email = "", $password = "") { + +global $userid, $token,$timestamp,$loginsession,$nickname; + if (($email === "") || (strlen($password) < 6)) { $result["error"] = -1005; $result["msg"] = "Data Error"; + echo(1); + return $result; + } + + $authiv = array(-22, -86, -86, 58, -69, 88, 98, -94, 25, 24, -75, 119, 29, 22, 21, -86); $password = sha1($password."4969fj#k23#"); $data_str = str_pad('{"email":"'.$email.'","password":"'.$password.'"}', 112, + "\0"); $token = md5('{"email":"'.$email.'","password":"'.$password.'"}'."xgx3d*fe3478\$ukx"); + + + + $host = "account.ibroadlink.com"; $post = "/v1/account/login/api?email=".$email."&password=".$password."&serialVersionUID=2297929119272048467"; $headers = array( "GET ".$post." HTTP/1.1", "language: zh_cn", + "serialVersionUID: -6225108491617746123", "Host: ".$host, "Connection: Keep-Alive" + ); $result = geturi($host, $post, $headers); if ($result["error"]) { return $result; + } + $result = json_decode($result["msg"], true); + + if (($result["error"] != 0) || ($result["msg"] != "ok")) { return $result; + } + + $timestamp = $result["timestamp"]; $key = byte(str2hex_array($result["key"])); $request = aes128_cbc_encrypt($key, $data_str, byte($authiv)); $post = "/v2/account/login/info"; $host = "secure.ibroadlink.com"; + + $headers = array( + "POST ".$post." HTTP/1.1", "Timestamp: ".$timestamp, "Token: ".$token, "language: zh_cn", "serialVersionUID: -6225108491617746123", "Content-Length: 112", "Host: ".$host, "Connection: Keep-Alive", "Expect: + 100-continue" + ); $result = geturi($host, $post, $headers, $request); if ($result["error"]) { return $result; + } + + + + $data = json_decode($result["msg"],true); + $loginsession = $data["loginsession"]; + $nickname = $data["nickname"]; + $userid = $data["userid"]; + // echo json_encode($result); + + + $result = array("loginsession"=>$data["loginsession"], "nickname"=>$data['nickname'],"userid"=>$data["userid"],"timestamp"=>$timestamp,"token"=>$token); + echo json_encode($result); + } + + + +function GetUserInfo() { +global $userid, $token,$timestamp,$loginsession,$nickname; + + + $post = "/v1/account/userinfo/get"; + $host = "account.ibroadlink.com"; + $headers = array( + "GET ".$post." HTTP/1.1", + "LOGINSESSION: ".$loginsession, + "USERID: ".$userid, + "language: zh_cn", + "serialVersionUID: -6225108491617746123", + "Host: ".$host, + "Connection: Keep-Alive" + ); + $result = geturi($host, $post, $headers); + if ($result["error"]) { + return $result; + } + return $result; + } + + + +function GetListBackups($userid,$loginsession,$nickname) { +// global $userid, $token,$timestamp,$loginsession,$nickname; + + $timestamp = round(microtime(true) * 1000); + $post = "/rest/1.0/backup?method=list&user=".$nickname."&id=".$userid."×tamp=".$timestamp."&token=".get_token($timestamp); + $host = "ebackup.ibroadlink.com"; + $headers = array( + "GET ".$post." HTTP/1.1", + "accountType: bl", + "reqUserId: ".$userid, + "reqUserSession: ".$loginsession, + "serialVersionUID: -855048957473660878", + "Host: ".$host, + "Connection: Keep-Alive" + ); + // echo(" ".$post." "); + $result = geturi($host, $post, $headers, 0); + if ($result["error"]) { + return $result; + } + $result = json_decode($result["msg"], true); + $result["error"] = 0; + echo json_encode($result); + return $result; + } + + +function GetTimeStemp(){ +echo round(microtime(true) * 1000); +} + + +// echo Auth(getenv("email"),getenv("password")); +// GetUserInfo(); +// GetListBackups(); +#GetTimeStemp(); + + +?> diff --git a/broadlinkmanager/calls.py b/broadlinkmanager/calls.py new file mode 100644 index 0000000..ad070ac --- /dev/null +++ b/broadlinkmanager/calls.py @@ -0,0 +1,38 @@ +import subprocess, json, os, time +userid = None +nickname = None +loginsession = None +timestamp = None +token = None + + +data = str_pad(data, math.ceil(len(data) / 16) * 16, chr(0), STR_PAD_RIGHT) +# # if the script don't need output. +# subprocess.call("php /path/to/your/script.php") + +def login(): + global userid, nickname, loginsession, timestamp, token + proc = subprocess.Popen("php -r \"require 'calls.php';Auth('tomer.klein@gmail.com','tklk@2301');\"", shell=True, stdout=subprocess.PIPE) + response = proc.stdout.read() + data = json.loads(response) + + userid = data["userid"] + nickname = data ["nickname"] + loginsession = data["loginsession"] + timestamp = data["timestamp"] + token = data["token"] + +def GetBackupsList(): + global userid, nickname, loginsession, timestamp, token + proc = subprocess.Popen("php -r \"require 'calls.php';GetListBackups('" + userid + "','" + loginsession + "','" + nickname + "');\"", shell=True, stdout=subprocess.PIPE) + response = proc.stdout.read() + data = json.loads(response) + print(data['list'][0]) + +def GetTimeStamp(): + return round(time.time() * 1000) + + +if __name__ == '__main__': + login() + GetBackupsList() \ No newline at end of file From 11390984bb709990a600c3d625633f56176362e7 Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Sun, 10 Apr 2022 17:35:25 +0300 Subject: [PATCH 2/4] Moving to python-broadlink 0.18.0 --- .../__pycache__/__init__.cpython-36.pyc | Bin 6403 -> 0 bytes .../broadlink/__pycache__/alarm.cpython-36.pyc | Bin 1540 -> 0 bytes .../__pycache__/climate.cpython-36.pyc | Bin 5654 -> 0 bytes .../broadlink/__pycache__/const.cpython-36.pyc | Bin 305 -> 0 bytes .../broadlink/__pycache__/cover.cpython-36.pyc | Bin 2017 -> 0 bytes .../broadlink/__pycache__/device.cpython-36.pyc | Bin 9563 -> 0 bytes .../__pycache__/exceptions.cpython-36.pyc | Bin 6215 -> 0 bytes .../__pycache__/helpers.cpython-36.pyc | Bin 1337 -> 0 bytes .../broadlink/__pycache__/light.cpython-36.pyc | Bin 6393 -> 0 bytes .../__pycache__/protocol.cpython-36.pyc | Bin 1973 -> 0 bytes .../broadlink/__pycache__/remote.cpython-36.pyc | Bin 5196 -> 0 bytes .../broadlink/__pycache__/sensor.cpython-36.pyc | Bin 1610 -> 0 bytes .../broadlink/__pycache__/switch.cpython-36.pyc | Bin 11788 -> 0 bytes 13 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 broadlinkmanager/broadlink/__pycache__/__init__.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/alarm.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/climate.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/const.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/cover.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/device.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/exceptions.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/helpers.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/light.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/protocol.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/remote.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/sensor.cpython-36.pyc delete mode 100644 broadlinkmanager/broadlink/__pycache__/switch.cpython-36.pyc diff --git a/broadlinkmanager/broadlink/__pycache__/__init__.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index 3d8f4230e101813c7b3dec6cd2f56be5fdec0e72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6403 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 B)oB0# diff --git a/broadlinkmanager/broadlink/__pycache__/alarm.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/alarm.cpython-36.pyc deleted file mode 100644 index 58b16d39d0f988d212e6e538b6894f9af8ac5745..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1540 zcmZuxU2oh(6rB%$?IvjFNn z=Ycu-3|e&?K#-USDp*d(Eanu)Ja@)!>{9Y25sq-riEyR6sHo1dTcLj$nf0 zz85@pCG2@Yg)f40C-y}L_U~HWzjJrCb5|8ctGo)+@N=zFkq?Wr5Inuv+JORMeOXjW z&-}0DH2gvx&o;hMNNHt{czp- z%T<=<)jkZKEz_^UlK;uj0Yf9h&oF3Y_z`755d>{R*N}Muxdw!~Mcarj$%qMONg?+f zvQLG(Lxi`)dhwetSF1R5g%A4N=Ku(%u#4J6B{WPYM=AeU`~HvsUMR9G;c&nRTp&D5kY)?eux`FazaG3Hq$ zWeWK-mP)&poyzPikyS{m!>v=~Usb6G^5j&PyZu89LJs}e2`Y5g*t=5V(4 zE%56P5Rekk#Rd9x1f*v-U(7S6v_jg11vH@RbQ50aS(k2c#=8FO#>)%jFbZstBq(l@ z*lv=Hm6+r>UQdz-lQds>{3L-QO_JC_HT8SQ!x6*GXuMfk?HU1}tJsJK&T1VXpbm35 wql^YzqvsG5`~-SSG?uXb5XX>0ygoHnLi5$jer$uimgGL3z^b{pAmVlYFYOq7hyVZp diff --git a/broadlinkmanager/broadlink/__pycache__/climate.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/climate.cpython-36.pyc deleted file mode 100644 index 9bc5e214d042c165e3ddb46c6fe16bd67700950b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5654 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 SJGhVNOye?ur5Cg2&iprDya4h5 diff --git a/broadlinkmanager/broadlink/__pycache__/const.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/const.cpython-36.pyc deleted file mode 100644 index 70fab033a19717a9d5ad8243dac7ee5935338859..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 305 zcmXr!<>k6`Cn%|$fq~&M5W@i@kmUfx#S%awg&~R|g)xdTg(-?Dg*l2jg(Zq5g*BK# zldX!&IX|zsBr&g~Sg(rT$kbF13BL>g>U{|^=_MDC@Y7_y#U3A@lAjzOe@n>4)y*-~ zCnVm<*)cdI-qFP+=oSx5EWke~BuW^@3vvw!ij4R43kmbN#Saq<@$_}|4-Hw#P{aW= z8ch81(9bU@(N9S&(@)PV(N8MMPfW?l%*)PA%u7s9Ez$*AoSj;PA&wBxPX_r#ub}c4 WhfQvNN@-529mw&;ARAa1ArJtI#8PAc diff --git a/broadlinkmanager/broadlink/__pycache__/cover.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/cover.cpython-36.pyc deleted file mode 100644 index 06271296c5012ace178510c1f8b11d07764e216d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2017 zcma)6&2Ah;5bmD;{k6S8K=@IH1O-OGIuHrvU>SrR!O5VkrHGZ#WIWw=#-5#?^o$el zvOaC#9YB5oo`EOe%CW_b)4oDZRP~I>Zb;D1Xuhth?wb1Ps=D3jH2?hT-ocL!A%ByF z#{&9G_=-CKj0_2*%t+~wGLu=UF*1i1B|zJ$J#vN)C0`TfF!w28E_Y_;&}GJe1m5Jz z;55&LEd4_v{a8Havgps@mt=2F0RyU<{4C~qnTV_a&e`RUlbFYNQo;2Jg8VO`*wz^$?M3xjR2 z9`G%;1`%9QHWOkTO|I>Ttdt@x{K(%$i65WJGDgw$0V`Ycv zWCwm)ybeGNk1oD#+Jkq}(sj|-X9FrdI2dD|@WB<6r0=#31l)q|pc!_BIobF&=b6rR zT||9}iPd?Af4v7ZEN`O-W2?XiN?8Ilaic&5VoaKq8z^4cd~YWe1z)z0Uanx@j!_kz zx^$qhOxwHt53enfbzB~|F?1Q6llt# Jp3$al^B;$O(W3wW diff --git a/broadlinkmanager/broadlink/__pycache__/device.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/device.cpython-36.pyc deleted file mode 100644 index 19e77630a779db2df8500db9222c7f1735d5d297..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9563 zcmbtaOOPAab?qO3#&<9?;QTnETe2t#wlt(jks=jI;cq0-l1B7!D3OqyHi*}l0R}(4 z?jbn{U?+kU$4OLDs*;MMPvWXnVrP-h(yLS|tE|0Asw=xxr81jTD%rTSQOKp zq26Z2RBx+dsdu81#M^A8+IGd(*~e<7+nGwHovmcsxk?UY36X5&+l9)MN~c=I_H<>s zJyV%!&sJvJrAkSa+pR1=_z+$W~UivmiIyr_vOQG8bu#gS3@nlP?v z<>~&Bt2@13R|ZbKE1e6nTNSNFXWJ3ptwzoBSN6aYZLO?_Q^KoPcUpmavs&BsI-+cb z$%~ENmM6nZwYAxmjbN+ohlzGqc>bP_Ug(tZ?$o?q(CBvjvK7u;T7BXCwO2OW3m4B{ z-Ehxey0jkV_RFrkw!RV0?&sE5H`Z^sm)AC4egsFPHZZPzsTz1eqwSRw zVT$kWP7oGb-CDKfHhOMV2@6M3Wy=r@J;)W{IUbJ{R9NKLey7Bgz(VzW5XhW4g5On<2N?GN=kTA<1X_JTIh zwzUs+`5%LcvIT9J!HTAXnP7I18SSsBG)NANfr&L5x_0MhyEABWNX(A(w~S$Sn0rI( zELmK~+;#>dUe`7Lu4p&3j&WTZWJL*U%8Ek};KcLV1#Or|Zcfa<2k}>{KFn1Q3xnLC zFv!=E}bE+NT z>^8iXa3F3y2ssXnOhRfT<-eagzM_76$C2B!NlGAVdkai1lNcajvegKJmbZ6=CG5WU z2uS1a{sH9k&yjUl_7KZ7$mrfv%$_27hU6T`|MG*{-uFp9$De!{sq%7|^t(8%L6|!K z0#4KFMwq$!+QpaMOE0dUe>KebUcmZpFJSpAF6+=Hcl9-Qef8Sa)#!vGcVYdt^OtyN zL+fUvBf{)v)U#7D~_0jgA+%)u7u6?V#(jp&y$5 zPCK-CYSj$(kFVKobYzADBz@?Ypc~rKtKF*0Zd>M6akbY&MVRn=U|5(GUJbeiGwOC* zVWQUR`d)cjHI)Ulms2D~_OxowS z=DuESS33|K`GjC&`GxYk1?&n1ywckZEx%Un%wtghB@iuD)MxY=!_o_S0n{>VeNJE2 zOS)|o^b&FwO6T+m2-K4%nl3uQ}sPCsJEmoXNUcq&$w@_FPXRbP!l$v^7D zES`&a{B;nV;t4(Y!I_~kFz(faK~g|73}Gw>rZAzU4Cp{}pbhj$L*R^A-`3W7zAT)t zME_r}VpDOndX3Je6K#$YbaA$vcxEe^jDx?8t%ko}U-vxWJHeLc+^z<-Ez#X{xLMNa z)}4Aowr^LlVb1f6@Ve6t zoSPmRR3RfG>VG+K+B;AID2H?g4#tqX&It!Xy91$i8Xd>qscoS2r(HKB8W+fr4O43Wg=N6fANdl-=2#125xGO+n{j?g9LAUCFcxc<#RI6F_p&N?M0^do1$;Lp z9E|*+cnJALHOsFf|FAfU{8BuxN5rGZ9mZNXlgEIN%HkU+Syn3l@i2Kw9lrkZ#cn5% z-IfpWycZg+gsE$tZ78`;#W{ZV>C-39oP6qJ-8*~MJ8|;q)2C`ro_g|RwN|U2xq19V z#W{8Qg!kmrXV0D%$IqS=C(a0Q^W@Fy>9eP*C%n`3Q)fc$g|%{8Ucx5H7f4n~sP^QG zAQ7x;g*oVtoqDwfxZ+7kbr7A{G*bNI+r)DbkN-y?1AU;mTA&Y%0QOlf1!mJ4;Cyy) za#54u$#jzg3p6!IfZDL;nwuV^Kr;gyG#liadB9urN3Nho(3(>~DVkdp>_0uwnlpIM z2Bqeq5wL?hCph9f$`|7HizA>Iz}@J~FJ105f=0E~=zG!8i}>8~lMvI=uWjh_y-pcK$^GqhfT1`jPN{m`t!s%3V1 z0-)4odivmaFR$Jxr=iBZR{cwN_@1fWd)95&ZGdI2>tQiIfr6Gx>PvcZ;s<+xoULb! zq~4#3R1QgKM(C*EMVyBzfS;z_yE*{7Pw7AxUJT%jIGWE=y z2JLP1pIBGe83zRZL%Z67)`WtkimVd$Qz27L$JVU>=23s;sQ=he|8WP_^SPt)aaNJJ zDjj11m8M};33?Q*C^aGds$gt(kt>(8VcK7T^zKgeF=~jHTsWCwIO!@*my%Is@0n<8nR!uC8eXj zgAH+xIY<3x;&)lnu6(9DYoF=Qj3-W9KuqBCA%-1j-zg5^6~V{RMh3C;+b}DXi;8*t z#C5ujNhKCR*$9o5pW^X3mWHMEr{hK4 zRajV8s#o1@!TCS9Hco<%TAPCeL=HLv9u<+_c#TL~BW<10vKcL92OpESNDi(iOvNMo1?u88W$tcNTselYUuglC zykD{acpY3_SIXO*o!XZ>NFA7X|9%sXd;avk6OSv$XN|kXM{)jdk)V=r$$uz_NI66n zHApDDjZoAkLQo~1#W;2VUse}3Vgh`p@y;}pu+F;3MzI2=Oc-4-hDHFuH!uVM8@*@c zOGC?}q$sANlA#%7n>k?mX^~LA`HaE?YDOqk-;8>SS;#@DfB5_kG=C>()L<3~yeI5{ zGVOa;u&li&N$92Rak;WYLLXQzvg`?vesaSD@_tehO79UL@6ig%2bg-8Ruyai8?^J6Kr|TS88~Ke!|*E_Ipx63f%dcUS!DaM(9$^2u&BNN zITFyYw9K^4(65M>@Xh#$`MxzYcITjLU>@E|49%f6ObnC56rwKABI;soXJk)Dlt|KN^|~i9zNJ3X%!VCa59tzrDB{$Y0PNx+&~;9S0Rpx;L0 zyZ`<9=MDQ8F)8Pihaau)5`+k$3hz!3=C20b-enj|h0o*wW3$RyF*n)wK0@4{(!(7RnOcQ|hbMa|X!;MubahHWh9VbTJqT>`FBo!pY zK|=5{c254~3Por_TiunyUo8ef+51)F5FFzx#3I;jLhN-FT37>acoqCmi1_~KNY(G5 zjY4KxYOJsrdl++=+#PdxO>tQ0n*cvUsw{9gHZn9^z_`MiFfwpQ?;4Mx2~(1Sf-{8; z^_FHp7|J5kKlGx98;Urx;~@6pLmF51w(zaJCWtZya>UfTAZt=ZLW!3oBl&XB2{ZcG@2O5IGO7g!DR^!Z+ zJidiQ0OJl`(IwZ(t&#q;0$3JGD#f$+P!gIguM@=^ z?qMO9x6eYgR28XGe7wgd+K4v0RLq~Di%)H#!6qVPsYg58~n1xA3DJNMNwv*|JY7PnUo3b@C9zl zk1(k(a5EioQ}Ku_l^>J*8p%B@eF&Ji4MZhZ{ev26!6B8MB_pamH~sn_0HdIhr{hEjdU|%5RP&HR zZF9V@+b!X4a9+>}KaLw(J$$fpx9`Ip&*1bzk?-K##b}?jMi7z)=VGwuKwGp9Kre zMFdtC#nQkA+B>Y!WG+UN;N&p(y|6-L4g)$Uiuf#TTb=$7o0m<1C4z;sJ3a zDhFmXM&>(uaJae5n?Z7b#sI1=4^CytDfNzP6py(NxE~x8woQ!rx*GEl;fMzZdCcvh zK|vu=j5^3dDL&d?j?w4c3x~-g%@Z*Zh@?rW6i-#F{C8^q9obWw7s=Zh<$pvb=rFQ- zkCA)>WKB6PN=-;c?$pJe{B5M<_ecmyl-_M}K+*q;!!KDfRQOo7;NsiH2s z9BA*&V9Qa?P5-emz^n$S;l<`tXB-txJ%~3OKjnzdeMeNtJ05WU?tl>@$E_LzfA`{7 zbtxIa)EmUA*Fnk;$m?9cGBwJXR{q*5%jgH& zQ6Go(55^^!b=i$yAlNepJw(4Bp;IeA;}L(eh;KP6=B*Rvq0einzc%%=c;vM zeH8>u1S7%n9NRocqP}{bsS6~^s4}Wl$#vjEj(g@2m}`x*_h4aV3}$pF(h7QNxVEF; zI0uzJr!N5i@`{fafEkry0N4gb=4BtTG6TRR)Dk@#FaeLCRg^B8C0zRLBjy>iKNI7X z2{*W`uazGN5o(SDrJG#E7D$K~8Ez~QJ0)C+oaq}(F@hZGh@)@p^bow?U>TVhCa!e= z&>{z1**K3?4H8BVBln!!s4$rP1E&6%MB&jtW$F`>KO^~b5@jUsLJF1o7Aw|3Xq|kD zkh=Zkt!Ro2;O{ZLFk6@@q>IU7qL?Vm7R!Zn!7gTsnS!PImL2(*97l=JZ!q-+38g1Y z!mM$R{|cGPauoR4l-1tW?#foR->wELam3D7!MMsJ_iNIVjW%vOZsA_ArvAf>e>b?N zLVA*c5Sb#OK=M`sHysknK$B?_CA#Ew#fJA6bZgy~x@wm{VeJn|6zj?9O8zDOABFYU zQ4P~CBEAY|4^fSm*-$ZX-OTe>i4tP%D3E&fK9(Z2jN#F&x2)roqG2oNz6!0mD%fK(N7lB`t_&D%O z%mTI~o&rA2UIuhR0@z#p?3@I2Gjl*)R1`r20w zz7x5DA6m^oSXV{hFwgZ{7UK`y1`n%UvWQxPWbe_lO!y8mT!BF-b&aWY{cEKO6Jh#e zvu-eh!|xM+*VGkbii>z=_t0&^sS61 z%E0rdC|^QqYfLwe^RI?DaYJvCB@@*PE09%xL#U#qo>n&@F9Dq5mdFC@An zHoj5B7+O5}cq!P4mY}nx4L4d^?`d({@tqAW-e5ta#l2KV__ zJL2*rks^kM0^Rifx390NZMzt*ZC5XOLBsLF%hi6nA1#1{?@spjkyvjF5ZH#K0iKB;R^ODaK#0ZD|~~Ee9@4R2CBu1-#)D zgrZHVCAFwpka@MN<<aMEpR_ebSu?bJn`8z8kTh?ypOk{2{K6m=L!u9BA5~Di7XeF_Dz8W?bDJ1s3(B&r>COKs75SL5j(VUT_I-G`3X=2e)J z5$!@h(`XOA#4*VQuwC&q)x2%<2ewTeNuyBoc;QHvRO{%OrGJl<2rG=%(3aNJ_GvEF zcq*f|>4clvWRevS6erLqrFZlc!my~GZJ+Kl)1IUCv|gn^#^^iS+_saVZEpvx?NPpD z+Yj1~mwkb1uykN+WMP67Bo;`Vgh(bbQ*3b0>zN~I6>6TPO42k*L8Q~L2w|#*R+=#L zW=>Ff18@H!)1xQ60P+7iRq0I0?U>2r$89ficRUX33q%-~d)ShzDm-wNM2C8lu5tyL zjw)v0*3<Y%^Mt$Vx&}}C705(ZV4Sf z-vdd*{sI1=*Y(`t0bov~*QuC-R=#+F#ET?eA|cnt8Oq58Vo^@c`!kDyR_CjDLs}Yg zF_@+ysI-B%|Inb`!yA?%2A6^~G(DNV(uQ;LBe&s@tw_bivxf^)tFP87#*v)+{04Rr zVs??bB?XwOAUqVWDWVVvHy)rhbEs?tR`u9baj9u;h{LrE(gA!uKZ;$2zvxUy;qDQA(f9nju!p(0djNlHBoZD0HAJ3*` zR*)8sKI(j=4N^;<%+Lr-meBJT155}H z#iaPDaHEVzN#;jjL1(7*0TzUZVliFg(W5}LZoAt&Xh-R^%w>gEl$MS_j5hS<05QVD z5nFL0=MJJtHn6G8QK5y?dsaFEu{S|vYk(Nxp@@|}@mqfIDBFD}Gc*E|x6rdcz=W_U zLOipjHyzjGOz8A&{$9|wHl2suTIbxiykKJky8~3$m7iyCBl1qKFf2Fh`N1xQmN7OP zxgj3j_PTU?5HAk23)3Y+?VD}S>*i(S1E-Jjp5O1%c_N;}b`^=hvs!$YS|NaTak}l& zX(uioIG`@Ys`2=Mv3KQoiwAzPJ5E*3OGklot;ZzUb&g9K@))jfUAcw(5x?nfw1u<` z_gip?t<3SeK5lciZ>ZY&BdZNEqo^}$@!P{KxZe=FJ2LE?i|J03(Du( zp1OytrH<0k_7Pzl2y9zLaY4ncjl6V1ut4Q^5n176uE!U$Ah&3U>mZx&DT{ z>+(}Z_`-NKo;b9!Y6yvEuCCs^vhvZ5+K2YFyUW*Z-M(?Nww9P5@?Gh+1TCP7F5X3w z=;$cXx5JHt6M1^+Q0O!G84Zf;Pti1aaWbh28YpHYC#Ocx@v}b{bU3E@kaJ-nLs24y zMEi&x7nD8RHi%Q{#o13(reoAbd}h^gajVzw(sG-Y(;H?l2&^XRW*R7BjClkcICItW zZJ*3ABP1wVtrsOLsi~(&H7@mS%L>0h-RwH0tQx9#AGrHpJl+40_&14vk@zQs`-H?l zNPJ7;?zowyCgm(afiewByK}g7Sf35WppLz%$XFf_3kQV#4lqV z8CVE9#|t_r3p$XBb0p}HCFqnR=$IjBR~JhpXg?FQcL?&+Qp4hh$R#>fwVa$Jx4s+k vP>yn7R>&`<3O6sMcN~`~x(aI$1_EGHFI(lAvhmzs9(A7mX@|>7M!E1mAgiN5 diff --git a/broadlinkmanager/broadlink/__pycache__/helpers.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/helpers.cpython-36.pyc deleted file mode 100644 index 896a3fe68c6cc2aa4b32672322b51bb94a44ff95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1337 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 UHeMYbuh;wu6{ez*T`H~fC%z)-*8l(j diff --git a/broadlinkmanager/broadlink/__pycache__/remote.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/remote.cpython-36.pyc deleted file mode 100644 index dfe86cc8afd53f9eb8306332943a0c5b0fcc1440..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5196 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 diff --git a/broadlinkmanager/broadlink/__pycache__/switch.cpython-36.pyc b/broadlinkmanager/broadlink/__pycache__/switch.cpython-36.pyc deleted file mode 100644 index 1f5476560fcbb396949c969c699a529e804001bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11788 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 EKbNlwod5s; From a9edead83da2a7b0bb334dc1027b60f6583185a2 Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Sun, 10 Apr 2022 17:35:57 +0300 Subject: [PATCH 3/4] Moving to python-broadlink 0.18.0 --- broadlinkmanager/calls.php | 154 ------------------------------------- broadlinkmanager/calls.py | 38 --------- 2 files changed, 192 deletions(-) delete mode 100644 broadlinkmanager/calls.php delete mode 100644 broadlinkmanager/calls.py diff --git a/broadlinkmanager/calls.php b/broadlinkmanager/calls.php deleted file mode 100644 index 7e700bd..0000000 --- a/broadlinkmanager/calls.php +++ /dev/null @@ -1,154 +0,0 @@ - 16) $ord1 = $ord1 - 7; if ($ord2 - > 16) $ord2 = $ord2 - 7; $str_hex[$i] = $ord1 * 16 + $ord2; - } - return $str_hex; - } - -function geturi($host, $post, $headers, $request = 0) { - - $url = "https://".$host.$post; $timeout = 7; $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); if (preg_match("/\bPOST\b/i", $headers[0])) curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, - CURLOPT_HTTPHEADER, $headers); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $timeout); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE); curl_setopt($curl, - CURLOPT_SSL_VERIFYPEER, FALSE); if ($request) curl_setopt($curl, CURLOPT_POSTFIELDS, $request); $result["msg"] = curl_exec($curl); $result["error"] = curl_errno($curl); if ($result["error"]) { - $result["msg"] = curl_error($curl); - } - return $result; - } - - - -function Auth($email = "", $password = "") { - -global $userid, $token,$timestamp,$loginsession,$nickname; - if (($email === "") || (strlen($password) < 6)) { $result["error"] = -1005; $result["msg"] = "Data Error"; - echo(1); - return $result; - } - - $authiv = array(-22, -86, -86, 58, -69, 88, 98, -94, 25, 24, -75, 119, 29, 22, 21, -86); $password = sha1($password."4969fj#k23#"); $data_str = str_pad('{"email":"'.$email.'","password":"'.$password.'"}', 112, - "\0"); $token = md5('{"email":"'.$email.'","password":"'.$password.'"}'."xgx3d*fe3478\$ukx"); - - - - $host = "account.ibroadlink.com"; $post = "/v1/account/login/api?email=".$email."&password=".$password."&serialVersionUID=2297929119272048467"; $headers = array( "GET ".$post." HTTP/1.1", "language: zh_cn", - "serialVersionUID: -6225108491617746123", "Host: ".$host, "Connection: Keep-Alive" - ); $result = geturi($host, $post, $headers); if ($result["error"]) { return $result; - } - $result = json_decode($result["msg"], true); - - if (($result["error"] != 0) || ($result["msg"] != "ok")) { return $result; - } - - $timestamp = $result["timestamp"]; $key = byte(str2hex_array($result["key"])); $request = aes128_cbc_encrypt($key, $data_str, byte($authiv)); $post = "/v2/account/login/info"; $host = "secure.ibroadlink.com"; - - $headers = array( - "POST ".$post." HTTP/1.1", "Timestamp: ".$timestamp, "Token: ".$token, "language: zh_cn", "serialVersionUID: -6225108491617746123", "Content-Length: 112", "Host: ".$host, "Connection: Keep-Alive", "Expect: - 100-continue" - ); $result = geturi($host, $post, $headers, $request); if ($result["error"]) { return $result; - } - - - - $data = json_decode($result["msg"],true); - $loginsession = $data["loginsession"]; - $nickname = $data["nickname"]; - $userid = $data["userid"]; - // echo json_encode($result); - - - $result = array("loginsession"=>$data["loginsession"], "nickname"=>$data['nickname'],"userid"=>$data["userid"],"timestamp"=>$timestamp,"token"=>$token); - echo json_encode($result); - } - - - -function GetUserInfo() { -global $userid, $token,$timestamp,$loginsession,$nickname; - - - $post = "/v1/account/userinfo/get"; - $host = "account.ibroadlink.com"; - $headers = array( - "GET ".$post." HTTP/1.1", - "LOGINSESSION: ".$loginsession, - "USERID: ".$userid, - "language: zh_cn", - "serialVersionUID: -6225108491617746123", - "Host: ".$host, - "Connection: Keep-Alive" - ); - $result = geturi($host, $post, $headers); - if ($result["error"]) { - return $result; - } - return $result; - } - - - -function GetListBackups($userid,$loginsession,$nickname) { -// global $userid, $token,$timestamp,$loginsession,$nickname; - - $timestamp = round(microtime(true) * 1000); - $post = "/rest/1.0/backup?method=list&user=".$nickname."&id=".$userid."×tamp=".$timestamp."&token=".get_token($timestamp); - $host = "ebackup.ibroadlink.com"; - $headers = array( - "GET ".$post." HTTP/1.1", - "accountType: bl", - "reqUserId: ".$userid, - "reqUserSession: ".$loginsession, - "serialVersionUID: -855048957473660878", - "Host: ".$host, - "Connection: Keep-Alive" - ); - // echo(" ".$post." "); - $result = geturi($host, $post, $headers, 0); - if ($result["error"]) { - return $result; - } - $result = json_decode($result["msg"], true); - $result["error"] = 0; - echo json_encode($result); - return $result; - } - - -function GetTimeStemp(){ -echo round(microtime(true) * 1000); -} - - -// echo Auth(getenv("email"),getenv("password")); -// GetUserInfo(); -// GetListBackups(); -#GetTimeStemp(); - - -?> diff --git a/broadlinkmanager/calls.py b/broadlinkmanager/calls.py deleted file mode 100644 index ad070ac..0000000 --- a/broadlinkmanager/calls.py +++ /dev/null @@ -1,38 +0,0 @@ -import subprocess, json, os, time -userid = None -nickname = None -loginsession = None -timestamp = None -token = None - - -data = str_pad(data, math.ceil(len(data) / 16) * 16, chr(0), STR_PAD_RIGHT) -# # if the script don't need output. -# subprocess.call("php /path/to/your/script.php") - -def login(): - global userid, nickname, loginsession, timestamp, token - proc = subprocess.Popen("php -r \"require 'calls.php';Auth('tomer.klein@gmail.com','tklk@2301');\"", shell=True, stdout=subprocess.PIPE) - response = proc.stdout.read() - data = json.loads(response) - - userid = data["userid"] - nickname = data ["nickname"] - loginsession = data["loginsession"] - timestamp = data["timestamp"] - token = data["token"] - -def GetBackupsList(): - global userid, nickname, loginsession, timestamp, token - proc = subprocess.Popen("php -r \"require 'calls.php';GetListBackups('" + userid + "','" + loginsession + "','" + nickname + "');\"", shell=True, stdout=subprocess.PIPE) - response = proc.stdout.read() - data = json.loads(response) - print(data['list'][0]) - -def GetTimeStamp(): - return round(time.time() * 1000) - - -if __name__ == '__main__': - login() - GetBackupsList() \ No newline at end of file From cd8fbb013e18786ae15122da029566c3d3a850dc Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Sun, 10 Apr 2022 17:36:55 +0300 Subject: [PATCH 4/4] Moving to python-broadlink 0.18.0 --- broadlinkmanager/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broadlinkmanager/VERSION b/broadlinkmanager/VERSION index ef8d756..28cbf7c 100644 --- a/broadlinkmanager/VERSION +++ b/broadlinkmanager/VERSION @@ -1 +1 @@ -4.2.0 \ No newline at end of file +5.0.0 \ No newline at end of file