/*
* 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
bool WebCustomization::_start = true;
WebCustomizationService::WebCustomizationService(PsychicHttpServer * server, FS * fs, SecurityManager * securityManager)
: _server(server)
, _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) {
}
void WebCustomizationService::registerURI() {
_httpEndpoint.registerURI();
_server->on(CUSTOMIZATION_ENTITIES_PATH,
HTTP_POST,
_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));
}
// 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 (HS1)\",\"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"];
if (_start && sensor.type == EMSESP::analogsensor_.AnalogType::DIGITAL_OUT && sensor.uom > DeviceValue::DeviceValueUOM::NONE) {
sensor.offset = sensor.uom - 1;
}
customizations.analogCustomizations.push_back(sensor); // 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()) {
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
esp_err_t WebCustomizationService::reset_customization(PsychicRequest * request) {
#ifndef EMSESP_STANDALONE
if (LittleFS.remove(EMSESP_CUSTOMIZATION_FILE)) {
EMSESP::system_.restart_requested(true);
return request->reply(205); // restart needed
}
// failed
return request->reply(400); // bad request
#endif
}
// send back a list of devices used in the customization web page
esp_err_t WebCustomizationService::devices(PsychicRequest * request) {
PsychicJsonResponse response = PsychicJsonResponse(request, 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
}
}
return response.send();
}
// send back list of device entities
esp_err_t WebCustomizationService::device_entities(PsychicRequest * request) {
uint8_t id;
if (request->hasParam(F_(id))) {
id = Helpers::atoint(request->getParam(F_(id))->value().c_str()); // get id from url
PsychicJsonResponse response = PsychicJsonResponse(request, true, EMSESP_JSON_SIZE_XXXXLARGE, true); // is array and also msgpack
JsonArray output = response.getRoot();
// TODO add back memory managegement. Be careful we do need to free()/delete() any object we extend with new()
// 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
emsdevice->generate_values_web_customization(output);
#endif
// request->send(response);
// return;
return response.send();
}
}
}
// invalid, but send OK anyway
return request->reply(200); // OK
}
// 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
esp_err_t WebCustomizationService::customization_entities(PsychicRequest * 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::logger().info(id.as());
}
// add deleted entities from file
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
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;
}
}
}
}
return request->reply(need_reboot ? 205 : 200); // reboot or just OK
}
// load the settings when the service starts
void WebCustomizationService::begin() {
_fsPersistence.readFromFS();
}
} // namespace emsesp