From 7dcb10a91172d6b9391912a7d0a205e4cb898643 Mon Sep 17 00:00:00 2001 From: Norbert Richter Date: Tue, 2 Oct 2018 13:42:21 +0200 Subject: [PATCH] v1.5.0012: 'decode-config.py' add args, add 6.2.1.x settings, fix filename with @ - add: developer version settings 6.2.1.2, 6.2.1.3, 6.2.1.6, 6.2.1.10 - add: args '--raw-keys', '--no-raw-keys', '--raw-values', '--no-raw-values', '--hide-pw' - add: value -1 for arg '--json-indent' to disable indent if default is enabled - changed: json output to file use always raw values - fix: add again removed return code desc - fix: needless dot . in filename for @v - fix: rule(123) none-raw outputs --- tools/decode-config.py | 1317 +++++++++++++++++++++++++++++++++++----- 1 file changed, 1177 insertions(+), 140 deletions(-) diff --git a/tools/decode-config.py b/tools/decode-config.py index 284904fb9..258a8accd 100644 --- a/tools/decode-config.py +++ b/tools/decode-config.py @@ -1,5 +1,7 @@ #!/usr/bin/env python +#!/usr/bin/env python # -*- coding: utf-8 -*- +VER = '1.5.0012' """ decode-config.py - Decode configuration of Sonoff-Tasmota device @@ -35,9 +37,11 @@ Instructions: Usage: decode-config.py [-h] [-f ] [-d ] [-u ] [-p ] [--json-indent ] - [--json-compact] [--unsort] [--raw] [--unhide-pw] - [-o ] [--output-file-format ] - [-c ] [--exit-on-error-only] [-V] + [--json-compact] [--sort] [--unsort] [--raw-values] + [--no-raw-values] [--raw-keys] [--no-raw-keys] + [--hide-pw] [--unhide-pw] [-o ] + [--output-file-format ] [-c ] + [--exit-on-error-only] [-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 @@ -66,18 +70,23 @@ Usage: -p , --password host HTTP access password (default: None) - output: + config: --json-indent pretty-printed JSON output using indent level - (default: 'None') - --json-compact compact JSON output by eliminate whitespace (default: - normal) - --unsort do not sort results (default: sort) - --raw output raw values (default: process) - --unhide-pw unhide passwords (default: hide) + (default: 'None'). Use values greater equal 0 to + indent or -1 to disabled indent. + --json-compact compact JSON output by eliminate whitespace + --sort sort json keywords (default) + --unsort do not sort json keywords + --raw-values, --raw output raw values + --no-raw-values output human readable values (default) + --raw-keys output bitfield raw keys (default) + --no-raw-keys do not output bitfield raw keys + --hide-pw hide passwords (default) + --unhide-pw unhide passwords -o , --output-file - file to store configuration to (default: None) Macros: - @v=Tasmota version, @f=friendly name + file to store configuration to (default: None). + Replacements: @v=Tasmota version, @f=friendly name --output-file-format output format ('json' or 'binary', default: 'json') @@ -86,41 +95,44 @@ Usage: Either argument -d or -f must be given. + +Returns: + 0: successful + 1: file not found + 2: configuration version not supported + 3: data size mismatch + 4: data CRC error + 5: configuration file read error + 6: argument error + 9: python module is missing + 4xx, 5xx: HTTP error + """ import os.path import io import sys -import struct -import re -import math -from datetime import datetime +def ModuleImportError(module): + er = str(module) + print("{}. Try 'pip install {}' to install it".format(er,er.split(' ')[len(er.split(' '))-1]) ) + sys.exit(9) try: + import struct + import re + import math + from datetime import datetime import json -except ImportError: - print("module not found. Try 'pip install json' to install it") - sys.exit(9) -try: import configargparse -except ImportError: - print("module not found. Try 'pip install configargparse' to install it") - sys.exit(9) -try: import pycurl -except ImportError: - print("module not found. Try 'pip install pycurl' to install it") - sys.exit(9) -try: import urllib2 -except ImportError: - print("module not found. Try 'pip install urllib2' to install it") - sys.exit(9) +except ImportError, e: + ModuleImportError(e) -VER = '1.5.0011' PROG='{} v{} by Norbert Richter'.format(os.path.basename(sys.argv[0]),VER) CONFIG_FILE_XOR = 0x5A +BINARYFILE_MAGIC = 0x63576223 args = {} DEFAULTS = { @@ -136,13 +148,14 @@ DEFAULTS = { 'password': None, 'tasmotafile': None, }, - 'output': + 'config': { 'jsonindent': None, 'jsoncompact': False, - 'unsort': False, - 'raw': False, - 'unhide-pw': False, + 'sort': True, + 'rawvalues': False, + 'rawkeys': True, + 'hidepw': True, 'outputfile': None, 'outputfileformat': 'json', }, @@ -166,7 +179,7 @@ Settings dictionary describes the config file fields definition: 'xxx': A string is used to interpret the data at The string defines the format interpretion as described - in 'struct module format string', see + in 'struct module format string', see https://docs.python.org/2.7/library/struct.html#format-strings In addition to this format string there is as special meaning of a dot '.' - this means a bit with an optional @@ -178,7 +191,7 @@ Settings dictionary describes the config file fields definition: The address (starting from 0) within config data. For bit fields must be a tuple. n: - Defines a simple address within config data. + Defines a simple address within config data. must be a positive integer. (n, b, s): A tuple defines a bit field: @@ -221,11 +234,12 @@ def int2ip(value): return '{:d}.{:d}.{:d}.{:d}'.format(value & 0xff, value>>8 & 0xff, value>>16 & 0xff, value>>24 & 0xff) def password(value): - if args.unhidepw: - return value - return '********' + if args.hidepw: + return '********' + return value -Setting_6_2_1 = { + +Setting_6_2_1_10 = { 'cfg_holder': ('>24) & 0xff) + minor = ((ver>>16) & 0xff) + release = ((ver>> 8) & 0xff) + subrelease = (ver & 0xff) + if major>=6: + if subrelease>0: + subreleasestr = str(subrelease) + else: + subreleasestr = '' + else: + if subrelease>0: + subreleasestr = str(chr(subrelease+ord('a')-1)) + else: + subreleasestr = '' + v = "{:d}.{:d}.{:d}{}{}".format( major, minor, release, '.' if (major>=6 and subreleasestr!='') else '', subreleasestr) + filename = filename.replace('@v', v) + if 'friendlyname' in configuration: + filename = filename.replace('@f', configuration['friendlyname'][0] ) + + return filename + + def GetSettingsCrc(dobj): """ Return binary config data calclulated crc @param dobj: - uncrypted binary config data + decrypted binary config data @return: 2 byte unsigned integer crc value @@ -1918,7 +2928,7 @@ def GetFieldLength(fielddef): if addr != baseaddr: addr = baseaddr length += len_ - + else: if format_[-1:].lower() in ['b','c','?']: length=1 @@ -1942,13 +2952,15 @@ def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0): Get field value from definition @param dobj: - uncrypted binary config data + decrypted binary config data @param fieldname: name of the field @param fielddef: see Settings desc above @param raw return raw values (True) or converted values (False) + @param addroffset + use offset for baseaddr (used for recursive calls) @return: read field value """ @@ -1963,7 +2975,7 @@ def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0): # get datadef from field definition datadef = None - if len(fielddef)>2: + if fielddef is not None and len(fielddef)>2: datadef = fielddef[2] if datadef is not None: @@ -1993,7 +3005,7 @@ def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0): subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), None, fielddef[3]) length = GetFieldLength(subfielddef) - if length != 0: + if length != 0 and (fieldname != 'raw' or args.rawkeys): result.append(GetField(dobj, fieldname, subfielddef, raw=raw, addroffset=addroffset+offset)) offset += length @@ -2004,7 +3016,8 @@ def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0): setting = fielddef[0] config = {} for name in setting: - config[name] = GetField(dobj, name, setting[name], raw=args.raw, addroffset=addroffset) + if name != 'raw' or args.rawkeys: + config[name] = GetField(dobj, name, setting[name], raw=raw, addroffset=addroffset) result = config else: # a simple value @@ -2047,39 +3060,56 @@ def DeEncrypt(obj): return dobj -def Decode(obj): +def GetTemplateSetting(version): + """ + Search for template, settings and size to be used depending on given version number + + @param version: + version number from read binary data to search for + + @return: template, settings to use, None if version is invalid + """ + # search setting definition + template = None + setting = None + size = None + for cfg in Settings: + if version >= cfg[0]: + template = cfg + size = template[1] + setting = template[2] + break + + return template, size, setting + + +def Decode(obj, raw=True): """ Decodes binary data stream @param obj: binary config data (decrypted) - + @param raw + decode raw values (True) or converted values (False) + @return: configuration dictionary """ # get header data version = GetField(obj, 'version', Setting_6_2_1['version'], raw=True) - # search setting definition - template = None - for cfg in Settings: - if version >= cfg[0]: - template = cfg - break - + template, size, setting = GetTemplateSetting(version) # if we did not found a mathching setting if template is None: exit(2, "Tasmota configuration version 0x{:x} not supported".format(version) ) - - setting = template[2] # check size if exists if 'cfg_size' in setting: cfg_size = GetField(obj, 'cfg_size', setting['cfg_size'], raw=True) # read size should be same as definied in template - if cfg_size > template[1]: + if cfg_size > size: # may be processed exit(3, "Number of bytes read does ot match - read {}, expected {} byte".format(cfg_size, template[1]), typ='WARNING', doexit=args.exitonwarning) - elif cfg_size < template[1]: + elif cfg_size < size: # less number of bytes can not be processed exit(3, "Number of bytes read to small to process - read {}, expected {} byte".format(cfg_size, template[1]), typ='ERROR') @@ -2091,9 +3121,8 @@ def Decode(obj): if cfg_crc != GetSettingsCrc(obj): exit(4, 'Data CRC error, read 0x{:x} should be 0x{:x}'.format(cfg_crc, GetSettingsCrc(obj)), typ='WARNING', doexit=args.exitonwarning) - config = {} - for name in setting: - config[name] = GetField(obj, name, setting[name], raw=args.raw) + # get config + config = GetField(obj, None, (setting,None,None), raw=raw) # add header info timestamp = datetime.now() @@ -2142,44 +3171,74 @@ if __name__ == "__main__": default=DEFAULTS['source']['password'], help="host HTTP access password (default: {})".format(DEFAULTS['source']['password'])) - output = parser.add_argument_group('output') - output.add_argument('--json-indent', + config = parser.add_argument_group('config') + config.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', + default=DEFAULTS['config']['jsonindent'], + help="pretty-printed JSON output using indent level (default: '{}'). Use values greater equal 0 to indent or -1 to disabled indent.".format(DEFAULTS['config']['jsonindent']) ) + config.add_argument('--json-compact', dest='jsoncompact', action='store_true', - default=DEFAULTS['output']['jsoncompact'], - help="compact JSON output by eliminate whitespace (default: {})".format('normal' if not DEFAULTS['output']['jsoncompact'] else 'compact') ) - output.add_argument('--unsort', - dest='unsort', + default=DEFAULTS['config']['jsoncompact'], + help="compact JSON output by eliminate whitespace{}".format(' (default)' if DEFAULTS['config']['jsoncompact'] else '') ) + + config.add_argument('--sort', + dest='sort', action='store_true', - default=DEFAULTS['output']['unsort'], - help="do not sort results (default: {})".format('sort' if not DEFAULTS['output']['unsort'] else 'unsort') ) - output.add_argument('--raw', - dest='raw', + default=DEFAULTS['config']['sort'], + help="sort json keywords{}".format(' (default)' if DEFAULTS['config']['sort'] else '') ) + config.add_argument('--unsort', + dest='sort', + action='store_false', + default=DEFAULTS['config']['sort'], + help="do not sort json keywords{}".format(' (default)' if not DEFAULTS['config']['sort'] else '') ) + + config.add_argument('--raw-values', '--raw', + dest='rawvalues', action='store_true', - default=DEFAULTS['output']['raw'], - help="output raw values (default: {})".format('raw' if DEFAULTS['output']['raw'] else 'process') ) - output.add_argument('--unhide-pw', - dest='unhidepw', + default=DEFAULTS['config']['rawvalues'], + help="output raw values{}".format(' (default)' if DEFAULTS['config']['rawvalues'] else '') ) + config.add_argument('--no-raw-values', + dest='rawvalues', + action='store_false', + default=DEFAULTS['config']['rawvalues'], + help="output human readable values{}".format(' (default)' if not DEFAULTS['config']['rawvalues'] else '') ) + + config.add_argument('--raw-keys', + dest='rawkeys', action='store_true', - default=DEFAULTS['output']['unhide-pw'], - help="unhide passwords (default: {})".format('unhide' if DEFAULTS['output']['unhide-pw'] else 'hide') ) - output.add_argument('-o', '--output-file', + default=DEFAULTS['config']['rawkeys'], + help="output bitfield raw keys{}".format(' (default)' if DEFAULTS['config']['rawkeys'] else '') ) + config.add_argument('--no-raw-keys', + dest='rawkeys', + action='store_false', + default=DEFAULTS['config']['rawkeys'], + help="do not output bitfield raw keys{}".format(' (default)' if not DEFAULTS['config']['rawkeys'] else '') ) + + config.add_argument('--hide-pw', + dest='hidepw', + action='store_true', + default=DEFAULTS['config']['hidepw'], + help="hide passwords{}".format(' (default)' if DEFAULTS['config']['hidepw'] else '') ) + config.add_argument('--unhide-pw', + dest='hidepw', + action='store_false', + default=DEFAULTS['config']['hidepw'], + help="unhide passwords{}".format(' (default)' if not DEFAULTS['config']['hidepw'] else '') ) + + config.add_argument('-o', '--output-file', metavar='', dest='outputfile', - default=DEFAULTS['output']['outputfile'], - help="file to store configuration to (default: {}) Macros: @v=Tasmota version, @f=friendly name".format(DEFAULTS['output']['outputfile'])) - output.add_argument('--output-file-format', + default=DEFAULTS['config']['outputfile'], + help="file to store configuration to (default: {}). Replacements: @v=Tasmota version, @f=friendly name".format(DEFAULTS['config']['outputfile'])) + config.add_argument('--output-file-format', metavar='', dest='outputfileformat', choices=['json', 'binary'], - default=DEFAULTS['output']['outputfileformat'], - help="output format ('json' or 'binary', default: '{}')".format(DEFAULTS['output']['outputfileformat']) ) + default=DEFAULTS['config']['outputfileformat'], + help="output format ('json' or 'binary', default: '{}')".format(DEFAULTS['config']['outputfileformat']) ) parser.add_argument('-c', '--config', metavar='', @@ -2197,14 +3256,14 @@ if __name__ == "__main__": info.add_argument('-V', '--version', action='version', version=PROG) args = parser.parse_args() - + # default no configuration available configobj = None - + # check source args if args.device is not None and args.tasmotafile is not None: exit(6, "Only one source allowed. Do not use -d and -f together") - + # read config direct from device via http if args.device is not None: @@ -2245,51 +3304,29 @@ if __name__ == "__main__": parser.print_help() sys.exit(0) - if configobj is not None and len(configobj)>0: cfg = DeEncrypt(configobj) - config = Decode(cfg) + configuration = Decode(cfg, args.rawvalues) # output to file if args.outputfile is not None: - outputfilename = args.outputfile - v = f1 = f2 = f3 = f4 = '' - if 'version' in config: - ver = int(str(config['version']), 0) - major = ((ver>>24) & 0xff) - minor = ((ver>>16) & 0xff) - release = ((ver>> 8) & 0xff) - subrelease = (ver & 0xff) - if major>=6: - if subrelease>0: - subreleasestr = str(subrelease) - else: - subreleasestr = '' - else: - if subrelease>0: - subreleasestr = str(chr(subrelease+ord('a')-1)) - else: - subreleasestr = '' - v = "{:d}.{:d}.{:d}{}{}".format( major, minor, release, '.' if major>=6 else '', subreleasestr) - outputfilename = outputfilename.replace('@v', v) - if 'friendlyname' in config: - outputfilename = outputfilename.replace('@f', config['friendlyname'][0] ) - + outputfilename = GetFilenameReplaced(args.outputfile, configuration) if args.outputfileformat == 'binary': outputfile = open(outputfilename, "wb") outputfile.write(struct.pack('