diff --git a/tools/decode-config.html b/tools/decode-config.html
new file mode 100644
index 000000000..dd3d43d01
--- /dev/null
+++ b/tools/decode-config.html
@@ -0,0 +1,230 @@
+
decode-config.py
+decode-config.py backup and restore Sonoff-Tasmota configuration.
+Comparing backup files created by decode-config.py and *.dmp files created by Tasmota "Backup/Restore Configuration":
+
+
+
+ |
+decode-config.py *.json file |
+Sonoff-Tasmota *.dmp file |
+
+
+
+
+Encrypted |
+No |
+Yes |
+
+
+Readable |
+Yes |
+No |
+
+
+Simply editable |
+Yes |
+No |
+
+
+Simply batch processing |
+Yes |
+No |
+
+
+
+decode-config.py handles Tasmota configurations for release version since 5.10.0 up to now.
+Content
+
+Prerequisite
+
+- Python)
This program is written in Python) so you need to install a python environment (for details see Python Setup and Usage)
+- Sonoff-Tasmota Firmware with enabled Web-Server
To backup or restore configurations from/to a Sonoff-Tasmota device you need a firmare with enabled web-server in admin mode (command WebServer 2).
+
Only self compiled firmware may do not have a web-server sod if you use your own compiled firmware be aware to enable the web-server, otherwise you can only use the --file
parameter as source.
+
+File Types
+decode-config.py can handle the following backup file types:
+
+Configuration data as used by Tasmota "Backup/Restore Configuration" web interface.
This format is binary and encrypted.
+
+Configuration data in JSON-format.
This format is decrypted, human readable and editable and can also be used for the --restore-file
command.
This file will becreated by decode-config.py using --backup-file
with --backup-type json
parameter (default).
+
+Configuration data in binary format.
This format is binary decryptet, editable (e.g. using a hex editor) and can also be used for --restore-file
command.
It will be created by decode-config.py using --backup-file
with --backup-type bin
.
Note:
This file is 4 byte longer than an original .dmp file due to an prefix header at the beginning. The file data starting at address position 4 are containing the same as the struct SYSCFG from Tasmota settings.h in decrypted format.
+File extensions
+decode-config.py uses auto extension as default for backup filenames; you don't need to append extensions to your backup file, it will be selected based on --backup-type
argument.
If you want using your own extension use the --no-extension
argument.
+Usage
+After download don't forget to set exec flag under linux with chmod +x decode-config.py
or call the program using python decode-config.py...
.
+Basics
+At least pass a source where you want to read the configuration data from using -f <filename>
or -d <host>
:
+The source can be either
+
+- a Tasmota device hostname or IP by passing it using the
-d <host>
arg
+- or a previously stored Tasmota *.dmp
configuration file by passing the filename using
-f ` arg
+
+Example:
+decode-config.py -d sonoff-4281
+
will output a human readable configuration in JSON-format:
+{
+ "altitude": 112,
+ "baudrate": 115200,
+ "blinkcount": 10,
+ "blinktime": 10,
+...
+ "ws_width": [
+ 1,
+ 3,
+ 5
+ ]
+}
+
Save backup file
+To save the output as backup file --backup-file <filename>
, you can use placeholder for Version, Friendlyname and Hostname:
+decode-config.py -d sonoff-4281 --backup-file Config_@f_@v
+
If you have setup a WebPassword within Tasmota, use
+decode-config.py -d sonoff-4281 -p <yourpassword> --backup-file Config_@f_@v
+
will create a file like Config_Sonoff_x.x.x.json
. Because it is in JSON format, you can read and edit the file with any raw text editor.
+Restore backup file
+Reading back a saved (and possible changed) backup file use the --restore-file <filename>
arg. This will read the (changed) configuration data from this file and send it back to the source device or filename.
+To restore the previously save backup file Config_Sonoff_6.2.1.json
to device sonoff-4281
use:
+decode-config.py -d sonoff-4281 --restore-file Config_Sonoff_6.2.1.json
+
with password set by WebPassword:
+decode-config.py -d sonoff-4281 -p <yourpassword> --restore-file Config_Sonoff_6.2.1.json
+
Configuration file
+Each argument that start with --
(eg. --file
) can also be set in a config file (specified via -c). Config file syntax allows: key=value, flag=true, stuff=[a,b,c] (for details, see syntax at https://pypi.org/project/ConfigArgParse).
+If an argument is specified in more than one place, then commandline values override config file values which override defaults. This is usefull if you always use the same argument or a basic set of arguments.
+The http authentication credentials --username
and --password
is predestinated to store it in a file instead using it on your command line as argument:
+e.g. my.conf:
+[source]
+username = admin
+password = myPaszxwo!z
+
To make a backup file from example above you can now pass the config file instead using the password on command line:
+decode-config.py -d sonoff-4281 -c my.conf --backup-file Config_@f_@v
+
More program arguments
+For better reading your porgram arguments each short written arg (minus sign -
) has a corresponding readable long version (two minus signs --
), eg. --device
for -d
or --file
for -f
(note: not even all --
arg has a corresponding -
one).
+A short list of possible program args is displayed using -h
or --help
.
+For advanced help use -H
or --full-help
:
+usage: decode-config.py [-f <filename>] [-d <host>] [-P <port>]
+ [-u <username>] [-p <password>] [-i <filename>]
+ [-o <filename>] [-F json|bin|dmp] [-E] [-e]
+ [
+ [
+ [-V] [-c <filename>] [
+
+Backup/Restore Sonoff-Tasmota configuration data. Args that start with '
+(eg. -f) can also be set in a config file (specified via -c). Config file
+syntax allows: key=value, flag=true, stuff=[a,b,c] (for details, see syntax at
+https://goo.gl/R74nmi). If an arg is specified in more than one place, then
+commandline values override config file values which override defaults.
+
+optional arguments:
+ -c,
+ program config file - can be used to set default
+ command args (default: None)
+
+ own responsibility!
+
+Source:
+ Read/Write Tasmota configuration from/to
+
+ -f,
+ file to retrieve/write Tasmota configuration from/to
+ (default: None)'
+ -d,
+ hostname or IP address to retrieve/send Tasmota
+ configuration from/to (default: None)
+ -P,
+ (default: 80)
+ -u,
+ host HTTP access username (default: admin)
+ -p,
+ host HTTP access password (default: None)
+
+Backup/Restore:
+ Backup/Restore configuration file specification
+
+ -i,
+ file to restore configuration from (default: None).
+ Replacements: @v=firmware version, @f=device friendly
+ name, @h=device hostname
+ -o,
+ file to backup configuration to (default: None).
+ Replacements: @v=firmware version, @f=device friendly
+ name, @h=device hostname
+ -F,
+ backup filetype (default: 'json')
+ -E,
+ (default)
+ -e,
+ filename as passed
+
+JSON:
+ JSON backup format specification
+
+
+ pretty-printed JSON output using indent level
+ (default: 'None'). -1 disables indent.
+
+
+
+
+Info:
+ additional information
+
+ -h,
+ -H,
+ -v,
+ -V,
+
+Either argument -d <host> or -f <filename> must be given.
+
Examples
+The most of the examples are for linux command line. Under Windows call the program using python decode-config.py ...
.
+Config file
+Note: The example contains .ini style sections [...]
. Sections are always treated as comment and serves as clarity only.
+For further details of config file syntax see https://pypi.org/project/ConfigArgParse.
+my.conf
+[Source]
+username = admin
+password = myPaszxwo!z
+
+[JSON]
+json-indent 2
+
Using Tasmota binary configuration files
+
+Restore a Tasmota configuration file
+ decode-config.py -c my.conf -d sonoff --restore-file Config_Sonoff_6.2.1.dmp
+
+Backup device using Tasmota configuration compatible format
+a) use file extension to choice the file format
+ decode-config.py -c my.conf -d sonoff --backup-file Config_@f_@v.dmp
+b) use args to choice the file format
+ decode-config.py -c my.conf -d sonoff --backup-type dmp --backup-file Config_@f_@v
+
+
+Use batch processing
+for device in sonoff1 sonoff2 sonoff3; do ./decode-config.py -c my.conf -d $device -o Config_@f_@v
+
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.
diff --git a/tools/decode-config.md b/tools/decode-config.md
new file mode 100644
index 000000000..d1cb05bd8
--- /dev/null
+++ b/tools/decode-config.md
@@ -0,0 +1,253 @@
+# decode-config.py
+_decode-config.py_ backup and restore Sonoff-Tasmota configuration.
+
+Comparing backup files created by *decode-config.py* and *.dmp files created by Tasmota "Backup/Restore Configuration":
+
+| | decode-config.py
*.json file | Sonoff-Tasmota
*.dmp file |
+|-------------------------|:-------------------------------:|:-----------------------------------:|
+| Encrypted | No | Yes |
+| Readable | Yes | No |
+| Simply editable | Yes | No |
+| Simply batch processing | Yes | No |
+
+_decode-config.py_ handles Tasmota configurations for release version since 5.10.0 up to now.
+
+# Content
+* [Prerequisite](decode-config.md#prerequisite)
+* [File Types](decode-config.md#file-types)
+ * [.dmp File Format](decode-config.md#-dmp-file-format)
+ * [.json File Format](decode-config.md#-json-file-format)
+ * [.bin File Format](decode-config.md#-bin-file-format)
+ * [File extensions](decode-config.md#file-extensions)
+* [Usage](decode-config.md#usage)
+ * [Basics](decode-config.md#basics)
+ * [Save backup file](decode-config.md#save-backup-file)
+ * [Restore backup file](decode-config.md#restore-backup-file)
+ * [Configuration file](decode-config.md#configuration-file)
+ * [More program arguments](decode-config.md#more-program-arguments)
+ * [Examples](decode-config.md#examples)
+ * [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)
+
+## Prerequisite
+* [Python](https://en.wikipedia.org/wiki/Python_(programming_language))
+ This program is written in [Python](https://en.wikipedia.org/wiki/Python_(programming_language)) so you need to install a python environment (for details see [Python Setup and Usage](https://docs.python.org/2.7/using/index.html))
+* [Sonoff-Tasmota](https://github.com/arendst/Sonoff-Tasmota) [Firmware](https://github.com/arendst/Sonoff-Tasmota/releases) with enabled Web-Server
+ To backup or restore configurations from/to a Sonoff-Tasmota device you need a firmare with enabled web-server in admin mode (command [WebServer 2](https://github.com/arendst/Sonoff-Tasmota/wiki/Commands#wifi)).
+
Only self compiled firmware may do not have a web-server sod if you use your own compiled firmware be aware to enable the web-server, otherwise you can only use the `--file` parameter as source.
+
+## File Types
+_decode-config.py_ can handle the following backup file types:
+### .dmp Format
+Configuration data as used by Tasmota "Backup/Restore Configuration" web interface.
+This format is binary and encrypted.
+### .json Format
+Configuration data in [JSON](http://www.json.org/)-format.
+This format is decrypted, human readable and editable and can also be used for the `--restore-file` command.
+This file will becreated by _decode-config.py_ using `--backup-file` with `--backup-type json` parameter (default).
+### .bin Format
+Configuration data in binary format.
+This format is binary decryptet, editable (e.g. using a hex editor) and can also be used for `--restore-file` command.
+It will be created by _decode-config.py_ using `--backup-file` with `--backup-type bin`.
+Note:
+This file is 4 byte longer than an original .dmp file due to an prefix header at the beginning. The file data starting at address position 4 are containing the same as the **struct SYSCFG** from Tasmota [settings.h](https://github.com/arendst/Sonoff-Tasmota/blob/master/sonoff/settings.h) in decrypted format.
+
+#### File extensions
+_decode-config.py_ uses auto extension as default for backup filenames; you don't need to append extensions to your backup file, it will be selected based on `--backup-type` argument.
+If you want using your own extension use the `--no-extension` argument.
+
+## Usage
+After download don't forget to set exec flag under linux with `chmod +x decode-config.py` or call the program using `python decode-config.py...`.
+
+### Basics
+At least pass a source where you want to read the configuration data from using `-f ` or `-d `:
+
+The source can be either
+* a Tasmota device hostname or IP by passing it using the `-d ` arg
+* or a previously stored Tasmota *.dmp` configuration file by passing the filename using `-f ` arg
+
+Example:
+
+ decode-config.py -d sonoff-4281
+
+will output a human readable configuration in [JSON](http://www.json.org/)-format:
+
+ {
+ "altitude": 112,
+ "baudrate": 115200,
+ "blinkcount": 10,
+ "blinktime": 10,
+ ...
+ "ws_width": [
+ 1,
+ 3,
+ 5
+ ]
+ }
+
+
+### Save backup file
+To save the output as backup file `--backup-file `, you can use placeholder for Version, Friendlyname and Hostname:
+
+ decode-config.py -d sonoff-4281 --backup-file Config_@f_@v
+
+If you have setup a WebPassword within Tasmota, use
+
+ decode-config.py -d sonoff-4281 -p --backup-file Config_@f_@v
+
+will create a file like `Config_Sonoff_x.x.x.json`. Because it is in JSON format, you can read and edit the file with any raw text editor.
+
+### Restore backup file
+Reading back a saved (and possible changed) backup file use the `--restore-file ` arg. This will read the (changed) configuration data from this file and send it back to the source device or filename.
+
+To restore the previously save backup file `Config_Sonoff_6.2.1.json` to device `sonoff-4281` use:
+
+ decode-config.py -d sonoff-4281 --restore-file Config_Sonoff_6.2.1.json
+
+with password set by WebPassword:
+
+ decode-config.py -d sonoff-4281 -p --restore-file Config_Sonoff_6.2.1.json
+
+### Configuration file
+Each argument that start with `--` (eg. `--file`) can also be set in a config file (specified via -c). Config file syntax allows: key=value, flag=true, stuff=[a,b,c] (for details, see syntax at [https://pypi.org/project/ConfigArgParse](https://pypi.org/project/ConfigArgParse/)).
+
+If an argument is specified in more than one place, then commandline values override config file values which override defaults. This is usefull if you always use the same argument or a basic set of arguments.
+
+The http authentication credentials `--username` and `--password` is predestinated to store it in a file instead using it on your command line as argument:
+
+e.g. my.conf:
+
+ [source]
+ username = admin
+ password = myPaszxwo!z
+
+To make a backup file from example above you can now pass the config file instead using the password on command line:
+
+ decode-config.py -d sonoff-4281 -c my.conf --backup-file Config_@f_@v
+
+
+
+### More program arguments
+For better reading your porgram arguments each short written arg (minus sign `-`) has a corresponding readable long version (two minus signs `--`), eg. `--device` for `-d` or `--file` for `-f` (note: not even all `--` arg has a corresponding `-` one).
+
+A short list of possible program args is displayed using `-h` or `--help`.
+
+For advanced help use `-H` or `--full-help`:
+
+ usage: decode-config.py [-f ] [-d ] [-P ]
+ [-u ] [-p ] [-i ]
+ [-o ] [-F json|bin|dmp] [-E] [-e]
+ [--json-indent ] [--json-compact]
+ [--json-hide-pw] [--json-unhide-pw] [-h] [-H] [-v]
+ [-V] [-c ] [--ignore-warnings]
+
+ Backup/Restore Sonoff-Tasmota configuration data. Args that start with '--'
+ (eg. -f) can also be set in a config file (specified via -c). Config file
+ syntax allows: key=value, flag=true, stuff=[a,b,c] (for details, see syntax at
+ https://goo.gl/R74nmi). If an arg is specified in more than one place, then
+ commandline values override config file values which override defaults.
+
+ optional arguments:
+ -c, --config
+ program config file - can be used to set default
+ command args (default: None)
+ --ignore-warnings do not exit on warnings. Not recommended, used by your
+ own responsibility!
+
+ Source:
+ Read/Write Tasmota configuration from/to
+
+ -f, --file, --tasmota-file
+ file to retrieve/write Tasmota configuration from/to
+ (default: None)'
+ -d, --device, --host
+ hostname or IP address to retrieve/send Tasmota
+ configuration from/to (default: None)
+ -P, --port TCP/IP port number to use for the host connection
+ (default: 80)
+ -u, --username
+ host HTTP access username (default: admin)
+ -p, --password
+ host HTTP access password (default: None)
+
+ Backup/Restore:
+ Backup/Restore configuration file specification
+
+ -i, --restore-file
+ file to restore configuration from (default: None).
+ Replacements: @v=firmware version, @f=device friendly
+ name, @h=device hostname
+ -o, --backup-file
+ file to backup configuration to (default: None).
+ Replacements: @v=firmware version, @f=device friendly
+ name, @h=device hostname
+ -F, --backup-type json|bin|dmp
+ backup filetype (default: 'json')
+ -E, --extension append filetype extension for -i and -o filename
+ (default)
+ -e, --no-extension do not append filetype extension, use -i and -o
+ filename as passed
+
+ JSON:
+ JSON backup format specification
+
+ --json-indent
+ pretty-printed JSON output using indent level
+ (default: 'None'). -1 disables indent.
+ --json-compact compact JSON output by eliminate whitespace
+ --json-hide-pw hide passwords (default)
+ --json-unhide-pw unhide passwords
+
+ Info:
+ additional information
+
+ -h, --help show usage help message and exit
+ -H, --full-help show full help message and exit
+ -v, --verbose produce more output about what the program does
+ -V, --version show program's version number and exit
+
+ Either argument -d or -f must be given.
+
+
+### Examples
+The most of the examples are for linux command line. Under Windows call the program using `python decode-config.py ...`.
+
+#### Config file
+Note: The example contains .ini style sections `[...]`. Sections are always treated as comment and serves as clarity only.
+For further details of config file syntax see [https://pypi.org/project/ConfigArgParse](https://pypi.org/project/ConfigArgParse/).
+
+*my.conf*
+
+ [Source]
+ username = admin
+ password = myPaszxwo!z
+
+ [JSON]
+ json-indent 2
+
+#### Using Tasmota binary configuration files
+
+1. Restore a Tasmota configuration file
+
+ `decode-config.py -c my.conf -d sonoff --restore-file Config_Sonoff_6.2.1.dmp`
+
+2. Backup device using Tasmota configuration compatible format
+
+ a) use file extension to choice the file format
+
+ `decode-config.py -c my.conf -d sonoff --backup-file Config_@f_@v.dmp`
+
+ b) use args to choice the file format
+
+ `decode-config.py -c my.conf -d sonoff --backup-type dmp --backup-file Config_@f_@v`
+
+#### Use batch processing
+
+ for device in sonoff1 sonoff2 sonoff3; do ./decode-config.py -c my.conf -d $device -o Config_@f_@v
+
+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.
diff --git a/tools/decode-config.py b/tools/decode-config.py
old mode 100644
new mode 100755
index ffa78c031..1eb991d11
--- a/tools/decode-config.py
+++ b/tools/decode-config.py
@@ -1,10 +1,9 @@
#!/usr/bin/env python
-#!/usr/bin/env python
# -*- coding: utf-8 -*-
-VER = '1.5.0013'
+VER = '2.0.0000'
"""
- decode-config.py - Decode configuration of Sonoff-Tasmota device
+ decode-config.py - Backup/Restore Sonoff-Tasmota configuration data
Copyright (C) 2018 Norbert Richter
@@ -29,68 +28,83 @@ Requirements:
Instructions:
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
+ or use -f to read a configuration file saved using Tasmota Web-UI
+
+ For further information read 'decode-config.md'
- For help execute command with argument -h
+ For help execute command with argument -h (or -H for advanced help)
-Usage:
- decode-config.py [-h] [-f ] [-d ] [-u ]
- [-p ] [--json-indent ]
- [--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]
+Usage: decode-config.py [-f ] [-d ] [-P ]
+ [-u ] [-p ] [-i ]
+ [-o ] [-F json|bin|dmp] [-E] [-e]
+ [--json-indent ] [--json-compact]
+ [--json-hide-pw] [--json-unhide-pw] [-h] [-H] [-v]
+ [-V] [-c ] [--ignore-warnings]
- 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
- allows: key=value, flag=true, stuff=[a,b,c] (for details, see syntax at
+ Backup/Restore Sonoff-Tasmota configuration data. Args that start with '--'
+ (eg. -f) can also be set in a config file (specified via -c). Config file
+ syntax allows: key=value, flag=true, stuff=[a,b,c] (for details, see syntax at
https://goo.gl/R74nmi). If an arg is specified in more than one place, then
commandline values override config file values which override defaults.
optional arguments:
- -h, --help show this help message and exit
- -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!
+ -c, --config
+ program config file - can be used to set default
+ command args (default: None)
+ --ignore-warnings do not exit on warnings. Not recommended, used by your
+ own responsibility!
- source:
- -f , --file
- file to retrieve Tasmota configuration from (default:
- None)'
- -d , --device
- hostname or IP address to retrieve Tasmota
- configuration from (default: None)
- -u , --username
+ Source:
+ Read/Write Tasmota configuration from/to
+
+ -f, --file, --tasmota-file
+ file to retrieve/write Tasmota configuration from/to
+ (default: None)'
+ -d, --device, --host
+ hostname or IP address to retrieve/send Tasmota
+ configuration from/to (default: None)
+ -P, --port TCP/IP port number to use for the host connection
+ (default: 80)
+ -u, --username
host HTTP access username (default: admin)
- -p , --password
+ -p, --password
host HTTP access password (default: None)
- config:
- --json-indent
- pretty-printed JSON output using indent level
- (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).
- Replacements: @v=Tasmota version, @f=friendly name
- --output-file-format
- output format ('json' or 'binary', default: 'json')
+ Backup/Restore:
+ Backup/Restore configuration file specification
- info:
+ -i, --restore-file
+ file to restore configuration from (default: None).
+ Replacements: @v=firmware version, @f=device friendly
+ name, @h=device hostname
+ -o, --backup-file
+ file to backup configuration to (default: None).
+ Replacements: @v=firmware version, @f=device friendly
+ name, @h=device hostname
+ -F, --backup-type json|bin|dmp
+ backup filetype (default: 'json')
+ -E, --extension append filetype extension for -i and -o filename
+ (default)
+ -e, --no-extension do not append filetype extension, use -i and -o
+ filename as passed
+
+ JSON:
+ JSON backup format specification
+
+ --json-indent
+ pretty-printed JSON output using indent level
+ (default: 'None'). -1 disables indent.
+ --json-compact compact JSON output by eliminate whitespace
+ --json-hide-pw hide passwords (default)
+ --json-unhide-pw unhide passwords
+
+ Info:
+ additional information
+
+ -h, --help show usage help message and exit
+ -H, --full-help show full help message and exit
+ -v, --verbose produce more output about what the program does
-V, --version show program's version number and exit
Either argument -d or -f must be given.
@@ -98,29 +112,54 @@ Usage:
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
+ 1: restore skipped
+ 2: program argument error
+ 3: file not found
+ 4: data size mismatch
+ 5: data CRC error
+ 6: unsupported configuration version
+ 7: configuration file read error
+ 8: JSON file decoding error
+ 9: Restore file data error
+ 10: Device data download error
+ 11: Device data upload error
+ 20: python module missing
+ 21: Internal error
+ >21: python library exit code
+ 4xx, 5xx: HTTP errors
"""
+class ExitCode:
+ OK = 0
+ RESTORE_SKIPPED = 1
+ ARGUMENT_ERROR = 2
+ FILE_NOT_FOUND = 3
+ DATA_SIZE_MISMATCH = 4
+ DATA_CRC_ERROR = 5
+ UNSUPPORTED_VERSION = 6
+ FILE_READ_ERROR = 7
+ JSON_READ_ERROR = 8
+ RESTORE_DATA_ERROR = 9
+ DOWNLOAD_CONFIG_ERROR = 10
+ UPLOAD_CONFIG_ERROR = 11
+ MODULE_NOT_FOUND = 20
+ INTERNAL_ERROR = 21
+
import os.path
import io
-import sys
+import sys, platform
def ModuleImportError(module):
er = str(module)
- print("{}. Try 'pip install {}' to install it".format(er,er.split(' ')[len(er.split(' '))-1]) )
- sys.exit(9)
+ print >> sys.stderr, "{}. Try 'pip install {}' to install it".format(er,er.split(' ')[len(er.split(' '))-1])
+ sys.exit(ExitCode.MODULE_NOT_FOUND)
try:
+ from datetime import datetime
import struct
+ import socket
import re
import math
- from datetime import datetime
+ import inspect
import json
import configargparse
import pycurl
@@ -129,37 +168,43 @@ except ImportError, e:
ModuleImportError(e)
-PROG='{} v{} by Norbert Richter'.format(os.path.basename(sys.argv[0]),VER)
+PROG='{} v{} by Norbert Richter '.format(os.path.basename(sys.argv[0]),VER)
-CONFIG_FILE_XOR = 0x5A
-BINARYFILE_MAGIC = 0x63576223
-
-args = {}
-DEFAULTS = {
+CONFIG_FILE_XOR = 0x5A
+BINARYFILE_MAGIC = 0x63576223
+STR_ENCODING = 'utf8'
+DEFAULTS = {
'DEFAULT':
{
'configfile': None,
- 'exitonwarning':True,
+ 'ignorewarning':False,
},
'source':
{
'device': None,
+ 'port': 80,
'username': 'admin',
'password': None,
'tasmotafile': None,
},
- 'config':
+ 'backup':
+ {
+ 'restorefile': None,
+ 'backupfile': None,
+ 'backupfileformat': 'json',
+ 'extension': True,
+ },
+ 'jsonformat':
{
'jsonindent': None,
'jsoncompact': False,
- 'sort': True,
- 'rawvalues': False,
- 'rawkeys': True,
- 'hidepw': True,
- 'outputfile': None,
- 'outputfileformat': 'json',
+ 'jsonsort': True,
+ 'jsonrawvalues':False,
+ 'jsonrawkeys': False,
+ 'jsonhidepw': True,
},
}
+args = {}
exitcode = 0
@@ -218,33 +263,47 @@ Settings dictionary describes the config file fields definition:
[n, n <,n...>]
Defines a multi-dimensional array
- convert (optional)
- Define an output/conversion methode, can be a simple string
- or a previously defined function name.
- 'xxx?':
- a string will be evaluate as is replacing all '?' chars
- with the current value. This can also be contain pyhton
- code.
+ converter (optional)
+ Conversion methode(s): ()|'xxx'|func
+ Read conversion is used if args.jsonrawvalues is False
+ Write conversion is used if jsonrawvalues from restore json
+ file is False or args.jsonrawvalues is False.
+ Converter is either a single methode 'xxx'|func or a tuple
+ Single methode will be used for reading conversion only:
+ 'xxx':
+ string will used for reading conversion and will be
+ evaluate as is, this can also contain python code.
+ Use '$' for current value.
func:
- a function defines the name of a formating function
+ name of a formating function that will be used for
+ reading conversion
+ None:
+ will read as definied in
+ (read, write):
+ a tuple with 2 objects. Each can be of the same type
+ as the single method above ('xxx'|func) or None.
+ read:
+ method will be used for read conversion
+ (unpack data from dmp object)
+ write:
+ method will be used for write conversion
+ (pack data to dmp object)
+ If write method is None indicates value is
+ readable only and will not be write
"""
-# config data conversion function and helper
-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.hidepw:
- return '********'
- return value
+def passwordread(value):
+ return "********" if args.jsonhidepw else value
+def passwordwrite(value):
+ return None if value=="********" else value
Setting_5_10_0 = {
- 'cfg_holder': (' version number from read binary data to search for
+
+ @return:
+ template sizes as list []
+ """
+ sizes = []
+ for cfg in Settings:
+ sizes.append(cfg[1])
+ # return unique sizes only (remove duplicates)
+ return list(set(sizes))
+
+
+def GetTemplateSetting(decode_cfg):
+ """
+ Search for version, template, size and settings to be used depending on given binary config data
+
+ @param decode_cfg:
+ binary config data (decrypted)
+
+ @return:
+ version, template, size, settings to use; None if version is invalid
+ """
+ try:
+ version = GetField(decode_cfg, 'version', Setting_6_2_1['version'], raw=True)
+ except:
+ return None,None,None,None
+
+ # 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 version, template, size, setting
+
+
+class LogType:
INFO = 'INFO'
WARNING = 'WARNING'
ERROR = 'ERROR'
- def message(self, msg, typ=None, status=None, jsonformat=False):
- """
- Writes a message to stdout
- @param msg: string
- message to output
- if msg is of type dict, json format will be used
- """
- if jsonformat:
- message = {}
- message['msg'] = msg
- if type is not None:
- message['type'] = typ
- if status is not None:
- message['status'] = status
- print json.dumps( message )
- else:
- print '{}{} {}{} {}'.format(typ if typ is not None else '',
- ' ' if status is not None and typ is not None else '',
- status if status is not None else '',
- ':' if typ is not None else '',
- msg)
+def message(msg, typ=None, status=None, line=None):
+ """
+ Writes a message to stdout
+
+ @param msg:
+ message to output
+ @param typ:
+ INFO, WARNING or ERROR
+ @param status:
+ status number
+ """
+ print >> sys.stderr, '{styp}{sdelimiter}{sstatus}{slineno}{scolon}{smgs}'.format(\
+ styp=typ if typ is not None else '',
+ sdelimiter=' ' if status is not None and status>0 and typ is not None else '',
+ sstatus=status if status is not None and status>0 else '',
+ scolon=': ' if typ is not None or line is not None else '',
+ smgs=msg,
+ slineno=' (@{:04d})'.format(line) if line is not None else '')
-def exit(status=0, message="end", typ='ERROR', doexit=True):
+def exit(status=0, msg="end", typ=LogType.ERROR, src=None, doexit=True, line=None):
"""
Called when the program should be exit
@param status:
the exit status program returns to callert
- @param message:
- the message logged before exit
+ @param msg:
+ the msg logged before exit
@param typ:
- message type: 'INFO', 'WARNING' or 'ERROR'
+ msg type: 'INFO', 'WARNING' or 'ERROR'
@param doexit:
True to exit program, otherwise return
"""
- logger = Log()
- logger.message(message, typ=typ if status!=0 else 'INFO', status=status, jsonformat=True )
+ if src is not None:
+ msg = '{} ({})'.format(src, msg)
+ message(msg, typ=typ if status!=ExitCode.OK else LogType.INFO, status=status, line=line)
exitcode = status
if doexit:
sys.exit(exitcode)
+def ShortHelp(doexit=True):
+ """
+ Show short help (usage) only - ued by own -h handling
+
+ @param doexit:
+ sys.exit with OK if True
+ """
+ print parser.description
+ print
+ parser.print_usage()
+ print
+ print "For advanced help use '{prog} -H' or '{prog} --full-help'".format(prog=os.path.basename(sys.argv[0]))
+ if doexit:
+ sys.exit(ExitCode.OK)
+
+
+class HTTPHeader:
+ """
+ pycurl helper class retrieving the request header
+ """
+ def __init__(self):
+ self.contents = ''
+
+ def clear(self):
+ self.contents = ''
+
+ def store(self, _buffer):
+ self.contents = "{}{}".format(self.contents, _buffer)
+
+ def response(self):
+ header = str(self.contents).split('\n')
+ if len(header)>0:
+ return header[0].rstrip()
+ return ''
+
+ def contenttype(self):
+ for item in str(self.contents).split('\n'):
+ ditem = item.split(":")
+ if ditem[0].strip().lower()=='content-type' and len(ditem)>1:
+ return ditem[1].strip()
+ return ''
+
+ def __str__(self):
+ return self.contents
+
+
+class CustomHelpFormatter(configargparse.HelpFormatter):
+ """
+ Class for customizing the help output
+ """
+
+ def _format_action_invocation(self, action):
+ """
+ Reformat multiple metavar output
+ -d , --device , --host
+ to single output
+ -d, --device, --host
+ """
+
+ orgstr = configargparse.HelpFormatter._format_action_invocation(self, action)
+ if orgstr and orgstr[0] != '-': # only optional arguments
+ return orgstr
+ res = getattr(action, '_formatted_action_invocation', None)
+ if res:
+ return res
+
+ options = orgstr.split(', ')
+ if len(options) <=1:
+ action._formatted_action_invocation = orgstr
+ return orgstr
+
+ return_list = []
+ for option in options:
+ meta = ""
+ arg = option.split(' ')
+ if len(arg)>1:
+ meta = arg[1]
+ return_list.append(arg[0])
+ if len(meta) >0 and len(return_list) >0:
+ return_list[len(return_list)-1] += " "+meta
+ action._formatted_action_invocation = ', '.join(return_list)
+ return action._formatted_action_invocation
+
+
# ----------------------------------------------------------------------
# Tasmota config data handling
# ----------------------------------------------------------------------
-def GetFilenameReplaced(filename, configuration):
+class FileType:
+ FILE_NOT_FOUND = None
+ DMP = 'dmp'
+ JSON = 'json'
+ BIN = 'bin'
+ UNKNOWN = 'unknown'
+ INCOMPLETE_JSON = 'incomplete json'
+ INVALID_JSON = 'invalid json'
+ INVALID_BIN = 'invalid bin'
+
+
+def GetFileType(filename):
+ """
+ Get the FileType class member of a given filename
+
+ @param filename:
+ filename of the file to analyse
+
+ @return:
+ FileType class member
+ """
+ filetype = FileType.UNKNOWN
+
+ # try filename
+ try:
+ isfile = os.path.isfile(filename)
+ try:
+ f = open(filename, "r")
+ try:
+ # try reading as json
+ inputjson = json.load(f)
+ if 'header' in inputjson:
+ filetype = FileType.JSON
+ else:
+ filetype = FileType.INCOMPLETE_JSON
+ except ValueError:
+ filetype = FileType.INVALID_JSON
+ # not a valid json, get filesize and compare it with all possible sizes
+ try:
+ size = os.path.getsize(filename)
+ except:
+ filetype = FileType.UNKNOWN
+ sizes = GetTemplateSizes()
+
+ # size is one of a dmp file size
+ if size in sizes:
+ filetype = FileType.DMP
+ elif (size - ((len(hex(BINARYFILE_MAGIC))-2)/2)) in sizes:
+ # check if the binary file has the magic header
+ try:
+ inputfile = open(filename, "rb")
+ inputbin = inputfile.read()
+ inputfile.close()
+ if struct.unpack_from('>24) & 0xff)
+ minor = ((version>>16) & 0xff)
+ release = ((version>> 8) & 0xff)
+ subrelease = (version & 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 = ''
+ return "{:d}.{:d}.{:d}{}{}".format( major, minor, release, '.' if (major>=6 and subreleasestr!='') else '', subreleasestr)
+
+
+def MakeValidFilename(filename):
+ """
+ Make a valid filename
+
+ @param filename:
+ filename src
+
+ @return:
+ valid filename removed invalid chars and replace space with _
+ """
+ try:
+ filename = filename.decode('unicode-escape').translate(dict((ord(char), None) for char in '\/*?:"<>|'))
+ except:
+ pass
+ return str(filename.replace(' ','_'))
+
+
+def MakeFilename(filename, filetype, decode_cfg):
"""
Replace variable within a filename
@@ -863,35 +1171,223 @@ def GetFilenameReplaced(filename, configuration):
@v:
Tasmota version
@f:
- FriendlyName
+ friendlyname
+ @h:
+ hostname
+ @param filetype:
+ FileType.x object - creates extension if not None
+ @param decode_cfg:
+ binary config data (decrypted)
- @return: New filename with replacements
+ @return:
+ New filename with replacements
"""
v = f1 = f2 = f3 = f4 = ''
- if 'version' in configuration:
- ver = int(str(configuration['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 and subreleasestr!='') else '', subreleasestr)
+ if 'version' in decode_cfg:
+ v = GetVersionStr( int(str(decode_cfg['version']), 0) )
filename = filename.replace('@v', v)
- if 'friendlyname' in configuration:
- filename = filename.replace('@f', configuration['friendlyname'][0] )
+ if 'friendlyname' in decode_cfg:
+ filename = filename.replace('@f', decode_cfg['friendlyname'][0] )
+ if 'hostname' in decode_cfg:
+ filename = filename.replace('@h', decode_cfg['hostname'] )
+
+ filename = MakeValidFilename(filename)
+ ext = ''
+ try:
+ name, ext = os.path.splitext(filename)
+ except:
+ pass
+ if len(ext) and ext[0]=='.':
+ ext = ext[1:]
+ if filetype is not None and args.extension and (len(ext)<2 or all(c.isdigit() for c in ext)):
+ filename += '.'+filetype.lower()
return filename
+def MakeUrl(host, port=80, location=''):
+ """
+ Create a Tasmota host url
+
+ @param host:
+ hostname or IP of Tasmota host
+ @param port:
+ port number to use for http connection
+ @param location:
+ http url location
+
+ @return:
+ Tasmota http url
+ """
+ return "http://{shost}{sdelimiter}{sport}/{slocation}".format(\
+ shost=host,
+ sdelimiter=':' if port != 80 else '',
+ sport=port if port != 80 else '',
+ slocation=location )
+
+
+def PullTasmotaConfig():
+ """
+ Pull config from Tasmota device/file
+
+ @return:
+ binary config data (encrypted) or None on error
+ """
+
+ if args.device is not 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(args.device, args.port, 'dl'))
+ 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.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()))
+ encode_cfg = buffer.getvalue()
+
+ elif args.tasmotafile is not None:
+ # read config from a file
+ if not os.path.isfile(args.tasmotafile): # check file exists
+ exit(ExitCode.FILE_NOT_FOUND, "File '{}' not found".format(args.tasmotafile),line=inspect.getlineno(inspect.currentframe()))
+ try:
+ tasmotafile = open(args.tasmotafile, "rb")
+ encode_cfg = tasmotafile.read()
+ tasmotafile.close()
+ except Exception, e:
+ exit(e[0], "'{}' {}".format(args.tasmotafile, e[1]),line=inspect.getlineno(inspect.currentframe()))
+
+ else:
+ return None
+
+ return encode_cfg
+
+
+def PushTasmotaConfig(encode_cfg, host, port, username=DEFAULTS['source']['username'], password=None):
+ """
+ Upload binary data to a Tasmota host using http
+
+ @param encode_cfg:
+ encrypted binary data or filename containing Tasmota encrypted binary config
+ @param host:
+ hostname or IP of Tasmota device
+ @param username:
+ optional username for Tasmota web login
+ @param password
+ optional password for Tasmota web login
+
+ @return
+ errorcode, errorstring
+ errorcode=0 if success, otherwise http response or exception code
+ """
+ # ~ return 0, 'OK'
+
+ 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)"
+
+ # post data
+ header.clear()
+ c.setopt(c.HEADERFUNCTION, header.store)
+ c.setopt(c.POST, 1)
+ c.setopt(c.URL, MakeUrl(host, port, 'u2'))
+ try:
+ isfile = os.path.isfile(encode_cfg)
+ except:
+ isfile = False
+ if isfile:
+ c.setopt(c.HTTPPOST, [("file", (c.FORM_FILE, encode_cfg))])
+ else:
+ # use as binary data
+ c.setopt(c.HTTPPOST, [
+ ('fileupload', (
+ c.FORM_BUFFER, '{sprog}_v{sver}.dmp'.format(sprog=os.path.basename(sys.argv[0]), sver=VER),
+ c.FORM_BUFFERPTR, encode_cfg
+ )),
+ ])
+
+ responsecode = 200
+ try:
+ c.perform()
+ responsecode = c.getinfo(c.RESPONSE_CODE)
+ except Exception, e:
+ return e[0], e[1]
+ c.close()
+
+ if responsecode>=400:
+ return responsecode, header.response()
+ elif header.contenttype()!='text/html':
+ return ExitCode.UPLOAD_CONFIG_ERROR, "Device did not response properly, may be Tasmota webserver admin mode is disabled (WebServer 2)"
+
+ return 0, 'OK'
+
+
+def DecryptEncrypt(obj):
+ """
+ Decrpt/Encrypt binary config data
+
+ @param obj:
+ binary config data
+
+ @return:
+ decrypted configuration (if obj contains encrypted data)
+ encrypted configuration (if obj contains decrypted data)
+ """
+ if isinstance(obj, bytearray):
+ obj = str(obj)
+ dobj = obj[0:2]
+ for i in range(2, len(obj)):
+ dobj += chr( (ord(obj[i]) ^ (CONFIG_FILE_XOR +i)) & 0xff )
+ return dobj
+
+
def GetSettingsCrc(dobj):
"""
Return binary config data calclulated crc
@@ -899,121 +1395,173 @@ def GetSettingsCrc(dobj):
@param dobj:
decrypted binary config data
- @return: 2 byte unsigned integer crc value
+ @return:
+ 2 byte unsigned integer crc value
"""
+ if isinstance(dobj, bytearray):
+ dobj = str(dobj)
crc = 0
for i in range(0, len(dobj)):
if not i in [14,15]: # Skip crc
- crc += ord(dobj[i]) * (i+1)
+ byte = ord(dobj[i])
+ crc += byte * (i+1)
+
return crc & 0xffff
-def GetFieldFormat(fielddef):
+def GetFieldDef(fielddef):
+
"""
- Return the format item of field definition
+ Get the field def items
@param fielddef:
field format - see "Settings dictionary" above
- @return: from fielddef[0]
-
+ @return:
+ , , , , ,
+ undefined items can be None
"""
- return fielddef[0]
+ _format = baseaddr = datadef = convert = None
+ bits = bitshift = 0
+ if len(fielddef)==3:
+ # def without convert tuple
+ _format, baseaddr, datadef = fielddef
+ elif len(fielddef)==4:
+ # def with convert tuple
+ _format, baseaddr, datadef, convert = fielddef
+
+ if isinstance(baseaddr, (list,tuple)):
+ baseaddr, bits, bitshift = baseaddr
+
+ if isinstance(datadef, int):
+ # convert single int into list with one item
+ datadef = [datadef]
+ return _format, baseaddr, bits, bitshift, datadef, convert
-def GetFieldBaseAddr(fielddef):
- """
- Return the format item of field definition
-
- @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):
+def MakeFieldBaseAddr(baseaddr, bits, bitshift):
"""
Return a based on given arguments
@param baseaddr:
baseaddr from Settings definition
- @param bitlen:
- 0 or bitlen
+ @param bits:
+ 0 or bits
@param bitshift:
0 or bitshift
- @return: (,,) if bitlen != 0
- baseaddr if bitlen == 0
+ @return:
+ (,,) if bits != 0
+ baseaddr if bits == 0
"""
- if bitlen!=0:
- return (baseaddr, bitlen, bitshift)
+ if bits!=0:
+ return (baseaddr, bits, bitshift)
return baseaddr
-def ConvertFieldValue(value, fielddef, raw=False):
+def ConvertFieldValue(value, fielddef, read=True, raw=False):
"""
Convert field value based on field desc
@param value:
- original value read from binary data
+ original value
@param fielddef
field definition - see "Settings dictionary" above
+ @param read
+ use read conversion if True, otherwise use write conversion
@param raw
return raw values (True) or converted values (False)
- @return: (un)converted value
+ @return:
+ (un)converted value
"""
- if not raw and len(fielddef)>3:
- 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)
+ _format, baseaddr, bits, bitshift, datadef, convert = GetFieldDef(fielddef)
+
+ # call password functions even if raw value should be processed
+ if callable(convert) and (convert==passwordread or convert==passwordwrite):
+ raw = False
+ if isinstance(convert, (list,tuple)) and len(convert)>0 and (convert[0]==passwordread or convert[0]==passwordwrite):
+ raw = False
+ if isinstance(convert, (list,tuple)) and len(convert)>1 and (convert[1]==passwordread or convert[1]==passwordwrite):
+ raw = False
+
+ if not raw and convert is not None:
+ if isinstance(convert, (list,tuple)): # extract read conversion if tuple is given
+ if read:
+ convert = convert[0]
+ else:
+ convert = convert[1]
+ try:
+ if isinstance(convert, str): # evaluate strings
+ return eval(convert.replace('$','value'))
+ elif callable(convert): # use as format function
+ return convert(value)
+ except:
+ pass
+
return value
-def GetFieldLength(fielddef):
+def GetFieldMinMax(fielddef):
"""
- Return length of a field in bytes based on field format definition
+ Get minimum, maximum of field based on field format definition
@param fielddef:
field format - see "Settings dictionary" above
- @return: length of field in bytes
+ @return:
+ min, max
+ """
+ minmax = {'c': (0, 1),
+ '?': (0, 1),
+ 'b': (~0x7f, 0x7f),
+ 'B': (0, 0xff),
+ 'h': (~0x7fff, 0x7fff),
+ 'H': (0, 0xffff),
+ 'i': (~0x7fffffff, 0x7fffffff),
+ 'I': (0, 0xffffffff),
+ 'l': (~0x7fffffff, 0x7fffffff),
+ 'L': (0, 0xffffffff),
+ 'q': (~0x7fffffffffffffff, 0x7fffffffffffffff),
+ 'Q': (0, 0x7fffffffffffffff),
+ 'f': (sys.float_info.min, sys.float_info.max),
+ 'd': (sys.float_info.min, sys.float_info.max),
+ }
+ _format, baseaddr, bits, bitshift, datadef, convert = GetFieldDef(fielddef)
+ _min = 0
+ _max = 0
+
+ if _format[-1:] in minmax:
+ _min, _max = minmax[_format[-1:]]
+ elif _format[-1:] in ['s','p']:
+ # s and p may have a prefix as length
+ match = re.search("\s*(\d+)", _format)
+ if match:
+ _max=int(match.group(0))
+ return _min,_max
+
+def GetFieldLength(fielddef):
+ """
+ Get 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]
+ _format, baseaddr, bits, bitshift, datadef, convert = GetFieldDef(fielddef)
if datadef is not None:
- # fielddef[2] contains a array or int
+ # datadef contains a list
# 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 ):
+ if isinstance(datadef, list):
+ for i in range(0, datadef[0]):
# multidimensional array
if isinstance(datadef, list) and len(datadef)>1:
@@ -1024,35 +1572,62 @@ def GetFieldLength(fielddef):
length += GetFieldLength( (fielddef[0], fielddef[1], None) )
else:
- if isinstance(fielddef[0], dict):
- # -> iterate through format_
- addr = -1
- setting = fielddef[0]
+ if isinstance(_format, dict):
+ # -> iterate through _format
+ addr = None
+ setting = _format
for name in setting:
- baseaddr, bitlen, bitshift = GetFieldBaseAddr(setting[name])
- len_ = GetFieldLength(setting[name])
+ _dummy1, baseaddr, bits, bitshift, _dummy2, _dummy3 = GetFieldDef(setting[name])
+ _len = GetFieldLength(setting[name])
if addr != baseaddr:
addr = baseaddr
- length += len_
+ length += _len
else:
- if format_[-1:].lower() in ['b','c','?']:
+ if _format[-1:] in ['b','B','c','?']:
length=1
- elif format_[-1:].lower() in ['h']:
+ elif _format[-1:] in ['h','H']:
length=2
- elif format_[-1:].lower() in ['i','l','f']:
+ elif _format[-1:] in ['i','I','l','L','f']:
length=4
- elif format_[-1:].lower() in ['q','d']:
+ elif _format[-1:] in ['q','Q','d']:
length=8
- elif format_[-1:].lower() in ['s','p']:
+ elif _format[-1:] in ['s','p']:
# s and p may have a prefix as length
- match = re.search("\s*(\d+)", format_)
+ match = re.search("\s*(\d+)", _format)
if match:
length=int(match.group(0))
return length
+def GetSubfieldDef(fielddef):
+ """
+ Get subfield definition from a given field definition
+
+ @param fielddef:
+ see Settings desc above
+
+ @return:
+ subfield definition
+ """
+ subfielddef = None
+
+ _format, baseaddr, bits, bitshift, datadef, convert = GetFieldDef(fielddef)
+ if isinstance(datadef, list) and len(datadef)>1:
+ if len(fielddef)<4:
+ subfielddef = (_format, MakeFieldBaseAddr(baseaddr, bits, bitshift), datadef[1:])
+ else:
+ subfielddef = (_format, MakeFieldBaseAddr(baseaddr, bits, bitshift), datadef[1:], convert)
+ # single array
+ else:
+ if len(fielddef)<4:
+ subfielddef = (_format, MakeFieldBaseAddr(baseaddr, bits, bitshift), None)
+ else:
+ subfielddef = (_format, MakeFieldBaseAddr(baseaddr, bits, bitshift), None, convert)
+ return subfielddef
+
+
def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0):
"""
Get field value from definition
@@ -1068,206 +1643,530 @@ def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0):
@param addroffset
use offset for baseaddr (used for recursive calls)
- @return: read field value
+ @return:
+ read field value
"""
+ if isinstance(dobj, bytearray):
+ dobj = str(dobj)
+
result = None
- # get format from field definition
- format_ = GetFieldFormat(fielddef)
+ # get field definition
+ _format, baseaddr, bits, bitshift, datadef, convert = GetFieldDef(fielddef)
- # get baseaddr from field definition
- baseaddr, bitlen, bitshift = GetFieldBaseAddr(fielddef)
-
- # get datadef from field definition
- datadef = None
- if fielddef is not None and len(fielddef)>2:
- datadef = fielddef[2]
-
- if datadef is not None:
+ # contains a integer list
+ if isinstance(datadef, list):
result = []
+ offset = 0
+ for i in range(0, datadef[0]):
+ subfielddef = GetSubfieldDef(fielddef)
+ length = GetFieldLength(subfielddef)
+ if length != 0 and (fieldname != 'raw' or args.jsonrawkeys):
+ result.append(GetField(dobj, fieldname, subfielddef, raw=raw, addroffset=addroffset+offset))
+ offset += length
- # 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):
+ # contains a dict
+ elif isinstance(_format, dict):
+ config = {}
+ for name in _format: # -> iterate through _format
+ if name != 'raw' or args.jsonrawkeys:
+ config[name] = GetField(dobj, name, _format[name], raw=raw, addroffset=addroffset)
+ result = config
- offset = 0
- for i in range(0, datadef[0] if isinstance(datadef, list) else datadef):
+ # a simple value
+ elif isinstance(_format, (str, bool, int, float, long)):
+ if GetFieldLength(fielddef) != 0:
+ result = struct.unpack_from(_format, dobj, baseaddr+addroffset)[0]
- # multidimensional array
- 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
+ if not _format[-1:].lower() in ['s','p']:
+ if bitshift>=0:
+ result >>= bitshift
else:
- if len(fielddef)<4:
- subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), None)
- else:
- subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), None, fielddef[3])
+ result <<= abs(bitshift)
+ if bits>0:
+ result &= (1< 127
+ result = unicode(s, errors='ignore')
+
+ result = ConvertFieldValue(result, fielddef, read=True, raw=raw)
else:
- # contains a dict
- if isinstance(fielddef[0], dict):
- # -> iterate through format_
- setting = fielddef[0]
- config = {}
- for name in setting:
- if name != 'raw' or args.rawkeys:
- config[name] = GetField(dobj, name, setting[name], raw=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)
+ exit(ExitCode.INTERNAL_ERROR, "Wrong mapping format definition: '{}'".format(_format), typ=LogType.WARNING, doexit=not args.ignorewarning, line=inspect.getlineno(inspect.currentframe()))
return result
-def DeEncrypt(obj):
+def SetField(dobj, fieldname, fielddef, restore, raw=False, addroffset=0, filename=""):
"""
- Decrpt/Encrypt binary config data
+ Get field value from definition
- @param obj:
- binary config data
-
- @return: decrypted configuration (if obj contains encrypted data)
- encrypted configuration (if obj contains decrypted data)
+ @param dobj:
+ decrypted binary config data
+ @param fieldname:
+ name of the field
+ @param fielddef:
+ see Settings desc above
+ @param raw
+ handle values as raw values (True) or converted (False)
+ @param addroffset
+ use offset for baseaddr (used for recursive calls)
+ @param restore
+ restore mapping with the new value(s)
"""
- dobj = obj[0:2]
- for i in range(2, len(obj)):
- dobj += chr( (ord(obj[i]) ^ (CONFIG_FILE_XOR +i)) & 0xff )
+ _format, baseaddr, bits, bitshift, datadef, convert = GetFieldDef(fielddef)
+ fieldname = str(fieldname)
+
+ # do not write readonly values
+ if isinstance(convert, (list,tuple)) and len(convert)>1 and convert[1]==None:
+ if args.debug:
+ print >> sys.stderr, "SetField(): Readonly '{}' using '{}'/{}{} @{} skipped".format(fieldname, _format, datadef, bits, hex(baseaddr+addroffset))
+ return dobj
+
+ # contains a list
+ if isinstance(datadef, list):
+ offset = 0
+ if len(restore)>datadef[0]:
+ exit(ExitCode.RESTORE_DATA_ERROR, "file '{sfile}', array '{sname}[{selem}]' exceeds max number of elements [{smax}]".format(sfile=filename, sname=fieldname, selem=len(restore), smax=datadef[0]), typ=LogType.WARNING, doexit=not args.ignorewarning, line=inspect.getlineno(inspect.currentframe()))
+ for i in range(0, datadef[0]):
+ subfielddef = GetSubfieldDef(fielddef)
+ length = GetFieldLength(subfielddef)
+ if length != 0:
+ if i>=len(restore): # restore data list may be shorter than definition
+ break
+ try:
+ subrestore = restore[i]
+ dobj = SetField(dobj, fieldname, subfielddef, subrestore, raw=raw, addroffset=addroffset+offset, filename=filename)
+ except:
+ pass
+ offset += length
+
+ # contains a dict
+ elif isinstance(_format, dict):
+ for name in _format: # -> iterate through _format
+ if name in restore:
+ dobj = SetField(dobj, name, _format[name], restore[name], raw=raw, addroffset=addroffset, filename=filename)
+
+ # a simple value
+ elif isinstance(_format, (str, bool, int, float, long)):
+ valid = True
+ err = "outside range"
+
+ _min, _max = GetFieldMinMax(fielddef)
+ value = _value = valid = None
+ # simple one value
+ if _format[-1:] in ['c']:
+ try:
+ value = ConvertFieldValue(restore.encode(STR_ENCODING)[0], fielddef, read=False, raw=raw)
+ except:
+ valid = False
+ # bool
+ elif _format[-1:] in ['?']:
+ try:
+ value = ConvertFieldValue(bool(restore), fielddef, read=False, raw=raw)
+ except:
+ valid = False
+ # integer
+ elif _format[-1:] in ['b','B','h','H','i','I','l','L','q','Q','P']:
+ try:
+ value = ConvertFieldValue(restore, fielddef, read=False, raw=raw)
+ if isinstance(value, (str, unicode)):
+ value = int(value, 0)
+ else:
+ value = int(value)
+ # bits
+ if bits!=0:
+ value = struct.unpack_from(_format, dobj, baseaddr+addroffset)[0]
+ bitvalue = int(restore)
+ mask = (1<mask:
+ _min = 0
+ _max = mask
+ _value = bitvalue
+ valid = False
+ else:
+ if bitshift>=0:
+ bitvalue <<= bitshift
+ mask <<= bitshift
+ else:
+ bitvalue >>= abs(bitshift)
+ mask >>= abs(bitshift)
+ value &= (0xffffffff ^ mask)
+ value |= bitvalue
+ else:
+ _value = value
+ except:
+ valid = False
+ # float
+ elif _format[-1:] in ['f','d']:
+ try:
+ value = ConvertFieldValue(float(restore), fielddef, read=False, raw=raw)
+ except:
+ valid = False
+ # string
+ elif _format[-1:] in ['s','p']:
+ try:
+ value = ConvertFieldValue(restore.encode(STR_ENCODING), fielddef, read=False, raw=raw)
+ # be aware 0 byte at end of string (str must be < max, not <= max)
+ _max -= 1
+ valid = (len(value)>=_min) and (len(value)<=_max)
+ err = "string exceeds max length"
+ except:
+ valid = False
+
+ if value is None:
+ valid = False
+ if valid is None:
+ valid = (value>=_min) and (value<=_max)
+ if _value is None:
+ _value = value
+ if isinstance(value, (str, unicode)):
+ _value = "'{}'".format(_value)
+
+ if valid:
+ if args.debug:
+ if bits:
+ sbits=" {} bits shift {}".format(bits, bitshift)
+ else:
+ sbits = ""
+ print >> sys.stderr, "SetField(): Set '{}' using '{}'/{}{} @{} to {}".format(fieldname, _format, datadef, sbits, hex(baseaddr+addroffset), _value)
+ struct.pack_into(_format, dobj, baseaddr+addroffset, value)
+ else:
+ exit(ExitCode.RESTORE_DATA_ERROR, "file '{sfile}', value for name '{sname}': {svalue} {serror} [{smin},{smax}]".format(sfile=filename, sname=fieldname, serror=err, svalue=_value, smin=_min, smax=_max), typ=LogType.WARNING, doexit=not args.ignorewarning)
+
return dobj
-def GetTemplateSetting(version):
+def Bin2Mapping(decode_cfg, raw=True):
"""
- Search for template, settings and size to be used depending on given version number
+ Decodes binary data stream into pyhton mappings dict
- @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:
+ @param decode_cfg:
binary config data (decrypted)
- @param raw
+ @param raw:
decode raw values (True) or converted values (False)
- @return: configuration dictionary
+ @return:
+ config data as mapping dictionary
"""
- # get header data
- version = GetField(obj, 'version', Setting_6_2_1['version'], raw=True)
+ if isinstance(decode_cfg, bytearray):
+ decode_cfg = str(decode_cfg)
+
+ # get binary header and template to use
+ version, template, size, setting = GetTemplateSetting(decode_cfg)
- 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) )
+ exit(ExitCode.UNSUPPORTED_VERSION, "Tasmota configuration version 0x{:x} not supported".format(version),line=inspect.getlineno(inspect.currentframe()))
# check size if exists
if 'cfg_size' in setting:
- cfg_size = GetField(obj, 'cfg_size', setting['cfg_size'], raw=True)
+ cfg_size = GetField(decode_cfg, 'cfg_size', setting['cfg_size'], raw=True)
# read size should be same as definied in template
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)
+ exit(ExitCode.DATA_SIZE_MISMATCH, "Number of bytes read does ot match - read {}, expected {} byte".format(cfg_size, template[1]), typ=LogType.ERROR,line=inspect.getlineno(inspect.currentframe()))
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')
+ exit(ExitCode.DATA_SIZE_MISMATCH, "Number of bytes read to small to process - read {}, expected {} byte".format(cfg_size, template[1]), typ=LogType.ERROR,line=inspect.getlineno(inspect.currentframe()))
# check crc if exists
if 'cfg_crc' in setting:
- cfg_crc = GetField(obj, 'cfg_crc', setting['cfg_crc'], raw=True)
+ cfg_crc = GetField(decode_cfg, 'cfg_crc', setting['cfg_crc'], raw=True)
else:
- cfg_crc = GetSettingsCrc(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)
+ cfg_crc = GetSettingsCrc(decode_cfg)
+ if cfg_crc != GetSettingsCrc(decode_cfg):
+ exit(ExitCode.DATA_CRC_ERROR, 'Data CRC error, read 0x{:x} should be 0x{:x}'.format(cfg_crc, GetSettingsCrc(decode_cfg)), typ=LogType.WARNING, doexit=not args.ignorewarning,line=inspect.getlineno(inspect.currentframe()))
# get config
- config = GetField(obj, None, (setting,None,None), raw=raw)
+ config = GetField(decode_cfg, None, (setting,None,None), raw=raw)
# 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,
+ config['header'] = {'timestamp':timestamp.strftime("%Y-%m-%d %H:%M:%S"),
+ 'format': {
+ 'jsonindent': args.jsonindent,
+ 'jsoncompact': args.jsoncompact,
+ 'jsonsort': args.jsonsort,
+ 'jsonrawvalues':args.jsonrawvalues,
+ 'jsonrawkeys': args.jsonrawkeys,
+ 'jsonhidepw': args.jsonhidepw,
+ },
+ 'src': {
+ 'crc': hex(cfg_crc),
+ 'size': cfg_size,
+ 'version': hex(version),
+ },
+ 'data': {
+ 'crc': hex(GetSettingsCrc(decode_cfg)),
+ 'size': len(decode_cfg),
+ 'version': hex(template[0]),
+ },
+ 'script': {
+ 'name': os.path.basename(__file__),
+ 'version': VER,
+ },
+ 'os': (platform.machine(), platform.system(), platform.release(), platform.version(), platform.platform()),
+ 'python': platform.python_version(),
}
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.')
+def Mapping2Bin(decode_cfg, jsonconfig, filename=""):
+ """
+ Encodes into binary data stream
- source = parser.add_argument_group('source')
- source.add_argument('-f', '--file',
+ @param decode_cfg:
+ binary config data (decrypted)
+ @param jsonconfig:
+ restore data mapping
+ @param filename:
+ name of the restore file (for error output only)
+
+ @return:
+ changed binary config data (decrypted)
+ """
+ if isinstance(decode_cfg, str):
+ decode_cfg = bytearray(decode_cfg)
+
+
+ # get binary header data to use the correct version template from device
+ version, template, size, setting = GetTemplateSetting(decode_cfg)
+
+ _buffer = bytearray()
+ _buffer.extend(decode_cfg)
+
+ if template is not None:
+ try:
+ raw = jsonconfig['header']['format']['jsonrawvalues']
+ except:
+ if 'header' not in jsonconfig:
+ errkey = 'header'
+ elif 'format' not in jsonconfig['header']:
+ errkey = 'header.format'
+ elif 'jsonrawvalues' not in jsonconfig['header']['format']:
+ errkey = 'header.format.jsonrawvalues'
+ exit(ExitCode.RESTORE_DATA_ERROR, "Restore file '{sfile}' name '{skey}' missing, don't know how to evaluate restore data!".format(sfile=filename, skey=errkey), typ=LogType.ERROR, doexit=not args.ignorewarning)
+
+ # iterate through restore data mapping
+ for name in jsonconfig:
+ # key must exist in both dict
+ if name in setting:
+ SetField(_buffer, name, setting[name], jsonconfig[name], raw=raw, addroffset=0, filename=filename)
+ else:
+ if name != 'header':
+ exit(ExitCode.RESTORE_DATA_ERROR, "Restore file '{}' contains obsolete name '{}', skipped".format(filename, name), typ=LogType.WARNING, doexit=not args.ignorewarning)
+
+ crc = GetSettingsCrc(_buffer)
+ struct.pack_into(setting['cfg_crc'][0], _buffer, setting['cfg_crc'][1], crc)
+ return _buffer
+
+ else:
+ exit(ExitCode.UNSUPPORTED_VERSION,"File '{}', Tasmota configuration version 0x{:x} not supported".format(filename, version), typ=LogType.WARNING, doexit=not args.ignorewarning)
+
+ return decode_cfg
+
+
+def Backup(backupfile, backupfileformat, encode_cfg, decode_cfg, configuration):
+ """
+ Create backup file
+
+ @param backupfile:
+ Raw backup filename from program args
+ @param backupfileformat:
+ Backup file format
+ @param encode_cfg:
+ binary config data (encrypted)
+ @param decode_cfg:
+ binary config data (decrypted)
+ @param configuration:
+ config data mapppings
+ """
+
+ backupfileformat = args.backupfileformat
+ try:
+ name, ext = os.path.splitext(backupfile)
+ if ext.lower() == '.'+FileType.BIN.lower():
+ backupfileformat = FileType.BIN
+ elif ext.lower() == '.'+FileType.DMP.lower():
+ backupfileformat = FileType.DMP
+ elif ext.lower() == '.'+FileType.JSON.lower():
+ backupfileformat = FileType.JSON
+ except:
+ pass
+
+ fileformat = ""
+ # binary format
+ if backupfileformat.lower() == FileType.BIN.lower():
+ fileformat = "binary"
+ backup_filename = MakeFilename(backupfile, FileType.BIN, configuration)
+ try:
+ backupfp = open(backup_filename, "wb")
+ magic = BINARYFILE_MAGIC
+ backupfp.write(struct.pack('> sys.stderr, parser.format_values()
+ print >> sys.stderr, "Settings:"
+ for k in args.__dict__:
+ print >> sys.stderr, " "+str(k), "= ",eval('args.{}'.format(k))
+ return args
+
+
+if __name__ == "__main__":
+ args = ParseArgs()
+ if args.shorthelp:
+ ShortHelp()
+
# default no configuration available
- configobj = None
+ encode_cfg = 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")
+ exit(ExitCode.ARGUMENT_ERROR, "Unable to select source, do not use -d and -f together",line=inspect.getlineno(inspect.currentframe()))
- # read config direct from device via http
- if args.device is not None:
+ # pull config from Tasmota device/file
+ encode_cfg = PullTasmotaConfig()
+ if encode_cfg is None:
+ # no config source given
+ ShortHelp(False)
+ print
+ print parser.epilog
+ sys.exit(ExitCode.OK)
- buffer = io.BytesIO()
- url = str("http://{}/dl".format(args.device))
- c = pycurl.Curl()
- c.setopt(c.URL, url)
- c.setopt(c.VERBOSE, 0)
- 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.WRITEDATA, buffer)
- try:
- c.perform()
- except Exception, e:
- exit(e[0], e[1])
- response = c.getinfo(c.RESPONSE_CODE)
- c.close()
- if response>=400:
- exit(response, 'HTTP returns {}'.format(response) )
+ if len(encode_cfg) == 0:
+ exit(ExitCode.FILE_READ_ERROR, "Unable to 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) \
+ ,line=inspect.getlineno(inspect.currentframe()) )
+ # decrypt Tasmota config
+ decode_cfg = DecryptEncrypt(encode_cfg)
- configobj = buffer.getvalue()
+ # decode into mappings dictionary
+ configuration = Bin2Mapping(decode_cfg, args.jsonrawvalues)
- # read config from a file
- elif args.tasmotafile is not None:
+ # backup to file
+ if args.backupfile is not None:
+ Backup(args.backupfile, args.backupfileformat, encode_cfg, decode_cfg, configuration)
- if not os.path.isfile(args.tasmotafile): # check file exists
- exit(1, "File '{}' not found".format(args.tasmotafile))
- try:
- tasmotafile = open(args.tasmotafile, "rb")
- configobj = tasmotafile.read()
- tasmotafile.close()
- except Exception, e:
- exit(e[0], e[1])
+ # restore from file
+ if args.restorefile is not None:
+ Restore(args.restorefile, encode_cfg, decode_cfg, configuration)
- # no config source given
- else:
- parser.print_help()
- sys.exit(0)
-
- if configobj is not None and len(configobj)>0:
- cfg = DeEncrypt(configobj)
-
- configuration = Decode(cfg, args.rawvalues)
-
- # output to file
- if args.outputfile is not None:
- outputfilename = GetFilenameReplaced(args.outputfile, configuration)
- if args.outputfileformat == 'binary':
- outputfile = open(outputfilename, "wb")
- outputfile.write(struct.pack('