From 26e4badc1b4d03169fe499e9091e0499f6c2ed40 Mon Sep 17 00:00:00 2001 From: Proddy Date: Fri, 5 Jan 2024 22:39:08 +0100 Subject: [PATCH] add sections for MQTT Discovery --- src/analogsensor.cpp | 17 ++++++++++---- src/mqtt.cpp | 36 ++++++++++++++--------------- src/mqtt.h | 14 +++++------ src/temperaturesensor.cpp | 26 ++++++++++----------- src/web/WebCustomEntityService.cpp | 9 +++++++- src/web/WebCustomizationService.cpp | 1 + src/web/WebSchedulerService.cpp | 19 ++++++++++++++- 7 files changed, 77 insertions(+), 45 deletions(-) diff --git a/src/analogsensor.cpp b/src/analogsensor.cpp index 137ffbd4a..1396776f9 100644 --- a/src/analogsensor.cpp +++ b/src/analogsensor.cpp @@ -468,7 +468,7 @@ void AnalogSensor::publish_values(const bool force) { if (sensor.type() != AnalogType::NOTUSED) { if (Mqtt::is_nested()) { char s[10]; - JsonObject dataSensor = doc[Helpers::smallitoa(s, sensor.gpio())].add(); + JsonObject dataSensor = doc[Helpers::smallitoa(s, sensor.gpio())].to(); dataSensor["name"] = sensor.name(); switch (sensor.type()) { case AnalogType::COUNTER: @@ -508,7 +508,7 @@ void AnalogSensor::publish_values(const bool force) { doc[sensor.name()] = serialized(Helpers::render_value(s, sensor.value(), 2)); } - // create HA config + // create HA config if hasn't already been done if (Mqtt::ha_enabled() && (!sensor.ha_registered || force)) { LOG_DEBUG("Recreating HA config for analog sensor GPIO %02d", sensor.gpio()); @@ -615,7 +615,16 @@ void AnalogSensor::publish_values(const bool force) { config["stat_cla"] = "measurement"; } - Mqtt::add_ha_sections_to_doc("analog", stat_t, config, true, val_cond); + // see if we need to create the [devs] discovery section, as this needs only to be done once for all sensors + bool is_ha_device_created = false; + for (auto & sensor : sensors_) { + if (sensor.ha_registered) { + is_ha_device_created = true; + break; + } + } + + Mqtt::add_ha_sections_to_doc("analog", stat_t, config, !is_ha_device_created, val_cond); sensor.ha_registered = Mqtt::queue_ha(topic, config.as()); } @@ -697,7 +706,7 @@ bool AnalogSensor::command_info(const char * value, const int8_t id, JsonObject for (const auto & sensor : sensors_) { if (id == -1) { // show number and id for info command - JsonObject dataSensor = output[sensor.name()].add(); + JsonObject dataSensor = output[sensor.name()].to(); dataSensor["gpio"] = sensor.gpio(); dataSensor["type"] = F_(number); dataSensor["value"] = sensor.value(); diff --git a/src/mqtt.cpp b/src/mqtt.cpp index 1020e847d..abc0019d0 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -500,8 +500,7 @@ void Mqtt::on_connect() { } // send initial MQTT messages for some of our services - EMSESP::shower_.set_shower_state(false, true); // Send shower_activated as false - EMSESP::system_.send_heartbeat(); // send heartbeat + EMSESP::system_.send_heartbeat(); // send heartbeat // re-subscribe to all custom registered MQTT topics resubscribe(); @@ -539,7 +538,7 @@ void Mqtt::ha_status() { // doc["avty_t"] = "~/status"; // commented out, as it causes errors in HA sometimes // doc["json_attr_t"] = "~/heartbeat"; // store also as HA attributes - JsonObject dev = doc.add(); + JsonObject dev = doc["dev"].to(); dev["name"] = Mqtt::basename(); dev["sw"] = "v" + std::string(EMSESP_APP_VERSION); dev["mf"] = "EMS-ESP"; @@ -776,7 +775,7 @@ bool Mqtt::publish_ha_sensor_config(DeviceValue & dv, const char * model, const // publish HA sensor for System using the heartbeat tag bool Mqtt::publish_system_ha_sensor_config(uint8_t type, const char * name, const char * entity, const uint8_t uom) { JsonDocument doc; - JsonObject dev_json = doc["dev"].add(); + JsonObject dev_json = doc["dev"].to(); dev_json["name"] = Mqtt::basename(); JsonArray ids = dev_json["ids"].to(); @@ -1137,8 +1136,8 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev } } - doc["dev"] = dev_json; // add the dev json object to the end - add_ha_sections_to_doc("", stat_t, doc, false, val_cond); // no name, since the "dev" has already been added + doc["dev"] = dev_json; // add the dev json object to the end + add_ha_sections_to_doc(nullptr, stat_t, doc, false, val_cond); // no name, since the "dev" has already been added return queue_ha(topic, doc.as()); } @@ -1217,7 +1216,7 @@ bool Mqtt::publish_ha_climate_config(const uint8_t tag, const bool has_roomtemp, snprintf(temp_cmd_s, sizeof(temp_cmd_s), "~/thermostat/hc%d/seltemp", hc_num); snprintf(mode_cmd_s, sizeof(mode_cmd_s), "~/thermostat/hc%d/mode", hc_num); - JsonDocument doc; // 1024 is not enough + JsonDocument doc; doc["~"] = Mqtt::base(); doc["uniq_id"] = uniq_id_s; @@ -1272,19 +1271,19 @@ std::string Mqtt::tag_to_topic(uint8_t device_type, uint8_t tag) { } // adds availability, dev, ids to the config section to HA Discovery config -void Mqtt::add_ha_sections_to_doc(const std::string & name, - const char * state_t, - JsonDocument & config, - const bool is_first, - const char * cond1, - const char * cond2, - const char * negcond) { +void Mqtt::add_ha_sections_to_doc(const char * name, + const char * state_t, + JsonDocument & config, + const bool is_first, + const char * cond1, + const char * cond2, + const char * negcond) { // adds dev section to HA Discovery config - if (!name.empty()) { - JsonObject dev = config.add(); - auto cap_name = name; + if (name != nullptr) { + JsonObject dev = config["dev"].to(); + char * cap_name = strdup(name); cap_name[0] = toupper(name[0]); // capitalize first letter - dev["name"] = Mqtt::basename() + " " + cap_name; + dev["name"] = std::string(Mqtt::basename()) + " " + cap_name; // if it's the first in the category, attach the group to the main HA device if (is_first) { dev["mf"] = "EMS-ESP"; @@ -1293,6 +1292,7 @@ void Mqtt::add_ha_sections_to_doc(const std::string & name, } JsonArray ids = dev["ids"].to(); ids.add(Mqtt::basename() + "-" + Helpers::toLower(name)); + free(cap_name); } // adds "availability" section to HA Discovery config diff --git a/src/mqtt.h b/src/mqtt.h index 00ec99b27..235a4cd68 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -217,13 +217,13 @@ class Mqtt { static std::string tag_to_topic(uint8_t device_type, uint8_t tag); - static void add_ha_sections_to_doc(const std::string & name, - const char * state_t, - JsonDocument & config, - const bool is_first = false, - const char * cond1 = nullptr, - const char * cond2 = nullptr, - const char * negcond = nullptr); + static void add_ha_sections_to_doc(const char * name, + const char * state_t, + JsonDocument & config, + const bool is_first = false, + const char * cond1 = nullptr, + const char * cond2 = nullptr, + const char * negcond = nullptr); private: static uuid::log::Logger logger_; diff --git a/src/temperaturesensor.cpp b/src/temperaturesensor.cpp index a6b60a7ef..a38c57d3b 100644 --- a/src/temperaturesensor.cpp +++ b/src/temperaturesensor.cpp @@ -376,7 +376,7 @@ bool TemperatureSensor::command_info(const char * value, const int8_t id, JsonOb for (const auto & sensor : sensors_) { char val[10]; if (id == -1) { // show number and id, info command - JsonObject dataSensor = output[sensor.name()].add(); + JsonObject dataSensor = output[sensor.name()].to(); dataSensor["id"] = sensor.id(); dataSensor["uom"] = EMSdevice::uom_to_string(DeviceValueUOM::DEGREES); dataSensor["type"] = F_(number); @@ -492,23 +492,12 @@ void TemperatureSensor::publish_values(const bool force) { JsonDocument doc; - // used to see if we need to create the [devs] discovery section, as this needs only to be done once for all sensors - bool is_first_ha = true; - if (Mqtt::ha_enabled()) { - for (auto & sensor : sensors_) { - if (sensor.ha_registered) { - is_first_ha = false; - break; - } - } - } - for (auto & sensor : sensors_) { bool has_value = Helpers::hasValue(sensor.temperature_c); if (has_value) { char val[10]; if (Mqtt::is_nested()) { - JsonObject dataSensor = doc[sensor.id()].add(); + JsonObject dataSensor = doc[sensor.id()].to(); dataSensor["name"] = sensor.name(); dataSensor["temp"] = serialized(Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0)); } else { @@ -566,7 +555,16 @@ void TemperatureSensor::publish_values(const bool force) { snprintf(name, sizeof(name), "%s", sensor.name().c_str()); config["name"] = name; - Mqtt::add_ha_sections_to_doc("temperature", stat_t, config, is_first_ha, val_cond); + // see if we need to create the [devs] discovery section, as this needs only to be done once for all sensors + bool is_ha_device_created = false; + for (auto & sensor : sensors_) { + if (sensor.ha_registered) { + is_ha_device_created = true; + break; + } + } + + Mqtt::add_ha_sections_to_doc("temperature", stat_t, config, !is_ha_device_created, val_cond); char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; // use '_' as HA doesn't like '-' in the topic name diff --git a/src/web/WebCustomEntityService.cpp b/src/web/WebCustomEntityService.cpp index e1d280056..cfe1c9488 100644 --- a/src/web/WebCustomEntityService.cpp +++ b/src/web/WebCustomEntityService.cpp @@ -353,9 +353,11 @@ void WebCustomEntityService::publish(const bool force) { if (force) { ha_registered_ = false; } + if (!Mqtt::enabled()) { return; } + EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; }); if (customEntityItems->size() == 0) { return; @@ -369,6 +371,7 @@ void WebCustomEntityService::publish(const bool force) { JsonDocument doc; JsonObject output = doc.to(); bool ha_created = ha_registered_; + for (const CustomEntityItem & entityItem : *customEntityItems) { render_value(output, entityItem); // create HA config @@ -392,6 +395,7 @@ void WebCustomEntityService::publish(const bool force) { config["name"] = entityItem.name.c_str(); char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; + if (entityItem.writeable) { if (entityItem.value_type == DeviceValueType::BOOL) { snprintf(topic, sizeof(topic), "switch/%s/custom_%s/config", Mqtt::basename().c_str(), entityItem.name.c_str()); @@ -412,6 +416,7 @@ void WebCustomEntityService::publish(const bool force) { snprintf(topic, sizeof(topic), "sensor/%s/custom_%s/config", Mqtt::basename().c_str(), entityItem.name.c_str()); } } + if (entityItem.value_type == DeviceValueType::BOOL) { // applies to both Binary Sensor (read only) and a Switch (for a command) if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { @@ -426,11 +431,13 @@ void WebCustomEntityService::publish(const bool force) { config["pl_off"] = Helpers::render_boolean(result, false); } } - Mqtt::add_ha_sections_to_doc("custom", stat_t, config, true, val_cond); + + Mqtt::add_ha_sections_to_doc("custom", stat_t, config, !ha_created, val_cond); ha_created |= Mqtt::queue_ha(topic, config.as()); } } + ha_registered_ = ha_created; if (output.size() > 0) { Mqtt::queue_publish("custom_data", output); diff --git a/src/web/WebCustomizationService.cpp b/src/web/WebCustomizationService.cpp index 35232782c..0eb72e688 100644 --- a/src/web/WebCustomizationService.cpp +++ b/src/web/WebCustomizationService.cpp @@ -58,6 +58,7 @@ WebCustomizationService::WebCustomizationService(AsyncWebServer * server, FS * f void WebCustomization::read(WebCustomization & customizations, JsonObject & root) { // Temperature Sensor customization JsonArray sensorsJson = root["ts"].to(); + for (const SensorCustomization & sensor : customizations.sensorCustomizations) { JsonObject sensorJson = sensorsJson.add(); sensorJson["id"] = sensor.id; // ID of chip diff --git a/src/web/WebSchedulerService.cpp b/src/web/WebSchedulerService.cpp index 5409713e6..ae85767b1 100644 --- a/src/web/WebSchedulerService.cpp +++ b/src/web/WebSchedulerService.cpp @@ -113,6 +113,7 @@ bool WebSchedulerService::command_setvalue(const char * value, const std::string if (!Helpers::value2bool(value, v)) { return false; } + EMSESP::webSchedulerService.read([&](WebScheduler & webScheduler) { scheduleItems = &webScheduler.scheduleItems; }); for (ScheduleItem & scheduleItem : *scheduleItems) { if (scheduleItem.name == name) { @@ -143,9 +144,11 @@ bool WebSchedulerService::get_value_info(JsonObject & output, const char * cmd) } return true; } + if (scheduleItems->size() == 0) { return true; } + if (strlen(cmd) == 0 || Helpers::toLower(cmd) == F_(values) || Helpers::toLower(cmd) == F_(info)) { // list all names for (const ScheduleItem & scheduleItem : *scheduleItems) { @@ -162,6 +165,7 @@ bool WebSchedulerService::get_value_info(JsonObject & output, const char * cmd) } return (output.size() > 0); } + char command_s[30]; strlcpy(command_s, cmd, sizeof(command_s)); char * attribute_s = nullptr; @@ -172,6 +176,7 @@ bool WebSchedulerService::get_value_info(JsonObject & output, const char * cmd) *breakp = '\0'; attribute_s = breakp + 1; } + JsonVariant data; for (const ScheduleItem & scheduleItem : *scheduleItems) { if (Helpers::toLower(scheduleItem.name) == Helpers::toLower(command_s)) { @@ -192,14 +197,17 @@ bool WebSchedulerService::get_value_info(JsonObject & output, const char * cmd) output["visible"] = true; } } + if (attribute_s && output.containsKey(attribute_s)) { data = output[attribute_s]; output.clear(); output["api_data"] = data; } + if (output.size()) { return true; } + output["message"] = "unknown command"; return false; } @@ -209,12 +217,14 @@ void WebSchedulerService::publish_single(const char * name, const bool state) { if (!Mqtt::publish_single() || name == nullptr || name[0] == '\0') { return; } + char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; if (Mqtt::publish_single2cmd()) { snprintf(topic, sizeof(topic), "%s/%s", F_(scheduler), name); } else { snprintf(topic, sizeof(topic), "%s%s/%s", F_(scheduler), "_data", name); } + char payload[12]; Mqtt::queue_publish(topic, Helpers::render_boolean(payload, state)); } @@ -224,13 +234,16 @@ void WebSchedulerService::publish(const bool force) { if (force) { ha_registered_ = false; } + if (!Mqtt::enabled()) { return; } + EMSESP::webSchedulerService.read([&](WebScheduler & webScheduler) { scheduleItems = &webScheduler.scheduleItems; }); if (scheduleItems->size() == 0) { return; } + if (Mqtt::publish_single() && force) { for (const ScheduleItem & scheduleItem : *scheduleItems) { publish_single(scheduleItem.name.c_str(), scheduleItem.active); @@ -272,9 +285,11 @@ void WebSchedulerService::publish(const bool force) { char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; char command_topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; + snprintf(topic, sizeof(topic), "switch/%s/scheduler_%s/config", Mqtt::basename().c_str(), scheduleItem.name.c_str()); snprintf(command_topic, sizeof(command_topic), "%s/scheduler/%s", Mqtt::base().c_str(), scheduleItem.name.c_str()); config["cmd_t"] = command_topic; + if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { config["pl_on"] = true; config["pl_off"] = false; @@ -287,7 +302,7 @@ void WebSchedulerService::publish(const bool force) { config["pl_off"] = Helpers::render_boolean(result, false); } - Mqtt::add_ha_sections_to_doc("scheduler", stat_t, config, true, val_cond); + Mqtt::add_ha_sections_to_doc("scheduler", stat_t, config, !ha_created, val_cond); ha_created |= Mqtt::queue_ha(topic, config.as()); } @@ -304,6 +319,7 @@ bool WebSchedulerService::has_commands() { if (scheduleItems->size() == 0) { return false; } + for (const ScheduleItem & scheduleItem : *scheduleItems) { if (!scheduleItem.name.empty()) { return true; @@ -347,6 +363,7 @@ bool WebSchedulerService::command(const char * cmd, const char * data) { } else { snprintf(error, sizeof(error), "Scheduled command %s failed with error code (%s)", cmd, Command::return_code_string(return_code).c_str()); } + emsesp::EMSESP::logger().err(error); return false; }