Examples
Prerequisite
File Types
decode-config.py can handle the following backup file types:
@@ -83,7 +100,7 @@
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
+- or a previously stored Tasmota
*.dmp
configuration file by passing the filename using -f <filename>
arg
Example:
decode-config.py -d sonoff-4281
@@ -112,6 +129,72 @@
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
+
Output to screen
+Output to screen is default enabled when calling the program with a source arg but without a backup or restore arg.
+--output
arg will force screen output even if you use backup or restore arg.
+JSON output
+The default output format is JSON. You can force JSON output with --output-format json
arg.
+Example:
+decode-config.py -d sonoff-4281 -c my.conf -x Wifi --output-format json
+
+{
+ ...
+ "hostname": "%s-%04d",
+ "ip_address": [
+ "0.0.0.0",
+ "192.168.12.1",
+ "255.255.255.0",
+ "192.168.12.1"
+ ],
+ "ntp_server": [
+ "ntp.localnet.home",
+ "ntp2.localnet.home",
+ "192.168.12.1"
+ ],
+ "sta_active": 0,
+ "sta_config": 5,
+ "sta_pwd": [
+ "myWlAnPaszxwo!z",
+ "myWlAnPaszxwo!z2"
+ ],
+ "sta_ssid": [
+ "wlan.1",
+ "my-wlan"
+ ],
+ "web_password": "myPaszxwo!z",
+ "webserver": 2
+ ...
+}
+
Note: JSON output always contains all configuration data like the backup file except you are using --group
arg.
+Tasmota command output
+decode-config.py is able to translate the configuration data to (most all) Tasmota commands. To output your configuration as Tasmota commands use --output-format cmnd
or --output-format command
.
+Example:
+decode-config.py -d sonoff-4281 -c my.conf -g Wifi --output-format cmnd
+
+# Wifi:
+ AP 0
+ Hostname %s-%04d
+ IPAddress1 0.0.0.0
+ IPAddress2 192.168.12.1
+ IPAddress3 255.255.255.0
+ IPAddress4 192.168.12.1
+ NtpServer1 ntp.localnet.home
+ NtpServer2 ntp2.localnet.home
+ NtpServer3 192.168.12.1
+ Password1 myWlAnPaszxwo!z
+ Password2 myWlAnPaszxwo!z2
+ SSId1 wlan.1
+ SSId2 wlan.1
+ WebPassword myPaszxwo!z
+ WebServer 2
+ 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.
+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.
+decode-config.py -d sonoff-4281 -c my.conf --output-format cmnd --group Main MQTT Management Wifi
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.
@@ -123,83 +206,111 @@
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).
+For better reading each short written arg (minus sign -
) has a corresponding 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>]
+usage: decode-config.py [-f <filename>] [-d <host>] [-P <port>]
[-u <username>] [-p <password>] [-i <filename>]
- [-o <filename>] [-F json|bin|dmp] [-E] [-e]
+ [-o <filename>] [-t json|bin|dmp] [-E] [-e] [-F]
[
- [
- [-V] [-c <filename>] [
+ [
+ [
+ [
+ [-c <filename>] [-S] [-T json|cmnd|command]
+ [-g {Display,Domoticz,Internal,KNX,Led,Logging,MCP230xx,MQTT,Main,Management,Pow,Sensor,Serial,SetOption,SonoffRF,System,Timers,Wifi} [{Display,Domoticz,Internal,KNX,Led,Logging,MCP230xx,MQTT,Main,Management,Pow,Sensor,Serial,SetOption,SonoffRF,System,Timers,Wifi} ...]]
+ [
-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.
+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.
+
+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 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
+ -t,
+ backup filetype (default: 'json')
+ -E,
+ (default)
+ -e,
+ filename as passed
+ -F,
+
+JSON output:
+ JSON format specification
+
+
+ pretty-printed JSON output using indent level
+ (default: 'None'). -1 disables indent.
+
+
+
+ unhide passwords (default)
+
+Tasmota command output:
+ Tasmota command output format specification
+
+
+ Tasmota command grouping indent level (default: '2').
+ 0 disables indent
+
+
+
+
+
+Common:
+ Optional arguments
-optional arguments:
-c,
- program config file - can be used to set default
- command args (default: None)
+ program config file - can be used to set default
+ command args (default: None)
+ -S,
+ (default do not output on backup or restore usage)
+ -T,
+ display output format (default: 'json')
+ -g,
+ limit data processing to command groups (default no
+ filter)
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
+Info:
+ Extra information
-h,
- -H,
+ -H,
-v,
- -V,
+ -V,
-Either argument -d <host> or -f <filename> must be given.
-
Examples
+Either argument -d <host> or -f <filename> must be given.
+
Program parameter notes
+decode-config.py
+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.
diff --git a/tools/decode-config.md b/tools/decode-config.md
index d1cb05bd8..dab16a2c8 100644
--- a/tools/decode-config.md
+++ b/tools/decode-config.md
@@ -1,5 +1,10 @@
# decode-config.py
-_decode-config.py_ backup and restore Sonoff-Tasmota configuration.
+_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
Comparing backup files created by *decode-config.py* and *.dmp files created by Tasmota "Backup/Restore Configuration":
@@ -10,19 +15,23 @@ Comparing backup files created by *decode-config.py* and *.dmp files created by
| 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.
+_decode-config.py_ is able to handle Tasmota configurations for release version starting from 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)
+ * [.dmp File Format](decode-config.md#-dmp-format)
+ * [.json File Format](decode-config.md#-json-format)
+ * [.bin File Format](decode-config.md#-bin-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)
+ * [Output to screen](decode-config.md#output-to-screen)
+ * [JSON output](decode-config.md#json-output)
+ * [Tasmota command output](decode-config.md#tasmota-command-output)
+ * [Filter data](decode-config.md#filter-data)
* [Configuration file](decode-config.md#configuration-file)
* [More program arguments](decode-config.md#more-program-arguments)
* [Examples](decode-config.md#examples)
@@ -33,9 +42,10 @@ _decode-config.py_ handles Tasmota configurations for release version since 5.10
## 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.
+
+* [Sonoff-Tasmota](https://github.com/arendst/Sonoff-Tasmota) [Firmware](https://github.com/arendst/Sonoff-Tasmota/releases) with Web-Server enabled:
+ * 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)).
+ * If using your own compiled firmware be aware to enable the web-server (`#define USE_WEBSERVER` and `#define WEB_SERVER 2`).
## File Types
_decode-config.py_ can handle the following backup file types:
@@ -65,7 +75,7 @@ At least pass a source where you want to read the configuration data from using
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
+* or a previously stored Tasmota `*.dmp` configuration file by passing the filename using `-f ` arg
Example:
@@ -109,6 +119,89 @@ with password set by WebPassword:
decode-config.py -d sonoff-4281 -p --restore-file Config_Sonoff_6.2.1.json
+### Output to screen
+Output to screen is default enabled when calling the program with a source arg but without a backup or restore arg.
+
+`--output` arg will force screen output even if you use backup or restore arg.
+
+#### JSON output
+The default output format is JSON. You can force JSON output with `--output-format json` arg.
+
+Example:
+
+ decode-config.py -d sonoff-4281 -c my.conf -x Wifi --output-format json
+
+ {
+ ...
+ "hostname": "%s-%04d",
+ "ip_address": [
+ "0.0.0.0",
+ "192.168.12.1",
+ "255.255.255.0",
+ "192.168.12.1"
+ ],
+ "ntp_server": [
+ "ntp.localnet.home",
+ "ntp2.localnet.home",
+ "192.168.12.1"
+ ],
+ "sta_active": 0,
+ "sta_config": 5,
+ "sta_pwd": [
+ "myWlAnPaszxwo!z",
+ "myWlAnPaszxwo!z2"
+ ],
+ "sta_ssid": [
+ "wlan.1",
+ "my-wlan"
+ ],
+ "web_password": "myPaszxwo!z",
+ "webserver": 2
+ ...
+ }
+
+Note: JSON output always contains all configuration data like the backup file except you are using `--group` arg.
+
+
+#### Tasmota command output
+_decode-config.py_ is able to translate the configuration data to (most all) Tasmota commands. To output your configuration as Tasmota commands use `--output-format cmnd` or `--output-format command`.
+
+Example:
+
+ decode-config.py -d sonoff-4281 -c my.conf -g Wifi --output-format cmnd
+
+ # Wifi:
+ AP 0
+ Hostname %s-%04d
+ IPAddress1 0.0.0.0
+ IPAddress2 192.168.12.1
+ IPAddress3 255.255.255.0
+ IPAddress4 192.168.12.1
+ NtpServer1 ntp.localnet.home
+ NtpServer2 ntp2.localnet.home
+ NtpServer3 192.168.12.1
+ Password1 myWlAnPaszxwo!z
+ Password2 myWlAnPaszxwo!z2
+ SSId1 wlan.1
+ SSId2 wlan.1
+ WebPassword myPaszxwo!z
+ WebServer 2
+ 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.
+
+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](Tasmota Command Wiki).
+
+To filter outputs to a subset of groups use the `-g` or `--group` arg concatenating the grooup you want, e. g.
+
+ decode-config.py -d sonoff-4281 -c my.conf --output-format cmnd --group Main MQTT Management Wifi
+
+
### 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/)).
@@ -129,7 +222,7 @@ To make a backup file from example above you can now pass the config file instea
### 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).
+For better reading each short written arg (minus sign `-`) has a corresponding 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`.
@@ -137,10 +230,14 @@ 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]
+ [-o ] [-t json|bin|dmp] [-E] [-e] [-F]
[--json-indent ] [--json-compact]
- [--json-hide-pw] [--json-unhide-pw] [-h] [-H] [-v]
- [-V] [-c ] [--ignore-warnings]
+ [--json-hide-pw] [--json-show-pw]
+ [--cmnd-indent ] [--cmnd-groups]
+ [--cmnd-nogroups] [--cmnd-sort] [--cmnd-unsort]
+ [-c ] [-S] [-T json|cmnd|command]
+ [-g {Display,Domoticz,Internal,KNX,Led,Logging,MCP230xx,MQTT,Main,Management,Pow,Sensor,Serial,SetOption,SonoffRF,System,Timers,Wifi} [{Display,Domoticz,Internal,KNX,Led,Logging,MCP230xx,MQTT,Main,Management,Pow,Sensor,Serial,SetOption,SonoffRF,System,Timers,Wifi} ...]]
+ [--ignore-warnings] [-h] [-H] [-v] [-V]
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
@@ -148,13 +245,6 @@ For advanced help use `-H` or `--full-help`:
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
@@ -172,7 +262,7 @@ For advanced help use `-H` or `--full-help`:
host HTTP access password (default: None)
Backup/Restore:
- Backup/Restore configuration file specification
+ Backup & restore specification
-i, --restore-file
file to restore configuration from (default: None).
@@ -182,25 +272,54 @@ For advanced help use `-H` or `--full-help`:
file to backup configuration to (default: None).
Replacements: @v=firmware version, @f=device friendly
name, @h=device hostname
- -F, --backup-type json|bin|dmp
+ -t, --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
+ -F, --force-restore force restore even configuration is identical
- JSON:
- JSON backup format specification
+ JSON output:
+ JSON 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
+ --json-hide-pw hide passwords
+ --json-show-pw, --json-unhide-pw
+ unhide passwords (default)
+
+ Tasmota command output:
+ Tasmota command output format specification
+
+ --cmnd-indent
+ Tasmota command grouping indent level (default: '2').
+ 0 disables indent
+ --cmnd-groups group Tasmota commands (default)
+ --cmnd-nogroups leave Tasmota commands ungrouped
+ --cmnd-sort sort Tasmota commands (default)
+ --cmnd-unsort leave Tasmota commands unsorted
+
+ Common:
+ Optional arguments
+
+ -c, --config
+ program config file - can be used to set default
+ command args (default: None)
+ -S, --output display output regardsless of backup/restore usage
+ (default do not output on backup or restore usage)
+ -T, --output-format json|cmnd|command
+ display output format (default: 'json')
+ -g, --group {Display,Domoticz,Internal,KNX,Led,Logging,MCP230xx,MQTT,Main,Management,Pow,Sensor,Serial,SetOption,SonoffRF,System,Timers,Wifi}
+ limit data processing to command groups (default no
+ filter)
+ --ignore-warnings do not exit on warnings. Not recommended, used by your
+ own responsibility!
Info:
- additional information
+ Extra information
-h, --help show usage help message and exit
-H, --full-help show full help message and exit
@@ -209,6 +328,10 @@ For advanced help use `-H` or `--full-help`:
Either argument -d or -f must be given.
+### Program parameter notes
+
+_decode-config.py_
+
### Examples
The most of the examples are for linux command line. Under Windows call the program using `python decode-config.py ...`.
diff --git a/tools/decode-config.py b/tools/decode-config.py
index 187eec5f3..d6632dbda 100755
--- a/tools/decode-config.py
+++ b/tools/decode-config.py
@@ -1,13 +1,13 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-VER = '2.0.0005'
+VER = '2.1.0006'
"""
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 modify
+ This program is free software: you can redistribute it and/or modfy
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.
@@ -37,10 +37,14 @@ Instructions:
Usage: decode-config.py [-f ] [-d ] [-P ]
[-u ] [-p ] [-i ]
- [-o ] [-F json|bin|dmp] [-E] [-e]
+ [-o ] [-t json|bin|dmp] [-E] [-e] [-F]
[--json-indent ] [--json-compact]
- [--json-hide-pw] [--json-unhide-pw] [-h] [-H] [-v]
- [-V] [-c ] [--ignore-warnings]
+ [--json-hide-pw] [--json-show-pw]
+ [--cmnd-indent ] [--cmnd-groups]
+ [--cmnd-nogroups] [--cmnd-sort] [--cmnd-unsort]
+ [-c ] [-S] [-T json|cmnd|command]
+ [-g {Display,Domoticz,Internal,KNX,Led,Logging,MCP230xx,MQTT,Main,Management,Pow,Sensor,Serial,SetOption,SonoffRF,System,Timers,Wifi} [{Display,Domoticz,Internal,KNX,Led,Logging,MCP230xx,MQTT,Main,Management,Pow,Sensor,Serial,SetOption,SonoffRF,System,Timers,Wifi} ...]]
+ [--ignore-warnings] [-h] [-H] [-v] [-V]
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
@@ -48,13 +52,6 @@ Usage: decode-config.py [-f ] [-d ] [-P ]
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
@@ -72,7 +69,7 @@ Usage: decode-config.py [-f ] [-d ] [-P ]
host HTTP access password (default: None)
Backup/Restore:
- Backup/Restore configuration file specification
+ Backup & restore specification
-i, --restore-file
file to restore configuration from (default: None).
@@ -82,25 +79,54 @@ Usage: decode-config.py [-f ] [-d ] [-P ]
file to backup configuration to (default: None).
Replacements: @v=firmware version, @f=device friendly
name, @h=device hostname
- -F, --backup-type json|bin|dmp
+ -t, --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
+ -F, --force-restore force restore even configuration is identical
- JSON:
- JSON backup format specification
+ JSON output:
+ JSON 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
+ --json-hide-pw hide passwords
+ --json-show-pw, --json-unhide-pw
+ unhide passwords (default)
+
+ Tasmota command output:
+ Tasmota command output format specification
+
+ --cmnd-indent
+ Tasmota command grouping indent level (default: '2').
+ 0 disables indent
+ --cmnd-groups group Tasmota commands (default)
+ --cmnd-nogroups leave Tasmota commands ungrouped
+ --cmnd-sort sort Tasmota commands (default)
+ --cmnd-unsort leave Tasmota commands unsorted
+
+ Common:
+ Optional arguments
+
+ -c, --config
+ program config file - can be used to set default
+ command args (default: None)
+ -S, --output display output regardsless of backup/restore usage
+ (default do not output on backup or restore usage)
+ -T, --output-format json|cmnd|command
+ display output format (default: 'json')
+ -g, --group {Display,Domoticz,Internal,KNX,Led,Logging,MCP230xx,MQTT,Main,Management,Pow,Sensor,Serial,SetOption,SonoffRF,System,Timers,Wifi}
+ limit data processing to command groups (default no
+ filter)
+ --ignore-warnings do not exit on warnings. Not recommended, used by your
+ own responsibility!
Info:
- additional information
+ Extra information
-h, --help show usage help message and exit
-H, --full-help show full help message and exit
@@ -146,6 +172,9 @@ class ExitCode:
MODULE_NOT_FOUND = 20
INTERNAL_ERROR = 21
+# ======================================================================
+# imports
+# ======================================================================
import os.path
import io
import sys, platform
@@ -155,6 +184,7 @@ def ModuleImportError(module):
sys.exit(ExitCode.MODULE_NOT_FOUND)
try:
from datetime import datetime
+ import time
import copy
import struct
import socket
@@ -168,18 +198,18 @@ try:
except ImportError, e:
ModuleImportError(e)
-
+# ======================================================================
+# globals
+# ======================================================================
PROG='{} v{} by Norbert Richter '.format(os.path.basename(sys.argv[0]),VER)
CONFIG_FILE_XOR = 0x5A
BINARYFILE_MAGIC = 0x63576223
STR_ENCODING = 'utf8'
+HIDDEN_PASSWORD = '********'
+INTERNAL = 'Internal'
+
DEFAULTS = {
- 'DEFAULT':
- {
- 'configfile': None,
- 'ignorewarning':False,
- },
'source':
{
'device': None,
@@ -194,483 +224,542 @@ DEFAULTS = {
'backupfile': None,
'backupfileformat': 'json',
'extension': True,
+ 'forcerestore': False,
},
'jsonformat':
{
'jsonindent': None,
'jsoncompact': False,
'jsonsort': True,
- 'jsonrawvalues':False,
- 'jsonrawkeys': False,
'jsonhidepw': False,
},
+ 'cmndformat':
+ {
+ 'cmndindent': 2,
+ 'cmndgroup': True,
+ 'cmndsort': True,
+ },
+ 'common':
+ {
+ 'output': False,
+ 'outputformat': 'json',
+ 'configfile': None,
+ 'ignorewarning':False,
+ 'filter': None,
+ },
}
args = {}
exitcode = 0
+# ======================================================================
+# Settings mapping
+# ======================================================================
"""
Settings dictionary describes the config file fields definition:
- Each setting name has a tuple containing the following items:
+ = { : }
+
+ : "string"
+ a python valid dictionary key (string)
- (format, baseaddr, datadef, )
+ : ( , , [,] )
+ a tuple containing the following items:
- where
+ : |
+ data type & format definition
+ :
+ defines the use of data at
+ format is defined in 'struct module format string'
+ see
+ https://docs.python.org/2.7/library/struct.html#format-strings
+ :
+ A dictionary describes a (sub)setting dictonary
+ and can recursively define another
- format
- Define the data interpretation.
- 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)
+ : | (, , )
+ address definition
+ :
+ The address (starting from 0) within binary config data.
+ :
+ number of bits used (positive integer)
+ :
+ bit shift :
+ >= 0: shift the result right
+ < 0: shift the result left
- baseaddr
- 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
- Data definition, is either a array definition or a
- tuple containing an array definition and min/max values
- Format: arraydef|(arraydef, min, max)
- arraydef:
+ : | (, [,cmd])
+ data definition
+ : None | | [] | [ ,...]
None:
- None must be given if the field contains a
- simple value desrcibed by the prefix
- n:
- [n]:
+ Single value, not an array
+ :
+ []
Defines a one-dimensional array of size
- [n, m <,o...>]
- Defines a multi-dimensional array
- min:
- defines a minimum valid value or None if all values
- for this format is allowed.
- max:
- defines a maximum valid value or None if all values
- for this format is allowed.
+ [ ,...]
+ Defines a one- or multi-dimensional array
+ :
+ value validation function
+ : (, )
+ Tasmota command definition
+ :
+ command group string
+ :
+ convert data into Tasmota command function
- converter (optional)
- Conversion methode(s): 'xxx'|func or ('xxx'|func, '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:
- 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
+ : | (, )
+ read/write converter
+ : None |
+ Will be used in Bin2Mapping to convert values read
+ from the binary data object into mapping dictionary
+ None
+ None indicates not read conversion
+
+ to convert value from binary object to JSON.
+ : None | False |
+ Will be used in Mapping2Bin to convert values read
+ from mapping dictionary before write to binary
+ data object
+ None
+ None indicates not write conversion
+ False
+ False indicates the value is readonly and will
+ not be written into the binary object.
+
+ to convert value from JSON back to binary object
+ Common definitions
+
+ : | | None
+ function to be called or string to evaluate:
+ :
+ A function name will be called with one or two parameter:
+ The value to be processed
+ (optional) the current array index (1,n)
+
+ A string will be evaluate as is. The following
+ placeholder can be used to replace it by runtime values:
+ '$':
+ will be replaced by the mapping name value
+ '#':
+ will be replace by array index (if any)
+ '@':
+ can be used as reference to other mapping values
+ see definition below for examples
+
+ : 'string' | "string"
+ characters enclosed in ' or "
+
+ : integer
+ numbers in the range -2147483648 through 2147483647
+ : unsigned integer
+ numbers in the range 0 through 4294967295
+
"""
+# ----------------------------------------------------------------------
+# Settings helper
+# ----------------------------------------------------------------------
def passwordread(value):
- return "********" if args.jsonhidepw else value
+ return HIDDEN_PASSWORD if args.jsonhidepw else value
def passwordwrite(value):
- return None if value=="********" else value
+ return None if value == HIDDEN_PASSWORD else value
+def bitsRead(x, n=0, c=1):
+ """
+ Reads bit(s) of a number
+
+ @param x:
+ the number from which to read
+
+ @param n:
+ which bit position to read
+
+ @param c:
+ how many bits to read (1 if omitted)
+
+ @return:
+ the bit value(s)
+ """
+ if isinstance(x,str):
+ x = int(x, 0)
+ if isinstance(x,str):
+ n = int(n, 0)
+ if n >= 0:
+ x >>= n
+ else:
+ x <<= abs(n)
+ if c>0:
+ x &= (1<, , [,]
+ 'cfg_holder': ('0 and bitsRead($,0,11)>(12*60) else "",time=time.strftime("%H:%M",time.gmtime((bitsRead($,0,11) if bitsRead($,29,2)==0 else bitsRead($,0,11) if bitsRead($,0,11)<=(12*60) else bitsRead($,0,11)-(12*60))*60)),window=bitsRead($,11,4),repeat=bitsRead($,15),days="{:07b}".format(bitsRead($,16,7))[::-1],device=bitsRead($,23,4)+1,power=bitsRead($,27,2) )')), ('"0x{:08x}".format($)', False) ),
+ 'time': (' 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, size and settings to be used depending on given binary config data
-
- @param decode_cfg:
- binary config data (decrypted)
-
- @return:
- version, size, settings to use; None if version is invalid
- """
- version = 0x0
- size = setting = None
-
- version = GetField(decode_cfg, 'version', Setting_6_2_1['version'], raw=True)
- # search setting definition top-down
- for cfg in sorted(Settings, key=lambda s: s[0], reverse=True):
- if version >= cfg[0]:
- version = cfg[0]
- size = cfg[1]
- setting = cfg[2].copy()
- break
-
- return version, size, setting
-
-
+# ======================================================================
+# Common helper
+# ======================================================================
class LogType:
INFO = 'INFO'
WARNING = 'WARNING'
@@ -756,8 +803,8 @@ def message(msg, typ=None, status=None, line=None):
"""
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 '',
+ 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 '')
@@ -816,14 +863,14 @@ class HTTPHeader:
def response(self):
header = str(self.contents).split('\n')
- if len(header)>0:
+ 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:
+ if ditem[0].strip().lower() == 'content-type' and len(ditem) > 1:
return ditem[1].strip()
return ''
@@ -852,7 +899,7 @@ class CustomHelpFormatter(configargparse.HelpFormatter):
return res
options = orgstr.split(', ')
- if len(options) <=1:
+ if len(options) <= 1:
action._formatted_action_invocation = orgstr
return orgstr
@@ -860,18 +907,83 @@ class CustomHelpFormatter(configargparse.HelpFormatter):
for option in options:
meta = ""
arg = option.split(' ')
- if len(arg)>1:
+ if len(arg) > 1:
meta = arg[1]
return_list.append(arg[0])
- if len(meta) >0 and len(return_list) >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 GetTemplateSizes():
+ """
+ Get all possible template sizes as list
+
+ @param version:
+ 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, size and settings to be used depending on given binary config data
+
+ @param decode_cfg:
+ binary config data (decrypted)
+
+ @return:
+ version, size, settings to use; None if version is invalid
+ """
+ version = 0x0
+ size = setting = None
+ version = GetField(decode_cfg, 'version', Setting_6_2_1['version'], raw=True)
+ # search setting definition top-down
+ for cfg in sorted(Settings, key=lambda s: s[0], reverse=True):
+ if version >= cfg[0]:
+ size = cfg[1]
+ setting = cfg[2]
+ break
+
+ return version, size, setting
+
+
+def GetGroupList(setting):
+ """
+ Get all avilable group definition from setting
+
+ @return:
+ configargparse.parse_args() result
+ """
+ groups = set()
+
+ for name in setting:
+ dev = setting[name]
+ format, group = GetFieldDef(dev, fields="format, group")
+ if group is not None and len(group) > 0:
+ groups.add(group)
+ if isinstance(format, dict):
+ subgroups = GetGroupList(format)
+ if subgroups is not None and len(subgroups) > 0:
+ for group in subgroups:
+ groups.add(group)
+
+ groups=list(groups)
+ groups.sort()
+ return groups
+
+
class FileType:
FILE_NOT_FOUND = None
DMP = 'dmp'
@@ -882,7 +994,6 @@ class FileType:
INVALID_JSON = 'invalid json'
INVALID_BIN = 'invalid bin'
-
def GetFileType(filename):
"""
Get the FileType class member of a given filename
@@ -960,39 +1071,22 @@ def GetVersionStr(version):
minor = ((version>>16) & 0xff)
release = ((version>> 8) & 0xff)
subrelease = (version & 0xff)
- if major>=6:
- if subrelease>0:
+ if major >= 6:
+ if subrelease > 0:
subreleasestr = str(subrelease)
else:
subreleasestr = ''
else:
- if subrelease>0:
+ 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)
+ return "{:d}.{:d}.{:d}{}{}".format( major, minor, release, '.' if (major >= 6 and subreleasestr != '') else '', subreleasestr)
-def MakeValidFilename(filename):
+def MakeFilename(filename, filetype, configmapping):
"""
- 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
+ Replace variables within a filename
@param filename:
original filename possible containing replacements:
@@ -1004,40 +1098,49 @@ def MakeFilename(filename, filetype, decode_cfg):
hostname
@param filetype:
FileType.x object - creates extension if not None
- @param decode_cfg:
+ @param configmapping:
binary config data (decrypted)
@return:
New filename with replacements
"""
v = f1 = f2 = f3 = f4 = ''
- if 'version' in decode_cfg:
- v = GetVersionStr( int(str(decode_cfg['version']), 0) )
+ if 'version' in configmapping:
+ v = GetVersionStr( int(str(configmapping['version']), 0) )
filename = filename.replace('@v', v)
- 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'] )
+ if 'friendlyname' in configmapping:
+ filename = filename.replace('@f', configmapping['friendlyname'][0] )
+ if 'hostname' in configmapping:
+ filename = filename.replace('@h', configmapping['hostname'] )
dirname = basename = ext = ''
+ name = filename
+
+ # split file parts
+ dirname = os.path.normpath(os.path.dirname(filename))
+ basename = os.path.basename(filename)
+ name, ext = os.path.splitext(basename)
+
+ # make a valid filename
try:
- dirname = os.path.normpath(os.path.dirname(filename))
- basename = os.path.basename(filename)
- name, ext = os.path.splitext(basename)
+ name = name.decode('unicode-escape').translate(dict((ord(char), None) for char in '\/*?:"<>|'))
except:
pass
- name = MakeValidFilename(name)
+ name = str(name.replace(' ','_'))
+ # append extension based on filetype if not given
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)):
ext = filetype.lower()
+ # join filename + extension
if len(ext):
name_ext = name+'.'+ext
else:
name_ext = name
+ # join path and filename
try:
filename = os.path.join(dirname, name_ext)
except:
@@ -1156,6 +1259,8 @@ def PushTasmotaConfig(encode_cfg, host, port, username=DEFAULTS['source']['usern
encrypted binary data or filename containing Tasmota encrypted binary config
@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
@@ -1165,8 +1270,6 @@ def PushTasmotaConfig(encode_cfg, host, port, username=DEFAULTS['source']['usern
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)
@@ -1192,10 +1295,10 @@ def PushTasmotaConfig(encode_cfg, host, port, username=DEFAULTS['source']['usern
c.close()
return e[0], e[1]
- if responsecode>=400:
+ if responsecode >= 400:
c.close()
return responsecode, header.response()
- elif header.contenttype()!='text/html':
+ 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)"
@@ -1227,9 +1330,9 @@ def PushTasmotaConfig(encode_cfg, host, port, username=DEFAULTS['source']['usern
return e[0], e[1]
c.close()
- if responsecode>=400:
+ if responsecode >= 400:
return responsecode, header.response()
- elif header.contenttype()!='text/html':
+ 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'
@@ -1244,7 +1347,6 @@ def DecryptEncrypt(obj):
@return:
decrypted configuration (if obj contains encrypted data)
- encrypted configuration (if obj contains decrypted data)
"""
if isinstance(obj, bytearray):
obj = str(obj)
@@ -1276,58 +1378,124 @@ def GetSettingsCrc(dobj):
return crc & 0xffff
-def GetFieldDef(fielddef):
+def GetFieldDef(fielddef, fields="format, addrdef, baseaddr, bits, bitshift, datadef, arraydef, validate, cmd, group, tasmotacmnd, converter, readconverter, writeconverter"):
"""
- Get the field def items
+ Get field definition items
@param fielddef:
field format - see "Settings dictionary" above
+ @param fields:
+ comma separated string list of values to be returned
+ possible values see fields default
@return:
- , , , , ,
- undefined items can be None
+ set of values defined in
"""
- format = baseaddr = datadef = convert = None
+ format = addrdef = baseaddr = datadef = arraydef = validate = cmd = group = tasmotacmnd = converter = readconverter = writeconverter = 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
+ # calling with nothing is wrong
+ if fielddef is None:
+ print >> sys.stderr, ' is None'
+ raise SyntaxError(' error')
+
+ # get top level items
+ if len(fielddef) == 3:
+ # converter not present
+ format, addrdef, datadef = fielddef
+ elif len(fielddef) == 4:
+ # converter present
+ format, addrdef, datadef, converter = fielddef
+ else:
+ print >> sys.stderr, 'wrong {} length ({}) in setting'.format(fielddef, len(fielddef))
+ raise SyntaxError(' error')
+
+ # ignore calls with 'root' setting
+ if isinstance(format, dict) and baseaddr is None and datadef is None:
+ return eval(fields)
+
+ if not isinstance(format, (unicode,str,dict)):
+ print >> sys.stderr, 'wrong {} type {} in {}'.format(format, type(format), fielddef)
+ raise SyntaxError(' error')
+
+ # extract addrdef items
+ baseaddr = addrdef
if isinstance(baseaddr, (list,tuple)):
- baseaddr, bits, bitshift = baseaddr
+ if len(baseaddr) == 3:
+ # baseaddr bit definition
+ baseaddr, bits, bitshift = baseaddr
+ if not isinstance(bits, int):
+ print >> sys.stderr, ' must be a integer in {}'.format(bits, fielddef)
+ raise SyntaxError(' error')
+ if not isinstance(bitshift, int):
+ print >> sys.stderr, ' must be a integer in {}'.format(bitshift, fielddef)
+ raise SyntaxError(' error')
+ else:
+ print >> sys.stderr, 'wrong {} length ({}) in {}'.format(addrdef, len(addrdef), fielddef)
+ raise SyntaxError(' error')
+ if not isinstance(baseaddr, int):
+ print >> sys.stderr, ' must be a integer in {}'.format(baseaddr, fielddef)
+ raise SyntaxError(' error')
- if isinstance(datadef, int):
- # convert single int into list with one item
- datadef = [datadef]
- return format, baseaddr, bits, bitshift, datadef, convert
+ # extract datadef items
+ arraydef = datadef
+ if isinstance(datadef, (tuple)):
+ if len(datadef) == 2:
+ # datadef has a validator
+ arraydef, validate = datadef
+ elif len(datadef) == 3:
+ # datadef has a validator and cmd set
+ arraydef, validate, cmd = datadef
+ # cmd must be a tuple with 2 objects
+ if isinstance(cmd, (tuple)) and len(cmd) == 2:
+ group, tasmotacmnd = cmd
+ if group is not None and not isinstance(group, (str, unicode)):
+ print >> sys.stderr, 'wrong {} in {}'.format(group, fielddef)
+ raise SyntaxError(' error')
+ if tasmotacmnd is not None and not isinstance(tasmotacmnd, (str, unicode)):
+ print >> sys.stderr, 'wrong {} in {}'.format(tasmotacmnd, fielddef)
+ raise SyntaxError(' error')
+ else:
+ print >> sys.stderr, 'wrong {} length ({}) in {}'.format(cmd, len(cmd), fielddef)
+ raise SyntaxError(' error')
+ else:
+ print >> sys.stderr, 'wrong {} length ({}) in {}'.format(datadef, len(datadef), fielddef)
+ raise SyntaxError(' error')
+
+ if validate is not None and (not isinstance(validate, (unicode,str)) and not callable(validate)):
+ print >> sys.stderr, 'wrong {} type {} in {}'.format(validate, type(validate), fielddef)
+ raise SyntaxError(' error')
+
+ # convert single int into one-dimensional list
+ if isinstance(arraydef, int):
+ arraydef = [arraydef]
+
+ if arraydef is not None and not isinstance(arraydef, (list)):
+ print >> sys.stderr, 'wrong {} type {} in {}'.format(arraydef, type(arraydef), fielddef)
+ raise SyntaxError(' error')
+
+ # get read/write converter items
+ readconverter = converter
+ if isinstance(converter, (tuple)):
+ if len(converter) == 2:
+ # converter has read/write converter
+ readconverter, writeconverter = converter
+ if readconverter is not None and not isinstance(readconverter, (str,unicode)) and not callable(readconverter):
+ print >> sys.stderr, 'wrong {} type {} in {}'.format(readconverter, type(readconverter), fielddef)
+ raise SyntaxError(' error')
+ if writeconverter is not None and (not isinstance(writeconverter, (bool,str,unicode)) and not callable(writeconverter)):
+ print >> sys.stderr, 'wrong {} type {} in {}'.format(writeconverter, type(writeconverter), fielddef)
+ raise SyntaxError(' error')
+ else:
+ print >> sys.stderr, 'wrong {} length ({}) in {}'.format(converter, len(converter), fielddef)
+ raise SyntaxError(' error')
+
+
+ return eval(fields)
-def MakeFieldBaseAddr(baseaddr, bits, bitshift):
- """
- Return a based on given arguments
-
- @param baseaddr:
- baseaddr from Settings definition
- @param bits:
- 0 or bits
- @param bitshift:
- 0 or bitshift
-
- @return:
- (,,) if bits != 0
- baseaddr if bits == 0
-
- """
- if bits!=0:
- return (baseaddr, bits, bitshift)
- return baseaddr
-
-
-def ConvertFieldValue(value, fielddef, read=True, raw=False):
+def ReadWriteConverter(value, fielddef, read=True, raw=False):
"""
Convert field value based on field desc
@@ -1343,33 +1511,107 @@ def ConvertFieldValue(value, fielddef, read=True, raw=False):
@return:
(un)converted value
"""
- format, baseaddr, bits, bitshift, datadef, convert = GetFieldDef(fielddef)
+ converter, readconverter, writeconverter = GetFieldDef(fielddef, fields='converter, readconverter, writeconverter')
# call password functions even if raw value should be processed
- if callable(convert) and (convert==passwordread or convert==passwordwrite):
+ if read and callable(readconverter) and readconverter == passwordread:
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):
+ if not read and callable(writeconverter) and writeconverter == 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]
+ if not raw and converter is not None:
+ conv = readconverter if read else writeconverter
try:
- if isinstance(convert, str): # evaluate strings
- return eval(convert.replace('$','value'))
- elif callable(convert): # use as format function
- return convert(value)
- except:
- pass
+ if isinstance(conv, str): # evaluate strings
+ return eval(conv.replace('$','value'))
+ elif callable(conv): # use as format function
+ return conv(value)
+ except Exception, e:
+ exit(e[0], e[1], typ=LogType.WARNING, line=inspect.getlineno(inspect.currentframe()))
return value
+def CmndConverter(valuemapping, value, idx, fielddef):
+ """
+ Convert field value into Tasmota command if available
+
+ @param valuemapping:
+ data mapping
+ @param value:
+ original value
+ @param fielddef
+ field definition - see "Settings dictionary" above
+
+ @return:
+ converted value or None if unable to convert
+ """
+ converter, readconverter, writeconverter, group, tasmotacmnd = GetFieldDef(fielddef, fields='converter, readconverter, writeconverter, group, tasmotacmnd')
+
+ result = None
+
+ if (callable(readconverter) and readconverter == passwordread) or (callable(writeconverter) and writeconverter == passwordwrite):
+ if value == HIDDEN_PASSWORD:
+ return None
+ else:
+ result = value
+
+ if tasmotacmnd is not None and len(tasmotacmnd) > 0:
+ if idx is not None:
+ idx += 1
+ if isinstance(tasmotacmnd, str): # evaluate strings
+ if idx is not None:
+ evalstr = tasmotacmnd.replace('$','value').replace('#','idx').replace('@','valuemapping')
+ else:
+ evalstr = tasmotacmnd.replace('$','value').replace('@','valuemapping')
+ # ~ try:
+ result = eval(evalstr)
+ # ~ except:
+ # ~ print evalstr
+ # ~ print value
+
+ elif callable(tasmotacmnd): # use as format function
+ if idx is not None:
+ result = tasmotacmnd(value, idx)
+ else:
+ result = tasmotacmnd(value)
+
+ return result
+
+
+def ValidateValue(value, fielddef):
+ """
+ Validate a value if validator is defined in fielddef
+
+ @param value:
+ original value
+ @param fielddef
+ field definition - see "Settings dictionary" above
+
+ @return:
+ True if value is valid, False if invalid
+ """
+ validate = GetFieldDef(fielddef, fields='validate')
+
+ if value == 0:
+ # can not complete all validate condition
+ # some Tasmota values are not allowed to be 0 on input
+ # even though these values are set to 0 on Tasmota initial.
+ # so we can't validate 0 values
+ return True;
+
+ valid = True
+ try:
+ if isinstance(validate, str): # evaluate strings
+ valid = eval(validate.replace('$','value'))
+ elif callable(validate): # use as format function
+ valid = validate(value)
+ except:
+ valid = False
+
+ return valid
+
+
def GetFieldMinMax(fielddef):
"""
Get minimum, maximum of field based on field format definition
@@ -1395,7 +1637,7 @@ def GetFieldMinMax(fielddef):
'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)
+ format = GetFieldDef(fielddef, fields='format')
_min = 0
_max = 0
@@ -1421,48 +1663,46 @@ def GetFieldLength(fielddef):
"""
length=0
- format, baseaddr, bits, bitshift, datadef, convert = GetFieldDef(fielddef)
+ format, addrdef, arraydef = GetFieldDef(fielddef, fields='format, addrdef, arraydef')
- if datadef is not None:
- # datadef contains a list
+ # contains a integer list
+ if isinstance(arraydef, list) and len(arraydef) > 0:
+ # arraydef contains a list
# calc size recursive by sum of all elements
- if isinstance(datadef, list):
- for i in range(0, datadef[0]):
+ for i in range(0, arraydef[0]):
+ subfielddef = GetSubfieldDef(fielddef)
+ if len(arraydef) > 1:
+ length += GetFieldLength( (format, addrdef, subfielddef) )
+ # single array
+ else:
+ length += GetFieldLength( (format, addrdef, None) )
- # 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(format, dict):
+ elif isinstance(format, dict):
# -> iterate through format
addr = None
setting = format
for name in setting:
- _dummy1, baseaddr, bits, bitshift, _dummy2, _dummy3 = GetFieldDef(setting[name])
+ baseaddr, bits, bitshift = GetFieldDef(setting[name], fields='baseaddr, bits, bitshift')
_len = GetFieldLength(setting[name])
if addr != baseaddr:
addr = baseaddr
length += _len
- else:
- if format[-1:] in ['b','B','c','?']:
- length=1
- elif format[-1:] in ['h','H']:
- length=2
- elif format[-1:] in ['i','I','l','L','f']:
- length=4
- elif format[-1:] in ['q','Q','d']:
- length=8
- elif format[-1:] 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))
+ # a simple value
+ elif isinstance(format, str):
+ if format[-1:] in ['b','B','c','?']:
+ length=1
+ elif format[-1:] in ['h','H']:
+ length=2
+ elif format[-1:] in ['i','I','l','L','f']:
+ length=4
+ elif format[-1:] in ['q','Q','d']:
+ length=8
+ elif format[-1:] 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
@@ -1477,23 +1717,52 @@ def GetSubfieldDef(fielddef):
@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
+ format, addrdef, datadef, arraydef, validate, cmd, converter = GetFieldDef(fielddef, fields='format, addrdef, datadef, arraydef, validate, cmd, converter')
+
+ # create new arraydef
+ if len(arraydef) > 1:
+ arraydef = arraydef[1:]
else:
- if len(fielddef)<4:
- subfielddef = (format, MakeFieldBaseAddr(baseaddr, bits, bitshift), None)
+ arraydef = None
+
+ # create new datadef
+ if isinstance(datadef, tuple):
+ if cmd is not None:
+ datadef = (arraydef, validate, cmd)
else:
- subfielddef = (format, MakeFieldBaseAddr(baseaddr, bits, bitshift), None, convert)
+ datadef = (arraydef, validate)
+ else:
+ datadef = arraydef
+
+ # set new field def
+ subfielddef = None
+ if converter is not None:
+ subfielddef = (format, addrdef, datadef, converter)
+ else:
+ subfielddef = (format, addrdef, datadef)
+
return subfielddef
+def IsFilterGroup(group):
+ """
+ Check if group is valid on filter
+
+ @param grooup:
+ group name to check
+
+ @return:
+ True if group is in filter, otherwise False
+ """
+ if args.filter is not None:
+ if group is None:
+ return False
+ if group != INTERNAL and group != '*' and group not in args.filter:
+ return False
+ return True
+
+
def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0):
"""
Get field value from definition
@@ -1510,68 +1779,69 @@ def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0):
use offset for baseaddr (used for recursive calls)
@return:
- read field value
+ field mapping
"""
if isinstance(dobj, bytearray):
dobj = str(dobj)
- result = None
-
- if fieldname == 'raw' and not args.jsonrawkeys:
- return result
+ valuemapping = None
# get field definition
- format, baseaddr, bits, bitshift, datadef, convert = GetFieldDef(fielddef)
+ format, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd = GetFieldDef(fielddef, fields='format, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd')
- # contains a integer list
- if isinstance(datadef, list):
- result = []
+ # filter groups
+ if not IsFilterGroup(group):
+ return valuemapping
+
+ # contains a integer list
+ if isinstance(arraydef, list) and len(arraydef) > 0:
+ valuemapping = []
offset = 0
- for i in range(0, datadef[0]):
+ for i in range(0, arraydef[0]):
subfielddef = GetSubfieldDef(fielddef)
length = GetFieldLength(subfielddef)
if length != 0:
- result.append(GetField(dobj, fieldname, subfielddef, raw=raw, addroffset=addroffset+offset))
+ value = GetField(dobj, fieldname, subfielddef, raw=raw, addroffset=addroffset+offset)
+ valuemapping.append(value)
offset += length
-
+
# 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
+ mapping_value = {}
+ # -> iterate through format
+ for name in format:
+ value = None
+ value = GetField(dobj, name, format[name], raw=raw, addroffset=addroffset)
+ if value is not None:
+ mapping_value[name] = value
+ # copy complete returned mapping
+ valuemapping = copy.deepcopy(mapping_value)
# a simple value
elif isinstance(format, (str, bool, int, float, long)):
if GetFieldLength(fielddef) != 0:
- result = struct.unpack_from(format, dobj, baseaddr+addroffset)[0]
+ valuemapping = 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 bits>0:
- result &= (1< 127
- result = unicode(s, errors='ignore')
+ valuemapping = unicode(s, errors='ignore')
- result = ConvertFieldValue(result, fielddef, read=True, raw=raw)
+ valuemapping = ReadWriteConverter(valuemapping, fielddef, read=True, raw=raw)
else:
exit(ExitCode.INTERNAL_ERROR, "Wrong mapping format definition: '{}'".format(format), typ=LogType.WARNING, doexit=not args.ignorewarning, line=inspect.getlineno(inspect.currentframe()))
- return result
+ return valuemapping
-def SetField(dobj, fieldname, fielddef, restore, raw=False, addroffset=0, filename=""):
+def SetField(dobj, fieldname, fielddef, restore, addroffset=0, filename=""):
"""
Get field value from definition
@@ -1581,48 +1851,50 @@ def SetField(dobj, fieldname, fielddef, restore, raw=False, addroffset=0, filena
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)
+ @param addroffset
+ use offset for baseaddr (used for recursive calls)
+ @param filename
+ related filename (for messages only)
+
+ @return:
+ new decrypted binary config data
"""
- format, baseaddr, bits, bitshift, datadef, convert = GetFieldDef(fielddef)
+ format, baseaddr, bits, bitshift, arraydef, group, writeconverter = GetFieldDef(fielddef, fields='format, baseaddr, bits, bitshift, arraydef, group, writeconverter')
+ # cast unicode
fieldname = str(fieldname)
- if fieldname == 'raw' and not args.jsonrawkeys:
+ # filter groups
+ if not IsFilterGroup(group):
return dobj
# do not write readonly values
- if isinstance(convert, (list,tuple)) and len(convert)>1 and convert[1]==None:
+ if writeconverter is False:
if args.debug:
- print >> sys.stderr, "SetField(): Readonly '{}' using '{}'/{}{} @{} skipped".format(fieldname, format, datadef, bits, hex(baseaddr+addroffset))
+ print >> sys.stderr, "SetField(): Readonly '{}' using '{}'/{}{} @{} skipped".format(fieldname, format, arraydef, bits, hex(baseaddr+addroffset))
return dobj
- # contains a list
- if isinstance(datadef, list):
+ # contains a list
+ if isinstance(arraydef, list) and len(arraydef) > 0:
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]):
+ if len(restore) > arraydef[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=arraydef[0]), typ=LogType.WARNING, doexit=not args.ignorewarning, line=inspect.getlineno(inspect.currentframe()))
+ for i in range(0, arraydef[0]):
subfielddef = GetSubfieldDef(fielddef)
length = GetFieldLength(subfielddef)
if length != 0:
- if i>=len(restore): # restore data list may be shorter than definition
+ 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
+ subrestore = restore[i]
+ dobj = SetField(dobj, fieldname, subfielddef, subrestore, addroffset=addroffset+offset, filename=filename)
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)
+ dobj = SetField(dobj, name, format[name], restore[name], addroffset=addroffset, filename=filename)
# a simple value
elif isinstance(format, (str, bool, int, float, long)):
@@ -1633,76 +1905,82 @@ def SetField(dobj, fieldname, fielddef, restore, raw=False, addroffset=0, filena
_min, _max = GetFieldMinMax(fielddef)
value = _value = None
skip = False
-
- # simple one value
+
+ # simple char value
if format[-1:] in ['c']:
try:
- value = ConvertFieldValue(restore.encode(STR_ENCODING)[0], fielddef, read=False, raw=raw)
- except:
- err = "valid range exceeding"
+ value = ReadWriteConverter(restore.encode(STR_ENCODING)[0], fielddef, read=False)
+ except Exception, e:
+ exit(e[0], e[1], typ=LogType.WARNING, line=inspect.getlineno(inspect.currentframe()))
valid = False
+
# bool
elif format[-1:] in ['?']:
try:
- value = ConvertFieldValue(bool(restore), fielddef, read=False, raw=raw)
- except:
- err = "valid range exceeding"
+ value = ReadWriteConverter(bool(restore), fielddef, read=False)
+ except Exception, e:
+ exit(e[0], e[1], typ=LogType.WARNING, line=inspect.getlineno(inspect.currentframe()))
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)
+ value = ReadWriteConverter(restore, fielddef, read=False)
+ if isinstance(value, (str, unicode)):
+ value = int(value, 0)
+ else:
+ value = int(value)
+ # bits
+ if bits != 0:
+ bitvalue = value
+ value = struct.unpack_from(format, dobj, baseaddr+addroffset)[0]
+ # validate restore value
+ valid = ValidateValue(bitvalue, fielddef)
+ if not valid:
+ err = "valid bit range exceeding"
else:
- value = int(value)
- # bit value
- if bits!=0:
- value = struct.unpack_from(format, dobj, baseaddr+addroffset)[0]
- bitvalue = int(restore)
mask = (1<mask:
+ if bitvalue > mask:
_min = 0
_max = mask
_value = bitvalue
valid = False
- err = "valid bit range exceeding"
else:
- if bitshift>=0:
+ if bitshift >= 0:
bitvalue <<= bitshift
mask <<= bitshift
else:
bitvalue >>= abs(bitshift)
mask >>= abs(bitshift)
+ v=value
value &= (0xffffffff ^ mask)
value |= bitvalue
- # full size values
- else:
- _value = value
- except:
- valid = False
- err = "valid range exceeding"
+
+ # full size values
+ else:
+ # validate restore function
+ valid = ValidateValue(value, fielddef)
+ if not valid:
+ err = "valid range exceeding"
+ _value = value
+
# float
elif format[-1:] in ['f','d']:
try:
- value = ConvertFieldValue(float(restore), fielddef, read=False, raw=raw)
+ value = ReadWriteConverter(float(restore), fielddef, read=False)
except:
- err = "valid range exceeding"
valid = False
+
# string
elif format[-1:] in ['s','p']:
- try:
- value = ConvertFieldValue(restore.encode(STR_ENCODING), fielddef, read=False, raw=raw)
- err = "string length exceeding"
- if value is not None:
- # be aware 0 byte at end of string (str must be < max, not <= max)
- _max -= 1
- valid = _min <= len(value) < _max
- else:
- skip = True
- valid = True
- except:
- valid = False
+ value = ReadWriteConverter(restore.encode(STR_ENCODING), fielddef, read=False)
+ err = "string length exceeding"
+ if value is not None:
+ # be aware 0 byte at end of string (str must be < max, not <= max)
+ _max -= 1
+ valid = _min <= len(value) < _max
+ else:
+ skip = True
+ valid = True
if value is None and not skip:
# None is an invalid value
@@ -1719,7 +1997,7 @@ def SetField(dobj, fieldname, fielddef, restore, raw=False, addroffset=0, filena
# copy value before possible change below
_value = value
- if isinstance(value, (str, unicode)):
+ if isinstance(_value, (str, unicode)):
_value = "'{}'".format(_value)
if valid:
@@ -1729,7 +2007,7 @@ def SetField(dobj, fieldname, fielddef, restore, raw=False, addroffset=0, filena
sbits=" {} bits shift {}".format(bits, bitshift)
else:
sbits = ""
- print >> sys.stderr, "SetField(): Set '{}' using '{}'/{}{} @{} to {}".format(fieldname, format, datadef, sbits, hex(baseaddr+addroffset), _value)
+ print >> sys.stderr, "SetField(): Set '{}' using '{}'/{}{} @{} to {}".format(fieldname, format, arraydef, sbits, hex(baseaddr+addroffset), _value)
if fieldname != 'cfg_crc':
prevvalue = struct.unpack_from(format, dobj, baseaddr+addroffset)[0]
struct.pack_into(format, dobj, baseaddr+addroffset, value)
@@ -1743,22 +2021,84 @@ def SetField(dobj, fieldname, fielddef, restore, raw=False, addroffset=0, filena
return dobj
-def Bin2Mapping(decode_cfg, raw=True):
+def SetCmnd(cmnds, fieldname, fielddef, valuemapping, mappedvalue, addroffset=0, idx=None):
+ """
+ Get field value from definition
+
+ @param cmnds:
+ Tasmota command mapping: { 'group': ['cmnd' <,'cmnd'...>] ... }
+ @param fieldname:
+ name of the field
+ @param fielddef:
+ see Settings desc above
+ @param valuemapping:
+ data mapping
+ @param mappedvalue
+ mappedvalue mapping with the new value(s)
+ @param addroffset
+ use offset for baseaddr (used for recursive calls)
+ @param idx
+ optional array index
+
+ @return:
+ new Tasmota command mapping
+ """
+ format, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd, writeconverter = GetFieldDef(fielddef, fields='format, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd, writeconverter')
+
+ # cast unicode
+ fieldname = str(fieldname)
+
+ # filter groups
+ if not IsFilterGroup(group):
+ return cmnds
+
+ # contains a list
+ if isinstance(arraydef, list) and len(arraydef) > 0:
+ offset = 0
+ if len(mappedvalue) > arraydef[0]:
+ exit(ExitCode.RESTORE_DATA_ERROR, "array '{sname}[{selem}]' exceeds max number of elements [{smax}]".format(sname=fieldname, selem=len(mappedvalue), smax=arraydef[0]), typ=LogType.WARNING, doexit=not args.ignorewarning, line=inspect.getlineno(inspect.currentframe()))
+ for i in range(0, arraydef[0]):
+ subfielddef = GetSubfieldDef(fielddef)
+ length = GetFieldLength(subfielddef)
+ if length != 0:
+ if i >= len(mappedvalue): # mappedvalue data list may be shorter than definition
+ break
+ subrestore = mappedvalue[i]
+ cmnds = SetCmnd(cmnds, fieldname, subfielddef, valuemapping, subrestore, addroffset=addroffset+offset, idx=i)
+ offset += length
+
+ # contains a dict
+ elif isinstance(format, dict):
+ for name in format: # -> iterate through format
+ if name in mappedvalue:
+ cmnds = SetCmnd(cmnds, name, format[name], valuemapping, mappedvalue[name], addroffset=addroffset, idx=idx)
+
+ # a simple value
+ elif isinstance(format, (str, bool, int, float, long)):
+ cmnd = CmndConverter(valuemapping, mappedvalue, idx, fielddef)
+
+ if group is not None and cmnd is not None:
+ if group not in cmnds:
+ cmnds[group] = []
+ cmnds[group].append(cmnd)
+
+ return cmnds
+
+
+def Bin2Mapping(decode_cfg):
"""
Decodes binary data stream into pyhton mappings dict
@param decode_cfg:
binary config data (decrypted)
- @param raw:
- decode raw values (True) or converted values (False)
@return:
- config data as mapping dictionary
+ valuemapping data as mapping dictionary
"""
if isinstance(decode_cfg, bytearray):
decode_cfg = str(decode_cfg)
- # get binary header to use
+ # get binary header and template to use
version, size, setting = GetTemplateSetting(decode_cfg)
# if we did not found a mathching setting
@@ -1771,7 +2111,7 @@ def Bin2Mapping(decode_cfg, raw=True):
# check size if exists
if 'cfg_size' in setting:
cfg_size = GetField(decode_cfg, 'cfg_size', setting['cfg_size'], raw=True)
- # read size should be same as definied in template
+ # read size should be same as definied in setting
if cfg_size > size:
# may be processed
exit(ExitCode.DATA_SIZE_MISMATCH, "Number of bytes read does ot match - read {}, expected {} byte".format(cfg_size, size), typ=LogType.ERROR,line=inspect.getlineno(inspect.currentframe()))
@@ -1787,41 +2127,39 @@ def Bin2Mapping(decode_cfg, raw=True):
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(decode_cfg, None, (setting,None,None), raw=raw)
+ # get valuemapping
+ valuemapping = GetField(decode_cfg, None, (setting,0,(None, None, (INTERNAL, None))))
# add header info
timestamp = datetime.now()
- 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,
- },
- 'template': {
- 'version': hex(version),
- 'crc': hex(cfg_crc),
- },
- 'data': {
- 'crc': hex(GetSettingsCrc(decode_cfg)),
- 'size': len(decode_cfg),
- },
- 'script': {
- 'name': os.path.basename(__file__),
- 'version': VER,
- },
- 'os': (platform.machine(), platform.system(), platform.release(), platform.version(), platform.platform()),
- 'python': platform.python_version(),
- }
+ valuemapping['header'] = { 'timestamp':timestamp.strftime("%Y-%m-%d %H:%M:%S"),
+ 'format': {
+ 'jsonindent': args.jsonindent,
+ 'jsoncompact': args.jsoncompact,
+ 'jsonsort': args.jsonsort,
+ 'jsonhidepw': args.jsonhidepw,
+ },
+ 'template': {
+ 'version': hex(version),
+ 'crc': hex(cfg_crc),
+ },
+ 'data': {
+ 'crc': hex(GetSettingsCrc(decode_cfg)),
+ 'size': len(decode_cfg),
+ },
+ 'script': {
+ 'name': os.path.basename(__file__),
+ 'version': VER,
+ },
+ 'os': (platform.machine(), platform.system(), platform.release(), platform.version(), platform.platform()),
+ 'python': platform.python_version(),
+ }
if 'cfg_crc' in setting:
- config['header']['template'].update({'size': cfg_size})
+ valuemapping['header']['template'].update({'size': cfg_size})
if 'version' in setting:
- config['header']['data'].update({'version': hex(cfg_version)})
+ valuemapping['header']['data'].update({'version': hex(cfg_version)})
- return config
+ return valuemapping
def Mapping2Bin(decode_cfg, jsonconfig, filename=""):
@@ -1836,7 +2174,7 @@ def Mapping2Bin(decode_cfg, jsonconfig, filename=""):
name of the restore file (for error output only)
@return:
- changed binary config data (decrypted)
+ changed binary config data (decrypted) or None on error
"""
if isinstance(decode_cfg, str):
decode_cfg = bytearray(decode_cfg)
@@ -1845,41 +2183,73 @@ def Mapping2Bin(decode_cfg, jsonconfig, filename=""):
# get binary header data to use the correct version template from device
version, size, setting = GetTemplateSetting(decode_cfg)
+ # make empty binarray array
_buffer = bytearray()
+ # add data
_buffer.extend(decode_cfg)
if setting 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)
+ SetField(_buffer, name, setting[name], jsonconfig[name], 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)
+ if 'cfg_crc' in setting:
+ 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
+ return None
-def Backup(backupfile, backupfileformat, encode_cfg, decode_cfg, configuration):
+def Mapping2Cmnd(decode_cfg, valuemapping, filename=""):
+ """
+ Encodes mapping data into Tasmota command mapping
+
+ @param decode_cfg:
+ binary config data (decrypted)
+ @param valuemapping:
+ data mapping
+ @param filename:
+ name of the restore file (for error output only)
+
+ @return:
+ Tasmota command mapping {group: [cmnd <,cmnd <,...>>]}
+ """
+ if isinstance(decode_cfg, str):
+ decode_cfg = bytearray(decode_cfg)
+
+ # get binary header data to use the correct version template from device
+ version, size, setting = GetTemplateSetting(decode_cfg)
+
+ cmnds = {}
+
+ if setting is not None:
+ # iterate through restore data mapping
+ for name in valuemapping:
+ # key must exist in both dict
+ if name in setting:
+ cmnds = SetCmnd(cmnds, name, setting[name], valuemapping, valuemapping[name], addroffset=0)
+ 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)
+
+ return cmnds
+
+ else:
+ exit(ExitCode.UNSUPPORTED_VERSION,"File '{}', Tasmota configuration version 0x{:x} not supported".format(filename, version), typ=LogType.WARNING, doexit=not args.ignorewarning)
+
+ return None
+
+
+def Backup(backupfile, backupfileformat, encode_cfg, decode_cfg, configmapping):
"""
Create backup file
@@ -1891,27 +2261,38 @@ def Backup(backupfile, backupfileformat, encode_cfg, decode_cfg, configuration):
binary config data (encrypted)
@param decode_cfg:
binary config data (decrypted)
- @param configuration:
+ @param configmapping:
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
+ 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
fileformat = ""
+ # Tasmota format
+ if backupfileformat.lower() == FileType.DMP.lower():
+ fileformat = "Tasmota"
+ backup_filename = MakeFilename(backupfile, FileType.DMP, configmapping)
+ if args.verbose:
+ message("Writing backup file '{}' ({} format)".format(backup_filename, fileformat), typ=LogType.INFO)
+ try:
+ backupfp = open(backup_filename, "wb")
+ backupfp.write(encode_cfg)
+ except Exception, e:
+ exit(e[0], "'{}' {}".format(backup_filename, e[1]),line=inspect.getlineno(inspect.currentframe()))
+ finally:
+ backupfp.close()
+
# binary format
- if backupfileformat.lower() == FileType.BIN.lower():
+ elif backupfileformat.lower() == FileType.BIN.lower():
fileformat = "binary"
- backup_filename = MakeFilename(backupfile, FileType.BIN, configuration)
+ backup_filename = MakeFilename(backupfile, FileType.BIN, configmapping)
if args.verbose:
message("Writing backup file '{}' ({} format)".format(backup_filename, fileformat), typ=LogType.INFO)
try:
@@ -1924,33 +2305,20 @@ def Backup(backupfile, backupfileformat, encode_cfg, decode_cfg, configuration):
finally:
backupfp.close()
- # Tasmota format
- if backupfileformat.lower() == FileType.DMP.lower():
- fileformat = "Tasmota"
- backup_filename = MakeFilename(backupfile, FileType.DMP, configuration)
+ # JSON format
+ elif backupfileformat.lower() == FileType.JSON.lower():
+ fileformat = "JSON"
+ backup_filename = MakeFilename(backupfile, FileType.JSON, configmapping)
if args.verbose:
message("Writing backup file '{}' ({} format)".format(backup_filename, fileformat), typ=LogType.INFO)
try:
- backupfp = open(backup_filename, "wb")
- backupfp.write(encode_cfg)
+ backupfp = open(backup_filename, "w")
+ json.dump(configmapping, backupfp, sort_keys=args.jsonsort, indent=None if args.jsonindent < 0 else args.jsonindent, separators=(',', ':') if args.jsoncompact else (', ', ': ') )
except Exception, e:
exit(e[0], "'{}' {}".format(backup_filename, e[1]),line=inspect.getlineno(inspect.currentframe()))
finally:
backupfp.close()
- # JSON format
- elif backupfileformat.lower() == FileType.JSON.lower():
- fileformat = "JSON"
- backup_filename = MakeFilename(backupfile, FileType.JSON, configuration)
- if args.verbose:
- message("Writing backup file '{}' ({} format)".format(backup_filename, fileformat), typ=LogType.INFO)
- try:
- backupfp = open(backup_filename, "w")
- json.dump(configuration, backupfp, sort_keys=args.jsonsort, indent=None if args.jsonindent<0 else args.jsonindent, separators=(',', ':') if args.jsoncompact else (', ', ': ') )
- except Exception, e:
- exit(e[0], "'{}' {}".format(backup_filename, e[1]),line=inspect.getlineno(inspect.currentframe()))
- finally:
- backupfp.close()
if args.verbose:
srctype = 'device'
src = args.device
@@ -1960,7 +2328,7 @@ def Backup(backupfile, backupfileformat, encode_cfg, decode_cfg, configuration):
message("Backup successful from {} '{}' to file '{}' ({} format)".format(srctype, src, backup_filename, fileformat), typ=LogType.INFO)
-def Restore(restorefile, encode_cfg, decode_cfg, configuration):
+def Restore(restorefile, encode_cfg, decode_cfg, configmapping):
"""
Restore from file
@@ -1968,13 +2336,13 @@ def Restore(restorefile, encode_cfg, decode_cfg, configuration):
binary config data (encrypted)
@param decode_cfg:
binary config data (decrypted)
- @param configuration:
+ @param configmapping:
config data mapppings
"""
new_encode_cfg = None
- restorefilename = MakeFilename(restorefile, None, configuration)
+ restorefilename = MakeFilename(restorefile, None, configmapping)
filetype = GetFileType(restorefilename)
if filetype == FileType.DMP:
@@ -2025,7 +2393,7 @@ def Restore(restorefile, encode_cfg, decode_cfg, configuration):
exit(ExitCode.FILE_READ_ERROR, "File '{}' unknown error".format(restorefilename),line=inspect.getlineno(inspect.currentframe()))
if new_encode_cfg is not None:
- if new_encode_cfg != encode_cfg or args.ignorewarning:
+ if args.forcerestore or new_encode_cfg != encode_cfg:
# write config direct to device via http
if args.device is not None:
if args.verbose:
@@ -2058,6 +2426,36 @@ def Restore(restorefile, encode_cfg, decode_cfg, configuration):
message("Configuration data leaving unchanged", typ=LogType.INFO)
+def OutputTasmotaCmnds(tasmotacmnds):
+ """
+ Print Tasmota command mapping
+
+ @param tasmotacmnds:
+ Tasmota command mapping {group: [cmnd <,cmnd <,...>>]}
+ """
+ def OutputTasmotaSubCmnds(cmnds):
+ if args.cmndsort:
+ for cmnd in sorted(cmnds, key = lambda cmnd:[int(c) if c.isdigit() else c for c in re.split('(\d+)', cmnd)]):
+ print "{}{}".format(" "*args.cmndindent, cmnd)
+ else:
+ for cmnd in cmnds:
+ print "{}{}".format(" "*args.cmndindent, cmnd)
+
+ if args.cmndgroup:
+ for group in Groups:
+ if group in tasmotacmnds:
+ cmnds = tasmotacmnds[group]
+ print
+ print "# {}:".format(group)
+ OutputTasmotaSubCmnds(cmnds)
+
+ else:
+ cmnds = []
+ for group in Groups:
+ if group in tasmotacmnds:
+ cmnds.extend(tasmotacmnds[group])
+ OutputTasmotaSubCmnds(cmnds)
+
def ParseArgs():
"""
Program argument parser
@@ -2098,7 +2496,7 @@ def ParseArgs():
default=DEFAULTS['source']['password'],
help="host HTTP access password (default: {})".format(DEFAULTS['source']['password']))
- backup = parser.add_argument_group('Backup/Restore', 'Backup/Restore configuration file specification')
+ backup = parser.add_argument_group('Backup/Restore', 'Backup & restore specification')
backup.add_argument('-i', '--restore-file',
metavar='',
dest='restorefile',
@@ -2109,11 +2507,11 @@ def ParseArgs():
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']))
- output_file_formats = ['json', 'bin', 'dmp']
- backup.add_argument('-F', '--backup-type',
- metavar='|'.join(output_file_formats),
+ backup_file_formats = ['json', 'bin', 'dmp']
+ backup.add_argument('-t', '--backup-type',
+ metavar='|'.join(backup_file_formats),
dest='backupfileformat',
- choices=output_file_formats,
+ choices=backup_file_formats,
default=DEFAULTS['backup']['backupfileformat'],
help="backup filetype (default: '{}')".format(DEFAULTS['backup']['backupfileformat']) )
backup.add_argument('-E', '--extension',
@@ -2126,8 +2524,13 @@ def ParseArgs():
action='store_false',
default=DEFAULTS['backup']['extension'],
help="do not append filetype extension, use -i and -o filename as passed{}".format(' (default)' if not DEFAULTS['backup']['extension'] else '') )
+ backup.add_argument('-F', '--force-restore',
+ dest='forcerestore',
+ action='store_true',
+ default=DEFAULTS['backup']['forcerestore'],
+ help="force restore even configuration is identical{}".format(' (default)' if DEFAULTS['backup']['forcerestore'] else '') )
- jsonformat = parser.add_argument_group('JSON', 'JSON backup format specification')
+ jsonformat = parser.add_argument_group('JSON output', 'JSON format specification')
jsonformat.add_argument('--json-indent',
metavar='',
dest='jsonindent',
@@ -2151,40 +2554,82 @@ def ParseArgs():
default=DEFAULTS['jsonformat']['jsonsort'],
help=configargparse.SUPPRESS) #"do not sort json keywords{}".format(' (default)' if not DEFAULTS['jsonformat']['jsonsort'] else '') )
- jsonformat.add_argument('--json-raw-values',
- dest='jsonrawvalues',
- action='store_true',
- default=DEFAULTS['jsonformat']['jsonrawvalues'],
- help=configargparse.SUPPRESS) #"output raw values{}".format(' (default)' if DEFAULTS['jsonformat']['jsonrawvalues'] else '') )
- jsonformat.add_argument('--json-convert-values',
- dest='jsonrawvalues',
- action='store_false',
- default=DEFAULTS['jsonformat']['jsonrawvalues'],
- help=configargparse.SUPPRESS) #"output converted, human readable values{}".format(' (default)' if not DEFAULTS['jsonformat']['jsonrawvalues'] else '') )
-
- jsonformat.add_argument('--json-raw-keys',
- dest='jsonrawkeys',
- action='store_true',
- default=DEFAULTS['jsonformat']['jsonrawkeys'],
- help=configargparse.SUPPRESS) #"output bitfield raw keys{}".format(' (default)' if DEFAULTS['jsonformat']['jsonrawkeys'] else '') )
- jsonformat.add_argument('--json-no-raw-keys',
- dest='jsonrawkeys',
- action='store_false',
- default=DEFAULTS['jsonformat']['jsonrawkeys'],
- help=configargparse.SUPPRESS) #"do not output bitfield raw keys{}".format(' (default)' if not DEFAULTS['jsonformat']['jsonrawkeys'] else '') )
-
jsonformat.add_argument('--json-hide-pw',
dest='jsonhidepw',
action='store_true',
default=DEFAULTS['jsonformat']['jsonhidepw'],
help="hide passwords{}".format(' (default)' if DEFAULTS['jsonformat']['jsonhidepw'] else '') )
- jsonformat.add_argument('--json-unhide-pw',
+ jsonformat.add_argument('--json-show-pw', '--json-unhide-pw',
dest='jsonhidepw',
action='store_false',
default=DEFAULTS['jsonformat']['jsonhidepw'],
help="unhide passwords{}".format(' (default)' if not DEFAULTS['jsonformat']['jsonhidepw'] else '') )
- info = parser.add_argument_group('Info','additional information')
+ cmndformat = parser.add_argument_group('Tasmota command output', 'Tasmota command output format specification')
+ cmndformat.add_argument('--cmnd-indent',
+ metavar='',
+ dest='cmndindent',
+ type=int,
+ default=DEFAULTS['cmndformat']['cmndindent'],
+ help="Tasmota command grouping indent level (default: '{}'). 0 disables indent".format(DEFAULTS['cmndformat']['cmndindent']) )
+ cmndformat.add_argument('--cmnd-groups',
+ dest='cmndgroup',
+ action='store_true',
+ default=DEFAULTS['cmndformat']['cmndgroup'],
+ help="group Tasmota commands{}".format(' (default)' if DEFAULTS['cmndformat']['cmndgroup'] else '') )
+ cmndformat.add_argument('--cmnd-nogroups',
+ dest='cmndgroup',
+ action='store_false',
+ default=DEFAULTS['cmndformat']['cmndgroup'],
+ help="leave Tasmota commands ungrouped{}".format(' (default)' if not DEFAULTS['cmndformat']['cmndgroup'] else '') )
+ cmndformat.add_argument('--cmnd-sort',
+ dest='cmndsort',
+ action='store_true',
+ default=DEFAULTS['cmndformat']['cmndsort'],
+ help="sort Tasmota commands{}".format(' (default)' if DEFAULTS['cmndformat']['cmndsort'] else '') )
+ cmndformat.add_argument('--cmnd-unsort',
+ dest='cmndsort',
+ action='store_false',
+ default=DEFAULTS['cmndformat']['cmndsort'],
+ help="leave Tasmota commands unsorted{}".format(' (default)' if not DEFAULTS['cmndformat']['cmndsort'] else '') )
+
+ common = parser.add_argument_group('Common', 'Optional arguments')
+ common.add_argument('-c', '--config',
+ metavar='',
+ dest='configfile',
+ default=DEFAULTS['common']['configfile'],
+ is_config_file=True,
+ help="program config file - can be used to set default command args (default: {})".format(DEFAULTS['common']['configfile']) )
+
+ common.add_argument('-S', '--output',
+ dest='output',
+ action='store_true',
+ default=DEFAULTS['common']['output'],
+ help="display output regardsless of backup/restore usage{}".format(" (default)" if DEFAULTS['common']['output'] else " (default do not output on backup or restore usage)") )
+ output_formats = ['json', 'cmnd','command']
+ common.add_argument('-T', '--output-format',
+ metavar='|'.join(output_formats),
+ dest='outputformat',
+ choices=output_formats,
+ default=DEFAULTS['common']['outputformat'],
+ help="display output format (default: '{}')".format(DEFAULTS['common']['outputformat']) )
+ groups = GetGroupList(Settings[0][2])
+ if '*' in groups:
+ groups.remove('*')
+ common.add_argument('-g', '--group',
+ dest='filter',
+ choices=groups,
+ nargs='+',
+ default=DEFAULTS['common']['filter'],
+ help="limit data processing to command groups (default {})".format("no filter" if DEFAULTS['common']['filter'] == None else DEFAULTS['common']['filter']) )
+ common.add_argument('--ignore-warnings',
+ dest='ignorewarning',
+ action='store_true',
+ default=DEFAULTS['common']['ignorewarning'],
+ help="do not exit on warnings{}. Not recommended, used by your own responsibility!".format(' (default)' if DEFAULTS['common']['ignorewarning'] else '') )
+
+
+ info = parser.add_argument_group('Info','Extra information')
info.add_argument('-D', '--debug',
dest='debug',
action='store_true',
@@ -2204,19 +2649,6 @@ def ParseArgs():
action='version',
version=PROG)
- # optional arguments
- parser.add_argument('-c', '--config',
- metavar='',
- dest='configfile',
- default=DEFAULTS['DEFAULT']['configfile'],
- is_config_file=True,
- help="program config file - can be used to set default command args (default: {})".format(DEFAULTS['DEFAULT']['configfile']) )
- parser.add_argument('--ignore-warnings',
- dest='ignorewarning',
- action='store_true',
- default=DEFAULTS['DEFAULT']['ignorewarning'],
- help="do not exit on warnings{}. Not recommended, used by your own responsibility!".format(' (default)' if DEFAULTS['DEFAULT']['ignorewarning'] else '') )
-
args = parser.parse_args()
if args.debug:
@@ -2266,23 +2698,28 @@ if __name__ == "__main__":
decode_cfg = DecryptEncrypt(encode_cfg)
# decode into mappings dictionary
- configuration = Bin2Mapping(decode_cfg, args.jsonrawvalues)
- if args.verbose and 'version' in configuration:
- if args.tasmotafile is not None:
- message("File '{}' contains data for Tasmota v{}".format(args.tasmotafile, GetVersionStr(configuration['version'])),typ=LogType.INFO)
- else:
- message("Device '{}' runs Tasmota v{}".format(args.device,GetVersionStr(configuration['version'])),typ=LogType.INFO)
+ configmapping = Bin2Mapping(decode_cfg)
+ if args.verbose and 'version' in configmapping:
+ message("{} '{}' is using version {}".format('File' if args.tasmotafile is not None else 'Device',
+ args.tasmotafile if args.tasmotafile is not None else args.device,
+ GetVersionStr(configmapping['version'])),
+ typ=LogType.INFO)
# backup to file
if args.backupfile is not None:
- Backup(args.backupfile, args.backupfileformat, encode_cfg, decode_cfg, configuration)
+ Backup(args.backupfile, args.backupfileformat, encode_cfg, decode_cfg, configmapping)
# restore from file
if args.restorefile is not None:
- Restore(args.restorefile, encode_cfg, decode_cfg, configuration)
+ Restore(args.restorefile, encode_cfg, decode_cfg, configmapping)
# json screen output
- if args.backupfile is None and args.restorefile is None:
- print json.dumps(configuration, sort_keys=args.jsonsort, indent=None if args.jsonindent<0 else args.jsonindent, separators=(',', ':') if args.jsoncompact else (', ', ': ') )
+ if (args.backupfile is None and args.restorefile is None) or args.output:
+ if args.outputformat == 'json':
+ print json.dumps(configmapping, sort_keys=args.jsonsort, indent=None if args.jsonindent<0 else args.jsonindent, separators=(',', ':') if args.jsoncompact else (', ', ': ') )
+ if args.outputformat == 'cmnd' or args.outputformat == 'command':
+ tasmotacmnds = Mapping2Cmnd(decode_cfg, configmapping)
+ OutputTasmotaCmnds(tasmotacmnds)
+
sys.exit(exitcode)