51 Commits

Author SHA1 Message Date
Proddy
64058b0f61 Merge pull request #2792 from proddy/dev
small updates
2025-12-04 20:32:21 +01:00
proddy
d7b5c81b0e package update 2025-12-04 20:29:20 +01:00
proddy
02e8dba971 fix max size to 32MB 2025-12-03 22:16:39 +01:00
proddy
59878fb190 remove vscode settings and package.json 2025-12-03 22:11:44 +01:00
proddy
9ff0f83af9 remove obsolete double auto-commit 2025-12-03 22:06:06 +01:00
proddy
e6f825371e fix make for Windows 2025-12-03 22:05:45 +01:00
proddy
45f3f23033 package update 2025-12-03 21:53:33 +01:00
proddy
ffd27db208 show elapsed time 2025-12-03 21:53:26 +01:00
proddy
a452d6131b DiffTemp ranged to 4-15 K - fix #2783 2025-12-03 21:53:16 +01:00
Proddy
03ef981765 Merge pull request #2788 from proddy/dev
fix mem issue
2025-12-02 19:42:05 +01:00
proddy
9ca9f25fd3 rollback modbus hash map - fix #2752 2025-12-02 19:41:17 +01:00
proddy
41122dddb2 dev-34 2025-12-02 19:40:45 +01:00
proddy
1e0c94d007 package update 2025-12-02 19:40:38 +01:00
proddy
3e42a7fb4c package update 2025-12-01 19:56:59 +01:00
proddy
a8fcc1fb44 show mem 2025-12-01 19:56:53 +01:00
proddy
e43416019d package update 2025-12-01 17:40:53 +01:00
Proddy
9f467ecec1 Merge pull request #2782 from proddy/dev
MQTT base fixes
2025-11-30 23:28:16 +01:00
proddy
273d87dbf1 use ~ as MQTT base 2025-11-30 23:27:28 +01:00
proddy
befd21f8cb fixe #2780 2025-11-30 23:27:18 +01:00
Proddy
15e05c4abc Merge pull request #2778 from proddy/dev
update to show heap
2025-11-30 20:51:42 +01:00
proddy
748a2f5fcf update to show heap 2025-11-30 20:51:17 +01:00
Proddy
37ba42faf8 Merge pull request #2775 from proddy/dev
loop tests
2025-11-30 17:24:56 +01:00
proddy
19e343e517 loop tests 2025-11-30 17:23:58 +01:00
Proddy
8f39129bf8 Merge pull request #2773 from proddy/dev
a collection of changes
2025-11-30 15:43:18 +01:00
proddy
9c3521caf2 add target native-test-create 2025-11-30 15:39:36 +01:00
proddy
40fc0fd2f9 anti-rollback! 2025-11-30 15:39:13 +01:00
proddy
ff8566498f typo 2025-11-30 15:15:03 +01:00
proddy
dd06882860 auto-formatting 2025-11-30 14:05:50 +01:00
proddy
fb2294c945 added Prometheus metrics 2025-11-30 14:05:02 +01:00
proddy
26ea8320ce rollback for #2752 2025-11-30 14:04:45 +01:00
proddy
0f4963d91e formatting 2025-11-30 14:04:14 +01:00
proddy
91020abc90 formatting 2025-11-30 14:04:05 +01:00
Proddy
64906f3ea0 Merge branch 'emsesp:dev' into dev 2025-11-30 13:54:26 +01:00
Proddy
28f85b4c5a Merge pull request #2774 from gr3enk/dev
Adding api/metrics endpoint for Prometheus integration
2025-11-30 13:51:47 +01:00
Jakob
b44a0d6813 test: add tests for api/metrics endpoint 2025-11-30 10:37:34 +01:00
Jakob
8af7cde2d6 feat: add api/metrics endpoint 2025-11-30 10:35:19 +01:00
proddy
3fcd656bb6 package update with new alova 2025-11-30 10:22:20 +01:00
proddy
76c827257e mqtt heartbeat, no need to check if connected 2025-11-30 10:22:08 +01:00
proddy
8ca9f7ee30 change ip 2025-11-29 15:36:24 +01:00
proddy
da7a06646a rollback AsyncWS fix #2752 2025-11-29 15:35:57 +01:00
proddy
0a36f1df7a updated 2025-11-29 15:14:33 +01:00
proddy
80e5d30781 dev-33 2025-11-29 15:14:26 +01:00
proddy
9c4beba3b1 add LWT to HA discovery config topic 2025-11-29 15:12:07 +01:00
proddy
0cf932f57e comment change 2025-11-29 14:49:08 +01:00
proddy
5cb9f3b014 init board profile correctly 2025-11-29 14:48:57 +01:00
proddy
3eb581142a default keep alive 60 seconds 2025-11-29 14:48:42 +01:00
proddy
d6c460e7fd ESP32Async/ESPAsyncWebServer @ 3.9.2 2025-11-29 14:48:24 +01:00
proddy
a2baa50530 show users reports error if not admin 2025-11-29 14:48:13 +01:00
proddy
6569b8c038 enable TLS for test data 2025-11-29 14:47:42 +01:00
proddy
48b4bf02a3 wider box for TLS 2025-11-29 14:47:35 +01:00
proddy
693054a92a package update 2025-11-29 14:47:27 +01:00
34 changed files with 645 additions and 437 deletions

View File

@@ -64,29 +64,7 @@ jobs:
- name: Commit the generated files
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: update generated files"
- name: Configure Git
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
- name: Check for changes and commit
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "Changes detected, committing..."
git add .
git commit -m "Auto-commit build artifacts and configuration updates
- Updated build configurations
- Generated build artifacts
- Version: ${{steps.build_info.outputs.VERSION}}"
echo "Pushing changes to repository..."
git push origin dev
else
echo "No changes to commit"
fi
commit_message: "chore: update generated files for v${{steps.build_info.outputs.VERSION}}"
- name: Create GitHub Release
id: 'automatic_releases'

2
.gitignore vendored
View File

