mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
MQTT updates: added HA discovery, removed heartbeat - HomeAssistant Discovery #288
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -36,5 +36,4 @@ export interface MqttSettings {
|
||||
mqtt_format: number;
|
||||
mqtt_qos: number;
|
||||
mqtt_retain: boolean;
|
||||
system_heartbeat: boolean;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -59,8 +59,6 @@
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
enum MQTT_format : uint8_t { SINGLE = 1, NESTED, HA, CUSTOM };
|
||||
|
||||
class EMSESPSettings {
|
||||
public:
|
||||
uint8_t tx_mode;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
264
src/mqtt.cpp
264
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::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
|
||||
|
||||
16
src/mqtt.h
16
src/mqtt.h
@@ -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
|
||||
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user