/* * 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 (Helpers::translated_word(DeviceValue::DeviceValueTAG_s[tag])); } std::string EMSdevice::tag_to_mqtt(uint8_t tag) { return (DeviceValue::DeviceValueTAG_mqtt[tag]); } // convert UOM to a string - these don't need translating std::string EMSdevice::uom_to_string(uint8_t uom) { if (EMSESP::system_.fahrenheit() && (uom == DeviceValueUOM::DEGREES || uom == DeviceValueUOM::DEGREES_R)) { return (DeviceValue::DeviceValueUOM_s[DeviceValueUOM::FAHRENHEIT]); } return (DeviceValue::DeviceValueUOM_s[uom]); } std::string EMSdevice::brand_to_string() const { switch (brand_) { case EMSdevice::Brand::BOSCH: return "Bosch"; case EMSdevice::Brand::JUNKERS: return "Junkers"; case EMSdevice::Brand::BUDERUS: return "Buderus"; case EMSdevice::Brand::NEFIT: return "Nefit"; case EMSdevice::Brand::SIEGER: return "Sieger"; case EMSdevice::Brand::WORCESTER: return "Worcester"; case EMSdevice::Brand::IVT: return "IVT"; default: return ""; } } // returns the name of the MQTT topic to use for a specific device, without the base const char * EMSdevice::device_type_2_device_name(const uint8_t device_type) { switch (device_type) { case DeviceType::SYSTEM: return F_(system); case DeviceType::BOILER: return F_(boiler); case DeviceType::THERMOSTAT: return F_(thermostat); case DeviceType::HEATPUMP: return F_(heatpump); case DeviceType::SOLAR: return F_(solar); case DeviceType::CONNECT: return F_(connect); case DeviceType::MIXER: return F_(mixer); case DeviceType::DALLASSENSOR: return F_(dallassensor); case DeviceType::ANALOGSENSOR: return F_(analogsensor); case DeviceType::CONTROLLER: return F_(controller); case DeviceType::SWITCH: return F_(switch); case DeviceType::GATEWAY: return F_(gateway); default: return Helpers::translated_word(FL_(unknown)); } } // returns device_type from a string uint8_t EMSdevice::device_name_2_device_type(const char * topic) { if (!topic) { return DeviceType::UNKNOWN; } // 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, F_(boiler))) { return DeviceType::BOILER; } if (!strcmp(lowtopic, F_(thermostat))) { return DeviceType::THERMOSTAT; } if (!strcmp(lowtopic, F_(system))) { return DeviceType::SYSTEM; } if (!strcmp(lowtopic, F_(heatpump))) { return DeviceType::HEATPUMP; } if (!strcmp(lowtopic, F_(solar))) { return DeviceType::SOLAR; } if (!strcmp(lowtopic, F_(mixer))) { return DeviceType::MIXER; } if (!strcmp(lowtopic, F_(dallassensor))) { return DeviceType::DALLASSENSOR; } if (!strcmp(lowtopic, F_(analogsensor))) { return DeviceType::ANALOGSENSOR; } if (!strcmp(lowtopic, F_(switch))) { return DeviceType::SWITCH; } if (!strcmp(lowtopic, F_(gateway))) { return DeviceType::GATEWAY; } 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 std::string(name_) + " (DeviceID:" + Helpers::hextoa(device_id_) + ")"; } if (brand_ == Brand::NO_BRAND) { return std::string(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("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("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; } // check for a tag to create a nest bool EMSdevice::has_tag(const uint8_t tag) const { for (const auto & dv : devicevalues_) { if (dv.tag == tag && tag >= DeviceValueTAG::TAG_HC1) { return true; } } 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_) { auto fullname = dv.get_fullname(); if (!dv.has_state(DeviceValueState::DV_WEB_EXCLUDE) && dv.type != DeviceValueType::CMD && !fullname.empty()) { // 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(), dv.short_name); } else { snprintf(key, sizeof(key), "%s", dv.short_name); } JsonArray details = output.createNestedArray(key); // add the full name description details.add(fullname); // 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(" 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("0x%02X ", tf.telegram_type_id_); } shell.printf(COLOR_RESET); */ shell.printf(" Received telegram type IDs: "); for (const auto & tf : telegram_functions_) { if (tf.received_ && !tf.fetch_) { shell.printf("0x%02X ", tf.telegram_type_id_); } } shell.println(); shell.printf(" Fetched telegram type IDs: "); for (const auto & tf : telegram_functions_) { if (tf.fetch_) { shell.printf("0x%02X ", tf.telegram_type_id_); } } shell.println(); shell.printf(" Pending telegram type IDs: "); for (const auto & tf : telegram_functions_) { if (!tf.received_ && !tf.fetch_) { shell.printf("0x%02X ", tf.telegram_type_id_); } } shell.println(); shell.printf(" Ignored telegram type IDs: "); for (auto handlers : handlers_ignored_) { shell.printf("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 char * 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, which are translated as a list of lists // options_single: list of names // numeric_operator: to divide or multiply, see DeviceValueNumOps:: // short_name: used in MQTT as keys // fullname: used in Web and Console unless empty (nullptr) - can be translated // 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::add_device_value(uint8_t tag, void * value_p, uint8_t type, const char * const ** options, const char * const * options_single, int8_t numeric_operator, const char * const * name, uint8_t uom, const cmd_function_p f, int16_t min, uint16_t max) { bool has_cmd = (f != nullptr); auto short_name = name[0]; const char * const * fullname; if (Helpers::count_items(name) == 1) { fullname = nullptr; // no translations available, use empty to prevent crash } else { fullname = &name[1]; // translations start at index 1 } // 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 } // determine state uint8_t state = DeviceValueState::DV_DEFAULT; // custom fullname std::string custom_fullname = std::string(""); // 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 ? short_name : tag_to_mqtt(tag) + "/" + short_name; for (std::string entity_id : entityCustomization.entity_ids) { // if there is an appended custom name, strip it to get the true entity name // and extract the new custom name std::string shortname; auto custom_name_pos = entity_id.find('|'); bool has_custom_name = (custom_name_pos != std::string::npos); if (has_custom_name) { shortname = entity_id.substr(2, custom_name_pos - 2); } else { shortname = entity_id.substr(2); } // we found the device entity if (shortname == entity) { // get Mask 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 // see if there is a custom name in the entity string if (has_custom_name) { custom_fullname = entity_id.substr(custom_name_pos + 1); } break; } } } } }); // add the device entity devicevalues_.emplace_back( device_type_, tag, value_p, type, options, options_single, numeric_operator, short_name, fullname, custom_fullname, uom, has_cmd, min, max, state); // add a new command if it has a function attached if (!has_cmd) { 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 || tag == DeviceValueTAG::TAG_BOILER_DATA_WW) { flags |= CommandFlag::MQTT_SUB_FLAG_WW; } // add the command to our library Command::add(device_type_, short_name, f, fullname, flags); } // single list of options void EMSdevice::register_device_value(uint8_t tag, void * value_p, uint8_t type, const char * const * options_single, const char * const * name, uint8_t uom, const cmd_function_p f) { // create a multi-list from the options add_device_value(tag, value_p, type, nullptr, options_single, 0, name, uom, f, 0, 0); }; // single list of options, with no translations, with min and max void EMSdevice::register_device_value(uint8_t tag, void * value_p, uint8_t type, const char * const * options_single, const char * const * name, uint8_t uom, const cmd_function_p f, int16_t min, uint16_t max) { // create a multi-list from the options add_device_value(tag, value_p, type, nullptr, options_single, 0, name, uom, f, min, max); }; void EMSdevice::register_device_value(uint8_t tag, void * value_p, uint8_t type, int8_t numeric_operator, const char * const * name, uint8_t uom, const cmd_function_p f) { add_device_value(tag, value_p, type, nullptr, nullptr, numeric_operator, name, uom, f, 0, 0); } void EMSdevice::register_device_value(uint8_t tag, void * value_p, uint8_t type, int8_t numeric_operator, const char * const * name, uint8_t uom, const cmd_function_p f, int16_t min, uint16_t max) { add_device_value(tag, value_p, type, nullptr, nullptr, numeric_operator, name, uom, f, min, max); } // no options, no function void EMSdevice::register_device_value(uint8_t tag, void * value_p, uint8_t type, const char * const * name, uint8_t uom, const cmd_function_p f) { add_device_value(tag, value_p, type, nullptr, nullptr, 0, name, uom, f, 0, 0); }; // no options, with min/max void EMSdevice::register_device_value(uint8_t tag, void * value_p, uint8_t type, const char * const * name, uint8_t uom, const cmd_function_p f, int16_t min, uint16_t max) { add_device_value(tag, value_p, type, nullptr, nullptr, 0, name, uom, f, min, max); }; // function with min and max values // adds a new command to the command list // in this function we separate out the short and long names and take any translations void EMSdevice::register_device_value(uint8_t tag, void * value_p, uint8_t type, const char * const ** options, const char * const * name, uint8_t uom, const cmd_function_p f, int16_t min, uint16_t max) { add_device_value(tag, value_p, type, options, nullptr, 0, name, uom, f, min, max); } // 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 char * const ** options, const char * const * name, uint8_t uom, const cmd_function_p f) { add_device_value(tag, value_p, type, options, nullptr, 0, 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 char * const ** options, const char * const * name, uint8_t uom) { add_device_value(tag, value_p, type, options, nullptr, 0, 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 && std::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) { snprintf(topic, sizeof(topic), "%s/%s/%s", device_type_2_device_name(device_type_), tag_to_mqtt(dv.tag).c_str(), dv.short_name); } else { snprintf(topic, sizeof(topic), "%s/%s", device_type_2_device_name(device_type_), (dv.short_name)); } } 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(), dv.short_name); } else { snprintf(topic, sizeof(topic), "%s/%s", Mqtt::tag_to_topic(device_type_, dv.tag).c_str(), dv.short_name); } int8_t num_op = dv.numeric_operator; char payload[55] = {'\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 { auto enum_str = Helpers::translated_word(dv.options[*(uint8_t *)(value_p)]); strlcpy(payload, enum_str, sizeof(payload)); } } break; } case DeviceValueType::USHORT: Helpers::render_value(payload, *(uint16_t *)(value_p), num_op, fahrenheit); break; case DeviceValueType::UINT: Helpers::render_value(payload, *(uint8_t *)(value_p), num_op, fahrenheit); break; case DeviceValueType::SHORT: Helpers::render_value(payload, *(int16_t *)(value_p), num_op, fahrenheit); break; case DeviceValueType::INT: Helpers::render_value(payload, *(int8_t *)(value_p), num_op, fahrenheit); break; case DeviceValueType::ULONG: Helpers::render_value(payload, *(uint32_t *)(value_p), num_op, 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), num_op); 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 // key is the fullname 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 = Helpers::translated_word(DeviceValue::DeviceValueTAG_s[i]); if (tag) { std::string key2 = key; // copy string to a std::string so we can use the find function uint8_t length = strlen(tag); if ((key2.find(tag) != std::string::npos) && (key[length] == ' ')) { key_p += length + 1; // remove the tag break; } } } // look up key in our device value list for (const auto & dv : devicevalues_) { auto fullname = dv.get_fullname(); if ((!dv.has_state(DeviceValueState::DV_WEB_EXCLUDE) && !fullname.empty()) && (fullname == 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_) { auto fullname = dv.get_fullname(); // check conditions: // 1. fullname 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) && !fullname.empty() && (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[12]; 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"] = Helpers::translated_word(dv.options[*(uint8_t *)(dv.value_p)]); } // handle numbers else { // note, the nested if's is necessary due to the way the ArduinoJson templates are pre-processed by the compiler 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::transformNumFloat(*(int8_t *)(dv.value_p), dv.numeric_operator, fahrenheit); } else if ((dv.type == DeviceValueType::UINT) && Helpers::hasValue(*(uint8_t *)(dv.value_p))) { obj["v"] = Helpers::transformNumFloat(*(uint8_t *)(dv.value_p), dv.numeric_operator, fahrenheit); } else if ((dv.type == DeviceValueType::SHORT) && Helpers::hasValue(*(int16_t *)(dv.value_p))) { obj["v"] = Helpers::transformNumFloat(*(int16_t *)(dv.value_p), dv.numeric_operator, fahrenheit); } else if ((dv.type == DeviceValueType::USHORT) && Helpers::hasValue(*(uint16_t *)(dv.value_p))) { obj["v"] = Helpers::transformNumFloat(*(uint16_t *)(dv.value_p), dv.numeric_operator, fahrenheit); } else if ((dv.type == DeviceValueType::ULONG) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) { obj["v"] = Helpers::transformNumFloat(*(uint32_t *)(dv.value_p), dv.numeric_operator); } else if ((dv.type == DeviceValueType::TIME) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) { obj["v"] = Helpers::transformNumFloat(*(uint32_t *)(dv.value_p), dv.numeric_operator); } else { obj["v"] = ""; // must have a value for sorting to work } } // 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 in the WebUI table and must be unique if ((dv.tag == DeviceValueTAG::TAG_NONE) || tag_to_string(dv.tag).empty()) { obj["id"] = mask + fullname; } else { obj["id"] = mask + tag_to_string(dv.tag) + " " + fullname; } // 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) + "/" + 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++) { auto enum_str = Helpers::translated_word(dv.options[i]); if (enum_str) { l.add(enum_str); } } } else if (dv.type == DeviceValueType::BOOL) { JsonArray l = obj.createNestedArray("l"); char result[12]; 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_single[0]; // NOT translated } } // handle INTs // add steps to numeric values with numeric_operator else { char s[10]; if (dv.numeric_operator > 0) { obj["s"] = Helpers::render_value(s, (float)1 / dv.numeric_operator, 1); } else if (dv.numeric_operator < 0) { obj["s"] = Helpers::render_value(s, (float)(-1) * dv.numeric_operator, 0); } int16_t dv_set_min; uint16_t 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_customization(JsonArray & output) { for (auto & dv : devicevalues_) { // also show commands and entities that have an empty full name JsonObject obj = output.createNestedObject(); uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (dv.uom == DeviceValueUOM::DEGREES) ? 2 : (dv.uom == DeviceValueUOM::DEGREES_R) ? 1 : 0; // 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[12]; 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"] = Helpers::translated_word(dv.options[*(uint8_t *)(dv.value_p)]); } // handle Integers and Floats else { if (dv.type == DeviceValueType::INT) { obj["v"] = Helpers::transformNumFloat(*(int8_t *)(dv.value_p), dv.numeric_operator, fahrenheit); } else if (dv.type == DeviceValueType::UINT) { obj["v"] = Helpers::transformNumFloat(*(uint8_t *)(dv.value_p), dv.numeric_operator, fahrenheit); } else if (dv.type == DeviceValueType::SHORT) { obj["v"] = Helpers::transformNumFloat(*(int16_t *)(dv.value_p), dv.numeric_operator, fahrenheit); } else if (dv.type == DeviceValueType::USHORT) { obj["v"] = Helpers::transformNumFloat(*(uint16_t *)(dv.value_p), dv.numeric_operator, fahrenheit); } else if (dv.type == DeviceValueType::ULONG) { obj["v"] = Helpers::transformNumFloat(*(uint32_t *)(dv.value_p), dv.numeric_operator); } else if (dv.type == DeviceValueType::TIME) { obj["v"] = Helpers::transformNumFloat(*(uint32_t *)(dv.value_p), dv.numeric_operator); } } } // id holds the shortname and must always have a value for the WebUI table to work if (dv.tag >= DeviceValueTAG::TAG_HC1) { obj["id"] = tag_to_mqtt(dv.tag) + "/" + dv.short_name; } else { obj["id"] = dv.short_name; } // n is the fullname, and can be optional // don't add the fullname if its a command auto fullname = Helpers::translated_word(dv.fullname); if (dv.type != DeviceValueType::CMD) { if (fullname) { if ((dv.tag == DeviceValueTAG::TAG_NONE) || tag_to_string(dv.tag).empty()) { obj["n"] = fullname; } else { char name[50]; snprintf(name, sizeof(name), "%s %s", tag_to_string(dv.tag).c_str(), fullname); obj["n"] = name; } } // add the custom name, is optional std::string custom_fullname = dv.get_custom_fullname(); if (!custom_fullname.empty()) { obj["cn"] = custom_fullname; } } else { obj["n"] = "!" + std::string(fullname); // prefix commands with a ! } 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 if (dv.has_cmd) { // set the custom min and max values if there are any int16_t dv_set_min; uint16_t dv_set_max; if (dv.get_custom_min(dv_set_min)) { obj["mi"] = fahrenheit ? (int)(dv_set_min * 1.8 + 32 * (fahrenheit - 1)) : dv_set_min; } if (dv.get_custom_max(dv_set_max)) { obj["ma"] = fahrenheit ? (int)(dv_set_max * 1.8 + 32 * (fahrenheit - 1)) : dv_set_max; } } } } void EMSdevice::set_climate_minmax(uint8_t tag, int16_t min, uint16_t max) { for (auto & dv : devicevalues_) { if (dv.tag == tag && dv.short_name == FL_(climate[0])) { if (dv.min != min || dv.max != max) { dv.min = min; dv.max = max; dv.remove_state(DeviceValueState::DV_HA_CONFIG_CREATED); Mqtt::publish_ha_climate_config(dv.tag, false, true); // delete topic (remove = true) } return; } } } // 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::setCustomEntity(const std::string & entity_id) { for (auto & dv : devicevalues_) { std::string entity_name = dv.tag < DeviceValueTAG::TAG_HC1 ? dv.short_name : tag_to_mqtt(dv.tag) + "/" + dv.short_name; // extra shortname std::string shortname; auto custom_name_pos = entity_id.find('|'); bool has_custom_name = (custom_name_pos != std::string::npos); if (has_custom_name) { shortname = entity_id.substr(2, custom_name_pos - 2); } else { shortname = entity_id.substr(2); } if (entity_name == shortname) { // check the masks 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 it's a new mask, reconfigure HA if (Mqtt::ha_enabled() && (has_custom_name || ((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) } // always write the mask dv.state = ((dv.state & 0x0F) | (new_mask << 4)); // set state high bits to flag // set the custom name if it has one, or clear it if (has_custom_name) { dv.custom_fullname = entity_id.substr(custom_name_pos + 1); } else { dv.custom_fullname = ""; } auto min = dv.min; auto max = dv.max; // set the min / max dv.set_custom_minmax(); if (Mqtt::ha_enabled() && dv.short_name == FL_(seltemp)[0] && (min != dv.min || max != dv.max)) { set_climate_minmax(dv.tag, dv.min, dv.max); } return; } } } // populate a string vector with entities that have masks set or have a custom name void EMSdevice::getCustomEntities(std::vector & entity_ids) { for (const auto & dv : devicevalues_) { std::string entity_name = dv.tag < DeviceValueTAG::TAG_HC1 ? dv.short_name : tag_to_mqtt(dv.tag) + "/" + dv.short_name; uint8_t mask = dv.state >> 4; if (mask || !dv.custom_fullname.empty()) { if (dv.custom_fullname.empty()) { entity_ids.push_back(Helpers::hextoa(mask, false) + entity_name); } else { entity_ids.push_back(Helpers::hextoa(mask, false) + entity_name + "|" + dv.custom_fullname); } } } } // 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 or hs if (id >= 1 && id <= (1 + DeviceValueTAG::TAG_HS16 - DeviceValueTAG::TAG_HC1)) { tag = DeviceValueTAG::TAG_HC1 + id - 1; } // 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, dv.short_name) && (tag <= 0 || tag == dv.tag)) { 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; auto fullname = dv.get_fullname(); if (!fullname.empty()) { if ((dv.tag == DeviceValueTAG::TAG_NONE) || tag_to_string(dv.tag).empty()) { json["fullname"] = fullname; } else { json["fullname"] = tag_to_string(dv.tag) + " " + fullname; } } if (!tag_to_mqtt(dv.tag).empty()) { json["circuit"] = tag_to_mqtt(dv.tag); } char val[10]; 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] = Helpers::translated_word(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(Helpers::translated_word(dv.options[i])); } break; } case DeviceValueType::USHORT: if (Helpers::hasValue(*(uint16_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(uint16_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } json[type] = F_(number); break; case DeviceValueType::UINT: if (Helpers::hasValue(*(uint8_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(uint8_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } json[type] = F_(number); break; case DeviceValueType::SHORT: if (Helpers::hasValue(*(int16_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(int16_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } json[type] = F_(number); break; case DeviceValueType::INT: if (Helpers::hasValue(*(int8_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(int8_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } json[type] = F_(number); break; case DeviceValueType::ULONG: if (Helpers::hasValue(*(uint32_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(uint32_t *)(dv.value_p), dv.numeric_operator)); } 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[12]; json[value] = Helpers::render_boolean(s, value_b); } } json[type] = ("boolean"); break; case DeviceValueType::TIME: if (Helpers::hasValue(*(uint32_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(uint32_t *)(dv.value_p), dv.numeric_operator)); } json[type] = F_(number); break; case DeviceValueType::STRING: if (Helpers::hasValue((char *)(dv.value_p))) { json[value] = (char *)(dv.value_p); } json[type] = ("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(Helpers::translated_word(dv.options[i])); } } break; default: json[type] = Helpers::translated_word(FL_(unknown)); break; } // set the min and max only for commands if (dv.has_cmd) { int16_t dv_set_min; uint16_t 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("[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); } auto fullname = dv.get_fullname(); // 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) && !fullname.empty() && (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(), fullname.c_str()); // prefix the tag } else { strlcpy(name, fullname.c_str(), sizeof(name)); // use full name } } else { strlcpy(name, (dv.short_name), 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_mqtt(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[12]; json[name] = Helpers::render_boolean(s, value_b); // for HA always render as string } else if (output_target == OUTPUT_TARGET::CONSOLE) { char s[12]; json[name] = Helpers::render_boolean(s, value_b, true); // console use web settings } 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[12]; 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, console use text format if (EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX && output_target != OUTPUT_TARGET::CONSOLE) { json[name] = (uint8_t)(*(uint8_t *)(dv.value_p)); } else { json[name] = Helpers::translated_word(dv.options[*(uint8_t *)(dv.value_p)]); } } // handle Numbers else { // fahrenheit, 0 is no conversion 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; char val[10]; if (dv.type == DeviceValueType::INT) { json[name] = serialized(Helpers::render_value(val, *(int8_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } else if (dv.type == DeviceValueType::UINT) { json[name] = serialized(Helpers::render_value(val, *(uint8_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } else if (dv.type == DeviceValueType::SHORT) { json[name] = serialized(Helpers::render_value(val, *(int16_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } else if (dv.type == DeviceValueType::USHORT) { json[name] = serialized(Helpers::render_value(val, *(uint16_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } else if (dv.type == DeviceValueType::ULONG) { json[name] = serialized(Helpers::render_value(val, *(uint32_t *)(dv.value_p), dv.numeric_operator)); } else if ((dv.type == DeviceValueType::TIME) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) { uint32_t time_value = *(uint32_t *)(dv.value_p); if (dv.numeric_operator == DeviceValueNumOp::DV_NUMOP_DIV60) { time_value /= 60; // sometimes we need to divide by 60 } if (output_target == OUTPUT_TARGET::API_VERBOSE || output_target == OUTPUT_TARGET::CONSOLE) { char time_s[60]; snprintf(time_s, sizeof(time_s), "%d %s %d %s %d %s", (time_value / 1440), Helpers::translated_word(FL_(days)), ((time_value % 1440) / 60), Helpers::translated_word(FL_(hours)), (time_value % 60), Helpers::translated_word(FL_(minutes))); 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] = ""; // } // check for value outside min/max range and adapt the limits to avoid HA complains // Should this also check for api output? if ((output_target == OUTPUT_TARGET::MQTT) && (dv.min != 0 || dv.max != 0)) { if (json[name].is() || json[name].is()) { int v = json[name]; if (fahrenheit) { v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C } if (v < dv.min) { dv.min = v; dv.remove_state(DeviceValueState::DV_HA_CONFIG_CREATED); } else if (v > dv.max) { dv.max = v; dv.remove_state(DeviceValueState::DV_HA_CONFIG_CREATED); } } } } } } 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, false, dv.min, dv.max); } 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, false, dv.min, dv.max); } } 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 "Version"; } else if (telegram->type_id == EMS_TYPE_UBADevices) { return "UBADevices"; } for (const auto & tf : telegram_functions_) { if ((tf.telegram_type_id_ == telegram->type_id) && (telegram->type_id != 0xFF)) { return 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("This telegram (%s) is not recognized by the EMS bus", tf.telegram_type_name_); 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