diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index 232b748fb..305f92ea2 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -8,10 +8,12 @@ For more details go to [emsesp.org](https://emsesp.org/). - comfortpoint for BC400 [#2935](https://github.com/emsesp/EMS-ESP32/issues/2935) - 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) +- prometheus metrics for temperature/analog/scheduler/custom [#2962](https://github.com/emsesp/EMS-ESP32/issues/2962) ## Fixed -- SRC climate creation [#2936](https://github.com/emsesp/EMS-ESP32/issues/2936) +- SRC climate creation [#2936](https://github.com/emsesp/EMS-ESP32/issues/2936) and [#2960](https://github.com/emsesp/EMS-ESP32/issues/2960) ## Changed diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index cefa009f5..84c3fb679 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -508,13 +508,9 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.1': - resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} - engines: {node: 20 || >=22} + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1062,6 +1058,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.2: + resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==} + engines: {node: 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1101,6 +1101,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.2: + resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} + engines: {node: 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1155,8 +1159,8 @@ packages: resolution: {integrity: sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==} engines: {node: '>=0.10.0'} - caniuse-lite@1.0.30001769: - resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + caniuse-lite@1.0.30001770: + resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==} caw@2.0.1: resolution: {integrity: sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==} @@ -2077,6 +2081,10 @@ packages: resolution: {integrity: sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==} engines: {node: '>= 4'} + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + javascript-natural-sort@0.7.1: resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} @@ -2247,8 +2255,8 @@ packages: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} - minimatch@10.1.2: - resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} + minimatch@10.2.0: + resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==} engines: {node: 20 || >=22} minimatch@3.1.2: @@ -3432,7 +3440,7 @@ snapshots: dependencies: '@eslint/object-schema': 3.0.1 debug: 4.4.3 - minimatch: 10.1.2 + minimatch: 10.2.0 transitivePeerDependencies: - supports-color @@ -3466,11 +3474,7 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.1': - dependencies: - '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@9.0.0': {} '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -3805,7 +3809,7 @@ snapshots: '@types/minimatch@6.0.0': dependencies: - minimatch: 10.1.2 + minimatch: 10.2.0 '@types/node@25.2.3': dependencies: @@ -3986,6 +3990,10 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.2: + dependencies: + jackspeak: 4.2.3 + base64-js@1.5.1: {} baseline-browser-mapping@2.9.19: {} @@ -4039,6 +4047,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.2: + dependencies: + balanced-match: 4.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -4046,7 +4058,7 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001769 + caniuse-lite: 1.0.30001770 electron-to-chromium: 1.5.286 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -4105,7 +4117,7 @@ snapshots: camelcase@2.1.1: {} - caniuse-lite@1.0.30001769: {} + caniuse-lite@1.0.30001770: {} caw@2.0.1: dependencies: @@ -4581,7 +4593,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.1.2 + minimatch: 10.2.0 natural-compare: 1.4.0 optionator: 0.9.4 transitivePeerDependencies: @@ -5116,6 +5128,10 @@ snapshots: has-to-string-tag-x: 1.4.1 is-object: 1.0.2 + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + javascript-natural-sort@0.7.1: {} jpegtran-bin@5.0.2: @@ -5274,9 +5290,9 @@ snapshots: mimic-response@1.0.1: {} - minimatch@10.1.2: + minimatch@10.2.0: dependencies: - '@isaacs/brace-expansion': 5.0.1 + brace-expansion: 5.0.2 minimatch@3.1.2: dependencies: diff --git a/platformio.ini b/platformio.ini index 19b466fdc..c0319c603 100644 --- a/platformio.ini +++ b/platformio.ini @@ -106,7 +106,7 @@ board_build.filesystem = littlefs lib_deps = bblanchon/ArduinoJson @ 7.4.2 ESP32Async/AsyncTCP @ 3.4.10 - ESP32Async/ESPAsyncWebServer @ 3.9.6 + ESP32Async/ESPAsyncWebServer @ 3.10.0 https://github.com/emsesp/EMS-ESP-Modules.git @ 1.0.8 diff --git a/src/ESP32React/MqttSettingsService.cpp b/src/ESP32React/MqttSettingsService.cpp index 1c07748f0..5bcaa9562 100644 --- a/src/ESP32React/MqttSettingsService.cpp +++ b/src/ESP32React/MqttSettingsService.cpp @@ -316,6 +316,10 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings) changed = true; } + if (newSettings.ha_number_mode != settings.ha_number_mode) { + changed = true; + } + if (newSettings.entity_format != settings.entity_format) { changed = true; } diff --git a/src/core/analogsensor.cpp b/src/core/analogsensor.cpp index 84b803679..4147e5c34 100644 --- a/src/core/analogsensor.cpp +++ b/src/core/analogsensor.cpp @@ -852,6 +852,15 @@ bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int 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 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 } +// 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 void AnalogSensor::get_value_json(JsonObject output, const Sensor & sensor) { output["name"] = (const char *)sensor.name(); diff --git a/src/core/analogsensor.h b/src/core/analogsensor.h index f466adb75..65e200662 100644 --- a/src/core/analogsensor.h +++ b/src/core/analogsensor.h @@ -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 get_value_info(JsonObject output, const char * cmd, const int8_t id = -1); void store_counters(); + std::string get_metrics_prometheus(); static std::vector exclude_types() { return exclude_types_; } diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index dc558c413..4296e599c 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -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 std::string EMSdevice::get_value_uom(const std::string & shortname) const { 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 if ((dv.uom == DeviceValueUOM::NONE) || (dv.uom == DeviceValueUOM::MINUTES)) { break; @@ -1282,7 +1282,7 @@ void EMSdevice::setCustomizationEntity(const std::string & entity_id) { // set the min / max 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); } @@ -2135,7 +2135,7 @@ void EMSdevice::mqtt_ha_entity_config_create() { if (needs_update) { const char * const ** mode_options = nullptr; - for (auto & d : devicevalues_) { + for (const auto & d : devicevalues_) { // make sure mode in same circuit is DeviceValueType::ENUM if ((d.tag == dv.tag) && (d.type == DeviceValueType::ENUM) && !strcmp(d.short_name, FL_(mode)[0]) && (d.options_size > 0)) { // get options @@ -2166,20 +2166,26 @@ void EMSdevice::mqtt_ha_entity_config_create() { count++; } - // SRC thermostats mapped to connect/src1/... always contains mode, seltemp, currtemp - if (dv.tag >= DeviceValueTAG::TAG_SRC1 && dv.tag <= DeviceValueTAG::TAG_SRC16 && !strcmp(dv.short_name, FL_(mode)[0])) { - // add icon if we have one - const char * icon = nullptr; - for (auto & d : devicevalues_) { - if (d.tag == dv.tag && !strcmp(d.short_name, FL_(icon)[0]) && (dv.type == DeviceValueType::ENUM)) { + // 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_(selRoomTemp)[0])) { + // add modes and icon if we have one + const char * icon = nullptr; + const char * const ** mode_options = nullptr; + for (const auto & d : devicevalues_) { + if ((d.tag != dv.tag) || (d.type != DeviceValueType::ENUM)) { + continue; + } + if (!strcmp(d.short_name, FL_(mode)[0]) && (d.options_size > 0)) { + mode_options = d.options; + } + if (!strcmp(d.short_name, FL_(icon)[0])) { uint8_t val = *(uint8_t *)(d.value_p); if (val != 0 && val < d.options_size) { icon = d.options[val][0]; } - break; } } - Mqtt::publish_ha_climate_config(dv, true, dv.options, false, icon); + Mqtt::publish_ha_climate_config(dv, true, mode_options, false, icon); count++; } diff --git a/src/core/mqtt.cpp b/src/core/mqtt.cpp index d559efcac..225c7f56e 100644 --- a/src/core/mqtt.cpp +++ b/src/core/mqtt.cpp @@ -1545,9 +1545,7 @@ void Mqtt::add_ha_dev_section(JsonObject doc, const char * name, const bool crea // add mf (manufacturer/brand), mdl (model), sw (software version) and via_device dev_json["mf"] = brand != nullptr ? brand : "EMS-ESP"; - if (model != nullptr) { - dev_json["mdl"] = model; - } + dev_json["mdl"] = model != nullptr ? model : "EMS-ESP"; dev_json["sw"] = version != nullptr ? version : "v" + std::string(EMSESP_APP_VERSION); dev_json["via_device"] = Mqtt::basename(); } diff --git a/src/core/temperaturesensor.cpp b/src/core/temperaturesensor.cpp index 83624450b..afcfb4673 100644 --- a/src/core/temperaturesensor.cpp +++ b/src/core/temperaturesensor.cpp @@ -400,6 +400,15 @@ bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, cons 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 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 } +// 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 void TemperatureSensor::get_value_json(JsonObject output, const Sensor & sensor) { output["id"] = sensor.id(); diff --git a/src/core/temperaturesensor.h b/src/core/temperaturesensor.h index 14c53a5e5..a4aa63dfe 100644 --- a/src/core/temperaturesensor.h +++ b/src/core/temperaturesensor.h @@ -96,6 +96,8 @@ class TemperatureSensor { bool updated_values(); 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 std::vector> sensors() const { return sensors_; diff --git a/src/devices/connect.cpp b/src/devices/connect.cpp index 4a96c0700..a3e22af38 100644 --- a/src/devices/connect.cpp +++ b/src/devices/connect.cpp @@ -129,15 +129,20 @@ void Connect::process_roomThermostat(std::shared_ptr telegram) { } has_update(telegram, rc->temp_, 0); has_update(telegram, rc->humidity_, 2); // could show -3 if not set - has_update(telegram, rc->seltemp_, 3); + // make sure we have read mode and icon, needed for ha climate + if (!Mqtt::ha_enabled() || (Helpers::hasValue(rc->mode_) && Helpers::hasValue(rc->icon_))) { + has_update(telegram, rc->seltemp_, 3); + } // calculate dew temperature - const float k2 = 17.62; - const float k3 = 243.12; - const float t = (float)rc->temp_ / 10; - const float h = (float)rc->humidity_ / 100; - int16_t dt = (10 * k3 * (((k2 * t) / (k3 + t)) + log(h)) / (((k2 * k3) / (k3 + t)) - log(h))); - has_update(rc->dewtemp_, dt); + if (rc->humidity_ >= 0 && rc->humidity_ <= 100) { + const float k2 = 17.62; + const float k3 = 243.12; + const float t = (float)rc->temp_ / 10; + const float h = (float)rc->humidity_ / 100; + int16_t dt = (10 * k3 * (((k2 * t) / (k3 + t)) + log(h)) / (((k2 * k3) / (k3 + t)) - log(h))); + has_update(rc->dewtemp_, dt); + } } // gateway(0x48) W gateway(0x50), ?(0x0B42), data: 01 // icon in offset 0 @@ -206,12 +211,13 @@ bool Connect::set_mode(const char * value, const int8_t id) { return false; } uint8_t v; - if (Helpers::value2enum(value, v, FL_(enum_mode2), {3, 1, 0})) { - // if (Helpers::value2enum(value, v, FL_(enum_mode8))) { - write_command(0xBB5 + rc->room(), 0, v); // no validate, mode change is broadcasted - return true; + if (!Helpers::value2enum(value, v, FL_(enum_mode2), {3, 1, 0})) { + if (!Helpers::value2enum(value, v, FL_(enum_mode_ha), {3, 1, 0})) { + return false; + } } - return false; + write_command(0xBB5 + rc->room(), 0, v); // no validate, mode change is broadcasted + return true; } bool Connect::set_seltemp(const char * value, const int8_t id) { @@ -221,8 +227,9 @@ bool Connect::set_seltemp(const char * value, const int8_t id) { } float v; if (Helpers::value2float(value, v)) { - // write_command(0xBB5 + rc->room(), rc->mode_ == 2 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2)); - write_command(0xBB5 + rc->room(), rc->mode_ == 0 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2)); + // depends on mode, auto (2 for enum_mode2, 0 for enum_mode8) set in offset 1 + write_command(0xBB5 + rc->room(), rc->mode_ == 2 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2)); + // write_command(0xBB5 + rc->room(), rc->mode_ == 0 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2)); return true; } return false; diff --git a/src/emsesp_version.h b/src/emsesp_version.h index 1d71f4366..73961265b 100644 --- a/src/emsesp_version.h +++ b/src/emsesp_version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.8.2-dev.5" +#define EMSESP_APP_VERSION "3.8.2-dev.7" diff --git a/src/web/WebCustomEntityService.cpp b/src/web/WebCustomEntityService.cpp index 3e4356318..9de8f3da8 100644 --- a/src/web/WebCustomEntityService.cpp +++ b/src/web/WebCustomEntityService.cpp @@ -343,6 +343,15 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) 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 const char * attribute_s = Command::get_attribute(cmd); for (auto const & entity : *customEntityItems_) { @@ -354,6 +363,54 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) 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 void WebCustomEntityService::get_value_json(JsonObject output, CustomEntityItem const & entity) { output["name"] = (const char *)entity.name; diff --git a/src/web/WebCustomEntityService.h b/src/web/WebCustomEntityService.h index d96254846..4cfec1ac4 100644 --- a/src/web/WebCustomEntityService.h +++ b/src/web/WebCustomEntityService.h @@ -68,6 +68,8 @@ class WebCustomEntityService : public StatefulService { void show_values(JsonObject output); void generate_value_web(JsonObject output, const bool is_dashboard = false); + std::string get_metrics_prometheus(); + uint8_t count_entities(); void ha_reset() { ha_configdone_ = false; diff --git a/src/web/WebSchedulerService.cpp b/src/web/WebSchedulerService.cpp index b92930357..e708c94e7 100644 --- a/src/web/WebSchedulerService.cpp +++ b/src/web/WebSchedulerService.cpp @@ -102,7 +102,7 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu if (webScheduler.scheduleItems.back().name[0] != '\0') { char key[sizeof(webScheduler.scheduleItems.back().name) + 2]; 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); } Command::add( @@ -138,20 +138,11 @@ bool WebSchedulerService::command_setvalue(const char * value, const int8_t id, publish(); } // save new state to nvs #2946 - char key[sizeof(scheduleItem.name) + 2]; - snprintf(key, sizeof(key), "s:%s", scheduleItem.name); - 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; - }); - */ + if (scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) { + char key[sizeof(scheduleItem.name) + 2]; + snprintf(key, sizeof(key), "s:%s", scheduleItem.name); + EMSESP::nvs_.putBool(key, scheduleItem.active); + } return true; } } @@ -184,6 +175,15 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) { 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); for (const ScheduleItem & scheduleItem : *scheduleItems_) { if (Helpers::toLower(scheduleItem.name) == cmd) { @@ -195,6 +195,21 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) { 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 void WebSchedulerService::get_value_json(JsonObject output, const ScheduleItem & scheduleItem) { output["name"] = (const char *)scheduleItem.name; @@ -483,6 +498,10 @@ void WebSchedulerService::loop() { if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE) { command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str())); scheduleItem.active = false; + publish_single(scheduleItem.name, false); + if (EMSESP::mqtt_.get_publish_onchange(0)) { + publish(); + } } } diff --git a/src/web/WebSchedulerService.h b/src/web/WebSchedulerService.h index 8a1bbbf55..0d1ad2fa6 100644 --- a/src/web/WebSchedulerService.h +++ b/src/web/WebSchedulerService.h @@ -88,6 +88,8 @@ class WebSchedulerService : public StatefulService { uint8_t count_entities(bool cmd_only = false); bool onChange(const char * cmd); + std::string get_metrics_prometheus(); + std::string raw_value; std::string computed_value;