diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0baecb42a..42cc68e3f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -47,10 +47,28 @@ The following binary downloads have been compiled with ESP8266/Arduino library c ## Changelog -### Version 7.1.2 Betty +### Version 7.2.0 Constance -- 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 with user Saturation control -- Change log buffer size from 520 to 700 characters accomodating full rule text (#7110) +- Change Exception reporting removing exception details from ``Status 1`` and consolidated in ``Status 12`` if available +- Change HTTP CORS from command ``SetOption73 0/1`` to ``Cors `` allowing user control of specific CORS domain by Shantur Rathore (#7066) +- Change GUI Shutter button text to Up and Down Arrows based on PR by Xavier Muller (#7166) +- Change amount of supported DHT sensors from 3 to 4 by Xavier Muller (#7167) +- Change some Settings locations freeing up space for future single char allowing variable length text +- Fix flashing H801 led at boot by Stefan Hadinger (#7165, #649) +- Fix duplicated ``Backlog`` when using Event inside a Backlog by Adrian Scillato (#7178, #7147) +- Fix Gui Timer when using a negative zero offset of -00:00 by Peter Ooms (#7174) +- Add command ``SerialConfig 0..23`` or ``SerialConfig 8N1`` to select Serial Config based in PR by Luis Teixeira (#7108) +- Add command ``Sensor34 9 `` to set minimum delta to trigger JSON message by @tobox (#7188) +- Add rule var ``%topic%`` by Adrian Scillato (#5522) +- Add rule triggers ``tele-wifi1#xxx`` by Adrian Scillato (#7093) +- Add SML bus decoder syntax support for byte order by Gerhard Mutz (#7112) +- Add experimental support for stepper motor shutter control by Stefan Bode +- Add optional USE_MQTT_TLS to tasmota-minimal.bin by Bohdan Kmit (#7115) +- Add save call stack in RTC memory in case of crash, command ``Status 12`` to dump the stack by Stefan Hadinger +- Add Home Assistant force update by Frederico Leoni (#7140, #7074) +- Add Wifi Signal Strength in dBm in addition to RSSI Wifi Experience by Andreas Schultz (#7145) +- Add Yaw, Pitch and Roll support for MPU6050 by Philip Barclay (#7058) +- Add reporting of raw weight to JSON from HX711 to overcome auto-tare functionality by @tobox (#7171) +- Add Zigbee support for Xiaomi Aqara Vibration Sensor and Presence Sensor by Stefan Hadinger +- Add Shutter functions ramp up/down and MQTT reporting by Stefan Bode +- Add fallback functionality from version 8.x diff --git a/lib/esp-knx-ip-0.5.2/esp-knx-ip.h b/lib/esp-knx-ip-0.5.2/esp-knx-ip.h index eb5ecf7b0..7c6377bec 100644 --- a/lib/esp-knx-ip-0.5.2/esp-knx-ip.h +++ b/lib/esp-knx-ip-0.5.2/esp-knx-ip.h @@ -32,8 +32,18 @@ #define DISABLE_RESTORE_BUTTON 1 // [Default 0] Set to 1 to disable the "restore defaults" button in the web ui. // These values normally don't need adjustment -#define MULTICAST_PORT 3671 // [Default 3671] +#ifndef MULTICAST_IP #define MULTICAST_IP IPAddress(224, 0, 23, 12) // [Default IPAddress(224, 0, 23, 12)] +#else +#warning USING CUSTOM MULTICAST_IP +#endif + +#ifndef MULTICAST_PORT +#define MULTICAST_PORT 3671 // [Default 3671] +#else +#warning USING CUSTOM MULTICAST_PORT +#endif + #define SEND_CHECKSUM 0 // Uncomment to enable printing out debug messages. diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index 9d8e5cb72..c56de751e 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -2,6 +2,56 @@ ## Released +### 7.2.0 20191221 + +- Release + +### 7.1.2.6 20191214 + +- Change some more Settings locations freeing up space for future single char allowing variable length text +- Add Zigbee send automatic ZigbeeRead after sending a command +- Add Zigbee improving Occupancy:false detection for Aqara sensor +- Add fallback functionality from version 8.x + +### 7.1.2.5 20191213 + +- Change some Settings locations freeing up space for future single char allowing variable length text +- Add Zigbee support for Xiaomi Aqara Vibration Sensor and Presence Sensor by Stefan Hadinger +- Add Shutter functions ramp up/down and MQTT reporting by Stefan Bode + +### 7.1.2.4 20191209 + +- Change HTTP CORS from command ``SetOption73 0/1`` to ``Cors `` allowing user control of specific CORS domain by Shantur Rathore (#7066) +- Change GUI Shutter button text to Up and Down Arrows based on PR by Xavier Muller (#7166) +- Change amount of supported DHT sensors from 3 to 4 by Xavier Muller (#7167) +- Revert removal of exception details from MQTT info on restart +- Add Wifi Signal Strength in dBm in addition to RSSI Wifi Experience by Andreas Schultz (#7145) +- Add Yaw, Pitch and Roll support for MPU6050 by Philip Barclay (#7058) +- Add reporting of raw weight to JSON from HX711 to overcome auto-tare functionality by @tobox (#7171) +- Add command ``Sensor34 9 `` to set minimum delta to trigger JSON message by @tobox (#7188) +- Fix flashing H801 led at boot by Stefan Hadinger (#7165, #649) +- Fix duplicated ``Backlog`` when using Event inside a Backlog by Adrian Scillato (#7178, #7147) +- Fix Gui Timer when using a negative zero offset of -00:00 by Peter Ooms (#7174) + +### 7.1.2.3 20191208 + +- Change Exception reporting removing exception details from both MQTT info and ``Status 1``. Now consolidated in ``Status 12`` if available. + +### 7.1.2.2 20191206 + +- Remove rule trigger ``tele_power1#state`` due to compatibility +- Add command ``SerialConfig 0..23`` or ``SerialConfig 8N1`` to select Serial Config based in PR by Luis Teixeira (#7108) +- Add save call stack in RTC memory in case of crash, command ``Status 12`` to dump the stack by Stefan Hadinger +- Add Home Assistant force update by Frederico Leoni (#7140, #7074) + +### 7.1.2.1 20191206 + +- Add SML bus decoder syntax support for byte order by Gerhard Mutz (#7112) +- Add rule var ``%topic%`` by Adrian Scillato (#5522) +- Add rule triggers ``tele_power1#state`` and multiple ``tele-wifi1#xxx`` by Adrian Scillato (#7093) +- Add experimental support for stepper motor shutter control by Stefan Bode +- Add optional USE_MQTT_TLS to tasmota-minimal.bin by Bohdan Kmit (#7115) + ### 7.1.2 20191206 - Maintenance Release diff --git a/tasmota/i18n.h b/tasmota/i18n.h index 5de4b7a1d..fed6930d7 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -134,6 +134,7 @@ #define D_JSON_SELECTED "selected" #define D_JSON_SERIALRECEIVED "SerialReceived" #define D_JSON_SET "Set" +#define D_JSON_SIGNAL "Signal" #define D_JSON_SSID "SSId" #define D_JSON_STARTDST "StartDST" // Start Daylight Savings Time #define D_JSON_STARTED "Started" @@ -208,6 +209,7 @@ #define D_STATUS9_MARGIN "PTH" #define D_STATUS10_SENSOR "SNS" #define D_STATUS11_STATUS "STS" + #define D_STATUS12_STATUS "STK" #define D_CMND_STATE "State" #define D_CMND_POWER "Power" #define D_CMND_FANSPEED "FanSpeed" @@ -285,6 +287,7 @@ #define D_CMND_SERIALSEND "SerialSend" #define D_CMND_SERIALDELIMITER "SerialDelimiter" #define D_CMND_BAUDRATE "Baudrate" +#define D_CMND_SERIALCONFIG "SerialConfig" #define D_CMND_TEMPLATE "Template" #define D_JSON_NAME "NAME" #define D_JSON_GPIO "GPIO" @@ -332,6 +335,7 @@ #define D_CMND_WEBSENSOR "WebSensor" #define D_CMND_EMULATION "Emulation" #define D_CMND_SENDMAIL "Sendmail" +#define D_CMND_CORS "CORS" // Commands xdrv_03_energy.ino #define D_CMND_POWERLOW "PowerLow" @@ -490,6 +494,24 @@ #define D_JSON_MOTOR_MIS "setMIS" #endif + // Commands xdrv_27_Shutter.ino + #ifdef USE_SHUTTER + #define D_PRFX_SHUTTER "Shutter" + #define D_CMND_SHUTTER_OPEN "Open" + #define D_CMND_SHUTTER_CLOSE "Close" + #define D_CMND_SHUTTER_STOP "Stop" + #define D_CMND_SHUTTER_POSITION "Position" + #define D_CMND_SHUTTER_OPENTIME "OpenDuration" + #define D_CMND_SHUTTER_CLOSETIME "CloseDuration" + #define D_CMND_SHUTTER_RELAY "Relay" + #define D_CMND_SHUTTER_SETHALFWAY "SetHalfway" + #define D_CMND_SHUTTER_SETCLOSE "SetClose" + #define D_CMND_SHUTTER_INVERT "Invert" + #define D_CMND_SHUTTER_CLIBRATION "Calibration" + #define D_CMND_SHUTTER_MOTORDELAY "MotorDelay" + #define D_CMND_SHUTTER_FREQUENCY "Frequency" + #endif + /********************************************************************************************/ // Log message prefix diff --git a/tasmota/language/bg-BG.h b/tasmota/language/bg-BG.h index 4bc5feebd..1f18d8efb 100644 --- a/tasmota/language/bg-BG.h +++ b/tasmota/language/bg-BG.h @@ -28,7 +28,7 @@ * Use online command StateText to translate ON, OFF, HOLD and TOGGLE. * Use online command Prefix to translate cmnd, stat and tele. * - * Updated until v6.5.0.8 + * Updated until v7.1.2.4 \*********************************************************************/ //#define LANGUAGE_MODULE_NAME // Enable to display "Module Generic" (ie Spanish), Disable to display "Generic Module" (ie English) @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Хладна" #define D_COMMAND "Команда" #define D_CONNECTED "Свързан" +#define D_CORS_DOMAIN "CORS домейн" #define D_COUNT "Брой" #define D_COUNTER "Брояч" #define D_CURRENT "Ток" // As in Voltage and Current @@ -113,7 +114,7 @@ #define D_LWT "LWT" #define D_MODULE "Модул" #define D_MQTT "MQTT" -#define D_MULTI_PRESS "множествено натискане" +#define D_MULTI_PRESS "неколкократно натискане" #define D_NOISE "Шум" #define D_NONE "Няма" #define D_OFF "Изкл." @@ -284,10 +285,10 @@ #define D_LOGGING_PARAMETERS "Параметри на лога" #define D_SERIAL_LOG_LEVEL "Степен на серийния лог" -#define D_MQTT_LOG_LEVEL "Mqtt log level" +#define D_MQTT_LOG_LEVEL "Степен на MQTT лога" #define D_WEB_LOG_LEVEL "Степен на уеб лога" #define D_SYS_LOG_LEVEL "Степен на системния лог" -#define D_MORE_DEBUG "Още дебъгване" +#define D_MORE_DEBUG "Допълнителна debug информация" #define D_SYSLOG_HOST "Хост на системния лог" #define D_SYSLOG_PORT "Порт на системния лог" #define D_TELEMETRY_PERIOD "Период на телеметрия" @@ -381,7 +382,7 @@ #define D_HUE "Hue" #define D_HUE_BRIDGE_SETUP "Настройка на Hue bridge" -#define D_HUE_API_NOT_IMPLEMENTED "Hue API не е внедрено" +#define D_HUE_API_NOT_IMPLEMENTED "Hue API не е внедрен" #define D_HUE_API "Hue API" #define D_HUE_POST_ARGS "Hue POST аргументи" #define D_3_RESPONSE_PACKETS_SENT "Изпратени са 3 пакета за отговор" @@ -443,17 +444,17 @@ #define D_ENERGY_TOTAL "Използвана енергия общо" // xdrv_27_shutter.ino -#define D_OPEN "Open" -#define D_CLOSE "Close" -#define D_DOMOTICZ_SHUTTER "Shutter" +#define D_OPEN "Отворена" +#define D_CLOSE "Затворена" +#define D_DOMOTICZ_SHUTTER "Щора" // xdrv_28_pcf8574.ino -#define D_CONFIGURE_PCF8574 "Configure PCF8574" -#define D_PCF8574_PARAMETERS "PCF8574 parameters" -#define D_INVERT_PORTS "Invert Ports" -#define D_DEVICE "Device" -#define D_DEVICE_INPUT "Input" -#define D_DEVICE_OUTPUT "Output" +#define D_CONFIGURE_PCF8574 "Конфигуриране на PCF8574" +#define D_PCF8574_PARAMETERS "PCF8574 параметри" +#define D_INVERT_PORTS "Обърни портовете" +#define D_DEVICE "Устройство" +#define D_DEVICE_INPUT "Вход" +#define D_DEVICE_OUTPUT "Изход" // xsns_05_ds18b20.ino #define D_SENSOR_BUSY "Датчикът DS18x20 е зает" @@ -674,27 +675,27 @@ #define D_UNIT_ANGLE "°" //SOLAXX1 -#define D_PV1_VOLTAGE "PV1 Voltage" -#define D_PV1_CURRENT "PV1 Current" -#define D_PV1_POWER "PV1 Power" -#define D_PV2_VOLTAGE "PV2 Voltage" -#define D_PV2_CURRENT "PV2 Current" -#define D_PV2_POWER "PV2 Power" -#define D_SOLAR_POWER "Solar Power" -#define D_INVERTER_POWER "Inverter Power" -#define D_STATUS "Status" -#define D_WAITING "Waiting" -#define D_CHECKING "Checking" -#define D_WORKING "Working" -#define D_FAILURE "Failure" -#define D_SOLAX_ERROR_0 "No Error Code" -#define D_SOLAX_ERROR_1 "Grid Lost Fault" -#define D_SOLAX_ERROR_2 "Grid Voltage Fault" -#define D_SOLAX_ERROR_3 "Grid Frequency Fault" -#define D_SOLAX_ERROR_4 "Pv Voltage Fault" -#define D_SOLAX_ERROR_5 "Isolation Fault" -#define D_SOLAX_ERROR_6 "Over Temperature Fault" -#define D_SOLAX_ERROR_7 "Fan Fault" -#define D_SOLAX_ERROR_8 "Other Device Fault" +#define D_PV1_VOLTAGE "Напрежение на PV1" +#define D_PV1_CURRENT "Ток на PV1" +#define D_PV1_POWER "Мощност на PV1" +#define D_PV2_VOLTAGE "Напрежение на PV2" +#define D_PV2_CURRENT "Ток на PV2" +#define D_PV2_POWER "Мощност на PV2" +#define D_SOLAR_POWER "Слънчева мощност" +#define D_INVERTER_POWER "Мощност на инвертера" +#define D_STATUS "Състояние" +#define D_WAITING "Очакване" +#define D_CHECKING "Проверка" +#define D_WORKING "Работи" +#define D_FAILURE "Грешка" +#define D_SOLAX_ERROR_0 "Грешка - няма код" +#define D_SOLAX_ERROR_1 "Грешка - загуба на мрежата" +#define D_SOLAX_ERROR_2 "Грешка - мрежово напрежение" +#define D_SOLAX_ERROR_3 "Грешка - мрежова честота" +#define D_SOLAX_ERROR_4 "Грешка - напрежение на Pv" +#define D_SOLAX_ERROR_5 "Грешка - проблем с изолацията" +#define D_SOLAX_ERROR_6 "Грешка - прегряване" +#define D_SOLAX_ERROR_7 "Грешка - вентилатор" +#define D_SOLAX_ERROR_8 "Грешка - друго оборудване" #endif // _LANGUAGE_BG_BG_H_ diff --git a/tasmota/language/cs-CZ.h b/tasmota/language/cs-CZ.h index 8800368a3..1d7c6f381 100644 --- a/tasmota/language/cs-CZ.h +++ b/tasmota/language/cs-CZ.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Studené světlo" #define D_COMMAND "Příkaz" #define D_CONNECTED "...připojeno" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Počítej" #define D_COUNTER "Počítadlo" #define D_CURRENT "Proud" // As in Voltage and Current diff --git a/tasmota/language/de-DE.h b/tasmota/language/de-DE.h index c50de823f..6569dd232 100644 --- a/tasmota/language/de-DE.h +++ b/tasmota/language/de-DE.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "kalt" #define D_COMMAND "Befehl" #define D_CONNECTED "verbunden" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "zählen" #define D_COUNTER "Zähler" #define D_CURRENT "Strom" // As in Voltage and Current diff --git a/tasmota/language/el-GR.h b/tasmota/language/el-GR.h index 4fe47ae6d..f8b9c9614 100644 --- a/tasmota/language/el-GR.h +++ b/tasmota/language/el-GR.h @@ -72,6 +72,7 @@ #define D_COMMAND "Εντολή" #define D_CONNECTED "Συνδεδεμένο" #define D_COUNT "Μέτρηση" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNTER "Μετρητής" #define D_CURRENT "Ένταση" // As in Voltage and Current #define D_DATA "Δεδομένα" diff --git a/tasmota/language/en-GB.h b/tasmota/language/en-GB.h index a98087a18..257a34c86 100644 --- a/tasmota/language/en-GB.h +++ b/tasmota/language/en-GB.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Cold" #define D_COMMAND "Command" #define D_CONNECTED "Connected" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Count" #define D_COUNTER "Counter" #define D_CURRENT "Current" // As in Voltage and Current diff --git a/tasmota/language/es-ES.h b/tasmota/language/es-ES.h index 014d822a9..7527bb77a 100644 --- a/tasmota/language/es-ES.h +++ b/tasmota/language/es-ES.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Fría" #define D_COMMAND "Comando" #define D_CONNECTED "Conectado" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Conteo" #define D_COUNTER "Contador" #define D_CURRENT "Corriente" // As in Voltage and Current diff --git a/tasmota/language/fr-FR.h b/tasmota/language/fr-FR.h index 4704c49e7..e107409e3 100644 --- a/tasmota/language/fr-FR.h +++ b/tasmota/language/fr-FR.h @@ -28,7 +28,7 @@ * Use online command StateText to translate ON, OFF, HOLD and TOGGLE. * Use online command Prefix to translate cmnd, stat and tele. * - * Updated until v6.6.0.15 + * Updated until v7.1.2.4 \*********************************************************************/ #define LANGUAGE_MODULE_NAME // Enable to display "Module Generic" (ie Spanish), Disable to display "Generic Module" (ie English) @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Froid" #define D_COMMAND "Commande" #define D_CONNECTED "Connecté" +#define D_CORS_DOMAIN "Domaine CORS" #define D_COUNT "Compte" #define D_COUNTER "Compteur" #define D_CURRENT "Courant" // As in Voltage and Current @@ -280,7 +281,7 @@ #define D_MQTT_PARAMETERS "Paramètres MQTT" #define D_CLIENT "Client" -#define D_FULL_TOPIC "topic complet" +#define D_FULL_TOPIC "Topic complet" #define D_LOGGING_PARAMETERS "Paramètres du journal" #define D_SERIAL_LOG_LEVEL "Niveau de journalisation série" @@ -687,7 +688,7 @@ #define D_CHECKING "En test" #define D_WORKING "En marche" #define D_FAILURE "Défault" -#define D_SOLAX_ERROR_0 "Aucun Code d'erreur" +#define D_SOLAX_ERROR_0 "Aucun code d'erreur" #define D_SOLAX_ERROR_1 "Défaut Perte de réseau" #define D_SOLAX_ERROR_2 "Défaut Tension réseau" #define D_SOLAX_ERROR_3 "Défaut Fréquence réseau" diff --git a/tasmota/language/he-HE.h b/tasmota/language/he-HE.h index 69387ceca..ac4dadb8e 100644 --- a/tasmota/language/he-HE.h +++ b/tasmota/language/he-HE.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "אור קר" #define D_COMMAND "פקודה" #define D_CONNECTED "מחובר" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "סופר" #define D_COUNTER "מונה" #define D_CURRENT "נוכחי" // As in Voltage and Current diff --git a/tasmota/language/hu-HU.h b/tasmota/language/hu-HU.h index 58f85eb22..62806fa46 100644 --- a/tasmota/language/hu-HU.h +++ b/tasmota/language/hu-HU.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Hideg fény" #define D_COMMAND "Parancs" #define D_CONNECTED "Csatlakoztatva" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Szám" #define D_COUNTER "Számláló" #define D_CURRENT "Áramerősség" // As in Voltage and Current diff --git a/tasmota/language/it-IT.h b/tasmota/language/it-IT.h index 3029f37da..0585c12d8 100644 --- a/tasmota/language/it-IT.h +++ b/tasmota/language/it-IT.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Fredda" #define D_COMMAND "Comando" #define D_CONNECTED "Connesso" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Conteggio" #define D_COUNTER "Contatore" #define D_CURRENT "Corrente" // As in Voltage and Current diff --git a/tasmota/language/ko-KO.h b/tasmota/language/ko-KO.h index c6236a05f..bff508474 100644 --- a/tasmota/language/ko-KO.h +++ b/tasmota/language/ko-KO.h @@ -72,6 +72,7 @@ #define D_COMMAND "커맨드" #define D_CONNECTED "연결됨" #define D_COUNT "횟수" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNTER "Counter" #define D_CURRENT "전류" // As in Voltage and Current #define D_DATA "Data" diff --git a/tasmota/language/nl-NL.h b/tasmota/language/nl-NL.h index da20ff1d7..75d841ddc 100644 --- a/tasmota/language/nl-NL.h +++ b/tasmota/language/nl-NL.h @@ -72,6 +72,7 @@ #define D_COMMAND "Opdracht" #define D_CONNECTED "Verbonden" #define D_COUNT "Aantal" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNTER "Teller" #define D_CURRENT "Stroom" // As in Voltage and Current #define D_DATA "Data" diff --git a/tasmota/language/pl-PL.h b/tasmota/language/pl-PL.h index 7a631f95f..8f5af6813 100644 --- a/tasmota/language/pl-PL.h +++ b/tasmota/language/pl-PL.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Zimny" #define D_COMMAND "Komenda" #define D_CONNECTED "Połączony" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Licz" #define D_COUNTER "Licznik" #define D_CURRENT "Prąd" // As in Voltage and Current diff --git a/tasmota/language/pt-BR.h b/tasmota/language/pt-BR.h index 2eb81f294..0efa57789 100644 --- a/tasmota/language/pt-BR.h +++ b/tasmota/language/pt-BR.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Luz fria" #define D_COMMAND "Comando" #define D_CONNECTED "Ligado" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Contagem" #define D_COUNTER "Contador" #define D_CURRENT "Corrente" // As in Voltage and Current diff --git a/tasmota/language/pt-PT.h b/tasmota/language/pt-PT.h index 46bafdef6..a6299c0c8 100644 --- a/tasmota/language/pt-PT.h +++ b/tasmota/language/pt-PT.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Luz Fria" #define D_COMMAND "Comando" #define D_CONNECTED "Ligado" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Contagem" #define D_COUNTER "Contador" #define D_CURRENT "Corrente" // As in Voltage and Current diff --git a/tasmota/language/ru-RU.h b/tasmota/language/ru-RU.h index 5ff9bff50..3bd788ede 100644 --- a/tasmota/language/ru-RU.h +++ b/tasmota/language/ru-RU.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Холодный" #define D_COMMAND "Команда" #define D_CONNECTED "Соединен" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Подсчет" #define D_COUNTER "Счетчик" #define D_CURRENT "Ток" // As in Voltage and Current diff --git a/tasmota/language/sk-SK.h b/tasmota/language/sk-SK.h index 57ec74f8a..a64e49e01 100644 --- a/tasmota/language/sk-SK.h +++ b/tasmota/language/sk-SK.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Studené svetlo" #define D_COMMAND "Príkaz" #define D_CONNECTED "...pripojené" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Počítaj" #define D_COUNTER "Počítadlo" #define D_CURRENT "Prúd" // As in Voltage and Current diff --git a/tasmota/language/sv-SE.h b/tasmota/language/sv-SE.h index aee8fd25d..51ffc2ae3 100644 --- a/tasmota/language/sv-SE.h +++ b/tasmota/language/sv-SE.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Kallt" #define D_COMMAND "Kommando" #define D_CONNECTED "Ansluten" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Räkna" #define D_COUNTER "Räknare" #define D_CURRENT "Ström" // As in Voltage and Current diff --git a/tasmota/language/tr-TR.h b/tasmota/language/tr-TR.h index 78612dace..6bc48c40f 100644 --- a/tasmota/language/tr-TR.h +++ b/tasmota/language/tr-TR.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Soğuk" #define D_COMMAND "Komut" #define D_CONNECTED "Bağlandı" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Sayı" #define D_COUNTER "Sayaç" #define D_CURRENT "Current" // As in Voltage and Current diff --git a/tasmota/language/uk-UK.h b/tasmota/language/uk-UK.h index f16d66648..b4a314630 100644 --- a/tasmota/language/uk-UK.h +++ b/tasmota/language/uk-UK.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "Холодний" #define D_COMMAND "Команда" #define D_CONNECTED "Під'єднано" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "Розмір" #define D_COUNTER "Лічильник" #define D_CURRENT "Струм" // As in Voltage and Current diff --git a/tasmota/language/zh-CN.h b/tasmota/language/zh-CN.h index 2c8610400..21aa63be3 100644 --- a/tasmota/language/zh-CN.h +++ b/tasmota/language/zh-CN.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "冷" #define D_COMMAND "命令:" #define D_CONNECTED "已连接" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "数量:" #define D_COUNTER "计数器" #define D_CURRENT "电流" // As in Voltage and Current diff --git a/tasmota/language/zh-TW.h b/tasmota/language/zh-TW.h index e5ef8926a..1d78f181e 100644 --- a/tasmota/language/zh-TW.h +++ b/tasmota/language/zh-TW.h @@ -71,6 +71,7 @@ #define D_COLDLIGHT "冷" #define D_COMMAND "命令:" #define D_CONNECTED "已連接" +#define D_CORS_DOMAIN "CORS Domain" #define D_COUNT "數量:" #define D_COUNTER "Counter" #define D_CURRENT "電流" // As in Voltage and Current diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 7ef154d53..5ff6bb6a2 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -132,6 +132,7 @@ #define WEB_PASSWORD "" // [WebPassword] Web server Admin mode Password for WEB_USERNAME (empty string = Disable) #define FRIENDLY_NAME "Tasmota" // [FriendlyName] Friendlyname up to 32 characters used by webpages and Alexa #define EMULATION EMUL_NONE // [Emulation] Select Belkin WeMo (single relay/light) or Hue Bridge emulation (multi relay/light) (EMUL_NONE, EMUL_WEMO or EMUL_HUE) +#define CORS_DOMAIN "" // [Cors] CORS Domain for preflight requests // -- HTTP GUI Colors ----------------------------- // HTML hex color codes. Only 3 and 6 digit hex string values are supported!! See https://www.w3schools.com/colors/colors_hex.asp @@ -219,7 +220,7 @@ #define APP_BLINKTIME 10 // [BlinkTime] Time in 0.1 Sec to blink/toggle power for relay 1 #define APP_BLINKCOUNT 10 // [BlinkCount] Number of blinks (0 = 32000) #define APP_SLEEP 0 // [Sleep] Sleep time to lower energy consumption (0 = Off, 1 - 250 mSec), -#define PWM_MAX_SLEEP 10 // Sleep will be lowered to this value when light is on, to avoid flickering +#define PWM_MAX_SLEEP 10 // Sleep will be lowered to this value when light is on, to avoid flickering #define KEY_DEBOUNCE_TIME 50 // [ButtonDebounce] Number of mSeconds button press debounce time #define KEY_HOLD_TIME 40 // [SetOption32] Number of 0.1 seconds to hold Button or external Pushbutton before sending HOLD message @@ -284,14 +285,14 @@ #define DOMOTICZ_OUT_TOPIC "domoticz/out" // Domoticz Output Topic // -- MQTT - Home Assistant Discovery ------------- -#define USE_HOME_ASSISTANT // Enable Home Assistant Discovery Support (+7k code) +#define USE_HOME_ASSISTANT // Enable Home Assistant Discovery Support (+4.1k code, +6 bytes mem) #define HOME_ASSISTANT_DISCOVERY_PREFIX "homeassistant" // Home Assistant discovery prefix // -- MQTT - TLS - AWS IoT ------------------------ // Using TLS starting with version v6.5.0.16 compilation will only work using Core 2.4.2 and 2.5.2. No longer supported: 2.3.0 //#define USE_MQTT_TLS // Use TLS for MQTT connection (+34.5k code, +7.0k mem and +4.8k additional during connection handshake) // #define USE_MQTT_TLS_CA_CERT // Force full CA validation instead of fingerprints, slower, but simpler to use. (+2.2k code, +1.9k mem during connection handshake) - // This includes the LetsEncrypt CA in tasmota_ca.ino for verifying server certificates + // This includes the LetsEncrypt CA in tasmota_ca.ino for verifying server certificates // #define USE_MQTT_TLS_FORCE_EC_CIPHER // Force Elliptic Curve cipher (higher security) required by some servers (automatically enabled with USE_MQTT_AWS_IOT) (+11.4k code, +0.4k mem) // #define USE_MQTT_AWS_IOT // Enable MQTT for AWS IoT - requires a private key (+11.9k code, +0.4k mem) // Note: you need to generate a private key + certificate per device and update 'tasmota/tasmota_aws_iot.cpp' diff --git a/tasmota/settings.h b/tasmota/settings.h index 554711cad..af74422df 100644 --- a/tasmota/settings.h +++ b/tasmota/settings.h @@ -86,7 +86,7 @@ typedef union { // Restricted by MISRA-C Rule 18.4 bu uint32_t energy_weekend : 1; // bit 20 (v6.6.0.8) - CMND_TARIFF uint32_t dds2382_model : 1; // bit 21 (v6.6.0.14) - SetOption71 - Select different Modbus registers for Active Energy (#6531) uint32_t hardware_energy_total : 1; // bit 22 (v6.6.0.15) - SetOption72 - Enable hardware energy total counter as reference (#6561) - uint32_t cors_enabled : 1; // bit 23 (v7.0.0.1) - SetOption73 - Enable HTTP CORS + uint32_t ex_cors_enabled : 1; // bit 23 (v7.0.0.1) - SetOption73 - Enable HTTP CORS uint32_t ds18x20_internal_pullup : 1; // bit 24 (v7.0.0.1) - SetOption74 - Enable internal pullup for single DS18x20 sensor uint32_t grouptopic_mode : 1; // bit 25 (v7.0.0.1) - SetOption75 - GroupTopic replaces %topic% (0) or fixed topic cmnd/grouptopic (1) uint32_t bootcount_update : 1; // bit 26 (v7.0.0.4) - SetOption76 - Enable incrementing bootcount when deepsleep is enabled @@ -248,42 +248,54 @@ struct SYSCFG { SysBitfield flag; // 010 int16_t save_data; // 014 int8_t timezone; // 016 + + // Start of single char array Settings.text + char ota_url[101]; // 017 char mqtt_prefix[3][11]; // 07C + uint8_t ex_baudrate; // 09D - Free since 6.6.0.9 - uint8_t seriallog_level; // 09E - uint8_t sta_config; // 09F - uint8_t sta_active; // 0A0 + uint8_t ex_seriallog_level; // 09E + uint8_t ex_sta_config; // 09F + uint8_t ex_sta_active; // 0A0 + char sta_ssid[2][33]; // 0A1 - Keep together with sta_pwd as being copied as one chunck with reset 5 char sta_pwd[2][65]; // 0E3 - Keep together with sta_ssid as being copied as one chunck with reset 5 char hostname[33]; // 165 char syslog_host[33]; // 186 - uint8_t rule_stop; // 1A7 - uint16_t syslog_port; // 1A8 - uint8_t syslog_level; // 1AA - uint8_t webserver; // 1AB - uint8_t weblog_level; // 1AC - uint8_t mqtt_fingerprint[2][20]; // 1AD - uint8_t adc_param_type; // 1D5 - uint8_t free_1d6[10]; // 1D6 + uint8_t ex_rule_stop; // 1A7 + uint16_t ex_syslog_port; // 1A8 + uint8_t ex_syslog_level; // 1AA + uint8_t ex_webserver; // 1AB + uint8_t ex_weblog_level; // 1AC + uint8_t ex_mqtt_fingerprint[2][20]; // 1AD + uint8_t ex_adc_param_type; // 1D5 - SysBitfield4 flag4; // 1E0 + uint8_t ex_free_1d6[10]; // 1D6 - uint8_t free_1e4; // 1E4 + // End of single char array of 456 chars max (phase 3) + + SysBitfield4 ex_flag4; // 1E0 + uint8_t ex_serial_config; // 1E4 + uint8_t ex_wifi_output_power; // 1E5 + uint8_t ex_shutter_accuracy; // 1E6 + uint8_t ex_mqttlog_level; // 1E7 + uint8_t ex_sps30_inuse_hours; // 1E8 - uint8_t wifi_output_power; // 1E5 - uint8_t shutter_accuracy; // 1E6 - uint8_t mqttlog_level; // 1E7 - uint8_t sps30_inuse_hours; // 1E8 char mqtt_host[33]; // 1E9 - Keep together with below as being copied as one chunck with reset 6 - uint16_t mqtt_port; // 20A - Keep together + + uint16_t ex_mqtt_port; // 20A - Keep together + char mqtt_client[33]; // 20C - Keep together char mqtt_user[33]; // 22D - Keep together char mqtt_pwd[33]; // 24E - Keep together char mqtt_topic[33]; // 26F - Keep together with above items as being copied as one chunck with reset 6 char button_topic[33]; // 290 char mqtt_grptopic[33]; // 2B1 + + // Optional end of single char array of 698 chars max (phase 5) + uint8_t display_model; // 2D2 uint8_t display_mode; // 2D3 uint8_t display_refresh; // 2D4 @@ -304,7 +316,9 @@ struct SYSCFG { int16_t toffset[2]; // 30E uint8_t display_font; // 312 char state_text[4][11]; // 313 + uint8_t ex_energy_power_delta; // 33F - Free since 6.6.0.20 + uint16_t domoticz_update_timer; // 340 uint16_t pwm_range; // 342 unsigned long domoticz_relay_idx[MAX_DOMOTICZ_IDX]; // 344 @@ -340,7 +354,7 @@ struct SYSCFG { char friendlyname[MAX_FRIENDLYNAMES][33]; // 3AC char switch_topic[33]; // 430 char serial_delimiter; // 451 - uint8_t ex_sbaudrate; // 452 - Free since 6.6.0.9 + uint8_t seriallog_level; // 452 uint8_t sleep; // 453 uint16_t domoticz_switch_idx[MAX_DOMOTICZ_IDX]; // 454 uint16_t domoticz_sensor_idx[MAX_DOMOTICZ_SNS_IDX]; // 45C @@ -400,7 +414,6 @@ struct SYSCFG { uint16_t baudrate; // 778 uint16_t sbaudrate; // 77A EnergyUsage energy_usage; // 77C -// uint32_t drivers[3]; // 794 - 6.5.0.12 replaced by below three entries uint32_t adc_param1; // 794 uint32_t adc_param2; // 798 int adc_param3; // 79C @@ -416,7 +429,9 @@ struct SYSCFG { unsigned long energy_frequency_calibration; // 7C8 also used by HX711 to save last weight uint16_t web_refresh; // 7CC char mems[MAX_RULE_MEMS][10]; // 7CE + char rules[MAX_RULE_SETS][MAX_RULE_SIZE]; // 800 uses 512 bytes in v5.12.0m, 3 x 512 bytes in v5.14.0b + TuyaFnidDpidMap tuya_fnid_map[MAX_TUYA_FUNCTIONS]; // E00 32 bytes uint16_t ina226_r_shunt[4]; // E20 uint16_t ina226_i_fs[4]; // E28 @@ -435,12 +450,27 @@ struct SYSCFG { uint16_t energy_power_delta; // E98 uint8_t shutter_motordelay[MAX_SHUTTERS]; // E9A int8_t temp_comp; // E9E - - uint8_t free_e9f[1]; // E9F - + uint8_t weight_change; // E9F uint8_t web_color2[2][3]; // EA0 - Needs to be on integer / 3 distance from web_color + char cors_domain[33]; // EA6 + uint8_t sta_config; // EC7 + uint8_t sta_active; // EC8 + uint8_t rule_stop; // EC9 + uint16_t syslog_port; // ECA + uint8_t syslog_level; // ECC + uint8_t webserver; // ECD + uint8_t weblog_level; // ECE + uint8_t mqtt_fingerprint[2][20]; // ECF + uint8_t adc_param_type; // EF7 + SysBitfield4 flag4; // EF8 + uint16_t mqtt_port; // EFC + uint8_t serial_config; // EFE + uint8_t wifi_output_power; // EFF + uint8_t shutter_accuracy; // F00 + uint8_t mqttlog_level; // F01 + uint8_t sps30_inuse_hours; // F02 - uint8_t free_ea4[326]; // EA6 + uint8_t free_f03[233]; // F03 uint32_t i2c_drivers[3]; // FEC I2cDriver uint32_t cfg_timestamp; // FF8 diff --git a/tasmota/settings.ino b/tasmota/settings.ino index 9ca979b45..d5c59246f 100644 --- a/tasmota/settings.ino +++ b/tasmota/settings.ino @@ -140,6 +140,10 @@ #ifndef DEFAULT_LIGHT_COMPONENT #define DEFAULT_LIGHT_COMPONENT 255 #endif +#ifndef CORS_ENABLED_ALL +#define CORS_ENABLED_ALL "*" +#endif + enum WebColors { COL_TEXT, COL_BACKGROUND, COL_FORM, @@ -156,6 +160,23 @@ const char kWebColors[] PROGMEM = COLOR_BUTTON_TEXT "|" COLOR_BUTTON "|" COLOR_BUTTON_HOVER "|" COLOR_BUTTON_RESET "|" COLOR_BUTTON_RESET_HOVER "|" COLOR_BUTTON_SAVE "|" COLOR_BUTTON_SAVE_HOVER "|" COLOR_TIMER_TAB_TEXT "|" COLOR_TIMER_TAB_BACKGROUND "|" COLOR_TITLE_TEXT; +enum TasmotaSerialConfig { + TS_SERIAL_5N1, TS_SERIAL_6N1, TS_SERIAL_7N1, TS_SERIAL_8N1, + TS_SERIAL_5N2, TS_SERIAL_6N2, TS_SERIAL_7N2, TS_SERIAL_8N2, + TS_SERIAL_5E1, TS_SERIAL_6E1, TS_SERIAL_7E1, TS_SERIAL_8E1, + TS_SERIAL_5E2, TS_SERIAL_6E2, TS_SERIAL_7E2, TS_SERIAL_8E2, + TS_SERIAL_5O1, TS_SERIAL_6O1, TS_SERIAL_7O1, TS_SERIAL_8O1, + TS_SERIAL_5O2, TS_SERIAL_6O2, TS_SERIAL_7O2, TS_SERIAL_8O2 }; + +const uint8_t kTasmotaSerialConfig[] PROGMEM = { + SERIAL_5N1, SERIAL_6N1, SERIAL_7N1, SERIAL_8N1, + SERIAL_5N2, SERIAL_6N2, SERIAL_7N2, SERIAL_8N2, + SERIAL_5E1, SERIAL_6E1, SERIAL_7E1, SERIAL_8E1, + SERIAL_5E2, SERIAL_6E2, SERIAL_7E2, SERIAL_8E2, + SERIAL_5O1, SERIAL_6O1, SERIAL_7O1, SERIAL_8O1, + SERIAL_5O2, SERIAL_6O2, SERIAL_7O2, SERIAL_8O2 +}; + /*********************************************************************************************\ * RTC memory \*********************************************************************************************/ @@ -421,6 +442,197 @@ void UpdateQuickPowerCycle(bool update) } } +/*********************************************************************************************\ + * Config Settings.text char array support +\*********************************************************************************************/ + +char aws_mqtt_host[66]; +char aws_mqtt_user[1] { 0 }; + +const uint32_t settings_text_size = 457; // Settings.flag4 (1E0) - Settings.ota_url (017) + +uint32_t GetSettingsTextLen(void) +{ + char* position = Settings.ota_url; + for (uint32_t size = 0; size < SET_MAX; size++) { + while (*position++ != '\0') { } + } + return position - Settings.ota_url; +} + +bool SettingsUpdateText(uint32_t index, const char* replace_me) +{ + if (index >= SET_MAX) { + return false; // Setting not supported - internal error + } + + // Make a copy first in case we use source from Settings.text + uint32_t replace_len = strlen(replace_me); + char replace[replace_len +1]; + memcpy(replace, replace_me, sizeof(replace)); + + if (Settings.version < 0x08000000) { + uint32_t idx = 0; + switch (index) { + case SET_OTAURL: strlcpy(Settings.ota_url, replace, sizeof(Settings.ota_url)); break; + case SET_MQTTPREFIX3: idx++; + case SET_MQTTPREFIX2: idx++; + case SET_MQTTPREFIX1: strlcpy(Settings.mqtt_prefix[idx], replace, sizeof(Settings.mqtt_prefix[idx])); break; + case SET_STASSID2: idx++; + case SET_STASSID1: strlcpy(Settings.sta_ssid[idx], replace, sizeof(Settings.sta_ssid[idx])); break; + case SET_STAPWD2: idx++; + case SET_STAPWD1: strlcpy(Settings.sta_pwd[idx], replace, sizeof(Settings.sta_pwd[idx])); break; + case SET_HOSTNAME: strlcpy(Settings.hostname, replace, sizeof(Settings.hostname)); break; + case SET_SYSLOG_HOST: strlcpy(Settings.syslog_host, replace, sizeof(Settings.syslog_host)); break; + case SET_WEBPWD: strlcpy(Settings.web_password, replace, sizeof(Settings.web_password)); break; +#if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) + case SET_MQTT_HOST: + if (strlen(replace) <= sizeof(Settings.mqtt_host)) { + strlcpy(Settings.mqtt_host, replace, sizeof(Settings.mqtt_host)); + Settings.mqtt_user[0] = 0; + } else { + // need to split in mqtt_user first then mqtt_host + strlcpy(Settings.mqtt_user, replace, sizeof(Settings.mqtt_user)); + strlcpy(Settings.mqtt_host, &replace[sizeof(Settings.mqtt_user)-1], sizeof(Settings.mqtt_host)); + } + break; + case SET_MQTT_USER: break; +#else + case SET_MQTT_HOST: strlcpy(Settings.mqtt_host, replace, sizeof(Settings.mqtt_host)); break; + case SET_MQTT_USER: strlcpy(Settings.mqtt_user, replace, sizeof(Settings.mqtt_user)); break; +#endif + case SET_MQTT_CLIENT: strlcpy(Settings.mqtt_client, replace, sizeof(Settings.mqtt_client)); break; + case SET_MQTT_PWD: strlcpy(Settings.mqtt_pwd, replace, sizeof(Settings.mqtt_pwd)); break; + case SET_MQTT_FULLTOPIC: strlcpy(Settings.mqtt_fulltopic, replace, sizeof(Settings.mqtt_fulltopic)); break; + case SET_MQTT_TOPIC: strlcpy(Settings.mqtt_topic, replace, sizeof(Settings.mqtt_topic)); break; + case SET_MQTT_BUTTON_TOPIC: strlcpy(Settings.button_topic, replace, sizeof(Settings.button_topic)); break; + case SET_MQTT_SWITCH_TOPIC: strlcpy(Settings.switch_topic, replace, sizeof(Settings.switch_topic)); break; + case SET_MQTT_GRP_TOPIC: strlcpy(Settings.mqtt_grptopic, replace, sizeof(Settings.mqtt_grptopic)); break; + case SET_STATE_TXT4: idx++; + case SET_STATE_TXT3: idx++; + case SET_STATE_TXT2: idx++; + case SET_STATE_TXT1: strlcpy(Settings.state_text[idx], replace, sizeof(Settings.state_text[idx])); break; + case SET_NTPSERVER3: idx++; + case SET_NTPSERVER2: idx++; + case SET_NTPSERVER1: strlcpy(Settings.ntp_server[idx], replace, sizeof(Settings.ntp_server[idx])); break; + case SET_MEM5: idx++; + case SET_MEM4: idx++; + case SET_MEM3: idx++; + case SET_MEM2: idx++; + case SET_MEM1: strlcpy(Settings.mems[idx], replace, sizeof(Settings.mems[idx])); break; + case SET_CORS: strlcpy(Settings.cors_domain, replace, sizeof(Settings.cors_domain)); break; + case SET_FRIENDLYNAME4: idx++; + case SET_FRIENDLYNAME3: idx++; + case SET_FRIENDLYNAME2: idx++; + case SET_FRIENDLYNAME1: strlcpy(Settings.friendlyname[idx], replace, sizeof(Settings.friendlyname[idx])); break; + } + } else { + uint32_t start_pos = 0; + uint32_t end_pos = 0; + char* position = Settings.ota_url; + for (uint32_t size = 0; size < SET_MAX; size++) { + while (*position++ != '\0') { } + if (1 == index) { + start_pos = position - Settings.ota_url; + } + else if (0 == index) { + end_pos = position - Settings.ota_url -1; + } + index--; + } + uint32_t char_len = position - Settings.ota_url; + + uint32_t current_len = end_pos - start_pos; + int diff = replace_len - current_len; + + // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("TST: start %d, end %d, len %d, current %d, replace %d, diff %d"), + // start_pos, end_pos, char_len, current_len, replace_len, diff); + + int too_long = (char_len + diff) - settings_text_size; + if (too_long > 0) { + // AddLog_P2(LOG_LEVEL_INFO, PSTR("CFG: Text too long by %d char(s)"), too_long); + return false; // Replace text too long + } + + if (diff != 0) { + // Shift Settings.text up or down + memmove_P(Settings.ota_url + start_pos + replace_len, Settings.ota_url + end_pos, char_len - end_pos); + } + // Replace text + memmove_P(Settings.ota_url + start_pos, replace, replace_len); + // Fill for future use + memset(Settings.ota_url + char_len + diff, 0x00, settings_text_size - char_len - diff); + } + + return true; +} + +char* SettingsText(uint32_t index) +{ + if (index >= SET_MAX) { + return nullptr; // Setting not supported - internal error + } + + char* position = Settings.ota_url; + + if (Settings.version < 0x08000000) { + uint32_t idx = 0; + switch (index) { + case SET_MQTTPREFIX3: idx++; + case SET_MQTTPREFIX2: idx++; + case SET_MQTTPREFIX1: position = Settings.mqtt_prefix[idx]; break; + case SET_STASSID2: idx++; + case SET_STASSID1: position = Settings.sta_ssid[idx]; break; + case SET_STAPWD2: idx++; + case SET_STAPWD1: position = Settings.sta_pwd[idx]; break; + case SET_HOSTNAME: position = Settings.hostname; break; + case SET_SYSLOG_HOST: position = Settings.syslog_host; break; + case SET_WEBPWD: position = Settings.web_password; break; +#if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) + case SET_MQTT_HOST: + strlcpy(aws_mqtt_host, Settings.mqtt_user, strlen(Settings.mqtt_user)); + strlcpy(&aws_mqtt_host[strlen(Settings.mqtt_user)], Settings.mqtt_host, sizeof(Settings.mqtt_host)); + position = aws_mqtt_host; break; + case SET_MQTT_USER: position = aws_mqtt_user; break; +#else + case SET_MQTT_HOST: position = Settings.mqtt_host; break; + case SET_MQTT_USER: position = Settings.mqtt_user; break; +#endif + case SET_MQTT_CLIENT: position = Settings.mqtt_client; break; + case SET_MQTT_PWD: position = Settings.mqtt_pwd; break; + case SET_MQTT_FULLTOPIC: position = Settings.mqtt_fulltopic; break; + case SET_MQTT_TOPIC: position = Settings.mqtt_topic; break; + case SET_MQTT_BUTTON_TOPIC: position = Settings.button_topic; break; + case SET_MQTT_SWITCH_TOPIC: position = Settings.switch_topic; break; + case SET_MQTT_GRP_TOPIC: position = Settings.mqtt_grptopic; break; + case SET_STATE_TXT4: idx++; + case SET_STATE_TXT3: idx++; + case SET_STATE_TXT2: idx++; + case SET_STATE_TXT1: position = Settings.state_text[idx]; break; + case SET_NTPSERVER3: idx++; + case SET_NTPSERVER2: idx++; + case SET_NTPSERVER1: position = Settings.ntp_server[idx]; break; + case SET_MEM5: idx++; + case SET_MEM4: idx++; + case SET_MEM3: idx++; + case SET_MEM2: idx++; + case SET_MEM1: position = Settings.mems[idx]; break; + case SET_CORS: position = Settings.cors_domain; break; + case SET_FRIENDLYNAME4: idx++; + case SET_FRIENDLYNAME3: idx++; + case SET_FRIENDLYNAME2: idx++; + case SET_FRIENDLYNAME1: position = Settings.friendlyname[idx]; break; + } + + } else { + for (;index > 0; index--) { + while (*position++ != '\0') { } + } + } + + return position; +} + /*********************************************************************************************\ * Config Save - Save parameters to Flash ONLY if any parameter has changed \*********************************************************************************************/ @@ -652,11 +864,11 @@ void SettingsDefaultSet2(void) Settings.module = MODULE; ModuleDefault(WEMOS); // for (uint32_t i = 0; i < sizeof(Settings.my_gp); i++) { Settings.my_gp.io[i] = GPIO_NONE; } - strlcpy(Settings.friendlyname[0], FRIENDLY_NAME, sizeof(Settings.friendlyname[0])); - strlcpy(Settings.friendlyname[1], FRIENDLY_NAME"2", sizeof(Settings.friendlyname[1])); - strlcpy(Settings.friendlyname[2], FRIENDLY_NAME"3", sizeof(Settings.friendlyname[2])); - strlcpy(Settings.friendlyname[3], FRIENDLY_NAME"4", sizeof(Settings.friendlyname[3])); - strlcpy(Settings.ota_url, OTA_URL, sizeof(Settings.ota_url)); + SettingsUpdateText(SET_FRIENDLYNAME1, FRIENDLY_NAME); + SettingsUpdateText(SET_FRIENDLYNAME2, FRIENDLY_NAME"2"); + SettingsUpdateText(SET_FRIENDLYNAME3, FRIENDLY_NAME"3"); + SettingsUpdateText(SET_FRIENDLYNAME4, FRIENDLY_NAME"4"); + SettingsUpdateText(SET_OTAURL, OTA_URL); // Power Settings.flag.save_state = SAVE_STATE; @@ -670,6 +882,7 @@ void SettingsDefaultSet2(void) // for (uint32_t i = 1; i < MAX_PULSETIMERS; i++) { Settings.pulse_timer[i] = 0; } // Serial + Settings.serial_config = TS_SERIAL_8N1; Settings.baudrate = APP_BAUDRATE / 300; Settings.sbaudrate = SOFT_BAUDRATE / 300; Settings.serial_delimiter = 0xff; @@ -683,14 +896,14 @@ void SettingsDefaultSet2(void) ParseIp(&Settings.ip_address[3], WIFI_DNS); Settings.sta_config = WIFI_CONFIG_TOOL; // Settings.sta_active = 0; - strlcpy(Settings.sta_ssid[0], STA_SSID1, sizeof(Settings.sta_ssid[0])); - strlcpy(Settings.sta_pwd[0], STA_PASS1, sizeof(Settings.sta_pwd[0])); - strlcpy(Settings.sta_ssid[1], STA_SSID2, sizeof(Settings.sta_ssid[1])); - strlcpy(Settings.sta_pwd[1], STA_PASS2, sizeof(Settings.sta_pwd[1])); - strlcpy(Settings.hostname, WIFI_HOSTNAME, sizeof(Settings.hostname)); + SettingsUpdateText(SET_STASSID1, STA_SSID1); + SettingsUpdateText(SET_STASSID2, STA_SSID2); + SettingsUpdateText(SET_STAPWD1, STA_PASS1); + SettingsUpdateText(SET_STAPWD2, STA_PASS2); + SettingsUpdateText(SET_HOSTNAME, WIFI_HOSTNAME); // Syslog - strlcpy(Settings.syslog_host, SYS_LOG_HOST, sizeof(Settings.syslog_host)); + SettingsUpdateText(SET_SYSLOG_HOST, SYS_LOG_HOST); Settings.syslog_port = SYS_LOG_PORT; Settings.syslog_level = SYS_LOG_LEVEL; @@ -698,8 +911,9 @@ void SettingsDefaultSet2(void) Settings.flag2.emulation = EMULATION; Settings.webserver = WEB_SERVER; Settings.weblog_level = WEB_LOG_LEVEL; - strlcpy(Settings.web_password, WEB_PASSWORD, sizeof(Settings.web_password)); + SettingsUpdateText(SET_WEBPWD, WEB_PASSWORD); Settings.flag3.mdns_enabled = MDNS_ENABLED; + SettingsUpdateText(SET_CORS, CORS_DOMAIN); // Button // Settings.flag.button_restrict = 0; @@ -722,24 +936,24 @@ void SettingsDefaultSet2(void) // Settings.flag.mqtt_offline = 0; // Settings.flag.mqtt_serial = 0; // Settings.flag.device_index_enable = 0; - strlcpy(Settings.mqtt_host, MQTT_HOST, sizeof(Settings.mqtt_host)); + SettingsUpdateText(SET_MQTT_HOST, MQTT_HOST); Settings.mqtt_port = MQTT_PORT; - strlcpy(Settings.mqtt_client, MQTT_CLIENT_ID, sizeof(Settings.mqtt_client)); - strlcpy(Settings.mqtt_user, MQTT_USER, sizeof(Settings.mqtt_user)); - strlcpy(Settings.mqtt_pwd, MQTT_PASS, sizeof(Settings.mqtt_pwd)); - strlcpy(Settings.mqtt_topic, MQTT_TOPIC, sizeof(Settings.mqtt_topic)); - strlcpy(Settings.button_topic, MQTT_BUTTON_TOPIC, sizeof(Settings.button_topic)); - strlcpy(Settings.switch_topic, MQTT_SWITCH_TOPIC, sizeof(Settings.switch_topic)); - strlcpy(Settings.mqtt_grptopic, MQTT_GRPTOPIC, sizeof(Settings.mqtt_grptopic)); - strlcpy(Settings.mqtt_fulltopic, MQTT_FULLTOPIC, sizeof(Settings.mqtt_fulltopic)); + SettingsUpdateText(SET_MQTT_CLIENT, MQTT_CLIENT_ID); + SettingsUpdateText(SET_MQTT_USER, MQTT_USER); + SettingsUpdateText(SET_MQTT_PWD, MQTT_PASS); + SettingsUpdateText(SET_MQTT_TOPIC, MQTT_TOPIC); + SettingsUpdateText(SET_MQTT_BUTTON_TOPIC, MQTT_BUTTON_TOPIC); + SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, MQTT_SWITCH_TOPIC); + SettingsUpdateText(SET_MQTT_GRP_TOPIC, MQTT_GRPTOPIC); + SettingsUpdateText(SET_MQTT_FULLTOPIC, MQTT_FULLTOPIC); Settings.mqtt_retry = MQTT_RETRY_SECS; - strlcpy(Settings.mqtt_prefix[0], SUB_PREFIX, sizeof(Settings.mqtt_prefix[0])); - strlcpy(Settings.mqtt_prefix[1], PUB_PREFIX, sizeof(Settings.mqtt_prefix[1])); - strlcpy(Settings.mqtt_prefix[2], PUB_PREFIX2, sizeof(Settings.mqtt_prefix[2])); - strlcpy(Settings.state_text[0], MQTT_STATUS_OFF, sizeof(Settings.state_text[0])); - strlcpy(Settings.state_text[1], MQTT_STATUS_ON, sizeof(Settings.state_text[1])); - strlcpy(Settings.state_text[2], MQTT_CMND_TOGGLE, sizeof(Settings.state_text[2])); - strlcpy(Settings.state_text[3], MQTT_CMND_HOLD, sizeof(Settings.state_text[3])); + SettingsUpdateText(SET_MQTTPREFIX1, SUB_PREFIX); + SettingsUpdateText(SET_MQTTPREFIX2, PUB_PREFIX); + SettingsUpdateText(SET_MQTTPREFIX3, PUB_PREFIX2); + SettingsUpdateText(SET_STATE_TXT1, MQTT_STATUS_OFF); + SettingsUpdateText(SET_STATE_TXT2, MQTT_STATUS_ON); + SettingsUpdateText(SET_STATE_TXT3, MQTT_CMND_TOGGLE); + SettingsUpdateText(SET_STATE_TXT4, MQTT_CMND_HOLD); char fingerprint[60]; strlcpy(fingerprint, MQTT_FINGERPRINT1, sizeof(fingerprint)); char *p = fingerprint; @@ -891,15 +1105,11 @@ void SettingsDefaultSet2(void) Settings.timezone = APP_TIMEZONE / 60; Settings.timezone_minutes = abs(APP_TIMEZONE % 60); } - strlcpy(Settings.ntp_server[0], NTP_SERVER1, sizeof(Settings.ntp_server[0])); - strlcpy(Settings.ntp_server[1], NTP_SERVER2, sizeof(Settings.ntp_server[1])); - strlcpy(Settings.ntp_server[2], NTP_SERVER3, sizeof(Settings.ntp_server[2])); - for (uint32_t j = 0; j < 3; j++) { - for (uint32_t i = 0; i < strlen(Settings.ntp_server[j]); i++) { - if (Settings.ntp_server[j][i] == ',') { - Settings.ntp_server[j][i] = '.'; - } - } + SettingsUpdateText(SET_NTPSERVER1, NTP_SERVER1); + SettingsUpdateText(SET_NTPSERVER2, NTP_SERVER2); + SettingsUpdateText(SET_NTPSERVER3, NTP_SERVER3); + for (uint32_t i = 0; i < 3; i++) { + SettingsUpdateText(SET_NTPSERVER1 +i, ReplaceCommaWithDot(SettingsText(SET_NTPSERVER1 +i))); } Settings.latitude = (int)((double)LATITUDE * 1000000); Settings.longitude = (int)((double)LONGITUDE * 1000000); @@ -1059,8 +1269,8 @@ void SettingsDelta(void) } } if (Settings.version < 0x06060009) { - Settings.baudrate = Settings.ex_baudrate * 4; - Settings.sbaudrate = Settings.ex_sbaudrate * 4; + Settings.baudrate = APP_BAUDRATE / 300; + Settings.sbaudrate = SOFT_BAUDRATE / 300; } if (Settings.version < 0x0606000A) { uint8_t tuyaindex = 0; @@ -1155,6 +1365,54 @@ void SettingsDelta(void) if (Settings.version < 0x07000004) { Settings.wifi_output_power = 170; } + if (Settings.version < 0x07010202) { + Settings.serial_config = TS_SERIAL_8N1; + } + if (Settings.version < 0x07010204) { + if (Settings.flag3.ex_cors_enabled == 1) { + strlcpy(Settings.cors_domain, CORS_ENABLED_ALL, sizeof(Settings.cors_domain)); + } else { + Settings.cors_domain[0] = 0; + } + } + if (Settings.version < 0x07010205) { + Settings.seriallog_level = Settings.ex_seriallog_level; // 09E -> 452 + Settings.sta_config = Settings.ex_sta_config; // 09F -> EC7 + Settings.sta_active = Settings.ex_sta_active; // 0A0 -> EC8 + memcpy((char*)&Settings.rule_stop, (char*)&Settings.ex_rule_stop, 47); // 1A7 -> EC9 + } + if (Settings.version < 0x07010206) { + Settings.flag4 = Settings.ex_flag4; // 1E0 -> EF8 + Settings.mqtt_port = Settings.ex_mqtt_port; // 20A -> EFC + memcpy((char*)&Settings.serial_config, (char*)&Settings.ex_serial_config, 5); // 1E4 -> EFE + } + + if ((VERSION < 0x08000000) && (Settings.version > VERSION)) { + char temp[strlen(SettingsText(SET_OTAURL)) +1]; strncpy(temp, SettingsText(SET_OTAURL), sizeof(temp)); + char temp21[strlen(SettingsText(SET_MQTTPREFIX1)) +1]; strncpy(temp21, SettingsText(SET_MQTTPREFIX1), sizeof(temp21)); + char temp22[strlen(SettingsText(SET_MQTTPREFIX2)) +1]; strncpy(temp22, SettingsText(SET_MQTTPREFIX2), sizeof(temp22)); + char temp23[strlen(SettingsText(SET_MQTTPREFIX3)) +1]; strncpy(temp23, SettingsText(SET_MQTTPREFIX3), sizeof(temp23)); + char temp31[strlen(SettingsText(SET_STASSID1)) +1]; strncpy(temp31, SettingsText(SET_STASSID1), sizeof(temp31)); + char temp32[strlen(SettingsText(SET_STASSID2)) +1]; strncpy(temp32, SettingsText(SET_STASSID2), sizeof(temp32)); + char temp41[strlen(SettingsText(SET_STAPWD1)) +1]; strncpy(temp41, SettingsText(SET_STAPWD1), sizeof(temp41)); + char temp42[strlen(SettingsText(SET_STAPWD2)) +1]; strncpy(temp42, SettingsText(SET_STAPWD2), sizeof(temp42)); + char temp5[strlen(SettingsText(SET_HOSTNAME)) +1]; strncpy(temp5, SettingsText(SET_HOSTNAME), sizeof(temp5)); + char temp6[strlen(SettingsText(SET_SYSLOG_HOST)) +1]; strncpy(temp6, SettingsText(SET_SYSLOG_HOST), sizeof(temp5)); + + uint32_t version = Settings.version; + Settings.version = VERSION; + SettingsUpdateText(SET_OTAURL, temp); + SettingsUpdateText(SET_MQTTPREFIX1, temp21); + SettingsUpdateText(SET_MQTTPREFIX2, temp22); + SettingsUpdateText(SET_MQTTPREFIX3, temp23); + SettingsUpdateText(SET_STASSID1, temp31); + SettingsUpdateText(SET_STASSID2, temp32); + SettingsUpdateText(SET_STAPWD1, temp41); + SettingsUpdateText(SET_STAPWD2, temp42); + SettingsUpdateText(SET_HOSTNAME, temp5); + SettingsUpdateText(SET_SYSLOG_HOST, temp6); + Settings.version = version; + } Settings.version = VERSION; SettingsSave(1); diff --git a/tasmota/support.ino b/tasmota/support.ino index 8ce7f2840..87f05ad27 100644 --- a/tasmota/support.ino +++ b/tasmota/support.ino @@ -51,7 +51,7 @@ void OsWatchTicker(void) uint32_t last_run = abs(t - oswatch_last_loop_time); #ifdef DEBUG_THEO - AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_APPLICATION D_OSWATCH " FreeRam %d, rssi %d, last_run %d"), ESP.getFreeHeap(), WifiGetRssiAsQuality(WiFi.RSSI()), last_run); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_APPLICATION D_OSWATCH " FreeRam %d, rssi %d %% (%d dBm), last_run %d"), ESP.getFreeHeap(), WifiGetRssiAsQuality(WiFi.RSSI()), WiFi.RSSI(), last_run); #endif // DEBUG_THEO if (last_run >= (OSWATCH_RESET_TIME * 1000)) { // AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_OSWATCH " " D_BLOCKED_LOOP ". " D_RESTARTING)); // Save iram space @@ -107,12 +107,6 @@ String GetResetReason(void) } } -String GetResetReasonInfo(void) -{ - // "Fatal exception:0 flag:2 (EXCEPTION) epc1:0x704022a7 epc2:0x00000000 epc3:0x00000000 excvaddr:0x00000000 depc:0x00000000" - return (ResetReason() == REASON_EXCEPTION_RST) ? ESP.getResetInfo() : GetResetReason(); -} - /*********************************************************************************************\ * Miscellaneous \*********************************************************************************************/ @@ -334,6 +328,22 @@ char* RemoveSpace(char* p) return p; } +char* ReplaceCommaWithDot(char* p) +{ + char* write = (char*)p; + char* read = (char*)p; + char ch = '.'; + + while (ch != '\0') { + ch = *read++; + if (ch == ',') { + ch = '.'; + } + *write++ = ch; + } + return p; +} + char* LowerCase(char* dest, const char* source) { char* write = dest; @@ -383,6 +393,23 @@ char* Trim(char* p) return p; } +char* RemoveAllSpaces(char* p) +{ + // remove any white space from the base64 + char *cursor = p; + uint32_t offset = 0; + while (1) { + *cursor = *(cursor + offset); + if ((' ' == *cursor) || ('\t' == *cursor) || ('\n' == *cursor)) { // if space found, remove this char until end of string + offset++; + } else { + if (0 == *cursor) { break; } + cursor++; + } + } + return p; +} + char* NoAlNumToUnderscore(char* dest, const char* source) { char* write = dest; @@ -737,19 +764,50 @@ int GetStateNumber(char *state_text) return state_number; } +String GetSerialConfig(void) +{ + // Settings.serial_config layout + // b000000xx - 5, 6, 7 or 8 data bits + // b00000x00 - 1 or 2 stop bits + // b000xx000 - None, Even or Odd parity + + const char kParity[] = "NEOI"; + + char config[4]; + config[0] = '5' + (Settings.serial_config & 0x3); + config[1] = kParity[(Settings.serial_config >> 3) & 0x3]; + config[2] = '1' + ((Settings.serial_config >> 2) & 0x1); + config[3] = '\0'; + return String(config); +} + +void SetSerialBegin(uint32_t baudrate) +{ + if (seriallog_level) { + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION "Set Serial to %s %d bit/s"), GetSerialConfig().c_str(), baudrate); + delay(100); + } + Serial.flush(); + Serial.begin(baudrate, (SerialConfig)pgm_read_byte(kTasmotaSerialConfig + Settings.serial_config)); + delay(10); + Serial.println(); +} + +void SetSerialConfig(uint32_t serial_config) +{ + if (serial_config == Settings.serial_config) { return; } + if (serial_config > TS_SERIAL_8O2) { return; } + + Settings.serial_config = serial_config; + SetSerialBegin(Serial.baudRate()); +} + void SetSerialBaudrate(int baudrate) { Settings.baudrate = baudrate / 300; - if (Serial.baudRate() != baudrate) { - if (seriallog_level) { - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_SET_BAUDRATE_TO " %d"), baudrate); - } - delay(100); - Serial.flush(); - Serial.begin(baudrate, serial_config); - delay(10); - Serial.println(); - } + if (Serial.baudRate() == baudrate) { return; } + + SetSerialBegin(baudrate); } void ClaimSerial(void) @@ -1529,10 +1587,10 @@ void Syslog(void) { // Destroys log_data - uint32_t current_hash = GetHash(Settings.syslog_host, strlen(Settings.syslog_host)); + uint32_t current_hash = GetHash(SettingsText(SET_SYSLOG_HOST), strlen(SettingsText(SET_SYSLOG_HOST))); if (syslog_host_hash != current_hash) { syslog_host_hash = current_hash; - WiFi.hostByName(Settings.syslog_host, syslog_host_addr); // If sleep enabled this might result in exception so try to do it once using hash + WiFi.hostByName(SettingsText(SET_SYSLOG_HOST), syslog_host_addr); // If sleep enabled this might result in exception so try to do it once using hash } if (PortUdp.beginPacket(syslog_host_addr, Settings.syslog_port)) { char syslog_preamble[64]; // Hostname + Id diff --git a/tasmota/support_command.ino b/tasmota/support_command.ino index fe8640045..b8fbd4a7a 100644 --- a/tasmota/support_command.ino +++ b/tasmota/support_command.ino @@ -23,7 +23,7 @@ const char kTasmotaCommands[] PROGMEM = "|" // No prefix D_CMND_SETOPTION "|" D_CMND_TEMPERATURE_RESOLUTION "|" D_CMND_HUMIDITY_RESOLUTION "|" D_CMND_PRESSURE_RESOLUTION "|" D_CMND_POWER_RESOLUTION "|" D_CMND_VOLTAGE_RESOLUTION "|" D_CMND_FREQUENCY_RESOLUTION "|" D_CMND_CURRENT_RESOLUTION "|" D_CMND_ENERGY_RESOLUTION "|" D_CMND_WEIGHT_RESOLUTION "|" D_CMND_MODULE "|" D_CMND_MODULES "|" D_CMND_GPIO "|" D_CMND_GPIOS "|" D_CMND_TEMPLATE "|" D_CMND_PWM "|" D_CMND_PWMFREQUENCY "|" D_CMND_PWMRANGE "|" - D_CMND_BUTTONDEBOUNCE "|" D_CMND_SWITCHDEBOUNCE "|" D_CMND_SYSLOG "|" D_CMND_LOGHOST "|" D_CMND_LOGPORT "|" D_CMND_SERIALSEND "|" D_CMND_BAUDRATE "|" + D_CMND_BUTTONDEBOUNCE "|" D_CMND_SWITCHDEBOUNCE "|" D_CMND_SYSLOG "|" D_CMND_LOGHOST "|" D_CMND_LOGPORT "|" D_CMND_SERIALSEND "|" D_CMND_BAUDRATE "|" D_CMND_SERIALCONFIG "|" D_CMND_SERIALDELIMITER "|" D_CMND_IPADDRESS "|" D_CMND_NTPSERVER "|" D_CMND_AP "|" D_CMND_SSID "|" D_CMND_PASSWORD "|" D_CMND_HOSTNAME "|" D_CMND_WIFICONFIG "|" D_CMND_FRIENDLYNAME "|" D_CMND_SWITCHMODE "|" D_CMND_INTERLOCK "|" D_CMND_TELEPERIOD "|" D_CMND_RESET "|" D_CMND_TIME "|" D_CMND_TIMEZONE "|" D_CMND_TIMESTD "|" D_CMND_TIMEDST "|" D_CMND_ALTITUDE "|" D_CMND_LEDPOWER "|" D_CMND_LEDSTATE "|" D_CMND_LEDMASK "|" D_CMND_WIFIPOWER "|" D_CMND_TEMPOFFSET "|" @@ -38,7 +38,7 @@ void (* const TasmotaCommand[])(void) PROGMEM = { &CmndSetoption, &CmndTemperatureResolution, &CmndHumidityResolution, &CmndPressureResolution, &CmndPowerResolution, &CmndVoltageResolution, &CmndFrequencyResolution, &CmndCurrentResolution, &CmndEnergyResolution, &CmndWeightResolution, &CmndModule, &CmndModules, &CmndGpio, &CmndGpios, &CmndTemplate, &CmndPwm, &CmndPwmfrequency, &CmndPwmrange, - &CmndButtonDebounce, &CmndSwitchDebounce, &CmndSyslog, &CmndLoghost, &CmndLogport, &CmndSerialSend, &CmndBaudrate, + &CmndButtonDebounce, &CmndSwitchDebounce, &CmndSyslog, &CmndLoghost, &CmndLogport, &CmndSerialSend, &CmndBaudrate, &CmndSerialConfig, &CmndSerialDelimiter, &CmndIpAddress, &CmndNtpServer, &CmndAp, &CmndSsid, &CmndPassword, &CmndHostname, &CmndWifiConfig, &CmndFriendlyname, &CmndSwitchMode, &CmndInterlock, &CmndTeleperiod, &CmndReset, &CmndTime, &CmndTimezone, &CmndTimeStd, &CmndTimeDst, &CmndAltitude, &CmndLedPower, &CmndLedState, &CmndLedMask, &CmndWifiPower, &CmndTempOffset, @@ -147,7 +147,7 @@ void CommandHandler(char* topicBuf, char* dataBuf, uint32_t data_len) data_len--; } - bool grpflg = (strstr(topicBuf, Settings.mqtt_grptopic) != nullptr); + bool grpflg = (strstr(topicBuf, SettingsText(SET_MQTT_GRP_TOPIC)) != nullptr); char stemp1[TOPSZ]; GetFallbackTopic_P(stemp1, ""); // Full Fallback topic = cmnd/DVES_xxxxxxxx_fb/ @@ -325,15 +325,18 @@ void CmndStatus(void) uint32_t payload = ((XdrvMailbox.payload < 0) || (XdrvMailbox.payload > MAX_STATUS)) ? 99 : XdrvMailbox.payload; uint32_t option = STAT; - char stemp[MAX_FRIENDLYNAMES * (sizeof(Settings.friendlyname[0]) +MAX_FRIENDLYNAMES)]; + char stemp[200]; char stemp2[100]; // Workaround MQTT - TCP/IP stack queueing when SUB_PREFIX = PUB_PREFIX - if (!strcmp(Settings.mqtt_prefix[0],Settings.mqtt_prefix[1]) && (!payload)) { option++; } // TELE + if (!strcmp(SettingsText(SET_MQTTPREFIX1), SettingsText(SET_MQTTPREFIX2)) && (!payload)) { option++; } // TELE if ((!Settings.flag.mqtt_enabled) && (6 == payload)) { payload = 99; } // SetOption3 - Enable MQTT if (!energy_flg && (9 == payload)) { payload = 99; } + bool exception_flg = (ResetReason() == REASON_EXCEPTION_RST); + if (!exception_flg && (12 == payload)) { payload = 99; } + if ((0 == payload) || (99 == payload)) { uint32_t maxfn = (devices_present > MAX_FRIENDLYNAMES) ? MAX_FRIENDLYNAMES : (!devices_present) ? 1 : devices_present; #ifdef USE_SONOFF_IFAN @@ -341,7 +344,7 @@ void CmndStatus(void) #endif // USE_SONOFF_IFAN stemp[0] = '\0'; for (uint32_t i = 0; i < maxfn; i++) { - snprintf_P(stemp, sizeof(stemp), PSTR("%s%s\"%s\"" ), stemp, (i > 0 ? "," : ""), Settings.friendlyname[i]); + snprintf_P(stemp, sizeof(stemp), PSTR("%s%s\"%s\"" ), stemp, (i > 0 ? "," : ""), SettingsText(SET_FRIENDLYNAME1 +i)); } stemp2[0] = '\0'; for (uint32_t i = 0; i < MAX_SWITCHES; i++) { @@ -352,10 +355,10 @@ void CmndStatus(void) D_CMND_LEDMASK "\":\"%04X\",\"" D_CMND_SAVEDATA "\":%d,\"" D_JSON_SAVESTATE "\":%d,\"" D_CMND_SWITCHTOPIC "\":\"%s\",\"" D_CMND_SWITCHMODE "\":[%s],\"" D_CMND_BUTTONRETAIN "\":%d,\"" D_CMND_SWITCHRETAIN "\":%d,\"" D_CMND_SENSORRETAIN "\":%d,\"" D_CMND_POWERRETAIN "\":%d}}"), ModuleNr(), stemp, mqtt_topic, - Settings.button_topic, power, Settings.poweronstate, Settings.ledstate, + SettingsText(SET_MQTT_BUTTON_TOPIC), power, Settings.poweronstate, Settings.ledstate, Settings.ledmask, Settings.save_data, Settings.flag.save_state, // SetOption0 - Save power state and use after restart - Settings.switch_topic, + SettingsText(SET_MQTT_SWITCH_TOPIC), stemp2, Settings.flag.mqtt_button_retain, // CMND_BUTTONRETAIN Settings.flag.mqtt_switch_retain, // CMND_SWITCHRETAIN @@ -368,8 +371,8 @@ void CmndStatus(void) Response_P(PSTR("{\"" D_CMND_STATUS D_STATUS1_PARAMETER "\":{\"" D_JSON_BAUDRATE "\":%d,\"" D_CMND_GROUPTOPIC "\":\"%s\",\"" D_CMND_OTAURL "\":\"%s\",\"" D_JSON_RESTARTREASON "\":\"%s\",\"" D_JSON_UPTIME "\":\"%s\",\"" D_JSON_STARTUPUTC "\":\"%s\",\"" D_CMND_SLEEP "\":%d,\"" D_JSON_CONFIG_HOLDER "\":%d,\"" D_JSON_BOOTCOUNT "\":%d,\"" D_JSON_SAVECOUNT "\":%d,\"" D_JSON_SAVEADDRESS "\":\"%X\"}}"), - baudrate, Settings.mqtt_grptopic, Settings.ota_url, - GetResetReasonInfo().c_str(), GetUptime().c_str(), GetDateAndTime(DT_RESTART).c_str(), Settings.sleep, + baudrate, SettingsText(SET_MQTT_GRP_TOPIC), SettingsText(SET_OTAURL), + GetResetReason().c_str(), GetUptime().c_str(), GetDateAndTime(DT_RESTART).c_str(), Settings.sleep, Settings.cfg_holder, Settings.bootcount, Settings.save_flag, GetSettingsAddress()); MqttPublishPrefixTopic_P(option, PSTR(D_CMND_STATUS "1")); } @@ -377,9 +380,12 @@ void CmndStatus(void) if ((0 == payload) || (2 == payload)) { Response_P(PSTR("{\"" D_CMND_STATUS D_STATUS2_FIRMWARE "\":{\"" D_JSON_VERSION "\":\"%s%s\",\"" D_JSON_BUILDDATETIME "\":\"%s\",\"" D_JSON_BOOTVERSION "\":%d,\"" D_JSON_COREVERSION "\":\"" ARDUINO_ESP8266_RELEASE "\",\"" D_JSON_SDKVERSION "\":\"%s\"," - "\"Hardware\":\"%s\"}}"), + "\"Hardware\":\"%s\"" + "%s}}"), my_version, my_image, GetBuildDateAndTime().c_str(), - ESP.getBootVersion(), ESP.getSdkVersion(), GetDeviceHardware().c_str()); + ESP.getBootVersion(), ESP.getSdkVersion(), + GetDeviceHardware().c_str(), + GetStatistics().c_str()); MqttPublishPrefixTopic_P(option, PSTR(D_CMND_STATUS "2")); } @@ -388,7 +394,7 @@ void CmndStatus(void) D_CMND_LOGHOST "\":\"%s\",\"" D_CMND_LOGPORT "\":%d,\"" D_CMND_SSID "\":[\"%s\",\"%s\"],\"" D_CMND_TELEPERIOD "\":%d,\"" D_JSON_RESOLUTION "\":\"%08X\",\"" D_CMND_SETOPTION "\":[\"%08X\",\"%s\",\"%08X\",\"%08X\"]}}"), Settings.seriallog_level, Settings.weblog_level, Settings.mqttlog_level, Settings.syslog_level, - Settings.syslog_host, Settings.syslog_port, Settings.sta_ssid[0], Settings.sta_ssid[1], Settings.tele_period, + SettingsText(SET_SYSLOG_HOST), Settings.syslog_port, SettingsText(SET_STASSID1), SettingsText(SET_STASSID2), Settings.tele_period, Settings.flag2.data, Settings.flag.data, ToHex_P((unsigned char*)Settings.param, PARAM8_SIZE, stemp2, sizeof(stemp2)), Settings.flag3.data, Settings.flag4.data); MqttPublishPrefixTopic_P(option, PSTR(D_CMND_STATUS "3")); @@ -419,17 +425,10 @@ void CmndStatus(void) } if (((0 == payload) || (6 == payload)) && Settings.flag.mqtt_enabled) { // SetOption3 - Enable MQTT -#ifdef USE_MQTT_AWS_IOT - Response_P(PSTR("{\"" D_CMND_STATUS D_STATUS6_MQTT "\":{\"" D_CMND_MQTTHOST "\":\"%s%s\",\"" D_CMND_MQTTPORT "\":%d,\"" D_CMND_MQTTCLIENT D_JSON_MASK "\":\"%s\",\"" - D_CMND_MQTTCLIENT "\":\"%s\",\"" D_JSON_MQTT_COUNT "\":%d,\"MAX_PACKET_SIZE\":%d,\"KEEPALIVE\":%d}}"), - Settings.mqtt_user, Settings.mqtt_host, Settings.mqtt_port, Settings.mqtt_client, - mqtt_client, MqttConnectCount(), MQTT_MAX_PACKET_SIZE, MQTT_KEEPALIVE); -#else Response_P(PSTR("{\"" D_CMND_STATUS D_STATUS6_MQTT "\":{\"" D_CMND_MQTTHOST "\":\"%s\",\"" D_CMND_MQTTPORT "\":%d,\"" D_CMND_MQTTCLIENT D_JSON_MASK "\":\"%s\",\"" D_CMND_MQTTCLIENT "\":\"%s\",\"" D_CMND_MQTTUSER "\":\"%s\",\"" D_JSON_MQTT_COUNT "\":%d,\"MAX_PACKET_SIZE\":%d,\"KEEPALIVE\":%d}}"), - Settings.mqtt_host, Settings.mqtt_port, Settings.mqtt_client, - mqtt_client, Settings.mqtt_user, MqttConnectCount(), MQTT_MAX_PACKET_SIZE, MQTT_KEEPALIVE); -#endif + SettingsText(SET_MQTT_HOST), Settings.mqtt_port, SettingsText(SET_MQTT_CLIENT), + mqtt_client, SettingsText(SET_MQTT_USER), MqttConnectCount(), MQTT_MAX_PACKET_SIZE, MQTT_KEEPALIVE); MqttPublishPrefixTopic_P(option, PSTR(D_CMND_STATUS "6")); } @@ -483,6 +482,15 @@ void CmndStatus(void) MqttPublishPrefixTopic_P(option, PSTR(D_CMND_STATUS "11")); } + if (exception_flg) { + if ((0 == payload) || (12 == payload)) { + Response_P(PSTR("{\"" D_CMND_STATUS D_STATUS12_STATUS "\":")); + CrashDump(); + ResponseJsonEnd(); + MqttPublishPrefixTopic_P(option, PSTR(D_CMND_STATUS "12")); + } + } + #ifdef USE_SCRIPT_STATUS if (bitRead(Settings.rule_enabled, 0)) Run_Scripter(">U",2,mqtt_data); #endif @@ -542,10 +550,10 @@ void CmndUpgrade(void) void CmndOtaUrl(void) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.ota_url))) { - strlcpy(Settings.ota_url, (SC_DEFAULT == Shortcut()) ? OTA_URL : XdrvMailbox.data, sizeof(Settings.ota_url)); + if (XdrvMailbox.data_len > 0) { + SettingsUpdateText(SET_OTAURL, (SC_DEFAULT == Shortcut()) ? OTA_URL : XdrvMailbox.data); } - ResponseCmndChar(Settings.ota_url); + ResponseCmndChar(SettingsText(SET_OTAURL)); } void CmndSeriallog(void) @@ -564,6 +572,9 @@ void CmndRestart(void) restart_flag = 2; ResponseCmndChar(D_JSON_RESTARTING); break; + case -1: + CmndCrash(); // force a crash + break; case 99: AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_RESTARTING)); EspRestart(); @@ -1082,6 +1093,48 @@ void CmndBaudrate(void) ResponseCmndNumber(Settings.baudrate * 300); } +void CmndSerialConfig(void) +{ + // See TasmotaSerialConfig for possible options + // SerialConfig 0..23 where 3 equals 8N1 + // SerialConfig 8N1 + + if (XdrvMailbox.data_len > 0) { + if (XdrvMailbox.data_len < 3) { // Use 0..23 as serial config option + if ((XdrvMailbox.payload >= TS_SERIAL_5N1) && (XdrvMailbox.payload <= TS_SERIAL_8O2)) { + SetSerialConfig(XdrvMailbox.payload); + } + } + else if ((XdrvMailbox.payload >= 5) && (XdrvMailbox.payload <= 8)) { + uint8_t serial_config = XdrvMailbox.payload -5; // Data bits 5, 6, 7 or 8, No parity and 1 stop bit + + bool valid = true; + char parity = (XdrvMailbox.data[1] & 0xdf); + if ('E' == parity) { + serial_config += 0x08; // Even parity + } + else if ('O' == parity) { + serial_config += 0x10; // Odd parity + } + else if ('N' != parity) { + valid = false; + } + + if ('2' == XdrvMailbox.data[2]) { + serial_config += 0x04; // Stop bits 2 + } + else if ('1' != XdrvMailbox.data[2]) { + valid = false; + } + + if (valid) { + SetSerialConfig(serial_config); + } + } + } + ResponseCmndChar(GetSerialConfig().c_str()); +} + void CmndSerialSend(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 5)) { @@ -1133,10 +1186,10 @@ void CmndSyslog(void) void CmndLoghost(void) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.syslog_host))) { - strlcpy(Settings.syslog_host, (SC_DEFAULT == Shortcut()) ? SYS_LOG_HOST : XdrvMailbox.data, sizeof(Settings.syslog_host)); + if (XdrvMailbox.data_len > 0) { + SettingsUpdateText(SET_SYSLOG_HOST, (SC_DEFAULT == Shortcut()) ? SYS_LOG_HOST : XdrvMailbox.data); } - ResponseCmndChar(Settings.syslog_host); + ResponseCmndChar(SettingsText(SET_SYSLOG_HOST)); } void CmndLogport(void) @@ -1164,17 +1217,15 @@ void CmndIpAddress(void) void CmndNtpServer(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 3)) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.ntp_server[0]))) { - strlcpy(Settings.ntp_server[XdrvMailbox.index -1], - (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? (1==XdrvMailbox.index)?NTP_SERVER1:(2==XdrvMailbox.index)?NTP_SERVER2:NTP_SERVER3 : XdrvMailbox.data, - sizeof(Settings.ntp_server[0])); - for (uint32_t i = 0; i < strlen(Settings.ntp_server[XdrvMailbox.index -1]); i++) { - if (Settings.ntp_server[XdrvMailbox.index -1][i] == ',') Settings.ntp_server[XdrvMailbox.index -1][i] = '.'; - } + uint32_t ntp_server = SET_NTPSERVER1 + XdrvMailbox.index -1; + if (XdrvMailbox.data_len > 0) { + SettingsUpdateText(ntp_server, + (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? (1 == XdrvMailbox.index) ? NTP_SERVER1 : (2 == XdrvMailbox.index) ? NTP_SERVER2 : NTP_SERVER3 : XdrvMailbox.data); + SettingsUpdateText(ntp_server, ReplaceCommaWithDot(SettingsText(ntp_server))); // restart_flag = 2; // Issue #3890 ntp_force_sync = true; } - ResponseCmndIdxChar(Settings.ntp_server[XdrvMailbox.index -1]); + ResponseCmndIdxChar(SettingsText(ntp_server)); } } @@ -1191,33 +1242,31 @@ void CmndAp(void) } restart_flag = 2; } - Response_P(S_JSON_COMMAND_NVALUE_SVALUE, XdrvMailbox.command, Settings.sta_active +1, Settings.sta_ssid[Settings.sta_active]); + Response_P(S_JSON_COMMAND_NVALUE_SVALUE, XdrvMailbox.command, Settings.sta_active +1, SettingsText(SET_STASSID1 + Settings.sta_active)); } void CmndSsid(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 2)) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.sta_ssid[0]))) { - strlcpy(Settings.sta_ssid[XdrvMailbox.index -1], - (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? (1 == XdrvMailbox.index) ? STA_SSID1 : STA_SSID2 : XdrvMailbox.data, - sizeof(Settings.sta_ssid[0])); + if (XdrvMailbox.data_len > 0) { + SettingsUpdateText(SET_STASSID1 + XdrvMailbox.index -1, + (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? (1 == XdrvMailbox.index) ? STA_SSID1 : STA_SSID2 : XdrvMailbox.data); Settings.sta_active = XdrvMailbox.index -1; restart_flag = 2; } - ResponseCmndIdxChar(Settings.sta_ssid[XdrvMailbox.index -1]); + ResponseCmndIdxChar(SettingsText(SET_STASSID1 + XdrvMailbox.index -1)); } } void CmndPassword(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 2)) { - if ((XdrvMailbox.data_len > 4 || SC_CLEAR == Shortcut() || SC_DEFAULT == Shortcut()) && (XdrvMailbox.data_len < sizeof(Settings.sta_pwd[0]))) { - strlcpy(Settings.sta_pwd[XdrvMailbox.index -1], - (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? (1 == XdrvMailbox.index) ? STA_PASS1 : STA_PASS2 : XdrvMailbox.data, - sizeof(Settings.sta_pwd[0])); + if ((XdrvMailbox.data_len > 4) || (SC_CLEAR == Shortcut()) || (SC_DEFAULT == Shortcut())) { + SettingsUpdateText(SET_STAPWD1 + XdrvMailbox.index -1, + (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? (1 == XdrvMailbox.index) ? STA_PASS1 : STA_PASS2 : XdrvMailbox.data); Settings.sta_active = XdrvMailbox.index -1; restart_flag = 2; - ResponseCmndIdxChar(Settings.sta_pwd[XdrvMailbox.index -1]); + ResponseCmndIdxChar(SettingsText(SET_STAPWD1 + XdrvMailbox.index -1)); } else { Response_P(S_JSON_COMMAND_INDEX_ASTERISK, XdrvMailbox.command, XdrvMailbox.index); } @@ -1226,14 +1275,14 @@ void CmndPassword(void) void CmndHostname(void) { - if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.hostname))) { - strlcpy(Settings.hostname, (SC_DEFAULT == Shortcut()) ? WIFI_HOSTNAME : XdrvMailbox.data, sizeof(Settings.hostname)); - if (strstr(Settings.hostname, "%") != nullptr) { - strlcpy(Settings.hostname, WIFI_HOSTNAME, sizeof(Settings.hostname)); + if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0)) { + SettingsUpdateText(SET_HOSTNAME, (SC_DEFAULT == Shortcut()) ? WIFI_HOSTNAME : XdrvMailbox.data); + if (strstr(SettingsText(SET_HOSTNAME), "%") != nullptr) { + SettingsUpdateText(SET_HOSTNAME, WIFI_HOSTNAME); } restart_flag = 2; } - ResponseCmndChar(Settings.hostname); + ResponseCmndChar(SettingsText(SET_HOSTNAME)); } void CmndWifiConfig(void) @@ -1255,16 +1304,16 @@ void CmndWifiConfig(void) void CmndFriendlyname(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_FRIENDLYNAMES)) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.friendlyname[0]))) { + if (XdrvMailbox.data_len > 0) { char stemp1[TOPSZ]; if (1 == XdrvMailbox.index) { snprintf_P(stemp1, sizeof(stemp1), PSTR(FRIENDLY_NAME)); } else { snprintf_P(stemp1, sizeof(stemp1), PSTR(FRIENDLY_NAME "%d"), XdrvMailbox.index); } - strlcpy(Settings.friendlyname[XdrvMailbox.index -1], (SC_DEFAULT == Shortcut()) ? stemp1 : XdrvMailbox.data, sizeof(Settings.friendlyname[XdrvMailbox.index -1])); + SettingsUpdateText(SET_FRIENDLYNAME1 + XdrvMailbox.index -1, (SC_DEFAULT == Shortcut()) ? stemp1 : XdrvMailbox.data); } - ResponseCmndIdxChar(Settings.friendlyname[XdrvMailbox.index -1]); + ResponseCmndIdxChar(SettingsText(SET_FRIENDLYNAME1 + XdrvMailbox.index -1)); } } diff --git a/tasmota/support_crash_recorder.ino b/tasmota/support_crash_recorder.ino new file mode 100644 index 000000000..87009bbc5 --- /dev/null +++ b/tasmota/support_crash_recorder.ino @@ -0,0 +1,89 @@ +/* + support_crash_recorder.ino - record the call stack in RTC in case of crash + + Copyright (C) 2019 Stephan Hadinger, 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 uint32_t crash_magic = 0x53415400; // Stack trace magic number (TASx) +const uint32_t crash_rtc_offset = 32; // Offset in RTC memory skipping OTA used block +const uint32_t crash_dump_max_len = 31; // Dump only 31 call addresses to satisfy max JSON length of about 600 characters + +/** + * Save crash information in RTC memory + * This function is called automatically if ESP8266 suffers an exception + * It should be kept quick / consise to be able to execute before hardware wdt may kick in + */ +extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack, uint32_t stack_end ) +{ + uint32_t addr_written = 0; // how many addresses have we already written in RTC + uint32_t value; // 4 bytes buffer to write to RTC + + for (uint32_t i = stack; i < stack_end; i += 4) { + value = *((uint32_t*) i); // load value from stack + if ((value >= 0x40000000) && (value < 0x40300000)) { // keep only addresses in code area + ESP.rtcUserMemoryWrite(crash_rtc_offset + addr_written, (uint32_t*)&value, sizeof(value)); + addr_written++; + if (addr_written >= crash_dump_max_len) { break; } // we store only 31 addresses + } + } + value = crash_magic + addr_written; + ESP.rtcUserMemoryWrite(crash_rtc_offset + crash_dump_max_len, (uint32_t*)&value, sizeof(value)); +} + +// Generate a crash to test the crash recorder +void CmndCrash(void) +{ + volatile uint32_t dummy; + dummy = *((uint32_t*) 0x00000000); +} + +// Clear the RTC dump counter when we do a normal reboot, this avoids garbage data to stay in RTC +void CrashDumpClear(void) +{ + uint32_t value = 0; + ESP.rtcUserMemoryWrite(crash_rtc_offset + crash_dump_max_len, (uint32_t*)&value, sizeof(value)); +} + +/*********************************************************************************************\ + * CmndCrashDump - dump the crash history - called by `Status 12` +\*********************************************************************************************/ + +void CrashDump(void) +{ + ResponseAppend_P(PSTR("{\"Exception\":%d,\"Reason\":\"%s\",\"EPC\":[\"%08x\",\"%08x\",\"%08x\"],\"EXCVADDR\":\"%08x\",\"DEPC\":\"%08x\""), + resetInfo.exccause, // Exception Cause + GetResetReason().c_str(), // Reset Reason + resetInfo.epc1, // Exception Progam Counter + resetInfo.epc2, // Exception Progam Counter - High-Priority Interrupt 1 + resetInfo.epc3, // Exception Progam Counter - High-Priority Interrupt 2 + resetInfo.excvaddr, // Exception Virtual Address Register - Virtual address that caused last fetch, load, or store exception + resetInfo.depc); // Double Exception Program Counter + + uint32_t value; + ESP.rtcUserMemoryRead(crash_rtc_offset + crash_dump_max_len, (uint32_t*)&value, sizeof(value)); + if (crash_magic == (value & 0xFFFFFF00)) { + ResponseAppend_P(PSTR(",\"CallChain\":[")); + uint32_t count = value & 0x3F; + for (uint32_t i = 0; i < count; i++) { + ESP.rtcUserMemoryRead(crash_rtc_offset +i, (uint32_t*)&value, sizeof(value)); + if (i > 0) { ResponseAppend_P(PSTR(",")); } + ResponseAppend_P(PSTR("\"%08x\""), value); + } + ResponseAppend_P(PSTR("]")); + } + + ResponseJsonEnd(); +} diff --git a/tasmota/support_float.ino b/tasmota/support_float.ino index 21ec2f47d..d08fc13f9 100644 --- a/tasmota/support_float.ino +++ b/tasmota/support_float.ino @@ -172,6 +172,7 @@ double const f_sixthpi = f_pi / 6.0; // f_pi/6.0, used in double const f_tansixthpi = tan(f_sixthpi); // tan(f_pi/6), used in atan routines double const f_twelfthpi = f_pi / 12.0; // f_pi/12.0, used in atan routines double const f_tantwelfthpi = tan(f_twelfthpi); // tan(f_pi/12), used in atan routines +float const f_180pi = 180 / f_pi; // 180 / pi for angles in degrees // ******************************************************************* // *** diff --git a/tasmota/support_rtc.ino b/tasmota/support_rtc.ino index ffb7239a6..ffaf171a3 100644 --- a/tasmota/support_rtc.ino +++ b/tasmota/support_rtc.ino @@ -379,7 +379,9 @@ void RtcSecond(void) Rtc.ntp_sync_minute = 1; // If sync prepare for a new cycle } uint8_t offset = (uptime < 30) ? RtcTime.second : (((ESP.getChipId() & 0xF) * 3) + 3) ; // First try ASAP to sync. If fails try once every 60 seconds based on chip id - if ( (((offset == RtcTime.second) && ( (RtcTime.year < 2016) || (Rtc.ntp_sync_minute == uptime_minute))) || ntp_force_sync ) ) { + if ( (((offset == RtcTime.second) && ( (RtcTime.year < 2016) || // Never synced + (Rtc.ntp_sync_minute == uptime_minute))) || // Re-sync every hour + ntp_force_sync ) ) { // Forced sync Rtc.ntp_time = sntp_get_current_timestamp(); if (Rtc.ntp_time > START_VALID_TIME) { // Fix NTP bug in core 2.4.1/SDK 2.2.1 (returns Thu Jan 01 08:00:10 1970 after power on) ntp_force_sync = false; @@ -467,9 +469,9 @@ void RtcSetTime(uint32_t epoch) void RtcInit(void) { - sntp_setservername(0, Settings.ntp_server[0]); - sntp_setservername(1, Settings.ntp_server[1]); - sntp_setservername(2, Settings.ntp_server[2]); + sntp_setservername(0, SettingsText(SET_NTPSERVER1)); + sntp_setservername(1, SettingsText(SET_NTPSERVER2)); + sntp_setservername(2, SettingsText(SET_NTPSERVER3)); sntp_stop(); sntp_set_timezone(0); // UTC time sntp_init(); diff --git a/tasmota/support_statistics.ino b/tasmota/support_statistics.ino new file mode 100644 index 000000000..b8f65bd20 --- /dev/null +++ b/tasmota/support_statistics.ino @@ -0,0 +1,76 @@ +/* + support_statistics.ino - gather statistics 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 . +*/ + +#define USE_STATS_CODE + +#ifdef USE_STATS_CODE +/*********************************************************************************************\ + * Gather statistics +\*********************************************************************************************/ + +String GetStatistics(void) +{ + char data[40]; + + if (Settings.version < 0x08000000) { + uint32_t str_len = 0; + for (uint32_t i = 0; i < 2; i++) { + str_len += strlen(Settings.sta_ssid[i]); + str_len += strlen(Settings.sta_pwd[i]); + } + for (uint32_t i = 0; i < 3; i++) { + str_len += strlen(Settings.mqtt_prefix[i]); + str_len += strlen(Settings.ntp_server[i]); + } + for (uint32_t i = 0; i < 4; i++) { + str_len += strlen(Settings.state_text[i]); + str_len += strlen(Settings.friendlyname[i]); + } + for (uint32_t i = 0; i < MAX_RULE_MEMS; i++) { + str_len += strlen(Settings.mems[i]); + } + str_len += strlen(Settings.ota_url); + str_len += strlen(Settings.hostname); + str_len += strlen(Settings.syslog_host); + str_len += strlen(Settings.mqtt_host); + str_len += strlen(Settings.mqtt_client); + str_len += strlen(Settings.mqtt_user); + str_len += strlen(Settings.mqtt_pwd); + str_len += strlen(Settings.mqtt_topic); + str_len += strlen(Settings.button_topic); + str_len += strlen(Settings.switch_topic); + str_len += strlen(Settings.mqtt_grptopic); + str_len += strlen(Settings.web_password); + str_len += strlen(Settings.mqtt_fulltopic); + str_len += strlen(Settings.cors_domain); + snprintf_P(data, sizeof(data), PSTR(",\"CR\":\"%d/1151\""), 37 + str_len); // Char Usage Ratio + } else { + snprintf_P(data, sizeof(data), PSTR(",\"CR\":\"%d/%d\""), GetSettingsTextLen(), settings_text_size); // Char Usage Ratio + } + return String(data); +} + +#else + +String GetStatistics(void) +{ + return String(""); +} + +#endif // USE_STATS_CODE \ No newline at end of file diff --git a/tasmota/support_tasmota.ino b/tasmota/support_tasmota.ino index e7099affd..ff2b6f40a 100644 --- a/tasmota/support_tasmota.ino +++ b/tasmota/support_tasmota.ino @@ -60,15 +60,16 @@ char* Format(char* output, const char* input, int size) 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); + if (strstr(SettingsText(SET_OTAURL), "%04d") != nullptr) { // OTA url contains placeholder for chip ID + snprintf(otaurl, otaurl_size, SettingsText(SET_OTAURL), 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 if (strstr(SettingsText(SET_OTAURL), "%d") != nullptr) { // OTA url contains placeholder for chip ID + snprintf_P(otaurl, otaurl_size, SettingsText(SET_OTAURL), ESP.getChipId()); } else { - strlcpy(otaurl, Settings.ota_url, otaurl_size); + strlcpy(otaurl, SettingsText(SET_OTAURL), otaurl_size); } + return otaurl; } @@ -101,17 +102,19 @@ char* GetTopic_P(char *stopic, uint32_t prefix, char *topic, const char* subtopi fulltopic += topic; // cmnd/ } } else { - fulltopic = Settings.mqtt_fulltopic; + fulltopic = SettingsText(SET_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); + if ('\0' == SettingsText(SET_MQTTPREFIX1 + i)) { + char temp[TOPSZ]; + SettingsUpdateText(SET_MQTTPREFIX1 + i, GetTextIndexed(temp, sizeof(temp), i, kPrefixes)); } } - fulltopic.replace(FPSTR(MQTT_TOKEN_PREFIX), Settings.mqtt_prefix[prefix]); + fulltopic.replace(FPSTR(MQTT_TOKEN_PREFIX), SettingsText(SET_MQTTPREFIX1 + prefix)); + fulltopic.replace(FPSTR(MQTT_TOKEN_TOPIC), topic); fulltopic.replace(F("%hostname%"), my_hostname); String token_id = WiFi.macAddress(); @@ -131,7 +134,7 @@ 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) + return GetTopic_P(stopic, (Settings.flag3.grouptopic_mode) ? CMND +8 : CMND, SettingsText(SET_MQTT_GRP_TOPIC), subtopic); // SetOption75 - GroupTopic replaces %topic% (0) or fixed topic cmnd/grouptopic (1) } char* GetFallbackTopic_P(char *stopic, const char* subtopic) @@ -144,7 +147,7 @@ char* GetStateText(uint32_t state) if (state > 3) { state = 1; } - return Settings.state_text[state]; + return SettingsText(SET_STATE_TXT1 + state); } /********************************************************************************************/ @@ -358,10 +361,10 @@ bool SendKey(uint32_t key, uint32_t device, uint32_t state) char stopic[TOPSZ]; char scommand[CMDSZ]; - char key_topic[sizeof(Settings.button_topic)]; + char key_topic[TOPSZ]; bool result = false; - char *tmp = (key) ? Settings.switch_topic : Settings.button_topic; + char *tmp = (key) ? SettingsText(SET_MQTT_SWITCH_TOPIC) : SettingsText(SET_MQTT_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)) { @@ -374,7 +377,7 @@ bool SendKey(uint32_t key, uint32_t device, uint32_t state) } 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)) && + !strcmp(SettingsText(SET_MQTT_GRP_TOPIC), key_topic)) && (POWER_TOGGLE == state)) { state = ~(power >> (device -1)) &1; // POWER_OFF or POWER_ON } @@ -583,8 +586,9 @@ void MqttShowState(void) 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()); + 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_SIGNAL "\":%d,\"" D_JSON_LINK_COUNT "\":%d,\"" D_JSON_DOWNTIME "\":\"%s\"}}"), + Settings.sta_active +1, SettingsText(SET_STASSID1 + Settings.sta_active), WiFi.BSSIDstr().c_str(), WiFi.channel(), + WifiGetRssiAsQuality(WiFi.RSSI()), WiFi.RSSI(), WifiLinkCount(), WifiDowntime().c_str()); } void MqttPublishTeleState(void) @@ -592,7 +596,7 @@ void MqttPublishTeleState(void) mqtt_data[0] = '\0'; MqttShowState(); MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_STATE), MQTT_TELE_RETAIN); -#ifdef USE_SCRIPT +#if defined(USE_RULES) || defined(USE_SCRIPT) RulesTeleperiod(); // Allow rule based HA messages #endif // USE_SCRIPT } @@ -812,7 +816,6 @@ void Every250mSeconds(void) 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); @@ -838,7 +841,7 @@ void Every250mSeconds(void) 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 + char *ech = strrchr(SettingsText(SET_OTAURL), '.'); // 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 } } @@ -902,26 +905,45 @@ void Every250mSeconds(void) } 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 - } + // Backup current SSIDs and Passwords + char storage_ssid1[strlen(SettingsText(SET_STASSID1)) +1]; + strncpy(storage_ssid1, SettingsText(SET_STASSID1), sizeof(storage_ssid1)); + char storage_ssid2[strlen(SettingsText(SET_STASSID2)) +1]; + strncpy(storage_ssid2, SettingsText(SET_STASSID2), sizeof(storage_ssid2)); + char storage_pass1[strlen(SettingsText(SET_STAPWD1)) +1]; + strncpy(storage_pass1, SettingsText(SET_STAPWD1), sizeof(storage_pass1)); + char storage_pass2[strlen(SettingsText(SET_STAPWD2)) +1]; + strncpy(storage_pass2, SettingsText(SET_STAPWD2), sizeof(storage_pass2)); + + char storage_mqtthost[strlen(SettingsText(SET_MQTT_HOST)) +1]; + strncpy(storage_mqtthost, SettingsText(SET_MQTT_HOST), sizeof(storage_mqtthost)); + char storage_mqttuser[strlen(SettingsText(SET_MQTT_USER)) +1]; + strncpy(storage_mqttuser, SettingsText(SET_MQTT_USER), sizeof(storage_mqttuser)); + char storage_mqttpwd[strlen(SettingsText(SET_MQTT_PWD)) +1]; + strncpy(storage_mqttpwd, SettingsText(SET_MQTT_PWD), sizeof(storage_mqttpwd)); + char storage_mqtttopic[strlen(SettingsText(SET_MQTT_TOPIC)) +1]; + strncpy(storage_mqtttopic, SettingsText(SET_MQTT_TOPIC), sizeof(storage_mqtttopic)); + uint16_t mqtt_port = Settings.mqtt_port; + +// if (216 == restart_flag) { + // 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 + // Restore current SSIDs and Passwords + SettingsUpdateText(SET_STASSID1, storage_ssid1); + SettingsUpdateText(SET_STASSID2, storage_ssid2); + SettingsUpdateText(SET_STAPWD1, storage_pass1); + SettingsUpdateText(SET_STAPWD2, storage_pass2); 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 + // Restore the mqtt host, port, client, username and password + SettingsUpdateText(SET_MQTT_HOST, storage_mqtthost); + SettingsUpdateText(SET_MQTT_USER, storage_mqttuser); + SettingsUpdateText(SET_MQTT_PWD, storage_mqttpwd); + SettingsUpdateText(SET_MQTT_TOPIC, storage_mqtttopic); + Settings.mqtt_port = mqtt_port; } restart_flag = 2; } @@ -972,7 +994,7 @@ void ArduinoOTAInit(void) { ArduinoOTA.setPort(8266); ArduinoOTA.setHostname(my_hostname); - if (Settings.web_password[0] !=0) { ArduinoOTA.setPassword(Settings.web_password); } + if (strlen(SettingsText(SET_WEBPWD))) { ArduinoOTA.setPassword(SettingsText(SET_WEBPWD)); } ArduinoOTA.onStart([]() { @@ -1285,15 +1307,19 @@ void GpioInit(void) } #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) { + for (uint32_t i = 0; i < MAX_PWMS; i++) { // Basic PWM control only + if (pin[GPIO_PWM1 +i] < 99) { + pinMode(pin[GPIO_PWM1 +i], OUTPUT); + if (light_type) { + // force PWM GPIOs to low or high mode, see #7165 + analogWrite(pin[GPIO_PWM1 +i], bitRead(pwm_inverted, i) ? Settings.pwm_range : 0); + } else { 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); diff --git a/tasmota/support_wifi.ino b/tasmota/support_wifi.ino index 49faa0663..86cc81dd8 100644 --- a/tasmota/support_wifi.ino +++ b/tasmota/support_wifi.ino @@ -191,18 +191,20 @@ void WifiBegin(uint8_t flag, uint8_t channel) case 2: // Toggle Settings.sta_active ^= 1; } // 3: Current AP - if ('\0' == Settings.sta_ssid[Settings.sta_active][0]) { Settings.sta_active ^= 1; } // Skip empty SSID + if ('\0' == SettingsText(SET_STASSID1 + Settings.sta_active)) { + Settings.sta_active ^= 1; // Skip empty SSID + } if (Settings.ip_address[0]) { WiFi.config(Settings.ip_address[0], Settings.ip_address[1], Settings.ip_address[2], Settings.ip_address[3]); // Set static IP } WiFi.hostname(my_hostname); if (channel) { - WiFi.begin(Settings.sta_ssid[Settings.sta_active], Settings.sta_pwd[Settings.sta_active], channel, Wifi.bssid); + WiFi.begin(SettingsText(SET_STASSID1 + Settings.sta_active), SettingsText(SET_STAPWD1 + Settings.sta_active), channel, Wifi.bssid); } else { - WiFi.begin(Settings.sta_ssid[Settings.sta_active], Settings.sta_pwd[Settings.sta_active]); + WiFi.begin(SettingsText(SET_STASSID1 + Settings.sta_active), SettingsText(SET_STAPWD1 + Settings.sta_active)); } AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_WIFI D_CONNECTING_TO_AP "%d %s " D_IN_MODE " 11%c " D_AS " %s..."), - Settings.sta_active +1, Settings.sta_ssid[Settings.sta_active], kWifiPhyMode[WiFi.getPhyMode() & 0x3], my_hostname); + Settings.sta_active +1, SettingsText(SET_STASSID1 + Settings.sta_active), kWifiPhyMode[WiFi.getPhyMode() & 0x3], my_hostname); #if LWIP_IPV6 for (bool configured = false; !configured;) { @@ -278,10 +280,10 @@ void WifiBeginAfterScan(void) bool known = false; uint32_t j; for (j = 0; j < 2; j++) { - if (ssid_scan == Settings.sta_ssid[j]) { // SSID match + if (ssid_scan == SettingsText(SET_STASSID1 + j)) { // SSID match known = true; if (rssi_scan > best_network_db) { // Best network - if (sec_scan == ENC_TYPE_NONE || Settings.sta_pwd[j]) { // Check for passphrase if not open wlan + if (sec_scan == ENC_TYPE_NONE || SettingsText(SET_STAPWD1 + j)) { // Check for passphrase if not open wlan best_network_db = (int8_t)rssi_scan; channel = chan_scan; ap = j; // AP1 or AP2 @@ -352,6 +354,14 @@ bool WifiCheckIPv6(void) return ipv6_global; } +String WifiGetIPv6(void) +{ + for (auto a : addrList) { + if(!a.isLocal() && a.isV6()) return a.toString(); + } + return ""; +} + bool WifiCheckIPAddrStatus(void) // Return false for 169.254.x.x or fe80::/64 { bool ip_global=false; @@ -426,7 +436,7 @@ void WifiCheckIp(void) if (!Wifi.retry || ((Wifi.retry_init / 2) == Wifi.retry)) { AddLog_P(LOG_LEVEL_INFO, S_LOG_WIFI, PSTR(D_CONNECT_FAILED_AP_TIMEOUT)); } else { - if (('\0' == Settings.sta_ssid[0][0]) && ('\0' == Settings.sta_ssid[1][0])) { + if (('\0' == SettingsText(SET_STASSID1)) && ('\0' == SettingsText(SET_STASSID2))) { wifi_config_tool = WIFI_MANAGER; // Skip empty SSIDs and start Wifi config tool Wifi.retry = 0; } else { @@ -472,13 +482,13 @@ void WifiCheck(uint8_t param) if (Wifi.config_counter) { if (!Wifi.config_counter) { if (strlen(WiFi.SSID().c_str())) { - strlcpy(Settings.sta_ssid[0], WiFi.SSID().c_str(), sizeof(Settings.sta_ssid[0])); + SettingsUpdateText(SET_STASSID1, WiFi.SSID().c_str()); } if (strlen(WiFi.psk().c_str())) { - strlcpy(Settings.sta_pwd[0], WiFi.psk().c_str(), sizeof(Settings.sta_pwd[0])); + SettingsUpdateText(SET_STAPWD1, WiFi.psk().c_str()); } Settings.sta_active = 0; - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_WIFI D_WCFG_2_WIFIMANAGER D_CMND_SSID "1 %s"), Settings.sta_ssid[0]); + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_WIFI D_WCFG_2_WIFIMANAGER D_CMND_SSID "1 %s"), SettingsText(SET_STASSID1)); } } if (!Wifi.config_counter) { @@ -617,6 +627,7 @@ void WifiShutdown(void) void EspRestart(void) { WifiShutdown(); + CrashDumpClear(); // Clear the stack dump in RTC // ESP.restart(); // This results in exception 3 on restarts on core 2.3.0 ESP.reset(); } diff --git a/tasmota/tasmota.h b/tasmota/tasmota.h index cefef3a06..1f676acd4 100644 --- a/tasmota/tasmota.h +++ b/tasmota/tasmota.h @@ -131,7 +131,7 @@ const uint32_t SOFT_BAUDRATE = 9600; // Default software serial baudrate const uint32_t APP_BAUDRATE = 115200; // Default serial baudrate const uint32_t SERIAL_POLLING = 100; // Serial receive polling in ms const uint32_t ZIGBEE_POLLING = 100; // Serial receive polling in ms -const uint8_t MAX_STATUS = 11; // Max number of status lines +const uint8_t MAX_STATUS = 12; // Max number of status lines const uint32_t START_VALID_TIME = 1451602800; // Time is synced and after 2016-01-01 @@ -272,6 +272,30 @@ enum XsnsFunctions {FUNC_SETTINGS_OVERRIDE, FUNC_PIN_STATE, FUNC_MODULE_INIT, FU enum AddressConfigSteps { ADDR_IDLE, ADDR_RECEIVE, ADDR_SEND }; +enum SettingsTextIndex { SET_OTAURL, + SET_MQTTPREFIX1, SET_MQTTPREFIX2, SET_MQTTPREFIX3, + SET_STASSID1, SET_STASSID2, + SET_STAPWD1, SET_STAPWD2, + SET_HOSTNAME, SET_SYSLOG_HOST, + SET_WEBPWD, + SET_MQTT_HOST, SET_MQTT_CLIENT, + SET_MQTT_USER, SET_MQTT_PWD, + SET_MQTT_FULLTOPIC, SET_MQTT_TOPIC, + SET_MQTT_BUTTON_TOPIC, SET_MQTT_SWITCH_TOPIC, SET_MQTT_GRP_TOPIC, + SET_STATE_TXT1, SET_STATE_TXT2, SET_STATE_TXT3, SET_STATE_TXT4, + SET_NTPSERVER1, SET_NTPSERVER2, SET_NTPSERVER3, + SET_MEM1, SET_MEM2, SET_MEM3, SET_MEM4, SET_MEM5, + SET_CORS, + SET_FRIENDLYNAME1, SET_FRIENDLYNAME2, SET_FRIENDLYNAME3, SET_FRIENDLYNAME4, + +// SET_FRIENDLYNAME5, SET_FRIENDLYNAME6, SET_FRIENDLYNAME7, SET_FRIENDLYNAME8, // Future extension +// SET_BUTTON1, SET_BUTTON2, SET_BUTTON3, SET_BUTTON4, // Future extension +// SET_BUTTON5, SET_BUTTON6, SET_BUTTON7, SET_BUTTON8, // Future extension +// SET_BUTTON9, SET_BUTTON10, SET_BUTTON11, SET_BUTTON12, // Future extension +// SET_BUTTON13, SET_BUTTON14, SET_BUTTON15, SET_BUTTON16, // Future extension + + SET_MAX }; + enum CommandSource { SRC_IGNORE, SRC_MQTT, SRC_RESTART, SRC_BUTTON, SRC_SWITCH, SRC_BACKLOG, SRC_SERIAL, SRC_WEBGUI, SRC_WEBCOMMAND, SRC_WEBCONSOLE, SRC_PULSETIMER, SRC_TIMER, SRC_RULE, SRC_MAXPOWER, SRC_MAXENERGY, SRC_OVERTEMP, SRC_LIGHT, SRC_KNX, SRC_DISPLAY, SRC_WEMO, SRC_HUE, SRC_RETRY, SRC_REMOTE, SRC_SHUTTER, SRC_MAX }; diff --git a/tasmota/tasmota.ino b/tasmota/tasmota.ino index e10dfdb63..3b9e64a25 100644 --- a/tasmota/tasmota.ino +++ b/tasmota/tasmota.ino @@ -74,8 +74,6 @@ const char kCodeImage[] PROGMEM = "tasmota|minimal|sensors|knx|basic|display|ir" * Global variables \*********************************************************************************************/ -SerialConfig serial_config = SERIAL_8N1; // Serial interface configuration 8 data bits, No parity, 1 stop bit - WiFiUDP PortUdp; // UDP Syslog and Alexa unsigned long feature_drv1; // Compiled driver feature map @@ -112,7 +110,6 @@ uint32_t web_log_index = 1; // Index in Web log buffer (should n float global_temperature = 9999; // Provide a global temperature to be used by some sensors float global_humidity = 0; // Provide a global humidity to be used by some sensors float global_pressure = 0; // Provide a global pressure to be used by some sensors -char *ota_url; // OTA url string pointer uint16_t tele_period = 9999; // Tele period timer uint16_t mqtt_cmnd_publish = 0; // ignore flag for publish command uint16_t blink_counter = 0; // Number of blink cycles @@ -267,13 +264,13 @@ void setup(void) } } - Format(mqtt_client, Settings.mqtt_client, sizeof(mqtt_client)); - Format(mqtt_topic, Settings.mqtt_topic, sizeof(mqtt_topic)); - if (strstr(Settings.hostname, "%") != nullptr) { - strlcpy(Settings.hostname, WIFI_HOSTNAME, sizeof(Settings.hostname)); - snprintf_P(my_hostname, sizeof(my_hostname)-1, Settings.hostname, mqtt_topic, ESP.getChipId() & 0x1FFF); + Format(mqtt_client, SettingsText(SET_MQTT_CLIENT), sizeof(mqtt_client)); + Format(mqtt_topic, SettingsText(SET_MQTT_TOPIC), sizeof(mqtt_topic)); + if (strstr(SettingsText(SET_HOSTNAME), "%") != nullptr) { + SettingsUpdateText(SET_HOSTNAME, WIFI_HOSTNAME); + snprintf_P(my_hostname, sizeof(my_hostname)-1, SettingsText(SET_HOSTNAME), mqtt_topic, ESP.getChipId() & 0x1FFF); } else { - snprintf_P(my_hostname, sizeof(my_hostname)-1, Settings.hostname); + snprintf_P(my_hostname, sizeof(my_hostname)-1, SettingsText(SET_HOSTNAME)); } GetEspHardwareType(); @@ -332,7 +329,7 @@ void setup(void) } blink_powersave = power; - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_PROJECT " %s %s " D_VERSION " %s%s-" ARDUINO_ESP8266_RELEASE), PROJECT, Settings.friendlyname[0], my_version, my_image); + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_PROJECT " %s %s " D_VERSION " %s%s-" ARDUINO_ESP8266_RELEASE), PROJECT, SettingsText(SET_FRIENDLYNAME1), my_version, my_image); #ifdef FIRMWARE_MINIMAL AddLog_P2(LOG_LEVEL_INFO, PSTR(D_WARNING_MINIMAL_VERSION)); #endif // FIRMWARE_MINIMAL @@ -351,15 +348,18 @@ void BacklogLoop(void) { if (TimeReached(backlog_delay)) { if (!BACKLOG_EMPTY && !backlog_mutex) { - backlog_mutex = true; #ifdef SUPPORT_IF_STATEMENT - ExecuteCommand((char*)backlog.shift().c_str(), SRC_BACKLOG); + backlog_mutex = true; + String cmd = backlog.shift(); + backlog_mutex = false; + ExecuteCommand((char*)cmd.c_str(), SRC_BACKLOG); #else + backlog_mutex = true; ExecuteCommand((char*)backlog[backlog_pointer].c_str(), SRC_BACKLOG); backlog_pointer++; if (backlog_pointer >= MAX_BACKLOG) { backlog_pointer = 0; } -#endif backlog_mutex = false; +#endif } } } diff --git a/tasmota/tasmota_post.h b/tasmota/tasmota_post.h index be3b503f0..cc47e3fc1 100644 --- a/tasmota/tasmota_post.h +++ b/tasmota/tasmota_post.h @@ -512,7 +512,7 @@ char* ToHex_P(const unsigned char * in, size_t insz, char * out, size_t outsz, c #undef USE_ARDUINO_OTA // Disable support for Arduino OTA #undef USE_DOMOTICZ // Disable Domoticz #undef USE_HOME_ASSISTANT // Disable Home Assistant -#undef USE_MQTT_TLS // Disable TLS support won't work as the MQTTHost is not set +//#undef USE_MQTT_TLS // Disable TLS support won't work as the MQTTHost is not set #undef USE_KNX // Disable KNX IP Protocol Support //#undef USE_WEBSERVER // Disable Webserver #undef USE_WEBSEND_RESPONSE // Disable command WebSend response message (+1k code) diff --git a/tasmota/tasmota_version.h b/tasmota/tasmota_version.h index f8eee8abd..8fe799b5b 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 = 0x07010200; +const uint32_t VERSION = 0x07020000; #endif // _TASMOTA_VERSION_H_ diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index 48e2cd26a..9c8131ad3 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -414,7 +414,8 @@ const char HTTP_FORM_WIFI[] PROGMEM = "

