diff --git a/tools/decode-config.py b/tools/decode-config.py
index dc51e0f42..284904fb9 100644
--- a/tools/decode-config.py
+++ b/tools/decode-config.py
@@ -19,23 +19,25 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see .
+
Requirements:
- Python
- pip install json pycurl urllib2 configargparse
+
Instructions:
- Execute command with option -d to retrieve config data from device or
- use -f to read out a previously saved configuration file.
+ Execute command with option -d to retrieve config data from a host
+ or use -f to read out a configuration file saved using Tasmota Web-UI
For help execute command with argument -h
Usage:
decode-config.py [-h] [-f ] [-d ] [-u ]
- [-p ] [--format ]
- [--json-indent ] [--json-compact]
- [--sort ] [--raw] [--unhide-pw] [-o ]
- [-c ] [-V]
+ [-p ] [--json-indent ]
+ [--json-compact] [--unsort] [--raw] [--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
@@ -48,89 +50,84 @@ Usage:
-c , --config
Config file, can be used instead of command parameter
(default: None)
+ --exit-on-error-only exit on error only (default: exit on ERROR and
+ WARNING). Not recommended, used by your own
+ responsibility!
source:
-f , --file
file to retrieve Tasmota configuration from (default:
- None)
+ None)'
-d , --device
hostname or IP address to retrieve Tasmota
configuration from (default: None)
-u , --username
- host http access username (default: admin)
+ host HTTP access username (default: admin)
-p , --password
- host 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")
+ (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)
+ normal)
+ --unsort do not sort results (default: sort)
+ --raw output raw values (default: process)
--unhide-pw unhide passwords (default: hide)
-o , --output-file
- file to store decrypted raw binary configuration to
- (default: None)
+ file to store configuration to (default: None) Macros:
+ @v=Tasmota version, @f=friendly name
+ --output-file-format
+ output format ('json' or 'binary', default: 'json')
info:
-V, --version show program's version number and exit
Either argument -d or -f must be given.
-
-Examples:
- Read configuration from hostname 'sonoff1' and output default json config
- ./decode-config.py -d sonoff1
-
- Read configuration from file 'Config__6.2.1.dmp' and output default json config
- ./decode-config.py -f Config__6.2.1.dmp
-
- Read configuration from hostname 'sonoff1' using web login data
- ./decode-config.py -d sonoff1 -u admin -p xxxx
-
- Read configuration from hostname 'sonoff1' using web login data and unhide passwords
- ./decode-config.py -d sonoff1 -u admin -p xxxx --unhide-pw
-
- Read configuration from hostname 'sonoff1' using web login data, unhide passwords
- and sort key names
- ./decode-config.py -d sonoff1 -u admin -p xxxx --unhide-pw --sort name
"""
import os.path
import io
import sys
-import configargparse
-import collections
import struct
import re
-import json
+import math
+from datetime import datetime
+try:
+ 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 pycurl' to install it")
+ 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 urllib2' to install it")
+ print("module not found. Try 'pip install urllib2' to install it")
sys.exit(9)
-VER = '1.5.0009'
+VER = '1.5.0011'
PROG='{} v{} by Norbert Richter'.format(os.path.basename(sys.argv[0]),VER)
CONFIG_FILE_XOR = 0x5A
-
args = {}
DEFAULTS = {
'DEFAULT':
{
'configfile': None,
+ 'exitonwarning':True,
},
'source':
{
@@ -141,15 +138,16 @@ DEFAULTS = {
},
'output':
{
- 'format': 'json',
'jsonindent': None,
'jsoncompact': False,
- 'sort': 'name',
+ 'unsort': False,
'raw': False,
'unhide-pw': False,
'outputfile': None,
+ 'outputfileformat': 'json',
},
}
+exitcode = 0
"""
@@ -163,11 +161,35 @@ Settings dictionary describes the config file fields definition:
format
Define the data interpretation.
- For details see struct module format string
- https://docs.python.org/2.7/library/struct.html#format-strings
+ It is either a string or a tuple containing a string and a
+ sub-Settings dictionary.
+ 'xxx':
+ A string is used to interpret the data at
+ The string defines the format interpretion as described
+ 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
+ prefix length. If no prefix is given, 1 is assumed.
+ {}:
+ A dictionary describes itself a 'Settings' dictonary (recursive)
baseaddr
- The address (starting from 0) within config data
+ The address (starting from 0) within config data.
+ For bit fields must be a tuple.
+ n:
+ Defines a simple address within config data.
+ must be a positive integer.
+ (n, b, s):
+ A tuple defines a bit field:
+
+ is the address within config data (integer)
+
+ how many bits are used (positive integer)
+
+ bit shift (integer)
+ positive shift the result right bits
+ negative shift the result left bits
datadef
Define the field interpretation different from simple
@@ -182,91 +204,74 @@ Settings dictionary describes the config file fields definition:
Defines a one-dimensional array of size
[n, n <,n...>]
Defines a multi-dimensional array
- [{} <,{}...]
- Defines a bit struct. The items are simply dict
- {'bitname', bitlen}, the dict order is important.
convert (optional)
Define an output/conversion methode, can be a simple string
or a previously defined function name.
- 'xxx':
- a string defines a format specification of the string
- formatter, see
- https://docs.python.org/2.7/library/string.html#format-string-syntax
+ 'xxx?':
+ a string will be evaluate as is replacing all '?' chars
+ with the current value. This can also be contain pyhton
+ code.
func:
a function defines the name of a formating function
"""
# config data conversion function and helper
-def baudrate(value):
- return value * 1200
-
def int2ip(value):
return '{:d}.{:d}.{:d}.{:d}'.format(value & 0xff, value>>8 & 0xff, value>>16 & 0xff, value>>24 & 0xff)
-def int2geo(value):
- return float(value) / 1000000
-
def password(value):
if args.unhidepw:
return value
return '********'
-def fingerprintstr(value):
- s = list(value)
- result = ''
- for c in s:
- if c in '0123456789abcdefABCDEF':
- result += c
- return result
-
-
Setting_6_2_1 = {
'cfg_holder': (' from fielddef[0]
"""
+ return fielddef[0]
- length=0
- if fielddef[2] is not None:
- # fielddef[2] contains a array or int
- # calc size recursive by sum of all elements
+def GetFieldBaseAddr(fielddef):
+ """
+ Return the format item of field definition
- # tuple 2 contains a list with integer or an integer value
- if (isinstance(fielddef[2], list) and len(fielddef[2])>0 and isinstance(fielddef[2][0], int)) or isinstance(fielddef[2], int):
- for i in range(0, fielddef[2][0] if isinstance(fielddef[2], list) else fielddef[2] ):
- # multidimensional array
- if isinstance(fielddef[2], list) and len(fielddef[2])>1:
- length += GetFieldLength( (fielddef[0], fielddef[1], fielddef[2][1:]) )
- else:
- length += GetFieldLength( (fielddef[0], fielddef[1], None) )
- else:
- if fielddef[0][-1:].lower() in ['b','c','?']:
- length=1
- elif fielddef[0][-1:].lower() in ['h']:
- length=2
- elif fielddef[0][-1:].lower() in ['i','l','f']:
- length=4
- elif fielddef[0][-1:].lower() in ['q','d']:
- length=8
- elif fielddef[0][-1:].lower() in ['s','p']:
- # s and p needs prefix as length
- match = re.search("\s*(\d+)", fielddef[0])
- if match:
- length=int(match.group(0))
+ @param fielddef:
+ field format - see "Settings dictionary" above
+
+ @return: ,, from fielddef[1]
+
+ """
+ baseaddr = fielddef[1]
+ if isinstance(baseaddr, tuple):
+ return baseaddr[0], baseaddr[1], baseaddr[2]
+
+ return baseaddr, 0, 0
+
+
+def MakeFieldBaseAddr(baseaddr, bitlen, bitshift):
+ """
+ Return a based on given arguments
+
+ @param baseaddr:
+ baseaddr from Settings definition
+ @param bitlen:
+ 0 or bitlen
+ @param bitshift:
+ 0 or bitshift
+
+ @return: (,,) if bitlen != 0
+ baseaddr if bitlen == 0
+
+ """
+ if bitlen!=0:
+ return (baseaddr, bitlen, bitshift)
+ return baseaddr
- # it's a single value
- return length
def ConvertFieldValue(value, fielddef, raw=False):
"""
@@ -1682,21 +1850,94 @@ def ConvertFieldValue(value, fielddef, raw=False):
@param value:
original value read from binary data
@param fielddef
- field definition (contains possible conversion defiinition)
+ field definition - see "Settings dictionary" above
@param raw
return raw values (True) or converted values (False)
@return: (un)converted value
"""
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
- return fielddef[3](value)
+ convert = fielddef[3]
+ if isinstance(convert,str): # evaluate strings
+ try:
+ return eval(convert.replace('?','value'))
+ except:
+ return value
+ elif callable(convert): # use as format function
+ return convert(value)
return value
-def GetField(dobj, fieldname, fielddef, raw=False):
+def GetFieldLength(fielddef):
+ """
+ Return length of a field in bytes based on field format definition
+
+ @param fielddef:
+ field format - see "Settings dictionary" above
+
+ @return: length of field in bytes
+
+ """
+
+ length=0
+ format_ = GetFieldFormat(fielddef)
+
+ # get datadef from field definition
+ datadef = None
+ if len(fielddef)>2:
+ datadef = fielddef[2]
+
+ if datadef is not None:
+ # fielddef[2] contains a array or int
+ # calc size recursive by sum of all elements
+
+ # contains a integer list or an single integer value
+ if (isinstance(datadef, list) \
+ and len(datadef)>0 \
+ and isinstance(datadef[0], int)) \
+ or isinstance(datadef, int):
+
+ for i in range(0, datadef[0] if isinstance(datadef, list) else datadef ):
+
+ # multidimensional array
+ if isinstance(datadef, list) and len(datadef)>1:
+ length += GetFieldLength( (fielddef[0], fielddef[1], fielddef[2][1:]) )
+
+ # single array
+ else:
+ length += GetFieldLength( (fielddef[0], fielddef[1], None) )
+
+ else:
+ if isinstance(fielddef[0], dict):
+ # -> iterate through format_
+ addr = -1
+ setting = fielddef[0]
+ for name in setting:
+ baseaddr, bitlen, bitshift = GetFieldBaseAddr(setting[name])
+ len_ = GetFieldLength(setting[name])
+ if addr != baseaddr:
+ addr = baseaddr
+ length += len_
+
+ else:
+ if format_[-1:].lower() in ['b','c','?']:
+ length=1
+ elif format_[-1:].lower() in ['h']:
+ length=2
+ elif format_[-1:].lower() in ['i','l','f']:
+ length=4
+ elif format_[-1:].lower() in ['q','d']:
+ length=8
+ elif format_[-1:].lower() in ['s','p']:
+ # s and p may have a prefix as length
+ match = re.search("\s*(\d+)", format_)
+ if match:
+ length=int(match.group(0))
+
+ return length
+
+
+def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0):
"""
Get field value from definition
@@ -1714,45 +1955,78 @@ def GetField(dobj, fieldname, fielddef, raw=False):
result = None
- if fielddef[2] is not None:
+ # get format from field definition
+ format_ = GetFieldFormat(fielddef)
+
+ # get baseaddr from field definition
+ baseaddr, bitlen, bitshift = GetFieldBaseAddr(fielddef)
+
+ # get datadef from field definition
+ datadef = None
+ if len(fielddef)>2:
+ datadef = fielddef[2]
+
+ if datadef is not None:
result = []
- # tuple 2 contains a list with integer or an integer value
- if (isinstance(fielddef[2], list) and len(fielddef[2])>0 and isinstance(fielddef[2][0], int)) or isinstance(fielddef[2], int):
- addr = fielddef[1]
- for i in range(0, fielddef[2][0] if isinstance(fielddef[2], list) else fielddef[2] ):
+ # contains a integer list or an single integer value
+ if (isinstance(datadef, list) \
+ and len(datadef)>0 \
+ and isinstance(datadef[0], int)) \
+ or isinstance(datadef, int):
+
+ offset = 0
+ for i in range(0, datadef[0] if isinstance(datadef, list) else datadef):
+
# multidimensional array
- if isinstance(fielddef[2], list) and len(fielddef[2])>1:
- subfielddef = (fielddef[0], addr, fielddef[2][1:], None if len(fielddef)<4 else fielddef[3])
- else: # single array
- subfielddef = (fielddef[0], addr, None, None if len(fielddef)<4 else fielddef[3])
+ if isinstance(datadef, list) and len(datadef)>1:
+ if len(fielddef)<4:
+ subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), datadef[1:])
+ else:
+ subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), datadef[1:], fielddef[3])
+
+ # single array
+ else:
+ if len(fielddef)<4:
+ subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), None)
+ else:
+ subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), None, fielddef[3])
+
length = GetFieldLength(subfielddef)
if length != 0:
- 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, raw);
- union = fielddef[2]
- i = 0
- for l in union:
- for name,bits in l.items():
- bitval = (value & ( ((1<> i
- d[name] = bitval
- i += bits
- result = d
+ result.append(GetField(dobj, fieldname, subfielddef, raw=raw, addroffset=addroffset+offset))
+ offset += length
+
else:
- # it's a single value
- if GetFieldLength(fielddef) != 0:
- result = struct.unpack_from(fielddef[0], dobj, fielddef[1])[0]
- if fielddef[0][-1:].lower() in ['s','p']:
- if ord(result[:1])==0x00 or ord(result[:1])==0xff:
- result = ''
- s = str(result).split('\0')[0]
- result = unicode(s, errors='replace')
- result = ConvertFieldValue(result, fielddef, raw)
+ # contains a dict
+ if isinstance(fielddef[0], dict):
+ # -> iterate through format_
+ setting = fielddef[0]
+ config = {}
+ for name in setting:
+ config[name] = GetField(dobj, name, setting[name], raw=args.raw, addroffset=addroffset)
+ result = config
+ else:
+ # a simple value
+ if GetFieldLength(fielddef) != 0:
+ result = struct.unpack_from(format_, dobj, baseaddr+addroffset)[0]
+
+ if not format_[-1:].lower() in ['s','p']:
+ if bitshift>=0:
+ result >>= bitshift
+ else:
+ result <<= abs(bitshift)
+ if bitlen>0:
+ result &= (1< 127
+ result = unicode(s, errors='ignore')
+
+ result = ConvertFieldValue(result, fielddef, raw)
return result
@@ -1779,6 +2053,8 @@ def Decode(obj):
@param obj:
binary config data (decrypted)
+
+ @return: configuration dictionary
"""
# get header data
version = GetField(obj, 'version', Setting_6_2_1['version'], raw=True)
@@ -1792,16 +2068,20 @@ def Decode(obj):
# if we did not found a mathching setting
if template is None:
- exit(2, "Can't handle Tasmota configuration data for version 0x{:x}".format(version) )
-
+ 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)
- # 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) )
+ # read size should be same as definied in template
+ if cfg_size > template[1]:
+ # 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]:
+ # 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')
# check crc if exists
if 'cfg_crc' in setting:
@@ -1809,25 +2089,34 @@ def Decode(obj):
else:
cfg_crc = GetSettingsCrc(obj)
if cfg_crc != GetSettingsCrc(obj):
- exit(3, 'Data crc error' )
+ exit(4, 'Data CRC error, read 0x{:x} should be 0x{:x}'.format(cfg_crc, GetSettingsCrc(obj)), typ='WARNING', doexit=args.exitonwarning)
config = {}
- config['version_template'] = '0x{:x}'.format(template[0])
for name in setting:
- config[name] = GetField(obj, name, setting[name], args.raw)
+ config[name] = GetField(obj, name, setting[name], raw=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', indent=args.jsonindent, separators=(',', ':') if args.jsoncompact else (', ', ': ') )
- else:
- for key,value in config.items():
- print '{} = {}'.format(key, repr(value))
+ # add header info
+ timestamp = datetime.now()
+ config['header'] = { 'timestamp': timestamp.strftime("%Y-%m-%d %H:%M:%S"),
+ 'data': {
+ 'crc': hex(GetSettingsCrc(obj)),
+ 'size': len(obj),
+ 'template_version': hex(template[0]),
+ 'content': {
+ 'crc': hex(cfg_crc),
+ 'size': cfg_size,
+ 'version': hex(version),
+ },
+ },
+ 'scriptname': os.path.basename(__file__),
+ 'scriptversion': VER,
+ }
+ return config
if __name__ == "__main__":
+ # program argument processing
parser = configargparse.ArgumentParser(description='Decode configuration of Sonoff-Tasmota device.',
epilog='Either argument -d or -f must be given.')
@@ -1836,80 +2125,89 @@ if __name__ == "__main__":
metavar='',
dest='tasmotafile',
default=DEFAULTS['source']['tasmotafile'],
- help='file to retrieve Tasmota configuration from (default: {})'.format(DEFAULTS['source']['tasmotafile']))
+ help="file to retrieve Tasmota configuration from (default: {})'".format(DEFAULTS['source']['tasmotafile']))
source.add_argument('-d', '--device',
metavar='',
dest='device',
default=DEFAULTS['source']['device'],
- help='hostname or IP address to retrieve Tasmota 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='host 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='host 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',
- metavar='',
- dest='format',
- 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']) )
+ 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',
- choices=['none', 'name'],
- default=DEFAULTS['output']['sort'],
- help='sort result - can be "none" or "name" (default: "{}")'.format(DEFAULTS['output']['sort']) )
+ help="compact JSON output by eliminate whitespace (default: {})".format('normal' if not DEFAULTS['output']['jsoncompact'] else 'compact') )
+ output.add_argument('--unsort',
+ dest='unsort',
+ 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',
action='store_true',
default=DEFAULTS['output']['raw'],
- help='output raw values (default: {})'.format('raw' if DEFAULTS['output']['raw'] else 'processed') )
+ help="output raw values (default: {})".format('raw' if DEFAULTS['output']['raw'] else 'process') )
output.add_argument('--unhide-pw',
dest='unhidepw',
action='store_true',
default=DEFAULTS['output']['unhide-pw'],
- help='unhide passwords (default: {})'.format('unhide' if DEFAULTS['output']['unhide-pw'] else 'hide') )
+ help="unhide passwords (default: {})".format('unhide' if DEFAULTS['output']['unhide-pw'] else 'hide') )
output.add_argument('-o', '--output-file',
metavar='',
dest='outputfile',
default=DEFAULTS['output']['outputfile'],
- help='file to store decrypted raw binary configuration to (default: {})'.format(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',
+ metavar='',
+ dest='outputfileformat',
+ choices=['json', 'binary'],
+ default=DEFAULTS['output']['outputfileformat'],
+ help="output format ('json' or 'binary', default: '{}')".format(DEFAULTS['output']['outputfileformat']) )
parser.add_argument('-c', '--config',
metavar='',
dest='configfile',
default=DEFAULTS['DEFAULT']['configfile'],
is_config_file=True,
- help='Config file, can be used instead of command parameter (default: {})'.format(DEFAULTS['DEFAULT']['configfile']) )
+ help="Config file, can be used instead of command parameter (default: {})".format(DEFAULTS['DEFAULT']['configfile']) )
+ parser.add_argument('--exit-on-error-only',
+ dest='exitonwarning',
+ action='store_false',
+ default=DEFAULTS['DEFAULT']['exitonwarning'],
+ help="exit on error only (default: {}). Not recommended, used by your own responsibility!".format('exit on ERROR and WARNING' if DEFAULTS['DEFAULT']['exitonwarning'] else 'exit on ERROR') )
info = parser.add_argument_group('info')
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:
- # read config direct from device via http
buffer = io.BytesIO()
url = str("http://{}/dl".format(args.device))
c = pycurl.Curl()
@@ -1930,11 +2228,11 @@ if __name__ == "__main__":
configobj = buffer.getvalue()
+ # read config from a file
elif args.tasmotafile is not None:
- # read config from a file
if not os.path.isfile(args.tasmotafile): # check file exists
- exit(1, "file '{}' not found".format(args.tasmotafile))
+ exit(1, "File '{}' not found".format(args.tasmotafile))
try:
tasmotafile = open(args.tasmotafile, "rb")
configobj = tasmotafile.read()
@@ -1942,22 +2240,59 @@ if __name__ == "__main__":
except Exception, e:
exit(e[0], e[1])
+ # no config source given
else:
parser.print_help()
sys.exit(0)
+
if configobj is not None and len(configobj)>0:
cfg = DeEncrypt(configobj)
- if args.outputfile is not None:
- outputfile = open(args.outputfile, "wb")
- outputfile.write(cfg)
- outputfile.close()
+ config = Decode(cfg)
- Decode(cfg)
+ # 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] )
+
+ if args.outputfileformat == 'binary':
+ outputfile = open(outputfilename, "wb")
+ outputfile.write(struct.pack('