This commit is contained in:
MichaelDvP
2025-10-07 07:26:24 +02:00
43 changed files with 7927 additions and 7832 deletions

View File

@@ -51,6 +51,14 @@ void WebAPIService::webAPIService(AsyncWebServerRequest * request, JsonVariant j
parse(request, input);
}
// for POSTS accepting plain text data
void WebAPIService::webAPIService(AsyncWebServerRequest * request, const char * data) {
JsonDocument input_doc;
JsonObject input = input_doc.to<JsonObject>();
input["data"] = data;
parse(request, input);
}
#ifdef EMSESP_TEST
// for test.cpp and unit tests so we can invoke GETs to test the API
void WebAPIService::webAPIService(AsyncWebServerRequest * request) {
@@ -100,7 +108,7 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) {
emsesp::EMSESP::system_.refreshHeapMem();
// output json buffer
auto response = new AsyncJsonResponse(false);
auto response = new AsyncJsonResponse();
// add more mem if needed - won't be needed in ArduinoJson 7
// while (!response->getSize()) {
@@ -155,10 +163,19 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) {
storeResponse(output);
#endif
#if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY)
Serial.printf("%sweb output: %s[%s]", COLOR_WHITE, COLOR_BRIGHT_CYAN, request->url().c_str());
Serial.printf(" %s(%d)%s ", ret_codes[return_code] == 200 ? COLOR_BRIGHT_GREEN : COLOR_BRIGHT_RED, ret_codes[return_code], COLOR_YELLOW);
serializeJson(output, Serial);
Serial.println(COLOR_RESET);
std::string output_str;
serializeJson(output, output_str);
Serial.printf("%sweb output: %s[%s] %s(%d)%s %s%s",
COLOR_WHITE,
COLOR_BRIGHT_CYAN,
request->url().c_str(),
ret_codes[return_code] == 200 ? COLOR_BRIGHT_GREEN : COLOR_BRIGHT_RED,
ret_codes[return_code],
COLOR_YELLOW,
output_str.c_str(),
COLOR_RESET);
Serial.println();
EMSESP::logger().debug("web output: %s %s", request->url().c_str(), output_str.c_str());
#endif
}

View File

