Cleaned up pytuya code
This commit is contained in:
@@ -1,13 +1,58 @@
|
|||||||
# Python module to interface with Shenzhen Xenon ESP8266MOD WiFi smart devices
|
# TinyTuya Module
|
||||||
# E.g. https://wikidevi.com/wiki/Xenon_SM-PW701U
|
# -*- coding: utf-8 -*-
|
||||||
# SKYROKU SM-PW701U Wi-Fi Plug Smart Plug
|
"""
|
||||||
# Wuudi SM-S0301-US - WIFI Smart Power Socket Multi Plug with 4 AC Outlets and 4 USB Charging Works with Alexa
|
Python module to interface with Tuya WiFi smart devices
|
||||||
#
|
Mostly derived from Shenzhen Xenon ESP8266MOD WiFi smart devices
|
||||||
# This would not exist without the protocol reverse engineering from
|
E.g. https://wikidevi.com/wiki/Xenon_SM-PW701U
|
||||||
# https://github.com/codetheweb/tuyapi by codetheweb and blackrozes
|
|
||||||
#
|
|
||||||
# Tested with Python 2.7 and Python 3.6.1 only
|
|
||||||
|
|
||||||
|
Author: clach04
|
||||||
|
Maintained by: rospogrigio
|
||||||
|
|
||||||
|
For more information see https://github.com/clach04/python-tuya
|
||||||
|
|
||||||
|
Classes
|
||||||
|
OutletDevice(dev_id, address, local_key=None)
|
||||||
|
CoverDevice(dev_id, address, local_key=None)
|
||||||
|
BulbDevice(dev_id, address, local_key=None)
|
||||||
|
|
||||||
|
dev_id (str): Device ID e.g. 01234567891234567890
|
||||||
|
address (str): Device Network IP Address e.g. 10.0.1.99
|
||||||
|
local_key (str, optional): The encryption key. Defaults to None.
|
||||||
|
|
||||||
|
Functions
|
||||||
|
json = status() # returns json payload
|
||||||
|
set_version(version) # 3.1 [default] or 3.3
|
||||||
|
set_dpsUsed(dpsUsed)
|
||||||
|
set_status(on, switch=1) # Set status of the device to 'on' or 'off' (bool)
|
||||||
|
set_value(index, value) # Set int value of any index.
|
||||||
|
turn_on(switch=1):
|
||||||
|
turn_off(switch=1):
|
||||||
|
set_timer(num_secs):
|
||||||
|
|
||||||
|
CoverDevice:
|
||||||
|
open_cover(switch=1):
|
||||||
|
close_cover(switch=1):
|
||||||
|
stop_cover(switch=1):
|
||||||
|
|
||||||
|
BulbDevice
|
||||||
|
set_colour(r, g, b):
|
||||||
|
set_white(brightness, colourtemp):
|
||||||
|
set_brightness(brightness):
|
||||||
|
set_colourtemp(colourtemp):
|
||||||
|
result = brightness():
|
||||||
|
result = colourtemp():
|
||||||
|
(r, g, b) = colour_rgb():
|
||||||
|
(h,s,v) = colour_hsv()
|
||||||
|
result = state():
|
||||||
|
|
||||||
|
Credits
|
||||||
|
* TuyaAPI https://github.com/codetheweb/tuyapi by codetheweb and blackrozes
|
||||||
|
For protocol reverse engineering
|
||||||
|
* PyTuya https://github.com/clach04/python-tuya by clach04
|
||||||
|
The origin of this python module (now abandoned)
|
||||||
|
* LocalTuya https://github.com/rospogrigio/localtuya-homeassistant by rospogrigio
|
||||||
|
Updated pytuya to support devices with Device IDs of 22 characters
|
||||||
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
@@ -34,7 +79,7 @@ __author__ = 'rospogrigio'
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
logging.basicConfig() # TODO include function name/line numbers in log
|
logging.basicConfig() # TODO include function name/line numbers in log
|
||||||
#log.setLevel(level=logging.DEBUG) # Debug hack!
|
#log.setLevel(level=logging.DEBUG) # Uncomment to Debug
|
||||||
|
|
||||||
log.debug('%s version %s', __name__, version)
|
log.debug('%s version %s', __name__, version)
|
||||||
log.debug('Python %s on %s', sys.version, sys.platform)
|
log.debug('Python %s on %s', sys.version, sys.platform)
|
||||||
@@ -55,7 +100,6 @@ IS_PY2 = sys.version_info[0] == 2
|
|||||||
|
|
||||||
class AESCipher(object):
|
class AESCipher(object):
|
||||||
def __init__(self, key):
|
def __init__(self, key):
|
||||||
#self.bs = 32 # 32 work fines for ON, does not work for OFF. Padding different compared to js version https://github.com/codetheweb/tuyapi/
|
|
||||||
self.bs = 16
|
self.bs = 16
|
||||||
self.key = key
|
self.key = key
|
||||||
def encrypt(self, raw, use_base64 = True):
|
def encrypt(self, raw, use_base64 = True):
|
||||||
@@ -68,7 +112,6 @@ class AESCipher(object):
|
|||||||
cipher = pyaes.blockfeeder.Encrypter(pyaes.AESModeOfOperationECB(self.key)) # no IV, auto pads to 16
|
cipher = pyaes.blockfeeder.Encrypter(pyaes.AESModeOfOperationECB(self.key)) # no IV, auto pads to 16
|
||||||
crypted_text = cipher.feed(raw)
|
crypted_text = cipher.feed(raw)
|
||||||
crypted_text += cipher.feed() # flush final block
|
crypted_text += cipher.feed() # flush final block
|
||||||
#print('crypted_text %r' % crypted_text)
|
|
||||||
#print('crypted_text (%d) %r' % (len(crypted_text), crypted_text))
|
#print('crypted_text (%d) %r' % (len(crypted_text), crypted_text))
|
||||||
if use_base64:
|
if use_base64:
|
||||||
return base64.b64encode(crypted_text)
|
return base64.b64encode(crypted_text)
|
||||||
@@ -267,33 +310,17 @@ class XenonDevice(object):
|
|||||||
json_payload = PROTOCOL_VERSION_BYTES_33 + b"\0\0\0\0\0\0\0\0\0\0\0\0" + json_payload
|
json_payload = PROTOCOL_VERSION_BYTES_33 + b"\0\0\0\0\0\0\0\0\0\0\0\0" + json_payload
|
||||||
elif command == SET:
|
elif command == SET:
|
||||||
# need to encrypt
|
# need to encrypt
|
||||||
#print('json_payload %r' % json_payload)
|
|
||||||
self.cipher = AESCipher(self.local_key) # expect to connect and then disconnect to set new
|
self.cipher = AESCipher(self.local_key) # expect to connect and then disconnect to set new
|
||||||
json_payload = self.cipher.encrypt(json_payload)
|
json_payload = self.cipher.encrypt(json_payload)
|
||||||
#print('crypted json_payload %r' % json_payload)
|
|
||||||
preMd5String = b'data=' + json_payload + b'||lpv=' + PROTOCOL_VERSION_BYTES_31 + b'||' + self.local_key
|
preMd5String = b'data=' + json_payload + b'||lpv=' + PROTOCOL_VERSION_BYTES_31 + b'||' + self.local_key
|
||||||
#print('preMd5String %r' % preMd5String)
|
|
||||||
m = md5()
|
m = md5()
|
||||||
m.update(preMd5String)
|
m.update(preMd5String)
|
||||||
#print(repr(m.digest()))
|
|
||||||
hexdigest = m.hexdigest()
|
hexdigest = m.hexdigest()
|
||||||
#print(hexdigest)
|
|
||||||
#print(hexdigest[8:][:16])
|
|
||||||
json_payload = PROTOCOL_VERSION_BYTES_31 + hexdigest[8:][:16].encode('latin1') + json_payload
|
json_payload = PROTOCOL_VERSION_BYTES_31 + hexdigest[8:][:16].encode('latin1') + json_payload
|
||||||
#print('data_to_send')
|
|
||||||
#print(json_payload)
|
|
||||||
#print('crypted json_payload (%d) %r' % (len(json_payload), json_payload))
|
|
||||||
#print('json_payload %r' % repr(json_payload))
|
|
||||||
#print('json_payload len %r' % len(json_payload))
|
|
||||||
#print(bin2hex(json_payload))
|
|
||||||
self.cipher = None # expect to connect and then disconnect to set new
|
self.cipher = None # expect to connect and then disconnect to set new
|
||||||
|
|
||||||
|
|
||||||
postfix_payload = hex2bin(bin2hex(json_payload) + payload_dict[self.dev_type]['suffix'])
|
postfix_payload = hex2bin(bin2hex(json_payload) + payload_dict[self.dev_type]['suffix'])
|
||||||
#print('postfix_payload %r' % postfix_payload)
|
|
||||||
#print('postfix_payload %r' % len(postfix_payload))
|
|
||||||
#print('postfix_payload %x' % len(postfix_payload))
|
|
||||||
#print('postfix_payload %r' % hex(len(postfix_payload)))
|
|
||||||
assert len(postfix_payload) <= 0xff
|
assert len(postfix_payload) <= 0xff
|
||||||
postfix_payload_hex_len = '%x' % len(postfix_payload) # TODO this assumes a single byte 0-255 (0x00-0xff)
|
postfix_payload_hex_len = '%x' % len(postfix_payload) # TODO this assumes a single byte 0-255 (0x00-0xff)
|
||||||
buffer = hex2bin( payload_dict[self.dev_type]['prefix'] +
|
buffer = hex2bin( payload_dict[self.dev_type]['prefix'] +
|
||||||
@@ -304,11 +331,6 @@ class XenonDevice(object):
|
|||||||
# calc the CRC of everything except where the CRC goes and the suffix
|
# calc the CRC of everything except where the CRC goes and the suffix
|
||||||
hex_crc = format(binascii.crc32(buffer[:-8]) & 0xffffffff, '08X')
|
hex_crc = format(binascii.crc32(buffer[:-8]) & 0xffffffff, '08X')
|
||||||
buffer = buffer[:-8] + hex2bin(hex_crc) + buffer[-4:]
|
buffer = buffer[:-8] + hex2bin(hex_crc) + buffer[-4:]
|
||||||
#print('command', command)
|
|
||||||
#print('prefix')
|
|
||||||
#print(payload_dict[self.dev_type][command]['prefix'])
|
|
||||||
#print(repr(buffer))
|
|
||||||
#print(bin2hex(buffer, pretty=False))
|
|
||||||
#print('full buffer(%d) %r' % (len(buffer), bin2hex(buffer, pretty=True) ))
|
#print('full buffer(%d) %r' % (len(buffer), bin2hex(buffer, pretty=True) ))
|
||||||
#print('full buffer(%d) %r' % (len(buffer), " ".join("{:02x}".format(ord(c)) for c in buffer)))
|
#print('full buffer(%d) %r' % (len(buffer), " ".join("{:02x}".format(ord(c)) for c in buffer)))
|
||||||
return buffer
|
return buffer
|
||||||
@@ -359,9 +381,6 @@ class Device(XenonDevice):
|
|||||||
else:
|
else:
|
||||||
log.error('Unexpected status() payload=%r', result)
|
log.error('Unexpected status() payload=%r', result)
|
||||||
|
|
||||||
# if self.dev_type == 'cover':
|
|
||||||
# result = result.encode('utf-8')
|
|
||||||
# log.debug('encoded result=%r', result)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def set_status(self, on, switch=1):
|
def set_status(self, on, switch=1):
|
||||||
|
Reference in New Issue
Block a user