diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 847958093..f9622222f 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -38,7 +38,7 @@ _Make sure your have performed every step and checked the applicable boxes befor - [ ] Self-compiled - [ ] IDE / Compiler used: _____ - [ ] Flashing tools used: _____ -- [ ] Provide the output of command: ``Backlog Template; Module; GPIO``: +- [ ] Provide the output of command: ``Backlog Template; Module; GPIO 255``: ``` Configuration output here: diff --git a/MODULES.md b/MODULES.md new file mode 100644 index 000000000..bb02bfe73 --- /dev/null +++ b/MODULES.md @@ -0,0 +1,80 @@ +## Supported Modules + +The following hardware modules are supported. + +Module | Description +------------------|----------------------- +01 Sonoff Basic | Sonoff Basic Wifi Smart Switch +02 Sonoff RF | Sonoff RF Wifi Smart Switch with RF (434MHz) receiver +03 Sonoff SV | Sonoff SV Safe Voltage Wifi Smart Switch +04 Sonoff TH | Sonoff TH10/TH16 Wifi Smart Switch with Sensor connection +05 Sonoff Dual | Sonoff Dual Wifi Smart Switch +06 Sonoff Pow | Sonoff Pow Wifi Smart Switch with Energy Monitoring +07 Sonoff 4CH | Sonoff 4CH 4-gang Wifi Smart Switch +08 Sonoff S2X | Sonoff S20/S26 Wifi Smart Socket +09 Slampher | Sonoff Slampher Wifi Smart Light Bulb Socket with RF (434MHz) receiver +10 Sonoff Touch | Sonoff Touch Wifi Light Switch +11 Sonoff LED | Sonoff Led Wifi Led Pack (Retired) +12 1 Channel | 1 Channel Inching/Self Locking Wifi Switch 5V/12V +13 4 Channel | 4 Channel Inching/Self Locking Wifi Switch (Retired) +14 Motor C/AC | Motor Clockwise/Antoclockwise Wifi Switch (Retired) +15 ElectroDragon | Electrodragon Wifi IoT Board +16 EXS Relay(s) | Electronic Experience Store 1 or 2-gang Wifi Module +17 WiOn | WiOn Wifi Smart Socket +18 Generic | Any ESP8266/ESP8285 device like WeMos and NodeMCU +19 Sonoff Dev | Sonoff Dev Wifi Development Board +20 H801 | H801 Wifi RGBWW Led Controller +21 Sonoff SC | Sonoff SC Wifi Environmental Monitor +22 Sonoff BN-SZ | Sonoff BN-SZ01 Wifi Ceiling Led (Retired) +23 Sonoff 4CH Pro | Sonoff 4CH Pro 4-gang Wifi Smart Switch +24 Huafan SS | HuaFan Wifi Smart Socket +25 Sonoff Bridge | Sonoff RF (434MHz) transceive to Wifi Bridge +26 Sonoff B1 | Sonoff B1 Wifi RGBWW Led Bulb +27 AiLight | Ai-Thinker RGBW Led Bulb +28 Sonoff T1 1CH | Sonoff T1 1-gang Wifi Light Switch +29 Sonoff T1 2CH | Sonoff T1 2-gang Wifi Light Switch +30 Sonoff T1 3CH | Sonoff T1 3-gang Wifi Light Switch +31 Supla Espablo | 2-gang Wifi Module +32 Witty Cloud | Witty Cloud ESP8266 Wifi Development Board +33 Yunshan Relay | ESP8266 Wifi Network Relay Module +34 MagicHome | MagicHome, Flux-light and some Arilux LC10 RGB(W) Led Controller +35 Luani HVIO | Luani ESP8266 Wifi I/O Module +36 KMC 70011 | KMC Wifi Smart Socket with Energy Monitoring +37 Arilux LC01 | Arilux AL-LC01 RGB Led Controller +38 Arilux LC11 | Arilux AL-LC11 RGBWW Led Controller +39 Sonoff Dual R2 | Sonoff Dual R2 Wifi Smart Switch +40 Arilux LC06 | Arilux AL-LC06 RGB(WW) Led Controller +41 Sonoff S31 | Sonoff S31 Wifi Smart Socket with Energy Monitoring +42 Zengge WF017 | Zengge WF017 Wifi RGB(W) Led Controller +43 Sonoff Pow R2 | Sonoff Pow R2 Wifi Smart Switch with Energy Monitoring +44 Sonoff iFan02 | Sonoff iFan02 Wifi Smart Ceiling Fan with Light +45 BlitzWolf SHP | BlitzWolf BW-SHP2, BW-SHP6, HomeCube SP1, Gosund SP111, Teckin SP22 Wifi Smart Switch with Energy Monitoring +46 Shelly 1 | Shelly 1 Open Source Wifi Relay Module +47 Shelly 2 | Shelly 2 Wifi 2-gang Relay Module with Energy Monitoring +48 Xiaomi Philips | Xiaomi Philips Wifi WW Led Bulb +49 Neo Coolcam | Neo Coolcam Wifi Smart Socket +50 ESP Switch | ESP Switch 4-gang Wifi Switch with Leds +51 OBI Socket | OBI Wifi Smart Socket +52 Teckin | Teckin SP22 Wifi Smart Switch with Energy Monitoring +53 AplicWDP303075 | Aplic WDP 303075 CSL Wifi Smart Switch with Energy Monitoring +54 Tuya Dimmer | MIUO (and other Tuya based) Wifi Dimmer for Incandescent Lights and Led +55 Gosund SP1 v23 | Gosund SP1 v2.3 Wifi Smart Switch with Energy Monitoring +56 ARMTR Dimmer | ARMtronix Wifi dimmer for Incandescent Lights and Led +57 SK03 Outdoor | SK03 Outdoor Wifi Smart Switch with Energy Monitoring +58 PS-16-DZ | PS-16-DZ Wifi dimmer for Incandescent Lights and Led +59 Teckin US | Teckin SP20 and ZooZee SA102 Wifi Smart Switch with Energy Monitoring +60 Manzoku strip | Manzoku Wifi Smart Power Strip with four Relays +61 OBI Socket 2 | OBI 2 Wifi Smart Socket +62 YTF IR Bridge | YTF Infra Red Wifi Bridge +63 Digoo DG-SP202 | Digoo DG-SP202 Dual Wifi Smart Switch with Energy Monitoring +64 KA10 | Smanergy KA10 Wifi Smart Wall Switch with Energy Monitoring +65 Luminea ZX2820 | Luminea ZX2820 Wifi Smart Switch with Energy Monitoring +66 Mi Desk Lamp | Mi Desk Lamp with rotary switch and Wifi +67 SP10 | Tuya SP10 Wifi Smart Switch with Energy Monitoring +68 WAGA CHCZ02MB | WAGA life CHCZ02MB Wifi Smart Switch with Energy Monitoring +69 SYF05 | Sunyesmart SYF05 RGBWW Wifi Led Bulb +70 Sonoff L1 | Sonoff L1 light strip +71 Sonoff iFan03 | Sonoff iFan03 Wifi Smart Ceiling Fan with Light +72 EXS Dimmer | EXS Wifi Dimmer v4 + +Over 600 additional devices are supported using [templates](TEMPLATES.md). diff --git a/README.md b/README.md index c0b26ce70..7b101df63 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ In addition to the [release webpage](https://github.com/arendst/Tasmota/releases ## Development -[![Dev Version](https://img.shields.io/badge/development%20version-v7.1.0.x-blue.svg)](https://github.com/arendst/Tasmota) +[![Dev Version](https://img.shields.io/badge/development%20version-v7.1.1.x-blue.svg)](https://github.com/arendst/Tasmota) [![Download Dev](https://img.shields.io/badge/download-development-yellow.svg)](http://thehackbox.org/tasmota/) [![Build Status](https://img.shields.io/travis/arendst/Tasmota.svg)](https://travis-ci.org/arendst/Tasmota) @@ -76,19 +76,19 @@ For a database of supported devices see [Tasmota Device Templates Repository](ht If you're looking for support on **Tasmota** there are some options available: -### Documentation: +### Documentation * [Documentation Site](https://tasmota.github.io/docs): For information on how to flash Tasmota, configure, use and expand it * [FAQ and Troubleshooting](https://tasmota.github.io/docs/#/help/): For information on common problems and solutions. * [Commands Information](https://tasmota.github.io/docs/#/Commands): For information on all the commands supported by Tasmota. -### Support's Community: +### Support's Community * [Tasmota Forum](https://groups.google.com/d/forum/sonoffusers): For usage and discussions. * [Tasmota Support Chat](https://discord.gg/Ks2Kzd4): For support, troubleshooting and general questions. You have better chances to get fast answers from members of the Tasmota Community. * [Search in Issues](https://github.com/arendst/Tasmota/issues): You might find an answer to your question by searching current or closed issues. -### Developers' Community: +### Developers' Community * [Bug Report](https://github.com/arendst/Tasmota/issues/new?template=Bug_report.md): For reporting Bugs of Tasmota Software. * [Feature Request](https://github.com/arendst/Tasmota/issues/new?template=Feature_request.md): For requesting features/functions to Tasmota Software. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a46f2bd46..51d078352 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -28,87 +28,6 @@ To save resources when TLS is enabled mDNS needs to be disabled. In addition to For initial configuration this release supports Webserver based **WifiManager** or **Serial** based command interface only. Support for **WPS** and **SmartConfig** has been removed. -## Supported Modules - -The following hardware modules are supported. - -Module | Description -------------------|----------------------- -01 Sonoff Basic | Sonoff Basic Wifi Smart Switch -02 Sonoff RF | Sonoff RF Wifi Smart Switch with RF (434MHz) receiver -03 Sonoff SV | Sonoff SV Safe Voltage Wifi Smart Switch -04 Sonoff TH | Sonoff TH10/TH16 Wifi Smart Switch with Sensor connection -05 Sonoff Dual | Sonoff Dual Wifi Smart Switch -06 Sonoff Pow | Sonoff Pow Wifi Smart Switch with Energy Monitoring -07 Sonoff 4CH | Sonoff 4CH 4-gang Wifi Smart Switch -08 Sonoff S2X | Sonoff S20/S26 Wifi Smart Socket -09 Slampher | Sonoff Slampher Wifi Smart Light Bulb Socket with RF (434MHz) receiver -10 Sonoff Touch | Sonoff Touch Wifi Light Switch -11 Sonoff LED | Sonoff Led Wifi Led Pack (Retired) -12 1 Channel | 1 Channel Inching/Self Locking Wifi Switch 5V/12V -13 4 Channel | 4 Channel Inching/Self Locking Wifi Switch (Retired) -14 Motor C/AC | Motor Clockwise/Antoclockwise Wifi Switch (Retired) -15 ElectroDragon | Electrodragon Wifi IoT Board -16 EXS Relay(s) | Electronic Experience Store 1 or 2-gang Wifi Module -17 WiOn | WiOn Wifi Smart Socket -18 Generic | Any ESP8266/ESP8285 device like WeMos and NodeMCU -19 Sonoff Dev | Sonoff Dev Wifi Development Board -20 H801 | H801 Wifi RGBWW Led Controller -21 Sonoff SC | Sonoff SC Wifi Environmental Monitor -22 Sonoff BN-SZ | Sonoff BN-SZ01 Wifi Ceiling Led (Retired) -23 Sonoff 4CH Pro | Sonoff 4CH Pro 4-gang Wifi Smart Switch -24 Huafan SS | HuaFan Wifi Smart Socket -25 Sonoff Bridge | Sonoff RF (434MHz) transceive to Wifi Bridge -26 Sonoff B1 | Sonoff B1 Wifi RGBWW Led Bulb -27 AiLight | Ai-Thinker RGBW Led Bulb -28 Sonoff T1 1CH | Sonoff T1 1-gang Wifi Light Switch -29 Sonoff T1 2CH | Sonoff T1 2-gang Wifi Light Switch -30 Sonoff T1 3CH | Sonoff T1 3-gang Wifi Light Switch -31 Supla Espablo | 2-gang Wifi Module -32 Witty Cloud | Witty Cloud ESP8266 Wifi Development Board -33 Yunshan Relay | ESP8266 Wifi Network Relay Module -34 MagicHome | MagicHome, Flux-light and some Arilux LC10 RGB(W) Led Controller -35 Luani HVIO | Luani ESP8266 Wifi I/O Module -36 KMC 70011 | KMC Wifi Smart Socket with Energy Monitoring -37 Arilux LC01 | Arilux AL-LC01 RGB Led Controller -38 Arilux LC11 | Arilux AL-LC11 RGBWW Led Controller -39 Sonoff Dual R2 | Sonoff Dual R2 Wifi Smart Switch -40 Arilux LC06 | Arilux AL-LC06 RGB(WW) Led Controller -41 Sonoff S31 | Sonoff S31 Wifi Smart Socket with Energy Monitoring -42 Zengge WF017 | Zengge WF017 Wifi RGB(W) Led Controller -43 Sonoff Pow R2 | Sonoff Pow R2 Wifi Smart Switch with Energy Monitoring -44 Sonoff iFan02 | Sonoff iFan02 Wifi Smart Ceiling Fan with Light -45 BlitzWolf SHP | BlitzWolf BW-SHP2, BW-SHP6, HomeCube SP1, Gosund SP111, Teckin SP22 Wifi Smart Switch with Energy Monitoring -46 Shelly 1 | Shelly 1 Open Source Wifi Relay Module -47 Shelly 2 | Shelly 2 Wifi 2-gang Relay Module with Energy Monitoring -48 Xiaomi Philips | Xiaomi Philips Wifi WW Led Bulb -49 Neo Coolcam | Neo Coolcam Wifi Smart Socket -50 ESP Switch | ESP Switch 4-gang Wifi Switch with Leds -51 OBI Socket | OBI Wifi Smart Socket -52 Teckin | Teckin SP22 Wifi Smart Switch with Energy Monitoring -53 AplicWDP303075 | Aplic WDP 303075 CSL Wifi Smart Switch with Energy Monitoring -54 Tuya Dimmer | MIUO (and other Tuya based) Wifi Dimmer for Incandescent Lights and Led -55 Gosund SP1 v23 | Gosund SP1 v2.3 Wifi Smart Switch with Energy Monitoring -56 ARMTR Dimmer | ARMtronix Wifi dimmer for Incandescent Lights and Led -57 SK03 Outdoor | SK03 Outdoor Wifi Smart Switch with Energy Monitoring -58 PS-16-DZ | PS-16-DZ Wifi dimmer for Incandescent Lights and Led -59 Teckin US | Teckin SP20 and ZooZee SA102 Wifi Smart Switch with Energy Monitoring -60 Manzoku strip | Manzoku Wifi Smart Power Strip with four Relays -61 OBI Socket 2 | OBI 2 Wifi Smart Socket -62 YTF IR Bridge | YTF Infra Red Wifi Bridge -63 Digoo DG-SP202 | Digoo DG-SP202 Dual Wifi Smart Switch with Energy Monitoring -64 KA10 | Smanergy KA10 Wifi Smart Wall Switch with Energy Monitoring -65 Luminea ZX2820 | Luminea ZX2820 Wifi Smart Switch with Energy Monitoring -66 Mi Desk Lamp | Mi Desk Lamp with rotary switch and Wifi -67 SP10 | Tuya SP10 Wifi Smart Switch with Energy Monitoring -68 WAGA CHCZ02MB | WAGA life CHCZ02MB Wifi Smart Switch with Energy Monitoring -69 SYF05 | Sunyesmart SYF05 RGBWW Wifi Led Bulb -70 Sonoff L1 | Sonoff L1 light strip -71 Sonoff iFan03 | Sonoff iFan03 Wifi Smart Ceiling Fan with Light -72 EXS Dimmer | EXS Wifi Dimmer v4 - -Over 500 additional devices are supported using [templates](TEMPLATES.md). - ## Provided Binary Downloads The following binary downloads have been compiled with ESP8266/Arduino library core version **2.6.1**. @@ -122,46 +41,14 @@ The following binary downloads have been compiled with ESP8266/Arduino library c - **tasmota-display.bin** = The Display version without Energy Monitoring but adds display support. - **tasmota-minimal.bin** = The Minimal version allows intermediate OTA uploads to support larger versions and does NOT change any persistent parameter. This version **should NOT be used for initial installation**. +[List](MODULES.md) of embedded modules. + [Complete list](BUILDS.md) of available feature and sensors. ## Changelog -### Version 7.1.0 Betty +### Version 7.1.2 Betty -- Remove update support for versions before 6.0 -- Remove driver xsns_12_ads1115_i2cdev replaced by xsns_12_ads1115 -- Remove most IR protocols from non dedicated IR firmware except NEC, RC5 and RC6 -- Change repository name from Sonoff-Tasmota to Tasmota and all code references from Sonoff to Tasmota -- Change documentation from wiki to [documentation repository](https://tasmota.github.io/docs/) by @Blakadder -- Change default GUI to dark theme -- Change ArduinoSlave to TasmotaSlave -- Change IRremoteESP8266 library to v2.7.1 -- Change supported PCF8574 I2C address range to 0x20 - 0x26 allowing other I2C devices with address 0x27 to be used at the same time -- Change supported PCF8574A I2C address range to 0x39 - 0x3F allowing other I2C devices with address 0x38 to be used at the same time -- Change supported MCP230xx I2C address range to 0x20 - 0x26 allowing other I2C devices with address 0x27 to be used at the same time -- Change Reset erase end address from as seen by SDK (getFlashChipSize) to full flash size (getFlashChipRealSize) -- Change new Fade system much smoother, Speed now up to 40 (#6942, #3714) -- Fix better control of RGB/White when ``SetOption37`` >128, added ``Dimmer1`` and ``Dimmer2`` commands (#6714) -- Fix random crash caused by UPNP flood -- Fix check deepsleep for valid values in Settings (#6961) -- Fix Wifi instability when light is on, due to ``Sleep 0`` (#6961, #6608) -- Fix auto-power on/off when setting channel to non-zero or zero value, when ``SetOption68 1`` -- Fix postpone saving settings to flash until Fade is complete, avoids pause in Fade -- Add support for Tuya battery powered devices (#6735) -- Add support for Honeywell I2C HIH series Humidity and Temperetaure sensor (#6808) -- Add support for Honeywell HPMA115S0 particle concentration sensor by David Hunt (#6843) -- Add support for I2C sensor TLS2591 Light Intensity sensor (#6873) -- Add command ``SetOption73 0/1`` to re-enable HTTP Cross-Origin Resource Sharing (CORS) now default disabled (#6767) -- Add command ``SetOption74 0/1`` to enable DS18x20 internal pull-up and remove define DS18B20_INTERNAL_PULLUP (#6795) -- Add command ``SetOption75 0/1`` to switch between grouptopic (0) using fulltopic replacing %topic% or (1) is cmnd/\ (#6779) -- Add command ``SetOption76 0/1`` to enable incrementing bootcount when deepsleep is enabled (#6930) -- Add command ``SetOption77 0/1`` to keep power on when slider is far left -- Add command ``I2cDriver`` for I2C driver runtime control using document I2CDEVICES.md -- Add command ``TempOffset -12.6 .. 12.6`` to set global temperature sensor offset (#6958) -- Add command ``WebColor19`` to control color of Module and Name (#6811) -- Add command ``WifiPower 0 .. 20.5`` to set Wifi Output Power which will be default set to 17dBm -- Add frequency to ADE7953 energy monitor as used in Shelly 2.5 by ljakob (#6778) -- Add hide Alexa objects with friendlyname starting with '$' (#6722, #6762) -- Add Zigbee command support, considered as v1.0 for full Zigbee support -- Add hardware detection to be overruled with ``SetOption51`` (#6969) -- Add Colorpicker to WebUI by Christian Staars (#6984) +- Fix lost functionality of GPIO9 and GPIO10 on some devices (#7080) +- Fix Zigbee uses Hardware Serial if GPIO 1/3 or GPIO 13/15 and SerialLog 0 (#7071) +- Change light color schemes 2, 3 and 4 from color wheel to Hue driven diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini index 9827447dd..ad669b0a7 100644 --- a/platformio_override_sample.ini +++ b/platformio_override_sample.ini @@ -85,11 +85,11 @@ build_flags = ${esp82xx_defaults.build_flags} ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK221 ; NONOSDK22x_190313 ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190313 -; NONOSDK22x_190703 = 2.2.2-dev(38a443e) (Tasmota default) +; NONOSDK22x_190703 = 2.2.1+100-dev(38a443e) (Tasmota default) (Firmware 2K smaller than NONOSDK22x_191105) -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 -; NONOSDK22x_191024 = 2.2.2-dev(5ab15d1) +; NONOSDK22x_191024 = 2.2.1+111-dev(5ab15d1) ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_191024 -; NONOSDK22x_191105 = 2.2.2-dev(bb83b9b) +; NONOSDK22x_191105 = 2.2.1+113-dev(bb83b9b) ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_191105 ; NONOSDK3V0 (known issues) ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK3 @@ -129,11 +129,11 @@ build_flags = ${esp82xx_defaults.build_flags} ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK221 ; NONOSDK22x_190313 ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190313 -; NONOSDK22x_190703 = 2.2.2-dev(38a443e) (Tasmota default) +; NONOSDK22x_190703 = 2.2.1+100-dev(38a443e) (Tasmota default) (Firmware 2K smaller than NONOSDK22x_191105) -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 -; NONOSDK22x_191024 = 2.2.2-dev(5ab15d1) +; NONOSDK22x_191024 = 2.2.1+111-dev(5ab15d1) ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_191024 -; NONOSDK22x_191105 = 2.2.2-dev(bb83b9b) +; NONOSDK22x_191105 = 2.2.1+113-dev(bb83b9b) ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_191105 ; NONOSDK3V0 (known issues) ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK3 @@ -173,12 +173,14 @@ build_flags = ${esp82xx_defaults.build_flags} ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK221 ; NONOSDK22x_190313 ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190313 -; NONOSDK22x_190703 (Tasmota default) +; NONOSDK22x_190703 = 2.2.1+100-dev(38a443e) (Tasmota default) (Firmware 2K smaller than NONOSDK22x_191105) -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 -; NONOSDK22x_191024 +; NONOSDK22x_191024 = 2.2.1+111-dev(5ab15d1) ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_191024 -; NONOSDK22x_191105 +; NONOSDK22x_191105 = 2.2.1+113-dev(bb83b9b) ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_191105 +; NONOSDK22x_191122 = 2.2.1+119-dev(a58da79) +; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_191122 ; NONOSDK3V0 (known issues) ; -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK3 ; lwIP 1.4 diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index e9949c4f5..1706dbbb3 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -1,5 +1,18 @@ ## Unreleased (development) +### 7.1.1.1 20191201 + +- Fix lost functionality of GPIO9 and GPIO10 on some devices (#7080) +- Fix Zigbee uses Hardware Serial if GPIO 1/3 or GPIO 13/15 and SerialLog 0 (#7071) +- Fix WS2812 power control (#7090) +- Change light color schemes 2, 3 and 4 from color wheel to Hue driven + +## Released + +### 7.1.1 20191201 + +- Maintenance Release + ### 7.1.0.1 20191130 - Fix slider for devices with one or two channels like only white or white/yellow @@ -7,8 +20,6 @@ - Fix light scheme 4 speed (#7072) - Add support for TasmotaSlave executing commands on Tasmota -## Released - ### 7.1.0 20191129 - Release diff --git a/tasmota/support.ino b/tasmota/support.ino index 8f1ba2862..2c58631d1 100644 --- a/tasmota/support.ino +++ b/tasmota/support.ino @@ -521,7 +521,7 @@ char* GetPowerDevice(char* dest, uint32_t idx, size_t size) return GetPowerDevice(dest, idx, size, 0); } -bool IsEsp8285(void) +void GetEspHardwareType(void) { // esptool.py get_efuses uint32_t efuse1 = *(uint32_t*)(0x3FF00050); @@ -529,17 +529,16 @@ bool IsEsp8285(void) // uint32_t efuse3 = *(uint32_t*)(0x3FF00058); // uint32_t efuse4 = *(uint32_t*)(0x3FF0005C); - bool is_8285 = ( (efuse1 & (1 << 4)) || (efuse2 & (1 << 16)) ); + is_8285 = ( (efuse1 & (1 << 4)) || (efuse2 & (1 << 16)) ); if (is_8285 && (ESP.getFlashChipRealSize() > 1048576)) { is_8285 = false; // ESP8285 can only have 1M flash } - return is_8285; } String GetDeviceHardware(void) { char buff[10]; - if (IsEsp8285()) { + if (is_8285) { strcpy_P(buff, PSTR("ESP8285")); } else { strcpy_P(buff, PSTR("ESP8266EX")); @@ -1063,18 +1062,18 @@ bool FlashPin(uint32_t pin) uint8_t ValidPin(uint32_t pin, uint32_t gpio) { - uint8_t result = gpio; - if (FlashPin(pin)) { - result = GPIO_NONE; // Disable flash pins GPIO6, GPIO7, GPIO8 and GPIO11 + return GPIO_NONE; // Disable flash pins GPIO6, GPIO7, GPIO8 and GPIO11 } - if (!IsEsp8285() && !Settings.flag3.user_esp8285_enable) { // SetOption51 - Enable ESP8285 user GPIO's + +// if (!is_8285 && !Settings.flag3.user_esp8285_enable) { // SetOption51 - Enable ESP8285 user GPIO's + if ((WEMOS == Settings.module) && !Settings.flag3.user_esp8285_enable) { // SetOption51 - Enable ESP8285 user GPIO's if ((pin == 9) || (pin == 10)) { - result = GPIO_NONE; // Disable possible flash GPIO9 and GPIO10 + return GPIO_NONE; // Disable possible flash GPIO9 and GPIO10 } } - return result; + return gpio; } bool ValidGPIO(uint32_t pin, uint32_t gpio) diff --git a/tasmota/support_tasmota.ino b/tasmota/support_tasmota.ino new file mode 100644 index 000000000..2f54cec7d --- /dev/null +++ b/tasmota/support_tasmota.ino @@ -0,0 +1,1339 @@ +/* + support_tasmota.ino - Core support for Tasmota + + Copyright (C) 2019 Theo Arends + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +const char kSleepMode[] PROGMEM = "Dynamic|Normal"; +const char kPrefixes[] PROGMEM = D_CMND "|" D_STAT "|" D_TELE; + +char* Format(char* output, const char* input, int size) +{ + char *token; + uint32_t digits = 0; + + if (strstr(input, "%") != nullptr) { + strlcpy(output, input, size); + token = strtok(output, "%"); + if (strstr(input, "%") == input) { + output[0] = '\0'; + } else { + token = strtok(nullptr, ""); + } + if (token != nullptr) { + digits = atoi(token); + if (digits) { + char tmp[size]; + if (strchr(token, 'd')) { + snprintf_P(tmp, size, PSTR("%s%c0%dd"), output, '%', digits); + snprintf_P(output, size, tmp, ESP.getChipId() & 0x1fff); // %04d - short chip ID in dec, like in hostname + } else { + snprintf_P(tmp, size, PSTR("%s%c0%dX"), output, '%', digits); + snprintf_P(output, size, tmp, ESP.getChipId()); // %06X - full chip ID in hex + } + } else { + if (strchr(token, 'd')) { + snprintf_P(output, size, PSTR("%s%d"), output, ESP.getChipId()); // %d - full chip ID in dec + digits = 8; + } + } + } + } + if (!digits) { + strlcpy(output, input, size); + } + return output; +} + +char* GetOtaUrl(char *otaurl, size_t otaurl_size) +{ + if (strstr(Settings.ota_url, "%04d") != nullptr) { // OTA url contains placeholder for chip ID + snprintf(otaurl, otaurl_size, Settings.ota_url, ESP.getChipId() & 0x1fff); + } + else if (strstr(Settings.ota_url, "%d") != nullptr) { // OTA url contains placeholder for chip ID + snprintf_P(otaurl, otaurl_size, Settings.ota_url, ESP.getChipId()); + } + else { + strlcpy(otaurl, Settings.ota_url, otaurl_size); + } + return otaurl; +} + +char* GetTopic_P(char *stopic, uint32_t prefix, char *topic, const char* subtopic) +{ + /* prefix 0 = Cmnd + prefix 1 = Stat + prefix 2 = Tele + prefix 4 = Cmnd fallback + prefix 5 = Stat fallback + prefix 6 = Tele fallback + prefix 8 = Cmnd topic + prefix 9 = Stat topic + prefix 10 = Tele topic + */ + char romram[CMDSZ]; + String fulltopic; + + snprintf_P(romram, sizeof(romram), subtopic); + if (fallback_topic_flag || (prefix > 3)) { + bool fallback = (prefix < 8); + prefix &= 3; + char stemp[11]; + fulltopic = GetTextIndexed(stemp, sizeof(stemp), prefix, kPrefixes); + fulltopic += F("/"); + if (fallback) { + fulltopic += mqtt_client; + fulltopic += F("_fb"); // cmnd/_fb + } else { + fulltopic += topic; // cmnd/ + } + } else { + fulltopic = Settings.mqtt_fulltopic; + if ((0 == prefix) && (-1 == fulltopic.indexOf(FPSTR(MQTT_TOKEN_PREFIX)))) { + fulltopic += F("/"); + fulltopic += FPSTR(MQTT_TOKEN_PREFIX); // Need prefix for commands to handle mqtt topic loops + } + for (uint32_t i = 0; i < 3; i++) { + if ('\0' == Settings.mqtt_prefix[i][0]) { + GetTextIndexed(Settings.mqtt_prefix[i], sizeof(Settings.mqtt_prefix[i]), i, kPrefixes); + } + } + fulltopic.replace(FPSTR(MQTT_TOKEN_PREFIX), Settings.mqtt_prefix[prefix]); + fulltopic.replace(FPSTR(MQTT_TOKEN_TOPIC), topic); + fulltopic.replace(F("%hostname%"), my_hostname); + String token_id = WiFi.macAddress(); + token_id.replace(":", ""); + fulltopic.replace(F("%id%"), token_id); + } + fulltopic.replace(F("#"), ""); + fulltopic.replace(F("//"), "/"); + if (!fulltopic.endsWith("/")) { + fulltopic += "/"; + } + snprintf_P(stopic, TOPSZ, PSTR("%s%s"), fulltopic.c_str(), romram); + return stopic; +} + +char* GetGroupTopic_P(char *stopic, const char* subtopic) +{ + // SetOption75 0: %prefix%/nothing/%topic% = cmnd/nothing//# + // SetOption75 1: cmnd/ + return GetTopic_P(stopic, (Settings.flag3.grouptopic_mode) ? CMND +8 : CMND, Settings.mqtt_grptopic, subtopic); // SetOption75 - GroupTopic replaces %topic% (0) or fixed topic cmnd/grouptopic (1) +} + +char* GetFallbackTopic_P(char *stopic, const char* subtopic) +{ + return GetTopic_P(stopic, CMND +4, nullptr, subtopic); +} + +char* GetStateText(uint32_t state) +{ + if (state > 3) { + state = 1; + } + return Settings.state_text[state]; +} + +/********************************************************************************************/ + +void SetLatchingRelay(power_t lpower, uint32_t state) +{ + // power xx00 - toggle REL1 (Off) and REL3 (Off) - device 1 Off, device 2 Off + // power xx01 - toggle REL2 (On) and REL3 (Off) - device 1 On, device 2 Off + // power xx10 - toggle REL1 (Off) and REL4 (On) - device 1 Off, device 2 On + // power xx11 - toggle REL2 (On) and REL4 (On) - device 1 On, device 2 On + + if (state && !latching_relay_pulse) { // Set latching relay to power if previous pulse has finished + latching_power = lpower; + latching_relay_pulse = 2; // max 200mS (initiated by stateloop()) + } + + for (uint32_t i = 0; i < devices_present; i++) { + uint32_t port = (i << 1) + ((latching_power >> i) &1); + if (pin[GPIO_REL1 +port] < 99) { + digitalWrite(pin[GPIO_REL1 +port], bitRead(rel_inverted, port) ? !state : state); + } + } +} + +void SetDevicePower(power_t rpower, uint32_t source) +{ + ShowSource(source); + last_source = source; + + if (POWER_ALL_ALWAYS_ON == Settings.poweronstate) { // All on and stay on + power = (1 << devices_present) -1; + rpower = power; + } + + if (Settings.flag.interlock) { // Allow only one or no relay set - CMND_INTERLOCK - Enable/disable interlock + for (uint32_t i = 0; i < MAX_INTERLOCKS; i++) { + power_t mask = 1; + uint32_t count = 0; + for (uint32_t j = 0; j < devices_present; j++) { + if ((Settings.interlock[i] & mask) && (rpower & mask)) { + count++; + } + mask <<= 1; + } + if (count > 1) { + mask = ~Settings.interlock[i]; // Turn interlocked group off as there would be multiple relays on + power &= mask; + rpower &= mask; + } + } + } + + if (rpower) { // Any power set + last_power = rpower; + } + + XdrvMailbox.index = rpower; + XdrvCall(FUNC_SET_POWER); // Signal power state + + XdrvMailbox.index = rpower; + XdrvMailbox.payload = source; + if (XdrvCall(FUNC_SET_DEVICE_POWER)) { // Set power state and stop if serviced + // Serviced + } + else if ((SONOFF_DUAL == my_module_type) || (CH4 == my_module_type)) { + Serial.write(0xA0); + Serial.write(0x04); + Serial.write(rpower &0xFF); + Serial.write(0xA1); + Serial.write('\n'); + Serial.flush(); + } + else if (EXS_RELAY == my_module_type) { + SetLatchingRelay(rpower, 1); + } + else { + for (uint32_t i = 0; i < devices_present; i++) { + power_t state = rpower &1; + if ((i < MAX_RELAYS) && (pin[GPIO_REL1 +i] < 99)) { + digitalWrite(pin[GPIO_REL1 +i], bitRead(rel_inverted, i) ? !state : state); + } + rpower >>= 1; + } + } +} + +void RestorePower(bool publish_power, uint32_t source) +{ + if (power != last_power) { + SetDevicePower(last_power, source); + if (publish_power) { + MqttPublishAllPowerState(); + } + } +} + +void SetAllPower(uint32_t state, uint32_t source) +{ +// state 0 = POWER_OFF = Relay Off +// state 1 = POWER_ON = Relay On (turn off after Settings.pulse_timer * 100 mSec if enabled) +// state 2 = POWER_TOGGLE = Toggle relay +// state 8 = POWER_OFF_NO_STATE = Relay Off and no publishPowerState +// state 9 = POWER_ON_NO_STATE = Relay On and no publishPowerState +// state 10 = POWER_TOGGLE_NO_STATE = Toggle relay and no publishPowerState +// state 16 = POWER_SHOW_STATE = Show power state + + bool publish_power = true; + if ((state >= POWER_OFF_NO_STATE) && (state <= POWER_TOGGLE_NO_STATE)) { + state &= 3; // POWER_OFF, POWER_ON or POWER_TOGGLE + publish_power = false; + } + if ((state >= POWER_OFF) && (state <= POWER_TOGGLE)) { + power_t all_on = (1 << devices_present) -1; + switch (state) { + case POWER_OFF: + power = 0; + break; + case POWER_ON: + power = all_on; + break; + case POWER_TOGGLE: + power ^= all_on; // Complement current state + } + SetDevicePower(power, source); + } + if (publish_power) { + MqttPublishAllPowerState(); + } +} + +void SetLedPowerIdx(uint32_t led, uint32_t state) +{ + if ((99 == pin[GPIO_LEDLNK]) && (0 == led)) { // Legacy - LED1 is link led only if LED2 is present + if (pin[GPIO_LED2] < 99) { + led = 1; + } + } + if (pin[GPIO_LED1 + led] < 99) { + uint32_t mask = 1 << led; + if (state) { + state = 1; + led_power |= mask; + } else { + led_power &= (0xFF ^ mask); + } + digitalWrite(pin[GPIO_LED1 + led], bitRead(led_inverted, led) ? !state : state); + } +} + +void SetLedPower(uint32_t state) +{ + if (99 == pin[GPIO_LEDLNK]) { // Legacy - Only use LED1 and/or LED2 + SetLedPowerIdx(0, state); + } else { + power_t mask = 1; + for (uint32_t i = 0; i < leds_present; i++) { // Map leds to power + bool tstate = (power & mask); + SetLedPowerIdx(i, tstate); + mask <<= 1; + } + } +} + +void SetLedPowerAll(uint32_t state) +{ + for (uint32_t i = 0; i < leds_present; i++) { + SetLedPowerIdx(i, state); + } +} + +void SetLedLink(uint32_t state) +{ + uint32_t led_pin = pin[GPIO_LEDLNK]; + uint32_t led_inv = ledlnk_inverted; + if (99 == led_pin) { // Legacy - LED1 is status + led_pin = pin[GPIO_LED1]; + led_inv = bitRead(led_inverted, 0); + } + if (led_pin < 99) { + if (state) { state = 1; } + digitalWrite(led_pin, (led_inv) ? !state : state); + } +} + +void SetPulseTimer(uint32_t index, uint32_t time) +{ + pulse_timer[index] = (time > 111) ? millis() + (1000 * (time - 100)) : (time > 0) ? millis() + (100 * time) : 0L; +} + +uint32_t GetPulseTimer(uint32_t index) +{ + long time = TimePassedSince(pulse_timer[index]); + if (time < 0) { + time *= -1; + return (time > 11100) ? (time / 1000) + 100 : (time > 0) ? time / 100 : 0; + } + return 0; +} + +/********************************************************************************************/ + +bool SendKey(uint32_t key, uint32_t device, uint32_t state) +{ +// key 0 = KEY_BUTTON = button_topic +// key 1 = KEY_SWITCH = switch_topic +// state 0 = POWER_OFF = off +// state 1 = POWER_ON = on +// state 2 = POWER_TOGGLE = toggle +// state 3 = POWER_HOLD = hold +// state 9 = CLEAR_RETAIN = clear retain flag + + char stopic[TOPSZ]; + char scommand[CMDSZ]; + char key_topic[sizeof(Settings.button_topic)]; + bool result = false; + + char *tmp = (key) ? Settings.switch_topic : Settings.button_topic; + Format(key_topic, tmp, sizeof(key_topic)); + if (Settings.flag.mqtt_enabled && MqttIsConnected() && (strlen(key_topic) != 0) && strcmp(key_topic, "0")) { // SetOption3 - Enable MQTT + if (!key && (device > devices_present)) { + device = 1; // Only allow number of buttons up to number of devices + } + GetTopic_P(stopic, CMND, key_topic, + GetPowerDevice(scommand, device, sizeof(scommand), (key + Settings.flag.device_index_enable))); // cmnd/switchtopic/POWERx - SetOption26 - Switch between POWER or POWER1 + if (CLEAR_RETAIN == state) { + mqtt_data[0] = '\0'; + } else { + if ((Settings.flag3.button_switch_force_local || // SetOption61 - Force local operation when button/switch topic is set + !strcmp(mqtt_topic, key_topic) || + !strcmp(Settings.mqtt_grptopic, key_topic)) && + (POWER_TOGGLE == state)) { + state = ~(power >> (device -1)) &1; // POWER_OFF or POWER_ON + } + snprintf_P(mqtt_data, sizeof(mqtt_data), GetStateText(state)); + } +#ifdef USE_DOMOTICZ + if (!(DomoticzSendKey(key, device, state, strlen(mqtt_data)))) { +#endif // USE_DOMOTICZ + MqttPublishDirect(stopic, ((key) ? Settings.flag.mqtt_switch_retain // CMND_SWITCHRETAIN + : Settings.flag.mqtt_button_retain) && // CMND_BUTTONRETAIN + (state != POWER_HOLD || !Settings.flag3.no_hold_retain)); // SetOption62 - Don't use retain flag on HOLD messages +#ifdef USE_DOMOTICZ + } +#endif // USE_DOMOTICZ + result = !Settings.flag3.button_switch_force_local; // SetOption61 - Force local operation when button/switch topic is set + } else { + Response_P(PSTR("{\"%s%d\":{\"State\":%d}}"), (key) ? "Switch" : "Button", device, state); + result = XdrvRulesProcess(); + } + int32_t payload_save = XdrvMailbox.payload; + XdrvMailbox.payload = key << 16 | state << 8 | device; + XdrvCall(FUNC_ANY_KEY); + XdrvMailbox.payload = payload_save; + return result; +} + +void ExecuteCommandPower(uint32_t device, uint32_t state, uint32_t source) +{ +// device = Relay number 1 and up +// state 0 = POWER_OFF = Relay Off +// state 1 = POWER_ON = Relay On (turn off after Settings.pulse_timer * 100 mSec if enabled) +// state 2 = POWER_TOGGLE = Toggle relay +// state 3 = POWER_BLINK = Blink relay +// state 4 = POWER_BLINK_STOP = Stop blinking relay +// state 8 = POWER_OFF_NO_STATE = Relay Off and no publishPowerState +// state 9 = POWER_ON_NO_STATE = Relay On and no publishPowerState +// state 10 = POWER_TOGGLE_NO_STATE = Toggle relay and no publishPowerState +// state 16 = POWER_SHOW_STATE = Show power state + +// ShowSource(source); + +#ifdef USE_SONOFF_IFAN + if (IsModuleIfan()) { + blink_mask &= 1; // No blinking on the fan relays + Settings.flag.interlock = 0; // No interlock mode as it is already done by the microcontroller - CMND_INTERLOCK - Enable/disable interlock + Settings.pulse_timer[1] = 0; // No pulsetimers on the fan relays + Settings.pulse_timer[2] = 0; + Settings.pulse_timer[3] = 0; + } +#endif // USE_SONOFF_IFAN + + bool publish_power = true; + if ((state >= POWER_OFF_NO_STATE) && (state <= POWER_TOGGLE_NO_STATE)) { + state &= 3; // POWER_OFF, POWER_ON or POWER_TOGGLE + publish_power = false; + } + + if ((device < 1) || (device > devices_present)) { + device = 1; + } + active_device = device; + + if (device <= MAX_PULSETIMERS) { + SetPulseTimer(device -1, 0); + } + power_t mask = 1 << (device -1); // Device to control + if (state <= POWER_TOGGLE) { + if ((blink_mask & mask)) { + blink_mask &= (POWER_MASK ^ mask); // Clear device mask + MqttPublishPowerBlinkState(device); + } + + if (Settings.flag.interlock && // CMND_INTERLOCK - Enable/disable interlock + !interlock_mutex && + ((POWER_ON == state) || ((POWER_TOGGLE == state) && !(power & mask))) + ) { + interlock_mutex = true; // Clear all but masked relay in interlock group if new set requested + for (uint32_t i = 0; i < MAX_INTERLOCKS; i++) { + if (Settings.interlock[i] & mask) { // Find interlock group + for (uint32_t j = 0; j < devices_present; j++) { + power_t imask = 1 << j; + if ((Settings.interlock[i] & imask) && (power & imask) && (mask != imask)) { + ExecuteCommandPower(j +1, POWER_OFF, SRC_IGNORE); + delay(50); // Add some delay to make sure never have more than one relay on + } + } + break; // An interlocked relay is only present in one group so quit + } + } + interlock_mutex = false; + } + + switch (state) { + case POWER_OFF: { + power &= (POWER_MASK ^ mask); + break; } + case POWER_ON: + power |= mask; + break; + case POWER_TOGGLE: + power ^= mask; + } + SetDevicePower(power, source); +#ifdef USE_DOMOTICZ + DomoticzUpdatePowerState(device); +#endif // USE_DOMOTICZ +#ifdef USE_KNX + KnxUpdatePowerState(device, power); +#endif // USE_KNX + if (publish_power && Settings.flag3.hass_tele_on_power) { // SetOption59 - Send tele/%topic%/STATE in addition to stat/%topic%/RESULT + MqttPublishTeleState(); + } + if (device <= MAX_PULSETIMERS) { // Restart PulseTime if powered On + SetPulseTimer(device -1, (((POWER_ALL_OFF_PULSETIME_ON == Settings.poweronstate) ? ~power : power) & mask) ? Settings.pulse_timer[device -1] : 0); + } + } + else if (POWER_BLINK == state) { + if (!(blink_mask & mask)) { + blink_powersave = (blink_powersave & (POWER_MASK ^ mask)) | (power & mask); // Save state + blink_power = (power >> (device -1))&1; // Prep to Toggle + } + blink_timer = millis() + 100; + blink_counter = ((!Settings.blinkcount) ? 64000 : (Settings.blinkcount *2)) +1; + blink_mask |= mask; // Set device mask + MqttPublishPowerBlinkState(device); + return; + } + else if (POWER_BLINK_STOP == state) { + bool flag = (blink_mask & mask); + blink_mask &= (POWER_MASK ^ mask); // Clear device mask + MqttPublishPowerBlinkState(device); + if (flag) { + ExecuteCommandPower(device, (blink_powersave >> (device -1))&1, SRC_IGNORE); // Restore state + } + return; + } + if (publish_power) { + MqttPublishPowerState(device); + } +} + +void StopAllPowerBlink(void) +{ + power_t mask; + + for (uint32_t i = 1; i <= devices_present; i++) { + mask = 1 << (i -1); + if (blink_mask & mask) { + blink_mask &= (POWER_MASK ^ mask); // Clear device mask + MqttPublishPowerBlinkState(i); + ExecuteCommandPower(i, (blink_powersave >> (i -1))&1, SRC_IGNORE); // Restore state + } + } +} + +void MqttShowPWMState(void) +{ + ResponseAppend_P(PSTR("\"" D_CMND_PWM "\":{")); + bool first = true; + for (uint32_t i = 0; i < MAX_PWMS; i++) { + if (pin[GPIO_PWM1 + i] < 99) { + ResponseAppend_P(PSTR("%s\"" D_CMND_PWM "%d\":%d"), first ? "" : ",", i+1, Settings.pwm_value[i]); + first = false; + } + } + ResponseJsonEnd(); +} + +void MqttShowState(void) +{ + char stemp1[33]; + + ResponseAppendTime(); + ResponseAppend_P(PSTR(",\"" D_JSON_UPTIME "\":\"%s\",\"UptimeSec\":%u"), GetUptime().c_str(), UpTime()); + +#ifdef USE_ADC_VCC + dtostrfd((double)ESP.getVcc()/1000, 3, stemp1); + ResponseAppend_P(PSTR(",\"" D_JSON_VCC "\":%s"), stemp1); +#endif + + ResponseAppend_P(PSTR(",\"" D_JSON_HEAPSIZE "\":%d,\"SleepMode\":\"%s\",\"Sleep\":%u,\"LoadAvg\":%u,\"MqttCount\":%u"), + ESP.getFreeHeap()/1024, GetTextIndexed(stemp1, sizeof(stemp1), Settings.flag3.sleep_normal, kSleepMode), // SetOption60 - Enable normal sleep instead of dynamic sleep + sleep, loop_load_avg, MqttConnectCount()); + + for (uint32_t i = 1; i <= devices_present; i++) { +#ifdef USE_LIGHT + if ((LightDevice()) && (i >= LightDevice())) { + if (i == LightDevice()) { LightState(1); } // call it only once + } else { +#endif + ResponseAppend_P(PSTR(",\"%s\":{\"STATE\":\"%s\"}"), GetPowerDevice(stemp1, i, sizeof(stemp1), Settings.flag.device_index_enable), // SetOption26 - Switch between POWER or POWER1 + GetStateText(bitRead(power, i-1))); +#ifdef USE_SONOFF_IFAN + if (IsModuleIfan()) { + ResponseAppend_P(PSTR(",\"" D_CMND_FANSPEED "\":%d"), GetFanspeed()); + break; + } +#endif // USE_SONOFF_IFAN +#ifdef USE_LIGHT + } +#endif + } + + if (pwm_present) { + ResponseAppend_P(PSTR(",")); + MqttShowPWMState(); + } + + ResponseAppend_P(PSTR(",\"" D_JSON_WIFI "\":{\"" D_JSON_AP "\":%d,\"" D_JSON_SSID "\":\"%s\",\"" D_JSON_BSSID "\":\"%s\",\"" D_JSON_CHANNEL "\":%d,\"" D_JSON_RSSI "\":%d,\"" D_JSON_LINK_COUNT "\":%d,\"" D_JSON_DOWNTIME "\":\"%s\"}}"), + Settings.sta_active +1, Settings.sta_ssid[Settings.sta_active], WiFi.BSSIDstr().c_str(), WiFi.channel(), WifiGetRssiAsQuality(WiFi.RSSI()), WifiLinkCount(), WifiDowntime().c_str()); +} + +void MqttPublishTeleState(void) +{ + mqtt_data[0] = '\0'; + MqttShowState(); + MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_STATE), MQTT_TELE_RETAIN); +#if defined(USE_RULES) || defined(USE_SCRIPT) + RulesTeleperiod(); // Allow rule based HA messages +#endif // USE_SCRIPT +} + +bool MqttShowSensor(void) +{ + ResponseAppendTime(); + + int json_data_start = strlen(mqtt_data); + for (uint32_t i = 0; i < MAX_SWITCHES; i++) { +#ifdef USE_TM1638 + if ((pin[GPIO_SWT1 +i] < 99) || ((pin[GPIO_TM16CLK] < 99) && (pin[GPIO_TM16DIO] < 99) && (pin[GPIO_TM16STB] < 99))) { +#else + if (pin[GPIO_SWT1 +i] < 99) { +#endif // USE_TM1638 + bool swm = ((FOLLOW_INV == Settings.switchmode[i]) || (PUSHBUTTON_INV == Settings.switchmode[i]) || (PUSHBUTTONHOLD_INV == Settings.switchmode[i])); + ResponseAppend_P(PSTR(",\"" D_JSON_SWITCH "%d\":{\"STATE\":\"%s\"}"), i +1, GetStateText(swm ^ SwitchLastState(i))); + } + } + XsnsCall(FUNC_JSON_APPEND); + XdrvCall(FUNC_JSON_APPEND); + + bool json_data_available = (strlen(mqtt_data) - json_data_start); + if (strstr_P(mqtt_data, PSTR(D_JSON_PRESSURE)) != nullptr) { + ResponseAppend_P(PSTR(",\"" D_JSON_PRESSURE_UNIT "\":\"%s\""), PressureUnit().c_str()); + } + if (strstr_P(mqtt_data, PSTR(D_JSON_TEMPERATURE)) != nullptr) { + ResponseAppend_P(PSTR(",\"" D_JSON_TEMPERATURE_UNIT "\":\"%c\""), TempUnit()); + } + ResponseJsonEnd(); + + if (json_data_available) { XdrvCall(FUNC_SHOW_SENSOR); } + return json_data_available; +} + +void MqttPublishSensor(void) +{ + mqtt_data[0] = '\0'; + if (MqttShowSensor()) { + MqttPublishTeleSensor(); + } +} + +/********************************************************************************************/ + +void PerformEverySecond(void) +{ + uptime++; + + if (ntp_synced_message) { + // Moved here to fix syslog UDP exception 9 during RtcSecond + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("NTP: Drift %d, (" D_UTC_TIME ") %s, (" D_DST_TIME ") %s, (" D_STD_TIME ") %s"), + DriftTime(), GetTime(0).c_str(), GetTime(2).c_str(), GetTime(3).c_str()); + ntp_synced_message = false; + } + + if (POWER_CYCLE_TIME == uptime) { + UpdateQuickPowerCycle(false); + } + + if (BOOT_LOOP_TIME == uptime) { + RtcRebootReset(); + +#ifdef USE_DEEPSLEEP + if (!(DeepSleepEnabled() && !Settings.flag3.bootcount_update)) { +#endif + Settings.bootcount++; // Moved to here to stop flash writes during start-up + AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_APPLICATION D_BOOT_COUNT " %d"), Settings.bootcount); +#ifdef USE_DEEPSLEEP + } +#endif + } + + if (seriallog_timer) { + seriallog_timer--; + if (!seriallog_timer) { + if (seriallog_level) { + AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_SERIAL_LOGGING_DISABLED)); + } + seriallog_level = 0; + } + } + + if (syslog_timer) { // Restore syslog level + syslog_timer--; + if (!syslog_timer) { + syslog_level = Settings.syslog_level; + if (Settings.syslog_level) { + AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_SYSLOG_LOGGING_REENABLED)); // Might trigger disable again (on purpose) + } + } + } + + ResetGlobalValues(); + + if (Settings.tele_period) { + if (tele_period >= 9999) { + if (!global_state.wifi_down) { + tele_period = 0; // Allow teleperiod once wifi is connected + } + } else { + tele_period++; + if (tele_period >= Settings.tele_period) { + tele_period = 0; + + MqttPublishTeleState(); + + mqtt_data[0] = '\0'; + if (MqttShowSensor()) { + MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); // CMND_SENSORRETAIN +#if defined(USE_RULES) || defined(USE_SCRIPT) + RulesTeleperiod(); // Allow rule based HA messages +#endif // USE_RULES + } + + XdrvCall(FUNC_AFTER_TELEPERIOD); + } + } + } +} + +/*********************************************************************************************\ + * State loops +\*********************************************************************************************/ +/*-------------------------------------------------------------------------------------------*\ + * Every 0.1 second +\*-------------------------------------------------------------------------------------------*/ + +void Every100mSeconds(void) +{ + // As the max amount of sleep = 250 mSec this loop will shift in time... + power_t power_now; + + if (latching_relay_pulse) { + latching_relay_pulse--; + if (!latching_relay_pulse) SetLatchingRelay(0, 0); + } + + for (uint32_t i = 0; i < MAX_PULSETIMERS; i++) { + if (pulse_timer[i] != 0L) { // Timer active? + if (TimeReached(pulse_timer[i])) { // Timer finished? + pulse_timer[i] = 0L; // Turn off this timer + ExecuteCommandPower(i +1, (POWER_ALL_OFF_PULSETIME_ON == Settings.poweronstate) ? POWER_ON : POWER_OFF, SRC_PULSETIMER); + } + } + } + + if (blink_mask) { + if (TimeReached(blink_timer)) { + SetNextTimeInterval(blink_timer, 100 * Settings.blinktime); + blink_counter--; + if (!blink_counter) { + StopAllPowerBlink(); + } else { + blink_power ^= 1; + power_now = (power & (POWER_MASK ^ blink_mask)) | ((blink_power) ? blink_mask : 0); + SetDevicePower(power_now, SRC_IGNORE); + } + } + } +} + +/*-------------------------------------------------------------------------------------------*\ + * Every 0.25 second +\*-------------------------------------------------------------------------------------------*/ + +void Every250mSeconds(void) +{ +// As the max amount of sleep = 250 mSec this loop should always be taken... + + uint32_t blinkinterval = 1; + + state_250mS++; + state_250mS &= 0x3; + + if (mqtt_cmnd_publish) mqtt_cmnd_publish--; // Clean up + + if (!Settings.flag.global_state) { // Problem blinkyblinky enabled - SetOption31 - Control link led blinking + if (global_state.data) { // Any problem + if (global_state.mqtt_down) { blinkinterval = 7; } // MQTT problem so blink every 2 seconds (slowest) + if (global_state.wifi_down) { blinkinterval = 3; } // Wifi problem so blink every second (slow) + blinks = 201; // Allow only a single blink in case the problem is solved + } + } + if (blinks || restart_flag || ota_state_flag) { + if (restart_flag || ota_state_flag) { // Overrule blinks and keep led lit + blinkstate = true; // Stay lit + } else { + blinkspeed--; + if (!blinkspeed) { + blinkspeed = blinkinterval; // Set interval to 0.2 (default), 1 or 2 seconds + blinkstate ^= 1; // Blink + } + } + if ((!(Settings.ledstate &0x08)) && ((Settings.ledstate &0x06) || (blinks > 200) || (blinkstate))) { + SetLedLink(blinkstate); // Set led on or off + } + if (!blinkstate) { + blinks--; + if (200 == blinks) blinks = 0; // Disable blink + } + } + if (Settings.ledstate &1 && (pin[GPIO_LEDLNK] < 99 || !(blinks || restart_flag || ota_state_flag)) ) { + bool tstate = power & Settings.ledmask; + if ((SONOFF_TOUCH == my_module_type) || (SONOFF_T11 == my_module_type) || (SONOFF_T12 == my_module_type) || (SONOFF_T13 == my_module_type)) { + tstate = (!power) ? 1 : 0; // As requested invert signal for Touch devices to find them in the dark + } + SetLedPower(tstate); + } + +/*-------------------------------------------------------------------------------------------*\ + * Every second at 0.25 second interval +\*-------------------------------------------------------------------------------------------*/ + + switch (state_250mS) { + case 0: // Every x.0 second + if (ota_state_flag && BACKLOG_EMPTY) { + ota_state_flag--; + if (2 == ota_state_flag) { + ota_url = Settings.ota_url; + RtcSettings.ota_loader = 0; // Try requested image first + ota_retry_counter = OTA_ATTEMPTS; + ESPhttpUpdate.rebootOnUpdate(false); + SettingsSave(1); // Free flash for OTA update + } + if (ota_state_flag <= 0) { +#ifdef USE_WEBSERVER + if (Settings.webserver) StopWebserver(); +#endif // USE_WEBSERVER +#ifdef USE_ARILUX_RF + AriluxRfDisable(); // Prevent restart exception on Arilux Interrupt routine +#endif // USE_ARILUX_RF + ota_state_flag = 92; + ota_result = 0; + ota_retry_counter--; + if (ota_retry_counter) { + strlcpy(mqtt_data, GetOtaUrl(log_data, sizeof(log_data)), sizeof(mqtt_data)); +#ifndef FIRMWARE_MINIMAL + if (RtcSettings.ota_loader) { + char *bch = strrchr(mqtt_data, '/'); // Only consider filename after last backslash prevent change of urls having "-" in it + char *pch = strrchr((bch != nullptr) ? bch : mqtt_data, '-'); // Change from filename-DE.bin into filename-minimal.bin + char *ech = strrchr((bch != nullptr) ? bch : mqtt_data, '.'); // Change from filename.bin into filename-minimal.bin + if (!pch) { pch = ech; } + if (pch) { + mqtt_data[pch - mqtt_data] = '\0'; + char *ech = strrchr(Settings.ota_url, '.'); // Change from filename.bin into filename-minimal.bin + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s-" D_JSON_MINIMAL "%s"), mqtt_data, ech); // Minimal filename must be filename-minimal + } + } +#endif // FIRMWARE_MINIMAL + AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_UPLOAD "%s"), mqtt_data); +#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_1) || defined(ARDUINO_ESP8266_RELEASE_2_4_2) + ota_result = (HTTP_UPDATE_FAILED != ESPhttpUpdate.update(mqtt_data)); +#else + // If using core stage or 2.5.0+ the syntax has changed + WiFiClient OTAclient; + ota_result = (HTTP_UPDATE_FAILED != ESPhttpUpdate.update(OTAclient, mqtt_data)); +#endif + if (!ota_result) { +#ifndef FIRMWARE_MINIMAL + int ota_error = ESPhttpUpdate.getLastError(); + DEBUG_CORE_LOG(PSTR("OTA: Error %d"), ota_error); + if ((HTTP_UE_TOO_LESS_SPACE == ota_error) || (HTTP_UE_BIN_FOR_WRONG_FLASH == ota_error)) { + RtcSettings.ota_loader = 1; // Try minimal image next + } +#endif // FIRMWARE_MINIMAL + ota_state_flag = 2; // Upgrade failed - retry + } + } + } + if (90 == ota_state_flag) { // Allow MQTT to reconnect + ota_state_flag = 0; + if (ota_result) { +// SetFlashModeDout(); // Force DOUT for both ESP8266 and ESP8285 + Response_P(PSTR(D_JSON_SUCCESSFUL ". " D_JSON_RESTARTING)); + } else { + Response_P(PSTR(D_JSON_FAILED " %s"), ESPhttpUpdate.getLastErrorString().c_str()); + } + restart_flag = 2; // Restart anyway to keep memory clean webserver + MqttPublishPrefixTopic_P(STAT, PSTR(D_CMND_UPGRADE)); + } + } + break; + case 1: // Every x.25 second + if (MidnightNow()) { + XsnsCall(FUNC_SAVE_AT_MIDNIGHT); + } + if (save_data_counter && BACKLOG_EMPTY) { + save_data_counter--; + if (save_data_counter <= 0) { + if (Settings.flag.save_state) { // SetOption0 - Save power state and use after restart + power_t mask = POWER_MASK; + for (uint32_t i = 0; i < MAX_PULSETIMERS; i++) { + if ((Settings.pulse_timer[i] > 0) && (Settings.pulse_timer[i] < 30)) { // 3 seconds + mask &= ~(1 << i); + } + } + if (!((Settings.power &mask) == (power &mask))) { + Settings.power = power; + } + } else { + Settings.power = 0; + } + SettingsSave(0); + save_data_counter = Settings.save_data; + } + } + if (restart_flag && BACKLOG_EMPTY) { + if ((214 == restart_flag) || (215 == restart_flag) || (216 == restart_flag)) { + char storage_wifi[sizeof(Settings.sta_ssid) + + sizeof(Settings.sta_pwd)]; + char storage_mqtt[sizeof(Settings.mqtt_host) + + sizeof(Settings.mqtt_port) + + sizeof(Settings.mqtt_client) + + sizeof(Settings.mqtt_user) + + sizeof(Settings.mqtt_pwd) + + sizeof(Settings.mqtt_topic)]; + memcpy(storage_wifi, Settings.sta_ssid, sizeof(storage_wifi)); // Backup current SSIDs and Passwords + if (216 == restart_flag) { + memcpy(storage_mqtt, Settings.mqtt_host, sizeof(storage_mqtt)); // Backup mqtt host, port, client, username and password + } + if ((215 == restart_flag) || (216 == restart_flag)) { + SettingsErase(0); // Erase all flash from program end to end of physical flash + } + SettingsDefault(); + memcpy(Settings.sta_ssid, storage_wifi, sizeof(storage_wifi)); // Restore current SSIDs and Passwords + if (216 == restart_flag) { + memcpy(Settings.mqtt_host, storage_mqtt, sizeof(storage_mqtt)); // Restore the mqtt host, port, client, username and password + strlcpy(Settings.mqtt_client, MQTT_CLIENT_ID, sizeof(Settings.mqtt_client)); // Set client to default + } + restart_flag = 2; + } + else if (213 == restart_flag) { + SettingsSdkErase(); // Erase flash SDK parameters + restart_flag = 2; + } + else if (212 == restart_flag) { + SettingsErase(0); // Erase all flash from program end to end of physical flash + restart_flag = 211; + } + if (211 == restart_flag) { + SettingsDefault(); + restart_flag = 2; + } + if (2 == restart_flag) { + SettingsSaveAll(); + } + restart_flag--; + if (restart_flag <= 0) { + AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_RESTARTING)); + EspRestart(); + } + } + break; + case 2: // Every x.5 second + WifiCheck(wifi_state_flag); + wifi_state_flag = WIFI_RESTART; + break; + case 3: // Every x.75 second + if (!global_state.wifi_down) { MqttCheck(); } + break; + } +} + +#ifdef USE_ARDUINO_OTA +/*********************************************************************************************\ + * Allow updating via the Arduino OTA-protocol. + * + * - Once started disables current wifi clients and udp + * - Perform restart when done to re-init wifi clients +\*********************************************************************************************/ + +bool arduino_ota_triggered = false; +uint16_t arduino_ota_progress_dot_count = 0; + +void ArduinoOTAInit(void) +{ + ArduinoOTA.setPort(8266); + ArduinoOTA.setHostname(my_hostname); + if (Settings.web_password[0] !=0) { ArduinoOTA.setPassword(Settings.web_password); } + + ArduinoOTA.onStart([]() + { + SettingsSave(1); // Free flash for OTA update +#ifdef USE_WEBSERVER + if (Settings.webserver) { StopWebserver(); } +#endif // USE_WEBSERVER +#ifdef USE_ARILUX_RF + AriluxRfDisable(); // Prevent restart exception on Arilux Interrupt routine +#endif // USE_ARILUX_RF + if (Settings.flag.mqtt_enabled) { + MqttDisconnect(); // SetOption3 - Enable MQTT + } + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_UPLOAD "Arduino OTA " D_UPLOAD_STARTED)); + arduino_ota_triggered = true; + arduino_ota_progress_dot_count = 0; + delay(100); // Allow time for message xfer + }); + + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) + { + if ((LOG_LEVEL_DEBUG <= seriallog_level)) { + arduino_ota_progress_dot_count++; + Serial.printf("."); + if (!(arduino_ota_progress_dot_count % 80)) { Serial.println(); } + } + }); + + ArduinoOTA.onError([](ota_error_t error) + { + /* + From ArduinoOTA.h: + typedef enum { OTA_AUTH_ERROR, OTA_BEGIN_ERROR, OTA_CONNECT_ERROR, OTA_RECEIVE_ERROR, OTA_END_ERROR } ota_error_t; + */ + char error_str[100]; + + if ((LOG_LEVEL_DEBUG <= seriallog_level) && arduino_ota_progress_dot_count) { Serial.println(); } + switch (error) { + case OTA_BEGIN_ERROR: strncpy_P(error_str, PSTR(D_UPLOAD_ERR_2), sizeof(error_str)); break; + case OTA_RECEIVE_ERROR: strncpy_P(error_str, PSTR(D_UPLOAD_ERR_5), sizeof(error_str)); break; + case OTA_END_ERROR: strncpy_P(error_str, PSTR(D_UPLOAD_ERR_7), sizeof(error_str)); break; + default: + snprintf_P(error_str, sizeof(error_str), PSTR(D_UPLOAD_ERROR_CODE " %d"), error); + } + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_UPLOAD "Arduino OTA %s. " D_RESTARTING), error_str); + EspRestart(); + }); + + ArduinoOTA.onEnd([]() + { + if ((LOG_LEVEL_DEBUG <= seriallog_level)) { Serial.println(); } + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_UPLOAD "Arduino OTA " D_SUCCESSFUL ". " D_RESTARTING)); + EspRestart(); + }); + + ArduinoOTA.begin(); + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_UPLOAD "Arduino OTA " D_ENABLED " " D_PORT " 8266")); +} +#endif // USE_ARDUINO_OTA + +/********************************************************************************************/ + +void SerialInput(void) +{ + while (Serial.available()) { +// yield(); + delay(0); + serial_in_byte = Serial.read(); + +/*-------------------------------------------------------------------------------------------*\ + * Sonoff dual and ch4 19200 baud serial interface +\*-------------------------------------------------------------------------------------------*/ + if ((SONOFF_DUAL == my_module_type) || (CH4 == my_module_type)) { + serial_in_byte = ButtonSerial(serial_in_byte); + } + +/*-------------------------------------------------------------------------------------------*/ + + if (XdrvCall(FUNC_SERIAL)) { + serial_in_byte_counter = 0; + Serial.flush(); + return; + } + +/*-------------------------------------------------------------------------------------------*/ + + if (serial_in_byte > 127 && !Settings.flag.mqtt_serial_raw) { // Discard binary data above 127 if no raw reception allowed - CMND_SERIALSEND3 + serial_in_byte_counter = 0; + Serial.flush(); + return; + } + if (!Settings.flag.mqtt_serial) { // SerialSend active - CMND_SERIALSEND and CMND_SERIALLOG + if (isprint(serial_in_byte)) { // Any char between 32 and 127 + 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 { + serial_in_byte_counter = 0; + } + } + } else { + if (serial_in_byte || Settings.flag.mqtt_serial_raw) { // Any char between 1 and 127 or any char (0 - 255) - CMND_SERIALSEND3 + if ((serial_in_byte_counter < INPUT_BUFFER_SIZE -1) && // Add char to string if it still fits and ... + ((isprint(serial_in_byte) && (128 == Settings.serial_delimiter)) || // Any char between 32 and 127 + ((serial_in_byte != Settings.serial_delimiter) && (128 != Settings.serial_delimiter)) || // Any char between 1 and 127 and not being delimiter + Settings.flag.mqtt_serial_raw)) { // Any char between 0 and 255 - CMND_SERIALSEND3 + serial_in_buffer[serial_in_byte_counter++] = serial_in_byte; + serial_polling_window = millis(); + } else { + serial_polling_window = 0; // Reception done - send mqtt + break; + } + } + } + +#ifdef USE_SONOFF_SC +/*-------------------------------------------------------------------------------------------*\ + * Sonoff SC 19200 baud serial interface +\*-------------------------------------------------------------------------------------------*/ + if (SONOFF_SC == my_module_type) { + if (serial_in_byte == '\x1B') { // Sonoff SC status from ATMEGA328P + serial_in_buffer[serial_in_byte_counter] = 0; // Serial data completed + SonoffScSerialInput(serial_in_buffer); + serial_in_byte_counter = 0; + Serial.flush(); + return; + } + } else +#endif // USE_SONOFF_SC +/*-------------------------------------------------------------------------------------------*/ + + if (!Settings.flag.mqtt_serial && (serial_in_byte == '\n')) { // CMND_SERIALSEND and CMND_SERIALLOG + serial_in_buffer[serial_in_byte_counter] = 0; // Serial data completed + seriallog_level = (Settings.seriallog_level < LOG_LEVEL_INFO) ? (uint8_t)LOG_LEVEL_INFO : Settings.seriallog_level; + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_COMMAND "%s"), serial_in_buffer); + ExecuteCommand(serial_in_buffer, SRC_SERIAL); + serial_in_byte_counter = 0; + serial_polling_window = 0; + Serial.flush(); + return; + } + } + + if (Settings.flag.mqtt_serial && serial_in_byte_counter && (millis() > (serial_polling_window + SERIAL_POLLING))) { // CMND_SERIALSEND and CMND_SERIALLOG + serial_in_buffer[serial_in_byte_counter] = 0; // Serial data completed + char hex_char[(serial_in_byte_counter * 2) + 2]; + Response_P(PSTR("{\"" D_JSON_SERIALRECEIVED "\":\"%s\"}"), + (Settings.flag.mqtt_serial_raw) ? ToHex_P((unsigned char*)serial_in_buffer, serial_in_byte_counter, hex_char, sizeof(hex_char)) : serial_in_buffer); + MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_SERIALRECEIVED)); + XdrvRulesProcess(); + serial_in_byte_counter = 0; + } +} + +/********************************************************************************************/ + +void GpioInit(void) +{ + uint32_t mpin; + + if (!ValidModule(Settings.module)) { + uint32_t module = MODULE; + if (!ValidModule(MODULE)) { module = SONOFF_BASIC; } + Settings.module = module; + Settings.last_module = module; + } + SetModuleType(); + + if (Settings.module != Settings.last_module) { + baudrate = APP_BAUDRATE; + } + + for (uint32_t i = 0; i < sizeof(Settings.user_template.gp); i++) { + if ((Settings.user_template.gp.io[i] >= GPIO_SENSOR_END) && (Settings.user_template.gp.io[i] < GPIO_USER)) { + Settings.user_template.gp.io[i] = GPIO_USER; // Fix not supported sensor ids in template + } + } + + myio def_gp; + ModuleGpios(&def_gp); + for (uint32_t i = 0; i < sizeof(Settings.my_gp); i++) { + if ((Settings.my_gp.io[i] >= GPIO_SENSOR_END) && (Settings.my_gp.io[i] < GPIO_USER)) { + Settings.my_gp.io[i] = GPIO_NONE; // Fix not supported sensor ids in module + } + else if (Settings.my_gp.io[i] > GPIO_NONE) { + my_module.io[i] = Settings.my_gp.io[i]; // Set User selected Module sensors + } + if ((def_gp.io[i] > GPIO_NONE) && (def_gp.io[i] < GPIO_USER)) { + my_module.io[i] = def_gp.io[i]; // Force Template override + } + } + if ((Settings.my_adc0 >= ADC0_END) && (Settings.my_adc0 < ADC0_USER)) { + Settings.my_adc0 = ADC0_NONE; // Fix not supported sensor ids in module + } + else if (Settings.my_adc0 > ADC0_NONE) { + my_adc0 = Settings.my_adc0; // Set User selected Module sensors + } + my_module_flag = ModuleFlag(); + uint32_t template_adc0 = my_module_flag.data &15; + if ((template_adc0 > ADC0_NONE) && (template_adc0 < ADC0_USER)) { + my_adc0 = template_adc0; // Force Template override + } + + for (uint32_t i = 0; i < GPIO_MAX; i++) { + pin[i] = 99; + } + for (uint32_t i = 0; i < sizeof(my_module.io); i++) { + mpin = ValidPin(i, my_module.io[i]); + + DEBUG_CORE_LOG(PSTR("INI: gpio pin %d, mpin %d"), i, mpin); + + if (mpin) { + XdrvMailbox.index = mpin; + XdrvMailbox.payload = i; + + if ((mpin >= GPIO_SWT1_NP) && (mpin < (GPIO_SWT1_NP + MAX_SWITCHES))) { + SwitchPullupFlag(mpin - GPIO_SWT1_NP); + mpin -= (GPIO_SWT1_NP - GPIO_SWT1); + } + else if ((mpin >= GPIO_KEY1_NP) && (mpin < (GPIO_KEY1_NP + MAX_KEYS))) { + ButtonPullupFlag(mpin - GPIO_KEY1_NP); // 0 .. 3 + mpin -= (GPIO_KEY1_NP - GPIO_KEY1); + } + else if ((mpin >= GPIO_KEY1_INV) && (mpin < (GPIO_KEY1_INV + MAX_KEYS))) { + ButtonInvertFlag(mpin - GPIO_KEY1_INV); // 0 .. 3 + mpin -= (GPIO_KEY1_INV - GPIO_KEY1); + } + else if ((mpin >= GPIO_KEY1_INV_NP) && (mpin < (GPIO_KEY1_INV_NP + MAX_KEYS))) { + ButtonPullupFlag(mpin - GPIO_KEY1_INV_NP); // 0 .. 3 + ButtonInvertFlag(mpin - GPIO_KEY1_INV_NP); // 0 .. 3 + mpin -= (GPIO_KEY1_INV_NP - GPIO_KEY1); + } + else if ((mpin >= GPIO_REL1_INV) && (mpin < (GPIO_REL1_INV + MAX_RELAYS))) { + bitSet(rel_inverted, mpin - GPIO_REL1_INV); + mpin -= (GPIO_REL1_INV - GPIO_REL1); + } + else if ((mpin >= GPIO_LED1_INV) && (mpin < (GPIO_LED1_INV + MAX_LEDS))) { + bitSet(led_inverted, mpin - GPIO_LED1_INV); + mpin -= (GPIO_LED1_INV - GPIO_LED1); + } + else if (mpin == GPIO_LEDLNK_INV) { + ledlnk_inverted = 1; + mpin -= (GPIO_LEDLNK_INV - GPIO_LEDLNK); + } + else if ((mpin >= GPIO_PWM1_INV) && (mpin < (GPIO_PWM1_INV + MAX_PWMS))) { + bitSet(pwm_inverted, mpin - GPIO_PWM1_INV); + mpin -= (GPIO_PWM1_INV - GPIO_PWM1); + } + else if (XdrvCall(FUNC_PIN_STATE)) { + mpin = XdrvMailbox.index; + } + else if (XsnsCall(FUNC_PIN_STATE)) { + mpin = XdrvMailbox.index; + }; + } + if (mpin) pin[mpin] = i; + } + + if ((2 == pin[GPIO_TXD]) || (H801 == my_module_type)) { Serial.set_tx(2); } + + analogWriteRange(Settings.pwm_range); // Default is 1023 (Arduino.h) + analogWriteFreq(Settings.pwm_frequency); // Default is 1000 (core_esp8266_wiring_pwm.c) + +#ifdef USE_SPI + spi_flg = ((((pin[GPIO_SPI_CS] < 99) && (pin[GPIO_SPI_CS] > 14)) || (pin[GPIO_SPI_CS] < 12)) || (((pin[GPIO_SPI_DC] < 99) && (pin[GPIO_SPI_DC] > 14)) || (pin[GPIO_SPI_DC] < 12))); + if (spi_flg) { + for (uint32_t i = 0; i < GPIO_MAX; i++) { + if ((pin[i] >= 12) && (pin[i] <=14)) pin[i] = 99; + } + my_module.io[12] = GPIO_SPI_MISO; + pin[GPIO_SPI_MISO] = 12; + my_module.io[13] = GPIO_SPI_MOSI; + pin[GPIO_SPI_MOSI] = 13; + my_module.io[14] = GPIO_SPI_CLK; + pin[GPIO_SPI_CLK] = 14; + } + soft_spi_flg = ((pin[GPIO_SSPI_CS] < 99) && (pin[GPIO_SSPI_SCLK] < 99) && ((pin[GPIO_SSPI_MOSI] < 99) || (pin[GPIO_SSPI_MOSI] < 99))); +#endif // USE_SPI + +#ifdef USE_I2C + i2c_flg = ((pin[GPIO_I2C_SCL] < 99) && (pin[GPIO_I2C_SDA] < 99)); + if (i2c_flg) { + Wire.begin(pin[GPIO_I2C_SDA], pin[GPIO_I2C_SCL]); + } +#endif // USE_I2C + + devices_present = 0; + light_type = LT_BASIC; // Use basic PWM control if SetOption15 = 0 + if (XdrvCall(FUNC_MODULE_INIT)) { + // Serviced + } + else if (YTF_IR_BRIDGE == my_module_type) { + ClaimSerial(); // Stop serial loopback mode +// devices_present = 1; + } + else if (SONOFF_DUAL == my_module_type) { + Settings.flag.mqtt_serial = 0; // CMND_SERIALSEND and CMND_SERIALLOG + devices_present = 2; + baudrate = 19200; + } + else if (CH4 == my_module_type) { + Settings.flag.mqtt_serial = 0; // CMND_SERIALSEND and CMND_SERIALLOG + devices_present = 4; + baudrate = 19200; + } +#ifdef USE_SONOFF_SC + else if (SONOFF_SC == my_module_type) { + Settings.flag.mqtt_serial = 0; // CMND_SERIALSEND and CMND_SERIALLOG + devices_present = 0; + baudrate = 19200; + } +#endif // USE_SONOFF_SC + + if (!light_type) { + for (uint32_t i = 0; i < MAX_PWMS; i++) { // Basic PWM control only + if (pin[GPIO_PWM1 +i] < 99) { + pwm_present = true; + pinMode(pin[GPIO_PWM1 +i], OUTPUT); + analogWrite(pin[GPIO_PWM1 +i], bitRead(pwm_inverted, i) ? Settings.pwm_range - Settings.pwm_value[i] : Settings.pwm_value[i]); + } + } + } + for (uint32_t i = 0; i < MAX_RELAYS; i++) { + if (pin[GPIO_REL1 +i] < 99) { + pinMode(pin[GPIO_REL1 +i], OUTPUT); + devices_present++; + if (EXS_RELAY == my_module_type) { + digitalWrite(pin[GPIO_REL1 +i], bitRead(rel_inverted, i) ? 1 : 0); + if (i &1) { devices_present--; } + } + } + } + + for (uint32_t i = 0; i < MAX_LEDS; i++) { + if (pin[GPIO_LED1 +i] < 99) { +#ifdef USE_ARILUX_RF + if ((3 == i) && (leds_present < 2) && (99 == pin[GPIO_ARIRFSEL])) { + pin[GPIO_ARIRFSEL] = pin[GPIO_LED4]; // Legacy support where LED4 was Arilux RF enable + pin[GPIO_LED4] = 99; + } else { +#endif + pinMode(pin[GPIO_LED1 +i], OUTPUT); + leds_present++; + digitalWrite(pin[GPIO_LED1 +i], bitRead(led_inverted, i)); +#ifdef USE_ARILUX_RF + } +#endif + } + } + if (pin[GPIO_LEDLNK] < 99) { + pinMode(pin[GPIO_LEDLNK], OUTPUT); + digitalWrite(pin[GPIO_LEDLNK], ledlnk_inverted); + } + + ButtonInit(); + SwitchInit(); +#ifdef ROTARY_V1 + RotaryInit(); +#endif + + SetLedPower(Settings.ledstate &8); + SetLedLink(Settings.ledstate &8); + + XdrvCall(FUNC_PRE_INIT); +} diff --git a/tasmota/support_wifi.ino b/tasmota/support_wifi.ino index c920c8a63..49faa0663 100644 --- a/tasmota/support_wifi.ino +++ b/tasmota/support_wifi.ino @@ -118,8 +118,10 @@ void WifiSetMode(WiFiMode_t wifi_mode) delay(100); } - if (!WiFi.mode(wifi_mode)) { - AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_WIFI "Cannot set Mode")); + uint32_t retry = 2; + while (!WiFi.mode(wifi_mode) && retry--) { + AddLog_P(LOG_LEVEL_INFO, S_LOG_WIFI, PSTR("Retry set Mode...")); + delay(100); } if (wifi_mode == WIFI_OFF) { diff --git a/tasmota/tasmota.ino b/tasmota/tasmota.ino index e6511eb04..e18a22ce1 100644 --- a/tasmota/tasmota.ino +++ b/tasmota/tasmota.ino @@ -37,7 +37,6 @@ #include "i18n.h" // Language support configured by my_user_config.h #include "tasmota_template.h" // Hardware configuration - #ifdef ARDUINO_ESP8266_RELEASE_2_4_0 #include "lwip/init.h" #if LWIP_VERSION_MAJOR != 1 @@ -69,11 +68,12 @@ // Structs #include "settings.h" -const char kSleepMode[] PROGMEM = "Dynamic|Normal"; -const char kPrefixes[] PROGMEM = D_CMND "|" D_STAT "|" D_TELE; const char kCodeImage[] PROGMEM = "tasmota|minimal|sensors|knx|basic|display|ir"; -// Global variables +/*********************************************************************************************\ + * Global variables +\*********************************************************************************************/ + SerialConfig serial_config = SERIAL_8N1; // Serial interface configuration 8 data bits, No parity, 1 stop bit WiFiUDP PortUdp; // UDP Syslog and Alexa @@ -157,6 +157,7 @@ bool spi_flg = false; // SPI configured bool soft_spi_flg = false; // Software SPI configured bool ntp_force_sync = false; // Force NTP sync bool ntp_synced_message = false; // NTP synced message flag +bool is_8285 = false; // Hardware device ESP8266EX (0) or ESP8285 (1) myio my_module; // Active copy of Module GPIOs (17 x 8 bits) gpio_flag my_module_flag; // Active copy of Template GPIO flags StateBitfield global_state; // Global states (currently Wifi and Mqtt) (8 bits) @@ -180,1325 +181,9 @@ char web_log[WEB_LOG_SIZE] = {'\0'}; // Web log buffer #define BACKLOG_EMPTY (backlog_pointer == backlog_index) #endif -/********************************************************************************************/ - -char* Format(char* output, const char* input, int size) -{ - char *token; - uint32_t digits = 0; - - if (strstr(input, "%") != nullptr) { - strlcpy(output, input, size); - token = strtok(output, "%"); - if (strstr(input, "%") == input) { - output[0] = '\0'; - } else { - token = strtok(nullptr, ""); - } - if (token != nullptr) { - digits = atoi(token); - if (digits) { - char tmp[size]; - if (strchr(token, 'd')) { - snprintf_P(tmp, size, PSTR("%s%c0%dd"), output, '%', digits); - snprintf_P(output, size, tmp, ESP.getChipId() & 0x1fff); // %04d - short chip ID in dec, like in hostname - } else { - snprintf_P(tmp, size, PSTR("%s%c0%dX"), output, '%', digits); - snprintf_P(output, size, tmp, ESP.getChipId()); // %06X - full chip ID in hex - } - } else { - if (strchr(token, 'd')) { - snprintf_P(output, size, PSTR("%s%d"), output, ESP.getChipId()); // %d - full chip ID in dec - digits = 8; - } - } - } - } - if (!digits) { - strlcpy(output, input, size); - } - return output; -} - -char* GetOtaUrl(char *otaurl, size_t otaurl_size) -{ - if (strstr(Settings.ota_url, "%04d") != nullptr) { // OTA url contains placeholder for chip ID - snprintf(otaurl, otaurl_size, Settings.ota_url, ESP.getChipId() & 0x1fff); - } - else if (strstr(Settings.ota_url, "%d") != nullptr) { // OTA url contains placeholder for chip ID - snprintf_P(otaurl, otaurl_size, Settings.ota_url, ESP.getChipId()); - } - else { - strlcpy(otaurl, Settings.ota_url, otaurl_size); - } - return otaurl; -} - -char* GetTopic_P(char *stopic, uint32_t prefix, char *topic, const char* subtopic) -{ - /* prefix 0 = Cmnd - prefix 1 = Stat - prefix 2 = Tele - prefix 4 = Cmnd fallback - prefix 5 = Stat fallback - prefix 6 = Tele fallback - prefix 8 = Cmnd topic - prefix 9 = Stat topic - prefix 10 = Tele topic - */ - char romram[CMDSZ]; - String fulltopic; - - snprintf_P(romram, sizeof(romram), subtopic); - if (fallback_topic_flag || (prefix > 3)) { - bool fallback = (prefix < 8); - prefix &= 3; - char stemp[11]; - fulltopic = GetTextIndexed(stemp, sizeof(stemp), prefix, kPrefixes); - fulltopic += F("/"); - if (fallback) { - fulltopic += mqtt_client; - fulltopic += F("_fb"); // cmnd/_fb - } else { - fulltopic += topic; // cmnd/ - } - } else { - fulltopic = Settings.mqtt_fulltopic; - if ((0 == prefix) && (-1 == fulltopic.indexOf(FPSTR(MQTT_TOKEN_PREFIX)))) { - fulltopic += F("/"); - fulltopic += FPSTR(MQTT_TOKEN_PREFIX); // Need prefix for commands to handle mqtt topic loops - } - for (uint32_t i = 0; i < 3; i++) { - if ('\0' == Settings.mqtt_prefix[i][0]) { - GetTextIndexed(Settings.mqtt_prefix[i], sizeof(Settings.mqtt_prefix[i]), i, kPrefixes); - } - } - fulltopic.replace(FPSTR(MQTT_TOKEN_PREFIX), Settings.mqtt_prefix[prefix]); - fulltopic.replace(FPSTR(MQTT_TOKEN_TOPIC), topic); - fulltopic.replace(F("%hostname%"), my_hostname); - String token_id = WiFi.macAddress(); - token_id.replace(":", ""); - fulltopic.replace(F("%id%"), token_id); - } - fulltopic.replace(F("#"), ""); - fulltopic.replace(F("//"), "/"); - if (!fulltopic.endsWith("/")) { - fulltopic += "/"; - } - snprintf_P(stopic, TOPSZ, PSTR("%s%s"), fulltopic.c_str(), romram); - return stopic; -} - -char* GetGroupTopic_P(char *stopic, const char* subtopic) -{ - // SetOption75 0: %prefix%/nothing/%topic% = cmnd/nothing//# - // SetOption75 1: cmnd/ - return GetTopic_P(stopic, (Settings.flag3.grouptopic_mode) ? CMND +8 : CMND, Settings.mqtt_grptopic, subtopic); // SetOption75 - GroupTopic replaces %topic% (0) or fixed topic cmnd/grouptopic (1) -} - -char* GetFallbackTopic_P(char *stopic, const char* subtopic) -{ - return GetTopic_P(stopic, CMND +4, nullptr, subtopic); -} - -char* GetStateText(uint32_t state) -{ - if (state > 3) { - state = 1; - } - return Settings.state_text[state]; -} - -/********************************************************************************************/ - -void SetLatchingRelay(power_t lpower, uint32_t state) -{ - // power xx00 - toggle REL1 (Off) and REL3 (Off) - device 1 Off, device 2 Off - // power xx01 - toggle REL2 (On) and REL3 (Off) - device 1 On, device 2 Off - // power xx10 - toggle REL1 (Off) and REL4 (On) - device 1 Off, device 2 On - // power xx11 - toggle REL2 (On) and REL4 (On) - device 1 On, device 2 On - - if (state && !latching_relay_pulse) { // Set latching relay to power if previous pulse has finished - latching_power = lpower; - latching_relay_pulse = 2; // max 200mS (initiated by stateloop()) - } - - for (uint32_t i = 0; i < devices_present; i++) { - uint32_t port = (i << 1) + ((latching_power >> i) &1); - if (pin[GPIO_REL1 +port] < 99) { - digitalWrite(pin[GPIO_REL1 +port], bitRead(rel_inverted, port) ? !state : state); - } - } -} - -void SetDevicePower(power_t rpower, uint32_t source) -{ - ShowSource(source); - last_source = source; - - if (POWER_ALL_ALWAYS_ON == Settings.poweronstate) { // All on and stay on - power = (1 << devices_present) -1; - rpower = power; - } - - if (Settings.flag.interlock) { // Allow only one or no relay set - CMND_INTERLOCK - Enable/disable interlock - for (uint32_t i = 0; i < MAX_INTERLOCKS; i++) { - power_t mask = 1; - uint32_t count = 0; - for (uint32_t j = 0; j < devices_present; j++) { - if ((Settings.interlock[i] & mask) && (rpower & mask)) { - count++; - } - mask <<= 1; - } - if (count > 1) { - mask = ~Settings.interlock[i]; // Turn interlocked group off as there would be multiple relays on - power &= mask; - rpower &= mask; - } - } - } - - if (rpower) { // Any power set - last_power = rpower; - } - - XdrvMailbox.index = rpower; - XdrvCall(FUNC_SET_POWER); // Signal power state - - XdrvMailbox.index = rpower; - XdrvMailbox.payload = source; - if (XdrvCall(FUNC_SET_DEVICE_POWER)) { // Set power state and stop if serviced - // Serviced - } - else if ((SONOFF_DUAL == my_module_type) || (CH4 == my_module_type)) { - Serial.write(0xA0); - Serial.write(0x04); - Serial.write(rpower &0xFF); - Serial.write(0xA1); - Serial.write('\n'); - Serial.flush(); - } - else if (EXS_RELAY == my_module_type) { - SetLatchingRelay(rpower, 1); - } - else { - for (uint32_t i = 0; i < devices_present; i++) { - power_t state = rpower &1; - if ((i < MAX_RELAYS) && (pin[GPIO_REL1 +i] < 99)) { - digitalWrite(pin[GPIO_REL1 +i], bitRead(rel_inverted, i) ? !state : state); - } - rpower >>= 1; - } - } -} - -void RestorePower(bool publish_power, uint32_t source) -{ - if (power != last_power) { - SetDevicePower(last_power, source); - if (publish_power) { - MqttPublishAllPowerState(); - } - } -} - -void SetAllPower(uint32_t state, uint32_t source) -{ -// state 0 = POWER_OFF = Relay Off -// state 1 = POWER_ON = Relay On (turn off after Settings.pulse_timer * 100 mSec if enabled) -// state 2 = POWER_TOGGLE = Toggle relay -// state 8 = POWER_OFF_NO_STATE = Relay Off and no publishPowerState -// state 9 = POWER_ON_NO_STATE = Relay On and no publishPowerState -// state 10 = POWER_TOGGLE_NO_STATE = Toggle relay and no publishPowerState -// state 16 = POWER_SHOW_STATE = Show power state - - bool publish_power = true; - if ((state >= POWER_OFF_NO_STATE) && (state <= POWER_TOGGLE_NO_STATE)) { - state &= 3; // POWER_OFF, POWER_ON or POWER_TOGGLE - publish_power = false; - } - if ((state >= POWER_OFF) && (state <= POWER_TOGGLE)) { - power_t all_on = (1 << devices_present) -1; - switch (state) { - case POWER_OFF: - power = 0; - break; - case POWER_ON: - power = all_on; - break; - case POWER_TOGGLE: - power ^= all_on; // Complement current state - } - SetDevicePower(power, source); - } - if (publish_power) { - MqttPublishAllPowerState(); - } -} - -void SetLedPowerIdx(uint32_t led, uint32_t state) -{ - if ((99 == pin[GPIO_LEDLNK]) && (0 == led)) { // Legacy - LED1 is link led only if LED2 is present - if (pin[GPIO_LED2] < 99) { - led = 1; - } - } - if (pin[GPIO_LED1 + led] < 99) { - uint32_t mask = 1 << led; - if (state) { - state = 1; - led_power |= mask; - } else { - led_power &= (0xFF ^ mask); - } - digitalWrite(pin[GPIO_LED1 + led], bitRead(led_inverted, led) ? !state : state); - } -} - -void SetLedPower(uint32_t state) -{ - if (99 == pin[GPIO_LEDLNK]) { // Legacy - Only use LED1 and/or LED2 - SetLedPowerIdx(0, state); - } else { - power_t mask = 1; - for (uint32_t i = 0; i < leds_present; i++) { // Map leds to power - bool tstate = (power & mask); - SetLedPowerIdx(i, tstate); - mask <<= 1; - } - } -} - -void SetLedPowerAll(uint32_t state) -{ - for (uint32_t i = 0; i < leds_present; i++) { - SetLedPowerIdx(i, state); - } -} - -void SetLedLink(uint32_t state) -{ - uint32_t led_pin = pin[GPIO_LEDLNK]; - uint32_t led_inv = ledlnk_inverted; - if (99 == led_pin) { // Legacy - LED1 is status - led_pin = pin[GPIO_LED1]; - led_inv = bitRead(led_inverted, 0); - } - if (led_pin < 99) { - if (state) { state = 1; } - digitalWrite(led_pin, (led_inv) ? !state : state); - } -} - -void SetPulseTimer(uint32_t index, uint32_t time) -{ - pulse_timer[index] = (time > 111) ? millis() + (1000 * (time - 100)) : (time > 0) ? millis() + (100 * time) : 0L; -} - -uint32_t GetPulseTimer(uint32_t index) -{ - long time = TimePassedSince(pulse_timer[index]); - if (time < 0) { - time *= -1; - return (time > 11100) ? (time / 1000) + 100 : (time > 0) ? time / 100 : 0; - } - return 0; -} - -/********************************************************************************************/ - -bool SendKey(uint32_t key, uint32_t device, uint32_t state) -{ -// key 0 = KEY_BUTTON = button_topic -// key 1 = KEY_SWITCH = switch_topic -// state 0 = POWER_OFF = off -// state 1 = POWER_ON = on -// state 2 = POWER_TOGGLE = toggle -// state 3 = POWER_HOLD = hold -// state 9 = CLEAR_RETAIN = clear retain flag - - char stopic[TOPSZ]; - char scommand[CMDSZ]; - char key_topic[sizeof(Settings.button_topic)]; - bool result = false; - - char *tmp = (key) ? Settings.switch_topic : Settings.button_topic; - Format(key_topic, tmp, sizeof(key_topic)); - if (Settings.flag.mqtt_enabled && MqttIsConnected() && (strlen(key_topic) != 0) && strcmp(key_topic, "0")) { // SetOption3 - Enable MQTT - if (!key && (device > devices_present)) { - device = 1; // Only allow number of buttons up to number of devices - } - GetTopic_P(stopic, CMND, key_topic, - GetPowerDevice(scommand, device, sizeof(scommand), (key + Settings.flag.device_index_enable))); // cmnd/switchtopic/POWERx - SetOption26 - Switch between POWER or POWER1 - if (CLEAR_RETAIN == state) { - mqtt_data[0] = '\0'; - } else { - if ((Settings.flag3.button_switch_force_local || // SetOption61 - Force local operation when button/switch topic is set - !strcmp(mqtt_topic, key_topic) || - !strcmp(Settings.mqtt_grptopic, key_topic)) && - (POWER_TOGGLE == state)) { - state = ~(power >> (device -1)) &1; // POWER_OFF or POWER_ON - } - snprintf_P(mqtt_data, sizeof(mqtt_data), GetStateText(state)); - } -#ifdef USE_DOMOTICZ - if (!(DomoticzSendKey(key, device, state, strlen(mqtt_data)))) { -#endif // USE_DOMOTICZ - MqttPublishDirect(stopic, ((key) ? Settings.flag.mqtt_switch_retain // CMND_SWITCHRETAIN - : Settings.flag.mqtt_button_retain) && // CMND_BUTTONRETAIN - (state != POWER_HOLD || !Settings.flag3.no_hold_retain)); // SetOption62 - Don't use retain flag on HOLD messages -#ifdef USE_DOMOTICZ - } -#endif // USE_DOMOTICZ - result = !Settings.flag3.button_switch_force_local; // SetOption61 - Force local operation when button/switch topic is set - } else { - Response_P(PSTR("{\"%s%d\":{\"State\":%d}}"), (key) ? "Switch" : "Button", device, state); - result = XdrvRulesProcess(); - } - int32_t payload_save = XdrvMailbox.payload; - XdrvMailbox.payload = key << 16 | state << 8 | device; - XdrvCall(FUNC_ANY_KEY); - XdrvMailbox.payload = payload_save; - return result; -} - -void ExecuteCommandPower(uint32_t device, uint32_t state, uint32_t source) -{ -// device = Relay number 1 and up -// state 0 = POWER_OFF = Relay Off -// state 1 = POWER_ON = Relay On (turn off after Settings.pulse_timer * 100 mSec if enabled) -// state 2 = POWER_TOGGLE = Toggle relay -// state 3 = POWER_BLINK = Blink relay -// state 4 = POWER_BLINK_STOP = Stop blinking relay -// state 8 = POWER_OFF_NO_STATE = Relay Off and no publishPowerState -// state 9 = POWER_ON_NO_STATE = Relay On and no publishPowerState -// state 10 = POWER_TOGGLE_NO_STATE = Toggle relay and no publishPowerState -// state 16 = POWER_SHOW_STATE = Show power state - -// ShowSource(source); - -#ifdef USE_SONOFF_IFAN - if (IsModuleIfan()) { - blink_mask &= 1; // No blinking on the fan relays - Settings.flag.interlock = 0; // No interlock mode as it is already done by the microcontroller - CMND_INTERLOCK - Enable/disable interlock - Settings.pulse_timer[1] = 0; // No pulsetimers on the fan relays - Settings.pulse_timer[2] = 0; - Settings.pulse_timer[3] = 0; - } -#endif // USE_SONOFF_IFAN - - bool publish_power = true; - if ((state >= POWER_OFF_NO_STATE) && (state <= POWER_TOGGLE_NO_STATE)) { - state &= 3; // POWER_OFF, POWER_ON or POWER_TOGGLE - publish_power = false; - } - - if ((device < 1) || (device > devices_present)) { - device = 1; - } - active_device = device; - - if (device <= MAX_PULSETIMERS) { - SetPulseTimer(device -1, 0); - } - power_t mask = 1 << (device -1); // Device to control - if (state <= POWER_TOGGLE) { - if ((blink_mask & mask)) { - blink_mask &= (POWER_MASK ^ mask); // Clear device mask - MqttPublishPowerBlinkState(device); - } - - if (Settings.flag.interlock && // CMND_INTERLOCK - Enable/disable interlock - !interlock_mutex && - ((POWER_ON == state) || ((POWER_TOGGLE == state) && !(power & mask))) - ) { - interlock_mutex = true; // Clear all but masked relay in interlock group if new set requested - for (uint32_t i = 0; i < MAX_INTERLOCKS; i++) { - if (Settings.interlock[i] & mask) { // Find interlock group - for (uint32_t j = 0; j < devices_present; j++) { - power_t imask = 1 << j; - if ((Settings.interlock[i] & imask) && (power & imask) && (mask != imask)) { - ExecuteCommandPower(j +1, POWER_OFF, SRC_IGNORE); - delay(50); // Add some delay to make sure never have more than one relay on - } - } - break; // An interlocked relay is only present in one group so quit - } - } - interlock_mutex = false; - } - - switch (state) { - case POWER_OFF: { - power &= (POWER_MASK ^ mask); - break; } - case POWER_ON: - power |= mask; - break; - case POWER_TOGGLE: - power ^= mask; - } - SetDevicePower(power, source); -#ifdef USE_DOMOTICZ - DomoticzUpdatePowerState(device); -#endif // USE_DOMOTICZ -#ifdef USE_KNX - KnxUpdatePowerState(device, power); -#endif // USE_KNX - if (publish_power && Settings.flag3.hass_tele_on_power) { // SetOption59 - Send tele/%topic%/STATE in addition to stat/%topic%/RESULT - MqttPublishTeleState(); - } - if (device <= MAX_PULSETIMERS) { // Restart PulseTime if powered On - SetPulseTimer(device -1, (((POWER_ALL_OFF_PULSETIME_ON == Settings.poweronstate) ? ~power : power) & mask) ? Settings.pulse_timer[device -1] : 0); - } - } - else if (POWER_BLINK == state) { - if (!(blink_mask & mask)) { - blink_powersave = (blink_powersave & (POWER_MASK ^ mask)) | (power & mask); // Save state - blink_power = (power >> (device -1))&1; // Prep to Toggle - } - blink_timer = millis() + 100; - blink_counter = ((!Settings.blinkcount) ? 64000 : (Settings.blinkcount *2)) +1; - blink_mask |= mask; // Set device mask - MqttPublishPowerBlinkState(device); - return; - } - else if (POWER_BLINK_STOP == state) { - bool flag = (blink_mask & mask); - blink_mask &= (POWER_MASK ^ mask); // Clear device mask - MqttPublishPowerBlinkState(device); - if (flag) { - ExecuteCommandPower(device, (blink_powersave >> (device -1))&1, SRC_IGNORE); // Restore state - } - return; - } - if (publish_power) { - MqttPublishPowerState(device); - } -} - -void StopAllPowerBlink(void) -{ - power_t mask; - - for (uint32_t i = 1; i <= devices_present; i++) { - mask = 1 << (i -1); - if (blink_mask & mask) { - blink_mask &= (POWER_MASK ^ mask); // Clear device mask - MqttPublishPowerBlinkState(i); - ExecuteCommandPower(i, (blink_powersave >> (i -1))&1, SRC_IGNORE); // Restore state - } - } -} - -void MqttShowPWMState(void) -{ - ResponseAppend_P(PSTR("\"" D_CMND_PWM "\":{")); - bool first = true; - for (uint32_t i = 0; i < MAX_PWMS; i++) { - if (pin[GPIO_PWM1 + i] < 99) { - ResponseAppend_P(PSTR("%s\"" D_CMND_PWM "%d\":%d"), first ? "" : ",", i+1, Settings.pwm_value[i]); - first = false; - } - } - ResponseJsonEnd(); -} - -void MqttShowState(void) -{ - char stemp1[33]; - - ResponseAppendTime(); - ResponseAppend_P(PSTR(",\"" D_JSON_UPTIME "\":\"%s\",\"UptimeSec\":%u"), GetUptime().c_str(), UpTime()); - -#ifdef USE_ADC_VCC - dtostrfd((double)ESP.getVcc()/1000, 3, stemp1); - ResponseAppend_P(PSTR(",\"" D_JSON_VCC "\":%s"), stemp1); -#endif - - ResponseAppend_P(PSTR(",\"" D_JSON_HEAPSIZE "\":%d,\"SleepMode\":\"%s\",\"Sleep\":%u,\"LoadAvg\":%u,\"MqttCount\":%u"), - ESP.getFreeHeap()/1024, GetTextIndexed(stemp1, sizeof(stemp1), Settings.flag3.sleep_normal, kSleepMode), // SetOption60 - Enable normal sleep instead of dynamic sleep - sleep, loop_load_avg, MqttConnectCount()); - - for (uint32_t i = 1; i <= devices_present; i++) { -#ifdef USE_LIGHT - if ((LightDevice()) && (i >= LightDevice())) { - if (i == LightDevice()) { LightState(1); } // call it only once - } else { -#endif - ResponseAppend_P(PSTR(",\"%s\":\"%s\""), GetPowerDevice(stemp1, i, sizeof(stemp1), Settings.flag.device_index_enable), // SetOption26 - Switch between POWER or POWER1 - GetStateText(bitRead(power, i-1))); -#ifdef USE_SONOFF_IFAN - if (IsModuleIfan()) { - ResponseAppend_P(PSTR(",\"" D_CMND_FANSPEED "\":%d"), GetFanspeed()); - break; - } -#endif // USE_SONOFF_IFAN -#ifdef USE_LIGHT - } -#endif - } - - if (pwm_present) { - ResponseAppend_P(PSTR(",")); - MqttShowPWMState(); - } - - ResponseAppend_P(PSTR(",\"" D_JSON_WIFI "\":{\"" D_JSON_AP "\":%d,\"" D_JSON_SSID "\":\"%s\",\"" D_JSON_BSSID "\":\"%s\",\"" D_JSON_CHANNEL "\":%d,\"" D_JSON_RSSI "\":%d,\"" D_JSON_LINK_COUNT "\":%d,\"" D_JSON_DOWNTIME "\":\"%s\"}}"), - Settings.sta_active +1, Settings.sta_ssid[Settings.sta_active], WiFi.BSSIDstr().c_str(), WiFi.channel(), WifiGetRssiAsQuality(WiFi.RSSI()), WifiLinkCount(), WifiDowntime().c_str()); -} - -void MqttPublishTeleState(void) -{ - mqtt_data[0] = '\0'; - MqttShowState(); - MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_STATE), MQTT_TELE_RETAIN); -#ifdef USE_SCRIPT - RulesTeleperiod(); // Allow rule based HA messages -#endif // USE_SCRIPT -} - -bool MqttShowSensor(void) -{ - ResponseAppendTime(); - - int json_data_start = strlen(mqtt_data); - for (uint32_t i = 0; i < MAX_SWITCHES; i++) { -#ifdef USE_TM1638 - if ((pin[GPIO_SWT1 +i] < 99) || ((pin[GPIO_TM16CLK] < 99) && (pin[GPIO_TM16DIO] < 99) && (pin[GPIO_TM16STB] < 99))) { -#else - if (pin[GPIO_SWT1 +i] < 99) { -#endif // USE_TM1638 - bool swm = ((FOLLOW_INV == Settings.switchmode[i]) || (PUSHBUTTON_INV == Settings.switchmode[i]) || (PUSHBUTTONHOLD_INV == Settings.switchmode[i])); - ResponseAppend_P(PSTR(",\"" D_JSON_SWITCH "%d\":\"%s\""), i +1, GetStateText(swm ^ SwitchLastState(i))); - } - } - XsnsCall(FUNC_JSON_APPEND); - XdrvCall(FUNC_JSON_APPEND); - - bool json_data_available = (strlen(mqtt_data) - json_data_start); - if (strstr_P(mqtt_data, PSTR(D_JSON_PRESSURE)) != nullptr) { - ResponseAppend_P(PSTR(",\"" D_JSON_PRESSURE_UNIT "\":\"%s\""), PressureUnit().c_str()); - } - if (strstr_P(mqtt_data, PSTR(D_JSON_TEMPERATURE)) != nullptr) { - ResponseAppend_P(PSTR(",\"" D_JSON_TEMPERATURE_UNIT "\":\"%c\""), TempUnit()); - } - ResponseJsonEnd(); - - if (json_data_available) { XdrvCall(FUNC_SHOW_SENSOR); } - return json_data_available; -} - -void MqttPublishSensor(void) -{ - mqtt_data[0] = '\0'; - if (MqttShowSensor()) { - MqttPublishTeleSensor(); - } -} - -/********************************************************************************************/ - -void PerformEverySecond(void) -{ - uptime++; - - if (ntp_synced_message) { - // Moved here to fix syslog UDP exception 9 during RtcSecond - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("NTP: Drift %d, (" D_UTC_TIME ") %s, (" D_DST_TIME ") %s, (" D_STD_TIME ") %s"), - DriftTime(), GetTime(0).c_str(), GetTime(2).c_str(), GetTime(3).c_str()); - ntp_synced_message = false; - } - - if (POWER_CYCLE_TIME == uptime) { - UpdateQuickPowerCycle(false); - } - - if (BOOT_LOOP_TIME == uptime) { - RtcRebootReset(); - -#ifdef USE_DEEPSLEEP - if (!(DeepSleepEnabled() && !Settings.flag3.bootcount_update)) { -#endif - Settings.bootcount++; // Moved to here to stop flash writes during start-up - AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_APPLICATION D_BOOT_COUNT " %d"), Settings.bootcount); -#ifdef USE_DEEPSLEEP - } -#endif - } - - if (seriallog_timer) { - seriallog_timer--; - if (!seriallog_timer) { - if (seriallog_level) { - AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_SERIAL_LOGGING_DISABLED)); - } - seriallog_level = 0; - } - } - - if (syslog_timer) { // Restore syslog level - syslog_timer--; - if (!syslog_timer) { - syslog_level = Settings.syslog_level; - if (Settings.syslog_level) { - AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_SYSLOG_LOGGING_REENABLED)); // Might trigger disable again (on purpose) - } - } - } - - ResetGlobalValues(); - - if (Settings.tele_period) { - if (tele_period >= 9999) { - if (!global_state.wifi_down) { - tele_period = 0; // Allow teleperiod once wifi is connected - } - } else { - tele_period++; - if (tele_period >= Settings.tele_period) { - tele_period = 0; - - MqttPublishTeleState(); - - mqtt_data[0] = '\0'; - if (MqttShowSensor()) { - MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); // CMND_SENSORRETAIN -#if defined(USE_RULES) || defined(USE_SCRIPT) - RulesTeleperiod(); // Allow rule based HA messages -#endif // USE_RULES - } - - XdrvCall(FUNC_AFTER_TELEPERIOD); - } - } - } -} - /*********************************************************************************************\ - * State loops + * Main \*********************************************************************************************/ -/*-------------------------------------------------------------------------------------------*\ - * Every 0.1 second -\*-------------------------------------------------------------------------------------------*/ - -void Every100mSeconds(void) -{ - // As the max amount of sleep = 250 mSec this loop will shift in time... - power_t power_now; - - if (latching_relay_pulse) { - latching_relay_pulse--; - if (!latching_relay_pulse) SetLatchingRelay(0, 0); - } - - for (uint32_t i = 0; i < MAX_PULSETIMERS; i++) { - if (pulse_timer[i] != 0L) { // Timer active? - if (TimeReached(pulse_timer[i])) { // Timer finished? - pulse_timer[i] = 0L; // Turn off this timer - ExecuteCommandPower(i +1, (POWER_ALL_OFF_PULSETIME_ON == Settings.poweronstate) ? POWER_ON : POWER_OFF, SRC_PULSETIMER); - } - } - } - - if (blink_mask) { - if (TimeReached(blink_timer)) { - SetNextTimeInterval(blink_timer, 100 * Settings.blinktime); - blink_counter--; - if (!blink_counter) { - StopAllPowerBlink(); - } else { - blink_power ^= 1; - power_now = (power & (POWER_MASK ^ blink_mask)) | ((blink_power) ? blink_mask : 0); - SetDevicePower(power_now, SRC_IGNORE); - } - } - } -} - -/*-------------------------------------------------------------------------------------------*\ - * Every 0.25 second -\*-------------------------------------------------------------------------------------------*/ - -void Every250mSeconds(void) -{ -// As the max amount of sleep = 250 mSec this loop should always be taken... - - uint32_t blinkinterval = 1; - - state_250mS++; - state_250mS &= 0x3; - - if (mqtt_cmnd_publish) mqtt_cmnd_publish--; // Clean up - - if (!Settings.flag.global_state) { // Problem blinkyblinky enabled - SetOption31 - Control link led blinking - if (global_state.data) { // Any problem - if (global_state.mqtt_down) { blinkinterval = 7; } // MQTT problem so blink every 2 seconds (slowest) - if (global_state.wifi_down) { blinkinterval = 3; } // Wifi problem so blink every second (slow) - blinks = 201; // Allow only a single blink in case the problem is solved - } - } - if (blinks || restart_flag || ota_state_flag) { - if (restart_flag || ota_state_flag) { // Overrule blinks and keep led lit - blinkstate = true; // Stay lit - } else { - blinkspeed--; - if (!blinkspeed) { - blinkspeed = blinkinterval; // Set interval to 0.2 (default), 1 or 2 seconds - blinkstate ^= 1; // Blink - } - } - if ((!(Settings.ledstate &0x08)) && ((Settings.ledstate &0x06) || (blinks > 200) || (blinkstate))) { - SetLedLink(blinkstate); // Set led on or off - } - if (!blinkstate) { - blinks--; - if (200 == blinks) blinks = 0; // Disable blink - } - } - if (Settings.ledstate &1 && (pin[GPIO_LEDLNK] < 99 || !(blinks || restart_flag || ota_state_flag)) ) { - bool tstate = power & Settings.ledmask; - if ((SONOFF_TOUCH == my_module_type) || (SONOFF_T11 == my_module_type) || (SONOFF_T12 == my_module_type) || (SONOFF_T13 == my_module_type)) { - tstate = (!power) ? 1 : 0; // As requested invert signal for Touch devices to find them in the dark - } - SetLedPower(tstate); - } - -/*-------------------------------------------------------------------------------------------*\ - * Every second at 0.25 second interval -\*-------------------------------------------------------------------------------------------*/ - - switch (state_250mS) { - case 0: // Every x.0 second - if (ota_state_flag && BACKLOG_EMPTY) { - ota_state_flag--; - if (2 == ota_state_flag) { - ota_url = Settings.ota_url; - RtcSettings.ota_loader = 0; // Try requested image first - ota_retry_counter = OTA_ATTEMPTS; - ESPhttpUpdate.rebootOnUpdate(false); - SettingsSave(1); // Free flash for OTA update - } - if (ota_state_flag <= 0) { -#ifdef USE_WEBSERVER - if (Settings.webserver) StopWebserver(); -#endif // USE_WEBSERVER -#ifdef USE_ARILUX_RF - AriluxRfDisable(); // Prevent restart exception on Arilux Interrupt routine -#endif // USE_ARILUX_RF - ota_state_flag = 92; - ota_result = 0; - ota_retry_counter--; - if (ota_retry_counter) { - strlcpy(mqtt_data, GetOtaUrl(log_data, sizeof(log_data)), sizeof(mqtt_data)); -#ifndef FIRMWARE_MINIMAL - if (RtcSettings.ota_loader) { - char *bch = strrchr(mqtt_data, '/'); // Only consider filename after last backslash prevent change of urls having "-" in it - char *pch = strrchr((bch != nullptr) ? bch : mqtt_data, '-'); // Change from filename-DE.bin into filename-minimal.bin - char *ech = strrchr((bch != nullptr) ? bch : mqtt_data, '.'); // Change from filename.bin into filename-minimal.bin - if (!pch) { pch = ech; } - if (pch) { - mqtt_data[pch - mqtt_data] = '\0'; - char *ech = strrchr(Settings.ota_url, '.'); // Change from filename.bin into filename-minimal.bin - snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s-" D_JSON_MINIMAL "%s"), mqtt_data, ech); // Minimal filename must be filename-minimal - } - } -#endif // FIRMWARE_MINIMAL - AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_UPLOAD "%s"), mqtt_data); -#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_0) || defined(ARDUINO_ESP8266_RELEASE_2_4_1) || defined(ARDUINO_ESP8266_RELEASE_2_4_2) - ota_result = (HTTP_UPDATE_FAILED != ESPhttpUpdate.update(mqtt_data)); -#else - // If using core stage or 2.5.0+ the syntax has changed - WiFiClient OTAclient; - ota_result = (HTTP_UPDATE_FAILED != ESPhttpUpdate.update(OTAclient, mqtt_data)); -#endif - if (!ota_result) { -#ifndef FIRMWARE_MINIMAL - int ota_error = ESPhttpUpdate.getLastError(); - DEBUG_CORE_LOG(PSTR("OTA: Error %d"), ota_error); - if ((HTTP_UE_TOO_LESS_SPACE == ota_error) || (HTTP_UE_BIN_FOR_WRONG_FLASH == ota_error)) { - RtcSettings.ota_loader = 1; // Try minimal image next - } -#endif // FIRMWARE_MINIMAL - ota_state_flag = 2; // Upgrade failed - retry - } - } - } - if (90 == ota_state_flag) { // Allow MQTT to reconnect - ota_state_flag = 0; - if (ota_result) { -// SetFlashModeDout(); // Force DOUT for both ESP8266 and ESP8285 - Response_P(PSTR(D_JSON_SUCCESSFUL ". " D_JSON_RESTARTING)); - } else { - Response_P(PSTR(D_JSON_FAILED " %s"), ESPhttpUpdate.getLastErrorString().c_str()); - } - restart_flag = 2; // Restart anyway to keep memory clean webserver - MqttPublishPrefixTopic_P(STAT, PSTR(D_CMND_UPGRADE)); - } - } - break; - case 1: // Every x.25 second - if (MidnightNow()) { - XsnsCall(FUNC_SAVE_AT_MIDNIGHT); - } - if (save_data_counter && BACKLOG_EMPTY) { - save_data_counter--; - if (save_data_counter <= 0) { - if (Settings.flag.save_state) { // SetOption0 - Save power state and use after restart - power_t mask = POWER_MASK; - for (uint32_t i = 0; i < MAX_PULSETIMERS; i++) { - if ((Settings.pulse_timer[i] > 0) && (Settings.pulse_timer[i] < 30)) { // 3 seconds - mask &= ~(1 << i); - } - } - if (!((Settings.power &mask) == (power &mask))) { - Settings.power = power; - } - } else { - Settings.power = 0; - } - SettingsSave(0); - save_data_counter = Settings.save_data; - } - } - if (restart_flag && BACKLOG_EMPTY) { - if ((214 == restart_flag) || (215 == restart_flag) || (216 == restart_flag)) { - char storage_wifi[sizeof(Settings.sta_ssid) + - sizeof(Settings.sta_pwd)]; - char storage_mqtt[sizeof(Settings.mqtt_host) + - sizeof(Settings.mqtt_port) + - sizeof(Settings.mqtt_client) + - sizeof(Settings.mqtt_user) + - sizeof(Settings.mqtt_pwd) + - sizeof(Settings.mqtt_topic)]; - memcpy(storage_wifi, Settings.sta_ssid, sizeof(storage_wifi)); // Backup current SSIDs and Passwords - if (216 == restart_flag) { - memcpy(storage_mqtt, Settings.mqtt_host, sizeof(storage_mqtt)); // Backup mqtt host, port, client, username and password - } - if ((215 == restart_flag) || (216 == restart_flag)) { - SettingsErase(0); // Erase all flash from program end to end of physical flash - } - SettingsDefault(); - memcpy(Settings.sta_ssid, storage_wifi, sizeof(storage_wifi)); // Restore current SSIDs and Passwords - if (216 == restart_flag) { - memcpy(Settings.mqtt_host, storage_mqtt, sizeof(storage_mqtt)); // Restore the mqtt host, port, client, username and password - strlcpy(Settings.mqtt_client, MQTT_CLIENT_ID, sizeof(Settings.mqtt_client)); // Set client to default - } - restart_flag = 2; - } - else if (213 == restart_flag) { - SettingsSdkErase(); // Erase flash SDK parameters - restart_flag = 2; - } - else if (212 == restart_flag) { - SettingsErase(0); // Erase all flash from program end to end of physical flash - restart_flag = 211; - } - if (211 == restart_flag) { - SettingsDefault(); - restart_flag = 2; - } - if (2 == restart_flag) { - SettingsSaveAll(); - } - restart_flag--; - if (restart_flag <= 0) { - AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_RESTARTING)); - EspRestart(); - } - } - break; - case 2: // Every x.5 second - WifiCheck(wifi_state_flag); - wifi_state_flag = WIFI_RESTART; - break; - case 3: // Every x.75 second - if (!global_state.wifi_down) { MqttCheck(); } - break; - } -} - -#ifdef USE_ARDUINO_OTA -/*********************************************************************************************\ - * Allow updating via the Arduino OTA-protocol. - * - * - Once started disables current wifi clients and udp - * - Perform restart when done to re-init wifi clients -\*********************************************************************************************/ - -bool arduino_ota_triggered = false; -uint16_t arduino_ota_progress_dot_count = 0; - -void ArduinoOTAInit(void) -{ - ArduinoOTA.setPort(8266); - ArduinoOTA.setHostname(my_hostname); - if (Settings.web_password[0] !=0) { ArduinoOTA.setPassword(Settings.web_password); } - - ArduinoOTA.onStart([]() - { - SettingsSave(1); // Free flash for OTA update -#ifdef USE_WEBSERVER - if (Settings.webserver) { StopWebserver(); } -#endif // USE_WEBSERVER -#ifdef USE_ARILUX_RF - AriluxRfDisable(); // Prevent restart exception on Arilux Interrupt routine -#endif // USE_ARILUX_RF - if (Settings.flag.mqtt_enabled) { - MqttDisconnect(); // SetOption3 - Enable MQTT - } - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_UPLOAD "Arduino OTA " D_UPLOAD_STARTED)); - arduino_ota_triggered = true; - arduino_ota_progress_dot_count = 0; - delay(100); // Allow time for message xfer - }); - - ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) - { - if ((LOG_LEVEL_DEBUG <= seriallog_level)) { - arduino_ota_progress_dot_count++; - Serial.printf("."); - if (!(arduino_ota_progress_dot_count % 80)) { Serial.println(); } - } - }); - - ArduinoOTA.onError([](ota_error_t error) - { - /* - From ArduinoOTA.h: - typedef enum { OTA_AUTH_ERROR, OTA_BEGIN_ERROR, OTA_CONNECT_ERROR, OTA_RECEIVE_ERROR, OTA_END_ERROR } ota_error_t; - */ - char error_str[100]; - - if ((LOG_LEVEL_DEBUG <= seriallog_level) && arduino_ota_progress_dot_count) { Serial.println(); } - switch (error) { - case OTA_BEGIN_ERROR: strncpy_P(error_str, PSTR(D_UPLOAD_ERR_2), sizeof(error_str)); break; - case OTA_RECEIVE_ERROR: strncpy_P(error_str, PSTR(D_UPLOAD_ERR_5), sizeof(error_str)); break; - case OTA_END_ERROR: strncpy_P(error_str, PSTR(D_UPLOAD_ERR_7), sizeof(error_str)); break; - default: - snprintf_P(error_str, sizeof(error_str), PSTR(D_UPLOAD_ERROR_CODE " %d"), error); - } - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_UPLOAD "Arduino OTA %s. " D_RESTARTING), error_str); - EspRestart(); - }); - - ArduinoOTA.onEnd([]() - { - if ((LOG_LEVEL_DEBUG <= seriallog_level)) { Serial.println(); } - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_UPLOAD "Arduino OTA " D_SUCCESSFUL ". " D_RESTARTING)); - EspRestart(); - }); - - ArduinoOTA.begin(); - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_UPLOAD "Arduino OTA " D_ENABLED " " D_PORT " 8266")); -} -#endif // USE_ARDUINO_OTA - -/********************************************************************************************/ - -void SerialInput(void) -{ - while (Serial.available()) { -// yield(); - delay(0); - serial_in_byte = Serial.read(); - -/*-------------------------------------------------------------------------------------------*\ - * Sonoff dual and ch4 19200 baud serial interface -\*-------------------------------------------------------------------------------------------*/ - if ((SONOFF_DUAL == my_module_type) || (CH4 == my_module_type)) { - serial_in_byte = ButtonSerial(serial_in_byte); - } - -/*-------------------------------------------------------------------------------------------*/ - - if (XdrvCall(FUNC_SERIAL)) { - serial_in_byte_counter = 0; - Serial.flush(); - return; - } - -/*-------------------------------------------------------------------------------------------*/ - - if (serial_in_byte > 127 && !Settings.flag.mqtt_serial_raw) { // Discard binary data above 127 if no raw reception allowed - CMND_SERIALSEND3 - serial_in_byte_counter = 0; - Serial.flush(); - return; - } - if (!Settings.flag.mqtt_serial) { // SerialSend active - CMND_SERIALSEND and CMND_SERIALLOG - if (isprint(serial_in_byte)) { // Any char between 32 and 127 - 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 { - serial_in_byte_counter = 0; - } - } - } else { - if (serial_in_byte || Settings.flag.mqtt_serial_raw) { // Any char between 1 and 127 or any char (0 - 255) - CMND_SERIALSEND3 - if ((serial_in_byte_counter < INPUT_BUFFER_SIZE -1) && // Add char to string if it still fits and ... - ((isprint(serial_in_byte) && (128 == Settings.serial_delimiter)) || // Any char between 32 and 127 - ((serial_in_byte != Settings.serial_delimiter) && (128 != Settings.serial_delimiter)) || // Any char between 1 and 127 and not being delimiter - Settings.flag.mqtt_serial_raw)) { // Any char between 0 and 255 - CMND_SERIALSEND3 - serial_in_buffer[serial_in_byte_counter++] = serial_in_byte; - serial_polling_window = millis(); - } else { - serial_polling_window = 0; // Reception done - send mqtt - break; - } - } - } - -#ifdef USE_SONOFF_SC -/*-------------------------------------------------------------------------------------------*\ - * Sonoff SC 19200 baud serial interface -\*-------------------------------------------------------------------------------------------*/ - if (SONOFF_SC == my_module_type) { - if (serial_in_byte == '\x1B') { // Sonoff SC status from ATMEGA328P - serial_in_buffer[serial_in_byte_counter] = 0; // Serial data completed - SonoffScSerialInput(serial_in_buffer); - serial_in_byte_counter = 0; - Serial.flush(); - return; - } - } else -#endif // USE_SONOFF_SC -/*-------------------------------------------------------------------------------------------*/ - - if (!Settings.flag.mqtt_serial && (serial_in_byte == '\n')) { // CMND_SERIALSEND and CMND_SERIALLOG - serial_in_buffer[serial_in_byte_counter] = 0; // Serial data completed - seriallog_level = (Settings.seriallog_level < LOG_LEVEL_INFO) ? (uint8_t)LOG_LEVEL_INFO : Settings.seriallog_level; - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_COMMAND "%s"), serial_in_buffer); - ExecuteCommand(serial_in_buffer, SRC_SERIAL); - serial_in_byte_counter = 0; - serial_polling_window = 0; - Serial.flush(); - return; - } - } - - if (Settings.flag.mqtt_serial && serial_in_byte_counter && (millis() > (serial_polling_window + SERIAL_POLLING))) { // CMND_SERIALSEND and CMND_SERIALLOG - serial_in_buffer[serial_in_byte_counter] = 0; // Serial data completed - char hex_char[(serial_in_byte_counter * 2) + 2]; - Response_P(PSTR("{\"" D_JSON_SERIALRECEIVED "\":\"%s\"}"), - (Settings.flag.mqtt_serial_raw) ? ToHex_P((unsigned char*)serial_in_buffer, serial_in_byte_counter, hex_char, sizeof(hex_char)) : serial_in_buffer); - MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_SERIALRECEIVED)); - XdrvRulesProcess(); - serial_in_byte_counter = 0; - } -} - -/********************************************************************************************/ - -void GpioInit(void) -{ - uint32_t mpin; - - if (!ValidModule(Settings.module)) { - uint32_t module = MODULE; - if (!ValidModule(MODULE)) { module = SONOFF_BASIC; } - Settings.module = module; - Settings.last_module = module; - } - SetModuleType(); - - if (Settings.module != Settings.last_module) { - baudrate = APP_BAUDRATE; - } - - for (uint32_t i = 0; i < sizeof(Settings.user_template.gp); i++) { - if ((Settings.user_template.gp.io[i] >= GPIO_SENSOR_END) && (Settings.user_template.gp.io[i] < GPIO_USER)) { - Settings.user_template.gp.io[i] = GPIO_USER; // Fix not supported sensor ids in template - } - } - - myio def_gp; - ModuleGpios(&def_gp); - for (uint32_t i = 0; i < sizeof(Settings.my_gp); i++) { - if ((Settings.my_gp.io[i] >= GPIO_SENSOR_END) && (Settings.my_gp.io[i] < GPIO_USER)) { - Settings.my_gp.io[i] = GPIO_NONE; // Fix not supported sensor ids in module - } - else if (Settings.my_gp.io[i] > GPIO_NONE) { - my_module.io[i] = Settings.my_gp.io[i]; // Set User selected Module sensors - } - if ((def_gp.io[i] > GPIO_NONE) && (def_gp.io[i] < GPIO_USER)) { - my_module.io[i] = def_gp.io[i]; // Force Template override - } - } - if ((Settings.my_adc0 >= ADC0_END) && (Settings.my_adc0 < ADC0_USER)) { - Settings.my_adc0 = ADC0_NONE; // Fix not supported sensor ids in module - } - else if (Settings.my_adc0 > ADC0_NONE) { - my_adc0 = Settings.my_adc0; // Set User selected Module sensors - } - my_module_flag = ModuleFlag(); - uint32_t template_adc0 = my_module_flag.data &15; - if ((template_adc0 > ADC0_NONE) && (template_adc0 < ADC0_USER)) { - my_adc0 = template_adc0; // Force Template override - } - - for (uint32_t i = 0; i < GPIO_MAX; i++) { - pin[i] = 99; - } - for (uint32_t i = 0; i < sizeof(my_module.io); i++) { - mpin = ValidPin(i, my_module.io[i]); - - DEBUG_CORE_LOG(PSTR("INI: gpio pin %d, mpin %d"), i, mpin); - - if (mpin) { - XdrvMailbox.index = mpin; - XdrvMailbox.payload = i; - - if ((mpin >= GPIO_SWT1_NP) && (mpin < (GPIO_SWT1_NP + MAX_SWITCHES))) { - SwitchPullupFlag(mpin - GPIO_SWT1_NP); - mpin -= (GPIO_SWT1_NP - GPIO_SWT1); - } - else if ((mpin >= GPIO_KEY1_NP) && (mpin < (GPIO_KEY1_NP + MAX_KEYS))) { - ButtonPullupFlag(mpin - GPIO_KEY1_NP); // 0 .. 3 - mpin -= (GPIO_KEY1_NP - GPIO_KEY1); - } - else if ((mpin >= GPIO_KEY1_INV) && (mpin < (GPIO_KEY1_INV + MAX_KEYS))) { - ButtonInvertFlag(mpin - GPIO_KEY1_INV); // 0 .. 3 - mpin -= (GPIO_KEY1_INV - GPIO_KEY1); - } - else if ((mpin >= GPIO_KEY1_INV_NP) && (mpin < (GPIO_KEY1_INV_NP + MAX_KEYS))) { - ButtonPullupFlag(mpin - GPIO_KEY1_INV_NP); // 0 .. 3 - ButtonInvertFlag(mpin - GPIO_KEY1_INV_NP); // 0 .. 3 - mpin -= (GPIO_KEY1_INV_NP - GPIO_KEY1); - } - else if ((mpin >= GPIO_REL1_INV) && (mpin < (GPIO_REL1_INV + MAX_RELAYS))) { - bitSet(rel_inverted, mpin - GPIO_REL1_INV); - mpin -= (GPIO_REL1_INV - GPIO_REL1); - } - else if ((mpin >= GPIO_LED1_INV) && (mpin < (GPIO_LED1_INV + MAX_LEDS))) { - bitSet(led_inverted, mpin - GPIO_LED1_INV); - mpin -= (GPIO_LED1_INV - GPIO_LED1); - } - else if (mpin == GPIO_LEDLNK_INV) { - ledlnk_inverted = 1; - mpin -= (GPIO_LEDLNK_INV - GPIO_LEDLNK); - } - else if ((mpin >= GPIO_PWM1_INV) && (mpin < (GPIO_PWM1_INV + MAX_PWMS))) { - bitSet(pwm_inverted, mpin - GPIO_PWM1_INV); - mpin -= (GPIO_PWM1_INV - GPIO_PWM1); - } - else if (XdrvCall(FUNC_PIN_STATE)) { - mpin = XdrvMailbox.index; - } - else if (XsnsCall(FUNC_PIN_STATE)) { - mpin = XdrvMailbox.index; - }; - } - if (mpin) pin[mpin] = i; - } - - if ((2 == pin[GPIO_TXD]) || (H801 == my_module_type)) { Serial.set_tx(2); } - - analogWriteRange(Settings.pwm_range); // Default is 1023 (Arduino.h) - analogWriteFreq(Settings.pwm_frequency); // Default is 1000 (core_esp8266_wiring_pwm.c) - -#ifdef USE_SPI - spi_flg = ((((pin[GPIO_SPI_CS] < 99) && (pin[GPIO_SPI_CS] > 14)) || (pin[GPIO_SPI_CS] < 12)) || (((pin[GPIO_SPI_DC] < 99) && (pin[GPIO_SPI_DC] > 14)) || (pin[GPIO_SPI_DC] < 12))); - if (spi_flg) { - for (uint32_t i = 0; i < GPIO_MAX; i++) { - if ((pin[i] >= 12) && (pin[i] <=14)) pin[i] = 99; - } - my_module.io[12] = GPIO_SPI_MISO; - pin[GPIO_SPI_MISO] = 12; - my_module.io[13] = GPIO_SPI_MOSI; - pin[GPIO_SPI_MOSI] = 13; - my_module.io[14] = GPIO_SPI_CLK; - pin[GPIO_SPI_CLK] = 14; - } - soft_spi_flg = ((pin[GPIO_SSPI_CS] < 99) && (pin[GPIO_SSPI_SCLK] < 99) && ((pin[GPIO_SSPI_MOSI] < 99) || (pin[GPIO_SSPI_MOSI] < 99))); -#endif // USE_SPI - -#ifdef USE_I2C - i2c_flg = ((pin[GPIO_I2C_SCL] < 99) && (pin[GPIO_I2C_SDA] < 99)); - if (i2c_flg) { - Wire.begin(pin[GPIO_I2C_SDA], pin[GPIO_I2C_SCL]); - } -#endif // USE_I2C - - devices_present = 0; - light_type = LT_BASIC; // Use basic PWM control if SetOption15 = 0 - if (XdrvCall(FUNC_MODULE_INIT)) { - // Serviced - } - else if (YTF_IR_BRIDGE == my_module_type) { - ClaimSerial(); // Stop serial loopback mode -// devices_present = 1; - } - else if (SONOFF_DUAL == my_module_type) { - Settings.flag.mqtt_serial = 0; // CMND_SERIALSEND and CMND_SERIALLOG - devices_present = 2; - baudrate = 19200; - } - else if (CH4 == my_module_type) { - Settings.flag.mqtt_serial = 0; // CMND_SERIALSEND and CMND_SERIALLOG - devices_present = 4; - baudrate = 19200; - } -#ifdef USE_SONOFF_SC - else if (SONOFF_SC == my_module_type) { - Settings.flag.mqtt_serial = 0; // CMND_SERIALSEND and CMND_SERIALLOG - devices_present = 0; - baudrate = 19200; - } -#endif // USE_SONOFF_SC - - if (!light_type) { - for (uint32_t i = 0; i < MAX_PWMS; i++) { // Basic PWM control only - if (pin[GPIO_PWM1 +i] < 99) { - pwm_present = true; - pinMode(pin[GPIO_PWM1 +i], OUTPUT); - analogWrite(pin[GPIO_PWM1 +i], bitRead(pwm_inverted, i) ? Settings.pwm_range - Settings.pwm_value[i] : Settings.pwm_value[i]); - } - } - } - for (uint32_t i = 0; i < MAX_RELAYS; i++) { - if (pin[GPIO_REL1 +i] < 99) { - pinMode(pin[GPIO_REL1 +i], OUTPUT); - devices_present++; - if (EXS_RELAY == my_module_type) { - digitalWrite(pin[GPIO_REL1 +i], bitRead(rel_inverted, i) ? 1 : 0); - if (i &1) { devices_present--; } - } - } - } - - for (uint32_t i = 0; i < MAX_LEDS; i++) { - if (pin[GPIO_LED1 +i] < 99) { -#ifdef USE_ARILUX_RF - if ((3 == i) && (leds_present < 2) && (99 == pin[GPIO_ARIRFSEL])) { - pin[GPIO_ARIRFSEL] = pin[GPIO_LED4]; // Legacy support where LED4 was Arilux RF enable - pin[GPIO_LED4] = 99; - } else { -#endif - pinMode(pin[GPIO_LED1 +i], OUTPUT); - leds_present++; - digitalWrite(pin[GPIO_LED1 +i], bitRead(led_inverted, i)); -#ifdef USE_ARILUX_RF - } -#endif - } - } - if (pin[GPIO_LEDLNK] < 99) { - pinMode(pin[GPIO_LEDLNK], OUTPUT); - digitalWrite(pin[GPIO_LEDLNK], ledlnk_inverted); - } - - ButtonInit(); - SwitchInit(); -#ifdef ROTARY_V1 - RotaryInit(); -#endif - - SetLedPower(Settings.ledstate &8); - SetLedLink(Settings.ledstate &8); - - XdrvCall(FUNC_PRE_INIT); -} void setup(void) { @@ -1593,6 +278,7 @@ void setup(void) snprintf_P(my_hostname, sizeof(my_hostname)-1, Settings.hostname); } + GetEspHardwareType(); GpioInit(); SetSerialBaudrate(baudrate); diff --git a/tasmota/tasmota_version.h b/tasmota/tasmota_version.h index 7484f5bc9..54e4c165f 100644 --- a/tasmota/tasmota_version.h +++ b/tasmota/tasmota_version.h @@ -20,6 +20,6 @@ #ifndef _TASMOTA_VERSION_H_ #define _TASMOTA_VERSION_H_ -const uint32_t VERSION = 0x07010001; +const uint32_t VERSION = 0x07010101; #endif // _TASMOTA_VERSION_H_ diff --git a/tasmota/xdrv_04_light.ino b/tasmota/xdrv_04_light.ino index b50d8e4df..7f1e66fc6 100644 --- a/tasmota/xdrv_04_light.ino +++ b/tasmota/xdrv_04_light.ino @@ -1510,33 +1510,6 @@ void LightPreparePower(power_t channels = 0xFFFFFFFF) { // 1 = only RGB, 2 = LightState(0); } -void LightWheel(uint8_t wheel_pos) -{ - wheel_pos = 255 - wheel_pos; - if (wheel_pos < 85) { - Light.entry_color[0] = 255 - wheel_pos * 3; - Light.entry_color[1] = 0; - Light.entry_color[2] = wheel_pos * 3; - } else if (wheel_pos < 170) { - wheel_pos -= 85; - Light.entry_color[0] = 0; - Light.entry_color[1] = wheel_pos * 3; - Light.entry_color[2] = 255 - wheel_pos * 3; - } else { - wheel_pos -= 170; - Light.entry_color[0] = wheel_pos * 3; - Light.entry_color[1] = 255 - wheel_pos * 3; - Light.entry_color[2] = 0; - } - Light.entry_color[3] = 0; - Light.entry_color[4] = 0; - float dimmer = 100 / (float)Settings.light_dimmer; - for (uint32_t i = 0; i < LST_RGB; i++) { - float temp = (float)Light.entry_color[i] / dimmer + 0.5f; - Light.entry_color[i] = (uint8_t)temp; - } -} - void LightCycleColor(int8_t direction) { if (Light.strip_timer_counter % (Settings.light_speed * 2)) { @@ -1547,12 +1520,17 @@ void LightCycleColor(int8_t direction) if (Light.random == Light.wheel) { Light.random = random(255); } - Light.wheel += (Light.random < Light.wheel) ? -1 : 1; - } else { - Light.wheel += direction; + direction = (Light.random < Light.wheel) ? -1 : 1; } - LightWheel(Light.wheel); - memcpy(Light.new_color, Light.entry_color, sizeof(Light.new_color)); + Light.wheel += direction; + uint16_t hue = changeUIntScale(Light.wheel, 0, 255, 0, 359); // Scale to hue to keep amount of steps low (max 255 instead of 359) + +// AddLog_P2(LOG_LEVEL_DEBUG, PSTR("DBG: random %d, wheel %d, hue %d"), Light.random, Light.wheel, hue); + + uint8_t sat; + light_state.getHSB(nullptr, &sat, nullptr); // Allow user control over Saturation + light_state.setHS(hue, sat); + light_controller.calcLevels(Light.new_color); } void LightSetPower(void) @@ -1593,6 +1571,7 @@ void LightAnimate(void) { uint8_t cur_col[LST_MAX]; uint16_t light_still_on = 0; + bool power_off = false; Light.strip_timer_counter++; if (!Light.power) { // All channels powered off @@ -1600,6 +1579,9 @@ void LightAnimate(void) if (!Light.fade_running) { sleep = Settings.sleep; } + if (Settings.light_scheme >= LS_MAX) { + power_off = true; + } } else { if (Settings.sleep > PWM_MAX_SLEEP) { sleep = PWM_MAX_SLEEP; // set a maxumum value of 50 milliseconds to ensure that animations are smooth @@ -1651,7 +1633,7 @@ void LightAnimate(void) } } - if (Settings.light_scheme < LS_MAX) { // exclude WS281X Neopixel + if ((Settings.light_scheme < LS_MAX) || power_off) { // exclude WS281X Neopixel schemes // Apply power modifiers to Light.new_color LightApplyPower(Light.new_color, Light.power); @@ -1736,7 +1718,7 @@ void LightAnimate(void) cur_col_10bits[i] = orig_col_10bits[Light.color_remap[i]]; } - if (!Settings.light_fade) { // no fade + if (!Settings.light_fade || power_off) { // no fade // record the current value for a future Fade memcpy(Light.fade_start_8, cur_col, sizeof(Light.fade_start_8)); memcpy(Light.fade_start_10, cur_col_10bits, sizeof(Light.fade_start_10)); diff --git a/tasmota/xdrv_10_rules.ino b/tasmota/xdrv_10_rules.ino index 4025ef5d4..441722662 100644 --- a/tasmota/xdrv_10_rules.ino +++ b/tasmota/xdrv_10_rules.ino @@ -452,6 +452,7 @@ bool RuleSetProcess(uint8_t rule_set, String &event_saved) RulesVarReplace(commands, F("%TIME%"), String(MinutesPastMidnight())); RulesVarReplace(commands, F("%UPTIME%"), String(MinutesUptime())); RulesVarReplace(commands, F("%TIMESTAMP%"), GetDateAndTime(DT_LOCAL)); + RulesVarReplace(commands, F("%TOPIC%"), Settings.mqtt_topic); #if defined(USE_TIMERS) && defined(USE_SUNRISE) RulesVarReplace(commands, F("%SUNRISE%"), String(SunMinutes(0))); RulesVarReplace(commands, F("%SUNSET%"), String(SunMinutes(1))); diff --git a/tasmota/xdrv_23_zigbee_9_impl.ino b/tasmota/xdrv_23_zigbee_9_impl.ino index 1cd3955ce..cb08eeaee 100644 --- a/tasmota/xdrv_23_zigbee_9_impl.ino +++ b/tasmota/xdrv_23_zigbee_9_impl.ino @@ -25,16 +25,8 @@ const uint32_t ZIGBEE_BUFFER_SIZE = 256; // Max ZNP frame is SOF+LEN+CMD1+CMD2+ const uint8_t ZIGBEE_SOF = 0xFE; const uint8_t ZIGBEE_SOF_ALT = 0xFF; -//#define Z_USE_SOFTWARE_SERIAL - -#ifdef Z_USE_SOFTWARE_SERIAL -#include -SoftwareSerial *ZigbeeSerial = nullptr; -#else #include TasmotaSerial *ZigbeeSerial = nullptr; -#endif - const char kZigbeeCommands[] PROGMEM = "|" D_CMND_ZIGBEEZNPSEND "|" D_CMND_ZIGBEE_PERMITJOIN "|" @@ -185,9 +177,7 @@ void ZigbeeInput(void) char hex_char[(zigbee_buffer->len() * 2) + 2]; ToHex_P((unsigned char*)zigbee_buffer->getBuffer(), zigbee_buffer->len(), hex_char, sizeof(hex_char)); -#ifndef Z_USE_SOFTWARE_SERIAL AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_ZIGBEE "Bytes follow_read_metric = %0d"), ZigbeeSerial->getLoopReadMetric()); -#endif // buffer received, now check integrity if (zigbee_buffer->len() != zigbee_frame_len) { // Len is not correct, log and reject frame @@ -224,21 +214,16 @@ void ZigbeeInit(void) zigbee.active = false; if ((pin[GPIO_ZIGBEE_RX] < 99) && (pin[GPIO_ZIGBEE_TX] < 99)) { AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("Zigbee: GPIOs Rx:%d Tx:%d"), pin[GPIO_ZIGBEE_RX], pin[GPIO_ZIGBEE_TX]); -#ifdef Z_USE_SOFTWARE_SERIAL - ZigbeeSerial = new SoftwareSerial(); - ZigbeeSerial->begin(115200, pin[GPIO_ZIGBEE_RX], pin[GPIO_ZIGBEE_TX], SWSERIAL_8N1, false, 256); // ZNP is 115200, RTS/CTS (ignored), 8N1 - ZigbeeSerial->enableIntTx(false); - zigbee_buffer = new SBuffer(ZIGBEE_BUFFER_SIZE); -#else - ZigbeeSerial = new TasmotaSerial(pin[GPIO_ZIGBEE_RX], pin[GPIO_ZIGBEE_TX], 0, 0, 256); // set a receive buffer of 256 bytes + // if seriallog_level is 0, we allow GPIO 13/15 to switch to Hardware Serial + ZigbeeSerial = new TasmotaSerial(pin[GPIO_ZIGBEE_RX], pin[GPIO_ZIGBEE_TX], seriallog_level ? 1 : 2, 0, 256); // set a receive buffer of 256 bytes ZigbeeSerial->begin(115200); if (ZigbeeSerial->hardwareSerial()) { ClaimSerial(); - zigbee_buffer = new PreAllocatedSBuffer(sizeof(serial_in_buffer), serial_in_buffer); + uint32_t aligned_buffer = ((uint32_t)serial_in_buffer + 3) & ~3; + zigbee_buffer = new PreAllocatedSBuffer(sizeof(serial_in_buffer) - 3, (char*) aligned_buffer); } else { zigbee_buffer = new SBuffer(ZIGBEE_BUFFER_SIZE); } -#endif zigbee.active = true; zigbee.init_phase = true; // start the state machine zigbee.state_machine = true; // start the state machine diff --git a/tasmota/xdrv_27_shutter.ino b/tasmota/xdrv_27_shutter.ino index 3c3039dda..f47b53376 100644 --- a/tasmota/xdrv_27_shutter.ino +++ b/tasmota/xdrv_27_shutter.ino @@ -37,10 +37,13 @@ #define D_CMND_SHUTTER_INVERT "Invert" #define D_CMND_SHUTTER_CLIBRATION "Calibration" #define D_CMND_SHUTTER_MOTORDELAY "MotorDelay" +#define D_CMND_SHUTTER_FREQUENCY "Frequency" #define D_SHUTTER "SHUTTER" -const uint16_t MOTOR_STOP_TIME = 500; // in mS +const uint16_t MOTOR_STOP_TIME = 500; // in mS + + uint8_t calibrate_pos[6] = {0,30,50,70,90,100}; uint16_t messwerte[5] = {30,50,70,90,100}; @@ -51,12 +54,13 @@ const char kShutterCommands[] PROGMEM = D_PRFX_SHUTTER "|" D_CMND_SHUTTER_OPEN "|" D_CMND_SHUTTER_CLOSE "|" D_CMND_SHUTTER_STOP "|" D_CMND_SHUTTER_POSITION "|" D_CMND_SHUTTER_OPENTIME "|" D_CMND_SHUTTER_CLOSETIME "|" D_CMND_SHUTTER_RELAY "|" D_CMND_SHUTTER_SETHALFWAY "|" D_CMND_SHUTTER_SETCLOSE "|" D_CMND_SHUTTER_INVERT "|" D_CMND_SHUTTER_CLIBRATION "|" - D_CMND_SHUTTER_MOTORDELAY; + D_CMND_SHUTTER_MOTORDELAY "|" D_CMND_SHUTTER_FREQUENCY; void (* const ShutterCommand[])(void) PROGMEM = { &CmndShutterOpen, &CmndShutterClose, &CmndShutterStop, &CmndShutterPosition, &CmndShutterOpenTime, &CmndShutterCloseTime, &CmndShutterRelay, - &CmndShutterSetHalfway, &CmndShutterSetClose, &CmndShutterInvert, &CmndShutterCalibration , &CmndShutterMotorDelay}; + &CmndShutterSetHalfway, &CmndShutterSetClose, &CmndShutterInvert, &CmndShutterCalibration , &CmndShutterMotorDelay, + &CmndShutterFrequency}; const char JSON_SHUTTER_POS[] PROGMEM = "\"" D_PRFX_SHUTTER "%d\":{\"Position\":%d,\"direction\":%d}"; @@ -68,23 +72,24 @@ struct SHUTTER { power_t mask = 0; // bit mask with 11 at the position of relays that belong to at least ONE shutter power_t old_power = 0; // preserve old bitmask for power to extract the relay that changes. power_t switched_relay = 0; // bitmatrix that contain the relays that was lastly changed. - uint32_t time[MAX_SHUTTERS]; + uint32_t time[MAX_SHUTTERS]; // operating time of the shutter in 0.05sec int32_t open_max[MAX_SHUTTERS]; // max value on maximum open calculated int32_t target_position[MAX_SHUTTERS]; // position to go to - int32_t start_position[MAX_SHUTTERS]; + int32_t start_position[MAX_SHUTTERS]; // position before a movement is started. init at start int32_t real_position[MAX_SHUTTERS]; // value between 0 and Shutter.open_max - uint16_t open_time[MAX_SHUTTERS]; // duration to open the shutter - uint16_t close_time[MAX_SHUTTERS]; // duration to close the shutter + uint16_t open_time[MAX_SHUTTERS]; // duration to open the shutter. 112 = 11.2sec + uint16_t close_time[MAX_SHUTTERS]; // duration to close the shutter. 112 = 11.2sec uint16_t close_velocity[MAX_SHUTTERS]; // in relation to open velocity. higher value = faster - uint16_t operations[MAX_SHUTTERS]; int8_t direction[MAX_SHUTTERS]; // 1 == UP , 0 == stop; -1 == down uint8_t mode = 0; // operation mode definition. see enum type above SHT_OFF_OPEN__OFF_CLOSE, SHT_OFF_ON__OPEN_CLOSE, SHT_PULSE_OPEN__PULSE_CLOSE - uint8_t motordelay[MAX_SHUTTERS]; // initial motorstarttime in 0.05sec. + uint8_t motordelay[MAX_SHUTTERS]; // initial motorstarttime in 0.05sec. + uint16_t pwm_frequency; // frequency of PWN for stepper motors + uint16_t max_pwm_frequency = 1000; } Shutter; void ShutterRtc50mS(void) { - for (uint32_t i = 0; i < MAX_SHUTTERS; i++) { + for (uint32_t i = 0; i < shutters_present; i++) { Shutter.time[i]++; } } @@ -189,6 +194,11 @@ void ShutterInit(void) } } else { Shutter.mode = SHT_OFF_ON__OPEN_CLOSE; + if (pin[GPIO_PWM1 ]+i < 99) { + Shutter.pwm_frequency = 0; + analogWriteFreq(Shutter.pwm_frequency); + analogWrite(pin[GPIO_PWM1]+i, 50); + } } TickerShutter.attach_ms(50, ShutterRtc50mS ); @@ -237,45 +247,32 @@ void ShutterUpdatePosition(void) { char scommand[CMDSZ]; char stopic[TOPSZ]; + char stemp2[10]; for (uint32_t i = 0; i < shutters_present; i++) { if (Shutter.direction[i] != 0) { //char stemp1[20]; + + // frequency start at 0. Stepper will start moving with first change of the Speed + // Counter should be initiated to 0 to count movement. + // 0..1000 in step 100 = 10 steps with 0.05 sec = 0.5sec total ramp time from start to + // full speed. + if (pin[GPIO_PWM1]+i < 99 && Shutter.pwm_frequency != Shutter.max_pwm_frequency) { + Shutter.pwm_frequency += Shutter.max_pwm_frequency/20; + Shutter.pwm_frequency = (Shutter.pwm_frequency > Shutter.max_pwm_frequency ? Shutter.max_pwm_frequency : Shutter.pwm_frequency); + analogWriteFreq(Shutter.pwm_frequency); + analogWrite(pin[GPIO_PWM1]+i, 50); + } + Shutter.real_position[i] = Shutter.start_position[i] + ( (Shutter.time[i] - Shutter.motordelay[i]) * (Shutter.direction[i] > 0 ? 100 : -Shutter.close_velocity[i])); // avoid real position leaving the boundaries. Shutter.real_position[i] = Shutter.real_position[i] < 0 ? 0 : (Shutter.real_position[i] > Shutter.open_max[i] ? Shutter.open_max[i] : Shutter.real_position[i]) ; - // Add additional runtime, if shutter did not reach the endstop for some time. - if (Shutter.target_position[i] == Shutter.real_position[i] && Shutter.target_position[i] == 0) { - // for every operation add 5x50ms = 250ms to stop position - //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Adding additional runtime")); - Shutter.real_position[i] += 500 * Shutter.operations[i] ; - Shutter.operations[i] = 0; - } + if (Shutter.real_position[i] * Shutter.direction[i] >= Shutter.target_position[i] * Shutter.direction[i] ) { // calculate relay number responsible for current movement. //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Stop Condition detected: real: %d, Target: %d, direction: %d"),Shutter.real_position[i], Shutter.target_position[i],Shutter.direction[i]); uint8_t cur_relay = Settings.shutter_startrelay[i] + (Shutter.direction[i] == 1 ? 0 : 1) ; - char stemp2[10]; - - Settings.shutter_position[i] = ShutterRealToPercentPosition(Shutter.real_position[i], i); - //Settings.shutter_position[i] = Settings.shuttercoeff[2][i] * 5 > Shutter.real_position[i] ? (Shutter.real_position[i] * 10 / Settings.shuttercoeff[2][i] + 4)/10 : ((Shutter.real_position[i]-Settings.shuttercoeff[0,i]) *10 / Settings.shuttercoeff[1][i] +4) / 10; - - if (0 < Settings.shutter_position[i] && Settings.shutter_position[i] < 100) { - Shutter.operations[i]++; - } else { - Shutter.operations[i] = 0; - } - - dtostrfd((float)Shutter.time[i] / 20, 1, stemp2); - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Shutter %d: Real Pos. %d, Stoppos: %ld, relay: %d, direction %d, pulsetimer: %d, rtcshutter: %s [s], operationtime %d"), i, Shutter.real_position[i], Settings.shutter_position[i], cur_relay -1, Shutter.direction[i], Settings.pulse_timer[cur_relay -1], stemp2, Shutter.operations[i]); - Shutter.start_position[i] = Shutter.real_position[i]; - - // sending MQTT result to broker - snprintf_P(scommand, sizeof(scommand),PSTR(D_SHUTTER "%d"), i+1); - GetTopic_P(stopic, STAT, mqtt_topic, scommand); - Response_P("%d", Settings.shutter_invert[i] ? 100 - Settings.shutter_position[i]: Settings.shutter_position[i]); - MqttPublish(stopic, Settings.flag.mqtt_power_retain); // CMND_POWERRETAIN switch (Shutter.mode) { case SHT_PULSE_OPEN__PULSE_CLOSE: @@ -288,8 +285,30 @@ void ShutterUpdatePosition(void) break; case SHT_OFF_ON__OPEN_CLOSE: // This is a failsafe configuration. Relay1 ON/OFF Relay2 -1/1 direction + // Only allow PWM microstepping if PWM and COUNTER are defined. + // see wiki to connect PWM and COUNTER + if (pin[GPIO_PWM1 ]+i < 99 && pin[GPIO_CNTR1 ]+i < 99 ) { + int16_t missing_steps = ((Shutter.target_position[i]-Shutter.start_position[i])*Shutter.direction[i]*Shutter.max_pwm_frequency/2000) - RtcSettings.pulse_counter[i]; + Shutter.pwm_frequency = 0; + //slow down for acurate position + analogWriteFreq(500); + analogWrite(pin[GPIO_PWM1]+i, 50); + //prepare for stop PWM + Shutter.motordelay[i] = -2 + Shutter.motordelay[i] + missing_steps/(Shutter.max_pwm_frequency/20); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Missing steps %d, adjust motordelay %d, counter %d, temp realpos %d"), missing_steps, Shutter.motordelay[i],RtcSettings.pulse_counter[i] ,Shutter.real_position[i]); + Settings.shutter_motordelay[i]=Shutter.motordelay[i]; + analogWriteFreq(0); + while (RtcSettings.pulse_counter[i] < (uint32_t)(Shutter.target_position[i]-Shutter.start_position[i])*Shutter.direction[i]*Shutter.max_pwm_frequency/2000) { + delay(1); + } + analogWrite(pin[GPIO_PWM1]+i, 0); + Shutter.real_position[i] = ((int32_t)RtcSettings.pulse_counter[i]*Shutter.direction[i]*2000 / Shutter.max_pwm_frequency)+Shutter.start_position[i]; + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT:Realpos %d, pulsecount %d, startpos %d, int32 %d"), Shutter.real_position[i],RtcSettings.pulse_counter[i], Shutter.start_position[i], ((int32_t)RtcSettings.pulse_counter[i]*Shutter.direction[i]*2000 / Shutter.max_pwm_frequency)); + + } if ((1 << (Settings.shutter_startrelay[i]-1)) & power) { ExecuteCommandPower(Settings.shutter_startrelay[i], 0, SRC_SHUTTER); + ExecuteCommandPower(Settings.shutter_startrelay[i]+1, 0, SRC_SHUTTER); } break; case SHT_OFF_OPEN__OFF_CLOSE: @@ -300,6 +319,18 @@ void ShutterUpdatePosition(void) } break; } + Settings.shutter_position[i] = ShutterRealToPercentPosition(Shutter.real_position[i], i); + + dtostrfd((float)Shutter.time[i] / 20, 1, stemp2); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Shutter %d: Real Pos. %d, Stoppos: %ld, relay: %d, direction %d, pulsetimer: %d, motordelay %d, rtcshutter: %s [s]"), i, Shutter.real_position[i], Settings.shutter_position[i], cur_relay -1, Shutter.direction[i], Settings.pulse_timer[cur_relay -1], Shutter.motordelay[i],stemp2); + Shutter.start_position[i] = Shutter.real_position[i]; + + // sending MQTT result to broker + snprintf_P(scommand, sizeof(scommand),PSTR(D_SHUTTER "%d"), i+1); + GetTopic_P(stopic, STAT, mqtt_topic, scommand); + Response_P("%d", Settings.shutter_invert[i] ? 100 - Settings.shutter_position[i]: Settings.shutter_position[i]); + MqttPublish(stopic, Settings.flag.mqtt_power_retain); // CMND_POWERRETAIN + Shutter.direction[i] = 0; uint8_t position = Settings.shutter_invert[i] ? 100 - Settings.shutter_position[i]: Settings.shutter_position[i]; Response_P(PSTR("{")); @@ -326,6 +357,15 @@ void ShutterStartInit(uint8_t index, uint8_t direction, int32_t target_pos) Shutter.target_position[index] = target_pos; Shutter.start_position[index] = Shutter.real_position[index]; Shutter.time[index] = 0; + if (pin[GPIO_PWM1]+index < 99) { + Shutter.pwm_frequency = 0; + analogWriteFreq(Shutter.pwm_frequency); + analogWrite(pin[GPIO_PWM1]+index, 0); + // can be operated without counter, but then not that acurate. + if (pin[GPIO_CNTR1]+index < 99) { + RtcSettings.pulse_counter[index] = 0; + } + } //AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Start shutter: %d from %d to %d in directin %d"), index, Shutter.start_position[index], Shutter.target_position[index], Shutter.direction[index]); } @@ -342,10 +382,10 @@ void ShutterReportPosition(void) if (Shutter.direction[i] != 0) { char stemp1[20]; char stemp2[10]; - dtostrfd((float)Shutter.time[i] / 20, 1, stemp2); + dtostrfd((float)Shutter.time[i] / 20, 2, stemp2); shutter_moving = 1; //Settings.shutter_position[i] = Settings.shuttercoeff[2][i] * 5 > Shutter.real_position[i] ? Shutter.real_position[i] / Settings.shuttercoeff[2][i] : (Shutter.real_position[i]-Settings.shuttercoeff[0,i]) / Settings.shuttercoeff[1][i]; - AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Shutter %d: Real Pos: %d, Target %d, source: %s, start-pos: %d %%, direction: %d, rtcshutter: %s [s]"), i,Shutter.real_position[i], Shutter.target_position[i], GetTextIndexed(stemp1, sizeof(stemp1), last_source, kCommandSource), Settings.shutter_position[i], Shutter.direction[i], stemp2 ); + AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Shutter %d: Real Pos: %d, Target %d, source: %s, start-pos: %d %%, direction: %d, motordelay %d, rtcshutter: %s [s]"), i,Shutter.real_position[i], Shutter.target_position[i], GetTextIndexed(stemp1, sizeof(stemp1), last_source, kCommandSource), Settings.shutter_position[i], Shutter.direction[i], Shutter.motordelay[i], stemp2 ); } } if (rules_flag.shutter_moving > shutter_moving) { @@ -406,12 +446,6 @@ void ShutterRelayChanged(void) } } -/////////////////////////////////////////////////////////////////////////////////// -// Shutter specific functions -// TODO: move to shutter driver and make them accessible in a generic way - -// device: 1.. -// position: 0-100 void ShutterSetPosition(uint8_t device, uint8_t position) { char svalue[32]; // Command and number parameter @@ -426,7 +460,6 @@ void ShutterSetPosition(uint8_t device, uint8_t position) void CmndShutterOpen(void) { XdrvMailbox.payload = 100; - XdrvMailbox.data_len = 3; last_source = SRC_WEBGUI; CmndShutterPosition(); } @@ -434,7 +467,7 @@ void CmndShutterOpen(void) void CmndShutterClose(void) { XdrvMailbox.payload = 0; - XdrvMailbox.data_len = 1; + XdrvMailbox.data_len = 0; last_source = SRC_WEBGUI; CmndShutterPosition(); } @@ -445,8 +478,8 @@ void CmndShutterStop(void) uint32_t index = XdrvMailbox.index -1; if (Shutter.direction[index] != 0) { - //AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Stop moving shutter %d: direction: %d"), XdrvMailbox.index, Shutter.direction[index]); - + AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Stop moving shutter %d: direction: %d"), XdrvMailbox.index, Shutter.direction[index]); + // set stop position 10 steps ahead (0.5sec to allow normal stop) int32_t temp_realpos = Shutter.start_position[index] + ( (Shutter.time[index]+10) * (Shutter.direction[index] > 0 ? 100 : -Shutter.close_velocity[index])); XdrvMailbox.payload = ShutterRealToPercentPosition(temp_realpos, index); //XdrvMailbox.payload = Settings.shuttercoeff[2][index] * 5 > temp_realpos ? temp_realpos / Settings.shuttercoeff[2][index] : (temp_realpos-Settings.shuttercoeff[0,index]) / Settings.shuttercoeff[1][index]; @@ -472,7 +505,7 @@ void CmndShutterPosition(void) if (!strcmp(XdrvMailbox.data,"STOP")) { CmndShutterStop(); } return; } - + int8_t target_pos_percent = XdrvMailbox.payload < 0 ? 0 : (XdrvMailbox.payload > 100 ? 100 : XdrvMailbox.payload); // webgui still send also on inverted shutter the native position. target_pos_percent = Settings.shutter_invert[index] && SRC_WEBGUI != last_source ? 100 - target_pos_percent : target_pos_percent; @@ -497,7 +530,6 @@ void CmndShutterPosition(void) } if (Shutter.direction[index] != new_shutterdirection ) { ShutterStartInit(index, new_shutterdirection, Shutter.target_position[index]); - Shutter.operations[index]++; if (Shutter.mode == SHT_OFF_ON__OPEN_CLOSE) { ExecuteCommandPower(Settings.shutter_startrelay[index], 0, SRC_SHUTTER); //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Delay5 5s, xdrv %d"), XdrvMailbox.payload); @@ -556,7 +588,7 @@ void CmndShutterMotorDelay(void) ShutterInit(); } char time_chr[10]; - dtostrfd((float)(Settings.shutter_motordelay[XdrvMailbox.index -1]) / 20, 1, time_chr); + dtostrfd((float)(Settings.shutter_motordelay[XdrvMailbox.index -1]) / 20, 2, time_chr); ResponseCmndIdxChar(time_chr); } } @@ -593,6 +625,16 @@ void CmndShutterSetHalfway(void) } } +void CmndShutterFrequency(void) +{ + if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 10000)) { + Shutter.max_pwm_frequency = XdrvMailbox.payload; + ResponseCmndNumber(XdrvMailbox.payload); // ???? + } else { + ResponseCmndNumber(Shutter.max_pwm_frequency); + } +} + void CmndShutterSetClose(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= shutters_present)) { diff --git a/tools/decode-config.md b/tools/decode-config.md index 4097638fa..10019a23f 100644 --- a/tools/decode-config.md +++ b/tools/decode-config.md @@ -1,407 +1,3 @@ -# decode-config.py -_decode-config.py_ is able to backup and restore Tasmota configuration. +A tool to backup and restore the configuration of [Tasmota](http://tasmota.com/)-devices. -In comparison with the Tasmota build-in "Backup/Restore Configuration" function _decode-config.py_ -* uses human readable and editable [JSON](http://www.json.org/)-format for backup/restore, -* can restore previously backup and changed [JSON](http://www.json.org/)-format files, -* is able to create Tasmota compatible command list with related config parameter - -Comparing backup files created by *decode-config.py* and *.dmp files created by Tasmota "Backup/Restore Configuration": - -|   | decode-config.py
*.json file | Tasmota
*.dmp file | -|-------------------------|:-------------------------------:|:-----------------------------------:| -| Encrypted | No | Yes | -| Readable | Yes | No | -| Simply editable | Yes | No | -| Simply batch processing | Yes | No | - -_decode-config.py_ is compatible with Tasmota version from v5.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-format) - * [.json File Format](decode-config.md#-json-format) - * [.bin File Format](decode-config.md#-bin-format) - * [File extensions](decode-config.md#file-extensions) -* [Usage](decode-config.md#usage) - * [Basics](decode-config.md#basics) - * [Save backup file](decode-config.md#save-backup-file) - * [Restore backup file](decode-config.md#restore-backup-file) - * [Output to screen](decode-config.md#output-to-screen) - * [JSON output](decode-config.md#json-output) - * [Tasmota command output](decode-config.md#tasmota-command-output) - * [Filter data](decode-config.md#filter-data) - * [Configuration file](decode-config.md#configuration-file) - * [More program arguments](decode-config.md#more-program-arguments) - * [Examples](decode-config.md#examples) - * [Config file](decode-config.md#config-file) - * [Using Tasmota binary configuration files](decode-config.md#using-tasmota-binary-configuration-files) - * [Use batch processing](decode-config.md#use-batch-processing) - * [Notes](decode-config.md#notes) - -## Prerequisite -* This program is written in [Python](https://en.wikipedia.org/wiki/Python_(programming_language)) so you need to install a working python environment for your operating system. - -### Linux -``` -sudo apt-get install python python-pip libcurl4-openssl-dev libssl-dev -``` -``` -pip install pycurl configargparse -``` - -### Windows 10 - -Install [Python 2.7](https://www.python.org/download/releases/2.7/) then install dependencies. For PyCurl you need to [download pycurl‑7.43.0.3‑cp27‑cp27m‑win_amd64.whl](https://www.lfd.uci.edu/~gohlke/pythonlibs/#pycurl) for Windows 10 64bit. -``` -pip install pycurl-7.43.0.3-cp27-cp27m-win_amd64.whl -// run the command from the folder where you downloaded the file - -pip install configargparse -``` - -* [Tasmota](https://github.com/arendst/Tasmota) [Firmware](https://github.com/arendst/Tasmota/releases) with Web-Server enabled: - * To backup or restore configurations from or to a Tasmota device you need a firmare with enabled web-server in admin mode (command [WebServer 2](https://tasmota.github.io/docs/#/Commands#wifi)). This is the Tasmota default. - * If using your own compiled firmware be aware to enable the web-server (`#define USE_WEBSERVER` and `#define WEB_SERVER 2`). - -## File Types -_decode-config.py_ can handle the following backup file types: -### .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` parameter. -This file will be created by _decode-config.py_ using the `--backup-file` with `--backup-type json` parameter, this is the 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: -The .bin file contains the same information as the original .dmp file from Tasmota "Backup/Restore Configuration" but it is decrpted and 4 byte longer than an original (it is a prefix header at the beginning). .bin file data starting at address 4 contains the same as the **struct SYSCFG** from Tasmota [settings.h](https://github.com/arendst/Tasmota/blob/master/tasmota/settings.h) in decrypted format. - -#### File extensions -You don't need to append exensions for your file name as _decode-config.py_ uses auto extension as default. The extension will be choose based on file contents and `--backup-type` parameter. -If you do not want using auto extensions use the `--no-extension` parameter. - -## Usage -After download don't forget to set the executable 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 using the `-d ` parameter -* a Tasmota `*.dmp` configuration file using `-f ` parameter - -Example: - - decode-config.py -d tasmota-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 use `--backup-file `, you can use placeholder for Version, Friendlyname and Hostname: - - decode-config.py -d tasmota-4281 --backup-file Config_@f_@v - -If you have setup a WebPassword within Tasmota, use - - decode-config.py -d tasmota-4281 -p --backup-file Config_@f_@v - -will create a file like `Config_Tasmota_6.4.0.json` (the part `Tasmota` and `6.4.0` will choosen related to your device configuration). Because the default backup file format is JSON, you can read and change it with any raw text editor. - -### Restore backup file -Reading back a saved (and possible changed) backup file use the `--restore-file ` parameter. 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_Tasmota_6.2.1.json` to device `tasmota-4281` use: - - decode-config.py -d tasmota-4281 --restore-file Config_Tasmota_6.2.1.json - -with password set by WebPassword: - - decode-config.py -d tasmota-4281 -p --restore-file Config_Tasmota_6.2.1.json - -### Output to screen -To force screen output use the `--output` parameter. - -Output to screen is default enabled when calling the program with a source parameter (-f or -d) but without any backup or restore parameter. - -#### JSON output -The default output format is [JSON](decode-config.md#-json-format). You can force JSON output using the `--output-format json` parameter. - -Example: - - decode-config.py -d tasmota-4281 -c my.conf -x Wifi --output-format json - - { - ... - "hostname": "%s-%04d", - "ip_address": [ - "0.0.0.0", - "192.168.12.1", - "255.255.255.0", - "192.168.12.1" - ], - "ntp_server": [ - "ntp.localnet.home", - "ntp2.localnet.home", - "192.168.12.1" - ], - "sta_active": 0, - "sta_config": 5, - "sta_pwd": [ - "myWlAnPaszxwo!z", - "myWlAnPaszxwo!z2" - ], - "sta_ssid": [ - "wlan.1", - "my-wlan" - ], - "web_password": "myPaszxwo!z", - "webserver": 2 - ... - } - -Note: JSON output always contains all configuration data like the backup file except you are using `--group` arg. - - -#### Tasmota command output -_decode-config.py_ is able to translate the configuration data to (most all) Tasmota commands. To output your configuration as Tasmota commands use `--output-format cmnd` or `--output-format command`. - -Example: - - decode-config.py -d tasmota-4281 -c my.conf -g Wifi --output-format cmnd - - # Wifi: - AP 0 - Hostname %s-%04d - IPAddress1 0.0.0.0 - IPAddress2 192.168.12.1 - IPAddress3 255.255.255.0 - IPAddress4 192.168.12.1 - NtpServer1 ntp.localnet.home - NtpServer2 ntp2.localnet.home - NtpServer3 192.168.12.1 - Password1 myWlAnPaszxwo!z - Password2 myWlAnPaszxwo!z2 - SSId1 wlan.1 - SSId2 wlan.1 - WebPassword myPaszxwo!z - WebServer 2 - WifiConfig 5 - -Note: A few very specific module commands like MPC230xx, KNX and some Display commands are not supported. These are still available by JSON output. - -### Filter data -The huge number of Tasmota configuration data can be overstrained and confusing, so the most of the configuration data are grouped into categories. - -With _decode-config.py_ the following categories are available: `Display`, `Domoticz`, `Internal`, `KNX`, `Led`, `Logging`, `MCP230xx`, `MQTT`, `Main`, `Management`, `Pow`, `Sensor`, `Serial`, `SetOption`, `RF`, `System`, `Timers`, `Wifi` - -These are similary to the categories on [https://tasmota.github.io/docs/#/Commands](Tasmota Command Wiki). - -To filter outputs to a subset of groups use the `-g` or `--group` arg concatenating the grooup you want, e. g. - - decode-config.py -d tasmota-4281 -c my.conf --output-format cmnd --group Main MQTT Management Wifi - - -### Configuration file -Each argument that start with `--` (eg. `--file`) can also be set in a config file (specified via -c). Config file syntax allows: key=value, flag=true, stuff=[a,b,c] (for details, see syntax at [https://pypi.org/project/ConfigArgParse](https://pypi.org/project/ConfigArgParse/)). - -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 tasmota-4281 -c my.conf --backup-file Config_@f_@v - - - -### More program arguments -For better reading each short written arg (minus sign `-`) has a corresponding long version (two minus signs `--`), eg. `--device` for `-d` or `--file` for `-f` (note: not even all `--` arg has a corresponding `-` one). - -A short list of possible program args is displayed using `-h` or `--help`. - -For advanced help use `-H` or `--full-help`: - - usage: decode-config.py [-f ] [-d ] [-P ] - [-u ] [-p ] [-i ] - [-o ] [-t json|bin|dmp] [-E] [-e] [-F] - [--json-indent ] [--json-compact] - [--json-hide-pw] [--json-show-pw] - [--cmnd-indent ] [--cmnd-groups] - [--cmnd-nogroups] [--cmnd-sort] [--cmnd-unsort] - [-c ] [-S] [-T json|cmnd|command] - [-g {Control,Devices,Display,Domoticz,Internal,Knx,Light,Management,Mqtt,Power,Rf,Rules,Sensor,Serial,Setoption,Shutter,System,Timer,Wifi} [{Control,Devices,Display,Domoticz,Internal,Knx,Light,Management,Mqtt,Power,Rf,Rules,Sensor,Serial,Setoption,Shutter,System,Timer,Wifi} ...]] - [--ignore-warnings] [-h] [-H] [-v] [-V] - - Backup/Restore Tasmota configuration data. Args that start with '--' - (eg. -f) can also be set in a config file (specified via -c). Config file - syntax allows: key=value, flag=true, stuff=[a,b,c] (for details, see syntax at - https://goo.gl/R74nmi). If an arg is specified in more than one place, then - commandline values override config file values which override defaults. - - Source: - Read/Write Tasmota configuration from/to - - -f, --file, --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 specification - - -i, --restore-file - file to restore configuration from (default: None). - Replacements: @v=firmware version from config, - @f=device friendly name from config, @h=device - hostname from config, @H=device hostname from device - (-d arg only) - -o, --backup-file - file to backup configuration to (default: None). - Replacements: @v=firmware version from config, - @f=device friendly name from config, @h=device - hostname from config, @H=device hostname from device - (-d arg only) - -t, --backup-type json|bin|dmp - backup filetype (default: 'json') - -E, --extension append filetype extension for -i and -o filename - (default) - -e, --no-extension do not append filetype extension, use -i and -o - filename as passed - -F, --force-restore force restore even configuration is identical - - JSON output: - JSON format specification - - --json-indent - pretty-printed JSON output using indent level - (default: 'None'). -1 disables indent. - --json-compact compact JSON output by eliminate whitespace - --json-hide-pw hide passwords - --json-show-pw, --json-unhide-pw - unhide passwords (default) - - Tasmota command output: - Tasmota command output format specification - - --cmnd-indent - Tasmota command grouping indent level (default: '2'). - 0 disables indent - --cmnd-groups group Tasmota commands (default) - --cmnd-nogroups leave Tasmota commands ungrouped - --cmnd-sort sort Tasmota commands (default) - --cmnd-unsort leave Tasmota commands unsorted - - Common: - Optional arguments - - -c, --config - program config file - can be used to set default - command args (default: None) - -S, --output display output regardsless of backup/restore usage - (default do not output on backup or restore usage) - -T, --output-format json|cmnd|command - display output format (default: 'json') - -g, --group {Control,Devices,Display,Domoticz,Internal,Knx,Light,Management,Mqtt,Power,Rf,Rules,Sensor,Serial,Setoption,Shutter,System,Timer,Wifi} - limit data processing to command groups (default no - filter) - --ignore-warnings do not exit on warnings. Not recommended, used by your - own responsibility! - - Info: - Extra 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. - -### Program parameter notes - -_decode-config.py_ - - -### Examples -The most of the examples are for linux command line. Under Windows call the program using `python decode-config.py ...`. - -#### Config file -Note: The example contains .ini style sections `[...]`. Sections are always treated as comment and serves as clarity only. -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 tasmota --restore-file Config_Tasmota_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 tasmota --backup-file Config_@f_@v.dmp` - - b) use args to choice the file format - - `decode-config.py -c my.conf -d tasmota --backup-type dmp --backup-file Config_@f_@v` - -#### Use batch processing - - for device in tasmota1 tasmota2 tasmota3; do ./decode-config.py -c my.conf -d $device -o Config_@f_@v - -or under windows - - for device in (tasmota1 tasmota2 tasmota3) do python decode-config.py -c my.conf -d %device -o Config_@f_@v - -will produce JSON configuration files for host tasmota1, tasmota2 and tasmota3 using friendly name and Tasmota firmware version for backup filenames. - -## Notes -Some general notes: -* Filename replacement macros **@h** and **@H**: - * **@h** -The **@h** replacement macro uses the hostname configured with the Tasomta Wifi `Hostname ` command (defaults to `%s-%04d`). It will not use the network hostname of your device because this is not available when working with files only (e.g. `--file ` as source). -To prevent having a useless % in your filename, **@h** will not replaced by configuration data hostname if this contains '%' characters. - * **@H** -If you want to use the network hostname within your filename, use the **@H** replacement macro instead - but be aware this will only replaced if you are using a network device as source (`-d`, `--device`, `--host`); it will not work when using a file as source (`-f`, `--file`) +## decode-config has moved to [https://github.com/tasmota/decode-config](https://github.com/tasmota/decode-config) diff --git a/tools/decode-config.py b/tools/decode-config.py deleted file mode 100755 index f15c696ae..000000000 --- a/tools/decode-config.py +++ /dev/null @@ -1,3322 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import print_function -from past.builtins import long -VER = '2.4.0039' - -""" - decode-config.py - Backup/Restore Tasmota configuration data - - Copyright (C) 2019 Norbert Richter - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - -Requirements: - - Python 2.x: - pip install json requests urllib2 configargparse - - -Instructions: - Execute command with option -d to retrieve config data from a host - 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 (or -H for advanced help) - - -Usage: decode-config.py [-f ] [-d ] [-P ] - [-u ] [-p ] [-i ] - [-o ] [-t json|bin|dmp] [-E] [-e] [-F] - [--json-indent ] [--json-compact] - [--json-hide-pw] [--json-show-pw] - [--cmnd-indent ] [--cmnd-groups] - [--cmnd-nogroups] [--cmnd-sort] [--cmnd-unsort] - [-c ] [-S] [-T json|cmnd|command] - [-g {Control,Devices,Display,Domoticz,Internal,Knx,Light,Management,Mqtt,Power,Rules,Sensor,Serial,Setoption,Shutter,Rf,System,Timer,Wifi} [{Control,Devices,Display,Domoticz,Internal,Knx,Light,Management,Mqtt,Power,Rules,Sensor,Serial,Setoption,Shutter,Rf,System,Timer,Wifi} ...]] - [--ignore-warnings] [-h] [-H] [-v] [-V] - - Backup/Restore Tasmota configuration data. Args that start with '--' - (eg. -f) can also be set in a config file (specified via -c). Config file - syntax allows: key=value, flag=true, stuff=[a,b,c] (for details, see syntax at - https://goo.gl/R74nmi). If an arg is specified in more than one place, then - commandline values override config file values which override defaults. - - Source: - Read/Write Tasmota configuration from/to - - -f, --file, --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 specification - - -i, --restore-file - file to restore configuration from (default: None). - Replacements: @v=firmware version from config, - @f=device friendly name from config, @h=device - hostname from config, @H=device hostname from device - (-d arg only) - -o, --backup-file - file to backup configuration to (default: None). - Replacements: @v=firmware version from config, - @f=device friendly name from config, @h=device - hostname from config, @H=device hostname from device - (-d arg only) - -t, --backup-type json|bin|dmp - backup filetype (default: 'json') - -E, --extension append filetype extension for -i and -o filename - (default) - -e, --no-extension do not append filetype extension, use -i and -o - filename as passed - -F, --force-restore force restore even configuration is identical - - JSON output: - JSON format specification - - --json-indent - pretty-printed JSON output using indent level - (default: 'None'). -1 disables indent. - --json-compact compact JSON output by eliminate whitespace - --json-hide-pw hide passwords - --json-show-pw, --json-unhide-pw - unhide passwords (default) - - Tasmota command output: - Tasmota command output format specification - - --cmnd-indent - Tasmota command grouping indent level (default: '2'). - 0 disables indent - --cmnd-groups group Tasmota commands (default) - --cmnd-nogroups leave Tasmota commands ungrouped - --cmnd-sort sort Tasmota commands (default) - --cmnd-unsort leave Tasmota commands unsorted - - Common: - Optional arguments - - -c, --config - program config file - can be used to set default - command args (default: None) - -S, --output display output regardsless of backup/restore usage - (default do not output on backup or restore usage) - -T, --output-format json|cmnd|command - display output format (default: 'json') - -g, --group {Control,Devices,Display,Domoticz,Internal,Knx,Light,Management,Mqtt,Power,Rules,Sensor,Serial,Setoption,Shutter,Rf,System,Timer,Wifi} - limit data processing to command groups (default no - filter) - --ignore-warnings do not exit on warnings. Not recommended, used by your - own responsibility! - - Info: - Extra 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. - - -Returns: - 0: successful - 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 - -# ====================================================================== -# imports -# ====================================================================== -import os.path -import io -import sys, platform -def ModuleImportError(module): - er = str(module) - print('{}, try "pip install {}"'.format(er,er.split(' ')[len(er.split(' '))-1]), file=sys.stderr) - sys.exit(ExitCode.MODULE_NOT_FOUND) -try: - from datetime import datetime - import time - import copy - import struct - import socket - import re - import math - import inspect - import json - import configargparse - import requests - if sys.version_info.major==2: - import urllib2 - else: - import urllib -except ImportError as e: - ModuleImportError(e) - -# ====================================================================== -# globals -# ====================================================================== -PROG='{} v{} by Norbert Richter '.format(os.path.basename(sys.argv[0]),VER) - -CONFIG_FILE_XOR = 0x5A -BINARYFILE_MAGIC = 0x63576223 -STR_ENCODING = 'utf8' -HIDDEN_PASSWORD = '********' -INTERNAL = 'Internal' - -DEFAULTS = { - 'source': - { - 'device': None, - 'port': 80, - 'username': 'admin', - 'password': None, - 'tasmotafile': None, - }, - 'backup': - { - 'restorefile': None, - 'backupfile': None, - 'backupfileformat': 'json', - 'extension': True, - 'forcerestore': False, - }, - 'jsonformat': - { - 'jsonindent': None, - 'jsoncompact': False, - 'jsonsort': True, - 'jsonhidepw': False, - }, - 'cmndformat': - { - 'cmndindent': 2, - 'cmndgroup': True, - 'cmndsort': True, - }, - 'common': - { - 'output': False, - 'outputformat': 'json', - 'configfile': None, - 'ignorewarning':False, - 'filter': None, - }, -} -args = {} -exitcode = 0 - - -# ====================================================================== -# Settings mapping -# ====================================================================== -""" -Settings dictionary describes the config file fields definition: - - = { : } - - : "string" - a python valid dictionary key (string) - - : ( , , [,] ) - a tuple containing the following items: - - : | - data type & format definition - : - defines the use of data at - format is defined in 'struct module format string' - see - https://docs.python.org/3.8/library/struct.html#format-strings - : - A dictionary describes a (sub)setting dictonary - and can recursively define another - - : | (, , ) - address definition - : - The address (starting from 0) within binary config data. - : - number of bits used (positive integer) - : - bit shift : - >= 0: shift the result right - < 0: shift the result left - - : | (, [,cmd]) - data definition - : None | | [] | [ ,...] - None: - Single value, not an array - : - [] - Defines a one-dimensional array of size - [ ,...] - Defines a one- or multi-dimensional array - : - value validation function - : (, ) - Tasmota command definition - : - command group string - : | (,...) - convert data into Tasmota command function - - : | (, ) - read/write converter - : None | - Will be used in Bin2Mapping to convert values read - from the binary data object into mapping dictionary - None - None indicates not read conversion - - to convert value from binary object to JSON. - : None | False | - Will be used in Mapping2Bin to convert values read - from mapping dictionary before write to binary - data object - None - None indicates not write conversion - False - False indicates the value is readonly and will - not be written into the binary object. - - to convert value from JSON back to binary object - - Common definitions - - : | | None - function to be called or string to evaluate: - : - A function name will be called with one or two parameter: - The value to be processed - (optional) the current array index (1,n) - - A string will be evaluate as is. The following - placeholder can be used to replace it by runtime values: - '$': - will be replaced by the mapping name value - '#': - will be replace by array index (if any) - '@': - can be used as reference to other mapping values - see definition below for examples - - : 'string' | "string" - characters enclosed in ' or " - - : integer - numbers in the range -2147483648 through 2147483647 - : unsigned integer - numbers in the range 0 through 4294967295 - -""" -# ---------------------------------------------------------------------- -# Settings helper -# ---------------------------------------------------------------------- -def passwordread(value): - return HIDDEN_PASSWORD if args.jsonhidepw else value -def passwordwrite(value): - return None if value == HIDDEN_PASSWORD else value -def bitsRead(x, n=0, c=1): - """ - Reads bit(s) of a number - - @param x: - the number from which to read - - @param n: - which bit position to read - - @param c: - how many bits to read (1 if omitted) - - @return: - the bit value(s) - """ - if isinstance(x,str): - x = int(x, 0) - if isinstance(x,str): - n = int(n, 0) - - if n >= 0: - x >>= n - else: - x <<= abs(n) - if c>0: - x &= (1<, , [,] - 'cfg_holder': ('0 and bitsRead($,0,11)>(12*60) else "",time=time.strftime("%H:%M",time.gmtime((bitsRead($,0,11) if bitsRead($,29,2)==0 else bitsRead($,0,11) if bitsRead($,0,11)<=(12*60) else bitsRead($,0,11)-(12*60))*60)),window=bitsRead($,11,4),repeat=bitsRead($,15),days="{:07b}".format(bitsRead($,16,7))[::-1],device=bitsRead($,23,4)+1,power=bitsRead($,27,2) )')), ('"0x{:08x}".format($)', False) ), - 'time': (' 0 and type_ is not None else '', - sstatus=status if status is not None and status > 0 else '', - scolon=': ' if type_ is not None or line is not None else '', - smgs=msg, - slineno=' (@{:04d})'.format(line) if line is not None else '') - , file=sys.stderr) - - -def exit(status=0, msg="end", type_=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 msg: - the msg logged before exit - @param type_: - msg type: 'INFO', 'WARNING' or 'ERROR' - @param doexit: - True to exit program, otherwise return - """ - - if src is not None: - msg = '{} ({})'.format(src, msg) - message(msg, type_=type_ if status!=ExitCode.OK else LogType.INFO, status=status, line=line) - exitcode = status - if doexit: - sys.exit(exitcode) - - -def debug(args): - """ - Get debug level - - @param args: - configargparse.parse_args() result - - @return: - debug level - """ - return 0 if args.debug is None else args.debug - - -def instance(type_): - """ - Creates Python2/3 compatible isinstance test type(s) - - @param args: - Python3 instance type - - @return: - Python2/3 compatible isinstance type(s) - """ - newtype = type_ - if sys.version_info.major==2: - if type_==str: - newtype = (str,unicode) - elif isinstance(type_, tuple) and str in type_: - newtype = newtype + (unicode,) - return newtype - - -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 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 GetTemplateSizes(): - """ - Get all possible template sizes as list - - @return: - template sizes as list [] - """ - sizes = [] - for cfg in Settings: - sizes.append(cfg[1]) - # return unique sizes only (remove duplicates) - return list(set(sizes)) - - -def GetTemplateSetting(decode_cfg): - """ - Search for version, size and settings to be used depending on given binary config data - - @param decode_cfg: - binary config data (decrypted) - - @return: - version, size, settings to use; None if version is invalid - """ - version = 0x0 - size = setting = None - version = GetField(decode_cfg, 'version', Setting_6_2_1['version'], raw=True) - # search setting definition top-down - for cfg in sorted(Settings, key=lambda s: s[0], reverse=True): - if version >= cfg[0]: - size = cfg[1] - setting = cfg[2] - break - - return version, size, setting - - -def GetGroupList(setting): - """ - Get all avilable group definition from setting - - @return: - configargparse.parse_args() result - """ - groups = set() - - for name in setting: - dev = setting[name] - format_, group = GetFieldDef(dev, fields="format_, group") - if group is not None and len(group) > 0: - groups.add(group.title()) - if isinstance(format_, dict): - subgroups = GetGroupList(format_) - if subgroups is not None and len(subgroups) > 0: - for group in subgroups: - groups.add(group.title()) - - groups=list(groups) - groups.sort() - return groups - - -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: - with open(filename, "r") as f: - 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 - with open(filename, "rb") as inputfile: - inputbin = inputfile.read() - 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 MakeFilename(filename, filetype, configmapping): - """ - Replace variables within a filename - - @param filename: - original filename possible containing replacements: - @v: - Tasmota version from config data - @f: - friendlyname from config data - @h: - hostname from config data - @H: - hostname from device (-d arg only) - @param filetype: - FileType.x object - creates extension if not None - @param configmapping: - binary config data (decrypted) - - @return: - New filename with replacements - """ - config_version = config_friendlyname = config_hostname = device_hostname = '' - - if 'version' in configmapping: - config_version = GetVersionStr( int(str(configmapping['version']), 0) ) - if 'friendlyname' in configmapping: - config_friendlyname = re.sub('[^0-9a-zA-Z]','_', configmapping['friendlyname'][0]) - if 'hostname' in configmapping: - if configmapping['hostname'].find('%') < 0: - config_hostname = re.sub('[^0-9a-zA-Z]','_', configmapping['hostname']) - if filename.find('@H') >= 0 and args.device is not None: - device_hostname = GetTasmotaHostname(args.device, args.port, username=args.username, password=args.password) - if device_hostname is None: - device_hostname = '' - - dirname = basename = ext = '' - - # split file parts - dirname = os.path.normpath(os.path.dirname(filename)) - basename = os.path.basename(filename) - name, ext = os.path.splitext(basename) - - # make a valid filename - try: - name = name.decode('unicode-escape').translate(dict((ord(char), None) for char in '\/*?:"<>|')) - except: - pass - name = str(name.replace(' ','_')) - - # append extension based on filetype if not given - if len(ext) and ext[0]=='.': - ext = ext[1:] - if filetype is not None and args.extension and (len(ext)<2 or all(c.isdigit() for c in ext)): - ext = filetype.lower() - - # join filename + extension - if len(ext): - name_ext = name+'.'+ext - else: - name_ext = name - - # join path and filename - try: - filename = os.path.join(dirname, name_ext) - except: - pass - - filename = filename.replace('@v', config_version) - filename = filename.replace('@f', config_friendlyname ) - filename = filename.replace('@h', config_hostname ) - filename = filename.replace('@H', device_hostname ) - - 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 LoadTasmotaConfig(filename): - """ - Load config from Tasmota file - - @param filename: - filename to load - - @return: - binary config data (encrypted) or None on error - """ - - encode_cfg = None - - # read config from a file - if not os.path.isfile(filename): # check file exists - exit(ExitCode.FILE_NOT_FOUND, "File '{}' not found".format(filename),line=inspect.getlineno(inspect.currentframe())) - try: - with open(filename, "rb") as tasmotafile: - encode_cfg = tasmotafile.read() - except Exception as e: - exit(e.args[0], "'{}' {}".format(filename, e[1]),line=inspect.getlineno(inspect.currentframe())) - - return encode_cfg - - -def TasmotaGet(cmnd, host, port, username=DEFAULTS['source']['username'], password=None, contenttype = None): - """ - Tasmota http request - - @param host: - hostname or IP of Tasmota device - @param port: - http port of Tasmota device - @param username: - optional username for Tasmota web login - @param password - optional password for Tasmota web login - - @return: - binary config data (encrypted) or None on error - """ - - # read config direct from device via http - url = MakeUrl(host, port, cmnd) - auth = None - if username is not None and password is not None: - auth = (username, password) - res = requests.get(url, auth=auth) - - if not res.ok: - exit(res.status_code, "Error on http GET request for {} - {}".format(url,res.reason), line=inspect.getlineno(inspect.currentframe())) - - if contenttype is not None and res.headers['Content-Type']!=contenttype: - exit(ExitCode.DOWNLOAD_CONFIG_ERROR, "Device did not response properly, may be Tasmota webserver admin mode is disabled (WebServer 2)",line=inspect.getlineno(inspect.currentframe())) - - return res.status_code, res.content - - -def GetTasmotaHostname(host, port, username=DEFAULTS['source']['username'], password=None): - """ - Get Tasmota hostname from device - - @param host: - hostname or IP of Tasmota device - @param port: - http port of Tasmota device - @param username: - optional username for Tasmota web login - @param password - optional password for Tasmota web login - - @return: - Tasmota real hostname or None on error - """ - hostname = None - - loginstr = "" - if password is not None: - loginstr = "user={}&password={}&".format(urllib2.quote(username), urllib2.quote(password)) - # get hostname - responsecode, body = TasmotaGet("cm?{}cmnd=status%205".format(loginstr), host, port, username=username, password=password) - if body is not None: - jsonbody = json.loads(body) - if "StatusNET" in jsonbody and "Hostname" in jsonbody["StatusNET"]: - hostname = jsonbody["StatusNET"]["Hostname"] - if args.verbose: - message("Hostname for '{}' retrieved: '{}'".format(host, hostname), type_=LogType.INFO) - - return hostname - - -def PullTasmotaConfig(host, port, username=DEFAULTS['source']['username'], password=None): - """ - Pull config from Tasmota device - - @param host: - hostname or IP of Tasmota device - @param port: - http port of Tasmota device - @param username: - optional username for Tasmota web login - @param password - optional password for Tasmota web login - - @return: - binary config data (encrypted) or None on error - """ - responsecode, body = TasmotaGet('dl', host, port, username, password, contenttype='application/octet-stream') - - return body - - -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 port: - http port 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 - """ - if isinstance(encode_cfg, (bytes,bytearray)): - encode_cfg = str(encode_cfg) - - # get restore config page first to set internal Tasmota vars - responsecode, body = TasmotaGet('rs?', host, port, username, password, contenttype='text/html') - if body is None: - return responsecode, "ERROR" - - # ~ # post data - url = MakeUrl(host, port, "u2") - auth = None - if username is not None and password is not None: - auth = (username, password) - files = {'u2':('{sprog}_v{sver}.dmp'.format(sprog=os.path.basename(sys.argv[0]), sver=VER), encode_cfg)} - res = requests.post(url, auth=auth, files=files) - - if not res.ok: - exit(res.status_code, "Error on http POST request for {} - {}".format(url,res.reason), line=inspect.getlineno(inspect.currentframe())) - - if res.headers['Content-Type']!='text/html': - 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())) - - body = res.content - - findUpload = body.find("Upload") - if findUpload < 0: - return ExitCode.UPLOAD_CONFIG_ERROR, "Device did not response properly with upload result page" - - body = body[findUpload:] - findSuccessful = body.find("Successful") - if findSuccessful < 0: - errmatch = re.search("(\S*)

