Skip to content
This repository has been archived by the owner on Sep 4, 2021. It is now read-only.

Commit

Permalink
Merge pull request #56 from belzedaar/master
Browse files Browse the repository at this point in the history
Add support for version 3.3 protocol, retaining 3.1 support.
  • Loading branch information
clach04 authored Jul 4, 2019
2 parents 3ee53b8 + bee5b97 commit 765f359
Showing 1 changed file with 42 additions and 14 deletions.
56 changes: 42 additions & 14 deletions pytuya/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import sys
import time
import colorsys
import binascii

try:
#raise ImportError
Expand All @@ -27,7 +28,7 @@
import pyaes # https://github.com/ricmoo/pyaes


version_tuple = (7, 0, 3)
version_tuple = (7, 0, 4)
version = version_string = __version__ = '%d.%d.%d' % version_tuple
__author__ = 'clach04'

Expand All @@ -45,8 +46,10 @@
log.info('Using PyCrypto from %r', Crypto.__file__)

SET = 'set'
STATUS = 'status'

PROTOCOL_VERSION_BYTES = b'3.1'
PROTOCOL_VERSION_BYTES_31 = b'3.1'
PROTOCOL_VERSION_BYTES_33 = b'3.3'

IS_PY2 = sys.version_info[0] == 2

Expand All @@ -55,7 +58,7 @@ 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.key = key
def encrypt(self, raw):
def encrypt(self, raw, use_base64 = True):
if Crypto:
raw = self._pad(raw)
cipher = AES.new(self.key, mode=AES.MODE_ECB)
Expand All @@ -67,11 +70,14 @@ def encrypt(self, raw):
crypted_text += cipher.feed() # flush final block
#print('crypted_text %r' % crypted_text)
#print('crypted_text (%d) %r' % (len(crypted_text), crypted_text))
crypted_text_b64 = base64.b64encode(crypted_text)
#print('crypted_text_b64 (%d) %r' % (len(crypted_text_b64), crypted_text_b64))
return crypted_text_b64
def decrypt(self, enc):
enc = base64.b64decode(enc)
if use_base64:
return base64.b64encode(crypted_text)
else:
return crypted_text

def decrypt(self, enc, use_base64=True):
if use_base64:
enc = base64.b64decode(enc)
#print('enc (%d) %r' % (len(enc), enc))
#enc = self._unpad(enc)
#enc = self._pad(enc)
Expand Down Expand Up @@ -151,6 +157,7 @@ def __init__(self, dev_id, address, local_key=None, dev_type=None, connection_ti
self.local_key = local_key.encode('latin1')
self.dev_type = dev_type
self.connection_timeout = connection_timeout
self.version = 3.1

self.port = 6668 # default - do not expect caller to pass in

Expand All @@ -173,6 +180,9 @@ def _send_receive(self, payload):
s.close()
return data

def set_version(self, version):
self.version = version

def generate_payload(self, command, data=None):
"""
Generate the payload to send.
Expand Down Expand Up @@ -204,21 +214,28 @@ def generate_payload(self, command, data=None):
json_payload = json_payload.encode('utf-8')
log.debug('json_payload=%r', json_payload)

if command == SET:
if self.version == 3.3:
self.cipher = AESCipher(self.local_key) # expect to connect and then disconnect to set new
json_payload = self.cipher.encrypt(json_payload, False)
self.cipher = None
if command != STATUS:
# add the 3.3 header
json_payload = PROTOCOL_VERSION_BYTES_33 + b"\0\0\0\0\0\0\0\0\0\0\0\0" + json_payload
elif command == SET:
# need to encrypt
#print('json_payload %r' % json_payload)
self.cipher = AESCipher(self.local_key) # expect to connect and then disconnect to set new
json_payload = self.cipher.encrypt(json_payload)
#print('crypted json_payload %r' % json_payload)
preMd5String = b'data=' + json_payload + b'||lpv=' + PROTOCOL_VERSION_BYTES + 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.update(preMd5String)
#print(repr(m.digest()))
hexdigest = m.hexdigest()
#print(hexdigest)
#print(hexdigest[8:][:16])
json_payload = PROTOCOL_VERSION_BYTES + 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))
Expand All @@ -239,13 +256,17 @@ def generate_payload(self, command, data=None):
payload_dict[self.dev_type][command]['hexByte'] +
'000000' +
postfix_payload_hex_len ) + postfix_payload

# calc the CRC of everything except where the CRC goes and the suffix
hex_crc = format(binascii.crc32(buffer[:-8]) & 0xffffffff, '08X')
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=True))
#print(bin2hex(buffer, pretty=False))
#print('full buffer(%d) %r' % (len(buffer), buffer))
#print('full buffer(%d) %r' % (len(buffer), " ".join("{:02x}".format(ord(c)) for c in buffer)))
return buffer

class Device(XenonDevice):
Expand All @@ -269,18 +290,25 @@ def status(self):
if not isinstance(result, str):
result = result.decode()
result = json.loads(result)
elif result.startswith(PROTOCOL_VERSION_BYTES):
elif result.startswith(PROTOCOL_VERSION_BYTES_31):
# got an encrypted payload, happens occasionally
# expect resulting json to look similar to:: {"devId":"ID","dps":{"1":true,"2":0},"t":EPOCH_SECS,"s":3_DIGIT_NUM}
# NOTE dps.2 may or may not be present
result = result[len(PROTOCOL_VERSION_BYTES):] # remove version header
result = result[len(PROTOCOL_VERSION_BYTES_31):] # remove version header
result = result[16:] # remove (what I'm guessing, but not confirmed is) 16-bytes of MD5 hexdigest of payload
cipher = AESCipher(self.local_key)
result = cipher.decrypt(result)
log.debug('decrypted result=%r', result)
if not isinstance(result, str):
result = result.decode()
result = json.loads(result)
elif self.version == 3.3:
cipher = AESCipher(self.local_key)
result = cipher.decrypt(result, False)
log.debug('decrypted result=%r', result)
if not isinstance(result, str):
result = result.decode()
result = json.loads(result)
else:
log.error('Unexpected status() payload=%r', result)

Expand Down

0 comments on commit 765f359

Please # to comment.