Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch the device list a 2nd time to make sure we have the local key #306

Merged
merged 2 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 60 additions & 6 deletions tinytuya/Cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
getfunctions(deviceid)
getproperties(deviceid)
getdps(deviceid)
sendcommand(deviceid, commands)
sendcommand(deviceid, commands [, uri])
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

getconnectstatus(deviceid)
getdevicelog(deviceid, start=[now - 1 day], end=[now], evtype="1,2,3,4,5,6,7,8,9,10", size=100, params={})
-> when start or end are negative, they are the number of days before "right now"
Expand Down Expand Up @@ -213,6 +213,9 @@ def _tuyaplatform(self, uri, action='GET', post=None, ver='v1.0', recursive=Fals
"POST: URL=%s HEADERS=%s DATA=%s", url, headers, body,
)
response = requests.post(url, headers=headers, data=body)
log.debug(
"POST RESPONSE: code=%d text=%s token=%s", response.status_code, response.text, self.token
)

# Check to see if token is expired
if "token invalid" in response.text:
Expand Down Expand Up @@ -307,17 +310,48 @@ def cloudrequest(self, url, action=None, post=None, query=None):
action = 'POST' if post else 'GET'
return self._tuyaplatform(url, action=action, post=post, ver=None, query=query)

def _get_all_devices(self):
# merge device list 'result2' into 'result1'
# if result2 has a device which is not in result1 then it will be added
# if result2 has a key which does not exist or is empty in result1 then that key will be copied over
def _update_device_list( self, result1, result2 ):
for new_device in result2:
if 'id' not in new_device or not new_device['id']:
continue
found = False
for existing_device in result1:
if 'id' in existing_device and existing_device['id'] == new_device['id']:
found = True
for k in new_device:
if k not in existing_device or not existing_device[k]:
existing_device[k] = new_device[k]
if not found:
result1.append( new_device )

def _get_all_devices( self, uid=None, device_ids=None ):
fetches = 0
our_result = { 'result': [] }
last_row_key = None
has_more = True
total = 0
query = {'size':'50'}

while has_more:
if uid:
# get device list for specified user id
query = {'page_size':'75', 'source_type': 'tuyaUser', 'source_id': uid}
# API docu: https://developer.tuya.com/en/docs/cloud/dc413408fe?id=Kc09y2ons2i3b
uri = '/v1.3/iot-03/devices'
if device_ids:
if isinstance( device_ids, tuple ) or isinstance( device_ids, list ):
query['device_ids'] = ','.join(device_ids)
else:
query['device_ids'] = device_ids
else:
# get all devices
query = {'size':'50'}
# API docu: https://developer.tuya.com/en/docs/cloud/fc19523d18?id=Kakr4p8nq5xsc
result = self.cloudrequest( '/v1.0/iot-01/associated-users/devices', query=query )
uri = '/v1.0/iot-01/associated-users/devices'

while has_more:
result = self.cloudrequest( uri, query=query )
fetches += 1
has_more = False

Expand All @@ -330,7 +364,12 @@ def _get_all_devices(self):
# format it the same as before, basically just moves result->devices into result
for i in result:
if i == 'result':
our_result[i] += result[i]['devices']
# by-user-id has the result in 'list' while all-devices has it in 'devices'
if 'list' in result[i] and 'devices' not in result[i]:
our_result[i] += result[i]['list']
elif 'devices' in result[i]:
our_result[i] += result[i]['devices']

if 'total' in result[i]: total = result[i]['total']
if 'last_row_key' in result[i]:
has_more = result[i]['has_more']
Expand Down Expand Up @@ -364,6 +403,17 @@ def getdevices(self, verbose=False):
json_data = self._tuyaplatform(uri)
else:
json_data = self._get_all_devices()
users = {}
# loop through all devices and build a list of user IDs
for dev in json_data['result']:
if 'uid' in dev:
users[dev['uid']] = True
if users:
# we have at least 1 user id, so fetch the device list again to make sure we have the local key
# this also gets us the gateway_id for child devices
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this!

