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
|
||||
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
2
.gitignore
vendored
@@ -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
|
||||
|
||||
5
Makefile
5
Makefile
@@ -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
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"upload": {
|
||||
"flash_size": "32MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"maximum_size": 33554432,
|
||||
"require_upload_port": true,
|
||||
"speed": 460800
|
||||
},
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"@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.4.0",
|
||||
@@ -37,11 +37,11 @@
|
||||
"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,11 +59,11 @@
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.7.3",
|
||||
"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"
|
||||
},
|
||||
|
||||
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",
|
||||
"formidable": "^3.5.4",
|
||||
"itty-router": "^5.0.22",
|
||||
"prettier": "^3.7.3"
|
||||
"prettier": "^3.7.4"
|
||||
},
|
||||
"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
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(prettier@3.7.3)
|
||||
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.7.3
|
||||
version: 3.7.3
|
||||
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.7.3:
|
||||
resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==}
|
||||
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.7.3)':
|
||||
'@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.7.3
|
||||
prettier: 3.7.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -311,6 +311,6 @@ snapshots:
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
prettier@3.7.3: {}
|
||||
prettier@3.7.4: {}
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
@@ -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
|
||||
@@ -1776,8 +1773,11 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) {
|
||||
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) {
|
||||
if (!isalnum(c) && c != '_') {
|
||||
if (isupper(c)) {
|
||||
c = tolower(c);
|
||||
} else if (!isalnum(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.
|
||||
// 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
|
||||
@@ -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) {
|
||||
// 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) {
|
||||
|
||||
@@ -556,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
|
||||
|
||||
@@ -1465,6 +1465,16 @@ bool System::get_value_info(JsonObject output, const char * cmd) {
|
||||
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
|
||||
JsonDocument doc;
|
||||
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
|
||||
// http://ems-esp/api/system/info
|
||||
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 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)
|
||||
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,
|
||||
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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
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 results = [];
|
||||
const testStartTime = Date.now();
|
||||
|
||||
for (let i = 0; i < loopCount; i++) {
|
||||
let logMessage = '';
|
||||
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 {
|
||||
const startTime = Date.now();
|
||||
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));
|
||||
|
||||
// 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));
|
||||
}
|
||||
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);
|
||||
}
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
return loopCount === 1 ? results[0] : results;
|
||||
@@ -52,10 +54,11 @@ async function testAPI(ip = "ems-esp.local", apiPath = "system", loopCount = 1,
|
||||
|
||||
// Run the test
|
||||
// Examples:
|
||||
// testAPI("192.168.1.65", "system") - single call
|
||||
// testAPI("192.168.1.65", "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", "system", 20000, 5)
|
||||
// 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);
|
||||
|
||||
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() {
|
||||
RUN_TEST(manual_test1);
|
||||
RUN_TEST(manual_test2);
|
||||
@@ -328,6 +354,7 @@ void run_manual_tests() {
|
||||
RUN_TEST(manual_test7);
|
||||
RUN_TEST(manual_test8);
|
||||
RUN_TEST(manual_test9);
|
||||
RUN_TEST(manual_test10);
|
||||
}
|
||||
|
||||
const char * run_console_command(const char * command) {
|
||||
@@ -411,6 +438,7 @@ void create_tests() {
|
||||
// system
|
||||
capture("/api/system");
|
||||
capture("/api/system/info");
|
||||
capture("/api/system/metrics");
|
||||
capture("/api/system/settings/locale");
|
||||
capture("/api/system/fetch");
|
||||
capture("/api/system/network/values");
|
||||
|
||||
Reference in New Issue
Block a user