8 Commits

Author SHA1 Message Date
Proddy
f5af4fb52f Merge pull request #2975 from mrkev-gh/esp32s3-no-psram
fix allowed pins for S32S3 without PSRAM
2026-03-11 17:36:38 +01:00
mrkev-gh
96a7ea8a02 fix allowed pins for S32S3 without PSRAM
Some S32S3 do not have PSRAM (e.g. ESP32-S3FN8) and use those GPIO pins
2026-02-28 11:44:53 +01:00
Proddy
c05e1cb77b Merge pull request #2966 from MichaelDvP/dev
fixes for #2960 and #2962
2026-02-19 21:58:20 +01:00
MichaelDvP
5879ce4090 fix SRC mode setting from HA #2960 2026-02-18 08:14:47 +01:00
MichaelDvP
ac3e5c793c fix typo for SRC ha-climate creation 2026-02-17 10:09:22 +01:00
MichaelDvP
4326fb931b add prometheus metrics for analog/scheduler/custom #2962 2026-02-16 15:56:23 +01:00
MichaelDvP
ced7051ce7 add prometheus metrics for temperaturesensors 2026-02-16 12:05:45 +01:00
Proddy
e9f77c1bde Merge pull request #2954 from MichaelDvP/dev
fix brand in HA
2026-02-12 17:54:41 +01:00
13 changed files with 178 additions and 27 deletions

View File

