From 24216d7b4fa9cf4932b633bab7369a36310c8670 Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Wed, 23 Feb 2022 10:29:56 +0100 Subject: [PATCH 1/2] move HA climate to mqtt --- src/devices/thermostat.cpp | 188 ++----------------------------------- src/devices/thermostat.h | 13 --- src/emsdevice.cpp | 18 +++- src/emsdevicevalue.h | 3 +- src/mqtt.cpp | 90 ++++++++++++++++++ src/mqtt.h | 10 ++ 6 files changed, 126 insertions(+), 196 deletions(-) diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index ac18865e4..d126638e2 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -333,8 +333,6 @@ std::shared_ptr Thermostat::heating_circuit(std::sha auto new_hc = std::make_shared(hc_num, model()); heating_circuits_.push_back(new_hc); - new_hc->ha_climate_created(false); // set flag saying we're ready to create the MQTT Discovery Climate topic for this hc - // sort based on hc number so there's a nice order when displaying // TODO temporarily commented out the HC sorting until I'm 100% sure the return object still references the newly created object // not sure if new_hc and heating_circuits_.back() will still reference the new HC after its sorted - to check! @@ -371,123 +369,6 @@ std::shared_ptr Thermostat::heating_circuit(std::sha return new_hc; // return back point to new HC object } -// publish config topic for HA MQTT Discovery for a heating circuit -// e.g. homeassistant/climate/ems-esp/thermostat_hc1/config -void Thermostat::publish_ha_config_hc(std::shared_ptr hc) { - uint8_t hc_num = hc->hc_num(); - StaticJsonDocument doc; - - char topic_t[Mqtt::MQTT_TOPIC_MAX_SIZE]; - char hc_mode_s[30]; - char seltemp_s[30]; - char currtemp_s[30]; - char mode_str_tpl[400]; - char name_s[30]; - char uniq_id_s[30]; - char temp_cmd_s[30]; - char mode_cmd_s[30]; - char min_s[10]; - char max_s[10]; - - // https://github.com/emsesp/EMS-ESP32/issues/325#issuecomment-1022249093 - // before you had to have a seltemp and roomtemp for the HA to work, now its optional - bool have_current_room_temp = Helpers::hasValue(hc->roomTemp); - - if (Mqtt::is_nested()) { - // nested format - snprintf(hc_mode_s, sizeof(hc_mode_s), "value_json.hc%d.mode", hc_num); - snprintf(seltemp_s, sizeof(seltemp_s), "{{value_json.hc%d.seltemp}}", hc_num); - if (have_current_room_temp) { - snprintf(currtemp_s, sizeof(currtemp_s), "{{value_json.hc%d.currtemp}}", hc_num); - } - snprintf(topic_t, sizeof(topic_t), "~/%s", Mqtt::tag_to_topic(EMSdevice::DeviceType::THERMOSTAT, DeviceValueTAG::TAG_NONE).c_str()); - } else { - // single format - snprintf(hc_mode_s, sizeof(hc_mode_s), "value_json.mode"); - snprintf(seltemp_s, sizeof(seltemp_s), "{{value_json.seltemp}}"); - if (have_current_room_temp) { - snprintf(currtemp_s, sizeof(currtemp_s), "{{value_json.currtemp}}"); - } - snprintf(topic_t, sizeof(topic_t), "~/%s", Mqtt::tag_to_topic(EMSdevice::DeviceType::THERMOSTAT, DeviceValueTAG::TAG_HC1 + hc_num - 1).c_str()); - } - - snprintf(mode_str_tpl, - sizeof(mode_str_tpl), - "{%%if %s=='manual'%%}heat{%%elif %s=='day'%%}heat{%%elif %s=='night'%%}off{%%elif %s=='off'%%}off{%%else%%}auto{%%endif%%}", - hc_mode_s, - hc_mode_s, - hc_mode_s, - hc_mode_s); - - snprintf(name_s, sizeof(name_s), "Thermostat hc%d", hc_num); - snprintf(uniq_id_s, sizeof(uniq_id_s), "thermostat_hc%d", hc_num); - snprintf(temp_cmd_s, sizeof(temp_cmd_s), "~/thermostat/hc%d/seltemp", hc_num); - snprintf(mode_cmd_s, sizeof(temp_cmd_s), "~/thermostat/hc%d/mode", hc_num); - - doc["~"] = Mqtt::base(); - doc["name"] = name_s; - doc["uniq_id"] = uniq_id_s; - doc["mode_stat_t"] = topic_t; - doc["mode_stat_tpl"] = mode_str_tpl; - doc["temp_cmd_t"] = temp_cmd_s; - doc["temp_stat_t"] = topic_t; - doc["temp_stat_tpl"] = seltemp_s; - doc["mode_cmd_t"] = mode_cmd_s; - - if (have_current_room_temp) { - doc["curr_temp_t"] = topic_t; - doc["curr_temp_tpl"] = currtemp_s; - } - - doc["min_temp"] = Helpers::render_value(min_s, 5, 0, EMSESP::system_.fahrenheit() ? 2 : 0); - doc["max_temp"] = Helpers::render_value(max_s, 30, 0, EMSESP::system_.fahrenheit() ? 2 : 0); - doc["temp_step"] = "0.5"; - - // the HA climate component only responds to auto, heat and off - JsonArray modes = doc.createNestedArray("modes"); - - if (model() != EMSdevice::EMS_DEVICE_FLAG_RC10) { - modes.add("auto"); - } - - modes.add("heat"); - modes.add("off"); - - JsonObject dev = doc.createNestedObject("dev"); - JsonArray ids = dev.createNestedArray("ids"); - ids.add("ems-esp-thermostat"); - - char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; - snprintf(topic, sizeof(topic), "climate/%s/thermostat_hc%d/config", Mqtt::base().c_str(), hc_num); - Mqtt::publish_ha(topic, doc.as()); // publish the config payload with retain flag -} - -// for HA specifically when receiving over MQTT in the thermostat topic -// e.g. thermostat_hc1 -// it could be either a 'mode' or a float value for a temperature. we try brute force both and see which one works. -// return true if it parses the message correctly -bool Thermostat::thermostat_ha_cmd(const char * message, uint8_t hc_num) { - // check if it's json. We know the message isn't empty - if (message[0] == '{') { - return false; - } - - // check for mode first, which is a string - if (message[0] >= 'A') { - if (set_mode(message, hc_num)) { - return true; - } - } - if ((message[0] >= '0' && message[0] <= '9') || message[0] == '-') { - // otherwise handle as a numerical temperature value and set the setpoint temp - float f = strtof((char *)message, 0); - set_temperature(f, HeatingCircuit::Mode::AUTO, hc_num); - return true; - } - - return false; -} - // decodes the thermostat mode for the heating circuit based on the thermostat type // modes are off, manual, auto, day, night and holiday uint8_t Thermostat::HeatingCircuit::get_mode() const { @@ -665,29 +546,6 @@ void Thermostat::process_RC10Monitor(std::shared_ptr telegram) { has_update(telegram, hc->selTemp, 1, 1); // is * 2, force as single byte has_update(telegram, hc->roomTemp, 2); // is * 10 has_update(telegram, hc->reduceminutes, 5); - - add_ha_climate(hc); -} - -// add the HVAC/Climate HA component for the HC -void Thermostat::add_ha_climate(std::shared_ptr hc) { - if (!Mqtt::ha_enabled()) { - return; - } - - // note, this doesn't account for whether any of the device values have been excluded - if (hc->ha_climate_created()) { - // see if we've lost the selTemp (roomTemp/currTemp is optional and checked in the publish_ha_config_hc() function) - if (!Helpers::hasValue(hc->selTemp)) { - char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; - snprintf(topic, sizeof(topic), "climate/%s/thermostat_hc%d/config", Mqtt::base().c_str(), hc->hc_num()); - Mqtt::publish_ha(topic); - } - } else { - // create the climate component, only once - publish_ha_config_hc(hc); - hc->ha_climate_created(true); - } } // type 0xB0 - for reading the mode from the RC10 thermostat (0x17) @@ -791,8 +649,6 @@ void Thermostat::process_RC20Monitor_2(std::shared_ptr telegram) has_update(telegram, hc->selTemp, 2, 1); // is * 2, force as single byte has_update(telegram, hc->roomTemp, 3); // is * 10 has_bitupdate(telegram, hc->summermode, 1, 0); - - add_ha_climate(hc); } // 0xAD - for reading the mode from the RC20/ES72 thermostat (0x17) @@ -817,8 +673,6 @@ void Thermostat::process_RC20Set_2(std::shared_ptr telegram) { has_update(telegram, hc->minflowtemp, 15); has_update(telegram, hc->maxflowtemp, 16); has_update(telegram, hc->summertemp, 17); - - add_ha_climate(hc); } // 0xAF - for reading the roomtemperature from the RC20/ES72 thermostat (0x18, 0x19, ..) @@ -827,8 +681,7 @@ void Thermostat::process_RC20Remote(std::shared_ptr telegram) { if (hc == nullptr) { return; } - has_update(telegram, hc->roomTemp, 0); - add_ha_climate(hc); + has_update(telegram, hc->remotetemp, 0); } // type 0x0165, ff @@ -844,8 +697,6 @@ void Thermostat::process_JunkersSet(std::shared_ptr telegram) { has_update(telegram, hc->control, 1); // remote: 0-off, 1-FB10, 2-FB100 has_enumupdate(telegram, hc->program, 13, 1); // 1-6: 1 = A, 2 = B,... has_enumupdate(telegram, hc->mode, 14, 1); // 0 = nofrost, 1 = eco, 2 = heat, 3 = auto - - add_ha_climate(hc); } // type 0x0179, ff @@ -860,8 +711,6 @@ void Thermostat::process_JunkersSet2(std::shared_ptr telegram) { has_update(telegram, hc->nofrosttemp, 5); // is * 2 has_enumupdate(telegram, hc->program, 10, 1); // 1-6: 1 = A, 2 = B,... has_enumupdate(telegram, hc->mode, 4, 1); // 0 = nofrost, 1 = eco, 2 = heat, 3 = auto - - add_ha_climate(hc); } // type 0x123 - FR10/FR110 Junkers as remote @@ -870,8 +719,7 @@ void Thermostat::process_JunkersRemoteMonitor(std::shared_ptr te if (hc == nullptr) { return; } - has_update(telegram, hc->roomTemp, 0); // roomTemp from remote - add_ha_climate(hc); + has_update(telegram, hc->remotetemp, 0); // roomTemp from remote } // type 0xA3 - for external temp settings from the the RC* thermostats (e.g. RC35) @@ -892,7 +740,6 @@ void Thermostat::process_RC20Monitor(std::shared_ptr telegram) { has_update(telegram, hc->selTemp, 1, 1); // is * 2, force as single byte has_update(telegram, hc->roomTemp, 2); // is * 10 - add_ha_climate(hc); } // type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long @@ -904,7 +751,6 @@ void Thermostat::process_EasyMonitor(std::shared_ptr telegram) { has_update(telegram, hc->roomTemp, 8); // is * 100 has_update(telegram, hc->selTemp, 10); // is * 100 - add_ha_climate(hc); } // Settings Parameters - 0xA5 - RC30_1 @@ -984,9 +830,7 @@ void Thermostat::process_JunkersMonitor(std::shared_ptr telegram } else { has_update(telegram, hc->roomTemp, 4); // value is * 10 } - - add_ha_climate(hc); -} + } // type 0x02A5 - data from Worchester CRF200 void Thermostat::process_CRFMonitor(std::shared_ptr telegram) { @@ -1000,8 +844,6 @@ void Thermostat::process_CRFMonitor(std::shared_ptr telegram) { has_bitupdate(telegram, hc->mode, 2, 4); // bit 4, mode (auto=0, off=1) has_update(telegram, hc->selTemp, 6, 1); // is * 2, force as single byte has_update(telegram, hc->targetflowtemp, 4); - - add_ha_climate(hc); } // type 0x02A5 - data from the Nefit RC1010/3000 thermostat (0x18) and RC300/310s on 0x10 @@ -1026,8 +868,6 @@ void Thermostat::process_RC300Monitor(std::shared_ptr telegram) has_bitupdate(telegram, hc->summermode, 2, 4); has_update(telegram, hc->targetflowtemp, 4); has_update(telegram, hc->curroominfl, 27); - - add_ha_climate(hc); } // type 0x02B9 EMS+ for reading from RC300/RC310 thermostat @@ -1182,8 +1022,6 @@ void Thermostat::process_RC30Monitor(std::shared_ptr telegram) { has_update(telegram, hc->selTemp, 1, 1); // is * 2, force as single byte has_update(telegram, hc->roomTemp, 2); - - add_ha_climate(hc); } // type 0xA7 - for reading the mode from the RC30 thermostat (0x10) @@ -1193,8 +1031,6 @@ void Thermostat::process_RC30Set(std::shared_ptr telegram) { return; } has_update(telegram, hc->mode, 23); - - add_ha_climate(hc); } // type 0x3E (HC1), 0x48 (HC2), 0x52 (HC3), 0x5C (HC4) - data from the RC35 thermostat (0x10) - 16 bytes @@ -1216,23 +1052,13 @@ void Thermostat::process_RC35Monitor(std::shared_ptr telegram) { } has_update(telegram, hc->selTemp, 2, 1); // is * 2, force to single byte, is 0 in summermode - int16_t roomTemp = hc->roomTemp; - has_update(telegram, hc->roomTemp, 3); // is * 10 - or 0x7D00 if thermostat is mounted on boiler - // publish zero if there was a roomtemp, but now is missing - if (Helpers::hasValue(roomTemp) && !Helpers::hasValue(hc->roomTemp)) { - roomTemp = hc->roomTemp; - hc->roomTemp = 0; - has_update(hc->roomTemp); - hc->roomTemp = roomTemp; - } + has_update(telegram, hc->roomTemp, 3); // is * 10 - or 0x7D00 if thermostat is mounted on boiler has_bitupdate(telegram, hc->modetype, 1, 1); has_bitupdate(telegram, hc->summermode, 1, 0); has_bitupdate(telegram, hc->holidaymode, 0, 5); has_update(telegram, hc->targetflowtemp, 14); - - add_ha_climate(hc); } // type 0x3D (HC1), 0x47 (HC2), 0x51 (HC3), 0x5B (HC4) - Working Mode Heating - for reading the mode from the RC35 thermostat (0x10) @@ -1273,8 +1099,6 @@ void Thermostat::process_RC35Set(std::shared_ptr telegram) { has_update(telegram, hc->designtemp, 17); // is * 1 has_update(telegram, hc->maxflowtemp, 15); // is * 1 } - - add_ha_climate(hc); } // type 0x3F (HC1), 0x49 (HC2), 0x53 (HC3), 0x5D (HC4) - timer setting @@ -1512,7 +1336,7 @@ bool Thermostat::set_remotetemp(const char * value, const int8_t id) { } Roomctrl::set_remotetemp(hc->hc(), hc->remotetemp); - // has_update(hc->remotetemp); + has_update(hc->remotetemp); return true; } @@ -3423,6 +3247,7 @@ void Thermostat::register_device_values_hc(std::shared_ptrheatingtype, DeviceValueType::ENUM, FL_(enum_heatingtype), FL_(heatingtype), DeviceValueUOM::NONE, MAKE_CF_CB(set_heatingtype)); register_device_value(tag, &hc->summertemp, DeviceValueType::UINT, nullptr, FL_(summertemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_summertemp)); register_device_value(tag, &hc->summermode, DeviceValueType::ENUM, FL_(enum_summer), FL_(summermode), DeviceValueUOM::NONE); + register_device_value(tag, &hc->remotetemp, DeviceValueType::SHORT, FL_(div10), FL_(remotetemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_remotetemp)); break; case EMS_DEVICE_FLAG_RC25: register_device_value(tag, &hc->mode, DeviceValueType::ENUM, FL_(enum_mode3), FL_(mode), DeviceValueUOM::NONE, MAKE_CF_CB(set_mode)); @@ -3488,6 +3313,7 @@ void Thermostat::register_device_values_hc(std::shared_ptrnofrosttemp, DeviceValueType::INT, FL_(div2), FL_(nofrosttemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_nofrosttemp)); register_device_value(tag, &hc->control, DeviceValueType::ENUM, FL_(enum_j_control), FL_(control), DeviceValueUOM::NONE, MAKE_CF_CB(set_control)); register_device_value(tag, &hc->program, DeviceValueType::ENUM, FL_(enum_progMode4), FL_(program), DeviceValueUOM::NONE, MAKE_CF_CB(set_program)); + register_device_value(tag, &hc->remotetemp, DeviceValueType::SHORT, FL_(div10), FL_(remotetemp), DeviceValueUOM::DEGREES); break; } } diff --git a/src/devices/thermostat.h b/src/devices/thermostat.h index dc2d20771..8f1f77b7b 100644 --- a/src/devices/thermostat.h +++ b/src/devices/thermostat.h @@ -96,14 +96,6 @@ class Thermostat : public EMSdevice { return Helpers::hasValue(selTemp); } - bool ha_climate_created() { - return ha_climate_created_; - } - - void ha_climate_created(bool ha_climate_created) { - ha_climate_created_ = ha_climate_created; - } - uint8_t get_mode() const; uint8_t get_mode_type() const; @@ -142,7 +134,6 @@ class Thermostat : public EMSdevice { private: uint8_t hc_num_; // heating circuit number 1..10 uint8_t model_; // the model type - bool ha_climate_created_; // if we need to create the HA climate control }; static std::string mode_tostring(uint8_t mode); @@ -298,12 +289,8 @@ class Thermostat : public EMSdevice { std::shared_ptr heating_circuit(std::shared_ptr telegram); std::shared_ptr heating_circuit(const uint8_t hc_num); - void publish_ha_config_hc(std::shared_ptr hc); void register_device_values_hc(std::shared_ptr hc); - bool thermostat_ha_cmd(const char * message, uint8_t hc_num); - void add_ha_climate(std::shared_ptr hc); - void process_RCOutdoorTemp(std::shared_ptr telegram); void process_IBASettings(std::shared_ptr telegram); void process_RCTime(std::shared_ptr telegram); diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index 8d4cd336d..020eef77e 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -1229,6 +1229,11 @@ bool EMSdevice::generate_values(JsonObject & output, const uint8_t tag_filter, c // 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.short_name == FL_(roomTemp)[0]) && (!dv.has_state(DeviceValueState::DV_VISIBLE)) + && (dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED) || dv.has_state(DeviceValueState::DV_HA_CLIMATE_NO_RT))) { + Mqtt::publish_ha_climate_config(dv.tag, false, true); // delete topic (remove = true) + dv.remove_state(DeviceValueState::DV_HA_CLIMATE_NO_RT); + } if (dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED) && ((!dv.has_state(DeviceValueState::DV_VISIBLE)) || (!dv.has_state(DeviceValueState::DV_ACTIVE)))) { Mqtt::publish_ha_sensor_config(dv, "", "", true); // delete topic (remove = true) @@ -1243,9 +1248,20 @@ 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.has_state(DeviceValueState::DV_HA_CONFIG_CREATED) && dv.type != DeviceValueType::CMD) && dv.has_state(DeviceValueState::DV_ACTIVE) + if ((dv.short_name == FL_(roomTemp)[0]) && dv.has_state(DeviceValueState::DV_VISIBLE)) { + if (dv.has_state(DeviceValueState::DV_ACTIVE) + && (dv.has_state(DeviceValueState::DV_HA_CLIMATE_NO_RT) || !dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED))) { + dv.remove_state(DeviceValueState::DV_HA_CLIMATE_NO_RT); + Mqtt::publish_ha_climate_config(dv.tag, true); + } else if (!dv.has_state(DeviceValueState::DV_ACTIVE) && !dv.has_state(DeviceValueState::DV_HA_CLIMATE_NO_RT)) { + dv.add_state(DeviceValueState::DV_HA_CLIMATE_NO_RT); + 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_VISIBLE)) { // 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); diff --git a/src/emsdevicevalue.h b/src/emsdevicevalue.h index c6bf629b6..e41533fd4 100644 --- a/src/emsdevicevalue.h +++ b/src/emsdevicevalue.h @@ -113,7 +113,8 @@ class DeviceValue { DV_DEFAULT = 0, // 0 - does not yet have a value DV_ACTIVE = (1 << 0), // 1 - has a validated real value DV_VISIBLE = (1 << 1), // 2 - shown on web, console and on MQTT payload. Otherwise hidden - DV_HA_CONFIG_CREATED = (1 << 2) // 4 - set if the HA config topic has been created + DV_HA_CONFIG_CREATED = (1 << 2), // 4 - set if the HA config topic has been created + DV_HA_CLIMATE_NO_RT = (1 << 3) // 8 - climate created without roomTemp }; uint8_t device_type; // EMSdevice::DeviceType diff --git a/src/mqtt.cpp b/src/mqtt.cpp index 3f642dcd3..6e1cd21ee 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -49,6 +49,7 @@ std::vector Mqtt::mqtt_subfunctions_; uint32_t Mqtt::mqtt_publish_fails_ = 0; bool Mqtt::connecting_ = false; bool Mqtt::initialized_ = false; +bool Mqtt::ha_climate_reset_ = false; uint8_t Mqtt::connectcount_ = 0; uint32_t Mqtt::mqtt_message_id_ = 0; char will_topic_[Mqtt::MQTT_TOPIC_MAX_SIZE]; // because MQTT library keeps only char pointer @@ -581,6 +582,7 @@ void Mqtt::on_connect() { queue_unsubscribe_message(discovery_prefix_ + "/switch/" + mqtt_base_ + "/#"); EMSESP::reset_mqtt_ha(); // re-create all HA devices if there are any ha_status(); // create the EMS-ESP device in HA, which is MQTT retained + ha_climate_reset(true); } else { queue_subscribe_message(discovery_prefix_ + "/climate/" + mqtt_base_ + "/#"); queue_subscribe_message(discovery_prefix_ + "/sensor/" + mqtt_base_ + "/#"); @@ -1220,6 +1222,94 @@ void Mqtt::publish_ha_sensor_config(uint8_t type, publish_ha(topic, doc.as()); } +void Mqtt::publish_ha_climate_config(uint8_t tag, bool has_roomtemp, bool remove) { + uint8_t hc_num = tag - DeviceValueTAG::TAG_HC1 + 1; + + char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; + char topic_t[Mqtt::MQTT_TOPIC_MAX_SIZE]; + char hc_mode_s[30]; + char seltemp_s[30]; + char currtemp_s[30]; + char mode_str_tpl[400]; + char name_s[30]; + char uniq_id_s[30]; + char temp_cmd_s[30]; + char mode_cmd_s[30]; + char min_s[10]; + char max_s[10]; + + snprintf(topic, sizeof(topic), "climate/%s/thermostat_hc%d/config", Mqtt::base().c_str(), hc_num); + if (remove) { + publish_ha(topic); // publish empty payload with retain flag + return; + } + + if (Mqtt::is_nested()) { + // nested format + snprintf(hc_mode_s, sizeof(hc_mode_s), "value_json.hc%d.mode", hc_num); + snprintf(seltemp_s, sizeof(seltemp_s), "{{value_json.hc%d.seltemp}}", hc_num); + if (has_roomtemp) { + snprintf(currtemp_s, sizeof(currtemp_s), "{{value_json.hc%d.currtemp}}", hc_num); + } + snprintf(topic_t, sizeof(topic_t), "~/%s", Mqtt::tag_to_topic(EMSdevice::DeviceType::THERMOSTAT, DeviceValueTAG::TAG_NONE).c_str()); + } else { + // single format + snprintf(hc_mode_s, sizeof(hc_mode_s), "value_json.mode"); + snprintf(seltemp_s, sizeof(seltemp_s), "{{value_json.seltemp}}"); + if (has_roomtemp) { + snprintf(currtemp_s, sizeof(currtemp_s), "{{value_json.currtemp}}"); + } + snprintf(topic_t, sizeof(topic_t), "~/%s", Mqtt::tag_to_topic(EMSdevice::DeviceType::THERMOSTAT, DeviceValueTAG::TAG_HC1 + hc_num - 1).c_str()); + } + + snprintf(mode_str_tpl, + sizeof(mode_str_tpl), + "{%%if %s=='manual'%%}heat{%%elif %s=='day'%%}heat{%%elif %s=='night'%%}off{%%elif %s=='off'%%}off{%%else%%}auto{%%endif%%}", + hc_mode_s, + hc_mode_s, + hc_mode_s, + hc_mode_s); + + snprintf(name_s, sizeof(name_s), "Thermostat hc%d", hc_num); + snprintf(uniq_id_s, sizeof(uniq_id_s), "thermostat_hc%d", hc_num); + snprintf(temp_cmd_s, sizeof(temp_cmd_s), "~/thermostat/hc%d/seltemp", hc_num); + snprintf(mode_cmd_s, sizeof(temp_cmd_s), "~/thermostat/hc%d/mode", hc_num); + + StaticJsonDocument doc; + + doc["~"] = base(); + doc["name"] = name_s; + doc["uniq_id"] = uniq_id_s; + doc["mode_stat_t"] = topic_t; + doc["mode_stat_tpl"] = mode_str_tpl; + doc["temp_cmd_t"] = temp_cmd_s; + doc["temp_stat_t"] = topic_t; + doc["temp_stat_tpl"] = seltemp_s; + doc["mode_cmd_t"] = mode_cmd_s; + + if (has_roomtemp) { + doc["curr_temp_t"] = topic_t; + doc["curr_temp_tpl"] = currtemp_s; + } + + doc["min_temp"] = Helpers::render_value(min_s, 5, 0, EMSESP::system_.fahrenheit() ? 2 : 0); + doc["max_temp"] = Helpers::render_value(max_s, 30, 0, EMSESP::system_.fahrenheit() ? 2 : 0); + doc["temp_step"] = "0.5"; + + // the HA climate component only responds to auto, heat and off + JsonArray modes = doc.createNestedArray("modes"); + + modes.add("auto"); + modes.add("heat"); + modes.add("off"); + + JsonObject dev = doc.createNestedObject("dev"); + JsonArray ids = dev.createNestedArray("ids"); + ids.add("ems-esp-thermostat"); + + publish_ha(topic, doc.as()); // publish the config payload with retain flag +} + // based on the device and tag, create the MQTT topic name (without the basename) // differs based on whether MQTT nested is enabled // tag = EMSdevice::DeviceValueTAG diff --git a/src/mqtt.h b/src/mqtt.h index 9fe8442f8..413d28fd1 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -107,6 +107,7 @@ class Mqtt { const JsonObject & dev_json); static void publish_system_ha_sensor_config(uint8_t type, const __FlashStringHelper * name, const __FlashStringHelper * entity, const uint8_t uom); + static void publish_ha_climate_config(uint8_t tag, bool has_roomtemp, 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); @@ -191,6 +192,14 @@ class Mqtt { ha_enabled_ = ha_enabled; } + static bool ha_climate_reset() { + return ha_climate_reset_; + } + + static void ha_climate_reset(bool reset) { + ha_climate_reset_ = reset;; + } + static bool send_response() { return send_response_; } @@ -276,6 +285,7 @@ class Mqtt { static bool initialized_; static uint32_t mqtt_publish_fails_; static uint8_t connectcount_; + static bool ha_climate_reset_; // settings, copied over static std::string mqtt_base_; From 9c15ddf952cf4e1c412c36a7650b7ce3d28b314c Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Sat, 26 Feb 2022 14:23:45 +0100 Subject: [PATCH 2/2] HA climate as extra value, can be disabled --- src/devices/thermostat.cpp | 32 ++++++++++++++++++++++++++++++-- src/devices/thermostat.h | 3 +++ src/emsdevice.cpp | 15 ++++++--------- src/locale_DE.h | 2 ++ src/locale_EN.h | 2 ++ 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index d126638e2..3d4ea2eb2 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -369,6 +369,15 @@ std::shared_ptr Thermostat::heating_circuit(std::sha return new_hc; // return back point to new HC object } +// add the HVAC/Climate HA component for the HC +void Thermostat::add_ha_climate(std::shared_ptr hc) { + if (!Mqtt::ha_enabled()) { + hc->climate = EMS_VALUE_UINT_NOTSET; + return; + } + hc->climate = Helpers::hasValue(hc->roomTemp) ? 1 : Helpers::hasValue(hc->selTemp) ? 0 : EMS_VALUE_UINT_NOTSET; +} + // decodes the thermostat mode for the heating circuit based on the thermostat type // modes are off, manual, auto, day, night and holiday uint8_t Thermostat::HeatingCircuit::get_mode() const { @@ -546,6 +555,8 @@ void Thermostat::process_RC10Monitor(std::shared_ptr telegram) { has_update(telegram, hc->selTemp, 1, 1); // is * 2, force as single byte has_update(telegram, hc->roomTemp, 2); // is * 10 has_update(telegram, hc->reduceminutes, 5); + + add_ha_climate(hc); } // type 0xB0 - for reading the mode from the RC10 thermostat (0x17) @@ -649,6 +660,8 @@ void Thermostat::process_RC20Monitor_2(std::shared_ptr telegram) has_update(telegram, hc->selTemp, 2, 1); // is * 2, force as single byte has_update(telegram, hc->roomTemp, 3); // is * 10 has_bitupdate(telegram, hc->summermode, 1, 0); + + add_ha_climate(hc); } // 0xAD - for reading the mode from the RC20/ES72 thermostat (0x17) @@ -740,6 +753,8 @@ void Thermostat::process_RC20Monitor(std::shared_ptr telegram) { has_update(telegram, hc->selTemp, 1, 1); // is * 2, force as single byte has_update(telegram, hc->roomTemp, 2); // is * 10 + + add_ha_climate(hc); } // type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long @@ -751,6 +766,8 @@ void Thermostat::process_EasyMonitor(std::shared_ptr telegram) { has_update(telegram, hc->roomTemp, 8); // is * 100 has_update(telegram, hc->selTemp, 10); // is * 100 + + add_ha_climate(hc); } // Settings Parameters - 0xA5 - RC30_1 @@ -830,7 +847,9 @@ void Thermostat::process_JunkersMonitor(std::shared_ptr telegram } else { has_update(telegram, hc->roomTemp, 4); // value is * 10 } - } + + add_ha_climate(hc); +} // type 0x02A5 - data from Worchester CRF200 void Thermostat::process_CRFMonitor(std::shared_ptr telegram) { @@ -844,6 +863,8 @@ void Thermostat::process_CRFMonitor(std::shared_ptr telegram) { has_bitupdate(telegram, hc->mode, 2, 4); // bit 4, mode (auto=0, off=1) has_update(telegram, hc->selTemp, 6, 1); // is * 2, force as single byte has_update(telegram, hc->targetflowtemp, 4); + + add_ha_climate(hc); } // type 0x02A5 - data from the Nefit RC1010/3000 thermostat (0x18) and RC300/310s on 0x10 @@ -868,6 +889,8 @@ void Thermostat::process_RC300Monitor(std::shared_ptr telegram) has_bitupdate(telegram, hc->summermode, 2, 4); has_update(telegram, hc->targetflowtemp, 4); has_update(telegram, hc->curroominfl, 27); + + add_ha_climate(hc); } // type 0x02B9 EMS+ for reading from RC300/RC310 thermostat @@ -1022,6 +1045,8 @@ void Thermostat::process_RC30Monitor(std::shared_ptr telegram) { has_update(telegram, hc->selTemp, 1, 1); // is * 2, force as single byte has_update(telegram, hc->roomTemp, 2); + + add_ha_climate(hc); } // type 0xA7 - for reading the mode from the RC30 thermostat (0x10) @@ -1059,6 +1084,8 @@ void Thermostat::process_RC35Monitor(std::shared_ptr telegram) { has_bitupdate(telegram, hc->holidaymode, 0, 5); has_update(telegram, hc->targetflowtemp, 14); + + add_ha_climate(hc); } // type 0x3D (HC1), 0x47 (HC2), 0x51 (HC3), 0x5B (HC4) - Working Mode Heating - for reading the mode from the RC35 thermostat (0x10) @@ -3181,6 +3208,7 @@ void Thermostat::register_device_values_hc(std::shared_ptrclimate, DeviceValueType::ENUM, FL_(enum_climate), FL_(climate), DeviceValueUOM::NONE); switch (model) { case EMS_DEVICE_FLAG_RC10: @@ -3247,7 +3275,7 @@ void Thermostat::register_device_values_hc(std::shared_ptrheatingtype, DeviceValueType::ENUM, FL_(enum_heatingtype), FL_(heatingtype), DeviceValueUOM::NONE, MAKE_CF_CB(set_heatingtype)); register_device_value(tag, &hc->summertemp, DeviceValueType::UINT, nullptr, FL_(summertemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_summertemp)); register_device_value(tag, &hc->summermode, DeviceValueType::ENUM, FL_(enum_summer), FL_(summermode), DeviceValueUOM::NONE); - register_device_value(tag, &hc->remotetemp, DeviceValueType::SHORT, FL_(div10), FL_(remotetemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_remotetemp)); + register_device_value(tag, &hc->remotetemp, DeviceValueType::SHORT, FL_(div10), FL_(remotetemp), DeviceValueUOM::DEGREES); break; case EMS_DEVICE_FLAG_RC25: register_device_value(tag, &hc->mode, DeviceValueType::ENUM, FL_(enum_mode3), FL_(mode), DeviceValueUOM::NONE, MAKE_CF_CB(set_mode)); diff --git a/src/devices/thermostat.h b/src/devices/thermostat.h index 8f1f77b7b..d9f8969a3 100644 --- a/src/devices/thermostat.h +++ b/src/devices/thermostat.h @@ -74,6 +74,7 @@ class Thermostat : public EMSdevice { char vacation[26]; char switchtime1[16]; char switchtime2[16]; + uint8_t climate; // RC 10 uint8_t reducehours; // night reduce duration @@ -291,6 +292,8 @@ class Thermostat : public EMSdevice { void register_device_values_hc(std::shared_ptr hc); + void add_ha_climate(std::shared_ptr hc); + void process_RCOutdoorTemp(std::shared_ptr telegram); void process_IBASettings(std::shared_ptr telegram); void process_RCTime(std::shared_ptr telegram); diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index 020eef77e..9d3183772 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -1229,15 +1229,13 @@ bool EMSdevice::generate_values(JsonObject & output, const uint8_t tag_filter, c // 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.short_name == FL_(roomTemp)[0]) && (!dv.has_state(DeviceValueState::DV_VISIBLE)) - && (dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED) || dv.has_state(DeviceValueState::DV_HA_CLIMATE_NO_RT))) { - Mqtt::publish_ha_climate_config(dv.tag, false, true); // delete topic (remove = true) - dv.remove_state(DeviceValueState::DV_HA_CLIMATE_NO_RT); - } if (dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED) && ((!dv.has_state(DeviceValueState::DV_VISIBLE)) || (!dv.has_state(DeviceValueState::DV_ACTIVE)))) { Mqtt::publish_ha_sensor_config(dv, "", "", true); // delete topic (remove = true) 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) + } } } } @@ -1251,12 +1249,11 @@ void EMSdevice::mqtt_ha_entity_config_create() { // 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_(roomTemp)[0]) && dv.has_state(DeviceValueState::DV_VISIBLE)) { - if (dv.has_state(DeviceValueState::DV_ACTIVE) - && (dv.has_state(DeviceValueState::DV_HA_CLIMATE_NO_RT) || !dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED))) { + if ((dv.short_name == FL_(climate)[0]) && dv.has_state(DeviceValueState::DV_VISIBLE) && 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); Mqtt::publish_ha_climate_config(dv.tag, true); - } else if (!dv.has_state(DeviceValueState::DV_ACTIVE) && !dv.has_state(DeviceValueState::DV_HA_CLIMATE_NO_RT)) { + } 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); Mqtt::publish_ha_climate_config(dv.tag, false); } diff --git a/src/locale_DE.h b/src/locale_DE.h index c57701bfd..633c00a0b 100644 --- a/src/locale_DE.h +++ b/src/locale_DE.h @@ -387,6 +387,7 @@ MAKE_PSTR_LIST(enum_progMode3, F("Familie"), F("Morgends"), F("Abends"), F("Vorm MAKE_PSTR_LIST(enum_progMode4, F("prog_a"), F("prog_b"), F("prog_c"), F("prog_d"), F("prog_e"), F("prog_f")) MAKE_PSTR_LIST(enum_switchmode, F_(off), F_(eco), F_(comfort), F_(heat)) +MAKE_PSTR_LIST(enum_climate, F("Solltemperature"), F("Raumtemperatur")) // solar list MAKE_PSTR_LIST(enum_solarmode, F_(constant), F("pwm"), F("analog")) @@ -581,6 +582,7 @@ MAKE_PSTR_LIST(wwExtra2, F("wwextra2"), F("Kreis 2 Extra")) MAKE_PSTR_LIST(wwDailyHeating, F("wwdailyheating"), F("daily heating")) MAKE_PSTR_LIST(wwDailyHeatTime, F("wwdailyheattime"), F("daily heating time")) // thermostat hc +MAKE_PSTR_LIST(climate, F("climate"), F("Klima Anzeige")) MAKE_PSTR_LIST(selRoomTemp, F("seltemp"), F("Sollwert Raumtemperatur")) MAKE_PSTR_LIST(roomTemp, F("currtemp"), F("aktuelle Raumtemperatur")) MAKE_PSTR_LIST(mode, F("mode"), F("modus")) diff --git a/src/locale_EN.h b/src/locale_EN.h index e6908a12c..6146c4a83 100644 --- a/src/locale_EN.h +++ b/src/locale_EN.h @@ -387,6 +387,7 @@ MAKE_PSTR_LIST(enum_progMode3, F("family"), F("morning"), F("evening"), F("am"), MAKE_PSTR_LIST(enum_progMode4, F("prog_a"), F("prog_b"), F("prog_c"), F("prog_d"), F("prog_e"), F("prog_f")) MAKE_PSTR_LIST(enum_switchmode, F_(off), F_(eco), F_(comfort), F_(heat)) +MAKE_PSTR_LIST(enum_climate, F("selTemp"), F("roomTemp")) // solar list MAKE_PSTR_LIST(enum_solarmode, F_(constant), F("pwm"), F("analog")) @@ -582,6 +583,7 @@ MAKE_PSTR_LIST(wwExtra2, F("wwextra2"), F("circuit 2 extra")) MAKE_PSTR_LIST(wwDailyHeating, F("wwdailyheating"), F("daily heating")) MAKE_PSTR_LIST(wwDailyHeatTime, F("wwdailyheattime"), F("daily heating time")) // thermostat hc +MAKE_PSTR_LIST(climate, F("climate"), F("climate entity")) MAKE_PSTR_LIST(selRoomTemp, F("seltemp"), F("selected room temperature")) MAKE_PSTR_LIST(roomTemp, F("currtemp"), F("current room temperature")) MAKE_PSTR_LIST(mode, F("mode"), F("mode"))