mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-09 17:29:50 +03:00
Compare commits
19 Commits
9f467ecec1
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9889b1b5c4 | ||
|
|
bffccc585a | ||
|
|
a01f10b042 | ||
|
|
64058b0f61 | ||
|
|
d7b5c81b0e | ||
|
|
02e8dba971 | ||
|
|
59878fb190 | ||
|
|
9ff0f83af9 | ||
|
|
e6f825371e | ||
|
|
45f3f23033 | ||
|
|
ffd27db208 | ||
|
|
a452d6131b | ||
|
|
03ef981765 | ||
|
|
9ca9f25fd3 | ||
|
|
41122dddb2 | ||
|
|
1e0c94d007 | ||
|
|
3e42a7fb4c | ||
|
|
a8fcc1fb44 | ||
|
|
e43416019d |
24
.github/workflows/dev_release.yml
vendored
24
.github/workflows/dev_release.yml
vendored
@@ -64,29 +64,7 @@ jobs:
|
|||||||
- name: Commit the generated files
|
- name: Commit the generated files
|
||||||
uses: stefanzweifel/git-auto-commit-action@v5
|
uses: stefanzweifel/git-auto-commit-action@v5
|
||||||
with:
|
with:
|
||||||
commit_message: "chore: update generated files"
|
commit_message: "chore: update generated files for v${{steps.build_info.outputs.VERSION}}"
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
id: 'automatic_releases'
|
id: 'automatic_releases'
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,7 +2,6 @@
|
|||||||
.vscode/c_cpp_properties.json
|
.vscode/c_cpp_properties.json
|
||||||
.vscode/extensions.json
|
.vscode/extensions.json
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode/settings.json
|
|
||||||
|
|
||||||
# c++ compiling
|
# c++ compiling
|
||||||
.clang_complete
|
.clang_complete
|
||||||
@@ -73,7 +72,6 @@ logs/*
|
|||||||
sdkconfig.*
|
sdkconfig.*
|
||||||
sdkconfig_tasmota_esp32
|
sdkconfig_tasmota_esp32
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
package.json
|
|
||||||
.cache/
|
.cache/
|
||||||
interface/.tsbuildinfo
|
interface/.tsbuildinfo
|
||||||
test/test_api/package-lock.json
|
test/test_api/package-lock.json
|
||||||
|
|||||||
5
Makefile
5
Makefile
@@ -21,13 +21,14 @@ endif
|
|||||||
|
|
||||||
# Optimize parallel build configuration
|
# Optimize parallel build configuration
|
||||||
UNAME_S := $(shell uname -s)
|
UNAME_S := $(shell uname -s)
|
||||||
|
JOBS ?= 1
|
||||||
ifeq ($(UNAME_S),Linux)
|
ifeq ($(UNAME_S),Linux)
|
||||||
EXTRA_CPPFLAGS = -D LINUX
|
EXTRA_CPPFLAGS = -D LINUX
|
||||||
JOBS ?= $(shell nproc)
|
JOBS := $(shell nproc)
|
||||||
endif
|
endif
|
||||||
ifeq ($(UNAME_S),Darwin)
|
ifeq ($(UNAME_S),Darwin)
|
||||||
EXTRA_CPPFLAGS = -D OSX -Wno-tautological-constant-out-of-range-compare
|
EXTRA_CPPFLAGS = -D OSX -Wno-tautological-constant-out-of-range-compare
|
||||||
JOBS ?= $(shell sysctl -n hw.ncpu)
|
JOBS := $(shell sysctl -n hw.ncpu)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Set optimal parallel build settings
|
# Set optimal parallel build settings
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"upload": {
|
"upload": {
|
||||||
"flash_size": "32MB",
|
"flash_size": "32MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
"maximum_size": 16777216,
|
"maximum_size": 33554432,
|
||||||
"require_upload_port": true,
|
"require_upload_port": true,
|
||||||
"speed": 460800
|
"speed": 460800
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
"@alova/adapter-xhr": "2.3.0",
|
"@alova/adapter-xhr": "2.3.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^7.3.5",
|
"@mui/icons-material": "^7.3.6",
|
||||||
"@mui/material": "^7.3.5",
|
"@mui/material": "^7.3.6",
|
||||||
"@preact/compat": "^18.3.1",
|
"@preact/compat": "^18.3.1",
|
||||||
"@table-library/react-table-library": "4.1.15",
|
"@table-library/react-table-library": "4.1.15",
|
||||||
"alova": "3.4.0",
|
"alova": "3.4.0",
|
||||||
@@ -37,11 +37,11 @@
|
|||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"mime-types": "^3.0.2",
|
"mime-types": "^3.0.2",
|
||||||
"preact": "^10.27.2",
|
"preact": "^10.28.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router": "^7.9.6",
|
"react-router": "^7.10.1",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"typesafe-i18n": "^5.26.2",
|
"typesafe-i18n": "^5.26.2",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
@@ -59,11 +59,11 @@
|
|||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"prettier": "^3.7.3",
|
"prettier": "^3.7.4",
|
||||||
"rollup-plugin-visualizer": "^6.0.5",
|
"rollup-plugin-visualizer": "^6.0.5",
|
||||||
"terser": "^5.44.1",
|
"terser": "^5.44.1",
|
||||||
"typescript-eslint": "^8.48.0",
|
"typescript-eslint": "^8.48.1",
|
||||||
"vite": "^7.2.4",
|
"vite": "^7.2.6",
|
||||||
"vite-plugin-imagemin": "^0.6.1",
|
"vite-plugin-imagemin": "^0.6.1",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
476
interface/pnpm-lock.yaml
generated
476
interface/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@
|
|||||||
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
|
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
|
||||||
"formidable": "^3.5.4",
|
"formidable": "^3.5.4",
|
||||||
"itty-router": "^5.0.22",
|
"itty-router": "^5.0.22",
|
||||||
"prettier": "^3.7.3"
|
"prettier": "^3.7.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a"
|
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a"
|
||||||
}
|
}
|
||||||
|
|||||||
16
mock-api/pnpm-lock.yaml
generated
16
mock-api/pnpm-lock.yaml
generated
@@ -13,7 +13,7 @@ importers:
|
|||||||
version: 3.1.2
|
version: 3.1.2
|
||||||
'@trivago/prettier-plugin-sort-imports':
|
'@trivago/prettier-plugin-sort-imports':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0(prettier@3.7.3)
|
version: 6.0.0(prettier@3.7.4)
|
||||||
formidable:
|
formidable:
|
||||||
specifier: ^3.5.4
|
specifier: ^3.5.4
|
||||||
version: 3.5.4
|
version: 3.5.4
|
||||||
@@ -21,8 +21,8 @@ importers:
|
|||||||
specifier: ^5.0.22
|
specifier: ^5.0.22
|
||||||
version: 5.0.22
|
version: 5.0.22
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.7.3
|
specifier: ^3.7.4
|
||||||
version: 3.7.3
|
version: 3.7.4
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -167,8 +167,8 @@ packages:
|
|||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
prettier@3.7.3:
|
prettier@3.7.4:
|
||||||
resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==}
|
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 1.8.0
|
||||||
|
|
||||||
'@trivago/prettier-plugin-sort-imports@6.0.0(prettier@3.7.3)':
|
'@trivago/prettier-plugin-sort-imports@6.0.0(prettier@3.7.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/generator': 7.28.5
|
'@babel/generator': 7.28.5
|
||||||
'@babel/parser': 7.28.5
|
'@babel/parser': 7.28.5
|
||||||
@@ -256,7 +256,7 @@ snapshots:
|
|||||||
lodash-es: 4.17.21
|
lodash-es: 4.17.21
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
parse-imports-exports: 0.2.4
|
parse-imports-exports: 0.2.4
|
||||||
prettier: 3.7.3
|
prettier: 3.7.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -311,6 +311,6 @@ snapshots:
|
|||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
prettier@3.7.3: {}
|
prettier@3.7.4: {}
|
||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|||||||
@@ -632,9 +632,6 @@ void EMSdevice::add_device_value(int8_t tag, // to b
|
|||||||
devicevalues_.emplace_back(
|
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);
|
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
|
// add a new command if it has a function attached
|
||||||
if (has_cmd) {
|
if (has_cmd) {
|
||||||
uint8_t flags = CommandFlag::ADMIN_ONLY; // executing commands require admin privileges
|
uint8_t flags = CommandFlag::ADMIN_ONLY; // executing commands require admin privileges
|
||||||
@@ -1776,8 +1773,11 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) {
|
|||||||
metric_name = metric_name.substr(last_dot + 1);
|
metric_name = metric_name.substr(last_dot + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sanitize metric name: convert to lowercase and replace non-alphanumeric with underscores
|
||||||
for (char & c : metric_name) {
|
for (char & c : metric_name) {
|
||||||
if (!isalnum(c) && c != '_') {
|
if (isupper(c)) {
|
||||||
|
c = tolower(c);
|
||||||
|
} else if (!isalnum(c) && c != '_') {
|
||||||
c = '_';
|
c = '_';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2212,13 +2212,14 @@ std::string EMSdevice::name() {
|
|||||||
// copy a raw value (i.e. without applying the numeric_operator) to the output buffer.
|
// copy a raw value (i.e. without applying the numeric_operator) to the output buffer.
|
||||||
// returns true on success.
|
// returns true on success.
|
||||||
int EMSdevice::get_modbus_value(uint8_t tag, const std::string & shortname, std::vector<uint16_t> & result) {
|
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
|
// find device value by shortname
|
||||||
auto index_it = devicevalue_index_.find({tag, shortname});
|
// TODO replace linear search which is inefficient
|
||||||
if (index_it == devicevalue_index_.end()) {
|
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;
|
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
|
// 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
|
// not that this will override any previously removed states
|
||||||
@@ -2299,13 +2300,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) {
|
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());
|
// 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
|
// find device value by shortname
|
||||||
auto index_it = devicevalue_index_.find({tag, shortname});
|
const auto & it = std::find_if(devicevalues_.begin(), devicevalues_.end(), [&](const DeviceValue & x) { return x.tag == tag && x.short_name == shortname; });
|
||||||
if (index_it == devicevalue_index_.end()) {
|
if (it == devicevalues_.end() && (it->short_name != shortname || it->tag != tag)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto & dv = devicevalues_[index_it->second];
|
auto & dv = *it;
|
||||||
|
|
||||||
// handle Booleans
|
// handle Booleans
|
||||||
if (dv.type == DeviceValueType::BOOL) {
|
if (dv.type == DeviceValueType::BOOL) {
|
||||||
|
|||||||
@@ -556,26 +556,6 @@ class EMSdevice {
|
|||||||
#endif
|
#endif
|
||||||
std::vector<TelegramFunction> telegram_functions_; // each EMS device has its own set of registered telegram types
|
std::vector<TelegramFunction> telegram_functions_; // each EMS device has its own set of registered telegram types
|
||||||
std::vector<DeviceValue> devicevalues_; // all the device values
|
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
|
} // namespace emsesp
|
||||||
|
|||||||
@@ -1465,6 +1465,16 @@ bool System::get_value_info(JsonObject output, const char * cmd) {
|
|||||||
return command_info("", 0, output);
|
return command_info("", 0, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check for metrics
|
||||||
|
if (!strcmp(cmd, F_(metrics))) {
|
||||||
|
std::string metrics = get_metrics_prometheus();
|
||||||
|
if (!metrics.empty()) {
|
||||||
|
output["api_data"] = metrics;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// fetch all the data from the system in a different json
|
// fetch all the data from the system in a different json
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
JsonObject root = doc.to<JsonObject>();
|
JsonObject root = doc.to<JsonObject>();
|
||||||
@@ -1548,6 +1558,233 @@ 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::unordered_map<std::string, bool> seen_metrics;
|
||||||
|
|
||||||
|
// get system data
|
||||||
|
JsonDocument doc;
|
||||||
|
JsonObject root = doc.to<JsonObject>();
|
||||||
|
(void)command_info("", 0, root);
|
||||||
|
|
||||||
|
// helper function to escape Prometheus label values
|
||||||
|
auto escape_label = [](const std::string & str) -> std::string {
|
||||||
|
std::string escaped;
|
||||||
|
for (char c : str) {
|
||||||
|
if (c == '\\') {
|
||||||
|
escaped += "\\\\";
|
||||||
|
} else if (c == '"') {
|
||||||
|
escaped += "\\\"";
|
||||||
|
} else if (c == '\n') {
|
||||||
|
escaped += "\\n";
|
||||||
|
} else {
|
||||||
|
escaped += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return escaped;
|
||||||
|
};
|
||||||
|
|
||||||
|
// helper function to sanitize metric name (convert to lowercase and replace dots with underscores)
|
||||||
|
auto sanitize_name = [](const std::string & name) -> std::string {
|
||||||
|
std::string sanitized = name;
|
||||||
|
for (char & c : sanitized) {
|
||||||
|
if (c == '.') {
|
||||||
|
c = '_';
|
||||||
|
} else if (isupper(c)) {
|
||||||
|
c = tolower(c);
|
||||||
|
} else if (!isalnum(c) && c != '_') {
|
||||||
|
c = '_';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sanitized;
|
||||||
|
};
|
||||||
|
|
||||||
|
// helper function to convert label name to lowercase
|
||||||
|
auto to_lowercase = [](const std::string & str) -> std::string {
|
||||||
|
std::string result = str;
|
||||||
|
for (char & c : result) {
|
||||||
|
if (isupper(c)) {
|
||||||
|
c = tolower(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// helper function to check if a field should be ignored
|
||||||
|
auto should_ignore = [](const std::string & path, const std::string & key) -> bool {
|
||||||
|
if (path == "system" && key == "uptime") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (path == "ntp" && key == "timestamp") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (path.find("devices[") != std::string::npos) {
|
||||||
|
if (key == "handlersReceived" || key == "handlersFetched" || key == "handlersPending" || key == "handlersIgnored") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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 (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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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 += "}";
|
||||||
|
}
|
||||||
|
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 *>();
|
||||||
|
|
||||||
|
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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, "");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// export status information including the device information
|
// export status information including the device information
|
||||||
// http://ems-esp/api/system/info
|
// http://ems-esp/api/system/info
|
||||||
bool System::command_info(const char * value, const int8_t id, JsonObject output) {
|
bool System::command_info(const char * value, const int8_t id, JsonObject output) {
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class System {
|
|||||||
|
|
||||||
static bool get_value_info(JsonObject root, const char * cmd);
|
static bool get_value_info(JsonObject root, const char * cmd);
|
||||||
static void get_value_json(JsonObject output, const std::string & circuit, const std::string & name, JsonVariant val);
|
static void get_value_json(JsonObject output, const std::string & circuit, const std::string & name, JsonVariant val);
|
||||||
|
static std::string get_metrics_prometheus();
|
||||||
|
|
||||||
#if defined(EMSESP_TEST)
|
#if defined(EMSESP_TEST)
|
||||||
static bool command_test(const char * value, const int8_t id);
|
static bool command_test(const char * value, const int8_t id);
|
||||||
|
|||||||
@@ -964,17 +964,17 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
|
|||||||
48,
|
48,
|
||||||
63);
|
63);
|
||||||
register_device_value(
|
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(
|
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,
|
register_device_value(DeviceValueTAG::TAG_DHW1,
|
||||||
&wwEcoPlusDiffTemp_,
|
&wwEcoPlusDiffTemp_,
|
||||||
DeviceValueType::UINT8,
|
DeviceValueType::UINT8,
|
||||||
FL_(wwEcoPlusDiffTemp),
|
FL_(wwEcoPlusDiffTemp),
|
||||||
DeviceValueUOM::K,
|
DeviceValueUOM::K,
|
||||||
MAKE_CF_CB(set_wwEcoPlusDiffTemp),
|
MAKE_CF_CB(set_wwEcoPlusDiffTemp),
|
||||||
6,
|
4,
|
||||||
12);
|
15);
|
||||||
register_device_value(DeviceValueTAG::TAG_DHW1,
|
register_device_value(DeviceValueTAG::TAG_DHW1,
|
||||||
&wwComfStopTemp_,
|
&wwComfStopTemp_,
|
||||||
DeviceValueType::UINT8,
|
DeviceValueType::UINT8,
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#define EMSESP_APP_VERSION "3.7.3-dev.33"
|
#define EMSESP_APP_VERSION "3.7.3-dev.34"
|
||||||
|
|||||||
@@ -3,48 +3,50 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
|
||||||
async function testAPI(ip = "ems-esp.local", apiPath = "system", loopCount = 1, delayMs = 1000) {
|
async function testAPI(ip = "ems-esp.local", apiPath = "system", loopCount = 1, delayMs = 1000) {
|
||||||
const baseUrl = `http://${ip}/api`;
|
const baseUrl = `http://${ip}`;
|
||||||
const url = `${baseUrl}/${apiPath}`;
|
const url = `${baseUrl}/${apiPath}`;
|
||||||
const results = [];
|
const results = [];
|
||||||
|
const testStartTime = Date.now();
|
||||||
|
|
||||||
for (let i = 0; i < loopCount; i++) {
|
for (let i = 0; i < loopCount; i++) {
|
||||||
let logMessage = '';
|
let logMessage = '';
|
||||||
if (loopCount > 1) {
|
if (loopCount > 1) {
|
||||||
logMessage = `--- Request ${i + 1} of ${loopCount} ---`;
|
const totalElapsed = ((Date.now() - testStartTime) / 1000).toFixed(1);
|
||||||
|
logMessage = `[${totalElapsed}s] Request: ${i + 1}/${loopCount},`;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
const response = await axios.get(url, {
|
const response = await axios.get(url, {
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logMessage += (logMessage ? ' ' : '') + `URL: ${url}, Status: ${response.status}`;
|
||||||
// console.log('Status:', response.status);
|
|
||||||
// console.log('Data:', JSON.stringify(response.data, null, 2));
|
|
||||||
|
|
||||||
// Extract and print freeMem
|
|
||||||
const freeMem = response.data?.freeMem || response.data?.system?.freeMem;
|
|
||||||
if (freeMem !== undefined) {
|
|
||||||
logMessage += (logMessage ? ' ' : '') + `System Free Memory: ${freeMem}`;
|
|
||||||
} else {
|
|
||||||
logMessage += (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));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error.message);
|
console.error('Error:', error.message);
|
||||||
if (error.response) {
|
// if (error.response) {
|
||||||
console.error('Response status:', error.response.status);
|
// console.error('Response status:', error.response.status);
|
||||||
console.error('Response data:', error.response.data);
|
// console.error('Response data:', error.response.data);
|
||||||
}
|
// }
|
||||||
throw error;
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return loopCount === 1 ? results[0] : results;
|
return loopCount === 1 ? results[0] : results;
|
||||||
@@ -52,10 +54,11 @@ async function testAPI(ip = "ems-esp.local", apiPath = "system", loopCount = 1,
|
|||||||
|
|
||||||
// Run the test
|
// Run the test
|
||||||
// Examples:
|
// Examples:
|
||||||
// testAPI("192.168.1.65", "system") - single call
|
// testAPI("192.168.1.65", "api/system") - single call
|
||||||
// testAPI("192.168.1.65", "system", 5) - 5 calls with 1000ms delay
|
// testAPI("192.168.1.65", "api/system", 5) - 5 calls with 1000ms delay
|
||||||
// testAPI("192.168.1.65", "system", 10, 2000) - 10 calls with 2000ms delay
|
// testAPI("192.168.1.65", "api/system", 10, 2000) - 10 calls with 2000ms delay
|
||||||
testAPI("192.168.1.65", "system", 20000, 5)
|
// testAPI("192.168.1.65", "status", 20000, 5)
|
||||||
|
testAPI("192.168.1.65", "api/custom/test_custom", 1000, 5)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Test completed successfully');
|
console.log('Test completed successfully');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
5
test/test_api/package.json
Normal file
5
test/test_api/package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -318,6 +318,32 @@ void manual_test9() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void manual_test10() {
|
||||||
|
const char * response = call_url("/api/system/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);
|
||||||
|
|
||||||
|
// Check for some expected system metrics
|
||||||
|
TEST_ASSERT_TRUE(strstr(response, "emsesp_system_") != nullptr || strstr(response, "emsesp_network_") != nullptr
|
||||||
|
|| strstr(response, "emsesp_api_") != nullptr);
|
||||||
|
|
||||||
|
// Check for _info metrics if present
|
||||||
|
if (strstr(response, "_info") != nullptr) {
|
||||||
|
TEST_ASSERT_TRUE(strstr(response, "_info{") != nullptr || strstr(response, "_info ") != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for device metrics if devices are present
|
||||||
|
if (strstr(response, "device") != nullptr) {
|
||||||
|
TEST_ASSERT_TRUE(strstr(response, "emsesp_device_") != nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void run_manual_tests() {
|
void run_manual_tests() {
|
||||||
RUN_TEST(manual_test1);
|
RUN_TEST(manual_test1);
|
||||||
RUN_TEST(manual_test2);
|
RUN_TEST(manual_test2);
|
||||||
@@ -328,6 +354,7 @@ void run_manual_tests() {
|
|||||||
RUN_TEST(manual_test7);
|
RUN_TEST(manual_test7);
|
||||||
RUN_TEST(manual_test8);
|
RUN_TEST(manual_test8);
|
||||||
RUN_TEST(manual_test9);
|
RUN_TEST(manual_test9);
|
||||||
|
RUN_TEST(manual_test10);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char * run_console_command(const char * command) {
|
const char * run_console_command(const char * command) {
|
||||||
@@ -411,6 +438,7 @@ void create_tests() {
|
|||||||
// system
|
// system
|
||||||
capture("/api/system");
|
capture("/api/system");
|
||||||
capture("/api/system/info");
|
capture("/api/system/info");
|
||||||
|
capture("/api/system/metrics");
|
||||||
capture("/api/system/settings/locale");
|
capture("/api/system/settings/locale");
|
||||||
capture("/api/system/fetch");
|
capture("/api/system/fetch");
|
||||||
capture("/api/system/network/values");
|
capture("/api/system/network/values");
|
||||||
|
|||||||
Reference in New Issue
Block a user