From d3be0ac645fc994f2f2830ab981c9d480871563f Mon Sep 17 00:00:00 2001 From: uzlonewolf Date: Sat, 22 Jun 2024 23:36:05 -0700 Subject: [PATCH 01/22] Fix scanner force-scanning --- tinytuya/scanner.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tinytuya/scanner.py b/tinytuya/scanner.py index 2c39a6e..697fb3d 100644 --- a/tinytuya/scanner.py +++ b/tinytuya/scanner.py @@ -1129,8 +1129,19 @@ def tuyaLookup(deviceid): if verbose: print(term.subbold + " Option: " + term.dim + "Network force scanning requested.\n") - fstype = type(forcescan) - if fstype != list and fstype != tuple: + add_connected = True + if isinstance( forcescan, list ) or isinstance( forcescan, tuple ): + # argparse gives us a list of lists [[]] when no address specified + for ip in forcescan: + if isinstance( ip, list ) or isinstance( ip, tuple ): + for ip2 in ip: + networks.append( ip2 ) + add_connected = False + else: + networks.append( ip ) + add_connected = False + + if add_connected: if not NETIFLIBS: print(term.alert + ' NOTE: netifaces module not available, multi-interface machines will be limited.\n' @@ -1151,9 +1162,6 @@ def tuyaLookup(deviceid): if not networks: print(term.alert + 'No networks to force-scan, exiting.' + term.normal) return None - else: - for ip in forcescan: - networks.append( ip ) if snapshot: for ip in snapshot: @@ -1162,6 +1170,9 @@ def tuyaLookup(deviceid): snapshot = [] if networks: + if verbose: + log.debug("Force-scanning networks: %r", networks) + scan_ips = _generate_ip( networks, verbose, term ) ip_scan = ip_scan_running = True if discover: From d3bc3d08ba5652c06db4cf97e3027d730ec2485c Mon Sep 17 00:00:00 2001 From: uzlonewolf Date: Sat, 22 Jun 2024 23:46:54 -0700 Subject: [PATCH 02/22] Allow a string as the force-scan list --- tinytuya/scanner.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tinytuya/scanner.py b/tinytuya/scanner.py index 697fb3d..15e6171 100644 --- a/tinytuya/scanner.py +++ b/tinytuya/scanner.py @@ -1129,9 +1129,10 @@ def tuyaLookup(deviceid): if verbose: print(term.subbold + " Option: " + term.dim + "Network force scanning requested.\n") + # argparse gives us a list of lists + # the inner list is empty [[]] when no address specified add_connected = True if isinstance( forcescan, list ) or isinstance( forcescan, tuple ): - # argparse gives us a list of lists [[]] when no address specified for ip in forcescan: if isinstance( ip, list ) or isinstance( ip, tuple ): for ip2 in ip: @@ -1141,6 +1142,10 @@ def tuyaLookup(deviceid): networks.append( ip ) add_connected = False + if isinstance( forcescan, str ) or isinstance( forcescan, bytes ): + networks.append( forcescan ) + add_connected = False + if add_connected: if not NETIFLIBS: print(term.alert + From d088823727d9814cdd40df1b28aa0ba44951644d Mon Sep 17 00:00:00 2001 From: uzlonewolf Date: Sun, 23 Jun 2024 04:35:24 -0700 Subject: [PATCH 03/22] Add force-scanning v3.5 devices to scanner --- tinytuya/scanner.py | 46 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/tinytuya/scanner.py b/tinytuya/scanner.py index 15e6171..2b214cd 100644 --- a/tinytuya/scanner.py +++ b/tinytuya/scanner.py @@ -164,6 +164,7 @@ def __init__( self, ip, deviceinfo, options, debug ): self.timeo = 0 self.resets = 0 self.step = FSCAN_NOT_STARTED + self.try_v35 = False self.cur_key = None self.hard_time_limit = time.time() + 30 self.initial_connect_retries = options['retries'] @@ -275,6 +276,11 @@ def v34_negotiate_sess_key_start( self ): print('v3.4/5 trying key', self.ip, self.device.real_local_key) step1 = self.device._negotiate_session_key_generate_step_1() self.sock.sendall( self.device._encode_message( step1 ) ) + if self.try_v35 and self.device.version == 3.4: + self.device.version = 3.5 + step1 = self.device._negotiate_session_key_generate_step_1() + self.sock.sendall( self.device._encode_message( step1 ) ) + self.device.version = 3.4 if self.debug: print('v3.4/5 session key neg start, debug ip', self.ip) @@ -297,6 +303,8 @@ def __init__( self, ip, deviceinfo, options, debug ): self.retries = 0 self.keygen = None self.brute_force_data = [] + self.try_v35 = True + self.v34_connect_ok = False self.connect() @@ -334,7 +342,7 @@ def timeout( self, forced=False ): print('ForceScannedDevice: Debug sock', self.ip, 'connect timed out!') elif self.step == FSCAN_INITIAL_CONNECT: if self.debug: - print('ForceScannedDevice: Debug sock', self.ip, 'socket send failed,', 'no data received' if forced else 'receive timed out') + print('ForceScannedDevice: Debug sock', self.ip, 'socket send failed,', 'no data received,' if forced else 'receive timed out,', 'current retry:', self.retries) if self.retries < 2: self.retries += 1 self.connect() @@ -366,8 +374,9 @@ def timeout( self, forced=False ): self.remove = True else: self.connect() - elif self.step == FSCAN_v34_BRUTE_FORCE_ACTIVE: # or self.step == FSCAN_v33_BRUTE_FORCE_ACTIVE or self.step == FSCAN_v31_BRUTE_FORCE_ACTIVE: - if not forced: + self.v34_connect_ok = False + elif self.step == FSCAN_v34_BRUTE_FORCE_ACTIVE: + if( (not forced) and (not self.v34_connect_ok) ): # actual timeout, connect failed if self.retries < 2: self.retries += 1 @@ -382,6 +391,7 @@ def timeout( self, forced=False ): return # brute-forcing the key self.v3x_brute_force_try_next_key() + self.v34_connect_ok = False elif self.step == FSCAN_v31_BRUTE_FORCE_ACTIVE: # brute-forcing the key self.v3x_brute_force_try_next_key() @@ -418,6 +428,8 @@ def write_data( self ): # 'False' when connection was made but then closed # The IP address when the connection is still open addr = self.get_peer() + if self.debug: + print('ForceScannedDevice: device', self.ip, 'addr is:', addr) if addr is None: # refused self.close() @@ -435,6 +447,7 @@ def write_data( self ): #self.timeo = time.time() + self.options['data_timeout'] self.timeo = time.time() + 1.5 self.found = True + self.v34_connect_ok = True if len(self.send_queue) > 0: self.sock.sendall( self.device._encode_message( self.send_queue[0] ) ) @@ -502,15 +515,30 @@ def read_data( self ): if self.debug: print('ForceScannedDevice:', self.ip, 'got step', self.step, 'data:', data ) - if len(data) == 0: + if len(data) == 0: self.timeout( True ) return while len(data): try: - prefix_offset = data.find(tinytuya.PREFIX_BIN) - if prefix_offset > 0: - data = data[prefix_offset:] + if self.deviceinfo['version'] == 3.5: + prefix_offset = data.find(tinytuya.PREFIX_6699_BIN) + if prefix_offset > 0: + data = data[prefix_offset:] + else: + prefix_offset = data.find(tinytuya.PREFIX_BIN) + if prefix_offset >= 0: + data = data[prefix_offset:] + elif self.try_v35 and self.deviceinfo['version'] == 3.4: + prefix_offset = data.find(tinytuya.PREFIX_6699_BIN) + if prefix_offset >= 0: + if self.debug: + print('ForceScannedDevice: device is v3.5!') + data = data[prefix_offset:] + self.try_v35 = False + self.deviceinfo['version'] = 3.5 + self.device.set_version(3.5) + self.ver_found = True hmac_key = self.device.local_key if self.deviceinfo['version'] >= 3.4 else None msg = tinytuya.unpack_message(data, hmac_key=hmac_key) except: @@ -668,7 +696,7 @@ def brute_force_v3x_data( self ): self.brute_force_found_key() return True - self.brute_force_data = [] + self.brute_force_data = [] return False def v3x_brute_force_try_next_key( self ): @@ -794,7 +822,7 @@ def write_data( self ): self.write = False try: - # connected, send the query + # connected, send the query if self.device.version >= 3.4 : # self.device.real_local_key, self.device.local_key self.v34_negotiate_sess_key_start() From e9fd881cff547f4dd76fb7baa0acaad7edf654cb Mon Sep 17 00:00:00 2001 From: uzlonewolf Date: Sun, 23 Jun 2024 04:38:24 -0700 Subject: [PATCH 04/22] Add force-scanning v3.5 devices to scanner --- tinytuya/scanner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tinytuya/scanner.py b/tinytuya/scanner.py index 2b214cd..a5772ef 100644 --- a/tinytuya/scanner.py +++ b/tinytuya/scanner.py @@ -529,6 +529,7 @@ def read_data( self ): prefix_offset = data.find(tinytuya.PREFIX_BIN) if prefix_offset >= 0: data = data[prefix_offset:] + self.try_v35 = False elif self.try_v35 and self.deviceinfo['version'] == 3.4: prefix_offset = data.find(tinytuya.PREFIX_6699_BIN) if prefix_offset >= 0: From 0690e8c60bfc1a52e708d932e894cef2df54cd6d Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Sun, 23 Jun 2024 16:38:32 -0400 Subject: [PATCH 05/22] v1.14.2 Notes --- RELEASE.md | 4 ++++ tinytuya/core.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 3c59b32..9e81e55 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,9 @@ # RELEASE NOTES +## v1.14.1 - Scanner Fixes + +* Fix force-scanning bug in scanner introduced in last release by @uzlonewolf in https://github.com/jasonacox/tinytuya/pull/511. + ## v1.14.0 - Command Line Updates * PyPI 1.14.0 rewrite of main to use argparse and add additional options by @uzlonewolf in https://github.com/jasonacox/tinytuya/pull/503 diff --git a/tinytuya/core.py b/tinytuya/core.py index f94969a..d7a8cbb 100644 --- a/tinytuya/core.py +++ b/tinytuya/core.py @@ -123,7 +123,7 @@ # Colorama terminal color capability for all platforms init() -version_tuple = (1, 14, 0) +version_tuple = (1, 14, 1) version = __version__ = "%d.%d.%d" % version_tuple __author__ = "jasonacox" From 7259b8dec70a8631607cee6b931d77f7181a47b6 Mon Sep 17 00:00:00 2001 From: uzlonewolf Date: Sun, 23 Jun 2024 21:11:16 -0700 Subject: [PATCH 06/22] Rename scanner variable try_v35 to a more descriptive try_v35_with_v34 --- tinytuya/scanner.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tinytuya/scanner.py b/tinytuya/scanner.py index a5772ef..ac9df94 100644 --- a/tinytuya/scanner.py +++ b/tinytuya/scanner.py @@ -164,7 +164,7 @@ def __init__( self, ip, deviceinfo, options, debug ): self.timeo = 0 self.resets = 0 self.step = FSCAN_NOT_STARTED - self.try_v35 = False + self.try_v35_with_v34 = False self.cur_key = None self.hard_time_limit = time.time() + 30 self.initial_connect_retries = options['retries'] @@ -276,7 +276,7 @@ def v34_negotiate_sess_key_start( self ): print('v3.4/5 trying key', self.ip, self.device.real_local_key) step1 = self.device._negotiate_session_key_generate_step_1() self.sock.sendall( self.device._encode_message( step1 ) ) - if self.try_v35 and self.device.version == 3.4: + if self.try_v35_with_v34 and self.device.version == 3.4: self.device.version = 3.5 step1 = self.device._negotiate_session_key_generate_step_1() self.sock.sendall( self.device._encode_message( step1 ) ) @@ -303,7 +303,7 @@ def __init__( self, ip, deviceinfo, options, debug ): self.retries = 0 self.keygen = None self.brute_force_data = [] - self.try_v35 = True + self.try_v35_with_v34 = True self.v34_connect_ok = False self.connect() @@ -529,14 +529,14 @@ def read_data( self ): prefix_offset = data.find(tinytuya.PREFIX_BIN) if prefix_offset >= 0: data = data[prefix_offset:] - self.try_v35 = False - elif self.try_v35 and self.deviceinfo['version'] == 3.4: + self.try_v35_with_v34 = False + elif self.try_v35_with_v34 and self.deviceinfo['version'] == 3.4: prefix_offset = data.find(tinytuya.PREFIX_6699_BIN) if prefix_offset >= 0: if self.debug: print('ForceScannedDevice: device is v3.5!') data = data[prefix_offset:] - self.try_v35 = False + self.try_v35_with_v34 = False self.deviceinfo['version'] = 3.5 self.device.set_version(3.5) self.ver_found = True From 9420ec66f8d3219d8ff06f05afb456690cdf3a76 Mon Sep 17 00:00:00 2001 From: uzlonewolf Date: Sun, 23 Jun 2024 21:12:02 -0700 Subject: [PATCH 07/22] Make sure set_version() is given a float (#507) --- tinytuya/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tinytuya/core.py b/tinytuya/core.py index d7a8cbb..dc44466 100644 --- a/tinytuya/core.py +++ b/tinytuya/core.py @@ -1654,6 +1654,7 @@ def add_dps_to_request(self, dp_indicies): self.dps_to_request.update({str(index): None for index in dp_indicies}) def set_version(self, version): # pylint: disable=W0621 + version = float(version) self.version = version self.version_str = "v" + str(version) self.version_bytes = str(version).encode('latin1') From 133fd359fdd8f30907083078f81524b34cf0335a Mon Sep 17 00:00:00 2001 From: uzlonewolf Date: Mon, 24 Jun 2024 20:21:07 -0700 Subject: [PATCH 08/22] Allow host bits in the force-scan network list --- tinytuya/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinytuya/scanner.py b/tinytuya/scanner.py index ac9df94..2159680 100644 --- a/tinytuya/scanner.py +++ b/tinytuya/scanner.py @@ -928,7 +928,7 @@ def _generate_ip(networks, verbose, term): if tinytuya.IS_PY2 and type(netblock) == str: netblock = netblock.decode('latin1') try: - network = ipaddress.ip_network(netblock) + network = ipaddress.ip_network(netblock, strict=False) log.debug("Starting brute force network scan %s", network) except: log.debug("Unable to get network for %r, ignoring", netblock) From da6d01b808da9e5882d2108f807019cf6164594a Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Tue, 25 Jun 2024 01:10:58 -0400 Subject: [PATCH 09/22] Refactor _print_device_info function to include verbose flag --- tinytuya/scanner.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tinytuya/scanner.py b/tinytuya/scanner.py index ac9df94..019eddc 100644 --- a/tinytuya/scanner.py +++ b/tinytuya/scanner.py @@ -326,7 +326,7 @@ def stop( self ): self.deviceinfo['version'] = 0.0 if self.options['verbose'] and self.found and not self.displayed: - _print_device_info( self.deviceinfo, 'Failed to Force-Scan, FORCED STOP', self.options['termcolors'], self.message ) + _print_device_info( self.deviceinfo, 'Failed to Force-Scan, FORCED STOP', self.options['termcolors'], self.message, self.options['verbose'] ) self.displayed = True def timeout( self, forced=False ): @@ -385,7 +385,7 @@ def timeout( self, forced=False ): self.err_found = True self.deviceinfo['version'] = 0.0 self.message = "%s Polling %s Failed: Device stopped responding before key was found" % (self.options['termcolors'].alertdim, self.ip) - _print_device_info( self.deviceinfo, 'Failed to Force-Scan', self.options['termcolors'], self.message ) + _print_device_info( self.deviceinfo, 'Failed to Force-Scan', self.options['termcolors'], self.message, self.options['verbose']) self.displayed = True self.close() return @@ -398,7 +398,7 @@ def timeout( self, forced=False ): elif forced: self.err_found = True self.message = "%s Polling %s Failed: Unexpected close during read/write operation" % (self.options['termcolors'].alertdim, self.ip) - _print_device_info( self.deviceinfo, 'Failed to Force-Scan', self.options['termcolors'], self.message ) + _print_device_info( self.deviceinfo, 'Failed to Force-Scan', self.options['termcolors'], self.message, self.options['verbose']) self.displayed = True self.remove = True elif self.step == FSCAN_v31_PASSIVE_LISTEN or self.step == FSCAN_v33_BRUTE_FORCE_ACQUIRE: @@ -409,14 +409,14 @@ def timeout( self, forced=False ): elif self.step == FSCAN_FINAL_POLL: if not self.message: self.message = "%s Polling %s Failed: No response to poll request" % (self.options['termcolors'].alertdim, self.ip) - _print_device_info( self.deviceinfo, 'Force-Scanned', self.options['termcolors'], self.message ) + _print_device_info( self.deviceinfo, 'Force-Scanned', self.options['termcolors'], self.message, self.options['verbose']) self.displayed = True self.remove = True else: if self.debug: print('ForceScannedDevice: Debug sock', self.ip, 'timeout on unhandled step', self.step) self.remove = True - _print_device_info( self.deviceinfo, 'Failed to Force-Scan', self.options['termcolors'], self.message ) + _print_device_info( self.deviceinfo, 'Failed to Force-Scan', self.options['termcolors'], self.message, self.options['verbose']) self.displayed = True if self.remove: @@ -723,7 +723,7 @@ def v3x_brute_force_try_next_key( self ): self.remove = True self.deviceinfo['version'] = 0.0 self.message = "%s Polling %s Failed: No matching key found" % (self.options['termcolors'].alertdim, self.ip) - _print_device_info( self.deviceinfo, 'Failed to Force-Scan', self.options['termcolors'], self.message ) + _print_device_info( self.deviceinfo, 'Failed to Force-Scan', self.options['termcolors'], self.message, self.options['verbose'] ) self.displayed = True else: if self.debug: @@ -944,7 +944,9 @@ def _generate_ip(networks, verbose, term): for addr in ipaddress.IPv4Network(network): yield str(addr) -def _print_device_info( result, note, term, extra_message=None ): +def _print_device_info( result, note, term, extra_message=None, verbose=True ): + if not verbose: + return ip = result["ip"] gwId = result["gwId"] productKey = result["productKey"] if result["productKey"] else '?' @@ -985,7 +987,9 @@ def _print_device_info( result, note, term, extra_message=None ): # Scan function -def devices(verbose=False, scantime=None, color=True, poll=True, forcescan=False, byID=False, show_timer=None, discover=True, wantips=None, wantids=None, snapshot=None, assume_yes=False, tuyadevices=[], maxdevices=0): # pylint: disable=W0621, W0102 +def devices(verbose=False, scantime=None, color=True, poll=True, forcescan=False, byID=False, show_timer=None, + discover=True, wantips=None, wantids=None, snapshot=None, assume_yes=False, tuyadevices=[], + maxdevices=0): # pylint: disable=W0621, W0102 """Scans your network for Tuya devices and returns dictionary of devices discovered devices = tinytuya.deviceScan(verbose) From 7d160a0f98acf107d83310298c61d97a751ee85c Mon Sep 17 00:00:00 2001 From: uzlonewolf Date: Sun, 7 Jul 2024 09:25:40 -0700 Subject: [PATCH 10/22] Allow device discovery packets on port 7000 --- tinytuya/scanner.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tinytuya/scanner.py b/tinytuya/scanner.py index 038dca9..1a029ea 100644 --- a/tinytuya/scanner.py +++ b/tinytuya/scanner.py @@ -1402,13 +1402,19 @@ def tuyaLookup(deviceid): if ip_force_wants_end: continue - if sock is clientapp: + if 'from' in result and result['from'] == 'app': #sock is clientapp: if ip not in broadcasted_apps: broadcasted_apps[ip] = result if verbose: print( term.alertdim + 'New Broadcast from App at ' + str(ip) + term.dim + ' - ' + str(result) + term.normal ) continue + if 'gwId' not in result: + if verbose: + print(term.alertdim + "* Payload missing required 'gwId' - from %r to port %r:%s %r (%r)\n" % (ip, tgt_port, term.normal, result, data)) + log.debug("UDP Packet payload missing required 'gwId' - from %r port %r - %r", ip, tgt_port, data) + continue + # check to see if we have seen this device before and add to devices array #if tinytuya.appenddevice(result, deviceslist) is False: if ip not in broadcasted_devices: From ff6abf2275daacef76743814aea0ecbbe36b84b4 Mon Sep 17 00:00:00 2001 From: uzlonewolf Date: Sun, 7 Jul 2024 10:33:10 -0700 Subject: [PATCH 11/22] Add get_ip_to_broadcast() function to scanner --- tinytuya/scanner.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tinytuya/scanner.py b/tinytuya/scanner.py index 1a029ea..9b69185 100644 --- a/tinytuya/scanner.py +++ b/tinytuya/scanner.py @@ -44,9 +44,15 @@ try: import netifaces # pylint: disable=E0401 NETIFLIBS = True -except: +except ImportError: NETIFLIBS = False +try: + import psutil # pylint: disable=E0401 + PSULIBS = True +except ImportError: + PSULIBS = False + # Colorama terminal color capability for all platforms init() @@ -131,6 +137,36 @@ def getmyIPs( term, verbose, ask ): ips[k] = True return ips.keys() +def get_ip_to_broadcast(): + ip_to_broadcast = {} + + if NETIFLIBS: + interfaces = netifaces.interfaces() + for interface in interfaces: + addresses = netifaces.ifaddresses(interface) + ipv4 = addresses.get(netifaces.AF_INET) + + if ipv4: + for addr in ipv4: + if 'broadcast' in addr: + ip_to_broadcast[addr['broadcast']] = addr['addr'] + + if ip_to_broadcast: + return ip_to_broadcast + + if PSULIBS: + interfaces = psutil.net_if_addrs() + for addresses in interfaces.values(): + for addr in addresses: + if addr.family == socket.AF_INET and addr.broadcast: # AF_INET is for IPv4 + ip_to_broadcast[addr.broadcast] = addr.address + + if ip_to_broadcast: + return ip_to_broadcast + + ip_to_broadcast['255.255.255.255'] = getmyIP() + return ip_to_broadcast + class KeyObj(object): def __init__( self, gwId, key ): self.gwId = gwId From 5f38b49e01462d451d12463a8d337ef89e7b2614 Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Sun, 7 Jul 2024 11:24:48 -0700 Subject: [PATCH 12/22] Add "Force Scan" button and UI updates --- server/README.md | 6 ++ server/server.py | 106 ++++++++++++++++++++++++--------- server/web/index.html | 39 ++++++++++-- server/web/jquery-3.7.1.min.js | 2 + server/web/tinytuya.css | 28 +++++++++ 5 files changed, 148 insertions(+), 33 deletions(-) create mode 100644 server/web/jquery-3.7.1.min.js diff --git a/server/README.md b/server/README.md index dad0ff5..1568b07 100644 --- a/server/README.md +++ b/server/README.md @@ -110,6 +110,12 @@ The UI at http://localhost:8888 allows you to view and control the devices. ## Release Notes +### t12 - Force Scan + +* Added "Force Scan" button to cause server to run a network scan for devices not broadcasting. +* Minor updates to UI for a cleaner title and footer to accommodate button. +* Added logic to allow settings via environmental variables. + ### t11 - Minimize Container * Reduce size of Docker container by removing rust build and using python:3.12-bookworm. diff --git a/server/server.py b/server/server.py index 8c087de..ac3c311 100644 --- a/server/server.py +++ b/server/server.py @@ -48,7 +48,7 @@ import sys import os import urllib.parse -from http.server import BaseHTTPRequestHandler, HTTPServer, ThreadingHTTPServer +from http.server import BaseHTTPRequestHandler, HTTPServer from socketserver import ThreadingMixIn # Terminal color capability for all platforms @@ -59,30 +59,28 @@ pass import tinytuya +from tinytuya import scanner +import os -BUILD = "t11" - -# Defaults -APIPORT = 8888 -DEBUGMODE = False -DEVICEFILE = tinytuya.DEVICEFILE -SNAPSHOTFILE = tinytuya.SNAPSHOTFILE -CONFIGFILE = tinytuya.CONFIGFILE -TCPTIMEOUT = tinytuya.TCPTIMEOUT # Seconds to wait for socket open for scanning -TCPPORT = tinytuya.TCPPORT # Tuya TCP Local Port -MAXCOUNT = tinytuya.MAXCOUNT # How many tries before stopping -UDPPORT = tinytuya.UDPPORT # Tuya 3.1 UDP Port -UDPPORTS = tinytuya.UDPPORTS # Tuya 3.3 encrypted UDP Port -UDPPORTAPP = tinytuya.UDPPORTAPP # Tuya App -TIMEOUT = tinytuya.TIMEOUT # Socket Timeout -RETRYTIME = 30 -RETRYCOUNT = 5 -SAVEDEVICEFILE = True - -# Check for Environmental Overrides -debugmode = os.getenv("DEBUG", "no") -if debugmode.lower() == "yes": - DEBUGMODE = True +BUILD = "t12" + +# Defaults from Environment +APIPORT = int(os.getenv("APIPORT", "8888")) +DEBUGMODE = os.getenv("DEBUGMODE", "False").lower() == "true" +DEVICEFILE = os.getenv("DEVICEFILE", tinytuya.DEVICEFILE) +SNAPSHOTFILE = os.getenv("SNAPSHOTFILE", tinytuya.SNAPSHOTFILE) +CONFIGFILE = os.getenv("CONFIGFILE", tinytuya.CONFIGFILE) +TCPTIMEOUT = float(os.getenv("TCPTIMEOUT", str(tinytuya.TCPTIMEOUT))) +TCPPORT = int(os.getenv("TCPPORT", str(tinytuya.TCPPORT))) +MAXCOUNT = int(os.getenv("MAXCOUNT", str(tinytuya.MAXCOUNT))) +UDPPORT = int(os.getenv("UDPPORT", str(tinytuya.UDPPORT))) +UDPPORTS = int(os.getenv("UDPPORTS", str(tinytuya.UDPPORTS))) +UDPPORTAPP = int(os.getenv("UDPPORTAPP", str(tinytuya.UDPPORTAPP))) +TIMEOUT = float(os.getenv("TIMEOUT", str(tinytuya.TIMEOUT))) +RETRYTIME = int(os.getenv("RETRYTIME", "30")) +RETRYCOUNT = int(os.getenv("RETRYCOUNT", "5")) +SAVEDEVICEFILE = os.getenv("SAVEDEVICEFILE", "True").lower() == "true" +DEBUGMODE = os.getenv("DEBUGMODE", "no").lower() == "yes" # Logging log = logging.getLogger(__name__) @@ -124,7 +122,8 @@ def sig_term_handle(signum, frame): retrydevices = {} retrytimer = 0 cloudconfig = {'apiKey':'', 'apiSecret':'', 'apiRegion':'', 'apiDeviceID':''} - +forcescan = False +forcescandone = True # Terminal formatting (bold, subbold, normal, dim, alert, alertdim, cyan, red, yellow) = tinytuya.termcolor(True) @@ -310,6 +309,7 @@ def tuyalisten(port): result["mac"] = mac result["key"] = dkey result["id"] = gwId + result["forced"] = False # add device if new if not appenddevice(result, deviceslist): @@ -323,7 +323,6 @@ def tuyalisten(port): class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True - pass def delayoff(d, sw): d.turn_off(switch=sw, nowait=True) @@ -355,8 +354,10 @@ def do_POST(self): self.wfile.write(bytes(message, "utf8")) def do_GET(self): + # pylint: disable=global-variable-not-assigned global retrytimer, retrydevices global cloudconfig, deviceslist + global forcescan, forcescandone self.send_response(200) message = "Error" @@ -516,6 +517,8 @@ def do_GET(self): jout = {} jout["found"] = len(deviceslist) jout["registered"] = len(tuyadevices) + jout["forcescan"] = forcescan + jout["forcescandone"] = forcescandone message = json.dumps(jout) elif self.path.startswith('/status/'): id = self.path.split('/status/')[1] @@ -561,6 +564,11 @@ def do_GET(self): retrydevices['*'] = 1 elif self.path == '/offline': message = json.dumps(offlineDevices()) + elif self.path == '/scan': + # Force Scan for new devices + forcescan = True + forcescandone = False + message = json.dumps({"OK": "Forcing a scan for new devices."}) else: # Serve static assets from web root first, if found. fcontent, ftype = get_static(web_root, self.path) @@ -606,7 +614,7 @@ def api(port): tuyaUDPs = threading.Thread(target=tuyalisten, args=(UDPPORTS,)) tuyaUDP7 = threading.Thread(target=tuyalisten, args=(UDPPORTAPP,)) apiServer = threading.Thread(target=api, args=(APIPORT,)) - + print( "\n%sTinyTuya %s(Server)%s [%s%s]\n" % (bold, normal, dim, tinytuya.__version__, BUILD) @@ -626,10 +634,50 @@ def api(port): print(" * API and UI Endpoint on http://localhost:%d" % APIPORT) log.debug("Server URL http://localhost:%d" % APIPORT) - + try: while(True): log.debug("Discovered Devices: %d " % len(deviceslist)) + if forcescan: + print(" + ForceScan: Scan for new devices started...") + forcescan = False + retrytimer = time.time() + RETRYTIME + # def devices(verbose=False, scantime=None, color=True, poll=True, forcescan=False, byID=False, show_timer=None, + # discover=True, wantips=None, wantids=None, snapshot=None, assume_yes=False, tuyadevices=[], + # maxdevices=0) + try: + found = scanner.devices(forcescan=True, verbose=False, discover=False, tuyadevices=tuyadevices) + except: + log.error("Error during scanner.devices()") + found = [] + print(f" - ForceScan: Found {len(found)} devices") + for f in found: + log.debug(f" - {found[f]}") + gwId = found[f]["id"] + result = {} + dname = dkey = mac = "" + try: + # Try to pull name and key data + (dname, dkey, mac) = tuyaLookup(gwId) + except: + pass + # set values + result["name"] = dname + result["mac"] = mac + result["key"] = dkey + result["id"] = gwId + result["ip"] = found[f]["ip"] + result["version"] = found[f]["version"] + result["forced"] = True + + # add device if new + if not appenddevice(result, deviceslist): + # Added device to list + if dname == "" and dkey == "" and result["id"] not in newdevices: + # If fetching the key failed, save it to retry later + retrydevices[result["id"]] = RETRYCOUNT + newdevices.append(result["id"]) + forcescandone = True if retrytimer <= time.time() or '*' in retrydevices: if len(retrydevices) > 0: @@ -671,4 +719,4 @@ def api(port): # Close down API thread print("Stopping threads...") log.debug("Stoppping threads") - requests.get('http://localhost:%d/stop' % APIPORT) + requests.get('http://localhost:%d/stop' % APIPORT, timeout=5) diff --git a/server/web/index.html b/server/web/index.html index 58a6f3f..3d6843d 100644 --- a/server/web/index.html +++ b/server/web/index.html @@ -2,7 +2,7 @@ TinyTuya API Server - + @@ -18,7 +18,9 @@
-
+
+
+