(.*)
", body) - reason = "Unknown error" - if errmatch and len(errmatch.groups()) > 1: - reason = errmatch.group(2) - return ExitCode.UPLOAD_CONFIG_ERROR, reason - - return 0, 'OK' - - -def DecryptEncrypt(obj): - """ - Decrpt/Encrypt binary config data - - @param obj: - binary config data - - @return: - decrypted configuration (if obj contains encrypted data) - """ - if isinstance(obj, (bytes,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 - - @param dobj: - decrypted binary config data - - @return: - 2 byte unsigned integer crc value - - """ - if isinstance(dobj, (bytes,bytearray)): - dobj = str(dobj) - version, size, setting = GetTemplateSetting(dobj) - if version < 0x06060007 or version > 0x0606000A: - size = 3584 - crc = 0 - for i in range(0, size): - if not i in [14,15]: # Skip crc - byte = ord(dobj[i]) - crc += byte * (i+1) - - return crc & 0xffff - - -def GetSettingsCrc32(dobj): - """ - Return binary config data calclulated crc32 - - @param dobj: - decrypted binary config data - - @return: - 4 byte unsigned integer crc value - - """ - if isinstance(dobj, (bytes,bytearray)): - dobj = str(dobj) - crc = 0 - for i in range(0, len(dobj)-4): - crc ^= ord(dobj[i]) - for j in range(0, 8): - crc = (crc >> 1) ^ (-int(crc & 1) & 0xEDB88320); - - return ~crc & 0xffffffff - - -def GetFieldDef(fielddef, fields="format_, addrdef, baseaddr, bits, bitshift, datadef, arraydef, validate, cmd, group, tasmotacmnd, converter, readconverter, writeconverter"): - - """ - Get field definition items - - @param fielddef: - field format - see "Settings dictionary" above - @param fields: - comma separated string list of values to be returned - possible values see fields default - - @return: - set of values defined in - """ - format_ = addrdef = baseaddr = datadef = arraydef = validate = cmd = group = tasmotacmnd = converter = readconverter = writeconverter = None - bits = bitshift = 0 - - # calling with nothing is wrong - if fielddef is None: - print(' is None', file=sys.stderr) - raise SyntaxError(' error') - - # get top level items - if len(fielddef) == 3: - # converter not present - format_, addrdef, datadef = fielddef - elif len(fielddef) == 4: - # converter present - format_, addrdef, datadef, converter = fielddef - else: - print('wrong {} length ({}) in setting'.format(fielddef, len(fielddef)), file=sys.stderr) - raise SyntaxError(' error') - - # ignore calls with 'root' setting - if isinstance(format_, instance(dict)) and baseaddr is None and datadef is None: - return eval(fields) - - if not isinstance(format_, instance((str,dict))): - print('wrong {} type {} in {}'.format(format_, type(format_), fielddef), file=sys.stderr) - raise SyntaxError(' error') - - # extract addrdef items - baseaddr = addrdef - if isinstance(baseaddr, instance((list,tuple))): - if len(baseaddr) == 3: - # baseaddr bit definition - baseaddr, bits, bitshift = baseaddr - if not isinstance(bits, instance(int)): - print(' must be defined as integer in {}'.format(bits, fielddef), file=sys.stderr) - raise SyntaxError(' error') - if not isinstance(bitshift, instance(int)): - print(' must be defined as integer in {}'.format(bitshift, fielddef), file=sys.stderr) - raise SyntaxError(' error') - else: - print('wrong {} length ({}) in {}'.format(addrdef, len(addrdef), fielddef), file=sys.stderr) - raise SyntaxError(' error') - if not isinstance(baseaddr, instance(int)): - print(' must be defined as integer in {}'.format(baseaddr, fielddef), file=sys.stderr) - raise SyntaxError(' error') - - # extract datadef items - arraydef = datadef - if isinstance(datadef, instance((tuple))): - if len(datadef) == 2: - # datadef has a validator - arraydef, validate = datadef - elif len(datadef) == 3: - # datadef has a validator and cmd set - arraydef, validate, cmd = datadef - # cmd must be a tuple with 2 objects - if isinstance(cmd, instance((tuple))) and len(cmd) == 2: - group, tasmotacmnd = cmd - if group is not None and not isinstance(group, instance(str)): - print('wrong {} in {}'.format(group, fielddef), file=sys.stderr) - raise SyntaxError(' error') - if tasmotacmnd is isinstance(tasmotacmnd, instance(tuple)): - tasmotacmnds = tasmotacmnd - for tasmotacmnd in tasmotacmnds: - if tasmotacmnd is not None and not callable(tasmotacmnd) and not isinstance(tasmotacmnd, instance(str)): - print('wrong {} in {}'.format(tasmotacmnd, fielddef), file=sys.stderr) - raise SyntaxError(' error') - else: - if tasmotacmnd is not None and not callable(tasmotacmnd) and not isinstance(tasmotacmnd, instance(str)): - print('wrong {} in {}'.format(tasmotacmnd, fielddef), file=sys.stderr) - raise SyntaxError(' error') - else: - print('wrong {} length ({}) in {}'.format(cmd, len(cmd), fielddef), file=sys.stderr) - raise SyntaxError(' error') - else: - print('wrong {} length ({}) in {}'.format(datadef, len(datadef), fielddef), file=sys.stderr) - raise SyntaxError(' error') - - if validate is not None and (not isinstance(validate, instance(str)) and not callable(validate)): - print('wrong {} type {} in {}'.format(validate, type(validate), fielddef), file=sys.stderr) - raise SyntaxError(' error') - - # convert single int into one-dimensional list - if isinstance(arraydef, instance(int)): - arraydef = [arraydef] - - if arraydef is not None and not isinstance(arraydef, instance((list))): - print('wrong {} type {} in {}'.format(arraydef, type(arraydef), fielddef), file=sys.stderr) - raise SyntaxError(' error') - - # get read/write converter items - readconverter = converter - if isinstance(converter, instance((tuple))): - if len(converter) == 2: - # converter has read/write converter - readconverter, writeconverter = converter - if readconverter is not None and not isinstance(readconverter, instance(str)) and not callable(readconverter): - print('wrong {} type {} in {}'.format(readconverter, type(readconverter), fielddef), file=sys.stderr) - raise SyntaxError(' error') - if writeconverter is not None and (not isinstance(writeconverter, instance((bool,str))) and not callable(writeconverter)): - print('wrong {} type {} in {}'.format(writeconverter, type(writeconverter), fielddef), file=sys.stderr) - raise SyntaxError(' error') - else: - print('wrong {} length ({}) in {}'.format(converter, len(converter), fielddef), file=sys.stderr) - raise SyntaxError(' error') - - - return eval(fields) - - -def ReadWriteConverter(value, fielddef, read=True, raw=False): - """ - Convert field value based on field desc - - @param value: - 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 - """ - converter, readconverter, writeconverter = GetFieldDef(fielddef, fields='converter, readconverter, writeconverter') - - # call password functions even if raw value should be processed - if read and callable(readconverter) and readconverter == passwordread: - raw = False - if not read and callable(writeconverter) and writeconverter == passwordwrite: - raw = False - - if not raw and converter is not None: - conv = readconverter if read else writeconverter - try: - if isinstance(conv, instance(str)): # evaluate strings - return eval(conv.replace('$','value')) - elif callable(conv): # use as format function - return conv(value) - except Exception as e: - exit(e.args[0], e.args[1], type_=LogType.WARNING, line=inspect.getlineno(inspect.currentframe())) - - return value - - -def CmndConverter(valuemapping, value, idx, fielddef): - """ - Convert field value into Tasmota command if available - - @param valuemapping: - data mapping - @param value: - original value - @param fielddef - field definition - see "Settings dictionary" above - - @return: - converted value, list of values or None if unable to convert - """ - converter, readconverter, writeconverter, group, tasmotacmnd = GetFieldDef(fielddef, fields='converter, readconverter, writeconverter, group, tasmotacmnd') - - result = None - - if (callable(readconverter) and readconverter == passwordread) or (callable(writeconverter) and writeconverter == passwordwrite): - if value == HIDDEN_PASSWORD: - return None - else: - result = value - - if tasmotacmnd is not None and (callable(tasmotacmnd) or len(tasmotacmnd) > 0): - if idx is not None: - idx += 1 - if isinstance(tasmotacmnd, instance(str)): # evaluate strings - if idx is not None: - evalstr = tasmotacmnd.replace('$','value').replace('#','idx').replace('@','valuemapping') - else: - evalstr = tasmotacmnd.replace('$','value').replace('@','valuemapping') - result = eval(evalstr) - - elif callable(tasmotacmnd): # use as format function - if idx is not None: - result = tasmotacmnd(value, idx) - else: - result = tasmotacmnd(value) - - return result - - -def ValidateValue(value, fielddef): - """ - Validate a value if validator is defined in fielddef - - @param value: - original value - @param fielddef - field definition - see "Settings dictionary" above - - @return: - True if value is valid, False if invalid - """ - validate = GetFieldDef(fielddef, fields='validate') - - if value == 0: - # can not complete all validate condition - # some Tasmota values are not allowed to be 0 on input - # even though these values are set to 0 on Tasmota initial. - # so we can't validate 0 values - return True; - - valid = True - try: - if isinstance(validate, instance(str)): # evaluate strings - valid = eval(validate.replace('$','value')) - elif callable(validate): # use as format function - valid = validate(value) - except: - valid = False - - return valid - - -def GetFormatCount(format_): - """ - Get format prefix count - - @param format_: - format specifier - - @return: - prefix count or 1 if not specified - """ - - if isinstance(format_, instance(str)): - match = re.search("\s*(\d+)", format_) - if match: - return int(match.group(0)) - - return 1 - - -def GetFormatType(format_): - """ - Get format type and bitsize without prefix - - @param format_: - format specifier - - @return: - (format_, 0) or (format without prefix, bitsize) - """ - - formattype = format_ - bitsize = 0 - if isinstance(format_, instance(str)): - match = re.search("\s*(\D+)", format_) - if match: - formattype = match.group(0) - bitsize = struct.calcsize(formattype) * 8 - return formattype, bitsize - - -def GetFieldMinMax(fielddef): - """ - Get minimum, maximum of field based on field format definition - - @param fielddef: - field format - see "Settings dictionary" above - - @return: - min, max - """ - minmax = {'c': (0, 0xff), - '?': (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_ = GetFieldDef(fielddef, fields='format_') - min_ = 0 - max_ = 0 - - if format_[-1:] in minmax: - min_, max_ = minmax[format_[-1:]] - max_ *= GetFormatCount(format_) - elif format_[-1:] in ['s','p']: - # s and p may have a prefix as length - max_ = GetFormatCount(format_) - - 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_, addrdef, arraydef = GetFieldDef(fielddef, fields='format_, addrdef, arraydef') - - # contains a integer list - if isinstance(arraydef, instance(list)) and len(arraydef) > 0: - # arraydef contains a list - # calc size recursive by sum of all elements - for i in range(0, arraydef[0]): - subfielddef = GetSubfieldDef(fielddef) - if len(arraydef) > 1: - length += GetFieldLength( (format_, addrdef, subfielddef) ) - # single array - else: - length += GetFieldLength( (format_, addrdef, None) ) - - elif isinstance(format_, instance(dict)): - # -> iterate through format - addr = None - setting = format_ - for name in setting: - baseaddr, bits, bitshift = GetFieldDef(setting[name], fields='baseaddr, bits, bitshift') - _len = GetFieldLength(setting[name]) - if addr != baseaddr: - addr = baseaddr - length += _len - - # a simple value - elif isinstance(format_, instance(str)): - length = struct.calcsize(format_) - - return length - - -def GetSubfieldDef(fielddef): - """ - Get subfield definition from a given field definition - - @param fielddef: - see Settings desc above - - @return: - subfield definition - """ - - format_, addrdef, datadef, arraydef, validate, cmd, converter = GetFieldDef(fielddef, fields='format_, addrdef, datadef, arraydef, validate, cmd, converter') - - # create new arraydef - if len(arraydef) > 1: - arraydef = arraydef[1:] - else: - arraydef = None - - # create new datadef - if isinstance(datadef, instance(tuple)): - if cmd is not None: - datadef = (arraydef, validate, cmd) - else: - datadef = (arraydef, validate) - else: - datadef = arraydef - - # set new field def - subfielddef = None - if converter is not None: - subfielddef = (format_, addrdef, datadef, converter) - else: - subfielddef = (format_, addrdef, datadef) - - return subfielddef - - -def IsFilterGroup(group): - """ - Check if group is valid on filter - - @param grooup: - group name to check - - @return: - True if group is in filter, otherwise False - """ - - if args.filter is not None: - if group is None: - return False - if group == '*': - return True - if group.title() != INTERNAL.title() and group.title() not in (groupname.title() for groupname in args.filter): - return False - return True - - -def GetFieldValue(fielddef, dobj, addr): - """ - Get single field value from definition - - @param fielddef: - see Settings desc - @param dobj: - decrypted binary config data - @param addr - addr within dobj - - @return: - value read from dobj - """ - - format_, bits, bitshift = GetFieldDef(fielddef, fields='format_, bits, bitshift') - - value_ = 0 - unpackedvalue = struct.unpack_from(format_, dobj, addr) - singletype, bitsize = GetFormatType(format_) - - if not format_[-1:].lower() in ['s','p']: - for val in unpackedvalue: - value_ <<= bitsize - value_ = value_ + val - value_ = bitsRead(value_, bitshift, bits) - else: - value_ = unpackedvalue[0] - s = str(value_).split('\0')[0] # use left string until \0 - value_ = unicode(s, errors='ignore') # remove character > 127 - - return value_ - - -def SetFieldValue(fielddef, dobj, addr, value): - """ - Set single field value from definition - - @param fielddef: - see Settings desc - @param dobj: - decrypted binary config data - @param addr - addr within dobj - @param value - new value - - @return: - new decrypted binary config data - """ - - format_, bits, bitshift = GetFieldDef(fielddef, fields='format_, bits, bitshift') - formatcnt = GetFormatCount(format_) - singletype, bitsize = GetFormatType(format_) - if debug(args) >= 2: - print("SetFieldValue(): fielddef {}, addr 0x{:04x} value {} formatcnt {} singletype {} bitsize {} ".format(fielddef,addr,value,formatcnt,singletype,bitsize), file=sys.stderr) - if not format_[-1:].lower() in ['s','p']: - addr += (bitsize / 8) * formatcnt - for _ in range(0, formatcnt): - addr -= (bitsize / 8) - maxunsigned = ((2**bitsize) - 1) - maxsigned = ((2**bitsize)>>1)-1 - val = value & maxunsigned - if isinstance(value,instance(int)) and value < 0 and val > maxsigned: - val = ((maxunsigned+1)-val) * (-1) - if debug(args) >= 3: - print("SetFieldValue(): Single type - fielddef {}, addr 0x{:04x} value {} singletype {} bitsize {}".format(fielddef,addr,val,singletype,bitsize), file=sys.stderr) - try: - struct.pack_into(singletype, dobj, addr, val) - except struct.error as e: - exit(ExitCode.RESTORE_DATA_ERROR, - "Single type {} [fielddef={}, addr=0x{:04x}, value={}] - skipped!".format(e,fielddef,addr,val), - type_=LogType.WARNING, - doexit=not args.ignorewarning, - line=inspect.getlineno(inspect.currentframe())) - value >>= bitsize - else: - if debug(args) >= 3: - print("SetFieldValue(): String type - fielddef {}, addr 0x{:04x} value {} format_ {}".format(fielddef,addr,value,format_), file=sys.stderr) - try: - struct.pack_into(format_, dobj, addr, value) - except struct.error as e: - exit(ExitCode.RESTORE_DATA_ERROR, - "String type {} [fielddef={}, addr=0x{:04x}, value={}} - skipped!".format(e,fielddef,addr,value), - type_=LogType.WARNING, - doexit=not args.ignorewarning, - line=inspect.getlineno(inspect.currentframe())) - - return dobj - - -def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0): - """ - Get field value from definition - - @param dobj: - decrypted binary config data - @param fieldname: - name of the field - @param fielddef: - see Settings desc above - @param raw - return raw values (True) or converted values (False) - @param addroffset - use offset for baseaddr (used for recursive calls) - - @return: - field mapping - """ - - if isinstance(dobj, instance((bytes,bytearray))): - dobj = str(dobj) - - valuemapping = None - - # get field definition - format_, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd = GetFieldDef(fielddef, fields='format_, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd') - - # filter groups - if not IsFilterGroup(group): - return valuemapping - - # contains a integer list - if isinstance(arraydef, instance(list)) and len(arraydef) > 0: - valuemapping = [] - offset = 0 - for i in range(0, arraydef[0]): - subfielddef = GetSubfieldDef(fielddef) - length = GetFieldLength(subfielddef) - if length != 0: - value = GetField(dobj, fieldname, subfielddef, raw=raw, addroffset=addroffset+offset) - valuemapping.append(value) - offset += length - - # contains a dict - elif isinstance(format_, instance(dict)): - mapping_value = {} - # -> iterate through format - for name in format_: - value = None - value = GetField(dobj, name, format_[name], raw=raw, addroffset=addroffset) - if value is not None: - mapping_value[name] = value - # copy complete returned mapping - valuemapping = copy.deepcopy(mapping_value) - - # a simple value - elif isinstance(format_, instance((str, bool, int, float, long))): - if GetFieldLength(fielddef) != 0: - valuemapping = ReadWriteConverter(GetFieldValue(fielddef, dobj, baseaddr+addroffset), fielddef, read=True, raw=raw) - - else: - exit(ExitCode.INTERNAL_ERROR, "Wrong mapping format definition: '{}'".format(format_), type_=LogType.WARNING, doexit=not args.ignorewarning, line=inspect.getlineno(inspect.currentframe())) - - return valuemapping - - -def SetField(dobj, fieldname, fielddef, restore, addroffset=0, filename=""): - """ - Get field value from definition - - @param dobj: - decrypted binary config data - @param fieldname: - name of the field - @param fielddef: - see Settings desc above - @param restore - restore mapping with the new value(s) - @param addroffset - use offset for baseaddr (used for recursive calls) - @param filename - related filename (for messages only) - - @return: - new decrypted binary config data - """ - format_, baseaddr, bits, bitshift, arraydef, group, writeconverter = GetFieldDef(fielddef, fields='format_, baseaddr, bits, bitshift, arraydef, group, writeconverter') - # cast unicode - fieldname = str(fieldname) - - # filter groups - if not IsFilterGroup(group): - return dobj - - # do not write readonly values - if writeconverter is False: - if debug(args) >= 2: - print("SetField(): Readonly '{}' using '{}'/{}{} @{} skipped".format(fieldname, format_, arraydef, bits, hex(baseaddr+addroffset)), file=sys.stderr) - return dobj - - # contains a list - if isinstance(arraydef, instance(list)) and len(arraydef) > 0: - offset = 0 - if len(restore) > arraydef[0]: - exit(ExitCode.RESTORE_DATA_ERROR, "file '{sfile}', array '{sname}[{selem}]' exceeds max number of elements [{smax}]".format(sfile=filename, sname=fieldname, selem=len(restore), smax=arraydef[0]), type_=LogType.WARNING, doexit=not args.ignorewarning, line=inspect.getlineno(inspect.currentframe())) - for i in range(0, arraydef[0]): - subfielddef = GetSubfieldDef(fielddef) - length = GetFieldLength(subfielddef) - if length != 0: - if i >= len(restore): # restore data list may be shorter than definition - break - subrestore = restore[i] - dobj = SetField(dobj, fieldname, subfielddef, subrestore, addroffset=addroffset+offset, filename=filename) - offset += length - - # contains a dict - elif isinstance(format_, instance(dict)): - for name in format_: # -> iterate through format - if name in restore: - dobj = SetField(dobj, name, format_[name], restore[name], addroffset=addroffset, filename=filename) - - # a simple value - elif isinstance(format_, instance((str, bool, int, float, long))): - valid = True - err = "" - errformat = "" - - min_, max_ = GetFieldMinMax(fielddef) - value = _value = None - skip = False - - # simple char value - if format_[-1:] in ['c']: - try: - value = ReadWriteConverter(restore.encode(STR_ENCODING)[0], fielddef, read=False) - except Exception as e: - exit(e.args[0], e.args[1], type_=LogType.WARNING, line=inspect.getlineno(inspect.currentframe())) - valid = False - - # bool - elif format_[-1:] in ['?']: - try: - value = ReadWriteConverter(bool(restore), fielddef, read=False) - except Exception as e: - exit(e.args[0], e.args[1], type_=LogType.WARNING, line=inspect.getlineno(inspect.currentframe())) - valid = False - - # integer - elif format_[-1:] in ['b','B','h','H','i','I','l','L','q','Q','P']: - value = ReadWriteConverter(restore, fielddef, read=False) - if isinstance(value, instance(str)): - value = int(value, 0) - else: - value = int(value) - # bits - if bits != 0: - bitvalue = value - value = struct.unpack_from(format_, dobj, baseaddr+addroffset)[0] - # validate restore value - valid = ValidateValue(bitvalue, fielddef) - if not valid: - err = "valid bit range exceeding" - value = bitvalue - else: - 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) - v=value - value &= (0xffffffff ^ mask) - value |= bitvalue - - # full size values - else: - # validate restore function - valid = ValidateValue(value, fielddef) - if not valid: - err = "valid range exceeding" - _value = value - - # float - elif format_[-1:] in ['f','d']: - try: - value = ReadWriteConverter(float(restore), fielddef, read=False) - except: - valid = False - - # string - elif format_[-1:] in ['s','p']: - value = ReadWriteConverter(restore.encode(STR_ENCODING), fielddef, read=False) - err = "string length exceeding" - if value is not None: - max_ -= 1 - valid = min_ <= len(value) <= max_ - else: - skip = True - valid = True - - if value is None and not skip: - # None is an invalid value - valid = False - - if valid is None and not skip: - # validate against object type size - valid = min_ <= value <= max_ - if not valid: - err = "type range exceeding" - errformat = " [{smin},{smax}]" - - if _value is None: - # copy value before possible change below - _value = value - - if isinstance(_value, instance(str)): - _value = "'{}'".format(_value) - - if valid: - if not skip: - if debug(args) >= 2: - sbits = " {} bits shift {}".format(bits, bitshift) if bits else "" - strvalue = "{} [{}]".format(_value, hex(value)) if isinstance(_value, instance(int)) else _value - print("SetField(): Set '{}' using '{}'/{}{} @{} to {}".format(fieldname, format_, arraydef, sbits, hex(baseaddr+addroffset), strvalue), file=sys.stderr) - if fieldname != 'cfg_crc' and fieldname != '_': - prevvalue = GetFieldValue(fielddef, dobj, baseaddr+addroffset) - dobj = SetFieldValue(fielddef, dobj, baseaddr+addroffset, value) - curvalue = GetFieldValue(fielddef, dobj, baseaddr+addroffset) - if prevvalue != curvalue and args.verbose: - message("Value for '{}' changed from {} to {}".format(fieldname, prevvalue, curvalue), type_=LogType.INFO) - else: - if debug(args) >= 2: - print("SetField(): Special field '{}' using '{}'/{}{} @{} skipped".format(fieldname, format_, arraydef, bits, hex(baseaddr+addroffset)), file=sys.stderr) - else: - sformat = "file '{sfile}' - {{'{sname}': {svalue}}} ({serror})"+errformat - exit(ExitCode.RESTORE_DATA_ERROR, sformat.format(sfile=filename, sname=fieldname, serror=err, svalue=_value, smin=min_, smax=max_), type_=LogType.WARNING, doexit=not args.ignorewarning) - - return dobj - - -def SetCmnd(cmnds, fieldname, fielddef, valuemapping, mappedvalue, addroffset=0, idx=None): - """ - Get field value from definition - - @param cmnds: - Tasmota command mapping: { 'group': ['cmnd' <,'cmnd'...>] ... } - @param fieldname: - name of the field - @param fielddef: - see Settings desc above - @param valuemapping: - data mapping - @param mappedvalue - mappedvalue mapping with the new value(s) - @param addroffset - use offset for baseaddr (used for recursive calls) - @param idx - optional array index - - @return: - new Tasmota command mapping - """ - format_, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd, writeconverter = GetFieldDef(fielddef, fields='format_, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd, writeconverter') - - # cast unicode - fieldname = str(fieldname) - - # filter groups - if not IsFilterGroup(group): - return cmnds - - # contains a list - if isinstance(arraydef, instance(list)) and len(arraydef) > 0: - offset = 0 - if len(mappedvalue) > arraydef[0]: - exit(ExitCode.RESTORE_DATA_ERROR, "array '{sname}[{selem}]' exceeds max number of elements [{smax}]".format(sname=fieldname, selem=len(mappedvalue), smax=arraydef[0]), type_=LogType.WARNING, doexit=not args.ignorewarning, line=inspect.getlineno(inspect.currentframe())) - for i in range(0, arraydef[0]): - subfielddef = GetSubfieldDef(fielddef) - length = GetFieldLength(subfielddef) - if length != 0: - if i >= len(mappedvalue): # mappedvalue data list may be shorter than definition - break - subrestore = mappedvalue[i] - cmnds = SetCmnd(cmnds, fieldname, subfielddef, valuemapping, subrestore, addroffset=addroffset+offset, idx=i) - offset += length - - # contains a dict - elif isinstance(format_, instance(dict)): - for name in format_: # -> iterate through format - if name in mappedvalue: - cmnds = SetCmnd(cmnds, name, format_[name], valuemapping, mappedvalue[name], addroffset=addroffset, idx=idx) - - # a simple value - elif isinstance(format_, instance((str, bool, int, float, long))): - if group is not None: - group = group.title(); - if isinstance(tasmotacmnd, instance(tuple)): - tasmotacmnds = tasmotacmnd - for tasmotacmnd in tasmotacmnds: - cmnd = CmndConverter(valuemapping, mappedvalue, idx, fielddef) - if group is not None and cmnd is not None: - if group not in cmnds: - cmnds[group] = [] - if isinstance(cmnd, instance(list)): - for c in cmnd: - cmnds[group].append(c) - else: - cmnds[group].append(cmnd) - else: - cmnd = CmndConverter(valuemapping, mappedvalue, idx, fielddef) - if group is not None and cmnd is not None: - if group not in cmnds: - cmnds[group] = [] - if isinstance(cmnd, instance(list)): - for c in cmnd: - cmnds[group].append(c) - else: - cmnds[group].append(cmnd) - - return cmnds - - -def Bin2Mapping(decode_cfg): - """ - Decodes binary data stream into pyhton mappings dict - - @param decode_cfg: - binary config data (decrypted) - - @return: - valuemapping data as mapping dictionary - """ - if isinstance(decode_cfg, instance((bytes,bytearray))): - decode_cfg = str(decode_cfg) - - # get binary header and template to use - version, size, setting = GetTemplateSetting(decode_cfg) - - # if we did not found a mathching setting - if setting is None: - exit(ExitCode.UNSUPPORTED_VERSION, "Tasmota configuration version {} not supported".format(version),line=inspect.getlineno(inspect.currentframe())) - - if 'version' in setting: - cfg_version = GetField(decode_cfg, 'version', setting['version'], raw=True) - - # check size if exists - if 'cfg_size' in setting: - cfg_size = GetField(decode_cfg, 'cfg_size', setting['cfg_size'], raw=True) - # read size should be same as definied in setting - if cfg_size > size: - # may be processed - exit(ExitCode.DATA_SIZE_MISMATCH, "Number of bytes read does ot match - read {}, expected {} byte".format(cfg_size, size), type_=LogType.ERROR,line=inspect.getlineno(inspect.currentframe())) - elif cfg_size < size: - # less number of bytes can not be processed - exit(ExitCode.DATA_SIZE_MISMATCH, "Number of bytes read to small to process - read {}, expected {} byte".format(cfg_size, size), type_=LogType.ERROR,line=inspect.getlineno(inspect.currentframe())) - - # check crc if exists - if 'cfg_crc' in setting: - cfg_crc = GetField(decode_cfg, 'cfg_crc', setting['cfg_crc'], raw=True) - else: - cfg_crc = GetSettingsCrc(decode_cfg) - if 'cfg_crc32' in setting: - cfg_crc32 = GetField(decode_cfg, 'cfg_crc32', setting['cfg_crc32'], raw=True) - else: - cfg_crc32 = GetSettingsCrc32(decode_cfg) - if version < 0x0606000B: - if cfg_crc != GetSettingsCrc(decode_cfg): - exit(ExitCode.DATA_CRC_ERROR, 'Data CRC error, read 0x{:4x} should be 0x{:4x}'.format(cfg_crc, GetSettingsCrc(decode_cfg)), type_=LogType.WARNING, doexit=not args.ignorewarning,line=inspect.getlineno(inspect.currentframe())) - else: - if cfg_crc32 != GetSettingsCrc32(decode_cfg): - exit(ExitCode.DATA_CRC_ERROR, 'Data CRC32 error, read 0x{:8x} should be 0x{:8x}'.format(cfg_crc32, GetSettingsCrc32(decode_cfg)), type_=LogType.WARNING, doexit=not args.ignorewarning,line=inspect.getlineno(inspect.currentframe())) - - # get valuemapping - valuemapping = GetField(decode_cfg, None, (setting,0,(None, None, (INTERNAL, None)))) - - # add header info - timestamp = datetime.now() - valuemapping['header'] = { 'timestamp':timestamp.strftime("%Y-%m-%d %H:%M:%S"), - 'format': { - 'jsonindent': args.jsonindent, - 'jsoncompact': args.jsoncompact, - 'jsonsort': args.jsonsort, - 'jsonhidepw': args.jsonhidepw, - }, - 'template': { - 'version': hex(version), - 'crc': hex(cfg_crc), - }, - 'data': { - 'crc': hex(GetSettingsCrc(decode_cfg)), - 'size': len(decode_cfg), - }, - 'script': { - 'name': os.path.basename(__file__), - 'version': VER, - }, - 'os': (platform.machine(), platform.system(), platform.release(), platform.version(), platform.platform()), - 'python': platform.python_version(), - } - if 'cfg_crc' in setting: - valuemapping['header']['template'].update({'size': cfg_size}) - if 'cfg_crc32' in setting: - valuemapping['header']['template'].update({'crc32': hex(cfg_crc32)}) - valuemapping['header']['data'].update({'crc32': hex(GetSettingsCrc32(decode_cfg))}) - if 'version' in setting: - valuemapping['header']['data'].update({'version': hex(cfg_version)}) - - return valuemapping - - -def Mapping2Bin(decode_cfg, jsonconfig, filename=""): - """ - Encodes into binary data stream - - @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) or None on error - """ - if isinstance(decode_cfg, instance(str)): - decode_cfg = bytearray(decode_cfg) - - - # get binary header data to use the correct version template from device - version, size, setting = GetTemplateSetting(decode_cfg) - - # make empty binarray array - _buffer = bytearray() - # add data - _buffer.extend(decode_cfg) - - if setting is not None: - # 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], addroffset=0, filename=filename) - else: - if name != 'header': - exit(ExitCode.RESTORE_DATA_ERROR, "Restore file '{}' contains obsolete name '{}', skipped".format(filename, name), type_=LogType.WARNING, doexit=not args.ignorewarning) - - if 'cfg_crc' in setting: - crc = GetSettingsCrc(_buffer) - struct.pack_into(setting['cfg_crc'][0], _buffer, setting['cfg_crc'][1], crc) - if 'cfg_crc32' in setting: - crc32 = GetSettingsCrc32(_buffer) - struct.pack_into(setting['cfg_crc32'][0], _buffer, setting['cfg_crc32'][1], crc32) - return _buffer - - else: - exit(ExitCode.UNSUPPORTED_VERSION,"File '{}', Tasmota configuration version 0x{:x} not supported".format(filename, version), type_=LogType.WARNING, doexit=not args.ignorewarning) - - return None - - -def Mapping2Cmnd(decode_cfg, valuemapping, filename=""): - """ - Encodes mapping data into Tasmota command mapping - - @param decode_cfg: - binary config data (decrypted) - @param valuemapping: - data mapping - @param filename: - name of the restore file (for error output only) - - @return: - Tasmota command mapping {group: [cmnd <,cmnd <,...>>]} - """ - if isinstance(decode_cfg, instance(str)): - decode_cfg = bytearray(decode_cfg) - - # get binary header data to use the correct version template from device - version, size, setting = GetTemplateSetting(decode_cfg) - - cmnds = {} - - if setting is not None: - # iterate through restore data mapping - for name in valuemapping: - # key must exist in both dict - if name in setting: - cmnds = SetCmnd(cmnds, name, setting[name], valuemapping, valuemapping[name], addroffset=0) - else: - if name != 'header': - exit(ExitCode.RESTORE_DATA_ERROR, "Restore file '{}' contains obsolete name '{}', skipped".format(filename, name), type_=LogType.WARNING, doexit=not args.ignorewarning) - - return cmnds - - else: - exit(ExitCode.UNSUPPORTED_VERSION,"File '{}', Tasmota configuration version 0x{:x} not supported".format(filename, version), type_=LogType.WARNING, doexit=not args.ignorewarning) - - return None - - -def Backup(backupfile, backupfileformat, encode_cfg, decode_cfg, configmapping): - """ - 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 configmapping: - config data mapppings - """ - - name, ext = os.path.splitext(backupfile) - if ext.lower() == '.'+FileType.BIN.lower(): - backupfileformat = FileType.BIN - elif ext.lower() == '.'+FileType.DMP.lower(): - backupfileformat = FileType.DMP - elif ext.lower() == '.'+FileType.JSON.lower(): - backupfileformat = FileType.JSON - - fileformat = "" - # Tasmota format - if backupfileformat.lower() == FileType.DMP.lower(): - fileformat = "Tasmota" - backup_filename = MakeFilename(backupfile, FileType.DMP, configmapping) - if args.verbose: - message("Writing backup file '{}' ({} format)".format(backup_filename, fileformat), type_=LogType.INFO) - try: - with open(backup_filename, "wb") as backupfp: - backupfp.write(encode_cfg) - except Exception as e: - exit(e.args[0], "'{}' {}".format(backup_filename, e[1]),line=inspect.getlineno(inspect.currentframe())) - - # binary format - elif backupfileformat.lower() == FileType.BIN.lower(): - fileformat = "binary" - backup_filename = MakeFilename(backupfile, FileType.BIN, configmapping) - if args.verbose: - message("Writing backup file '{}' ({} format)".format(backup_filename, fileformat), type_=LogType.INFO) - try: - with open(backup_filename, "wb") as backupfp: - backupfp.write(struct.pack('>]} - """ - def OutputTasmotaSubCmnds(cmnds): - if args.cmndsort: - for cmnd in sorted(cmnds, key = lambda cmnd:[int(c) if c.isdigit() else c for c in re.split('(\d+)', cmnd)]): - print("{}{}".format(" "*args.cmndindent, cmnd)) - else: - for cmnd in cmnds: - print("{}{}".format(" "*args.cmndindent, cmnd)) - - groups = GetGroupList(Settings[0][2]) - - if args.cmndgroup: - for group in groups: - if group.title() in (groupname.title() for groupname in tasmotacmnds): - cmnds = tasmotacmnds[group] - print - print("# {}:".format(group)) - OutputTasmotaSubCmnds(cmnds) - - else: - cmnds = [] - for group in groups: - if group.title() in (groupname.title() for groupname in tasmotacmnds): - cmnds.extend(tasmotacmnds[group]) - OutputTasmotaSubCmnds(cmnds) - -def ParseArgs(): - """ - Program argument parser - - @return: - configargparse.parse_args() result - """ - global parser - parser = configargparse.ArgumentParser(description='Backup/Restore Tasmota configuration data.', - epilog='Either argument -d or -f must be given.', - add_help=False, - formatter_class=lambda prog: CustomHelpFormatter(prog)) - - source = parser.add_argument_group('Source', 'Read/Write Tasmota configuration from/to') - source.add_argument('-f', '--file', '--tasmota-file', - metavar='', - dest='tasmotafile', - default=DEFAULTS['source']['tasmotafile'], - help="file to retrieve/write Tasmota configuration from/to (default: {})'".format(DEFAULTS['source']['tasmotafile'])) - source.add_argument('-d', '--device', '--host', - metavar='', - dest='device', - default=DEFAULTS['source']['device'], - help="hostname or IP address to retrieve/send Tasmota configuration from/to (default: {})".format(DEFAULTS['source']['device']) ) - source.add_argument('-P', '--port', - metavar='', - dest='port', - default=DEFAULTS['source']['port'], - help="TCP/IP port number to use for the host connection (default: {})".format(DEFAULTS['source']['port']) ) - source.add_argument('-u', '--username', - metavar='', - dest='username', - default=DEFAULTS['source']['username'], - help="host HTTP access username (default: {})".format(DEFAULTS['source']['username'])) - source.add_argument('-p', '--password', - metavar='', - dest='password', - default=DEFAULTS['source']['password'], - help="host HTTP access password (default: {})".format(DEFAULTS['source']['password'])) - - backup = parser.add_argument_group('Backup/Restore', 'Backup & restore specification') - backup.add_argument('-i', '--restore-file', - metavar='', - dest='restorefile', - default=DEFAULTS['backup']['backupfile'], - help="file to restore configuration from (default: {}). Replacements: @v=firmware version from config, @f=device friendly name from config, @h=device hostname from config, @H=device hostname from device (-d arg only)".format(DEFAULTS['backup']['restorefile'])) - backup.add_argument('-o', '--backup-file', - metavar='', - dest='backupfile', - default=DEFAULTS['backup']['backupfile'], - help="file to backup configuration to (default: {}). Replacements: @v=firmware version from config, @f=device friendly name from config, @h=device hostname from config, @H=device hostname from device (-d arg only)".format(DEFAULTS['backup']['backupfile'])) - backup_file_formats = ['json', 'bin', 'dmp'] - backup.add_argument('-t', '--backup-type', - metavar='|'.join(backup_file_formats), - dest='backupfileformat', - choices=backup_file_formats, - default=DEFAULTS['backup']['backupfileformat'], - help="backup filetype (default: '{}')".format(DEFAULTS['backup']['backupfileformat']) ) - backup.add_argument('-E', '--extension', - dest='extension', - action='store_true', - default=DEFAULTS['backup']['extension'], - help="append filetype extension for -i and -o filename{}".format(' (default)' if DEFAULTS['backup']['extension'] else '') ) - backup.add_argument('-e', '--no-extension', - dest='extension', - action='store_false', - default=DEFAULTS['backup']['extension'], - help="do not append filetype extension, use -i and -o filename as passed{}".format(' (default)' if not DEFAULTS['backup']['extension'] else '') ) - backup.add_argument('-F', '--force-restore', - dest='forcerestore', - action='store_true', - default=DEFAULTS['backup']['forcerestore'], - help="force restore even configuration is identical{}".format(' (default)' if DEFAULTS['backup']['forcerestore'] else '') ) - - jsonformat = parser.add_argument_group('JSON output', 'JSON format specification') - jsonformat.add_argument('--json-indent', - metavar='', - dest='jsonindent', - type=int, - default=DEFAULTS['jsonformat']['jsonindent'], - help="pretty-printed JSON output using indent level (default: '{}'). -1 disables indent.".format(DEFAULTS['jsonformat']['jsonindent']) ) - jsonformat.add_argument('--json-compact', - dest='jsoncompact', - action='store_true', - default=DEFAULTS['jsonformat']['jsoncompact'], - help="compact JSON output by eliminate whitespace{}".format(' (default)' if DEFAULTS['jsonformat']['jsoncompact'] else '') ) - - jsonformat.add_argument('--json-sort', - dest='jsonsort', - action='store_true', - default=DEFAULTS['jsonformat']['jsonsort'], - help=configargparse.SUPPRESS) #"sort json keywords{}".format(' (default)' if DEFAULTS['jsonformat']['jsonsort'] else '') ) - jsonformat.add_argument('--json-unsort', - dest='jsonsort', - action='store_false', - default=DEFAULTS['jsonformat']['jsonsort'], - help=configargparse.SUPPRESS) #"do not sort json keywords{}".format(' (default)' if not DEFAULTS['jsonformat']['jsonsort'] else '') ) - - jsonformat.add_argument('--json-hide-pw', - dest='jsonhidepw', - action='store_true', - default=DEFAULTS['jsonformat']['jsonhidepw'], - help="hide passwords{}".format(' (default)' if DEFAULTS['jsonformat']['jsonhidepw'] else '') ) - jsonformat.add_argument('--json-show-pw', '--json-unhide-pw', - dest='jsonhidepw', - action='store_false', - default=DEFAULTS['jsonformat']['jsonhidepw'], - help="unhide passwords{}".format(' (default)' if not DEFAULTS['jsonformat']['jsonhidepw'] else '') ) - - cmndformat = parser.add_argument_group('Tasmota command output', 'Tasmota command output format specification') - cmndformat.add_argument('--cmnd-indent', - metavar='', - dest='cmndindent', - type=int, - default=DEFAULTS['cmndformat']['cmndindent'], - help="Tasmota command grouping indent level (default: '{}'). 0 disables indent".format(DEFAULTS['cmndformat']['cmndindent']) ) - cmndformat.add_argument('--cmnd-groups', - dest='cmndgroup', - action='store_true', - default=DEFAULTS['cmndformat']['cmndgroup'], - help="group Tasmota commands{}".format(' (default)' if DEFAULTS['cmndformat']['cmndgroup'] else '') ) - cmndformat.add_argument('--cmnd-nogroups', - dest='cmndgroup', - action='store_false', - default=DEFAULTS['cmndformat']['cmndgroup'], - help="leave Tasmota commands ungrouped{}".format(' (default)' if not DEFAULTS['cmndformat']['cmndgroup'] else '') ) - cmndformat.add_argument('--cmnd-sort', - dest='cmndsort', - action='store_true', - default=DEFAULTS['cmndformat']['cmndsort'], - help="sort Tasmota commands{}".format(' (default)' if DEFAULTS['cmndformat']['cmndsort'] else '') ) - cmndformat.add_argument('--cmnd-unsort', - dest='cmndsort', - action='store_false', - default=DEFAULTS['cmndformat']['cmndsort'], - help="leave Tasmota commands unsorted{}".format(' (default)' if not DEFAULTS['cmndformat']['cmndsort'] else '') ) - - common = parser.add_argument_group('Common', 'Optional arguments') - common.add_argument('-c', '--config', - metavar='', - dest='configfile', - default=DEFAULTS['common']['configfile'], - is_config_file=True, - help="program config file - can be used to set default command args (default: {})".format(DEFAULTS['common']['configfile']) ) - - common.add_argument('-S', '--output', - dest='output', - action='store_true', - default=DEFAULTS['common']['output'], - help="display output regardsless of backup/restore usage{}".format(" (default)" if DEFAULTS['common']['output'] else " (default do not output on backup or restore usage)") ) - output_formats = ['json', 'cmnd','command'] - common.add_argument('-T', '--output-format', - metavar='|'.join(output_formats), - dest='outputformat', - choices=output_formats, - default=DEFAULTS['common']['outputformat'], - help="display output format (default: '{}')".format(DEFAULTS['common']['outputformat']) ) - groups = GetGroupList(Settings[0][2]) - if '*' in groups: - groups.remove('*') - common.add_argument('-g', '--group', - dest='filter', - choices=groups, - nargs='+', - type=lambda s : s.title(), - default=DEFAULTS['common']['filter'], - help="limit data processing to command groups (default {})".format("no filter" if DEFAULTS['common']['filter'] == None else DEFAULTS['common']['filter']) ) - common.add_argument('--ignore-warnings', - dest='ignorewarning', - action='store_true', - default=DEFAULTS['common']['ignorewarning'], - help="do not exit on warnings{}. Not recommended, used by your own responsibility!".format(' (default)' if DEFAULTS['common']['ignorewarning'] else '') ) - - - info = parser.add_argument_group('Info','Extra information') - info.add_argument('-D', '--debug', - dest='debug', - action='count', - help=configargparse.SUPPRESS) - info.add_argument('-h', '--help', - dest='shorthelp', - action='store_true', - help='show usage help message and exit') - info.add_argument("-H", "--full-help", - action="help", - help="show full help message and exit") - info.add_argument('-v', '--verbose', - dest='verbose', - action='store_true', - help='produce more output about what the program does') - info.add_argument('-V', '--version', - action='version', - version=PROG) - - args = parser.parse_args() - - if debug(args) >= 1: - print(parser.format_values(), file=sys.stderr) - print("Settings:", file=sys.stderr) - for k in args.__dict__: - print(" "+str(k), "= ",eval('args.{}'.format(k)), file=sys.stderr) - return args - - -if __name__ == "__main__": - args = ParseArgs() - if args.shorthelp: - ShortHelp() - - # check source args - if args.device is not None and args.tasmotafile is not None: - exit(ExitCode.ARGUMENT_ERROR, "Unable to select source, do not use -d and -f together",line=inspect.getlineno(inspect.currentframe())) - - # default no configuration available - encode_cfg = None - - # pull config from Tasmota device - if args.tasmotafile is not None: - if args.verbose: - message("Load data from file '{}'".format(args.tasmotafile), type_=LogType.INFO) - encode_cfg = LoadTasmotaConfig(args.tasmotafile) - - # load config from Tasmota file - if args.device is not None: - if args.verbose: - message("Load data from device '{}'".format(args.device), type_=LogType.INFO) - encode_cfg = PullTasmotaConfig(args.device, args.port, username=args.username, password=args.password) - - if encode_cfg is None: - # no config source given - ShortHelp(False) - print - print(parser.epilog) - sys.exit(ExitCode.OK) - - 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) - - # decode into mappings dictionary - configmapping = Bin2Mapping(decode_cfg) - if args.verbose and 'version' in configmapping: - message("{} '{}' is using Tasmota {}".format('File' if args.tasmotafile is not None else 'Device', - args.tasmotafile if args.tasmotafile is not None else args.device, - GetVersionStr(configmapping['version'])), - type_=LogType.INFO) - - # backup to file - if args.backupfile is not None: - Backup(args.backupfile, args.backupfileformat, encode_cfg, decode_cfg, configmapping) - - # restore from file - if args.restorefile is not None: - Restore(args.restorefile, args.backupfileformat, encode_cfg, decode_cfg, configmapping) - - # json screen output - if (args.backupfile is None and args.restorefile is None) or args.output: - if args.outputformat == 'json': - print(json.dumps(configmapping, sort_keys=args.jsonsort, indent=None if args.jsonindent<0 else args.jsonindent, separators=(',', ':') if args.jsoncompact else (', ', ': ') )) - - if args.outputformat == 'cmnd' or args.outputformat == 'command': - tasmotacmnds = Mapping2Cmnd(decode_cfg, configmapping) - OutputTasmotaCmnds(tasmotacmnds) - - sys.exit(exitcode) diff --git a/tools/fw_efm8bb1/RF-Bridge-EFM8BB1-20191006.hex b/tools/fw_efm8bb1/RF-Bridge-EFM8BB1-20191006.hex deleted file mode 100644 index c8cb43003..000000000 --- a/tools/fw_efm8bb1/RF-Bridge-EFM8BB1-20191006.hex +++ /dev/null @@ -1,491 +0,0 @@ -:020000040000FA -:100000000210D8A2029290A2029280AD40AC3F7F33 -:100010000A7E0012002E12001DA202B322E59120DA -:10002000E2FB220210487597A5222202141B8E4182 -:100030008F428C438D4412163112188EE54424BF32 -:100040009000E8F0E54334FF9000E7F09000E3E52E -:1000500041F0A3E542F043910422220211671219F4 -:100060002253D87853DAFE1218F3E4900087F02276 -:10007000D2DE2202146E1217C0C290C296C280E471 -:10008000FBFD7F101218DA12053174A4F0D2AF1202 -:10009000170AD2969000ECE004F070069000EBE0B6 -:1000A00004F09000EBE0B427E9A3E0B410E4C296BA -:1000B0001200263003091216B18E228F2380067596 -:1000C0002201752300E5237004E522640170469047 -:1000D00000DEE0700612170B0202959000EBE4754B -:1000E000F001120807FED3E5F09410EE94274002C9 -:1000F000D296D39000ECE094309000EBE0947550F1 -:1001000003020295E4F0A3F09000DEF0900099F075 -:10011000C29602029512170A9000DEE014602A14BB -:1001200070030202591470030202171470030202D2 -:100130003024046003020295E52364AA60030202EE -:10014000959000DE04F0020295E523900099F0906E -:1001500000DE7402F0E52312092F0201A0017FA145 -:10016000018BA501A9A601BDA701C6A801DDA901B2 -:10017000CCB001D5B1019DC00295FF0000020C1268 -:10018000051EE490008AF07FA1806512054E900064 -:10019000EA7408F0E4F52575240902029512054E6B -:1001A000E4F52575240202029590008A7401F07F1F -:1001B000A61215A690007974A6F0020295120531D8 -:1001C00074A4F00202959000EA7408F09000DE74C6 -:1001D00003F00202957FB112056202029512051E1C -:1001E00090008AE09000E9F090008A7401F07FA905 -:1001F0001205627D307C757F017E00121797020226 -:100200009512056B900079EFF0E48005E490009979 -:10021000F09000DEF0807EE4F525E523F524E5246A -:10022000D39400402C12005E9000DE7404F08065D0 -:1002300074032525F582E43400F583E523F00525D4 -:10024000E525B52402800AE525C39470404775244E -:10025000709000DE7402F0803CE5236455703690A7 -:1002600000DEF0C203900099E02460601B24FC6073 -:100270001224FE600E14600B24F760101460042436 -:100280001070127FA0121875D2038009900004E04C -:10029000049000EAF0900099E012092F02BEA10339 -:1002A00033A40377A50333A60447A802BEA9047CA0 -:1002B000B004E5B1041EC00442FF000000B090008D -:1002C00086E030E73F7DC87C0012053F900099E052 -:1002D0002457600E2408701F12056B7FA31215ECC3 -:1002E00080159000E9E090008AF0900079E0FF121C -:1002F00015A67FAB12057CE4900086F043DA01D2AC -:10030000030200B01218EB50207DE87C0312053F79 -:10031000900099E02457600C240860030200B07F2D -:10032000A20204D77FAA0204D71205744003020078 -:10033000B0802E900086E030E71F900099E0245AAC -:10034000600F240260030204F17FA41215EC020482 -:10035000F17FA612057C0204F1120574400302002D -:10036000B01216E7C006C0071216F69200D007D0EA -:10037000061209810200B09000DEE060030200B0C6 -:100380001217127003020411240560030200B01258 -:100390000558900005E0FFA3E090008BCFF0A3EF9D -:1003A000F0900007E0FFA3E090008DCFF0A3EFF006 -:1003B000900003E0FFA3E090008FCFF0A3EFF07B6D -:1003C000017A00798B901A0EE493FC740193FD90EE -:1003D0001A10E493F52E1217028E2FF530901A138F -:1003E000E493F5311217028E32F533901A16E49326 -:1003F000F5341217028E35F536901A19E493F53755 -:10040000A3E493F538753900753A09120EBC020061 -:10041000B09000EAE07063C2807FA00204D7900031 -:10042000DEE060030200B0900003E0FCA3E0FD7F8B -:10043000017E00121797D29612001DC2967FA0026D -:1004400004D77F030204D79000DEE060030200B00F -:100450001217126019240560030200B012055890AB -:100460000003E0FF7C007D041213570200B09000EF -:10047000EAE07006C2807FA0805D80619000DEE0CF -:1004800060030200B01217126043240560030200EB -:10049000B09000EAE014F012005E7E007F05C00616 -:1004A000C007900003E0FB25E0FFE433FE74052F56 -:1004B000F58274003EAD82FCEB25E0FFE52424FECE -:1004C000C39FFBD007D006120FA70200B09000EA2E -:1004D000E0700AC2807FA01218680200B0E49000A9 -:1004E00087F00200B0900086E030E7107FB1121173 -:1004F000ECE4900086F043DA010200B07E007FED6C -:1005000012151040030200B01216E7C006C0071211 -:1005100016F69200D007D006120D160200B07D32FA -:100520007C007F017E00121797D29612001DC296A2 -:1005300022E490008AF07FA41215A6900079227F11 -:10054000017E00121797D29612001DC29622120049 -:100550005E9000DE7404F0229000EAE014F01200D5 -:100560005E221215A6900079EFF022900079E0FF4C -:100570001215A6227E007FED12151022900086E053 -:10058000547FFD12155B228F29E4F536EF25E02517 -:10059000E02468F8E6701EEF25E025E02469F8768F -:1005A000087E007F707D007B017A00790312095577 -:1005B000E490007AF0AB2CAA2DA92EC001120B6793 -:1005C0002466F9E7540F120CA3120B86D001121403 -:1005D000C1503D120B672466F9E7540FFF120CA3BC -:1005E000120C90300101B34031EF700A900077E5B2 -:1005F0002AF0A3E52BF0120B67120CAF2401FFEFDA -:10060000FEEC54F04EFEEDFF120B67120C9A800ABE -:10061000120B672466F874F056F6AB2CAA2DA92E9F -:10062000C001120B67120CBB120BD1120CC7120BBC -:1006300086D0011214C15052120B67120CBB120B60 -:10064000D1FF120CC75408FE120C92300101B340C6 -:1006500043EF700A900088E52AF0A3E52BF0120B17 -:1006600067120BC22401FFE433FEEFC4F8540FC835 -:1006700068FFEEC454F048FEED540FFDEC4EFEED65 -:100680004FFF120B67120C9A800A120B672467F84F -:10069000740F56F6120B67120CAFFB700F120BCAD9 -:1006A000700A120B4D2469F8E4F6C322120B67247A -:1006B00068F8E53514667003753601C3E531953683 -:1006C0006B7020D290120B672466F874F056F674A3 -:1006D0000F0856120B662468F806120B672469F897 -:1006E00016804C120B67120BC2FFC3E53495366FB0 -:1006F000703DD290120B672466F8EC54F0FEED5476 -:100700000FA60608120B662468F806120B67246908 -:10071000F816120B67120BD8E0FF120B672469F86A -:10072000E6FE7401A806088002C333D8FC4FF0121D -:100730000B672469F8E67017120B67120BD8120CBE -:100740004CFF12182DEFF0120B672469F87608128F -:100750000B672468F8E6C3953540311218E350055D -:10076000E4900085F0120C4960181218F37D207C8B -:10077000037F017E0012176E120CD3A3E529F0440B -:1007800080F0120B4D2469F8E4F6D322C322BB019A -:100790000689828A83F0225002F722BBFE01F322EF -:1007A000EF8DF0A4A8F0CF8CF0A428CE8DF0A42E6D -:1007B000FE22BC000BBE0029EF8DF084FFADF022BD -:1007C000E4CCF875F008EF2FFFEE33FEEC33FCEECF -:1007D0009DEC984005FCEE9DFE0FD5F0E9E4CEFDC2 -:1007E00022EDF8F5F0EE8420D21CFEADF075F00895 -:1007F000EF2FFFED33FD4007985006D5F0F222C3EE -:1008000098FD0FD5F0EA22C5F0F8A3E028F0C5F076 -:10081000F8E582158270021583E038F022BB0110E2 -:10082000E58229F582E5833AF583E0F5F0A3E0223D -:100830005009E92582F886F008E622BBFE0AE92580 -:1008400082F8E2F5F008E222E5832AF583E993F5E0 -:10085000F0A3E9932275F008758200EF2FFFEE33C5 -:10086000FECD33CDCC33CCC58233C5829BED9AEC23 -:1008700099E58298400CF582EE9BFEED9AFDEC998D -:10088000FC0FD5F0D6E4CEFBE4CDFAE4CCF9A88297 -:1008900022B800C1B90059BA002DEC8BF084CFCE3C -:1008A000CDFCE5F0CBF97818EF2FFFEE33FEED33FA -:1008B000FDEC33FCEB33FB10D703994004EB99FBC1 -:1008C0000FD8E5E4F9FA227818EF2FFFEE33FEEDAA -:1008D00033FDEC33FCC933C910D7059BE99A4007B7 -:1008E000EC9BFCE99AF90FD8E0E4C9FAE4CCFB22CE -:1008F00075F010EF2FFFEE33FEED33FDCC33CCC897 -:1009000033C810D7079BEC9AE899400AED9BFDECA1 -:100910009AFCE899F80FD5F0DAE4CDFBE4CCFAE4E0 -:10092000C8F922A42582F582E5F03583F58322D02B -:1009300083D082F8E4937012740193700DA3A39393 -:10094000F8740193F5828883E4737402936860EF0E -:10095000A3A3A380DFEF4E6012EF60010EEDBB0199 -:100960000B89828A83F0A3DFFCDEFA2289F050072C -:10097000F709DFFCA9F022BBFEFCF309DFFCA9F0BC -:10098000228E268F27E4F528C3E5279464E5269474 -:10099000005017E4F528E528120B922469F8E4F6D4 -:1009A0000528E528B40FEFC2902290008AE0147069 -:1009B00003020A6F046003020B4C7866E6C4540F0E -:1009C000F97065D3E5279494E52694115003020B42 -:1009D0004C300003020B4CE9120C32E6540FFC08B9 -:1009E000E6FDEC4E18F6ED08F618120C29EC540F43 -:1009F0004E18F6ED08F6900001E526F0A3E527F085 -:100A0000AE26FF7C007D1F1207B290008BEEF0A394 -:100A1000EFF07C007D031207A0A3EEF0A3EFF0A39C -:100A2000E526F0A3E527F0227866E6C4540F6402B9 -:100A30006003020B4C120CEB752C01752D00752E0A -:100A40008B901BD793FE7401938E2FF530901BD99A -:100A5000E493F531A3120BBB8E32F533901BDCE42B -:100A600093F534901BE0E493F535E4FF020587E449 -:100A7000F528120BA8120D0CFFE528120C54F9EF03 -:100A8000C399506EE528120C16F5828C832FF582DF -:100A9000E43583F583E493FF120C90300001B350EA -:100AA00003020B40E528C454F024D1F582E4341B42 -:100AB000120C20F5828C83EF540775F00212092383 -:100AC000120C22FDAF27AE261214CBE528501925B3 -:100AD000E025E02466F8120C29EC540F4EFEEDFFE1 -:100AE000120BA8120C9A8058120B922469F8E4F6A3 -:100AF000804E120BA8120D0C697045120CEBE52804 -:100B0000120BAFAA06752CFF8A2DF52EE528120CC4 -:100B1000F6120BB98E2FF530E528120C63F531E58E -:100B200028120D01120BB98E32F533E528120C7222 -:100B3000F534E528120C81F535AF28120587400CF5 -:100B40000528E528C3940F5003020A7222C290E5DB -:100B50002925E025E02466F8E4F608F6E52925E0F5 -:100B600025E02468F8E4F6E52925E025E022F58370 -:100B7000E493FF5408FE131313541F24FF9202AB97 -:100B800029AA2AA92BEF540775F002A4F58285F053 -:100B9000832225E025E02466F8E4F608F6E528251A -:100BA000E025E02468F8E4F6E52825E025E022C405 -:100BB00054F024D1F582E4341BF583E493FE7401F0 -:100BC00093222466F8E6FC08E6FDECC4F854F0C86D -:100BD000EDC4540F48540F222468F8E6141313137D -:100BE000541F2403F582E43400F58322E529252BE4 -:100BF000F582E43528F583E022A200E433C43333E0 -:100C00003354804526FFE527900074CFF0A3EFF022 -:100C1000E490007BF022C454F024D4F582E4341B29 -:100C2000F583E493FC74019322E6FC08E6FDECC432 -:100C3000540F2401FFEFC454F0FE22E52E25E024DA -:100C40008BF582E43400F58322900085E0FF90006C -:100C50007AE06F22C454F024D6F582E4341BF58385 -:100C6000E49322C454F024D9F582E4341BF583E4E0 -:100C70009322C454F024DCF582E4341BF583E4931E -:100C800022C454F024E0F582E4341BF583E493227B -:100C90005408131313541F24FF222466F8A60608D1 -:100CA000A607222530F582E4352FF583E49322242C -:100CB00066F8E6FC08E6FDEC540F222466F9E7C46A -:100CC000F854F0C809E7222533F582E43532F5837C -:100CD000E4932290007AE0900085F053DAFE22251A -:100CE000E0247DF582E43400F58322A200920185A0 -:100CF000262A85272B22C454F024D7F582E4341BFE -:100D000022C454F024DAF582E4341B222466F8E687 -:100D1000FEEEC4540F228E268F27C3E5279464E588 -:100D20002694005003020EAA900087E024FE60255E -:100D3000147003020DB724036003020EAFC290AF1C -:100D400027AE2612185A4003020EAF120BF990007C -:100D5000877402F022120EB0503090007BE09404B1 -:100D60004021D290E4900000F0900073F0C2049013 -:100D7000007CF090007AF0900003F09000877403FC -:100D8000F08025E4900087F0801E900074E0547F8E -:100D9000FEA3E0FFD3E5279FE5269E4005120BF951 -:100DA000800690007BE004F090007BE0C394E0506C -:100DB00003020EAF020EAA90007CE004F090007BCC -:100DC000E0FFA3E0D39F4003020E587B007A007936 -:100DD00028AF27AE261212DF4022900000E0FF125B -:100DE0000C3DE526F0A3E527F08F28900000E004F5 -:100DF000F0E0D394074005E4900087F030041DA292 -:100E000000E433C43333335480FFE528C454F04F37 -:100E1000FF900073E0120BE2EFF08039900073E076 -:100E2000FF120BE2E0FEA200E43333333354F84503 -:100E300028FDEE4DF074032F120BE4120C4CFF1240 -:100E4000182DEFF0900073E004F0E0D394704005AB -:100E5000E4900087F0B20422120EB0504D1218E355 -:100E60005005E4900085F0120C49603A1218F37DA9 -:100E7000207C037F017E0012176E120CD3900003BA -:100E8000E0FD900074E05480FF900000E0C454F056 -:100E90004FFFED4F900003F0900074E0547FF0900E -:100EA0000086E04480F0C2908000E4900087F02249 -:100EB000A2009201AF27AE26121744228B298A2A5C -:100EC000892B8C2C8D2DE4F53D753E80F53BE53B63 -:100ED000C3952E5010E52D253BF582E4352C120FDD -:100EE0006C053B80E9E4F53BE53BC395385057E59D -:100EF0003A253DF582E43539F583E0553E7019F524 -:100F00003CE53CC39531502DE530253CF582E43578 -:100F10002F120F6C053C80E9E4F53CE53CC39534A9 -:100F20005013E533253CF582E43532120B6E120F77 -:100F30009B053C80E6E53EC313F53E7005053D7517 -:100F40003E80053B80A2E4F53BE53BC3953750135B -:100F5000E536253BF582E43535120B6E120F9B0505 -:100F60003B80E6C2909000877405F022F583E493FD -:100F7000FF5408FE131313541F24FF9202AB29AA37 -:100F80002AA92BEF540775F002A4F58285F083128D -:100F9000081DF54085F03F1200032212081DF540A0 -:100FA00085F03F120003228E268F278C288D298BF7 -:100FB0002AD200C201852982852883E05488D394EF -:100FC000004003D38001C39201E4F52BE52BC395C8 -:100FD0002A505930010D120BECC413131354012481 -:100FE000FF8002A2009202852782852683C083C0EB -:100FF00082120BECFFC45407D082D083121035301C -:10100000010C120BECFF131313541F138002A200E8 -:101010009202852782852683C083C082120BEC54FE -:1010200007D082D083121035052B80A0C29090008B -:10103000877405F02275F002120923E0F53FA3E062 -:10104000F540120003920022C0E0C0F0C083C082CD -:10105000C0D075D000C000C001C002C003C004C031 -:1010600005C006C007E5985403F545F45298E545D8 -:1010700030E01712192B9000DD121739EFF09000B5 -:10108000DDE004F0E0B44002E4F0E54530E12E900C -:1010900000E0E0D39400401A9000DCE02446F8E63B -:1010A000FF1219289000DCE004F09000E0E014F05A -:1010B0008002D2059000DCE0B42002E4F0D007D03A -:1010C00006D005D004D003D002D001D000D0D0D0BB -:1010D00082D083D0F0D0E03212005A787FE4F6D884 -:1010E000FD7581A1021122020076E493A3F8E49336 -:1010F000A34003F68001F208DFF48029E493A3F80B -:101100005407240CC8C333C4540F4420C88340047C -:10111000F456800146F6DFE4800B0102040810203B -:101120004080901266E47E019360BCA3FF543F3080 -:10113000E509541FFEE493A360010ECF54C025E0DF -:1011400060A840B8E493A3FAE493A3F8E493A3C897 -:10115000C582C8CAC583CAF0A3C8C582C8CAC58328 -:10116000CADFE9DEE780BEC0E0C0F0C083C082C055 -:10117000D075D000C000C001C002C003C004C005CB -:10118000C006C007E5D85487F521F452D8E5F730FA -:10119000E508E5F730E60312193253F7DFE52130B1 -:1011A000E708E5D930E003121931E52130E008E520 -:1011B000DA30E003121675E52130E108E5DB30E0B6 -:1011C00003121933E52130E208E5DC30E00312199F -:1011D00034D007D006D005D004D003D002D001D03F -:1011E00000D0D0D082D083D0F0D0E032AE07E4F58A -:1011F0002612180A900000E004FF12181112125F64 -:10120000900000E0FFE526C39F501412172BE05416 -:101210007FFF12181112172B121725052680E19057 -:101220000074E0547FFF12181190007412172512F9 -:10123000125FE4F526900073E0FFE526C39F501788 -:10124000740325261217190526E526541F70E61289 -:10125000192512191E80DE7F551218110219251248 -:10126000192512191E224200E500004200E100008B -:101270004200E700004200E30000C1834100860015 -:101280004100870041008A004100790042000100CE -:101290000042008800004200770000C10441007352 -:1012A000004100850041007A004100000048007DB7 -:1012B0000000000000000000410076004100DD0059 -:1012C0004100DF004100DB004100DC004100E000A4 -:1012D0004100DA00C1054100DE0041009900008EA6 -:1012E000298F2A8B2B8A2C892DE4F52E900000E083 -:1012F000FFE52EC39F505EE52AAE297803CEC313C7 -:10130000CE13D8F9FDAC06E52AAE297802CEC31378 -:10131000CE13D8F92DFFEE3CAB07FA120C3BE0FEE2 -:10132000A3E0FFC39BEE9A50028004AE02AF03AA73 -:1013300006AB07120C3BE0FCA3E0FDAF2AAE29127E -:1013400017E7500DAB2BAA2CA92DE52E12078ED333 -:1013500022052E8097C3228F268C278D28EF120B13 -:10136000AFAA06F97BFFEF120C16FDEF120C54F535 -:101370002EEF120CF6120BB98E2FF530EF120C6314 -:10138000F531EF120D01120BB98E32F533EF120C5D -:1013900072F534EFC454F024DDF582E4341B120BF3 -:1013A000B98E35F536EFC454F024DFF582E4341BF2 -:1013B000F583E493F537EF120C81F53885273985ED -:1013C000283A020EBC900076E0FDC4540F2401FBC5 -:1013D000E433FAED540FF96B7001EA603DE97010E7 -:1013E000E0C4540F2401FDE433FCED64044C602A96 -:1013F000900076E0C4540FFD540F120CDFEEF0A302 -:10140000EFF0ED04C454F049900076F0E0FFC454CE -:101410000FC394044004EF540FF022C0E0C083C017 -:1014200082C0D075D000C004C005C006C00753C834 -:101430007F9000E5E0FEA3E0FF4E700353C8FB90F1 -:1014400000E112166A50099000E5E4F0A3F0800D67 -:10145000C39000E6E09DF09000E5E09CF0D007D05E -:1014600006D005D004D0D0D082D083D0E032C0E006 -:10147000C083C082C0D075D000C004C005C006C003 -:101480000753917F9000E7E0FEA3E0FF4E70035307 -:1014900091FB9000E312166A50099000E7E4F0A374 -:1014A000F0800DC39000E8E09DF09000E7E09CF034 -:1014B000D007D006D005D004D0D0D082D083D0E0E1 -:1014C0003212081DFDACF0AF2BAE2A8F828E83AF97 -:1014D00005AE041218A6AB07AA06D3EB94F4EA945F -:1014E0000140067E017FF48004AE02AF03AA06AB82 -:1014F00007C3EB9464EA940050067E007F64800486 -:10150000AE02AF03AA06AB07AF82AE831217E72283 -:10151000AD07AC06ABDA900076E0FEC4540FFFEEE8 -:10152000540FFEB50702C32253DAFE900076E0FAAC -:10153000EE120CDFE0FFA3E08D828C83CFF0A3EFEF -:10154000F0EA54F0FF900076E0044FF0540FC3949B -:10155000044004E054F0F08BDAD322AE05AD07E48A -:10156000FCFB7FAA121811AF05121811EEC454F03B -:1015700024A6F582E4341DF583E493FFECC39F5069 -:101580000774082CFC0B80F4EB04FF12180CE4FC2D -:10159000ECC39B500974032C1217190C80F27F5571 -:1015A0001218110219258F26900079E0F5277E0088 -:1015B0007F3C7D007B007A0079661209557F0B1213 -:1015C000192E43DA011200707D0A7C007F017E0033 -:1015D00012179712001DE4900087F0900086F0909B -:1015E0000099E526F0900079F0AF2722AE07E4FDE0 -:1015F000F52612180A900001E0FF12181190000160 -:10160000121725900077E0FF12181190007712173B -:1016100025900088E0FF1218119000881217257499 -:10162000032D1217190DBD03F67F5512181102195B -:1016300025AB07AA06E4F9F87F407E427D0FFC1235 -:101640000891A804A905AA06AB077F207ED77D755F -:101650007C01120891C3E49FFFE49EFE22AB07AA1F -:1016600006E4F9F87FE87E03FD22E0FCA3E0FDC379 -:10167000EF9DEE9C22AFFBAEFC7C007D0A1207A022 -:10168000AD07AC06AFD953D9BFE4F5FAF5F98FD958 -:10169000C3EC948050157F002093027F01EFC43388 -:1016A000333354804CFEEDFF0213C5E4900076F016 -:1016B000229000DDE0FF9000DBE0B507057E017FB2 -:1016C00000229000DB121739E0FD7C009000DBE087 -:1016D00004F0E0B44002E4F09000DAE0FEEE4204F0 -:1016E000E4F0AE04AF05229000EDE0FCA3E0FDECD9 -:1016F000547FFEAF0522EC5480C41313135401240D -:10170000FF22A3E493FE74019322E49000EBF0A384 -:10171000F022900087E024FB22F582E43400F58378 -:10172000E0FF021811A3E0FF021811E52625E024CE -:101730008BF582E43400F58322E0249AF582E434C8 -:1017400000F58322AD07AC06900074E0FAA3E0FB3D -:10175000EA5480C413131354017005300102C322EC -:10176000AF05AE04EA547FFCAD031214CB228E37D2 -:101770008F388C398D3A12165D12163E12188290EF -:1017800000E5E539F0A3E53AF09000E1E537F0A394 -:10179000E538F043C804228E288F298C2A8D2B121D -:1017A000165D12163E12188E9000E7E52AF0A3E5AA -:1017B0002BF09000E3E528F0A3E529F04391042203 -:1017C00012002A1218FA1219011218B2121916125E -:1017D00018441218D01218BC1218C612189A1219EE -:1017E0000812191A02190FC3ED9BF582EC9AF583C2 -:1017F000C3E5829FE5839E500FED2BFDEC3AFCC3C1 -:10180000EF9DEE9C50028001C3227FAA121811AFF7 -:1018100006C2059000DFE0B42002E4F09000DFE0B3 -:101820002446F8A607E004F0A3E004F0227E1DE4BD -:10183000FDEF30E70625E06EFF8004EF25E0FF0DA9 -:10184000BD08EE22AF885388AF758CA0758DCBEFA5 -:101850005440FEEF54104E428822C3EF942CEE9475 -:10186000014003D38001C322121875D2039000797E -:10187000E0FF0215A6AE0712180A7F5512181102D2 -:101880001925AD07AC06ECF5CBAF058FCA22AD0725 -:10189000AC06ECF593AF058F9222C2DE75D90575C3 -:1018A000F9FF75960122EF7802CEC313CE13D8F953 -:1018B000FF2275E34075E10175E20122E5915404D0 -:1018C0005391FB429122758E547589224388502290 -:1018D000E5C8540453C8FB42C82253984FEB4F4D00 -:1018E000F59822E5C8C320E201D322E591C320E2A6 -:1018F00001D32253C8FB53C87F2275A41175D4CFDE -:101900002275A54175D5772253F77F75DA30227598 -:10191000E69075A8B022E4F5A9224398102230057C -:10192000FD22C2DE22D299228F9922AF99228F8C7A -:101930002222222222015E041A2A620802080109D8 -:1019400000019004B00BB81C520A03080109000101 -:10195000C20384286E020800090108014A02762A9F -:101960004E020800090108028A1E82071C0F8C081B -:101970000108020803016802D012C005DC0A03084E -:10198000010900024E05DC01AE348A0A0308010990 -:1019900000012C0A00008C047E27F60801080308C9 -:1019A0000208020803080401040A9A051428A00882 -:1019B000010800080208020800080301720438192F -:1019C00082020801080009029407D00FA02328080A -:1019D00003080108020801061802D000D208010A13 -:1019E0000109020A0101D603FC0BFE0208010800EE -:1019F00009019002D0132E010A00090108023007E4 -:101A0000760F322274030801080208193503193BC6 -:101A100002193D02193F020000001819410419493A -:101A200002194B02194D0200000018194F031955F5 -:101A30000219570219590200000018195B031961B5 -:101A4000021963021965020000000C196704196F7E -:101A50000219710219730200000028197504197D1A -:101A600002197F0219810200000028198304198BD2 -:101A700002198D02198F0200000018199105199B97 -:101A800002199D0419A10419A5022019A70419AF70 -:101A90000219B10419B50419B9022019BB0319C1FF -:101AA0000219C30219C5020000001119C70419CF99 -:101AB0000219D10219D30219D5022419D70319DD4D -:101AC0000219DF0219E10219E3022819E50319EBF3 -:101AD0000219ED0219EF020000001419F10319F7C1 -:101AE0000219F90219FB020000002719FD041A056A -:101AF000021A07021A090200000024015E041A2AD1 -:101B000062080208010900019004B00BB81C520AD7 -:101B1000030801090001C20384286E0208000901BC -:101B200008014A02762A4E020800090108028A1EAC -:101B300082071C0F8C080108020803016802D012FA -:101B4000C005DC0A0308010900024E05DC01AE34C1 -:101B50008A0A0308010900012C0A00008C047E2770 -:101B6000F608010803080208020803080401040A31 -:101B70009A051428A00801080008020802080008B5 -:101B8000030172043819820208010800090294074F -:101B9000D00FA02328080308010802080106180234 -:101BA000D000D208010A0109020A0101D603FC0B88 -:101BB000FE020801080009019002D0132E010A005C -:101BC000090108023007760F322274030801080267 -:101BD000081AFB031B01021B03021B050200000085 -:101BE000181B07041B0F021B11021B13020000002D -:101BF000181B15031B1B021B1D021B1F02000000EC -:101C0000181B21031B27021B29021B2B02000000AB -:101C10000C1B2D041B35021B37021B390200000070 -:101C2000281B3B041B43021B45021B47020000000C -:101C3000281B49041B51021B53021B5502000000C4 -:101C4000181B57051B61021B63041B67041B6B02F7 -:101C5000201B6D041B75021B77041B7B041B7F027A -:101C6000201B81031B87021B89021B8B02000000C3 -:101C7000111B8D041B95021B97021B99021B9B02D3 -:101C8000241B9D031BA3021BA5021BA7021BA90269 -:101C9000281BAB031BB1021BB3021BB502000000E3 -:101CA000141BB7031BBD021BBF021BC102000000B7 -:101CB000271BC3041BCB021BCD021BCF020000005D -:101CC00024015E041A2A6208020801090001900436 -:101CD000B00BB81C520A030801090001C203842892 -:101CE0006E020800090108014A02762A4E02080025 -:101CF000090108028A1E82071C0F8C0801080208CD -:101D000003016802D012C005DC0A030801090002C1 -:101D10004E05DC01AE348A0A0308010900012C0AD1 -:101D200000008C047E27F608010803080208020858 -:101D300003080401040A9A051428A00801080008F1 -:101D4000020802080008030172043819820208011F -:101D5000080009029407D00FA023280803080108EF -:101D6000020801061802D000D208010A0109020A7D -:101D70000101D603FC0BFE020801080009019002D4 -:101D8000D0132E010A00090108023007760F322213 -:101D9000740308010802081CC1031CC7021CC90205 -:101DA0001CCB02000000181CCD041CD5021CD7025D -:101DB0001CD902000000181CDB031CE1021CE3021A -:101DC0001CE502000000181CE7031CED021CEF02DA -:101DD0001CF1020000000C1CF3041CFB021CFD02A1 -:101DE0001CFF02000000281D01041D09021D0B023A -:101DF0001D0D02000000281D0F041D17021D1902F1 -:101E00001D1B02000000181D1D051D27021D2904B1 -:101E10001D2D041D3102201D33041D3B021D3D04F8 -:101E20001D41041D4502201D47031D4D021D4F028B -:101E30001D5102000000111D53041D5B021D5D02B7 -:101E40001D5F021D6102241D63031D69021D6B02DB -:101E50001D6D021D6F02281D71031D77021D790281 -:101E60001D7B02000000141D7D031D83021D8502E1 -:101E70001D8702000000271D89041D91021D930289 -:071E80001D95020000002483 -:00000001FF