@@ -9,6 +9,7 @@ For more details go to [emsesp.org](https://emsesp.org/).
- comfortpoint for BC400 [#2935](https://github.com/emsesp/EMS-ESP32/issues/2935) - comfortpoint for BC400 [#2935](https://github.com/emsesp/EMS-ESP32/issues/2935)
- customize device brand [#2784](https://github.com/emsesp/EMS-ESP32/issues/2784) - customize device brand [#2784](https://github.com/emsesp/EMS-ESP32/issues/2784)
- set model for ems-esp devices temperature, analog, etc. [#2958](https://github.com/emsesp/EMS-ESP32/discussions/2958) - set model for ems-esp devices temperature, analog, etc. [#2958](https://github.com/emsesp/EMS-ESP32/discussions/2958)
- prometheus metrics for temperature/analog/scheduler/custom [#2962](https://github.com/emsesp/EMS-ESP32/issues/2962)
## Fixed ## Fixed

View File

@@ -852,6 +852,15 @@ bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int
return true; return true;
} }
if (!strcmp(cmd, F_(metrics))) {
std::string metrics = get_metrics_prometheus();
if (!metrics.empty()) {
output["api_data"] = metrics;
return true;
}
return false;
}
// this is for a specific sensor, return the value // this is for a specific sensor, return the value
const char * attribute_s = Command::get_attribute(cmd); const char * attribute_s = Command::get_attribute(cmd);
@@ -866,6 +875,35 @@ bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int
return false; // not found return false; // not found
} }
// generate Prometheus metrics format from analog values
std::string AnalogSensor::get_metrics_prometheus() {
std::string result;
result.reserve(sensors_.size() * 140);
char val[10];
for (auto & sensor : sensors_) {
result += (std::string) "# HELP emsesp_" + sensor.name() + " " + sensor.name();
if (sensor.type() != AnalogType::DIGITAL_OUT && sensor.type() != AnalogType::DIGITAL_IN) {
result += (std::string) ", " + EMSdevice::uom_to_string(sensor.uom());
} else {
result += (std::string) ", boolean";
}
result += (std::string) ", readable, visible";
if (sensor.type() == AnalogType::COUNTER || sensor.type() == AnalogType::RGB || sensor.type() == AnalogType::PULSE
|| (sensor.type() >= AnalogType::DIGITAL_OUT && sensor.type() <= AnalogType::PWM_2)
|| (sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2)) {
result += (std::string) ", writable";
}
result += (std::string) "\n# TYPE emsesp_" + sensor.name() + " gauge\n";
result += (std::string) "emsesp_" + sensor.name() + " ";
if (sensor.type() != AnalogType::DIGITAL_OUT && sensor.type() != AnalogType::DIGITAL_IN) {
result += (std::string) Helpers::render_value(val, sensor.value(), 2) + "\n";
} else {
result += (std::string) (sensor.value() == 0 ? "0\n" : "1\n");
}
}
return result;
}
// note we don't add the device and state classes here, as we do in the custom entity service // note we don't add the device and state classes here, as we do in the custom entity service
void AnalogSensor::get_value_json(JsonObject output, const Sensor & sensor) { void AnalogSensor::get_value_json(JsonObject output, const Sensor & sensor) {
output["name"] = (const char *)sensor.name(); output["name"] = (const char *)sensor.name();

View File

@@ -177,6 +177,7 @@ class AnalogSensor {
bool update(uint8_t gpio, const char * name, double offset, double factor, uint8_t uom, int8_t type, bool deleted, bool is_system); bool update(uint8_t gpio, const char * name, double offset, double factor, uint8_t uom, int8_t type, bool deleted, bool is_system);
bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1); bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1);
void store_counters(); void store_counters();
std::string get_metrics_prometheus();
static std::vector<uint8_t> exclude_types() { static std::vector<uint8_t> exclude_types() {
return exclude_types_; return exclude_types_;
} }

View File

@@ -912,7 +912,7 @@ void EMSdevice::publish_value(void * value_p) const {
// looks up the UOM for a given key from the device value table // looks up the UOM for a given key from the device value table
std::string EMSdevice::get_value_uom(const std::string & shortname) const { std::string EMSdevice::get_value_uom(const std::string & shortname) const {
for (const auto & dv : devicevalues_) { for (const auto & dv : devicevalues_) {
if ((!dv.has_state(DeviceValueState::DV_WEB_EXCLUDE)) && (dv.short_name == shortname)) { if ((!dv.has_state(DeviceValueState::DV_WEB_EXCLUDE)) && !strcmp(dv.short_name, shortname.c_str())) {
// ignore TIME since "minutes" is already added to the string value // ignore TIME since "minutes" is already added to the string value
if ((dv.uom == DeviceValueUOM::NONE) || (dv.uom == DeviceValueUOM::MINUTES)) { if ((dv.uom == DeviceValueUOM::NONE) || (dv.uom == DeviceValueUOM::MINUTES)) {
break; break;
@@ -1282,7 +1282,7 @@ void EMSdevice::setCustomizationEntity(const std::string & entity_id) {
// set the min / max // set the min / max
dv.set_custom_minmax(); dv.set_custom_minmax();
if (Mqtt::ha_enabled() && dv.short_name == FL_(seltemp)[0] && (min != dv.min || max != dv.max)) { if (Mqtt::ha_enabled() && dv.tag <= DeviceValueTAG::TAG_HC8 && !strcmp(dv.short_name, FL_(selRoomTemp)[0]) && (min != dv.min || max != dv.max)) {
set_climate_minmax(dv.tag, dv.min, dv.max); set_climate_minmax(dv.tag, dv.min, dv.max);
} }
@@ -2166,8 +2166,8 @@ void EMSdevice::mqtt_ha_entity_config_create() {
count++; count++;
} }
// SRC thermostats mapped to connect/src1/... always contains mode, seltemp, currtemp // SRC thermostats mapped to connect/src1/... always contains mode, selRoomTemp, currtemp
if (dv.tag >= DeviceValueTAG::TAG_SRC1 && dv.tag <= DeviceValueTAG::TAG_SRC16 && !strcmp(dv.short_name, FL_(seltemp)[0])) { if (dv.tag >= DeviceValueTAG::TAG_SRC1 && dv.tag <= DeviceValueTAG::TAG_SRC16 && !strcmp(dv.short_name, FL_(selRoomTemp)[0])) {
// add modes and icon if we have one // add modes and icon if we have one
const char * icon = nullptr; const char * icon = nullptr;
const char * const ** mode_options = nullptr; const char * const ** mode_options = nullptr;

View File

@@ -2952,7 +2952,6 @@ void System::set_valid_system_gpios() {
// excluded: // excluded:
// GPIO3, GPIO45 - GPIO46 = strapping pins // GPIO3, GPIO45 - GPIO46 = strapping pins
// GPIO26 - GPIO32 = SPI flash and PSRAM and not recommended // GPIO26 - GPIO32 = SPI flash and PSRAM and not recommended
// GPIO33 - GPIO37 = Octal flash/PSRAM
// GPIO19 - GPIO20 = USB-JTAG // GPIO19 - GPIO20 = USB-JTAG
// GPIO22 - GPIO25 = don't exist // GPIO22 - GPIO25 = don't exist
// //
@@ -2960,7 +2959,12 @@ void System::set_valid_system_gpios() {
// GPIO11 - GPIO19 = ADC analog input only pins // GPIO11 - GPIO19 = ADC analog input only pins
// GPIO47 - GPIO48 = valid on a Wemos S3 // GPIO47 - GPIO48 = valid on a Wemos S3
// GPIO8 = used by Liligo S3 board profile for Rx // GPIO8 = used by Liligo S3 board profile for Rx
if (ESP.getPsramSize() > 0) {
// GPIO33 - GPIO37 = Octal flash/PSRAM
valid_system_gpios_ = string_range_to_vector("0-48", "3, 45-46, 26-32, 33-37, 19-20, 22-25"); valid_system_gpios_ = string_range_to_vector("0-48", "3, 45-46, 26-32, 33-37, 19-20, 22-25");
} else {
valid_system_gpios_ = string_range_to_vector("0-48", "3, 45-46, 26-32, 19-20, 22-25");
}
#elif CONFIG_IDF_TARGET_ESP32 #elif CONFIG_IDF_TARGET_ESP32
// https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/gpio.html // https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/gpio.html

View File

@@ -400,6 +400,15 @@ bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, cons
return true; return true;
} }
if (!strcmp(cmd, F_(metrics))) {
std::string metrics = get_metrics_prometheus();
if (!metrics.empty()) {
output["api_data"] = metrics;
return true;
}
return false;
}
// this is for a specific sensor // this is for a specific sensor
const char * attribute_s = Command::get_attribute(cmd); const char * attribute_s = Command::get_attribute(cmd);
@@ -414,6 +423,21 @@ bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, cons
return false; // not found return false; // not found
} }
// generate Prometheus metrics format from temperature values
std::string TemperatureSensor::get_metrics_prometheus() {
std::string result;
result.reserve(sensors_.size() * 120);
char val[10];
for (auto & sensor : sensors_) {
result += (std::string) "# HELP emsesp_" + sensor.name() + " " + sensor.name() + ", "
+ EMSdevice::uom_to_string(EMSESP::system_.fahrenheit() ? DeviceValueUOM::FAHRENHEIT : DeviceValueUOM::DEGREES) + ", readable, visible\n";
result += (std::string) "# TYPE emsesp_" + sensor.name() + " gauge\n";
result +=
(std::string) "emsesp_" + sensor.name() + " " + Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0) + "\n";
}
return result;
}
// note we don't add the device and state classes here, as we do in the custom entity service // note we don't add the device and state classes here, as we do in the custom entity service
void TemperatureSensor::get_value_json(JsonObject output, const Sensor & sensor) { void TemperatureSensor::get_value_json(JsonObject output, const Sensor & sensor) {
output["id"] = sensor.id(); output["id"] = sensor.id();

View File

@@ -96,6 +96,8 @@ class TemperatureSensor {
bool updated_values(); bool updated_values();
bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1); bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1);
std::string get_metrics_prometheus();
// return back reference to the sensor list, used by other classes // return back reference to the sensor list, used by other classes
std::vector<Sensor, AllocatorPSRAM<Sensor>> sensors() const { std::vector<Sensor, AllocatorPSRAM<Sensor>> sensors() const {
return sensors_; return sensors_;

View File

@@ -211,13 +211,14 @@ bool Connect::set_mode(const char * value, const int8_t id) {
return false; return false;
} }
uint8_t v; uint8_t v;
if (Helpers::value2enum(value, v, FL_(enum_mode2), {3, 1, 0})) { if (!Helpers::value2enum(value, v, FL_(enum_mode2), {3, 1, 0})) {
// if (Helpers::value2enum(value, v, FL_(enum_mode8))) { if (!Helpers::value2enum(value, v, FL_(enum_mode_ha), {3, 1, 0})) {
return false;
}
}
write_command(0xBB5 + rc->room(), 0, v); // no validate, mode change is broadcasted write_command(0xBB5 + rc->room(), 0, v); // no validate, mode change is broadcasted
return true; return true;
} }
return false;
}
bool Connect::set_seltemp(const char * value, const int8_t id) { bool Connect::set_seltemp(const char * value, const int8_t id) {
auto rc = room_circuit(id - DeviceValueTAG::TAG_SRC1); auto rc = room_circuit(id - DeviceValueTAG::TAG_SRC1);

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.8.2-dev.6" #define EMSESP_APP_VERSION "3.8.2-dev.7"

View File

@@ -343,6 +343,15 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd)
return true; return true;
} }
if (!strcmp(cmd, F_(metrics))) {
std::string metrics = get_metrics_prometheus();
if (!metrics.empty()) {
output["api_data"] = metrics;
return true;
}
return false;
}
// specific value info // specific value info
const char * attribute_s = Command::get_attribute(cmd); const char * attribute_s = Command::get_attribute(cmd);
for (auto const & entity : *customEntityItems_) { for (auto const & entity : *customEntityItems_) {
@@ -354,6 +363,54 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd)
return false; // not found return false; // not found
} }
// generate Prometheus metrics format from custom entities
std::string WebCustomEntityService::get_metrics_prometheus() {
std::string result;
result.reserve(customEntityItems_->size() * 140);
char val[10];
for (CustomEntityItem & entity : *customEntityItems_) {
if (entity.hide || entity.name[0] == '\0') {
continue;
}
result += (std::string) "# HELP emsesp_" + entity.name + " " + entity.name;
if (entity.uom != 0) {
result += (std::string) ", " + EMSdevice::uom_to_string(entity.uom);
}
result += (std::string) ", readable, visible" + (entity.writeable ? ", writable\n" : "\n");
result += (std::string) "# TYPE emsesp_" + entity.name + " gauge\n";
result += (std::string) "emsesp_" + entity.name + " ";
switch (entity.value_type) {
case DeviceValueType::BOOL:
result += (std::string)(entity.value == 0 ? "0" : "1");
break;
case DeviceValueType::INT8:
result += (std::string)Helpers::render_value(val, entity.factor * (int8_t)entity.value, 2);
break;
case DeviceValueType::UINT8:
result += (std::string)Helpers::render_value(val, entity.factor * (uint8_t)entity.value, 2);
break;
case DeviceValueType::INT16:
result += (std::string)Helpers::render_value(val, entity.factor * (int16_t)entity.value, 2);
break;
case DeviceValueType::UINT16:
result += (std::string)Helpers::render_value(val, entity.factor * (uint16_t)entity.value, 2);
break;
case DeviceValueType::UINT24:
case DeviceValueType::TIME:
case DeviceValueType::UINT32:
result += (std::string)Helpers::render_value(val, entity.factor * entity.value, 2);
break;
default:
if (entity.data.length() > 0) {
result += entity.data;
}
break;
}
result += (std::string) "\n";
}
return result;
}
// build the json for specific entity // build the json for specific entity
void WebCustomEntityService::get_value_json(JsonObject output, CustomEntityItem const & entity) { void WebCustomEntityService::get_value_json(JsonObject output, CustomEntityItem const & entity) {
output["name"] = (const char *)entity.name; output["name"] = (const char *)entity.name;

View File

@@ -68,6 +68,8 @@ class WebCustomEntityService : public StatefulService<WebCustomEntity> {
void show_values(JsonObject output); void show_values(JsonObject output);
void generate_value_web(JsonObject output, const bool is_dashboard = false); void generate_value_web(JsonObject output, const bool is_dashboard = false);
std::string get_metrics_prometheus();
uint8_t count_entities(); uint8_t count_entities();
void ha_reset() { void ha_reset() {
ha_configdone_ = false; ha_configdone_ = false;

View File

@@ -102,7 +102,7 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu
if (webScheduler.scheduleItems.back().name[0] != '\0') { if (webScheduler.scheduleItems.back().name[0] != '\0') {
char key[sizeof(webScheduler.scheduleItems.back().name) + 2]; char key[sizeof(webScheduler.scheduleItems.back().name) + 2];
snprintf(key, sizeof(key), "s:%s", webScheduler.scheduleItems.back().name); snprintf(key, sizeof(key), "s:%s", webScheduler.scheduleItems.back().name);
if (EMSESP::nvs_.isKey(key)) { if (EMSESP::nvs_.isKey(key) && webScheduler.scheduleItems.back().flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
webScheduler.scheduleItems.back().active = EMSESP::nvs_.getBool(key); webScheduler.scheduleItems.back().active = EMSESP::nvs_.getBool(key);
} }
Command::add( Command::add(
@@ -138,20 +138,11 @@ bool WebSchedulerService::command_setvalue(const char * value, const int8_t id,
publish(); publish();
} }
// save new state to nvs #2946 // save new state to nvs #2946
if (scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
char key[sizeof(scheduleItem.name) + 2]; char key[sizeof(scheduleItem.name) + 2];
snprintf(key, sizeof(key), "s:%s", scheduleItem.name); snprintf(key, sizeof(key), "s:%s", scheduleItem.name);
EMSESP::nvs_.putBool(key, scheduleItem.active); EMSESP::nvs_.putBool(key, scheduleItem.active);
/* save to filesystem
EMSESP::webSchedulerService.update([&](WebScheduler & webSchedule) {
for (auto si : webSchedule.scheduleItems) {
if (!strcmp(si.name, scheduleItem.name)) {
si.active = scheduleItem.active;
break;
} }
}
return StateUpdateResult::CHANGED;
});
*/
return true; return true;
} }
} }
@@ -184,6 +175,15 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) {
return true; return true;
} }
if (!strcmp(cmd, F_(metrics))) {
std::string metrics = get_metrics_prometheus();
if (!metrics.empty()) {
output["api_data"] = metrics;
return true;
}
return false;
}
const char * attribute_s = Command::get_attribute(cmd); const char * attribute_s = Command::get_attribute(cmd);
for (const ScheduleItem & scheduleItem : *scheduleItems_) { for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (Helpers::toLower(scheduleItem.name) == cmd) { if (Helpers::toLower(scheduleItem.name) == cmd) {
@@ -195,6 +195,21 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) {
return false; // not found return false; // not found
} }
// generate Prometheus metrics format from scheduler values
std::string WebSchedulerService::get_metrics_prometheus() {
std::string result;
result.reserve(scheduleItems_->size() * 140);
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (scheduleItem.name[0] == '\0') {
continue;
}
result += (std::string) "# HELP emsesp_" + scheduleItem.name + " " + scheduleItem.name + ", boolean, readable, visible, writable\n";
result += (std::string) "# TYPE emsesp_" + scheduleItem.name + " gauge\n";
result += (std::string) "emsesp_" + scheduleItem.name + " " + (scheduleItem.active ? "1\n" : "0\n");
}
return result;
}
// build the json for specific entity // build the json for specific entity
void WebSchedulerService::get_value_json(JsonObject output, const ScheduleItem & scheduleItem) { void WebSchedulerService::get_value_json(JsonObject output, const ScheduleItem & scheduleItem) {
output["name"] = (const char *)scheduleItem.name; output["name"] = (const char *)scheduleItem.name;
@@ -483,6 +498,10 @@ void WebSchedulerService::loop() {
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE) { if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str())); command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()));
scheduleItem.active = false; scheduleItem.active = false;
publish_single(scheduleItem.name, false);
if (EMSESP::mqtt_.get_publish_onchange(0)) {
publish();
}
} }
} }

View File

@@ -88,6 +88,8 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
uint8_t count_entities(bool cmd_only = false); uint8_t count_entities(bool cmd_only = false);
bool onChange(const char * cmd); bool onChange(const char * cmd);
std::string get_metrics_prometheus();
std::string raw_value; std::string raw_value;
std::string computed_value; std::string computed_value;