add own entities read from ems-bus with free factor

This commit is contained in:
MichaelDvP
2023-03-31 16:50:49 +02:00
parent 36d5df65b8
commit fa50846a05
29 changed files with 1232 additions and 20 deletions

View File

@@ -37,6 +37,7 @@ WebAPIService::WebAPIService(AsyncWebServer * server, SecurityManager * security
HTTP_GET,
securityManager->wrapRequest(std::bind(&WebAPIService::getCustomizations, this, _1), AuthenticationPredicates::IS_ADMIN));
server->on(GET_SCHEDULE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WebAPIService::getSchedule, this, _1), AuthenticationPredicates::IS_ADMIN));
server->on(GET_ENTITIES_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&WebAPIService::getEntities, this, _1), AuthenticationPredicates::IS_ADMIN));
}
// HTTP GET
@@ -209,4 +210,16 @@ void WebAPIService::getSchedule(AsyncWebServerRequest * request) {
request->send(response);
}
void WebAPIService::getEntities(AsyncWebServerRequest * request) {
auto * response = new AsyncJsonResponse(false, FS_BUFFER_SIZE);
JsonObject root = response->getRoot();
root["type"] = "entities";
System::extractSettings(EMSESP_ENTITY_FILE, "Entites", root);
response->setLength();
request->send(response);
}
} // namespace emsesp

View File

@@ -23,6 +23,7 @@
#define GET_SETTINGS_PATH "/rest/getSettings"
#define GET_CUSTOMIZATIONS_PATH "/rest/getCustomizations"
#define GET_SCHEDULE_PATH "/rest/getSchedule"
#define GET_ENTITIES_PATH "/rest/getEntities"
namespace emsesp {
@@ -53,6 +54,7 @@ class WebAPIService {
void getSettings(AsyncWebServerRequest * request);
void getCustomizations(AsyncWebServerRequest * request);
void getSchedule(AsyncWebServerRequest * request);
void getEntities(AsyncWebServerRequest * request);
};
} // namespace emsesp

View File

@@ -91,6 +91,18 @@ void WebDataService::core_data(AsyncWebServerRequest * request) {
obj["e"] = emsdevice->count_entities(); // number of entities (device values)
}
}
if (EMSESP::webEntityService.count_entities()) {
JsonObject obj = devices.createNestedObject();
obj["id"] = "99"; // the last unique id as a string
obj["tn"] = "Custom"; // translated device type name
obj["t"] = EMSdevice::DeviceType::CUSTOM; // device type number
obj["b"] = 0; // brand
obj["n"] = Helpers::translated_word(FL_(custom_device)); // name
obj["d"] = 0; // deviceid
obj["p"] = 0; // productid
obj["v"] = 0; // version
obj["e"] = EMSESP::webEntityService.count_entities(); // number of entities (device values)
}
// sensors stuff
root["s_n"] = Helpers::translated_word(FL_(sensors_device));
@@ -196,6 +208,15 @@ void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant &
return;
}
}
#ifndef EMSESP_STANDALONE
if (json["id"] == 99) {
JsonObject output = response->getRoot();
EMSESP::webEntityService.generate_value_web(output);
response->setLength();
request->send(response);
return;
}
#endif
}
// invalid but send ok
@@ -256,6 +277,37 @@ void WebDataService::write_value(AsyncWebServerRequest * request, JsonVariant &
return;
}
}
if (unique_id == 99) {
// parse the command as it could have a hc or wwc prefixed, e.g. hc2/seltemp
const char * cmd = dv["c"];
int8_t id = -1;
cmd = Command::parse_command_string(cmd, id);
auto * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL);
JsonObject output = response->getRoot();
JsonVariant data = dv["v"]; // the value in any format
uint8_t return_code = CommandRet::OK;
uint8_t device_type = EMSdevice::DeviceType::CUSTOM;
if (data.is<int>()) {
char s[10];
return_code = Command::call(device_type, cmd, Helpers::render_value(s, data.as<int16_t>(), 0), true, id, output);
} else if (data.is<float>()) {
char s[10];
return_code = Command::call(device_type, cmd, Helpers::render_value(s, data.as<float>(), 1), true, id, output);
} else if (data.is<bool>()) {
return_code = Command::call(device_type, cmd, data.as<bool>() ? "true" : "false", true, id, output);
}
if (return_code != CommandRet::OK) {
EMSESP::logger().err("Write command failed %s (%s)", (const char *)output["message"], Command::return_code_string(return_code).c_str());
} else {
#if defined(EMSESP_DEBUG)
EMSESP::logger().debug("Write command successful");
#endif
}
response->setCode((return_code == CommandRet::OK) ? 200 : 204);
response->setLength();
request->send(response);
return;
}
}
AsyncWebServerResponse * response = request->beginResponse(204); // Write command failed