" D_AP1_PASSWORD "

" "

" D_AP2_SSID " (" STA_SSID2 ")

" "

" D_AP2_PASSWORD "

" - "

" D_HOSTNAME " (%s)

"; + "

" D_HOSTNAME " (%s)

" + "

" D_CORS_DOMAIN "

"; const char HTTP_FORM_LOG1[] PROGMEM = "
 " D_LOGGING_PARAMETERS " " @@ -597,7 +598,13 @@ void StartWebserver(int type, IPAddress ipweb) WebServer->begin(); // Web server start } if (Web.state != type) { +#if LWIP_IPV6 + String ipv6_addr = WifiGetIPv6(); + if(ipv6_addr!="") AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_HTTP D_WEBSERVER_ACTIVE_ON " %s%s " D_WITH_IP_ADDRESS " %s and IPv6 global address %s "), my_hostname, (Wifi.mdns_begun) ? ".local" : "", ipweb.toString().c_str(),ipv6_addr.c_str()); + else AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_HTTP D_WEBSERVER_ACTIVE_ON " %s%s " D_WITH_IP_ADDRESS " %s"), my_hostname, (Wifi.mdns_begun) ? ".local" : "", ipweb.toString().c_str()); +#else AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_HTTP D_WEBSERVER_ACTIVE_ON " %s%s " D_WITH_IP_ADDRESS " %s"), my_hostname, (Wifi.mdns_begun) ? ".local" : "", ipweb.toString().c_str()); +#endif // LWIP_IPV6 = 1 rules_flag.http_init = 1; } if (type) { Web.state = type; } @@ -651,8 +658,8 @@ void PollDnsWebserver(void) bool WebAuthenticate(void) { - if (Settings.web_password[0] != 0 && HTTP_MANAGER_RESET_ONLY != Web.state) { - return WebServer->authenticate(WEB_USERNAME, Settings.web_password); + if (strlen(SettingsText(SET_WEBPWD)) && (HTTP_MANAGER_RESET_ONLY != Web.state)) { + return WebServer->authenticate(WEB_USERNAME, SettingsText(SET_WEBPWD)); } else { return true; } @@ -673,8 +680,8 @@ bool HttpCheckPriviledgedAccess(bool autorequestauth = true) void HttpHeaderCors(void) { - if (Settings.flag3.cors_enabled) { // SetOption73 - Enable HTTP CORS - WebServer->sendHeader(F("Access-Control-Allow-Origin"), F("*")); + if (strlen(SettingsText(SET_CORS))) { + WebServer->sendHeader(F("Access-Control-Allow-Origin"), SettingsText(SET_CORS)); } } @@ -809,7 +816,7 @@ void WSContentSend_PD(const char* formatP, ...) // Content send snprintf_P ch void WSContentStart_P(const char* title, bool auth) { - if (auth && (Settings.web_password[0] != 0) && !WebServer->authenticate(WEB_USERNAME, Settings.web_password)) { + if (auth && strlen(SettingsText(SET_WEBPWD)) && !WebServer->authenticate(WEB_USERNAME, SettingsText(SET_WEBPWD))) { return WebServer->requestAuthentication(); } @@ -818,7 +825,7 @@ void WSContentStart_P(const char* title, bool auth) if (title != nullptr) { char ctitle[strlen_P(title) +1]; strcpy_P(ctitle, title); // Get title from flash to RAM - WSContentSend_P(HTTP_HEADER, Settings.friendlyname[0], ctitle); + WSContentSend_P(HTTP_HEADER, SettingsText(SET_FRIENDLYNAME1), ctitle); } } @@ -862,7 +869,7 @@ void WSContentSendStyle_P(const char* formatP, ...) WebColor(COL_TEXT_WARNING), #endif WebColor(COL_TITLE), - ModuleName().c_str(), Settings.friendlyname[0]); + ModuleName().c_str(), SettingsText(SET_FRIENDLYNAME1)); if (Settings.flag3.gui_hostname_ip) { // SetOption53 - Show hostanme and IP address in GUI main menu bool lip = (static_cast(WiFi.localIP()) != 0); bool sip = (static_cast(WiFi.softAPIP()) != 0); @@ -986,10 +993,10 @@ void HandleRoot(void) if (WifiIsInManagerMode()) { #ifndef FIRMWARE_MINIMAL - if ((Settings.web_password[0] != 0) && !(WebServer->hasArg("USER1")) && !(WebServer->hasArg("PASS1")) && HTTP_MANAGER_RESET_ONLY != Web.state) { + if (strlen(SettingsText(SET_WEBPWD)) && !(WebServer->hasArg("USER1")) && !(WebServer->hasArg("PASS1")) && HTTP_MANAGER_RESET_ONLY != Web.state) { HandleWifiLogin(); } else { - if (!(Settings.web_password[0] != 0) || (((WebServer->arg("USER1") == WEB_USERNAME ) && (WebServer->arg("PASS1") == Settings.web_password )) || HTTP_MANAGER_RESET_ONLY == Web.state)) { + if (!strlen(SettingsText(SET_WEBPWD)) || (((WebServer->arg("USER1") == WEB_USERNAME ) && (WebServer->arg("PASS1") == SettingsText(SET_WEBPWD) )) || HTTP_MANAGER_RESET_ONLY == Web.state)) { HandleWifiConfiguration(); } else { // wrong user and pass @@ -1106,6 +1113,21 @@ void HandleRoot(void) } else { #endif // USE_SONOFF_IFAN for (uint32_t idx = 1; idx <= devices_present; idx++) { +#ifdef USE_SHUTTER + if (Settings.flag3.shutter_mode) { // SetOption80 - Enable shutter support + bool shutter_used = false; + for (uint32_t i = 0; i < MAX_SHUTTERS; i++) { + if (Settings.shutter_startrelay[i] == (((idx -1) & 0xFFFFFFFE) +1)) { + shutter_used = true; + break; + } + } + if (shutter_used) { + WSContentSend_P(HTTP_DEVICE_CONTROL, 100 / devices_present, idx, (idx % 2) ? "▲" : "▼" , ""); + continue; + } + } +#endif // USE_SHUTTER snprintf_P(stemp, sizeof(stemp), PSTR(" %d"), idx); WSContentSend_P(HTTP_DEVICE_CONTROL, 100 / devices_present, idx, (devices_present < 5) ? D_BUTTON_TOGGLE : "", (devices_present > 1) ? stemp : ""); } @@ -1638,11 +1660,11 @@ void HandleWifiConfiguration(void) int quality = WifiGetRssiAsQuality(WiFi.RSSI(indices[i])); int auth = WiFi.encryptionType(indices[i]); char encryption[20]; - WSContentSend_P(PSTR("
