mirror of
https://github.com/mjg59/python-broadlink.git
synced 2024-11-14 03:20:14 +01:00
Call Mr. Krueger
This commit is contained in:
parent
76012c6cd4
commit
08c020e597
@ -1,19 +1,19 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
"""The python-broadlink library."""
|
"""The python-broadlink library."""
|
||||||
import codecs
|
|
||||||
import json
|
|
||||||
import random
|
|
||||||
import socket
|
import socket
|
||||||
import struct
|
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Union, Tuple
|
from typing import List, Union, Tuple
|
||||||
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
from .alarm import S1C
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
from .cover import dooya
|
||||||
|
from .climate import hysen
|
||||||
|
from .device import device
|
||||||
from .exceptions import check_error, exception
|
from .exceptions import check_error, exception
|
||||||
|
from .light import lb1
|
||||||
|
from .remote import rm, rm2, rm4
|
||||||
|
from .sensor import a1
|
||||||
|
from .switch import bg1, mp1, sp1, sp2
|
||||||
from .helpers import calculate_crc16, get_local_ip
|
from .helpers import calculate_crc16, get_local_ip
|
||||||
|
|
||||||
|
|
||||||
@ -196,965 +196,6 @@ def discover(
|
|||||||
return devices
|
return devices
|
||||||
|
|
||||||
|
|
||||||
class device:
|
|
||||||
"""Controls a Broadlink device."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
host: Tuple[str, int],
|
|
||||||
mac: Union[bytes, str],
|
|
||||||
devtype: int,
|
|
||||||
timeout: int = 10,
|
|
||||||
name: str = None,
|
|
||||||
model: str = None,
|
|
||||||
manufacturer: str = None,
|
|
||||||
is_locked: bool = None,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the controller."""
|
|
||||||
self.host = host
|
|
||||||
self.mac = mac.encode() if isinstance(mac, str) else mac
|
|
||||||
self.devtype = devtype if devtype is not None else 0x272a
|
|
||||||
self.timeout = timeout
|
|
||||||
self.name = name
|
|
||||||
self.model = model
|
|
||||||
self.manufacturer = manufacturer
|
|
||||||
self.is_locked = is_locked
|
|
||||||
self.count = random.randrange(0xffff)
|
|
||||||
self.iv = bytearray(
|
|
||||||
[0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58])
|
|
||||||
self.id = bytearray([0, 0, 0, 0])
|
|
||||||
self.type = "Unknown"
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.aes = None
|
|
||||||
key = bytearray(
|
|
||||||
[0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02])
|
|
||||||
self.update_aes(key)
|
|
||||||
|
|
||||||
def update_aes(self, key: bytes) -> None:
|
|
||||||
"""Update AES."""
|
|
||||||
self.aes = Cipher(
|
|
||||||
algorithms.AES(key), modes.CBC(self.iv), backend=default_backend()
|
|
||||||
)
|
|
||||||
|
|
||||||
def encrypt(self, payload: bytes) -> bytes:
|
|
||||||
"""Encrypt the payload."""
|
|
||||||
encryptor = self.aes.encryptor()
|
|
||||||
return encryptor.update(payload) + encryptor.finalize()
|
|
||||||
|
|
||||||
def decrypt(self, payload: bytes) -> bytes:
|
|
||||||
"""Decrypt the payload."""
|
|
||||||
decryptor = self.aes.decryptor()
|
|
||||||
return decryptor.update(payload) + decryptor.finalize()
|
|
||||||
|
|
||||||
def auth(self) -> bool:
|
|
||||||
"""Authenticate to the device."""
|
|
||||||
payload = bytearray(0x50)
|
|
||||||
payload[0x04] = 0x31
|
|
||||||
payload[0x05] = 0x31
|
|
||||||
payload[0x06] = 0x31
|
|
||||||
payload[0x07] = 0x31
|
|
||||||
payload[0x08] = 0x31
|
|
||||||
payload[0x09] = 0x31
|
|
||||||
payload[0x0a] = 0x31
|
|
||||||
payload[0x0b] = 0x31
|
|
||||||
payload[0x0c] = 0x31
|
|
||||||
payload[0x0d] = 0x31
|
|
||||||
payload[0x0e] = 0x31
|
|
||||||
payload[0x0f] = 0x31
|
|
||||||
payload[0x10] = 0x31
|
|
||||||
payload[0x11] = 0x31
|
|
||||||
payload[0x12] = 0x31
|
|
||||||
payload[0x1e] = 0x01
|
|
||||||
payload[0x2d] = 0x01
|
|
||||||
payload[0x30] = ord('T')
|
|
||||||
payload[0x31] = ord('e')
|
|
||||||
payload[0x32] = ord('s')
|
|
||||||
payload[0x33] = ord('t')
|
|
||||||
payload[0x34] = ord(' ')
|
|
||||||
payload[0x35] = ord(' ')
|
|
||||||
payload[0x36] = ord('1')
|
|
||||||
|
|
||||||
response = self.send_packet(0x65, payload)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(response[0x38:])
|
|
||||||
|
|
||||||
key = payload[0x04:0x14]
|
|
||||||
if len(key) % 16 != 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.id = payload[0x03::-1]
|
|
||||||
self.update_aes(key)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_fwversion(self) -> int:
|
|
||||||
"""Get firmware version."""
|
|
||||||
packet = bytearray([0x68])
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(response[0x38:])
|
|
||||||
return payload[0x4] | payload[0x5] << 8
|
|
||||||
|
|
||||||
def set_name(self, name: str) -> None:
|
|
||||||
"""Set device name."""
|
|
||||||
packet = bytearray(4)
|
|
||||||
packet += name.encode('utf-8')
|
|
||||||
packet += bytearray(0x50 - len(packet))
|
|
||||||
packet[0x43] = bool(self.is_locked)
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def set_lock(self, state: bool) -> None:
|
|
||||||
"""Lock/unlock the device."""
|
|
||||||
packet = bytearray(4)
|
|
||||||
packet += self.name.encode('utf-8')
|
|
||||||
packet += bytearray(0x50 - len(packet))
|
|
||||||
packet[0x43] = bool(state)
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
self.is_locked = bool(state)
|
|
||||||
|
|
||||||
def get_type(self) -> str:
|
|
||||||
"""Return device type."""
|
|
||||||
return self.type
|
|
||||||
|
|
||||||
def send_packet(self, command: int, payload: bytearray) -> bytearray:
|
|
||||||
"""Send a packet to the device."""
|
|
||||||
self.count = (self.count + 1) & 0xffff
|
|
||||||
packet = bytearray(0x38)
|
|
||||||
packet[0x00] = 0x5a
|
|
||||||
packet[0x01] = 0xa5
|
|
||||||
packet[0x02] = 0xaa
|
|
||||||
packet[0x03] = 0x55
|
|
||||||
packet[0x04] = 0x5a
|
|
||||||
packet[0x05] = 0xa5
|
|
||||||
packet[0x06] = 0xaa
|
|
||||||
packet[0x07] = 0x55
|
|
||||||
packet[0x24] = self.devtype & 0xff
|
|
||||||
packet[0x25] = self.devtype >> 8
|
|
||||||
packet[0x26] = command
|
|
||||||
packet[0x28] = self.count & 0xff
|
|
||||||
packet[0x29] = self.count >> 8
|
|
||||||
packet[0x2a] = self.mac[5]
|
|
||||||
packet[0x2b] = self.mac[4]
|
|
||||||
packet[0x2c] = self.mac[3]
|
|
||||||
packet[0x2d] = self.mac[2]
|
|
||||||
packet[0x2e] = self.mac[1]
|
|
||||||
packet[0x2f] = self.mac[0]
|
|
||||||
packet[0x30] = self.id[3]
|
|
||||||
packet[0x31] = self.id[2]
|
|
||||||
packet[0x32] = self.id[1]
|
|
||||||
packet[0x33] = self.id[0]
|
|
||||||
|
|
||||||
# pad the payload for AES encryption
|
|
||||||
if payload:
|
|
||||||
payload += bytearray((16 - len(payload)) % 16)
|
|
||||||
|
|
||||||
checksum = sum(payload, 0xbeaf) & 0xffff
|
|
||||||
packet[0x34] = checksum & 0xff
|
|
||||||
packet[0x35] = checksum >> 8
|
|
||||||
|
|
||||||
payload = self.encrypt(payload)
|
|
||||||
for i in range(len(payload)):
|
|
||||||
packet.append(payload[i])
|
|
||||||
|
|
||||||
checksum = sum(packet, 0xbeaf) & 0xffff
|
|
||||||
packet[0x20] = checksum & 0xff
|
|
||||||
packet[0x21] = checksum >> 8
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
with self.lock:
|
|
||||||
cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
cs.sendto(packet, self.host)
|
|
||||||
cs.settimeout(1)
|
|
||||||
resp, _ = cs.recvfrom(2048)
|
|
||||||
resp = bytearray(resp)
|
|
||||||
break
|
|
||||||
except socket.timeout:
|
|
||||||
if (time.time() - start_time) > self.timeout:
|
|
||||||
cs.close()
|
|
||||||
raise exception(-4000) # Network timeout.
|
|
||||||
cs.close()
|
|
||||||
|
|
||||||
if len(resp) < 0x30:
|
|
||||||
raise exception(-4007) # Length error.
|
|
||||||
|
|
||||||
checksum = resp[0x20] | (resp[0x21] << 8)
|
|
||||||
if sum(resp, 0xbeaf) - sum(resp[0x20:0x22]) & 0xffff != checksum:
|
|
||||||
raise exception(-4008) # Checksum error.
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
class mp1(device):
|
|
||||||
"""Controls a Broadlink MP1."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
"""Initialize the controller."""
|
|
||||||
device.__init__(self, *args, **kwargs)
|
|
||||||
self.type = "MP1"
|
|
||||||
|
|
||||||
def set_power_mask(self, sid_mask: int, state: bool) -> None:
|
|
||||||
"""Set the power state of the device."""
|
|
||||||
packet = bytearray(16)
|
|
||||||
packet[0x00] = 0x0d
|
|
||||||
packet[0x02] = 0xa5
|
|
||||||
packet[0x03] = 0xa5
|
|
||||||
packet[0x04] = 0x5a
|
|
||||||
packet[0x05] = 0x5a
|
|
||||||
packet[0x06] = 0xb2 + ((sid_mask << 1) if state else sid_mask)
|
|
||||||
packet[0x07] = 0xc0
|
|
||||||
packet[0x08] = 0x02
|
|
||||||
packet[0x0a] = 0x03
|
|
||||||
packet[0x0d] = sid_mask
|
|
||||||
packet[0x0e] = sid_mask if state else 0
|
|
||||||
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
|
|
||||||
def set_power(self, sid: int, state: bool) -> None:
|
|
||||||
"""Set the power state of the device."""
|
|
||||||
sid_mask = 0x01 << (sid - 1)
|
|
||||||
self.set_power_mask(sid_mask, state)
|
|
||||||
|
|
||||||
def check_power_raw(self) -> bool:
|
|
||||||
"""Return the power state of the device in raw format."""
|
|
||||||
packet = bytearray(16)
|
|
||||||
packet[0x00] = 0x0a
|
|
||||||
packet[0x02] = 0xa5
|
|
||||||
packet[0x03] = 0xa5
|
|
||||||
packet[0x04] = 0x5a
|
|
||||||
packet[0x05] = 0x5a
|
|
||||||
packet[0x06] = 0xae
|
|
||||||
packet[0x07] = 0xc0
|
|
||||||
packet[0x08] = 0x01
|
|
||||||
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
return payload[0x0e]
|
|
||||||
|
|
||||||
def check_power(self) -> dict:
|
|
||||||
"""Return the power state of the device."""
|
|
||||||
state = self.check_power_raw()
|
|
||||||
if state is None:
|
|
||||||
return {'s1': None, 's2': None, 's3': None, 's4': None}
|
|
||||||
data = {}
|
|
||||||
data['s1'] = bool(state & 0x01)
|
|
||||||
data['s2'] = bool(state & 0x02)
|
|
||||||
data['s3'] = bool(state & 0x04)
|
|
||||||
data['s4'] = bool(state & 0x08)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class bg1(device):
|
|
||||||
"""Controls a BG Electrical smart outlet."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
"""Initialize the controller."""
|
|
||||||
device.__init__(self, *args, **kwargs)
|
|
||||||
self.type = "BG1"
|
|
||||||
|
|
||||||
def get_state(self) -> dict:
|
|
||||||
"""Return the power state of the device.
|
|
||||||
|
|
||||||
Example: `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}`
|
|
||||||
"""
|
|
||||||
packet = self._encode(1, b'{}')
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
return self._decode(response)
|
|
||||||
|
|
||||||
def set_state(
|
|
||||||
self,
|
|
||||||
pwr: bool = None,
|
|
||||||
pwr1: bool = None,
|
|
||||||
pwr2: bool = None,
|
|
||||||
maxworktime: int = None,
|
|
||||||
maxworktime1: int = None,
|
|
||||||
maxworktime2: int = None,
|
|
||||||
idcbrightness: int = None,
|
|
||||||
) -> dict:
|
|
||||||
"""Set the power state of the device."""
|
|
||||||
data = {}
|
|
||||||
if pwr is not None:
|
|
||||||
data['pwr'] = int(bool(pwr))
|
|
||||||
if pwr1 is not None:
|
|
||||||
data['pwr1'] = int(bool(pwr1))
|
|
||||||
if pwr2 is not None:
|
|
||||||
data['pwr2'] = int(bool(pwr2))
|
|
||||||
if maxworktime is not None:
|
|
||||||
data['maxworktime'] = maxworktime
|
|
||||||
if maxworktime1 is not None:
|
|
||||||
data['maxworktime1'] = maxworktime1
|
|
||||||
if maxworktime2 is not None:
|
|
||||||
data['maxworktime2'] = maxworktime2
|
|
||||||
if idcbrightness is not None:
|
|
||||||
data['idcbrightness'] = idcbrightness
|
|
||||||
js = json.dumps(data).encode('utf8')
|
|
||||||
packet = self._encode(2, js)
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
return self._decode(response)
|
|
||||||
|
|
||||||
def _encode(self, flag: int, js: str) -> bytearray:
|
|
||||||
"""Encode a message."""
|
|
||||||
# The packet format is:
|
|
||||||
# 0x00-0x01 length
|
|
||||||
# 0x02-0x05 header
|
|
||||||
# 0x06-0x07 00
|
|
||||||
# 0x08 flag (1 for read or 2 write?)
|
|
||||||
# 0x09 unknown (0xb)
|
|
||||||
# 0x0a-0x0d length of json
|
|
||||||
# 0x0e- json data
|
|
||||||
packet = bytearray(14)
|
|
||||||
length = 4 + 2 + 2 + 4 + len(js)
|
|
||||||
struct.pack_into('<HHHHBBI', packet, 0, length, 0xa5a5, 0x5a5a, 0x0000, flag, 0x0b, len(js))
|
|
||||||
for i in range(len(js)):
|
|
||||||
packet.append(js[i])
|
|
||||||
|
|
||||||
checksum = sum(packet[0x08:], 0xc0ad) & 0xffff
|
|
||||||
packet[0x06] = checksum & 0xff
|
|
||||||
packet[0x07] = checksum >> 8
|
|
||||||
return packet
|
|
||||||
|
|
||||||
def _decode(self, response: bytes) -> dict:
|
|
||||||
"""Decode a message."""
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
js_len = struct.unpack_from('<I', payload, 0x0a)[0]
|
|
||||||
state = json.loads(payload[0x0e:0x0e+js_len])
|
|
||||||
return state
|
|
||||||
|
|
||||||
|
|
||||||
class sp1(device):
|
|
||||||
"""Controls a Broadlink SP1."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
"""Initialize the device."""
|
|
||||||
device.__init__(self, *args, **kwargs)
|
|
||||||
self.type = "SP1"
|
|
||||||
|
|
||||||
def set_power(self, state: bool) -> None:
|
|
||||||
"""Set the power state of the device."""
|
|
||||||
packet = bytearray(4)
|
|
||||||
packet[0] = state
|
|
||||||
response = self.send_packet(0x66, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
|
|
||||||
|
|
||||||
class sp2(device):
|
|
||||||
"""Controls a Broadlink SP2."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
"""Initialize the controller."""
|
|
||||||
device.__init__(self, *args, **kwargs)
|
|
||||||
self.type = "SP2"
|
|
||||||
|
|
||||||
def set_power(self, state: bool) -> None:
|
|
||||||
"""Set the power state of the device."""
|
|
||||||
packet = bytearray(16)
|
|
||||||
packet[0] = 2
|
|
||||||
if self.check_nightlight():
|
|
||||||
packet[4] = 3 if state else 2
|
|
||||||
else:
|
|
||||||
packet[4] = 1 if state else 0
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
|
|
||||||
def set_nightlight(self, state: bool) -> None:
|
|
||||||
"""Set the night light state of the device."""
|
|
||||||
packet = bytearray(16)
|
|
||||||
packet[0] = 2
|
|
||||||
if self.check_power():
|
|
||||||
packet[4] = 3 if state else 1
|
|
||||||
else:
|
|
||||||
packet[4] = 2 if state else 0
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
|
|
||||||
def check_power(self) -> bool:
|
|
||||||
"""Return the power state of the device."""
|
|
||||||
packet = bytearray(16)
|
|
||||||
packet[0] = 1
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
if isinstance(payload[0x4], int):
|
|
||||||
return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD)
|
|
||||||
return bool(ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFD)
|
|
||||||
|
|
||||||
def check_nightlight(self) -> bool:
|
|
||||||
"""Return the state of the night light."""
|
|
||||||
packet = bytearray(16)
|
|
||||||
packet[0] = 1
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
if isinstance(payload[0x4], int):
|
|
||||||
return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF)
|
|
||||||
return bool(ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFF)
|
|
||||||
|
|
||||||
def get_energy(self) -> int:
|
|
||||||
"""Return the energy state of the device."""
|
|
||||||
packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45])
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
if isinstance(payload[0x7], int):
|
|
||||||
energy = int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0
|
|
||||||
else:
|
|
||||||
energy = int(hex(ord(payload[0x07]) * 256 + ord(payload[0x06]))[2:]) + int(
|
|
||||||
hex(ord(payload[0x05]))[2:]) / 100.0
|
|
||||||
return energy
|
|
||||||
|
|
||||||
|
|
||||||
class a1(device):
|
|
||||||
"""Controls a Broadlink A1."""
|
|
||||||
|
|
||||||
_SENSORS_AND_LEVELS = (
|
|
||||||
('light', ('dark', 'dim', 'normal', 'bright')),
|
|
||||||
('air_quality', ('excellent', 'good', 'normal', 'bad')),
|
|
||||||
('noise', ('quiet', 'normal', 'noisy')),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
"""Initialize the controller."""
|
|
||||||
device.__init__(self, *args, **kwargs)
|
|
||||||
self.type = "A1"
|
|
||||||
|
|
||||||
def check_sensors(self) -> dict:
|
|
||||||
"""Return the state of the sensors."""
|
|
||||||
data = self.check_sensors_raw()
|
|
||||||
for sensor, levels in self._SENSORS_AND_LEVELS:
|
|
||||||
try:
|
|
||||||
data[sensor] = levels[data[sensor]]
|
|
||||||
except IndexError:
|
|
||||||
data[sensor] = 'unknown'
|
|
||||||
return data
|
|
||||||
|
|
||||||
def check_sensors_raw(self) -> dict:
|
|
||||||
"""Return the state of the sensors in raw format."""
|
|
||||||
packet = bytearray([0x1])
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
data = bytearray(payload[0x4:])
|
|
||||||
return {
|
|
||||||
'temperature': data[0x0] + data[0x1] / 10.0,
|
|
||||||
'humidity': data[0x2] + data[0x3] / 10.0,
|
|
||||||
'light': data[0x4],
|
|
||||||
'air_quality': data[0x6],
|
|
||||||
'noise': data[0x8],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class rm(device):
|
|
||||||
"""Controls a Broadlink RM."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
"""Initialize the controller."""
|
|
||||||
device.__init__(self, *args, **kwargs)
|
|
||||||
self.type = "RM2"
|
|
||||||
self._request_header = bytes()
|
|
||||||
self._code_sending_header = bytes()
|
|
||||||
|
|
||||||
def check_data(self) -> bytes:
|
|
||||||
"""Return the last captured code."""
|
|
||||||
packet = bytearray(self._request_header)
|
|
||||||
packet.append(0x04)
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
return payload[len(self._request_header) + 4:]
|
|
||||||
|
|
||||||
def send_data(self, data: bytes) -> None:
|
|
||||||
"""Send a code to the device."""
|
|
||||||
packet = bytearray(self._code_sending_header)
|
|
||||||
packet += bytearray([0x02, 0x00, 0x00, 0x00])
|
|
||||||
packet += data
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
|
|
||||||
def enter_learning(self) -> None:
|
|
||||||
"""Enter infrared learning mode."""
|
|
||||||
packet = bytearray(self._request_header)
|
|
||||||
packet.append(0x03)
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
|
|
||||||
def sweep_frequency(self) -> None:
|
|
||||||
"""Sweep frequency."""
|
|
||||||
packet = bytearray(self._request_header)
|
|
||||||
packet.append(0x19)
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
|
|
||||||
def cancel_sweep_frequency(self) -> None:
|
|
||||||
"""Cancel sweep frequency."""
|
|
||||||
packet = bytearray(self._request_header)
|
|
||||||
packet.append(0x1e)
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
|
|
||||||
def check_frequency(self) -> bool:
|
|
||||||
"""Return True if the frequency was identified successfully."""
|
|
||||||
packet = bytearray(self._request_header)
|
|
||||||
packet.append(0x1a)
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
if payload[len(self._request_header) + 4] == 1:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def find_rf_packet(self) -> bool:
|
|
||||||
"""Enter radiofrequency learning mode."""
|
|
||||||
packet = bytearray(self._request_header)
|
|
||||||
packet.append(0x1b)
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
if payload[len(self._request_header) + 4] == 1:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _check_sensors(self, command: int) -> bytes:
|
|
||||||
"""Return the state of the sensors in raw format."""
|
|
||||||
packet = bytearray(self._request_header)
|
|
||||||
packet.append(command)
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
return bytearray(payload[len(self._request_header) + 4:])
|
|
||||||
|
|
||||||
def check_temperature(self) -> int:
|
|
||||||
"""Return the temperature."""
|
|
||||||
data = self._check_sensors(0x1)
|
|
||||||
return data[0x0] + data[0x1] / 10.0
|
|
||||||
|
|
||||||
def check_sensors(self) -> dict:
|
|
||||||
"""Return the state of the sensors."""
|
|
||||||
data = self._check_sensors(0x1)
|
|
||||||
return {'temperature': data[0x0] + data[0x1] / 10.0}
|
|
||||||
|
|
||||||
|
|
||||||
class rm4(rm):
|
|
||||||
"""Controls a Broadlink RM4."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
"""Initialize the controller."""
|
|
||||||
device.__init__(self, *args, **kwargs)
|
|
||||||
self.type = "RM4"
|
|
||||||
self._request_header = b'\x04\x00'
|
|
||||||
self._code_sending_header = b'\xda\x00'
|
|
||||||
|
|
||||||
def check_temperature(self) -> int:
|
|
||||||
"""Return the temperature."""
|
|
||||||
data = self._check_sensors(0x24)
|
|
||||||
return data[0x0] + data[0x1] / 100.0
|
|
||||||
|
|
||||||
def check_humidity(self) -> int:
|
|
||||||
"""Return the humidity."""
|
|
||||||
data = self._check_sensors(0x24)
|
|
||||||
return data[0x2] + data[0x3] / 100.0
|
|
||||||
|
|
||||||
def check_sensors(self) -> dict:
|
|
||||||
"""Return the state of the sensors."""
|
|
||||||
data = self._check_sensors(0x24)
|
|
||||||
return {
|
|
||||||
'temperature': data[0x0] + data[0x1] / 100.0,
|
|
||||||
'humidity': data[0x2] + data[0x3] / 100.0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# For legacy compatibility - don't use this
|
|
||||||
class rm2(rm):
|
|
||||||
def __init__(self):
|
|
||||||
device.__init__(self, None, None, None)
|
|
||||||
|
|
||||||
def discover(self):
|
|
||||||
dev = discover()
|
|
||||||
self.host = dev.host
|
|
||||||
self.mac = dev.mac
|
|
||||||
|
|
||||||
|
|
||||||
class hysen(device):
|
|
||||||
"""Controls a Hysen HVAC."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
"""Initialize the controller."""
|
|
||||||
device.__init__(self, *args, **kwargs)
|
|
||||||
self.type = "Hysen heating controller"
|
|
||||||
|
|
||||||
# Send a request
|
|
||||||
# input_payload should be a bytearray, usually 6 bytes, e.g. bytearray([0x01,0x06,0x00,0x02,0x10,0x00])
|
|
||||||
# Returns decrypted payload
|
|
||||||
# New behaviour: raises a ValueError if the device response indicates an error or CRC check fails
|
|
||||||
# The function prepends length (2 bytes) and appends CRC
|
|
||||||
|
|
||||||
def send_request(self, input_payload: bytes) -> bytes:
|
|
||||||
"""Send a request to the device."""
|
|
||||||
crc = calculate_crc16(bytes(input_payload))
|
|
||||||
|
|
||||||
# first byte is length, +2 for CRC16
|
|
||||||
request_payload = bytearray([len(input_payload) + 2, 0x00])
|
|
||||||
request_payload.extend(input_payload)
|
|
||||||
|
|
||||||
# append CRC
|
|
||||||
request_payload.append(crc & 0xFF)
|
|
||||||
request_payload.append((crc >> 8) & 0xFF)
|
|
||||||
|
|
||||||
# send to device
|
|
||||||
response = self.send_packet(0x6a, request_payload)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
response_payload = bytearray(self.decrypt(bytes(response[0x38:])))
|
|
||||||
|
|
||||||
# experimental check on CRC in response (first 2 bytes are len, and trailing bytes are crc)
|
|
||||||
response_payload_len = response_payload[0]
|
|
||||||
if response_payload_len + 2 > len(response_payload):
|
|
||||||
raise ValueError('hysen_response_error', 'first byte of response is not length')
|
|
||||||
crc = calculate_crc16(bytes(response_payload[2:response_payload_len]))
|
|
||||||
if (response_payload[response_payload_len] == crc & 0xFF) and (
|
|
||||||
response_payload[response_payload_len + 1] == (crc >> 8) & 0xFF):
|
|
||||||
return response_payload[2:response_payload_len]
|
|
||||||
raise ValueError('hysen_response_error', 'CRC check on response failed')
|
|
||||||
|
|
||||||
def get_temp(self) -> int:
|
|
||||||
"""Return the room temperature in degrees celsius."""
|
|
||||||
payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]))
|
|
||||||
return payload[0x05] / 2.0
|
|
||||||
|
|
||||||
def get_external_temp(self) -> int:
|
|
||||||
"""Return the external temperature in degrees celsius."""
|
|
||||||
payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]))
|
|
||||||
return payload[18] / 2.0
|
|
||||||
|
|
||||||
def get_full_status(self) -> dict:
|
|
||||||
"""Return the state of the device.
|
|
||||||
|
|
||||||
Timer schedule included.
|
|
||||||
"""
|
|
||||||
payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x16]))
|
|
||||||
data = {}
|
|
||||||
data['remote_lock'] = payload[3] & 1
|
|
||||||
data['power'] = payload[4] & 1
|
|
||||||
data['active'] = (payload[4] >> 4) & 1
|
|
||||||
data['temp_manual'] = (payload[4] >> 6) & 1
|
|
||||||
data['room_temp'] = (payload[5] & 255) / 2.0
|
|
||||||
data['thermostat_temp'] = (payload[6] & 255) / 2.0
|
|
||||||
data['auto_mode'] = payload[7] & 15
|
|
||||||
data['loop_mode'] = (payload[7] >> 4) & 15
|
|
||||||
data['sensor'] = payload[8]
|
|
||||||
data['osv'] = payload[9]
|
|
||||||
data['dif'] = payload[10]
|
|
||||||
data['svh'] = payload[11]
|
|
||||||
data['svl'] = payload[12]
|
|
||||||
data['room_temp_adj'] = ((payload[13] << 8) + payload[14]) / 2.0
|
|
||||||
if data['room_temp_adj'] > 32767:
|
|
||||||
data['room_temp_adj'] = 32767 - data['room_temp_adj']
|
|
||||||
data['fre'] = payload[15]
|
|
||||||
data['poweron'] = payload[16]
|
|
||||||
data['unknown'] = payload[17]
|
|
||||||
data['external_temp'] = (payload[18] & 255) / 2.0
|
|
||||||
data['hour'] = payload[19]
|
|
||||||
data['min'] = payload[20]
|
|
||||||
data['sec'] = payload[21]
|
|
||||||
data['dayofweek'] = payload[22]
|
|
||||||
|
|
||||||
weekday = []
|
|
||||||
for i in range(0, 6):
|
|
||||||
weekday.append(
|
|
||||||
{'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0})
|
|
||||||
|
|
||||||
data['weekday'] = weekday
|
|
||||||
weekend = []
|
|
||||||
for i in range(6, 8):
|
|
||||||
weekend.append(
|
|
||||||
{'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0})
|
|
||||||
|
|
||||||
data['weekend'] = weekend
|
|
||||||
return data
|
|
||||||
|
|
||||||
# Change controller mode
|
|
||||||
# auto_mode = 1 for auto (scheduled/timed) mode, 0 for manual mode.
|
|
||||||
# Manual mode will activate last used temperature.
|
|
||||||
# In typical usage call set_temp to activate manual control and set temp.
|
|
||||||
# loop_mode refers to index in [ "12345,67", "123456,7", "1234567" ]
|
|
||||||
# E.g. loop_mode = 0 ("12345,67") means Saturday and Sunday follow the "weekend" schedule
|
|
||||||
# loop_mode = 2 ("1234567") means every day (including Saturday and Sunday) follows the "weekday" schedule
|
|
||||||
# The sensor command is currently experimental
|
|
||||||
def set_mode(self, auto_mode: int, loop_mode: int, sensor: int = 0) -> None:
|
|
||||||
"""Set the mode of the device."""
|
|
||||||
mode_byte = ((loop_mode + 1) << 4) + auto_mode
|
|
||||||
self.send_request(bytearray([0x01, 0x06, 0x00, 0x02, mode_byte, sensor]))
|
|
||||||
|
|
||||||
# Advanced settings
|
|
||||||
# Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor,
|
|
||||||
# 2 for internal control temperature, external limit temperature. Factory default: 0.
|
|
||||||
# Set temperature range for external sensor (OSV) osv = 5..99. Factory default: 42C
|
|
||||||
# Deadzone for floor temprature (dIF) dif = 1..9. Factory default: 2C
|
|
||||||
# Upper temperature limit for internal sensor (SVH) svh = 5..99. Factory default: 35C
|
|
||||||
# Lower temperature limit for internal sensor (SVL) svl = 5..99. Factory default: 5C
|
|
||||||
# Actual temperature calibration (AdJ) adj = -0.5. Prescision 0.1C
|
|
||||||
# Anti-freezing function (FrE) fre = 0 for anti-freezing function shut down,
|
|
||||||
# 1 for anti-freezing function open. Factory default: 0
|
|
||||||
# Power on memory (POn) poweron = 0 for power on memory off, 1 for power on memory on. Factory default: 0
|
|
||||||
def set_advanced(
|
|
||||||
self,
|
|
||||||
loop_mode: int,
|
|
||||||
sensor: int,
|
|
||||||
osv: int,
|
|
||||||
dif: int,
|
|
||||||
svh: int,
|
|
||||||
svl: int,
|
|
||||||
adj: float,
|
|
||||||
fre: int,
|
|
||||||
poweron: int,
|
|
||||||
) -> None:
|
|
||||||
"""Set advanced options."""
|
|
||||||
input_payload = bytearray([0x01, 0x10, 0x00, 0x02, 0x00, 0x05, 0x0a, loop_mode, sensor, osv, dif, svh, svl,
|
|
||||||
(int(adj * 2) >> 8 & 0xff), (int(adj * 2) & 0xff), fre, poweron])
|
|
||||||
self.send_request(input_payload)
|
|
||||||
|
|
||||||
# For backwards compatibility only. Prefer calling set_mode directly.
|
|
||||||
# Note this function invokes loop_mode=0 and sensor=0.
|
|
||||||
def switch_to_auto(self) -> None:
|
|
||||||
"""Switch mode to auto."""
|
|
||||||
self.set_mode(auto_mode=1, loop_mode=0)
|
|
||||||
|
|
||||||
def switch_to_manual(self) -> None:
|
|
||||||
"""Switch mode to manual."""
|
|
||||||
self.set_mode(auto_mode=0, loop_mode=0)
|
|
||||||
|
|
||||||
# Set temperature for manual mode (also activates manual mode if currently in automatic)
|
|
||||||
def set_temp(self, temp: float) -> None:
|
|
||||||
"""Set the target temperature."""
|
|
||||||
self.send_request(bytearray([0x01, 0x06, 0x00, 0x01, 0x00, int(temp * 2)]))
|
|
||||||
|
|
||||||
# Set device on(1) or off(0), does not deactivate Wifi connectivity.
|
|
||||||
# Remote lock disables control by buttons on thermostat.
|
|
||||||
def set_power(self, power: int = 1, remote_lock: int = 0) -> None:
|
|
||||||
"""Set the power state of the device."""
|
|
||||||
self.send_request(bytearray([0x01, 0x06, 0x00, 0x00, remote_lock, power]))
|
|
||||||
|
|
||||||
# set time on device
|
|
||||||
# n.b. day=1 is Monday, ..., day=7 is Sunday
|
|
||||||
def set_time(self, hour: int, minute: int, second: int, day: int) -> None:
|
|
||||||
"""Set the time."""
|
|
||||||
self.send_request(bytearray([0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day]))
|
|
||||||
|
|
||||||
# Set timer schedule
|
|
||||||
# Format is the same as you get from get_full_status.
|
|
||||||
# weekday is a list (ordered) of 6 dicts like:
|
|
||||||
# {'start_hour':17, 'start_minute':30, 'temp': 22 }
|
|
||||||
# Each one specifies the thermostat temp that will become effective at start_hour:start_minute
|
|
||||||
# weekend is similar but only has 2 (e.g. switch on in morning and off in afternoon)
|
|
||||||
def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None:
|
|
||||||
"""Set timer schedule."""
|
|
||||||
# Begin with some magic values ...
|
|
||||||
input_payload = bytearray([0x01, 0x10, 0x00, 0x0a, 0x00, 0x0c, 0x18])
|
|
||||||
|
|
||||||
# Now simply append times/temps
|
|
||||||
# weekday times
|
|
||||||
for i in range(0, 6):
|
|
||||||
input_payload.append(weekday[i]['start_hour'])
|
|
||||||
input_payload.append(weekday[i]['start_minute'])
|
|
||||||
|
|
||||||
# weekend times
|
|
||||||
for i in range(0, 2):
|
|
||||||
input_payload.append(weekend[i]['start_hour'])
|
|
||||||
input_payload.append(weekend[i]['start_minute'])
|
|
||||||
|
|
||||||
# weekday temperatures
|
|
||||||
for i in range(0, 6):
|
|
||||||
input_payload.append(int(weekday[i]['temp'] * 2))
|
|
||||||
|
|
||||||
# weekend temperatures
|
|
||||||
for i in range(0, 2):
|
|
||||||
input_payload.append(int(weekend[i]['temp'] * 2))
|
|
||||||
|
|
||||||
self.send_request(input_payload)
|
|
||||||
|
|
||||||
|
|
||||||
S1C_SENSORS_TYPES = {
|
|
||||||
0x31: 'Door Sensor', # 49 as hex
|
|
||||||
0x91: 'Key Fob', # 145 as hex, as serial on fob corpse
|
|
||||||
0x21: 'Motion Sensor' # 33 as hex
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class S1C(device):
|
|
||||||
"""Controls a Broadlink S1C."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
"""Initialize the controller."""
|
|
||||||
device.__init__(self, *args, **kwargs)
|
|
||||||
self.type = 'S1C'
|
|
||||||
|
|
||||||
def get_sensors_status(self) -> dict:
|
|
||||||
"""Return the state of the sensors."""
|
|
||||||
packet = bytearray(16)
|
|
||||||
packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
if not payload:
|
|
||||||
return None
|
|
||||||
count = payload[0x4]
|
|
||||||
sensors = payload[0x6:]
|
|
||||||
sensors_a = [bytearray(sensors[i * 83:(i + 1) * 83]) for i in range(len(sensors) // 83)]
|
|
||||||
|
|
||||||
sens_res = []
|
|
||||||
for sens in sensors_a:
|
|
||||||
status = ord(chr(sens[0]))
|
|
||||||
_name = str(bytes(sens[4:26]).decode())
|
|
||||||
_order = ord(chr(sens[1]))
|
|
||||||
_type = ord(chr(sens[3]))
|
|
||||||
_serial = bytes(codecs.encode(sens[26:30], "hex")).decode()
|
|
||||||
|
|
||||||
type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown')
|
|
||||||
|
|
||||||
r = {
|
|
||||||
'status': status,
|
|
||||||
'name': _name.strip('\x00'),
|
|
||||||
'type': type_str,
|
|
||||||
'order': _order,
|
|
||||||
'serial': _serial,
|
|
||||||
}
|
|
||||||
if r['serial'] != '00000000':
|
|
||||||
sens_res.append(r)
|
|
||||||
result = {
|
|
||||||
'count': count,
|
|
||||||
'sensors': sens_res
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class dooya(device):
|
|
||||||
"""Controls a Dooya curtain motor."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
"""Initialize the controller."""
|
|
||||||
device.__init__(self, *args, **kwargs)
|
|
||||||
self.type = "Dooya DT360E"
|
|
||||||
|
|
||||||
def _send(self, magic1: int, magic2: int) -> int:
|
|
||||||
"""Send a packet to the device."""
|
|
||||||
packet = bytearray(16)
|
|
||||||
packet[0] = 0x09
|
|
||||||
packet[2] = 0xbb
|
|
||||||
packet[3] = magic1
|
|
||||||
packet[4] = magic2
|
|
||||||
packet[9] = 0xfa
|
|
||||||
packet[10] = 0x44
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x22:0x24])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
return ord(payload[4])
|
|
||||||
|
|
||||||
def open(self) -> int:
|
|
||||||
"""Open the curtain."""
|
|
||||||
return self._send(0x01, 0x00)
|
|
||||||
|
|
||||||
def close(self) -> int:
|
|
||||||
"""Close the curtain."""
|
|
||||||
return self._send(0x02, 0x00)
|
|
||||||
|
|
||||||
def stop(self) -> int:
|
|
||||||
"""Stop the curtain."""
|
|
||||||
return self._send(0x03, 0x00)
|
|
||||||
|
|
||||||
def get_percentage(self) -> int:
|
|
||||||
"""Return the position of the curtain."""
|
|
||||||
return self._send(0x06, 0x5d)
|
|
||||||
|
|
||||||
def set_percentage_and_wait(self, new_percentage: int) -> None:
|
|
||||||
"""Set the position of the curtain."""
|
|
||||||
current = self.get_percentage()
|
|
||||||
if current > new_percentage:
|
|
||||||
self.close()
|
|
||||||
while current is not None and current > new_percentage:
|
|
||||||
time.sleep(0.2)
|
|
||||||
current = self.get_percentage()
|
|
||||||
|
|
||||||
elif current < new_percentage:
|
|
||||||
self.open()
|
|
||||||
while current is not None and current < new_percentage:
|
|
||||||
time.sleep(0.2)
|
|
||||||
current = self.get_percentage()
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
|
|
||||||
class lb1(device):
|
|
||||||
"""Controls a Broadlink LB1."""
|
|
||||||
|
|
||||||
state_dict = []
|
|
||||||
effect_map_dict = {
|
|
||||||
'lovely color': 0,
|
|
||||||
'flashlight': 1,
|
|
||||||
'lightning': 2,
|
|
||||||
'color fading': 3,
|
|
||||||
'color breathing': 4,
|
|
||||||
'multicolor breathing': 5,
|
|
||||||
'color jumping': 6,
|
|
||||||
'multicolor jumping': 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
"""Initialize the controller."""
|
|
||||||
device.__init__(self, *args, **kwargs)
|
|
||||||
self.type = "SmartBulb"
|
|
||||||
|
|
||||||
def send_command(self, command: str, type: str = 'set') -> None:
|
|
||||||
"""Send a command to the device."""
|
|
||||||
packet = bytearray(16+(int(len(command)/16) + 1)*16)
|
|
||||||
packet[0x00] = 0x0c + len(command) & 0xff
|
|
||||||
packet[0x02] = 0xa5
|
|
||||||
packet[0x03] = 0xa5
|
|
||||||
packet[0x04] = 0x5a
|
|
||||||
packet[0x05] = 0x5a
|
|
||||||
packet[0x08] = 0x02 if type == "set" else 0x01 # 0x01 => query, # 0x02 => set
|
|
||||||
packet[0x09] = 0x0b
|
|
||||||
packet[0x0a] = len(command)
|
|
||||||
packet[0x0e:] = map(ord, command)
|
|
||||||
|
|
||||||
checksum = sum(packet, 0xbeaf) & 0xffff
|
|
||||||
packet[0x06] = checksum & 0xff # Checksum 1 position
|
|
||||||
packet[0x07] = checksum >> 8 # Checksum 2 position
|
|
||||||
|
|
||||||
response = self.send_packet(0x6a, packet)
|
|
||||||
check_error(response[0x36:0x38])
|
|
||||||
payload = self.decrypt(bytes(response[0x38:]))
|
|
||||||
|
|
||||||
responseLength = int(payload[0x0a]) | (int(payload[0x0b]) << 8)
|
|
||||||
if responseLength > 0:
|
|
||||||
self.state_dict = json.loads(payload[0x0e:0x0e+responseLength])
|
|
||||||
|
|
||||||
def set_json(self, jsonstr: str) -> str:
|
|
||||||
"""Send a command to the device and return state."""
|
|
||||||
reconvert = json.loads(jsonstr)
|
|
||||||
if 'bulb_sceneidx' in reconvert.keys():
|
|
||||||
reconvert['bulb_sceneidx'] = self.effect_map_dict.get(reconvert['bulb_sceneidx'], 255)
|
|
||||||
|
|
||||||
self.send_command(json.dumps(reconvert))
|
|
||||||
return json.dumps(self.state_dict)
|
|
||||||
|
|
||||||
def set_state(self, state: Union[str, int]) -> None:
|
|
||||||
"""Set the state of the device."""
|
|
||||||
cmd = '{"pwr":%d}' % (1 if state == "ON" or state == 1 else 0)
|
|
||||||
self.send_command(cmd)
|
|
||||||
|
|
||||||
def get_state(self) -> dict:
|
|
||||||
"""Return the state of the device."""
|
|
||||||
cmd = "{}"
|
|
||||||
self.send_command(cmd)
|
|
||||||
return self.state_dict
|
|
||||||
|
|
||||||
# Setup a new Broadlink device via AP Mode. Review the README to see how to enter AP Mode.
|
# 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)
|
# Only tested with Broadlink RM3 Mini (Blackbean)
|
||||||
def setup(ssid: str, password: str, security_mode: int) -> None:
|
def setup(ssid: str, password: str, security_mode: int) -> None:
|
||||||
|
58
broadlink/alarm.py
Normal file
58
broadlink/alarm.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import codecs
|
||||||
|
|
||||||
|
from .device import device
|
||||||
|
from .exceptions import check_error
|
||||||
|
|
||||||
|
|
||||||
|
S1C_SENSORS_TYPES = {
|
||||||
|
0x31: 'Door Sensor', # 49 as hex
|
||||||
|
0x91: 'Key Fob', # 145 as hex, as serial on fob corpse
|
||||||
|
0x21: 'Motion Sensor' # 33 as hex
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class S1C(device):
|
||||||
|
"""Controls a Broadlink S1C."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Initialize the controller."""
|
||||||
|
device.__init__(self, *args, **kwargs)
|
||||||
|
self.type = 'S1C'
|
||||||
|
|
||||||
|
def get_sensors_status(self) -> dict:
|
||||||
|
"""Return the state of the sensors."""
|
||||||
|
packet = bytearray(16)
|
||||||
|
packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
if not payload:
|
||||||
|
return None
|
||||||
|
count = payload[0x4]
|
||||||
|
sensors = payload[0x6:]
|
||||||
|
sensors_a = [bytearray(sensors[i * 83:(i + 1) * 83]) for i in range(len(sensors) // 83)]
|
||||||
|
|
||||||
|
sens_res = []
|
||||||
|
for sens in sensors_a:
|
||||||
|
status = ord(chr(sens[0]))
|
||||||
|
_name = str(bytes(sens[4:26]).decode())
|
||||||
|
_order = ord(chr(sens[1]))
|
||||||
|
_type = ord(chr(sens[3]))
|
||||||
|
_serial = bytes(codecs.encode(sens[26:30], "hex")).decode()
|
||||||
|
|
||||||
|
type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown')
|
||||||
|
|
||||||
|
r = {
|
||||||
|
'status': status,
|
||||||
|
'name': _name.strip('\x00'),
|
||||||
|
'type': type_str,
|
||||||
|
'order': _order,
|
||||||
|
'serial': _serial,
|
||||||
|
}
|
||||||
|
if r['serial'] != '00000000':
|
||||||
|
sens_res.append(r)
|
||||||
|
result = {
|
||||||
|
'count': count,
|
||||||
|
'sensors': sens_res
|
||||||
|
}
|
||||||
|
return result
|
203
broadlink/climate.py
Normal file
203
broadlink/climate.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .device import device
|
||||||
|
from .exceptions import check_error
|
||||||
|
from .helpers import calculate_crc16
|
||||||
|
|
||||||
|
|
||||||
|
class hysen(device):
|
||||||
|
"""Controls a Hysen HVAC."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Initialize the controller."""
|
||||||
|
device.__init__(self, *args, **kwargs)
|
||||||
|
self.type = "Hysen heating controller"
|
||||||
|
|
||||||
|
# Send a request
|
||||||
|
# input_payload should be a bytearray, usually 6 bytes, e.g. bytearray([0x01,0x06,0x00,0x02,0x10,0x00])
|
||||||
|
# Returns decrypted payload
|
||||||
|
# New behaviour: raises a ValueError if the device response indicates an error or CRC check fails
|
||||||
|
# The function prepends length (2 bytes) and appends CRC
|
||||||
|
|
||||||
|
def send_request(self, input_payload: bytes) -> bytes:
|
||||||
|
"""Send a request to the device."""
|
||||||
|
crc = calculate_crc16(bytes(input_payload))
|
||||||
|
|
||||||
|
# first byte is length, +2 for CRC16
|
||||||
|
request_payload = bytearray([len(input_payload) + 2, 0x00])
|
||||||
|
request_payload.extend(input_payload)
|
||||||
|
|
||||||
|
# append CRC
|
||||||
|
request_payload.append(crc & 0xFF)
|
||||||
|
request_payload.append((crc >> 8) & 0xFF)
|
||||||
|
|
||||||
|
# send to device
|
||||||
|
response = self.send_packet(0x6a, request_payload)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
response_payload = bytearray(self.decrypt(bytes(response[0x38:])))
|
||||||
|
|
||||||
|
# experimental check on CRC in response (first 2 bytes are len, and trailing bytes are crc)
|
||||||
|
response_payload_len = response_payload[0]
|
||||||
|
if response_payload_len + 2 > len(response_payload):
|
||||||
|
raise ValueError('hysen_response_error', 'first byte of response is not length')
|
||||||
|
crc = calculate_crc16(bytes(response_payload[2:response_payload_len]))
|
||||||
|
if (response_payload[response_payload_len] == crc & 0xFF) and (
|
||||||
|
response_payload[response_payload_len + 1] == (crc >> 8) & 0xFF):
|
||||||
|
return response_payload[2:response_payload_len]
|
||||||
|
raise ValueError('hysen_response_error', 'CRC check on response failed')
|
||||||
|
|
||||||
|
def get_temp(self) -> int:
|
||||||
|
"""Return the room temperature in degrees celsius."""
|
||||||
|
payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]))
|
||||||
|
return payload[0x05] / 2.0
|
||||||
|
|
||||||
|
def get_external_temp(self) -> int:
|
||||||
|
"""Return the external temperature in degrees celsius."""
|
||||||
|
payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x08]))
|
||||||
|
return payload[18] / 2.0
|
||||||
|
|
||||||
|
def get_full_status(self) -> dict:
|
||||||
|
"""Return the state of the device.
|
||||||
|
|
||||||
|
Timer schedule included.
|
||||||
|
"""
|
||||||
|
payload = self.send_request(bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x16]))
|
||||||
|
data = {}
|
||||||
|
data['remote_lock'] = payload[3] & 1
|
||||||
|
data['power'] = payload[4] & 1
|
||||||
|
data['active'] = (payload[4] >> 4) & 1
|
||||||
|
data['temp_manual'] = (payload[4] >> 6) & 1
|
||||||
|
data['room_temp'] = (payload[5] & 255) / 2.0
|
||||||
|
data['thermostat_temp'] = (payload[6] & 255) / 2.0
|
||||||
|
data['auto_mode'] = payload[7] & 15
|
||||||
|
data['loop_mode'] = (payload[7] >> 4) & 15
|
||||||
|
data['sensor'] = payload[8]
|
||||||
|
data['osv'] = payload[9]
|
||||||
|
data['dif'] = payload[10]
|
||||||
|
data['svh'] = payload[11]
|
||||||
|
data['svl'] = payload[12]
|
||||||
|
data['room_temp_adj'] = ((payload[13] << 8) + payload[14]) / 2.0
|
||||||
|
if data['room_temp_adj'] > 32767:
|
||||||
|
data['room_temp_adj'] = 32767 - data['room_temp_adj']
|
||||||
|
data['fre'] = payload[15]
|
||||||
|
data['poweron'] = payload[16]
|
||||||
|
data['unknown'] = payload[17]
|
||||||
|
data['external_temp'] = (payload[18] & 255) / 2.0
|
||||||
|
data['hour'] = payload[19]
|
||||||
|
data['min'] = payload[20]
|
||||||
|
data['sec'] = payload[21]
|
||||||
|
data['dayofweek'] = payload[22]
|
||||||
|
|
||||||
|
weekday = []
|
||||||
|
for i in range(0, 6):
|
||||||
|
weekday.append(
|
||||||
|
{'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0})
|
||||||
|
|
||||||
|
data['weekday'] = weekday
|
||||||
|
weekend = []
|
||||||
|
for i in range(6, 8):
|
||||||
|
weekend.append(
|
||||||
|
{'start_hour': payload[2 * i + 23], 'start_minute': payload[2 * i + 24], 'temp': payload[i + 39] / 2.0})
|
||||||
|
|
||||||
|
data['weekend'] = weekend
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Change controller mode
|
||||||
|
# auto_mode = 1 for auto (scheduled/timed) mode, 0 for manual mode.
|
||||||
|
# Manual mode will activate last used temperature.
|
||||||
|
# In typical usage call set_temp to activate manual control and set temp.
|
||||||
|
# loop_mode refers to index in [ "12345,67", "123456,7", "1234567" ]
|
||||||
|
# E.g. loop_mode = 0 ("12345,67") means Saturday and Sunday follow the "weekend" schedule
|
||||||
|
# loop_mode = 2 ("1234567") means every day (including Saturday and Sunday) follows the "weekday" schedule
|
||||||
|
# The sensor command is currently experimental
|
||||||
|
def set_mode(self, auto_mode: int, loop_mode: int, sensor: int = 0) -> None:
|
||||||
|
"""Set the mode of the device."""
|
||||||
|
mode_byte = ((loop_mode + 1) << 4) + auto_mode
|
||||||
|
self.send_request(bytearray([0x01, 0x06, 0x00, 0x02, mode_byte, sensor]))
|
||||||
|
|
||||||
|
# Advanced settings
|
||||||
|
# Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor,
|
||||||
|
# 2 for internal control temperature, external limit temperature. Factory default: 0.
|
||||||
|
# Set temperature range for external sensor (OSV) osv = 5..99. Factory default: 42C
|
||||||
|
# Deadzone for floor temprature (dIF) dif = 1..9. Factory default: 2C
|
||||||
|
# Upper temperature limit for internal sensor (SVH) svh = 5..99. Factory default: 35C
|
||||||
|
# Lower temperature limit for internal sensor (SVL) svl = 5..99. Factory default: 5C
|
||||||
|
# Actual temperature calibration (AdJ) adj = -0.5. Prescision 0.1C
|
||||||
|
# Anti-freezing function (FrE) fre = 0 for anti-freezing function shut down,
|
||||||
|
# 1 for anti-freezing function open. Factory default: 0
|
||||||
|
# Power on memory (POn) poweron = 0 for power on memory off, 1 for power on memory on. Factory default: 0
|
||||||
|
def set_advanced(
|
||||||
|
self,
|
||||||
|
loop_mode: int,
|
||||||
|
sensor: int,
|
||||||
|
osv: int,
|
||||||
|
dif: int,
|
||||||
|
svh: int,
|
||||||
|
svl: int,
|
||||||
|
adj: float,
|
||||||
|
fre: int,
|
||||||
|
poweron: int,
|
||||||
|
) -> None:
|
||||||
|
"""Set advanced options."""
|
||||||
|
input_payload = bytearray([0x01, 0x10, 0x00, 0x02, 0x00, 0x05, 0x0a, loop_mode, sensor, osv, dif, svh, svl,
|
||||||
|
(int(adj * 2) >> 8 & 0xff), (int(adj * 2) & 0xff), fre, poweron])
|
||||||
|
self.send_request(input_payload)
|
||||||
|
|
||||||
|
# For backwards compatibility only. Prefer calling set_mode directly.
|
||||||
|
# Note this function invokes loop_mode=0 and sensor=0.
|
||||||
|
def switch_to_auto(self) -> None:
|
||||||
|
"""Switch mode to auto."""
|
||||||
|
self.set_mode(auto_mode=1, loop_mode=0)
|
||||||
|
|
||||||
|
def switch_to_manual(self) -> None:
|
||||||
|
"""Switch mode to manual."""
|
||||||
|
self.set_mode(auto_mode=0, loop_mode=0)
|
||||||
|
|
||||||
|
# Set temperature for manual mode (also activates manual mode if currently in automatic)
|
||||||
|
def set_temp(self, temp: float) -> None:
|
||||||
|
"""Set the target temperature."""
|
||||||
|
self.send_request(bytearray([0x01, 0x06, 0x00, 0x01, 0x00, int(temp * 2)]))
|
||||||
|
|
||||||
|
# Set device on(1) or off(0), does not deactivate Wifi connectivity.
|
||||||
|
# Remote lock disables control by buttons on thermostat.
|
||||||
|
def set_power(self, power: int = 1, remote_lock: int = 0) -> None:
|
||||||
|
"""Set the power state of the device."""
|
||||||
|
self.send_request(bytearray([0x01, 0x06, 0x00, 0x00, remote_lock, power]))
|
||||||
|
|
||||||
|
# set time on device
|
||||||
|
# n.b. day=1 is Monday, ..., day=7 is Sunday
|
||||||
|
def set_time(self, hour: int, minute: int, second: int, day: int) -> None:
|
||||||
|
"""Set the time."""
|
||||||
|
self.send_request(bytearray([0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04, hour, minute, second, day]))
|
||||||
|
|
||||||
|
# Set timer schedule
|
||||||
|
# Format is the same as you get from get_full_status.
|
||||||
|
# weekday is a list (ordered) of 6 dicts like:
|
||||||
|
# {'start_hour':17, 'start_minute':30, 'temp': 22 }
|
||||||
|
# Each one specifies the thermostat temp that will become effective at start_hour:start_minute
|
||||||
|
# weekend is similar but only has 2 (e.g. switch on in morning and off in afternoon)
|
||||||
|
def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None:
|
||||||
|
"""Set timer schedule."""
|
||||||
|
# Begin with some magic values ...
|
||||||
|
input_payload = bytearray([0x01, 0x10, 0x00, 0x0a, 0x00, 0x0c, 0x18])
|
||||||
|
|
||||||
|
# Now simply append times/temps
|
||||||
|
# weekday times
|
||||||
|
for i in range(0, 6):
|
||||||
|
input_payload.append(weekday[i]['start_hour'])
|
||||||
|
input_payload.append(weekday[i]['start_minute'])
|
||||||
|
|
||||||
|
# weekend times
|
||||||
|
for i in range(0, 2):
|
||||||
|
input_payload.append(weekend[i]['start_hour'])
|
||||||
|
input_payload.append(weekend[i]['start_minute'])
|
||||||
|
|
||||||
|
# weekday temperatures
|
||||||
|
for i in range(0, 6):
|
||||||
|
input_payload.append(int(weekday[i]['temp'] * 2))
|
||||||
|
|
||||||
|
# weekend temperatures
|
||||||
|
for i in range(0, 2):
|
||||||
|
input_payload.append(int(weekend[i]['temp'] * 2))
|
||||||
|
|
||||||
|
self.send_request(input_payload)
|
59
broadlink/cover.py
Normal file
59
broadlink/cover.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from .device import device
|
||||||
|
from .exceptions import check_error
|
||||||
|
|
||||||
|
|
||||||
|
class dooya(device):
|
||||||
|
"""Controls a Dooya curtain motor."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Initialize the controller."""
|
||||||
|
device.__init__(self, *args, **kwargs)
|
||||||
|
self.type = "Dooya DT360E"
|
||||||
|
|
||||||
|
def _send(self, magic1: int, magic2: int) -> int:
|
||||||
|
"""Send a packet to the device."""
|
||||||
|
packet = bytearray(16)
|
||||||
|
packet[0] = 0x09
|
||||||
|
packet[2] = 0xbb
|
||||||
|
packet[3] = magic1
|
||||||
|
packet[4] = magic2
|
||||||
|
packet[9] = 0xfa
|
||||||
|
packet[10] = 0x44
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
return ord(payload[4])
|
||||||
|
|
||||||
|
def open(self) -> int:
|
||||||
|
"""Open the curtain."""
|
||||||
|
return self._send(0x01, 0x00)
|
||||||
|
|
||||||
|
def close(self) -> int:
|
||||||
|
"""Close the curtain."""
|
||||||
|
return self._send(0x02, 0x00)
|
||||||
|
|
||||||
|
def stop(self) -> int:
|
||||||
|
"""Stop the curtain."""
|
||||||
|
return self._send(0x03, 0x00)
|
||||||
|
|
||||||
|
def get_percentage(self) -> int:
|
||||||
|
"""Return the position of the curtain."""
|
||||||
|
return self._send(0x06, 0x5d)
|
||||||
|
|
||||||
|
def set_percentage_and_wait(self, new_percentage: int) -> None:
|
||||||
|
"""Set the position of the curtain."""
|
||||||
|
current = self.get_percentage()
|
||||||
|
if current > new_percentage:
|
||||||
|
self.close()
|
||||||
|
while current is not None and current > new_percentage:
|
||||||
|
time.sleep(0.2)
|
||||||
|
current = self.get_percentage()
|
||||||
|
|
||||||
|
elif current < new_percentage:
|
||||||
|
self.open()
|
||||||
|
while current is not None and current < new_percentage:
|
||||||
|
time.sleep(0.2)
|
||||||
|
current = self.get_percentage()
|
||||||
|
self.stop()
|
205
broadlink/device.py
Normal file
205
broadlink/device.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from typing import Tuple, Union
|
||||||
|
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
|
||||||
|
from .exceptions import check_error, exception
|
||||||
|
|
||||||
|
|
||||||
|
class device:
|
||||||
|
"""Controls a Broadlink device."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
host: Tuple[str, int],
|
||||||
|
mac: Union[bytes, str],
|
||||||
|
devtype: int,
|
||||||
|
timeout: int = 10,
|
||||||
|
name: str = None,
|
||||||
|
model: str = None,
|
||||||
|
manufacturer: str = None,
|
||||||
|
is_locked: bool = None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the controller."""
|
||||||
|
self.host = host
|
||||||
|
self.mac = mac.encode() if isinstance(mac, str) else mac
|
||||||
|
self.devtype = devtype if devtype is not None else 0x272a
|
||||||
|
self.timeout = timeout
|
||||||
|
self.name = name
|
||||||
|
self.model = model
|
||||||
|
self.manufacturer = manufacturer
|
||||||
|
self.is_locked = is_locked
|
||||||
|
self.count = random.randrange(0xffff)
|
||||||
|
self.iv = bytearray(
|
||||||
|
[0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58])
|
||||||
|
self.id = bytearray([0, 0, 0, 0])
|
||||||
|
self.type = "Unknown"
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
self.aes = None
|
||||||
|
key = bytearray(
|
||||||
|
[0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02])
|
||||||
|
self.update_aes(key)
|
||||||
|
|
||||||
|
def update_aes(self, key: bytes) -> None:
|
||||||
|
"""Update AES."""
|
||||||
|
self.aes = Cipher(
|
||||||
|
algorithms.AES(key), modes.CBC(self.iv), backend=default_backend()
|
||||||
|
)
|
||||||
|
|
||||||
|
def encrypt(self, payload: bytes) -> bytes:
|
||||||
|
"""Encrypt the payload."""
|
||||||
|
encryptor = self.aes.encryptor()
|
||||||
|
return encryptor.update(payload) + encryptor.finalize()
|
||||||
|
|
||||||
|
def decrypt(self, payload: bytes) -> bytes:
|
||||||
|
"""Decrypt the payload."""
|
||||||
|
decryptor = self.aes.decryptor()
|
||||||
|
return decryptor.update(payload) + decryptor.finalize()
|
||||||
|
|
||||||
|
def auth(self) -> bool:
|
||||||
|
"""Authenticate to the device."""
|
||||||
|
payload = bytearray(0x50)
|
||||||
|
payload[0x04] = 0x31
|
||||||
|
payload[0x05] = 0x31
|
||||||
|
payload[0x06] = 0x31
|
||||||
|
payload[0x07] = 0x31
|
||||||
|
payload[0x08] = 0x31
|
||||||
|
payload[0x09] = 0x31
|
||||||
|
payload[0x0a] = 0x31
|
||||||
|
payload[0x0b] = 0x31
|
||||||
|
payload[0x0c] = 0x31
|
||||||
|
payload[0x0d] = 0x31
|
||||||
|
payload[0x0e] = 0x31
|
||||||
|
payload[0x0f] = 0x31
|
||||||
|
payload[0x10] = 0x31
|
||||||
|
payload[0x11] = 0x31
|
||||||
|
payload[0x12] = 0x31
|
||||||
|
payload[0x1e] = 0x01
|
||||||
|
payload[0x2d] = 0x01
|
||||||
|
payload[0x30] = ord('T')
|
||||||
|
payload[0x31] = ord('e')
|
||||||
|
payload[0x32] = ord('s')
|
||||||
|
payload[0x33] = ord('t')
|
||||||
|
payload[0x34] = ord(' ')
|
||||||
|
payload[0x35] = ord(' ')
|
||||||
|
payload[0x36] = ord('1')
|
||||||
|
|
||||||
|
response = self.send_packet(0x65, payload)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(response[0x38:])
|
||||||
|
|
||||||
|
key = payload[0x04:0x14]
|
||||||
|
if len(key) % 16 != 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.id = payload[0x03::-1]
|
||||||
|
self.update_aes(key)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_fwversion(self) -> int:
|
||||||
|
"""Get firmware version."""
|
||||||
|
packet = bytearray([0x68])
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(response[0x38:])
|
||||||
|
return payload[0x4] | payload[0x5] << 8
|
||||||
|
|
||||||
|
def set_name(self, name: str) -> None:
|
||||||
|
"""Set device name."""
|
||||||
|
packet = bytearray(4)
|
||||||
|
packet += name.encode('utf-8')
|
||||||
|
packet += bytearray(0x50 - len(packet))
|
||||||
|
packet[0x43] = bool(self.is_locked)
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def set_lock(self, state: bool) -> None:
|
||||||
|
"""Lock/unlock the device."""
|
||||||
|
packet = bytearray(4)
|
||||||
|
packet += self.name.encode('utf-8')
|
||||||
|
packet += bytearray(0x50 - len(packet))
|
||||||
|
packet[0x43] = bool(state)
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
self.is_locked = bool(state)
|
||||||
|
|
||||||
|
def get_type(self) -> str:
|
||||||
|
"""Return device type."""
|
||||||
|
return self.type
|
||||||
|
|
||||||
|
def send_packet(self, command: int, payload: bytearray) -> bytearray:
|
||||||
|
"""Send a packet to the device."""
|
||||||
|
self.count = (self.count + 1) & 0xffff
|
||||||
|
packet = bytearray(0x38)
|
||||||
|
packet[0x00] = 0x5a
|
||||||
|
packet[0x01] = 0xa5
|
||||||
|
packet[0x02] = 0xaa
|
||||||
|
packet[0x03] = 0x55
|
||||||
|
packet[0x04] = 0x5a
|
||||||
|
packet[0x05] = 0xa5
|
||||||
|
packet[0x06] = 0xaa
|
||||||
|
packet[0x07] = 0x55
|
||||||
|
packet[0x24] = self.devtype & 0xff
|
||||||
|
packet[0x25] = self.devtype >> 8
|
||||||
|
packet[0x26] = command
|
||||||
|
packet[0x28] = self.count & 0xff
|
||||||
|
packet[0x29] = self.count >> 8
|
||||||
|
packet[0x2a] = self.mac[5]
|
||||||
|
packet[0x2b] = self.mac[4]
|
||||||
|
packet[0x2c] = self.mac[3]
|
||||||
|
packet[0x2d] = self.mac[2]
|
||||||
|
packet[0x2e] = self.mac[1]
|
||||||
|
packet[0x2f] = self.mac[0]
|
||||||
|
packet[0x30] = self.id[3]
|
||||||
|
packet[0x31] = self.id[2]
|
||||||
|
packet[0x32] = self.id[1]
|
||||||
|
packet[0x33] = self.id[0]
|
||||||
|
|
||||||
|
# pad the payload for AES encryption
|
||||||
|
if payload:
|
||||||
|
payload += bytearray((16 - len(payload)) % 16)
|
||||||
|
|
||||||
|
checksum = sum(payload, 0xbeaf) & 0xffff
|
||||||
|
packet[0x34] = checksum & 0xff
|
||||||
|
packet[0x35] = checksum >> 8
|
||||||
|
|
||||||
|
payload = self.encrypt(payload)
|
||||||
|
for i in range(len(payload)):
|
||||||
|
packet.append(payload[i])
|
||||||
|
|
||||||
|
checksum = sum(packet, 0xbeaf) & 0xffff
|
||||||
|
packet[0x20] = checksum & 0xff
|
||||||
|
packet[0x21] = checksum >> 8
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
with self.lock:
|
||||||
|
cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
cs.sendto(packet, self.host)
|
||||||
|
cs.settimeout(1)
|
||||||
|
resp, _ = cs.recvfrom(2048)
|
||||||
|
resp = bytearray(resp)
|
||||||
|
break
|
||||||
|
except socket.timeout:
|
||||||
|
if (time.time() - start_time) > self.timeout:
|
||||||
|
cs.close()
|
||||||
|
raise exception(-4000) # Network timeout.
|
||||||
|
cs.close()
|
||||||
|
|
||||||
|
if len(resp) < 0x30:
|
||||||
|
raise exception(-4007) # Length error.
|
||||||
|
|
||||||
|
checksum = resp[0x20] | (resp[0x21] << 8)
|
||||||
|
if sum(resp, 0xbeaf) - sum(resp[0x20:0x22]) & 0xffff != checksum:
|
||||||
|
raise exception(-4008) # Checksum error.
|
||||||
|
|
||||||
|
return resp
|
71
broadlink/light.py
Normal file
71
broadlink/light.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import json
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from .device import device
|
||||||
|
from .exceptions import check_error
|
||||||
|
|
||||||
|
|
||||||
|
class lb1(device):
|
||||||
|
"""Controls a Broadlink LB1."""
|
||||||
|
|
||||||
|
state_dict = []
|
||||||
|
effect_map_dict = {
|
||||||
|
'lovely color': 0,
|
||||||
|
'flashlight': 1,
|
||||||
|
'lightning': 2,
|
||||||
|
'color fading': 3,
|
||||||
|
'color breathing': 4,
|
||||||
|
'multicolor breathing': 5,
|
||||||
|
'color jumping': 6,
|
||||||
|
'multicolor jumping': 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Initialize the controller."""
|
||||||
|
device.__init__(self, *args, **kwargs)
|
||||||
|
self.type = "SmartBulb"
|
||||||
|
|
||||||
|
def send_command(self, command: str, type: str = 'set') -> None:
|
||||||
|
"""Send a command to the device."""
|
||||||
|
packet = bytearray(16+(int(len(command)/16) + 1)*16)
|
||||||
|
packet[0x00] = 0x0c + len(command) & 0xff
|
||||||
|
packet[0x02] = 0xa5
|
||||||
|
packet[0x03] = 0xa5
|
||||||
|
packet[0x04] = 0x5a
|
||||||
|
packet[0x05] = 0x5a
|
||||||
|
packet[0x08] = 0x02 if type == "set" else 0x01 # 0x01 => query, # 0x02 => set
|
||||||
|
packet[0x09] = 0x0b
|
||||||
|
packet[0x0a] = len(command)
|
||||||
|
packet[0x0e:] = map(ord, command)
|
||||||
|
|
||||||
|
checksum = sum(packet, 0xbeaf) & 0xffff
|
||||||
|
packet[0x06] = checksum & 0xff # Checksum 1 position
|
||||||
|
packet[0x07] = checksum >> 8 # Checksum 2 position
|
||||||
|
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x36:0x38])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
|
||||||
|
responseLength = int(payload[0x0a]) | (int(payload[0x0b]) << 8)
|
||||||
|
if responseLength > 0:
|
||||||
|
self.state_dict = json.loads(payload[0x0e:0x0e+responseLength])
|
||||||
|
|
||||||
|
def set_json(self, jsonstr: str) -> str:
|
||||||
|
"""Send a command to the device and return state."""
|
||||||
|
reconvert = json.loads(jsonstr)
|
||||||
|
if 'bulb_sceneidx' in reconvert.keys():
|
||||||
|
reconvert['bulb_sceneidx'] = self.effect_map_dict.get(reconvert['bulb_sceneidx'], 255)
|
||||||
|
|
||||||
|
self.send_command(json.dumps(reconvert))
|
||||||
|
return json.dumps(self.state_dict)
|
||||||
|
|
||||||
|
def set_state(self, state: Union[str, int]) -> None:
|
||||||
|
"""Set the state of the device."""
|
||||||
|
cmd = '{"pwr":%d}' % (1 if state == "ON" or state == 1 else 0)
|
||||||
|
self.send_command(cmd)
|
||||||
|
|
||||||
|
def get_state(self) -> dict:
|
||||||
|
"""Return the state of the device."""
|
||||||
|
cmd = "{}"
|
||||||
|
self.send_command(cmd)
|
||||||
|
return self.state_dict
|
133
broadlink/remote.py
Normal file
133
broadlink/remote.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
from .device import device
|
||||||
|
from .exceptions import check_error
|
||||||
|
|
||||||
|
|
||||||
|
class rm(device):
|
||||||
|
"""Controls a Broadlink RM."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Initialize the controller."""
|
||||||
|
device.__init__(self, *args, **kwargs)
|
||||||
|
self.type = "RM2"
|
||||||
|
self._request_header = bytes()
|
||||||
|
self._code_sending_header = bytes()
|
||||||
|
|
||||||
|
def check_data(self) -> bytes:
|
||||||
|
"""Return the last captured code."""
|
||||||
|
packet = bytearray(self._request_header)
|
||||||
|
packet.append(0x04)
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
return payload[len(self._request_header) + 4:]
|
||||||
|
|
||||||
|
def send_data(self, data: bytes) -> None:
|
||||||
|
"""Send a code to the device."""
|
||||||
|
packet = bytearray(self._code_sending_header)
|
||||||
|
packet += bytearray([0x02, 0x00, 0x00, 0x00])
|
||||||
|
packet += data
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
|
||||||
|
def enter_learning(self) -> None:
|
||||||
|
"""Enter infrared learning mode."""
|
||||||
|
packet = bytearray(self._request_header)
|
||||||
|
packet.append(0x03)
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
|
||||||
|
def sweep_frequency(self) -> None:
|
||||||
|
"""Sweep frequency."""
|
||||||
|
packet = bytearray(self._request_header)
|
||||||
|
packet.append(0x19)
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
|
||||||
|
def cancel_sweep_frequency(self) -> None:
|
||||||
|
"""Cancel sweep frequency."""
|
||||||
|
packet = bytearray(self._request_header)
|
||||||
|
packet.append(0x1e)
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
|
||||||
|
def check_frequency(self) -> bool:
|
||||||
|
"""Return True if the frequency was identified successfully."""
|
||||||
|
packet = bytearray(self._request_header)
|
||||||
|
packet.append(0x1a)
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
if payload[len(self._request_header) + 4] == 1:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def find_rf_packet(self) -> bool:
|
||||||
|
"""Enter radiofrequency learning mode."""
|
||||||
|
packet = bytearray(self._request_header)
|
||||||
|
packet.append(0x1b)
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
if payload[len(self._request_header) + 4] == 1:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_sensors(self, command: int) -> bytes:
|
||||||
|
"""Return the state of the sensors in raw format."""
|
||||||
|
packet = bytearray(self._request_header)
|
||||||
|
packet.append(command)
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
return bytearray(payload[len(self._request_header) + 4:])
|
||||||
|
|
||||||
|
def check_temperature(self) -> int:
|
||||||
|
"""Return the temperature."""
|
||||||
|
data = self._check_sensors(0x1)
|
||||||
|
return data[0x0] + data[0x1] / 10.0
|
||||||
|
|
||||||
|
def check_sensors(self) -> dict:
|
||||||
|
"""Return the state of the sensors."""
|
||||||
|
data = self._check_sensors(0x1)
|
||||||
|
return {'temperature': data[0x0] + data[0x1] / 10.0}
|
||||||
|
|
||||||
|
|
||||||
|
class rm4(rm):
|
||||||
|
"""Controls a Broadlink RM4."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Initialize the controller."""
|
||||||
|
device.__init__(self, *args, **kwargs)
|
||||||
|
self.type = "RM4"
|
||||||
|
self._request_header = b'\x04\x00'
|
||||||
|
self._code_sending_header = b'\xda\x00'
|
||||||
|
|
||||||
|
def check_temperature(self) -> int:
|
||||||
|
"""Return the temperature."""
|
||||||
|
data = self._check_sensors(0x24)
|
||||||
|
return data[0x0] + data[0x1] / 100.0
|
||||||
|
|
||||||
|
def check_humidity(self) -> int:
|
||||||
|
"""Return the humidity."""
|
||||||
|
data = self._check_sensors(0x24)
|
||||||
|
return data[0x2] + data[0x3] / 100.0
|
||||||
|
|
||||||
|
def check_sensors(self) -> dict:
|
||||||
|
"""Return the state of the sensors."""
|
||||||
|
data = self._check_sensors(0x24)
|
||||||
|
return {
|
||||||
|
'temperature': data[0x0] + data[0x1] / 100.0,
|
||||||
|
'humidity': data[0x2] + data[0x3] / 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# For legacy compatibility - don't use this
|
||||||
|
class rm2(rm):
|
||||||
|
def __init__(self):
|
||||||
|
device.__init__(self, None, None, None)
|
||||||
|
|
||||||
|
def discover(self):
|
||||||
|
dev = discover()
|
||||||
|
self.host = dev.host
|
||||||
|
self.mac = dev.mac
|
||||||
|
|
42
broadlink/sensor.py
Normal file
42
broadlink/sensor.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from .device import device
|
||||||
|
from .exceptions import check_error
|
||||||
|
|
||||||
|
|
||||||
|
class a1(device):
|
||||||
|
"""Controls a Broadlink A1."""
|
||||||
|
|
||||||
|
_SENSORS_AND_LEVELS = (
|
||||||
|
('light', ('dark', 'dim', 'normal', 'bright')),
|
||||||
|
('air_quality', ('excellent', 'good', 'normal', 'bad')),
|
||||||
|
('noise', ('quiet', 'normal', 'noisy')),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Initialize the controller."""
|
||||||
|
device.__init__(self, *args, **kwargs)
|
||||||
|
self.type = "A1"
|
||||||
|
|
||||||
|
def check_sensors(self) -> dict:
|
||||||
|
"""Return the state of the sensors."""
|
||||||
|
data = self.check_sensors_raw()
|
||||||
|
for sensor, levels in self._SENSORS_AND_LEVELS:
|
||||||
|
try:
|
||||||
|
data[sensor] = levels[data[sensor]]
|
||||||
|
except IndexError:
|
||||||
|
data[sensor] = 'unknown'
|
||||||
|
return data
|
||||||
|
|
||||||
|
def check_sensors_raw(self) -> dict:
|
||||||
|
"""Return the state of the sensors in raw format."""
|
||||||
|
packet = bytearray([0x1])
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
data = bytearray(payload[0x4:])
|
||||||
|
return {
|
||||||
|
'temperature': data[0x0] + data[0x1] / 10.0,
|
||||||
|
'humidity': data[0x2] + data[0x3] / 10.0,
|
||||||
|
'light': data[0x4],
|
||||||
|
'air_quality': data[0x6],
|
||||||
|
'noise': data[0x8],
|
||||||
|
}
|
227
broadlink/switch.py
Normal file
227
broadlink/switch.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import json
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from .device import device
|
||||||
|
from .exceptions import check_error
|
||||||
|
|
||||||
|
|
||||||
|
class mp1(device):
|
||||||
|
"""Controls a Broadlink MP1."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Initialize the controller."""
|
||||||
|
device.__init__(self, *args, **kwargs)
|
||||||
|
self.type = "MP1"
|
||||||
|
|
||||||
|
def set_power_mask(self, sid_mask: int, state: bool) -> None:
|
||||||
|
"""Set the power state of the device."""
|
||||||
|
packet = bytearray(16)
|
||||||
|
packet[0x00] = 0x0d
|
||||||
|
packet[0x02] = 0xa5
|
||||||
|
packet[0x03] = 0xa5
|
||||||
|
packet[0x04] = 0x5a
|
||||||
|
packet[0x05] = 0x5a
|
||||||
|
packet[0x06] = 0xb2 + ((sid_mask << 1) if state else sid_mask)
|
||||||
|
packet[0x07] = 0xc0
|
||||||
|
packet[0x08] = 0x02
|
||||||
|
packet[0x0a] = 0x03
|
||||||
|
packet[0x0d] = sid_mask
|
||||||
|
packet[0x0e] = sid_mask if state else 0
|
||||||
|
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
|
||||||
|
def set_power(self, sid: int, state: bool) -> None:
|
||||||
|
"""Set the power state of the device."""
|
||||||
|
sid_mask = 0x01 << (sid - 1)
|
||||||
|
self.set_power_mask(sid_mask, state)
|
||||||
|
|
||||||
|
def check_power_raw(self) -> bool:
|
||||||
|
"""Return the power state of the device in raw format."""
|
||||||
|
packet = bytearray(16)
|
||||||
|
packet[0x00] = 0x0a
|
||||||
|
packet[0x02] = 0xa5
|
||||||
|
packet[0x03] = 0xa5
|
||||||
|
packet[0x04] = 0x5a
|
||||||
|
packet[0x05] = 0x5a
|
||||||
|
packet[0x06] = 0xae
|
||||||
|
packet[0x07] = 0xc0
|
||||||
|
packet[0x08] = 0x01
|
||||||
|
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
return payload[0x0e]
|
||||||
|
|
||||||
|
def check_power(self) -> dict:
|
||||||
|
"""Return the power state of the device."""
|
||||||
|
state = self.check_power_raw()
|
||||||
|
if state is None:
|
||||||
|
return {'s1': None, 's2': None, 's3': None, 's4': None}
|
||||||
|
data = {}
|
||||||
|
data['s1'] = bool(state & 0x01)
|
||||||
|
data['s2'] = bool(state & 0x02)
|
||||||
|
data['s3'] = bool(state & 0x04)
|
||||||
|
data['s4'] = bool(state & 0x08)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class bg1(device):
|
||||||
|
"""Controls a BG Electrical smart outlet."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Initialize the controller."""
|
||||||
|
device.__init__(self, *args, **kwargs)
|
||||||
|
self.type = "BG1"
|
||||||
|
|
||||||
|
def get_state(self) -> dict:
|
||||||
|
"""Return the power state of the device.
|
||||||
|
|
||||||
|
Example: `{"pwr":1,"pwr1":1,"pwr2":0,"maxworktime":60,"maxworktime1":60,"maxworktime2":0,"idcbrightness":50}`
|
||||||
|
"""
|
||||||
|
packet = self._encode(1, b'{}')
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
return self._decode(response)
|
||||||
|
|
||||||
|
def set_state(
|
||||||
|
self,
|
||||||
|
pwr: bool = None,
|
||||||
|
pwr1: bool = None,
|
||||||
|
pwr2: bool = None,
|
||||||
|
maxworktime: int = None,
|
||||||
|
maxworktime1: int = None,
|
||||||
|
maxworktime2: int = None,
|
||||||
|
idcbrightness: int = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Set the power state of the device."""
|
||||||
|
data = {}
|
||||||
|
if pwr is not None:
|
||||||
|
data['pwr'] = int(bool(pwr))
|
||||||
|
if pwr1 is not None:
|
||||||
|
data['pwr1'] = int(bool(pwr1))
|
||||||
|
if pwr2 is not None:
|
||||||
|
data['pwr2'] = int(bool(pwr2))
|
||||||
|
if maxworktime is not None:
|
||||||
|
data['maxworktime'] = maxworktime
|
||||||
|
if maxworktime1 is not None:
|
||||||
|
data['maxworktime1'] = maxworktime1
|
||||||
|
if maxworktime2 is not None:
|
||||||
|
data['maxworktime2'] = maxworktime2
|
||||||
|
if idcbrightness is not None:
|
||||||
|
data['idcbrightness'] = idcbrightness
|
||||||
|
js = json.dumps(data).encode('utf8')
|
||||||
|
packet = self._encode(2, js)
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
return self._decode(response)
|
||||||
|
|
||||||
|
def _encode(self, flag: int, js: str) -> bytearray:
|
||||||
|
"""Encode a message."""
|
||||||
|
# The packet format is:
|
||||||
|
# 0x00-0x01 length
|
||||||
|
# 0x02-0x05 header
|
||||||
|
# 0x06-0x07 00
|
||||||
|
# 0x08 flag (1 for read or 2 write?)
|
||||||
|
# 0x09 unknown (0xb)
|
||||||
|
# 0x0a-0x0d length of json
|
||||||
|
# 0x0e- json data
|
||||||
|
packet = bytearray(14)
|
||||||
|
length = 4 + 2 + 2 + 4 + len(js)
|
||||||
|
struct.pack_into('<HHHHBBI', packet, 0, length, 0xa5a5, 0x5a5a, 0x0000, flag, 0x0b, len(js))
|
||||||
|
for i in range(len(js)):
|
||||||
|
packet.append(js[i])
|
||||||
|
|
||||||
|
checksum = sum(packet[0x08:], 0xc0ad) & 0xffff
|
||||||
|
packet[0x06] = checksum & 0xff
|
||||||
|
packet[0x07] = checksum >> 8
|
||||||
|
return packet
|
||||||
|
|
||||||
|
def _decode(self, response: bytes) -> dict:
|
||||||
|
"""Decode a message."""
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
js_len = struct.unpack_from('<I', payload, 0x0a)[0]
|
||||||
|
state = json.loads(payload[0x0e:0x0e+js_len])
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
class sp1(device):
|
||||||
|
"""Controls a Broadlink SP1."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Initialize the device."""
|
||||||
|
device.__init__(self, *args, **kwargs)
|
||||||
|
self.type = "SP1"
|
||||||
|
|
||||||
|
def set_power(self, state: bool) -> None:
|
||||||
|
"""Set the power state of the device."""
|
||||||
|
packet = bytearray(4)
|
||||||
|
packet[0] = state
|
||||||
|
response = self.send_packet(0x66, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
|
||||||
|
|
||||||
|
class sp2(device):
|
||||||
|
"""Controls a Broadlink SP2."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Initialize the controller."""
|
||||||
|
device.__init__(self, *args, **kwargs)
|
||||||
|
self.type = "SP2"
|
||||||
|
|
||||||
|
def set_power(self, state: bool) -> None:
|
||||||
|
"""Set the power state of the device."""
|
||||||
|
packet = bytearray(16)
|
||||||
|
packet[0] = 2
|
||||||
|
if self.check_nightlight():
|
||||||
|
packet[4] = 3 if state else 2
|
||||||
|
else:
|
||||||
|
packet[4] = 1 if state else 0
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
|
||||||
|
def set_nightlight(self, state: bool) -> None:
|
||||||
|
"""Set the night light state of the device."""
|
||||||
|
packet = bytearray(16)
|
||||||
|
packet[0] = 2
|
||||||
|
if self.check_power():
|
||||||
|
packet[4] = 3 if state else 1
|
||||||
|
else:
|
||||||
|
packet[4] = 2 if state else 0
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
|
||||||
|
def check_power(self) -> bool:
|
||||||
|
"""Return the power state of the device."""
|
||||||
|
packet = bytearray(16)
|
||||||
|
packet[0] = 1
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
if isinstance(payload[0x4], int):
|
||||||
|
return bool(payload[0x4] == 1 or payload[0x4] == 3 or payload[0x4] == 0xFD)
|
||||||
|
return bool(ord(payload[0x4]) == 1 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFD)
|
||||||
|
|
||||||
|
def check_nightlight(self) -> bool:
|
||||||
|
"""Return the state of the night light."""
|
||||||
|
packet = bytearray(16)
|
||||||
|
packet[0] = 1
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
if isinstance(payload[0x4], int):
|
||||||
|
return bool(payload[0x4] == 2 or payload[0x4] == 3 or payload[0x4] == 0xFF)
|
||||||
|
return bool(ord(payload[0x4]) == 2 or ord(payload[0x4]) == 3 or ord(payload[0x4]) == 0xFF)
|
||||||
|
|
||||||
|
def get_energy(self) -> int:
|
||||||
|
"""Return the energy state of the device."""
|
||||||
|
packet = bytearray([8, 0, 254, 1, 5, 1, 0, 0, 0, 45])
|
||||||
|
response = self.send_packet(0x6a, packet)
|
||||||
|
check_error(response[0x22:0x24])
|
||||||
|
payload = self.decrypt(bytes(response[0x38:]))
|
||||||
|
if isinstance(payload[0x7], int):
|
||||||
|
energy = int(hex(payload[0x07] * 256 + payload[0x06])[2:]) + int(hex(payload[0x05])[2:]) / 100.0
|
||||||
|
else:
|
||||||
|
energy = int(hex(ord(payload[0x07]) * 256 + ord(payload[0x06]))[2:]) + int(
|
||||||
|
hex(ord(payload[0x05]))[2:]) / 100.0
|
||||||
|
return energy
|
Loading…
Reference in New Issue
Block a user