@@ -2,7 +2,6 @@
.vscode/c_cpp_properties.json
.vscode/extensions.json
.vscode/launch.json
.vscode/settings.json
# c++ compiling
.clang_complete
@@ -73,7 +72,6 @@ logs/*
sdkconfig.*
sdkconfig_tasmota_esp32
pnpm-lock.yaml
package.json
.cache/
interface/.tsbuildinfo
test/test_api/package-lock.json

View File

@@ -33,6 +33,9 @@ For more details go to [docs.emsesp.org](https://docs.emsesp.org/).
- pumpmode enum for HT3 boilers, add commands for manual defrost, chimneysweeper [#2727](https://github.com/emsesp/EMS-ESP32/issues/2727)
- pid settings [#2735](https://github.com/emsesp/EMS-ESP32/issues/2735)
- refresh MQTT button added to MQTT Settings page
- added LWT (Last Will and Testament) to MQTT entities in Home Assistant
- added api/metrics endpoint for prometheus integration by @gr3enk
[#2774](https://github.com/emsesp/EMS-ESP32/pull/2774)
## Fixed

View File

@@ -21,13 +21,14 @@ endif
# Optimize parallel build configuration
UNAME_S := $(shell uname -s)
JOBS ?= 1
ifeq ($(UNAME_S),Linux)
EXTRA_CPPFLAGS = -D LINUX
JOBS ?= $(shell nproc)
JOBS := $(shell nproc)
endif
ifeq ($(UNAME_S),Darwin)
EXTRA_CPPFLAGS = -D OSX -Wno-tautological-constant-out-of-range-compare
JOBS ?= $(shell sysctl -n hw.ncpu)
JOBS := $(shell sysctl -n hw.ncpu)
endif
# Set optimal parallel build settings

View File

@@ -25,7 +25,7 @@
"upload": {
"flash_size": "32MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"maximum_size": 33554432,
"require_upload_port": true,
"speed": 460800
},

View File

@@ -23,25 +23,25 @@
"standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\""
},
"dependencies": {
"@alova/adapter-xhr": "2.2.1",
"@alova/adapter-xhr": "2.3.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.5",
"@mui/material": "^7.3.5",
"@mui/icons-material": "^7.3.6",
"@mui/material": "^7.3.6",
"@preact/compat": "^18.3.1",
"@table-library/react-table-library": "4.1.15",
"alova": "3.3.4",
"alova": "3.4.0",
"async-validator": "^4.2.5",
"etag": "^1.8.1",
"formidable": "^3.5.4",
"jwt-decode": "^4.0.0",
"magic-string": "^0.30.21",
"mime-types": "^3.0.2",
"preact": "^10.27.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"preact": "^10.28.0",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-icons": "^5.5.0",
"react-router": "^7.9.6",
"react-router": "^7.10.1",
"react-toastify": "^11.0.5",
"typesafe-i18n": "^5.26.2",
"typescript": "^5.9.3"
@@ -59,13 +59,13 @@
"concurrently": "^9.2.1",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"rollup-plugin-visualizer": "^6.0.5",
"terser": "^5.44.1",
"typescript-eslint": "^8.48.0",
"vite": "^7.2.4",
"typescript-eslint": "^8.48.1",
"vite": "^7.2.6",
"vite-plugin-imagemin": "^0.6.1",
"vite-tsconfig-paths": "^5.1.4"
},
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b"
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a"
}

514
interface/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -266,6 +266,7 @@ const MqttSettings = () => {
label={LL.CERT()}
variant="outlined"
value={data.rootCA}
sx={{ width: '50ch' }}
onChange={updateFormValue}
margin="normal"
/>

View File

@@ -13,7 +13,7 @@
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
"formidable": "^3.5.4",
"itty-router": "^5.0.22",
"prettier": "^3.6.2"
"prettier": "^3.7.4"
},
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b"
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a"
}

View File

@@ -13,7 +13,7 @@ importers:
version: 3.1.2
'@trivago/prettier-plugin-sort-imports':
specifier: ^6.0.0
version: 6.0.0(prettier@3.6.2)
version: 6.0.0(prettier@3.7.4)
formidable:
specifier: ^3.5.4
version: 3.5.4
@@ -21,8 +21,8 @@ importers:
specifier: ^5.0.22
version: 5.0.22
prettier:
specifier: ^3.6.2
version: 3.6.2
specifier: ^3.7.4
version: 3.7.4
packages:
@@ -167,8 +167,8 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
prettier@3.7.4:
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
engines: {node: '>=14'}
hasBin: true
@@ -246,7 +246,7 @@ snapshots:
dependencies:
'@noble/hashes': 1.8.0
'@trivago/prettier-plugin-sort-imports@6.0.0(prettier@3.6.2)':
'@trivago/prettier-plugin-sort-imports@6.0.0(prettier@3.7.4)':
dependencies:
'@babel/generator': 7.28.5
'@babel/parser': 7.28.5
@@ -256,7 +256,7 @@ snapshots:
lodash-es: 4.17.21
minimatch: 9.0.5
parse-imports-exports: 0.2.4
prettier: 3.6.2
prettier: 3.7.4
transitivePeerDependencies:
- supports-color
@@ -311,6 +311,6 @@ snapshots:
picocolors@1.1.1: {}
prettier@3.6.2: {}
prettier@3.7.4: {}
wrappy@1.0.2: {}

View File

@@ -569,14 +569,15 @@ let mqtt_settings = {
publish_time_heartbeat: 60,
publish_time_water: 60,
mqtt_qos: 0,
rootCA: '',
mqtt_retain: false,
ha_enabled: true,
nested_format: 1,
discovery_type: 0,
discovery_prefix: 'homeassistant',
send_response: true,
publish_single: false
publish_single: false,
enableTLS: true,
rootCA: ''
};
const mqtt_status = {
enabled: true,

View File

@@ -106,7 +106,7 @@ board_build.filesystem = littlefs
lib_deps =
bblanchon/ArduinoJson @ 7.4.2
ESP32Async/AsyncTCP @ 3.4.9
ESP32Async/ESPAsyncWebServer @ 3.9.0
ESP32Async/ESPAsyncWebServer @ 3.9.2
https://github.com/emsesp/EMS-ESP-Modules.git @ 1.0.8
@@ -214,23 +214,20 @@ lib_ldf_mode = off
lib_deps =
; unit tests
; The code is in ./test/test_api.*
; The test code is in ./test/test_api.cpp and the test_api.h file is created by the native-test-create environment.
; to run use `platformio run -e native-test -t exec`. All tests should PASS.
; to update the test results, compile with -DEMSESP_UNITY_CREATE by uncommenting the line below
; then re-run and capture the output between "START - CUT HERE" and "END - CUT HERE" into the test_api.h file
; tip: use https://jsondiff.com/ to compare the expected and actual responses.
[env:native-test]
platform = native
test_build_src = true
build_flags =
; -DEMSESP_UNITY_CREATE
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
-DEMSESP_STANDALONE -DEMSESP_TEST
-DEMSESP_UNITY
-DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev.0\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
-std=gnu++17 -Og -ggdb
build_type = debug
build_src_flags =
-DEMSESP_STANDALONE -DEMSESP_TEST
-DEMSESP_UNITY
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
-DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev.0\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
-std=gnu++17 -Og -ggdb
-Wall -Wextra
-Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces
-Wno-vla-cxx-extension -Wno-tautological-constant-out-of-range-compare
@@ -260,6 +257,12 @@ lib_deps = Unity
test_testing_command =
${platformio.build_dir}/${this.__env__}/program
; builds the test cases and creates the test_api.h file
; run with `pio run -e native-test-create -t exec` and capture the output between "START - CUT HERE" and "END - CUT HERE" and paste it into the test_api.h file
[env:native-test-create]
extends = env:native-test
build_flags =
-DEMSESP_UNITY_CREATE
;
; Building and testing locally on OS, which we call "standalone" without an ESP32.
; See https://docs.platformio.org/en/latest/platforms/native.html

View File

@@ -173,7 +173,7 @@ bool MqttSettingsService::configureMqtt() {
// only connect if WiFi is connected and MQTT is enabled
if (_state.enabled && emsesp::EMSESP::system_.network_connected() && !_state.host.isEmpty()) {
// create last will topic with the base prefixed. It has to be static because the client destroys the reference
// create the Last Will Testament topic (LWT) with the base prefixed. It has to be static because the client destroys the reference
static char will_topic[FACTORY_MQTT_MAX_TOPIC_LENGTH];
if (_state.base.isEmpty()) {
snprintf(will_topic, sizeof(will_topic), "status");

View File

@@ -39,7 +39,7 @@
#endif
#ifndef FACTORY_MQTT_KEEP_ALIVE
#define FACTORY_MQTT_KEEP_ALIVE 16
#define FACTORY_MQTT_KEEP_ALIVE 60
#endif
#ifndef FACTORY_MQTT_CLEAN_SESSION

View File

@@ -667,9 +667,10 @@ void AnalogSensor::publish_values(const bool force) {
LOG_DEBUG("Recreating HA config for analog sensor GPIO %02d", sensor.gpio());
JsonDocument config;
config["~"] = Mqtt::base();
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/%s_data", Mqtt::base().c_str(), F_(analogsensor)); // use base path
snprintf(stat_t, sizeof(stat_t), "~/%s_data", F_(analogsensor)); // use base path
config["stat_t"] = stat_t;
char val_obj[50];
@@ -699,6 +700,7 @@ void AnalogSensor::publish_values(const bool force) {
snprintf(uniq_s, sizeof(uniq_s), "%s_%02d", F_(analogsensor), sensor.gpio());
}
config["~"] = Mqtt::base();
config["uniq_id"] = uniq_s;
char name[50];

View File

@@ -763,6 +763,8 @@ void Command::show_all(uuid::console::Shell & shell) {
shell.println(COLOR_RESET);
shell.printf(" entities \t\t\t%slist all entities %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.println(COLOR_RESET);
shell.printf(" metrics \t\t\t%slist all prometheus metrics %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.println(COLOR_RESET);
// show system ones first
show(shell, EMSdevice::DeviceType::SYSTEM, true);

View File

@@ -87,8 +87,8 @@ static void setup_commands(std::shared_ptr<Commands> const & commands) {
Command::show_all(shell);
} else if (command == F_(system)) {
EMSESP::system_.show_system(shell);
} else if (command == F_(users) && (shell.has_flags(CommandFlags::ADMIN))) {
EMSESP::system_.show_users(shell); // admin only
} else if (command == F_(users)) {
EMSESP::system_.show_users(shell);
} else if (command == F_(devices)) {
EMSESP::show_devices(shell);
} else if (command == F_(log)) {

View File

@@ -632,9 +632,6 @@ void EMSdevice::add_device_value(int8_t tag, // to b
devicevalues_.emplace_back(
device_type_, tag, value_p, type, options, options_single, numeric_operator, short_name, fullname, custom_fullname, uom, has_cmd, min, max, state);
// add to index for fast lookup by (tag, short_name)
devicevalue_index_[{static_cast<uint8_t>(tag), short_name}] = devicevalues_.size() - 1;
// add a new command if it has a function attached
if (has_cmd) {
uint8_t flags = CommandFlag::ADMIN_ONLY; // executing commands require admin privileges
@@ -1533,6 +1530,14 @@ bool EMSdevice::get_value_info(JsonObject output, const char * cmd, const int8_t
}
return true;
}
if (!strcmp(cmd, F_(metrics))) {
std::string metrics = get_metrics_prometheus(tag);
if (!metrics.empty()) {
output["api_data"] = metrics;
return true;
}
return false;
}
// search device value with this tag
// make a copy of cmd and split attribute (leave cmd untouched for other devices)
@@ -1696,6 +1701,175 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) {
json["visible"] = !dv.has_state(DeviceValueState::DV_WEB_EXCLUDE);
}
// generate Prometheus metrics format from device values
std::string EMSdevice::get_metrics_prometheus(const int8_t tag) {
std::string result;
std::unordered_map<std::string, bool> seen_metrics;
for (auto & dv : devicevalues_) {
if (tag >= 0 && tag != dv.tag) {
continue;
}
// only process number and boolean types for now
if (dv.type != DeviceValueType::BOOL && dv.type != DeviceValueType::UINT8 && dv.type != DeviceValueType::INT8 && dv.type != DeviceValueType::UINT16
&& dv.type != DeviceValueType::INT16 && dv.type != DeviceValueType::UINT24 && dv.type != DeviceValueType::UINT32 && dv.type != DeviceValueType::TIME) {
continue;
}
bool has_value = false;
double metric_value = 0.0;
switch (dv.type) {
case DeviceValueType::BOOL:
if (Helpers::hasValue(*(uint8_t *)(dv.value_p), EMS_VALUE_BOOL)) {
has_value = true;
metric_value = (bool)*(uint8_t *)(dv.value_p) ? 1.0 : 0.0;
}
break;
case DeviceValueType::UINT8:
if (Helpers::hasValue(*(uint8_t *)(dv.value_p))) {
has_value = true;
metric_value = *(uint8_t *)(dv.value_p);
}
break;
case DeviceValueType::INT8:
if (Helpers::hasValue(*(int8_t *)(dv.value_p))) {
has_value = true;
metric_value = *(int8_t *)(dv.value_p);
}
break;
case DeviceValueType::UINT16:
if (Helpers::hasValue(*(uint16_t *)(dv.value_p))) {
has_value = true;
metric_value = *(uint16_t *)(dv.value_p);
}
break;
case DeviceValueType::INT16:
if (Helpers::hasValue(*(int16_t *)(dv.value_p))) {
has_value = true;
metric_value = *(int16_t *)(dv.value_p);
}
break;
case DeviceValueType::UINT24:
case DeviceValueType::UINT32:
case DeviceValueType::TIME:
if (Helpers::hasValue(*(uint32_t *)(dv.value_p))) {
has_value = true;
metric_value = *(uint32_t *)(dv.value_p);
}
break;
default:
break;
}
if (!has_value) {
continue;
}
std::string metric_name = dv.short_name;
size_t last_dot = metric_name.find_last_of('.');
if (last_dot != std::string::npos) {
metric_name = metric_name.substr(last_dot + 1);
}
for (char & c : metric_name) {
if (!isalnum(c) && c != '_') {
c = '_';
}
}
std::string full_metric_name = "emsesp_" + metric_name;
std::string circuit_label;
if (dv.tag != DeviceValueTAG::TAG_NONE) {
const char * circuit = tag_to_mqtt(dv.tag);
if (circuit && strlen(circuit) > 0) {
circuit_label = circuit;
}
}
auto fullname = dv.get_fullname();
std::string help_text;
if (!fullname.empty()) {
help_text = fullname;
} else {
help_text = metric_name;
}
std::string uom_str;
if (dv.type == DeviceValueType::BOOL) {
uom_str = "boolean";
} else if (dv.uom != DeviceValueUOM::NONE) {
uom_str = uom_to_string(dv.uom);
}
std::string help_line = help_text;
if (!uom_str.empty()) {
help_line += ", " + uom_str;
}
bool readable = dv.type != DeviceValueType::CMD && !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE);
bool writeable = dv.has_cmd && !dv.has_state(DeviceValueState::DV_READONLY);
bool visible = !dv.has_state(DeviceValueState::DV_WEB_EXCLUDE);
if (readable) {
help_line += ", readable";
}
if (writeable) {
help_line += ", writeable";
}
if (visible) {
help_line += ", visible";
}
std::string escaped_help;
for (char c : help_line) {
if (c == '\\') {
escaped_help += "\\\\";
} else if (c == '\n') {
escaped_help += "\\n";
} else {
escaped_help += c;
}
}
if (seen_metrics.find(full_metric_name) == seen_metrics.end()) {
result += "# HELP " + full_metric_name + " " + escaped_help + "\n";
result += "# TYPE " + full_metric_name + " gauge\n";
seen_metrics[full_metric_name] = true;
}
result += full_metric_name;
if (!circuit_label.empty()) {
result += "{circuit=\"" + circuit_label + "\"}";
}
result += " ";
char val_str[30];
double final_value = metric_value;
if (dv.numeric_operator != 0) {
if (dv.numeric_operator > 0) {
final_value = metric_value / dv.numeric_operator;
} else {
final_value = metric_value * (-dv.numeric_operator);
}
}
double rounded = (final_value >= 0) ? (double)((int64_t)(final_value + 0.5)) : (double)((int64_t)(final_value - 0.5));
if (dv.type == DeviceValueType::BOOL || (final_value == rounded)) {
snprintf(val_str, sizeof(val_str), "%.0f", final_value);
} else {
snprintf(val_str, sizeof(val_str), "%.2f", final_value);
}
result += val_str;
result += "\n";
}
return result;
}
// mqtt publish all single values from one device (used for time schedule)
void EMSdevice::publish_all_values() {
for (const auto & dv : devicevalues_) {
@@ -2035,13 +2209,14 @@ std::string EMSdevice::name() {
// copy a raw value (i.e. without applying the numeric_operator) to the output buffer.
// returns true on success.
int EMSdevice::get_modbus_value(uint8_t tag, const std::string & shortname, std::vector<uint16_t> & result) {
// find device value by shortname using hash map index
auto index_it = devicevalue_index_.find({tag, shortname});
if (index_it == devicevalue_index_.end()) {
// find device value by shortname
// TODO replace linear search which is inefficient
const auto & it = std::find_if(devicevalues_.begin(), devicevalues_.end(), [&](const DeviceValue & x) { return x.tag == tag && x.short_name == shortname; });
if (it == devicevalues_.end() && (it->short_name != shortname || it->tag != tag)) {
return -1;
}
auto & dv = devicevalues_[index_it->second];
auto & dv = *it;
// check if it exists, there is a value for the entity. Set the flag to ACTIVE
// not that this will override any previously removed states
@@ -2122,13 +2297,13 @@ int EMSdevice::get_modbus_value(uint8_t tag, const std::string & shortname, std:
int EMSdevice::modbus_value_to_json(uint8_t tag, const std::string & shortname, const std::vector<uint8_t> & modbus_data, JsonObject jsonValue) {
// LOG_DEBUG("modbus_value_to_json(%d,%s,[%d bytes])\n", tag, shortname.c_str(), modbus_data.size());
// find device value by shortname using hash map index
auto index_it = devicevalue_index_.find({tag, shortname});
if (index_it == devicevalue_index_.end()) {
// find device value by shortname
const auto & it = std::find_if(devicevalues_.begin(), devicevalues_.end(), [&](const DeviceValue & x) { return x.tag == tag && x.short_name == shortname; });
if (it == devicevalues_.end() && (it->short_name != shortname || it->tag != tag)) {
return -1;
}
auto & dv = devicevalues_[index_it->second];
auto & dv = *it;
// handle Booleans
if (dv.type == DeviceValueType::BOOL) {

View File

@@ -251,6 +251,7 @@ class EMSdevice {
std::string get_value_uom(const std::string & shortname) const;
bool get_value_info(JsonObject root, const char * cmd, const int8_t id);
void get_value_json(JsonObject output, DeviceValue & dv);
std::string get_metrics_prometheus(const int8_t tag = -1);
void get_dv_info(JsonObject json);
enum OUTPUT_TARGET : uint8_t { API_VERBOSE, API_SHORTNAMES, MQTT, CONSOLE };
@@ -555,26 +556,6 @@ class EMSdevice {
#endif
std::vector<TelegramFunction> telegram_functions_; // each EMS device has its own set of registered telegram types
std::vector<DeviceValue> devicevalues_; // all the device values
// added for modbus
// Hash map for O(1) lookup of device values by (tag, short_name) key
struct DeviceValueKey {
uint8_t tag;
std::string short_name;
bool operator==(const DeviceValueKey & other) const {
return tag == other.tag && short_name == other.short_name;
}
};
struct DeviceValueKeyHash {
std::size_t operator()(const DeviceValueKey & key) const {
// Combine hash of tag and short_name
return std::hash<uint8_t>()(key.tag) ^ (std::hash<std::string>()(key.short_name) << 1);
}
};
std::unordered_map<DeviceValueKey, size_t, DeviceValueKeyHash> devicevalue_index_; // index: key -> devicevalues_ position
};
} // namespace emsesp

View File

@@ -86,6 +86,7 @@ MAKE_WORD(info)
MAKE_WORD(settings)
MAKE_WORD(value)
MAKE_WORD(entities)
MAKE_WORD(metrics)
MAKE_WORD(coldshot)
// device types - lowercase, used in MQTT

View File

@@ -63,6 +63,7 @@ MAKE_WORD_TRANSLATION(pool_device, "Pool Module", "Poolmodul", "", "Poolmodul",
MAKE_WORD_TRANSLATION(info_cmd, "list all values (verbose)", "Liste aller Werte", "lijst van alle waardes", "lista alla värden", "wyświetl wszystkie wartości", "Viser alle verdier", "", "Tüm değerleri listele", "elenca tutti i valori", "zobraziť všetky hodnoty", "vypsat všechny hodnoty (podrobně)") // TODO translate
MAKE_WORD_TRANSLATION(commands_cmd, "list all commands", "Liste aller Kommandos", "lijst van alle commando's", "lista alla kommandon", "wyświetl wszystkie komendy", "Viser alle kommandoer", "", "Tüm komutları listele", "elencaa tutti i comandi", "zobraziť všetky príkazy", "vypsat všechny příkazy") // TODO translate
MAKE_WORD_TRANSLATION(entities_cmd, "list all entities", "Liste aller Entitäten", "lijst van alle entiteiten", "lista all entiteter", "wyświetl wszsytkie encje", "Viser alle enheter", "", "Tüm varlıkları listele", "elenca tutte le entità", "zobraziť všetky entity", "vypsat všechny entity") // TODO translate
MAKE_WORD_TRANSLATION(metrics_cmd, "list all prometheus metrics", "Liste aller Prometheus Metriken", "lijst van alle Prometheus metriken", "lista alla Prometheus metriker", "wyświetl wszystkie Prometheus metryki", "Viser alle Prometheus metrikker", "", "Tüm Prometheus metriklerini listele", "elenca tutte le metriche Prometheus", "zobraziť všetky Prometheus metriky", "vypsat všechny Prometheus metriky") // TODO translate
MAKE_WORD_TRANSLATION(send_cmd, "send a telegram", "Sende EMS-Telegramm", "stuur een telegram", "skicka ett telegram", "wyślij telegram", "send et telegram", "", "Bir telegram gönder", "invia un telegramma", "poslať telegram", "odeslat telegram") // TODO translate
MAKE_WORD_TRANSLATION(read_cmd, "send read request", "", "", "skicka en läsförfrågan", "", "", "", "", "", "odoslať žiadosť o prečítanie", "") // TODO translate
MAKE_WORD_TRANSLATION(setiovalue_cmd, "set I/O value", "Setze Werte E/A", "instellen standaardwaarde", "sätt ett I/O-värde", "ustaw wartość", "sett en io verdi", "", "Giriş/Çıkış değerlerini ayarla", "imposta valore io", "nastaviť hodnotu io", "nastavit hodnotu I/O") // TODO translate

View File

@@ -509,14 +509,14 @@ void Mqtt::on_connect() {
queue_subscribe_message(discovery_prefix_ + "/+/" + Mqtt::basename() + "/#");
}
// send initial MQTT messages for some of our services
EMSESP::system_.send_heartbeat(); // send heartbeat
// re-subscribe to all custom registered MQTT topics
resubscribe();
// publish to the last will topic (see Mqtt::start() function) to say we're alive
queue_publish_retain("status", "online"); // retain: https://github.com/emsesp/EMS-ESP32/discussions/2086
// send initial MQTT messages for some of our services
EMSESP::system_.send_heartbeat(); // send heartbeat
}
// Home Assistant Discovery - the main master Device called EMS-ESP
@@ -532,9 +532,10 @@ void Mqtt::ha_status() {
strcpy(uniq, "system_status");
}
doc["~"] = Mqtt::base();
doc["uniq_id"] = uniq;
doc["def_ent_id"] = (std::string) "binary_sensor." + uniq;
doc["stat_t"] = Mqtt::base() + "/status";
doc["stat_t"] = "~/status";
doc["name"] = "System status";
doc["pl_on"] = "online";
doc["pl_off"] = "offline";
@@ -981,8 +982,9 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
return queue_remove_topic(topic);
}
// build the full payload
// build the full topic's payload
JsonDocument doc;
doc["~"] = Mqtt::base();
doc["uniq_id"] = uniq_id;
// set the entity_id. This is breaking change in HA 2025.10.0 - see https://github.com/home-assistant/core/pull/151775
@@ -1000,9 +1002,9 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
char command_topic[MQTT_TOPIC_MAX_SIZE];
// add command topic
if (tag >= DeviceValueTAG::TAG_HC1) {
snprintf(command_topic, sizeof(command_topic), "%s/%s/%s/%s", Mqtt::base().c_str(), device_name, EMSdevice::tag_to_mqtt(tag), entity);
snprintf(command_topic, sizeof(command_topic), "~/%s/%s/%s", device_name, EMSdevice::tag_to_mqtt(tag), entity);
} else {
snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), device_name, entity);
snprintf(command_topic, sizeof(command_topic), "~/%s/%s", device_name, entity);
}
doc["cmd_t"] = command_topic;
@@ -1063,9 +1065,9 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
// This is where we determine which MQTT topic to pull the data from
// There is one exception for DeviceType::SYSTEM, which uses the heartbeat topic, and when fetching the version we want to take this from the info topic instead
if ((device_type == EMSdevice::DeviceType::SYSTEM) && (strncmp(entity, "version", 7) == 0)) {
snprintf(stat_t, sizeof(stat_t), "%s/%s", Mqtt::base().c_str(), F_(info));
snprintf(stat_t, sizeof(stat_t), "~/%s", F_(info));
} else {
snprintf(stat_t, sizeof(stat_t), "%s/%s", Mqtt::base().c_str(), tag_to_topic(device_type, tag).c_str());
snprintf(stat_t, sizeof(stat_t), "~/%s", tag_to_topic(device_type, tag).c_str());
}
doc["stat_t"] = stat_t;
@@ -1484,6 +1486,11 @@ void Mqtt::add_ha_avail_section(JsonObject doc, const char * state_t, const bool
avty.add(avty_json); // returns 0 if no mem
}
// add LWT (Last Will and Testament)
avty_json.clear();
avty_json["t"] = "~/status"; // as a topic
avty.add(avty_json);
doc["avty_mode"] = "all";
}

View File

@@ -197,6 +197,8 @@ void Shower::create_ha_discovery() {
char str[70];
char stat_t[50];
doc["~"] = Mqtt::base();
// shower active
doc["name"] = "Shower Active";
@@ -207,9 +209,7 @@ void Shower::create_ha_discovery() {
}
doc["uniq_id"] = str;
doc["def_ent_id"] = (std::string) "binary_sensor." + str;
snprintf(stat_t, sizeof(stat_t), "%s/shower_active", Mqtt::base().c_str());
doc["stat_t"] = stat_t;
doc["stat_t"] = "~/shower_active";
Mqtt::add_ha_bool(doc.as<JsonObject>());
Mqtt::add_ha_dev_section(doc.as<JsonObject>(), "Shower Sensor", nullptr, nullptr, nullptr, false);
@@ -225,10 +225,7 @@ void Shower::create_ha_discovery() {
doc["uniq_id"] = str;
doc["def_ent_id"] = (std::string) "sensor." + str;
snprintf(stat_t, sizeof(stat_t), "%s/shower_data", Mqtt::base().c_str());
doc["stat_t"] = stat_t;
doc["stat_t"] = "~/shower_data",
doc["name"] = "Shower Duration";
// don't bother with value template conditions if using Domoticz which doesn't fully support MQTT Discovery
@@ -248,29 +245,6 @@ void Shower::create_ha_discovery() {
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
//
// shower timestamp
//
/* commented out as the publish of timestamp
doc.clear();
snprintf(str, sizeof(str), "%s_shower_timestamp", Mqtt::basename().c_str());
doc["uniq_id"] = str;
snprintf(stat_t, sizeof(stat_t), "%s/shower_data", Mqtt::base().c_str());
doc["stat_t"] = stat_t;
doc["name"] = "Shower Timestamp";
doc["val_tpl"] = "{{value_json.timestamp if value_json.timestamp is defined else 0}}";
// doc["ent_cat"] = "diagnostic";
Mqtt::add_ha_sections_to_doc("shower", stat_t, doc, false, "value_json.timestamp is defined");
snprintf(topic, sizeof(topic), "sensor/%s/shower_timestamp/config", Mqtt::basename().c_str());
Mqtt::queue_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
*/
}
}