%s (%d) %s %d%%
"), + WSContentSend_P(PSTR("
%s (%d) %s %d%% (%d dBm)
"), HtmlEscape(WiFi.SSID(indices[i])).c_str(), WiFi.channel(indices[i]), GetTextIndexed(encryption, sizeof(encryption), auth +1, kEncryptionType), - quality + quality, WiFi.RSSI() ); delay(0); @@ -1654,7 +1676,7 @@ void HandleWifiConfiguration(void) } // As WIFI_HOSTNAME may contain %s-%04d it cannot be part of HTTP_FORM_WIFI where it will exception - WSContentSend_P(HTTP_FORM_WIFI, Settings.sta_ssid[0], Settings.sta_ssid[1], WIFI_HOSTNAME, WIFI_HOSTNAME, Settings.hostname); + WSContentSend_P(HTTP_FORM_WIFI, SettingsText(SET_STASSID1), SettingsText(SET_STASSID2), WIFI_HOSTNAME, WIFI_HOSTNAME, SettingsText(SET_HOSTNAME), SettingsText(SET_CORS)); WSContentSend_P(HTTP_FORM_END); } @@ -1671,22 +1693,25 @@ void HandleWifiConfiguration(void) void WifiSaveSettings(void) { - char tmp[sizeof(Settings.sta_pwd[0])]; // Max length is currently 65 + char tmp[100]; // Max length is currently 65 WebGetArg("h", tmp, sizeof(tmp)); - strlcpy(Settings.hostname, (!strlen(tmp)) ? WIFI_HOSTNAME : tmp, sizeof(Settings.hostname)); - if (strstr(Settings.hostname, "%") != nullptr) { - strlcpy(Settings.hostname, WIFI_HOSTNAME, sizeof(Settings.hostname)); + SettingsUpdateText(SET_HOSTNAME, (!strlen(tmp)) ? WIFI_HOSTNAME : tmp); + if (strstr(SettingsText(SET_HOSTNAME), "%") != nullptr) { + SettingsUpdateText(SET_HOSTNAME, WIFI_HOSTNAME); } + WebGetArg("c", tmp, sizeof(tmp)); + SettingsUpdateText(SET_CORS, (!strlen(tmp)) ? CORS_DOMAIN : tmp); WebGetArg("s1", tmp, sizeof(tmp)); - strlcpy(Settings.sta_ssid[0], (!strlen(tmp)) ? STA_SSID1 : tmp, sizeof(Settings.sta_ssid[0])); + SettingsUpdateText(SET_STASSID1, (!strlen(tmp)) ? STA_SSID1 : tmp); WebGetArg("s2", tmp, sizeof(tmp)); - strlcpy(Settings.sta_ssid[1], (!strlen(tmp)) ? STA_SSID2 : tmp, sizeof(Settings.sta_ssid[1])); + SettingsUpdateText(SET_STASSID2, (!strlen(tmp)) ? STA_SSID2 : tmp); WebGetArg("p1", tmp, sizeof(tmp)); - strlcpy(Settings.sta_pwd[0], (!strlen(tmp)) ? "" : (strlen(tmp) < 5) ? Settings.sta_pwd[0] : tmp, sizeof(Settings.sta_pwd[0])); + SettingsUpdateText(SET_STAPWD1, (!strlen(tmp)) ? "" : (strlen(tmp) < 5) ? SettingsText(SET_STAPWD1) : tmp); WebGetArg("p2", tmp, sizeof(tmp)); - strlcpy(Settings.sta_pwd[1], (!strlen(tmp)) ? "" : (strlen(tmp) < 5) ? Settings.sta_pwd[1] : tmp, sizeof(Settings.sta_pwd[1])); - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_WIFI D_CMND_HOSTNAME " %s, " D_CMND_SSID "1 %s, " D_CMND_SSID "2 %s"), Settings.hostname, Settings.sta_ssid[0], Settings.sta_ssid[1]); + SettingsUpdateText(SET_STAPWD2, (!strlen(tmp)) ? "" : (strlen(tmp) < 5) ? SettingsText(SET_STAPWD2) : tmp); + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_WIFI D_CMND_HOSTNAME " %s, " D_CMND_SSID "1 %s, " D_CMND_SSID "2 %s, " D_CMND_CORS " %s"), + SettingsText(SET_HOSTNAME), SettingsText(SET_STASSID1), SettingsText(SET_STASSID2), SettingsText(SET_CORS)); } /*-------------------------------------------------------------------------------------------*/ @@ -1723,7 +1748,7 @@ void HandleLoggingConfiguration(void) } WSContentSend_P(PSTR("

