mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
336 lines
15 KiB
C++
336 lines
15 KiB
C++
/*
|
|
* EMS-ESP - https://github.com/emsesp/EMS-ESP
|
|
* Copyright 2020-2024 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "emsesp.h"
|
|
|
|
namespace emsesp {
|
|
|
|
using namespace std::placeholders; // for `_1` etc
|
|
|
|
bool WebCustomization::_start = true;
|
|
|
|
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);
|
|
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["ts"].to<JsonArray>();
|
|
|
|
for (const SensorCustomization & sensor : customizations.sensorCustomizations) {
|
|
JsonObject sensorJson = sensorsJson.add<JsonObject>();
|
|
sensorJson["id"] = sensor.id; // ID of chip
|
|
sensorJson["name"] = sensor.name; // n
|
|
sensorJson["offset"] = sensor.offset; // o
|
|
}
|
|
|
|
// Analog Sensor customization
|
|
JsonArray analogJson = root["as"].to<JsonArray>();
|
|
for (const AnalogCustomization & sensor : customizations.analogCustomizations) {
|
|
JsonObject sensorJson = analogJson.add<JsonObject>();
|
|
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["masked_entities"].to<JsonArray>();
|
|
for (const EntityCustomization & entityCustomization : customizations.entityCustomizations) {
|
|
JsonObject entityJson = masked_entitiesJson.add<JsonObject>();
|
|
entityJson["product_id"] = entityCustomization.product_id;
|
|
entityJson["device_id"] = entityCustomization.device_id;
|
|
|
|
// entries are in the form <XX><shortname>[|optional customname] e.g "08heatingactive|heating is on"
|
|
JsonArray masked_entityJson = entityJson["entity_ids"].to<JsonArray>();
|
|
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\"]}]}";
|
|
JsonDocument doc;
|
|
deserializeJson(doc, json);
|
|
root = doc.as<JsonObject>();
|
|
Serial.print(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<JsonArray>()) {
|
|
for (const JsonObject sensorJson : root["ts"].as<JsonArray>()) {
|
|
// create each of the sensor, overwriting any previous settings
|
|
auto sensor = SensorCustomization();
|
|
sensor.id = sensorJson["id"].as<std::string>();
|
|
sensor.name = sensorJson["name"].as<std::string>();
|
|
sensor.offset = sensorJson["offset"];
|
|
customizations.sensorCustomizations.push_back(sensor); // add to list
|
|
}
|
|
}
|
|
|
|
// Analog Sensor customization
|
|
customizations.analogCustomizations.clear();
|
|
if (root["as"].is<JsonArray>()) {
|
|
for (const JsonObject analogJson : root["as"].as<JsonArray>()) {
|
|
// create each of the sensor, overwriting any previous settings
|
|
auto sensor = AnalogCustomization();
|
|
sensor.gpio = analogJson["gpio"];
|
|
sensor.name = analogJson["name"].as<std::string>();
|
|
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<JsonArray>()) {
|
|
for (const JsonObject masked_entities : root["masked_entities"].as<JsonArray>()) {
|
|
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<JsonArray>()) {
|
|
if (masked_entity_id.is<std::string>()) {
|
|
new_entry.entity_ids.push_back(masked_entity_id.as<std::string>()); // 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);
|
|
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["devices"].to<JsonArray>();
|
|
for (const auto & emsdevice : EMSESP::emsdevices) {
|
|
if (emsdevice->has_entities()) {
|
|
JsonObject obj = devices.add<JsonObject>();
|
|
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
|
|
|
|
auto * response = new MsgpackAsyncJsonResponse(true);
|
|
|
|
// 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);
|
|
#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<JsonObject>()) {
|
|
// 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<std::string> entity_ids;
|
|
for (const JsonVariant id : entity_ids_json) {
|
|
std::string id_s = id.as<std::string>();
|
|
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<const char *>());
|
|
}
|
|
// 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<std::string>();
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|