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 = + "
 " D_DOMOTICZ_PARAMETERS " " + "
" + ""; +const char HTTP_FORM_DOMOTICZ_RELAY[] PROGMEM = + "" + ""; +const char HTTP_FORM_DOMOTICZ_SWITCH[] PROGMEM = + ""; +const char HTTP_FORM_DOMOTICZ_SENSOR[] PROGMEM = + ""; +const char HTTP_FORM_DOMOTICZ_TIMER[] PROGMEM = + ""; + +void HandleDomoticzConfiguration(void) { + if (!HttpCheckPriviledgedAccess()) { return; } + + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_CONFIGURE_DOMOTICZ)); + + if (Webserver->hasArg(F("save"))) { + DomoticzSaveSettings(); + HandleConfiguration(); + return; + } + + char stemp[40]; + + WSContentStart_P(PSTR(D_CONFIGURE_DOMOTICZ)); + WSContentSendStyle(); + WSContentSend_P(HTTP_FORM_DOMOTICZ); + for (uint32_t i = 0; i < MAX_RELAYS_SET; i++) { + if (i < TasmotaGlobal.devices_present) { + WSContentSend_P(HTTP_FORM_DOMOTICZ_RELAY, + i +1, i, Domoticz->Settings.relay_idx[i], + i +1, i, Domoticz->Settings.key_idx[i]); + } + if (PinUsed(GPIO_SWT1, i)) { + WSContentSend_P(HTTP_FORM_DOMOTICZ_SWITCH, + i +1, i, Domoticz->Settings.switch_idx[i]); + } + } + for (uint32_t i = 0; i < DZ_MAX_SENSORS; i++) { + WSContentSend_P(HTTP_FORM_DOMOTICZ_SENSOR, + i +1, GetTextIndexed(stemp, sizeof(stemp), i, kDomoticzSensors), i, Domoticz->Settings.sensor_idx[i]); + } + WSContentSend_P(HTTP_FORM_DOMOTICZ_TIMER, Domoticz->Settings.update_timer); + WSContentSend_P(PSTR("
" D_DOMOTICZ_IDX " %d
" D_DOMOTICZ_KEY_IDX " %d
" D_DOMOTICZ_SWITCH_IDX " %d
" D_DOMOTICZ_SENSOR_IDX " %d %s
" D_DOMOTICZ_UPDATE_TIMER " (" STR(DOMOTICZ_UPDATE_TIMER) ")
")); + WSContentSend_P(HTTP_FORM_END); + WSContentSpaceButton(BUTTON_CONFIGURATION); + WSContentStop(); +} + +String DomoticzAddWebCommand(const char* command, const char* arg, uint32_t value) { + char tmp[8]; // WebGetArg numbers only + WebGetArg(arg, tmp, sizeof(tmp)); + if (!strlen(tmp)) { return ""; } + if (atoi(tmp) == value) { return ""; } + return AddWebCommand(command, arg, PSTR("0")); +} + +void DomoticzSaveSettings(void) { + String cmnd = F(D_CMND_BACKLOG "0 "); + cmnd += AddWebCommand(PSTR(D_PRFX_DOMOTICZ D_CMND_UPDATETIMER), PSTR("ut"), STR(DOMOTICZ_UPDATE_TIMER)); + char arg_idx[5]; + char cmnd2[24]; + for (uint32_t i = 0; i < MAX_RELAYS_SET; i++) { + snprintf_P(cmnd2, sizeof(cmnd2), PSTR(D_PRFX_DOMOTICZ D_CMND_IDX "%d"), i +1); + snprintf_P(arg_idx, sizeof(arg_idx), PSTR("r%d"), i); + cmnd += DomoticzAddWebCommand(cmnd2, arg_idx, Domoticz->Settings.relay_idx[i]); + snprintf_P(cmnd2, sizeof(cmnd2), PSTR(D_PRFX_DOMOTICZ D_CMND_KEYIDX "%d"), i +1); + arg_idx[0] = 'k'; + cmnd += DomoticzAddWebCommand(cmnd2, arg_idx, Domoticz->Settings.key_idx[i]); + snprintf_P(cmnd2, sizeof(cmnd2), PSTR(D_PRFX_DOMOTICZ D_CMND_SWITCHIDX "%d"), i +1); + arg_idx[0] = 's'; + cmnd += DomoticzAddWebCommand(cmnd2, arg_idx, Domoticz->Settings.switch_idx[i]); + } + for (uint32_t i = 0; i < DZ_MAX_SENSORS; i++) { + snprintf_P(cmnd2, sizeof(cmnd2), PSTR(D_PRFX_DOMOTICZ D_CMND_SENSORIDX "%d"), i +1); + snprintf_P(arg_idx, sizeof(arg_idx), PSTR("l%d"), i); + cmnd += DomoticzAddWebCommand(cmnd2, arg_idx, Domoticz->Settings.sensor_idx[i]); + } + ExecuteWebCommand((char*)cmnd.c_str()); +} +#endif // USE_WEBSERVER + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xdrv07(uint32_t function) { + bool result = false; + + if (FUNC_PRE_INIT == function) { + DomoticzInit(); + } + else if (Domoticz) { + switch (function) { + case FUNC_EVERY_SECOND: + DomoticzMqttUpdate(); + break; + case FUNC_MQTT_DATA: + result = DomoticzMqttData(); + break; + case FUNC_RESET_SETTINGS: + DomoticzSettingsLoad(1); + break; + case FUNC_SAVE_SETTINGS: + DomoticzSettingsSave(); + break; +#ifdef USE_WEBSERVER + case FUNC_WEB_ADD_BUTTON: + WSContentSend_P(HTTP_BTN_MENU_DOMOTICZ); + break; + case FUNC_WEB_ADD_HANDLER: + WebServer_on(PSTR("/" WEB_HANDLE_DOMOTICZ), HandleDomoticzConfiguration); + break; +#endif // USE_WEBSERVER + case FUNC_MQTT_SUBSCRIBE: + DomoticzMqttSubscribe(); +#ifdef USE_SHUTTER + if (Domoticz->Settings.sensor_idx[DZ_SHUTTER]) { + Domoticz->is_shutter = true; + } +#endif // USE_SHUTTER + break; + case FUNC_MQTT_INIT: + Domoticz->update_timer = 2; + break; + case FUNC_SHOW_SENSOR: +// DomoticzSendSensor(); + break; + case FUNC_COMMAND: + result = DecodeCommand(kDomoticzCommands, DomoticzCommand); + break; + case FUNC_ACTIVE: + result = true; + break; + } + } + return result; +} + +#endif // USE_DOMOTICZ +#endif // ESP32