From 4dbef2570ca06327e6e0b0ebb0e73b42a091adc9 Mon Sep 17 00:00:00 2001 From: George G <11027390+george43g@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:20:41 +1100 Subject: [PATCH 1/4] Add .python-version to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cdb93cd --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.python-version From 8ea91beb1b0098f806dfaa8e24431d682f694833 Mon Sep 17 00:00:00 2001 From: George G <11027390+george43g@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:23:38 +1100 Subject: [PATCH 2/4] Fix shebang and update API URLs for Tapo device support --- tapo-cli.py | 50 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/tapo-cli.py b/tapo-cli.py index 3a0aa8a..fde40fd 100755 --- a/tapo-cli.py +++ b/tapo-cli.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 # DO NOT RUN ANY OF THIS CODE UNLESS YOU UNDERSTAND WHAT IT DOES # I TAKE NO RESPONSIBILITY FOR ANYTHING, USE ON YOUR OWN RISK @@ -25,6 +25,16 @@ access_key = '4d11b6b9d5ea4d19a829adbb9714b057' secret = '6ed7d97f3e73467f8a5bab90b577ba4c' +api_url = 'https://aps1-app-tapo-care.i.tplinknbu.com' +api_url2 = 'http://use1-relay-dcipc.i.tplinknbu.com' +api_url3 = "http://aps1-relay-dcipc-beta.i.tplinknbu.com" +api_url4 = 'http://euw1-relay-dcipc.i.tplinknbu.com' +api_url5 = 'http://aps1-relay-dcipc.i.tplinknbu.com' +api_url6 = 'http://aps1-relay-dcipc-beta.i.tplinknbu.com' + +# 'https://aps1-wap.tplinkcloud.com' +# 'https://eu-wap.tplinkcloud.com/' + # Every request needs a uuid nonce and time, any value seems to work but let's not raise any suspicions. nonce = str(uuid.uuid1()) now = str(int(time.time())) @@ -56,7 +66,7 @@ def get_config(): email = config['email'] app_server_url_post = config['appServerUrl'] - return token, email, app_server_url_post, 'https://euw1-app-tapo-care.i.tplinknbu.com' + return token, email, app_server_url_post, api_url except: print('Please login first.') exit(1) @@ -146,7 +156,7 @@ def login(username, password): res = post(url, content, headers_post(content, '/api/v2/account/login')) if (res['error_code'] != 0): error(res) - + config = json.dumps(res['result'], indent = 4) # Login but with extra steps @@ -157,11 +167,11 @@ def login(username, password): content = json.dumps(content) res = post(url, content, headers_post(content, '/api/v2/account/getPushVC4TerminalMFA')) if (res['error_code'] != 0): - error(res) + error(res) print('Check your Tapo App for the MFA code!') mfa_code = str(input('MFA Code (no spaces or dashes): ')) - + url = 'https://n-wap-gw.tplinkcloud.com/api/v2/account/checkMFACodeAndLogin' content = {"appType":"TP-Link_Tapo_Android","cloudUserName":username,"code":mfa_code,"MFAProcessId":mfa_process_id,"MFAType":1,"terminalBindEnabled":True} content = json.dumps(content) @@ -200,7 +210,7 @@ def devices(): """Lists your first 20 Tapo devices.""" get_config() # Checks if logged in endpoint = '/api/v2/common/getDeviceListByPage' - content = '{"deviceTypeList":["SMART.TAPOPLUG","SMART.TAPOBULB","SMART.IPCAMERA","SMART.TAPOROBOVAC","SMART.TAPOHUB","SMART.TAPOSENSOR","SMART.TAPOSWITCH"],"index":0,"limit":20}' + content = '{"deviceTypeList":["SMART.TAPOPLUG","SMART.TAPOBULB","SMART.IPCAMERA","SMART.TAPOROBOVAC","SMART.TAPOHUB","SMART.TAPOSENSOR","SMART.TAPOSWITCH","SMART.TAPODOORBELL"],"index":0,"limit":20}' res = probe_endpoint_post(content, endpoint) print(json.dumps(res, indent = 4)) @@ -209,7 +219,7 @@ def devices_limit(): """Lists the device limits for your account by device type.""" get_config() # Checks if logged in endpoint = '/api/v2/common/batchGetDeviceUserNumberLimit' - content = '{"deviceTypeList":["SMART.TAPOPLUG","SMART.TAPOBULB","SMART.IPCAMERA","SMART.TAPOROBOVAC","SMART.TAPOHUB","SMART.TAPOSENSOR","SMART.TAPOSWITCH"]}' + content = '{"deviceTypeList":["SMART.TAPOPLUG","SMART.TAPOBULB","SMART.IPCAMERA","SMART.TAPOROBOVAC","SMART.TAPOHUB","SMART.TAPOSENSOR","SMART.TAPOSWITCH","SMART.TAPODOORBELL"]}' res = probe_endpoint_post(content, endpoint) print(json.dumps(res, indent = 4)) @@ -218,7 +228,7 @@ def devices_info(): """Lists A LOT of parameters for your devices.""" get_config() # Checks if logged in endpoint = '/api/v2/common/getDeviceListByPage' - content = '{"deviceTypeList":["SMART.TAPOPLUG","SMART.TAPOBULB","SMART.IPCAMERA","SMART.TAPOROBOVAC","SMART.TAPOHUB","SMART.TAPOSENSOR","SMART.TAPOSWITCH"],"index":0,"limit":20}' + content = '{"deviceTypeList":["SMART.TAPOPLUG","SMART.TAPOBULB","SMART.IPCAMERA","SMART.TAPOROBOVAC","SMART.TAPOHUB","SMART.TAPOSENSOR","SMART.TAPOSWITCH","SMART.TAPODOORBELL"],"index":0,"limit":20}' devs = probe_endpoint_post(content, endpoint) endpoint = '/api/v2/common/passthrough' @@ -234,7 +244,7 @@ def devices_info(): @click.command() def service_urls(): """Lists URLs for various Tapo services.""" - get_config() # Checks if logged in + get_config() # Checks if logged in endpoint = '/api/v2/common/getAppServiceUrl' content = '{"serviceIds":["nbu.iot-app-server.app","nbu.iot-cloud-gateway.app","nbu.iot-security.appdevice","cipc.api"]}' res = probe_endpoint_post(content, endpoint) @@ -279,9 +289,9 @@ def list_videos(days): """Lists videos for the last X days.""" get_config() # Checks if logged in endpoint = '/api/v2/common/getDeviceListByPage' - content = '{"deviceTypeList":["SMART.IPCAMERA"],"index":0,"limit":20}' + content = '{"deviceTypeList":["SMART.IPCAMERA","SMART.TAPODOORBELL"],"index":0,"limit":20}' devs = probe_endpoint_post(content, endpoint) - + end_unixtime = time.time() + 86400 start_unixtime = end_unixtime - (days + 1) * 86400 end_time = datetime.datetime.utcfromtimestamp(end_unixtime).strftime('%Y-%m-%d 00:00:00') @@ -291,6 +301,9 @@ def list_videos(days): for dev in devs['deviceList']: params = 'deviceId=' + dev['deviceId'] + '&page=0&pageSize=3000&order=desc&startTime=' + start_time + '&endTime=' + end_time videos = probe_endpoint_get(params, endpoint) + if 'total' not in videos: + print('\nError getting videos for ' + dev['alias'] + ': ' + str(videos)) + continue print('\nFound ' + str(videos['total']) + ' videos for ' + dev['alias'] + ':') if 'index' in videos: for video in videos['index']: @@ -305,14 +318,14 @@ def list_videos(days): def download_videos(days, path, overwrite): """Downloads videos for the last X days to path.""" get_config() # Checks if logged in - + path = path if path[-1] == '/' else path + '/' path = os.path.expanduser(path) - + endpoint = '/api/v2/common/getDeviceListByPage' - content = '{"deviceTypeList":["SMART.IPCAMERA"],"index":0,"limit":20}' + content = '{"deviceTypeList":["SMART.IPCAMERA","SMART.TAPODOORBELL"],"index":0,"limit":20}' devs = probe_endpoint_post(content, endpoint) - + end_unixtime = time.time() + 86400 start_unixtime = end_unixtime - (days + 1) * 86400 end_time = datetime.datetime.utcfromtimestamp(end_unixtime).strftime('%Y-%m-%d 00:00:00') @@ -323,6 +336,9 @@ def download_videos(days, path, overwrite): for dev in devs['deviceList']: params = 'deviceId=' + dev['deviceId'] + '&page=0&pageSize=3000&order=desc&startTime=' + start_time + '&endTime=' + end_time videos = probe_endpoint_get(params, endpoint) + if 'total' not in videos: + print('\nError getting videos for ' + dev['alias'] + ': ' + str(videos)) + continue print('\nFound ' + str(videos['total']) + ' videos for ' + dev['alias'] + ':') if 'index' in videos: for video in videos['index']: @@ -338,11 +354,11 @@ def download_videos(days, path, overwrite): exit(1) key_b64 = video['video'][0]['decryptionInfo']['key'] - + file_path = path + dev['alias'] + '/' + datetime.datetime.strptime(video['eventLocalTime'], '%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') + '/' file_name = video['eventLocalTime'].replace(':','-') + '.mp4' if os.path.exists(file_path + file_name) and overwrite == 0: - print('Already exists ' + file_path + file_name) + print('Already exists ' + file_path + file_name) result.append({'file': file_path + file_name, 'device': dev['alias'], 'new_video': False, 'video': video}) else: print('Downloading to ' + file_path + file_name) From 700508fe61b1570b86a04b15aa406d2c115d5b82 Mon Sep 17 00:00:00 2001 From: George G <11027390+george43g@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:36:45 +1100 Subject: [PATCH 3/4] Add region handling for API URLs and refactor device type lists --- tapo-cli.py | 51 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/tapo-cli.py b/tapo-cli.py index fde40fd..bdab877 100755 --- a/tapo-cli.py +++ b/tapo-cli.py @@ -18,6 +18,7 @@ import time import json import datetime +import re from Crypto.Cipher import AES from Crypto.Util.Padding import unpad @@ -25,15 +26,21 @@ access_key = '4d11b6b9d5ea4d19a829adbb9714b057' secret = '6ed7d97f3e73467f8a5bab90b577ba4c' -api_url = 'https://aps1-app-tapo-care.i.tplinknbu.com' -api_url2 = 'http://use1-relay-dcipc.i.tplinknbu.com' -api_url3 = "http://aps1-relay-dcipc-beta.i.tplinknbu.com" -api_url4 = 'http://euw1-relay-dcipc.i.tplinknbu.com' -api_url5 = 'http://aps1-relay-dcipc.i.tplinknbu.com' -api_url6 = 'http://aps1-relay-dcipc-beta.i.tplinknbu.com' - -# 'https://aps1-wap.tplinkcloud.com' -# 'https://eu-wap.tplinkcloud.com/' +DEVICE_TYPES_ALL = [ + "SMART.TAPOPLUG", + "SMART.TAPOBULB", + "SMART.IPCAMERA", + "SMART.TAPOROBOVAC", + "SMART.TAPOHUB", + "SMART.TAPOSENSOR", + "SMART.TAPOSWITCH", + "SMART.TAPODOORBELL" +] + +DEVICE_TYPES_CAMERA = [ + "SMART.IPCAMERA", + "SMART.TAPODOORBELL" +] # Every request needs a uuid nonce and time, any value seems to work but let's not raise any suspicions. nonce = str(uuid.uuid1()) @@ -66,6 +73,22 @@ def get_config(): email = config['email'] app_server_url_post = config['appServerUrl'] + # Derive region from appServerUrl + # e.g. https://n-aps1-wap-gw.tplinkcloud.com -> aps1 + # e.g. https://n-euw1-wap-gw.tplinkcloud.com -> euw1 + region_match = re.search(r'https://n-([a-z0-9]+)-wap-gw', app_server_url_post) + if region_match: + region = region_match.group(1) + api_url = f'https://{region}-app-tapo-care.i.tplinknbu.com' + else: + # Fallback to a default if regex fails, or check if it's eu-wap + if 'eu-wap' in app_server_url_post: + api_url = 'https://euw1-app-tapo-care.i.tplinknbu.com' + else: + # Default to aps1 or raise error? + # Defaulting to aps1 as it seems to be common for non-EU + api_url = 'https://aps1-app-tapo-care.i.tplinknbu.com' + return token, email, app_server_url_post, api_url except: print('Please login first.') @@ -210,7 +233,7 @@ def devices(): """Lists your first 20 Tapo devices.""" get_config() # Checks if logged in endpoint = '/api/v2/common/getDeviceListByPage' - content = '{"deviceTypeList":["SMART.TAPOPLUG","SMART.TAPOBULB","SMART.IPCAMERA","SMART.TAPOROBOVAC","SMART.TAPOHUB","SMART.TAPOSENSOR","SMART.TAPOSWITCH","SMART.TAPODOORBELL"],"index":0,"limit":20}' + content = json.dumps({"deviceTypeList": DEVICE_TYPES_ALL, "index": 0, "limit": 20}) res = probe_endpoint_post(content, endpoint) print(json.dumps(res, indent = 4)) @@ -219,7 +242,7 @@ def devices_limit(): """Lists the device limits for your account by device type.""" get_config() # Checks if logged in endpoint = '/api/v2/common/batchGetDeviceUserNumberLimit' - content = '{"deviceTypeList":["SMART.TAPOPLUG","SMART.TAPOBULB","SMART.IPCAMERA","SMART.TAPOROBOVAC","SMART.TAPOHUB","SMART.TAPOSENSOR","SMART.TAPOSWITCH","SMART.TAPODOORBELL"]}' + content = json.dumps({"deviceTypeList": DEVICE_TYPES_ALL}) res = probe_endpoint_post(content, endpoint) print(json.dumps(res, indent = 4)) @@ -228,7 +251,7 @@ def devices_info(): """Lists A LOT of parameters for your devices.""" get_config() # Checks if logged in endpoint = '/api/v2/common/getDeviceListByPage' - content = '{"deviceTypeList":["SMART.TAPOPLUG","SMART.TAPOBULB","SMART.IPCAMERA","SMART.TAPOROBOVAC","SMART.TAPOHUB","SMART.TAPOSENSOR","SMART.TAPOSWITCH","SMART.TAPODOORBELL"],"index":0,"limit":20}' + content = json.dumps({"deviceTypeList": DEVICE_TYPES_ALL, "index": 0, "limit": 20}) devs = probe_endpoint_post(content, endpoint) endpoint = '/api/v2/common/passthrough' @@ -289,7 +312,7 @@ def list_videos(days): """Lists videos for the last X days.""" get_config() # Checks if logged in endpoint = '/api/v2/common/getDeviceListByPage' - content = '{"deviceTypeList":["SMART.IPCAMERA","SMART.TAPODOORBELL"],"index":0,"limit":20}' + content = json.dumps({"deviceTypeList": DEVICE_TYPES_CAMERA, "index": 0, "limit": 20}) devs = probe_endpoint_post(content, endpoint) end_unixtime = time.time() + 86400 @@ -323,7 +346,7 @@ def download_videos(days, path, overwrite): path = os.path.expanduser(path) endpoint = '/api/v2/common/getDeviceListByPage' - content = '{"deviceTypeList":["SMART.IPCAMERA","SMART.TAPODOORBELL"],"index":0,"limit":20}' + content = json.dumps({"deviceTypeList": DEVICE_TYPES_CAMERA, "index": 0, "limit": 20}) devs = probe_endpoint_post(content, endpoint) end_unixtime = time.time() + 86400 From 4a1d1c69eecddb8ee5177827d7580d029463b03b Mon Sep 17 00:00:00 2001 From: George G <11027390+george43g@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:44:47 +1100 Subject: [PATCH 4/4] Add .editorconfig for consistent coding styles --- .editorconfig | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c1322dc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file