@@ -28,6 +28,7 @@ class WebAPIService {
WebAPIService(AsyncWebServer * server, SecurityManager * securityManager);
void webAPIService(AsyncWebServerRequest * request, JsonVariant input);
void webAPIService(AsyncWebServerRequest * request, const char * data); // for plain text data
#if defined(EMSESP_TEST)
// for test.cpp and running unit tests

View File

@@ -153,6 +153,12 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web
// set value by api command
bool WebCustomEntityService::command_setvalue(const char * value, const int8_t id, const char * name) {
// don't write if there is no value, to prevent setting an empty value by mistake when parsing attributes
if (!strlen(value)) {
EMSESP::logger().debug("can't set empty value!");
return false;
}
for (CustomEntityItem & entityItem : *customEntityItems_) {
if (Helpers::toLower(entityItem.name) == Helpers::toLower(name)) {
if (entityItem.ram == 1) {
@@ -217,7 +223,7 @@ bool WebCustomEntityService::command_setvalue(const char * value, const int8_t i
// output of a single value
// if add_uom is true it will add the UOM string to the value
void WebCustomEntityService::render_value(JsonObject output, CustomEntityItem & entity, const bool useVal, const bool web, const bool add_uom) {
void WebCustomEntityService::render_value(JsonObject output, CustomEntityItem const & entity, const bool useVal, const bool web, const bool add_uom) {
char payload[20];
std::string name = useVal ? "value" : entity.name;
switch (entity.value_type) {
@@ -286,6 +292,10 @@ void WebCustomEntityService::show_values(JsonObject output) {
// process json output for info/commands and value_info
bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) {
if (cmd == nullptr || strlen(cmd) == 0) {
return false;
}
// if no custom entries, return empty json
// even if we're looking for a specific entity
// https://github.com/emsesp/EMS-ESP32/issues/1297
@@ -315,17 +325,17 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd)
// specific value info
const char * attribute_s = Command::get_attribute(cmd);
for (auto & entity : *customEntityItems_) {
for (auto const & entity : *customEntityItems_) {
if (Helpers::toLower(entity.name) == cmd) {
get_value_json(output, entity);
return Command::set_attribute(output, cmd, attribute_s);
return Command::get_attribute(output, cmd, attribute_s);
}
}
return false; // not found
}
// build the json for specific entity
void WebCustomEntityService::get_value_json(JsonObject output, CustomEntityItem & entity) {
void WebCustomEntityService::get_value_json(JsonObject output, CustomEntityItem const & entity) {
output["name"] = entity.name;
output["fullname"] = entity.name;
output["storage"] = entity.ram ? "ram" : "ems";
@@ -681,9 +691,10 @@ bool WebCustomEntityService::get_value(std::shared_ptr<const Telegram> telegram)
// hard coded tests
// add the entity and also add the command for writeable entities
#ifdef EMSESP_TEST
void WebCustomEntityService::test() {
void WebCustomEntityService::load_test_data() {
update([&](WebCustomEntity & webCustomEntity) {
webCustomEntity.customEntityItems.clear();
webCustomEntity.customEntityItems.clear(); // delete all existing entities
auto entityItem = CustomEntityItem();
// test 1
@@ -698,6 +709,7 @@ void WebCustomEntityService::test() {
entityItem.value_type = 1;
entityItem.writeable = true;
entityItem.data = "70";
entityItem.value = 70;
webCustomEntity.customEntityItems.push_back(entityItem);
Command::add(
EMSdevice::DeviceType::CUSTOM,
@@ -751,12 +763,12 @@ void WebCustomEntityService::test() {
entityItem.type_id = 0;
entityItem.offset = 0;
entityItem.factor = 1;
entityItem.name = "seltemp";
entityItem.name = "test_seltemp";
entityItem.uom = 0;
entityItem.value_type = 8;
entityItem.writeable = true;
entityItem.data = "14";
entityItem.value = 12;
entityItem.value = 14;
webCustomEntity.customEntityItems.push_back(entityItem);
Command::add(
EMSdevice::DeviceType::CUSTOM,

View File

@@ -60,10 +60,10 @@ class WebCustomEntityService : public StatefulService<WebCustomEntity> {
void publish(const bool force = false);
bool command_setvalue(const char * value, const int8_t id, const char * name);
bool get_value_info(JsonObject output, const char * cmd);
void get_value_json(JsonObject output, CustomEntityItem & entity);
void get_value_json(JsonObject output, CustomEntityItem const & entity);
bool get_value(std::shared_ptr<const Telegram> telegram);
void fetch();
void render_value(JsonObject output, CustomEntityItem & entity, const bool useVal = false, const bool web = false, const bool add_uom = false);
void render_value(JsonObject output, CustomEntityItem const & entity, const bool useVal = false, const bool web = false, const bool add_uom = false);
void show_values(JsonObject output);
void generate_value_web(JsonObject output, const bool is_dashboard = false);
@@ -73,7 +73,7 @@ class WebCustomEntityService : public StatefulService<WebCustomEntity> {
}
#if defined(EMSESP_TEST)
void test();
void load_test_data();
#endif
private:

View File

@@ -366,10 +366,11 @@ void WebCustomizationService::begin() {
// hard coded tests
#ifdef EMSESP_TEST
void WebCustomizationService::test() {
void WebCustomizationService::load_test_data() {
update([&](WebCustomization & webCustomization) {
// Temperature sensors
webCustomization.sensorCustomizations.clear();
webCustomization.sensorCustomizations.clear(); // delete all existing sensors
auto sensor = SensorCustomization();
sensor.id = "01_0203_0405_0607";
sensor.name = "test_tempsensor1";

View File

@@ -85,7 +85,7 @@ class WebCustomizationService : public StatefulService<WebCustomization> {
void begin();
#if defined(EMSESP_TEST)
void test();
void load_test_data();
#endif
// make all functions public so we can test in the debug and standalone mode

View File

@@ -18,7 +18,8 @@
#include "emsesp.h"
#include "WebSchedulerService.h"
#include <HTTPClient.h>
#include "shuntingYard.h"
namespace emsesp {
@@ -173,7 +174,7 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) {
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (Helpers::toLower(scheduleItem.name) == cmd) {
get_value_json(output, scheduleItem);
return Command::set_attribute(output, cmd, attribute_s);
return Command::get_attribute(output, cmd, attribute_s);
}
}
@@ -321,14 +322,14 @@ uint8_t WebSchedulerService::count_entities(bool cmd_only) {
return count;
}
#include "shuntingYard.hpp"
// execute scheduled command
bool WebSchedulerService::command(const char * name, const std::string & command, const std::string & data) {
std::string cmd = Helpers::toLower(command);
// check http commands. e.g.
// tasmota(get): http://<tasmotsIP>/cm?cmnd=power%20ON
// tasmota(get): http://<tasmotaIP>/cm?cmnd=power%20ON
// shelly(get): http://<shellyIP>/relais/0?turn=on
// parse json
JsonDocument doc;
@@ -351,6 +352,7 @@ bool WebSchedulerService::command(const char * name, const std::string & command
}
std::string value = doc["value"] | data.c_str(); // extract value if its in the command, or take the data
std::string method = doc["method"] | "GET"; // default GET
commands(value, false);
// if there is data, force a POST
int httpResult = 0;
@@ -544,110 +546,38 @@ void WebSchedulerService::scheduler_task(void * pvParameters) {
// hard coded tests
#if defined(EMSESP_TEST)
void WebSchedulerService::test() {
static bool already_added = false;
if (!already_added) {
update([&](WebScheduler & webScheduler) {
// webScheduler.scheduleItems.clear();
// test 1
auto si = ScheduleItem();
si.active = true;
si.flags = 1;
si.time = "12:00";
si.cmd = "system/fetch";
si.value = "10";
si.name = "test_scheduler";
si.elapsed_min = 0;
si.retry_cnt = 0xFF; // no startup retries
void WebSchedulerService::load_test_data() {
update([&](WebScheduler & webScheduler) {
webScheduler.scheduleItems.clear(); // delete all existing schedules
webScheduler.scheduleItems.push_back(si);
// test 1
auto si = ScheduleItem();
si.active = true;
si.flags = 1;
si.time = "12:00";
si.cmd = "system/fetch";
si.value = "10";
si.name = "test_scheduler";
si.elapsed_min = 0;
si.retry_cnt = 0xFF; // no startup retries
// test 2
si = ScheduleItem();
si.active = false;
si.flags = 1;
si.time = "13:00";
si.cmd = "system/message";
si.value = "20";
si.name = ""; // to make sure its excluded from Dashboard
si.elapsed_min = 0;
si.retry_cnt = 0xFF; // no startup retries
webScheduler.scheduleItems.push_back(si);
webScheduler.scheduleItems.push_back(si);
already_added = true;
// test 2
si = ScheduleItem();
si.active = false;
si.flags = 1;
si.time = "13:00";
si.cmd = "system/message";
si.value = "20";
si.name = ""; // to make sure its excluded from Dashboard
si.elapsed_min = 0;
si.retry_cnt = 0xFF; // no startup retries
return StateUpdateResult::CHANGED; // persist the changes
});
}
webScheduler.scheduleItems.push_back(si);
// test shunting yard
std::string test_cmd = "system/message";
std::string test_value;
// should output 'locale is en'
test_value = "\"locale is \"system/settings/locale";
command("test", test_cmd.c_str(), compute(test_value).c_str());
// test with negative value
// should output 'rssi is -23'
test_value = "\"rssi is \"0+system/network/rssi";
command("test1", test_cmd.c_str(), compute(test_value).c_str());
// should output 'rssi is -23 dbm'
test_value = "\"rssi is \"(system/network/rssi)\" dBm\"";
command("test2", test_cmd.c_str(), compute(test_value).c_str());
test_value = "(custom/seltemp/value)";
command("test3", test_cmd.c_str(), compute(test_value).c_str());
test_value = "\"seltemp=\"(custom/seltemp/value)";
command("test4", test_cmd.c_str(), compute(test_value).c_str());
test_value = "(custom/seltemp)";
command("test5", test_cmd.c_str(), compute(test_value).c_str());
test_value = "boiler/flowtempoffset";
command("test7", test_cmd.c_str(), compute(test_value).c_str());
test_value = "(boiler/flowtempoffset/value)";
command("test8", test_cmd.c_str(), compute(test_value).c_str());
test_value = "(boiler/storagetemp1/value)";
command("test9", test_cmd.c_str(), compute(test_value).c_str());
// (14 - 40) * 2.8 + 5 = -67.8
test_value = "(custom/seltemp - boiler/flowtempoffset) * 2.8 + 5";
command("test10", test_cmd.c_str(), compute(test_value).c_str());
// test case conversion
test_value = "(thermostat/hc1/modetype == \"comfort\")";
command("test11a", test_cmd.c_str(), compute(test_value).c_str()); // should be 1 true
test_value = "(thermostat/hc1/modetype == \"Comfort\")";
command("test11b", test_cmd.c_str(), compute(test_value).c_str()); // should be 1 true
test_value = "(thermostat/hc1/modetype == \"unknown\")";
command("test11c", test_cmd.c_str(), compute(test_value).c_str()); // should be 0 false
// can't find entity, should fail
test_value = "(boiler/storagetemp/value1)";
command("test12", test_cmd.c_str(), compute(test_value).c_str());
// can't find attribute, should fail
test_value = "(boiler/storagetemp1/value1)";
command("test13", test_cmd.c_str(), compute(test_value).c_str());
// check when entity has no value, should pass
test_value = "(boiler/storagetemp2/value)";
command("test14", test_cmd.c_str(), compute(test_value).c_str());
// should pass
test_value = "(boiler/storagetemp1/value)";
command("test15", test_cmd.c_str(), compute(test_value).c_str());
// test HTTP POST to call HA script
// test_cmd = "{\"method\":\"POST\",\"url\":\"http://192.168.1.42:8123/api/services/script/test_notify2\", \"header\":{\"authorization\":\"Bearer "
// "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhMmNlYWI5NDgzMmI0ODE2YWQ2NzU4MjkzZDE2YWMxZSIsImlhdCI6MTcyMTM5MTI0NCwiZXhwIjoyMDM2NzUxMjQ0fQ."
// "S5sago1tEI6lNhrDCO0dM_WsVQHkD_laAjcks8tWAqo\"}}";
// command("test99", test_cmd.c_str(), "");
return StateUpdateResult::CHANGED; // persist the changes
});
}
#endif

View File

@@ -87,7 +87,7 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
bool onChange(const char * cmd);
#if defined(EMSESP_TEST)
void test();
void load_test_data();
#endif
// make all functions public so we can test in the debug and standalone mode