diff --git a/tinytuya/Cloud.py b/tinytuya/Cloud.py index 042b83f5..6350fd60 100644 --- a/tinytuya/Cloud.py +++ b/tinytuya/Cloud.py @@ -18,7 +18,7 @@ getfunctions(deviceid) getproperties(deviceid) getdps(deviceid) - sendcommand(deviceid, commands) + sendcommand(deviceid, commands [, uri]) 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" @@ -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: @@ -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 @@ -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'] @@ -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 + 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 @@ -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 diff --git a/tinytuya/wizard.py b/tinytuya/wizard.py index 4cf9bf4f..121a0e35 100644 --- a/tinytuya/wizard.py +++ b/tinytuya/wizard.py @@ -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 @@ -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 = '' if not config['apiDeviceID'] else config['apiDeviceID'] + apiDeviceID = '' 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 @@ -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']: @@ -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: + 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 = {}