View File

@@ -731,11 +731,6 @@ void System::heartbeat_json(JsonObject output) {
// send periodic MQTT message with system information
void System::send_heartbeat() {
// don't send heartbeat if WiFi or MQTT is not connected
if (!Mqtt::connected()) {
return;
}
refreshHeapMem(); // refresh free heap and max alloc heap
JsonDocument doc;
@@ -997,6 +992,11 @@ int8_t System::wifi_quality(int8_t dBm) {
// print users to console
void System::show_users(uuid::console::Shell & shell) {
if (!shell.has_flags(CommandFlags::ADMIN)) {
shell.printfln("Unauthorized. You need to be an admin to view users.");
return;
}
shell.printfln("Users:");
#ifndef EMSESP_STANDALONE

View File

@@ -507,11 +507,12 @@ void TemperatureSensor::publish_values(const bool force) {
LOG_DEBUG("Recreating HA config for sensor ID %s", sensor.id().c_str());
JsonDocument config;
config["~"] = Mqtt::base();
config["dev_cla"] = "temperature";
config["stat_cla"] = "measurement";
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/%s_data", Mqtt::base().c_str(), F_(temperaturesensor)); // use base path
snprintf(stat_t, sizeof(stat_t), "~/%s_data", F_(temperaturesensor)); // use base path
config["stat_t"] = stat_t;
config["unit_of_meas"] = EMSdevice::uom_to_string(DeviceValueUOM::DEGREES);

View File

@@ -964,17 +964,17 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
48,
63);
register_device_value(
DeviceValueTAG::TAG_DHW1, &wwComfDiffTemp_, DeviceValueType::UINT8, FL_(wwComfDiffTemp), DeviceValueUOM::K, MAKE_CF_CB(set_wwComfDiffTemp), 4, 12);
DeviceValueTAG::TAG_DHW1, &wwComfDiffTemp_, DeviceValueType::UINT8, FL_(wwComfDiffTemp), DeviceValueUOM::K, MAKE_CF_CB(set_wwComfDiffTemp), 4, 15);
register_device_value(
DeviceValueTAG::TAG_DHW1, &wwEcoDiffTemp_, DeviceValueType::UINT8, FL_(wwEcoDiffTemp), DeviceValueUOM::K, MAKE_CF_CB(set_wwEcoDiffTemp), 4, 12);
DeviceValueTAG::TAG_DHW1, &wwEcoDiffTemp_, DeviceValueType::UINT8, FL_(wwEcoDiffTemp), DeviceValueUOM::K, MAKE_CF_CB(set_wwEcoDiffTemp), 4, 15);
register_device_value(DeviceValueTAG::TAG_DHW1,
&wwEcoPlusDiffTemp_,
DeviceValueType::UINT8,
FL_(wwEcoPlusDiffTemp),
DeviceValueUOM::K,
MAKE_CF_CB(set_wwEcoPlusDiffTemp),
6,
12);
4,
15);
register_device_value(DeviceValueTAG::TAG_DHW1,
&wwComfStopTemp_,
DeviceValueType::UINT8,

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.7.3-dev.32"
#define EMSESP_APP_VERSION "3.7.3-dev.34"

View File

@@ -144,6 +144,8 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) {
return;
}
api_count_++;
// send the json that came back from the command call
// sequence matches CommandRet in command.h (FAIL, OK, NOT_FOUND, ERROR, NOT_ALLOWED, INVALID, NO_VALUE)
// 400 (bad request)
@@ -153,16 +155,23 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) {
// 400 (invalid)
int ret_codes[7] = {400, 200, 404, 400, 401, 400, 404};
response->setCode(ret_codes[return_code]);
response->setLength();
response->setContentType("application/json; charset=utf-8");
request->send(response);
// serialize JSON to string to ensure correct content-length and avoid HTTP parsing errors (issue #2752)
std::string output_str;
serializeJson(output, output_str);
request->send(ret_codes[return_code], "application/json; charset=utf-8", output_str.c_str());
// std::string output_str;
// serializeJson(output, output_str);
// request->send(ret_codes[return_code], "application/json; charset=utf-8", output_str.c_str());
#if defined(EMSESP_UNITY)
// store the result so we can test with Unity later
storeResponse(output);
#endif
#if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY)
std::string output_str;
serializeJson(output, output_str);
Serial.printf("%sweb output: %s[%s] %s(%d)%s %s%s",
COLOR_WHITE,
COLOR_BRIGHT_CYAN,
@@ -175,9 +184,6 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) {
Serial.println();
EMSESP::logger().debug("web output: %s %s", request->url().c_str(), output_str.c_str());
#endif
api_count_++;
delete response;
}
#if defined(EMSESP_UNITY)

View File

@@ -411,8 +411,10 @@ void WebCustomEntityService::publish(const bool force) {
// create HA config
if (Mqtt::ha_enabled() && !ha_registered_) {
JsonDocument config;
config["~"] = Mqtt::base();
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/%s_data", Mqtt::base().c_str(), F_(custom));
snprintf(stat_t, sizeof(stat_t), "~/%s_data", F_(custom));
config["stat_t"] = stat_t;
char val_obj[50];
@@ -445,7 +447,7 @@ void WebCustomEntityService::publish(const bool force) {
snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name.c_str());
}
char command_topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(custom), entityItem.name.c_str());
snprintf(command_topic, sizeof(command_topic), "~/%s/%s", F_(custom), entityItem.name.c_str());
config["cmd_t"] = command_topic;
} else {
if (entityItem.value_type == DeviceValueType::BOOL) {

View File

@@ -263,9 +263,12 @@ void WebSchedulerService::publish(const bool force) {
// create HA config
if (Mqtt::ha_enabled() && !ha_registered_) {
JsonDocument config;
config["~"] = Mqtt::base();
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/%s_data", Mqtt::base().c_str(), F_(scheduler));
snprintf(stat_t, sizeof(stat_t), "~/%s_data", F_(scheduler));
config["stat_t"] = stat_t;
char val_obj[50];
@@ -290,7 +293,7 @@ void WebSchedulerService::publish(const bool force) {
char command_topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "switch/%s/%s_%s/config", Mqtt::basename().c_str(), F_(scheduler), scheduleItem.name.c_str());
snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(scheduler), scheduleItem.name.c_str());
snprintf(command_topic, sizeof(command_topic), "~/%s/%s", F_(scheduler), scheduleItem.name.c_str());
config["cmd_t"] = command_topic;
Mqtt::add_ha_bool(config.as<JsonObject>());

View File

@@ -422,7 +422,7 @@ void WebSettings::set_board_profile(WebSettings & settings) {
// load the board profile into the data vector
// 0=led, 1=dallas, 2=rx, 3=tx, 4=button, 5=phy_type, 6=eth_power, 7=eth_phy_addr, 8=eth_clock_mode, 9=led_type
std::vector<int8_t> data(99, 0); // initialize with 99 for all values, just as a safe guard to catch bad gpios
std::vector<int8_t> data(10, 99); // initialize with 99 for all values, just as a safe guard to catch bad gpios
if (settings.board_profile != "default") {
if (!System::load_board_profile(data, settings.board_profile.c_str())) {
#if defined(EMSESP_DEBUG)

View File

@@ -2,34 +2,63 @@
// node api_test.js
const axios = require('axios');
async function testAPI(ip = "ems-esp.local", apiPath = "system") {
const baseUrl = `http://${ip}/api`;
async function testAPI(ip = "ems-esp.local", apiPath = "system", loopCount = 1, delayMs = 1000) {
const baseUrl = `http://${ip}`;
const url = `${baseUrl}/${apiPath}`;
const results = [];
const testStartTime = Date.now();
try {
const response = await axios.get(url, {
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
});
console.log('Status:', response.status);
console.log('Data:', JSON.stringify(response.data, null, 2));
return response.data;
} catch (error) {
console.error('Error:', error.message);
if (error.response) {
console.error('Response status:', error.response.status);
console.error('Response data:', error.response.data);
for (let i = 0; i < loopCount; i++) {
let logMessage = '';
if (loopCount > 1) {
const totalElapsed = ((Date.now() - testStartTime) / 1000).toFixed(1);
logMessage = `[${totalElapsed}s] Request: ${i + 1}/${loopCount},`;
}
try {
const startTime = Date.now();
const response = await axios.get(url, {
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
});
logMessage += (logMessage ? ' ' : '') + `URL: ${url}, Status: ${response.status}`;
} catch (error) {
console.error('Error:', error.message);
// if (error.response) {
// console.error('Response status:', error.response.status);
// console.error('Response data:', error.response.data);
// }
throw error;
}
// if successful make another request to the /api/system/info endpoint to fetch the freeMem
const response = await axios.get(`${baseUrl}/api/system/info`);
const freeMem = response.data?.freeMem || response.data?.system?.freeMem;
if (freeMem !== undefined) {
logMessage += `, freeMem: ${freeMem}`;
} else {
logMessage += 'freeMem not found in response';
}
console.log(logMessage);
// Delay before next request (except for the last one)
if (i < loopCount - 1) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
throw error;
}
return loopCount === 1 ? results[0] : results;
}
// Run the test
testAPI("192.168.1.223", "system")
// Examples:
// testAPI("192.168.1.65", "api/system") - single call
// testAPI("192.168.1.65", "api/system", 5) - 5 calls with 1000ms delay
// testAPI("192.168.1.65", "api/system", 10, 2000) - 10 calls with 2000ms delay
// testAPI("192.168.1.65", "status", 20000, 5)
testAPI("192.168.1.65", "api/custom/test_custom", 1000, 5)
.then(() => {
console.log('Test completed successfully');
process.exit(0);

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"axios": "^1.13.2"
}
}

View File

@@ -288,6 +288,36 @@ void manual_test7() {
TEST_ASSERT_EQUAL_STRING(expected_response, call_url("/api/custom/test_ram", data));
}
void manual_test8() {
const char * response = call_url("/api/boiler/metrics");
TEST_ASSERT_NOT_NULL(response);
TEST_ASSERT_TRUE(strlen(response) > 0);
TEST_ASSERT_TRUE(strstr(response, "# HELP") != nullptr);
TEST_ASSERT_TRUE(strstr(response, "# TYPE") != nullptr);
TEST_ASSERT_TRUE(strstr(response, "emsesp_") != nullptr);
TEST_ASSERT_TRUE(strstr(response, " gauge") != nullptr);
TEST_ASSERT_TRUE(strstr(response, "emsesp_tapwateractive") != nullptr || strstr(response, "emsesp_selflowtemp") != nullptr
|| strstr(response, "emsesp_curflowtemp") != nullptr);
}
void manual_test9() {
const char * response = call_url("/api/thermostat/metrics");
TEST_ASSERT_NOT_NULL(response);
TEST_ASSERT_TRUE(strlen(response) > 0);
TEST_ASSERT_TRUE(strstr(response, "# HELP") != nullptr);
TEST_ASSERT_TRUE(strstr(response, "# TYPE") != nullptr);
TEST_ASSERT_TRUE(strstr(response, "emsesp_") != nullptr);
if (strstr(response, "circuit=") != nullptr) {
TEST_ASSERT_TRUE(strstr(response, "{circuit=") != nullptr);
}
}
void run_manual_tests() {
RUN_TEST(manual_test1);
RUN_TEST(manual_test2);
@@ -296,6 +326,8 @@ void run_manual_tests() {
RUN_TEST(manual_test5);
RUN_TEST(manual_test6);
RUN_TEST(manual_test7);
RUN_TEST(manual_test8);
RUN_TEST(manual_test9);
}
const char * run_console_command(const char * command) {
@@ -353,6 +385,7 @@ void create_tests() {
capture("/api/boiler/values");
capture("/api/boiler/info");
// capture("/api/boiler/entities"); // skipping since payload is too large
capture("/api/boiler/metrics");
capture("/api/boiler/comfort");
capture("/api/boiler/comfort/value");
capture("/api/boiler/comfort/fullname");
@@ -364,6 +397,7 @@ void create_tests() {
// thermostat
capture("/api/thermostat");
capture("/api/thermostat/hc1/values");
capture("/api/thermostat/metrics");
capture("/api/thermostat/hc1/seltemp");
capture("/api/thermostat/hc2/seltemp");