/*
xdrv_07_domoticz.ino - domoticz support for Tasmota
Copyright (C) 2021 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 USE_DOMOTICZ
#define XDRV_07 7
//#define D_PRFX_DOMOTICZ "Domoticz"
#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}";
#if MAX_DOMOTICZ_SNS_IDX < DZ_MAX_SENSORS
#error "Domoticz: Too many sensors or change settings.h layout"
#endif
const char kDomoticzSensors[] PROGMEM =
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;
int domoticz_update_timer = 0;
uint32_t domoticz_fan_debounce = 0; // iFan02 state debounce timer
bool domoticz_subscribe = false;
bool domoticz_update_flag = true;
#ifdef USE_SHUTTER
bool domoticz_is_shutter = false;
#endif // USE_SHUTTER
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;
}
#ifdef USE_SONOFF_IFAN
void MqttPublishDomoticzFanState(void) {
if (Settings.flag.mqtt_enabled && Settings.domoticz_relay_idx[1]) { // SetOption3 - Enable MQTT
char svalue[8]; // Fanspeed value
int fan_speed = GetFanspeed();
snprintf_P(svalue, sizeof(svalue), PSTR("%d"), fan_speed * 10);
Response_P(DOMOTICZ_MESSAGE, (int)Settings.domoticz_relay_idx[1], (0 == fan_speed) ? 0 : 2, svalue, DomoticzBatteryQuality(), DomoticzRssiQuality());
MqttPublish(domoticz_in_topic);
domoticz_fan_debounce = millis();
}
}
void DomoticzUpdateFanState(void) {
if (domoticz_update_flag) {
MqttPublishDomoticzFanState();
}
domoticz_update_flag = true;
}
#endif // USE_SONOFF_IFAN
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_DOMOTICZ_IDX)) { return; }
if (Settings.domoticz_relay_idx[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
#ifdef USE_SONOFF_IFAN
if (IsModuleIfan() && (device > 1)) {
// Fan handled by MqttPublishDomoticzFanState
} else {
#endif // USE_SONOFF_IFAN
char svalue[8]; // Dimmer value
snprintf_P(svalue, sizeof(svalue), PSTR("%d"), Settings.light_dimmer);
Response_P(DOMOTICZ_MESSAGE, (int)Settings.domoticz_relay_idx[device -1], (TasmotaGlobal.power & (1 << (device -1))) ? 1 : 0, (TasmotaGlobal.light_type) ? svalue : "", DomoticzBatteryQuality(), DomoticzRssiQuality());
MqttPublish(domoticz_in_topic);
#ifdef USE_SONOFF_IFAN
}
#endif // USE_SONOFF_IFAN
#ifdef USE_SHUTTER
}
#endif //USE_SHUTTER
}
}
}
void DomoticzUpdatePowerState(uint8_t device) {
if (domoticz_update_flag) {
MqttPublishDomoticzPowerState(device);
}
domoticz_update_flag = true;
}
void DomoticzMqttUpdate(void) {
if (domoticz_subscribe && (Settings.domoticz_update_timer || domoticz_update_timer)) {
domoticz_update_timer--;
if (domoticz_update_timer <= 0) {
domoticz_update_timer = Settings.domoticz_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
#ifdef USE_SONOFF_IFAN
if (IsModuleIfan() && (i > 1)) {
MqttPublishDomoticzFanState();
break;
} else {
#endif // USE_SONOFF_IFAN
MqttPublishDomoticzPowerState(i);
#ifdef USE_SONOFF_IFAN
}
#endif // USE_SONOFF_IFAN
}
}
}
}
void DomoticzMqttSubscribe(void) {
uint8_t maxdev = (TasmotaGlobal.devices_present > MAX_DOMOTICZ_IDX) ? MAX_DOMOTICZ_IDX : TasmotaGlobal.devices_present;
for (uint32_t i = 0; i < maxdev; i++) {
if (Settings.domoticz_relay_idx[i]) {
domoticz_subscribe = true;
}
}
if (domoticz_subscribe) {
char stopic[TOPSZ];
snprintf_P(stopic, sizeof(stopic), PSTR(DOMOTICZ_OUT_TOPIC "/#")); // domoticz topic
MqttSubscribe(stopic);
}
}
bool DomoticzMqttData(void) {
/*
XdrvMailbox.topic = topic;
XdrvMailbox.index = strlen(topic);
XdrvMailbox.data = (char*)data;
XdrvMailbox.data_len = data_len;
*/
domoticz_update_flag = true;
if (strncasecmp_P(XdrvMailbox.topic, PSTR(DOMOTICZ_OUT_TOPIC), strlen(DOMOTICZ_OUT_TOPIC)) != 0) {
return false; // Process unchanged data
}
// topic is domoticz/out so try to analyse
if (XdrvMailbox.data_len < 20) {
return true; // No valid data
}
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
}
// if (strcmp_P(domoticz["dtype"],PSTR("Light/Switch"))) {
// return true;
// }
uint32_t idx = domoticz.getUInt(PSTR("idx"), 0);
int16_t nvalue = domoticz.getInt(PSTR("nvalue"), -1);
AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_DOMOTICZ "idx %d, nvalue %d"), idx, nvalue);
bool found = false;
if ((idx > 0) && (nvalue >= 0) && (nvalue <= 15)) {
uint8_t maxdev = (TasmotaGlobal.devices_present > MAX_DOMOTICZ_IDX) ? MAX_DOMOTICZ_IDX : TasmotaGlobal.devices_present;
for (uint32_t i = 0; i < maxdev; i++) {
if (idx == Settings.domoticz_relay_idx[i]) {
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;
char stemp1[10];
snprintf_P(stemp1, sizeof(stemp1), PSTR("%d"), i +1);
#ifdef USE_SONOFF_IFAN
if (IsModuleIfan() && (1 == i)) { // Idx 2 is fanspeed
JsonParserToken svalue_tok = domoticz[PSTR("svalue1")];
if (!svalue_tok) {
return true;
}
uint8_t svalue = svalue_tok.getUInt();
svalue = (2 == nvalue) ? svalue / 10 : 0;
if (GetFanspeed() == svalue) {
return true; // Stop loop as already set
}
if (TimePassedSince(domoticz_fan_debounce) < 1000) {
return true; // Stop loop if device in limbo
}
snprintf_P(XdrvMailbox.topic, XdrvMailbox.index, PSTR("/" D_CMND_FANSPEED));
snprintf_P(XdrvMailbox.data, XdrvMailbox.data_len, PSTR("%d"), svalue);
found = true;
} else
#endif // USE_SONOFF_IFAN
#ifdef USE_SHUTTER
if (isShutter) {
uint8_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);
found = true;
} 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"];
uint16_t level = nvalue = domoticz.getUInt(PSTR("svalue1"), 0);
uint16_t r = color.getUInt(PSTR("r"), 0) * level / 100;
uint16_t g = color.getUInt(PSTR("g"), 0) * level / 100;
uint16_t b = color.getUInt(PSTR("b"), 0) * level / 100;
uint16_t cw = color.getUInt(PSTR("cw"), 0) * level / 100;
uint16_t ww = color.getUInt(PSTR("ww"), 0) * level / 100;
uint16_t m = color.getUInt(PSTR("m"), 0);
uint16_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);
}
found = true;
}
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 >> i) &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);
found = true;
} else
#endif // USE_LIGHT
if (1 == nvalue || 0 == nvalue) {
if (((TasmotaGlobal.power >> i) &1) == (power_t)nvalue) {
return true; // Stop loop
}
snprintf_P(XdrvMailbox.topic, XdrvMailbox.index, PSTR("/" D_CMND_POWER "%s"), (TasmotaGlobal.devices_present > 1) ? stemp1 : "");
snprintf_P(XdrvMailbox.data, XdrvMailbox.data_len, PSTR("%d"), nvalue);
found = true;
}
break;
}
}
}
if (!found) { return true; } // No command received
AddLog_P(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_DOMOTICZ_IDX) {
if ((Settings.domoticz_key_idx[device -1] || Settings.domoticz_switch_idx[device -1]) && (svalflg)) {
DomoticzSendSwitch(0, (key) ? Settings.domoticz_switch_idx[device -1] : Settings.domoticz_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) {
if (DZ_AIRQUALITY == sensor_idx) {
Response_P(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
Response_P(DOMOTICZ_MESSAGE, // "{\"idx\":%d,\"nvalue\":%d,\"svalue\":\"%s\",\"Battery\":%d,\"RSSI\":%d}"
idx, nvalue, data, DomoticzBatteryQuality(), DomoticzRssiQuality());
}
MqttPublish(domoticz_in_topic);
}
void DomoticzSensor(uint8_t idx, char *data) {
if (Settings.domoticz_sensor_idx[idx]) {
char dmess[128]; // {"idx":26700,"nvalue":0,"svalue":"22330.1;10234.4;22000.5;10243.4;1006;3000","Battery":100,"RSSI":10}
memcpy(dmess, TasmotaGlobal.mqtt_data, sizeof(dmess));
DomoticzSendData(idx, Settings.domoticz_sensor_idx[idx], data);
memcpy(TasmotaGlobal.mqtt_data, dmess, sizeof(dmess));
}
}
uint8_t DomoticzHumidityState(float h) {
return (!h) ? 0 : (h < 40) ? 2 : (h > 70) ? 3 : 1;
}
void DomoticzSensor(uint8_t idx, uint32_t value) {
char data[16];
snprintf_P(data, sizeof(data), PSTR("%d"), value);
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);
}
/*********************************************************************************************\
* Commands
\*********************************************************************************************/
void CmndDomoticzIdx(void) {
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_DOMOTICZ_IDX)) {
if (XdrvMailbox.payload >= 0) {
Settings.domoticz_relay_idx[XdrvMailbox.index -1] = XdrvMailbox.payload;
TasmotaGlobal.restart_flag = 2;
}
ResponseCmndIdxNumber(Settings.domoticz_relay_idx[XdrvMailbox.index -1]);
}
}
void CmndDomoticzKeyIdx(void) {
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_DOMOTICZ_IDX)) {
if (XdrvMailbox.payload >= 0) {
Settings.domoticz_key_idx[XdrvMailbox.index -1] = XdrvMailbox.payload;
}
ResponseCmndIdxNumber(Settings.domoticz_key_idx[XdrvMailbox.index -1]);
}
}
void CmndDomoticzSwitchIdx(void) {
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_DOMOTICZ_IDX)) {
if (XdrvMailbox.payload >= 0) {
Settings.domoticz_switch_idx[XdrvMailbox.index -1] = XdrvMailbox.payload;
}
ResponseCmndIdxNumber(Settings.domoticz_switch_idx[XdrvMailbox.index -1]);
}
}
void CmndDomoticzSensorIdx(void) {
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= DZ_MAX_SENSORS)) {
if (XdrvMailbox.payload >= 0) {
Settings.domoticz_sensor_idx[XdrvMailbox.index -1] = XdrvMailbox.payload;
}
ResponseCmndIdxNumber(Settings.domoticz_sensor_idx[XdrvMailbox.index -1]);
}
}
void CmndDomoticzUpdateTimer(void) {
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload < 3601)) {
Settings.domoticz_update_timer = XdrvMailbox.payload;
}
ResponseCmndNumber(Settings.domoticz_update_timer);
}
void CmndDomoticzSend(void) {
// DzSend1 , - {\"idx\":,\"nvalue\":0,\"svalue\":\"\",\"Battery\":xx,\"RSSI\":yy}
// 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)) {
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);
}
}
}
}
}
}
/*********************************************************************************************\
* Presentation
\*********************************************************************************************/
#ifdef USE_WEBSERVER
#define WEB_HANDLE_DOMOTICZ "dm"
const char HTTP_BTN_MENU_DOMOTICZ[] PROGMEM =
"
";
const char HTTP_FORM_DOMOTICZ[] PROGMEM =
"