From d3b4bab40a71ce90779e2f48ceb301a96c9cebf2 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 13 Dec 2025 11:22:22 +0100 Subject: [PATCH] dynamically add HA thermostat modes based on model --- src/core/emsdevice.cpp | 42 ++++++++++++++++++++--------- src/core/mqtt.cpp | 61 +++++++++++++++++++++++++++--------------- src/core/mqtt.h | 2 +- 3 files changed, 69 insertions(+), 36 deletions(-) diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index 828c38e40..fdf87b43d 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -2041,24 +2041,40 @@ bool EMSdevice::generate_values(JsonObject output, const int8_t tag_filter, cons // 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? - uint16_t count = 0; + bool create_device_config = !ha_config_done(); // do we need to create the main Discovery device config with this entity? + uint16_t count = 0; + const char * const ** mode_options = nullptr; + + // if it's a thermostat go fetch the list of modes + if (device_type() == EMSdevice::DeviceType::THERMOSTAT) { + for (auto & dv : devicevalues_) { + // make sure it's a type DeviceValueType::ENUM + if ((dv.type == DeviceValueType::ENUM) && !strcmp(dv.short_name, FL_(mode)[0])) { + // get options + mode_options = dv.options; + break; + } + } + } // check the state of each of the device values // 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_) { // create climate when we reach the haclimate entity if (!strcmp(dv.short_name, FL_(haclimate)[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))) { - if (Mqtt::publish_ha_climate_config(dv, true, false)) { // roomTemp - dv.remove_state(DeviceValueState::DV_HA_CLIMATE_NO_RT); - dv.add_state(DeviceValueState::DV_HA_CONFIG_CREATED); - count++; - } - } 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))) { - if (Mqtt::publish_ha_climate_config(dv, false, false)) { // no roomTemp - dv.add_state(DeviceValueState::DV_HA_CLIMATE_NO_RT); + int8_t haclimate_value = *(int8_t *)(dv.value_p); + bool has_config_created = dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED); + bool has_climate_no_rt = dv.has_state(DeviceValueState::DV_HA_CLIMATE_NO_RT); + bool needs_update = !has_config_created || (haclimate_value == 1 ? has_climate_no_rt : !has_climate_no_rt); + + if (needs_update) { + bool has_room_temp = (haclimate_value == 1); + if (Mqtt::publish_ha_climate_config(dv, has_room_temp, mode_options, false)) { + if (has_room_temp) { + dv.remove_state(DeviceValueState::DV_HA_CLIMATE_NO_RT); + } else { + dv.add_state(DeviceValueState::DV_HA_CLIMATE_NO_RT); + } dv.add_state(DeviceValueState::DV_HA_CONFIG_CREATED); count++; } @@ -2075,7 +2091,7 @@ void EMSdevice::mqtt_ha_entity_config_create() { } // SRC thermostats mapped to connect/src1/... if (dv.tag >= DeviceValueTAG::TAG_SRC1 && dv.tag <= DeviceValueTAG::TAG_SRC16 && !strcmp(dv.short_name, FL_(selRoomTemp)[0])) { - Mqtt::publish_ha_climate_config(dv, true, false); + Mqtt::publish_ha_climate_config(dv, true, mode_options, false); } #ifndef EMSESP_STANDALONE diff --git a/src/core/mqtt.cpp b/src/core/mqtt.cpp index 6fecc5d3a..d7e31aabb 100644 --- a/src/core/mqtt.cpp +++ b/src/core/mqtt.cpp @@ -546,7 +546,7 @@ void Mqtt::ha_status() { dev["mf"] = "EMS-ESP"; dev["mdl"] = "EMS-ESP"; #ifndef EMSESP_STANDALONE - dev["cu"] = "http://" + (EMSESP::system_.ethernet_connected() ? ETH.localIP().toString() : WiFi.localIP().toString()); + dev["cu"] = std::string("http://") + EMSESP::system_.get_ip_or_hostname().c_str(); #endif JsonArray ids = dev["ids"].to(); ids.add(Mqtt::basename()); @@ -1103,6 +1103,12 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev // https://github.com/emsesp/EMS-ESP32/discussions/1459#discussioncomment-7694873 add_ha_classes(doc.as(), device_type, type, uom, entity); + // add origin + JsonObject origin_json = doc["o"].to(); + origin_json["name"] = "EMS-ESP"; + origin_json["sw"] = EMSESP_APP_VERSION; + origin_json["url"] = "https://emsesp.org"; + // add dev section if (device_type == EMSdevice::DeviceType::SYSTEM) { add_ha_dev_section(doc.as(), nullptr, nullptr, nullptr, nullptr, false); @@ -1236,7 +1242,9 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8 } } -bool Mqtt::publish_ha_climate_config(const DeviceValue & dv, const bool has_roomtemp, const bool remove) { +// publish the HA climate config +// https://www.home-assistant.io/integrations/climate.mqtt/ +bool Mqtt::publish_ha_climate_config(const DeviceValue & dv, const bool has_roomtemp, const char * const ** mode_options, const bool remove) { int8_t tag = dv.tag; int16_t min = dv.min; uint32_t max = dv.max; @@ -1356,34 +1364,43 @@ bool Mqtt::publish_ha_climate_config(const DeviceValue & dv, const bool has_room doc["act_t"] = "~/boiler_data"; doc["act_tpl"] = "{% if value_json.hpactivity=='cooling'%}cooling{%elif value_json.heatingactive=='on'%}heating{%else%}idle{%endif%}"; - // the HA climate component only responds to auto, heat and off - JsonArray modes = doc["modes"].to(); - - // go through dv.options and map to HA climate modes - // https://www.home-assistant.io/integrations/climate.mqtt/ - // HA supports: ["auto", "off", "cool", "heat", "dry", "fan_only"] - if (dv.options != nullptr) { + // map EMS modes to HA climate modes + // EMS modes: auto, manual, heat, off, night, day, nofrost, eco, comfort, cool) + // HA supports: auto, off, cool, heat, dry, fan_only + if (mode_options != nullptr) { + // scan through mode_options and add to modes bool found_auto = false; bool found_heat = false; bool found_off = false; - for (uint8_t i = 0; i < dv.options_size; i++) { - const char * option = dv.options[i][0]; - if (strcmp(option, "auto") == 0) { + bool found_cool = false; + for (uint8_t i = 0; i < Helpers::count_items(mode_options); i++) { + const char * mode = mode_options[i][0]; // take EN + if (!strcmp(mode, FL_(auto)[0])) { found_auto = true; - } else if (strcmp(option, "heat") == 0) { + } else if (!strcmp(mode, FL_(heat)[0])) { found_heat = true; - } else if (strcmp(option, "off") == 0) { + } else if (!strcmp(mode, FL_(off)[0])) { found_off = true; + } else if (!strcmp(mode, FL_(cool)[0])) { + found_cool = true; } } - if (found_auto) { - modes.add("auto"); - } - if (found_heat) { - modes.add("heat"); - } - if (found_off) { - modes.add("off"); + + // only add modes if we found at least one + if (found_auto || found_heat || found_off || found_cool) { + JsonArray modes = doc["modes"].to(); + if (found_auto) { + modes.add("auto"); + } + if (found_heat) { + modes.add("heat"); + } + if (found_off) { + modes.add("off"); + } + if (found_cool) { + modes.add("cool"); + } } } diff --git a/src/core/mqtt.h b/src/core/mqtt.h index 60b4f2a5e..60e0fc1ea 100644 --- a/src/core/mqtt.h +++ b/src/core/mqtt.h @@ -113,7 +113,7 @@ class Mqtt { const bool create_device_config = false); static bool publish_system_ha_sensor_config(uint8_t type, const char * name, const char * entity, const uint8_t uom); - static bool publish_ha_climate_config(const DeviceValue & dv, const bool has_roomtemp, const bool remove = false); + static bool publish_ha_climate_config(const DeviceValue & dv, const bool has_roomtemp, const char * const ** mode_options, const bool remove = false); static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_type); static void show_mqtt(uuid::console::Shell & shell);