fix standalone/make, fix HA avty, fix deprecated arduinojson, update packages

This commit is contained in:
proddy
2025-12-11 23:52:44 +01:00
parent 8bd1cd03b4
commit 55b8c2d04c
18 changed files with 535 additions and 488 deletions

View File

@@ -792,20 +792,15 @@ void AnalogSensor::publish_values(const bool force) {
}
// 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 const & sensor : sensors_) {
if (sensor.ha_registered) {
is_ha_device_created = true;
break;
}
if (std::none_of(sensors_.begin(), sensors_.end(), [](const auto & sensor) { return sensor.ha_registered; })) {
Mqtt::add_ha_dev_section(config.as<JsonObject>(), "Analog Sensors", nullptr, nullptr, nullptr, false);
}
// add default_entity_id
std::string topic_str(topic);
doc["def_ent_id"] = topic_str.substr(0, topic_str.find("/")) + "." + uniq_s;
Mqtt::add_ha_dev_section(config.as<JsonObject>(), "Analog Sensors", nullptr, nullptr, nullptr, false);
Mqtt::add_ha_avail_section(config.as<JsonObject>(), stat_t, !is_ha_device_created, val_cond);
Mqtt::add_ha_avty_section(config.as<JsonObject>(), stat_t, val_cond);
sensor.ha_registered = Mqtt::queue_ha(topic, config.as<JsonObject>());
}

View File