for uid in users.keys():
json_data2 = self._get_all_devices( uid=uid )
self._update_device_list( json_data['result'], json_data2['result'] )

if verbose:
return json_data
Expand Down Expand Up @@ -422,6 +472,10 @@ def filter_devices( self, devs, ip_list=None ):
else:
item[k] = i[k]

if 'gateway_id' in i:
k = 'gateway_id'
item[k] = i[k]

tuyadevices.append(item)

return tuyadevices
Expand Down
36 changes: 26 additions & 10 deletions tinytuya/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
TCPTIMEOUT = tinytuya.TCPTIMEOUT # Seconds to wait for socket open for scanning
TCPPORT = tinytuya.TCPPORT # Tuya TCP Local Port

def wizard(color=True, retries=None, forcescan=False, nocloud=False):
def wizard(color=True, retries=None, forcescan=False, nocloud=False, quicklist=False):
"""
TinyTuya Setup Wizard Tuya based WiFi smart devices

Expand Down Expand Up @@ -94,16 +94,18 @@ def wizard(color=True, retries=None, forcescan=False, nocloud=False):
if (config['apiKey'] != '' and config['apiSecret'] != '' and
config['apiRegion'] != ''):
needconfigs = False
apiDeviceID = '<None>' if not config['apiDeviceID'] else config['apiDeviceID']
apiDeviceID = '<None>' if ('apiDeviceID' not in config or not config['apiDeviceID']) else config['apiDeviceID']
print(" " + subbold + "Existing settings:" + dim +
"\n API Key=%s \n Secret=%s\n DeviceID=%s\n Region=%s" %
(config['apiKey'], config['apiSecret'], apiDeviceID,
config['apiRegion']))
print('')
answer = input(subbold + ' Use existing credentials ' +
normal + '(Y/n): ')
if answer[0:1].lower() == 'n':
if quicklist:
needconfigs = True
else:
answer = input(subbold + ' Use existing credentials ' + normal + '(Y/n): ')
if answer[0:1].lower() == 'n':
needconfigs = True

if needconfigs:
# Ask user for config settings
Expand Down Expand Up @@ -157,17 +159,28 @@ def wizard(color=True, retries=None, forcescan=False, nocloud=False):
# Filter to only Name, ID and Key, IP and mac-address
tuyadevices = cloud.filter_devices( json_data['result'] )

# The device list does not tell us which device is the parent for a sub-device, so we need to try and figure it out
# The device list does not (always) tell us which device is the parent for a sub-device, so we need to try and figure it out
# The only link between parent and child appears to be the local key

# Result:
# if 'parent' not in device: device is not a sub-device
# if 'parent' in device: device is a sub-device
# if device['parent'] == '': device is a sub-device with an unknown parent
# else: device['parent'] == device_id of parent
for dev in tuyadevices:
if 'gateway_id' in dev:
# if the Cloud gave us the parent then just use that
if dev['gateway_id']:
dev['parent'] = dev['gateway_id']
del dev['gateway_id']

if 'sub' in dev and dev['sub']:
if 'parent' not in dev:
# Set 'parent' to an empty string in case we can't find it
dev['parent'] = ''
# no parent from cloud, try to find it via the local key
if 'parent' in dev and dev['parent']:
continue

# Set 'parent' to an empty string in case we can't find it
dev['parent'] = ''

# Only try to find the parent if the device has a local key
if 'key' in dev and dev['key']:
Expand Down Expand Up @@ -213,7 +226,10 @@ def wizard(color=True, retries=None, forcescan=False, nocloud=False):
print('\n\n' + bold + 'Unable to save raw file' + dim )

# Find out if we should poll all devices
answer = input(subbold + '\nPoll local devices? ' + normal + '(Y/n): ')
if quicklist:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

answer = 'n'
else:
answer = input(subbold + '\nPoll local devices? ' + normal + '(Y/n): ')
if answer.lower().find('n') < 0:
result = tinytuya.scanner.poll_and_display( tuyadevices, color=color, scantime=retries, snapshot=True, forcescan=forcescan )
iplist = {}
Expand Down