View File

@@ -0,0 +1,429 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "emsesp.h"
namespace emsesp {
using namespace std::placeholders; // for `_1` etc
WebEntityService::WebEntityService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(WebEntity::read, WebEntity::update, this, server, EMSESP_ENTITY_SERVICE_PATH, securityManager, AuthenticationPredicates::IS_AUTHENTICATED)
, _fsPersistence(WebEntity::read, WebEntity::update, this, fs, EMSESP_ENTITY_FILE, FS_BUFFER_SIZE) {
}
// load the settings when the service starts
void WebEntityService::begin() {
_fsPersistence.readFromFS();
EMSESP::logger().info("Starting custom entity service");
}
// this creates the scheduler file, saving it to the FS
// and also calls when the Scheduler web page is refreshed
void WebEntity::read(WebEntity & webEntity, JsonObject & root) {
JsonArray entity = root.createNestedArray("entity");
for (const EntityItem & entityItem : webEntity.entityItems) {
JsonObject ei = entity.createNestedObject();
ei["device_id"] = Helpers::hextoa(entityItem.device_id, false);
ei["type_id"] = Helpers::hextoa(entityItem.type_id, false);
ei["offset"] = entityItem.offset;
ei["factor"] = entityItem.factor;
ei["name"] = entityItem.name;
ei["uom"] = entityItem.uom;
ei["val_type"] = entityItem.valuetype;
EMSESP::webEntityService.render_value(ei, entityItem, true);
}
}
// call on initialization and also when the Schedule web page is updated
// this loads the data into the internal class
StateUpdateResult WebEntity::update(JsonObject & root, WebEntity & webEntity) {
for (EntityItem & entityItem : webEntity.entityItems) {
Command::erase_command(EMSdevice::DeviceType::CUSTOM, entityItem.name.c_str());
}
webEntity.entityItems.clear();
if (root["entity"].is<JsonArray>()) {
for (const JsonObject ei : root["entity"].as<JsonArray>()) {
auto entityItem = EntityItem();
entityItem.device_id = Helpers::hextoint(ei["device_id"]);
entityItem.type_id = Helpers::hextoint(ei["type_id"]);
entityItem.offset = ei["offset"];
entityItem.factor = ei["factor"];
entityItem.name = ei["name"].as<std::string>();
entityItem.uom = ei["uom"];
entityItem.valuetype = ei["val_type"];
if (entityItem.valuetype == DeviceValueType::BOOL) {
entityItem.val = EMS_VALUE_DEFAULT_BOOL;
} else if (entityItem.valuetype == DeviceValueType::INT) {
entityItem.val = EMS_VALUE_DEFAULT_INT;
} else if (entityItem.valuetype == DeviceValueType::UINT) {
entityItem.val = EMS_VALUE_DEFAULT_UINT;
} else if (entityItem.valuetype == DeviceValueType::SHORT) {
entityItem.val = EMS_VALUE_DEFAULT_SHORT;
} else if (entityItem.valuetype == DeviceValueType::USHORT) {
entityItem.val = EMS_VALUE_DEFAULT_USHORT;
} else { // if (entityItem.valuetype == DeviceValueType::ULONG || entityItem.valuetype == DeviceValueType::TIME) {
entityItem.val = EMS_VALUE_DEFAULT_ULONG;
}
webEntity.entityItems.push_back(entityItem); // add to list
Command::add(
EMSdevice::DeviceType::CUSTOM,
webEntity.entityItems.back().name.c_str(),
[webEntity](const char * value, const int8_t id) { return EMSESP::webEntityService.command_setvalue(value, webEntity.entityItems.back().name); },
FL_(entity_cmd),
CommandFlag::ADMIN_ONLY);
}
}
return StateUpdateResult::CHANGED;
}
// set value by api command
bool WebEntityService::command_setvalue(const char * value, const std::string name) {
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
for (EntityItem & entityItem : *entityItems) {
if (entityItem.name == name) {
if (entityItem.valuetype == DeviceValueType::BOOL) {
bool v;
if (!Helpers::value2bool(value, v)) {
return false;
}
EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v ? 0xFF : 0, 0);
} else {
float f;
if (!Helpers::value2float(value, f)) {
return false;
}
int v = f / entityItem.factor;
if (entityItem.valuetype == DeviceValueType::UINT || entityItem.valuetype == DeviceValueType::INT) {
EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v, 0);
} else if (entityItem.valuetype == DeviceValueType::USHORT || entityItem.valuetype == DeviceValueType::SHORT) {
uint8_t v1[2] = {(uint8_t)(v >> 8), (uint8_t)(v & 0xFF)};
EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v1, 2, 0);
} else {
uint8_t v1[3] = {(uint8_t)(v >> 16), (uint8_t)((v & 0xFF00) >> 8), (uint8_t)(v & 0xFF)};
EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v1, 3, 0);
}
}
publish_single(entityItem);
if (EMSESP::mqtt_.get_publish_onchange(0)) {
publish();
}
return true;
}
}
return false;
}
// output of a single value
void WebEntityService::render_value(JsonObject & output, EntityItem entity, const bool useVal) {
char payload[12];
std::string name = useVal ? "value" : entity.name;
switch (entity.valuetype) {
case DeviceValueType::BOOL:
if ((uint8_t)entity.val != EMS_VALUE_BOOL_NOTSET) {
if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) {
output[name] = (uint8_t)entity.val ? true : false;
} else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) {
output[name] = (uint8_t)entity.val ? 1 : 0;
} else {
output[name] = Helpers::render_boolean(payload, (uint8_t)entity.val);
}
}
break;
case DeviceValueType::INT:
if ((int8_t)entity.val != EMS_VALUE_INT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (int8_t)entity.val, 2));
}
break;
case DeviceValueType::UINT:
if ((uint8_t)entity.val != EMS_VALUE_UINT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint8_t)entity.val, 2));
}
break;
case DeviceValueType::SHORT:
if ((int16_t)entity.val != EMS_VALUE_SHORT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (int16_t)entity.val, 2));
}
break;
case DeviceValueType::USHORT:
if ((uint16_t)entity.val != EMS_VALUE_USHORT_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * (uint16_t)entity.val, 2));
}
break;
case DeviceValueType::ULONG:
case DeviceValueType::TIME:
if (entity.val != EMS_VALUE_ULONG_NOTSET) {
output[name] = serialized(Helpers::render_value(payload, entity.factor * entity.val, 2));
}
break;
default:
// EMSESP::logger().warning("unknown value type");
break;
}
}
// process json output for info/commands and value_info
bool WebEntityService::get_value_info(JsonObject & output, const char * cmd) {
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
if (entityItems->size() == 0) {
return false;
}
if (Helpers::toLower(cmd) == "commands") {
output["info"] = "lists all values";
output["commands"] = "lists all commands";
for (const auto & entity : *entityItems) {
output[entity.name] = "custom entitiy";
}
return true;
}
if (strlen(cmd) == 0 || Helpers::toLower(cmd) == "values" || Helpers::toLower(cmd) == "info") {
// list all names
for (const EntityItem & entity : *entityItems) {
render_value(output, entity);
}
return (output.size() != 0);
}
char command_s[30];
strlcpy(command_s, cmd, sizeof(command_s));
char * attribute_s = nullptr;
// check specific attribute to fetch instead of the complete record
char * breakp = strchr(command_s, '/');
if (breakp) {
*breakp = '\0';
attribute_s = breakp + 1;
}
for (const auto & entity : *entityItems) {
if (Helpers::toLower(entity.name) == Helpers::toLower(command_s)) {
output["name"] = entity.name;
output["uom"] = EMSdevice::uom_to_string(entity.uom);
output["readable"] = true;
output["writeable"] = true;
output["visible"] = true;
render_value(output, entity, true);
if (attribute_s) {
if (output.containsKey(attribute_s)) {
JsonVariant data = output[attribute_s];
output.clear();
output["api_data"] = data;
} else {
char error[100];
snprintf(error, sizeof(error), "cannot find attribute %s in entity %s", attribute_s, command_s);
output.clear();
output["message"] = error;
}
}
}
if (output.size()) {
return true;
}
}
output["message"] = "unknown command";
return false;
}
// publish single value
void WebEntityService::publish_single(const EntityItem & entity) {
if (!Mqtt::enabled() || !Mqtt::publish_single()) {
return;
}
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
if (Mqtt::publish_single2cmd()) {
snprintf(topic, sizeof(topic), "%s/%s", "custom", entity.name.c_str());
} else {
snprintf(topic, sizeof(topic), "%s/%s", "custom_data", entity.name.c_str());
}
StaticJsonDocument<256> doc;
JsonObject output = doc.to<JsonObject>();
render_value(output, entity, true);
Mqtt::queue_publish(topic, output["value"].as<std::string>());
}
// publish to Mqtt
void WebEntityService::publish(const bool force) {
if (force) {
ha_registered_ = false;
}
if (!Mqtt::enabled()) {
return;
}
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
if (entityItems->size() == 0) {
return;
}
if (Mqtt::publish_single() && force) {
for (const EntityItem & entityItem : *entityItems) {
publish_single(entityItem);
}
}
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE);
JsonObject output = doc.to<JsonObject>();
for (const EntityItem & entityItem : *entityItems) {
render_value(output, entityItem);
// create HA config
if (Mqtt::ha_enabled() && !ha_registered_) {
StaticJsonDocument<EMSESP_JSON_SIZE_MEDIUM> config;
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/custom_data", Mqtt::base().c_str());
config["stat_t"] = stat_t;
char val_obj[50];
char val_cond[65];
snprintf(val_obj, sizeof(val_obj), "value_json['%s']", entityItem.name.c_str());
snprintf(val_cond, sizeof(val_cond), "%s is defined", val_obj);
config["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + "}}";
char uniq_s[70];
snprintf(uniq_s, sizeof(uniq_s), "custom_%s", entityItem.name.c_str());
config["obj_id"] = uniq_s;
config["uniq_id"] = uniq_s; // same as object_id
config["name"] = entityItem.name.c_str();
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "sensor/%s/custom_%s/config", Mqtt::basename().c_str(), entityItem.name.c_str());
//char command_topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
// snprintf(command_topic, sizeof(command_topic), "%s/custom/%s", Mqtt::basename().c_str(), entityItem.name.c_str());
// config["cmd_t"] = command_topic;
JsonObject dev = config.createNestedObject("dev");
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp");
// add "availability" section
Mqtt::add_avty_to_doc(stat_t, config.as<JsonObject>(), val_cond);
Mqtt::queue_ha(topic, config.as<JsonObject>());
ha_registered_ = true;
}
}
if (output.size() > 0) {
Mqtt::queue_publish("custom_data", output);
}
// EMSESP::logger().debug("publish %d custom entities", output.size());
}
// count only entities with valid value
uint8_t WebEntityService::count_entities() {
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
if (entityItems->size() == 0) {
return 0;
}
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE);
JsonObject output = doc.to<JsonObject>();
for (const EntityItem & entity : *entityItems) {
render_value(output, entity);
}
return output.size();
}
// send to dashboard, msgpack don't like serialized, use number
void WebEntityService::generate_value_web(JsonObject & output) {
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
output["label"] = (std::string) "Custom Entities";
JsonArray data = output.createNestedArray("data");
for (const EntityItem & entity : *entityItems) {
JsonObject obj = data.createNestedObject(); // create the object, we know there is a value
obj["id"] = "00" + entity.name;
obj["u"] = entity.uom;
obj["c"] = entity.name;
switch (entity.valuetype) {
case DeviceValueType::BOOL: {
char s[12];
obj["v"] = Helpers::render_boolean(s, (uint8_t)entity.val);
JsonArray l = obj.createNestedArray("l");
l.add(Helpers::render_boolean(s, false, true));
l.add(Helpers::render_boolean(s, true, true));
break;
}
case DeviceValueType::INT:
if ((int8_t)entity.val != EMS_VALUE_INT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (int8_t)entity.val, 0);
}
break;
case DeviceValueType::UINT:
if ((uint8_t)entity.val != EMS_VALUE_UINT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (uint8_t)entity.val, 0);
}
break;
case DeviceValueType::SHORT:
if ((int16_t)entity.val != EMS_VALUE_SHORT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (int16_t)entity.val, 0);
}
break;
case DeviceValueType::USHORT:
if ((uint16_t)entity.val != EMS_VALUE_USHORT_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * (uint16_t)entity.val, 0);
}
break;
case DeviceValueType::ULONG:
case DeviceValueType::TIME:
if (entity.val != EMS_VALUE_ULONG_NOTSET) {
obj["v"] = Helpers::transformNumFloat(entity.factor * entity.val, 0);
}
break;
default:
break;
}
}
}
// fetch telegram, called from emsesp::fetch
void WebEntityService::fetch() {
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
for (auto & entity : *entityItems) {
EMSESP::send_read_request(entity.type_id, entity.device_id, entity.offset);
}
// EMSESP::logger().debug("fetch custom entities");
}
// called on process telegram, read from telegram
bool WebEntityService::get_value(std::shared_ptr<const Telegram> telegram) {
bool has_change = false;
EMSESP::webEntityService.read([&](WebEntity & webEntity) { entityItems = &webEntity.entityItems; });
// read-length of BOOL, INT, UINT, SHORT, USHORT, ULONG, TIME
const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3};
for (auto & entity : *entityItems) {
if (telegram->type_id == entity.type_id && telegram->src == entity.device_id && telegram->offset <= entity.offset
&& (telegram->offset + telegram->message_length) >= (entity.offset + len[entity.valuetype])) {
uint32_t val = 0;
for (uint8_t i = 0; i < len[entity.valuetype]; i++) {
val = (val << 8) + telegram->message_data[i + entity.offset - telegram->offset];
}
if (val != entity.val) {
entity.val = val;
if (Mqtt::publish_single()) {
publish_single(entity);
} else if (EMSESP::mqtt_.get_publish_onchange(0)) {
has_change = true;
}
}
// EMSESP::logger().debug("custom entity %s received with value %d", entity.name.c_str(), (int)entity.val);
break;
}
}
if (has_change) {
publish();
return true;
}
return false;
}
} // namespace emsesp

