diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4520a3599..88ed03260 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
### Changed
- ESP32 Platform from 2025.05.30 to 2025.07.30, Framework (Arduino Core) from v3.1.3.250504 to v3.1.3.250707 and IDF from v5.3.3.250501 to v5.3.3.250707 (#23642)
+- ESP32 Domoticz supports persistent settings for all relays, keys and switches using filesystem
### Fixed
diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 835a5f237..05b2c6035 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -131,6 +131,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm
- Library names [#23560](https://github.com/arendst/Tasmota/issues/23560)
- CSS uses named colors variables [#23597](https://github.com/arendst/Tasmota/issues/23597)
- VEML6070 and AHT2x device detection [#23581](https://github.com/arendst/Tasmota/issues/23581)
+- ESP32 Domoticz supports persistent settings for all relays, keys and switches using filesystem
- ESP32 LoRaWan decoding won't duplicate non-decoded message if `SO147 0`
- BLE updates for esp-nimble-cpp v2.x [#23553](https://github.com/arendst/Tasmota/issues/23553)
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_07_domoticz.ino b/tasmota/tasmota_xdrv_driver/xdrv_07_domoticz.ino
index 31211fe58..1f6ec6771 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_07_domoticz.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_07_domoticz.ino
@@ -17,6 +17,7 @@
along with this program. If not, see .
*/
+#ifdef ESP8266
#ifdef USE_DOMOTICZ
/*********************************************************************************************\
* Domoticz support
@@ -770,3 +771,4 @@ bool Xdrv07(uint32_t function) {
}
#endif // USE_DOMOTICZ
+#endif // ESP8266
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_07_esp32_domoticz.ino b/tasmota/tasmota_xdrv_driver/xdrv_07_esp32_domoticz.ino
new file mode 100644
index 000000000..d89fe6d56
--- /dev/null
+++ b/tasmota/tasmota_xdrv_driver/xdrv_07_esp32_domoticz.ino
@@ -0,0 +1,869 @@
+/*
+ xdrv_07_esp32_domoticz.ino - domoticz support for Tasmota
+
+ Copyright (C) 2025 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 .
+*/
+
+#ifdef ESP32
+#ifdef USE_DOMOTICZ
+/*********************************************************************************************\
+ * Domoticz support with all relays/buttons/switches using more RAM and Settings from filesystem
+ *
+ * Adds commands:
+ * DzIdx - Set power number to Domoticz Idx allowing Domoticz to control Tasmota power
+ * DzKeyIdx - Set button number to Domoticz Idx allowing Tasmota button as input to Domoticz
+ * DzSwitchId - Set switch number to Domoticz Idx allowing Tasmota switch as input to Domoticz
+ * DzSensorIdx - Set sensor type to Domoticz Idx
+ * DzUpdateTimer 0 - Send power state at teleperiod to Domoticz (default)
+ * DzUpdateTimer - Send power state at interval to Domoticz
+ * DzSend1 , - {\"idx\":,\"nvalue\":0,\"svalue\":\"\",\"Battery\":xx,\"RSSI\":yy}
+ * Example: rule1 on power1#state do dzsend1 9001,%value% endon
+ * DzSend1 418,%var1%;%var2% or DzSend1 418,%var1%:%var2% - Notice colon as substitute to semi-colon
+ * DzSend2 , - USE_SHUTTER only - {\"idx\":,\"nvalue\":,\"svalue\":\"\",\"Battery\":xx,\"RSSI\":yy}
+ * DzSend3 , - {\"idx\":,\"nvalue\":,\"Battery\":xx,\"RSSI\":yy}
+ * DzSend4 , - {\"command\":\"switchlight\",\"idx\":,\"switchcmd\":\"\"}
+ * DzSend5 , - {\"command\":\"switchscene\",\"idx\":,\"switchcmd\":\"\"}
+\*********************************************************************************************/
+
+#define XDRV_07 7
+
+//#define USE_DOMOTICZ_DEBUG // Enable additional debug logging
+
+#define D_PRFX_DOMOTICZ "Dz"
+#define D_CMND_IDX "Idx"
+#define D_CMND_KEYIDX "KeyIdx"
+#define D_CMND_SWITCHIDX "SwitchIdx"
+#define D_CMND_SENSORIDX "SensorIdx"
+#define D_CMND_UPDATETIMER "UpdateTimer"
+#define D_CMND_DZSEND "Send"
+
+const char kDomoticzCommands[] PROGMEM = D_PRFX_DOMOTICZ "|" // Prefix
+ D_CMND_IDX "|" D_CMND_KEYIDX "|" D_CMND_SWITCHIDX "|" D_CMND_SENSORIDX "|" D_CMND_UPDATETIMER "|" D_CMND_DZSEND ;
+
+void (* const DomoticzCommand[])(void) PROGMEM = {
+ &CmndDomoticzIdx, &CmndDomoticzKeyIdx, &CmndDomoticzSwitchIdx, &CmndDomoticzSensorIdx, &CmndDomoticzUpdateTimer, &CmndDomoticzSend };
+
+const char DOMOTICZ_MESSAGE[] PROGMEM = "{\"idx\":%d,\"nvalue\":%d,\"svalue\":\"%s\",\"Battery\":%d,\"RSSI\":%d}";
+
+const char kDomoticzSensors[] PROGMEM = // Relates to enum DomoticzSensors
+ D_DOMOTICZ_TEMP "|" D_DOMOTICZ_TEMP_HUM "|" D_DOMOTICZ_TEMP_HUM_BARO "|" D_DOMOTICZ_POWER_ENERGY "|" D_DOMOTICZ_ILLUMINANCE "|"
+ D_DOMOTICZ_COUNT "|" D_DOMOTICZ_VOLTAGE "|" D_DOMOTICZ_CURRENT "|" D_DOMOTICZ_AIRQUALITY "|" D_DOMOTICZ_P1_SMART_METER "|" D_DOMOTICZ_SHUTTER ;
+
+const char kDomoticzCommand[] PROGMEM = "switchlight|switchscene";
+
+char domoticz_in_topic[] = DOMOTICZ_IN_TOPIC;
+
+typedef struct DzSettings_t {
+ uint32_t crc32; // To detect file changes
+ uint32_t update_timer;
+ uint32_t relay_idx[MAX_RELAYS_SET];
+ uint32_t key_idx[MAX_RELAYS_SET]; // = MAX_KEYS_SET
+ uint32_t switch_idx[MAX_RELAYS_SET]; // = MAX_SWITCHES_SET
+ uint32_t sensor_idx[DZ_MAX_SENSORS];
+} DzSettings_t;
+
+typedef struct Domoticz_t {
+ DzSettings_t Settings; // Persistent settings
+ int update_timer;
+ bool subscribe;
+ bool update_flag;
+#ifdef USE_SHUTTER
+ bool is_shutter;
+#endif // USE_SHUTTER
+} Domoticz_t;
+Domoticz_t* Domoticz;
+
+/*********************************************************************************************\
+ * Driver Settings load and save
+\*********************************************************************************************/
+
+#ifdef USE_UFILESYS
+#define XDRV_07_KEY "drvset03"
+
+bool DomoticzLoadData(void) {
+ char key[] = XDRV_07_KEY;
+ String json = UfsJsonSettingsRead(key);
+ if (json.length() == 0) { return false; }
+
+ // {"Crc":1882268982,"Update":0,"Relay":[1,2,3,4],"Key":[5,6,7,8],"Switch":[9,10,11,12],"Sensor":[13,14]}
+ JsonParser parser((char*)json.c_str());
+ JsonParserObject root = parser.getRootObject();
+ if (!root) { return false; }
+
+ Domoticz->Settings.crc32 = root.getUInt(PSTR("Crc"), Domoticz->Settings.crc32);
+ Domoticz->Settings.update_timer = root.getUInt(PSTR("Update"), Domoticz->Settings.update_timer);
+ JsonParserArray arr = root[PSTR("Relay")];
+ if (arr) {
+ for (uint32_t i = 0; i < MAX_RELAYS_SET; i++) {
+ if (arr[i]) { Domoticz->Settings.relay_idx[i] = arr[i].getUInt(); }
+ }
+ }
+ arr = root[PSTR("Key")];
+ if (arr) {
+ for (uint32_t i = 0; i < MAX_RELAYS_SET; i++) {
+ if (arr[i]) { Domoticz->Settings.key_idx[i] = arr[i].getUInt(); }
+ }
+ }
+ arr = root[PSTR("Switch")];
+ if (arr) {
+ for (uint32_t i = 0; i < MAX_RELAYS_SET; i++) {
+ if (arr[i]) { Domoticz->Settings.switch_idx[i] = arr[i].getUInt(); }
+ }
+ }
+ arr = root[PSTR("Sensor")];
+ if (arr) {
+ for (uint32_t i = 0; i < DZ_MAX_SENSORS; i++) {
+ if (arr[i]) { Domoticz->Settings.sensor_idx[i] = arr[i].getUInt(); }
+ }
+ }
+ return true;
+}
+
+bool DomoticzSaveData(void) {
+ Response_P(PSTR("{\"" XDRV_07_KEY "\":{"
+ "\"Crc\":%u,"
+ "\"Update\":%u"),
+ Domoticz->Settings.crc32,
+ Domoticz->Settings.update_timer);
+ ResponseAppend_P(PSTR(",\"Relay\":"));
+ for (uint32_t i = 0; i < MAX_RELAYS_SET; i++) {
+ ResponseAppend_P(PSTR("%c%d"), (0==i)?'[':',', Domoticz->Settings.relay_idx[i]);
+ }
+ ResponseAppend_P(PSTR("],\"Key\":"));
+ for (uint32_t i = 0; i < MAX_RELAYS_SET; i++) {
+ ResponseAppend_P(PSTR("%c%d"), (0==i)?'[':',', Domoticz->Settings.key_idx[i]);
+ }
+ ResponseAppend_P(PSTR("],\"Switch\":"));
+ for (uint32_t i = 0; i < MAX_RELAYS_SET; i++) {
+ ResponseAppend_P(PSTR("%c%d"), (0==i)?'[':',', Domoticz->Settings.switch_idx[i]);
+ }
+ ResponseAppend_P(PSTR("],\"Sensor\":"));
+ for (uint32_t i = 0; i < DZ_MAX_SENSORS; i++) {
+ ResponseAppend_P(PSTR("%c%d"), (0==i)?'[':',', Domoticz->Settings.sensor_idx[i]);
+ }
+ ResponseAppend_P(PSTR("]}}"));
+
+ if (!UfsJsonSettingsWrite(ResponseData())) {
+ return false;
+ }
+ return true;
+}
+
+void DomoticzDeleteData(void) {
+ char key[] = XDRV_07_KEY;
+ UfsJsonSettingsDelete(key); // Use defaults
+}
+#endif // USE_UFILESYS
+
+/*********************************************************************************************/
+
+void DomoticzSettingsLoad(bool erase) {
+ // Called from FUNC_PRE_INIT (erase = 0) once at restart
+ // Called from FUNC_RESET_SETTINGS (erase = 1) after command reset 4, 5, or 6
+ memset(&Domoticz->Settings, 0x00, sizeof(DzSettings_t));
+
+#ifndef CONFIG_IDF_TARGET_ESP32P4
+ // Init any other parameter in struct DzSettings
+ Domoticz->Settings.update_timer = Settings->domoticz_update_timer;
+ for (uint32_t i = 0; i < MAX_DOMOTICZ_IDX; i++) {
+ Domoticz->Settings.relay_idx[i] = Settings->domoticz_relay_idx[i];
+ Domoticz->Settings.key_idx[i] = Settings->domoticz_key_idx[i];
+ Domoticz->Settings.switch_idx[i] = Settings->domoticz_switch_idx[i];
+ }
+ uint32_t max_sns_idx = MAX_DOMOTICZ_SNS_IDX;
+ if (max_sns_idx > DZ_MAX_SENSORS) {
+ max_sns_idx = DZ_MAX_SENSORS;
+ }
+ for (uint32_t i = 0; i < max_sns_idx; i++) {
+ Domoticz->Settings.sensor_idx[i] = Settings->domoticz_sensor_idx[i];
+ }
+ // *** End Init default values ***
+#endif // CONFIG_IDF_TARGET_ESP32P4
+
+#ifndef USE_UFILESYS
+ AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: Domoticz use defaults as file system not enabled"));
+#else
+ // Try to load key
+ if (erase) {
+ DomoticzDeleteData();
+ }
+ else if (DomoticzLoadData()) {
+ AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: Domoticz loaded from file"));
+ }
+ else {
+ // File system not ready: No flash space reserved for file system
+ AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("CFG: Domoticz use defaults as file system not ready or key not found"));
+ }
+#endif // USE_UFILESYS
+}
+
+void DomoticzSettingsSave(void) {
+ // Called from FUNC_SAVE_SETTINGS every SaveData second and at restart
+#ifdef USE_UFILESYS
+ uint32_t crc32 = GetCfgCrc32((uint8_t*)&Domoticz->Settings +4, sizeof(DzSettings_t) -4); // Skip crc32
+ if (crc32 != Domoticz->Settings.crc32) {
+ Domoticz->Settings.crc32 = crc32;
+ if (DomoticzSaveData()) {
+ AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: Domoticz saved to file"));
+ } else {
+ // File system not ready: No flash space reserved for file system
+ AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("CFG: Domoticz ERROR File system not ready or unable to save file"));
+ }
+ }
+#endif // USE_UFILESYS
+}
+
+/*********************************************************************************************/
+
+int DomoticzBatteryQuality(void) {
+ // Battery 0%: ESP 2.6V (minimum operating voltage is 2.5)
+ // Battery 100%: ESP 3.6V (maximum operating voltage is 3.6)
+ // Battery 101% to 200%: ESP over 3.6V (means over maximum operating voltage)
+
+ int quality = 100; // Voltage range from 2,6V > 0% to 3,6V > 100%
+
+#ifdef ESP8266
+#ifdef USE_ADC_VCC
+ uint16_t voltage = ESP.getVcc();
+ if (voltage <= 2600) {
+ quality = 0;
+ } else if (voltage >= 4600) {
+ quality = 200;
+ } else {
+ quality = (voltage - 2600) / 10;
+ }
+#endif // USE_ADC_VCC
+#endif // ESP8266
+ return quality;
+}
+
+int DomoticzRssiQuality(void) {
+ // RSSI range: 0% to 10% (12 means disable RSSI in Domoticz)
+
+ return WifiGetRssiAsQuality(WiFi.RSSI()) / 10;
+}
+
+uint32_t DomoticzRelayIdx(uint32_t relay) {
+ if (relay >= MAX_RELAYS_SET) { return 0; }
+ return Domoticz->Settings.relay_idx[relay];
+}
+
+void DomoticzSetRelayIdx(uint32_t relay, uint32_t idx) {
+ if (relay >= MAX_RELAYS_SET) { return; }
+ Domoticz->Settings.relay_idx[relay] = idx;
+}
+
+/*********************************************************************************************/
+
+void MqttPublishDomoticzPowerState(uint8_t device) {
+ if (Settings->flag.mqtt_enabled) { // SetOption3 - Enable MQTT
+ if (device < 1) { device = 1; }
+ if ((device > TasmotaGlobal.devices_present) || (device > MAX_RELAYS_SET)) { return; }
+ if (DomoticzRelayIdx(device -1)) {
+#ifdef USE_SHUTTER
+ if (Domoticz->is_shutter) {
+ // Shutter is updated by sensor update - power state should not be sent
+ } else {
+#endif // USE_SHUTTER
+ char svalue[8]; // Dimmer value
+
+ snprintf_P(svalue, sizeof(svalue), PSTR("%d"), Settings->light_dimmer);
+ Response_P(DOMOTICZ_MESSAGE, (int)DomoticzRelayIdx(device -1), (TasmotaGlobal.power & (1 << (device -1))) ? 1 : 0, (TasmotaGlobal.light_type) ? svalue : "", DomoticzBatteryQuality(), DomoticzRssiQuality());
+ MqttPublish(domoticz_in_topic);
+#ifdef USE_SHUTTER
+ }
+#endif //USE_SHUTTER
+ }
+ }
+}
+
+void DomoticzUpdatePowerState(uint8_t device) {
+ if (Domoticz) {
+ if (Domoticz->update_flag) {
+ MqttPublishDomoticzPowerState(device);
+ }
+ Domoticz->update_flag = true;
+ }
+}
+
+/*********************************************************************************************/
+
+void DomoticzMqttUpdate(void) {
+ if (Domoticz->subscribe && (Domoticz->Settings.update_timer || Domoticz->update_timer)) {
+ Domoticz->update_timer--;
+ if (Domoticz->update_timer <= 0) {
+ Domoticz->update_timer = Domoticz->Settings.update_timer;
+ for (uint32_t i = 1; i <= TasmotaGlobal.devices_present; i++) {
+#ifdef USE_SHUTTER
+ if (Domoticz->is_shutter) {
+ // no power state updates for shutters
+ break;
+ }
+#endif // USE_SHUTTER
+ MqttPublishDomoticzPowerState(i);
+ }
+ }
+ }
+}
+
+void DomoticzMqttSubscribe(void) {
+ uint8_t maxdev = (TasmotaGlobal.devices_present > MAX_RELAYS_SET) ? MAX_RELAYS_SET : TasmotaGlobal.devices_present;
+ bool any_relay = false;
+ for (uint32_t i = 0; i < maxdev; i++) {
+ if (DomoticzRelayIdx(i)) {
+ any_relay = true;
+ break;
+ }
+ }
+ char stopic[TOPSZ];
+ snprintf_P(stopic, sizeof(stopic), PSTR(DOMOTICZ_OUT_TOPIC "/#")); // domoticz topic
+ if (Domoticz->subscribe && !any_relay) {
+ Domoticz->subscribe = false;
+ MqttUnsubscribe(stopic);
+ }
+// if (!Domoticz->subscribe && any_relay) { // Fails on MQTT server reconnect
+ if (any_relay) {
+ Domoticz->subscribe = true;
+ MqttSubscribe(stopic);
+ }
+}
+
+int DomoticzIdx2Relay(uint32_t idx) {
+ if (idx > 0) {
+ uint32_t maxdev = (TasmotaGlobal.devices_present > MAX_RELAYS_SET) ? MAX_RELAYS_SET : TasmotaGlobal.devices_present;
+ for (uint32_t i = 0; i < maxdev; i++) {
+ if (idx == DomoticzRelayIdx(i)) {
+ return i;
+ }
+ }
+ }
+ return -1; // Idx not mine
+}
+
+bool DomoticzMqttData(void) {
+/*
+ XdrvMailbox.topic = topic;
+ XdrvMailbox.index = strlen(topic);
+ XdrvMailbox.data = (char*)data;
+ XdrvMailbox.data_len = data_len;
+*/
+ Domoticz->update_flag = true;
+
+ if (!Domoticz->subscribe) {
+ return false; // No Domoticz driver subscription so try user subscribes
+ }
+
+ // Default subscibed to domoticz/out/#
+ if (strncasecmp_P(XdrvMailbox.topic, PSTR(DOMOTICZ_OUT_TOPIC), strlen(DOMOTICZ_OUT_TOPIC)) != 0) {
+ return false; // Process unchanged data
+ }
+
+ // topic is domoticz/out so check if valid data could be available
+ if (XdrvMailbox.data_len < 20) {
+ return true; // No valid data
+ }
+
+#ifdef USE_DOMOTICZ_DEBUG
+ char dom_data[XdrvMailbox.data_len +1];
+ strcpy(dom_data, XdrvMailbox.data);
+ AddLog(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_DOMOTICZ "%s = %s"), XdrvMailbox.topic, RemoveControlCharacter(dom_data));
+#endif // USE_DOMOTICZ_DEBUG
+
+ // Quick check if this is mine using topic domoticz/out/{$idx}
+ if (strlen(XdrvMailbox.topic) > strlen(DOMOTICZ_OUT_TOPIC)) {
+ char* topic_index = &XdrvMailbox.topic[strlen(DOMOTICZ_OUT_TOPIC) +1];
+ if (strchr(topic_index, '/') == nullptr) { // Skip if topic ...floor/room
+ if (DomoticzIdx2Relay(atoi(topic_index)) < 0) {
+ return true; // Idx not mine
+ }
+ }
+ }
+
+ String domoticz_data = XdrvMailbox.data; // Copy the string into a new buffer that will be modified
+ JsonParser parser((char*)domoticz_data.c_str());
+ JsonParserObject domoticz = parser.getRootObject();
+ if (!domoticz) {
+ return true; // To much or invalid data
+ }
+ int32_t relay_index = DomoticzIdx2Relay(domoticz.getUInt(PSTR("idx"), 0));
+ if (relay_index < 0) {
+ return true; // Idx not mine
+ }
+ int32_t nvalue = domoticz.getInt(PSTR("nvalue"), -1);
+ if ((nvalue < 0) || (nvalue > 16)) {
+ return true; // Nvalue out of boundaries
+ }
+
+ AddLog(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_DOMOTICZ "%s, idx %d, nvalue %d"), XdrvMailbox.topic, DomoticzRelayIdx(relay_index), nvalue);
+
+ bool iscolordimmer = (strcmp_P(domoticz.getStr(PSTR("dtype")), PSTR("Color Switch")) == 0);
+ bool isShutter = (strcmp_P(domoticz.getStr(PSTR("dtype")), PSTR("Light/Switch")) == 0) && (strncmp_P(domoticz.getStr(PSTR("switchType")),PSTR("Blinds"), 6) == 0);
+
+#ifdef USE_SHUTTER
+ if (isShutter) {
+ uint32_t position = domoticz.getUInt(PSTR("svalue1"), 0);
+ if (nvalue != 2) {
+ position = (0 == nvalue) ? 0 : 100;
+ }
+ snprintf_P(XdrvMailbox.topic, TOPSZ, PSTR("/" D_PRFX_SHUTTER D_CMND_SHUTTER_POSITION));
+ snprintf_P(XdrvMailbox.data, XdrvMailbox.data_len, PSTR("%d"), position);
+ XdrvMailbox.data_len = position > 99 ? 3 : (position > 9 ? 2 : 1);
+ } else
+#endif // USE_SHUTTER
+#ifdef USE_LIGHT
+ if (iscolordimmer && 10 == nvalue) { // Color_SetColor
+ // https://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s#Set_a_light_to_a_certain_color_or_color_temperature
+ JsonParserObject color = domoticz[PSTR("Color")].getObject();
+ // JsonObject& color = domoticz["Color"];
+ uint32_t level = nvalue = domoticz.getUInt(PSTR("svalue1"), 0);
+ uint32_t r = color.getUInt(PSTR("r"), 0) * level / 100;
+ uint32_t g = color.getUInt(PSTR("g"), 0) * level / 100;
+ uint32_t b = color.getUInt(PSTR("b"), 0) * level / 100;
+ uint32_t cw = color.getUInt(PSTR("cw"), 0) * level / 100;
+ uint32_t ww = color.getUInt(PSTR("ww"), 0) * level / 100;
+ uint32_t m = color.getUInt(PSTR("m"), 0);
+ uint32_t t = color.getUInt(PSTR("t"), 0);
+ if (2 == m) { // White with color temperature. Valid fields: t
+ snprintf_P(XdrvMailbox.topic, XdrvMailbox.index, PSTR("/" D_CMND_BACKLOG));
+ snprintf_P(XdrvMailbox.data, XdrvMailbox.data_len, PSTR(D_CMND_COLORTEMPERATURE " %d;" D_CMND_DIMMER " %d"), changeUIntScale(t, 0, 255, CT_MIN, CT_MAX), level);
+ } else {
+ snprintf_P(XdrvMailbox.topic, XdrvMailbox.index, PSTR("/" D_CMND_COLOR));
+ snprintf_P(XdrvMailbox.data, XdrvMailbox.data_len, PSTR("%02x%02x%02x%02x%02x"), r, g, b, cw, ww);
+ }
+ }
+ else if ((!iscolordimmer && 2 == nvalue) || // gswitch_sSetLevel
+ (iscolordimmer && 15 == nvalue)) { // Color_SetBrightnessLevel
+ if (domoticz[PSTR("svalue1")]) {
+ nvalue = domoticz.getUInt(PSTR("svalue1"), 0);
+ } else {
+ return true; // Invalid data
+ }
+ if (TasmotaGlobal.light_type && (Settings->light_dimmer == nvalue) && ((TasmotaGlobal.power >> relay_index) &1)) {
+ return true; // State already set
+ }
+ snprintf_P(XdrvMailbox.topic, XdrvMailbox.index, PSTR("/" D_CMND_DIMMER));
+ snprintf_P(XdrvMailbox.data, XdrvMailbox.data_len, PSTR("%d"), nvalue);
+ } else
+#endif // USE_LIGHT
+ if (1 == nvalue || 0 == nvalue) {
+ if (((TasmotaGlobal.power >> relay_index) &1) == (power_t)nvalue) {
+ return true; // Stop loop
+ }
+ char stemp1[10];
+ snprintf_P(XdrvMailbox.topic, XdrvMailbox.index, PSTR("/" D_CMND_POWER "%s"), (TasmotaGlobal.devices_present > 1) ? itoa(relay_index +1, stemp1, 10) : "");
+ snprintf_P(XdrvMailbox.data, XdrvMailbox.data_len, PSTR("%d"), nvalue);
+ } else {
+ return true; // No command received
+ }
+
+ AddLog(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_DOMOTICZ D_RECEIVED_TOPIC " %s, " D_DATA " %s"), XdrvMailbox.topic, XdrvMailbox.data);
+
+ Domoticz->update_flag = false;
+ return false; // Process new data
+}
+
+/*********************************************************************************************/
+
+void DomoticzSendSwitch(uint32_t type, uint32_t index, uint32_t state) {
+ char stemp[16]; // "switchlight" or "switchscene"
+ Response_P(PSTR("{\"command\":\"%s\",\"idx\":%d,\"switchcmd\":\"%s\"}"),
+ GetTextIndexed(stemp, sizeof(stemp), type, kDomoticzCommand), index, (state) ? (POWER_TOGGLE == state) ? "Toggle" : "On" : "Off"); // Domoticz case sensitive
+ MqttPublish(domoticz_in_topic);
+}
+
+bool DomoticzSendKey(uint8_t key, uint8_t device, uint8_t state, uint8_t svalflg) {
+ bool result = false;
+
+ if (device <= MAX_RELAYS_SET) {
+ if ((Domoticz->Settings.key_idx[device -1] || Domoticz->Settings.switch_idx[device -1]) && (svalflg)) {
+ DomoticzSendSwitch(0, (key) ? Domoticz->Settings.switch_idx[device -1] : Domoticz->Settings.key_idx[device -1], state);
+ result = true;
+ }
+ }
+ return result;
+}
+
+/*********************************************************************************************\
+ * Sensors
+ *
+ * Source : https://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s
+ * https://www.domoticz.com/wiki/MQTT
+ *
+ * Percentage, Barometric, Air Quality:
+ * {\"idx\":%d,\"nvalue\":%s}, Idx, Value
+ *
+ * Humidity:
+ * {\"idx\":%d,\"nvalue\":%s,\"svalue\":\"%s\"}, Idx, Humidity, HumidityStatus
+ *
+ * All other:
+ * {\"idx\":%d,\"nvalue\":0,\"svalue\":\"%s\"}, Idx, Value(s)
+ *
+\*********************************************************************************************/
+
+void DomoticzSendData(uint32_t sensor_idx, uint32_t idx, char *data) {
+ char payload[128]; // {"idx":26700,"nvalue":0,"svalue":"22330.1;10234.4;22000.5;10243.4;1006;3000","Battery":100,"RSSI":10}
+ if (DZ_AIRQUALITY == sensor_idx) {
+ snprintf_P(payload, sizeof(payload), PSTR("{\"idx\":%d,\"nvalue\":%s,\"Battery\":%d,\"RSSI\":%d}"),
+ idx, data, DomoticzBatteryQuality(), DomoticzRssiQuality());
+ } else {
+ uint8_t nvalue = 0;
+#ifdef USE_SHUTTER
+ if (DZ_SHUTTER == sensor_idx) {
+ uint8_t position = atoi(data);
+ nvalue = position < 2 ? 0 : (position == 100 ? 1 : 2);
+ }
+#endif // USE_SHUTTER
+ snprintf_P(payload, sizeof(payload), DOMOTICZ_MESSAGE, // "{\"idx\":%d,\"nvalue\":%d,\"svalue\":\"%s\",\"Battery\":%d,\"RSSI\":%d}"
+ idx, nvalue, data, DomoticzBatteryQuality(), DomoticzRssiQuality());
+ }
+ MqttPublishPayload(domoticz_in_topic, payload);
+}
+
+void DomoticzSensor(uint8_t idx, char *data) {
+ if (Domoticz->Settings.sensor_idx[idx]) {
+ DomoticzSendData(idx, Domoticz->Settings.sensor_idx[idx], data);
+ }
+}
+
+uint8_t DomoticzHumidityState(float h) {
+ return (!h) ? 0 : (h < 40) ? 2 : (h > 70) ? 3 : 1;
+}
+
+void DomoticzSensor(uint8_t idx, int value) {
+ char data[16];
+ snprintf_P(data, sizeof(data), PSTR("%d"), value);
+ DomoticzSensor(idx, data);
+}
+
+void DomoticzFloatSensor(uint8_t idx, float value) {
+ uint32_t resolution = 1;
+/*
+ switch (idx) {
+ case DZ_TEMP: resolution = Settings->flag2.temperature_resolution; break;
+ case DZ_POWER_ENERGY: resolution = Settings->flag2.wattage_resolution; break;
+ case DZ_VOLTAGE: resolution = Settings->flag2.voltage_resolution; break;
+ case DZ_CURRENT: resolution = Settings->flag2.current_resolution; break;
+ }
+*/
+ if (DZ_TEMP == idx) { resolution = Settings->flag2.temperature_resolution; }
+ else if (DZ_POWER_ENERGY == idx) { resolution = Settings->flag2.wattage_resolution; }
+ else if (DZ_VOLTAGE == idx) { resolution = Settings->flag2.voltage_resolution; }
+ else if (DZ_CURRENT == idx) { resolution = Settings->flag2.current_resolution; }
+ char data[FLOATSZ];
+ dtostrfd(value, resolution, data);
+ DomoticzSensor(idx, data);
+}
+
+//void DomoticzTempHumPressureSensor(float temp, float hum, float baro = -1);
+void DomoticzTempHumPressureSensor(float temp, float hum, float baro) {
+ char temperature[FLOATSZ];
+ dtostrfd(temp, Settings->flag2.temperature_resolution, temperature);
+ char humidity[FLOATSZ];
+ dtostrfd(hum, Settings->flag2.humidity_resolution, humidity);
+
+ char data[32];
+ if (baro > -1) {
+ char pressure[FLOATSZ];
+ dtostrfd(baro, Settings->flag2.pressure_resolution, pressure);
+
+ snprintf_P(data, sizeof(data), PSTR("%s;%s;%d;%s;5"), temperature, humidity, DomoticzHumidityState(hum), pressure);
+ DomoticzSensor(DZ_TEMP_HUM_BARO, data);
+ } else {
+ snprintf_P(data, sizeof(data), PSTR("%s;%s;%d"), temperature, humidity, DomoticzHumidityState(hum));
+ DomoticzSensor(DZ_TEMP_HUM, data);
+ }
+}
+
+void DomoticzSensorPowerEnergy(int power, char *energy) {
+ char data[16];
+ snprintf_P(data, sizeof(data), PSTR("%d;%s"), power, energy);
+ DomoticzSensor(DZ_POWER_ENERGY, data);
+}
+
+void DomoticzSensorP1SmartMeter(char *usage1, char *usage2, char *return1, char *return2, int power) {
+ //usage1 = energy usage meter tariff 1, This is an incrementing counter
+ //usage2 = energy usage meter tariff 2, This is an incrementing counter
+ //return1 = energy return meter tariff 1, This is an incrementing counter
+ //return2 = energy return meter tariff 2, This is an incrementing counter
+ //power = if >= 0 actual usage power. if < 0 actual return power (Watt)
+ int consumed = power;
+ int produced = 0;
+ if (power < 0) {
+ consumed = 0;
+ produced = -power;
+ }
+ char data[64];
+ snprintf_P(data, sizeof(data), PSTR("%s;%s;%s;%s;%d;%d"), usage1, usage2, return1, return2, consumed, produced);
+ DomoticzSensor(DZ_P1_SMART_METER, data);
+}
+
+/*********************************************************************************************/
+
+void DomoticzInit(void) {
+ if (Settings->flag.mqtt_enabled) { // SetOption3 - Enable MQTT
+ Domoticz = (Domoticz_t*)calloc(1, sizeof(Domoticz_t)); // Need calloc to reset registers to 0/false
+ if (nullptr == Domoticz) { return; }
+
+ DomoticzSettingsLoad(0);
+ Domoticz->update_flag = true;
+ }
+}
+
+/*********************************************************************************************\
+ * Commands
+\*********************************************************************************************/
+
+void CmndDomoticzIdx(void) {
+ // DzIdx0 0 - Reset all disabling subscription too
+ // DzIdx1 403 - Relate relay1 (=Power1) to Domoticz Idx 403 persistent
+ // DzIdx5 403 - Relate relay5 (=Power5) to Domoticz Idx 403 non-persistent (need a rule at boot to become persistent)
+ if ((XdrvMailbox.index >= 0) && (XdrvMailbox.index <= MAX_RELAYS_SET)) {
+ if (XdrvMailbox.payload >= 0) {
+ if (0 == XdrvMailbox.index) {
+ for (uint32_t i = 0; i < MAX_RELAYS_SET; i++) {
+ DomoticzSetRelayIdx(i, 0);
+ }
+ } else {
+ DomoticzSetRelayIdx(XdrvMailbox.index -1, XdrvMailbox.payload);
+ }
+ DomoticzMqttSubscribe();
+ }
+ ResponseCmndIdxNumber(DomoticzRelayIdx(XdrvMailbox.index -1));
+ }
+}
+
+void CmndDomoticzKeyIdx(void) {
+ if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_RELAYS_SET)) {
+ if (XdrvMailbox.payload >= 0) {
+ Domoticz->Settings.key_idx[XdrvMailbox.index -1] = XdrvMailbox.payload;
+ }
+ ResponseCmndIdxNumber(Domoticz->Settings.key_idx[XdrvMailbox.index -1]);
+ }
+}
+
+void CmndDomoticzSwitchIdx(void) {
+ if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_RELAYS_SET)) {
+ if (XdrvMailbox.payload >= 0) {
+ Domoticz->Settings.switch_idx[XdrvMailbox.index -1] = XdrvMailbox.payload;
+ }
+ ResponseCmndIdxNumber(Domoticz->Settings.switch_idx[XdrvMailbox.index -1]);
+ }
+}
+
+void CmndDomoticzSensorIdx(void) {
+ if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= DZ_MAX_SENSORS)) {
+ if (XdrvMailbox.payload >= 0) {
+ Domoticz->Settings.sensor_idx[XdrvMailbox.index -1] = XdrvMailbox.payload;
+ }
+ ResponseCmndIdxNumber(Domoticz->Settings.sensor_idx[XdrvMailbox.index -1]);
+ }
+}
+
+void CmndDomoticzUpdateTimer(void) {
+ if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload < 3601)) {
+ Domoticz->Settings.update_timer = XdrvMailbox.payload;
+ }
+ ResponseCmndNumber(Domoticz->Settings.update_timer);
+}
+
+void CmndDomoticzSend(void) {
+ /*
+ DzSend1 , - {\"idx\":,\"nvalue\":0,\"svalue\":\"\",\"Battery\":xx,\"RSSI\":yy}
+ rule1 on power1#state do dzsend1 9001,%value% endon
+ DzSend1 418,%var1%;%var2% or DzSend1 418,%var1%:%var2% - Notice colon as substitute to semi-colon
+ DzSend2 , - USE_SHUTTER only - {\"idx\":,\"nvalue\":,\"svalue\":\"\",\"Battery\":xx,\"RSSI\":yy}
+ DzSend3 , - {\"idx\":,\"nvalue\":,\"Battery\":xx,\"RSSI\":yy}
+ DzSend4 , - {\"command\":\"switchlight\",\"idx\":,\"switchcmd\":\"\"}
+ DzSend5 , - {\"command\":\"switchscene\",\"idx\":,\"switchcmd\":\"\"}
+ */
+ if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 5)) {
+ if (XdrvMailbox.data_len > 0) {
+ if (strchr(XdrvMailbox.data, ',') != nullptr) { // Process parameter entry
+ char *data;
+ uint32_t index = strtoul(strtok_r(XdrvMailbox.data, ",", &data), nullptr, 10);
+ if ((index > 0) && (data != nullptr)) {
+ ReplaceChar(data,':',';'); // As a workaround for command backlog inter-command separator
+ if (XdrvMailbox.index > 3) {
+ uint32_t state = strtoul(data, nullptr, 10); // 0, 1 or 2
+ DomoticzSendSwitch(XdrvMailbox.index -4, index, state);
+ } else {
+ uint32_t type = DZ_TEMP;
+ if (2 == XdrvMailbox.index) { type = DZ_SHUTTER; }
+ else if (3 == XdrvMailbox.index) { type = DZ_AIRQUALITY; }
+ DomoticzSendData(type, index, data);
+ }
+ ResponseCmndDone();
+ }
+ }
+ }
+ }
+}
+
+/*********************************************************************************************\
+ * Presentation
+\*********************************************************************************************/
+
+#ifdef USE_WEBSERVER
+
+#define WEB_HANDLE_DOMOTICZ "dm"
+
+const char HTTP_BTN_MENU_DOMOTICZ[] PROGMEM =
+ "
";
+
+const char HTTP_FORM_DOMOTICZ[] PROGMEM =
+ "