8686 Crypto = AES = None
8787 import pyaes # https://github.com/ricmoo/pyaes
8888
89- version_tuple = (1 , 1 , 2 )
89+ version_tuple = (1 , 1 , 3 )
9090version = __version__ = '%d.%d.%d' % version_tuple
9191__author__ = 'jasonacox'
9292
@@ -227,7 +227,7 @@ def hex2bin(x):
227227 },
228228 HEART_BEAT : {
229229 "hexByte" : "09" ,
230- "command" : {}
230+ "command" : {"gwId" : "" , "devId" : "" }
231231 },
232232 DP_QUERY : { # Get Data Points from Device
233233 "hexByte" : "0a" ,
@@ -261,7 +261,7 @@ def hex2bin(x):
261261 },
262262 HEART_BEAT : {
263263 "hexByte" : "09" ,
264- "command" : {}
264+ "command" : {"gwId" : "" , "devId" : "" }
265265 },
266266 "prefix" : "000055aa00000000000000" ,
267267 "suffix" : "000000000000aa55"
@@ -296,10 +296,19 @@ def __init__(self, dev_id, address, local_key="", dev_type="default", connection
296296 self .socketPersistent = False
297297 self .socketNODELAY = True
298298 self .socketRetryLimit = 5
299+ if (address == None or address == 'Auto' or address == '0.0.0.0' ):
300+ # try to determine IP address automatically
301+ (addr , ver ) = self .find (dev_id )
302+ if (addr == None ):
303+ raise Exception ('Unable to find device on network (specify IP address)' )
304+ self .address = addr
305+ if (ver == "3.3" ):
306+ self .version = 3.3
299307
300308 def __del__ (self ):
301309 # In case we have a lingering socket connection, close it
302310 if self .socket != None :
311+ # self.socket.shutdown(socket.SHUT_RDWR)
303312 self .socket .close ()
304313 self .socket = None
305314
@@ -309,6 +318,7 @@ def __repr__(self):
309318
310319 def _get_socket (self , renew ):
311320 if (renew and self .socket != None ):
321+ # self.socket.shutdown(socket.SHUT_RDWR)
312322 self .socket .close ()
313323 self .socket = None
314324 if (self .socket == None ):
@@ -335,8 +345,10 @@ def _send_receive(self, payload):
335345 self .socket .send (payload )
336346 data = self .socket .recv (1024 )
337347 # Some devices fail to send full payload in first response
338- # Note - some devices respond with len = 28 for error response
339- if self .retry and len (data ) < 28 :
348+ # At minimum requires: prefix (4), sequence (4), command (4), length (4),
349+ # CRC (4), and suffix (4) for 24 total bytes
350+ # Messages from the device also include return code (4), for 28 total bytes
351+ if self .retry and len (data ) <= 28 :
340352 time .sleep (0.1 )
341353 data = self .socket .recv (1024 ) # try again
342354 success = True
@@ -362,6 +374,7 @@ def _send_receive(self, payload):
362374 self ._get_socket (True )
363375 # except
364376 # while
377+ # signal we are done reading
365378 return data
366379
367380 def set_version (self , version ):
@@ -381,6 +394,80 @@ def set_dpsUsed(self, dpsUsed):
381394
382395 def set_retry (self , retry ):
383396 self .retry = retry
397+
398+ def find (self , did = None ):
399+ """Scans network for Tuya devices with ID = did
400+
401+ Parameters:
402+ did = The specific Device ID you are looking for (returns only IP and Version)
403+
404+ Response:
405+ (ip, version)
406+ """
407+ if (did == None ):
408+ return (None , None )
409+ # Enable UDP listening broadcasting mode on UDP port 6666 - 3.1 Devices
410+ client = socket .socket (
411+ socket .AF_INET , socket .SOCK_DGRAM , socket .IPPROTO_UDP )
412+ client .setsockopt (socket .SOL_SOCKET , socket .SO_BROADCAST , 1 )
413+ client .bind (("" , UDPPORT ))
414+ client .settimeout (TIMEOUT )
415+ # Enable UDP listening broadcasting mode on encrypted UDP port 6667 - 3.3 Devices
416+ clients = socket .socket (
417+ socket .AF_INET , socket .SOCK_DGRAM , socket .IPPROTO_UDP )
418+ clients .setsockopt (socket .SOL_SOCKET , socket .SO_BROADCAST , 1 )
419+ clients .bind (("" , UDPPORTS ))
420+ clients .settimeout (TIMEOUT )
421+
422+ count = 0
423+ counts = 0
424+ maxretry = 30
425+ ret = (None , None )
426+
427+ while (count + counts ) <= maxretry :
428+ if (count <= counts ): # alternate between 6666 and 6667 ports
429+ try :
430+ data , addr = client .recvfrom (4048 )
431+ count = count + 1
432+ except :
433+ # Timeout
434+ count = count + 1
435+ continue
436+ else :
437+ try :
438+ data , addr = clients .recvfrom (4048 )
439+ counts = counts + 1
440+ except :
441+ # Timeout
442+ counts = counts + 1
443+ continue
444+ ip = addr [0 ]
445+ gwId = version = ""
446+ result = data
447+ try :
448+ result = data [20 :- 8 ]
449+ try :
450+ result = decrypt_udp (result )
451+ except :
452+ result = result .decode ()
453+
454+ result = json .loads (result )
455+ ip = result ['ip' ]
456+ gwId = result ['gwId' ]
457+ version = result ['version' ]
458+ except :
459+ result = {"ip" : ip }
460+
461+ # Check to see if we are only looking for one device
462+ if (gwId == did ):
463+ # We found it!
464+ ret = (ip , version )
465+ break
466+
467+ # while
468+ clients .close ()
469+ client .close ()
470+ return (ret )
384471
385472 def generate_payload (self , command , data = None ):
386473 """
@@ -438,7 +525,7 @@ def generate_payload(self, command, data=None):
438525 # some tuya libraries strip 8: to :24
439526 json_payload = PROTOCOL_VERSION_BYTES_31 + \
440527 hexdigest [8 :][:16 ].encode ('latin1' ) + json_payload
441- self .cipher = None # expect to connect and then disconnect to set new
528+ self .cipher = None
442529
443530 postfix_payload = hex2bin (
444531 bin2hex (json_payload ) + payload_dict [self .dev_type ]['suffix' ])
@@ -484,7 +571,7 @@ def status(self):
484571 # got an encrypted payload, happens occasionally
485572 # expect resulting json to look similar to:: {"devId":"ID","dps":{"1":true,"2":0},"t":EPOCH_SECS,"s":3_DIGIT_NUM}
486573 # NOTE dps.2 may or may not be present
487- result = result [len (PROTOCOL_VERSION_BYTES_31 ) :] # remove version header
574+ result = result [len (PROTOCOL_VERSION_BYTES_31 ):] # remove version header
488575 # Remove 16-bytes appears to be MD5 hexdigest of payload
489576 result = result [16 :]
490577 cipher = AESCipher (self .local_key )
@@ -523,6 +610,17 @@ def set_status(self, on, switch=1):
523610
524611 return data
525612
613+ def heartbeat (self ):
614+ """
615+ Send a simple HEART_BEAT command to device.
616+
617+ """
618+ # open device, send request, then close connection
619+ payload = self .generate_payload (HEART_BEAT )
620+ data = self ._send_receive (payload )
621+ log .debug ('heartbeat received data=%r' , data )
622+ return data
623+
526624 def set_value (self , index , value ):
527625 """
528626 Set int value of any index.
@@ -1274,6 +1372,8 @@ def tuyaLookup(deviceid):
12741372 print (" \n %sScan Complete! Found %s devices.\n " %
12751373 (normal , len (devices )))
12761374
1375+ clients .close ()
1376+ client .close ()
12771377 return (devices )
12781378
12791379
0 commit comments