diff --git a/tools/decode-config.py b/tools/decode-config.py index 299f152c4..dc51e0f42 100644 --- a/tools/decode-config.py +++ b/tools/decode-config.py @@ -21,7 +21,7 @@ Requirements: - Python - - pip json pycurl urllib2 configargparse + - pip install json pycurl urllib2 configargparse Instructions: Execute command with option -d to retrieve config data from device or @@ -31,10 +31,11 @@ Instructions: Usage: - decode-config.py [-h] [-f ] [-d ] - [-u ] [-p ] [--format ] - [--sort ] [--raw] [--unhide-pw] [-o ] - [-c ] [-V] + decode-config.py [-h] [-f ] [-d ] [-u ] + [-p ] [--format ] + [--json-indent ] [--json-compact] + [--sort ] [--raw] [--unhide-pw] [-o ] + [-c ] [-V] Decode configuration of Sonoff-Tasmota device. Args that start with '--' (eg. -f) can also be set in a config file (specified via -c). Config file syntax @@ -46,21 +47,27 @@ Usage: -h, --help show this help message and exit -c , --config Config file, can be used instead of command parameter - (defaults to None) + (default: None) source: -f , --file file to retrieve Tasmota configuration from (default: None) - -d , --device - device to retrieve configuration from (default: None) + -d , --device + hostname or IP address to retrieve Tasmota + configuration from (default: None) -u , --username - for -d usage: http access username (default: admin) + host http access username (default: admin) -p , --password - for -d usage: http access password (default: None) + host http access password (default: None) output: --format output format ("json" or "text", default: "json") + --json-indent + pretty-printed JSON output using indent level + (default: "None") + --json-compact compact JSON output by eliminate whitespace (default: + "not compact") --sort sort result - can be "none" or "name" (default: "name") --raw output raw values (default: processed) @@ -72,7 +79,7 @@ Usage: info: -V, --version show program's version number and exit - Note: Either argument -d or -f must be given. + Either argument -d or -f must be given. Examples: @@ -113,7 +120,7 @@ except ImportError: sys.exit(9) -VER = '1.5.0008' +VER = '1.5.0009' PROG='{} v{} by Norbert Richter'.format(os.path.basename(sys.argv[0]),VER) CONFIG_FILE_XOR = 0x5A @@ -135,6 +142,8 @@ DEFAULTS = { 'output': { 'format': 'json', + 'jsonindent': None, + 'jsoncompact': False, 'sort': 'name', 'raw': False, 'unhide-pw': False, @@ -940,7 +949,7 @@ Setting_5_14_0 = { 'knx_CB_addr': ('3: + if not raw and len(fielddef)>3: if isinstance(fielddef[3],str): # use a format string return fielddef[3].format(value) elif callable(fielddef[3]): # use a format function @@ -1685,7 +1696,7 @@ def ConvertFieldValue(value, fielddef): return value -def GetField(dobj, fieldname, fielddef): +def GetField(dobj, fieldname, fielddef, raw=False): """ Get field value from definition @@ -1695,6 +1706,8 @@ def GetField(dobj, fieldname, fielddef): name of the field @param fielddef: see Settings desc above + @param raw + return raw values (True) or converted values (False) @return: read field value """ @@ -1715,13 +1728,13 @@ def GetField(dobj, fieldname, fielddef): subfielddef = (fielddef[0], addr, None, None if len(fielddef)<4 else fielddef[3]) length = GetFieldLength(subfielddef) if length != 0: - result.append(GetField(dobj, fieldname, subfielddef)) + result.append(GetField(dobj, fieldname, subfielddef, raw)) addr += length # tuple 2 contains a list with dict elif isinstance(fielddef[2], list) and len(fielddef[2])>0 and isinstance(fielddef[2][0], dict): d = {} value = struct.unpack_from(fielddef[0], dobj, fielddef[1])[0] - d['base'] = ConvertFieldValue(value, fielddef); + d['base'] = ConvertFieldValue(value, fielddef, raw); union = fielddef[2] i = 0 for l in union: @@ -1738,8 +1751,8 @@ def GetField(dobj, fieldname, fielddef): if ord(result[:1])==0x00 or ord(result[:1])==0xff: result = '' s = str(result).split('\0')[0] - result = s #unicode(s, errors='replace') - result = ConvertFieldValue(result, fielddef) + result = unicode(s, errors='replace') + result = ConvertFieldValue(result, fielddef, raw) return result @@ -1762,40 +1775,52 @@ def DeEncrypt(obj): def Decode(obj): """ - Decodes (already decrypted) binary data stream + Decodes binary data stream @param obj: - binary config data + binary config data (decrypted) """ # get header data - cfg_size = GetField(obj, 'cfg_size', Setting_6_2_1['cfg_size']) - version = GetField(obj, 'version', Setting_6_2_1['version']) + version = GetField(obj, 'version', Setting_6_2_1['version'], raw=True) # search setting definition - setting = None + template = None for cfg in Settings: - if version >= cfg[0] and cfg_size == cfg[1]: + if version >= cfg[0]: template = cfg break - setting = template[2] # if we did not found a mathching setting - if setting is None: - exit(2, "Can't handle Tasmota configuration data for version 0x{:x} with {} bytes".format(version, cfg_size) ) + if template is None: + exit(2, "Can't handle Tasmota configuration data for version 0x{:x}".format(version) ) - if GetField(obj, 'cfg_crc', setting['cfg_crc']) != GetSettingsCrc(obj): + setting = template[2] + + # check size if exists + if 'cfg_size' in setting: + cfg_size = GetField(obj, 'cfg_size', setting['cfg_size'], raw=True) + # if we did not found a mathching setting + if cfg_size != template[1]: + exit(2, "Data size does not match. Expected {} bytes, read {} bytes.".format(template[1], cfg_size) ) + + # check crc if exists + if 'cfg_crc' in setting: + cfg_crc = GetField(obj, 'cfg_crc', setting['cfg_crc'], raw=True) + else: + cfg_crc = GetSettingsCrc(obj) + if cfg_crc != GetSettingsCrc(obj): exit(3, 'Data crc error' ) config = {} config['version_template'] = '0x{:x}'.format(template[0]) for name in setting: - config[name] = GetField(obj, name, setting[name]) + config[name] = GetField(obj, name, setting[name], args.raw) if args.sort == 'name': config = collections.OrderedDict(sorted(config.items())) if args.format == 'json': - print json.dumps(config, sort_keys=args.sort=='name') + print json.dumps(config, sort_keys=args.sort=='name', indent=args.jsonindent, separators=(',', ':') if args.jsoncompact else (', ', ': ') ) else: for key,value in config.items(): print '{} = {}'.format(key, repr(value)) @@ -1804,7 +1829,7 @@ def Decode(obj): if __name__ == "__main__": parser = configargparse.ArgumentParser(description='Decode configuration of Sonoff-Tasmota device.', - epilog='Note: Either argument -d or -f must be given.') + epilog='Either argument -d or -f must be given.') source = parser.add_argument_group('source') source.add_argument('-f', '--file', @@ -1813,20 +1838,20 @@ if __name__ == "__main__": default=DEFAULTS['source']['tasmotafile'], help='file to retrieve Tasmota configuration from (default: {})'.format(DEFAULTS['source']['tasmotafile'])) source.add_argument('-d', '--device', - metavar='', + metavar='', dest='device', default=DEFAULTS['source']['device'], - help='device to retrieve configuration from (default: {})'.format(DEFAULTS['source']['device']) ) + help='hostname or IP address to retrieve Tasmota configuration from (default: {})'.format(DEFAULTS['source']['device']) ) source.add_argument('-u', '--username', metavar='', dest='username', default=DEFAULTS['source']['username'], - help='for -d usage: http access username (default: {})'.format(DEFAULTS['source']['username'])) + help='host http access username (default: {})'.format(DEFAULTS['source']['username'])) source.add_argument('-p', '--password', metavar='', dest='password', default=DEFAULTS['source']['password'], - help='for -d usage: http access password (default: {})'.format(DEFAULTS['source']['password'])) + help='host http access password (default: {})'.format(DEFAULTS['source']['password'])) output = parser.add_argument_group('output') output.add_argument('--format', @@ -1835,6 +1860,17 @@ if __name__ == "__main__": choices=['json', 'text'], default=DEFAULTS['output']['format'], help='output format ("json" or "text", default: "{}")'.format(DEFAULTS['output']['format']) ) + output.add_argument('--json-indent', + metavar='', + dest='jsonindent', + type=int, + default=DEFAULTS['output']['jsonindent'], + help='pretty-printed JSON output using indent level (default: "{}")'.format(DEFAULTS['output']['jsonindent']) ) + output.add_argument('--json-compact', + dest='jsoncompact', + action='store_true', + default=DEFAULTS['output']['jsoncompact'], + help='compact JSON output by eliminate whitespace (default: "{}")'.format('compact' if DEFAULTS['output']['jsoncompact'] else 'not compact') ) output.add_argument('--sort', metavar='', dest='sort', @@ -1862,7 +1898,7 @@ if __name__ == "__main__": dest='configfile', default=DEFAULTS['DEFAULT']['configfile'], is_config_file=True, - help='Config file, can be used instead of command parameter (defaults to {})'.format(DEFAULTS['DEFAULT']['configfile']) ) + help='Config file, can be used instead of command parameter (default: {})'.format(DEFAULTS['DEFAULT']['configfile']) ) info = parser.add_argument_group('info') info.add_argument('-V', '--version', action='version', version=PROG) @@ -1921,4 +1957,7 @@ if __name__ == "__main__": Decode(cfg) else: - exit(4, "Could not read configuration data from {} '{}'".format('device' if args.device is not None else 'file', args.device if args.device is not None else args.tasmotafile) ) \ No newline at end of file + exit(4, "Could not read configuration data from {} '{}'".format('device' if args.device is not None else 'file', \ + args.device if args.device is not None else args.tasmotafile) ) + + sys.exit(0)