/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2023 Paul Derbyshire
*
* 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 {
using namespace std::placeholders; // for `_1` etc
WebCustomizationService::WebCustomizationService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(WebCustomization::read,
WebCustomization::update,
this,
server,
EMSESP_CUSTOMIZATION_SERVICE_PATH,
securityManager,
AuthenticationPredicates::IS_AUTHENTICATED)
, _fsPersistence(WebCustomization::read, WebCustomization::update, this, fs, EMSESP_CUSTOMIZATION_FILE)
, _masked_entities_handler(CUSTOMIZATION_ENTITIES_PATH,
securityManager->wrapCallback(std::bind(&WebCustomizationService::customization_entities, this, _1, _2),
AuthenticationPredicates::IS_AUTHENTICATED)) {
server->on(DEVICE_ENTITIES_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&WebCustomizationService::device_entities, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(DEVICES_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&WebCustomizationService::devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(RESET_CUSTOMIZATION_SERVICE_PATH,
HTTP_POST,
securityManager->wrapRequest(std::bind(&WebCustomizationService::reset_customization, this, _1), AuthenticationPredicates::IS_ADMIN));
_masked_entities_handler.setMethod(HTTP_POST);
_masked_entities_handler.setMaxContentLength(2048);
_masked_entities_handler.setMaxJsonBufferSize(2048);
server->addHandler(&_masked_entities_handler);
}
// this creates the customization file, saving it to the FS
void WebCustomization::read(WebCustomization & customizations, JsonObject & root) {
// Temperature Sensor customization
JsonArray sensorsJson = root.createNestedArray("ts");
for (const SensorCustomization & sensor : customizations.sensorCustomizations) {
JsonObject sensorJson = sensorsJson.createNestedObject();
sensorJson["id"] = sensor.id; // ID of chip
sensorJson["name"] = sensor.name; // n
sensorJson["offset"] = sensor.offset; // o
}
// Analog Sensor customization
JsonArray analogJson = root.createNestedArray("as");
for (const AnalogCustomization & sensor : customizations.analogCustomizations) {
JsonObject sensorJson = analogJson.createNestedObject();
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
}
// Masked entities customization
JsonArray masked_entitiesJson = root.createNestedArray("masked_entities");
for (const EntityCustomization & entityCustomization : customizations.entityCustomizations) {
JsonObject entityJson = masked_entitiesJson.createNestedObject();
entityJson["product_id"] = entityCustomization.product_id;
entityJson["device_id"] = entityCustomization.device_id;
// entries are in the form [|optional customname] e.g "08heatingactive|heating is on"
JsonArray masked_entityJson = entityJson.createNestedArray("entity_ids");
for (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) {
#ifdef EMSESP_STANDALONE
// invoke some fake data for testing
const char * json = "{\"ts\":[],\"as\":[],\"masked_entities\":[{\"product_id\":123,\"device_id\":8,\"entity_ids\":[\"08heatingactive|my custom "
"name for heating active\",\"08tapwateractive\"]}]}";
StaticJsonDocument<500> doc;
deserializeJson(doc, json);
root = doc.as();
Serial.println(COLOR_BRIGHT_MAGENTA);
Serial.print(" Using fake customization file: ");
serializeJson(root, Serial);
Serial.println(COLOR_RESET);
#endif
// Temperature Sensor customization
customizations.sensorCustomizations.clear();
if (root["ts"].is()) {
for (const JsonObject sensorJson : root["ts"].as()) {
// 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"];
customizations.sensorCustomizations.push_back(sensor); // add to list
}
}
// Analog Sensor customization
customizations.analogCustomizations.clear();
if (root["as"].is()) {
for (const JsonObject analogJson : root["as"].as()) {
// create each of the sensor, overwriting any previous settings
auto sensor = AnalogCustomization();
sensor.gpio = analogJson["gpio"];
sensor.name = analogJson["name"].as();
sensor.offset = analogJson["offset"];
sensor.factor = analogJson["factor"];
sensor.uom = analogJson["uom"];
sensor.type = analogJson["type"];
customizations.analogCustomizations.push_back(sensor); // add to list
}
}
// load array of entities id's with masks, building up the object class
customizations.entityCustomizations.clear();
if (root["masked_entities"].is()) {
for (const JsonObject masked_entities : root["masked_entities"].as()) {
auto new_entry = EntityCustomization();
new_entry.product_id = masked_entities["product_id"];
new_entry.device_id = masked_entities["device_id"];
for (const JsonVariant masked_entity_id : masked_entities["entity_ids"].as()) {
if (masked_entity_id.is()) {
new_entry.entity_ids.push_back(masked_entity_id.as()); // add entity list
}
}
customizations.entityCustomizations.push_back(new_entry); // 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::system_.restart_requested(true);
return;
}
// failed
AsyncWebServerResponse * response = request->beginResponse(400); // bad request
request->send(response);
#endif
}
// send back a list of devices used in the customization web page
void WebCustomizationService::devices(AsyncWebServerRequest * request) {
auto * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_XLARGE);
JsonObject root = response->getRoot();
// list is already sorted by device type
// controller is ignored since it doesn't have any associated entities
JsonArray devices = root.createNestedArray("devices");
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->has_entities()) {
JsonObject obj = devices.createNestedObject();
obj["i"] = emsdevice->unique_id(); // its unique id
obj["s"] = std::string(emsdevice->device_type_2_device_name_translated()) + " (" + emsdevice->name() + ")"; // shortname, is device type translated
obj["tn"] = emsdevice->device_type_name(); // non-translated, lower-case
obj["t"] = emsdevice->device_type(); // internal device type ID
}
}
response->setLength();
request->send(response);
}
// send back list of device entities
void WebCustomizationService::device_entities(AsyncWebServerRequest * request) {
uint8_t id;
if (request->hasParam(F_(id))) {
id = Helpers::atoint(request->getParam(F_(id))->value().c_str()); // get id from url
size_t buffer = EMSESP_JSON_SIZE_XXXXLARGE;
auto * response = new MsgpackAsyncJsonResponse(true, buffer);
while (!response->getSize()) {
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);
#endif
#if defined(EMSESP_DEBUG)
size_t length = response->setLength();
EMSESP::logger().debug("Customization 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);
}
// 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);
}
// emsesp::EMSESP::logger().info(id.as());
}
// add deleted entities from file
EMSESP::webCustomizationService.read([&](WebCustomization & settings) {
for (EntityCustomization entityCustomization : settings.entityCustomizations) {
if (entityCustomization.device_id == device_id) {
for (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
EMSESP::webCustomizationService.update(
[&](WebCustomization & settings) {
// see if we already have a mask list for this device, if so remove it
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;
}
}
// create a new entry for this device if there are values
EntityCustomization new_entry;
new_entry.product_id = product_id;
new_entry.device_id = device_id;
new_entry.entity_ids = entity_ids;
// add the record and save
settings.entityCustomizations.push_back(new_entry);
return StateUpdateResult::CHANGED;
},
"local");
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();
}
} // namespace emsesp