mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-16 20:59:51 +03:00
fix standalone/make, fix HA avty, fix deprecated arduinojson, update packages
This commit is contained in:
@@ -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>());
|
||||
}
|
||||
|
||||
@@ -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] != '.') {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, "");
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user