/* * 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