diff --git a/README.md b/README.md index 524ad49ab..cf9d79d5a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ If you like **Sonoff-Tasmota**, give it a star, or fork it and contribute! [![GitHub forks](https://img.shields.io/github/forks/arendst/Sonoff-Tasmota.svg?style=social&label=Fork)](https://github.com/arendst/Sonoff-Tasmota/network) [![donate](https://img.shields.io/badge/donate-PayPal-blue.svg)](https://paypal.me/tasmota) +See [RELEASENOTES.md](https://github.com/arendst/Sonoff-Tasmota/blob/development/RELEASENOTES.md) for release information. + +In addition to the [release webpage](https://github.com/arendst/Sonoff-Tasmota/releases/latest) the binaries can also be OTA downloaded from http://thehackbox.org/tasmota/release/ + ### Development [![Dev Version](https://img.shields.io/badge/development%20version-6.2.1.x-blue.svg)](https://github.com/arendst/Sonoff-Tasmota) [![Download Dev](https://img.shields.io/badge/download-development-yellow.svg)](http://thehackbox.org/tasmota/) @@ -114,21 +118,25 @@ You can contribute to Sonoff-Tasmota by Libraries used with Sonoff-Tasmota are: - [ESP8266 core for Arduino](https://github.com/esp8266/Arduino) - [Adafruit CCS811](https://github.com/adafruit/Adafruit_CCS811) +- [Adafruit ILI9341](https://github.com/adafruit/Adafruit_ILI9341) +- [Adafruit LED Backpack](https://github.com/adafruit/Adafruit-LED-Backpack-Library) - [Adafruit SGP30](https://github.com/adafruit/Adafruit_SGP30) +- [Adafruit SSD1306](https://github.com/adafruit/Adafruit_SSD1306) +- [Adafruit GFX](https://github.com/adafruit/Adafruit-GFX-Library) - [ArduinoJson](https://arduinojson.org/) +- [arduino mqtt](https://github.com/256dpi/arduino-mqtt) - [Bosch BME680](https://github.com/BoschSensortec/BME680_driver) - [C2 Programmer](http://app.cear.ufpb.br/~lucas.hartmann/tag/efm8bb1/) -- [Esp8266MqttClient](https://github.com/tuanpmt/ESP8266MQTTClient) - [esp-knx-ip](https://github.com/envy/esp-knx-ip) -- [esp-mqtt-arduino](https://github.com/i-n-g-o/esp-mqtt-arduino) -- [ESPAsyncUDP](https://github.com/me-no-dev/ESPAsyncUDP) - [I2Cdevlib](https://github.com/jrowberg/i2cdevlib) - [IRremoteEsp8266](https://github.com/markszabo/IRremoteESP8266) - [JobaTsl2561](https://github.com/joba-1/Joba_Tsl2561) +- [Liquid Cristal](https://github.com/marcoschwartz/LiquidCrystal_I2C) - [MultiChannelGasSensor](http://wiki.seeedstudio.com/Grove-Multichannel_Gas_Sensor/) - [NeoPixelBus](https://github.com/Makuna/NeoPixelBus) - [OneWire](https://github.com/PaulStoffregen/OneWire) - [PubSubClient](https://github.com/knolleary/pubsubclient) +- [rc-switch](https://github.com/sui77/rc-switch) #### People inspiring me People helping to keep the show on the road: @@ -142,16 +150,19 @@ People helping to keep the show on the road: - Flexiti for his initial timer implementation - reloxx13 for his [TasmoAdmin](https://github.com/reloxx13/TasmoAdmin) management tool - Joachim Banzhaf for his TSL2561 library and driver -- Gijs Noorlander for his MHZ19 and SenseAir drivers +- Gijs Noorlander for his MHZ19, SenseAir and updated PubSubClient drivers - Emontnemery for his HomeAssistant Discovery concept and many code tuning tips - Aidan Mountford for his HSB support - Daniel Ztolnai for his Serial Bridge implementation -- Gerhard Mutz for his SGP30 and Sunrise/Sunset driver +- Gerhard Mutz for his SGP30, Sunrise/Sunset and display support drivers - Nuno Ferreira for his HC-SR04 driver - Adrian Scillato for his (security)fixes and implementing and maintaining KNX - Gennaro Tortone for implementing and maintaining Eastron drivers - Raymond Mouthaan for managing Wemos Wiki information -- Norbert Richter, Frogmore42 and Jason2866 for providing many issue answers +- Norbert Richter for his decode-config.py tool +- Andre Thomas for providing [thehackbox](http://thehackbox.org/tasmota/) OTA support and daily development builds +- Joel Stein and digiblur for their Tuya research and driver +- Frogmore42 and Jason2866 for providing many issue answers - Many more providing Tips, Pocs or PRs ### License diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a98f96b33..b7a1537bb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,7 +15,7 @@ To save memory space all other binaries support **WifiManager only**. See _changelog.ino how to enable them again. - Define WIFI_CONFIG_TOOL now contains the default behaviour once a SSID has been configured. - If no SSID is configured making a wifi connection impossible the new define WIFI_CONFIG_NO_SSID will be used. -- While define WIFI_CONFIG_NO_SSID is set to WIFI_WPSCONFIG in user_config.h the compiler will check for define USE_WPS and if not enabled WIFI_CONFIG_NO_SSID will default to WIFI_MANAGER using the webserver. If define USE_WEBSERVER is also not enabled WIFI_CONFIG_NO_SSID will default to WIFI_SMARTCONFIG. If define USE_SMARTCONFIG is also not enabled WIFI_CONFIG_NO_SSID will default to a new option WIFI_SERIAL allowing to enter wifi parameters to serial which is always possible. +- While define WIFI_CONFIG_NO_SSID is set to WIFI_WPSCONFIG in my_user_config.h the compiler will check for define USE_WPS and if not enabled WIFI_CONFIG_NO_SSID will default to WIFI_MANAGER using the webserver. If define USE_WEBSERVER is also not enabled WIFI_CONFIG_NO_SSID will default to WIFI_SMARTCONFIG. If define USE_SMARTCONFIG is also not enabled WIFI_CONFIG_NO_SSID will default to a new option WIFI_SERIAL allowing to enter wifi parameters to serial which is always possible. ## Provided Binary Downloads The following binary downloads have been compiled with ESP8266/Arduino library version **2.3.0** @@ -25,6 +25,7 @@ The following binary downloads have been compiled with ESP8266/Arduino library v - **sonoff.bin** = The Sonoff version without Wps and SmartConfig configuration but adds more sensors. - **sonoff-BG.bin** to **sonoff-TW.bin** = The Sonoff version without Wps and SmartConfig configuration in different languages. - **sonoff-sensors.bin** = The Sensors version without Wps and SmartConfig configuration but adds even more useful sensors. +- **sonoff-display.bin** = The Display version without Wps and SmartConfig configuration but adds display support. - **sonoff-knx.bin** = The Knx version without Wps and SmartConfig configuration and some other features but adds KNX support. See [Tasmota ESP/Arduino library version related issues](https://github.com/arendst/Sonoff-Tasmota/wiki/Theo's-Tasmota-Tips#20180523---relation-tasmota-and-esp8266arduino-core-version) why these files are still released using ESP/Arduino library version v2.3.0. @@ -33,8 +34,8 @@ See [Tasmota ESP/Arduino library version related issues](https://github.com/aren | Feature or Sensor | minimal | classic | sonoff | knx | sensors | Remarks |--------------------------------|---------|---------|--------|------|---------|-------- -| ESP/Arduino lib v2.3.0 | 340k | 477k | 473k | 492k | 497k | -| ESP/Arduino lib v2.4.2 | 360k | 491k | 491k | 509k | 513k | No sleep +| ESP/Arduino lib v2.3.0 | 344k | 485k | 491k | 510k | 516k | +| ESP/Arduino lib v2.4.2 | 363k | 499k | 509k | 526k | 532k | No sleep | | | | | | | | MY_LANGUAGE en-GB | x | x | x | x | x | | USE_WPS | - | x | - | - | - | WPS @@ -55,7 +56,7 @@ See [Tasmota ESP/Arduino library version related issues](https://github.com/aren | USE_SUNRISE | - | - | x | x | x | | USE_RULES | - | - | x | x | x | | | | | | | | -| USE_ADC_VCC | x | x | x | x | | +| USE_ADC_VCC | x | x | x | x | - | | USE_DS18B20 | - | - | - | - | - | Single sensor | USE_DS18x20 | - | x | x | x | x | Multiple sensors | USE_DS18x20_LEGACY | - | - | - | - | - | Multiple sensors @@ -111,76 +112,89 @@ See [Tasmota ESP/Arduino library version related issues](https://github.com/aren | USE_RF_FLASH | - | - | x | x | x | | USE_TUYA_DIMMER | - | - | x | x | x | | USE_TX20_WIND_SENSOR | - | - | x | x | x | +| USE_RC_SWITCH | - | - | x | x | x | | USE_DISPLAY | - | - | - | - | - | ## Changelog -Version 6.2.1 20180905 - * Fix possible ambiguity on command parameters if StateText contains numbers only (#3656) - * Fix Wemo emulation to select the first relay when more than one relay is present (#3657) - * Fix possible exception due to buffer overflow (#3659) - * Fix lost energy today and total energy value after power cycle (#3689) - -Version 6.2.0 20180901 - * Allow user override of define MAX_RULE_VARS and MAX_RULE_TIMERS (#3561) - * Disable wifi sleep for both Esp8266/Arduino core 2.4.1 and 2.4.2 to solve device freeze caused by Espressif SDK bug (#3554) - * Change DS18B20 driver to provide better instant results - * Change some sensor drivers to provide instant results - * Change define USE_ALL_SENSORS to USE_SENSORS as it doesn't contain all sensors due to duplicate I2C addresses - * Change some sensor update timings: AdcEvery 200 -> 250, Senseair 300 -> 250, SDM120 300 -> 250, SDM630 300 -> 250 - * Change default Wifi config option from WPS to Wifi Manager if WPS is disabled or Wifi Smartconfig if webserver is disabled or Wifi Serial input if Smartconfig is disabled - * Change SHT1x driver to provide better instant results and fix I2C interference - * Change DHT driver to provide better instant results and add decimals to DHT11 (#3164) - * Change DS18x20 driver to provide better instant results (#3169) - * Change CounterType 1 from milliseconds to microseconds (#3437) - * Change scheduler for better sleep support using Uptime, Delay, PulseTime and TelePeriod, Blinktime (#3581) - * Remove unused functionality from Sonoff-minimal to save space - * Remove WPS and SmartConfig from sonoff-minimal saving 56k code space - * Remove TSL2561 debug message and update library (#2415) - * Remove forced restart when sleep command is executed (#3554) - * Fix invalid response using more than 4 switches and domoticz - * Fix sonoff-minimal not using default settings - * Fix unsecure main webpage update - * Fix DHT driver mixing values for different sensors (#1797) - * Fix EnergyReset3 regression not clearing total energy (#2723) - * Fix rules once regression from v6.1.0 (#3198, #3226) - * Fix command Scale buffer overflow (#3236) - * Fix possible WDT due to long MQTT publish handling (#3313) - * Fix command TimeDst/TimeStd invalid JSON (#3322) - * Fix handling of default names when using names starting with shortcut character ",0,1 or 2 (#3392, #3600, #3618) - * Fix LM75AD I2C sensor detection (#3408) - * Fix iFan02 power on state (#3412, #3530) - * Fix some Pow R2 and S31 checksum errors using optimized re-sync (#3425) - * Fix SDM120 reporting wrong negative values to Domoticz (#3521) - * Fix MQTT reconnection detection when using TasmotaMqtt library (#3558) - * Fix OtaMagic when file path contains a dash (-) (#3563) - * Fix Sonoff Bridge data reception when using Portisch EFM8 firmware using in data buffer length (#3605) - * Add read sensor retry to DS18B20, DS18x20, DHT, SHT1X and HTU21 - * Add user selection of Wifi Smartconfig as define USE_SMARTCONFIG in user_config.h - * Add boot loop detection and perform some solutions - * Add wifi and mqtt status led blinkyblinky to be disabled by SetOption31 1. Does not work when LedPower is On (deliberate) (#871, #2230, #3114, #3155) - * Add support for TM1638 switch (#2226) - * Add GPIO options ButtonXn, SwitchXn and CounterXn to select INPUT mode instead of INPUT_PULLUP (#2525) - * Add support for APDS9960 proximity sensor (#3051) - * Add support for MPR121 controller in input mode for touch buttons (#3142) - * Add support for MCP230xx for general purpose input expansion and command Sensor29 (#3188) - * Add default Wifi Configuration tool as define WIFI_CONFIG_NO_SSID in user_config.h if no SSID is configured (#3224) - * Add command Timers 0/1 to globally disable or enable armed timers (#3270) - * Add support for CCS811 sensor (#3309) - * Add Turkish language file (#3332) - * Add command SerialSend4 to send binary serial data (#3345) - * Add initial support for sensor MPU6050 (#3352) - * Add rule triggers Wifi#Connected and Wifi#Disconnected (#3359) - * Add option + to command Rule to concatenate new rule with existing rules (#3365) - * Add message when JavaScript is not enabled in webbrowser (#3388) - * Add build time setting of ButtonTopic and SwitchTopic (#3414) - * Add iFan02 Fanspeed + and Fanspeed - command options (#3415) - * Add Individual HSBColorX commands (#3430, #3615) - * Add output support on MCP23008/MCP23017 (#3436) - * Add modulo option to rules like rule1 on Time#Minute|5 do backlog power on;delay 200;power off endon (#3466) - * Add RGB support for Domoticz (#3547) - * Add all ruletimer values to command RuleTimer result message (#3571) - * Add command Publish2 for publishing retained MQTT messages (#3593) - * Add commands ButtonDebounce 40..1000 and SwitchDebounce 40..1000 to have user control over debounce timing. Default is 50mS (#3594) - * Add RuleX debug options 8,9,10 (StopOnError) to control RuleX execution status after an exception restart (#3607) - * Add rule variables %sunrise%, %sunset%, %uptime% and %time% (#3608) - * Add optional MQTT_TELE_RETAIN to Energy Margins message (#3612, 3614) +Version 6.3.0 soon + * Change web Configure Module GPIO drop down list order for better readability + * Change status JSON message providing more switch and retain information + * Change xsns_17_senseair.ino to use TasmotaModbus library + * Change MCP230xx driver + * Change PubSubClient Mqtt library to non-blocking EspEasy version + * Change energy monitoring using energy sensor driver modules + * Change Webserver page handler for easier extension (thx to Adrian Scillato) + * Change pinmode for no-pullup defined switches to pullup when configured as switchmode PUSHBUTTON (=3 and up) (#3896) + * Change default OTA Url to http://thehackbox.org/tasmota/release/sonoff.bin (#4170) + * Remove support for MQTT Client esp-mqtt-arduino by #define MQTT_LIBRARY_TYPE MQTT_ESPMQTTARDUINO + * Remove commands PowerCal, VoltageCal and CurrentCal as more functionality is provided by commands PowerSet, VoltageSet and CurrentSet + * Remove restart after ntpserver change and force NTP re-sync (#3890) + * Fix showing Period Power in energy threshold messages + * Fix header file execution order by renaming user_config.h to my_user_config.h + * Fix some TSL2561 driver issues (#3681) + * Fix KNX PA exception. Regression from 6.2.1 buffer overflow caused by subStr() (#3700, #3710) + * Fix setting and getting color temperature for Philips Hue emulation (#3733) + * Fix ButtonRetain to not use default topic for clearing retain messages (#3737) + * Fix syslog when emulation is selected (#2109, #3784) + * Fix rule trigger POWER1#STATE execution after restart and SetOption0 is 0 (#3856) + * Fix Home Assistant forced light discovery (#3908) + * Fix invalid configuration restores and decode_config.py crc error when savedata = 0 (#3918) + * Fix timer offset -00:00 causing 12:00 hour offset (#3923) + * Fix I2CScan invalid JSON error message (#3925) + * Fix exception when wrong Domoticz JSON message is received (#3963) + * Fix Sonoff Bridge RfRaw receive (#4080, #4085) + * Fix possible wifi connection error (#4044, #4083) + * Fix invalid JSON floating point result from nan (Not a Number) and inf (Infinity) into null (#4147) + * Fix rule mqtt#connected trigger when mqtt is disabled (#4149) + * Add support for LCD, Matrix, TFT and Oled displays + * Add support for Neo Coolcam Wifi Smart Power Plug + * Add support for Michael Haustein ESP Switch + * Add support for MQTT Client based on lwmqtt to be selected by #define MQTT_LIBRARY_TYPE MQTT_ARDUINOMQTT + * Add support for Neo Coolcam Wifi Smart Power Plug + * Add support for Michael Haustein ESP Switch + * Add support for MQTT Client based on lwmqtt to be selected by #define MQTT_LIBRARY_TYPE MQTT_ARDUINOMQTT + * Add support for DS3231 Real Time Clock + * Add support for HX711 Load Cell with optional web GUI scale interface to demonstrate easy GUI plug-in + * Add support for serial 8N2 communication to TasmotaModbus and TasmotaSerial libraries + * Add support for RF transceiving using library RcSwitch (#2702) + * Add support for Shelly 1 and Shelly 2 (#2789) + * Add support for La Crosse TX20 Anemometer (#2654, #3146) + * Add support for MP3 player using DFRobot RB-DFR-562 (#3723) + * Add Support for Xiaomi-Philips Bulbs (#3787) + * Add support for PCA9685 12bit 16pin hardware PWM driver (#3866) + * Add support for EXS Relay V5.0 (#3810) + * Add support for OBI Power Socket (#1988, #3944) + * Add support for Teckin Power Socket with Energy Monitoring (#3950) + * Add support for Pzem-003/017 DC Energy monitoring module (#3694) + * Add support for Pzem-014/016 AC Energy monitoring module (#3694) + * Add support for CSL Aplic WDP 303075 Power Socket with Energy Monitoring (#3991, #3996) + * Add support for Tuya Dimmer (#469, #4075) + * Add command Display to show all settings at once + * Add command SerialSend5 to send raw serial data like "A5074100545293" + * Add command WebRefresh 1000..10000 to control web page refresh in milliseconds. Default is 2345 + * Add command WeightRes 0..3 to control display of decimals for kilogram + * Add command SetOption52 to control display of optional time offset from UTC in JSON messages (#3629, #3711) + * Add command RGBWWTable to support color calibration (#3933) + * Add command Reset 4 (reset to defaults but keep wifi params) and Reset 5 (as reset 4 and also erase flash) (#4061) + * Add authentication to HTTP web pages + * Add decimals as input to commands PowerSet, VoltageSet and CurrentSet + * Add tools/decode-config.py by Norbert Richter to decode configuration data. See file for information + * Add define USE_DISPLAYS for selecting image sonoff-display + * Add auto reload of main web page to some web restarts + * Add TasmotaModbus library as very basic modbus wrapper for TasmotaSerial + * Add more API callbacks and document API.md + * Add Wifi channel number to state message (#3664) + * Add user configurable GPIO02 and GPIO03 on H801 devices (#3692) + * Add network information to display start screen (#3704) + * Add toggle function RGBW lights (#3695, #3697) + * Add sleep to Nova Fitness SDS01X sensor (#2841, #3724, #3749) + * Add Analog input AD0 enabled to sonoff-sensors.bin (#3756, #3757) + * Add userid/password option to decode-status.py (#3796) + * Add power value below 5W to Sonoff Pow R2 and S31 (#3745) + * Add force_update to Home Assistant discovery (#3873) + * Add delay after restart before processing rule sensor data (#3811) + * Add rule triggers SWITCH1#BOOT and POWER1#BOOT (#3904, #3910) + * Add Apparent Power and Reactive Power to Energy Monitoring devices (#251) + * Add RF Receiver control to module MagicHome to be used on Arilux LC10 (#3792) + * Add Hebrew language file (#3960) + * Add whitespace removal from RfRaw and SerialSend5 (#4020) diff --git a/platformio.ini b/platformio.ini index 852db9128..e685cf34f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,6 +13,7 @@ src_dir = sonoff ; *** Uncomment one of the lines below to build/upload only one environment env_default = sonoff ;env_default = sonoff-minimal +;env_default = sonoff-basic ;env_default = sonoff-classic ;env_default = sonoff-knx ;env_default = sonoff-sensors @@ -45,6 +46,8 @@ platform = espressif8266@1.5.0 ;platform = espressif8266@1.7.3 ; *** Esp8266 core for Arduino version 2.4.2 ;platform = espressif8266@1.8.0 +; *** Esp8266 core for Arduino version Core 2.5.0 beta tested for Tasmota +;platform = https://github.com/Jason2866/platform-espressif8266.git#Tasmota ; *** Esp8266 core for Arduino version latest beta ;platform = https://github.com/platformio/platform-espressif8266.git#feature/stage ; *** Esp8266 core for Arduino current version (located in %USERPROFILE%\.platformio\platforms\espressif8266) @@ -130,6 +133,20 @@ upload_resetmethod = ${common.upload_resetmethod} upload_speed = ${common.upload_speed} extra_scripts = ${common.extra_scripts} +[env:sonoff-basic] +platform = ${common.platform} +framework = ${common.framework} +board = ${common.board} +board_build.flash_mode = ${common.board_build.flash_mode} +board_build.f_cpu = ${common.board_build.f_cpu} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} -DUSE_BASIC +monitor_speed = ${common.monitor_speed} +upload_port = ${common.upload_port} +upload_resetmethod = ${common.upload_resetmethod} +upload_speed = ${common.upload_speed} +extra_scripts = ${common.extra_scripts} + [env:sonoff-classic] platform = ${common.platform} framework = ${common.framework} @@ -165,7 +182,7 @@ board = ${common.board} board_build.flash_mode = ${common.board_build.flash_mode} board_build.f_cpu = ${common.board_build.f_cpu} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} -DUSE_ALL_SENSORS +build_flags = ${common.build_flags} -DUSE_SENSORS monitor_speed = ${common.monitor_speed} upload_port = ${common.upload_port} upload_resetmethod = ${common.upload_resetmethod} diff --git a/sonoff/_changelog.ino b/sonoff/_changelog.ino index 563e555d1..b1417029e 100644 --- a/sonoff/_changelog.ino +++ b/sonoff/_changelog.ino @@ -3,6 +3,9 @@ * Fix invalid JSON floating point result from nan (Not a Number) and inf (Infinity) into null (#4147) * Fix rule mqtt#connected trigger when mqtt is disabled (#4149) * Initial release of RF transceiving using library RcSwitch (#2702) + * Change default OTA Url to http://thehackbox.org/tasmota/release/sonoff.bin (#4170) + * Add Tuya Software Serial to support additional Tuya configurations (#4178) + * Add sonoff-basic.bin without most sensors * * 6.2.1.18 20181019 * Add more API callbacks and document API.md diff --git a/sonoff/language/bg-BG.h b/sonoff/language/bg-BG.h index 7f56e33d3..d91c66bda 100644 --- a/sonoff/language/bg-BG.h +++ b/sonoff/language/bg-BG.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "h" #define D_UNIT_KILOGRAM "kg" #define D_UNIT_INCREMENTS "inc" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kΩ" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/cs-CZ.h b/sonoff/language/cs-CZ.h index a29f5741a..0cf34043b 100644 --- a/sonoff/language/cs-CZ.h +++ b/sonoff/language/cs-CZ.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/sonoff/language/de-DE.h b/sonoff/language/de-DE.h index 9f11e57ca..0c2ab9f6a 100644 --- a/sonoff/language/de-DE.h +++ b/sonoff/language/de-DE.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "h" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/el-GR.h b/sonoff/language/el-GR.h index beaaef57d..c5a3db324 100644 --- a/sonoff/language/el-GR.h +++ b/sonoff/language/el-GR.h @@ -1,3 +1,4 @@ + /* el-GR.h - localization for Greek - Greece for Sonoff-Tasmota @@ -527,6 +528,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +538,7 @@ #define D_UNIT_HOUR "Hr" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/en-GB.h b/sonoff/language/en-GB.h index 4e3aa0080..48641d0ff 100644 --- a/sonoff/language/en-GB.h +++ b/sonoff/language/en-GB.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "Hr" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/es-AR.h b/sonoff/language/es-AR.h index 39f198681..a5d044985 100644 --- a/sonoff/language/es-AR.h +++ b/sonoff/language/es-AR.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "Hr" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/fr-FR.h b/sonoff/language/fr-FR.h index 44ecea2b1..0e37d9990 100644 --- a/sonoff/language/fr-FR.h +++ b/sonoff/language/fr-FR.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "h" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kΩ" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/he-HE.h b/sonoff/language/he-HE.h index 73b44c67b..355fe8231 100644 --- a/sonoff/language/he-HE.h +++ b/sonoff/language/he-HE.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "Hr" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/hu-HU.h b/sonoff/language/hu-HU.h index 532d45643..cbe52378d 100644 --- a/sonoff/language/hu-HU.h +++ b/sonoff/language/hu-HU.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "ó" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/it-IT.h b/sonoff/language/it-IT.h index 61f361b89..8449d250d 100644 --- a/sonoff/language/it-IT.h +++ b/sonoff/language/it-IT.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "Hr" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/nl-NL.h b/sonoff/language/nl-NL.h index d2e913357..afaf9d08a 100644 --- a/sonoff/language/nl-NL.h +++ b/sonoff/language/nl-NL.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "h" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/pl-PL.h b/sonoff/language/pl-PL.h index 0f1303b30..686f5cc76 100644 --- a/sonoff/language/pl-PL.h +++ b/sonoff/language/pl-PL.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "Godz" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/pt-BR.h b/sonoff/language/pt-BR.h index 54d8448d7..ef5104fdb 100644 --- a/sonoff/language/pt-BR.h +++ b/sonoff/language/pt-BR.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "H" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/pt-PT.h b/sonoff/language/pt-PT.h index 0c8e03d62..97750f38c 100644 --- a/sonoff/language/pt-PT.h +++ b/sonoff/language/pt-PT.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "Hr" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/ru-RU.h b/sonoff/language/ru-RU.h index 11fdf90b0..28aa1d7f2 100644 --- a/sonoff/language/ru-RU.h +++ b/sonoff/language/ru-RU.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "А" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "Ч" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "кОм" #define D_UNIT_KILOWATTHOUR "кВт" #define D_UNIT_LUX "лк" diff --git a/sonoff/language/tr-TR.h b/sonoff/language/tr-TR.h index 86838d0df..0588b5175 100755 --- a/sonoff/language/tr-TR.h +++ b/sonoff/language/tr-TR.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "A" @@ -534,7 +536,7 @@ #define D_UNIT_HOUR "Hr" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "kOhm" #define D_UNIT_KILOWATTHOUR "kWh" #define D_UNIT_LUX "lx" diff --git a/sonoff/language/uk-UK.h b/sonoff/language/uk-UK.h index efc125351..fc32479fc 100644 --- a/sonoff/language/uk-UK.h +++ b/sonoff/language/uk-UK.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "А" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "Г" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "кОм" #define D_UNIT_KILOWATTHOUR "кВт" #define D_UNIT_LUX "лк" diff --git a/sonoff/language/zh-CN.h b/sonoff/language/zh-CN.h index 3414bc8d1..f4a19e324 100644 --- a/sonoff/language/zh-CN.h +++ b/sonoff/language/zh-CN.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "安" @@ -534,7 +536,7 @@ #define D_UNIT_HOUR "时" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "千欧" #define D_UNIT_KILOWATTHOUR "千瓦时" #define D_UNIT_LUX "勒克斯" diff --git a/sonoff/language/zh-TW.h b/sonoff/language/zh-TW.h index b5f14dace..61fa90d4d 100644 --- a/sonoff/language/zh-TW.h +++ b/sonoff/language/zh-TW.h @@ -527,6 +527,8 @@ #define D_SENSOR_TX20_TX "TX20" #define D_SENSOR_RFSEND "RFSend" #define D_SENSOR_RFRECV "RFrecv" +#define D_SENSOR_TUYA_TX "Tuya Tx" +#define D_SENSOR_TUYA_RX "Tuya Rx" // Units #define D_UNIT_AMPERE "安" @@ -535,7 +537,7 @@ #define D_UNIT_HOUR "時" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" -#define D_UNIT_KILOMETER_PER_HOUR "kmph" // or "km/h" +#define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" #define D_UNIT_KILOOHM "千歐" #define D_UNIT_KILOWATTHOUR "千瓦時" #define D_UNIT_LUX "勒克斯" diff --git a/sonoff/my_user_config.h b/sonoff/my_user_config.h index e970bcdf5..81eb4e987 100644 --- a/sonoff/my_user_config.h +++ b/sonoff/my_user_config.h @@ -79,7 +79,7 @@ #define WEB_LOG_LEVEL LOG_LEVEL_INFO // [WebLog] (LOG_LEVEL_NONE, LOG_LEVEL_ERROR, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, LOG_LEVEL_DEBUG_MORE) // -- Ota ----------------------------------------- -#define OTA_URL "http://sonoff.maddox.co.uk/tasmota/sonoff.bin" // [OtaUrl] +#define OTA_URL "http://thehackbox.org/tasmota/release/sonoff.bin" // [OtaUrl] // -- MQTT ---------------------------------------- #define MQTT_USE 1 // [SetOption3] Select default MQTT use (0 = Off, 1 = On) @@ -353,6 +353,8 @@ #define SDM630_SPEED 9600 // SDM630-Modbus RS485 serial speed (default: 9600 baud) //#define USE_MP3_PLAYER // Use of the DFPlayer Mini MP3 Player RB-DFR-562 commands: play, volume and stop #define MP3_VOLUME 10 // Set the startup volume on init, the range can be 0..30(max) +#define USE_TUYA_DIMMER // Add support for Tuya Serial Dimmer + #define TUYA_DIMMER_ID 3 // Default dimmer Id // Power monitoring sensors ----------------------- #define USE_PZEM004T // Add support for PZEM004T Energy monitor (+2k code) @@ -379,12 +381,9 @@ #define USE_RF_FLASH // Add support for flashing the EFM8BB1 chip on the Sonoff RF Bridge. C2CK must be connected to GPIO4, C2D to GPIO5 on the PCB (+3k code) -#define USE_TUYA_DIMMER // Add support for Tuya Serial Dimmer - #define TUYA_DIMMER_ID 3 // Default dimmer Id - #define USE_TX20_WIND_SENSOR // Add support for La Crosse TX20 anemometer (+2k code) -#define USE_RC_SWITCH // Add support for RF transceiver using library RcSwitch (+2k7 code) +#define USE_RC_SWITCH // Add support for RF transceiver using library RcSwitch (+2k7 code, 460 iram) /*********************************************************************************************\ * Debug features are only supported in development branch @@ -399,6 +398,7 @@ \*********************************************************************************************/ //#define USE_CLASSIC // Create sonoff-classic with initial configuration tools WPS, SmartConfig and WifiManager +//#define USE_BASIC // Create sonoff-basic with no sensors //#define USE_SENSORS // Create sonoff-sensors with useful sensors enabled //#define USE_KNX_NO_EMULATION // Create sonoff-knx with KNX but without Emulation //#define USE_DISPLAYS // Create sonoff-display with display drivers enabled diff --git a/sonoff/sonoff_post.h b/sonoff/sonoff_post.h index 62b3fc88a..186d0a349 100755 --- a/sonoff/sonoff_post.h +++ b/sonoff/sonoff_post.h @@ -57,10 +57,7 @@ void KNX_CB_Action(message_t const &msg, void *arg); #ifdef USE_SENSORS -#ifdef USE_ADC_VCC -#undef USE_ADC_VCC -#endif -//#define USE_ADC_VCC // Display Vcc in Power status. Disable for use as Analog input on selected devices +#undef USE_ADC_VCC // Add Analog input on selected devices #define USE_DS18x20 // For more than one DS18x20 sensors with id sort, single scan and read retry (+1k3 code) //#define USE_DS18x20_LEGACY // For more than one DS18x20 sensors with dynamic scan using library OneWire (+1k5 code) #define USE_I2C // I2C using library wire (+10k code, 0k2 mem, 124 iram) @@ -80,7 +77,17 @@ void KNX_CB_Action(message_t const &msg, void *arg); #define USE_INA219 // Add I2C code for INA219 Low voltage and current sensor (+1k code) #define USE_MGS // Add I2C code for Xadow and Grove Mutichannel Gas sensor using library Multichannel_Gas_Sensor (+10k code) //#define USE_APDS9960 // Add I2C code for APDS9960 Proximity Sensor. Disables SHT and VEML6070 (+4k7 code) +//#define USE_MCP230xx // Enable MCP23008/MCP23017 - Must define I2C Address in #define USE_MCP230xx_ADDR below - range 0x20 - 0x27 (+4k7 code) +// #define USE_MCP230xx_ADDR 0x20 // Enable MCP23008/MCP23017 I2C Address to use (Must be within range 0x20 through 0x27 - set according to your wired setup) +// #define USE_MCP230xx_OUTPUT // Enable MCP23008/MCP23017 OUTPUT support through sensor29 commands (+1k5 code) +// #define USE_MCP230xx_DISPLAYOUTPUT // Enable MCP23008/MCP23017 to display state of OUTPUT pins on Web UI (+0k2 code) +//#define USE_PCA9685 // Enable PCA9685 I2C HW PWM Driver - Must define I2C Address in #define USE_PCA9685_ADDR below - range 0x40 - 0x47 (+1k4 code) +// #define USE_PCA9685_ADDR 0x40 // Enable PCA9685 I2C Address to use (Must be within range 0x40 through 0x47 - set according to your wired setup) +// #define USE_PCA9685_FREQ 50 // Define default PWM frequency in Hz to be used (must be within 24 to 1526) - If other value is used, it will rever to 50Hz +//#define USE_MPR121 // Enable MPR121 controller (I2C addresses 0x5A, 0x5B, 0x5C and 0x5D) in input mode for touch buttons (+1k3 code) //#define USE_CCS811 // Add I2C code for CCS811 sensor (+2k2 code) +//#define USE_MPU6050 // Enable MPU6050 sensor (I2C address 0x68 AD0 low or 0x69 AD0 high) (+2k6 code) +//#define USE_DS3231 // Enable DS3231 external RTC in case no Wifi is avaliable. See docs in the source file (+1k2 code) #define USE_MHZ19 // Add support for MH-Z19 CO2 sensor (+2k code) #define USE_SENSEAIR // Add support for SenseAir K30, K70 and S8 CO2 sensor (+2k3 code) #ifndef CO2_LOW @@ -91,10 +98,17 @@ void KNX_CB_Action(message_t const &msg, void *arg); #endif #define USE_PMS5003 // Add support for PMS5003 and PMS7003 particle concentration sensor (+1k3 code) #define USE_NOVA_SDS // Add support for SDS011 and SDS021 particle concentration sensor (+0k7 code) -#define USE_PZEM004T // Add support for PZEM004T Energy monitor (+2k code) #define USE_SERIAL_BRIDGE // Add support for software Serial Bridge (+0k8 code) #define USE_SDM120 // Add support for Eastron SDM120-Modbus energy meter (+1k7 code) #define USE_SDM630 // Add support for Eastron SDM630-Modbus energy meter (+2k code) +#define USE_MP3_PLAYER // Use of the DFPlayer Mini MP3 Player RB-DFR-562 commands: play, volume and stop + #define MP3_VOLUME 10 // Set the startup volume on init, the range can be 0..30(max) +#define USE_TUYA_DIMMER // Add support for Tuya Serial Dimmer + #define TUYA_DIMMER_ID 3 // Default dimmer Id +#define USE_PZEM004T // Add support for PZEM004T Energy monitor (+2k code) +#define USE_PZEM_AC // Add support for PZEM014,016 Energy monitor (+1k1 code) +#define USE_PZEM_DC // Add support for PZEM003,017 Energy monitor (+1k1 code) +#define USE_MCP39F501 // Add support for MCP39F501 Energy monitor as used in Shelly 2 (+3k1 code) #define USE_IR_REMOTE // Send IR remote commands using library IRremoteESP8266 and ArduinoJson (+4k code, 0k3 mem, 48 iram) #define USE_IR_HVAC // Support for HVAC system using IR (+2k code) #define USE_IR_RECEIVE // Support for IR receiver (+5k5 code, 264 iram) @@ -105,6 +119,12 @@ void KNX_CB_Action(message_t const &msg, void *arg); // #define USE_WS2812_DMA // DMA supports only GPIO03 (= Serial RXD) (+1k mem). When USE_WS2812_DMA is enabled expect Exceptions on Pow #define USE_ARILUX_RF // Add support for Arilux RF remote controller (+0k8 code, 252 iram (non 2.3.0)) #define USE_SR04 // Add support for HC-SR04 ultrasonic devices (+1k code) +#define USE_TM1638 // Add support for TM1638 switches copying Switch1 .. Switch8 (+1k code) +#define USE_HX711 // Add support for HX711 load cell (+1k5 code) +//#define USE_HX711_GUI // Add optional web GUI to HX711 as scale (+1k8 code) +#define USE_RF_FLASH // Add support for flashing the EFM8BB1 chip on the Sonoff RF Bridge. C2CK must be connected to GPIO4, C2D to GPIO5 on the PCB (+3k code) +#define USE_TX20_WIND_SENSOR // Add support for La Crosse TX20 anemometer (+2k code) +#define USE_RC_SWITCH // Add support for RF transceiver using library RcSwitch (+2k7 code, 460 iram) #endif // USE_SENSORS /*********************************************************************************************\ @@ -141,6 +161,8 @@ void KNX_CB_Action(message_t const &msg, void *arg); #undef USE_SERIAL_BRIDGE // Disable support for software Serial Bridge #undef USE_SDM120 // Disable support for Eastron SDM120-Modbus energy meter #undef USE_SDM630 // Disable support for Eastron SDM630-Modbus energy meter +#undef USE_MP3_PLAYER // Disable DFPlayer Mini MP3 Player RB-DFR-562 commands: play, volume and stop +#undef USE_TUYA_DIMMER // Disable support for Tuya Serial Dimmer #undef USE_IR_REMOTE // Disable IR remote commands using library IRremoteESP8266 and ArduinoJson #undef USE_IR_RECEIVE // Disable support for IR receiver #undef USE_ARILUX_RF // Disable support for Arilux RF remote controller @@ -148,8 +170,8 @@ void KNX_CB_Action(message_t const &msg, void *arg); #undef USE_TM1638 // Disable support for TM1638 switches copying Switch1 .. Switch8 #undef USE_HX711 // Disable support for HX711 load cell #undef USE_RF_FLASH // Disable support for flashing the EFM8BB1 chip on the Sonoff RF Bridge. C2CK must be connected to GPIO4, C2D to GPIO5 on the PCB -#undef USE_TUYA_DIMMER // Disable support for Tuya Serial Dimmer #undef USE_TX20_WIND_SENSOR // Disable support for La Crosse TX20 anemometer +#undef USE_RC_SWITCH // Disable support for RF transceiver using library RcSwitch #undef DEBUG_THEO // Disable debug code #undef USE_DEBUG_DRIVER // Disable debug code #endif // USE_CLASSIC @@ -200,6 +222,62 @@ void KNX_CB_Action(message_t const &msg, void *arg); #define USE_DS18B20 // Default DS18B20 sensor needs no external library #endif +/*********************************************************************************************\ + * [sonoff-basic.bin] + * Provide an image without sensors +\*********************************************************************************************/ + +#ifdef USE_BASIC + +//#undef USE_ENERGY_SENSOR // Disable energy sensors +#undef USE_ARDUINO_OTA // Disable support for Arduino OTA +#undef USE_WPS // Disable support for WPS as initial wifi configuration tool +#undef USE_SMARTCONFIG // Disable support for Wifi SmartConfig as initial wifi configuration tool +#undef USE_DOMOTICZ // Disable Domoticz +#undef USE_HOME_ASSISTANT // Disable Home Assistant +#undef USE_MQTT_TLS // Disable TLS support won't work as the MQTTHost is not set +#undef USE_KNX // Disable KNX IP Protocol Support +//#undef USE_WEBSERVER // Disable Webserver +//#undef USE_EMULATION // Disable Wemo or Hue emulation +#undef USE_CUSTOM // Disable Custom features +#undef USE_DISCOVERY // Disable Discovery services for both MQTT and web server +//#undef USE_TIMERS // Disable support for up to 16 timers +//#undef USE_TIMERS_WEB // Disable support for timer webpage +//#undef USE_SUNRISE // Disable support for Sunrise and sunset tools +//#undef USE_RULES // Disable support for rules +#undef USE_DHT // Disable internal DHT sensor +#undef USE_DS18x20 // Disable DS18x20 sensor +#undef USE_DS18x20_LEGACY // Disable DS18x20 sensor +#undef USE_DS18B20 // Disable internal DS18B20 sensor +#undef USE_I2C // Disable all I2C sensors and devices +#undef USE_SPI // Disable all SPI devices +#undef USE_DISPLAY // Disable Display support +#undef USE_MHZ19 // Disable support for MH-Z19 CO2 sensor +#undef USE_SENSEAIR // Disable support for SenseAir K30, K70 and S8 CO2 sensor +#undef USE_PMS5003 // Disable support for PMS5003 and PMS7003 particle concentration sensor +#undef USE_NOVA_SDS // Disable support for SDS011 and SDS021 particle concentration sensor +#undef USE_SERIAL_BRIDGE // Disable support for software Serial Bridge +#undef USE_SDM120 // Disable support for Eastron SDM120-Modbus energy meter +#undef USE_SDM630 // Disable support for Eastron SDM630-Modbus energy meter +#undef USE_MP3_PLAYER // Disable DFPlayer Mini MP3 Player RB-DFR-562 commands: play, volume and stop +//#undef USE_TUYA_DIMMER // Disable support for Tuya Serial Dimmer +#undef USE_PZEM004T // Disable PZEM004T energy sensor +#undef USE_PZEM_AC // Disable PZEM014,016 Energy monitor +#undef USE_PZEM_DC // Disable PZEM003,017 Energy monitor +//#undef USE_MCP39F501 // Disable MCP39F501 Energy monitor as used in Shelly 2 +#undef USE_IR_REMOTE // Disable IR driver +#undef USE_WS2812 // Disable WS2812 Led string +#undef USE_ARILUX_RF // Disable support for Arilux RF remote controller +#undef USE_SR04 // Disable support for for HC-SR04 ultrasonic devices +#undef USE_TM1638 // Disable support for TM1638 switches copying Switch1 .. Switch8 +#undef USE_HX711 // Disable support for HX711 load cell +#undef USE_RF_FLASH // Disable support for flashing the EFM8BB1 chip on the Sonoff RF Bridge. C2CK must be connected to GPIO4, C2D to GPIO5 on the PCB +#undef USE_TX20_WIND_SENSOR // Disable support for La Crosse TX20 anemometer +#undef USE_RC_SWITCH // Disable support for RF transceiver using library RcSwitch +#undef DEBUG_THEO // Disable debug code +#undef USE_DEBUG_DRIVER // Disable debug code +#endif // USE_BASIC + /*********************************************************************************************\ * [sonoff-minimal.bin] * Provide the smallest image possible while still enabling a webserver for intermediate image load @@ -216,9 +294,9 @@ void KNX_CB_Action(message_t const &msg, void *arg); #undef USE_MQTT_TLS // Disable TLS support won't work as the MQTTHost is not set #undef USE_KNX // Disable KNX IP Protocol Support //#undef USE_WEBSERVER // Disable Webserver +#undef USE_EMULATION // Disable Wemo or Hue emulation #undef USE_CUSTOM // Disable Custom features #undef USE_DISCOVERY // Disable Discovery services for both MQTT and web server -#undef USE_EMULATION // Disable Wemo or Hue emulation #undef USE_TIMERS // Disable support for up to 16 timers #undef USE_TIMERS_WEB // Disable support for timer webpage #undef USE_SUNRISE // Disable support for Sunrise and sunset tools @@ -234,10 +312,15 @@ void KNX_CB_Action(message_t const &msg, void *arg); #undef USE_SENSEAIR // Disable support for SenseAir K30, K70 and S8 CO2 sensor #undef USE_PMS5003 // Disable support for PMS5003 and PMS7003 particle concentration sensor #undef USE_NOVA_SDS // Disable support for SDS011 and SDS021 particle concentration sensor -#undef USE_PZEM004T // Disable PZEM004T energy sensor #undef USE_SERIAL_BRIDGE // Disable support for software Serial Bridge #undef USE_SDM120 // Disable support for Eastron SDM120-Modbus energy meter #undef USE_SDM630 // Disable support for Eastron SDM630-Modbus energy meter +#undef USE_MP3_PLAYER // Disable DFPlayer Mini MP3 Player RB-DFR-562 commands: play, volume and stop +#undef USE_TUYA_DIMMER // Disable support for Tuya Serial Dimmer +#undef USE_PZEM004T // Disable PZEM004T energy sensor +#undef USE_PZEM_AC // Disable PZEM014,016 Energy monitor +#undef USE_PZEM_DC // Disable PZEM003,017 Energy monitor +#undef USE_MCP39F501 // Disable MCP39F501 Energy monitor as used in Shelly 2 #undef USE_IR_REMOTE // Disable IR driver #undef USE_WS2812 // Disable WS2812 Led string #undef USE_ARILUX_RF // Disable support for Arilux RF remote controller @@ -245,8 +328,8 @@ void KNX_CB_Action(message_t const &msg, void *arg); #undef USE_TM1638 // Disable support for TM1638 switches copying Switch1 .. Switch8 #undef USE_HX711 // Disable support for HX711 load cell #undef USE_RF_FLASH // Disable support for flashing the EFM8BB1 chip on the Sonoff RF Bridge. C2CK must be connected to GPIO4, C2D to GPIO5 on the PCB -#undef USE_TUYA_DIMMER // Disable support for Tuya Serial Dimmer #undef USE_TX20_WIND_SENSOR // Disable support for La Crosse TX20 anemometer +#undef USE_RC_SWITCH // Disable support for RF transceiver using library RcSwitch #undef DEBUG_THEO // Disable debug code #undef USE_DEBUG_DRIVER // Disable debug code #endif // BE_MINIMAL diff --git a/sonoff/sonoff_template.h b/sonoff/sonoff_template.h index 13a6ffd0c..f3712672f 100644 --- a/sonoff/sonoff_template.h +++ b/sonoff/sonoff_template.h @@ -128,9 +128,11 @@ enum UserSelectablePins { GPIO_SDS0X1_TX, // Nova Fitness SDS011 Serial interface GPIO_HX711_SCK, // HX711 Load Cell clock GPIO_HX711_DAT, // HX711 Load Cell data - GPIO_TX20_TXD_BLACK, // TX20 Transmission Pin + GPIO_TX20_TXD_BLACK, // TX20 Transmission Pin GPIO_RFSEND, // RF transmitter GPIO_RFRECV, // RF receiver + GPIO_TUYA_TX, // Tuya Serial interface + GPIO_TUYA_RX, // Tuya Serial interface GPIO_SENSOR_END }; // Programmer selectable GPIO functionality offset by user selectable GPIOs @@ -187,7 +189,8 @@ const char kSensorNames[] PROGMEM = D_SENSOR_DFR562 "|" D_SENSOR_SDS0X1_TX "|" D_SENSOR_HX711_SCK "|" D_SENSOR_HX711_DAT "|" D_SENSOR_TX20_TX "|" - D_SENSOR_RFSEND "|" D_SENSOR_RFRECV; + D_SENSOR_RFSEND "|" D_SENSOR_RFRECV "|" + D_SENSOR_TUYA_TX "|" D_SENSOR_TUYA_RX; /********************************************************************************************/ @@ -371,7 +374,9 @@ const uint8_t kGpioNiceList[GPIO_SENSOR_END] PROGMEM = { GPIO_SDM630_RX, // SDM630 Serial interface GPIO_PMS5003, // Plantower PMS5003 Serial interface GPIO_TX20_TXD_BLACK, // TX20 Transmission Pin - GPIO_MP3_DFR562 // RB-DFR-562, DFPlayer Mini MP3 Player Serial interface + GPIO_MP3_DFR562, // RB-DFR-562, DFPlayer Mini MP3 Player Serial interface + GPIO_TUYA_TX, // Tuya Serial interface + GPIO_TUYA_RX // Tuya Serial interface }; const uint8_t kModuleNiceList[MAXMODULE] PROGMEM = { @@ -1150,9 +1155,9 @@ const mytmplt kModules[MAXMODULE] PROGMEM = { { "Tuya Dimmer", // Tuya Dimmer (ESP8266 w/ separate MCU dimmer) // https://www.amazon.com/gp/product/B07CTNSZZ8/ref=oh_aui_detailpage_o00_s00?ie=UTF8&psc=1 GPIO_KEY1, // Virtual Button (controlled by MCU) - GPIO_TXD, // GPIO01 MCU serial control + GPIO_USER, // GPIO01 MCU serial control GPIO_USER, - GPIO_RXD, // GPIO03 MCU serial control + GPIO_USER, // GPIO03 MCU serial control GPIO_USER, GPIO_USER, 0, 0, 0, 0, 0, 0, // Flash connection diff --git a/sonoff/xdrv_16_tuyadimmer.ino b/sonoff/xdrv_16_tuyadimmer.ino index 1a5776d4a..eb700c322 100644 --- a/sonoff/xdrv_16_tuyadimmer.ino +++ b/sonoff/xdrv_16_tuyadimmer.ino @@ -20,8 +20,13 @@ #ifdef USE_TUYA_DIMMER #ifndef TUYA_DIMMER_ID -#define TUYA_DIMMER_ID 3 +#define TUYA_DIMMER_ID 3 #endif +#define TUYA_BUFFER_SIZE 256 + +#include + +TasmotaSerial *TuyaSerial = nullptr; uint8_t tuya_new_dim = 0; // Tuya dimmer value temp boolean tuya_ignore_dim = false; // Flag to skip serial send to prevent looping when processing inbound states from the faceplate interaction @@ -29,6 +34,9 @@ uint8_t tuya_cmd_status = 0; // Current status of serial-read uint8_t tuya_cmd_checksum = 0; // Checksum of tuya command uint8_t tuya_data_len = 0; // Data lenght of command +char tuya_buffer[TUYA_BUFFER_SIZE]; // Serial receive buffer +int tuya_byte_counter = 0; // Index in serial receive buffer + boolean TuyaSetPower() { boolean status = false; @@ -36,24 +44,24 @@ boolean TuyaSetPower() uint8_t rpower = XdrvMailbox.index; int16_t source = XdrvMailbox.payload; - if (source != SRC_SWITCH ) { // ignore to prevent loop from pushing state from faceplate interaction + if (source != SRC_SWITCH && TuyaSerial) { // ignore to prevent loop from pushing state from faceplate interaction snprintf_P(log_data, sizeof(log_data), PSTR("TYA: SetDevicePower.rpower=%d"), rpower); AddLog(LOG_LEVEL_DEBUG); - Serial.write(0x55); // Tuya header 55AA - Serial.write(0xAA); - Serial.write(0x00); // version 00 - Serial.write(0x06); // Tuya command 06 - Serial.write(0x00); - Serial.write(0x05); // following data length 0x05 - Serial.write(0x01); // relay number 1,2,3 - Serial.write(0x01); - Serial.write(0x00); - Serial.write(0x01); - Serial.write(rpower); // status - Serial.write(0x0D + rpower); // checksum sum of all bytes in packet mod 256 - Serial.flush(); + TuyaSerial->write((uint8_t)0x55); // Tuya header 55AA + TuyaSerial->write((uint8_t)0xAA); + TuyaSerial->write((uint8_t)0x00); // version 00 + TuyaSerial->write((uint8_t)0x06); // Tuya command 06 + TuyaSerial->write((uint8_t)0x00); + TuyaSerial->write((uint8_t)0x05); // following data length 0x05 + TuyaSerial->write((uint8_t)0x01); // relay number 1,2,3 + TuyaSerial->write((uint8_t)0x01); + TuyaSerial->write((uint8_t)0x00); + TuyaSerial->write((uint8_t)0x01); + TuyaSerial->write((uint8_t)rpower); // status + TuyaSerial->write((uint8_t)0x0D + rpower); // checksum sum of all bytes in packet mod 256 + TuyaSerial->flush(); status = true; } @@ -62,26 +70,26 @@ boolean TuyaSetPower() void LightSerialDuty(uint8_t duty) { - if (duty > 0 && !tuya_ignore_dim ) { + if (duty > 0 && !tuya_ignore_dim && TuyaSerial) { if (duty < 25) { duty = 25; // dimming acts odd below 25(10%) - this mirrors the threshold set on the faceplate itself } - Serial.write(0x55); // Tuya header 55AA - Serial.write(0xAA); - Serial.write(0x00); // version 00 - Serial.write(0x06); // Tuya command 06 - send order - Serial.write(0x00); - Serial.write(0x08); // following data length 0x08 - Serial.write(Settings.param[P_TUYA_DIMMER_ID]); // dimmer id - Serial.write(0x02); // type=value - Serial.write(0x00); // length hi - Serial.write(0x04); // length low - Serial.write(0x00); // - Serial.write(0x00); // - Serial.write(0x00); // - Serial.write( duty ); // dim value (0-255) - Serial.write( byte(Settings.param[P_TUYA_DIMMER_ID] + 19 + duty) ); // checksum:sum of all bytes in packet mod 256 - Serial.flush(); + TuyaSerial->write((uint8_t)0x55); // Tuya header 55AA + TuyaSerial->write((uint8_t)0xAA); + TuyaSerial->write((uint8_t)0x00); // version 00 + TuyaSerial->write((uint8_t)0x06); // Tuya command 06 - send order + TuyaSerial->write((uint8_t)0x00); + TuyaSerial->write((uint8_t)0x08); // following data length 0x08 + TuyaSerial->write((uint8_t)Settings.param[P_TUYA_DIMMER_ID]); // dimmer id + TuyaSerial->write((uint8_t)0x02); // type=value + TuyaSerial->write((uint8_t)0x00); // length hi + TuyaSerial->write((uint8_t)0x04); // length low + TuyaSerial->write((uint8_t)0x00); // + TuyaSerial->write((uint8_t)0x00); // + TuyaSerial->write((uint8_t)0x00); // + TuyaSerial->write((uint8_t) duty ); // dim value (0-255) + TuyaSerial->write((uint8_t) byte(Settings.param[P_TUYA_DIMMER_ID] + 19 + duty) ); // checksum:sum of all bytes in packet mod 256 + TuyaSerial->flush(); snprintf_P(log_data, sizeof(log_data), PSTR( "TYA: Send Serial Packet Dim Value=%d (id=%d)"), duty, Settings.param[P_TUYA_DIMMER_ID]); AddLog(LOG_LEVEL_DEBUG); @@ -99,27 +107,27 @@ void TuyaPacketProcess() { char scmnd[20]; - snprintf_P(log_data, sizeof(log_data), PSTR("TYA: Packet Size=%d"), serial_in_byte_counter); + snprintf_P(log_data, sizeof(log_data), PSTR("TYA: Packet Size=%d"), tuya_byte_counter); AddLog(LOG_LEVEL_DEBUG); - if (serial_in_byte_counter == 7 && serial_in_buffer[3] == 14 ) { // heartbeat packet + if (tuya_byte_counter == 7 && tuya_buffer[3] == 14 ) { // heartbeat packet AddLog_P(LOG_LEVEL_DEBUG, PSTR("TYA: Heartbeat")); } - else if (serial_in_byte_counter == 12 && serial_in_buffer[3] == 7 && serial_in_buffer[5] == 5) { // on/off packet + else if (tuya_byte_counter == 12 && tuya_buffer[3] == 7 && tuya_buffer[5] == 5) { // on/off packet - snprintf_P(log_data, sizeof(log_data),PSTR("TYA: Rcvd - %s State"),serial_in_buffer[10]?"On":"Off"); + snprintf_P(log_data, sizeof(log_data),PSTR("TYA: Rcvd - %s State"),tuya_buffer[10]?"On":"Off"); AddLog(LOG_LEVEL_DEBUG); - if((power || Settings.light_dimmer > 0) && (power != serial_in_buffer[10])) { - ExecuteCommandPower(1, serial_in_buffer[10], SRC_SWITCH); // send SRC_SWITCH? to use as flag to prevent loop from inbound states from faceplate interaction + if((power || Settings.light_dimmer > 0) && (power != tuya_buffer[10])) { + ExecuteCommandPower(1, tuya_buffer[10], SRC_SWITCH); // send SRC_SWITCH? to use as flag to prevent loop from inbound states from faceplate interaction } } - else if (serial_in_byte_counter == 15 && serial_in_buffer[3] == 7 && serial_in_buffer[5] == 8) { // dim packet + else if (tuya_byte_counter == 15 && tuya_buffer[3] == 7 && tuya_buffer[5] == 8) { // dim packet - snprintf_P(log_data, sizeof(log_data), PSTR("TYA: Rcvd Dim State=%d"), serial_in_buffer[13]); + snprintf_P(log_data, sizeof(log_data), PSTR("TYA: Rcvd Dim State=%d"), tuya_buffer[13]); AddLog(LOG_LEVEL_DEBUG); - tuya_new_dim = round(serial_in_buffer[13] * (100. / 255.)); + tuya_new_dim = round(tuya_buffer[13] * (100. / 255.)); if((power) && (tuya_new_dim > 0) && (abs(tuya_new_dim - Settings.light_dimmer) > 2)) { snprintf_P(log_data, sizeof(log_data), PSTR("TYA: Send CMND_DIMMER=%d"), tuya_new_dim ); @@ -134,7 +142,7 @@ void TuyaPacketProcess() ExecuteCommand(scmnd, SRC_SWITCH); } } - else if (serial_in_byte_counter == 8 && serial_in_buffer[3] == 5 && serial_in_buffer[5] == 1 && serial_in_buffer[7] == 5 ) { // reset WiFi settings packet - to do: reset red MCU LED after WiFi is up + else if (tuya_byte_counter == 8 && tuya_buffer[3] == 5 && tuya_buffer[5] == 1 && tuya_buffer[7] == 5 ) { // reset WiFi settings packet - to do: reset red MCU LED after WiFi is up AddLog_P(LOG_LEVEL_DEBUG, PSTR("TYA: WiFi Reset Rcvd")); @@ -145,16 +153,16 @@ void TuyaPacketProcess() void TuyaSerialInput() { - while (Serial.available()) { + while (TuyaSerial->available()) { yield(); - serial_in_byte = Serial.read(); + byte serial_in_byte = TuyaSerial->read(); - //snprintf_P(log_data, sizeof(log_data), PSTR("TYA: serial_in_byte %d, tuya_cmd_status %d, tuya_cmd_checksum %d, tuya_data_len %d, serial_in_byte_counter %d"), serial_in_byte, tuya_cmd_status, tuya_cmd_checksum, tuya_data_len, serial_in_byte_counter); + //snprintf_P(log_data, sizeof(log_data), PSTR("TYA: serial_in_byte %d, tuya_cmd_status %d, tuya_cmd_checksum %d, tuya_data_len %d, tuya_byte_counter %d"), serial_in_byte, tuya_cmd_status, tuya_cmd_checksum, tuya_data_len, tuya_byte_counter); //AddLog(LOG_LEVEL_DEBUG); if (serial_in_byte == 0x55) { // Start TUYA Packet tuya_cmd_status = 1; - serial_in_buffer[serial_in_byte_counter++] = serial_in_byte; + tuya_buffer[tuya_byte_counter++] = serial_in_byte; tuya_cmd_checksum += serial_in_byte; } else if (tuya_cmd_status == 1 && serial_in_byte == 0xAA){ // Only packtes with header 0x55AA are valid @@ -162,40 +170,40 @@ void TuyaSerialInput() AddLog_P(LOG_LEVEL_DEBUG, PSTR("TYA: 0x55AA Packet Start")); - serial_in_byte_counter = 0; - serial_in_buffer[serial_in_byte_counter++] = 0x55; - serial_in_buffer[serial_in_byte_counter++] = 0xAA; + tuya_byte_counter = 0; + tuya_buffer[tuya_byte_counter++] = 0x55; + tuya_buffer[tuya_byte_counter++] = 0xAA; tuya_cmd_checksum = 0xFF; } else if (tuya_cmd_status == 2){ - if(serial_in_byte_counter == 5){ // Get length of data + if(tuya_byte_counter == 5){ // Get length of data tuya_cmd_status = 3; tuya_data_len = serial_in_byte; } tuya_cmd_checksum += serial_in_byte; - serial_in_buffer[serial_in_byte_counter++] = serial_in_byte; + tuya_buffer[tuya_byte_counter++] = serial_in_byte; } - else if ((tuya_cmd_status == 3) && (serial_in_byte_counter == (6 + tuya_data_len)) && (tuya_cmd_checksum == serial_in_byte)){ // Compare checksum and process packet - serial_in_buffer[serial_in_byte_counter++] = serial_in_byte; + else if ((tuya_cmd_status == 3) && (tuya_byte_counter == (6 + tuya_data_len)) && (tuya_cmd_checksum == serial_in_byte)){ // Compare checksum and process packet + tuya_buffer[tuya_byte_counter++] = serial_in_byte; snprintf_P(log_data, sizeof(log_data), PSTR("TYA: 0x55 Packet End: \"")); - for (int i = 0; i < serial_in_byte_counter; i++) { - snprintf_P(log_data, sizeof(log_data), PSTR("%s%02x"), log_data, serial_in_buffer[i]); + for (int i = 0; i < tuya_byte_counter; i++) { + snprintf_P(log_data, sizeof(log_data), PSTR("%s%02x"), log_data, tuya_buffer[i]); } snprintf_P(log_data, sizeof(log_data), PSTR("%s\""), log_data); AddLog(LOG_LEVEL_DEBUG); TuyaPacketProcess(); - serial_in_byte_counter = 0; + tuya_byte_counter = 0; tuya_cmd_status = 0; tuya_cmd_checksum = 0; tuya_data_len = 0; } // read additional packets from TUYA - else if(serial_in_byte_counter < INPUT_BUFFER_SIZE -1) { // add char to string if it still fits - serial_in_buffer[serial_in_byte_counter++] = serial_in_byte; + else if(tuya_byte_counter < TUYA_BUFFER_SIZE -1) { // add char to string if it still fits + tuya_buffer[tuya_byte_counter++] = serial_in_byte; tuya_cmd_checksum += serial_in_byte; } else { - serial_in_byte_counter = 0; + tuya_byte_counter = 0; tuya_cmd_status = 0; tuya_cmd_checksum = 0; tuya_data_len = 0; @@ -205,7 +213,10 @@ void TuyaSerialInput() boolean TuyaModuleSelected() { - baudrate = 9600; + if (!(pin[GPIO_TUYA_RX] < 99) || !(pin[GPIO_TUYA_TX] < 99)) { // fallback to hardware-serial if not explicitly selected + pin[GPIO_TUYA_RX] = 1; + pin[GPIO_TUYA_TX] = 3; + } light_type = LT_SERIAL; return true; } @@ -215,20 +226,23 @@ void TuyaInit() if (!Settings.param[P_TUYA_DIMMER_ID]) { Settings.param[P_TUYA_DIMMER_ID] = TUYA_DIMMER_ID; } - Serial.setDebugOutput(false); - ClaimSerial(); + TuyaSerial = new TasmotaSerial(pin[GPIO_TUYA_RX], pin[GPIO_TUYA_TX], 1); + if (TuyaSerial->begin(9600)) { + if (TuyaSerial->hardwareSerial()) { ClaimSerial(); } - // Get current status of MCU - snprintf_P(log_data, sizeof(log_data), "TYA: Request MCU state"); - AddLog(LOG_LEVEL_DEBUG); - Serial.write(0x55); // header 55AA - Serial.write(0xAA); - Serial.write(0x00); // version 00 - Serial.write(0x08); // command 08 - get status - Serial.write(0x00); - Serial.write(0x00); // following data length 0x00 - Serial.write(0x07); // checksum:sum of all bytes in packet mod 256 - Serial.flush(); + // Get current status of MCU + snprintf_P(log_data, sizeof(log_data), "TYA: Request MCU state"); + AddLog(LOG_LEVEL_DEBUG); + + TuyaSerial->write((uint8_t)0x55); // header 55AA + TuyaSerial->write((uint8_t)0xAA); + TuyaSerial->write((uint8_t)0x00); // version 00 + TuyaSerial->write((uint8_t)0x08); // command 08 - get status + TuyaSerial->write((uint8_t)0x00); + TuyaSerial->write((uint8_t)0x00); // following data length 0x00 + TuyaSerial->write((uint8_t)0x07); // checksum:sum of all bytes in packet mod 256 + TuyaSerial->flush(); + } } boolean TuyaButtonPressed() @@ -266,7 +280,7 @@ boolean Xdrv16(byte function) TuyaInit(); break; case FUNC_LOOP: - TuyaSerialInput(); + if (TuyaSerial) { TuyaSerialInput(); } break; case FUNC_SET_DEVICE_POWER: result = TuyaSetPower(); diff --git a/sonoff/xdrv_17_rcswitch.ino b/sonoff/xdrv_17_rcswitch.ino index 6c65dd3b0..e4cccb273 100644 --- a/sonoff/xdrv_17_rcswitch.ino +++ b/sonoff/xdrv_17_rcswitch.ino @@ -19,7 +19,7 @@ #ifdef USE_RC_SWITCH /*********************************************************************************************\ - * RF send and receive using RCSwitch library + * RF send and receive using RCSwitch library https://github.com/sui77/rc-switch/ \*********************************************************************************************/ #define D_JSON_RF_PROTOCOL "Protocol" @@ -34,39 +34,38 @@ RCSwitch mySwitch = RCSwitch(); -#define RF_TIME_AVOID_DUPLICATE 500 // Milliseconds +#define RF_TIME_AVOID_DUPLICATE 1000 // Milliseconds -unsigned long rf_lasttime = 0; +uint32_t rf_lasttime = 0; void RfReceiveCheck() { if (mySwitch.available()) { - unsigned long value = mySwitch.getReceivedValue(); - unsigned int bit_length = mySwitch.getReceivedBitlength(); - unsigned int delay = mySwitch.getReceivedDelay(); - unsigned int protocol = mySwitch.getReceivedProtocol(); + unsigned long data = mySwitch.getReceivedValue(); + unsigned int bits = mySwitch.getReceivedBitlength(); + int protocol = mySwitch.getReceivedProtocol(); + int delay = mySwitch.getReceivedDelay(); - snprintf_P(log_data, sizeof(log_data), PSTR("RFR: BitLen %d, Delay %d, Protocol %d, Value %lX (%u)"), - bit_length, delay, protocol, value, value); + snprintf_P(log_data, sizeof(log_data), PSTR("RFR: Data %lX (%u), Bits %d, Protocol %d, Delay %d"), data, data, bits, protocol, delay); AddLog(LOG_LEVEL_DEBUG); - unsigned long now = millis(); - if ((now - rf_lasttime > RF_TIME_AVOID_DUPLICATE) && (value > 0)) { + uint32_t now = millis(); + if ((now - rf_lasttime > RF_TIME_AVOID_DUPLICATE) && (data > 0)) { rf_lasttime = now; char stemp[16]; - if (Settings.flag.rf_receive_decimal) { - snprintf_P(stemp, sizeof(stemp), PSTR("%u"), (uint32_t)value); + if (Settings.flag.rf_receive_decimal) { // SetOption28 (0 = hexadecimal, 1 = decimal) + snprintf_P(stemp, sizeof(stemp), PSTR("%u"), (uint32_t)data); } else { - snprintf_P(stemp, sizeof(stemp), PSTR("\"%lX\""), (uint32_t)value); + snprintf_P(stemp, sizeof(stemp), PSTR("\"%lX\""), (uint32_t)data); } - snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_JSON_RFRECEIVED "\":{\"" D_JSON_RF_PROTOCOL "\":%d,\"" D_JSON_RF_BITS "\":%d,\"" D_JSON_RF_DATA "\":%s}}"), - protocol, bit_length, stemp); + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_JSON_RFRECEIVED "\":{\"" D_JSON_RF_DATA "\":%s,\"" D_JSON_RF_BITS "\":%d,\"" D_JSON_RF_PROTOCOL "\":%d}}"), + stemp, bits, protocol); MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_RFRECEIVED)); XdrvRulesProcess(); #ifdef USE_DOMOTICZ - DomoticzSensor(DZ_COUNT, value); // Send value as Domoticz Counter value + DomoticzSensor(DZ_COUNT, data); // Send data as Domoticz Counter value #endif // USE_DOMOTICZ } mySwitch.resetAvailable(); @@ -87,63 +86,73 @@ void RfInit() * Commands \*********************************************************************************************/ -/* - * ArduinoJSON entry used to calculate jsonBuf: JSON_OBJECT_SIZE(3) + 40 = 96 - RFsend: - { "protocol":1, "pulse":320, "repeat":15, "bits":24, "data":551502015 } -*/ - boolean RfSendCommand() { boolean serviced = true; - boolean error = false; - char dataBufUc[XdrvMailbox.data_len]; - uint32_t protocol = 0; - uint32_t pulse = 0; - uint32_t repeat = 0; - uint32_t bits = 0; - uint32_t data = 0; - - UpperCase(dataBufUc, XdrvMailbox.data); if (!strcasecmp_P(XdrvMailbox.topic, PSTR(D_CMND_RFSEND))) { if (XdrvMailbox.data_len) { - StaticJsonBuffer<128> jsonBuf; + unsigned long data = 0; + unsigned int bits = 24; + int protocol = 1; + int repeat = 10; + int pulse = 350; + + char dataBufUc[XdrvMailbox.data_len]; + UpperCase(dataBufUc, XdrvMailbox.data); + StaticJsonBuffer<150> jsonBuf; // ArduinoJSON entry used to calculate jsonBuf: JSON_OBJECT_SIZE(5) + 40 = 134 JsonObject &root = jsonBuf.parseObject(dataBufUc); - if (!root.success()) { - snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_CMND_RFSEND "\":\"" D_JSON_INVALID_JSON "\"}")); // JSON decode failed - } - else { - snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_CMND_RFSEND "\":\"" D_JSON_DONE "\"}")); - + if (root.success()) { + // RFsend {"data":0x501014,"bits":24,"protocol":1,"repeat":10,"pulse":350} char parm_uc[10]; - protocol = root[UpperCase_P(parm_uc, PSTR(D_JSON_RF_PROTOCOL))]; - pulse = root[UpperCase_P(parm_uc, PSTR(D_JSON_RF_PULSE))]; - repeat = root[UpperCase_P(parm_uc, PSTR(D_JSON_RF_REPEAT))]; + data = strtoul(root[UpperCase_P(parm_uc, PSTR(D_JSON_RF_DATA))], NULL, 0); // Allow decimal (5246996) and hexadecimal (0x501014) input bits = root[UpperCase_P(parm_uc, PSTR(D_JSON_RF_BITS))]; - data = strtoul(root[UpperCase_P(parm_uc, PSTR(D_JSON_RF_DATA))], NULL, 0); - - if (!protocol) { protocol = 1; } - mySwitch.setProtocol(protocol); - if (!pulse) { pulse = 350; } // Default pulse length for protocol 1 - mySwitch.setPulseLength(pulse); - if (!repeat) { repeat = 10; } // Default at init - mySwitch.setRepeatTransmit(repeat); - if (!bits) { bits = 24; } // Default 24 bits - if (data) { - mySwitch.send(data, bits); - } - else { - error = true; + protocol = root[UpperCase_P(parm_uc, PSTR(D_JSON_RF_PROTOCOL))]; + repeat = root[UpperCase_P(parm_uc, PSTR(D_JSON_RF_REPEAT))]; + pulse = root[UpperCase_P(parm_uc, PSTR(D_JSON_RF_PULSE))]; + } else { + // RFsend data, bits, protocol, repeat, pulse + char *p; + byte i = 0; + for (char *str = strtok_r(XdrvMailbox.data, ", ", &p); str && i < 5; str = strtok_r(NULL, ", ", &p)) { + switch (i++) { + case 0: + data = strtoul(str, NULL, 0); // Allow decimal (5246996) and hexadecimal (0x501014) input + break; + case 1: + bits = atoi(str); + break; + case 2: + protocol = atoi(str); + break; + case 3: + repeat = atoi(str); + break; + case 4: + pulse = atoi(str); + } } } - } - else { + + if (!protocol) { protocol = 1; } + mySwitch.setProtocol(protocol); + if (!pulse) { pulse = 350; } // Default pulse length for protocol 1 + mySwitch.setPulseLength(pulse); + if (!repeat) { repeat = 10; } // Default at init + mySwitch.setRepeatTransmit(repeat); + if (!bits) { bits = 24; } // Default 24 bits + if (data) { + mySwitch.send(data, bits); + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_CMND_RFSEND "\":\"" D_JSON_DONE "\"}")); + } else { + error = true; + } + } else { error = true; } if (error) { - snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_CMND_RFSEND "\":\"" D_JSON_NO " " D_JSON_RF_PROTOCOL ", " D_JSON_RF_PULSE ", " D_JSON_RF_REPEAT ", " D_JSON_RF_BITS " " D_JSON_OR " " D_JSON_RF_DATA "\"}")); + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_CMND_RFSEND "\":\"" D_JSON_NO " " D_JSON_RF_DATA ", " D_JSON_RF_BITS ", " D_JSON_RF_PROTOCOL ", " D_JSON_RF_REPEAT " " D_JSON_OR " " D_JSON_RF_PULSE "\"}")); } } else serviced = false; // Unknown command 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
EncryptedNoYes
ReadableYesNo
Simply editableYesNo
Simply batch processingYesNo
+

decode-config.py handles Tasmota configurations for release version since 5.10.0 up to now.

+

Content

+ +

Prerequisite

+ +

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-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 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

+ +

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]
+                        [--json-indent <indent>] [--json-compact]
+                        [--json-hide-pw] [--json-unhide-pw] [-h] [-H] [-v]
+                        [-V] [-c <filename>] [--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 <filename>
+                        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 <filename>
+                        file to retrieve/write Tasmota configuration from/to
+                        (default: None)'
+  -d, --device, --host <host>
+                        hostname or IP address to retrieve/send Tasmota
+                        configuration from/to (default: None)
+  -P, --port <port>     TCP/IP port number to use for the host connection
+                        (default: 80)
+  -u, --username <username>
+                        host HTTP access username (default: admin)
+  -p, --password <password>
+                        host HTTP access password (default: None)
+
+Backup/Restore:
+  Backup/Restore configuration file specification
+
+  -i, --restore-file <filename>
+                        file to restore configuration from (default: None).
+                        Replacements: @v=firmware version, @f=device friendly
+                        name, @h=device hostname
+  -o, --backup-file <filename>
+                        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 <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 <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

+
    +
  1. Restore a Tasmota configuration file

    +

    decode-config.py -c my.conf -d sonoff --restore-file Config_Sonoff_6.2.1.dmp

    +
  2. +
  3. 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

    +
  4. +
+

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('