/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "emsesp.h"
namespace emsesp {
bool WebCustomization::_start = true;
WebCustomizationService::WebCustomizationService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _fsPersistence(WebCustomization::read, WebCustomization::update, this, fs, EMSESP_CUSTOMIZATION_FILE) {
// GET
securityManager->addEndpoint(server, EMSESP_DEVICE_ENTITIES_PATH, AuthenticationPredicates::IS_AUTHENTICATED, [this](AsyncWebServerRequest * request) {
device_entities(request);
});
// POST
securityManager->addEndpoint(
server,
EMSESP_RESET_CUSTOMIZATION_SERVICE_PATH,
AuthenticationPredicates::IS_ADMIN,
[this](AsyncWebServerRequest * request) { reset_customization(request); },
HTTP_POST); // force POST
securityManager->addEndpoint(server,
EMSESP_WRITE_DEVICE_NAME_PATH,
AuthenticationPredicates::IS_AUTHENTICATED,
[this](AsyncWebServerRequest * request, JsonVariant json) { writeDeviceName(request, json); });
securityManager->addEndpoint(server,
EMSESP_CUSTOMIZATION_ENTITIES_PATH,
AuthenticationPredicates::IS_AUTHENTICATED,
[this](AsyncWebServerRequest * request, JsonVariant json) { customization_entities(request, json); });
}
// this creates the customization file, saving it to the FS
void WebCustomization::read(WebCustomization & customizations, JsonObject root) {
// Temperature Sensor customization
JsonArray sensorsJson = root["ts"].to();
for (const SensorCustomization & sensor : customizations.sensorCustomizations) {
JsonObject sensorJson = sensorsJson.add();
sensorJson["id"] = sensor.id; // ID of chip
sensorJson["name"] = sensor.name; // n
sensorJson["offset"] = sensor.offset; // o
sensorJson["is_system"] = sensor.is_system; // s for core_voltage, supply_voltage
}
// Analog Sensor customization
JsonArray analogJson = root["as"].to();
for (const AnalogCustomization & sensor : customizations.analogCustomizations) {
JsonObject sensorJson = analogJson.add();
sensorJson["gpio"] = sensor.gpio; // g
sensorJson["name"] = sensor.name; // n
sensorJson["offset"] = sensor.offset; // o
sensorJson["factor"] = sensor.factor; // f
sensorJson["uom"] = sensor.uom; // u
sensorJson["type"] = sensor.type; // t
sensorJson["is_system"] = sensor.is_system; // s for core_voltage, supply_voltage
}
// Masked entities customization and custom device name (optional)
JsonArray masked_entitiesJson = root["masked_entities"].to();
for (const EntityCustomization & entityCustomization : customizations.entityCustomizations) {
JsonObject entityJson = masked_entitiesJson.add();
entityJson["product_id"] = entityCustomization.product_id;
entityJson["device_id"] = entityCustomization.device_id;
entityJson["custom_name"] = entityCustomization.custom_name;
// entries are in the form [optional customname] e.g "08heatingactive|heating is on"
JsonArray masked_entityJson = entityJson["entity_ids"].to();
for (const std::string & entity_id : entityCustomization.entity_ids) {
masked_entityJson.add(entity_id);
}
}
}
// call on initialization and also when the page is saved via web UI
// this loads the data into the internal class
StateUpdateResult WebCustomization::update(JsonObject root, WebCustomization & customizations) {
// Temperature Sensor customization
customizations.sensorCustomizations.clear();
if (root["ts"].is()) {
auto sensorsJsons = root["ts"].as();
for (const JsonObject sensorJson : sensorsJsons) {
// create each of the sensor, overwriting any previous settings
auto sensor = SensorCustomization();
sensor.id = sensorJson["id"].as();
sensor.name = sensorJson["name"].as();
sensor.offset = sensorJson["offset"];
if (sensor.id == sensor.name) {
sensor.name = ""; // no need to store id as name
}
sensor.is_system = sensorJson["is_system"] | false;
std::replace(sensor.id.begin(), sensor.id.end(), '-', '_'); // change old ids to v3.7 style
customizations.sensorCustomizations.push_back(sensor); // add to list
}
}
// Analog Sensor customization
customizations.analogCustomizations.clear();
if (root["as"].is()) {
auto analogJsons = root["as"].as();
for (const JsonObject analogJson : analogJsons) {
// create each of the sensor, overwriting any previous settings
auto analog = AnalogCustomization();
analog.gpio = analogJson["gpio"];
analog.name = analogJson["name"].as();
analog.offset = analogJson["offset"];
analog.factor = analogJson["factor"];
analog.uom = analogJson["uom"];
analog.type = analogJson["type"];
analog.is_system = analogJson["is_system"] | false;
if (_start && analog.type == EMSESP::analogsensor_.AnalogType::DIGITAL_OUT && analog.uom > DeviceValue::DeviceValueUOM::NONE) {
analog.offset = analog.uom - 1;
}
customizations.analogCustomizations.push_back(analog); // add to list
}
}
_start = false;
// load array of entities id's with masks, building up the object class
customizations.entityCustomizations.clear();
if (root["masked_entities"].is()) {
auto masked_entities = root["masked_entities"].as();
for (const JsonObject masked_entity : masked_entities) {
auto emsEntity = EntityCustomization();
emsEntity.product_id = masked_entity["product_id"];
emsEntity.device_id = masked_entity["device_id"];
emsEntity.custom_name = masked_entity["custom_name"] | "";
auto masked_entity_ids = masked_entity["entity_ids"].as();
for (const JsonVariant masked_entity_id : masked_entity_ids) {
if (masked_entity_id.is()) {
emsEntity.entity_ids.push_back(masked_entity_id.as()); // add entity list
}
}
customizations.entityCustomizations.push_back(emsEntity); // save the new object
}
}
return StateUpdateResult::CHANGED;
}
// deletes the customization file
void WebCustomizationService::reset_customization(AsyncWebServerRequest * request) {
#ifndef EMSESP_STANDALONE
if (LittleFS.remove(EMSESP_CUSTOMIZATION_FILE)) {
AsyncWebServerResponse * response = request->beginResponse(205); // restart needed
request->send(response);
emsesp::EMSESP::system_.systemStatus(
emsesp::SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART); // will be handled by the main loop. We use pending for the Web's SystemMonitor
return;
}
// failed
AsyncWebServerResponse * response = request->beginResponse(400); // bad request
request->send(response);
#endif
}
// 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 AsyncMessagePackResponse(true); // array and msgpack
// while (!response) {
// delete response;
// buffer -= 1024;
// response = new MsgpackAsyncJsonResponse(true, buffer);
// }
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->unique_id() == id) {
#ifndef EMSESP_STANDALONE
JsonArray output = response->getRoot();
emsdevice->generate_values_web_customization(output);
#else
JsonDocument doc;
JsonArray output = doc.to();
emsdevice->generate_values_web_customization(output);
#endif
#if defined(EMSESP_DEBUG)
size_t length = response->setLength();
EMSESP::logger().debug("Customizations buffer used: %d", length);
#else
response->setLength();
#endif
request->send(response);
return;
}
}
}
// invalid, but send OK anyway
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);
}
// renames a device
// takes the unique ID and the custom name
void WebCustomizationService::writeDeviceName(AsyncWebServerRequest * request, JsonVariant json) {
if (json.is()) {
// find the device using the unique_id
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice) {
uint8_t unique_device_id = json["id"];
// find product id and device id using the unique id
if (emsdevice->unique_id() == unique_device_id) {
uint8_t product_id = emsdevice->product_id();
uint8_t device_id = emsdevice->device_id();
auto custom_name = json["name"].as();
// updates current record or creates a new one
bool entry_exists = false;
update([&](WebCustomization & settings) {
for (auto it = settings.entityCustomizations.begin(); it != settings.entityCustomizations.end();) {
if ((*it).product_id == product_id && (*it).device_id == device_id) {
(*it).custom_name = custom_name;
entry_exists = true;
break;
} else {
++it;
}
}
// if we don't have any customization for this device, create a new entry
if (!entry_exists) {
EntityCustomization new_entry;
new_entry.product_id = product_id;
new_entry.device_id = device_id;
new_entry.custom_name = custom_name;
settings.entityCustomizations.push_back(new_entry);
}
return StateUpdateResult::CHANGED;
});
// update the EMS Device record real-time
emsdevice->custom_name(custom_name);
}
}
}
}
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);
}
// takes a list of updated entities with new masks from the web UI
// saves it in the customization service
// and updates the entity list real-time
void WebCustomizationService::customization_entities(AsyncWebServerRequest * request, JsonVariant json) {
bool need_reboot = false;
if (json.is()) {
// find the device using the unique_id
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice) {
uint8_t unique_device_id = json["id"];
if (emsdevice->unique_id() == unique_device_id) {
uint8_t product_id = emsdevice->product_id();
uint8_t device_id = emsdevice->device_id();
// and set the mask and custom names immediately for any listed entities
JsonArray entity_ids_json = json["entity_ids"];
std::vector entity_ids;
for (const JsonVariant id : entity_ids_json) {
std::string id_s = id.as();
if (id_s[0] == '8') {
entity_ids.push_back(id_s);
need_reboot = true;
} else {
emsdevice->setCustomizationEntity(id_s);
}
}
// add deleted entities from file
read([&](WebCustomization & settings) {
for (EntityCustomization entityCustomization : settings.entityCustomizations) {
if (entityCustomization.device_id == device_id) {
for (const std::string & entity_id : entityCustomization.entity_ids) {
uint8_t mask = Helpers::hextoint(entity_id.substr(0, 2).c_str());
std::string name = DeviceValue::get_name(entity_id);
if (mask & 0x80) {
bool is_set = false;
for (const JsonVariant id : entity_ids_json) {
std::string id_s = id.as();
if (name == DeviceValue::get_name(id_s)) {
is_set = true;
need_reboot = true;
break;
}
}
if (!is_set) {
entity_ids.push_back(entity_id);
}
}
}
break;
}
}
});
// get list of entities that have masks set or a custom fullname
emsdevice->getCustomizationEntities(entity_ids);
// Save the list to the customization file
update([&](WebCustomization & settings) {
// see if we already have a mask list for this device, if so remove the entry
for (auto it = settings.entityCustomizations.begin(); it != settings.entityCustomizations.end();) {
if ((*it).product_id == product_id && (*it).device_id == device_id) {
it = settings.entityCustomizations.erase(it);
break;
} else {
++it;
}
}
// re-create a new entry
EntityCustomization new_entry;
new_entry.product_id = product_id;
new_entry.device_id = device_id;
new_entry.entity_ids = entity_ids;
settings.entityCustomizations.push_back(new_entry);
return StateUpdateResult::CHANGED; // save the changes
});
break;
}
}
}
}
AsyncWebServerResponse * response = request->beginResponse(need_reboot ? 205 : 200); // reboot or just OK
request->send(response);
}
// load the settings when the service starts
void WebCustomizationService::begin() {
_fsPersistence.readFromFS();
}
// hard coded tests
#ifdef EMSESP_TEST
void WebCustomizationService::load_test_data() {
update([&](WebCustomization & webCustomization) {
// Temperature sensors
webCustomization.sensorCustomizations.clear(); // delete all existing sensors
auto sensor1 = SensorCustomization();
sensor1.id = "01_0203_0405_0607";
sensor1.name = "test_tempsensor1";
sensor1.offset = 0;
sensor1.is_system = false;
webCustomization.sensorCustomizations.push_back(sensor1);
auto sensor2 = SensorCustomization();
sensor2.id = "0B_0C0D_0E0F_1011";
sensor2.name = "test_tempsensor2";
sensor2.offset = 4;
sensor2.is_system = false;
webCustomization.sensorCustomizations.push_back(sensor2);
auto sensor3 = SensorCustomization();
sensor3.id = "28_1767_7B13_2502";
sensor3.name = "gateway_temperature";
sensor3.offset = 0;
sensor3.is_system = true;
webCustomization.sensorCustomizations.push_back(sensor3);
// Analog sensors
// This actually adds the sensors as we use customizations to store them
webCustomization.analogCustomizations.clear();
auto analog = AnalogCustomization();
analog.gpio = 36;
analog.name = "test_analogsensor1";
analog.offset = 0;
analog.factor = 0.2;
analog.uom = 17;
analog.type = 3; // ADC
analog.is_system = false;
webCustomization.analogCustomizations.push_back(analog);
analog = AnalogCustomization();
analog.gpio = 37;
analog.name = "test_analogsensor2";
analog.offset = 0;
analog.factor = 1;
analog.uom = 0;
analog.type = 1; // DIGITAL_IN
analog.is_system = false;
webCustomization.analogCustomizations.push_back(analog);
analog = AnalogCustomization();
analog.gpio = 38;
analog.name = "test_analogsensor3";
analog.offset = 0;
analog.factor = 1;
analog.uom = 0;
analog.type = 0; // disabled, not-used
analog.is_system = false;
webCustomization.analogCustomizations.push_back(analog);
analog = AnalogCustomization();
analog.gpio = 33;
analog.name = "test_analogsensor4";
analog.offset = 0;
analog.factor = 1;
analog.uom = 0;
analog.type = 2; // COUNTER
analog.is_system = false;
webCustomization.analogCustomizations.push_back(analog);
analog = AnalogCustomization();
analog.gpio = 39;
analog.name = "test_analogsensor5"; // core_voltage
analog.offset = 0;
analog.factor = 0.003771;
analog.uom = 23;
analog.type = 3; // ADC
analog.is_system = true;
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 = "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
});
EMSESP::analogsensor_.reload(); // this is needed to active the analog sensors
}
#endif
} // namespace emsesp