@@ -237,12 +237,20 @@ const char * Command::parse_command_string(const char * command, int8_t & id) {
const char * cmd_org = command;
int8_t id_org = id;
// convert cmd to lowercase and compare
char * lowerCmd = strdup(command);
for (char * p = lowerCmd; *p; p++) {
*p = tolower(*p);
// Optimized: Use stack buffer instead of strdup() to avoid heap allocation
// Most command strings are short, 64 bytes is more than enough
char lowerCmd[64];
size_t len = strlen(command);
if (len >= sizeof(lowerCmd)) {
len = sizeof(lowerCmd) - 1; // truncate if too long (rare case)
}
// Convert to lowercase in place using stack buffer
for (size_t i = 0; i < len; i++) {
lowerCmd[i] = tolower(command[i]);
}
lowerCmd[len] = '\0';
// check prefix and valid number range, also check 'id'
if (!strncmp(lowerCmd, "hc", 2) && command[2] >= '1' && command[2] <= '8') {
id = command[2] - '0';
@@ -279,7 +287,7 @@ const char * Command::parse_command_string(const char * command, int8_t & id) {
command += 3;
}
free(lowerCmd);
// No free() needed - stack buffer is automatically cleaned up
// return original if no seperator
if (command[0] != '/' && command[0] != '.') {

View File

@@ -848,7 +848,7 @@ std::string EMSESP::device_tostring(const uint8_t device_id) {
}
}
// created a pretty print telegram as a text string
// create a pretty print telegram as a text string
// e.g. Boiler(0x08) -> Me(0x0B), Version(0x02), data: 7B 06 01 00 00 00 00 00 00 04 (offset 1)
std::string EMSESP::pretty_telegram(std::shared_ptr<const Telegram> telegram) {
uint8_t src = telegram->src & 0x7F;
@@ -950,27 +950,58 @@ std::string EMSESP::pretty_telegram(std::shared_ptr<const Telegram> telegram) {
}
}
std::string str;
str.reserve(200);
// Optimized: Use stack buffer and build string once to avoid multiple temporary allocations
char buf[250];
if (telegram->operation == Telegram::Operation::RX_READ) {
str = src_name + "(" + Helpers::hextoa(src) + ") R " + dest_name + "(" + Helpers::hextoa(dest) + "), " + type_name + "("
+ Helpers::hextoa(telegram->type_id) + "), length: " + Helpers::itoa(telegram->message_data[0])
+ ((telegram->message_length > 1) ? ", data: " + Helpers::data_to_hex(telegram->message_data + 1, telegram->message_length - 1) : "");
auto pos = snprintf(buf,
sizeof(buf),
"%s(%s) R %s(%s), %s(%s), length: %d",
src_name.c_str(),
Helpers::hextoa(src).c_str(),
dest_name.c_str(),
Helpers::hextoa(dest).c_str(),
type_name.c_str(),
Helpers::hextoa(telegram->type_id).c_str(),
telegram->message_data[0]);
if (telegram->message_length > 1 && pos > 0 && pos < (int)sizeof(buf)) {
std::string data_hex = Helpers::data_to_hex(telegram->message_data + 1, telegram->message_length - 1);
snprintf(buf + pos, sizeof(buf) - pos, ", data: %s", data_hex.c_str());
}
} else if (telegram->dest == 0) {
str = src_name + "(" + Helpers::hextoa(src) + ") B " + dest_name + "(" + Helpers::hextoa(dest) + "), " + type_name + "("
+ Helpers::hextoa(telegram->type_id) + "), data: " + telegram->to_string_message();
snprintf(buf,
sizeof(buf),
"%s(%s) B %s(%s), %s(%s), data: %s",
src_name.c_str(),
Helpers::hextoa(src).c_str(),
dest_name.c_str(),
Helpers::hextoa(dest).c_str(),
type_name.c_str(),
Helpers::hextoa(telegram->type_id).c_str(),
telegram->to_string_message().c_str());
} else {
str = src_name + "(" + Helpers::hextoa(src) + ") W " + dest_name + "(" + Helpers::hextoa(dest) + "), " + type_name + "("
+ Helpers::hextoa(telegram->type_id) + "), data: " + telegram->to_string_message();
snprintf(buf,
sizeof(buf),
"%s(%s) W %s(%s), %s(%s), data: %s",
src_name.c_str(),
Helpers::hextoa(src).c_str(),
dest_name.c_str(),
Helpers::hextoa(dest).c_str(),
type_name.c_str(),
Helpers::hextoa(telegram->type_id).c_str(),
telegram->to_string_message().c_str());
}
if (offset) {
str += " (offset " + Helpers::itoa(offset) + ")";
size_t len = strlen(buf);
if (len < sizeof(buf) - 20) {
snprintf(buf + len, sizeof(buf) - len, " (offset %d)", offset);
}
}
return str;
return std::string(buf);
}
/*
* Type 0x07 - UBADevices - shows us the connected EMS devices
* e.g. 08 00 07 00 0B 80 00 00 00 00 00 00 00 00 00 00 00

View File

@@ -34,12 +34,19 @@ char * Helpers::hextoa(char * result, const uint8_t value) {
}
// same as hextoa but uses to a hex std::string
// Optimized: Avoid string concatenation to reduce temporary allocations
std::string Helpers::hextoa(const uint8_t value, bool prefix) {
char buf[3];
if (prefix) {
return std::string("0x") + hextoa(buf, value);
char buf[5]; // "0x" + 2 hex chars + null
buf[0] = '0';
buf[1] = 'x';
hextoa(&buf[2], value);
return std::string(buf);
} else {
char buf[3];
hextoa(buf, value);
return std::string(buf);
}
return std::string(hextoa(buf, value));
}
// same for 16 bit values
@@ -53,12 +60,19 @@ char * Helpers::hextoa(char * result, const uint16_t value) {
}
// same as above but to a hex string
// Optimized: Avoid string concatenation to reduce temporary allocations
std::string Helpers::hextoa(const uint16_t value, bool prefix) {
char buf[5];
if (prefix) {
return std::string("0x") + hextoa(buf, value);
char buf[7]; // "0x" + 4 hex chars + null
buf[0] = '0';
buf[1] = 'x';
hextoa(&buf[2], value);
return std::string(buf);
} else {
char buf[5];
hextoa(buf, value);
return std::string(buf);
}
return std::string(hextoa(buf, value));
}
#ifdef EMSESP_STANDALONE
@@ -100,29 +114,34 @@ char * Helpers::ultostr(char * ptr, uint32_t value, const uint8_t base) {
// fast itoa returning a std::string
// http://www.strudel.org.uk/itoa/
// Optimized: Use stack buffer to avoid heap allocation, then create string once
std::string Helpers::itoa(int16_t value) {
std::string buf;
buf.reserve(25); // Pre-allocate enough space.
int quotient = value;
// int16_t max: -32768 to 32767 = max 6 chars + null
char buf[8];
char * p = buf + sizeof(buf) - 1;
*p = '\0';
bool negative = value < 0;
int32_t abs_val = negative ? -(int32_t)value : value; // cast to int32 to handle -32768
// Build string in reverse
do {
buf += "0123456789abcdef"[std::abs(quotient % 10)];
quotient /= 10;
} while (quotient);
*--p = '0' + (abs_val % 10);
abs_val /= 10;
} while (abs_val > 0);
// Append the negative sign
if (value < 0)
buf += '-';
if (negative) {
*--p = '-';
}
std::reverse(buf.begin(), buf.end());
return buf;
return std::string(p);
}
/*
* fast itoa
* written by Lukás Chmela, Released under GPLv3. http://www.strudel.org.uk/itoa/ version 0.4
* optimized for ESP32
*/
* fast itoa
* written by Lukás Chmela, Released under GPLv3. http://www.strudel.org.uk/itoa/ version 0.4
* optimized for ESP32
*/
char * Helpers::itoa(int32_t value, char * result, const uint8_t base) {
// check that the base if valid
if (base < 2 || base > 36) {
@@ -470,25 +489,26 @@ char * Helpers::utf8tolatin1(char * result, const char * c, const uint8_t len) {
*p = '\0'; // terminate result
return result;
}
// creates string of hex values from an array of bytes
std::string Helpers::data_to_hex(const uint8_t * data, const uint8_t length) {
if (length == 0) {
return "<empty>";
}
std::string str;
str.reserve(length * 3 + 1);
char str[length * 3];
memset(str, 0, sizeof(str));
char buffer[4];
char buffer[4];
char * p = &str[0];
for (uint8_t i = 0; i < length; i++) {
str.append(Helpers::hextoa(buffer, data[i]));
str.push_back(' ');
Helpers::hextoa(buffer, data[i]);
*p++ = buffer[0];
*p++ = buffer[1];
*p++ = ' '; // space
}
if (!str.empty()) {
str.pop_back();
}
return str;
*--p = '\0'; // null terminate just in case, loosing the trailing space
return std::string(str);
}
// takes a hex string and convert it to an unsigned 32bit number (max 8 hex digits)

View File

@@ -596,15 +596,17 @@ void Mqtt::ha_status() {
// add sub or pub task to the queue.
// the base is not included in the topic
bool Mqtt::queue_message(const uint8_t operation, const std::string & topic, const std::string & payload, const bool retain) {
if (!mqtt_enabled_ || topic.empty() || !connected()) {
return false; // quit, not using MQTT
}
if (topic == "response" && operation == Operation::PUBLISH) {
lastresponse_ = payload;
if (!send_response_) {
return true;
}
}
if (!mqtt_enabled_ || topic.empty() || !connected()) {
return false; // quit, not using MQTT
}
// check free mem
#ifndef EMSESP_STANDALONE
// if (ESP.getFreeHeap() < 60 * 1024 || ESP.getMaxAllocHeap() < 40 * 1024) {
@@ -1094,10 +1096,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
// don't bother with value template conditions if using Domoticz which doesn't fully support MQTT Discovery
if (discovery_type() == discoveryType::HOMEASSISTANT) {
doc["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + " else " + sample_val + "}}";
// adds availability, dev, ids to the config section to HA Discovery config
// except for commands
add_ha_avail_section(doc.as<JsonObject>(), stat_t, false, val_cond);
add_ha_avty_section(doc.as<JsonObject>(), stat_t, val_cond); // adds availability section
} else {
// Domoticz doesn't support value templates, so we just use the value directly
// Also omit the uom and other state classes
@@ -1367,8 +1366,8 @@ bool Mqtt::publish_ha_climate_config(const int8_t tag, const bool has_roomtemp,
modes.add("heat");
modes.add("off");
add_ha_dev_section(doc.as<JsonObject>(), devicename, nullptr, nullptr, nullptr, false); // add dev section
add_ha_avail_section(doc.as<JsonObject>(), topic_t, false, seltemp_cond, has_roomtemp ? currtemp_cond : nullptr, hc_mode_cond); // add availability section
add_ha_dev_section(doc.as<JsonObject>(), devicename, nullptr, nullptr, nullptr, false); // add dev section
add_ha_avty_section(doc.as<JsonObject>(), topic_t, seltemp_cond, has_roomtemp ? currtemp_cond : nullptr, hc_mode_cond); // add availability section
return queue_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
}
@@ -1434,59 +1433,59 @@ void Mqtt::add_ha_dev_section(JsonObject doc, const char * name, const char * mo
}
}
// adds sections for HA Discovery to an existing JSON doc
// adds dev section with ids, name, mf, mdl, via_device
// adds optional availability section
void Mqtt::add_ha_avail_section(JsonObject doc, const char * state_t, const bool is_first, const char * cond1, const char * cond2, const char * negcond) {
// adds avty section for HA Discovery to an existing JSON doc
void Mqtt::add_ha_avty_section(JsonObject doc, const char * state_t, const char * cond1, const char * cond2, const char * negcond) {
// only works for HA
if (discovery_type() != discoveryType::HOMEASSISTANT) {
return;
}
// skip availability section if no conditions set
if (!cond1 && !cond2 && !negcond) {
return;
}
// adds "availability" section to HA Discovery config
JsonArray avty = doc["avty"].to<JsonArray>();
JsonDocument avty_json;
// make local copy of state, as the pointer will get de-referenced
char state[50];
strlcpy(state, state_t, sizeof(state));
if (state_t != nullptr) {
// make local copy of state, as the pointer will get de-referenced
char state[40];
strlcpy(state, state_t, sizeof(state));
char tpl[150];
const char * tpl_draft = "{{'online' if %s else 'offline'}}";
char tpl[150];
// condition 1
if (cond1 != nullptr) {
avty_json.clear();
avty_json["t"] = state;
snprintf(tpl, sizeof(tpl), tpl_draft, cond1);
avty_json["val_tpl"] = tpl;
avty.add(avty_json); // returns 0 if no mem
// condition 1
if (cond1 != nullptr) {
avty_json.clear();
avty_json["t"] = state;
snprintf(tpl, sizeof(tpl), "{{'online' if %s else 'offline'}}", cond1);
avty_json["val_tpl"] = tpl;
if (!avty.add(avty_json)) {
LOG_WARNING("Failed to add availability condition 1 (low memory)");
}
}
// condition 2
if (cond2 != nullptr) {
avty_json.clear();
avty_json["t"] = state;
snprintf(tpl, sizeof(tpl), "{{'online' if %s else 'offline'}}", cond2);
avty_json["val_tpl"] = tpl;
if (!avty.add(avty_json)) {
LOG_WARNING("Failed to add availability condition 2 (low memory)");
}
}
// negative condition
if (negcond != nullptr) {
avty_json.clear();
avty_json["t"] = state;
snprintf(tpl, sizeof(tpl), "{{'offline' if %s else 'online'}}", negcond);
avty_json["val_tpl"] = tpl;
if (!avty.add(avty_json)) {
LOG_WARNING("Failed to add negative availability condition (low memory)");
}
}
}
// condition 2
if (cond2 != nullptr) {
avty_json.clear();
avty_json["t"] = state;
snprintf(tpl, sizeof(tpl), tpl_draft, cond2);
avty_json["val_tpl"] = tpl;
avty.add(avty_json); // returns 0 if no mem
}
// negative condition
if (negcond != nullptr) {
avty_json.clear();
avty_json["t"] = state;
snprintf(tpl, sizeof(tpl), "{{'offline' if %s else 'online'}}", negcond);
avty_json["val_tpl"] = tpl;
avty.add(avty_json); // returns 0 if no mem
}
// add LWT (Last Will and Testament)
// always add LWT (Last Will and Testament)
avty_json.clear();
avty_json["t"] = "~/status"; // as a topic
avty.add(avty_json);

View File

@@ -256,12 +256,11 @@ class Mqtt {
static void
add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8_t type, const uint8_t uom, const char * entity = nullptr, bool is_discovery = true);
static void add_ha_dev_section(JsonObject doc, const char * name, const char * model, const char * brand, const char * version, const bool create_model);
static void add_ha_avail_section(JsonObject doc,
const char * state_t,
const bool is_first,
const char * cond1 = nullptr,
const char * cond2 = nullptr,
const char * negcond = nullptr);
static void add_ha_avty_section(JsonObject doc,
const char * state_t = nullptr,
const char * cond1 = nullptr,
const char * cond2 = nullptr,
const char * negcond = nullptr);
static void add_ha_bool(JsonObject doc);
static void add_value_bool(JsonObject doc, const char * name, bool value);

View File

@@ -212,7 +212,7 @@ void Shower::create_ha_discovery() {
Mqtt::add_ha_bool(doc.as<JsonObject>());
Mqtt::add_ha_dev_section(doc.as<JsonObject>(), "Shower Sensor", nullptr, nullptr, nullptr, false);
Mqtt::add_ha_avail_section(doc.as<JsonObject>(), "~/shower_active", true); // no conditions
Mqtt::add_ha_avty_section(doc.as<JsonObject>()); // no conditions
snprintf(topic, sizeof(topic), "binary_sensor/%s/shower_active/config", Mqtt::basename().c_str());
ha_configdone_ = Mqtt::queue_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
@@ -240,7 +240,7 @@ void Shower::create_ha_discovery() {
// doc["ent_cat"] = "diagnostic";
Mqtt::add_ha_dev_section(doc.as<JsonObject>(), "Shower Sensor", nullptr, nullptr, nullptr, false);
Mqtt::add_ha_avail_section(doc.as<JsonObject>(), "~/shower_data", false, "value_json.duration is defined");
Mqtt::add_ha_avty_section(doc.as<JsonObject>(), "~/shower_data", "value_json.duration is defined");
snprintf(topic, sizeof(topic), "sensor/%s/shower_duration/config", Mqtt::basename().c_str());
Mqtt::queue_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag

View File

@@ -1565,7 +1565,7 @@ void System::get_value_json(JsonObject output, const std::string & circuit, cons
// generate Prometheus metrics format from system values
std::string System::get_metrics_prometheus() {
std::string result;
std::string result;
std::unordered_map<std::string, bool> seen_metrics;
// get system data
@@ -1633,156 +1633,155 @@ std::string System::get_metrics_prometheus() {
};
// helper function to process a JSON object recursively
std::function<void(const JsonObject &, const std::string &)> process_object =
[&](const JsonObject & obj, const std::string & prefix) {
std::vector<std::pair<std::string, std::string>> local_info_labels;
bool has_nested_objects = false;
std::function<void(const JsonObject &, const std::string &)> process_object = [&](const JsonObject & obj, const std::string & prefix) {
std::vector<std::pair<std::string, std::string>> local_info_labels;
bool has_nested_objects = false;
for (JsonPair p : obj) {
std::string key = p.key().c_str();
std::string path = prefix.empty() ? key : prefix + "." + key;
std::string metric_name = prefix.empty() ? key : prefix + "_" + key;
for (JsonPair p : obj) {
std::string key = p.key().c_str();
std::string path = prefix.empty() ? key : prefix + "." + key;
std::string metric_name = prefix.empty() ? key : prefix + "_" + key;
if (should_ignore(prefix, key)) {
continue;
}
if (should_ignore(prefix, key)) {
continue;
}
if (p.value().is<JsonObject>()) {
// recursive call for nested objects
has_nested_objects = true;
process_object(p.value().as<JsonObject>(), metric_name);
} else if (p.value().is<JsonArray>()) {
// handle arrays (devices)
if (key == "devices") {
JsonArray devices = p.value().as<JsonArray>();
for (JsonObject device : devices) {
std::vector<std::pair<std::string, std::string>> device_labels;
if (p.value().is<JsonObject>()) {
// recursive call for nested objects
has_nested_objects = true;
process_object(p.value().as<JsonObject>(), metric_name);
} else if (p.value().is<JsonArray>()) {
// handle arrays (devices)
if (key == "devices") {
JsonArray devices = p.value().as<JsonArray>();
for (JsonObject device : devices) {
std::vector<std::pair<std::string, std::string>> device_labels;
// collect labels from device object
for (JsonPair dp : device) {
std::string dkey = dp.key().c_str();
if (dkey == "type" || dkey == "name" || dkey == "deviceID" || dkey == "brand" || dkey == "version") {
if (dp.value().is<const char *>()) {
std::string val = dp.value().as<const char *>();
if (!val.empty()) {
device_labels.push_back({to_lowercase(dkey), val});
}
// collect labels from device object
for (JsonPair dp : device) {
std::string dkey = dp.key().c_str();
if (dkey == "type" || dkey == "name" || dkey == "deviceID" || dkey == "brand" || dkey == "version") {
if (dp.value().is<const char *>()) {
std::string val = dp.value().as<const char *>();
if (!val.empty()) {
device_labels.push_back({to_lowercase(dkey), val});
}
}
}
}
// create productID metric
if (device.containsKey("productID") && device["productID"].is<int>()) {
std::string metric = "emsesp_device_productid";
if (seen_metrics.find(metric) == seen_metrics.end()) {
result += "# HELP emsesp_device_productid productID\n";
result += "# TYPE emsesp_device_productid gauge\n";
seen_metrics[metric] = true;
}
result += metric;
if (!device_labels.empty()) {
result += "{";
bool first = true;
for (const auto & label : device_labels) {
if (!first) {
result += ", ";
}
result += label.first + "=\"" + escape_label(label.second) + "\"";
first = false;
}
result += "}";
}
result += " " + std::to_string(device["productID"].as<int>()) + "\n";
// create productID metric
if (device["productID"].is<int>()) {
std::string metric = "emsesp_device_productid";
if (seen_metrics.find(metric) == seen_metrics.end()) {
result += "# HELP emsesp_device_productid productID\n";
result += "# TYPE emsesp_device_productid gauge\n";
seen_metrics[metric] = true;
}
// create entities metric
if (device.containsKey("entities") && device["entities"].is<int>()) {
std::string metric = "emsesp_device_entities";
if (seen_metrics.find(metric) == seen_metrics.end()) {
result += "# HELP emsesp_device_entities entities\n";
result += "# TYPE emsesp_device_entities gauge\n";
seen_metrics[metric] = true;
}
result += metric;
if (!device_labels.empty()) {
result += "{";
bool first = true;
for (const auto & label : device_labels) {
if (!first) {
result += ", ";
}
result += label.first + "=\"" + escape_label(label.second) + "\"";
first = false;
result += metric;
if (!device_labels.empty()) {
result += "{";
bool first = true;
for (const auto & label : device_labels) {
if (!first) {
result += ", ";
}
result += "}";
result += label.first + "=\"" + escape_label(label.second) + "\"";
first = false;
}
result += " " + std::to_string(device["entities"].as<int>()) + "\n";
result += "}";
}
result += " " + std::to_string(device["productID"].as<int>()) + "\n";
}
// create entities metric
if (device["entities"].is<int>()) {
std::string metric = "emsesp_device_entities";
if (seen_metrics.find(metric) == seen_metrics.end()) {
result += "# HELP emsesp_device_entities entities\n";
result += "# TYPE emsesp_device_entities gauge\n";
seen_metrics[metric] = true;
}
result += metric;
if (!device_labels.empty()) {
result += "{";
bool first = true;
for (const auto & label : device_labels) {
if (!first) {
result += ", ";
}
result += label.first + "=\"" + escape_label(label.second) + "\"";
first = false;
}
result += "}";
}
result += " " + std::to_string(device["entities"].as<int>()) + "\n";
}
}
} else {
// handle primitive values
bool is_number = p.value().is<int>() || p.value().is<float>();
bool is_bool = p.value().is<bool>();
bool is_string = p.value().is<const char *>();
}
} else {
// handle primitive values
bool is_number = p.value().is<int>() || p.value().is<float>();
bool is_bool = p.value().is<bool>();
bool is_string = p.value().is<const char *>();
if (is_number || is_bool) {
// add metric
std::string full_metric_name = "emsesp_" + sanitize_name(metric_name);
if (seen_metrics.find(full_metric_name) == seen_metrics.end()) {
result += "# HELP emsesp_" + sanitize_name(metric_name) + " " + key + "\n";
result += "# TYPE emsesp_" + sanitize_name(metric_name) + " gauge\n";
seen_metrics[full_metric_name] = true;
}
if (is_number || is_bool) {
// add metric
std::string full_metric_name = "emsesp_" + sanitize_name(metric_name);
if (seen_metrics.find(full_metric_name) == seen_metrics.end()) {
result += "# HELP emsesp_" + sanitize_name(metric_name) + " " + key + "\n";
result += "# TYPE emsesp_" + sanitize_name(metric_name) + " gauge\n";
seen_metrics[full_metric_name] = true;
}
result += full_metric_name + " ";
if (is_bool) {
result += p.value().as<bool>() ? "1" : "0";
} else if (p.value().is<int>()) {
result += std::to_string(p.value().as<int>());
} else {
char val_str[30];
snprintf(val_str, sizeof(val_str), "%.2f", p.value().as<float>());
result += val_str;
}
result += "\n";
} else if (is_string) {
// collect string for info metric (skip dynamic strings like uptime and timestamp)
std::string val = p.value().as<const char *>();
if (!val.empty() && key != "uptime" && key != "timestamp") {
local_info_labels.push_back({to_lowercase(key), val});
}
result += full_metric_name + " ";
if (is_bool) {
result += p.value().as<bool>() ? "1" : "0";
} else if (p.value().is<int>()) {
result += std::to_string(p.value().as<int>());
} else {
char val_str[30];
snprintf(val_str, sizeof(val_str), "%.2f", p.value().as<float>());
result += val_str;
}
result += "\n";
} else if (is_string) {
// collect string for info metric (skip dynamic strings like uptime and timestamp)
std::string val = p.value().as<const char *>();
if (!val.empty() && key != "uptime" && key != "timestamp") {
local_info_labels.push_back({to_lowercase(key), val});
}
}
}
}
// create _info metric for this object level if we have labels and this is a leaf node (no nested objects)
if (!local_info_labels.empty() && !prefix.empty() && !has_nested_objects) {
std::string info_metric = "emsesp_" + sanitize_name(prefix) + "_info";
if (seen_metrics.find(info_metric) == seen_metrics.end()) {
result += "# HELP " + info_metric + " info\n";
result += "# TYPE " + info_metric + " gauge\n";
seen_metrics[info_metric] = true;
}
result += info_metric;
if (!local_info_labels.empty()) {
result += "{";
bool first = true;
for (const auto & label : local_info_labels) {
if (!first) {
result += ", ";
}
result += label.first + "=\"" + escape_label(label.second) + "\"";
first = false;
}
result += "}";
}
result += " 1\n";
// create _info metric for this object level if we have labels and this is a leaf node (no nested objects)
if (!local_info_labels.empty() && !prefix.empty() && !has_nested_objects) {
std::string info_metric = "emsesp_" + sanitize_name(prefix) + "_info";
if (seen_metrics.find(info_metric) == seen_metrics.end()) {
result += "# HELP " + info_metric + " info\n";
result += "# TYPE " + info_metric + " gauge\n";
seen_metrics[info_metric] = true;
}
};
result += info_metric;
if (!local_info_labels.empty()) {
result += "{";
bool first = true;
for (const auto & label : local_info_labels) {
if (!first) {
result += ", ";
}
result += label.first + "=\"" + escape_label(label.second) + "\"";
first = false;
}
result += "}";
}
result += " 1\n";
}
};
// process root object
process_object(root, "");

View File

@@ -336,7 +336,7 @@ bool TemperatureSensor::update(const char * id, const char * name, int16_t offse
sensor.set_is_system(is_system);
// store the new name and offset in our configuration
EMSESP::webCustomizationService.update([&id, &name, &offset, &sensor, &hide, &is_system](WebCustomization & settings) {
EMSESP::webCustomizationService.update([&id, &name, &offset, &sensor, &is_system](WebCustomization & settings) {
// look it up to see if it exists
bool found = false;
for (auto & SensorCustomization : settings.sensorCustomizations) {
@@ -544,16 +544,11 @@ void TemperatureSensor::publish_values(const bool force) {
config["name"] = sensor.name();
// 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 (const auto & sensor : sensors_) {
if (sensor.ha_registered) {
is_ha_device_created = true;
break;
}
if (std::none_of(sensors_.begin(), sensors_.end(), [](const auto & sensor) { return sensor.ha_registered; })) {
Mqtt::add_ha_dev_section(config.as<JsonObject>(), "Temperature Sensors", nullptr, nullptr, nullptr, false);
}
Mqtt::add_ha_dev_section(config.as<JsonObject>(), "Temperature Sensors", nullptr, nullptr, nullptr, false);
Mqtt::add_ha_avail_section(config.as<JsonObject>(), stat_t, !is_ha_device_created, val_cond);
Mqtt::add_ha_avty_section(config.as<JsonObject>(), stat_t, val_cond);
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(temperaturesensor), sensor.id());

View File

@@ -464,8 +464,11 @@ void WebCustomEntityService::publish(const bool force) {
config["def_ent_id"] = topic_str.substr(0, topic_str.find("/")) + "." + uniq_s;
Mqtt::add_ha_classes(config.as<JsonObject>(), EMSdevice::DeviceType::SYSTEM, entityItem.value_type, entityItem.uom);
Mqtt::add_ha_dev_section(config.as<JsonObject>(), "Custom Entities", nullptr, nullptr, nullptr, false);
Mqtt::add_ha_avail_section(config.as<JsonObject>(), stat_t, !ha_created, val_cond);
if (!ha_created) {
Mqtt::add_ha_dev_section(config.as<JsonObject>(), "Custom Entities", nullptr, nullptr, nullptr, false);
}
Mqtt::add_ha_avty_section(config.as<JsonObject>(), stat_t, val_cond);
ha_created |= Mqtt::queue_ha(topic, config.as<JsonObject>());
}
@@ -700,13 +703,13 @@ void WebCustomEntityService::load_test_data() {
auto entityItem = CustomEntityItem();
// test 1
entityItem.id = 1;
entityItem.ram = 0;
entityItem.device_id = 8;
entityItem.type_id = 24;
entityItem.offset = 0;
entityItem.factor = 1;
strcpy(entityItem.name,"test_custom");
entityItem.id = 1;
entityItem.ram = 0;
entityItem.device_id = 8;
entityItem.type_id = 24;
entityItem.offset = 0;
entityItem.factor = 1;
strcpy(entityItem.name, "test_custom");
entityItem.uom = 1;
entityItem.value_type = 1;
entityItem.writeable = true;
@@ -723,12 +726,12 @@ void WebCustomEntityService::load_test_data() {
CommandFlag::ADMIN_ONLY);
// test 2
entityItem.id = 2;
entityItem.ram = 0;
entityItem.device_id = 24;
entityItem.type_id = 677;
entityItem.offset = 3;
entityItem.factor = 1;
entityItem.id = 2;
entityItem.ram = 0;
entityItem.device_id = 24;
entityItem.type_id = 677;
entityItem.offset = 3;
entityItem.factor = 1;
strcpy(entityItem.name, "test_read_only");
entityItem.uom = 0;
entityItem.value_type = 2;
@@ -737,12 +740,12 @@ void WebCustomEntityService::load_test_data() {
webCustomEntity.customEntityItems.push_back(entityItem);
// test 3
entityItem.id = 3;
entityItem.ram = 1;
entityItem.device_id = 0;
entityItem.type_id = 0;
entityItem.offset = 0;
entityItem.factor = 1;
entityItem.id = 3;
entityItem.ram = 1;
entityItem.device_id = 0;
entityItem.type_id = 0;
entityItem.offset = 0;
entityItem.factor = 1;
strcpy(entityItem.name, "test_ram");
entityItem.uom = 0;
entityItem.value_type = 8;
@@ -759,12 +762,12 @@ void WebCustomEntityService::load_test_data() {
CommandFlag::ADMIN_ONLY);
// test 4
entityItem.id = 4;
entityItem.ram = 1;
entityItem.device_id = 0;
entityItem.type_id = 0;
entityItem.offset = 0;
entityItem.factor = 1;
entityItem.id = 4;
entityItem.ram = 1;
entityItem.device_id = 0;
entityItem.type_id = 0;
entityItem.offset = 0;
entityItem.factor = 1;
strcpy(entityItem.name, "test_seltemp");
entityItem.uom = 0;
entityItem.value_type = 8;

View File

@@ -275,8 +275,10 @@ void WebSchedulerService::publish(const bool force) {
config["cmd_t"] = command_topic;
Mqtt::add_ha_bool(config.as<JsonObject>());
Mqtt::add_ha_dev_section(config.as<JsonObject>(), F_(scheduler), nullptr, nullptr, nullptr, false);
Mqtt::add_ha_avail_section(config.as<JsonObject>(), stat_t, !ha_created, val_cond);
if (!ha_created) {
Mqtt::add_ha_dev_section(config.as<JsonObject>(), F_(scheduler), nullptr, nullptr, nullptr, false);
}
Mqtt::add_ha_avty_section(config.as<JsonObject>(), stat_t, val_cond);
ha_created |= Mqtt::queue_ha(topic, config.as<JsonObject>());
}

View File

@@ -109,7 +109,7 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
bool ha_registered_ = false;
std::list<ScheduleItem, AllocatorPSRAM<ScheduleItem>> * scheduleItems_; // pointer to the list of schedule events
std::list<ScheduleItem *, AllocatorPSRAM<ScheduleItem *>> cmd_changed_; // pointer to commands in list that are triggert by change
std::list<ScheduleItem *, AllocatorPSRAM<ScheduleItem *>> cmd_changed_; // pointer to commands in list that are triggered by change
};
} // namespace emsesp