Skip to content

Commit

Permalink
Add auto-find IP Address and fix #17
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonacox committed Jan 13, 2021
1 parent fa6dcf6 commit 99d8ac8
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 11 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Classes
BulbDevice(dev_id, address, local_key=None, dev_type='default')
dev_id (str): Device ID e.g. 01234567891234567890
address (str): Device Network IP Address e.g. 10.0.1.99
address (str): Device Network IP Address e.g. 10.0.1.99 or 0.0.0.0 to auto-find
local_key (str, optional): The encryption key. Defaults to None.
dev_type (str): Device type for payload options (see below)
Expand All @@ -88,9 +88,10 @@ Classes
set_retry(retry=True) # retry if response payload is truncated
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):
turn_on(switch=1)
turn_off(switch=1)
set_timer(num_secs)
heartbeat() # Send Tuya Heartbeat
CoverDevice:
open_cover(switch=1):
Expand Down
114 changes: 107 additions & 7 deletions tinytuya/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
Crypto = AES = None
import pyaes # https://github.com/ricmoo/pyaes

version_tuple = (1, 1, 2)
version_tuple = (1, 1, 3)
version = __version__ = '%d.%d.%d' % version_tuple
__author__ = 'jasonacox'

Expand Down Expand Up @@ -227,7 +227,7 @@ def hex2bin(x):
},
HEART_BEAT: {
"hexByte": "09",
"command": {}
"command": {"gwId": "", "devId": ""}
},
DP_QUERY: { # Get Data Points from Device
"hexByte": "0a",
Expand Down Expand Up @@ -261,7 +261,7 @@ def hex2bin(x):
},
HEART_BEAT: {
"hexByte": "09",
"command": {}
"command": {"gwId": "", "devId": ""}
},
"prefix": "000055aa00000000000000",
"suffix": "000000000000aa55"
Expand Down Expand Up @@ -296,10 +296,19 @@ def __init__(self, dev_id, address, local_key="", dev_type="default", connection
self.socketPersistent = False
self.socketNODELAY = True
self.socketRetryLimit = 5
if(address == None or address == 'Auto' or address == '0.0.0.0'):
# try to determine IP address automatically
(addr, ver) = self.find(dev_id)
if(addr == None):
raise Exception('Unable to find device on network (specify IP address)')
self.address = addr
if(ver == "3.3"):
self.version = 3.3

def __del__(self):
# In case we have a lingering socket connection, close it
if self.socket != None:
# self.socket.shutdown(socket.SHUT_RDWR)
self.socket.close()
self.socket = None

Expand All @@ -309,6 +318,7 @@ def __repr__(self):

def _get_socket(self, renew):
if(renew and self.socket != None):
# self.socket.shutdown(socket.SHUT_RDWR)
self.socket.close()
self.socket = None
if(self.socket == None):
Expand All @@ -335,8 +345,10 @@ def _send_receive(self, payload):
self.socket.send(payload)
data = self.socket.recv(1024)
# Some devices fail to send full payload in first response
# Note - some devices respond with len = 28 for error response
if self.retry and len(data) < 28:
# At minimum requires: prefix (4), sequence (4), command (4), length (4),
# CRC (4), and suffix (4) for 24 total bytes
# Messages from the device also include return code (4), for 28 total bytes
if self.retry and len(data) <= 28:
time.sleep(0.1)
data = self.socket.recv(1024) # try again
success = True
Expand All @@ -362,6 +374,7 @@ def _send_receive(self, payload):
self._get_socket(True)
# except
# while
# signal we are done reading
return data

def set_version(self, version):
Expand All @@ -381,6 +394,80 @@ def set_dpsUsed(self, dpsUsed):

def set_retry(self, retry):
self.retry = retry

def find(self, did=None):
"""Scans network for Tuya devices with ID = did
Parameters:
did = The specific Device ID you are looking for (returns only IP and Version)
Response:
(ip, version)
"""
if(did == None):
return(None, None)
# Enable UDP listening broadcasting mode on UDP port 6666 - 3.1 Devices
client = socket.socket(
socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
client.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
client.bind(("", UDPPORT))
client.settimeout(TIMEOUT)
# Enable UDP listening broadcasting mode on encrypted UDP port 6667 - 3.3 Devices
clients = socket.socket(
socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
clients.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
clients.bind(("", UDPPORTS))
clients.settimeout(TIMEOUT)

count = 0
counts = 0
maxretry = 30
ret = (None, None)

while (count + counts) <= maxretry:
if (count <= counts): # alternate between 6666 and 6667 ports
try:
data, addr = client.recvfrom(4048)
count = count + 1
except:
# Timeout
count = count + 1
continue
else:
try:
data, addr = clients.recvfrom(4048)
counts = counts + 1
except:
# Timeout
counts = counts + 1
continue
ip = addr[0]
gwId = version = ""
result = data
try:
result = data[20:-8]
try:
result = decrypt_udp(result)
except:
result = result.decode()

result = json.loads(result)
ip = result['ip']
gwId = result['gwId']
version = result['version']
except:
result = {"ip": ip}

# Check to see if we are only looking for one device
if(gwId == did):
# We found it!
ret = (ip, version)
break

# while
clients.close()
client.close()
return(ret)

def generate_payload(self, command, data=None):
"""
Expand Down Expand Up @@ -438,7 +525,7 @@ def generate_payload(self, command, data=None):
# some tuya libraries strip 8: to :24
json_payload = PROTOCOL_VERSION_BYTES_31 + \
hexdigest[8:][:16].encode('latin1') + json_payload
self.cipher = None # expect to connect and then disconnect to set new
self.cipher = None

postfix_payload = hex2bin(
bin2hex(json_payload) + payload_dict[self.dev_type]['suffix'])
Expand Down Expand Up @@ -484,7 +571,7 @@ def status(self):
# 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_31) :] # remove version header
result = result[len(PROTOCOL_VERSION_BYTES_31):] # remove version header
# Remove 16-bytes appears to be MD5 hexdigest of payload
result = result[16:]
cipher = AESCipher(self.local_key)
Expand Down Expand Up @@ -523,6 +610,17 @@ def set_status(self, on, switch=1):

return data

def heartbeat(self):
"""
Send a simple HEART_BEAT command to device.
"""
# open device, send request, then close connection
payload = self.generate_payload(HEART_BEAT)
data = self._send_receive(payload)
log.debug('heartbeat received data=%r', data)
return data

def set_value(self, index, value):
"""
Set int value of any index.
Expand Down Expand Up @@ -1274,6 +1372,8 @@ def tuyaLookup(deviceid):
print(" \n%sScan Complete! Found %s devices.\n" %
(normal, len(devices)))

clients.close()
client.close()
return(devices)


Expand Down

0 comments on commit 99d8ac8

Please sign in to comment.