From b5062df8f4dc26b1b225f6d69db87a890bc6740e Mon Sep 17 00:00:00 2001 From: proddy Date: Mon, 28 Sep 2020 18:17:23 +0200 Subject: [PATCH] MQTT updates: added HA discovery, removed heartbeat - HomeAssistant Discovery #288 --- interface/src/mqtt/MqttSettingsForm.tsx | 14 +- interface/src/mqtt/types.ts | 1 - lib/framework/MqttSettingsService.cpp | 6 - lib/framework/MqttSettingsService.h | 2 - lib_standalone/ESP8266React.h | 1 - src/EMSESPSettingsService.h | 2 - src/devices/boiler.cpp | 76 ++++--- src/devices/boiler.h | 12 +- src/devices/mixing.cpp | 60 ++++-- src/devices/mixing.h | 12 +- src/devices/solar.cpp | 27 ++- src/devices/solar.h | 4 +- src/devices/thermostat.cpp | 148 ++++++------- src/devices/thermostat.h | 7 +- src/emsdevice.cpp | 1 - src/emsesp.cpp | 2 +- src/helpers.cpp | 6 +- src/helpers.h | 3 + src/mqtt.cpp | 264 +++++++++++++++++------- src/mqtt.h | 16 +- src/sensor.cpp | 50 ++--- src/sensor.h | 1 - src/shower.cpp | 10 +- src/system.cpp | 23 +-- src/system.h | 2 - src/test/test.cpp | 71 ++++--- 26 files changed, 503 insertions(+), 318 deletions(-) diff --git a/interface/src/mqtt/MqttSettingsForm.tsx b/interface/src/mqtt/MqttSettingsForm.tsx index 767164c4d..9c4703636 100644 --- a/interface/src/mqtt/MqttSettingsForm.tsx +++ b/interface/src/mqtt/MqttSettingsForm.tsx @@ -30,7 +30,7 @@ class MqttSettingsForm extends React.Component { value="enabled" /> } - label="Enable MQTT?" + label="Enable MQTT" /> { value="clean_session" /> } - label="Clean Session?" + label="Clean Session" /> { } label="Retain Flag" /> - - } - label="Heartbeat" - />

