mirror of https://github.com/arendst/Tasmota.git
340 lines
15 KiB
C++
340 lines
15 KiB
C++
/*
|
|
xdrv_12_discovery.ino - MQTT Discovery support for Tasmota
|
|
|
|
Copyright (C) 2021 Erik Montnemery, Federico Leoni and 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#ifdef USE_TASMOTA_DISCOVERY
|
|
#undef USE_HOME_ASSISTANT
|
|
/*********************************************************************************************\
|
|
* Tasmota discovery
|
|
*
|
|
* Supported by latest versions of Home Assistant (with hatasmota) and TasmoManager.
|
|
*
|
|
* SetOption19 0 - [DiscoverOff 0] [Discover 1] Enables discovery (default)
|
|
* SetOption19 1 - [DiscoverOff 1] [Discover 0] Disables discovery and removes retained message from MQTT server
|
|
* SetOption73 1 - [DiscoverButton] Enable discovery for buttons
|
|
* SetOption114 1 - [DiscoverSwitch] Enable discovery for switches
|
|
\*********************************************************************************************/
|
|
|
|
#define XDRV_12 12
|
|
|
|
void TasDiscoverMessage(void) {
|
|
uint32_t ip_address = (uint32_t)WiFi.localIP();
|
|
char* hostname = TasmotaGlobal.hostname;
|
|
#if defined(ESP32) && CONFIG_IDF_TARGET_ESP32 && defined(USE_ETHERNET)
|
|
if (static_cast<uint32_t>(EthernetLocalIP()) != 0) {
|
|
ip_address = (uint32_t)EthernetLocalIP();
|
|
hostname = EthernetHostname();
|
|
}
|
|
#endif
|
|
Response_P(PSTR("{\"ip\":\"%_I\"," // IP Address
|
|
"\"dn\":\"%s\"," // Device Name
|
|
"\"fn\":["), // Friendly Names (start)
|
|
ip_address,
|
|
SettingsText(SET_DEVICENAME));
|
|
|
|
uint32_t maxfn = (TasmotaGlobal.devices_present > MAX_FRIENDLYNAMES) ? MAX_FRIENDLYNAMES : (!TasmotaGlobal.devices_present) ? 1 : TasmotaGlobal.devices_present;
|
|
for (uint32_t i = 0; i < MAX_FRIENDLYNAMES; i++) {
|
|
char fname[TOPSZ];
|
|
snprintf_P(fname, sizeof(fname), PSTR("\"%s\""), EscapeJSONString(SettingsText(SET_FRIENDLYNAME1 +i)).c_str());
|
|
ResponseAppend_P(PSTR("%s%s"), (i > 0 ? "," : ""), (i < maxfn) ? fname : PSTR("null"));
|
|
}
|
|
|
|
bool TuyaMod = false;
|
|
#ifdef USE_TUYA_MCU
|
|
TuyaMod = IsModuleTuya();
|
|
#endif
|
|
bool iFanMod = false;
|
|
#ifdef ESP8266
|
|
iFanMod = ((SONOFF_IFAN02 == TasmotaGlobal.module_type) || (SONOFF_IFAN03 == TasmotaGlobal.module_type));
|
|
#endif // ESP8266
|
|
|
|
ResponseAppend_P(PSTR("]," // Friendly Names (end)
|
|
"\"hn\":\"%s\"," // Host Name
|
|
"\"mac\":\"%s\"," // Full MAC as Device id
|
|
"\"md\":\"%s\"," // Module or Template Name
|
|
"\"ty\":%d,\"if\":%d," // Flag for TuyaMCU and Ifan devices
|
|
"\"ofln\":\"" MQTT_LWT_OFFLINE "\"," // Payload Offline
|
|
"\"onln\":\"" MQTT_LWT_ONLINE "\"," // Payload Online
|
|
"\"state\":[\"%s\",\"%s\",\"%s\",\"%s\"]," // State text for "OFF","ON","TOGGLE","HOLD"
|
|
"\"sw\":\"%s\"," // Software Version
|
|
"\"t\":\"%s\"," // Topic
|
|
"\"ft\":\"%s\"," // Full Topic
|
|
"\"tp\":[\"%s\",\"%s\",\"%s\"]," // Topics for command, stat and tele
|
|
"\"rl\":["), // Relays (start)
|
|
hostname,
|
|
NetworkUniqueId().c_str(),
|
|
ModuleName().c_str(),
|
|
TuyaMod, iFanMod,
|
|
GetStateText(0), GetStateText(1), GetStateText(2), GetStateText(3),
|
|
TasmotaGlobal.version,
|
|
TasmotaGlobal.mqtt_topic,
|
|
SettingsText(SET_MQTT_FULLTOPIC),
|
|
SettingsText(SET_MQTTPREFIX1),
|
|
SettingsText(SET_MQTTPREFIX2),
|
|
SettingsText(SET_MQTTPREFIX3));
|
|
|
|
uint8_t light_idx = MAX_RELAYS_SET + 1; // Will store the starting position of the lights
|
|
uint8_t light_subtype = 0;
|
|
bool light_controller_isCTRGBLinked = false;
|
|
#ifdef USE_LIGHT
|
|
light_subtype = Light.subtype;
|
|
if (light_subtype > LST_NONE) {
|
|
light_controller_isCTRGBLinked = light_controller.isCTRGBLinked();
|
|
if (!light_controller_isCTRGBLinked) { // One or two lights present
|
|
light_idx = TasmotaGlobal.devices_present - 2;
|
|
} else {
|
|
light_idx = TasmotaGlobal.devices_present - 1;
|
|
}
|
|
}
|
|
|
|
if ((Light.device > 0) && Settings->flag3.pwm_multi_channels) { // How many relays are light devices?
|
|
light_idx = TasmotaGlobal.devices_present - light_subtype;
|
|
}
|
|
#endif // USE_LIGHT
|
|
|
|
uint16_t Relay[MAX_RELAYS_SET] = { 0 }; // Base array to store the relay type
|
|
uint16_t Shutter[MAX_RELAYS_SET] = { 0 }; // Array to store a temp list for shutters
|
|
|
|
#ifdef USE_SHUTTER
|
|
if (Settings->flag3.shutter_mode) {
|
|
for (uint32_t k = 0; k < TasmotaGlobal.shutters_present; k++) {
|
|
uint8_t sr = ShutterGetStartRelay(k);
|
|
if (sr > 0) {
|
|
Shutter[sr-1] = Shutter[sr] = 1;
|
|
} else {
|
|
// terminate loop at first INVALID ShutterGetStartRelay(k).
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
#endif // USE_SHUTTER
|
|
|
|
for (uint32_t i = 0; i < MAX_RELAYS_SET; i++) {
|
|
if (i < TasmotaGlobal.devices_present) {
|
|
if (Shutter[i] != 0) { // Check if there are shutters present
|
|
Relay[i] = 3; // Relay is a shutter
|
|
} else {
|
|
if (i >= light_idx || (iFanMod && (0 == i))) { // First relay on Ifan controls the light
|
|
Relay[i] = 2; // Relay is a light
|
|
} else {
|
|
if (!iFanMod) { // Relays 2-4 for ifan are controlled by FANSPEED and don't need to be present if TasmotaGlobal.module_type = SONOFF_IFAN02 or SONOFF_IFAN03
|
|
Relay[i] = 1; // Simple Relay
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ResponseAppend_P(PSTR("%s%d"), (i > 0 ? "," : ""), Relay[i]); // Vector for the Official Integration
|
|
}
|
|
|
|
ResponseAppend_P(PSTR("]," // Relays (end)
|
|
"\"swc\":[")); // Switch modes (start)
|
|
|
|
// Enable Discovery for Switches only if SetOption114 is enabled
|
|
for (uint32_t i = 0; i < MAX_SWITCHES_SET; i++) {
|
|
ResponseAppend_P(PSTR("%s%d"), (i > 0 ? "," : ""), (SwitchUsed(i) && Settings->flag5.mqtt_switches) ? Settings->switchmode[i] : -1);
|
|
}
|
|
|
|
ResponseAppend_P(PSTR("]," // Switch modes (end)
|
|
"\"swn\":[")); // Switch names (start)
|
|
|
|
// Enable Discovery for Switches only if SetOption114 is enabled
|
|
for (uint32_t i = 0; i < MAX_SWITCHES_SET; i++) {
|
|
char sname[TOPSZ];
|
|
snprintf_P(sname, sizeof(sname), PSTR("\"%s\""), GetSwitchText(i).c_str());
|
|
ResponseAppend_P(PSTR("%s%s"), (i > 0 ? "," : ""), (SwitchUsed(i) && Settings->flag5.mqtt_switches) ? sname : PSTR("null"));
|
|
}
|
|
|
|
ResponseAppend_P(PSTR("]," // Switch names (end)
|
|
"\"btn\":[")); // Button flag (start)
|
|
|
|
bool SerialButton = false;
|
|
// Enable Discovery for Buttons only if SetOption73 is enabled
|
|
for (uint32_t i = 0; i < MAX_KEYS_SET; i++) {
|
|
#ifdef ESP8266
|
|
SerialButton = ((0 == i) && (SONOFF_DUAL == TasmotaGlobal.module_type ));
|
|
#endif // ESP8266
|
|
ResponseAppend_P(PSTR("%s%d"), (i > 0 ? "," : ""), (SerialButton ? 1 : (ButtonUsed(i)) && Settings->flag3.mqtt_buttons));
|
|
}
|
|
|
|
ResponseAppend_P(PSTR("]," // Button flag (end)
|
|
"\"so\":{\"4\":%d," // SetOptions
|
|
"\"11\":%d,"
|
|
"\"13\":%d,"
|
|
"\"17\":%d,"
|
|
"\"20\":%d,"
|
|
"\"30\":%d,"
|
|
"\"68\":%d,"
|
|
"\"73\":%d,"
|
|
"\"82\":%d,"
|
|
"\"114\":%d,"
|
|
"\"117\":%d},"
|
|
"\"lk\":%d," // Light CTRGB linked
|
|
"\"lt_st\":%d," // Light SubType
|
|
"\"bat\":%d," // Battery operates yes/no
|
|
"\"dslp\":%d," // Deepsleep configured yes/no
|
|
"\"sho\":["), // Shutter Options (start)
|
|
Settings->flag.mqtt_response,
|
|
Settings->flag.button_swap,
|
|
Settings->flag.button_single,
|
|
Settings->flag.decimal_text,
|
|
Settings->flag.not_power_linked,
|
|
Settings->flag.hass_light,
|
|
Settings->flag3.pwm_multi_channels,
|
|
Settings->flag3.mqtt_buttons,
|
|
Settings->flag4.alexa_ct_range,
|
|
Settings->flag5.mqtt_switches,
|
|
Settings->flag5.fade_fixed_duration,
|
|
light_controller_isCTRGBLinked,
|
|
light_subtype,
|
|
(Settings->battery_level_percent == 101) ? 0 : 1,
|
|
(Settings->deepsleep == 0) ? 0 : 1
|
|
);
|
|
|
|
for (uint32_t i = 0; i < TasmotaGlobal.shutters_present; i++) {
|
|
#ifdef USE_SHUTTER
|
|
ResponseAppend_P(PSTR("%s%d"), (i > 0 ? "," : ""), ShutterGetOptions(i));
|
|
#else
|
|
ResponseAppend_P(PSTR("%s0"), (i > 0 ? "," : ""));
|
|
#endif // USE_SHUTTER
|
|
}
|
|
|
|
ResponseAppend_P(PSTR("]," // Shutter Options (end)
|
|
"\"sht\":[")); // Shutter Tilt (start)
|
|
for (uint32_t i = 0; i < TasmotaGlobal.shutters_present; i++) {
|
|
#ifdef USE_SHUTTER
|
|
ResponseAppend_P(PSTR("%s[%d,%d,%d]"), (i > 0 ? "," : ""),
|
|
ShutterGetTiltConfig(0,i),
|
|
ShutterGetTiltConfig(1,i),
|
|
ShutterGetTiltConfig(2,i));
|
|
#else
|
|
ResponseAppend_P(PSTR("%s[0,0,0]"), (i > 0 ? "," : ""));
|
|
#endif // USE_SHUTTER
|
|
}
|
|
ResponseAppend_P(PSTR("]," // Shutter Tilt (end)
|
|
"\"ver\":1}")); // Discovery version
|
|
}
|
|
|
|
void TasDiscovery(void) {
|
|
TasmotaGlobal.masterlog_level = LOG_LEVEL_DEBUG_MORE; // Hide topic on clean and remove use weblog 4 to show it
|
|
|
|
ResponseClear(); // Clear retained message
|
|
if (!Settings->flag.hass_discovery) { // SetOption19 - Clear retained message
|
|
TasDiscoverMessage(); // Build discovery message
|
|
}
|
|
char stopic[TOPSZ];
|
|
snprintf_P(stopic, sizeof(stopic), PSTR("tasmota/discovery/%s/config"), NetworkUniqueId().c_str());
|
|
MqttPublish(stopic, true);
|
|
|
|
if (!Settings->flag.hass_discovery) { // SetOption19 - Clear retained message
|
|
Response_P(PSTR("{\"sn\":"));
|
|
MqttShowSensor(true);
|
|
ResponseAppend_P(PSTR(",\"ver\":1}"));
|
|
}
|
|
snprintf_P(stopic, sizeof(stopic), PSTR("tasmota/discovery/%s/sensors"), NetworkUniqueId().c_str());
|
|
MqttPublish(stopic, true);
|
|
|
|
TasmotaGlobal.masterlog_level = LOG_LEVEL_NONE; // Restore WebLog state
|
|
}
|
|
|
|
void TasRediscover(void) {
|
|
TasmotaGlobal.discovery_counter = 1; // Delayed discovery or clear retained messages
|
|
}
|
|
|
|
void TasDiscoverInit(void) {
|
|
if (ResetReason() != REASON_DEEP_SLEEP_AWAKE) {
|
|
Settings->flag.hass_discovery = 0; // SetOption19 - Enable Tasmota discovery and Disable legacy Hass discovery
|
|
TasmotaGlobal.discovery_counter = 10; // Delayed discovery
|
|
}
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
* Stubs replacing legacy Hass discovery
|
|
\*********************************************************************************************/
|
|
|
|
void HAssPublishStatus(void) { return; }
|
|
void HAssDiscover(void) { TasRediscover(); }
|
|
|
|
/*********************************************************************************************\
|
|
* Commands
|
|
*
|
|
* Discover 0 - Disables discovery and removes retained message from MQTT server
|
|
* Discover 1 - Enables discovery (default)
|
|
* DiscoverOff 0 - Enables discovery (default)
|
|
* DiscoverOff 1 - Disables discovery and removes retained message from MQTT server
|
|
* DiscoverButton 1 - Enable discovery for buttons
|
|
* DiscoverSwitch 1 - Enable discovery for switches
|
|
\*********************************************************************************************/
|
|
|
|
const char kTasDiscoverCommands[] PROGMEM = "Discover|" // Prefix
|
|
// SetOption synonyms
|
|
"Off|Button|Switch|"
|
|
// Commands
|
|
"|";
|
|
|
|
SO_SYNONYMS(kTasDiscoverSynonyms,
|
|
19, 73, 114 );
|
|
|
|
void (* const TasDiscoverCommand[])(void) PROGMEM = {
|
|
&CmndTasDiscover };
|
|
|
|
void CmndTasDiscover(void) {
|
|
if (XdrvMailbox.payload >= 0) {
|
|
Settings->flag.hass_discovery = !(XdrvMailbox.payload & 1); // SetOption19 - Tasmota discovery (0)
|
|
TasRediscover();
|
|
}
|
|
ResponseCmndChar(GetStateText(!Settings->flag.hass_discovery));
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
* Interface
|
|
\*********************************************************************************************/
|
|
|
|
bool Xdrv12(uint32_t function) {
|
|
bool result = false;
|
|
|
|
if (Settings->flag.mqtt_enabled) { // SetOption3 - Enable MQTT
|
|
switch (function) {
|
|
case FUNC_EVERY_SECOND:
|
|
if (TasmotaGlobal.discovery_counter) {
|
|
TasmotaGlobal.discovery_counter--;
|
|
if (!TasmotaGlobal.discovery_counter) {
|
|
TasDiscovery(); // Send the topics for discovery
|
|
}
|
|
}
|
|
break;
|
|
case FUNC_COMMAND:
|
|
result = DecodeCommand(kTasDiscoverCommands, TasDiscoverCommand, kTasDiscoverSynonyms);
|
|
break;
|
|
case FUNC_MQTT_SUBSCRIBE:
|
|
if (0 == Mqtt.initial_connection_state) {
|
|
TasRediscover();
|
|
}
|
|
break;
|
|
case FUNC_MQTT_INIT:
|
|
TasDiscoverInit();
|
|
break;
|
|
case FUNC_ACTIVE:
|
|
result = true;
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#endif // USE_TASMOTA_DISCOVERY
|