")); } - WSContentSend_P(HTTP_FORM_LOG2, Settings.syslog_host, Settings.syslog_port, Settings.tele_period); + WSContentSend_P(HTTP_FORM_LOG2, SettingsText(SET_SYSLOG_HOST), Settings.syslog_port, Settings.tele_period); WSContentSend_P(HTTP_FORM_END); WSContentSpaceButton(BUTTON_CONFIGURATION); WSContentStop(); @@ -1731,7 +1756,7 @@ void HandleLoggingConfiguration(void) void LoggingSaveSettings(void) { - char tmp[sizeof(Settings.syslog_host)]; // Max length is currently 33 + char tmp[100]; // Max length is currently 33 WebGetArg("l0", tmp, sizeof(tmp)); SetSeriallog((!strlen(tmp)) ? SERIAL_LOG_LEVEL : atoi(tmp)); @@ -1742,7 +1767,7 @@ void LoggingSaveSettings(void) WebGetArg("l3", tmp, sizeof(tmp)); SetSyslog((!strlen(tmp)) ? SYS_LOG_LEVEL : atoi(tmp)); WebGetArg("lh", tmp, sizeof(tmp)); - strlcpy(Settings.syslog_host, (!strlen(tmp)) ? SYS_LOG_HOST : tmp, sizeof(Settings.syslog_host)); + SettingsUpdateText(SET_SYSLOG_HOST, (!strlen(tmp)) ? SYS_LOG_HOST : tmp); WebGetArg("lp", tmp, sizeof(tmp)); Settings.syslog_port = (!strlen(tmp)) ? SYS_LOG_PORT : atoi(tmp); WebGetArg("lt", tmp, sizeof(tmp)); @@ -1751,7 +1776,7 @@ void LoggingSaveSettings(void) Settings.tele_period = 10; // Do not allow periods < 10 seconds } AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_LOG D_CMND_SERIALLOG " %d, " D_CMND_WEBLOG " %d, " D_CMND_MQTTLOG " %d, " D_CMND_SYSLOG " %d, " D_CMND_LOGHOST " %s, " D_CMND_LOGPORT " %d, " D_CMND_TELEPERIOD " %d"), - Settings.seriallog_level, Settings.weblog_level, Settings.mqttlog_level, Settings.syslog_level, Settings.syslog_host, Settings.syslog_port, Settings.tele_period); + Settings.seriallog_level, Settings.weblog_level, Settings.mqttlog_level, Settings.syslog_level, SettingsText(SET_SYSLOG_HOST), Settings.syslog_port, Settings.tele_period); } /*-------------------------------------------------------------------------------------------*/ @@ -1787,7 +1812,7 @@ void HandleOtherConfiguration(void) (i) ? stemp : "", i, (i) ? stemp : "", - Settings.friendlyname[i]); + SettingsText(SET_FRIENDLYNAME1 + i)); } #ifdef USE_EMULATION @@ -1819,10 +1844,10 @@ void OtherSaveSettings(void) { char tmp[128]; char webindex[5]; - char friendlyname[sizeof(Settings.friendlyname[0])]; + char friendlyname[TOPSZ]; WebGetArg("wp", tmp, sizeof(tmp)); - strlcpy(Settings.web_password, (!strlen(tmp)) ? "" : (strchr(tmp,'*')) ? Settings.web_password : tmp, sizeof(Settings.web_password)); + SettingsUpdateText(SET_WEBPWD, (!strlen(tmp)) ? "" : (strchr(tmp,'*')) ? SettingsText(SET_WEBPWD) : tmp); Settings.flag.mqtt_enabled = WebServer->hasArg("b1"); // SetOption3 - Enable MQTT #ifdef USE_EMULATION WebGetArg("b2", tmp, sizeof(tmp)); @@ -1833,8 +1858,8 @@ void OtherSaveSettings(void) snprintf_P(webindex, sizeof(webindex), PSTR("a%d"), i); WebGetArg(webindex, tmp, sizeof(tmp)); snprintf_P(friendlyname, sizeof(friendlyname), PSTR(FRIENDLY_NAME"%d"), i +1); - strlcpy(Settings.friendlyname[i], (!strlen(tmp)) ? (i) ? friendlyname : FRIENDLY_NAME : tmp, sizeof(Settings.friendlyname[i])); - snprintf_P(log_data, sizeof(log_data), PSTR("%s%s %s"), log_data, (i) ? "," : "", Settings.friendlyname[i]); + SettingsUpdateText(SET_FRIENDLYNAME1 +i, (!strlen(tmp)) ? (i) ? friendlyname : FRIENDLY_NAME : tmp); + snprintf_P(log_data, sizeof(log_data), PSTR("%s%s %s"), log_data, (i) ? "," : "", SettingsText(SET_FRIENDLYNAME1 +i)); } AddLog(LOG_LEVEL_INFO); WebGetArg("t1", tmp, sizeof(tmp)); @@ -1866,8 +1891,8 @@ void HandleBackupConfiguration(void) char attachment[100]; -// char friendlyname[sizeof(Settings.friendlyname[0])]; -// snprintf_P(attachment, sizeof(attachment), PSTR("attachment; filename=Config_%s_%s.dmp"), NoAlNumToUnderscore(friendlyname, Settings.friendlyname[0]), my_version); +// char friendlyname[TOPSZ]; +// snprintf_P(attachment, sizeof(attachment), PSTR("attachment; filename=Config_%s_%s.dmp"), NoAlNumToUnderscore(friendlyname, SettingsText(SET_FRIENDLYNAME1)), my_version); char hostname[sizeof(my_hostname)]; snprintf_P(attachment, sizeof(attachment), PSTR("attachment; filename=Config_%s_%s.dmp"), NoAlNumToUnderscore(hostname, my_hostname), my_version); @@ -1967,11 +1992,17 @@ void HandleInformation(void) if (IsModuleIfan()) { maxfn = 1; } #endif // USE_SONOFF_IFAN for (uint32_t i = 0; i < maxfn; i++) { - WSContentSend_P(PSTR("}1" D_FRIENDLY_NAME " %d}2%s"), i +1, Settings.friendlyname[i]); + WSContentSend_P(PSTR("}1" D_FRIENDLY_NAME " %d}2%s"), i +1, SettingsText(SET_FRIENDLYNAME1 +i)); } WSContentSend_P(PSTR("}1}2 ")); // Empty line - WSContentSend_P(PSTR("}1" D_AP "%d " D_SSID " (" D_RSSI ")}2%s (%d%%)"), Settings.sta_active +1, Settings.sta_ssid[Settings.sta_active], WifiGetRssiAsQuality(WiFi.RSSI())); + WSContentSend_P(PSTR("}1" D_AP "%d " D_SSID " (" D_RSSI ")}2%s (%d%%, %d dBm)"), Settings.sta_active +1, SettingsText(SET_STASSID1 + Settings.sta_active), WifiGetRssiAsQuality(WiFi.RSSI()), WiFi.RSSI()); WSContentSend_P(PSTR("}1" D_HOSTNAME "}2%s%s"), my_hostname, (Wifi.mdns_begun) ? ".local" : ""); +#if LWIP_IPV6 + String ipv6_addr = WifiGetIPv6(); + if(ipv6_addr != ""){ + WSContentSend_P(PSTR("}1 IPv6 Address }2%s"), ipv6_addr.c_str()); + } +#endif if (static_cast(WiFi.localIP()) != 0) { WSContentSend_P(PSTR("}1" D_IP_ADDRESS "}2%s"), WiFi.localIP().toString().c_str()); WSContentSend_P(PSTR("}1" D_GATEWAY "}2%s"), IPAddress(Settings.ip_address[1]).toString().c_str()); @@ -1986,17 +2017,12 @@ void HandleInformation(void) } WSContentSend_P(PSTR("}1}2 ")); // Empty line if (Settings.flag.mqtt_enabled) { // SetOption3 - Enable MQTT -#ifdef USE_MQTT_AWS_IOT - WSContentSend_P(PSTR("}1" D_MQTT_HOST "}2%s%s"), Settings.mqtt_user, Settings.mqtt_host); + WSContentSend_P(PSTR("}1" D_MQTT_HOST "}2%s"), SettingsText(SET_MQTT_HOST)); WSContentSend_P(PSTR("}1" D_MQTT_PORT "}2%d"), Settings.mqtt_port); -#else - WSContentSend_P(PSTR("}1" D_MQTT_HOST "}2%s"), Settings.mqtt_host); - WSContentSend_P(PSTR("}1" D_MQTT_PORT "}2%d"), Settings.mqtt_port); - WSContentSend_P(PSTR("}1" D_MQTT_USER "}2%s"), Settings.mqtt_user); -#endif + WSContentSend_P(PSTR("}1" D_MQTT_USER "}2%s"), SettingsText(SET_MQTT_USER)); WSContentSend_P(PSTR("}1" D_MQTT_CLIENT "}2%s"), mqtt_client); - WSContentSend_P(PSTR("}1" D_MQTT_TOPIC "}2%s"), Settings.mqtt_topic); -// WSContentSend_P(PSTR("}1" D_MQTT_GROUP_TOPIC "}2%s"), Settings.mqtt_grptopic); + WSContentSend_P(PSTR("}1" D_MQTT_TOPIC "}2%s"), SettingsText(SET_MQTT_TOPIC)); +// WSContentSend_P(PSTR("}1" D_MQTT_GROUP_TOPIC "}2%s"), SettingsText(SET_MQTT_GRP_TOPIC)); WSContentSend_P(PSTR("}1" D_MQTT_GROUP_TOPIC "}2%s"), GetGroupTopic_P(stopic, "")); WSContentSend_P(PSTR("}1" D_MQTT_FULL_TOPIC "}2%s"), GetTopic_P(stopic, CMND, mqtt_topic, "")); WSContentSend_P(PSTR("}1" D_MQTT " " D_FALLBACK_TOPIC "}2%s"), GetFallbackTopic_P(stopic, "")); @@ -2055,7 +2081,7 @@ void HandleUpgradeFirmware(void) WSContentStart_P(S_FIRMWARE_UPGRADE); WSContentSendStyle(); - WSContentSend_P(HTTP_FORM_UPG, Settings.ota_url); + WSContentSend_P(HTTP_FORM_UPG, SettingsText(SET_OTAURL)); WSContentSend_P(HTTP_FORM_RST_UPG, D_UPGRADE); WSContentSpaceButton(BUTTON_MAIN); WSContentStop(); @@ -2068,12 +2094,12 @@ void HandleUpgradeFirmwareStart(void) { if (!HttpCheckPriviledgedAccess()) { return; } - char command[sizeof(Settings.ota_url) + 10]; // OtaUrl + char command[128]; // OtaUrl AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_UPGRADE_STARTED)); WifiConfigCounter(); - char otaurl[sizeof(Settings.ota_url)]; + char otaurl[101]; WebGetArg("o", otaurl, sizeof(otaurl)); if (strlen(otaurl)) { snprintf_P(command, sizeof(command), PSTR(D_CMND_OTAURL " %s"), otaurl); @@ -2382,12 +2408,12 @@ void HandleHttpCommand(void) AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_COMMAND)); bool valid = true; - if (Settings.web_password[0] != 0) { - char tmp1[sizeof(Settings.web_password)]; + if (strlen(SettingsText(SET_WEBPWD))) { + char tmp1[33]; WebGetArg("user", tmp1, sizeof(tmp1)); - char tmp2[sizeof(Settings.web_password)]; + char tmp2[strlen(SettingsText(SET_WEBPWD)) +1]; WebGetArg("password", tmp2, sizeof(tmp2)); - if (!(!strcmp(tmp1, WEB_USERNAME) && !strcmp(tmp2, Settings.web_password))) { valid = false; } + if (!(!strcmp(tmp1, WEB_USERNAME) && !strcmp(tmp2, SettingsText(SET_WEBPWD)))) { valid = false; } } WSContentBegin(200, CT_JSON); @@ -2698,7 +2724,7 @@ const char kWebCommands[] PROGMEM = "|" // No prefix #ifdef USE_SENDMAIL D_CMND_SENDMAIL "|" #endif - D_CMND_WEBSERVER "|" D_CMND_WEBPASSWORD "|" D_CMND_WEBLOG "|" D_CMND_WEBREFRESH "|" D_CMND_WEBSEND "|" D_CMND_WEBCOLOR "|" D_CMND_WEBSENSOR; + D_CMND_WEBSERVER "|" D_CMND_WEBPASSWORD "|" D_CMND_WEBLOG "|" D_CMND_WEBREFRESH "|" D_CMND_WEBSEND "|" D_CMND_WEBCOLOR "|" D_CMND_WEBSENSOR "|" D_CMND_CORS; void (* const WebCommand[])(void) PROGMEM = { #ifdef USE_EMULATION @@ -2707,7 +2733,7 @@ void (* const WebCommand[])(void) PROGMEM = { #ifdef USE_SENDMAIL &CmndSendmail, #endif - &CmndWebServer, &CmndWebPassword, &CmndWeblog, &CmndWebRefresh, &CmndWebSend, &CmndWebColor, &CmndWebSensor }; + &CmndWebServer, &CmndWebPassword, &CmndWeblog, &CmndWebRefresh, &CmndWebSend, &CmndWebColor, &CmndWebSensor, &CmndCors }; /*********************************************************************************************\ * Commands @@ -2760,9 +2786,9 @@ void CmndWebServer(void) void CmndWebPassword(void) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.web_password))) { - strlcpy(Settings.web_password, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? WEB_PASSWORD : XdrvMailbox.data, sizeof(Settings.web_password)); - ResponseCmndChar(Settings.web_password); + if (XdrvMailbox.data_len > 0) { + SettingsUpdateText(SET_WEBPWD, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? WEB_PASSWORD : XdrvMailbox.data); + ResponseCmndChar(SettingsText(SET_WEBPWD)); } else { Response_P(S_JSON_COMMAND_ASTERISK, XdrvMailbox.command); } @@ -2828,6 +2854,14 @@ void CmndWebSensor(void) ResponseJsonEnd(); } +void CmndCors(void) +{ + if (XdrvMailbox.data_len > 0) { + SettingsUpdateText(SET_CORS, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? WEB_PASSWORD : XdrvMailbox.data); + } + ResponseCmndChar(SettingsText(SET_CORS)); +} + /*********************************************************************************************\ * Interface \*********************************************************************************************/ diff --git a/tasmota/xdrv_02_mqtt.ino b/tasmota/xdrv_02_mqtt.ino index 49f35d579..47838e223 100644 --- a/tasmota/xdrv_02_mqtt.ino +++ b/tasmota/xdrv_02_mqtt.ino @@ -91,10 +91,6 @@ tls_dir_t tls_dir; // memory copy of tls_dir from flash #endif // USE_MQTT_AWS_IOT -// A typical AWS IoT endpoint is 50 characters long, it does not fit -// in MqttHost field (32 chars). We need to concatenate both MqttUser and MqttHost -char AWS_endpoint[65]; // aWS IOT endpoint, concatenation of user and host - // check whether the fingerprint is filled with a single value // Filled with 0x00 = accept any fingerprint and learn it for next time // Filled with 0xFF = accept any fingerpring forever @@ -106,21 +102,6 @@ bool is_fingerprint_mono_value(uint8_t finger[20], uint8_t value) { } return true; } - -#ifdef USE_MQTT_AWS_IOT -void setLongMqttHost(const char *mqtt_host) { - if (strlen(mqtt_host) <= sizeof(Settings.mqtt_host)) { - strlcpy(Settings.mqtt_host, mqtt_host, sizeof(Settings.mqtt_host)); - Settings.mqtt_user[0] = 0; - } else { - // need to split in mqtt_user first then mqtt_host - strlcpy(Settings.mqtt_user, mqtt_host, sizeof(Settings.mqtt_user)); - strlcpy(Settings.mqtt_host, &mqtt_host[sizeof(Settings.mqtt_user)-1], sizeof(Settings.mqtt_host)); - } - strlcpy(AWS_endpoint, mqtt_host, sizeof(AWS_endpoint)); -} -#endif // USE_MQTT_AWS_IOT - #endif // USE_MQTT_TLS void MakeValidMqtt(uint32_t option, char* str) @@ -165,10 +146,10 @@ void MqttDiscoverServer(void) } } #endif // MDNS_HOSTNAME - snprintf_P(Settings.mqtt_host, sizeof(Settings.mqtt_host), MDNS.IP(i).toString().c_str()); + SettingsUpdateText(SET_MQTT_HOST, MDNS.IP(i).toString().c_str()); Settings.mqtt_port = MDNS.port(i); - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MDNS D_MQTT_SERVICE_FOUND " %s, " D_IP_ADDRESS " %s, " D_PORT " %d"), MDNS.hostname(i).c_str(), Settings.mqtt_host, Settings.mqtt_port); + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MDNS D_MQTT_SERVICE_FOUND " %s, " D_IP_ADDRESS " %s, " D_PORT " %d"), MDNS.hostname(i).c_str(), SettingsText(SET_MQTT_HOST), Settings.mqtt_port); } } #endif // MQTT_HOST_DISCOVERY @@ -202,8 +183,6 @@ void MqttInit(void) tlsClient = new BearSSL::WiFiClientSecure_light(1024,1024); #ifdef USE_MQTT_AWS_IOT - snprintf_P(AWS_endpoint, sizeof(AWS_endpoint), PSTR("%s%s"), Settings.mqtt_user, Settings.mqtt_host); - loadTlsDir(); // load key and certificate data from Flash tlsClient->setClientECCert(AWS_IoT_Client_Certificate, AWS_IoT_Private_Key, @@ -261,8 +240,8 @@ void MqttDataHandler(char* mqtt_topic, uint8_t* mqtt_data, unsigned int data_len if (data_len >= MQTT_MAX_PACKET_SIZE) { return; } // Do not execute multiple times if Prefix1 equals Prefix2 - if (!strcmp(Settings.mqtt_prefix[0], Settings.mqtt_prefix[1])) { - char *str = strstr(mqtt_topic, Settings.mqtt_prefix[0]); + if (!strcmp(SettingsText(SET_MQTTPREFIX1), SettingsText(SET_MQTTPREFIX2))) { + char *str = strstr(mqtt_topic, SettingsText(SET_MQTTPREFIX1)); if ((str == mqtt_topic) && mqtt_cmnd_publish) { if (mqtt_cmnd_publish > 3) { mqtt_cmnd_publish -= 3; @@ -330,8 +309,8 @@ void MqttPublishLogging(const char *mxtime) GetTopic_P(stopic, STAT, mqtt_topic, romram); char *me; - if (!strcmp(Settings.mqtt_prefix[0], Settings.mqtt_prefix[1])) { - me = strstr(stopic, Settings.mqtt_prefix[0]); + if (!strcmp(SettingsText(SET_MQTTPREFIX1), SettingsText(SET_MQTTPREFIX2))) { + me = strstr(stopic, SettingsText(SET_MQTTPREFIX1)); if (me == stopic) { mqtt_cmnd_publish += 3; } @@ -389,8 +368,8 @@ void MqttPublish(const char* topic, bool retained) retained = false; // AWS IoT does not support retained, it will disconnect if received #endif - if (!strcmp(Settings.mqtt_prefix[0],Settings.mqtt_prefix[1])) { - me = strstr(topic,Settings.mqtt_prefix[0]); + if (!strcmp(SettingsText(SET_MQTTPREFIX1), SettingsText(SET_MQTTPREFIX2))) { + me = strstr(topic, SettingsText(SET_MQTTPREFIX1)); if (me == topic) { mqtt_cmnd_publish += 3; } @@ -505,11 +484,7 @@ void MqttDisconnected(int state) MqttClient.disconnect(); -#if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT D_CONNECT_FAILED_TO " %s:%d, rc %d. " D_RETRY_IN " %d " D_UNIT_SECOND), AWS_endpoint, Settings.mqtt_port, state, Mqtt.retry_counter); -#else - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT D_CONNECT_FAILED_TO " %s:%d, rc %d. " D_RETRY_IN " %d " D_UNIT_SECOND), Settings.mqtt_host, Settings.mqtt_port, state, Mqtt.retry_counter); -#endif + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT D_CONNECT_FAILED_TO " %s:%d, rc %d. " D_RETRY_IN " %d " D_UNIT_SECOND), SettingsText(SET_MQTT_HOST), Settings.mqtt_port, state, Mqtt.retry_counter); rules_flag.mqtt_disconnected = 1; } @@ -533,7 +508,7 @@ void MqttConnected(void) GetTopic_P(stopic, CMND, mqtt_topic, PSTR("#")); MqttSubscribe(stopic); - if (strstr_P(Settings.mqtt_fulltopic, MQTT_TOKEN_TOPIC) != nullptr) { + if (strstr_P(SettingsText(SET_MQTT_FULLTOPIC), MQTT_TOKEN_TOPIC) != nullptr) { GetGroupTopic_P(stopic, PSTR("#")); // SetOption75 0: %prefix%/nothing/%topic% = cmnd/nothing//# or SetOption75 1: cmnd/ MqttSubscribe(stopic); GetFallbackTopic_P(stopic, PSTR("#")); @@ -550,12 +525,23 @@ void MqttConnected(void) MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_INFO "1")); #ifdef USE_WEBSERVER if (Settings.webserver) { +#if LWIP_IPV6 + Response_P(PSTR("{\"" D_JSON_WEBSERVER_MODE "\":\"%s\",\"" D_CMND_HOSTNAME "\":\"%s\",\"" D_CMND_IPADDRESS "\":\"%s\",\"IPv6Address\":\"%s\"}"), + (2 == Settings.webserver) ? D_ADMIN : D_USER, my_hostname, WiFi.localIP().toString().c_str(),WifiGetIPv6().c_str()); +#else Response_P(PSTR("{\"" D_JSON_WEBSERVER_MODE "\":\"%s\",\"" D_CMND_HOSTNAME "\":\"%s\",\"" D_CMND_IPADDRESS "\":\"%s\"}"), (2 == Settings.webserver) ? D_ADMIN : D_USER, my_hostname, WiFi.localIP().toString().c_str()); +#endif // LWIP_IPV6 = 1 MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_INFO "2")); } #endif // USE_WEBSERVER - Response_P(PSTR("{\"" D_JSON_RESTARTREASON "\":\"%s\"}"), GetResetReasonInfo().c_str()); + Response_P(PSTR("{\"" D_JSON_RESTARTREASON "\":")); + if (ResetReason() == REASON_EXCEPTION_RST) { + CrashDump(); + } else { + ResponseAppend_P(PSTR("\"%s\""), GetResetReason().c_str()); + } + ResponseJsonEnd(); MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_INFO "3")); MqttPublishAllPowerState(); if (Settings.tele_period) { @@ -583,7 +569,7 @@ void MqttReconnect(void) MqttDiscoverServer(); #endif // MQTT_HOST_DISCOVERY #endif // USE_DISCOVERY - if (!strlen(Settings.mqtt_host) || !Settings.mqtt_port) { + if (!strlen(SettingsText(SET_MQTT_HOST)) || !Settings.mqtt_port) { Mqtt.allowed = false; } #if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) @@ -610,8 +596,12 @@ void MqttReconnect(void) char *mqtt_user = nullptr; char *mqtt_pwd = nullptr; - if (strlen(Settings.mqtt_user) > 0) mqtt_user = Settings.mqtt_user; - if (strlen(Settings.mqtt_pwd) > 0) mqtt_pwd = Settings.mqtt_pwd; + if (strlen(SettingsText(SET_MQTT_USER))) { + mqtt_user = SettingsText(SET_MQTT_USER); + } + if (strlen(SettingsText(SET_MQTT_PWD))) { + mqtt_pwd = SettingsText(SET_MQTT_PWD); + } GetTopic_P(stopic, TELE, mqtt_topic, S_LWT); Response_P(S_OFFLINE); @@ -634,10 +624,8 @@ void MqttReconnect(void) tlsClient->setClientECCert(AWS_IoT_Client_Certificate, AWS_IoT_Private_Key, 0xFFFF /* all usages, don't care */, 0); - MqttClient.setServer(AWS_endpoint, Settings.mqtt_port); -#else - MqttClient.setServer(Settings.mqtt_host, Settings.mqtt_port); #endif + MqttClient.setServer(SettingsText(SET_MQTT_HOST), Settings.mqtt_port); uint32_t mqtt_connect_time = millis(); #if defined(USE_MQTT_TLS) && !defined(USE_MQTT_TLS_CA_CERT) @@ -651,7 +639,7 @@ void MqttReconnect(void) tlsClient->setPubKeyFingerprint(Settings.mqtt_fingerprint[0], Settings.mqtt_fingerprint[1], allow_all_fingerprints); #endif #if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) - AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "AWS IoT endpoint: %s"), AWS_endpoint); + AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "AWS IoT endpoint: %s"), SettingsText(SET_MQTT_HOST)); //if (MqttClient.connect(mqtt_client, nullptr, nullptr, nullptr, 0, false, nullptr)) { if (MqttClient.connect(mqtt_client, nullptr, nullptr, stopic, 1, false, mqtt_data, MQTT_CLEAN_SESSION)) { #else @@ -711,7 +699,7 @@ void MqttCheck(void) if (!Mqtt.retry_counter) { #ifdef USE_DISCOVERY #ifdef MQTT_HOST_DISCOVERY - if (!strlen(Settings.mqtt_host) && !Wifi.mdns_begun) { return; } + if (!strlen(SettingsText(SET_MQTT_HOST)) && !Wifi.mdns_begun) { return; } #endif // MQTT_HOST_DISCOVERY #endif // USE_DISCOVERY MqttReconnect(); @@ -752,18 +740,18 @@ void CmndMqttFingerprint(void) #if !defined(USE_MQTT_TLS) || !defined(USE_MQTT_AWS_IOT) // user and password are disabled with AWS IoT void CmndMqttUser(void) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.mqtt_user))) { - strlcpy(Settings.mqtt_user, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? MQTT_USER : XdrvMailbox.data, sizeof(Settings.mqtt_user)); + if (XdrvMailbox.data_len > 0) { + SettingsUpdateText(SET_MQTT_USER, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? MQTT_USER : XdrvMailbox.data); restart_flag = 2; } - ResponseCmndChar(Settings.mqtt_user); + ResponseCmndChar(SettingsText(SET_MQTT_USER)); } void CmndMqttPassword(void) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.mqtt_pwd))) { - strlcpy(Settings.mqtt_pwd, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? MQTT_PASS : XdrvMailbox.data, sizeof(Settings.mqtt_pwd)); - ResponseCmndChar(Settings.mqtt_pwd); + if (XdrvMailbox.data_len > 0) { + SettingsUpdateText(SET_MQTT_PWD, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? MQTT_PASS : XdrvMailbox.data); + ResponseCmndChar(SettingsText(SET_MQTT_PWD)); restart_flag = 2; } else { Response_P(S_JSON_COMMAND_ASTERISK, XdrvMailbox.command); @@ -781,19 +769,11 @@ void CmndMqttlog(void) void CmndMqttHost(void) { -#if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len <= sizeof(Settings.mqtt_host) + sizeof(Settings.mqtt_user) - 2)) { - setLongMqttHost((SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? MQTT_HOST : XdrvMailbox.data); + if (XdrvMailbox.data_len > 0) { + SettingsUpdateText(SET_MQTT_HOST, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? MQTT_HOST : XdrvMailbox.data); restart_flag = 2; } - ResponseCmndChar(AWS_endpoint); -#else - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.mqtt_host))) { - strlcpy(Settings.mqtt_host, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? MQTT_HOST : XdrvMailbox.data, sizeof(Settings.mqtt_host)); - restart_flag = 2; - } - ResponseCmndChar(Settings.mqtt_host); -#endif + ResponseCmndChar(SettingsText(SET_MQTT_HOST)); } void CmndMqttPort(void) @@ -817,11 +797,11 @@ void CmndMqttRetry(void) void CmndStateText(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 4)) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.state_text[0]))) { + if (XdrvMailbox.data_len > 0) { for (uint32_t i = 0; i <= XdrvMailbox.data_len; i++) { if (XdrvMailbox.data[i] == ' ') XdrvMailbox.data[i] = '_'; } - strlcpy(Settings.state_text[XdrvMailbox.index -1], XdrvMailbox.data, sizeof(Settings.state_text[0])); + SettingsUpdateText(SET_STATE_TXT1 + XdrvMailbox.index -1, XdrvMailbox.data); } ResponseCmndIdxChar(GetStateText(XdrvMailbox.index -1)); } @@ -829,40 +809,41 @@ void CmndStateText(void) void CmndMqttClient(void) { - if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.mqtt_client))) { - strlcpy(Settings.mqtt_client, (SC_DEFAULT == Shortcut()) ? MQTT_CLIENT_ID : XdrvMailbox.data, sizeof(Settings.mqtt_client)); + if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0)) { + SettingsUpdateText(SET_MQTT_CLIENT, (SC_DEFAULT == Shortcut()) ? MQTT_CLIENT_ID : XdrvMailbox.data); restart_flag = 2; } - ResponseCmndChar(Settings.mqtt_client); + ResponseCmndChar(SettingsText(SET_MQTT_CLIENT)); } void CmndFullTopic(void) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.mqtt_fulltopic))) { + if (XdrvMailbox.data_len > 0) { MakeValidMqtt(1, XdrvMailbox.data); if (!strcmp(XdrvMailbox.data, mqtt_client)) { SetShortcutDefault(); } char stemp1[TOPSZ]; strlcpy(stemp1, (SC_DEFAULT == Shortcut()) ? MQTT_FULLTOPIC : XdrvMailbox.data, sizeof(stemp1)); - if (strcmp(stemp1, Settings.mqtt_fulltopic)) { + if (strcmp(stemp1, SettingsText(SET_MQTT_FULLTOPIC))) { Response_P((Settings.flag.mqtt_offline) ? S_OFFLINE : ""); // SetOption10 - Control MQTT LWT message format MqttPublishPrefixTopic_P(TELE, PSTR(D_LWT), true); // Offline or remove previous retained topic - strlcpy(Settings.mqtt_fulltopic, stemp1, sizeof(Settings.mqtt_fulltopic)); + SettingsUpdateText(SET_MQTT_FULLTOPIC, stemp1); restart_flag = 2; } } - ResponseCmndChar(Settings.mqtt_fulltopic); + ResponseCmndChar(SettingsText(SET_MQTT_FULLTOPIC)); } void CmndPrefix(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 3)) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.mqtt_prefix[0]))) { + + if (XdrvMailbox.data_len > 0) { MakeValidMqtt(0, XdrvMailbox.data); - strlcpy(Settings.mqtt_prefix[XdrvMailbox.index -1], (SC_DEFAULT == Shortcut()) ? (1==XdrvMailbox.index)?SUB_PREFIX:(2==XdrvMailbox.index)?PUB_PREFIX:PUB_PREFIX2 : XdrvMailbox.data, sizeof(Settings.mqtt_prefix[0])); -// if (Settings.mqtt_prefix[XdrvMailbox.index -1][strlen(Settings.mqtt_prefix[XdrvMailbox.index -1])] == '/') Settings.mqtt_prefix[XdrvMailbox.index -1][strlen(Settings.mqtt_prefix[XdrvMailbox.index -1])] = 0; + SettingsUpdateText(SET_MQTTPREFIX1 + XdrvMailbox.index -1, + (SC_DEFAULT == Shortcut()) ? (1==XdrvMailbox.index) ? SUB_PREFIX : (2==XdrvMailbox.index) ? PUB_PREFIX : PUB_PREFIX2 : XdrvMailbox.data); restart_flag = 2; } - ResponseCmndIdxChar(Settings.mqtt_prefix[XdrvMailbox.index -1]); + ResponseCmndIdxChar(SettingsText(SET_MQTTPREFIX1 + XdrvMailbox.index -1)); } } @@ -888,60 +869,60 @@ void CmndPublish(void) void CmndGroupTopic(void) { - if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.mqtt_grptopic))) { + if (XdrvMailbox.data_len > 0) { MakeValidMqtt(0, XdrvMailbox.data); if (!strcmp(XdrvMailbox.data, mqtt_client)) { SetShortcutDefault(); } - strlcpy(Settings.mqtt_grptopic, (SC_DEFAULT == Shortcut()) ? MQTT_GRPTOPIC : XdrvMailbox.data, sizeof(Settings.mqtt_grptopic)); + SettingsUpdateText(SET_MQTT_GRP_TOPIC, (SC_DEFAULT == Shortcut()) ? MQTT_GRPTOPIC : XdrvMailbox.data); restart_flag = 2; } - ResponseCmndChar(Settings.mqtt_grptopic); + ResponseCmndChar(SettingsText(SET_MQTT_GRP_TOPIC)); } void CmndTopic(void) { - if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.mqtt_topic))) { + if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0)) { MakeValidMqtt(0, XdrvMailbox.data); if (!strcmp(XdrvMailbox.data, mqtt_client)) { SetShortcutDefault(); } char stemp1[TOPSZ]; strlcpy(stemp1, (SC_DEFAULT == Shortcut()) ? MQTT_TOPIC : XdrvMailbox.data, sizeof(stemp1)); - if (strcmp(stemp1, Settings.mqtt_topic)) { + if (strcmp(stemp1, SettingsText(SET_MQTT_TOPIC))) { Response_P((Settings.flag.mqtt_offline) ? S_OFFLINE : ""); // SetOption10 - Control MQTT LWT message format MqttPublishPrefixTopic_P(TELE, PSTR(D_LWT), true); // Offline or remove previous retained topic - strlcpy(Settings.mqtt_topic, stemp1, sizeof(Settings.mqtt_topic)); + SettingsUpdateText(SET_MQTT_TOPIC, stemp1); restart_flag = 2; } } - ResponseCmndChar(Settings.mqtt_topic); + ResponseCmndChar(SettingsText(SET_MQTT_TOPIC)); } void CmndButtonTopic(void) { - if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.button_topic))) { + if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0)) { MakeValidMqtt(0, XdrvMailbox.data); if (!strcmp(XdrvMailbox.data, mqtt_client)) { SetShortcutDefault(); } switch (Shortcut()) { - case SC_CLEAR: strlcpy(Settings.button_topic, "", sizeof(Settings.button_topic)); break; - case SC_DEFAULT: strlcpy(Settings.button_topic, mqtt_topic, sizeof(Settings.button_topic)); break; - case SC_USER: strlcpy(Settings.button_topic, MQTT_BUTTON_TOPIC, sizeof(Settings.button_topic)); break; - default: strlcpy(Settings.button_topic, XdrvMailbox.data, sizeof(Settings.button_topic)); + case SC_CLEAR: SettingsUpdateText(SET_MQTT_BUTTON_TOPIC, ""); break; + case SC_DEFAULT: SettingsUpdateText(SET_MQTT_BUTTON_TOPIC, mqtt_topic); break; + case SC_USER: SettingsUpdateText(SET_MQTT_BUTTON_TOPIC, MQTT_BUTTON_TOPIC); break; + default: SettingsUpdateText(SET_MQTT_BUTTON_TOPIC, XdrvMailbox.data); } } - ResponseCmndChar(Settings.button_topic); + ResponseCmndChar(SettingsText(SET_MQTT_BUTTON_TOPIC)); } void CmndSwitchTopic(void) { - if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(Settings.switch_topic))) { + if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0)) { MakeValidMqtt(0, XdrvMailbox.data); if (!strcmp(XdrvMailbox.data, mqtt_client)) { SetShortcutDefault(); } switch (Shortcut()) { - case SC_CLEAR: strlcpy(Settings.switch_topic, "", sizeof(Settings.switch_topic)); break; - case SC_DEFAULT: strlcpy(Settings.switch_topic, mqtt_topic, sizeof(Settings.switch_topic)); break; - case SC_USER: strlcpy(Settings.switch_topic, MQTT_SWITCH_TOPIC, sizeof(Settings.switch_topic)); break; - default: strlcpy(Settings.switch_topic, XdrvMailbox.data, sizeof(Settings.switch_topic)); + case SC_CLEAR: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, ""); break; + case SC_DEFAULT: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, mqtt_topic); break; + case SC_USER: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, MQTT_SWITCH_TOPIC); break; + default: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, XdrvMailbox.data); } } - ResponseCmndChar(Settings.switch_topic); + ResponseCmndChar(SettingsText(SET_MQTT_SWITCH_TOPIC)); } void CmndButtonRetain(void) @@ -1071,6 +1052,9 @@ void CmndTlsKey(void) { } memcpy_P(spi_buffer, tls_spi_start, tls_spi_len); + // remove any white space from the base64 + RemoveAllSpaces(XdrvMailbox.data); + // allocate buffer for decoded base64 uint32_t bin_len = decode_base64_length((unsigned char*)XdrvMailbox.data); uint8_t *bin_buf = nullptr; @@ -1206,22 +1190,18 @@ void HandleMqttConfiguration(void) return; } - char str[sizeof(Settings.mqtt_client)]; + char str[33]; WSContentStart_P(S_CONFIGURE_MQTT); WSContentSendStyle(); WSContentSend_P(HTTP_FORM_MQTT1, -#if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) - AWS_endpoint, -#else - Settings.mqtt_host, -#endif + SettingsText(SET_MQTT_HOST), Settings.mqtt_port, - Format(str, MQTT_CLIENT_ID, sizeof(str)), MQTT_CLIENT_ID, Settings.mqtt_client); + Format(str, MQTT_CLIENT_ID, sizeof(str)), MQTT_CLIENT_ID, SettingsText(SET_MQTT_CLIENT)); WSContentSend_P(HTTP_FORM_MQTT2, - (Settings.mqtt_user[0] == '\0') ? "0" : Settings.mqtt_user, - Format(str, MQTT_TOPIC, sizeof(str)), MQTT_TOPIC, Settings.mqtt_topic, - MQTT_FULLTOPIC, MQTT_FULLTOPIC, Settings.mqtt_fulltopic); + (!strlen(SettingsText(SET_MQTT_USER))) ? "0" : SettingsText(SET_MQTT_USER), + Format(str, MQTT_TOPIC, sizeof(str)), MQTT_TOPIC, SettingsText(SET_MQTT_TOPIC), + MQTT_FULLTOPIC, MQTT_FULLTOPIC, SettingsText(SET_MQTT_FULLTOPIC)); WSContentSend_P(HTTP_FORM_END); WSContentSpaceButton(BUTTON_CONFIGURATION); WSContentStop(); @@ -1239,32 +1219,28 @@ void MqttSaveSettings(void) WebGetArg("mf", tmp, sizeof(tmp)); strlcpy(stemp2, (!strlen(tmp)) ? MQTT_FULLTOPIC : tmp, sizeof(stemp2)); MakeValidMqtt(1, stemp2); - if ((strcmp(stemp, Settings.mqtt_topic)) || (strcmp(stemp2, Settings.mqtt_fulltopic))) { + if ((strcmp(stemp, SettingsText(SET_MQTT_TOPIC))) || (strcmp(stemp2, SettingsText(SET_MQTT_FULLTOPIC)))) { Response_P((Settings.flag.mqtt_offline) ? S_OFFLINE : ""); // SetOption10 - Control MQTT LWT message format MqttPublishPrefixTopic_P(TELE, S_LWT, true); // Offline or remove previous retained topic } - strlcpy(Settings.mqtt_topic, stemp, sizeof(Settings.mqtt_topic)); - strlcpy(Settings.mqtt_fulltopic, stemp2, sizeof(Settings.mqtt_fulltopic)); + SettingsUpdateText(SET_MQTT_TOPIC, stemp); + SettingsUpdateText(SET_MQTT_FULLTOPIC, stemp2); WebGetArg("mh", tmp, sizeof(tmp)); -#if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) - setLongMqttHost((!strlen(tmp)) ? MQTT_HOST : (!strcmp(tmp,"0")) ? "" : tmp); -#else - strlcpy(Settings.mqtt_host, (!strlen(tmp)) ? MQTT_HOST : (!strcmp(tmp,"0")) ? "" : tmp, sizeof(Settings.mqtt_host)); -#endif + SettingsUpdateText(SET_MQTT_HOST, (!strlen(tmp)) ? MQTT_HOST : (!strcmp(tmp,"0")) ? "" : tmp); WebGetArg("ml", tmp, sizeof(tmp)); Settings.mqtt_port = (!strlen(tmp)) ? MQTT_PORT : atoi(tmp); WebGetArg("mc", tmp, sizeof(tmp)); - strlcpy(Settings.mqtt_client, (!strlen(tmp)) ? MQTT_CLIENT_ID : tmp, sizeof(Settings.mqtt_client)); + SettingsUpdateText(SET_MQTT_CLIENT, (!strlen(tmp)) ? MQTT_CLIENT_ID : tmp); #if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT D_CMND_MQTTHOST " %s, " D_CMND_MQTTPORT " %d, " D_CMND_MQTTCLIENT " %s, " D_CMND_TOPIC " %s, " D_CMND_FULLTOPIC " %s"), - AWS_endpoint, Settings.mqtt_port, Settings.mqtt_client, Settings.mqtt_topic, Settings.mqtt_fulltopic); + SettingsText(SET_MQTT_HOST), Settings.mqtt_port, SettingsText(SET_MQTT_CLIENT), SettingsText(SET_MQTT_TOPIC), SettingsText(SET_MQTT_FULLTOPIC)); #else // USE_MQTT_AWS_IOT WebGetArg("mu", tmp, sizeof(tmp)); - strlcpy(Settings.mqtt_user, (!strlen(tmp)) ? MQTT_USER : (!strcmp(tmp,"0")) ? "" : tmp, sizeof(Settings.mqtt_user)); + SettingsUpdateText(SET_MQTT_USER, (!strlen(tmp)) ? MQTT_USER : (!strcmp(tmp,"0")) ? "" : tmp); WebGetArg("mp", tmp, sizeof(tmp)); - strlcpy(Settings.mqtt_pwd, (!strlen(tmp)) ? "" : (!strcmp(tmp, D_ASTERISK_PWD)) ? Settings.mqtt_pwd : tmp, sizeof(Settings.mqtt_pwd)); + SettingsUpdateText(SET_MQTT_PWD, (!strlen(tmp)) ? "" : (!strcmp(tmp, D_ASTERISK_PWD)) ? SettingsText(SET_MQTT_PWD) : tmp); AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT D_CMND_MQTTHOST " %s, " D_CMND_MQTTPORT " %d, " D_CMND_MQTTCLIENT " %s, " D_CMND_MQTTUSER " %s, " D_CMND_TOPIC " %s, " D_CMND_FULLTOPIC " %s"), - Settings.mqtt_host, Settings.mqtt_port, Settings.mqtt_client, Settings.mqtt_user, Settings.mqtt_topic, Settings.mqtt_fulltopic); + SettingsText(SET_MQTT_HOST), Settings.mqtt_port, SettingsText(SET_MQTT_CLIENT), SettingsText(SET_MQTT_USER), SettingsText(SET_MQTT_TOPIC), SettingsText(SET_MQTT_FULLTOPIC)); #endif } #endif // USE_WEBSERVER diff --git a/tasmota/xdrv_09_timers.ino b/tasmota/xdrv_09_timers.ino index a71960695..db1cde97e 100644 --- a/tasmota/xdrv_09_timers.ino +++ b/tasmota/xdrv_09_timers.ino @@ -554,10 +554,10 @@ const char HTTP_TIMER_SCRIPT2[] PROGMEM = "o=qs('#ho');" "e=o.childElementCount;" "if(b==1){" - "qs('#dr').disabled='';" + "qs('#dr').style.visibility='';" "if(e>12){for(i=12;i<=23;i++){o.removeChild(o.lastElementChild);}}" // Create offset hours select options "}else{" - "qs('#dr').disabled='disabled';" + "qs('#dr').style.visibility='hidden';" "if(e<23){for(i=12;i<=23;i++){ce(i,o);}}" // Create hours select options "}" "}"; @@ -583,7 +583,7 @@ const char HTTP_TIMER_SCRIPT3[] PROGMEM = "if(m==0){s|=l;}" // Get time #ifdef USE_SUNRISE "if((m==1)||(m==2)){" - "if(qs('#dr').selectedIndex>0){l+=720;}" // If negative offset, add 12h to given offset time + "if(qs('#dr').selectedIndex>0){if(l>0){l+=720;}}" // If negative offset and delta-time > 0, add 12h to given offset time "s|=l&0x7FF;" // Save offset instead of time "}" #endif diff --git a/tasmota/xdrv_10_rules.ino b/tasmota/xdrv_10_rules.ino index 4025ef5d4..6e1636518 100644 --- a/tasmota/xdrv_10_rules.ino +++ b/tasmota/xdrv_10_rules.ino @@ -217,7 +217,7 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule) for (uint32_t i = 0; i < MAX_RULE_MEMS; i++) { snprintf_P(stemp, sizeof(stemp), PSTR("%%MEM%d%%"), i +1); if (rule_param.startsWith(stemp)) { - rule_param = Settings.mems[i]; + rule_param = SettingsText(SET_MEM1 + i); break; } } @@ -438,7 +438,7 @@ bool RuleSetProcess(uint8_t rule_set, String &event_saved) String ucommand = commands; ucommand.toUpperCase(); // if (!ucommand.startsWith("BACKLOG")) { commands = "backlog " + commands; } // Always use Backlog to prevent power race exception - if (ucommand.indexOf("EVENT ") != -1) { commands = "backlog " + commands; } // Always use Backlog with event to prevent rule event loop exception + if ((ucommand.indexOf("EVENT ") != -1) && (ucommand.indexOf("BACKLOG ") == -1)) { commands = "backlog " + commands; } // Always use Backlog with event to prevent rule event loop exception RulesVarReplace(commands, F("%VALUE%"), Rules.event_value); for (uint32_t i = 0; i < MAX_RULE_VARS; i++) { @@ -447,11 +447,12 @@ bool RuleSetProcess(uint8_t rule_set, String &event_saved) } for (uint32_t i = 0; i < MAX_RULE_MEMS; i++) { snprintf_P(stemp, sizeof(stemp), PSTR("%%MEM%d%%"), i +1); - RulesVarReplace(commands, stemp, Settings.mems[i]); + RulesVarReplace(commands, stemp, SettingsText(SET_MEM1 +i)); } RulesVarReplace(commands, F("%TIME%"), String(MinutesPastMidnight())); RulesVarReplace(commands, F("%UPTIME%"), String(MinutesUptime())); RulesVarReplace(commands, F("%TIMESTAMP%"), GetDateAndTime(DT_LOCAL)); + RulesVarReplace(commands, F("%TOPIC%"), SettingsText(SET_MQTT_TOPIC)); #if defined(USE_TIMERS) && defined(USE_SUNRISE) RulesVarReplace(commands, F("%SUNRISE%"), String(SunMinutes(0))); RulesVarReplace(commands, F("%SUNSET%"), String(SunMinutes(1))); @@ -607,7 +608,7 @@ void RulesEvery50ms(void) for (uint32_t i = 0; i < MAX_RULE_MEMS; i++) { if (bitRead(Rules.mems_event, i)) { bitClear(Rules.mems_event, i); - snprintf_P(json_event, sizeof(json_event), PSTR("{\"Mem%d\":{\"State\":%s}}"), i+1, Settings.mems[i]); + snprintf_P(json_event, sizeof(json_event), PSTR("{\"Mem%d\":{\"State\":%s}}"), i+1, SettingsText(SET_MEM1 +i)); RulesProcessEvent(json_event); break; } @@ -1001,7 +1002,7 @@ bool findNextVariableValue(char * &pVarname, float &value) } else if (sVarName.startsWith(F("MEM"))) { int index = sVarName.substring(3).toInt(); if (index > 0 && index <= MAX_RULE_MEMS) { - value = CharToFloat(Settings.mems[index -1]); + value = CharToFloat(SettingsText(SET_MEM1 + index -1)); } } else if (sVarName.equals(F("TIME"))) { value = MinutesPastMidnight(); @@ -1809,23 +1810,23 @@ void CmndMemory(void) if (!XdrvMailbox.usridx) { mqtt_data[0] = '\0'; for (uint32_t i = 0; i < MAX_RULE_MEMS; i++) { - ResponseAppend_P(PSTR("%c\"Mem%d\":\"%s\""), (i) ? ',' : '{', i +1, Settings.mems[i]); + ResponseAppend_P(PSTR("%c\"Mem%d\":\"%s\""), (i) ? ',' : '{', i +1, SettingsText(SET_MEM1 +i)); } ResponseJsonEnd(); } else { if (XdrvMailbox.data_len > 0) { #ifdef USE_EXPRESSION if (XdrvMailbox.data[0] == '=') { // Spaces already been skipped in data - dtostrfd(evaluateExpression(XdrvMailbox.data + 1, XdrvMailbox.data_len - 1), Settings.flag2.calc_resolution, Settings.mems[XdrvMailbox.index -1]); + dtostrfd(evaluateExpression(XdrvMailbox.data + 1, XdrvMailbox.data_len - 1), Settings.flag2.calc_resolution, SettingsText(SET_MEM1 + XdrvMailbox.index -1)); } else { - strlcpy(Settings.mems[XdrvMailbox.index -1], ('"' == XdrvMailbox.data[0]) ? "" : XdrvMailbox.data, sizeof(Settings.mems[XdrvMailbox.index -1])); + SettingsUpdateText(SET_MEM1 + XdrvMailbox.index -1, ('"' == XdrvMailbox.data[0]) ? "" : XdrvMailbox.data); } #else - strlcpy(Settings.mems[XdrvMailbox.index -1], ('"' == XdrvMailbox.data[0]) ? "" : XdrvMailbox.data, sizeof(Settings.mems[XdrvMailbox.index -1])); + SettingsUpdateText(SET_MEM1 + XdrvMailbox.index -1, ('"' == XdrvMailbox.data[0]) ? "" : XdrvMailbox.data); #endif // USE_EXPRESSION bitSet(Rules.mems_event, XdrvMailbox.index -1); } - ResponseCmndIdxChar(Settings.mems[XdrvMailbox.index -1]); + ResponseCmndIdxChar(SettingsText(SET_MEM1 + XdrvMailbox.index -1)); } } } diff --git a/tasmota/xdrv_10_scripter.ino b/tasmota/xdrv_10_scripter.ino index 0bab141d7..68df13984 100755 --- a/tasmota/xdrv_10_scripter.ino +++ b/tasmota/xdrv_10_scripter.ino @@ -1030,8 +1030,10 @@ char *isvar(char *lp, uint8_t *vtype,struct T_INDEX *tind,float *fp,char *sp,Jso if ((*jo).is(vn)) { if (!strncmp(str_value,"ON",2)) { if (fp) *fp=1; + goto nexit; } else if (!strncmp(str_value,"OFF",3)) { if (fp) *fp=0; + goto nexit; } else { *vtype=STR_RES; tind->bits.constant=1; @@ -1039,6 +1041,7 @@ char *isvar(char *lp, uint8_t *vtype,struct T_INDEX *tind,float *fp,char *sp,Jso if (sp) strlcpy(sp,str_value,SCRIPT_MAXSSIZE); return lp+len; } + } else { if (fp) { if (!strncmp(vn.c_str(),"Epoch",5)) { @@ -1047,6 +1050,7 @@ char *isvar(char *lp, uint8_t *vtype,struct T_INDEX *tind,float *fp,char *sp,Jso *fp=CharToFloat((char*)str_value); } } + nexit: *vtype=NUM_RES; tind->bits.constant=1; tind->bits.is_string=0; @@ -1366,7 +1370,7 @@ chknext: goto exit; } if (!strncmp(vname,"gtopic",6)) { - if (sp) strlcpy(sp,Settings.mqtt_grptopic,glob_script_mem.max_ssize); + if (sp) strlcpy(sp,SettingsText(SET_MQTT_GRP_TOPIC),glob_script_mem.max_ssize); goto strexit; } break; @@ -1523,15 +1527,15 @@ chknext: goto exit; } if (!strncmp(vname,"prefix1",7)) { - if (sp) strlcpy(sp,Settings.mqtt_prefix[0],glob_script_mem.max_ssize); + if (sp) strlcpy(sp,SettingsText(SET_MQTTPREFIX1),glob_script_mem.max_ssize); goto strexit; } if (!strncmp(vname,"prefix2",7)) { - if (sp) strlcpy(sp,Settings.mqtt_prefix[1],glob_script_mem.max_ssize); + if (sp) strlcpy(sp,SettingsText(SET_MQTTPREFIX2),glob_script_mem.max_ssize); goto strexit; } if (!strncmp(vname,"prefix3",7)) { - if (sp) strlcpy(sp,Settings.mqtt_prefix[2],glob_script_mem.max_ssize); + if (sp) strlcpy(sp,SettingsText(SET_MQTTPREFIX3),glob_script_mem.max_ssize); goto strexit; } if (!strncmp(vname,"pow(",4)) { @@ -1740,7 +1744,7 @@ chknext: goto strexit; } if (!strncmp(vname,"topic",5)) { - if (sp) strlcpy(sp,Settings.mqtt_topic,glob_script_mem.max_ssize); + if (sp) strlcpy(sp,SettingsText(SET_MQTT_TOPIC),glob_script_mem.max_ssize); goto strexit; } #ifdef USE_DISPLAY @@ -4843,6 +4847,12 @@ bool Xdrv10(uint8_t function) result = ScriptCommand(); break; case FUNC_SET_POWER: +#ifdef SCRIPT_POWER_SECTION + if (bitRead(Settings.rule_enabled, 0)) Run_Scripter(">P",2,0); +#else + if (bitRead(Settings.rule_enabled, 0)) Run_Scripter(">E",2,0); +#endif + break; case FUNC_RULES_PROCESS: if (bitRead(Settings.rule_enabled, 0)) Run_Scripter(">E",2,mqtt_data); break; diff --git a/tasmota/xdrv_11_knx.ino b/tasmota/xdrv_11_knx.ino index c848bddfe..b52c37e4d 100644 --- a/tasmota/xdrv_11_knx.ino +++ b/tasmota/xdrv_11_knx.ino @@ -499,6 +499,7 @@ void KNX_INIT(void) if (GetUsedInModule(GPIO_DHT22, my_module.io)) { device_param[KNX_HUMIDITY-1].show = true; } if (GetUsedInModule(GPIO_SI7021, my_module.io)) { device_param[KNX_HUMIDITY-1].show = true; } +#if defined(USE_ENERGY_SENSOR) // Any device with a Power Monitoring if ( energy_flg != ENERGY_NONE ) { device_param[KNX_ENERGY_POWER-1].show = true; @@ -509,6 +510,7 @@ void KNX_INIT(void) device_param[KNX_ENERGY_CURRENT-1].show = true; device_param[KNX_ENERGY_POWERFACTOR-1].show = true; } +#endif #ifdef USE_RULES device_param[KNX_SLOT1-1].show = true; diff --git a/tasmota/xdrv_12_home_assistant.ino b/tasmota/xdrv_12_home_assistant.ino index 38c36ead1..58c266c61 100644 --- a/tasmota/xdrv_12_home_assistant.ino +++ b/tasmota/xdrv_12_home_assistant.ino @@ -46,7 +46,7 @@ const char HASS_DISCOVER_BUTTON_TOGGLE[] PROGMEM = const char HASS_DISCOVER_SWITCH_TOGGLE[] PROGMEM = ",\"value_template\":\"{%%if is_state(entity_id,\\\"on\\\")-%%}OFF{%%-else-%%}ON{%%-endif%%}\""; // A switch must maintain his state until the next update - + const char HASS_DISCOVER_BUTTON_SWITCH_ONOFF[] PROGMEM = ",\"value_template\":\"{{value_json.%s}}\"," // STATE "\"frc_upd\":true," // In ON/OFF case, enable force_update to make automations work @@ -86,6 +86,7 @@ const char HASS_DISCOVER_SENSOR[] PROGMEM = "{\"name\":\"%s\"," // dualr2 1 BTN "\"stat_t\":\"%s\"," // cmnd/dualr2/POWER (implies "\"optimistic\":\"false\",") "\"avty_t\":\"%s\"," // tele/dualr2/LWT + "\"frc_upd\":true," // force update for better graph representation "\"pl_avail\":\"" D_ONLINE "\"," // Online "\"pl_not_avail\":\"" D_OFFLINE "\""; // Offline @@ -238,9 +239,9 @@ void HAssAnnounceRelayLight(void) char *availability_topic = stemp3; if (i > MAX_FRIENDLYNAMES) { - snprintf_P(name, sizeof(name), PSTR("%s %d"), Settings.friendlyname[0], i); + snprintf_P(name, sizeof(name), PSTR("%s %d"), SettingsText(SET_FRIENDLYNAME1), i); } else { - snprintf_P(name, sizeof(name), Settings.friendlyname[i -1]); + snprintf_P(name, sizeof(name), SettingsText(SET_FRIENDLYNAME1 +i -1)); } GetPowerDevice(value_template, i, sizeof(value_template), Settings.flag.device_index_enable); // SetOption26 - Switch between POWER or POWER1 GetTopic_P(command_topic, CMND, mqtt_topic, value_template); @@ -252,7 +253,7 @@ void HAssAnnounceRelayLight(void) Shorten(&state_topic, prefix); Shorten(&availability_topic, prefix); - Response_P(HASS_DISCOVER_RELAY, name, command_topic, state_topic, value_template, Settings.state_text[0], Settings.state_text[1], availability_topic); + Response_P(HASS_DISCOVER_RELAY, name, command_topic, state_topic, value_template, SettingsText(SET_STATE_TXT1), SettingsText(SET_STATE_TXT2), availability_topic); TryResponseAppend_P(HASS_DISCOVER_DEVICE_INFO_SHORT, unique_id, ESP.getChipId(), WiFi.macAddress().c_str()); TryResponseAppend_P(HASS_DISCOVER_TOPIC_PREFIX, prefix); @@ -323,7 +324,7 @@ void HAssAnnounceButtonSwitch(uint8_t device, char* topic, uint8_t present, uint char *availability_topic = stemp2; char jsoname[8]; - snprintf_P(name, sizeof(name), PSTR("%s %s%d"), Settings.friendlyname[0], key?"Switch":"Button", device+1); + snprintf_P(name, sizeof(name), PSTR("%s %s%d"), SettingsText(SET_FRIENDLYNAME1), key?"Switch":"Button", device+1); snprintf_P(jsoname, sizeof(jsoname), PSTR("%s%d"), key?"SWITCH":"BUTTON", device+1); GetPowerDevice(value_template, device+1, sizeof(value_template), key + Settings.flag.device_index_enable); // Force index for Switch 1, Index on Button1 is controlled by SetOption26 - Switch between POWER or POWER1 @@ -338,11 +339,11 @@ void HAssAnnounceButtonSwitch(uint8_t device, char* topic, uint8_t present, uint TryResponseAppend_P(HASS_DISCOVER_DEVICE_INFO_SHORT, unique_id, ESP.getChipId(), WiFi.macAddress().c_str()); if (strlen(prefix) > 0 ) TryResponseAppend_P(HASS_DISCOVER_TOPIC_PREFIX, prefix); if (toggle) { - if (!key) { - TryResponseAppend_P(HASS_DISCOVER_BUTTON_TOGGLE, PSTR(D_RSLT_STATE), Settings.state_text[toggle?2:1]); + if (!key) { + TryResponseAppend_P(HASS_DISCOVER_BUTTON_TOGGLE, PSTR(D_RSLT_STATE), SettingsText(SET_STATE_TXT1 + toggle?2:1)); } else {TryResponseAppend_P(HASS_DISCOVER_SWITCH_TOGGLE);} } - else TryResponseAppend_P(HASS_DISCOVER_BUTTON_SWITCH_ONOFF, PSTR(D_RSLT_STATE), Settings.state_text[toggle?2:1], Settings.state_text[0]); + else TryResponseAppend_P(HASS_DISCOVER_BUTTON_SWITCH_ONOFF, PSTR(D_RSLT_STATE), SettingsText(SET_STATE_TXT1 + toggle?2:1), SettingsText(SET_STATE_TXT1)); TryResponseAppend_P(PSTR("}")); } @@ -351,10 +352,10 @@ void HAssAnnounceButtonSwitch(uint8_t device, char* topic, uint8_t present, uint void HAssAnnounceSwitches(void) { - char sw_topic[sizeof(Settings.switch_topic)]; + char sw_topic[TOPSZ]; // Send info about buttons - char *tmp = Settings.switch_topic; + char *tmp = SettingsText(SET_MQTT_SWITCH_TOPIC); Format(sw_topic, tmp, sizeof(sw_topic)); if (!strcmp_P(sw_topic, "0") || strlen(sw_topic) == 0 ) { for (uint32_t switch_index = 0; switch_index < MAX_SWITCHES; switch_index++) { @@ -368,7 +369,7 @@ void HAssAnnounceSwitches(void) // Check if MQTT message will be ON/OFF or TOGGLE if (Settings.switchmode[switch_index] == FOLLOW || Settings.switchmode[switch_index] == FOLLOW_INV || Settings.flag3.button_switch_force_local || // SetOption61 - Force local operation when button/switch topic is set - !strcmp(mqtt_topic, sw_topic) || !strcmp(Settings.mqtt_grptopic, sw_topic)) + !strcmp(mqtt_topic, sw_topic) || !strcmp(SettingsText(SET_MQTT_GRP_TOPIC), sw_topic)) { toggle = 0; // MQTT message will be ON/OFF } @@ -380,10 +381,10 @@ void HAssAnnounceSwitches(void) void HAssAnnounceButtons(void) { - char key_topic[sizeof(Settings.button_topic)]; + char key_topic[TOPSZ]; // Send info about buttons - char *tmp = Settings.button_topic; + char *tmp = SettingsText(SET_MQTT_BUTTON_TOPIC); Format(key_topic, tmp, sizeof(key_topic)); if (!strcmp_P(key_topic, "0") || strlen(key_topic) == 0 ) { for (uint32_t button_index = 0; button_index < MAX_KEYS; button_index++) { @@ -400,7 +401,7 @@ void HAssAnnounceButtons(void) // Check if MQTT message will be ON/OFF or TOGGLE 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)) + !strcmp(mqtt_topic, key_topic) || !strcmp(SettingsText(SET_MQTT_GRP_TOPIC), key_topic)) { toggle = 0; // MQTT message will be ON/OFF } @@ -444,7 +445,7 @@ void HAssAnnounceSensor(const char* sensorname, const char* subsensortype) GetTopic_P(state_topic, TELE, mqtt_topic, PSTR(D_RSLT_SENSOR)); } - snprintf_P(name, sizeof(name), PSTR("%s %s %s"), Settings.friendlyname[0], sensorname, subsensortype); + snprintf_P(name, sizeof(name), PSTR("%s %s %s"), SettingsText(SET_FRIENDLYNAME1), sensorname, subsensortype); GetTopic_P(availability_topic, TELE, mqtt_topic, S_LWT); FindPrefix(state_topic, availability_topic, prefix); Shorten(&state_topic, prefix); @@ -457,7 +458,7 @@ void HAssAnnounceSensor(const char* sensorname, const char* subsensortype) TryResponseAppend_P(HASS_DISCOVER_SENSOR_TEMP, TempUnit(), sensorname); } else if (!strcmp_P(subsensortype, PSTR(D_JSON_HUMIDITY))) { TryResponseAppend_P(HASS_DISCOVER_SENSOR_HUM, sensorname); - } else if (!strcmp_P(subsensortype, PSTR(D_JSON_PRESSURE)) + } else if (!strcmp_P(subsensortype, PSTR(D_JSON_PRESSURE)) || !strcmp_P(subsensortype, PSTR(D_JSON_PRESSUREATSEALEVEL))){ TryResponseAppend_P(HASS_DISCOVER_SENSOR_PRESS, PressureUnit().c_str(), sensorname, subsensortype); } else if (!strcmp_P(subsensortype, PSTR(D_JSON_TOTAL)) @@ -547,7 +548,7 @@ void HAssAnnounceStatusSensor(void) char *state_topic = stemp1; char *availability_topic = stemp2; - snprintf_P(name, sizeof(name), PSTR("%s status"), Settings.friendlyname[0]); + snprintf_P(name, sizeof(name), PSTR("%s status"), SettingsText(SET_FRIENDLYNAME1)); GetTopic_P(state_topic, TELE, mqtt_topic, PSTR(D_RSLT_HASS_STATE)); GetTopic_P(availability_topic, TELE, mqtt_topic, S_LWT); FindPrefix(state_topic, availability_topic, prefix); @@ -557,7 +558,7 @@ void HAssAnnounceStatusSensor(void) Response_P(HASS_DISCOVER_SENSOR, name, state_topic, availability_topic); TryResponseAppend_P(HASS_DISCOVER_SENSOR_HASS_STATUS, state_topic); TryResponseAppend_P(HASS_DISCOVER_DEVICE_INFO, unique_id, ESP.getChipId(), WiFi.macAddress().c_str(), - Settings.friendlyname[0], ModuleName().c_str(), my_version, my_image); + SettingsText(SET_FRIENDLYNAME1), ModuleName().c_str(), my_version, my_image); TryResponseAppend_P(HASS_DISCOVER_TOPIC_PREFIX, prefix); TryResponseAppend_P(PSTR("}")); } @@ -587,8 +588,8 @@ void HAssDiscovery(void) Settings.flag.decimal_text = 1; // SetOption17 - Switch between decimal or hexadecimal output - Respond with decimal color values Settings.flag3.hass_tele_on_power = 1; // SetOption59 - Send tele/%topic%/STATE in addition to stat/%topic%/RESULT - send tele/STATE message as stat/RESULT // Settings.light_scheme = 0; // To just control color it needs to be Scheme 0 - if (strcmp_P(Settings.mqtt_fulltopic, PSTR("%topic%/%prefix%/"))) { - strncpy_P(Settings.mqtt_fulltopic, PSTR("%topic%/%prefix%/"), sizeof(Settings.mqtt_fulltopic)); + if (strcmp_P(SettingsText(SET_MQTT_FULLTOPIC), PSTR("%topic%/%prefix%/"))) { + SettingsUpdateText(SET_MQTT_FULLTOPIC, "%topic%/%prefix%/"); restart_flag = 2; return; // As full topic has changed do restart first before sending discovery data } diff --git a/tasmota/xdrv_13_display.ino b/tasmota/xdrv_13_display.ino index 42bb9858b..b34b8ed27 100644 --- a/tasmota/xdrv_13_display.ino +++ b/tasmota/xdrv_13_display.ino @@ -1008,7 +1008,7 @@ void DisplayLogBufferInit(void) snprintf_P(buffer, sizeof(buffer), PSTR(D_CMND_HOSTNAME " %s"), my_hostname); DisplayLogBufferAdd(buffer); - snprintf_P(buffer, sizeof(buffer), PSTR(D_JSON_SSID " %s"), Settings.sta_ssid[Settings.sta_active]); + snprintf_P(buffer, sizeof(buffer), PSTR(D_JSON_SSID " %s"), SettingsText(SET_STASSID1 + Settings.sta_active)); DisplayLogBufferAdd(buffer); snprintf_P(buffer, sizeof(buffer), PSTR(D_JSON_MAC " %s"), WiFi.macAddress().c_str()); DisplayLogBufferAdd(buffer); @@ -1196,7 +1196,7 @@ void DisplayMqttSubscribe(void) char ntopic[TOPSZ]; ntopic[0] = '\0'; - strlcpy(stopic, Settings.mqtt_fulltopic, sizeof(stopic)); + strlcpy(stopic, SettingsText(SET_MQTT_FULLTOPIC), sizeof(stopic)); char *tp = strtok(stopic, "/"); while (tp != nullptr) { if (!strcmp_P(tp, MQTT_TOKEN_PREFIX)) { @@ -1205,7 +1205,7 @@ void DisplayMqttSubscribe(void) strncat_P(ntopic, PSTR("+/"), sizeof(ntopic) - strlen(ntopic) -1); // Add single-level wildcards tp = strtok(nullptr, "/"); } - strncat(ntopic, Settings.mqtt_prefix[2], sizeof(ntopic) - strlen(ntopic) -1); // Subscribe to tele messages + strncat(ntopic, SettingsText(SET_MQTTPREFIX3), sizeof(ntopic) - strlen(ntopic) -1); // Subscribe to tele messages strncat_P(ntopic, PSTR("/#"), sizeof(ntopic) - strlen(ntopic) -1); // Add multi-level wildcard MqttSubscribe(ntopic); disp_subscribed = true; @@ -1219,7 +1219,7 @@ bool DisplayMqttData(void) if (disp_subscribed) { char stopic[TOPSZ]; - snprintf_P(stopic, sizeof(stopic) , PSTR("%s/"), Settings.mqtt_prefix[2]); // tele/ + snprintf_P(stopic, sizeof(stopic) , PSTR("%s/"), SettingsText(SET_MQTTPREFIX3)); // tele/ char *tp = strstr(XdrvMailbox.topic, stopic); if (tp) { // tele/tasmota/SENSOR if (Settings.display_mode &0x04) { diff --git a/tasmota/xdrv_17_rcswitch.ino b/tasmota/xdrv_17_rcswitch.ino index dfe5dc66a..156535876 100644 --- a/tasmota/xdrv_17_rcswitch.ino +++ b/tasmota/xdrv_17_rcswitch.ino @@ -85,6 +85,7 @@ void RfInit(void) mySwitch.enableTransmit(pin[GPIO_RFSEND]); } if (pin[GPIO_RFRECV] < 99) { + pinMode( pin[GPIO_RFRECV], INPUT); mySwitch.enableReceive(pin[GPIO_RFRECV]); } } diff --git a/tasmota/xdrv_20_hue.ino b/tasmota/xdrv_20_hue.ino index 254fa4d99..1d13ee263 100644 --- a/tasmota/xdrv_20_hue.ino +++ b/tasmota/xdrv_20_hue.ino @@ -365,17 +365,18 @@ void HueLightStatus1(uint8_t device, String *response) // Any device whose friendly name start with "$" is considered hidden bool HueActive(uint8_t device) { if (device > MAX_FRIENDLYNAMES) { device = MAX_FRIENDLYNAMES; } - return '$' != Settings.friendlyname[device-1][0]; +// return '$' != Settings.friendlyname[device-1][0]; + return '$' != *SettingsText(SET_FRIENDLYNAME1 +device -1); } void HueLightStatus2(uint8_t device, String *response) { *response += FPSTR(HUE_LIGHTS_STATUS_JSON2); if (device <= MAX_FRIENDLYNAMES) { - response->replace("{j1", Settings.friendlyname[device-1]); + response->replace("{j1", SettingsText(SET_FRIENDLYNAME1 +device -1)); } else { char fname[33]; - strcpy(fname, Settings.friendlyname[MAX_FRIENDLYNAMES-1]); + strcpy(fname, SettingsText(SET_FRIENDLYNAME1 + MAX_FRIENDLYNAMES -1)); uint32_t fname_len = strlen(fname); if (fname_len > 30) { fname_len = 30; } fname[fname_len++] = '-'; diff --git a/tasmota/xdrv_21_wemo.ino b/tasmota/xdrv_21_wemo.ino index 4a50560e2..13e867ae6 100644 --- a/tasmota/xdrv_21_wemo.ino +++ b/tasmota/xdrv_21_wemo.ino @@ -241,7 +241,7 @@ void HandleUpnpSetupWemo(void) AddLog_P(LOG_LEVEL_DEBUG, S_LOG_HTTP, PSTR(D_WEMO_SETUP)); String setup_xml = FPSTR(WEMO_SETUP_XML); - setup_xml.replace("{x1", Settings.friendlyname[0]); + setup_xml.replace("{x1", SettingsText(SET_FRIENDLYNAME1)); setup_xml.replace("{x2", WemoUuid()); setup_xml.replace("{x3", WemoSerialnumber()); WSSend(200, CT_XML, setup_xml); diff --git a/tasmota/xdrv_23_zigbee_0_constants.ino b/tasmota/xdrv_23_zigbee_0_constants.ino index 996d877f8..0c74c9d8a 100644 --- a/tasmota/xdrv_23_zigbee_0_constants.ino +++ b/tasmota/xdrv_23_zigbee_0_constants.ino @@ -19,8 +19,6 @@ #ifdef USE_ZIGBEE -#define ZIGBEE_VERBOSE // output versbose MQTT Zigbee logs. Will remain active for now - typedef uint64_t Z_IEEEAddress; typedef uint16_t Z_ShortAddress; diff --git a/tasmota/xdrv_23_zigbee_1_headers.ino b/tasmota/xdrv_23_zigbee_1_headers.ino new file mode 100644 index 000000000..b017d8e2e --- /dev/null +++ b/tasmota/xdrv_23_zigbee_1_headers.ino @@ -0,0 +1,26 @@ +/* + xdrv_23_zigbee_1_headers.ino - zigbee support for Tasmota + + Copyright (C) 2019 Theo Arends and Stephan Hadinger + + 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 . +*/ + +#ifdef USE_ZIGBEE + +// contains some definitions for functions used before their declarations + +void ZigbeeZCLSend(uint16_t dtsAddr, uint16_t clusterId, uint8_t endpoint, uint8_t cmdId, bool clusterSpecific, const uint8_t *msg, size_t len, bool disableDefResp = true, uint8_t transacId = 1); + +#endif // USE_ZIGBEE diff --git a/tasmota/xdrv_23_zigbee_3_devices.ino b/tasmota/xdrv_23_zigbee_3_devices.ino index f76c882c7..de13990c2 100644 --- a/tasmota/xdrv_23_zigbee_3_devices.ino +++ b/tasmota/xdrv_23_zigbee_3_devices.ino @@ -22,6 +22,9 @@ #include #include + +typedef int32_t (*Z_DeviceTimer)(uint16_t shortaddr, uint16_t cluster, uint16_t endpoint, uint32_t value); + typedef struct Z_Device { uint16_t shortaddr; // unique key if not null, or unspecified if null uint64_t longaddr; // 0x00 means unspecified @@ -33,6 +36,12 @@ typedef struct Z_Device { std::vector endpoints; // encoded as high 16 bits is endpoint, low 16 bits is ProfileId std::vector clusters_in; // encoded as high 16 bits is endpoint, low 16 bits is cluster number std::vector clusters_out; // encoded as high 16 bits is endpoint, low 16 bits is cluster number + // below are per device timers, used for example to query the new state of the device + uint32_t timer; // millis() when to fire the timer, 0 if no timer + uint16_t cluster; // cluster to use for the timer + uint16_t endpoint; // endpoint to use for timer + uint32_t value; // any raw value to use for the timer + Z_DeviceTimer func; // function to call when timer occurs } Z_Device; // All devices are stored in a Vector @@ -70,6 +79,11 @@ public: // Dump json String dump(uint32_t dump_mode, int32_t device_num = 0) const; + // Timers + void resetTimer(uint32_t shortaddr); + void setTimer(uint32_t shortaddr, uint32_t wait_ms, uint16_t cluster, uint16_t endpoint, uint32_t value, Z_DeviceTimer func); + void runTimer(void); + private: std::vector _devices = {}; @@ -157,7 +171,9 @@ Z_Device & Z_Devices::createDeviceEntry(uint16_t shortaddr, uint64_t longaddr) { String(), // FriendlyName std::vector(), std::vector(), - std::vector() }; + std::vector(), + 0,0,0,0, + nullptr }; _devices.push_back(device); return _devices.back(); } @@ -346,6 +362,47 @@ void Z_Devices::updateLastSeen(uint16_t shortaddr) { _updateLastSeen(device); } +// Per device timers +// +// Reset the timer for a specific device +void Z_Devices::resetTimer(uint32_t shortaddr) { + Z_Device & device = getShortAddr(shortaddr); + if (&device == nullptr) { return; } // don't crash if not found + device.timer = 0; + device.func = nullptr; +} + +// Set timer for a specific device +void Z_Devices::setTimer(uint32_t shortaddr, uint32_t wait_ms, uint16_t cluster, uint16_t endpoint, uint32_t value, Z_DeviceTimer func) { + Z_Device & device = getShortAddr(shortaddr); + if (&device == nullptr) { return; } // don't crash if not found + + device.cluster = cluster; + device.endpoint = endpoint; + device.value = value; + device.func = func; + device.timer = wait_ms + millis(); +} + +// Run timer at each tick +void Z_Devices::runTimer(void) { + uint32_t now = millis(); + + for (std::vector::iterator it = _devices.begin(); it != _devices.end(); ++it) { + Z_Device &device = *it; + uint16_t shortaddr = device.shortaddr; + + uint32_t timer = device.timer; + if ((timer) && (timer <= now)) { + // trigger the timer + (*device.func)(device.shortaddr, device.cluster, device.endpoint, device.value); + + device.timer = 0; // cancel the timer + } + } +} + + // Dump the internal memory of Zigbee devices // Mode = 1: simple dump of devices addresses and names // Mode = 2: Mode 1 + also dump the endpoints, profiles and clusters diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index bf58a80b9..687ac2483 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -39,45 +39,44 @@ class ZCLFrame { public: ZCLFrame(uint8_t frame_control, uint16_t manuf_code, uint8_t transact_seq, uint8_t cmd_id, - const char *buf, size_t buf_len, uint16_t clusterid = 0, uint16_t groupid = 0): + const char *buf, size_t buf_len, uint16_t clusterid = 0, uint16_t groupid = 0, + uint16_t srcaddr = 0, uint8_t srcendpoint = 0, uint8_t dstendpoint = 0, uint8_t wasbroadcast = 0, + uint8_t linkquality = 0, uint8_t securityuse = 0, uint8_t seqnumber = 0, + uint32_t timestamp = 0): _cmd_id(cmd_id), _manuf_code(manuf_code), _transact_seq(transact_seq), _payload(buf_len ? buf_len : 250), // allocate the data frame from source or preallocate big enough - _cluster_id(clusterid), _group_id(groupid) + _cluster_id(clusterid), _group_id(groupid), + _srcaddr(srcaddr), _srcendpoint(srcendpoint), _dstendpoint(dstendpoint), _wasbroadcast(wasbroadcast), + _linkquality(linkquality), _securityuse(securityuse), _seqnumber(seqnumber), + _timestamp(timestamp) { _frame_control.d8 = frame_control; _payload.addBuffer(buf, buf_len); }; - void publishMQTTReceived(uint16_t groupid, uint16_t clusterid, Z_ShortAddress srcaddr, - uint8_t srcendpoint, uint8_t dstendpoint, uint8_t wasbroadcast, - uint8_t linkquality, uint8_t securityuse, uint8_t seqnumber, - uint32_t timestamp) { -#ifdef ZIGBEE_VERBOSE + void log(void) { char hex_char[_payload.len()*2+2]; ToHex_P((unsigned char*)_payload.getBuffer(), _payload.len(), hex_char, sizeof(hex_char)); - Response_P(PSTR("{\"" D_JSON_ZIGBEEZCL_RECEIVED "\":{" + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("{\"" D_JSON_ZIGBEEZCL_RECEIVED "\":{" "\"groupid\":%d," "\"clusterid\":%d," "\"srcaddr\":\"0x%04X\"," "\"srcendpoint\":%d," "\"dstendpoint\":%d," "\"wasbroadcast\":%d," "\"" D_CMND_ZIGBEE_LINKQUALITY "\":%d," "\"securityuse\":%d," "\"seqnumber\":%d," "\"timestamp\":%d," "\"fc\":\"0x%02X\",\"manuf\":\"0x%04X\",\"transact\":%d," - "\"cmdid\":\"0x%02X\",\"payload\":\"%s\""), - groupid, clusterid, srcaddr, - srcendpoint, dstendpoint, wasbroadcast, - linkquality, securityuse, seqnumber, - timestamp, + "\"cmdid\":\"0x%02X\",\"payload\":\"%s\"}}"), + _group_id, _cluster_id, _srcaddr, + _srcendpoint, _dstendpoint, _wasbroadcast, + _linkquality, _securityuse, _seqnumber, + _timestamp, _frame_control, _manuf_code, _transact_seq, _cmd_id, hex_char); - - ResponseJsonEnd(); // append '}' - ResponseJsonEnd(); // append '}' - MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCL_RECEIVED)); - XdrvRulesProcess(); -#endif } - static ZCLFrame parseRawFrame(const SBuffer &buf, uint8_t offset, uint8_t len, uint16_t clusterid, uint16_t groupid) { // parse a raw frame and build the ZCL frame object + static ZCLFrame parseRawFrame(const SBuffer &buf, uint8_t offset, uint8_t len, uint16_t clusterid, uint16_t groupid, + uint16_t srcaddr = 0, uint8_t srcendpoint = 0, uint8_t dstendpoint = 0, uint8_t wasbroadcast = 0, + uint8_t linkquality = 0, uint8_t securityuse = 0, uint8_t seqnumber = 0, + uint32_t timestamp = 0) { // parse a raw frame and build the ZCL frame object uint32_t i = offset; ZCLHeaderFrameControl_t frame_control; uint16_t manuf_code = 0; @@ -122,10 +121,18 @@ public: return _cluster_id; } + inline uint16_t getSrcEndpoint(void) const { + return _srcendpoint; + } + const SBuffer &getPayload(void) const { return _payload; } + uint16_t getManufCode(void) const { + return _manuf_code; + } + private: ZCLHeaderFrameControl_t _frame_control = { .d8 = 0 }; uint16_t _manuf_code = 0; // optional @@ -134,6 +141,15 @@ private: uint16_t _cluster_id = 0; uint16_t _group_id = 0; SBuffer _payload; + // information from decoded ZCL frame + uint16_t _srcaddr; + uint8_t _srcendpoint; + uint8_t _dstendpoint; + uint8_t _wasbroadcast; + uint8_t _linkquality; + uint8_t _securityuse; + uint8_t _seqnumber; + uint32_t _timestamp; }; // Zigbee ZCL converters @@ -456,7 +472,7 @@ void ZCLFrame::parseClusterSpecificCommand(JsonObject& json, uint8_t offset) { // return value: // 0 = keep initial value // 1 = remove initial value -typedef int32_t (*Z_AttrConverter)(uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper* new_name); +typedef int32_t (*Z_AttrConverter)(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper* new_name, uint16_t cluster, uint16_t attr); typedef struct Z_AttributeConverter { uint16_t cluster; uint16_t attribute; @@ -464,6 +480,8 @@ typedef struct Z_AttributeConverter { Z_AttrConverter func; } Z_AttributeConverter; +#define OCCUPANCY "Occupancy" // global define for Aqara + // list of post-processing directives const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { 0x0000, 0x0000, "ZCLVersion", &Z_Copy }, @@ -498,7 +516,7 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { 0x0007, 0x0000, "SwitchType", &Z_Copy }, // Level Control cluster - { 0x0008, 0x0000, "CurrentLevel", &Z_Copy }, + { 0x0008, 0x0000, "Dimmer", &Z_Copy }, // { 0x0008, 0x0001, "RemainingTime", &Z_Copy }, // { 0x0008, 0x0010, "OnOffTransitionTime", &Z_Copy }, // { 0x0008, 0x0011, "OnLevel", &Z_Copy }, @@ -625,6 +643,11 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { 0x0101, 0x0004, "DoorOpenEvents", &Z_Copy }, { 0x0101, 0x0005, "DoorClosedEvents", &Z_Copy }, { 0x0101, 0x0006, "OpenPeriod", &Z_Copy }, + // Aqara Lumi Vibration Sensor + { 0x0101, 0x0055, "AqaraVibrationMode", &Z_AqaraVibration }, + { 0x0101, 0x0503, "AqaraVibrationsOrAngle", &Z_Copy }, + { 0x0101, 0x0505, "AqaraVibration505", &Z_Copy }, + { 0x0101, 0x0508, "AqaraAccelerometer", &Z_AqaraVibration }, // Window Covering cluster { 0x0102, 0x0000, "WindowCoveringType", &Z_Copy }, { 0x0102, 0x0001, "PhysicalClosedLimitLift",&Z_Copy }, @@ -648,14 +671,14 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { 0x0102, 0x0019, "IntermediateSetpointsTilt",&Z_Copy }, // Color Control cluster - { 0x0300, 0x0000, "CurrentHue", &Z_Copy }, - { 0x0300, 0x0001, "CurrentSaturation", &Z_Copy }, + { 0x0300, 0x0000, "Hue", &Z_Copy }, + { 0x0300, 0x0001, "Sat", &Z_Copy }, { 0x0300, 0x0002, "RemainingTime", &Z_Copy }, - { 0x0300, 0x0003, "CurrentX", &Z_Copy }, - { 0x0300, 0x0004, "CurrentY", &Z_Copy }, + { 0x0300, 0x0003, "X", &Z_Copy }, + { 0x0300, 0x0004, "Y", &Z_Copy }, { 0x0300, 0x0005, "DriftCompensation", &Z_Copy }, { 0x0300, 0x0006, "CompensationText", &Z_Copy }, - { 0x0300, 0x0007, "ColorTemperatureMireds",&Z_Copy }, + { 0x0300, 0x0007, "CT", &Z_Copy }, { 0x0300, 0x0008, "ColorMode", &Z_Copy }, { 0x0300, 0x0010, "NumberOfPrimaries", &Z_Copy }, { 0x0300, 0x0011, "Primary1X", &Z_Copy }, @@ -727,7 +750,7 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { 0x0405, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values // Occupancy Sensing cluster - { 0x0406, 0x0000, "Occupancy", &Z_Copy }, // Occupancy (map8) + { 0x0406, 0x0000, OCCUPANCY, &Z_AqaraOccupancy }, // Occupancy (map8) { 0x0406, 0x0001, "OccupancySensorType", &Z_Copy }, // OccupancySensorType { 0x0406, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values @@ -753,13 +776,13 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { // ====================================================================== // Record Manuf -int32_t Z_ManufKeep(uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name) { +int32_t Z_ManufKeep(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) { json[new_name] = value; zigbee_devices.setManufId(shortaddr, value.as()); return 1; } // -int32_t Z_ModelKeep(uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name) { +int32_t Z_ModelKeep(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) { json[new_name] = value; zigbee_devices.setModelId(shortaddr, value.as()); return 1; @@ -767,34 +790,113 @@ int32_t Z_ModelKeep(uint16_t shortaddr, JsonObject& json, const char *name, Json // ====================================================================== // Remove attribute -int32_t Z_Remove(uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name) { +int32_t Z_Remove(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) { return 1; // remove original key } // Copy value as-is -int32_t Z_Copy(uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name) { +int32_t Z_Copy(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) { json[new_name] = value; return 1; // remove original key } // Add pressure unit -int32_t Z_AddPressureUnit(uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name) { +int32_t Z_AddPressureUnit(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) { json[new_name] = F(D_UNIT_PRESSURE); return 0; // keep original key } // Convert int to float and divide by 100 -int32_t Z_FloatDiv100(uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name) { +int32_t Z_FloatDiv100(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) { json[new_name] = ((float)value) / 100.0f; return 1; // remove original key } // Convert int to float and divide by 10 -int32_t Z_FloatDiv10(uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name) { +int32_t Z_FloatDiv10(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) { json[new_name] = ((float)value) / 10.0f; return 1; // remove original key } -int32_t Z_AqaraSensor(uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name) { + +// Aqara Occupancy behavior: the Aqara device only sends Occupancy: true events every 60 seconds. +// Here we add a timer so if we don't receive a Occupancy event for 90 seconds, we send Occupancy:false +const uint32_t OCCUPANCY_TIMEOUT = 90 * 1000; // 90 s + +int32_t Z_OccupancyCallback(uint16_t shortaddr, uint16_t cluster, uint16_t endpoint, uint32_t value) { + // send Occupancy:false message + Response_P(PSTR("{\"" D_CMND_ZIGBEE_RECEIVED "\":{\"0x%04X\":{\"" OCCUPANCY "\":0}}}"), shortaddr); + MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR)); + XdrvRulesProcess(); +} + +int32_t Z_AqaraOccupancy(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) { + json[new_name] = value; + uint32_t occupancy = value; + + if (occupancy) { + zigbee_devices.setTimer(shortaddr, OCCUPANCY_TIMEOUT, cluster, zcl->getSrcEndpoint(), 0, &Z_OccupancyCallback); + } else { + zigbee_devices.resetTimer(shortaddr); + } + return 1; // remove original key +} + +// Aqara Vibration Sensor - special proprietary attributes +int32_t Z_AqaraVibration(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) { + //json[new_name] = value; + switch (attr) { + case 0x0055: + { + int32_t ivalue = value; + const __FlashStringHelper * svalue; + switch (ivalue) { + case 1: svalue = F("vibrate"); break; + case 2: svalue = F("tilt"); break; + case 3: svalue = F("drop"); break; + default: svalue = F("unknown"); break; + } + json[new_name] = svalue; + } + break; + // case 0x0503: + // break; + // case 0x0505: + // break; + case 0x0508: + { + // see https://github.com/Koenkk/zigbee2mqtt/issues/295 and http://faire-ca-soi-meme.fr/domotique/2018/09/03/test-xiaomi-aqara-vibration-sensor/ + // report accelerometer measures + String hex = value; + SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); + int16_t x, y, z; + z = buf2.get16(0); + y = buf2.get16(2); + x = buf2.get16(4); + JsonArray& xyz = json.createNestedArray(new_name); + xyz.add(x); + xyz.add(y); + xyz.add(z); + // calculate angles + float X = x; + float Y = y; + float Z = z; + int32_t Angle_X = 0.5f + atanf(X/sqrtf(z*z+y*y)) * f_180pi; + int32_t Angle_Y = 0.5f + atanf(Y/sqrtf(x*x+z*z)) * f_180pi; + int32_t Angle_Z = 0.5f + atanf(Z/sqrtf(x*x+y*y)) * f_180pi; + // int32_t Angle_X = 0.5f + atanf(X/sqrtf(Z*Z+Y*Y)) * f_180pi; + // int32_t Angle_Y = 0.5f + atanf(Y/sqrtf(X*X+Z*Z)) * f_180pi; + // int32_t Angle_Z = 0.5f + atanf(Z/sqrtf(X*X+Y*Y)) * f_180pi; + JsonArray& angles = json.createNestedArray(F("AqaraAngles")); + angles.add(Angle_X); + angles.add(Angle_Y); + angles.add(Angle_Z); + } + break; + } + return 1; // remove original key +} + +int32_t Z_AqaraSensor(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) { String hex = value; SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); uint32_t i = 0; @@ -809,36 +911,31 @@ int32_t Z_AqaraSensor(uint16_t shortaddr, JsonObject& json, const char *name, Js i += parseSingleAttribute(json, tmp, buf2, i, len); float val = json[tmp]; json.remove(tmp); - if (0x64 == attrid) { - json[F(D_JSON_TEMPERATURE)] = val / 100.0f; - } else if (0x65 == attrid) { - json[F(D_JSON_HUMIDITY)] = val / 100.0f; - } else if (0x66 == attrid) { - json[F(D_JSON_PRESSURE)] = val / 100.0f; - json[F(D_JSON_PRESSURE_UNIT)] = F(D_UNIT_PRESSURE); // hPa - } else if (0x01 == attrid) { + if (0x01 == attrid) { json[F(D_JSON_VOLTAGE)] = val / 1000.0f; json[F("Battery")] = toPercentageCR2032(val); + } else if (0 == zcl->getManufCode()) { + // onla Aqara Temp/Humidity has manuf_code of zero. If non-zero we skip the parameters + if (0x64 == attrid) { + json[F(D_JSON_TEMPERATURE)] = val / 100.0f; + } else if (0x65 == attrid) { + json[F(D_JSON_HUMIDITY)] = val / 100.0f; + } else if (0x66 == attrid) { + json[F(D_JSON_PRESSURE)] = val / 100.0f; + json[F(D_JSON_PRESSURE_UNIT)] = F(D_UNIT_PRESSURE); // hPa + } else if (0x01 == attrid) { + json[F(D_JSON_VOLTAGE)] = val / 1000.0f; + json[F("Battery")] = toPercentageCR2032(val); + } + } else if (0x115F == zcl->getManufCode()) { + // Aqara Motion Sensor, still unknown field + json[F("AqaraUnknown")] = val; } } return 1; // remove original key } // ====================================================================== -// Cluster Specific commands -// #define ZCL_OO_OFF "s_0006_00" // Cluster 0x0006, cmd 0x00 - On/Off - Off -// #define ZCL_OO_ON "s_0006_01" // Cluster 0x0006, cmd 0x01 - On/Off - On -// #define ZCL_COLORTEMP_MOVE "s_0300_0A" // Cluster 0x0300, cmd 0x0A, Move to Color Temp -// #define ZCL_LC_MOVE "s_0008_00" // Cluster 0x0008, cmd 0x00, Level Control Move to Level -// #define ZCL_LC_MOVE_1 "s_0008_01" // Cluster 0x0008, cmd 0x01, Level Control Move -// #define ZCL_LC_STEP "s_0008_02" // Cluster 0x0008, cmd 0x02, Level Control Step -// #define ZCL_LC_STOP "s_0008_03" // Cluster 0x0008, cmd 0x03, Level Control Stop -// #define ZCL_LC_MOVE_WOO "s_0008_04" // Cluster 0x0008, cmd 0x04, Level Control Move to Level, with On/Off -// #define ZCL_LC_MOVE_1_WOO "s_0008_05" // Cluster 0x0008, cmd 0x05, Level Control Move, with On/Off -// #define ZCL_LC_STEP_WOO "s_0008_06" // Cluster 0x0008, cmd 0x05, Level Control Step, with On/Off -// #define ZCL_LC_STOP_WOO "s_0008_07" // Cluster 0x0008, cmd 0x07, Level Control Stop - - void ZCLFrame::postProcessAttributes(uint16_t shortaddr, JsonObject& json) { // iterate on json elements for (auto kv : json) { @@ -859,7 +956,7 @@ void ZCLFrame::postProcessAttributes(uint16_t shortaddr, JsonObject& json) { if ((conv_cluster == cluster) && ((conv_attribute == attribute) || (conv_attribute == 0xFFFF)) ) { - int32_t drop = (*converter->func)(shortaddr, json, key, value, (const __FlashStringHelper*) converter->name); + int32_t drop = (*converter->func)(this, shortaddr, json, key, value, (const __FlashStringHelper*) converter->name, conv_cluster, conv_attribute); if (drop) { json.remove(key); } @@ -870,157 +967,4 @@ void ZCLFrame::postProcessAttributes(uint16_t shortaddr, JsonObject& json) { } } -//void ZCLFrame::postProcessAttributes2(JsonObject& json) { -// void postProcessAttributes2(JsonObject& json) { -// const __FlashStringHelper *key; -// -// // Osram Mini Switch -// key = F(ZCL_OO_OFF); -// if (json.containsKey(key)) { -// json.remove(key); -// json[F(D_CMND_POWER)] = F("Off"); -// } -// key = F(ZCL_OO_ON); -// if (json.containsKey(key)) { -// json.remove(key); -// json[F(D_CMND_POWER)] = F("On"); -// } -// key = F(ZCL_COLORTEMP_MOVE); -// if (json.containsKey(key)) { -// String hex = json[key]; -// SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); -// uint16_t color_temp = buf2.get16(0); -// uint16_t transition_time = buf2.get16(2); -// json.remove(key); -// json[F("ColorTemp")] = color_temp; -// json[F("TransitionTime")] = transition_time / 10.0f; -// } -// key = F(ZCL_LC_MOVE_WOO); -// if (json.containsKey(key)) { -// String hex = json[key]; -// SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); -// uint8_t level = buf2.get8(0); -// uint16_t transition_time = buf2.get16(1); -// json.remove(key); -// json[F("Dimmer")] = changeUIntScale(level, 0, 255, 0, 100); // change to percentage -// json[F("TransitionTime")] = transition_time / 10.0f; -// if (0 == level) { -// json[F(D_CMND_POWER)] = F("Off"); -// } else { -// json[F(D_CMND_POWER)] = F("On"); -// } -// } -// key = F(ZCL_LC_MOVE); -// if (json.containsKey(key)) { -// String hex = json[key]; -// SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); -// uint8_t level = buf2.get8(0); -// uint16_t transition_time = buf2.get16(1); -// json.remove(key); -// json[F("Dimmer")] = changeUIntScale(level, 0, 255, 0, 100); // change to percentage -// json[F("TransitionTime")] = transition_time / 10.0f; -// } -// key = F(ZCL_LC_MOVE_1); -// if (json.containsKey(key)) { -// String hex = json[key]; -// SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); -// uint8_t move_mode = buf2.get8(0); -// uint8_t move_rate = buf2.get8(1); -// json.remove(key); -// json[F("Move")] = move_mode ? F("Down") : F("Up"); -// json[F("Rate")] = move_rate; -// } -// key = F(ZCL_LC_MOVE_1_WOO); -// if (json.containsKey(key)) { -// String hex = json[key]; -// SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); -// uint8_t move_mode = buf2.get8(0); -// uint8_t move_rate = buf2.get8(1); -// json.remove(key); -// json[F("Move")] = move_mode ? F("Down") : F("Up"); -// json[F("Rate")] = move_rate; -// if (0 == move_mode) { -// json[F(D_CMND_POWER)] = F("On"); -// } -// } -// key = F(ZCL_LC_STEP); -// if (json.containsKey(key)) { -// String hex = json[key]; -// SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); -// uint8_t step_mode = buf2.get8(0); -// uint8_t step_size = buf2.get8(1); -// uint16_t transition_time = buf2.get16(2); -// json.remove(key); -// json[F("Step")] = step_mode ? F("Down") : F("Up"); -// json[F("StepSize")] = step_size; -// json[F("TransitionTime")] = transition_time / 10.0f; -// } -// key = F(ZCL_LC_STEP_WOO); -// if (json.containsKey(key)) { -// String hex = json[key]; -// SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); -// uint8_t step_mode = buf2.get8(0); -// uint8_t step_size = buf2.get8(1); -// uint16_t transition_time = buf2.get16(2); -// json.remove(key); -// json[F("Step")] = step_mode ? F("Down") : F("Up"); -// json[F("StepSize")] = step_size; -// json[F("TransitionTime")] = transition_time / 10.0f; -// if (0 == step_mode) { -// json[F(D_CMND_POWER)] = F("On"); -// } -// } -// key = F(ZCL_LC_STOP); -// if (json.containsKey(key)) { -// json.remove(key); -// json[F("Stop")] = 1; -// } -// key = F(ZCL_LC_STOP_WOO); -// if (json.containsKey(key)) { -// json.remove(key); -// json[F("Stop")] = 1; -// } -// -// // Lumi.weather proprietary field -// key = F(ZCL_LUMI_WEATHER); -// if (json.containsKey(key)) { -// String hex = json[key]; -// SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); -// DynamicJsonBuffer jsonBuffer; -// JsonObject& json_lumi = jsonBuffer.createObject(); -// uint32_t i = 0; -// uint32_t len = buf2.len(); -// char shortaddr[8]; -// -// while (len - i >= 2) { -// uint8_t attrid = buf2.get8(i++); -// -// snprintf_P(shortaddr, sizeof(shortaddr), PSTR("0x%02X"), attrid); -// -// //json[shortaddr] = parseSingleAttribute(json_lumi, buf2, i, len, nullptr, 0); -// } -// // parse output -// if (json_lumi.containsKey("0x64")) { // Temperature -// int32_t temperature = json_lumi["0x64"]; -// json[F(D_JSON_TEMPERATURE)] = temperature / 100.0f; -// } -// if (json_lumi.containsKey("0x65")) { // Humidity -// uint32_t humidity = json_lumi["0x65"]; -// json[F(D_JSON_HUMIDITY)] = humidity / 100.0f; -// } -// if (json_lumi.containsKey("0x66")) { // Pressure -// int32_t pressure = json_lumi["0x66"]; -// json[F(D_JSON_PRESSURE)] = pressure / 100.0f; -// json[F(D_JSON_PRESSURE_UNIT)] = F(D_UNIT_PRESSURE); // hPa -// } -// if (json_lumi.containsKey("0x01")) { // Battery Voltage -// uint32_t voltage = json_lumi["0x01"]; -// json[F(D_JSON_VOLTAGE)] = voltage / 1000.0f; -// json[F("Battery")] = toPercentageCR2032(voltage); -// } -// json.remove(key); -// } -// -// } - #endif // USE_ZIGBEE diff --git a/tasmota/xdrv_23_zigbee_6_commands.ino b/tasmota/xdrv_23_zigbee_6_commands.ino index c877bd3a3..70a8f0788 100644 --- a/tasmota/xdrv_23_zigbee_6_commands.ino +++ b/tasmota/xdrv_23_zigbee_6_commands.ino @@ -47,6 +47,64 @@ const Z_CommandConverter Z_Commands[] = { { "ShutterTilt", "0102!08xx"}, // Tilt percentage }; +#define ZLE(x) ((x) & 0xFF), ((x) >> 8) // Little Endian + +// Below are the attributes we wand to read from each cluster +const uint8_t CLUSTER_0006[] = { ZLE(0x0000) }; // Power +const uint8_t CLUSTER_0008[] = { ZLE(0x0000) }; // CurrentLevel +const uint8_t CLUSTER_0009[] = { ZLE(0x0000) }; // AlarmCount +const uint8_t CLUSTER_0300[] = { ZLE(0x0000), ZLE(0x0001), ZLE(0x0003), ZLE(0x0004), ZLE(0x0007) }; // Hue, Sat, X, Y, CT + +int32_t Z_ReadAttrCallback(uint16_t shortaddr, uint16_t cluster, uint16_t endpoint, uint32_t value) { + size_t attrs_len = 0; + const uint8_t* attrs = nullptr; + + switch (cluster) { + case 0x0006: // for On/Off + attrs = CLUSTER_0006; + attrs_len = sizeof(CLUSTER_0006); + break; + case 0x0008: // for Dimmer + attrs = CLUSTER_0008; + attrs_len = sizeof(CLUSTER_0008); + break; + case 0x0009: // for Alarms + attrs = CLUSTER_0009; + attrs_len = sizeof(CLUSTER_0009); + break; + case 0x0300: // for Lights + attrs = CLUSTER_0300; + attrs_len = sizeof(CLUSTER_0300); + break; + } + if (attrs) { + ZigbeeZCLSend(shortaddr, cluster, endpoint, ZCL_READ_ATTRIBUTES, false, attrs, attrs_len, false /* we do want a response */); + } +} + + +// set a timer to read back the value in the future +void zigbeeSetCommandTimer(uint16_t shortaddr, uint16_t cluster, uint16_t endpoint) { + uint32_t wait_ms = 0; + + switch (cluster) { + case 0x0006: // for On/Off + case 0x0009: // for Alamrs + wait_ms = 200; // wait 0.2 s + break; + case 0x0008: // for Dimmer + case 0x0300: // for Color + wait_ms = 1050; // wait 1.0 s + break; + case 0x0102: // for Shutters + wait_ms = 10000; // wait 10.0 s + break; + } + if (wait_ms) { + zigbee_devices.setTimer(shortaddr, wait_ms, cluster, endpoint, 0 /* value */, &Z_ReadAttrCallback); + } +} + const __FlashStringHelper* zigbeeFindCommand(const char *command) { char parm_uc[16]; // used to convert JSON keys to uppercase for (uint32_t i = 0; i < sizeof(Z_Commands) / sizeof(Z_Commands[0]); i++) { @@ -114,6 +172,7 @@ const uint8_t kZ_Numbers[] PROGMEM = { 0,0,0,0,0, 2,2, 255 }; +// Convert an alias like "On" to the corresponding number uint32_t ZigbeeAliasOrNumber(const char *state_text) { char command[16]; int state_number = GetCommandCode(command, sizeof(command), state_text, kZ_Alias); diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index a610bb882..2f30080fa 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -370,15 +370,12 @@ int32_t Z_ReceiveAfIncomingMessage(int32_t res, const class SBuffer &buf) { uint8_t seqnumber = buf.get8(17); zigbee_devices.updateLastSeen(srcaddr); - ZCLFrame zcl_received = ZCLFrame::parseRawFrame(buf, 19, buf.get8(18), clusterid, groupid); - -#ifdef ZIGBEE_VERBOSE - zcl_received.publishMQTTReceived(groupid, clusterid, srcaddr, - srcendpoint, dstendpoint, wasbroadcast, - linkquality, securityuse, seqnumber, - timestamp); -#endif - + ZCLFrame zcl_received = ZCLFrame::parseRawFrame(buf, 19, buf.get8(18), clusterid, groupid, + srcaddr, + srcendpoint, dstendpoint, wasbroadcast, + linkquality, securityuse, seqnumber, + timestamp); + zcl_received.log(); char shortaddr[8]; snprintf_P(shortaddr, sizeof(shortaddr), PSTR("0x%04X"), srcaddr); @@ -398,7 +395,7 @@ int32_t Z_ReceiveAfIncomingMessage(int32_t res, const class SBuffer &buf) { String msg(""); msg.reserve(100); json_root.printTo(msg); - AddLog_P2(LOG_LEVEL_INFO, PSTR("ZigbeeZCLRawReceived: %s"), msg.c_str()); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZigbeeZCLRawReceived: %s"), msg.c_str()); zcl_received.postProcessAttributes(srcaddr, json); // Add linkquality diff --git a/tasmota/xdrv_23_zigbee_9_impl.ino b/tasmota/xdrv_23_zigbee_9_impl.ino index cb08eeaee..461b3af32 100644 --- a/tasmota/xdrv_23_zigbee_9_impl.ino +++ b/tasmota/xdrv_23_zigbee_9_impl.ino @@ -191,14 +191,9 @@ void ZigbeeInput(void) SBuffer znp_buffer = zigbee_buffer->subBuffer(2, zigbee_frame_len - 3); // remove SOF, LEN and FCS -#ifdef ZIGBEE_VERBOSE ToHex_P((unsigned char*)znp_buffer.getBuffer(), znp_buffer.len(), hex_char, sizeof(hex_char)); AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE D_JSON_ZIGBEEZNPRECEIVED " %s"), hex_char); - // Response_P(PSTR("{\"" D_JSON_ZIGBEEZNPRECEIVED "\":\"%s\"}"), hex_char); - // MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZNPRECEIVED)); - // XdrvRulesProcess(); -#endif // now process the message ZigbeeProcessInput(znp_buffer); @@ -235,13 +230,14 @@ void ZigbeeInit(void) * Commands \*********************************************************************************************/ -uint32_t strToUInt(const JsonVariant val) { +uint32_t strToUInt(const JsonVariant &val) { // if the string starts with 0x, it is considered Hex, otherwise it is an int if (val.is()) { return val.as(); } else { - if (val.is()) { - return strtoull(val.as(), nullptr, 0); + if (val.is()) { + String sval = val.as(); + return strtoull(sval.c_str(), nullptr, 0); } } return 0; // couldn't parse anything @@ -268,7 +264,7 @@ void CmndZigbeeReset(void) { void CmndZigbeeStatus(void) { if (ZigbeeSerial) { String dump = zigbee_devices.dump(XdrvMailbox.index, XdrvMailbox.payload); - Response_P(PSTR("{\"%s%d\":%s}"), XdrvMailbox.command, XdrvMailbox.payload, dump.c_str()); + Response_P(PSTR("{\"%s%d\":%s}"), XdrvMailbox.command, XdrvMailbox.index, dump.c_str()); } } @@ -319,15 +315,13 @@ void ZigbeeZNPSend(const uint8_t *msg, size_t len) { ZigbeeSerial->write(fcs); // finally send fcs checksum byte AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("ZNPSend FCS %02X"), fcs); } -#ifdef ZIGBEE_VERBOSE // Now send a MQTT message to report the sent message char hex_char[(len * 2) + 2]; AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE D_JSON_ZIGBEEZNPSENT " %s"), ToHex_P(msg, len, hex_char, sizeof(hex_char))); -#endif } -void ZigbeeZCLSend(uint16_t dtsAddr, uint16_t clusterId, uint8_t endpoint, uint8_t cmdId, bool clusterSpecific, const uint8_t *msg, size_t len, bool disableDefResp = true, uint8_t transacId = 1) { +void ZigbeeZCLSend(uint16_t dtsAddr, uint16_t clusterId, uint8_t endpoint, uint8_t cmdId, bool clusterSpecific, const uint8_t *msg, size_t len, bool disableDefResp, uint8_t transacId) { SBuffer buf(25+len); buf.add8(Z_SREQ | Z_AF); // 24 buf.add8(AF_DATA_REQUEST); // 01 @@ -422,6 +416,10 @@ void zigbeeZCLSendStr(uint16_t dstAddr, uint8_t endpoint, const char *data) { // everything is good, we can send the command ZigbeeZCLSend(dstAddr, cluster, endpoint, cmd, clusterSpecific, buf.getBuffer(), buf.len()); + // now set the timer, if any, to read back the state later + if (clusterSpecific) { + zigbeeSetCommandTimer(dstAddr, cluster, endpoint); + } ResponseCmndDone(); } @@ -539,7 +537,7 @@ void CmndZigbeeSend(void) { // we have an unsupported command type, just ignore it and fallback to missing command } - AddLog_P2(LOG_LEVEL_INFO, PSTR("ZigbeeCmd_actual: ZigbeeZCLSend {\"device\":\"0x%04X\",\"endpoint\":%d,\"send\":\"%s\"}"), + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZigbeeCmd_actual: ZigbeeZCLSend {\"device\":\"0x%04X\",\"endpoint\":%d,\"send\":\"%s\"}"), device, endpoint, cmd_str.c_str()); zigbeeZCLSendStr(device, endpoint, cmd_str.c_str()); } else { @@ -593,6 +591,7 @@ void CmndZigbeeRead(void) { const JsonVariant &val_attr = getCaseInsensitive(json, PSTR("Read")); if (nullptr != &val_attr) { + uint16_t val = strToUInt(val_attr); if (val_attr.is()) { JsonArray& attr_arr = val_attr; attrs_len = attr_arr.size() * 2; @@ -604,19 +603,18 @@ void CmndZigbeeRead(void) { attrs[i++] = val & 0xFF; attrs[i++] = val >> 8; } - } else { attrs_len = 2; attrs = new uint8_t[attrs_len]; - uint16_t val = strToUInt(val_attr); attrs[0] = val & 0xFF; // little endian attrs[1] = val >> 8; } } - ZigbeeZCLSend(device, cluster, endpoint, ZCL_READ_ATTRIBUTES, false, attrs, attrs_len, false /* we do want a response */); + ZigbeeZCLSend(device, cluster, endpoint, ZCL_READ_ATTRIBUTES, false, attrs, attrs_len, false /* we do want a response */); - if (attrs) { delete[] attrs; } + if (attrs) { delete[] attrs; } + ResponseCmndDone(); } // Allow or Deny pairing of new Zigbee devices @@ -647,6 +645,11 @@ bool Xdrv23(uint8_t function) if (zigbee.active) { switch (function) { + case FUNC_EVERY_50_MSECOND: + if (!zigbee.init_phase) { + zigbee_devices.runTimer(); + } + break; case FUNC_LOOP: if (ZigbeeSerial) { ZigbeeInput(); } if (zigbee.state_machine) { diff --git a/tasmota/xdrv_27_shutter.ino b/tasmota/xdrv_27_shutter.ino index 3c3039dda..e13bba771 100644 --- a/tasmota/xdrv_27_shutter.ino +++ b/tasmota/xdrv_27_shutter.ino @@ -24,23 +24,9 @@ #define XDRV_27 27 -#define D_PRFX_SHUTTER "Shutter" -#define D_CMND_SHUTTER_OPEN "Open" -#define D_CMND_SHUTTER_CLOSE "Close" -#define D_CMND_SHUTTER_STOP "Stop" -#define D_CMND_SHUTTER_POSITION "Position" -#define D_CMND_SHUTTER_OPENTIME "OpenDuration" -#define D_CMND_SHUTTER_CLOSETIME "CloseDuration" -#define D_CMND_SHUTTER_RELAY "Relay" -#define D_CMND_SHUTTER_SETHALFWAY "SetHalfway" -#define D_CMND_SHUTTER_SETCLOSE "SetClose" -#define D_CMND_SHUTTER_INVERT "Invert" -#define D_CMND_SHUTTER_CLIBRATION "Calibration" -#define D_CMND_SHUTTER_MOTORDELAY "MotorDelay" - #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,14 +37,16 @@ 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}"; +const char MSG_SHUTTER_POS[] PROGMEM = "SHT: " D_PRFX_SHUTTER " %d: Real. %d, Start: %d, Stop: %d, dir %d, motordelay %d, rtc: %s [s], freq %d"; #include @@ -68,23 +56,25 @@ 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. + int16_t motordelay[MAX_SHUTTERS]; // initial motorstarttime in 0.05sec. + int16_t pwm_frequency; // frequency of PWN for stepper motors + uint16_t max_pwm_frequency = 1000; // maximum of PWM frequency that can be used. depend on the motor and drivers + uint8_t skip_relay_change; // avoid overrun at endstops } 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]++; } } @@ -94,7 +84,7 @@ int32_t ShutterPercentToRealPosition(uint8_t percent,uint8_t index) if (Settings.shutter_set50percent[index] != 50) { return percent <= 5 ? Settings.shuttercoeff[2][index] * percent : Settings.shuttercoeff[1][index] * percent + Settings.shuttercoeff[0][index]; } else { - uint32_t realpos; + int32_t realpos; // check against DIV 0 for (uint8_t j=0 ; j < 5 ; j++) { if (Settings.shuttercoeff[j][index] == 0) { @@ -128,7 +118,7 @@ uint8_t ShutterRealToPercentPosition(int32_t realpos, uint8_t index) if (Settings.shutter_set50percent[index] != 50) { return Settings.shuttercoeff[2][index] * 5 > realpos ? realpos / Settings.shuttercoeff[2][index] : (realpos-Settings.shuttercoeff[0][index]) / Settings.shuttercoeff[1][index]; } else { - uint16_t realpercent; + int16_t realpercent; for (uint8_t i=0 ; i < 5 ; i++) { if (realpos > Shutter.open_max[index] * calibrate_pos[i+1] / 100) { @@ -146,7 +136,7 @@ uint8_t ShutterRealToPercentPosition(int32_t realpos, uint8_t index) break; } } - return realpercent; + return (realpercent < 0 ? 0 : (realpercent > 100 ? 0 : realpercent)); } } @@ -158,8 +148,6 @@ void ShutterInit(void) Shutter.old_power = power; bool relay_in_interlock = false; - AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Accuracy digits: %d"), Settings.shutter_accuracy); - for (uint32_t i = 0; i < MAX_SHUTTERS; i++) { // upgrade to 0.1sec calculation base. if ( Settings.shutter_accuracy == 0) { @@ -189,6 +177,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 ); @@ -220,8 +213,8 @@ void ShutterInit(void) dtostrfd((float)Shutter.open_time[i] / 10 , 1, shutter_open_chr); char shutter_close_chr[10]; dtostrfd((float)Shutter.close_time[i] / 10, 1, shutter_close_chr); - AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Shutter %d (Relay:%d): Init. Pos: %d [%d %%], Open Vel.: 100 Close Vel.: %d , Max Way: %d, Opentime %s [s], Closetime %s [s], CoedffCalc: c0: %d, c1 %d, c2: %d, c3: %d, c4: %d, binmask %d, is inverted %d, shuttermode %d,motordelay %d"), - i, Settings.shutter_startrelay[i], Shutter.real_position[i], Settings.shutter_position[i], Shutter.close_velocity[i], Shutter.open_max[i], shutter_open_chr, shutter_close_chr, + AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Shutter %d (Relay:%d): Init. Pos: %d [%d %%], Open Vel.: 100 Close Vel.: %d , Max Way: %d, Opentime %s [s], Closetime %s [s], CoeffCalc: c0: %d, c1 %d, c2: %d, c3: %d, c4: %d, binmask %d, is inverted %d, shuttermode %d,motordelay %d"), + i+1, Settings.shutter_startrelay[i], Shutter.real_position[i], Settings.shutter_position[i], Shutter.close_velocity[i], Shutter.open_max[i], shutter_open_chr, shutter_close_chr, Settings.shuttercoeff[0][i], Settings.shuttercoeff[1][i], Settings.shuttercoeff[2][i], Settings.shuttercoeff[3][i], Settings.shuttercoeff[4][i], Shutter.mask, Settings.shutter_invert[i], Shutter.mode, Shutter.motordelay[i]); @@ -229,6 +222,9 @@ void ShutterInit(void) // terminate loop at first INVALID shutter. break; } + if (shutters_present < 4) { + Shutter.max_pwm_frequency = Settings.shuttercoeff[4][4] > 0 ? Settings.shuttercoeff[4][4] : Shutter.max_pwm_frequency; + } Settings.shutter_accuracy = 1; } } @@ -237,45 +233,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]; - 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]) ; + if (Shutter.mode == SHT_OFF_ON__OPEN_CLOSE && pin[GPIO_PWM1+i] < 99 && pin[GPIO_CNTR1+i] < 99 ) { + // Calculate position with counter. Much more accurate and no need for motordelay workaround + // adding some steps to stop early + Shutter.real_position[i] = Shutter.direction[i] * 20 + ShutterCounterBasedPosition(i);; + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: real %d, start %d, counter %d, max_freq %d, dir %d, freq %d"),Shutter.real_position[i], Shutter.start_position[i] ,RtcSettings.pulse_counter[i],Shutter.max_pwm_frequency , Shutter.direction[i] ,Shutter.max_pwm_frequency ); + } else { + Shutter.real_position[i] = Shutter.start_position[i] + ( (Shutter.time[i] - Shutter.motordelay[i]) * (Shutter.direction[i] > 0 ? 100 : -Shutter.close_velocity[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.mode == SHT_OFF_ON__OPEN_CLOSE && pin[GPIO_PWM1+i] < 99) { + uint16_t freq_change = Shutter.max_pwm_frequency/(Shutter.motordelay[i]+1); + // ramp up phase. calculate frequency + Shutter.pwm_frequency = tmin(freq_change * Shutter.time[i],Shutter.max_pwm_frequency); + // ramp down at the end of the movement time will not be exactly motordelay + Shutter.pwm_frequency = tmax(tmin(freq_change * (Shutter.target_position[i]-Shutter.real_position[i])*Shutter.direction[i]/30, Shutter.pwm_frequency),10); + analogWriteFreq(Shutter.pwm_frequency); + analogWrite(pin[GPIO_PWM1+i], 50); } 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 +271,25 @@ 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]; + //prepare for stop PWM + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Remain steps %d, counter %d, freq %d"), missing_steps, RtcSettings.pulse_counter[i] ,Shutter.pwm_frequency); + Shutter.pwm_frequency = 0; + analogWriteFreq(Shutter.pwm_frequency); + 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] = ShutterCounterBasedPosition(i); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT:Real %d, pulsecount %d, start %d"), Shutter.real_position[i],RtcSettings.pulse_counter[i], Shutter.start_position[i]); + + } 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 +300,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_INFO, MSG_SHUTTER_POS, i+1, Shutter.real_position[i], Shutter.start_position[i], Shutter.target_position[i], Shutter.direction[i], Shutter.motordelay[i],stemp2,Shutter.pwm_frequency); + 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("{")); @@ -320,32 +332,73 @@ bool ShutterState(uint8_t device) (Shutter.mask & (1 << (Settings.shutter_startrelay[device]-1))) ); } -void ShutterStartInit(uint8_t index, uint8_t direction, int32_t target_pos) +void ShutterStartInit(uint8_t index, int8_t direction, int32_t target_pos) { - Shutter.direction[index] = direction; - Shutter.target_position[index] = target_pos; - Shutter.start_position[index] = Shutter.real_position[index]; - Shutter.time[index] = 0; + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: dir %d, delta1 %d, delta2 %d, grant %d"),direction, (Shutter.open_max[index] - Shutter.real_position[index]) / Shutter.close_velocity[index], Shutter.real_position[index] / Shutter.close_velocity[index], 2+Shutter.motordelay[index]); + if ( ( direction == 1 && (Shutter.open_max[index] - Shutter.real_position[index]) / 100 <= 2 ) + || ( direction == -1 && Shutter.real_position[index] / Shutter.close_velocity[index] <= 2)) { + Shutter.skip_relay_change = 1 ; + } else { + 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; + } + } + Shutter.target_position[index] = target_pos; + Shutter.start_position[index] = Shutter.real_position[index]; + Shutter.time[index] = 0; + Shutter.skip_relay_change = 0; + Shutter.direction[index] = direction; + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: real %d, start %d, counter %d, max_freq %d, dir %d, freq %d"),Shutter.real_position[index], Shutter.start_position[index] ,RtcSettings.pulse_counter[index],Shutter.max_pwm_frequency , Shutter.direction[index] ,Shutter.max_pwm_frequency ); + } //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]); } -void ShutterDelayForMotorStop(void) +void ShutterWaitForMotorStop(uint8_t index) { - AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Wait for Motorstop %d"), MOTOR_STOP_TIME); - delay(MOTOR_STOP_TIME); + AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Wait for Motorstop..")); + if (Shutter.mode == SHT_OFF_ON__OPEN_CLOSE) { + if (pin[GPIO_PWM1+index] < 99 && pin[GPIO_CNTR1+index] < 99 ) { + //AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Frequency change %d"), Shutter.pwm_frequency); + while (Shutter.pwm_frequency > 100) { + Shutter.pwm_frequency = tmax(Shutter.pwm_frequency-(Shutter.max_pwm_frequency/(Shutter.motordelay[index]+1)) , 0); + analogWriteFreq(Shutter.pwm_frequency); + analogWrite(pin[GPIO_PWM1+index], 50); + delay(50); + } + Shutter.pwm_frequency = 0; + analogWriteFreq(Shutter.pwm_frequency); + analogWrite(pin[GPIO_PWM1+index], 0); + Shutter.real_position[index] = ShutterCounterBasedPosition(index); + } else { + ExecuteCommandPower(Settings.shutter_startrelay[index], 0, SRC_SHUTTER); + delay(MOTOR_STOP_TIME); + } + } else { + delay(MOTOR_STOP_TIME); + } } void ShutterReportPosition(void) { uint16_t shutter_moving = 0; - for (uint32_t i = 0; i < shutters_present; i++) { + for (uint8_t i = 0; i < shutters_present; i++) { + //AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Shutter %d: Real Pos: %d"), i+1,Shutter.real_position[i]); if (Shutter.direction[i] != 0) { - char stemp1[20]; char stemp2[10]; - dtostrfd((float)Shutter.time[i] / 20, 1, stemp2); + uint8_t position = ShutterRealToPercentPosition(Shutter.real_position[i], i); + 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, MSG_SHUTTER_POS, i+1, Shutter.real_position[i], Shutter.start_position[i], Shutter.target_position[i], Shutter.direction[i], Shutter.motordelay[i],stemp2,Shutter.pwm_frequency); + Response_P(PSTR("{")); + ResponseAppend_P(JSON_SHUTTER_POS, i+1, Settings.shutter_invert[i] ? 100-position : position, Shutter.direction[i]); + ResponseJsonEnd(); + MqttPublishPrefixTopic_P(RESULT_OR_TELE, mqtt_data); } } if (rules_flag.shutter_moving > shutter_moving) { @@ -357,6 +410,11 @@ void ShutterReportPosition(void) //AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("SHT: rules_flag.shutter_moving: %d, moved %d"), rules_flag.shutter_moving, rules_flag.shutter_moved); } +int32_t ShutterCounterBasedPosition(uint8 i) +{ + return ((int32_t)RtcSettings.pulse_counter[i]*Shutter.direction[i]*2000 / Shutter.max_pwm_frequency)+Shutter.start_position[i]; +} + void ShutterRelayChanged(void) { @@ -369,49 +427,44 @@ void ShutterRelayChanged(void) power_t powerstate_local = (power >> (Settings.shutter_startrelay[i] -1)) & 3; //uint8 manual_relays_changed = ((Shutter.switched_relay >> (Settings.shutter_startrelay[i] -1)) & 3) && SRC_IGNORE != last_source && SRC_SHUTTER != last_source && SRC_PULSETIMER != last_source ; uint8 manual_relays_changed = ((Shutter.switched_relay >> (Settings.shutter_startrelay[i] -1)) & 3) && SRC_SHUTTER != last_source && SRC_PULSETIMER != last_source ; + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Shutter %d: source: %s, powerstate_local %ld, Shutter.switched_relay %d, manual change %d"), i+1, GetTextIndexed(stemp1, sizeof(stemp1), last_source, kCommandSource), powerstate_local,Shutter.switched_relay,manual_relays_changed); if (manual_relays_changed) { + //Shutter.skip_relay_change = true; if (Shutter.mode == SHT_OFF_ON__OPEN_CLOSE) { + ShutterWaitForMotorStop(i); switch (powerstate_local) { case 1: - ShutterDelayForMotorStop(); - ShutterStartInit(i, 1, Shutter.open_max[i]); + ShutterStartInit(i, 1, Shutter.open_max[i]); break; case 3: - ShutterDelayForMotorStop(); ShutterStartInit(i, -1, 0); break; default: - Shutter.direction[i] = 0; + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Shutter %d: Switch OFF motor."),i); Shutter.target_position[i] = Shutter.real_position[i]; } } else { if (Shutter.direction[i] != 0 && (!powerstate_local || (powerstate_local && Shutter.mode == SHT_PULSE_OPEN__PULSE_CLOSE))) { Shutter.target_position[i] = Shutter.real_position[i]; - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Shutter %d: Switch OFF motor. Target: %ld, source: %s, powerstate_local %ld, Shutter.switched_relay %d, manual change %d"), i, Shutter.target_position[i], GetTextIndexed(stemp1, sizeof(stemp1), last_source, kCommandSource), powerstate_local,Shutter.switched_relay,manual_relays_changed); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Shutter %d: Switch OFF motor. Target: %ld, source: %s, powerstate_local %ld, Shutter.switched_relay %d, manual change %d"), i+1, Shutter.target_position[i], GetTextIndexed(stemp1, sizeof(stemp1), last_source, kCommandSource), powerstate_local,Shutter.switched_relay,manual_relays_changed); } else { last_source = SRC_SHUTTER; // avoid switch off in the next loop if (powerstate_local == 2) { // testing on CLOSE relay, if ON // close with relay two - ShutterDelayForMotorStop(); + ShutterWaitForMotorStop(i); ShutterStartInit(i, -1, 0); } else { // opens with relay one - ShutterDelayForMotorStop(); + ShutterWaitForMotorStop(i); ShutterStartInit(i, 1, Shutter.open_max[i]); } } - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Shutter %d: Target: %ld, powerstatelocal %d"), i, Shutter.target_position[i], powerstate_local); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Shutter %d: Target: %ld, powerstatelocal %d"), i+1, Shutter.target_position[i], powerstate_local); } } } } -/////////////////////////////////////////////////////////////////////////////////// -// 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 @@ -425,16 +478,23 @@ void ShutterSetPosition(uint8_t device, uint8_t position) void CmndShutterOpen(void) { + //AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Payload close: %d, index %d"), XdrvMailbox.payload, XdrvMailbox.index); + if ( XdrvMailbox.index == 1 && XdrvMailbox.payload != -99) { + XdrvMailbox.index = XdrvMailbox.payload; + } XdrvMailbox.payload = 100; - XdrvMailbox.data_len = 3; last_source = SRC_WEBGUI; CmndShutterPosition(); } void CmndShutterClose(void) { + //AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Payload open: %d, index %d"), XdrvMailbox.payload, XdrvMailbox.index); + if ( XdrvMailbox.index == 1 && XdrvMailbox.payload != -99) { + XdrvMailbox.index = XdrvMailbox.payload; + } XdrvMailbox.payload = 0; - XdrvMailbox.data_len = 1; + XdrvMailbox.data_len = 0; last_source = SRC_WEBGUI; CmndShutterPosition(); } @@ -442,11 +502,14 @@ void CmndShutterClose(void) void CmndShutterStop(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= shutters_present)) { + if ( XdrvMailbox.index == 1 && XdrvMailbox.payload != -99) { + XdrvMailbox.index = XdrvMailbox.payload; + } 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 %d: dir: %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]; @@ -463,7 +526,7 @@ void CmndShutterPosition(void) if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= shutters_present)) { uint32_t index = XdrvMailbox.index -1; //limit the payload - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Position in: payload %s (%d), payload %d, index %d, source %d"), XdrvMailbox.data , XdrvMailbox.data_len, XdrvMailbox.payload , XdrvMailbox.index, last_source ); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Pos. in: payload %s (%d), payload %d, idx %d, src %d"), XdrvMailbox.data , XdrvMailbox.data_len, XdrvMailbox.payload , XdrvMailbox.index, last_source ); if (XdrvMailbox.data_len > 1 && XdrvMailbox.payload <=0) { UpperCase(XdrvMailbox.data, XdrvMailbox.data); @@ -472,7 +535,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; @@ -480,7 +543,7 @@ void CmndShutterPosition(void) //target_pos_percent = Settings.shutter_invert[index] ? 100 - target_pos_percent : target_pos_percent; Shutter.target_position[index] = ShutterPercentToRealPosition(target_pos_percent, index); //Shutter.target_position[index] = XdrvMailbox.payload < 5 ? Settings.shuttercoeff[2][index] * XdrvMailbox.payload : Settings.shuttercoeff[1][index] * XdrvMailbox.payload + Settings.shuttercoeff[0,index]; - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: lastsource %d:, realpos %d, target %d, payload %d"), last_source, Shutter.real_position[index] ,Shutter.target_position[index],target_pos_percent); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: lastsource %d:, real %d, target %d, payload %d"), last_source, Shutter.real_position[index] ,Shutter.target_position[index],target_pos_percent); } if ( (target_pos_percent >= 0) && (target_pos_percent <= 100) && abs(Shutter.target_position[index] - Shutter.real_position[index] ) / Shutter.close_velocity[index] > 2) { int8_t new_shutterdirection = Shutter.real_position[index] < Shutter.target_position[index] ? 1 : -1; @@ -491,24 +554,26 @@ void CmndShutterPosition(void) ExecuteCommandPower(Settings.shutter_startrelay[index] + (new_shutterdirection == 1 ? 0 : 1), 1, SRC_SHUTTER); delay(100); } else { - ExecuteCommandPower(Settings.shutter_startrelay[index] + (new_shutterdirection == 1 ? 1 : 0), 0, SRC_SHUTTER); - ShutterDelayForMotorStop(); + if (Shutter.mode == SHT_OFF_OPEN__OFF_CLOSE) { + ExecuteCommandPower(Settings.shutter_startrelay[index] + (new_shutterdirection == 1 ? 1 : 0), 0, SRC_SHUTTER); + ShutterWaitForMotorStop(index); + } } } 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); - ShutterDelayForMotorStop(); + ShutterWaitForMotorStop(index); + ExecuteCommandPower(Settings.shutter_startrelay[index], 0, SRC_SHUTTER); + ShutterStartInit(index, new_shutterdirection, Shutter.target_position[index]); // Code for shutters with circuit safe configuration, switch the direction Relay ExecuteCommandPower(Settings.shutter_startrelay[index] +1, new_shutterdirection == 1 ? 0 : 1, SRC_SHUTTER); // power on ExecuteCommandPower(Settings.shutter_startrelay[index], 1, SRC_SHUTTER); } else { // now start the motor for the right direction, work for momentary and normal shutters. - AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Start shutter in direction %d"), Shutter.direction[index]); + AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Start in dir %d"), Shutter.direction[index]); + ShutterStartInit(index, new_shutterdirection, Shutter.target_position[index]); ExecuteCommandPower(Settings.shutter_startrelay[index] + (new_shutterdirection == 1 ? 0 : 1), 1, SRC_SHUTTER); //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Delay6 5s, xdrv %d"), XdrvMailbox.payload); } @@ -556,7 +621,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); } } @@ -571,7 +636,6 @@ void CmndShutterRelay(void) } else { Shutter.mask ^= 3 << (Settings.shutter_startrelay[XdrvMailbox.index -1] - 1); } - AddLog_P2(LOG_LEVEL_INFO, PSTR("SHT: Relay %d is %d"), XdrvMailbox.index, XdrvMailbox.payload); Settings.shutter_startrelay[XdrvMailbox.index -1] = XdrvMailbox.payload; ShutterInit(); // if payload is 0 to disable the relay there must be a reboot. Otherwhise does not work @@ -586,10 +650,21 @@ void CmndShutterSetHalfway(void) if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 100)) { Settings.shutter_set50percent[XdrvMailbox.index -1] = Settings.shutter_invert[XdrvMailbox.index -1] ? 100 - XdrvMailbox.payload : XdrvMailbox.payload; ShutterInit(); - ResponseCmndIdxNumber(XdrvMailbox.payload); // ???? - } else { - ResponseCmndIdxNumber(Settings.shutter_set50percent[XdrvMailbox.index -1]); } + ResponseCmndIdxNumber(Settings.shutter_invert[XdrvMailbox.index -1] ? 100 - Settings.shutter_set50percent[XdrvMailbox.index -1] : Settings.shutter_set50percent[XdrvMailbox.index -1]); + } +} + +void CmndShutterFrequency(void) +{ + if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload <= 20000)) { + Shutter.max_pwm_frequency = XdrvMailbox.payload; + if (shutters_present < 4) { + Settings.shuttercoeff[4][4] = Shutter.max_pwm_frequency; + } + ResponseCmndNumber(XdrvMailbox.payload); // ???? + } else { + ResponseCmndNumber(Shutter.max_pwm_frequency); } } @@ -599,7 +674,7 @@ void CmndShutterSetClose(void) Shutter.real_position[XdrvMailbox.index -1] = 0; ShutterStartInit(XdrvMailbox.index -1, 0, 0); Settings.shutter_position[XdrvMailbox.index -1] = 0; - ResponseCmndChar(D_CONFIGURATION_RESET); + ResponseCmndIdxChar(D_CONFIGURATION_RESET); } } @@ -615,7 +690,7 @@ void CmndShutterInvert(void) void CmndShutterCalibration(void) // ???? { - if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_SHUTTERS)) { + if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= shutters_present)) { if (XdrvMailbox.data_len > 0) { uint32_t i = 0; char *str_ptr; @@ -658,13 +733,15 @@ bool Xdrv27(uint8_t function) ShutterUpdatePosition(); break; case FUNC_EVERY_SECOND: + //case FUNC_EVERY_250_MSECOND: ShutterReportPosition(); break; + case FUNC_COMMAND: result = DecodeCommand(kShutterCommands, ShutterCommand); break; case FUNC_JSON_APPEND: - for (uint32_t i = 0; i < shutters_present; i++) { + for (uint8_t i = 0; i < shutters_present; i++) { uint8_t position = Settings.shutter_invert[i] ? 100 - Settings.shutter_position[i]: Settings.shutter_position[i]; ResponseAppend_P(","); ResponseAppend_P(JSON_SHUTTER_POS, i+1, position, Shutter.direction[i]); @@ -678,10 +755,26 @@ bool Xdrv27(uint8_t function) case FUNC_SET_POWER: char stemp1[10]; // extract the number of the relay that was switched and save for later in Update Position. - Shutter.switched_relay = power ^ Shutter.old_power; - Shutter.old_power = power; + Shutter.switched_relay = XdrvMailbox.index ^ Shutter.old_power; AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("SHT: Switched relay: %d by %s"), Shutter.switched_relay,GetTextIndexed(stemp1, sizeof(stemp1), last_source, kCommandSource)); ShutterRelayChanged(); + Shutter.old_power = XdrvMailbox.index; + break; + case FUNC_SET_DEVICE_POWER: + if (Shutter.skip_relay_change ) { + uint8_t i; + for (i = 0; i < devices_present; i++) { + if (Shutter.switched_relay &1) { + break; + } + Shutter.switched_relay >>= 1; + } + //AddLog_P2(LOG_LEVEL_ERROR, PSTR("SHT: skip relay change: %d"),i+1); + result = true; + Shutter.skip_relay_change = 0; + AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("SHT: Skipping switch off relay %d"),i); + ExecuteCommandPower(i+1, 0, SRC_SHUTTER); + } break; } } diff --git a/tasmota/xdrv_99_debug.ino b/tasmota/xdrv_99_debug.ino index b347035e1..22bc5e162 100644 --- a/tasmota/xdrv_99_debug.ino +++ b/tasmota/xdrv_99_debug.ino @@ -25,8 +25,6 @@ #endif // USE_DEBUG_DRIVER #endif // DEBUG_THEO -//#define USE_DEBUG_SETTING_NAMES - #ifdef USE_DEBUG_DRIVER /*********************************************************************************************\ * Virtual debugging support - Part1 @@ -64,9 +62,6 @@ const char kDebugCommands[] PROGMEM = "|" // No prefix D_CMND_CFGDUMP "|" D_CMND_CFGPEEK "|" D_CMND_CFGPOKE "|" -#ifdef USE_DEBUG_SETTING_NAMES - D_CMND_CFGSHOW "|" -#endif #ifdef USE_WEBSERVER D_CMND_CFGXOR "|" #endif @@ -82,9 +77,6 @@ const char kDebugCommands[] PROGMEM = "|" // No prefix void (* const DebugCommand[])(void) PROGMEM = { &CmndCfgDump, &CmndCfgPeek, &CmndCfgPoke, -#ifdef USE_DEBUG_SETTING_NAMES - &CmndCfgShow, -#endif #ifdef USE_WEBSERVER &CmndCfgXor, #endif @@ -391,36 +383,6 @@ void DebugCfgPoke(char* parms) AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: 0x%0LX (%lu) poked to 0x%0LX (%lu)"), address, data32, data32, ndata32, ndata32); } -#ifdef USE_DEBUG_SETTING_NAMES -void DebugCfgShow(uint8_t more) -{ - uint8_t *SetAddr; - SetAddr = (uint8_t *)&Settings; - - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: Hostname (%d) [%s]"), (uint8_t *)&Settings.hostname - SetAddr, sizeof(Settings.hostname)-1, Settings.hostname); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: SSids (%d) [%s], [%s]"), (uint8_t *)&Settings.sta_ssid - SetAddr, sizeof(Settings.sta_ssid[0])-1, Settings.sta_ssid[0], Settings.sta_ssid[1]); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: Friendlynames (%d) [%s], [%s], [%s], [%s]"), (uint8_t *)&Settings.friendlyname - SetAddr, sizeof(Settings.friendlyname[0])-1, Settings.friendlyname[0], Settings.friendlyname[1], Settings.friendlyname[2], Settings.friendlyname[3]); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: OTA Url (%d) [%s]"), (uint8_t *)&Settings.ota_url - SetAddr, sizeof(Settings.ota_url)-1, Settings.ota_url); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: StateText (%d) [%s], [%s], [%s], [%s]"), (uint8_t *)&Settings.state_text - SetAddr, sizeof(Settings.state_text[0])-1, Settings.state_text[0], Settings.state_text[1], Settings.state_text[2], Settings.state_text[3]); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: Syslog Host (%d) [%s]"), (uint8_t *)&Settings.syslog_host - SetAddr, sizeof(Settings.syslog_host)-1, Settings.syslog_host); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: NTP Servers (%d) [%s], [%s], [%s]"), (uint8_t *)&Settings.ntp_server - SetAddr, sizeof(Settings.ntp_server[0])-1, Settings.ntp_server[0], Settings.ntp_server[1], Settings.ntp_server[2]); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: MQTT Host (%d) [%s]"), (uint8_t *)&Settings.mqtt_host - SetAddr, sizeof(Settings.mqtt_host)-1, Settings.mqtt_host); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: MQTT Client (%d) [%s]"), (uint8_t *)&Settings.mqtt_client - SetAddr, sizeof(Settings.mqtt_client)-1, Settings.mqtt_client); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: MQTT User (%d) [%s]"), (uint8_t *)&Settings.mqtt_user - SetAddr, sizeof(Settings.mqtt_user)-1, Settings.mqtt_user); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: MQTT FullTopic (%d) [%s]"), (uint8_t *)&Settings.mqtt_fulltopic - SetAddr, sizeof(Settings.mqtt_fulltopic)-1, Settings.mqtt_fulltopic); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: MQTT Topic (%d) [%s]"), (uint8_t *)&Settings.mqtt_topic - SetAddr, sizeof(Settings.mqtt_topic)-1, Settings.mqtt_topic); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: MQTT GroupTopic (%d) [%s]"), (uint8_t *)&Settings.mqtt_grptopic - SetAddr, sizeof(Settings.mqtt_grptopic)-1, Settings.mqtt_grptopic); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: MQTT ButtonTopic (%d) [%s]"), (uint8_t *)&Settings.button_topic - SetAddr, sizeof(Settings.button_topic)-1, Settings.button_topic); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: MQTT SwitchTopic (%d) [%s]"), (uint8_t *)&Settings.switch_topic - SetAddr, sizeof(Settings.switch_topic)-1, Settings.switch_topic); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: MQTT Prefixes (%d) [%s], [%s], [%s]"), (uint8_t *)&Settings.mqtt_prefix - SetAddr, sizeof(Settings.mqtt_prefix[0])-1, Settings.mqtt_prefix[0], Settings.mqtt_prefix[1], Settings.mqtt_prefix[2]); - if (17 == more) { - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: AP Passwords (%d) [%s], [%s]"), (uint8_t *)&Settings.sta_pwd - SetAddr, sizeof(Settings.sta_pwd[0])-1, Settings.sta_pwd[0], Settings.sta_pwd[1]); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: MQTT Password (%d) [%s]"), (uint8_t *)&Settings.mqtt_pwd - SetAddr, sizeof(Settings.mqtt_pwd)-1, Settings.mqtt_pwd); - AddLog_P2(LOG_LEVEL_INFO, PSTR("%03X: Web Password (%d) [%s]"), (uint8_t *)&Settings.web_password - SetAddr, sizeof(Settings.web_password)-1, Settings.web_password); - } -} -#endif // USE_DEBUG_SETTING_NAMES - void SetFlashMode(uint8_t mode) { uint8_t *_buffer; @@ -474,14 +436,6 @@ void CmndCfgPoke(void) ResponseCmndDone(); } -#ifdef USE_DEBUG_SETTING_NAMES -void CmndCfgShow(void) -{ - DebugCfgShow(XdrvMailbox.payload); - ResponseCmndDone(); -} -#endif // USE_DEBUG_SETTING_NAMES - #ifdef USE_WEBSERVER void CmndCfgXor(void) { diff --git a/tasmota/xnrg_02_cse7766.ino b/tasmota/xnrg_02_cse7766.ino index 339d4fee2..d52ee9ca3 100644 --- a/tasmota/xnrg_02_cse7766.ino +++ b/tasmota/xnrg_02_cse7766.ino @@ -211,7 +211,7 @@ void CseDrvInit(void) { if ((3 == pin[GPIO_CSE7766_RX]) && (1 == pin[GPIO_CSE7766_TX])) { // As it uses 8E1 currently only hardware serial is supported baudrate = 4800; - serial_config = SERIAL_8E1; + Settings.serial_config = TS_SERIAL_8E1; if (0 == Settings.param[P_CSE7766_INVALID_POWER]) { Settings.param[P_CSE7766_INVALID_POWER] = CSE_MAX_INVALID_POWER; // SetOption39 1..255 } diff --git a/tasmota/xsns_06_dht.ino b/tasmota/xsns_06_dht.ino index c4e877de9..6ef7055ac 100644 --- a/tasmota/xsns_06_dht.ino +++ b/tasmota/xsns_06_dht.ino @@ -28,7 +28,7 @@ #define XSNS_06 6 -#define DHT_MAX_SENSORS 3 +#define DHT_MAX_SENSORS 4 #define DHT_MAX_RETRY 8 uint32_t dht_max_cycles; diff --git a/tasmota/xsns_32_mpu6050.ino b/tasmota/xsns_32_mpu6050.ino index 48cd201bc..bcec100b0 100644 --- a/tasmota/xsns_32_mpu6050.ino +++ b/tasmota/xsns_32_mpu6050.ino @@ -57,6 +57,7 @@ int16_t MPU_6050_temperature = 0; VectorInt16 aaReal; // [x, y, z] gravity-free accel sensor measurements VectorFloat gravity; // [x, y, z] gravity vector float euler[3]; // [psi, theta, phi] Euler angle container + float yawPitchRoll[3]; // [yaw, pitch roll] Yaw-pitch-roll container } MPU6050_DMP; MPU6050_DMP MPU6050_dmp; @@ -68,7 +69,7 @@ MPU6050 mpu6050; void MPU_6050PerformReading(void) { #ifdef USE_MPU6050_DMP - mpu6050.resetFIFO(); // with a default dampling rate of 200Hz, we create a delay of approx. 5ms with a complete read cycle + mpu6050.resetFIFO(); // with a default sampling rate of 200Hz, we create a delay of approx. 5ms with a complete read cycle MPU6050_dmp.fifoCount = mpu6050.getFIFOCount(); while (MPU6050_dmp.fifoCount < MPU6050_dmp.packetSize) MPU6050_dmp.fifoCount = mpu6050.getFIFOCount(); mpu6050.getFIFOBytes(MPU6050_dmp.fifoBuffer, MPU6050_dmp.packetSize); @@ -79,6 +80,7 @@ void MPU_6050PerformReading(void) mpu6050.dmpGetAccel(&MPU6050_dmp.aa, MPU6050_dmp.fifoBuffer); mpu6050.dmpGetGravity(&MPU6050_dmp.gravity, &MPU6050_dmp.q); mpu6050.dmpGetLinearAccel(&MPU6050_dmp.aaReal, &MPU6050_dmp.aa, &MPU6050_dmp.gravity); + mpu6050.dmpGetYawPitchRoll(MPU6050_dmp.yawPitchRoll, &MPU6050_dmp.q, &MPU6050_dmp.gravity); MPU_6050_gx = MPU6050_dmp.euler[0] * 180/M_PI; MPU_6050_gy = MPU6050_dmp.euler[1] * 180/M_PI; MPU_6050_gz = MPU6050_dmp.euler[2] * 180/M_PI; @@ -145,6 +147,10 @@ void MPU_6050Detect(void) } } +#define D_YAW "Yaw" +#define D_PITCH "Pitch" +#define D_ROLL "Roll" + #ifdef USE_WEBSERVER const char HTTP_SNS_AXIS[] PROGMEM = "{s}" D_SENSOR_MPU6050 " " D_AX_AXIS "{m}%s{e}" // {s} = , {m} = , {e} = @@ -153,6 +159,12 @@ const char HTTP_SNS_AXIS[] PROGMEM = "{s}" D_SENSOR_MPU6050 " " D_GX_AXIS "{m}%s{e}" // {s} = , {m} = , {e} = "{s}" D_SENSOR_MPU6050 " " D_GY_AXIS "{m}%s{e}" // {s} = , {m} = , {e} = "{s}" D_SENSOR_MPU6050 " " D_GZ_AXIS "{m}%s{e}"; // {s} = , {m} = , {e} = +#ifdef USE_MPU6050_DMP +const char HTTP_SNS_YPR[] PROGMEM = + "{s}" D_SENSOR_MPU6050 " " D_YAW "{m}%s{e}" // {s} = , {m} = , {e} = + "{s}" D_SENSOR_MPU6050 " " D_PITCH "{m}%s{e}" // {s} = , {m} = , {e} = + "{s}" D_SENSOR_MPU6050 " " D_ROLL "{m}%s{e}"; // {s} = , {m} = , {e} = +#endif // USE_MPU6050_DMP #endif // USE_WEBSERVER #define D_JSON_AXIS_AX "AccelXAxis" @@ -161,6 +173,9 @@ const char HTTP_SNS_AXIS[] PROGMEM = #define D_JSON_AXIS_GX "GyroXAxis" #define D_JSON_AXIS_GY "GyroYAxis" #define D_JSON_AXIS_GZ "GyroZAxis" +#define D_JSON_YAW "Yaw" +#define D_JSON_PITCH "Pitch" +#define D_JSON_ROLL "Roll" void MPU_6050Show(bool json) { @@ -181,6 +196,14 @@ void MPU_6050Show(bool json) dtostrfd(MPU_6050_gy, Settings.flag2.axis_resolution, axis_gy); char axis_gz[33]; dtostrfd(MPU_6050_gz, Settings.flag2.axis_resolution, axis_gz); +#ifdef USE_MPU6050_DMP + char axis_yaw[33]; + dtostrfd(MPU6050_dmp.yawPitchRoll[0] / PI * 180.0, Settings.flag2.axis_resolution, axis_yaw); + char axis_pitch[33]; + dtostrfd(MPU6050_dmp.yawPitchRoll[1] / PI * 180.0, Settings.flag2.axis_resolution, axis_pitch); + char axis_roll[33]; + dtostrfd(MPU6050_dmp.yawPitchRoll[2] / PI * 180.0, Settings.flag2.axis_resolution, axis_roll); +#endif // USE_MPU6050_DMP if (json) { char json_axis_ax[25]; @@ -195,8 +218,20 @@ void MPU_6050Show(bool json) snprintf_P(json_axis_gy, sizeof(json_axis_gy), PSTR(",\"" D_JSON_AXIS_GY "\":%s"), axis_gy); char json_axis_gz[25]; snprintf_P(json_axis_gz, sizeof(json_axis_gz), PSTR(",\"" D_JSON_AXIS_GZ "\":%s"), axis_gz); +#ifdef USE_MPU6050_DMP + char json_ypr_y[25]; + snprintf_P(json_ypr_y, sizeof(json_ypr_y), PSTR(",\"" D_JSON_YAW "\":%s"), axis_yaw); + char json_ypr_p[25]; + snprintf_P(json_ypr_p, sizeof(json_ypr_p), PSTR(",\"" D_JSON_PITCH "\":%s"), axis_pitch); + char json_ypr_r[25]; + snprintf_P(json_ypr_r, sizeof(json_ypr_r), PSTR(",\"" D_JSON_ROLL "\":%s"), axis_roll); + ResponseAppend_P(PSTR(",\"%s\":{\"" D_JSON_TEMPERATURE "\":%s%s%s%s%s%s%s%s%s%s}"), + D_SENSOR_MPU6050, temperature, json_axis_ax, json_axis_ay, json_axis_az, json_axis_gx, json_axis_gy, json_axis_gz, + json_ypr_y, json_ypr_p, json_ypr_r); +#else ResponseAppend_P(PSTR(",\"%s\":{\"" D_JSON_TEMPERATURE "\":%s%s%s%s%s%s%s}"), D_SENSOR_MPU6050, temperature, json_axis_ax, json_axis_ay, json_axis_az, json_axis_gx, json_axis_gy, json_axis_gz); +#endif // USE_MPU6050_DMP #ifdef USE_DOMOTICZ DomoticzSensor(DZ_TEMP, temperature); #endif // USE_DOMOTICZ @@ -204,6 +239,9 @@ void MPU_6050Show(bool json) } else { WSContentSend_PD(HTTP_SNS_TEMP, D_SENSOR_MPU6050, temperature, TempUnit()); WSContentSend_PD(HTTP_SNS_AXIS, axis_ax, axis_ay, axis_az, axis_gx, axis_gy, axis_gz); +#ifdef USE_MPU6050_DMP + WSContentSend_PD(HTTP_SNS_YPR, axis_yaw, axis_pitch, axis_roll); +#endif // USE_MPU6050_DMP #endif // USE_WEBSERVER } } diff --git a/tasmota/xsns_34_hx711.ino b/tasmota/xsns_34_hx711.ino index d9076f61d..d64712a86 100644 --- a/tasmota/xsns_34_hx711.ino +++ b/tasmota/xsns_34_hx711.ino @@ -57,6 +57,8 @@ #define D_JSON_WEIGHT_MAX "WeightMax" #define D_JSON_WEIGHT_ITEM "WeightItem" #define D_JSON_WEIGHT_CHANGE "WeightChange" +#define D_JSON_WEIGHT_RAW "WeightRaw" +#define D_JSON_WEIGHT_DELTA "WeightDelta" enum HxCalibrationSteps { HX_CAL_END, HX_CAL_LIMBO, HX_CAL_FINISH, HX_CAL_FAIL, HX_CAL_DONE, HX_CAL_FIRST, HX_CAL_RESET, HX_CAL_START }; @@ -64,8 +66,10 @@ const char kHxCalibrationStates[] PROGMEM = D_HX_CAL_FAIL "|" D_HX_CAL_DONE "|" struct HX { long weight = 0; + long raw = 0; long last_weight = 0; long sum_weight = 0; + long sum_raw = 0; long offset = 0; long scale = 1; long weight_diff = 0; @@ -78,6 +82,7 @@ struct HX { uint8_t pin_dout; bool tare_flg = false; bool weight_changed = false; + uint16_t weight_delta = 4; } Hx; /*********************************************************************************************/ @@ -146,6 +151,24 @@ void HxCalibrationStateTextJson(uint8_t msg_id) if (msg_id < 3) { MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR("Sensor34")); } } +void SetWeightDelta() +{ + // backwards compatible: restore old default value of 4 grams + if (Settings.weight_change == 0) { + Hx.weight_delta = 4; + return; + } + + // map upper values 101-255 to + if (Settings.weight_change > 100) { + Hx.weight_delta = (Settings.weight_change - 100) * 10 + 100; + return; + } + + // map 1..100 to 0..99 grams + Hx.weight_delta = Settings.weight_change - 1; +} + /*********************************************************************************************\ * Supported commands for Sensor34: * @@ -163,6 +186,7 @@ void HxCalibrationStateTextJson(uint8_t msg_id) * Sensor34 7 - Save current weight to be used as start weight on restart * Sensor34 8 0 - Disable JSON weight change message * Sensor34 8 1 - Enable JSON weight change message + * Sensor34 9 - Set minimum delta to trigger JSON message \*********************************************************************************************/ bool HxCommand(void) @@ -225,6 +249,13 @@ bool HxCommand(void) } show_parms = true; break; + case 9: // WeightDelta + if (strstr(XdrvMailbox.data, ",") != nullptr) { + Settings.weight_change = strtol(subStr(sub_string, XdrvMailbox.data, ",", 2), nullptr, 10); + SetWeightDelta(); + } + show_parms = true; + break; default: show_parms = true; } @@ -232,8 +263,10 @@ bool HxCommand(void) if (show_parms) { char item[33]; dtostrfd((float)Settings.weight_item / 10, 1, item); - Response_P(PSTR("{\"Sensor34\":{\"" D_JSON_WEIGHT_REF "\":%d,\"" D_JSON_WEIGHT_CAL "\":%d,\"" D_JSON_WEIGHT_MAX "\":%d,\"" D_JSON_WEIGHT_ITEM "\":%s,\"" D_JSON_WEIGHT_CHANGE "\":\"%s\"}}"), - Settings.weight_reference, Settings.weight_calibration, Settings.weight_max * 1000, item, GetStateText(Settings.SensorBits1.hx711_json_weight_change)); + Response_P(PSTR("{\"Sensor34\":{\"" D_JSON_WEIGHT_REF "\":%d,\"" D_JSON_WEIGHT_CAL "\":%d,\"" D_JSON_WEIGHT_MAX "\":%d,\"" + D_JSON_WEIGHT_ITEM "\":%s,\"" D_JSON_WEIGHT_CHANGE "\":%s,\"" D_JSON_WEIGHT_DELTA "\":%d}}"), + Settings.weight_reference, Settings.weight_calibration, Settings.weight_max * 1000, + item, GetStateText(Settings.SensorBits1.hx711_json_weight_change), Settings.weight_change); } return serviced; @@ -258,6 +291,8 @@ void HxInit(void) digitalWrite(Hx.pin_sck, LOW); + SetWeightDelta(); + if (HxIsReady(8 * HX_TIMEOUT)) { // Can take 600 milliseconds after power on if (!Settings.weight_max) { Settings.weight_max = HX_MAX_WEIGHT / 1000; } if (!Settings.weight_calibration) { Settings.weight_calibration = HX_SCALE; } @@ -272,13 +307,17 @@ void HxInit(void) void HxEvery100mSecond(void) { - Hx.sum_weight += HxRead(); + long raw = HxRead(); + Hx.sum_raw += raw; + Hx.sum_weight += raw; Hx.sample_count++; if (HX_SAMPLES == Hx.sample_count) { long average = Hx.sum_weight / Hx.sample_count; // grams + long raw_average = Hx.sum_raw / Hx.sample_count; // grams long value = average - Hx.offset; // grams Hx.weight = value / Hx.scale; // grams + Hx.raw = raw_average / Hx.scale; if (Hx.weight < 0) { if (Settings.energy_frequency_calibration) { long difference = Settings.energy_frequency_calibration + Hx.weight; @@ -351,7 +390,7 @@ void HxEvery100mSecond(void) Hx.weight += Hx.last_weight; // grams if (Settings.SensorBits1.hx711_json_weight_change) { - if (abs(Hx.weight - Hx.weight_diff) > 4) { // Use 4 gram threshold to decrease "ghost" weights + if (abs(Hx.weight - Hx.weight_diff) > Hx.weight_delta) { // Use weight_delta threshold to decrease "ghost" weights Hx.weight_diff = Hx.weight; Hx.weight_changed = true; } @@ -367,6 +406,7 @@ void HxEvery100mSecond(void) } Hx.sum_weight = 0; + Hx.sum_raw = 0; Hx.sample_count = 0; } } @@ -405,7 +445,7 @@ void HxShow(bool json) dtostrfd(weight, Settings.flag2.weight_resolution, weight_chr); if (json) { - ResponseAppend_P(PSTR(",\"HX711\":{\"" D_JSON_WEIGHT "\":%s%s}"), weight_chr, scount); + ResponseAppend_P(PSTR(",\"HX711\":{\"" D_JSON_WEIGHT "\":%s%s, \"" D_JSON_WEIGHT_RAW "\":%d}"), weight_chr, scount, Hx.raw); #ifdef USE_WEBSERVER } else { WSContentSend_PD(HTTP_HX711_WEIGHT, weight_chr); @@ -565,4 +605,4 @@ bool Xsns34(uint8_t function) return result; } -#endif // USE_HX711 \ No newline at end of file +#endif // USE_HX711 diff --git a/tasmota/xsns_53_sml.ino b/tasmota/xsns_53_sml.ino index ddc3f8db0..7aa0fd22d 100755 --- a/tasmota/xsns_53_sml.ino +++ b/tasmota/xsns_53_sml.ino @@ -499,7 +499,10 @@ const uint8_t meter[]= #define USE_SML_MEDIAN_FILTER // max number of vars , may be adjusted +#ifndef MAX_VARS #define MAX_VARS 20 +#endif + // max number of meters , may be adjusted #define MAX_METERS 5 double meter_vars[MAX_VARS]; @@ -1427,14 +1430,25 @@ void SML_Decode(uint8_t index) { //ignore mp+=2; cp++; - } else if (!strncmp(mp,"uuuuuuuu",8)) { + } else if (!strncmp(mp,"UUuuUUuu",8)) { uint32_t val= (cp[0]<<24)|(cp[1]<<16)|(cp[2]<<8)|(cp[3]<<0); ebus_dval=val; mbus_dval=val; mp+=8; cp+=4; - } - else if (*mp=='u' && *(mp+1)=='u' && *(mp+2)=='u' && *(mp+3)=='u'){ + } else if (*mp=='U' && *(mp+1)=='U' && *(mp+2)=='u' && *(mp+3)=='u'){ + uint16_t val = cp[1]|(cp[0]<<8); + mbus_dval=val; + ebus_dval=val; + mp+=4; + cp+=2; + } else if (!strncmp(mp,"SSssSSss",8)) { + int32_t val= (cp[0]<<24)|(cp[1]<<16)|(cp[2]<<8)|(cp[3]<<0); + ebus_dval=val; + mbus_dval=val; + mp+=8; + cp+=4; + } else if (*mp=='u' && *(mp+1)=='u' && *(mp+2)=='U' && *(mp+3)=='U'){ uint16_t val = cp[0]|(cp[1]<<8); mbus_dval=val; ebus_dval=val; @@ -1442,17 +1456,25 @@ void SML_Decode(uint8_t index) { cp+=2; } else if (*mp=='u' && *(mp+1)=='u') { uint8_t val = *cp++; + mbus_dval=val; ebus_dval=val; mp+=2; - } - else if (*mp=='s' && *(mp+1)=='s' && *(mp+2)=='s' && *(mp+3)=='s') { + } else if (*mp=='s' && *(mp+1)=='s' && *(mp+2)=='S' && *(mp+3)=='S') { int16_t val = *cp|(*(cp+1)<<8); + mbus_dval=val; + ebus_dval=val; + mp+=4; + cp+=2; + } else if (*mp=='S' && *(mp+1)=='S' && *(mp+2)=='s' && *(mp+3)=='s') { + int16_t val = cp[1]|(cp[0]<<8); + mbus_dval=val; ebus_dval=val; mp+=4; cp+=2; } else if (*mp=='s' && *(mp+1)=='s') { int8_t val = *cp++; + mbus_dval=val; ebus_dval=val; mp+=2; } 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)