Publish Intervals diff --git a/interface/src/mqtt/types.ts b/interface/src/mqtt/types.ts index 1130bfa73..ec0a0f8c0 100644 --- a/interface/src/mqtt/types.ts +++ b/interface/src/mqtt/types.ts @@ -36,5 +36,4 @@ export interface MqttSettings { mqtt_format: number; mqtt_qos: number; mqtt_retain: boolean; - system_heartbeat: boolean; } diff --git a/lib/framework/MqttSettingsService.cpp b/lib/framework/MqttSettingsService.cpp index 53348bf8e..ef9d6b277 100644 --- a/lib/framework/MqttSettingsService.cpp +++ b/lib/framework/MqttSettingsService.cpp @@ -184,7 +184,6 @@ void MqttSettings::read(MqttSettings & settings, JsonObject & root) { root["max_topic_length"] = settings.maxTopicLength; // added by proddy for EMS-ESP - root["system_heartbeat"] = settings.system_heartbeat; root["publish_time_boiler"] = settings.publish_time_boiler; root["publish_time_thermostat"] = settings.publish_time_thermostat; root["publish_time_solar"] = settings.publish_time_solar; @@ -209,7 +208,6 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting newSettings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION; newSettings.maxTopicLength = root["max_topic_length"] | FACTORY_MQTT_MAX_TOPIC_LENGTH; - newSettings.system_heartbeat = root["system_heartbeat"] | EMSESP_DEFAULT_SYSTEM_HEARTBEAT; newSettings.publish_time_boiler = root["publish_time_boiler"] | EMSESP_DEFAULT_PUBLISH_TIME; newSettings.publish_time_thermostat = root["publish_time_thermostat"] | EMSESP_DEFAULT_PUBLISH_TIME; newSettings.publish_time_solar = root["publish_time_solar"] | EMSESP_DEFAULT_PUBLISH_TIME; @@ -220,10 +218,6 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting newSettings.mqtt_qos = root["mqtt_qos"] | EMSESP_DEFAULT_MQTT_QOS; newSettings.mqtt_retain = root["mqtt_retain"] | EMSESP_DEFAULT_MQTT_RETAIN; - if (newSettings.system_heartbeat != settings.system_heartbeat) { - emsesp::EMSESP::system_.set_heartbeat(newSettings.system_heartbeat); - } - if (newSettings.mqtt_qos != settings.mqtt_qos) { emsesp::EMSESP::mqtt_.set_qos(newSettings.mqtt_qos); } diff --git a/lib/framework/MqttSettingsService.h b/lib/framework/MqttSettingsService.h index 83e8f9d5a..abdd40f82 100644 --- a/lib/framework/MqttSettingsService.h +++ b/lib/framework/MqttSettingsService.h @@ -60,7 +60,6 @@ static String generateClientId() { #define FACTORY_MQTT_MAX_TOPIC_LENGTH 128 #endif -#define EMSESP_DEFAULT_SYSTEM_HEARTBEAT true #define EMSESP_DEFAULT_MQTT_FORMAT 2 // nested #define EMSESP_DEFAULT_MQTT_QOS 0 #define EMSESP_DEFAULT_MQTT_RETAIN false @@ -94,7 +93,6 @@ class MqttSettings { uint16_t publish_time_sensor; uint8_t mqtt_format; // 1=single, 2=nested, 3=ha, 4=custom uint8_t mqtt_qos; - bool system_heartbeat; bool mqtt_retain; static void read(MqttSettings & settings, JsonObject & root); diff --git a/lib_standalone/ESP8266React.h b/lib_standalone/ESP8266React.h index d990bdeaa..b38819c03 100644 --- a/lib_standalone/ESP8266React.h +++ b/lib_standalone/ESP8266React.h @@ -15,7 +15,6 @@ class DummySettings { public: uint8_t tx_mode = 1; uint8_t ems_bus_id = 0x0B; - bool system_heartbeat = false; int8_t syslog_level = 1; // uuid::log::Level uint32_t syslog_mark_interval = 0; String syslog_host = "192.168.1.4"; diff --git a/src/EMSESPSettingsService.h b/src/EMSESPSettingsService.h index 9a104024b..6e200260e 100644 --- a/src/EMSESPSettingsService.h +++ b/src/EMSESPSettingsService.h @@ -59,8 +59,6 @@ namespace emsesp { -enum MQTT_format : uint8_t { SINGLE = 1, NESTED, HA, CUSTOM }; - class EMSESPSettings { public: uint8_t tx_mode; diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index 3c511ec37..7fc263f84 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -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 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 doc; JsonObject output = doc.to(); if (export_values(output)) { Mqtt::publish(F("boiler_data"), doc.as()); } + + // 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 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); diff --git a/src/devices/boiler.h b/src/devices/boiler.h index 7c2fb6e37..95f7f0966 100644 --- a/src/devices/boiler.h +++ b/src/devices/boiler.h @@ -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 \ No newline at end of file +#endif diff --git a/src/devices/mixing.cpp b/src/devices/mixing.cpp index 51d60f394..f65e63945 100644 --- a/src/devices/mixing.cpp +++ b/src/devices/mixing.cpp @@ -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()); + + // 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 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 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 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 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 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 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 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 } diff --git a/src/devices/mixing.h b/src/devices/mixing.h index 8a18db8bc..a308c3f13 100644 --- a/src/devices/mixing.h +++ b/src/devices/mixing.h @@ -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 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 diff --git a/src/devices/solar.cpp b/src/devices/solar.cpp index ab2116640..afa0e502a 100644 --- a/src/devices/solar.cpp +++ b/src/devices/solar.cpp @@ -116,8 +116,33 @@ void Solar::publish_values() { StaticJsonDocument doc; JsonObject output = doc.to(); if (export_values(output)) { - Mqtt::publish(F("sm_data"), doc.as()); + Mqtt::publish(F("solar_data"), doc.as()); } + + // 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 diff --git a/src/devices/solar.h b/src/devices/solar.h index 6ad3273c2..6ba24e251 100644 --- a/src/devices/solar.h +++ b/src/devices/solar.h @@ -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 telegram); void process_SM100Monitor(std::shared_ptr telegram); diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index 030a6f56e..ac59f5288 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -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 doc; JsonObject output = doc.to(); - 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 - 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::heating_circuit(std::sha } // publish config topic for HA MQTT Discovery -// homeassistant/climate/ems-esp/hc -// state is /state -// config is /config +// homeassistant/climate/ems-esp/thermostat_hc1/config void Thermostat::register_mqtt_ha_config(uint8_t hc_num) { StaticJsonDocument 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(), 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) { diff --git a/src/devices/thermostat.h b/src/devices/thermostat.h index d6027bac3..7f2a604e3 100644 --- a/src/devices/thermostat.h +++ b/src/devices/thermostat.h @@ -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 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 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); diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index 08dfeca4f..69cce714e 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -296,7 +296,6 @@ void EMSdevice::show_mqtt_handlers(uuid::console::Shell & shell) { } void EMSdevice::register_mqtt_topic(const std::string & topic, mqtt_subfunction_p f) { - LOG_DEBUG(F("Registering MQTT topic %s for device ID %02X and type %s"), topic.c_str(), this->device_id_, this->device_type_name().c_str()); Mqtt::subscribe(this->device_type_, topic, f); } diff --git a/src/emsesp.cpp b/src/emsesp.cpp index a14b5ddac..23e517ca0 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -157,7 +157,7 @@ void EMSESP::init_tx() { } } -// return status of bus: connected, connected but Tx is broken, disconnected +// return status of bus: connected (0), connected but Tx is broken (1), disconnected (2) uint8_t EMSESP::bus_status() { if (!rxservice_.bus_connected()) { return BUS_STATUS_OFFLINE; diff --git a/src/helpers.cpp b/src/helpers.cpp index c8e9ce7e2..1d8080151 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -20,7 +20,7 @@ namespace emsesp { -uint8_t Helpers::bool_format_ = 1; // on/off +uint8_t Helpers::bool_format_ = BOOL_FORMAT_ONOFF; // on/off // like itoa but for hex, and quicker char * Helpers::hextoa(char * result, const uint8_t value) { @@ -124,9 +124,9 @@ char * Helpers::smallitoa(char * result, const uint16_t value) { // work out how to display booleans char * Helpers::render_boolean(char * result, bool value) { - if (bool_format() == 1) { + if (bool_format() == BOOL_FORMAT_ONOFF) { strlcpy(result, value ? "on" : "off", 5); - } else if (bool_format() == 2) { + } else if (bool_format() == BOOL_FORMAT_TRUEFALSE) { strlcpy(result, value ? "true" : "false", 7); } else { strlcpy(result, value ? "1" : "0", 2); diff --git a/src/helpers.h b/src/helpers.h index 28594825a..521f178be 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -24,6 +24,9 @@ #include "telegram.h" // for EMS_VALUE_* settings +#define BOOL_FORMAT_ONOFF 1 +#define BOOL_FORMAT_TRUEFALSE 2 + namespace emsesp { class Helpers { diff --git a/src/mqtt.cpp b/src/mqtt.cpp index cb9184132..4ba559de9 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -34,6 +34,7 @@ uint32_t Mqtt::publish_time_solar_; uint32_t Mqtt::publish_time_mixing_; uint32_t Mqtt::publish_time_other_; uint32_t Mqtt::publish_time_sensor_; +uint8_t Mqtt::mqtt_format_; std::vector Mqtt::mqtt_subfunctions_; @@ -50,13 +51,19 @@ uuid::log::Logger Mqtt::logger_{F_(mqtt), uuid::log::Facility::DAEMON}; void Mqtt::subscribe(const uint8_t device_type, const std::string & topic, mqtt_subfunction_p cb) { // check if we already have the topic subscribed, if so don't add it again if (!mqtt_subfunctions_.empty()) { - for (const auto & mqtt_subfunction : mqtt_subfunctions_) { + for (auto & mqtt_subfunction : mqtt_subfunctions_) { if ((mqtt_subfunction.device_type_ == device_type) && (strcmp(mqtt_subfunction.topic_.c_str(), topic.c_str()) == 0)) { + // add the function, in case its not there + if (cb) { + mqtt_subfunction.mqtt_subfunction_ = cb; + } return; // it exists, exit } } } + LOG_DEBUG(F("Subscribing MQTT topic %s for device type %s"), topic.c_str(), EMSdevice::device_type_2_device_name(device_type).c_str()); + // add to MQTT queue as a subscribe operation auto message = queue_subscribe_message(topic); @@ -77,6 +84,7 @@ void Mqtt::register_command(const uint8_t device_type, const uint8_t device_id, } } } + if (!exists) { Mqtt::subscribe(device_type, cmd_topic, nullptr); // use an empty function handler to signal this is a command function } @@ -151,10 +159,7 @@ void Mqtt::loop() { void Mqtt::show_mqtt(uuid::console::Shell & shell) { shell.printfln(F("MQTT is %s"), connected() ? uuid::read_flash_string(F_(connected)).c_str() : uuid::read_flash_string(F_(disconnected)).c_str()); - EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { - shell.printfln(F_(mqtt_heartbeat_fmt), settings.system_heartbeat ? F_(enabled) : F_(disabled)); - shell.printfln(F_(mqtt_format_fmt), settings.mqtt_format); - }); + EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { shell.printfln(F_(mqtt_format_fmt), settings.mqtt_format); }); shell.printfln(F("MQTT publish fails count: %lu"), mqtt_publish_fails_); shell.println(); @@ -215,7 +220,7 @@ void Mqtt::incoming(const char * topic, const char * payload) { // received an MQTT message that we subscribed too void Mqtt::on_message(const char * topic, const char * payload, size_t len) { if (len == 0) { - return; + return; // ignore empty payloads } // convert payload to a null-terminated char string @@ -230,56 +235,59 @@ void Mqtt::on_message(const char * topic, const char * payload, size_t len) { for (const auto & mf : mqtt_subfunctions_) { if (strcmp(topic, mf.full_topic_.c_str()) == 0) { if (mf.mqtt_subfunction_) { - (mf.mqtt_subfunction_)(message); // matching function, call it - return; - } else { - // empty function. It's a command then. Find the command from the json and call it directly. - StaticJsonDocument doc; - DeserializationError error = deserializeJson(doc, message); - if (error) { - LOG_ERROR(F("MQTT error: payload %s, error %s"), message, error.c_str()); - return; - } - - const char * command = doc["cmd"]; - if (command == nullptr) { - LOG_ERROR(F("MQTT error: invalid payload cmd format. message=%s"), message); - return; - } - - // check for hc and id, and convert to int - int8_t n = -1; // no value - if (doc.containsKey("hc")) { - n = doc["hc"]; - } else if (doc.containsKey("id")) { - n = doc["id"]; - } - - bool cmd_known = false; - JsonVariant data = doc["data"]; - - JsonObject output; // empty object - - if (data.is()) { - cmd_known = Command::call(mf.device_type_, command, data.as(), n, output); - } else if (data.is()) { - char data_str[10]; - cmd_known = Command::call(mf.device_type_, command, Helpers::itoa(data_str, (int16_t)data.as()), n, output); - } else if (data.is()) { - char data_str[10]; - cmd_known = Command::call(mf.device_type_, command, Helpers::render_value(data_str, (float)data.as(), 2), n, output); - } else if (data.isNull()) { - cmd_known = Command::call(mf.device_type_, command, "", n, output); - } - - if (!cmd_known) { - LOG_ERROR(F("MQTT: no matching cmd or invalid data: %s"), message); + // matching function, call it. If it returns true keep quit + if ((mf.mqtt_subfunction_)(message)) { + return; // function executed successfully } + } + // empty function. It's a command then. Find the command from the json and call it directly. + StaticJsonDocument doc; + DeserializationError error = deserializeJson(doc, message); + if (error) { + LOG_ERROR(F("MQTT error: payload %s, error %s"), message, error.c_str()); return; } + + const char * command = doc["cmd"]; + if (command == nullptr) { + LOG_ERROR(F("MQTT error: invalid payload cmd format. message=%s"), message); + return; + } + + // check for hc and id, and convert to int + int8_t n = -1; // no value + if (doc.containsKey("hc")) { + n = doc["hc"]; + } else if (doc.containsKey("id")) { + n = doc["id"]; + } + + bool cmd_known = false; + JsonVariant data = doc["data"]; + + JsonObject output; // empty object + + if (data.is()) { + cmd_known = Command::call(mf.device_type_, command, data.as(), n, output); + } else if (data.is()) { + char data_str[10]; + cmd_known = Command::call(mf.device_type_, command, Helpers::itoa(data_str, (int16_t)data.as()), n, output); + } else if (data.is()) { + char data_str[10]; + cmd_known = Command::call(mf.device_type_, command, Helpers::render_value(data_str, (float)data.as(), 2), n, output); + } else if (data.isNull()) { + cmd_known = Command::call(mf.device_type_, command, "", n, output); + } + + if (!cmd_known) { + LOG_ERROR(F("MQTT: no matching cmd (%s), invalid data or command failed"), command); + } + + return; } } + // if we got here we didn't find a topic match LOG_ERROR(F("No MQTT handler found for topic %s and payload %s"), topic, message); } @@ -343,6 +351,7 @@ void Mqtt::start() { publish_time_sensor_ = mqttSettings.publish_time_sensor * 1000; mqtt_qos_ = mqttSettings.mqtt_qos; mqtt_retain_ = mqttSettings.mqtt_retain; + mqtt_format_ = mqttSettings.mqtt_format; }); mqttClient_->onConnect([this](bool sessionPresent) { on_connect(); }); @@ -458,9 +467,36 @@ void Mqtt::on_connect() { resubscribe(); // in case this is a reconnect, re-subscribe again to all MQTT topics + if (mqtt_format() == Format::HA) { + ha_status(); // create a device in HA + } + LOG_INFO(F("MQTT connected")); } +// Home Assistant Discovery +// homeassistant/sensor/ems-esp/status/config +void Mqtt::ha_status() { + StaticJsonDocument doc; + doc["name"] = F("EMS-ESP status"); + doc["uniq_id"] = F("status"); + doc["avty_t"] = F("ems-esp/status"); + doc["json_attr_t"] = F("ems-esp/heartbeat"); + doc["stat_t"] = F("ems-esp/heartbeat"); + doc["val_tpl"] = F("{{value_json['status']}}"); + doc["ic"] = F("mdi:home-thermometer-outline"); + + JsonObject dev = doc.createNestedObject("dev"); + dev["name"] = F("EMS-ESP"); + dev["sw"] = EMSESP_APP_VERSION; + dev["mf"] = F("proddy"); + dev["mdl"] = F("EMS-ESP"); + JsonArray ids = dev.createNestedArray("ids"); + ids.add("ems-esp"); + + Mqtt::publish_retain(F("homeassistant/sensor/ems-esp/status/config"), doc.as(), true); // publish the config payload with retain flag +} + // add sub or pub task to the queue. // a fully-qualified topic is created by prefixing the hostname, unless it's HA // returns a pointer to the message created @@ -478,8 +514,8 @@ std::shared_ptr Mqtt::queue_message(const uint8_t operation, } else { // prefix the hostname - std::string full_topic(50, '\0'); - snprintf_P(&full_topic[0], full_topic.capacity() + 1, PSTR("%s/%s"), Mqtt::hostname_.c_str(), topic.c_str()); + std::string full_topic(100, '\0'); + snprintf_P(&full_topic[0], full_topic.capacity() + 1, PSTR("%s/%s"), hostname_.c_str(), topic.c_str()); // message = std::make_shared(operation, full_topic, std::move(payload), retain); message = std::make_shared(operation, full_topic, std::move(payload), retain); } @@ -508,6 +544,12 @@ void Mqtt::publish(const std::string & topic, const std::string & payload) { queue_publish_message(topic, payload, mqtt_retain_); } +// MQTT Publish, using a user's retain flag - except for char * strings +void Mqtt::publish(const __FlashStringHelper * topic, const char * payload) { + queue_publish_message(uuid::read_flash_string(topic), payload, mqtt_retain_); +} + + // MQTT Publish, using a specific retain flag, topic is a flash string void Mqtt::publish(const __FlashStringHelper * topic, const std::string & payload) { queue_publish_message(uuid::read_flash_string(topic), payload, mqtt_retain_); @@ -517,25 +559,13 @@ void Mqtt::publish(const __FlashStringHelper * topic, const JsonObject & payload publish(uuid::read_flash_string(topic), payload); } -// MQTT Publish, using a specific retain flag, topic is a flash string, forcing retain flag -void Mqtt::publish_retain(const __FlashStringHelper * topic, const std::string & payload, bool retain) { - queue_publish_message(uuid::read_flash_string(topic), payload, retain); -} - -void Mqtt::publish_retain(const std::string & topic, const JsonObject & payload, bool retain) { - std::string payload_text; - serializeJson(payload, payload_text); // convert json to string - queue_publish_message(topic, payload_text, retain); -} - -void Mqtt::publish_retain(const __FlashStringHelper * topic, const JsonObject & payload, bool retain) { - publish_retain(uuid::read_flash_string(topic), payload, retain); -} - +// publish json doc, only if its not empty void Mqtt::publish(const std::string & topic, const JsonObject & payload) { - std::string payload_text; - serializeJson(payload, payload_text); // convert json to string - queue_publish_message(topic, payload_text, mqtt_retain_); + if (payload.size()) { + std::string payload_text; + serializeJson(payload, payload_text); // convert json to string + queue_publish_message(topic, payload_text, mqtt_retain_); + } } // for booleans, which get converted to string values 1 and 0 @@ -552,6 +582,24 @@ void Mqtt::publish(const std::string & topic) { queue_publish_message(topic, "", false); } +// MQTT Publish, using a specific retain flag, topic is a flash string, forcing retain flag +void Mqtt::publish_retain(const __FlashStringHelper * topic, const std::string & payload, bool retain) { + queue_publish_message(uuid::read_flash_string(topic), payload, retain); +} + +// publish json doc, only if its not empty, using the retain flag +void Mqtt::publish_retain(const std::string & topic, const JsonObject & payload, bool retain) { + if (payload.size()) { + std::string payload_text; + serializeJson(payload, payload_text); // convert json to string + queue_publish_message(topic, payload_text, retain); + } +} + +void Mqtt::publish_retain(const __FlashStringHelper * topic, const JsonObject & payload, bool retain) { + publish_retain(uuid::read_flash_string(topic), payload, retain); +} + // take top from queue and perform the publish or subscribe action // assumes there is an MQTT connection void Mqtt::process_queue() { @@ -632,4 +680,80 @@ void Mqtt::process_queue() { mqtt_messages_.pop_front(); // remove the message from the queue } +// HA config for a binary_sensor +void Mqtt::register_mqtt_ha_binary_sensor(const __FlashStringHelper * name, const char * entity) { + if (mqtt_format() != Format::HA) { + return; + } + + StaticJsonDocument doc; + + doc["name"] = name; + doc["uniq_id"] = entity; + + std::string state_t(50, '\0'); + snprintf_P(&state_t[0], state_t.capacity() + 1, PSTR("%s/%s"), hostname_.c_str(), entity); + doc["stat_t"] = state_t; + + EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { + if (settings.bool_format == BOOL_FORMAT_ONOFF) { + doc[F("payload_on")] = F("on"); + doc[F("payload_off")] = F("off"); + } else if (settings.bool_format == BOOL_FORMAT_TRUEFALSE) { + doc[F("payload_on")] = F("true"); + doc[F("payload_off")] = F("false"); + } else { + doc[F("payload_on")] = "1"; + doc[F("payload_off")] = "0"; + } + }); + + JsonObject dev = doc.createNestedObject(F("dev")); + JsonArray ids = dev.createNestedArray(F("ids")); + ids.add(F("ems-esp")); + + std::string topic(100, '\0'); + snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/binary_sensor/ems-esp/%s/config"), entity); + + Mqtt::publish_retain(topic, doc.as(), true); // publish the config payload with retain flag +} + +// HA config for a normal sensor +void Mqtt::register_mqtt_ha_sensor(const __FlashStringHelper * name, const uint8_t device_type, const char * entity, const char * uom, const char * icon) { + if (mqtt_format() != Format::HA) { + return; + } + + StaticJsonDocument doc; + + doc["name"] = name; + + std::string uniq(50, '\0'); + snprintf_P(&uniq[0], uniq.capacity() + 1, PSTR("%s"), entity); + + doc["uniq_id"] = uniq; + doc["unit_of_meas"] = uom; + + std::string state_t(50, '\0'); + snprintf_P(&state_t[0], state_t.capacity() + 1, PSTR("%s/%s_data"), hostname_.c_str(), EMSdevice::device_type_2_device_name(device_type).c_str()); + doc["stat_t"] = state_t; + + std::string tpl(50, '\0'); + snprintf_P(&tpl[0], tpl.capacity() + 1, PSTR("{{value_json.%s}}"), entity); + doc["val_tpl"] = tpl; + + if (strlen(icon)) { + doc["ic"] = icon; + } + + JsonObject dev = doc.createNestedObject(F("dev")); + JsonArray ids = dev.createNestedArray(F("ids")); + ids.add(F("ems-esp")); + + std::string topic(100, '\0'); + snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/sensor/ems-esp/%s/config"), entity); + + Mqtt::publish_retain(topic, doc.as(), true); // publish the config payload with retain flag +} + } // namespace emsesp diff --git a/src/mqtt.h b/src/mqtt.h index ec1888e41..a05ee7a09 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -44,7 +44,7 @@ using uuid::console::Shell; namespace emsesp { -using mqtt_subfunction_p = std::function; +using mqtt_subfunction_p = std::function; using cmdfunction_p = std::function; struct MqttMessage { @@ -81,6 +81,8 @@ class Mqtt { enum Operation { PUBLISH, SUBSCRIBE }; + enum Format : uint8_t { SINGLE = 1, NESTED, HA, CUSTOM }; + static constexpr uint8_t MQTT_TOPIC_MAX_SIZE = 100; static void subscribe(const uint8_t device_type, const std::string & topic, mqtt_subfunction_p cb); @@ -90,6 +92,7 @@ class Mqtt { static void register_command(const uint8_t device_type, const uint8_t device_id, const __FlashStringHelper * cmd, cmdfunction_p cb); static void publish(const std::string & topic, const std::string & payload); + static void publish(const __FlashStringHelper * topic, const char * payload); static void publish(const std::string & topic, const JsonObject & payload); static void publish(const __FlashStringHelper * topic, const JsonObject & payload); static void publish(const __FlashStringHelper * topic, const std::string & payload); @@ -101,10 +104,14 @@ class Mqtt { static void publish_retain(const __FlashStringHelper * topic, const std::string & payload, bool retain); static void publish_retain(const __FlashStringHelper * topic, const JsonObject & payload, bool retain); + static void register_mqtt_ha_binary_sensor(const __FlashStringHelper * name, const char * entity); + static void register_mqtt_ha_sensor(const __FlashStringHelper * name, const uint8_t device_type, const char * entity, const char * uom, const char * icon); + static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_type); static void show_mqtt(uuid::console::Shell & shell); static void on_connect(); + static void ha_status(); void disconnect() { mqttClient_->disconnect(); @@ -126,6 +133,10 @@ class Mqtt { mqtt_publish_fails_ = 0; } + static uint8_t mqtt_format() { + return mqtt_format_; + } + private: static uuid::log::Logger logger_; @@ -151,7 +162,7 @@ class Mqtt { static size_t maximum_mqtt_messages_; static uint16_t mqtt_message_id_; - static constexpr size_t MAX_MQTT_MESSAGES = 20; // size of queue + static constexpr size_t MAX_MQTT_MESSAGES = 30; // size of queue static constexpr uint32_t MQTT_PUBLISH_WAIT = 200; // delay between sending publishes, to account for large payloads static constexpr uint8_t MQTT_PUBLISH_MAX_RETRY = 3; // max retries for giving up on publishing @@ -201,6 +212,7 @@ class Mqtt { static uint32_t publish_time_mixing_; static uint32_t publish_time_other_; static uint32_t publish_time_sensor_; + static uint8_t mqtt_format_; }; } // namespace emsesp diff --git a/src/sensor.cpp b/src/sensor.cpp index c290abcb2..4692bfb68 100644 --- a/src/sensor.cpp +++ b/src/sensor.cpp @@ -49,17 +49,12 @@ void Sensor::start() { // load the MQTT settings void Sensor::reload() { - // copy over values from MQTT so we don't keep on quering the filesystem - EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { - mqtt_format_ = settings.mqtt_format; // single, nested or ha - }); - EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { dallas_gpio_ = settings.dallas_gpio; parasite_ = settings.dallas_parasite; }); - if (mqtt_format_ == MQTT_format::HA) { + if (Mqtt::mqtt_format() == Mqtt::Format::HA) { for (uint8_t i = 0; i < MAX_SENSORS; registered_ha_[i++] = false) ; } @@ -308,8 +303,10 @@ void Sensor::publish_values() { return; } + uint8_t mqtt_format_ = Mqtt::mqtt_format(); + // single mode as e.g. ems-esp/sensor_28-EA41-9497-0E03-5F = {"temp":20.2} - if (mqtt_format_ == MQTT_format::SINGLE) { + if (mqtt_format_ == Mqtt::Format::SINGLE) { StaticJsonDocument<100> doc; for (const auto & device : devices_) { char topic[60]; @@ -328,10 +325,10 @@ void Sensor::publish_values() { for (const auto & device : devices_) { char s[7]; - if (mqtt_format_ == MQTT_format::CUSTOM) { + if (mqtt_format_ == Mqtt::Format::CUSTOM) { // e.g. sensor_data = {28-EA41-9497-0E03-5F":23.30,"28-233D-9497-0C03-8B":24.0} doc[device.to_string()] = Helpers::render_value(s, device.temperature_c, 1); - } else if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::HA)) { + } else if ((mqtt_format_ == Mqtt::Format::NESTED) || (mqtt_format_ == Mqtt::Format::HA)) { // e.g. sensor_data = {"sensor1":{"id":"28-EA41-9497-0E03-5F","temp":"23.30"},"sensor2":{"id":"28-233D-9497-0C03-8B","temp":"24.0"}} char sensorID[20]; // sensor{1-n} strlcpy(sensorID, "sensor", 20); @@ -342,34 +339,31 @@ void Sensor::publish_values() { } // special for HA - if (mqtt_format_ == MQTT_format::HA) { + if (mqtt_format_ == Mqtt::Format::HA) { std::string topic(100, '\0'); // create the config if this hasn't already been done - /* e.g. - { - "dev_cla": "temperature", - "stat_t": "homeassistant/sensor/ems-esp/state", - "unit_of_meas": "°C", - "val_tpl": "{{value_json.sensor2.temp}}", - "name": "ems-esp-sensor2", - "uniq_id": "ems-esp-sensor2" - } - */ + // to e.g. homeassistant/sensor/ems-esp/dallas_sensor1/config if (!(registered_ha_[i])) { StaticJsonDocument config; - config["dev_cla"] = "temperature"; - config["stat_t"] = "homeassistant/sensor/ems-esp/state"; - config["unit_of_meas"] = "°C"; + config["dev_cla"] = F("temperature"); + config["stat_t"] = F("ems-esp/sensor_data"); + config["unit_of_meas"] = F("°C"); std::string str(50, '\0'); snprintf_P(&str[0], 50, PSTR("{{value_json.sensor%d.temp}}"), i); config["val_tpl"] = str; - snprintf_P(&str[0], 50, PSTR("ems-esp-sensor%d"), i); - config["name"] = str; + snprintf_P(&str[0], 50, PSTR("Dallas sensor%d"), i); + config["name"] = str; + + snprintf_P(&str[0], 50, PSTR("dalas_sensor%d"), i); config["uniq_id"] = str; - snprintf_P(&topic[0], 50, PSTR("homeassistant/sensor/ems-esp/sensor%d/config"), i); + JsonObject dev = config.createNestedObject("dev"); + JsonArray ids = dev.createNestedArray("ids"); + ids.add("ems-esp"); + + snprintf_P(&topic[0], 60, PSTR("homeassistant/sensor/ems-esp/dallas_sensor%d/config"), i); Mqtt::publish_retain(topic, config.as(), false); // publish the config payload with no retain flag registered_ha_[i] = true; @@ -378,10 +372,8 @@ void Sensor::publish_values() { i++; // increment sensor count } - if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::CUSTOM)) { + if (mqtt_format_ != Mqtt::Format::SINGLE) { Mqtt::publish(F("sensor_data"), doc.as()); - } else if (mqtt_format_ == MQTT_format::HA) { - Mqtt::publish(F("homeassistant/sensor/ems-esp/state"), doc.as()); } } diff --git a/src/sensor.h b/src/sensor.h index e9e0b09a3..3e231a4e3 100644 --- a/src/sensor.h +++ b/src/sensor.h @@ -111,7 +111,6 @@ class Sensor { bool registered_ha_[MAX_SENSORS]; - uint8_t mqtt_format_; uint8_t retrycnt_ = 0; uint8_t dallas_gpio_ = 0; bool parasite_ = false; diff --git a/src/shower.cpp b/src/shower.cpp index f6fc1be51..f38622af5 100644 --- a/src/shower.cpp +++ b/src/shower.cpp @@ -27,6 +27,10 @@ void Shower::start() { shower_timer_ = settings.shower_timer; shower_alert_ = settings.shower_alert; }); + + if (Mqtt::mqtt_format() == Mqtt::Format::HA) { + Mqtt::register_mqtt_ha_binary_sensor(F("Shower Active"), "shower_active"); + } } void Shower::loop() { @@ -115,11 +119,11 @@ void Shower::shower_alert_start() { // returns true if added to MQTT queue went ok void Shower::publish_values() { StaticJsonDocument<90> doc; - doc["shower_timer"] = shower_timer_ ? "1" : "0"; - doc["shower_alert"] = shower_alert_ ? "1" : "0"; + char s[50]; + doc["shower_timer"] = Helpers::render_boolean(s, shower_timer_); + doc["shower_alert"] = Helpers::render_boolean(s, shower_alert_); // only publish shower duration if there is a value - char s[50]; if (duration_ > SHOWER_MIN_DURATION) { char buffer[16] = {0}; strlcpy(s, Helpers::itoa(buffer, (uint8_t)((duration_ / (1000 * 60)) % 60), 10), 50); diff --git a/src/system.cpp b/src/system.cpp index c756c0749..942c1dfef 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -130,10 +130,6 @@ void System::syslog_init() { #endif } -void System::set_heartbeat(bool system_heartbeat) { - system_heartbeat_ = system_heartbeat; -} - // first call. Sets memory and starts up the UART Serial bridge void System::start() { // set the inital free mem @@ -145,9 +141,6 @@ void System::start() { #endif } - // fetch system heartbeat - EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { system_heartbeat_ = settings.system_heartbeat; }); - // print boot message EMSESP::esp8266React.getWiFiSettingsService()->read( [&](WiFiSettings & wifiSettings) { LOG_INFO(F("System %s booted (EMS-ESP version %s)"), wifiSettings.hostname.c_str(), EMSESP_APP_VERSION); }); @@ -216,9 +209,7 @@ void System::loop() { uint32_t currentMillis = uuid::get_uptime(); if (!last_heartbeat_ || (currentMillis - last_heartbeat_ > SYSTEM_HEARTBEAT_INTERVAL)) { last_heartbeat_ = currentMillis; - if (system_heartbeat_) { send_heartbeat(); - } } #if defined(ESP8266) @@ -249,6 +240,16 @@ void System::send_heartbeat() { } StaticJsonDocument doc; + + uint8_t ems_status = EMSESP::bus_status(); + if (ems_status == EMSESP::BUS_STATUS_TX_ERRORS) { + doc["status"] = "txerror"; + } else if (ems_status == EMSESP::BUS_STATUS_CONNECTED) { + doc["status"] = "connected"; + } else { + doc["status"] = "disconnected"; + } + doc["rssid"] = rssid; doc["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3); doc["uptime_sec"] = uuid::get_uptime_sec(); @@ -725,7 +726,7 @@ bool System::check_upgrade() { EMSESP::esp8266React.getMqttSettingsService()->update( [&](MqttSettings & mqttSettings) { mqttSettings.host = mqtt["ip"] | FACTORY_MQTT_HOST; - mqttSettings.mqtt_format = (mqtt["nestedjson"] ? MQTT_format::NESTED : MQTT_format::SINGLE); + mqttSettings.mqtt_format = (mqtt["nestedjson"] ? Mqtt::Format::NESTED : Mqtt::Format::SINGLE); mqttSettings.mqtt_qos = mqtt["qos"] | 0; mqttSettings.mqtt_retain = mqtt["retain"]; mqttSettings.username = mqtt["user"] | ""; @@ -733,7 +734,6 @@ bool System::check_upgrade() { mqttSettings.port = mqtt["port"] | FACTORY_MQTT_PORT; mqttSettings.clientId = FACTORY_MQTT_CLIENT_ID; mqttSettings.enabled = mqtt["enabled"]; - mqttSettings.system_heartbeat = mqtt["heartbeat"]; mqttSettings.keepAlive = FACTORY_MQTT_KEEP_ALIVE; mqttSettings.cleanSession = FACTORY_MQTT_CLEAN_SESSION; mqttSettings.maxTopicLength = FACTORY_MQTT_MAX_TOPIC_LENGTH; @@ -865,7 +865,6 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & outp node["keep_alive"] = settings.keepAlive; node["clean_session"] = Helpers::render_boolean(s, settings.cleanSession); node["max_topic_length"] = settings.maxTopicLength; - node["system_heartbeat"] = Helpers::render_boolean(s, settings.system_heartbeat); node["publish_time_boiler"] = settings.publish_time_boiler; node["publish_time_thermostat"] = settings.publish_time_thermostat; node["publish_time_solar"] = settings.publish_time_solar; diff --git a/src/system.h b/src/system.h index 991f70deb..28059ed98 100644 --- a/src/system.h +++ b/src/system.h @@ -61,7 +61,6 @@ class System { bool check_upgrade(); void syslog_init(); - void set_heartbeat(bool system_heartbeat); void send_heartbeat(); private: @@ -99,7 +98,6 @@ class System { static uint16_t analog_; // settings - bool system_heartbeat_; static bool hide_led_; uint8_t syslog_level_; uint32_t syslog_mark_interval_; diff --git a/src/test/test.cpp b/src/test/test.cpp index 41b586de0..e73f78efc 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -28,7 +28,7 @@ namespace emsesp { // used with the 'test' command, under su/admin void Test::run_test(uuid::console::Shell & shell, const std::string & command) { if (command == "default") { - run_test(shell, "cmd"); // add the default test case here + run_test(shell, "mqtt"); // add the default test case here } if (command.empty()) { @@ -143,6 +143,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { uart_telegram({0x08, 0x0B, 0x14, 00, 0x3C, 0x1F, 0xAC, 0x70}); } + // check for boiler and controller on same product_id if (command == "double") { // question: do we need to set the mask? std::string version("1.2.3"); @@ -572,22 +573,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { if (command == "cmd") { shell.printfln(F("Testing Commands...")); - // change MQTT format - EMSESP::esp8266React.getMqttSettingsService()->updateWithoutPropagation([&](MqttSettings & mqttSettings) { - mqttSettings.mqtt_format = MQTT_format::SINGLE; - // mqttSettings.mqtt_format = MQTT_format::NESTED; - // mqttSettings.mqtt_format = MQTT_format::HA; - return StateUpdateResult::CHANGED; - }); - - shell.invoke_command("su"); - shell.invoke_command("call"); - shell.invoke_command("call system info"); - - char system_topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; - strcpy(system_topic, "ems-esp/system"); - EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"info\"}"); // this should fail - // add a thermostat with 3 HCs std::string version("1.2.3"); EMSESP::add_device(0x10, 192, version, EMSdevice::Brand::JUNKERS); // FW120 @@ -596,6 +581,19 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { uart_telegram({0x90, 0x00, 0xFF, 0x00, 0x00, 0x70, 0x00, 0xCF, 0x22, 0x2F, 0x10, 0x00, 0x2E, 0x24, 0x03, 0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03}); // HC2 uart_telegram({0x90, 0x00, 0xFF, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); // HC3 + + shell.invoke_command("help"); + shell.invoke_command("su"); + shell.invoke_command("call"); + shell.invoke_command("call system info"); + + char system_topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; + strcpy(system_topic, "ems-esp/system"); + EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"info\"}"); // this should fail + + shell.invoke_command("call thermostat wwmode"); // should do nothing + shell.invoke_command("call thermostat mode auto 2"); // should error, no hc2 + shell.invoke_command("call thermostat temp 22.56"); } if (command == "pin") { @@ -609,10 +607,13 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { if (command == "mqtt") { shell.printfln(F("Testing MQTT...")); - // EMSESP::esp8266React.getMqttSettingsService()->updateWithoutPropagation([&](MqttSettings & mqttSettings) { - // mqttSettings.mqtt_format = MQTT_format::SINGLE; - // return StateUpdateResult::CHANGED; - // }); + // change MQTT format + EMSESP::esp8266React.getMqttSettingsService()->updateWithoutPropagation([&](MqttSettings & mqttSettings) { + // mqttSettings.mqtt_format = Mqtt::Format::SINGLE; + // mqttSettings.mqtt_format = Mqtt::Format::NESTED; + mqttSettings.mqtt_format = Mqtt::Format::HA; + return StateUpdateResult::CHANGED; + }); // add a boiler // question: do we need to set the mask? @@ -636,46 +637,48 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & command) { // test publish and adding to queue EMSESP::txservice_.flush_tx_queue(); + EMSESP::EMSESP::mqtt_.publish("boiler", "test me"); Mqtt::show_mqtt(shell); // show queue strcpy(boiler_topic, "ems-esp/boiler"); strcpy(thermostat_topic, "ems-esp/thermostat"); - strcpy(system_topic, "ems-esp/saystem"); + strcpy(system_topic, "ems-esp/system"); + + EMSESP::mqtt_.incoming(boiler_topic, ""); // test if ignore empty payloads // invalid format EMSESP::mqtt_.incoming(boiler_topic, "12345"); // invalid format EMSESP::mqtt_.incoming("bad_topic", "12345"); // no matching topic EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"garbage\",\"data\":22.52}"); // should report error + EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"comfort\",\"data\":\"eco\"}"); - EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"wwactivated\",\"data\":\"1\"}"); - EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"wwactivated\",\"data\":1}"); + EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"wwactivated\",\"data\":\"1\"}"); // with quotes + EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"wwactivated\",\"data\":1}"); // without quotes EMSESP::mqtt_.incoming(boiler_topic, "{\"cmd\":\"flowtemp\",\"data\":55}"); EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"send\",\"data\":\"01 02 03 04 05\"}"); EMSESP::mqtt_.incoming(system_topic, "{\"cmd\":\"pin\",\"id\":12,\"data\":\"1\"}"); EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"wwmode\",\"data\":\"auto\"}"); - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"control\",\"data\":\"1\"}"); - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"control\",\"data\":1}"); + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"control\",\"data\":\"1\"}"); // RC35 only, should error + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"poep\",\"id\":2}"); // invalid mode EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"id\":2}"); EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"hc\":2}"); // hc as number EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":19.5,\"hc\":1}"); // data as number - EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"hc\":\"2\"}"); // hc as string + EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"mode\",\"data\":\"auto\",\"hc\":\"2\"}"); // hc as string. should error as no hc2 EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":22.56}"); EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":22}"); EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"data\":\"22.56\"}"); EMSESP::mqtt_.incoming(thermostat_topic, "{\"cmd\":\"temp\",\"id\":2,\"data\":22}"); + // test single commands + EMSESP::mqtt_.incoming(thermostat_topic, "auto"); + EMSESP::mqtt_.incoming(thermostat_topic, "heat"); + EMSESP::mqtt_.incoming(thermostat_topic, "28.8"); + // EMSESP::txservice_.show_tx_queue(); // EMSESP::publish_all_values(); - shell.invoke_command("su"); - shell.invoke_command("help"); - shell.invoke_command("call"); - shell.invoke_command("call thermostat wwmode"); - shell.invoke_command("call thermostat mode auto 2"); - shell.invoke_command("call thermostat temp 22.56"); - Mqtt::resubscribe(); Mqtt::show_mqtt(shell); // show queue }