diff --git a/tools/decode-config.html b/tools/decode-config.html index 4fa7b4e04..dd24495b7 100644 --- a/tools/decode-config.html +++ b/tools/decode-config.html @@ -4,7 +4,7 @@

Comparing backup files created by decode-config.py and *.dmp files created by Tasmota "Backup/Restore Configuration":

@@ -69,6 +69,7 @@
  • Use batch processing
  • +
  • Notes
  • @@ -190,7 +191,7 @@ WifiConfig 5

    Note: A few very specific module commands like MPC230xx, KNX and some Display commands are not supported. These are still available by JSON output.

    Filter data

    -

    The huge number of Tasomta configuration data can be overstrained and confusing, so the most of the configuration data are grouped into categories.

    +

    The huge number of Tasmota configuration data can be overstrained and confusing, so the most of the configuration data are grouped into categories.

    With decode-config.py the following categories are available: Display, Domoticz, Internal, KNX, Led, Logging, MCP230xx, MQTT, Main, Management, Pow, Sensor, Serial, SetOption, SonoffRF, System, Timers, Wifi

    These are similary to the categories on https://github.com/arendst/Sonoff-Tasmota/wiki/Commands.

    To filter outputs to a subset of groups use the -g or --group arg concatenating the grooup you want, e. g.

    @@ -247,12 +248,16 @@ -i, --restore-file <filename> file to restore configuration from (default: None). - Replacements: @v=firmware version, @f=device friendly - name, @h=device hostname + Replacements: @v=firmware version from config, + @f=device friendly name from config, @h=device + hostname from config, @H=device hostname from device + (-d arg only) -o, --backup-file <filename> file to backup configuration to (default: None). - Replacements: @v=firmware version, @f=device friendly - name, @h=device hostname + Replacements: @v=firmware version from config, + @f=device friendly name from config, @h=device + hostname from config, @H=device hostname from device + (-d arg only) -t, --backup-type json|bin|dmp backup filetype (default: 'json') -E, --extension append filetype extension for -i and -o filename @@ -339,3 +344,12 @@ json-indent 2

    or under windows

    for device in (sonoff1 sonoff2 sonoff3) do python decode-config.py -c my.conf -d %device -o Config_@f_@v
     

    will produce JSON configuration files for host sonoff1, sonoff2 and sonoff3 using friendly name and Tasmota firmware version for backup filenames.

    +

    Notes

    +

    Some general notes:

    + diff --git a/tools/decode-config.md b/tools/decode-config.md index dab16a2c8..97978114a 100644 --- a/tools/decode-config.md +++ b/tools/decode-config.md @@ -4,7 +4,7 @@ _decode-config.py_ is able to backup and restore Sonoff-Tasmota configuration. In contrast to the Tasmota build-in "Backup/Restore Configuration" function, * _decode-config.py_ uses human readable and editable [JSON](http://www.json.org/)-format for backup/restore, * _decode-config.py_ can restore previous backuped and changed [JSON](http://www.json.org/)-format files, -* _decode-config.py_ is able to create Tasomta commands based on given configuration +* _decode-config.py_ is able to create Tasmota commands based on given configuration Comparing backup files created by *decode-config.py* and *.dmp files created by Tasmota "Backup/Restore Configuration": @@ -38,6 +38,7 @@ _decode-config.py_ is able to handle Tasmota configurations for release version * [Config file](decode-config.md#config-file) * [Using Tasmota binary configuration files](decode-config.md#using-tasmota-binary-configuration-files) * [Use batch processing](decode-config.md#use-batch-processing) + * [Notes](decode-config.md#notes) ## Prerequisite * [Python](https://en.wikipedia.org/wiki/Python_(programming_language)) @@ -191,7 +192,7 @@ Example: Note: A few very specific module commands like MPC230xx, KNX and some Display commands are not supported. These are still available by JSON output. ### Filter data -The huge number of Tasomta configuration data can be overstrained and confusing, so the most of the configuration data are grouped into categories. +The huge number of Tasmota configuration data can be overstrained and confusing, so the most of the configuration data are grouped into categories. With _decode-config.py_ the following categories are available: `Display`, `Domoticz`, `Internal`, `KNX`, `Led`, `Logging`, `MCP230xx`, `MQTT`, `Main`, `Management`, `Pow`, `Sensor`, `Serial`, `SetOption`, `SonoffRF`, `System`, `Timers`, `Wifi` @@ -266,12 +267,16 @@ For advanced help use `-H` or `--full-help`: -i, --restore-file file to restore configuration from (default: None). - Replacements: @v=firmware version, @f=device friendly - name, @h=device hostname + Replacements: @v=firmware version from config, + @f=device friendly name from config, @h=device + hostname from config, @H=device hostname from device + (-d arg only) -o, --backup-file file to backup configuration to (default: None). - Replacements: @v=firmware version, @f=device friendly - name, @h=device hostname + Replacements: @v=firmware version from config, + @f=device friendly name from config, @h=device + hostname from config, @H=device hostname from device + (-d arg only) -t, --backup-type json|bin|dmp backup filetype (default: 'json') -E, --extension append filetype extension for -i and -o filename @@ -374,3 +379,12 @@ or under windows for device in (sonoff1 sonoff2 sonoff3) do python decode-config.py -c my.conf -d %device -o Config_@f_@v will produce JSON configuration files for host sonoff1, sonoff2 and sonoff3 using friendly name and Tasmota firmware version for backup filenames. + +## Notes +Some general notes: +* Filename replacement macros **@h** and **@H**: + * **@h** +The **@h** replacement macro uses the hostname configured with the Tasomta Wifi `Hostname ` command (defaults to `%s-%04d`). It will not use the network hostname of your device because this is not available when working with files only (e.g. `--file ` as source). +To prevent having a useless % in your filename, **@h** will not replaced by configuration data hostname if this contains '%' characters. + * **@H** +If you want to use the network hostname within your filename, use the **@H** replacement macro instead - but be aware this will only replaced if you are using a network device as source (`-d`, `--device`, `--host`); it will not work when using a file as source (`-f`, `--file`) diff --git a/tools/decode-config.py b/tools/decode-config.py index d6632dbda..a6299f104 100755 --- a/tools/decode-config.py +++ b/tools/decode-config.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -VER = '2.1.0006' +VER = '2.1.0007' """ decode-config.py - Backup/Restore Sonoff-Tasmota configuration data Copyright (C) 2018 Norbert Richter - This program is free software: you can redistribute it and/or modfy + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -73,12 +73,16 @@ Usage: decode-config.py [-f ] [-d ] [-P ] -i, --restore-file file to restore configuration from (default: None). - Replacements: @v=firmware version, @f=device friendly - name, @h=device hostname + Replacements: @v=firmware version from config, + @f=device friendly name from config, @h=device + hostname from config, @H=device hostname from device + (-d arg only) -o, --backup-file file to backup configuration to (default: None). - Replacements: @v=firmware version, @f=device friendly - name, @h=device hostname + Replacements: @v=firmware version from config, + @f=device friendly name from config, @h=device + hostname from config, @H=device hostname from device + (-d arg only) -t, --backup-type json|bin|dmp backup filetype (default: 'json') -E, --extension append filetype extension for -i and -o filename @@ -461,7 +465,7 @@ Setting_5_10_0 = { 'altitude': ('= 0 and args.device is not None: + device_hostname = GetTasmotaHostname(args.device, args.port, username=args.username, password=args.password) + if device_hostname is None: + device_hostname = '' + + filename = filename.replace('@v', config_version) + filename = filename.replace('@f', config_friendlyname ) + filename = filename.replace('@h', config_hostname ) + filename = filename.replace('@H', device_hostname ) + dirname = basename = ext = '' name = filename @@ -1196,6 +1212,94 @@ def LoadTasmotaConfig(filename): return encode_cfg +def TasmotaGet(cmnd, host, port, username=DEFAULTS['source']['username'], password=None, contenttype = None): + """ + Tasmota http request + + @param host: + hostname or IP of Tasmota device + @param port: + http port of Tasmota device + @param username: + optional username for Tasmota web login + @param password + optional password for Tasmota web login + + @return: + binary config data (encrypted) or None on error + """ + body = None + + # read config direct from device via http + c = pycurl.Curl() + buffer = io.BytesIO() + c.setopt(c.WRITEDATA, buffer) + header = HTTPHeader() + c.setopt(c.HEADERFUNCTION, header.store) + c.setopt(c.FOLLOWLOCATION, True) + c.setopt(c.URL, MakeUrl(host, port, cmnd)) + if username is not None and password is not None: + c.setopt(c.HTTPAUTH, c.HTTPAUTH_BASIC) + c.setopt(c.USERPWD, username + ':' + password) + c.setopt(c.HTTPGET, True) + c.setopt(c.VERBOSE, False) + + responsecode = 200 + try: + c.perform() + responsecode = c.getinfo(c.RESPONSE_CODE) + response = header.response() + except Exception, e: + exit(e[0], e[1],line=inspect.getlineno(inspect.currentframe())) + finally: + c.close() + + if responsecode >= 400: + exit(responsecode, 'HTTP result: {}'.format(header.response()),line=inspect.getlineno(inspect.currentframe())) + elif contenttype is not None and header.contenttype()!=contenttype: + exit(ExitCode.DOWNLOAD_CONFIG_ERROR, "Device did not response properly, may be Tasmota webserver admin mode is disabled (WebServer 2)",line=inspect.getlineno(inspect.currentframe())) + + try: + body = buffer.getvalue() + except: + pass + + return responsecode, body + + +def GetTasmotaHostname(host, port, username=DEFAULTS['source']['username'], password=None): + """ + Get Tasmota hostname from device + + @param host: + hostname or IP of Tasmota device + @param port: + http port of Tasmota device + @param username: + optional username for Tasmota web login + @param password + optional password for Tasmota web login + + @return: + Tasmota real hostname or None on error + """ + hostname = None + + loginstr = "" + if password is not None: + loginstr = "user={}&password={}&".format(urllib2.quote(username), urllib2.quote(password)) + # get hostname + responsecode, body = TasmotaGet("cm?{}cmnd=status%205".format(loginstr), host, port, username=username, password=password) + if body is not None: + jsonbody = json.loads(body) + if "StatusNET" in jsonbody and "Hostname" in jsonbody["StatusNET"]: + hostname = jsonbody["StatusNET"]["Hostname"] + if args.verbose: + message("Hostname for '{}' retrieved: '{}'".format(host, hostname), typ=LogType.INFO) + + return hostname + + def PullTasmotaConfig(host, port, username=DEFAULTS['source']['username'], password=None): """ Pull config from Tasmota device @@ -1212,43 +1316,9 @@ def PullTasmotaConfig(host, port, username=DEFAULTS['source']['username'], passw @return: binary config data (encrypted) or None on error """ + responsecode, body = TasmotaGet('dl', host, port, username, password, contenttype='application/octet-stream') - encode_cfg = None - - # read config direct from device via http - c = pycurl.Curl() - buffer = io.BytesIO() - c.setopt(c.WRITEDATA, buffer) - header = HTTPHeader() - c.setopt(c.HEADERFUNCTION, header.store) - c.setopt(c.FOLLOWLOCATION, True) - c.setopt(c.URL, MakeUrl(host, port, 'dl')) - if username is not None and password is not None: - c.setopt(c.HTTPAUTH, c.HTTPAUTH_BASIC) - c.setopt(c.USERPWD, username + ':' + password) - c.setopt(c.VERBOSE, False) - - responsecode = 200 - try: - c.perform() - responsecode = c.getinfo(c.RESPONSE_CODE) - response = header.response() - except Exception, e: - exit(e[0], e[1],line=inspect.getlineno(inspect.currentframe())) - finally: - c.close() - - if responsecode >= 400: - exit(responsecode, 'HTTP result: {}'.format(header.response()),line=inspect.getlineno(inspect.currentframe())) - elif header.contenttype()!='application/octet-stream': - exit(ExitCode.DOWNLOAD_CONFIG_ERROR, "Device did not response properly, may be Tasmota webserver admin mode is disabled (WebServer 2)",line=inspect.getlineno(inspect.currentframe())) - - try: - encode_cfg = buffer.getvalue() - except: - pass - - return encode_cfg + return body def PushTasmotaConfig(encode_cfg, host, port, username=DEFAULTS['source']['username'], password=None): @@ -1273,40 +1343,21 @@ def PushTasmotaConfig(encode_cfg, host, port, username=DEFAULTS['source']['usern if isinstance(encode_cfg, bytearray): encode_cfg = str(encode_cfg) - c = pycurl.Curl() - buffer = io.BytesIO() - c.setopt(c.WRITEDATA, buffer) - header = HTTPHeader() - c.setopt(c.HEADERFUNCTION, header.store) - c.setopt(c.FOLLOWLOCATION, True) # get restore config page first to set internal Tasmota vars - c.setopt(c.URL, MakeUrl(host, port, 'rs?')) - if args.username is not None and args.password is not None: - c.setopt(c.HTTPAUTH, c.HTTPAUTH_BASIC) - c.setopt(c.USERPWD, args.username + ':' + args.password) - c.setopt(c.HTTPGET, True) - c.setopt(c.VERBOSE, False) - - responsecode = 200 - try: - c.perform() - responsecode = c.getinfo(c.RESPONSE_CODE) - except Exception, e: - c.close() - return e[0], e[1] - - if responsecode >= 400: - c.close() - return responsecode, header.response() - elif header.contenttype() != 'text/html': - c.close() - return ExitCode.UPLOAD_CONFIG_ERROR, "Device did not response properly, may be Tasmota webserver admin mode is disabled (WebServer 2)" + responsecode, body = TasmotaGet('rs?', host, port, username, password, contenttype='text/html') + if body is None: + return responsecode, "ERROR" # post data - header.clear() + c = pycurl.Curl() + header = HTTPHeader() c.setopt(c.HEADERFUNCTION, header.store) + c.setopt(c.WRITEFUNCTION, lambda x: None) c.setopt(c.POST, 1) c.setopt(c.URL, MakeUrl(host, port, 'u2')) + if username is not None and password is not None: + c.setopt(c.HTTPAUTH, c.HTTPAUTH_BASIC) + c.setopt(c.USERPWD, username + ':' + password) try: isfile = os.path.isfile(encode_cfg) except: @@ -2501,12 +2552,12 @@ def ParseArgs(): metavar='', dest='restorefile', default=DEFAULTS['backup']['backupfile'], - help="file to restore configuration from (default: {}). Replacements: @v=firmware version, @f=device friendly name, @h=device hostname".format(DEFAULTS['backup']['restorefile'])) + help="file to restore configuration from (default: {}). Replacements: @v=firmware version from config, @f=device friendly name from config, @h=device hostname from config, @H=device hostname from device (-d arg only)".format(DEFAULTS['backup']['restorefile'])) backup.add_argument('-o', '--backup-file', metavar='', dest='backupfile', default=DEFAULTS['backup']['backupfile'], - help="file to backup configuration to (default: {}). Replacements: @v=firmware version, @f=device friendly name, @h=device hostname".format(DEFAULTS['backup']['backupfile'])) + help="file to backup configuration to (default: {}). Replacements: @v=firmware version from config, @f=device friendly name from config, @h=device hostname from config, @H=device hostname from device (-d arg only)".format(DEFAULTS['backup']['backupfile'])) backup_file_formats = ['json', 'bin', 'dmp'] backup.add_argument('-t', '--backup-type', metavar='|'.join(backup_file_formats),