/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020 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 "emsdevice.h"
#include "emsesp.h"
namespace emsesp {
// returns number of visible device values (entries) for this device
// this includes commands since they can also be entities and visible in the web UI
uint8_t EMSdevice::count_entities() {
uint8_t count = 0;
for (const auto & dv : devicevalues_) {
if (!dv.has_state(DeviceValueState::DV_WEB_EXCLUDE) && dv.hasValue()) {
count++;
}
}
return count;
}
// see if there are entities, excluding any commands
bool EMSdevice::has_entities() const {
for (const auto & dv : devicevalues_) {
if (dv.type != DeviceValueType::CMD) {
return true;
}
}
return false;
}
std::string EMSdevice::tag_to_string(uint8_t tag) {
return read_flash_string(DeviceValue::DeviceValueTAG_s[tag]);
}
std::string EMSdevice::tag_to_mqtt(uint8_t tag) {
return read_flash_string(DeviceValue::DeviceValueTAG_mqtt[tag]);
}
std::string EMSdevice::uom_to_string(uint8_t uom) {
if (EMSESP::system_.fahrenheit() && (uom == DeviceValueUOM::DEGREES || uom == DeviceValueUOM::DEGREES_R)) {
return read_flash_string(DeviceValue::DeviceValueUOM_s[DeviceValueUOM::FAHRENHEIT]);
}
return read_flash_string(DeviceValue::DeviceValueUOM_s[uom]);
}
std::string EMSdevice::brand_to_string() const {
switch (brand_) {
case EMSdevice::Brand::BOSCH:
return read_flash_string(F("Bosch"));
case EMSdevice::Brand::JUNKERS:
return read_flash_string(F("Junkers"));
case EMSdevice::Brand::BUDERUS:
return read_flash_string(F("Buderus"));
case EMSdevice::Brand::NEFIT:
return read_flash_string(F("Nefit"));
case EMSdevice::Brand::SIEGER:
return read_flash_string(F("Sieger"));
case EMSdevice::Brand::WORCESTER:
return read_flash_string(F("Worcester"));
case EMSdevice::Brand::IVT:
return read_flash_string(F("IVT"));
default:
return read_flash_string(F(""));
}
}
// returns the name of the MQTT topic to use for a specific device, without the base
std::string EMSdevice::device_type_2_device_name(const uint8_t device_type) {
switch (device_type) {
case DeviceType::SYSTEM:
return read_flash_string(F_(system));
case DeviceType::BOILER:
return read_flash_string(F_(boiler));
case DeviceType::THERMOSTAT:
return read_flash_string(F_(thermostat));
case DeviceType::HEATPUMP:
return read_flash_string(F_(heatpump));
case DeviceType::SOLAR:
return read_flash_string(F_(solar));
case DeviceType::CONNECT:
return read_flash_string(F_(connect));
case DeviceType::MIXER:
return read_flash_string(F_(mixer));
case DeviceType::DALLASSENSOR:
return read_flash_string(F_(dallassensor));
case DeviceType::ANALOGSENSOR:
return read_flash_string(F_(analogsensor));
case DeviceType::CONTROLLER:
return read_flash_string(F_(controller));
case DeviceType::SWITCH:
return read_flash_string(F_(switch));
case DeviceType::GATEWAY:
return read_flash_string(F_(gateway));
default:
return read_flash_string(F_(unknown));
}
}
// returns device_type from a string
uint8_t EMSdevice::device_name_2_device_type(const char * topic) {
if (!topic) {
return DeviceType::UNKNOWN; // nullptr
}
// convert topic to lowercase and compare
char lowtopic[20];
strlcpy(lowtopic, topic, sizeof(lowtopic));
for (char * p = lowtopic; *p; p++) {
*p = tolower(*p);
}
if (!strcmp(lowtopic, reinterpret_cast(F_(boiler)))) {
return DeviceType::BOILER;
}
if (!strcmp(lowtopic, reinterpret_cast(F_(thermostat)))) {
return DeviceType::THERMOSTAT;
}
if (!strcmp(lowtopic, reinterpret_cast(F_(system)))) {
return DeviceType::SYSTEM;
}
if (!strcmp(lowtopic, reinterpret_cast(F_(heatpump)))) {
return DeviceType::HEATPUMP;
}
if (!strcmp(lowtopic, reinterpret_cast(F_(solar)))) {
return DeviceType::SOLAR;
}
if (!strcmp(lowtopic, reinterpret_cast(F_(mixer)))) {
return DeviceType::MIXER;
}
if (!strcmp(lowtopic, reinterpret_cast(F_(dallassensor)))) {
return DeviceType::DALLASSENSOR;
}
if (!strcmp(lowtopic, reinterpret_cast(F_(analogsensor)))) {
return DeviceType::ANALOGSENSOR;
}
return DeviceType::UNKNOWN;
}
// return name of the device type, capitalized
std::string EMSdevice::device_type_name() const {
std::string s = device_type_2_device_name(device_type_);
s[0] = toupper(s[0]);
return s;
}
// 0=unknown, 1=bosch, 2=junkers, 3=buderus, 4=nefit, 5=sieger, 11=worcester
uint8_t EMSdevice::decode_brand(uint8_t value) {
switch (value) {
case 1:
return EMSdevice::Brand::BOSCH;
case 2:
return EMSdevice::Brand::JUNKERS;
case 3:
return EMSdevice::Brand::BUDERUS;
case 4:
return EMSdevice::Brand::NEFIT;
case 5:
return EMSdevice::Brand::SIEGER;
case 11:
return EMSdevice::Brand::WORCESTER;
case 13:
return EMSdevice::Brand::IVT;
default:
return EMSdevice::Brand::NO_BRAND;
}
}
// returns string of a human friendly description of the EMS device
std::string EMSdevice::to_string() const {
// for devices that haven't been lookup yet, don't show all details
if (product_id_ == 0) {
return name_ + " (DeviceID:" + Helpers::hextoa(device_id_) + ")";
}
if (brand_ == Brand::NO_BRAND) {
return name_ + " (DeviceID:" + Helpers::hextoa(device_id_) + ", ProductID:" + Helpers::itoa(product_id_) + ", Version:" + version_ + ")";
}
return brand_to_string() + " " + name_ + " (DeviceID:" + Helpers::hextoa(device_id_) + ", ProductID:" + Helpers::itoa(product_id_) + ", Version:" + version_
+ ")";
}
// returns out brand + device name
std::string EMSdevice::to_string_short() const {
if (brand_ == Brand::NO_BRAND) {
return device_type_name() + ": " + name_;
}
return device_type_name() + ": " + brand_to_string() + " " + name_;
}
// for each telegram that has the fetch value set (true) do a read request
void EMSdevice::fetch_values() {
EMSESP::logger().debug(F("Fetching values for deviceID 0x%02X"), device_id());
for (const auto & tf : telegram_functions_) {
if (tf.fetch_) {
read_command(tf.telegram_type_id_);
}
}
}
// toggle on/off automatic fetch for a telegramID
void EMSdevice::toggle_fetch(uint16_t telegram_id, bool toggle) {
EMSESP::logger().debug(F("Toggling fetch for deviceID 0x%02X, telegramID 0x%02X to %d"), device_id(), telegram_id, toggle);
for (auto & tf : telegram_functions_) {
if (tf.telegram_type_id_ == telegram_id) {
tf.fetch_ = toggle;
}
}
}
// get status of automatic fetch for a telegramID
bool EMSdevice::is_fetch(uint16_t telegram_id) const {
for (const auto & tf : telegram_functions_) {
if (tf.telegram_type_id_ == telegram_id) {
return tf.fetch_;
}
}
return false;
}
// list of registered device entries
// called from the command 'entities'
void EMSdevice::list_device_entries(JsonObject & output) const {
for (const auto & dv : devicevalues_) {
if (!dv.has_state(DeviceValueState::DV_WEB_EXCLUDE) && dv.type != DeviceValueType::CMD && dv.full_name) {
// if we have a tag prefix it
char key[50];
if (!EMSdevice::tag_to_mqtt(dv.tag).empty()) {
snprintf(key, sizeof(key), "%s.%s", EMSdevice::tag_to_mqtt(dv.tag).c_str(), read_flash_string(dv.short_name).c_str());
} else {
snprintf(key, sizeof(key), "%s", read_flash_string(dv.short_name).c_str());
}
JsonArray details = output.createNestedArray(key);
// add the full name description
details.add(dv.full_name);
// add uom
if (!uom_to_string(dv.uom).empty() && uom_to_string(dv.uom) != " ") {
details.add(EMSdevice::uom_to_string(dv.uom));
}
}
}
}
// list all the telegram type IDs for this device
void EMSdevice::show_telegram_handlers(uuid::console::Shell & shell) const {
if (telegram_functions_.empty()) {
return;
}
/*
// colored list of type-ids
shell.printf(F(" This %s will listen to telegram type IDs: "), device_type_name().c_str());
for (const auto & tf : telegram_functions_) {
if (tf.received_ && !tf.fetch_) {
shell.printf(COLOR_BRIGHT_GREEN);
} else if (tf.received_) {
shell.printf(COLOR_YELLOW);
} else {
shell.printf(COLOR_BRIGHT_RED);
}
shell.printf(F("0x%02X "), tf.telegram_type_id_);
}
shell.printf(COLOR_RESET);
*/
shell.printf(F(" Received telegram type IDs: "));
for (const auto & tf : telegram_functions_) {
if (tf.received_ && !tf.fetch_) {
shell.printf(F("0x%02X "), tf.telegram_type_id_);
}
}
shell.println();
shell.printf(F(" Fetched telegram type IDs: "));
for (const auto & tf : telegram_functions_) {
if (tf.fetch_) {
shell.printf(F("0x%02X "), tf.telegram_type_id_);
}
}
shell.println();
shell.printf(F(" Pending telegram type IDs: "));
for (const auto & tf : telegram_functions_) {
if (!tf.received_ && !tf.fetch_) {
shell.printf(F("0x%02X "), tf.telegram_type_id_);
}
}
shell.println();
shell.printf(F(" Ignored telegram type IDs: "));
for (auto handlers : handlers_ignored_) {
shell.printf(F("0x%02X "), handlers);
}
shell.println();
}
// list all the telegram type IDs for this device, outputting to a string (max size 200)
char * EMSdevice::show_telegram_handlers(char * result, const size_t len, const uint8_t handlers) {
uint8_t size = telegram_functions_.size();
strlcpy(result, "", len);
if (!size) {
return result;
}
uint8_t i = 0;
for (const auto & tf : telegram_functions_) {
if (handlers == Handlers::ALL || (handlers == Handlers::RECEIVED && tf.received_ && !tf.fetch_)
|| (handlers == Handlers::FETCHED && tf.received_ && tf.fetch_) || (handlers == Handlers::PENDING && !tf.received_ && !tf.fetch_)) {
if (i++ > 0) {
strlcat(result, " ", len);
}
strlcat(result, Helpers::hextoa(tf.telegram_type_id_, true).c_str(), len);
}
}
if (handlers == Handlers::ALL || handlers == Handlers::IGNORED) {
i = 0;
for (auto h : handlers_ignored_) {
if (i++ > 0) {
strlcat(result, " ", len);
}
strlcat(result, Helpers::hextoa(h).c_str(), len);
}
}
return result;
}
void EMSdevice::add_handlers_ignored(const uint16_t handler) {
for (auto handlers : handlers_ignored_) {
if (handler == handlers) {
return;
}
}
handlers_ignored_.push_back(handler);
}
// list all the mqtt handlers for this device
void EMSdevice::show_mqtt_handlers(uuid::console::Shell & shell) const {
Mqtt::show_topic_handlers(shell, device_type_);
}
// register a callback function for a specific telegram type
void EMSdevice::register_telegram_type(const uint16_t telegram_type_id, const __FlashStringHelper * telegram_type_name, bool fetch, const process_function_p f) {
telegram_functions_.emplace_back(telegram_type_id, telegram_type_name, fetch, false, f);
}
// add to device value library, also know now as a "device entity"
// arguments are:
// tag: to be used to group mqtt together, either as separate topics as a nested object
// value_p: pointer to the value from the .h file
// type: one of DeviceValueType
// options: options for enum or a divider for int (e.g. F("10"))
// short_name: used in Mqtt as keys
// full_name: used in Web and Console unless empty (nullptr)
// uom: unit of measure from DeviceValueUOM
// has_cmd: true if this is an associated command
// min: min allowed value
// max: max allowed value
void EMSdevice::register_device_value(uint8_t tag,
void * value_p,
uint8_t type,
const __FlashStringHelper * const * options,
const __FlashStringHelper * short_name,
const __FlashStringHelper * full_name,
uint8_t uom,
bool has_cmd,
int16_t min,
uint16_t max) {
// initialize the device value depending on it's type
if (type == DeviceValueType::STRING) {
*(char *)(value_p) = {'\0'}; // this is important for string functions like strlen() to work later
} else if (type == DeviceValueType::INT) {
*(int8_t *)(value_p) = EMS_VALUE_INT_NOTSET;
} else if (type == DeviceValueType::SHORT) {
*(int16_t *)(value_p) = EMS_VALUE_SHORT_NOTSET;
} else if (type == DeviceValueType::USHORT) {
*(uint16_t *)(value_p) = EMS_VALUE_USHORT_NOTSET;
} else if ((type == DeviceValueType::ULONG) || (type == DeviceValueType::TIME)) {
*(uint32_t *)(value_p) = EMS_VALUE_ULONG_NOTSET;
} else if (type == DeviceValueType::BOOL) {
*(int8_t *)(value_p) = EMS_VALUE_BOOL_NOTSET; // bool is uint8_t, but other initial value
} else {
*(uint8_t *)(value_p) = EMS_VALUE_UINT_NOTSET; // enums behave as uint8_t
}
// count #options
uint8_t options_size = 0;
if (options != nullptr) {
uint8_t i = 0;
while (options[i++]) {
options_size++;
}
}
// determine state
uint8_t state = DeviceValueState::DV_DEFAULT;
// scan through customizations to see if it's on the exclusion list by matching the productID and deviceID
EMSESP::webCustomizationService.read([&](WebCustomization & settings) {
for (EntityCustomization entityCustomization : settings.entityCustomizations) {
if ((entityCustomization.product_id == product_id()) && (entityCustomization.device_id == device_id())) {
std::string entity = tag < DeviceValueTAG::TAG_HC1 ? read_flash_string(short_name) : tag_to_string(tag) + "/" + read_flash_string(short_name);
for (std::string entity_id : entityCustomization.entity_ids) {
if (entity_id.substr(2) == entity) {
uint8_t mask = Helpers::hextoint(entity_id.substr(0, 2).c_str());
state = mask << 4; // set state high bits to flag, turn off active and ha flags
break;
}
}
}
}
});
// add the device
devicevalues_.emplace_back(device_type_, tag, value_p, type, options, options_size, short_name, full_name, uom, 0, has_cmd, min, max, state);
}
// function with min and max values
// adds a new command to the command list
void EMSdevice::register_device_value(uint8_t tag,
void * value_p,
uint8_t type,
const __FlashStringHelper * const * options,
const __FlashStringHelper * const * name,
uint8_t uom,
const cmd_function_p f,
int16_t min,
uint16_t max) {
auto short_name = name[0];
auto full_name = name[1];
register_device_value(tag, value_p, type, options, short_name, full_name, uom, (f != nullptr), min, max);
// add a new command if it has a function attached
if (f == nullptr) {
return;
}
uint8_t flags = CommandFlag::ADMIN_ONLY; // executing commands require admin privileges
if (tag >= DeviceValueTAG::TAG_HC1 && tag <= DeviceValueTAG::TAG_HC8) {
flags |= CommandFlag::MQTT_SUB_FLAG_HC;
} else if (tag >= DeviceValueTAG::TAG_WWC1 && tag <= DeviceValueTAG::TAG_WWC10) {
flags |= CommandFlag::MQTT_SUB_FLAG_WWC;
} else if (tag == DeviceValueTAG::TAG_DEVICE_DATA_WW) {
flags |= CommandFlag::MQTT_SUB_FLAG_WW;
}
// add the command to our library
// cmd is the short_name and the description is the full_name
Command::add(device_type_, short_name, f, full_name, flags);
}
// function with no min and max values (set to 0)
void EMSdevice::register_device_value(uint8_t tag,
void * value_p,
uint8_t type,
const __FlashStringHelper * const * options,
const __FlashStringHelper * const * name,
uint8_t uom,
const cmd_function_p f) {
register_device_value(tag, value_p, type, options, name, uom, f, 0, 0);
}
// no associated command function, or min/max values
void EMSdevice::register_device_value(uint8_t tag,
void * value_p,
uint8_t type,
const __FlashStringHelper * const * options,
const __FlashStringHelper * const * name,
uint8_t uom) {
register_device_value(tag, value_p, type, options, name, uom, nullptr, 0, 0);
}
// check if value is readable via mqtt/api
bool EMSdevice::is_readable(const void * value_p) const {
for (const auto & dv : devicevalues_) {
if (dv.value_p == value_p) {
return !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE);
}
}
return false;
}
// check if value/command is readonly
bool EMSdevice::is_readonly(const std::string & cmd, const int8_t id) const {
uint8_t tag = id > 0 ? DeviceValueTAG::TAG_HC1 + id - 1 : DeviceValueTAG::TAG_NONE;
for (const auto & dv : devicevalues_) {
// check command name and tag, id -1 is default hc and only checks name
if (dv.has_cmd && read_flash_string(dv.short_name) == cmd && (dv.tag < DeviceValueTAG::TAG_HC1 || dv.tag == tag || id == -1)) {
return dv.has_state(DeviceValueState::DV_READONLY);
}
}
return true; // not found, no write
}
// check if value has a registered command
bool EMSdevice::has_command(const void * value_p) const {
for (const auto & dv : devicevalues_) {
if (dv.value_p == value_p) {
return dv.has_cmd && !dv.has_state(DeviceValueState::DV_READONLY);
}
}
return false;
}
// publish a single value on change
void EMSdevice::publish_value(void * value_p) const {
if (!Mqtt::publish_single() || value_p == nullptr) {
return;
}
for (const auto & dv : devicevalues_) {
if (dv.value_p == value_p && !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE)) {
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
if (Mqtt::publish_single2cmd()) {
if (dv.tag >= DeviceValueTAG::TAG_HC1 && dv.tag <= DeviceValueTAG::TAG_WWC10) {
snprintf(topic,
sizeof(topic),
"%s/%s/%s",
device_type_2_device_name(device_type_).c_str(),
tag_to_mqtt(dv.tag).c_str(),
read_flash_string(dv.short_name).c_str());
} else {
snprintf(topic, sizeof(topic), "%s/%s", device_type_2_device_name(device_type_).c_str(), read_flash_string(dv.short_name).c_str());
}
} else if (Mqtt::is_nested() && dv.tag >= DeviceValueTAG::TAG_HC1) {
snprintf(topic,
sizeof(topic),
"%s/%s/%s",
Mqtt::tag_to_topic(device_type_, dv.tag).c_str(),
tag_to_mqtt(dv.tag).c_str(),
read_flash_string(dv.short_name).c_str());
} else {
snprintf(topic, sizeof(topic), "%s/%s", Mqtt::tag_to_topic(device_type_, dv.tag).c_str(), read_flash_string(dv.short_name).c_str());
}
int8_t divider = (dv.options_size == 1) ? Helpers::atoint(read_flash_string(dv.options[0]).c_str()) : 0;
char payload[50] = {'\0'};
uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (dv.uom == DeviceValueUOM::DEGREES) ? 2 : (dv.uom == DeviceValueUOM::DEGREES_R) ? 1 : 0;
switch (dv.type) {
case DeviceValueType::CMD: // publish a dummy value to show subscription in mqtt
strlcpy(payload, "-", 2);
break;
case DeviceValueType::ENUM: {
if ((*(uint8_t *)(value_p)) < dv.options_size) {
if (EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX) {
Helpers::render_value(payload, *(uint8_t *)(value_p), 0);
} else {
strlcpy(payload, read_flash_string(dv.options[*(uint8_t *)(value_p)]).c_str(), sizeof(payload));
}
}
break;
}
case DeviceValueType::USHORT:
Helpers::render_value(payload, *(uint16_t *)(value_p), divider, fahrenheit);
break;
case DeviceValueType::UINT:
Helpers::render_value(payload, *(uint8_t *)(value_p), divider, fahrenheit);
break;
case DeviceValueType::SHORT:
Helpers::render_value(payload, *(int16_t *)(value_p), divider, fahrenheit);
break;
case DeviceValueType::INT:
Helpers::render_value(payload, *(int8_t *)(value_p), divider, fahrenheit);
break;
case DeviceValueType::ULONG:
Helpers::render_value(payload, *(uint32_t *)(value_p), divider, fahrenheit);
break;
case DeviceValueType::BOOL: {
Helpers::render_boolean(payload, (bool)*(uint8_t *)(value_p));
break;
}
case DeviceValueType::TIME:
Helpers::render_value(payload, *(uint32_t *)(value_p), divider);
break;
case DeviceValueType::STRING:
if (Helpers::hasValue((char *)(value_p))) {
strlcpy(payload, (char *)(value_p), sizeof(payload));
}
break;
default:
break;
}
if (payload[0] != '\0') {
Mqtt::publish(topic, payload);
}
}
}
}
// looks up the UOM for a given key from the device value table
std::string EMSdevice::get_value_uom(const char * key) const {
// the key may have a TAG string prefixed at the beginning. If so, remove it
char new_key[80];
strlcpy(new_key, key, sizeof(new_key));
char * key_p = new_key;
for (uint8_t i = 0; i < DeviceValue::tag_count; i++) {
auto tag = read_flash_string(DeviceValue::DeviceValueTAG_s[i]);
if (!tag.empty()) {
std::string key2 = key; // copy char to a std::string
if ((key2.find(tag) != std::string::npos) && (key[tag.length()] == ' ')) {
key_p += tag.length() + 1; // remove the tag
break;
}
}
}
// look up key in our device value list
for (const auto & dv : devicevalues_) {
if ((!dv.has_state(DeviceValueState::DV_WEB_EXCLUDE) && dv.full_name) && (read_flash_string(dv.full_name) == key_p)) {
// ignore TIME since "minutes" is already added to the string value
if ((dv.uom == DeviceValueUOM::NONE) || (dv.uom == DeviceValueUOM::MINUTES)) {
break;
}
return EMSdevice::uom_to_string(dv.uom);
}
}
return std::string{}; // not found
}
// prepare array of device values used for the WebUI
// this is loosely based of the function generate_values used for the MQTT and Console
// except additional data is stored in the JSON document needed for the Web UI like the UOM and command
// v=value, u=uom, n=name, c=cmd, h=help string, s=step, m=min, x=max
void EMSdevice::generate_values_web(JsonObject & output) {
output["label"] = to_string_short();
JsonArray data = output.createNestedArray("data");
for (auto & dv : devicevalues_) {
// check conditions:
// 1. full_name cannot be empty
// 2. it must have a valid value, if it is not a command like 'reset'
// 3. show favorites first
if (!dv.has_state(DeviceValueState::DV_WEB_EXCLUDE) && dv.full_name && (dv.hasValue() || (dv.type == DeviceValueType::CMD))) {
JsonObject obj = data.createNestedObject(); // create the object, we know there is a value
uint8_t fahrenheit = 0;
// handle Booleans (true, false), use strings, no native true/false)
if (dv.type == DeviceValueType::BOOL) {
auto value_b = (bool)*(uint8_t *)(dv.value_p);
char s[7];
obj["v"] = Helpers::render_boolean(s, value_b, true);
}
// handle TEXT strings
else if (dv.type == DeviceValueType::STRING) {
obj["v"] = (char *)(dv.value_p);
}
// handle ENUMs
else if ((dv.type == DeviceValueType::ENUM) && (*(uint8_t *)(dv.value_p) < dv.options_size)) {
obj["v"] = dv.options[*(uint8_t *)(dv.value_p)];
}
// handle numbers
else {
// If a divider is specified, do the division to 2 decimals places and send back as double/float
// otherwise force as an integer whole
// the nested if's is necessary due to the way the ArduinoJson templates are pre-processed by the compiler
int8_t divider = (dv.options_size == 1) ? Helpers::atoint(read_flash_string(dv.options[0]).c_str()) : 0;
fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (dv.uom == DeviceValueUOM::DEGREES) ? 2 : (dv.uom == DeviceValueUOM::DEGREES_R) ? 1 : 0;
if ((dv.type == DeviceValueType::INT) && Helpers::hasValue(*(int8_t *)(dv.value_p))) {
obj["v"] = Helpers::round2(*(int8_t *)(dv.value_p), divider, fahrenheit);
} else if ((dv.type == DeviceValueType::UINT) && Helpers::hasValue(*(uint8_t *)(dv.value_p))) {
obj["v"] = Helpers::round2(*(uint8_t *)(dv.value_p), divider, fahrenheit);
} else if ((dv.type == DeviceValueType::SHORT) && Helpers::hasValue(*(int16_t *)(dv.value_p))) {
obj["v"] = Helpers::round2(*(int16_t *)(dv.value_p), divider, fahrenheit);
} else if ((dv.type == DeviceValueType::USHORT) && Helpers::hasValue(*(uint16_t *)(dv.value_p))) {
obj["v"] = Helpers::round2(*(uint16_t *)(dv.value_p), divider, fahrenheit);
} else if ((dv.type == DeviceValueType::ULONG) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) {
obj["v"] = Helpers::round2(*(uint32_t *)(dv.value_p), divider, fahrenheit);
} else if ((dv.type == DeviceValueType::TIME) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) {
uint32_t time_value = *(uint32_t *)(dv.value_p);
obj["v"] = (divider > 0) ? time_value / divider : time_value; // sometimes we need to divide by 60
} else {
// must have a value for sorting to work
obj["v"] = "";
}
}
// add the unit of measure (uom)
obj["u"] = fahrenheit ? (uint8_t)DeviceValueUOM::FAHRENHEIT : dv.uom;
auto mask = Helpers::hextoa((uint8_t)(dv.state >> 4), false); // create mask to a 2-char string
// add name, prefixing the tag if it exists. This is the id used for the table sorting
if ((dv.tag == DeviceValueTAG::TAG_NONE) || tag_to_string(dv.tag).empty()) {
obj["id"] = mask + read_flash_string(dv.full_name);
} else if (dv.tag < DeviceValueTAG::TAG_HC1) {
obj["id"] = mask + tag_to_string(dv.tag) + " " + read_flash_string(dv.full_name);
} else {
obj["id"] = mask + tag_to_string(dv.tag) + " " + read_flash_string(dv.full_name);
}
// add commands and options
if (dv.has_cmd && !dv.has_state(DeviceValueState::DV_READONLY)) {
// add the name of the Command function
if (dv.tag >= DeviceValueTAG::TAG_HC1) {
obj["c"] = tag_to_mqtt(dv.tag) + "/" + read_flash_string(dv.short_name);
} else {
obj["c"] = dv.short_name;
}
// add the Command options
if (dv.type == DeviceValueType::ENUM || (dv.type == DeviceValueType::CMD && dv.options_size > 1)) {
JsonArray l = obj.createNestedArray("l");
for (uint8_t i = 0; i < dv.options_size; i++) {
if (!read_flash_string(dv.options[i]).empty()) {
l.add(read_flash_string(dv.options[i]));
}
}
} else if (dv.type == DeviceValueType::BOOL) {
JsonArray l = obj.createNestedArray("l");
char result[10];
l.add(Helpers::render_boolean(result, false, true));
l.add(Helpers::render_boolean(result, true, true));
}
// add command help template
else if (dv.type == DeviceValueType::STRING || dv.type == DeviceValueType::CMD) {
if (dv.options_size == 1) {
obj["h"] = dv.options[0];
}
}
// add steps to numeric values with divider/multiplier
else {
int8_t divider = (dv.options_size == 1) ? Helpers::atoint(read_flash_string(dv.options[0]).c_str()) : 0;
char s[10];
if (divider > 0) {
obj["s"] = Helpers::render_value(s, (float)1 / divider, 1);
} else if (divider < 0) {
obj["s"] = Helpers::render_value(s, (-1) * divider, 0);
}
int16_t dv_set_min, dv_set_max;
if (dv.get_min_max(dv_set_min, dv_set_max)) {
obj["m"] = Helpers::render_value(s, dv_set_min, 0);
obj["x"] = Helpers::render_value(s, dv_set_max, 0);
}
}
}
}
}
}
// as generate_values_web() but stripped down to only show all entities and their state
// this is used only for WebCustomizationService::device_entities()
void EMSdevice::generate_values_web_all(JsonArray & output) {
for (const auto & dv : devicevalues_) {
// also show commands and entities that have an empty full name
JsonObject obj = output.createNestedObject();
// create the value
if (dv.hasValue()) {
// handle Booleans (true, false), use strings, no native true/false)
if (dv.type == DeviceValueType::BOOL) {
auto value_b = (bool)*(uint8_t *)(dv.value_p);
char s[7];
obj["v"] = Helpers::render_boolean(s, value_b, true);
}
// handle TEXT strings
else if (dv.type == DeviceValueType::STRING) {
obj["v"] = (char *)(dv.value_p);
}
// handle ENUMs
else if ((dv.type == DeviceValueType::ENUM) && (*(uint8_t *)(dv.value_p) < dv.options_size)) {
obj["v"] = dv.options[*(uint8_t *)(dv.value_p)];
}
// handle Integers and Floats
else {
// If a divider is specified, do the division to 2 decimals places and send back as double/float
// otherwise force as an integer whole
// the nested if's is necessary due to the way the ArduinoJson templates are pre-processed by the compiler
uint8_t divider = 0;
uint8_t factor = 1;
if (dv.options_size == 1) {
auto s_str = read_flash_string(dv.options[0]); // prevent object backing the pointer will be destroyed at the end of the full-expression
const char * s = s_str.c_str();
if (s[0] == '*') {
factor = Helpers::atoint(&s[1]);
} else {
divider = Helpers::atoint(s);
}
}
if (dv.type == DeviceValueType::INT) {
obj["v"] = divider ? Helpers::round2(*(int8_t *)(dv.value_p), divider) : *(int8_t *)(dv.value_p) * factor;
} else if (dv.type == DeviceValueType::UINT) {
obj["v"] = divider ? Helpers::round2(*(uint8_t *)(dv.value_p), divider) : *(uint8_t *)(dv.value_p) * factor;
} else if (dv.type == DeviceValueType::SHORT) {
obj["v"] = divider ? Helpers::round2(*(int16_t *)(dv.value_p), divider) : *(int16_t *)(dv.value_p) * factor;
} else if (dv.type == DeviceValueType::USHORT) {
obj["v"] = divider ? Helpers::round2(*(uint16_t *)(dv.value_p), divider) : *(uint16_t *)(dv.value_p) * factor;
} else if (dv.type == DeviceValueType::ULONG) {
obj["v"] = divider ? Helpers::round2(*(uint32_t *)(dv.value_p), divider) : *(uint32_t *)(dv.value_p) * factor;
} else if (dv.type == DeviceValueType::TIME) {
uint32_t time_value = *(uint32_t *)(dv.value_p);
obj["v"] = (divider > 0) ? time_value / divider : time_value * factor; // sometimes we need to divide by 60
}
}
} else {
// must always have v for sorting to work in web
obj["v"] = "";
}
// add name, prefixing the tag if it exists as the id (key for table sorting)
if (dv.full_name) {
if ((dv.tag == DeviceValueTAG::TAG_NONE) || tag_to_string(dv.tag).empty()) {
obj["id"] = dv.full_name;
} else {
char name[50];
snprintf(name, sizeof(name), "%s %s", tag_to_string(dv.tag).c_str(), read_flash_string(dv.full_name).c_str());
obj["id"] = name;
}
} else {
obj["id"] = "";
}
// shortname
if (dv.tag >= DeviceValueTAG::TAG_HC1) {
obj["s"] = tag_to_string(dv.tag) + "/" + read_flash_string(dv.short_name);
} else {
obj["s"] = dv.short_name;
}
obj["m"] = dv.state >> 4; // send back the mask state. We're only interested in the high nibble
obj["w"] = dv.has_cmd; // if writable
}
}
// set mask per device entity based on the id which is prefixed with the 2 char hex mask value
// returns true if the entity has a mask set (not 0 the default)
void EMSdevice::mask_entity(const std::string & entity_id) {
for (auto & dv : devicevalues_) {
std::string entity_name =
dv.tag < DeviceValueTAG::TAG_HC1 ? read_flash_string(dv.short_name) : tag_to_string(dv.tag) + "/" + read_flash_string(dv.short_name);
if (entity_name == entity_id.substr(2)) {
// this entity has a new mask set
uint8_t current_mask = dv.state >> 4;
uint8_t new_mask = Helpers::hextoint(entity_id.substr(0, 2).c_str()); // first character contains mask flags
if (Mqtt::ha_enabled() && ((current_mask ^ new_mask) & (DeviceValueState::DV_READONLY >> 4))) {
// remove ha config on change of dv_readonly flag
dv.remove_state(DeviceValueState::DV_HA_CONFIG_CREATED);
Mqtt::publish_ha_sensor_config(dv, "", "", true); // delete topic (remove = true)
}
dv.state = ((dv.state & 0x0F) | (new_mask << 4)); // set state high bits to flag
return;
}
}
}
// populate a string vector with entities that have masks set
void EMSdevice::getMaskedEntities(std::vector & entity_ids) {
for (const auto & dv : devicevalues_) {
std::string entity_name =
dv.tag < DeviceValueTAG::TAG_HC1 ? read_flash_string(dv.short_name) : tag_to_string(dv.tag) + "/" + read_flash_string(dv.short_name);
uint8_t mask = dv.state >> 4;
if (mask) {
entity_ids.push_back(Helpers::hextoa(mask, false) + entity_name);
}
}
}
// builds json for a specific device value / entity
// cmd is the endpoint or name of the device entity
// returns false if failed, otherwise true
bool EMSdevice::get_value_info(JsonObject & output, const char * cmd, const int8_t id) {
JsonObject json = output;
int8_t tag = id;
// check if we have hc or wwc
if (id >= 1 && id <= 8) {
tag = DeviceValueTAG::TAG_HC1 + id - 1;
} else if (id >= 9 && id <= 19) {
tag = DeviceValueTAG::TAG_WWC1 + id - 9;
}
// make a copy of the string command for parsing
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;
}
// search device value with this tag
for (auto & dv : devicevalues_) {
if (strcmp(command_s, Helpers::toLower(read_flash_string(dv.short_name)).c_str()) == 0 && (tag <= 0 || tag == dv.tag)) {
int8_t divider = (dv.options_size == 1) ? Helpers::atoint(read_flash_string(dv.options[0]).c_str()) : 0;
uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (dv.uom == DeviceValueUOM::DEGREES) ? 2 : (dv.uom == DeviceValueUOM::DEGREES_R) ? 1 : 0;
const char * type = "type";
const char * value = "value";
json["name"] = dv.short_name;
if (dv.full_name != nullptr) {
const char * fullname = "fullname";
if ((dv.tag == DeviceValueTAG::TAG_NONE) || tag_to_string(dv.tag).empty()) {
json[fullname] = dv.full_name;
} else {
json[fullname] = tag_to_string(dv.tag) + " " + read_flash_string(dv.full_name);
}
}
if (!tag_to_mqtt(dv.tag).empty()) {
json["circuit"] = tag_to_mqtt(dv.tag);
}
switch (dv.type) {
case DeviceValueType::ENUM: {
if (*(uint8_t *)(dv.value_p) < dv.options_size) {
if (EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX) {
json[value] = (uint8_t)(*(uint8_t *)(dv.value_p));
} else {
json[value] = dv.options[*(uint8_t *)(dv.value_p)]; // text
}
}
json[type] = F_(enum);
JsonArray enum_ = json.createNestedArray(F_(enum));
for (uint8_t i = 0; i < dv.options_size; i++) {
enum_.add(dv.options[i]);
}
break;
}
case DeviceValueType::USHORT:
if (Helpers::hasValue(*(uint16_t *)(dv.value_p))) {
json[value] = Helpers::round2(*(uint16_t *)(dv.value_p), divider, fahrenheit);
}
json[type] = F_(number);
break;
case DeviceValueType::UINT:
if (Helpers::hasValue(*(uint8_t *)(dv.value_p))) {
json[value] = Helpers::round2(*(uint8_t *)(dv.value_p), divider, fahrenheit);
}
json[type] = F_(number);
break;
case DeviceValueType::SHORT:
if (Helpers::hasValue(*(int16_t *)(dv.value_p))) {
json[value] = Helpers::round2(*(int16_t *)(dv.value_p), divider, fahrenheit);
}
json[type] = F_(number);
break;
case DeviceValueType::INT:
if (Helpers::hasValue(*(int8_t *)(dv.value_p))) {
json[value] = Helpers::round2(*(int8_t *)(dv.value_p), divider, fahrenheit);
}
json[type] = F_(number);
break;
case DeviceValueType::ULONG:
if (Helpers::hasValue(*(uint32_t *)(dv.value_p))) {
json[value] = Helpers::round2(*(uint32_t *)(dv.value_p), divider);
}
json[type] = F_(number);
break;
case DeviceValueType::BOOL:
if (Helpers::hasValue(*(uint8_t *)(dv.value_p), EMS_VALUE_BOOL)) {
auto value_b = (bool)*(uint8_t *)(dv.value_p);
if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) {
json[value] = value_b;
} else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) {
json[value] = value_b ? 1 : 0;
} else {
char s[7];
json[value] = Helpers::render_boolean(s, value_b);
}
}
json[type] = F("boolean");
break;
case DeviceValueType::TIME:
if (Helpers::hasValue(*(uint32_t *)(dv.value_p))) {
json[value] = Helpers::round2(*(uint32_t *)(dv.value_p), divider);
}
json[type] = F_(number);
break;
case DeviceValueType::STRING:
if (Helpers::hasValue((char *)(dv.value_p))) {
json[value] = (char *)(dv.value_p);
}
json[type] = F("string");
break;
case DeviceValueType::CMD:
json[type] = F_(command);
if (dv.options_size > 1) {
JsonArray enum_ = json.createNestedArray(F_(enum));
for (uint8_t i = 0; i < dv.options_size; i++) {
enum_.add(dv.options[i]);
}
}
break;
default:
json[type] = F_(unknown);
break;
}
// set the min and max
int16_t dv_set_min, dv_set_max;
if (dv.get_min_max(dv_set_min, dv_set_max)) {
json["min"] = dv_set_min;
json["max"] = dv_set_max;
}
// add uom if it's not a " " (single space)
if (!uom_to_string(dv.uom).empty() && uom_to_string(dv.uom) != " ") {
json["uom"] = uom_to_string(dv.uom);
}
json["readable"] = !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE);
json["writeable"] = dv.has_cmd && !dv.has_state(DeviceValueState::DV_READONLY);
json["visible"] = !dv.has_state(DeviceValueState::DV_WEB_EXCLUDE);
// if there is no value, mention it
if (!json.containsKey(value)) {
json[value] = "not set";
}
// if we're filtering on an attribute, go find it
if (attribute_s) {
#if defined(EMSESP_DEBUG)
EMSESP::logger().debug(F("[DEBUG] Attribute '%s'"), attribute_s);
#endif
if (json.containsKey(attribute_s)) {
JsonVariant data = json[attribute_s];
output.clear();
output["api_data"] = data;
return true;
} else {
char error[100];
snprintf(error, sizeof(error), "cannot find attribute %s in entity %s", attribute_s, command_s);
output.clear();
output["message"] = error;
return false;
}
}
return true;
}
}
char error[100];
snprintf(error, sizeof(error), "cannot find values for entity '%s'", cmd);
json["message"] = error;
return false;
}
// mqtt publish all single values from one device (used for time schedule)
void EMSdevice::publish_all_values() {
for (const auto & dv : devicevalues_) {
publish_value(dv.value_p);
}
}
// For each value in the device create the json object pair and add it to given json
// return false if empty
// this is used to create both the MQTT payloads, Console messages and Web API calls
bool EMSdevice::generate_values(JsonObject & output, const uint8_t tag_filter, const bool nested, const uint8_t output_target) {
bool has_values = false; // to see if we've added a value. it's faster than doing a json.size() at the end
uint8_t old_tag = 255; // NAN
JsonObject json = output;
for (auto & dv : devicevalues_) {
// check if it exists, there is a value for the entity. Set the flag to ACTIVE
// not that this will override any previously removed states
if (dv.hasValue()) {
dv.add_state(DeviceValueState::DV_ACTIVE);
} else {
dv.remove_state(DeviceValueState::DV_ACTIVE);
}
// check conditions:
// 1. it must have a valid value (state is active)
// 2. it must have a visible flag
// 3. it must match the given tag filter or have an empty tag
// 4. it must not have the exclude flag set or outputs to console
if (dv.has_state(DeviceValueState::DV_ACTIVE) && dv.full_name && (tag_filter == DeviceValueTAG::TAG_NONE || tag_filter == dv.tag)
&& (output_target == OUTPUT_TARGET::CONSOLE || !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE))) {
has_values = true; // flagged if we actually have data
// we have a tag if it matches the filter given, and that the tag name is not empty/""
bool have_tag = ((dv.tag != tag_filter) && !tag_to_string(dv.tag).empty());
// create the name for the JSON key
char name[80];
if (output_target == OUTPUT_TARGET::API_VERBOSE || output_target == OUTPUT_TARGET::CONSOLE) {
if (have_tag) {
snprintf(name, 80, "%s %s", tag_to_string(dv.tag).c_str(), read_flash_string(dv.full_name).c_str()); // prefix the tag
} else {
strlcpy(name, read_flash_string(dv.full_name).c_str(), sizeof(name)); // use full name
}
} else {
strlcpy(name, read_flash_string(dv.short_name).c_str(), sizeof(name)); // use short name
// if we have a tag, and its different to the last one create a nested object. only for hc, wwc and hs
if (dv.tag != old_tag) {
old_tag = dv.tag;
if (nested && have_tag && dv.tag >= DeviceValueTAG::TAG_HC1) {
json = output.createNestedObject(tag_to_string(dv.tag));
}
}
}
// handle Booleans
if (dv.type == DeviceValueType::BOOL && Helpers::hasValue(*(uint8_t *)(dv.value_p), EMS_VALUE_BOOL)) {
// see how to render the value depending on the setting
auto value_b = (bool)*(uint8_t *)(dv.value_p);
if (Mqtt::ha_enabled() && (output_target == OUTPUT_TARGET::MQTT)) {
char s[7];
json[name] = Helpers::render_boolean(s, value_b); // for HA always render as string
} else {
if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) {
json[name] = value_b;
} else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) {
json[name] = value_b ? 1 : 0;
} else {
char s[7];
json[name] = Helpers::render_boolean(s, value_b);
}
}
}
// handle TEXT strings
else if (dv.type == DeviceValueType::STRING) {
json[name] = (char *)(dv.value_p);
}
// handle ENUMs
else if ((dv.type == DeviceValueType::ENUM) && (*(uint8_t *)(dv.value_p) < dv.options_size)) {
// check for numeric enum-format
if (EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX) {
json[name] = (uint8_t)(*(uint8_t *)(dv.value_p));
} else {
json[name] = dv.options[*(uint8_t *)(dv.value_p)];
}
}
// handle Numbers
// If a divider is specified, do the division to 2 decimals places and send back as double/float
// otherwise force as a whole integer
// note: the strange nested if's is necessary due to the way the ArduinoJson templates are pre-processed by the compiler
else {
// If a divider is specified, do the division to 2 decimals places and send back as double/float
// otherwise force as an integer whole
uint8_t divider = 0;
uint8_t factor = 1;
if (dv.options_size == 1) {
auto s_str = read_flash_string(dv.options[0]); // prevent object backing the pointer will be destroyed at the end of the full-expression
const char * s = s_str.c_str();
if (s[0] == '*') {
factor = Helpers::atoint(&s[1]);
} else {
divider = Helpers::atoint(s);
}
}
// fahrenheit, 0 is no converstion other 1 or 2. not sure why?
uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0
: (dv.uom == DeviceValueUOM::DEGREES) ? 2
: (dv.uom == DeviceValueUOM::DEGREES_R) ? 1
: 0;
// always convert temperatures to floats with 1 decimal place
bool make_float = (divider || (dv.uom == DeviceValueUOM::DEGREES) || (dv.uom == DeviceValueUOM::DEGREES_R));
if (dv.type == DeviceValueType::INT) {
if (make_float) {
json[name] = Helpers::round2(*(int8_t *)(dv.value_p), divider, fahrenheit);
} else {
json[name] = *(int8_t *)(dv.value_p) * factor;
}
} else if (dv.type == DeviceValueType::UINT) {
if (make_float) {
json[name] = Helpers::round2(*(uint8_t *)(dv.value_p), divider, fahrenheit);
} else {
json[name] = *(uint8_t *)(dv.value_p) * factor;
}
} else if (dv.type == DeviceValueType::SHORT) {
if (make_float) {
json[name] = Helpers::round2(*(int16_t *)(dv.value_p), divider, fahrenheit);
} else {
json[name] = *(int16_t *)(dv.value_p) * factor;
}
} else if (dv.type == DeviceValueType::USHORT) {
if (make_float) {
json[name] = Helpers::round2(*(uint16_t *)(dv.value_p), divider, fahrenheit);
} else {
json[name] = *(uint16_t *)(dv.value_p) * factor;
}
} else if (dv.type == DeviceValueType::ULONG) {
if (make_float) {
json[name] = Helpers::round2(*(uint32_t *)(dv.value_p), divider, fahrenheit);
} else {
json[name] = *(uint32_t *)(dv.value_p) * factor;
}
} else if ((dv.type == DeviceValueType::TIME) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) {
uint32_t time_value = *(uint32_t *)(dv.value_p);
time_value = Helpers::round2(time_value, divider); // sometimes we need to divide by 60
if (output_target == OUTPUT_TARGET::API_VERBOSE || output_target == OUTPUT_TARGET::CONSOLE) {
char time_s[40];
snprintf(time_s,
sizeof(time_s),
"%d %s %d %s %d %s",
(time_value / 1440),
read_flash_string(F_(days)).c_str(),
((time_value % 1440) / 60),
read_flash_string(F_(hours)).c_str(),
(time_value % 60),
read_flash_string(F_(minutes)).c_str());
json[name] = time_s;
} else {
json[name] = time_value;
}
}
// commenting out - we don't want Commands in MQTT or Console
// else if (dv.type == DeviceValueType::CMD && output_target != EMSdevice::OUTPUT_TARGET::MQTT) {
// json[name] = "";
// }
}
}
}
return has_values;
}
// remove the Home Assistant configs for each device value/entity if its not visible or active or marked as read-only
// this is called when an MQTT publish is done via an EMS Device in emsesp.cpp::publish_device_values()
void EMSdevice::mqtt_ha_entity_config_remove() {
for (auto & dv : devicevalues_) {
if (dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED)
&& ((dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE)) || (!dv.has_state(DeviceValueState::DV_ACTIVE)))) {
dv.remove_state(DeviceValueState::DV_HA_CONFIG_CREATED);
if (dv.short_name == FL_(climate)[0]) {
Mqtt::publish_ha_climate_config(dv.tag, false, true); // delete topic (remove = true)
} else {
Mqtt::publish_ha_sensor_config(dv, "", "", true); // delete topic (remove = true)
}
}
}
}
// create the Home Assistant configs for each device value / entity
// this is called when an MQTT publish is done via an EMS Device in emsesp.cpp::publish_device_values()
void EMSdevice::mqtt_ha_entity_config_create() {
bool create_device_config = !ha_config_done(); // do we need to create the main Discovery device config with this entity?
// check the state of each of the device values
// create climate if roomtemp is visible
// create the discovery topic if if hasn't already been created, not a command (like reset) and is active and visible
for (auto & dv : devicevalues_) {
if ((dv.short_name == FL_(climate)[0]) && !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE) && dv.has_state(DeviceValueState::DV_ACTIVE)) {
if (*(int8_t *)(dv.value_p) == 1 && (!dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED) || dv.has_state(DeviceValueState::DV_HA_CLIMATE_NO_RT))) {
dv.remove_state(DeviceValueState::DV_HA_CLIMATE_NO_RT);
dv.add_state(DeviceValueState::DV_HA_CONFIG_CREATED);
Mqtt::publish_ha_climate_config(dv.tag, true);
} else if (*(int8_t *)(dv.value_p) == 0
&& (!dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED) || !dv.has_state(DeviceValueState::DV_HA_CLIMATE_NO_RT))) {
dv.add_state(DeviceValueState::DV_HA_CLIMATE_NO_RT);
dv.add_state(DeviceValueState::DV_HA_CONFIG_CREATED);
Mqtt::publish_ha_climate_config(dv.tag, false);
}
}
if (!dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED) && (dv.type != DeviceValueType::CMD) && dv.has_state(DeviceValueState::DV_ACTIVE)
&& !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE)) {
// create_device_config is only done once for the EMS device. It can added to any entity, so we take the first
Mqtt::publish_ha_sensor_config(dv, name(), brand_to_string(), false, create_device_config);
dv.add_state(DeviceValueState::DV_HA_CONFIG_CREATED);
create_device_config = false; // only create the main config once
}
}
ha_config_done(!create_device_config);
}
// remove all config topics in HA
void EMSdevice::ha_config_clear() {
for (auto & dv : devicevalues_) {
Mqtt::publish_ha_sensor_config(dv, "", "", true); // delete topic (remove = true)
dv.remove_state(DeviceValueState::DV_HA_CONFIG_CREATED);
}
ha_config_done(false); // this will force the recreation of the main HA device config
}
bool EMSdevice::has_telegram_id(uint16_t id) const {
for (const auto & tf : telegram_functions_) {
if (tf.telegram_type_id_ == id) {
return true;
}
}
return false;
}
// return the name of the telegram type
std::string EMSdevice::telegram_type_name(std::shared_ptr telegram) const {
// see if it's one of the common ones, like Version
if (telegram->type_id == EMS_TYPE_VERSION) {
return read_flash_string(F("Version"));
} else if (telegram->type_id == EMS_TYPE_UBADevices) {
return read_flash_string(F("UBADevices"));
}
for (const auto & tf : telegram_functions_) {
if ((tf.telegram_type_id_ == telegram->type_id) && (telegram->type_id != 0xFF)) {
return read_flash_string(tf.telegram_type_name_);
}
}
return std::string{};
}
// take a telegram_type_id and call the matching handler
// return true if match found
bool EMSdevice::handle_telegram(std::shared_ptr telegram) {
for (auto & tf : telegram_functions_) {
if (tf.telegram_type_id_ == telegram->type_id) {
// if the data block is empty and we have not received data before, assume that this telegram
// is not recognized by the bus master. So remove it from the automatic fetch list
if (telegram->message_length == 0 && telegram->offset == 0 && !tf.received_) {
EMSESP::logger().debug(F("This telegram (%s) is not recognized by the EMS bus"), read_flash_string(tf.telegram_type_name_).c_str());
tf.fetch_ = false;
return false;
}
if (telegram->message_length > 0) {
tf.received_ = true;
tf.process_function_(telegram);
}
return true;
}
}
return false; // type not found
}
// send Tx write with a data block
void EMSdevice::write_command(const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length, const uint16_t validate_typeid) const {
EMSESP::send_write_request(type_id, device_id(), offset, message_data, message_length, validate_typeid);
}
// send Tx write with a single value
void EMSdevice::write_command(const uint16_t type_id, const uint8_t offset, const uint8_t value, const uint16_t validate_typeid) const {
EMSESP::send_write_request(type_id, device_id(), offset, value, validate_typeid);
}
// send Tx write with a single value, with no post validation
void EMSdevice::write_command(const uint16_t type_id, const uint8_t offset, const uint8_t value) const {
EMSESP::send_write_request(type_id, device_id(), offset, value, 0);
}
// send Tx read command to the device
void EMSdevice::read_command(const uint16_t type_id, const uint8_t offset, const uint8_t length) const {
EMSESP::send_read_request(type_id, device_id(), offset, length);
}
} // namespace emsesp