MQTT updates: added HA discovery, removed heartbeat - HomeAssistant Discovery #288

This commit is contained in:
proddy
2020-09-28 18:17:23 +02:00
parent e7d069fdb7
commit b5062df8f4
26 changed files with 503 additions and 318 deletions

View File

@@ -75,34 +75,30 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
return command_info(value, id, object);
});
EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) {
mqtt_format_ = settings.mqtt_format; // single, nested or ha
if (mqtt_format_ == MQTT_format::HA) {
register_mqtt_ha_config();
}
});
if (Mqtt::mqtt_format() == Mqtt::Format::HA) {
register_mqtt_ha_config();
}
}
// create the config topic for Home Assistant MQTT Discovery
// homeassistant/sensor/ems-esp/boiler
// state is /state
// config is /config
// create the config topics for Home Assistant MQTT Discovery
// for each of the main elements
void Boiler::register_mqtt_ha_config() {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
Mqtt::register_mqtt_ha_binary_sensor(F("Boiler DHW"), "tapwater_active");
Mqtt::register_mqtt_ha_binary_sensor(F("Boiler Heating"), "heating_active");
/*
* not finished yet - see https://github.com/proddy/EMS-ESP/issues/288
doc["name"] = "boiler";
doc["uniq_id"] = "boiler";
// Mqtt::publish(topic); // empty payload, this remove any previous config sent to HA
Mqtt::publish("homeassistant/sensor/ems-esp/boiler/config", doc, true); // publish the config payload with retain flag
*/
Mqtt::register_mqtt_ha_sensor(F("Service Code"), this->device_type(), "serviceCode", "", "");
Mqtt::register_mqtt_ha_sensor(F("Service Code number"), this->device_type(), "serviceCodeNumber", "", "");
Mqtt::register_mqtt_ha_sensor(F("Boiler WW Selected Temp"), this->device_type(), "wWSelTemp", "°C", "mdi:coolant-temperature");
Mqtt::register_mqtt_ha_sensor(F("Selected flow temperature"), this->device_type(), "selFlowTemp", "°C", "mdi:coolant-temperature");
Mqtt::register_mqtt_ha_sensor(F("Current flow temperature"), this->device_type(), "curFlowTemp", "°C", "mdi:coolant-temperature");
Mqtt::register_mqtt_ha_sensor(F("Warm Water set temperature"), this->device_type(), "wWSetTemp", "°C", "mdi:coolant-temperature");
Mqtt::register_mqtt_ha_sensor(F("Warm Water current temperature (intern)"), this->device_type(), "wWCurTmp", "°C", "mdi:coolant-temperature");
Mqtt::register_mqtt_ha_sensor(F("Warm Water current temperature (extern)"), this->device_type(), "wWCurTmp2", "°C", "mdi:coolant-temperature");
Mqtt::register_mqtt_ha_sensor(F("Pump modulation"), this->device_type(), "pumpMod", "%", "mdi:sine-wave");
Mqtt::register_mqtt_ha_sensor(F("Heat Pump modulation"), this->device_type(), "pumpMod2", "%", "mdi:sine-wave");
}
// send stuff to the Web UI
void Boiler::device_info_web(JsonArray & root) {
JsonObject dataElement;
@@ -341,13 +337,14 @@ bool Boiler::export_values(JsonObject & output) {
// publish values via MQTT
void Boiler::publish_values() {
// const size_t capacity = JSON_OBJECT_SIZE(56); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/
// DynamicJsonDocument doc(capacity);
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_LARGE> doc;
JsonObject output = doc.to<JsonObject>();
if (export_values(output)) {
Mqtt::publish(F("boiler_data"), doc.as<JsonObject>());
}
// send out heating and tapwater status - even if there is no change (force = true)
check_active();
}
// called after a process command is called, to check values and see if we need to force an MQTT publish
@@ -491,6 +488,19 @@ void Boiler::show_values(uuid::console::Shell & shell) {
* If a value has changed, post it immediately to MQTT so we get real time data
*/
void Boiler::check_active() {
if ((boilerState_ & 0x09) != (last_boilerState & 0x09)) {
char s[5];
Mqtt::publish(F("heating_active"), Helpers::render_boolean(s, ((boilerState_ & 0x09) == 0x09)));
}
if ((boilerState_ & 0x0A) != (last_boilerState & 0x0A)) {
char s[5];
Mqtt::publish(F("tapwater_active"), Helpers::render_boolean(s, ((boilerState_ & 0x0A) == 0x0A)));
EMSESP::tap_water_active((boilerState_ & 0x0A) == 0x0A);
}
last_boilerState = boilerState_;
/*
// hot tap water, using flow to check instead of the burner power
// send these values back to the main EMSESP, so other classes (e.g. Shower) can use it
if (Helpers::hasValue(wWCurFlow_) && Helpers::hasValue(burnGas_) && (wWType_ > 0) && (wWType_ < 3)) {
@@ -500,20 +510,21 @@ void Boiler::check_active() {
// heating
// using a quick hack for checking the heating by looking at the Selected Flow Temp, but doesn't work for all boilers apparently
if (Helpers::hasValue(tap_water_active_, EMS_VALUE_BOOL) && Helpers::hasValue(selFlowTemp_) && Helpers::hasValue(burnGas_)) {
if (Helpers::hasValue(selFlowTemp_) && Helpers::hasValue(burnGas_)) {
heating_active_ = (!tap_water_active_ && ((selFlowTemp_ >= EMS_BOILER_SELFLOWTEMP_HEATING) && (burnGas_ != EMS_VALUE_BOOL_OFF)));
}
// see if the heating or hot tap water has changed, if so send
// last_boilerActive stores heating in bit 1 and tap water in bit 2
if (Helpers::hasValue(tap_water_active_, EMS_VALUE_BOOL) && Helpers::hasValue(heating_active_, EMS_VALUE_BOOL)) {
uint8_t latest_boilerState = (tap_water_active_ << 1) + heating_active_;
if (latest_boilerState != last_boilerState) {
last_boilerState = latest_boilerState;
Mqtt::publish(F("tapwater_active"), tap_water_active_);
Mqtt::publish(F("heating_active"), heating_active_);
}
uint8_t latest_boilerState = (tap_water_active_ << 1) + heating_active_;
if (latest_boilerState != last_boilerState) {
last_boilerState = latest_boilerState;
static char s[10];
Mqtt::publish(F("tapwater_active"), Helpers::render_boolean(s, tap_water_active_));
static char s2[10];
Mqtt::publish(F("heating_active"), Helpers::render_boolean(s2, heating_active_));
}
*/
}
// 0x33
@@ -533,6 +544,7 @@ void Boiler::process_UBAMonitorFast(std::shared_ptr<const Telegram> telegram) {
changed_ |= telegram->read_value(curFlowTemp_, 1);
changed_ |= telegram->read_value(selBurnPow_, 3); // burn power max setting
changed_ |= telegram->read_value(curBurnPow_, 4);
changed_ |= telegram->read_value(boilerState_, 5);
changed_ |= telegram->read_bitvalue(burnGas_, 7, 0);
changed_ |= telegram->read_bitvalue(fanWork_, 7, 2);

View File

@@ -47,12 +47,13 @@ class Boiler : public EMSdevice {
static uuid::log::Logger logger_;
void register_mqtt_ha_config();
void register_mqtt_ha_binary_sensor(const __FlashStringHelper * name, const char * entity);
void register_mqtt_ha_sensor(const __FlashStringHelper * name, const char * entity, const char * uom, const char * icon);
void check_active();
bool export_values(JsonObject & doc);
uint8_t last_boilerState = 0xFF; // remember last state of heating and warm water on/off
uint8_t mqtt_format_; // single, nested or ha
bool changed_ = false;
bool changed_ = false;
static constexpr uint8_t EMS_TYPE_UBAParameterWW = 0x33;
static constexpr uint8_t EMS_TYPE_UBAFunctionTest = 0x1D;
@@ -95,6 +96,7 @@ class Boiler : public EMSdevice {
uint8_t sysPress_ = EMS_VALUE_UINT_NOTSET; // System pressure
char serviceCodeChar_[3] = {'\0'}; // 2 character status/service code
uint16_t serviceCode_ = EMS_VALUE_USHORT_NOTSET; // error/service code
uint8_t boilerState_ = EMS_VALUE_BOOL_NOTSET; // State flag, used on HT3
// UBAMonitorSlow - 0x19 on EMS1
int16_t extTemp_ = EMS_VALUE_SHORT_NOTSET; // Outside temperature
@@ -141,8 +143,8 @@ class Boiler : public EMSdevice {
uint8_t setWWPumpPow_ = EMS_VALUE_UINT_NOTSET; // ww pump speed/power?
// other internal calculated params
uint8_t tap_water_active_ = EMS_VALUE_BOOL_NOTSET; // Hot tap water is on/off
uint8_t heating_active_ = EMS_VALUE_BOOL_NOTSET; // Central heating is on/off
bool tap_water_active_ = false; // Hot tap water is on/off
bool heating_active_ = false; // Central heating is on/off
uint8_t pumpMod2_ = EMS_VALUE_UINT_NOTSET; // heatpump modulation from 0xE3 (heatpumps)
bool command_info(const char * value, const int8_t id, JsonObject & output);
@@ -187,4 +189,4 @@ class Boiler : public EMSdevice {
} // namespace emsesp
#endif
#endif

View File

@@ -62,16 +62,17 @@ Mixing::Mixing(uint8_t device_type, uint8_t device_id, uint8_t product_id, const
// output json to web UI
void Mixing::device_info_web(JsonArray & root) {
if (type_ == Type::NONE) {
if (type() == Type::NONE) {
return; // don't have any values yet
}
if (type_ == Type::WWC) {
if (type() == Type::WWC) {
render_value_json(root, "", F("Warm Water Circuit"), hc_, nullptr);
render_value_json(root, "", F("Current warm water temperature"), flowTemp_, F_(degrees), 10);
render_value_json(root, "", F("Current pump status"), pump_, nullptr);
render_value_json(root, "", F("Current pump status"), pump_, nullptr, EMS_VALUE_BOOL);
render_value_json(root, "", F("Current temperature status"), status_, nullptr);
} else {
// HC
render_value_json(root, "", F("Heating Circuit"), hc_, nullptr);
render_value_json(root, "", F("Current flow temperature"), flowTemp_, F_(degrees), 10);
render_value_json(root, "", F("Setpoint flow temperature"), flowSetTemp_, F_(degrees));
@@ -93,14 +94,14 @@ bool Mixing::updated_values() {
void Mixing::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // always call this to show header
if (type_ == Type::NONE) {
if (type() == Type::NONE) {
return; // don't have any values yet
}
if (type_ == Type::WWC) {
if (type() == Type::WWC) {
print_value(shell, 2, F("Warm Water Circuit"), hc_, nullptr);
print_value(shell, 4, F("Current warm water temperature"), flowTemp_, F_(degrees), 10);
print_value(shell, 4, F("Current pump status"), pump_, nullptr);
print_value(shell, 4, F("Current pump status"), pump_, nullptr, EMS_VALUE_BOOL);
print_value(shell, 4, F("Current temperature status"), status_, nullptr);
} else {
print_value(shell, 2, F("Heating Circuit"), hc_, nullptr);
@@ -110,7 +111,6 @@ void Mixing::show_values(uuid::console::Shell & shell) {
print_value(shell, 4, F("Current valve status"), status_, F_(percent));
}
shell.println();
}
@@ -129,13 +129,34 @@ void Mixing::publish_values() {
strlcpy(topic, "mixing_data", 30);
strlcat(topic, Helpers::itoa(s, device_id() - 0x20 + 1), 30); // append hc to topic
Mqtt::publish(topic, doc.as<JsonObject>());
// if we're using Home Assistant and haven't created the MQTT Discovery topics, do it now
if ((Mqtt::mqtt_format() == Mqtt::Format::HA) && (!ha_created_)) {
register_mqtt_ha_config();
}
}
}
// publish config topic for HA MQTT Discovery
void Mixing::register_mqtt_ha_config() {
if (this->type() == Type::HC) {
Mqtt::register_mqtt_ha_sensor(F("Current flow temperature"), this->device_type(), "flowTemp", "°C", "");
Mqtt::register_mqtt_ha_sensor(F("Setpoint flow temperature"), this->device_type(), "flowSetTemp", "°C", "");
Mqtt::register_mqtt_ha_sensor(F("Current pump status"), this->device_type(), "pumpStatus", "", "");
Mqtt::register_mqtt_ha_sensor(F("Current valve status"), this->device_type(), "valveStatus", "", "");
} else {
// WWC
Mqtt::register_mqtt_ha_sensor(F("Current flow temperature"), this->device_type(), "wwTemp", "°C", "");
Mqtt::register_mqtt_ha_sensor(F("Current pump status"), this->device_type(), "pumpStatus", "", "");
Mqtt::register_mqtt_ha_sensor(F("Current temperature status"), this->device_type(), "tempStatus", "", "");
}
ha_created_ = true;
}
// creates JSON doc from values
// returns false if empty
bool Mixing::export_values(JsonObject & output) {
switch (type_) {
switch (this->type()) {
case Type::HC:
output["type"] = "hc";
if (Helpers::hasValue(flowTemp_)) {
@@ -159,7 +180,8 @@ bool Mixing::export_values(JsonObject & output) {
output["wwTemp"] = (float)flowTemp_ / 10;
}
if (Helpers::hasValue(pump_)) {
output["pumpStatus"] = pump_;
char s[5]; // for formatting strings
output["pumpStatus"] = Helpers::render_value(s, pump_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(status_)) {
output["tempStatus"] = status_;
@@ -179,11 +201,11 @@ bool Mixing::export_values(JsonObject & output) {
// e.g. A0 00 FF 00 01 D7 00 00 00 80 00 00 00 00 03 C5
// A0 0B FF 00 01 D7 00 00 00 80 00 00 00 00 03 80
void Mixing::process_MMPLUSStatusMessage_HC(std::shared_ptr<const Telegram> telegram) {
type_ = Type::HC;
hc_ = telegram->type_id - 0x02D7 + 1; // determine which circuit this is
type(Type::HC);
hc_ = telegram->type_id - 0x02D7 + 1; // determine which circuit this is
changed_ |= telegram->read_value(flowTemp_, 3); // is * 10
changed_ |= telegram->read_value(flowSetTemp_, 5);
changed_ |= telegram->read_value(pump_, 0);
changed_ |= telegram->read_bitvalue(pump_, 2, 0);
changed_ |= telegram->read_value(status_, 2); // valve status
}
@@ -191,10 +213,10 @@ void Mixing::process_MMPLUSStatusMessage_HC(std::shared_ptr<const Telegram> tele
// e.g. A9 00 FF 00 02 32 02 6C 00 3C 00 3C 3C 46 02 03 03 00 3C // on 0x28
// A8 00 FF 00 02 31 02 35 00 3C 00 3C 3C 46 02 03 03 00 3C // in 0x29
void Mixing::process_MMPLUSStatusMessage_WWC(std::shared_ptr<const Telegram> telegram) {
type_ = Type::WWC;
hc_ = telegram->type_id - 0x0331 + 1; // determine which circuit this is. There are max 2.
type(Type::WWC);
hc_ = telegram->type_id - 0x0331 + 1; // determine which circuit this is. There are max 2.
changed_ |= telegram->read_value(flowTemp_, 0); // is * 10
changed_ |= telegram->read_value(pump_, 2);
changed_ |= telegram->read_bitvalue(pump_, 2, 0);
changed_ |= telegram->read_value(status_, 11); // temp status
}
@@ -202,18 +224,20 @@ void Mixing::process_MMPLUSStatusMessage_WWC(std::shared_ptr<const Telegram> tel
// e.g. A0 00 FF 00 00 0C 01 00 00 00 00 00 54
// A1 00 FF 00 00 0C 02 04 00 01 1D 00 82
void Mixing::process_IPMStatusMessage(std::shared_ptr<const Telegram> telegram) {
type_ = Type::HC;
type(Type::HC);
hc_ = device_id() - 0x20 + 1;
uint8_t ismixed = 0;
changed_ |= telegram->read_value(ismixed, 0); // check if circuit is active, 0-off, 1-unmixed, 2-mixed
if (ismixed == 0) {
return;
}
if (ismixed == 2) { // we have a mixed circuit
changed_ |= telegram->read_value(flowTemp_, 3); // is * 10
changed_ |= telegram->read_value(flowSetTemp_, 5);
changed_ |= telegram->read_value(status_, 2); // valve status
}
changed_ |= telegram->read_bitvalue(pump_, 1, 0); // pump is also in unmixed circuits
}
@@ -221,14 +245,14 @@ void Mixing::process_IPMStatusMessage(std::shared_ptr<const Telegram> telegram)
// e.g. Mixing Module -> All, type 0xAB, telegram: 21 00 AB 00 2D 01 BE 64 04 01 00 (CRC=15) #data=7
// see also https://github.com/proddy/EMS-ESP/issues/386
void Mixing::process_MMStatusMessage(std::shared_ptr<const Telegram> telegram) {
type_ = Type::HC;
type(Type::HC);
// the heating circuit is determine by which device_id it is, 0x20 - 0x23
// 0x21 is position 2. 0x20 is typically reserved for the WM10 switch module
// see https://github.com/proddy/EMS-ESP/issues/270 and https://github.com/proddy/EMS-ESP/issues/386#issuecomment-629610918
hc_ = device_id() - 0x20 + 1;
changed_ |= telegram->read_value(flowTemp_, 1); // is * 10
changed_ |= telegram->read_value(pump_, 3);
changed_ |= telegram->read_bitvalue(pump_, 3, 0);
changed_ |= telegram->read_value(flowSetTemp_, 0);
changed_ |= telegram->read_value(status_, 4); // valve status -100 to 100
}

View File

@@ -45,6 +45,7 @@ class Mixing : public EMSdevice {
static uuid::log::Logger logger_;
bool export_values(JsonObject & doc);
void register_mqtt_ha_config();
bool command_info(const char * value, const int8_t id, JsonObject & output);
void process_MMPLUSStatusMessage_HC(std::shared_ptr<const Telegram> telegram);
@@ -60,6 +61,14 @@ class Mixing : public EMSdevice {
WWC // warm water circuit
};
Type type() const {
return type_;
}
void type(Type new_type) {
type_ = new_type;
}
private:
uint16_t hc_ = EMS_VALUE_USHORT_NOTSET;
uint16_t flowTemp_ = EMS_VALUE_USHORT_NOTSET;
@@ -68,7 +77,8 @@ class Mixing : public EMSdevice {
uint8_t flowSetTemp_ = EMS_VALUE_UINT_NOTSET;
Type type_ = Type::NONE;
bool changed_ = false;
bool changed_ = false;
bool ha_created_ = false; // for HA MQTT Discovery
};
} // namespace emsesp

View File

@@ -116,8 +116,33 @@ void Solar::publish_values() {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
JsonObject output = doc.to<JsonObject>();
if (export_values(output)) {
Mqtt::publish(F("sm_data"), doc.as<JsonObject>());
Mqtt::publish(F("solar_data"), doc.as<JsonObject>());
}
// if we're using Home Assistant and haven't created the MQTT Discovery topics, do it now
if ((Mqtt::mqtt_format() == Mqtt::Format::HA) && (!ha_created_)) {
register_mqtt_ha_config();
}
}
// publish config topic for HA MQTT Discovery
void Solar::register_mqtt_ha_config() {
Mqtt::register_mqtt_ha_sensor(F("Collector temperature (TS1)"), this->device_type(), "collectorTemp", "°C", "");
Mqtt::register_mqtt_ha_sensor(F("Bottom temperature (TS2)"), this->device_type(), "tankBottomTemp", "°C", "");
Mqtt::register_mqtt_ha_sensor(F("Bottom temperature (TS5)"), this->device_type(), "tankBottomTemp2", "°C", "");
Mqtt::register_mqtt_ha_sensor(F("Heat exchanger temperature (TS6)"), this->device_type(), "heatExchangerTemp", "°C", "");
Mqtt::register_mqtt_ha_sensor(F("Solar pump modulation (PS1)"), this->device_type(), "solarPumpModulation", "%", "");
Mqtt::register_mqtt_ha_sensor(F("Cylinder pump modulation (PS5)"), this->device_type(), "cylinderPumpModulation", "%", "");
Mqtt::register_mqtt_ha_sensor(F("Pump working time"), this->device_type(), "pumpWorkMin", "", "");
Mqtt::register_mqtt_ha_sensor(F("Energy last hour"), this->device_type(), "energyLastHour", "", "");
Mqtt::register_mqtt_ha_sensor(F("Energy today"), this->device_type(), "energyToday", "", "");
Mqtt::register_mqtt_ha_sensor(F("Energy total"), this->device_type(), "energyTotal", "", "");
Mqtt::register_mqtt_ha_sensor(F("Solar Pump (PS1) active"), this->device_type(), "solarPump", "", "");
Mqtt::register_mqtt_ha_sensor(F("Valve (VS2) status"), this->device_type(), "valveStatus", "", "");
Mqtt::register_mqtt_ha_sensor(F("Tank Heated"), this->device_type(), "tankHeated", "", "");
Mqtt::register_mqtt_ha_sensor(F("Collector shutdown"), this->device_type(), "collectorShutdown", "", "");
ha_created_ = true;
}
// creates JSON doc from values

View File

@@ -46,6 +46,7 @@ class Solar : public EMSdevice {
bool export_values(JsonObject & doc);
bool command_info(const char * value, const int8_t id, JsonObject & output);
void register_mqtt_ha_config();
int16_t collectorTemp_ = EMS_VALUE_SHORT_NOTSET; // TS1: Temperature sensor for collector array 1
int16_t tankBottomTemp_ = EMS_VALUE_SHORT_NOTSET; // TS2: Temperature sensor 1 cylinder, bottom (solar thermal system)
@@ -67,7 +68,8 @@ class Solar : public EMSdevice {
uint8_t configFlag_ = EMS_VALUE_BOOL_NOTSET;
uint8_t userFlag_ = EMS_VALUE_BOOL_NOTSET;
bool changed_ = false;
bool changed_ = false;
bool ha_created_ = false; // for HA MQTT Discovery
void process_SM10Monitor(std::shared_ptr<const Telegram> telegram);
void process_SM100Monitor(std::shared_ptr<const Telegram> telegram);

View File

@@ -137,10 +137,6 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i
}
}
EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) {
mqtt_format_ = settings.mqtt_format; // single, nested or ha
});
if (actual_master_thermostat != device_id) {
LOG_DEBUG(F("Adding new thermostat with device ID 0x%02X"), device_id);
return; // don't fetch data if more than 1 thermostat
@@ -236,7 +232,7 @@ bool Thermostat::updated_values() {
// info API command
// returns the same MQTT publish payload in Nested format
bool Thermostat::command_info(const char * value, const int8_t id, JsonObject & output) {
return (export_values(MQTT_format::NESTED, output));
return (export_values(Mqtt::Format::NESTED, output));
}
// publish values via MQTT
@@ -247,9 +243,11 @@ void Thermostat::publish_values() {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
JsonObject output = doc.to<JsonObject>();
export_values(mqtt_format_, output);
if (mqtt_format_ == MQTT_format::NESTED) {
export_values(Mqtt::mqtt_format(), output);
// if we're in SINGLE mode the MQTT would have been published on the export_values() function for each hc
if (Mqtt::mqtt_format() != Mqtt::Format::SINGLE) {
Mqtt::publish(F("thermostat_data"), output);
}
}
@@ -300,7 +298,7 @@ bool Thermostat::export_values(uint8_t mqtt_format, JsonObject & rootThermostat)
}
// send this specific data using the thermostat_data topic
if (mqtt_format != MQTT_format::NESTED) {
if (mqtt_format != Mqtt::Format::NESTED) {
Mqtt::publish(F("thermostat_data"), rootThermostat);
rootThermostat.clear(); // clear object
}
@@ -313,7 +311,7 @@ bool Thermostat::export_values(uint8_t mqtt_format, JsonObject & rootThermostat)
has_data = true;
// if the MQTT format is 'nested' or 'ha' then create the parent object hc<n>
if ((mqtt_format == MQTT_format::NESTED) || (mqtt_format == MQTT_format::HA)) {
if ((mqtt_format == Mqtt::Format::NESTED) || (mqtt_format == Mqtt::Format::HA)) {
char hc_name[10]; // hc{1-4}
strlcpy(hc_name, "hc", 10);
char s[3];
@@ -395,10 +393,10 @@ bool Thermostat::export_values(uint8_t mqtt_format, JsonObject & rootThermostat)
}
// mode - always force showing this when in HA so not to break HA's climate component
if ((Helpers::hasValue(hc->mode)) || (mqtt_format == MQTT_format::HA)) {
if ((Helpers::hasValue(hc->mode)) || (mqtt_format == Mqtt::Format::HA)) {
uint8_t hc_mode = hc->get_mode(flags);
// if we're sending to HA the only valid mode types are heat, auto and off
if (mqtt_format == MQTT_format::HA) {
if (mqtt_format == Mqtt::Format::HA) {
if ((hc_mode == HeatingCircuit::Mode::MANUAL) || (hc_mode == HeatingCircuit::Mode::DAY)) {
hc_mode = HeatingCircuit::Mode::HEAT;
} else if ((hc_mode == HeatingCircuit::Mode::NIGHT) || (hc_mode == HeatingCircuit::Mode::OFF)) {
@@ -422,23 +420,17 @@ bool Thermostat::export_values(uint8_t mqtt_format, JsonObject & rootThermostat)
// if format is single, send immediately and clear object for next hc
// the topic will have the hc number appended
if ((mqtt_format == MQTT_format::SINGLE) || (mqtt_format == MQTT_format::CUSTOM)) {
if ((mqtt_format == Mqtt::Format::SINGLE) || (mqtt_format == Mqtt::Format::CUSTOM)) {
char topic[30];
char s[3];
strlcpy(topic, "thermostat_data", 30);
strlcat(topic, Helpers::itoa(s, hc->hc_num()), 30); // append hc to topic
Mqtt::publish(topic, rootThermostat);
rootThermostat.clear(); // clear object
} else if (mqtt_format == MQTT_format::HA) {
} else if ((mqtt_format == Mqtt::Format::HA) && (!hc->ha_registered())) {
// see if we have already registered this with HA MQTT Discovery, if not send the config
if (!hc->ha_registered()) {
register_mqtt_ha_config(hc->hc_num());
hc->ha_registered(true);
}
// send the thermostat topic and payload data
std::string topic(100, '\0');
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d/state"), hc->hc_num());
Mqtt::publish(topic, rootThermostat);
register_mqtt_ha_config(hc->hc_num());
hc->ha_registered(true);
}
}
}
@@ -527,27 +519,24 @@ std::shared_ptr<Thermostat::HeatingCircuit> Thermostat::heating_circuit(std::sha
}
// publish config topic for HA MQTT Discovery
// homeassistant/climate/ems-esp/hc<num>
// state is /state
// config is /config
// homeassistant/climate/ems-esp/thermostat_hc1/config
void Thermostat::register_mqtt_ha_config(uint8_t hc_num) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
std::string hc_text(10, '\0');
snprintf_P(&hc_text[0], hc_text.capacity() + 1, PSTR("hc%d"), hc_num);
doc["name"] = hc_text;
doc["uniq_id"] = hc_text;
std::string str1(40, '\0');
snprintf_P(&str1[0], str1.capacity() + 1, PSTR("Thermostat hc%d"), hc_num);
doc["name"] = str1;
// topic root is homeassistant/climate/ems-esp/hc<1..n>/
std::string root(100, '\0');
snprintf_P(&root[0], root.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d"), hc_num);
doc["~"] = root;
std::string str2(40, '\0');
snprintf_P(&str2[0], str2.capacity() + 1, PSTR("thermostat_hc%d"), hc_num);
doc["uniq_id"] = str2;
doc["mode_cmd_t"] = "~/cmd_mode";
doc["mode_stat_t"] = "~/state";
doc["temp_cmd_t"] = "~/cmd_temp";
doc["temp_stat_t"] = "~/state";
doc["curr_temp_t"] = "~/state";
doc["~"] = F("ems-esp");
doc["mode_cmd_t"] = F("~/thermostat");
doc["temp_cmd_t"] = F("~/thermostat");
doc["mode_stat_t"] = F("~/thermostat_data");
doc["temp_stat_t"] = F("~/thermostat_data");
doc["curr_temp_t"] = F("~/thermostat_data");
std::string mode_str(30, '\0');
snprintf_P(&mode_str[0], 30, PSTR("{{value_json.hc%d.mode}}"), hc_num);
@@ -561,40 +550,60 @@ void Thermostat::register_mqtt_ha_config(uint8_t hc_num) {
snprintf_P(&currtemp_str[0], 30, PSTR("{{value_json.hc%d.currtemp}}"), hc_num);
doc["curr_temp_tpl"] = currtemp_str;
doc["min_temp"] = "5";
doc["max_temp"] = "40";
doc["temp_step"] = "0.5";
doc["min_temp"] = F("5");
doc["max_temp"] = F("40");
doc["temp_step"] = F("0.5");
JsonArray modes = doc.createNestedArray("modes");
JsonArray modes = doc.createNestedArray(F("modes"));
uint8_t flags = this->model();
if (flags == EMSdevice::EMS_DEVICE_FLAG_RC20_2) {
modes.add("night");
modes.add("day");
modes.add(F("night"));
modes.add(F("day"));
} else if ((flags == EMSdevice::EMS_DEVICE_FLAG_RC300) || (flags == EMSdevice::EMS_DEVICE_FLAG_RC100)) {
modes.add("eco");
modes.add("comfort");
modes.add("auto");
modes.add(F("eco"));
modes.add(F("comfort"));
modes.add(F("auto"));
} else if (flags == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) {
modes.add("nofrost");
modes.add("eco");
modes.add("heat");
modes.add("auto");
modes.add(F("nofrost"));
modes.add(F("eco"));
modes.add(F("heat"));
modes.add(F("auto"));
} else { // default for all other thermostats
modes.add("night");
modes.add("day");
modes.add("auto");
modes.add(F("night"));
modes.add(F("day"));
modes.add(F("auto"));
}
JsonObject dev = doc.createNestedObject(F("dev"));
JsonArray ids = dev.createNestedArray(F("ids"));
ids.add(F("ems-esp"));
std::string topic(100, '\0'); // e.g homeassistant/climate/hc1/thermostat/config
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d/config"), hc_num);
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/thermostat_hc%d/config"), hc_num);
// Mqtt::publish(topic); // empty payload, this remove any previous config sent to HA
Mqtt::publish_retain(topic, doc.as<JsonObject>(), true); // publish the config payload with retain flag
// subscribe to the temp and mode commands
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d/cmd_temp"), hc_num);
register_mqtt_topic(topic, [&](const char * m) { thermostat_cmd_temp(m); });
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d/cmd_mode"), hc_num);
register_mqtt_topic(topic, [&](const char * m) { thermostat_cmd_mode(m); });
// enable the thermostat topic to take both mode strings and floats
register_mqtt_topic("thermostat", [&](const char * m) { return thermostat_ha_cmd(m); });
}
// for HA specifically when receiving over MQTT in the thermostat topic
// it could be either a 'mode' or a float value
// return true if it parses the message correctly
bool Thermostat::thermostat_ha_cmd(const char * message) {
// check if it's json. We know the message isn't empty
if (message[0] == '{') {
return false;
}
// check for mode first
if (!set_mode(message, AUTO_HEATING_CIRCUIT)) {
// handle as a numerical temperature value
float f = strtof((char *)message, 0);
set_temperature(f, HeatingCircuit::Mode::AUTO, AUTO_HEATING_CIRCUIT);
}
return true;
}
// decodes the thermostat mode for the heating circuit based on the thermostat type
@@ -1463,7 +1472,6 @@ bool Thermostat::set_mode(const char * value, const int8_t id) {
return set_mode_n(HeatingCircuit::Mode::COMFORT, hc_num);
}
LOG_WARNING(F("Invalid mode %s. Cannot set"), mode.c_str());
return false;
}
@@ -1789,18 +1797,6 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co
return false;
}
// for HA specifically when receiving over MQTT
bool Thermostat::thermostat_cmd_temp(const char * message) {
float f = strtof((char *)message, 0);
return set_temperature(f, HeatingCircuit::Mode::AUTO, AUTO_HEATING_CIRCUIT);
}
// for HA specifically when receiving over MQTT
// message payload holds the text name of the mode e.g. "auto"
bool Thermostat::thermostat_cmd_mode(const char * message) {
return set_mode(message, AUTO_HEATING_CIRCUIT);
}
bool Thermostat::set_temperature_value(const char * value, const int8_t id, const uint8_t mode) {
float f = 0;
uint8_t hc_num = (id == -1) ? AUTO_HEATING_CIRCUIT : id;
@@ -1874,7 +1870,13 @@ void Thermostat::add_commands() {
// common to all thermostats
register_mqtt_cmd(F("wwmode"), [&](const char * value, const int8_t id) { return set_wwmode(value, id); });
register_mqtt_cmd(F("temp"), [&](const char * value, const int8_t id) { return set_temp(value, id); });
register_mqtt_cmd(F("mode"), [&](const char * value, const int8_t id) { return set_mode(value, id); });
register_mqtt_cmd(F("mode"), [=](const char * value, const int8_t id) {
if (!set_mode(value, id)) {
LOG_WARNING(F("Invalid mode %s. Cannot set"), value);
return false;
}
return true;
});
uint8_t model = this->model();
switch (model) {

View File

@@ -120,7 +120,6 @@ class Thermostat : public EMSdevice {
std::string datetime_; // date and time stamp
uint8_t mqtt_format_; // single, nested or ha
bool changed_ = false;
// Installation parameters
@@ -220,6 +219,8 @@ class Thermostat : public EMSdevice {
std::shared_ptr<Thermostat::HeatingCircuit> heating_circuit(const uint8_t hc_num);
void register_mqtt_ha_config(uint8_t hc_num);
bool thermostat_ha_cmd(const char * message);
bool command_info(const char * value, const int8_t id, JsonObject & output);
void process_RCOutdoorTemp(std::shared_ptr<const Telegram> telegram);
@@ -252,10 +253,6 @@ class Thermostat : public EMSdevice {
bool set_temperature(const float temperature, const std::string & mode, const uint8_t hc_num);
bool set_temperature(const float temperature, const uint8_t mode, const uint8_t hc_num);
// for HA specifically. MQTT functions.
bool thermostat_cmd_temp(const char * message);
bool thermostat_cmd_mode(const char * message);
// set functions - these use the id/hc
bool set_mode(const char * value, const int8_t id);
bool set_control(const char * value, const int8_t id);