View File

@@ -0,0 +1,74 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "../telegram.h"
#ifndef WebEntityService_h
#define WebEntityService_h
#define EMSESP_ENTITY_FILE "/config/emsespEntity.json"
#define EMSESP_ENTITY_SERVICE_PATH "/rest/entity" // GET and POST
namespace emsesp {
class EntityItem {
public:
uint8_t device_id;
uint16_t type_id;
uint8_t offset;
int8_t valuetype;
uint8_t uom;
std::string name;
double factor;
uint32_t val;
};
class WebEntity {
public:
std::list<EntityItem> entityItems;
static void read(WebEntity & webEntity, JsonObject & root);
static StateUpdateResult update(JsonObject & root, WebEntity & webEntity);
};
class WebEntityService : public StatefulService<WebEntity> {
public:
WebEntityService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager);
void begin();
void publish_single(const EntityItem & entity);
void publish(const bool force = false);
bool command_setvalue(const char * value, const std::string name);
bool get_value_info(JsonObject & output, const char * cmd);
bool get_value(std::shared_ptr<const Telegram> telegram);
void fetch();
void render_value(JsonObject & output, EntityItem entity, const bool useVal = false);
uint8_t count_entities();
void generate_value_web(JsonObject & output);
private:
HttpEndpoint<WebEntity> _httpEndpoint;
FSPersistence<WebEntity> _fsPersistence;
std::list<EntityItem> * entityItems; // pointer to the list of schedule events
bool ha_registered_ = false;
};
} // namespace emsesp
#endif