This commit is contained in:
MichaelDvP
2024-10-14 13:13:32 +02:00
69 changed files with 1696 additions and 721 deletions

View File

@@ -134,8 +134,8 @@ void AnalogSensor::reload(bool get_nvs) {
// first check if the GPIO is valid. If not, force set it to disabled
if (!System::is_valid_gpio(sensor.gpio())) {
LOG_WARNING("Bad GPIO %d for Sensor %s. Disabling.", sensor.gpio(), sensor.name().c_str());
sensor.set_type(AnalogType::NOTUSED);
continue; // skip this loop pass
sensor.set_type(AnalogType::NOTUSED); // set disabled
continue; // skip this loop pass
}
if (sensor.type() == AnalogType::ADC) {
@@ -402,7 +402,9 @@ bool AnalogSensor::update(uint8_t gpio, std::string & name, double offset, doubl
// reloads the sensors in the customizations file into the sensors list
reload();
return true;
// return false if it's an invalid GPIO, an error will show in WebUI
// and reported as an error in the log
return System::is_valid_gpio(gpio);
}
// check to see if values have been updated

View File

@@ -153,7 +153,11 @@ class AnalogSensor {
return (!sensors_.empty());
}
size_t count_entities() const {
size_t count_entities(bool include_disabled = true) const {
if (!include_disabled) {
// count number of items in sensors_ where type is not set to disabled
return std::count_if(sensors_.begin(), sensors_.end(), [](const Sensor & sensor) { return sensor.type() != AnalogSensor::AnalogType::NOTUSED; });
}
return sensors_.size();
}

View File

@@ -30,7 +30,7 @@
{ 72, DeviceType::BOILER, "Logano GB1*5, Logamatic MC10", DeviceFlags::EMS_DEVICE_FLAG_EMS},
{ 81, DeviceType::BOILER, "Cascade CM10", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 84, DeviceType::BOILER, "Logamax Plus GB022", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 95, DeviceType::BOILER, "Condens 2500/5000W, Logamax/Logomatic, Cerapur Top, Greenstar, Generic HT3", DeviceFlags::EMS_DEVICE_FLAG_HT3},
{ 95, DeviceType::BOILER, "Condens, Logamax/Logomatic, Cerapur Top, Greenstar, Generic HT3", DeviceFlags::EMS_DEVICE_FLAG_HT3},
{115, DeviceType::BOILER, "Topline, GB162", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{121, DeviceType::BOILER, "Cascade MCM10", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{122, DeviceType::BOILER, "Proline", DeviceFlags::EMS_DEVICE_FLAG_NONE},

View File

@@ -33,7 +33,16 @@ uint8_t EMSdevice::count_entities() {
return count;
}
// see if there are entities, excluding any commands
// count favorites, used in Dashboard
uint8_t EMSdevice::count_entities_fav() {
uint8_t count = 0;
for (const auto & dv : devicevalues_) {
count += dv.has_state(DeviceValueState::DV_FAVORITE);
}
return count;
}
// see if there are customized entities, excluding any commands
bool EMSdevice::has_entities() const {
for (const auto & dv : devicevalues_) {
if (dv.type != DeviceValueType::CMD) {
@@ -516,6 +525,7 @@ void EMSdevice::register_telegram_type(const uint16_t telegram_type_id, const ch
}
// add to device value library, also know now as a "device entity"
// this function will also apply any customizations to the entity
void EMSdevice::add_device_value(int8_t tag, // to be used to group mqtt together, either as separate topics as a nested object
void * value_p, // pointer to the value from the .h file
uint8_t type, // one of DeviceValueType
@@ -903,24 +913,39 @@ bool EMSdevice::export_values(uint8_t device_type, JsonObject output, const int8
}
// prepare array of device values used for the WebUI
// this is used for the Dashboard and also the Devices page
// this is loosely based of the function generate_values used for the MQTT and Console
// except additional data is stored in the JSON document needed for the Web UI like the UOM and command
// v=value, u=uom, n=name, c=cmd, h=help string, s=step, m=min, x=max
void EMSdevice::generate_values_web(JsonObject output) {
// see types.ts::DeviceValue for the structure
void EMSdevice::generate_values_web(JsonObject output, const bool is_dashboard) {
// output["label"] = to_string_short();
// output["label"] = name_;
JsonArray data = output["data"].to<JsonArray>();
JsonArray nodes = output["nodes"].to<JsonArray>();
uint8_t count = 0;
for (auto & dv : devicevalues_) {
auto fullname = dv.get_fullname();
// check conditions:
// 1. fullname cannot be empty
// 2. it must have a valid value, if it is not a command like 'reset'
// 3. show favorites first
if (!dv.has_state(DeviceValueState::DV_WEB_EXCLUDE) && !fullname.empty() && (dv.hasValue() || (dv.type == DeviceValueType::CMD))) {
JsonObject obj = data.add<JsonObject>(); // create the object, we know there is a value
uint8_t fahrenheit = 0;
// 2. it must have a valid value, unless its a command like 'reset'
// 3. if is_dashboard then only show favs
bool matching_states = (is_dashboard) ? dv.has_state(DeviceValueState::DV_FAVORITE) : !dv.has_state(DeviceValueState::DV_WEB_EXCLUDE);
if (matching_states && !fullname.empty() && (dv.hasValue() || (dv.type == DeviceValueType::CMD))) {
JsonObject root_obj = nodes.add<JsonObject>(); // create the object, we know there is a value
JsonObject obj;
if (is_dashboard) {
root_obj["id"] = (unique_id() * 100) + count++; // make unique
obj = root_obj["dv"].to<JsonObject>();
} else {
obj = root_obj;
}
uint8_t fahrenheit = 0;
// handle Booleans (true, false), output as strings according to the user settings
if (dv.type == DeviceValueType::BOOL) {
@@ -1024,11 +1049,11 @@ void EMSdevice::generate_values_web(JsonObject output) {
}
}
// as generate_values_web() but stripped down to only show all entities and their state
// this is used only for WebCustomizationService::device_entities()
// as generate_values_web() but with extra data for WebCustomizationService::device_entities() (rest/deviceEntities?id=n)
// also show commands and entities that have an empty fullname
// see types.ts::DeviceEntity for the structure
void EMSdevice::generate_values_web_customization(JsonArray output) {
for (auto & dv : devicevalues_) {
// also show commands and entities that have an empty fullname
JsonObject obj = output.add<JsonObject>();
uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (dv.uom == DeviceValueUOM::DEGREES) ? 2 : (dv.uom == DeviceValueUOM::DEGREES_R) ? 1 : 0;
@@ -1069,7 +1094,8 @@ void EMSdevice::generate_values_web_customization(JsonArray output) {
}
}
// id holds the shortname and must always have a value for the WebUI table to work
// create the id
// it holds the shortname and must always have a unique value for the WebUI table to work
if (dv.tag >= DeviceValueTAG::TAG_HC1) {
char id_s[50];
snprintf(id_s, sizeof(id_s), "%s/%s", tag_to_mqtt(dv.tag), dv.short_name);
@@ -1085,7 +1111,6 @@ void EMSdevice::generate_values_web_customization(JsonArray output) {
if (fullname) {
// obj["n"] = dv.has_tag() ? std::string(tag_to_string(dv.tag)) + " " + fullname : fullname; // prefix tag
obj["n"] = fullname;
// TAG https://github.com/emsesp/EMS-ESP32/issues/1338
// obj["n"] = (dv.has_tag()) ? fullname + " " + tag_to_string(dv.tag) : fullname; // suffix tag
}
@@ -1101,8 +1126,15 @@ void EMSdevice::generate_values_web_customization(JsonArray output) {
if (dv.has_tag()) {
obj["t"] = tag_to_string(dv.tag);
}
obj["m"] = dv.state >> 4; // send back the mask state. We're only interested in the high nibble
obj["w"] = dv.has_cmd; // if writable
// the mask state. We're only interested in the high nibble which contains the flags, so shift right
// 0x80 = 128 = DV_FAVORITE
// 0x40 = 64 = DV_READONLY
// 0x20 = 32 = DV_API_MQTT_EXCLUDE
// 0x10 = 16 = DV_WEB_EXCLUDE
obj["m"] = dv.state >> 4;
obj["w"] = dv.has_cmd; // if writable
if (dv.has_cmd && (obj["v"].is<float>() || obj["v"].is<int>())) {
// set the min and max values if there are any and if entity has a value
@@ -1115,16 +1147,20 @@ void EMSdevice::generate_values_web_customization(JsonArray output) {
}
}
// apply and blacklisted/removed entities
// this is when the mask has it's high bit (0x80) set
// https://github.com/emsesp/EMS-ESP32/issues/891
EMSESP::webCustomizationService.read([&](WebCustomization & settings) {
for (EntityCustomization entityCustomization : settings.entityCustomizations) {
if (entityCustomization.device_id == device_id()) {
// entity_ids is a list of all entities with the mask prefixed in the string
for (const std::string & entity_id : entityCustomization.entity_ids) {
uint8_t mask = Helpers::hextoint(entity_id.substr(0, 2).c_str());
if (mask & 0x80) {
JsonObject obj = output.add<JsonObject>();
obj["id"] = DeviceValue::get_name(entity_id);
obj["m"] = mask;
obj["w"] = false;
obj["id"] = DeviceValue::get_name(entity_id); // set the name, it could be custom following a '|'
obj["m"] = mask; // update the mask
obj["w"] = false; // not writeable as it won't be shown
}
}
break;

View File

@@ -234,7 +234,7 @@ class EMSdevice {
enum OUTPUT_TARGET : uint8_t { API_VERBOSE, API_SHORTNAMES, MQTT, CONSOLE };
bool generate_values(JsonObject output, const int8_t tag_filter, const bool nested, const uint8_t output_target);
void generate_values_web(JsonObject output);
void generate_values_web(JsonObject output, const bool is_dashboard = false);
void generate_values_web_customization(JsonArray output);
void add_device_value(int8_t tag,
@@ -350,6 +350,15 @@ class EMSdevice {
IVT // 13
};
// Unique Identifiers for each Device type, used in Dashboard table
// 100 and above is reserved for DeviceType
enum DeviceTypeUniqueID : uint8_t {
SCHEDULER_UID = 96,
ANALOGSENSOR_UID = 97,
TEMPERATURESENSOR_UID = 98,
CUSTOM_UID = 99 // always 99
};
enum DeviceType : uint8_t {
SYSTEM = 0, // this is us (EMS-ESP)
TEMPERATURESENSOR, // for internal temperature sensors
@@ -456,6 +465,7 @@ class EMSdevice {
static constexpr uint8_t EMS_DEVICE_FLAG_CR120 = 16; // mostly like RC300, but some changes
uint8_t count_entities();
uint8_t count_entities_fav();
bool has_entities() const;
// void reserve_device_values(uint8_t elements) {
@@ -514,9 +524,12 @@ class EMSdevice {
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<uint16_t> handlers_ignored_;
#if defined(EMSESP_STANDALONE) || defined(EMSESP_TEST)
public: // so we can call it from WebCustomizationService::test()
#endif
std::vector<DeviceValue> devicevalues_; // all the device values
};
} // namespace emsesp

View File

@@ -369,6 +369,7 @@ std::string DeviceValue::get_fullname() const {
return customname;
}
// returns any custom name defined in the entity_id
std::string DeviceValue::get_name(const std::string & entity) {
auto pos = entity.find('|');
if (pos != std::string::npos) {

View File

@@ -130,7 +130,7 @@ class DeviceValue {
DV_WEB_EXCLUDE = (1 << 4), // 16 - not shown on web
DV_API_MQTT_EXCLUDE = (1 << 5), // 32 - not shown on mqtt, API
DV_READONLY = (1 << 6), // 64 - read only
DV_FAVORITE = (1 << 7) // 128 - sort to front
DV_FAVORITE = (1 << 7) // 128 - marked as a favorite
};
// numeric operators
@@ -154,7 +154,6 @@ class DeviceValue {
const char * const ** options; // options as a flash char array
const char * const * options_single; // options are not translated
int8_t numeric_operator;
uint8_t options_size; // number of options in the char array, calculated
const char * const short_name; // used in MQTT and API
const char * const * fullname; // used in Web and Console, is translated
std::string custom_fullname; // optional, from customization
@@ -164,21 +163,24 @@ class DeviceValue {
uint32_t max; // max range
uint8_t state; // DeviceValueState::*
DeviceValue(uint8_t device_type,
int8_t tag,
void * value_p,
uint8_t type,
const char * const ** options,
const char * const * options_single,
uint8_t options_size; // number of options in the char array, calculated at class initialization
DeviceValue(uint8_t device_type, // EMSdevice::DeviceType
int8_t tag, // DeviceValueTAG::*
void * value_p, // pointer to variable of any type
uint8_t type, // DeviceValueType::*
const char * const ** options, // options as a flash char array
const char * const * options_single, // options are not translated
int8_t numeric_operator,
const char * const short_name,
const char * const * fullname,
std::string & custom_fullname,
uint8_t uom,
bool has_cmd,
int16_t min,
uint32_t max,
uint8_t state);
const char * const short_name, // used in MQTT and API
const char * const * fullname, // used in Web and Console, is translated
std::string & custom_fullname, // optional, from customization
uint8_t uom, // DeviceValueUOM::*
bool has_cmd, // true if there is a Console/MQTT command which matches the short_name
int16_t min, // min range
uint32_t max, // max range
uint8_t state // DeviceValueState::* (also known as the mask)
);
bool hasValue() const;
bool has_tag() const;

View File

@@ -1698,7 +1698,7 @@ void EMSESP::loop() {
static bool show_prompt = true;
// user has to ctrl-c to create a serial console stream, exit command will close it
// user has to CTRL-D to create a serial console stream, exit command will close it
// this saves around 2kb of heap memory
if (shell_) {
if (!shell_->running()) {
@@ -1715,9 +1715,11 @@ void EMSESP::loop() {
int c = serial_console_.read();
if (c != -1) {
show_prompt = true;
Serial.println(c);
}
// https://daleswanson.org/ascii.htm#:~:text=0
if (c == '\x03') {
// CTRL-D to open
if (c == '\x04') {
start_serial_console();
}
}
@@ -1738,7 +1740,7 @@ void EMSESP::start_serial_console() {
void EMSESP::shell_prompt() {
#ifndef EMSESP_STANDALONE
serial_console_.println();
serial_console_.printf("EMS-ESP %s: press CTRL-C to activate this serial console", EMSESP_APP_VERSION);
serial_console_.printf("EMS-ESP %s: press CTRL-D to activate this serial console", EMSESP_APP_VERSION);
serial_console_.println();
#endif
}

View File

@@ -849,7 +849,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/analogsensor/info");
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/analogsensor/test_analog1");
request.url("/api/analogsensor/test_analogsensor1");
request.url("/api/analogsensor/36");
EMSESP::webAPIService.webAPIService(&request);
@@ -973,48 +973,55 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
if (single) {
// run dedicated tests only
// EMSESP::webCustomEntityService.test(); // custom entities
// EMSESP::webCustomizationService.test(); // set customizations - this will overwrite any settings in the FS
// EMSESP::temperaturesensor_.test(); // add temperature sensors
// EMSESP::webSchedulerService.test(); // run scheduler tests, and conditions
EMSESP::webCustomEntityService.test(); // custom entities
EMSESP::webCustomizationService.test(); // set customizations - this will overwrite any settings in the FS
EMSESP::temperaturesensor_.test(); // add temperature sensors
EMSESP::webSchedulerService.test(); // run scheduler tests, and conditions
// request.url("/rest/deviceEntities");
// EMSESP::webCustomizationService.device_entities(&request);
request.url("/rest/dashboardData");
EMSESP::webDataService.dashboard_data(&request);
// COMMANDS
// shell.invoke_command("call system fetch");
// request.url("/api/system/fetch");
// EMSESP::webAPIService.webAPIService(&request);
// request.url("/api/system/restart");
// EMSESP::webAPIService.webAPIService(&request);
// request.url("/api/system/format");
// EMSESP::webAPIService.webAPIService(&request);
request.method(HTTP_POST);
char data1[] = "{\"device\":\"system\", \"cmd\":\"restart\",\"id\":-1}";
deserializeJson(doc, data1);
request.url("/api");
EMSESP::webAPIService.webAPIService(&request, doc.as<JsonVariant>());
char data2[] = "{\"action\":\"customSupport\", \"param\":\"hello\"}";
deserializeJson(doc, data2);
request.url("/rest/action");
EMSESP::webStatusService.action(&request, doc.as<JsonVariant>());
char data3[] = "{\"action\":\"export\", \"param\":\"schedule\"}";
deserializeJson(doc, data3);
request.url("/rest/action");
EMSESP::webStatusService.action(&request, doc.as<JsonVariant>());
char data4[] = "{\"action\":\"export\", \"param\":\"allvalues\"}";
deserializeJson(doc, data4);
request.url("/rest/action");
EMSESP::webStatusService.action(&request, doc.as<JsonVariant>());
// request.url("/api/thermostat");
// EMSESP::webAPIService.webAPIService(&request);
// request.url("/api/thermostat/hc1");
// EMSESP::webAPIService.webAPIService(&request);
// POST COMMANDS
// request.method(HTTP_POST);
// char data1[] = "{\"device\":\"system\", \"cmd\":\"restart\",\"id\":-1}";
// deserializeJson(doc, data1);
// request.url("/api");
// EMSESP::webAPIService.webAPIService(&request, doc.as<JsonVariant>());
// char data2[] = "{\"action\":\"customSupport\", \"param\":\"hello\"}";
// deserializeJson(doc, data2);
// request.url("/rest/action");
// EMSESP::webStatusService.action(&request, doc.as<JsonVariant>());
// char data3[] = "{\"action\":\"export\", \"param\":\"schedule\"}";
// deserializeJson(doc, data3);
// request.url("/rest/action");
// EMSESP::webStatusService.action(&request, doc.as<JsonVariant>());
// char data4[] = "{\"action\":\"export\", \"param\":\"allvalues\"}";
// deserializeJson(doc, data4);
// request.url("/rest/action");
// EMSESP::webStatusService.action(&request, doc.as<JsonVariant>());
} else {
EMSESP::webCustomEntityService.test(); // custom entities
EMSESP::webCustomizationService.test(); // set customizations - this will overwrite any settings in the FS
@@ -1092,11 +1099,11 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/temperaturesensor/info");
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/temperaturesensor/test_sensor2");
request.url("/api/temperaturesensor/test_tempsensor2");
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/temperaturesensor/0B_0C0D_0E0F_1011");
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/temperaturesensor/test_sensor2/value");
request.url("/api/temperaturesensor/test_tempsensor2/value");
EMSESP::webAPIService.webAPIService(&request);
// analogsensor
@@ -1104,9 +1111,9 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/analogsensor/info");
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/analogsensor/test_analog1");
request.url("/api/analogsensor/test_analogsensor1");
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/analogsensor/test_analog1/offset");
request.url("/api/analogsensor/test_analogsensor1/offset");
EMSESP::webAPIService.webAPIService(&request);
// system calls with POST
@@ -1183,11 +1190,11 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/temperaturesensor/0B_0C0D_0E0F_XXXX");
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/temperaturesensor/test_sensor2/bad");
request.url("/api/temperaturesensor/test_tempsensor2/bad");
EMSESP::webAPIService.webAPIService(&request);
// analogsensor
request.url("/api/analogsensor/test_analog1/bad");
request.url("/api/analogsensor/test_analogsensor1/bad");
EMSESP::webAPIService.webAPIService(&request);
request.url("/api/analogsensor/test_analog10");
EMSESP::webAPIService.webAPIService(&request);

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.7.0-dev.43"
#define EMSESP_APP_VERSION "3.7.0-dev.44"

View File

@@ -473,15 +473,24 @@ uint8_t WebCustomEntityService::count_entities() {
}
// send to dashboard, msgpack don't like serialized, use number
void WebCustomEntityService::generate_value_web(JsonObject output) {
JsonArray data = output["data"].to<JsonArray>();
void WebCustomEntityService::generate_value_web(JsonObject output, const bool is_dashboard) {
JsonArray nodes = output["nodes"].to<JsonArray>();
uint8_t index = 0;
for (const CustomEntityItem & entity : *customEntityItems_) {
bool include = false;
JsonObject obj = data.add<JsonObject>(); // create the object, we know there is a value
obj["id"] = "00" + entity.name;
obj["u"] = entity.uom;
bool include = false;
JsonObject root_obj = nodes.add<JsonObject>(); // create the object, we know there is a value
JsonObject obj;
if (is_dashboard) {
root_obj["id"] = (EMSdevice::DeviceTypeUniqueID::CUSTOM_UID * 100) + index; // make unique
obj = root_obj["dv"].to<JsonObject>();
} else {
obj = root_obj;
}
obj["id"] = "00" + entity.name;
obj["u"] = entity.uom;
if (entity.writeable) {
obj["c"] = entity.name;
@@ -549,7 +558,7 @@ void WebCustomEntityService::generate_value_web(JsonObject output) {
if (include) {
index++;
} else {
data.remove(index);
nodes.remove(index);
}
}
}

View File

@@ -64,7 +64,7 @@ class WebCustomEntityService : public StatefulService<WebCustomEntity> {
void fetch();
void render_value(JsonObject output, CustomEntityItem & 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);
void generate_value_web(JsonObject output, const bool is_dashboard = false);
uint8_t count_entities();
void ha_reset() {

View File

@@ -162,8 +162,15 @@ void WebCustomizationService::reset_customization(AsyncWebServerRequest * reques
// send back list of device entities
void WebCustomizationService::device_entities(AsyncWebServerRequest * request) {
uint8_t id;
// for testing we hardcode the id to 1 - the boiler
#if defined(EMSESP_STANDALONE) && defined(EMSESP_TEST)
if (1) {
id = 1;
#else
if (request->hasParam(F_(id))) {
id = Helpers::atoint(request->getParam(F_(id))->value().c_str()); // get id from url
#endif
auto * response = new AsyncJsonResponse(true, true); // array and msgpack
@@ -178,7 +185,12 @@ void WebCustomizationService::device_entities(AsyncWebServerRequest * request) {
#ifndef EMSESP_STANDALONE
JsonArray output = response->getRoot();
emsdevice->generate_values_web_customization(output);
#else
JsonDocument doc;
JsonArray output = doc.to<JsonArray>();
emsdevice->generate_values_web_customization(output);
#endif
#if defined(EMSESP_DEBUG)
size_t length = response->setLength();
EMSESP::logger().debug("Customizations buffer used: %d", length);
@@ -348,13 +360,13 @@ void WebCustomizationService::test() {
webCustomization.sensorCustomizations.clear();
auto sensor = SensorCustomization();
sensor.id = "01_0203_0405_0607";
sensor.name = "test_sensor1";
sensor.name = "test_tempsensor1";
sensor.offset = 0;
webCustomization.sensorCustomizations.push_back(sensor);
auto sensor2 = SensorCustomization();
sensor2.id = "0B_0C0D_0E0F_1011";
sensor2.name = "test_sensor2";
sensor2.name = "test_tempsensor2";
sensor2.offset = 4;
webCustomization.sensorCustomizations.push_back(sensor2);
@@ -363,7 +375,7 @@ void WebCustomizationService::test() {
webCustomization.analogCustomizations.clear();
auto analog = AnalogCustomization();
analog.gpio = 36;
analog.name = "test_analog1";
analog.name = "test_analogsensor1";
analog.offset = 0;
analog.factor = 0.1;
analog.uom = 17;
@@ -372,22 +384,60 @@ void WebCustomizationService::test() {
analog = AnalogCustomization();
analog.gpio = 37;
analog.name = "test_analog2";
analog.name = "test_analogsensor2";
analog.offset = 0;
analog.factor = 1;
analog.uom = 0;
analog.type = 1;
webCustomization.analogCustomizations.push_back(analog);
// EMS entities
analog = AnalogCustomization();
analog.gpio = 38;
analog.name = "test_analogsensor3";
analog.offset = 0;
analog.factor = 1;
analog.uom = 0;
analog.type = 0; // disabled, not-used
webCustomization.analogCustomizations.push_back(analog);
// EMS entities, mark some as favorites
webCustomization.entityCustomizations.clear();
auto emsEntity = EntityCustomization();
emsEntity.product_id = 123;
emsEntity.device_id = 8;
emsEntity.custom_name = "Custom Name!!";
emsEntity.custom_name = "My Custom Boiler";
emsEntity.entity_ids.push_back("08heatingactive|is my heating on?");
emsEntity.entity_ids.push_back("08tapwateractive");
emsEntity.entity_ids.push_back("08selflowtemp|<90");
webCustomization.entityCustomizations.push_back(emsEntity);
// since custom device name is loaded at discovery, we need to force it here
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->is_device_id(emsEntity.device_id)) {
emsdevice->custom_name(emsEntity.custom_name);
break;
}
}
// ...and the same with the custom masks and names for entity values. It's done in EMSdevice::add_device_value()
// so we need to force it here
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->is_device_id(emsEntity.device_id)) {
// find the device value and set the mask and custom name to match the above fake data
for (auto & dv : emsdevice->devicevalues_) {
if (strcmp(dv.short_name, "heatingactive") == 0) {
dv.state = DeviceValueState::DV_FAVORITE; // set as favorite
dv.custom_fullname = "is my heating on?";
} else if (strcmp(dv.short_name, "tapwateractive") == 0) {
dv.state = DeviceValueState::DV_FAVORITE; // set as favorite
} else if (strcmp(dv.short_name, "selflowtemp") == 0) {
dv.state = DeviceValueState::DV_FAVORITE; // set as favorite
}
}
break;
}
}
return StateUpdateResult::CHANGED; // persist the changes
});

View File

@@ -43,9 +43,13 @@ WebDataService::WebDataService(AsyncWebServer * server, SecurityManager * securi
server->on(EMSESP_SENSOR_DATA_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { sensor_data(request); }, AuthenticationPredicates::IS_AUTHENTICATED));
server->on(EMSESP_DASHBOARD_DATA_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest([this](AsyncWebServerRequest * request) { dashboard_data(request); }, AuthenticationPredicates::IS_AUTHENTICATED));
}
// this is used in the dashboard and contains all ems device information
// this is used in the Devices page and contains all EMS device information
// /coreData endpoint
void WebDataService::core_data(AsyncWebServerRequest * request) {
auto * response = new AsyncJsonResponse(false);
@@ -74,7 +78,7 @@ void WebDataService::core_data(AsyncWebServerRequest * request) {
uint8_t customEntities = EMSESP::webCustomEntityService.count_entities();
if (customEntities) {
JsonObject obj = devices.add<JsonObject>();
obj["id"] = 99; // the last unique id
obj["id"] = EMSdevice::DeviceTypeUniqueID::CUSTOM_UID;
obj["tn"] = Helpers::translated_word(FL_(custom_device)); // translated device type name
obj["t"] = EMSdevice::DeviceType::CUSTOM; // device type number
obj["b"] = Helpers::translated_word(FL_(na)); // brand
@@ -150,6 +154,7 @@ void WebDataService::sensor_data(AsyncWebServerRequest * request) {
}
// The unique_id is the unique record ID from the Web table to identify which device to load
// endpoint /rest/deviceData?id=n
// Compresses the JSON using MsgPack https://msgpack.org/index.html
void WebDataService::device_data(AsyncWebServerRequest * request) {
uint8_t id;
@@ -189,7 +194,7 @@ void WebDataService::device_data(AsyncWebServerRequest * request) {
}
#ifndef EMSESP_STANDALONE
if (id == 99) {
if (id == EMSdevice::DeviceTypeUniqueID::CUSTOM_UID) {
JsonObject output = response->getRoot();
EMSESP::webCustomEntityService.generate_value_web(output);
response->setLength();
@@ -219,84 +224,76 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar
}
// using the unique ID from the web find the real device type
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->unique_id() == unique_id) {
// create JSON for output
auto * response = new AsyncJsonResponse(false);
JsonObject output = response->getRoot();
// the data could be in any format, but we need string
// authenticated is always true
uint8_t return_code = CommandRet::NOT_FOUND;
uint8_t device_type = emsdevice->device_type();
// parse the command as it could have a hc or dhw prefixed, e.g. hc2/seltemp
int8_t id = -1; // default
if (device_type >= EMSdevice::DeviceType::BOILER) {
cmd = Command::parse_command_string(cmd, id); // extract hc or dhw
uint8_t device_type = EMSdevice::DeviceType::UNKNOWN;
switch (unique_id) {
case EMSdevice::DeviceTypeUniqueID::CUSTOM_UID:
device_type = EMSdevice::DeviceType::CUSTOM;
break;
case EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID:
device_type = EMSdevice::DeviceType::SCHEDULER;
break;
case EMSdevice::DeviceTypeUniqueID::TEMPERATURESENSOR_UID:
device_type = EMSdevice::DeviceType::TEMPERATURESENSOR;
break;
case EMSdevice::DeviceTypeUniqueID::ANALOGSENSOR_UID:
device_type = EMSdevice::DeviceType::ANALOGSENSOR;
break;
default:
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->unique_id() == unique_id) {
device_type = emsdevice->device_type();
break;
}
if (data.is<const char *>()) {
return_code = Command::call(device_type, cmd, data.as<const char *>(), true, id, output);
} else if (data.is<int>()) {
char s[10];
return_code = Command::call(device_type, cmd, Helpers::render_value(s, data.as<int32_t>(), 0), true, id, output);
} else if (data.is<float>()) {
char s[10];
return_code = Command::call(device_type, cmd, Helpers::render_value(s, data.as<float>(), 1), true, id, output);
} else if (data.is<bool>()) {
return_code = Command::call(device_type, cmd, data.as<bool>() ? "true" : "false", true, id, output);
}
// write log
if (return_code != CommandRet::OK) {
EMSESP::logger().err("Write command failed %s (%s)", (const char *)output["message"], Command::return_code_string(return_code));
} else {
#if defined(EMSESP_DEBUG)
EMSESP::logger().debug("Write command successful");
#endif
}
response->setCode((return_code == CommandRet::OK) ? 200 : 400); // bad request
response->setLength();
request->send(response);
return;
}
break;
}
// special check for custom entities (which have a unique id of 99)
if (unique_id == 99) {
auto * response = new AsyncJsonResponse(false);
JsonObject output = response->getRoot();
uint8_t return_code = CommandRet::NOT_FOUND;
uint8_t device_type = EMSdevice::DeviceType::CUSTOM;
// parse the command as it could have a hc or dhw prefixed, e.g. hc2/seltemp
int8_t id = -1;
if (device_type >= EMSdevice::DeviceType::BOILER) {
cmd = Command::parse_command_string(cmd, id); // extract hc or dhw
}
if (data.is<const char *>()) {
return_code = Command::call(device_type, cmd, data.as<const char *>(), true, id, output);
} else if (data.is<int>()) {
char s[10];
return_code = Command::call(device_type, cmd, Helpers::render_value(s, data.as<int32_t>(), 0), true, id, output);
} else if (data.is<float>()) {
char s[10];
return_code = Command::call(device_type, cmd, Helpers::render_value(s, data.as<float>(), 1), true, id, output);
}
if (return_code != CommandRet::OK) {
EMSESP::logger().err("Write command failed %s (%s)", (const char *)output["message"], Command::return_code_string(return_code));
} else {
#if defined(EMSESP_DEBUG)
EMSESP::logger().debug("Write command successful");
#endif
}
response->setCode((return_code == CommandRet::OK) ? 200 : 400); // bad request
response->setLength();
if (device_type == EMSdevice::DeviceType::UNKNOWN) {
EMSESP::logger().warning("Write command failed, bad device id: %d", unique_id);
AsyncWebServerResponse * response = request->beginResponse(400); // bad request
request->send(response);
return;
}
// create JSON for output
auto * response = new AsyncJsonResponse(false);
JsonObject output = response->getRoot();
// the data could be in any format, but we need string
// authenticated is always true
uint8_t return_code = CommandRet::NOT_FOUND;
// parse the command as it could have a hc or dhw prefixed, e.g. hc2/seltemp
int8_t id = -1; // default
if (device_type >= EMSdevice::DeviceType::BOILER) {
cmd = Command::parse_command_string(cmd, id); // extract hc or dhw
}
if (data.is<const char *>()) {
return_code = Command::call(device_type, cmd, data.as<const char *>(), true, id, output);
} else if (data.is<int>()) {
char s[10];
return_code = Command::call(device_type, cmd, Helpers::render_value(s, data.as<int32_t>(), 0), true, id, output);
} else if (data.is<float>()) {
char s[10];
return_code = Command::call(device_type, cmd, Helpers::render_value(s, data.as<float>(), 1), true, id, output);
} else if (data.is<bool>()) {
return_code = Command::call(device_type, cmd, data.as<bool>() ? "true" : "false", true, id, output);
}
// write log
if (return_code != CommandRet::OK) {
// is already logged by command and message contains code
// EMSESP::logger().err("Write command failed %s (%s)", (const char *)output["message"], Command::return_code_string(return_code));
} else {
#if defined(EMSESP_DEBUG)
EMSESP::logger().debug("Write command successful");
#endif
}
response->setCode((return_code == CommandRet::OK) ? 200 : 400); // bad request
response->setLength();
request->send(response);
return;
}
EMSESP::logger().warning("Write command failed, bad json");
// if we reach here, fail
AsyncWebServerResponse * response = request->beginResponse(400); // bad request
request->send(response);
@@ -346,4 +343,131 @@ void WebDataService::write_analog_sensor(AsyncWebServerRequest * request, JsonVa
request->send(response);
}
// this is used in the dashboard and contains all ems device information
// /dashboardData endpoint
void WebDataService::dashboard_data(AsyncWebServerRequest * request) {
auto * response = new AsyncJsonResponse(true, true); // its an Array and also msgpack'd
#if defined(EMSESP_STANDALONE)
JsonDocument doc;
JsonArray root = doc.to<JsonArray>();
#else
JsonArray root = response->getRoot();
#endif
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->count_entities_fav()) {
JsonObject obj = root.add<JsonObject>();
obj["id"] = emsdevice->unique_id(); // it's unique id
obj["n"] = emsdevice->name(); // custom name
obj["t"] = emsdevice->device_type(); // device type number
emsdevice->generate_values_web(obj, true); // is_dashboard = true
}
}
// add custom entities, if we have any
if (EMSESP::webCustomEntityService.count_entities()) {
JsonObject obj = root.add<JsonObject>();
obj["id"] = EMSdevice::DeviceTypeUniqueID::CUSTOM_UID; // it's unique id
obj["t"] = EMSdevice::DeviceType::CUSTOM; // device type number
EMSESP::webCustomEntityService.generate_value_web(obj, true);
}
// add temperature sensors
if (EMSESP::temperaturesensor_.have_sensors()) {
JsonObject obj = root.add<JsonObject>();
obj["id"] = EMSdevice::DeviceTypeUniqueID::TEMPERATURESENSOR_UID; // it's unique id
obj["t"] = EMSdevice::DeviceType::TEMPERATURESENSOR; // device type number
JsonArray nodes = obj["nodes"].to<JsonArray>();
uint8_t count = 0;
for (const auto & sensor : EMSESP::temperaturesensor_.sensors()) {
JsonObject node = nodes.add<JsonObject>();
node["id"] = (EMSdevice::DeviceTypeUniqueID::TEMPERATURESENSOR_UID * 100) + count++;
JsonObject dv = node["dv"].to<JsonObject>();
dv["id"] = "00" + sensor.name();
if (EMSESP::system_.fahrenheit()) {
if (Helpers::hasValue(sensor.temperature_c)) {
dv["v"] = (float)sensor.temperature_c * 0.18 + 32;
}
dv["u"] = DeviceValueUOM::FAHRENHEIT;
} else {
if (Helpers::hasValue(sensor.temperature_c)) {
dv["v"] = (float)sensor.temperature_c / 10;
}
dv["u"] = DeviceValueUOM::DEGREES;
}
}
}
// add analog sensors, count excludes disabled entries
if (EMSESP::analog_enabled() && EMSESP::analogsensor_.count_entities(false)) {
JsonObject obj = root.add<JsonObject>();
obj["id"] = EMSdevice::DeviceTypeUniqueID::ANALOGSENSOR_UID; // it's unique id
obj["t"] = EMSdevice::DeviceType::ANALOGSENSOR; // device type number
JsonArray nodes = obj["nodes"].to<JsonArray>();
uint8_t count = 0;
for (const auto & sensor : EMSESP::analogsensor_.sensors()) {
if (sensor.type() != AnalogSensor::AnalogType::NOTUSED) { // ignore disabled
JsonObject node = nodes.add<JsonObject>();
node["id"] = (EMSdevice::DeviceTypeUniqueID::ANALOGSENSOR_UID * 100) + count++;
JsonObject dv = node["dv"].to<JsonObject>();
dv["id"] = "00" + sensor.name();
if (sensor.type() == AnalogSensor::AnalogType::DIGITAL_OUT || sensor.type() == AnalogSensor::AnalogType::DIGITAL_IN) {
char s[12];
dv["v"] = Helpers::render_boolean(s, sensor.value() != 0, true);
JsonArray l = dv["l"].to<JsonArray>();
l.add(Helpers::render_boolean(s, false, true));
l.add(Helpers::render_boolean(s, true, true));
} else {
dv["v"] = Helpers::transformNumFloat(sensor.value(), 0);
dv["u"] = sensor.uom();
}
if (sensor.type() == AnalogSensor::AnalogType::COUNTER || sensor.type() >= AnalogSensor::AnalogType::DIGITAL_OUT) {
dv["c"] = sensor.name();
}
}
}
}
// show scheduler, with name, on/off
if (EMSESP::webSchedulerService.count_entities()) {
JsonObject obj = root.add<JsonObject>();
obj["id"] = EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID; // it's unique id
obj["t"] = EMSdevice::DeviceType::SCHEDULER; // device type number
JsonArray nodes = obj["nodes"].to<JsonArray>();
uint8_t count = 0;
EMSESP::webSchedulerService.read([&](const WebScheduler & webScheduler) {
for (const ScheduleItem & scheduleItem : webScheduler.scheduleItems) {
// only add if we have a name - we don't need a u (UOM) for this
if (!scheduleItem.name.empty()) {
JsonObject node = nodes.add<JsonObject>();
node["id"] = (EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID * 100) + count++;
JsonObject dv = node["dv"].to<JsonObject>();
dv["id"] = "00" + scheduleItem.name;
dv["c"] = scheduleItem.name;
char s[12];
dv["v"] = Helpers::render_boolean(s, scheduleItem.active, true);
JsonArray l = dv["l"].to<JsonArray>();
l.add(Helpers::render_boolean(s, false, true));
l.add(Helpers::render_boolean(s, true, true));
}
}
});
}
#if defined(EMSESP_TEST) && defined(EMSESP_STANDALONE)
Serial.println();
Serial.print("All dashboard_data: ");
serializeJson(root, Serial);
Serial.println();
#endif
response->setLength();
request->send(response);
}
} // namespace emsesp

View File

@@ -23,6 +23,7 @@
#define EMSESP_CORE_DATA_SERVICE_PATH "/rest/coreData"
#define EMSESP_DEVICE_DATA_SERVICE_PATH "/rest/deviceData"
#define EMSESP_SENSOR_DATA_SERVICE_PATH "/rest/sensorData"
#define EMSESP_DASHBOARD_DATA_SERVICE_PATH "/rest/dashboardData"
// POST
#define EMSESP_WRITE_DEVICE_VALUE_SERVICE_PATH "/rest/writeDeviceValue"
@@ -44,6 +45,7 @@ class WebDataService {
void core_data(AsyncWebServerRequest * request);
void sensor_data(AsyncWebServerRequest * request);
void device_data(AsyncWebServerRequest * request);
void dashboard_data(AsyncWebServerRequest * request);
// POST
void write_device_value(AsyncWebServerRequest * request, JsonVariant json);

View File

@@ -49,10 +49,10 @@ void WebSchedulerService::begin() {
// and also calls when the Scheduler web page is refreshed
void WebScheduler::read(WebScheduler & webScheduler, JsonObject root) {
JsonArray schedule = root["schedule"].to<JsonArray>();
uint8_t counter = 0;
uint8_t counter = 1;
for (const ScheduleItem & scheduleItem : webScheduler.scheduleItems) {
JsonObject si = schedule.add<JsonObject>();
si["id"] = counter++; // id is only used to render the table and must be unique
si["id"] = counter++; // id is only used to render the table and must be unique. 0 is for Dashboard
si["flags"] = scheduleItem.flags;
si["active"] = scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE ? scheduleItem.active : false;
si["time"] = scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE ? scheduleItem.time : "";
@@ -71,33 +71,31 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu
EMSESP::webSchedulerService.ha_reset();
// build up the list of schedule items
if (root["schedule"].is<JsonArray>()) {
for (const JsonObject schedule : root["schedule"].as<JsonArray>()) {
// create each schedule item, overwriting any previous settings
// ignore the id (as this is only used in the web for table rendering)
auto si = ScheduleItem();
si.active = schedule["active"];
si.flags = schedule["flags"];
si.time = si.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE ? "" : schedule["time"].as<std::string>();
si.cmd = schedule["cmd"].as<std::string>();
si.value = schedule["value"].as<std::string>();
si.name = schedule["name"].as<std::string>();
for (const JsonObject schedule : root["schedule"].as<JsonArray>()) {
// create each schedule item, overwriting any previous settings
// ignore the id (as this is only used in the web for table rendering)
auto si = ScheduleItem();
si.active = schedule["active"];
si.flags = schedule["flags"];
si.time = si.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE ? "" : schedule["time"].as<std::string>();
si.cmd = schedule["cmd"].as<std::string>();
si.value = schedule["value"].as<std::string>();
si.name = schedule["name"].as<std::string>();
// calculated elapsed minutes
si.elapsed_min = Helpers::string2minutes(si.time);
si.retry_cnt = 0xFF; // no startup retries
// calculated elapsed minutes
si.elapsed_min = Helpers::string2minutes(si.time);
si.retry_cnt = 0xFF; // no startup retries
webScheduler.scheduleItems.push_back(si); // add to list
if (!webScheduler.scheduleItems.back().name.empty()) {
Command::add(
EMSdevice::DeviceType::SCHEDULER,
webScheduler.scheduleItems.back().name.c_str(),
[webScheduler](const char * value, const int8_t id) {
return EMSESP::webSchedulerService.command_setvalue(value, id, webScheduler.scheduleItems.back().name.c_str());
},
FL_(schedule_cmd),
CommandFlag::ADMIN_ONLY);
}
webScheduler.scheduleItems.push_back(si); // add to list
if (!webScheduler.scheduleItems.back().name.empty()) {
Command::add(
EMSdevice::DeviceType::SCHEDULER,
webScheduler.scheduleItems.back().name.c_str(),
[webScheduler](const char * value, const int8_t id) {
return EMSESP::webSchedulerService.command_setvalue(value, id, webScheduler.scheduleItems.back().name.c_str());
},
FL_(schedule_cmd),
CommandFlag::ADMIN_ONLY);
}
}
@@ -551,6 +549,19 @@ void WebSchedulerService::test() {
si.elapsed_min = 0;
si.retry_cnt = 0xFF; // no startup retries
webScheduler.scheduleItems.push_back(si);
// 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);
already_added = true;