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

@@ -30,7 +30,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value="enabled"
/>
}
label="Enable MQTT?"
label="Enable MQTT"
/>
<TextValidator
validators={['required', 'isIPOrHostname']}
@@ -137,7 +137,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value="clean_session"
/>
}
label="Clean Session?"
label="Clean Session"
/>
<BlockFormControlLabel
control={
@@ -149,16 +149,6 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
}
label="Retain Flag"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.system_heartbeat}
onChange={handleValueChange('system_heartbeat')}
value="system_heartbeat"
/>
}
label="Heartbeat"
/>
<br></br>
<Typography variant="h6" color="primary" >
Publish Intervals

View File

@@ -36,5 +36,4 @@ export interface MqttSettings {
mqtt_format: number;
mqtt_qos: number;
mqtt_retain: boolean;
system_heartbeat: boolean;
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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";

View File

@@ -59,8 +59,6 @@
namespace emsesp {
enum MQTT_format : uint8_t { SINGLE = 1, NESTED, HA, CUSTOM };
class EMSESPSettings {
public:
uint8_t tx_mode;

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);

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);

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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::MQTTSubFunction> 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<EMSESP_MAX_JSON_SIZE_SMALL> 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<char *>()) {
cmd_known = Command::call(mf.device_type_, command, data.as<char *>(), n, output);
} else if (data.is<int>()) {
char data_str[10];
cmd_known = Command::call(mf.device_type_, command, Helpers::itoa(data_str, (int16_t)data.as<int>()), n, output);
} else if (data.is<float>()) {
char data_str[10];
cmd_known = Command::call(mf.device_type_, command, Helpers::render_value(data_str, (float)data.as<float>(), 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<EMSESP_MAX_JSON_SIZE_SMALL> 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<char *>()) {
cmd_known = Command::call(mf.device_type_, command, data.as<char *>(), n, output);
} else if (data.is<int>()) {
char data_str[10];
cmd_known = Command::call(mf.device_type_, command, Helpers::itoa(data_str, (int16_t)data.as<int>()), n, output);
} else if (data.is<float>()) {
char data_str[10];
cmd_known = Command::call(mf.device_type_, command, Helpers::render_value(data_str, (float)data.as<float>(), 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<EMSESP_MAX_JSON_SIZE_MEDIUM> 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<JsonObject>(), 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<const MqttMessage> 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<MqttMessage>(operation, full_topic, std::move(payload), retain);
message = std::make_shared<MqttMessage>(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<EMSESP_MAX_JSON_SIZE_MEDIUM> 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<JsonObject>(), 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<EMSESP_MAX_JSON_SIZE_MEDIUM> 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<JsonObject>(), true); // publish the config payload with retain flag
}
} // namespace emsesp

View File

@@ -44,7 +44,7 @@ using uuid::console::Shell;
namespace emsesp {
using mqtt_subfunction_p = std::function<void(const char * message)>;
using mqtt_subfunction_p = std::function<bool(const char * message)>;
using cmdfunction_p = std::function<bool(const char * data, const int8_t id)>;
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

View File

@@ -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<EMSESP_MAX_JSON_SIZE_MEDIUM> 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<JsonObject>(), 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<JsonObject>());
} else if (mqtt_format_ == MQTT_format::HA) {
Mqtt::publish(F("homeassistant/sensor/ems-esp/state"), doc.as<JsonObject>());
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<EMSESP_MAX_JSON_SIZE_SMALL> 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;

View File

@@ -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_;

View File

@@ -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
}