This commit is contained in:
Paul
2020-05-18 21:00:38 +02:00
parent 97ddf80ea8
commit be4f075663
18 changed files with 270 additions and 141 deletions

View File

@@ -8,24 +8,25 @@ Note: Version 2.0 is not backward compatible with v1.0. The File system structur
## Major changes since version 1.9.x ## Major changes since version 1.9.x
### **Design & Coding principles** ### **Design & coding principles**
- The code can be built and run without an ESP microcontroller, which is useful with testing and simulating handling of telegrams. Make sure you have GNU make and g++ installed and use 'make' to build the image and execute the file `emsesp` (on linux). - The code can be built and run without an ESP microcontroller, which is useful when testing and simulating handling of the different telegrams and devices. Make sure you have GNU make and g++ installed and use 'make' to build the image and execute the file `emsesp` (on linux).
- I used C++11 containers where I could (std::string, std::deque, std::list, std::multimap etc). - I used C++11 containers where I could (std::string, std::deque, std::list, std::multimap etc).
- The core is based off the great libraries from @nomis' and I adopted his general design pattens such as making everything as asynchronous as possible so that no one operation should starve another operation of it's time to execute (https://isocpp.org/wiki/faq/ctors#static-init-order). - The core is based off the great libraries from @nomis' and I adopted his general design pattens such as making everything as asynchronous as possible so that no one operation should starve another operation of it's time to execute (https://isocpp.org/wiki/faq/ctors#static-init-order).
- All EMS devices (e.g. boiler, thermostat, solar modules etc) are derived from a factory base class and each class handles its own registering of telegram and mqtt handlers. This makes the EMS device code easier to manage and extend with new telegrams types and features. - All EMS devices (e.g. boiler, thermostat, solar modules etc) are derived from a factory base class and each class handles its own registering of telegram and mqtt handlers. This makes the EMS device code easier to manage and extend with new telegrams types and features.
- Built to work with both EMS8266 and ESP32. - Built to work with both EMS8266 and ESP32.
- Extended MQTT to use MQTT discovery on Home Assistant, just for the thermostat
### **Features** ### **Features**
- A web interface built using React and TypeScript to be secure and cross-browser compatible. Each restful endpoint is protected and issues a JWT which is then sent using Bearer Authentication. Implements a Web captive portal. On first installs EMS-ESP starts an Access Point where system settings can be configured. Note, this is still in a separate repo and pending a merge into this project. - A web interface built using React and TypeScript to be secure and cross-browser compatible. Each restful endpoint is protected and issues a JWT which is then sent using Bearer Authentication. Implements a Web captive portal. On first installs EMS-ESP starts an Access Point where system settings can be configured. Note, this is still in a separate repo and pending a merge into this project.
- A new console. Like 1.9.x it works with both Serial and Telnet but a lot more intuitive behaving like a Linux shell and secure. Multiple telnet sessions are supported now but watch out for slow connections and low memory. A password is need to change any settings. You can use TAB to auto-complete commands. Some key commands: - A new console. Like 1.9.x it works with both Serial and Telnet but a lot more intuitive behaving like a Linux shell and secure. Multiple telnet sessions are supported now but watch out for slow connections and low memory. A password is need to change any settings. You can use TAB to auto-complete commands, ctrl-L, ctrl-U and the other typical console type shortcuts. Some key commands:
* `help` lists the commands and keywords * `help` lists the commands and keywords
* some commands take you into a new context, a bit like a sub-menu. e.g. `system`, `mqtt`, `thermostat`. Use `help` to show which commands this context has and `exit` to get back to the root. * some commands take you into a new context, a bit like a sub-menu. e.g. `system`, `mqtt`, `thermostat`. Use `help` to show which commands this context has and `exit` to get back to the root.
* To change a setting use the `set` command. Typing `set` shows the current settings. * To change a setting use the `set` command. Typing `set` shows the current settings.
* `show` shows the data specific to the context you're in. * `show` shows the data specific to the context you're in.
* `su` to switch to Admin which enables more commands such as most of the `set` commands. The default password is "neo". When in Admin mode the command prompt switches from `$` to `#`. * `su` to switch to Admin which enables more commands such as most of the `set` commands. The default password is "neo" which can be changed with `passwd` from the system menu. When in Admin mode the command prompt switches from `$` to `#`.
* `log` sets the logging. `log off` disables logging. Use `log trace` to see the telegram traffic and `log debug` for very verbose logging. To watch a specific telegram ID or device ID use `log trace [id]`. * `log` sets the logging. `log off` disables logging. Use `log trace` to see the telegram traffic and `log debug` for very verbose logging. To watch a specific telegram ID or device ID use `log trace [id]`.
- There is no "serial mode" anymore like with version 1.9. When the Wifi cannot connect to the SSID it will automatically enter a "safe" mode where the Serial console is activated, baud 115200. Note Serial is always available on the ESP32 because it has 2 UARTs. - There is no "serial mode" anymore like with version 1.9. When the Wifi cannot connect to the SSID it will automatically enter a "safe" mode where the Serial console is activated, baud 115200. Note Serial is always available on the ESP32 because it has 2 UARTs.
@@ -34,7 +35,7 @@ Note: Version 2.0 is not backward compatible with v1.0. The File system structur
- on a new install you will want to enter `su` and then go to the `system` context. Use `set wifi ...` to set the network up. Then go to the `mqtt` context to set the mqtt up. - on a new install you will want to enter `su` and then go to the `system` context. Use `set wifi ...` to set the network up. Then go to the `mqtt` context to set the mqtt up.
# Full Console # Full Console Commands
``` ```
common commands available in all contexts: common commands available in all contexts:
@@ -43,15 +44,15 @@ common commands available in all contexts:
log [level] [trace ID] log [level] [trace ID]
su su
top level/root (top level)
refresh refresh
show show
show version show version
ems (is a menu) ems (enters a context)
mqtt (is a menu) mqtt (enters a context)
system (is a menu) system (enters a context)
boiler (is a menu) boiler (enters a context)
thermostat (is a menu) thermostat (enters a context)
ems ems
scan devices [deep] scan devices [deep]
@@ -72,7 +73,7 @@ mqtt
set enabled <on | off> set enabled <on | off>
set heartbeat <on | off> set heartbeat <on | off>
set ip <IP address> set ip <IP address>
set nested_json <on | off> set format <single | nested | ha>
set password set password
set publish_time <seconds> set publish_time <seconds>
set qos <n> set qos <n>
@@ -125,8 +126,8 @@ thermostat
``` ```
TODO figure out why sometimes telnet on ESP32 (and sometimes ESP8266) has slow response times. After a manual reset it seems to fix itself. Perhaps the telnet service needs to start after the wifi is up & running. TODO figure out why sometimes telnet on ESP32 (and sometimes ESP8266) has slow response times. After a manual reset it seems to fix itself. Perhaps the telnet service needs to start after the wifi is up & running.
TODO Get the ESP32 UART code working. TODO Get the ESP32 UART code working.
TODO sometimes with tx_mode 0 there are a few CRC errors due to collision when waiting for a BRK signal.
TODO console auto-complete with 'set' command in the system context is not showing all commands, only the hostname. TODO console auto-complete with 'set' command in the system context is not showing all commands, only the hostname.
TODO sometimes get an error after a Tx send when first booting up. timeout perhaps?
``` ```
### **Features to add next** ### **Features to add next**
@@ -139,7 +140,6 @@ TODO validate 0xE9 with data from Koen. (https://github.com/proddy/EMS-ESP/issue
``` ```
TODO replace vectors of class objects with shared pointers and use emplace_back since it instantiates during construction. It may have a performance gain. TODO replace vectors of class objects with shared pointers and use emplace_back since it instantiates during construction. It may have a performance gain.
TODO decide if we really need to store the timestamp of each incoming Rx telegram.
TODO make more use of comparison operators in the Telegram class e.g. the compare like "friend inline bool operator==(const Telegram & lhs, const Telegram & rhs)" TODO make more use of comparison operators in the Telegram class e.g. the compare like "friend inline bool operator==(const Telegram & lhs, const Telegram & rhs)"
TODO exit from serial should be prevented? Because you never can really exit, just close it. TODO exit from serial should be prevented? Because you never can really exit, just close it.
TODO add real unit tests using platformio's test bed (https://docs.platformio.org/en/latest/plus/pio-remote.html) TODO add real unit tests using platformio's test bed (https://docs.platformio.org/en/latest/plus/pio-remote.html)
@@ -149,10 +149,6 @@ TODO See if it's easier to use timers instead of millis() timers, using https://
### **These features to add next** ### **These features to add next**
``` ```
TODO merge in the web code TODO merge in the web code which has the Captive AP and better wifi reconnect logic. Use IPV6 and NTP from lwip2
TODO merge in NTP code
TODO make ascii colors in the console optional?
TODO decide what to do with gateways, switches and other bogus EMS devices
TODO add MQTT subscribe topic to toggle on/off the shower alert and timer. If really needed.
TODO decide if I want to port over the shower one-shot cold water logic. Don't think its used. TODO decide if I want to port over the shower one-shot cold water logic. Don't think its used.
``` ```

View File

@@ -2,7 +2,7 @@
; For EMS-ESP ; For EMS-ESP
[platformio] [platformio]
;default_envs = esp8266 default_envs = esp8266
;default_envs = esp32 ;default_envs = esp32
# override any settings with your own local ones in pio_local.ini # override any settings with your own local ones in pio_local.ini
@@ -45,11 +45,22 @@ libs_esp32 =
[env] [env]
build_unflags = -fno-rtti ; for dynamic_cast<> build_unflags = -fno-rtti ; for dynamic_cast<>
;lib_ldf_mode = chain+ lib_ldf_mode = chain+
lib_compat_mode = strict
extra_scripts = scripts/main_script.py extra_scripts = scripts/main_script.py
framework = arduino framework = arduino
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 921600 upload_speed = 921600
check_tool = cppcheck, clangtidy
check_severity = high, medium
check_flags =
cppcheck: --std=c++11
clangtidy: --checks=-*,clang-analyzer-*,performance-*
; example ports for OSX
;upload_port = /dev/cu.wchusbserial14403
;upload_port = /dev/cu.usbserial-1440
;upload_port = /dev/cu.SLAB_USBtoUART
; OTA ; OTA
upload_protocol = esptool upload_protocol = esptool
@@ -63,8 +74,8 @@ upload_protocol = esptool
build_type = release build_type = release
platform = espressif8266 ; https://github.com/platformio/platform-espressif8266/releases platform = espressif8266 ; https://github.com/platformio/platform-espressif8266/releases
;platform = espressif8266@2.4.0 ; Arduino core 2.6.3 ;platform = espressif8266@2.4.0 ; Arduino core 2.6.3
; board = esp12e board = esp12e
board = d1_mini ; https://github.com/platformio/platform-espressif8266/blob/master/boards/d1_mini.json ; board = d1_mini ; https://github.com/platformio/platform-espressif8266/blob/master/boards/d1_mini.json
lib_deps = ${common.libs_core} ${common.libs_esp8266} lib_deps = ${common.libs_core} ${common.libs_esp8266}
board_build.f_cpu = 160000000L ; 160MHz board_build.f_cpu = 160000000L ; 160MHz
;board_build.ldscript = eagle.flash.4m1m.ld ; 1019 KB sketch, 1000 KB SPIFFS. 4KB EEPROM, 4KB RFCAL, 12KB WIFI stack, 2052 KB OTA & buffer ;board_build.ldscript = eagle.flash.4m1m.ld ; 1019 KB sketch, 1000 KB SPIFFS. 4KB EEPROM, 4KB RFCAL, 12KB WIFI stack, 2052 KB OTA & buffer

View File

@@ -83,6 +83,7 @@ MAKE_PSTR_WORD(disconnect)
MAKE_PSTR_WORD(debug) MAKE_PSTR_WORD(debug)
MAKE_PSTR_WORD(restart) MAKE_PSTR_WORD(restart)
MAKE_PSTR_WORD(reconnect) MAKE_PSTR_WORD(reconnect)
MAKE_PSTR_WORD(format)
// context menus // context menus
MAKE_PSTR_WORD(mqtt) MAKE_PSTR_WORD(mqtt)

View File

@@ -187,7 +187,7 @@ void EMSdevice::show_mqtt_handlers(uuid::console::Shell & shell) {
} }
void EMSdevice::register_mqtt_topic(const std::string & topic, mqtt_function_p f) { void EMSdevice::register_mqtt_topic(const std::string & topic, mqtt_function_p f) {
DEBUG_LOG(F("Registering new MQTT topic for device ID %02X"), this->device_id_); DEBUG_LOG(F("Registering MQTT topic %s for device ID %02X"), topic.c_str(), this->device_id_);
Mqtt::subscribe(this->device_id_, topic, f); Mqtt::subscribe(this->device_id_, topic, f);
} }

View File

@@ -570,7 +570,8 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
// are we waiting for a response from a recent Tx Read or Write? // are we waiting for a response from a recent Tx Read or Write?
if (EMSbus::tx_waiting()) { if (EMSbus::tx_waiting()) {
// if it's a single byte 1 or 4 then its a response from the last write // if it's a single byte 1 or 4 then its maybe a response from the last write action
EMSbus::tx_waiting(false); // reset Tx wait state
if (length == 1) { if (length == 1) {
if (first_value == TxService::TX_WRITE_SUCCESS) { if (first_value == TxService::TX_WRITE_SUCCESS) {
DEBUG_LOG(F("Last Tx write successful. Sending read request.")); DEBUG_LOG(F("Last Tx write successful. Sending read request."));
@@ -581,13 +582,12 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
DEBUG_LOG(F("Last Tx write rejected by host")); DEBUG_LOG(F("Last Tx write rejected by host"));
txservice_.send_poll(); // close the bus txservice_.send_poll(); // close the bus
} else { } else {
#ifdef EMSESP_DEBUG // ignore it, it's probably a poll and we can wait for the next one
logger_.err(F("Expecting Tx ACK (1/4) but got 0x%02X. Tx:%s"), first_value, txservice_.last_tx_to_string().c_str()); return;
#endif
} }
} else { } else {
// got a telegram. See if the src/dest matches that from the last one we sent // got a telegram with data in it. See if the src/dest matches that from the last one we sent
// and continue to process it
uint8_t src = data[0]; uint8_t src = data[0];
uint8_t dest = data[1]; uint8_t dest = data[1];
if (txservice_.is_last_tx(src, dest)) { if (txservice_.is_last_tx(src, dest)) {
@@ -605,18 +605,21 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
} }
} }
} }
EMSbus::tx_waiting(false); // reset Tx wait state
} }
// check for poll, if so, send Tx from the queue immediately // check for poll
// if ht3 poll must be ems_bus_id else if Buderus poll must be (ems_bus_id | 0x80) if (length == 1) {
if ((length == 1) && ((first_value ^ 0x80 ^ rxservice_.ems_mask()) == txservice_.ems_bus_id())) { // check for poll to us, if so send top message from Tx queue immediately and quit
EMSbus::last_bus_activity(millis()); // set the flag indication the EMS bus is active // if ht3 poll must be ems_bus_id else if Buderus poll must be (ems_bus_id | 0x80)
txservice_.send(); if ((first_value ^ 0x80 ^ rxservice_.ems_mask()) == txservice_.ems_bus_id()) {
EMSbus::last_bus_activity(millis()); // set the flag indication the EMS bus is active
txservice_.send();
}
return;
} else {
// add to RxQueue, what ever it is.
rxservice_.add(data, length);
} }
// add to RxQueue
rxservice_.add(data, length);
} }
// sends raw data of bytes along the Tx line // sends raw data of bytes along the Tx line

View File

@@ -145,10 +145,15 @@ void Mixing::process_MMPLUSStatusMessage_WWC(std::shared_ptr<const Telegram> tel
} }
// Mixing on a MM10 - 0xAB // Mixing on a MM10 - 0xAB
// We assume MM10 is on HC2 and WM10 is using HC1 - https://github.com/proddy/EMS-ESP/issues/270 // 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) { void Mixing::process_MMStatusMessage(std::shared_ptr<const Telegram> telegram) {
type_ = Type::HC; type_ = Type::HC;
hc_ = 1; // fixed to circuit 1
// 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_ = 0x22 - device_id();
telegram->read_value(flowTemp_, 1); // is * 10 telegram->read_value(flowTemp_, 1); // is * 10
telegram->read_value(pumpMod_, 3); telegram->read_value(pumpMod_, 3);
telegram->read_value(flowSetTemp_, 0); telegram->read_value(flowSetTemp_, 0);

View File

@@ -24,7 +24,9 @@ MAKE_PSTR_WORD(qos)
MAKE_PSTR_WORD(base) MAKE_PSTR_WORD(base)
MAKE_PSTR_WORD(heartbeat) MAKE_PSTR_WORD(heartbeat)
MAKE_PSTR_WORD(ip) MAKE_PSTR_WORD(ip)
MAKE_PSTR_WORD(nested_json) MAKE_PSTR_WORD(nested)
MAKE_PSTR_WORD(single)
MAKE_PSTR_WORD(ha)
MAKE_PSTR_WORD(publish_time) MAKE_PSTR_WORD(publish_time)
MAKE_PSTR_WORD(publish) MAKE_PSTR_WORD(publish)
MAKE_PSTR_WORD(connected) MAKE_PSTR_WORD(connected)
@@ -38,7 +40,7 @@ MAKE_PSTR(mqtt_enabled_fmt, "MQTT is %s")
MAKE_PSTR(mqtt_base_fmt, "Base = %s") MAKE_PSTR(mqtt_base_fmt, "Base = %s")
MAKE_PSTR(mqtt_qos_fmt, "QOS = %ld") MAKE_PSTR(mqtt_qos_fmt, "QOS = %ld")
MAKE_PSTR(mqtt_retain_fmt, "Retain Flag = %s") MAKE_PSTR(mqtt_retain_fmt, "Retain Flag = %s")
MAKE_PSTR(mqtt_nestedjson_fmt, "Use nested JSON = %s") MAKE_PSTR(mqtt_format_fmt, "JSON format = %s")
MAKE_PSTR(mqtt_heartbeat_fmt, "Heartbeat = %s") MAKE_PSTR(mqtt_heartbeat_fmt, "Heartbeat = %s")
MAKE_PSTR(mqtt_publish_time_fmt, "Publish time = %d seconds") MAKE_PSTR(mqtt_publish_time_fmt, "Publish time = %d seconds")
@@ -54,6 +56,7 @@ std::vector<Mqtt::MQTTFunction> Mqtt::mqtt_functions_;
bool Mqtt::mqtt_retain_; bool Mqtt::mqtt_retain_;
uint8_t Mqtt::mqtt_qos_; uint8_t Mqtt::mqtt_qos_;
std::string Mqtt::mqtt_hostname_; // copy of hostname std::string Mqtt::mqtt_hostname_; // copy of hostname
uint8_t Mqtt::mqtt_format_;
std::string Mqtt::mqtt_base_; std::string Mqtt::mqtt_base_;
uint16_t Mqtt::mqtt_publish_fails_ = 0; uint16_t Mqtt::mqtt_publish_fails_ = 0;
size_t Mqtt::maximum_mqtt_messages_ = Mqtt::MAX_MQTT_MESSAGES; size_t Mqtt::maximum_mqtt_messages_ = Mqtt::MAX_MQTT_MESSAGES;
@@ -71,9 +74,8 @@ Mqtt::QueuedMqttMessage::QueuedMqttMessage(uint16_t id, std::shared_ptr<MqttMess
packet_id_ = 0; packet_id_ = 0;
} }
MqttMessage::MqttMessage(uint64_t uptime_ms, uint8_t operation, const std::string & topic, const std::string & payload, bool retain) MqttMessage::MqttMessage(uint8_t operation, const std::string & topic, const std::string & payload, bool retain)
: uptime_ms(uptime_ms) : operation(operation)
, operation(operation)
, topic(topic) , topic(topic)
, payload(payload) , payload(payload)
, retain(retain) { , retain(retain) {
@@ -107,6 +109,7 @@ void Mqtt::start() {
mqtt_hostname_ = settings.hostname(); mqtt_hostname_ = settings.hostname();
mqtt_base_ = settings.mqtt_base(); mqtt_base_ = settings.mqtt_base();
mqtt_qos_ = settings.mqtt_qos(); mqtt_qos_ = settings.mqtt_qos();
mqtt_format_ = settings.mqtt_format();
mqtt_retain_ = settings.mqtt_retain(); mqtt_retain_ = settings.mqtt_retain();
mqtt_heartbeat_ = settings.mqtt_heartbeat(); mqtt_heartbeat_ = settings.mqtt_heartbeat();
mqtt_publish_time_ = settings.mqtt_publish_time() * 1000; // convert to seconds mqtt_publish_time_ = settings.mqtt_publish_time() * 1000; // convert to seconds
@@ -196,8 +199,12 @@ Mqtt::MQTTFunction::MQTTFunction(uint8_t device_id, const std::string && topic,
// subscribe to an MQTT topic, and store the associated callback function // subscribe to an MQTT topic, and store the associated callback function
void Mqtt::subscribe(const uint8_t device_id, const std::string & topic, mqtt_function_p cb) { void Mqtt::subscribe(const uint8_t device_id, const std::string & topic, mqtt_function_p cb) {
mqtt_functions_.emplace_back(device_id, std::move(topic), cb); // register a call back function for a specific telegram type // We don't want to store the whole topic string in our lookup, just the last cmd, as this is wasteful.
queue_subscribe_message(topic); // add subscription to queue // strip out everything until the last /
size_t found = topic.find_last_of("/"); // returns npos which is -1
mqtt_functions_.emplace_back(device_id, std::move(topic.substr(found + 1)), cb); // register a call back function for a specific telegram type
queue_subscribe_message(topic); // add subscription to queue
} }
// subscribe to an MQTT topic, and store the associated callback function. For generic functions not tied to a device // subscribe to an MQTT topic, and store the associated callback function. For generic functions not tied to a device
@@ -224,7 +231,7 @@ void Mqtt::loop() {
force_publish_ = false; force_publish_ = false;
send_heartbeat(); // create a heartbeat payload send_heartbeat(); // create a heartbeat payload
EMSESP::publish_all_values(); // add sensors and mqtt to queue EMSESP::publish_all_values(); // add sensors and mqtt to queue
publish_all_queue(); // publish everything on queue process_all_queue(); // publish everything on queue
} }
// send out heartbeat // send out heartbeat
@@ -243,7 +250,7 @@ void Mqtt::loop() {
// publish top item from MQTT queue to stop flooding // publish top item from MQTT queue to stop flooding
if ((uint32_t)(currentMillis - last_mqtt_poll_) > MQTT_PUBLISH_WAIT) { if ((uint32_t)(currentMillis - last_mqtt_poll_) > MQTT_PUBLISH_WAIT) {
last_mqtt_poll_ = currentMillis; last_mqtt_poll_ = currentMillis;
publish_queue(); process_queue();
} }
return; return;
@@ -325,7 +332,7 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) {
return; return;
} }
// convert payload to a null-terminate char string // convert payload to a null-terminated char string
char message[len + 2]; char message[len + 2];
strlcpy(message, payload, len + 1); strlcpy(message, payload, len + 1);
@@ -333,7 +340,8 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) {
DEBUG_LOG(F("Received %s => %s (length %d)"), topic, message, len); DEBUG_LOG(F("Received %s => %s (length %d)"), topic, message, len);
#endif #endif
char * topic_magnitude = strrchr(topic, '/'); // strip out everything until last / // strip out everything until the last /
char * topic_magnitude = strrchr(topic, '/');
if (topic_magnitude != nullptr) { if (topic_magnitude != nullptr) {
topic = topic_magnitude + 1; topic = topic_magnitude + 1;
} }
@@ -341,7 +349,7 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) {
// Send message event to custom service // Send message event to custom service
// this will pick the first topic that matches, so for multiple devices of the same type it's gonna fail // this will pick the first topic that matches, so for multiple devices of the same type it's gonna fail
for (const auto & mf : mqtt_functions_) { for (const auto & mf : mqtt_functions_) {
if (topic == mf.topic_) { if (strcmp(topic, mf.topic_.c_str()) == 0) {
(mf.mqtt_function_)(message); (mf.mqtt_function_)(message);
return; return;
} }
@@ -470,7 +478,7 @@ void Mqtt::queue_publish_message(const char * topic, const JsonDocument & payloa
std::string payload_text; std::string payload_text;
serializeJson(payload, payload_text); serializeJson(payload, payload_text);
auto message = std::make_shared<MqttMessage>(uuid::get_uptime_ms(), Operation::PUBLISH, topic, payload_text, retain); auto message = std::make_shared<MqttMessage>(Operation::PUBLISH, topic, payload_text, retain);
// DEBUG_LOG(F("Adding JSON publish message created with topic %s, message %s"), topic, payload_text.c_str()); // DEBUG_LOG(F("Adding JSON publish message created with topic %s, message %s"), topic, payload_text.c_str());
@@ -484,11 +492,12 @@ void Mqtt::queue_publish_message(const char * topic, const JsonDocument & payloa
// add MQTT message to queue, payload is a string // add MQTT message to queue, payload is a string
void Mqtt::queue_publish_message(const char * topic, const std::string & payload, const bool retain) { void Mqtt::queue_publish_message(const char * topic, const std::string & payload, const bool retain) {
// can't have bogus topics, but empty payloads are ok
if (strlen(topic) == 0) { if (strlen(topic) == 0) {
return; return;
} }
auto message = std::make_shared<MqttMessage>(uuid::get_uptime_ms(), Operation::PUBLISH, topic, payload, retain); auto message = std::make_shared<MqttMessage>(Operation::PUBLISH, topic, payload, retain);
// if the queue is full, make room but removing the last one // if the queue is full, make room but removing the last one
if (mqtt_messages_.size() >= maximum_mqtt_messages_) { if (mqtt_messages_.size() >= maximum_mqtt_messages_) {
@@ -498,13 +507,13 @@ void Mqtt::queue_publish_message(const char * topic, const std::string & payload
mqtt_messages_.emplace_back(mqtt_message_id_++, std::move(message)); mqtt_messages_.emplace_back(mqtt_message_id_++, std::move(message));
} }
// add MQTT message to queue, payload is a string // add MQTT subscribe message to queue
void Mqtt::queue_subscribe_message(const std::string & topic) { void Mqtt::queue_subscribe_message(const std::string & topic) {
if (topic.empty()) { if (topic.empty()) {
return; return;
} }
auto message = std::make_shared<MqttMessage>(uuid::get_uptime_ms(), Operation::SUBSCRIBE, topic, "", false); auto message = std::make_shared<MqttMessage>(Operation::SUBSCRIBE, topic, "", false);
DEBUG_LOG(F("Adding a subscription for %s"), topic.c_str()); DEBUG_LOG(F("Adding a subscription for %s"), topic.c_str());
// if the queue is full, make room but removing the last one // if the queue is full, make room but removing the last one
@@ -537,16 +546,21 @@ void Mqtt::publish(const char * topic, const bool value) {
queue_publish_message(topic, value ? "1" : "0", mqtt_retain_); queue_publish_message(topic, value ? "1" : "0", mqtt_retain_);
} }
// no payload
void Mqtt::publish(const char * topic) {
queue_publish_message(topic, "", mqtt_retain_);
}
// publish all queued messages to MQTT // publish all queued messages to MQTT
void Mqtt::publish_all_queue() { void Mqtt::process_all_queue() {
while (!mqtt_messages_.empty()) { while (!mqtt_messages_.empty()) {
publish_queue(); process_queue();
} }
} }
// take top from queue and try and publish it // take top from queue and try and publish it
// assumes there is an MQTT connection // assumes there is an MQTT connection
void Mqtt::publish_queue() { void Mqtt::process_queue() {
if (mqtt_messages_.empty()) { if (mqtt_messages_.empty()) {
return; return;
} }
@@ -554,8 +568,16 @@ void Mqtt::publish_queue() {
// fetch first from queue and create the full topic name // fetch first from queue and create the full topic name
auto mqtt_message = mqtt_messages_.front(); auto mqtt_message = mqtt_messages_.front();
auto message = mqtt_message.content_; auto message = mqtt_message.content_;
// append the hostname and base to the topic, unless we're doing native HA which has a different format
char full_topic[MQTT_TOPIC_MAX_SIZE]; char full_topic[MQTT_TOPIC_MAX_SIZE];
make_topic(full_topic, message->topic);
// if the topic starts with "homeassistant" we leave it untouched, otherwise append ho st and base
if (strncmp(message->topic.c_str(), "homeassistant/", 13) == 0) {
strcpy(full_topic, message->topic.c_str());
} else {
make_topic(full_topic, message->topic);
}
// if we're subscribing... // if we're subscribing...
if (message->operation == Operation::SUBSCRIBE) { if (message->operation == Operation::SUBSCRIBE) {
@@ -586,7 +608,7 @@ void Mqtt::publish_queue() {
#else #else
uint16_t packet_id = 1; uint16_t packet_id = 1;
#endif #endif
DEBUG_LOG(F("Published topic %s (#%02d, attempt #%d, pid %d)"), full_topic, mqtt_message.id_, mqtt_message.retry_count_ + 1, packet_id); DEBUG_LOG(F("Publishing topic %s (#%02d, attempt #%d, pid %d)"), full_topic, mqtt_message.id_, mqtt_message.retry_count_ + 1, packet_id);
if (packet_id == 0) { if (packet_id == 0) {
// it failed. if we retried n times, give up. remove from queue // it failed. if we retried n times, give up. remove from queue
@@ -612,7 +634,7 @@ void Mqtt::publish_queue() {
} }
mqtt_messages_.pop_front(); // remove the message from the queue mqtt_messages_.pop_front(); // remove the message from the queue
} } // namespace emsesp
// add console commands // add console commands
void Mqtt::console_commands() { void Mqtt::console_commands() {
@@ -641,25 +663,27 @@ void Mqtt::console_commands() {
EMSESPShell::commands->add_command( EMSESPShell::commands->add_command(
ShellContext::MQTT, ShellContext::MQTT,
CommandFlags::ADMIN, CommandFlags::ADMIN,
flash_string_vector{F_(set), F_(nested_json)}, flash_string_vector{F_(set), F_(format)},
flash_string_vector{F_(bool_mandatory)}, flash_string_vector{F_(name_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) { [](Shell & shell, const std::vector<std::string> & arguments) {
Settings settings; Settings settings;
if (arguments[0] == read_flash_string(F_(on))) { uint8_t value;
settings.mqtt_nestedjson(true); if (arguments[0] == read_flash_string(F_(single))) {
settings.commit(); value = Settings::MQTT_format::SINGLE;
shell.println(F("Please restart EMS-ESP")); } else if (arguments[0] == read_flash_string(F_(nested))) {
} else if (arguments[0] == read_flash_string(F_(off))) { value = Settings::MQTT_format::NESTED;
settings.mqtt_nestedjson(false); } else if (arguments[0] == read_flash_string(F_(ha))) {
settings.commit(); value = Settings::MQTT_format::HA;
shell.println(F("Please restart EMS-ESP"));
} else { } else {
shell.println(F("Must be on or off")); shell.println(F("Must be single, nested or ha"));
return; return;
} }
settings.mqtt_format(value);
settings.commit();
shell.println(F("Please restart EMS-ESP"));
}, },
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> const std::vector<std::string> { [](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> const std::vector<std::string> {
return std::vector<std::string>{read_flash_string(F_(on)), read_flash_string(F_(off))}; return std::vector<std::string>{read_flash_string(F_(single)), read_flash_string(F_(nested)), read_flash_string(F_(ha))};
}); });
EMSESPShell::commands->add_command(ShellContext::MQTT, EMSESPShell::commands->add_command(ShellContext::MQTT,
@@ -818,11 +842,17 @@ void Mqtt::console_commands() {
shell.printfln(F_(mqtt_base_fmt), settings.mqtt_base().empty() ? uuid::read_flash_string(F_(unset)).c_str() : settings.mqtt_base().c_str()); shell.printfln(F_(mqtt_base_fmt), settings.mqtt_base().empty() ? uuid::read_flash_string(F_(unset)).c_str() : settings.mqtt_base().c_str());
shell.printfln(F_(mqtt_qos_fmt), settings.mqtt_qos()); shell.printfln(F_(mqtt_qos_fmt), settings.mqtt_qos());
shell.printfln(F_(mqtt_retain_fmt), settings.mqtt_retain() ? F_(enabled) : F_(disabled)); shell.printfln(F_(mqtt_retain_fmt), settings.mqtt_retain() ? F_(enabled) : F_(disabled));
shell.printfln(F_(mqtt_nestedjson_fmt), settings.mqtt_nestedjson() ? F_(enabled) : F_(disabled)); if (settings.mqtt_format() == Settings::MQTT_format::SINGLE) {
shell.printfln(F_(mqtt_format_fmt), F_(single));
} else if (settings.mqtt_format() == Settings::MQTT_format::NESTED) {
shell.printfln(F_(mqtt_format_fmt), F_(nested));
} else if (settings.mqtt_format() == Settings::MQTT_format::HA) {
shell.printfln(F_(mqtt_format_fmt), F_(ha));
}
shell.printfln(F_(mqtt_heartbeat_fmt), settings.mqtt_heartbeat() ? F_(enabled) : F_(disabled)); shell.printfln(F_(mqtt_heartbeat_fmt), settings.mqtt_heartbeat() ? F_(enabled) : F_(disabled));
shell.printfln(F_(mqtt_publish_time_fmt), settings.mqtt_publish_time()); shell.printfln(F_(mqtt_publish_time_fmt), settings.mqtt_publish_time());
shell.println(); shell.println();
}); });
} } // namespace emsesp
} // namespace emsesp } // namespace emsesp

View File

@@ -49,10 +49,9 @@ using mqtt_function_p = std::function<void(const char * message)>;
using namespace std::placeholders; // for `_1` using namespace std::placeholders; // for `_1`
struct MqttMessage { struct MqttMessage {
MqttMessage(uint64_t uptime_ms, uint8_t operation, const std::string & topic, const std::string & payload, bool retain); MqttMessage(uint8_t operation, const std::string & topic, const std::string & payload, bool retain);
~MqttMessage() = default; ~MqttMessage() = default;
const uint64_t uptime_ms;
const uint8_t operation; const uint8_t operation;
const std::string topic; const std::string topic;
const std::string payload; const std::string payload;
@@ -75,6 +74,7 @@ class Mqtt {
static void publish(const char * topic, const JsonDocument & payload); static void publish(const char * topic, const JsonDocument & payload);
static void publish(const char * topic, const JsonDocument & payload, bool retain); static void publish(const char * topic, const JsonDocument & payload, bool retain);
static void publish(const char * topic, const bool value); static void publish(const char * topic, const bool value);
static void publish(const char * topic);
static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_id); static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_id);
@@ -139,8 +139,8 @@ class Mqtt {
void on_message(char * topic, char * payload, size_t len); void on_message(char * topic, char * payload, size_t len);
void on_connect(); void on_connect();
static char * make_topic(char * result, const std::string & topic); static char * make_topic(char * result, const std::string & topic);
void publish_queue(); void process_queue();
void publish_all_queue(); void process_all_queue();
void send_start_topic(); void send_start_topic();
static void reconnect(); static void reconnect();
void init(); void init();
@@ -177,6 +177,7 @@ class Mqtt {
static std::string mqtt_hostname_; static std::string mqtt_hostname_;
static std::string mqtt_base_; static std::string mqtt_base_;
static uint8_t mqtt_qos_; static uint8_t mqtt_qos_;
static uint8_t mqtt_format_;
std::string mqtt_ip_; std::string mqtt_ip_;
std::string mqtt_user_; std::string mqtt_user_;
std::string mqtt_password_; std::string mqtt_password_;

View File

@@ -28,7 +28,7 @@ uuid::log::Logger Sensors::logger_{F_(logger_name), uuid::log::Facility::DAEMON}
void Sensors::start() { void Sensors::start() {
// copy over values from MQTT so we don't keep on quering the filesystem // copy over values from MQTT so we don't keep on quering the filesystem
mqtt_nestedjson_ = Settings().mqtt_nestedjson(); mqtt_format_ = Settings().mqtt_format();
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
bus_.begin(SENSOR_GPIO); bus_.begin(SENSOR_GPIO);
@@ -39,34 +39,36 @@ void Sensors::loop() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
if (state_ == State::IDLE) { if (state_ == State::IDLE) {
if (millis() - last_activity_ >= READ_INTERVAL_MS) { if (millis() - last_activity_ >= READ_INTERVAL_MS) {
// DEBUG_LOG(F("Read sensor temperature")); // DEBUG_LOG(F("Read sensor temperature")); // uncomment for debug
if (bus_.reset()) { if (bus_.reset()) {
bus_.skip(); bus_.skip();
bus_.write(CMD_CONVERT_TEMP); bus_.write(CMD_CONVERT_TEMP);
state_ = State::READING; state_ = State::READING;
} else { } else {
// logger_.err(F("Bus reset failed")); // no sensors found
// logger_.err(F("Bus reset failed")); // uncomment for debug
devices_.clear(); // remove all know devices incase we have a disconnect
} }
last_activity_ = millis(); last_activity_ = millis();
} }
} else if (state_ == State::READING) { } else if (state_ == State::READING) {
if (temperature_convert_complete()) { if (temperature_convert_complete()) {
// DEBUG_LOG(F("Scanning for sensors")); // DEBUG_LOG(F("Scanning for sensors")); // uncomment for debug
bus_.reset_search(); bus_.reset_search();
found_.clear(); found_.clear();
state_ = State::SCANNING; state_ = State::SCANNING;
last_activity_ = millis(); last_activity_ = millis();
} else if (millis() - last_activity_ > READ_TIMEOUT_MS) { } else if (millis() - last_activity_ > READ_TIMEOUT_MS) {
// logger_.err(F("Sensor read timeout")); logger_.err(F("Sensor read timeout"));
state_ = State::IDLE; state_ = State::IDLE;
last_activity_ = millis(); last_activity_ = millis();
} }
} else if (state_ == State::SCANNING) { } else if (state_ == State::SCANNING) {
if (millis() - last_activity_ > SCAN_TIMEOUT_MS) { if (millis() - last_activity_ > SCAN_TIMEOUT_MS) {
// logger_.err(F("Sensor scan timeout")); logger_.err(F("Sensor scan timeout"));
state_ = State::IDLE; state_ = State::IDLE;
last_activity_ = millis(); last_activity_ = millis();
} else { } else {
@@ -84,8 +86,13 @@ void Sensors::loop() {
found_.emplace_back(addr); found_.emplace_back(addr);
found_.back().temperature_c_ = get_temperature_c(addr); found_.back().temperature_c_ = get_temperature_c(addr);
// char result[10]; /*
// DEBUG_LOG(F("Temperature of %s = %s"), found_.back().to_string().c_str(), Helpers::render_value(result, found_.back().temperature_c_, 2)); // comment out for debugging
char result[10];
DEBUG_LOG(F("Temp of %s = %s"),
found_.back().to_string().c_str(),
Helpers::render_value(result, found_.back().temperature_c_, 2));
*/
break; break;
default: default:
@@ -99,7 +106,7 @@ void Sensors::loop() {
bus_.depower(); bus_.depower();
devices_ = std::move(found_); devices_ = std::move(found_);
found_.clear(); found_.clear();
// DEBUG_LOG(F("Found %zu sensor(s)"), devices_.size()); // DEBUG_LOG(F("Found %zu sensor(s). Adding them."), devices_.size()); // uncomment for debug
state_ = State::IDLE; state_ = State::IDLE;
last_activity_ = millis(); last_activity_ = millis();
} }
@@ -220,14 +227,14 @@ void Sensors::publish_values() {
// if we're not using nested JSON, send each sensor out seperately // if we're not using nested JSON, send each sensor out seperately
// sensor1, sensor2 etc... // sensor1, sensor2 etc...
// e.g. sensor_1 = {"temp":20.2} // e.g. sensor_1 = {"temp":20.2}
if (!mqtt_nestedjson_) { if (mqtt_format_ != Settings::MQTT_format::NESTED) {
StaticJsonDocument<20> doc; StaticJsonDocument<100> doc;
for (const auto & device : devices_) { for (const auto & device : devices_) {
char s[5]; char s[5];
doc["temp"] = Helpers::render_value(s, device.temperature_c_, 2); doc["temp"] = Helpers::render_value(s, device.temperature_c_, 2);
char topic[60]; // sensors{1-n} char topic[60]; // sensors{1-n}
strlcpy(topic, "sensor_", 50); // create topic strlcpy(topic, "sensor_", 50); // create topic, e.g. home/ems-esp/sensor_28-EA41-9497-0E03-5F
strlcat(topic, device.to_string().c_str(), 50); strlcat(topic, device.to_string().c_str(), 60);
Mqtt::publish(topic, doc); Mqtt::publish(topic, doc);
doc.clear(); // clear json doc so we can reuse the buffer again doc.clear(); // clear json doc so we can reuse the buffer again
} }

View File

@@ -74,7 +74,7 @@ class Sensors {
static constexpr size_t SCRATCHPAD_TEMP_LSB = 0; static constexpr size_t SCRATCHPAD_TEMP_LSB = 0;
static constexpr size_t SCRATCHPAD_CONFIG = 4; static constexpr size_t SCRATCHPAD_CONFIG = 4;
// chips // dallas chips
static constexpr uint8_t TYPE_DS18B20 = 0x28; static constexpr uint8_t TYPE_DS18B20 = 0x28;
static constexpr uint8_t TYPE_DS18S20 = 0x10; static constexpr uint8_t TYPE_DS18S20 = 0x10;
static constexpr uint8_t TYPE_DS1822 = 0x22; static constexpr uint8_t TYPE_DS1822 = 0x22;
@@ -102,7 +102,7 @@ class Sensors {
std::vector<Device> found_; std::vector<Device> found_;
std::vector<Device> devices_; std::vector<Device> devices_;
bool mqtt_nestedjson_; uint8_t mqtt_format_;
}; };
} // namespace emsesp } // namespace emsesp

View File

@@ -50,7 +50,7 @@ namespace emsesp {
EMSESP_SETTINGS_SIMPLE(std::string, "", mqtt_base, "", (), EMSESP_DEFAULT_MQTT_BASE) \ EMSESP_SETTINGS_SIMPLE(std::string, "", mqtt_base, "", (), EMSESP_DEFAULT_MQTT_BASE) \
EMSESP_SETTINGS_SIMPLE(uint8_t, "", mqtt_qos, "", (), EMSESP_DEFAULT_MQTT_QOS) \ EMSESP_SETTINGS_SIMPLE(uint8_t, "", mqtt_qos, "", (), EMSESP_DEFAULT_MQTT_QOS) \
EMSESP_SETTINGS_SIMPLE(bool, "", mqtt_retain, "", (), EMSESP_DEFAULT_MQTT_RETAIN) \ EMSESP_SETTINGS_SIMPLE(bool, "", mqtt_retain, "", (), EMSESP_DEFAULT_MQTT_RETAIN) \
EMSESP_SETTINGS_SIMPLE(bool, "", mqtt_nestedjson, "", (), EMSESP_DEFAULT_MQTT_NESTEDJSON) \ EMSESP_SETTINGS_SIMPLE(uint8_t, "", mqtt_format, "", (), EMSESP_DEFAULT_MQTT_FORMAT) \
EMSESP_SETTINGS_SIMPLE(bool, "", mqtt_heartbeat, "", (), EMSESP_DEFAULT_MQTT_HEARTBEAT) EMSESP_SETTINGS_SIMPLE(bool, "", mqtt_heartbeat, "", (), EMSESP_DEFAULT_MQTT_HEARTBEAT)
#define EMSESP_SETTINGS_SIMPLE EMSESP_SETTINGS_GENERIC #define EMSESP_SETTINGS_SIMPLE EMSESP_SETTINGS_GENERIC

View File

@@ -51,7 +51,7 @@
#define EMSESP_DEFAULT_MQTT_PORT 1883 #define EMSESP_DEFAULT_MQTT_PORT 1883
#define EMSESP_DEFAULT_MQTT_QOS 0 #define EMSESP_DEFAULT_MQTT_QOS 0
#define EMSESP_DEFAULT_MQTT_RETAIN false #define EMSESP_DEFAULT_MQTT_RETAIN false
#define EMSESP_DEFAULT_MQTT_NESTEDJSON true #define EMSESP_DEFAULT_MQTT_FORMAT 2 // nested
#define EMSESP_DEFAULT_MQTT_HEARTBEAT true #define EMSESP_DEFAULT_MQTT_HEARTBEAT true
#define EMSESP_DEFAULT_EMS_READ_ONLY false #define EMSESP_DEFAULT_EMS_READ_ONLY false
#define EMSESP_DEFAULT_SHOWER_TIMER false #define EMSESP_DEFAULT_SHOWER_TIMER false
@@ -136,9 +136,6 @@ class Settings {
bool mqtt_retain() const; bool mqtt_retain() const;
void mqtt_retain(const bool & mqtt_retain); void mqtt_retain(const bool & mqtt_retain);
bool mqtt_nestedjson() const;
void mqtt_nestedjson(const bool & mqtt_nestedjson);
bool mqtt_heartbeat() const; bool mqtt_heartbeat() const;
void mqtt_heartbeat(const bool & mqtt_heartbeat); void mqtt_heartbeat(const bool & mqtt_heartbeat);
@@ -151,6 +148,10 @@ class Settings {
uint8_t master_thermostat() const; uint8_t master_thermostat() const;
void master_thermostat(const uint8_t & master_thermostat); void master_thermostat(const uint8_t & master_thermostat);
enum MQTT_format : uint8_t { SINGLE = 1, NESTED, HA };
uint8_t mqtt_format() const;
void mqtt_format(const uint8_t & mqtt_format);
private: private:
static constexpr size_t BUFFER_SIZE = 2048; // max size for the settings file static constexpr size_t BUFFER_SIZE = 2048; // max size for the settings file
@@ -194,9 +195,9 @@ class Settings {
static std::string mqtt_base_; static std::string mqtt_base_;
static uint8_t mqtt_qos_; static uint8_t mqtt_qos_;
static bool mqtt_retain_; static bool mqtt_retain_;
static bool mqtt_nestedjson_;
static bool mqtt_heartbeat_; static bool mqtt_heartbeat_;
static uint16_t mqtt_publish_time_; // frequency of MQTT publish in seconds static uint16_t mqtt_publish_time_; // seconds
static uint8_t mqtt_format_;
}; };
} // namespace emsesp } // namespace emsesp

View File

@@ -25,7 +25,6 @@ MAKE_PSTR_WORD(mark)
MAKE_PSTR_WORD(level) MAKE_PSTR_WORD(level)
MAKE_PSTR_WORD(host) MAKE_PSTR_WORD(host)
MAKE_PSTR_WORD(passwd) MAKE_PSTR_WORD(passwd)
MAKE_PSTR_WORD(format)
MAKE_PSTR_WORD(hostname) MAKE_PSTR_WORD(hostname)
MAKE_PSTR_WORD(wifi) MAKE_PSTR_WORD(wifi)
MAKE_PSTR_WORD(ssid) MAKE_PSTR_WORD(ssid)

View File

@@ -189,9 +189,8 @@ void Telegram::read_value8(int16_t & param, const uint8_t index) const {
param = message_data[pos]; param = message_data[pos];
} }
RxService::QueuedRxTelegram::QueuedRxTelegram(uint16_t id, uint32_t timestamp, std::shared_ptr<Telegram> && telegram) RxService::QueuedRxTelegram::QueuedRxTelegram(uint16_t id, std::shared_ptr<Telegram> && telegram)
: id_(id) : id_(id)
, timestamp_(timestamp)
, telegram_(std::move(telegram)) { , telegram_(std::move(telegram)) {
} }
@@ -238,11 +237,6 @@ void RxService::loop() {
// data is the whole telegram, assuming last byte holds the CRC // data is the whole telegram, assuming last byte holds the CRC
// for EMS+ the type_id has the value + 256. We look for these type of telegrams with F7, F9 and FF in 3rd byte // for EMS+ the type_id has the value + 256. We look for these type of telegrams with F7, F9 and FF in 3rd byte
void RxService::add(uint8_t * data, uint8_t length) { void RxService::add(uint8_t * data, uint8_t length) {
// ignore any telegrams which are 1 byte
if (length <= 1) {
return;
}
// validate the CRC // validate the CRC
uint8_t crc = calculate_crc(data, length - 1); uint8_t crc = calculate_crc(data, length - 1);
if (data[length - 1] != crc) { if (data[length - 1] != crc) {
@@ -313,8 +307,8 @@ void RxService::add(uint8_t * data, uint8_t length) {
} }
// add to queue, with timestamp // add to queue, with timestamp
DEBUG_LOG(F("New Rx [%d] telegram added, length %d"), rx_telegram_id_, message_length); DEBUG_LOG(F("New Rx [#%d] telegram added, length %d"), rx_telegram_id_, message_length);
rx_telegrams_.emplace_back(rx_telegram_id_++, millis(), std::move(telegram)); rx_telegrams_.emplace_back(rx_telegram_id_++, std::move(telegram));
} }

View File

@@ -187,11 +187,10 @@ class RxService : public EMSbus {
class QueuedRxTelegram { class QueuedRxTelegram {
public: public:
QueuedRxTelegram(uint16_t id, uint32_t timestamp, std::shared_ptr<Telegram> && telegram); QueuedRxTelegram(uint16_t id, std::shared_ptr<Telegram> && telegram);
~QueuedRxTelegram() = default; ~QueuedRxTelegram() = default;
uint16_t id_; // sequential identifier uint16_t id_; // sequential identifier
uint32_t timestamp_; // time it was received
const std::shared_ptr<const Telegram> telegram_; const std::shared_ptr<const Telegram> telegram_;
}; };

View File

@@ -114,24 +114,76 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i
uint8_t master_thermostat = settings.master_thermostat(); // what the user has defined uint8_t master_thermostat = settings.master_thermostat(); // what the user has defined
uint8_t actual_master_thermostat = EMSESP::actual_master_thermostat(); // what we're actually using uint8_t actual_master_thermostat = EMSESP::actual_master_thermostat(); // what we're actually using
uint8_t num_devices = EMSESP::count_devices(EMSdevice::DeviceType::THERMOSTAT) + 1; // including this thermostat uint8_t num_devices = EMSESP::count_devices(EMSdevice::DeviceType::THERMOSTAT) + 1; // including this thermostat
mqtt_nested_json_ = settings.mqtt_nestedjson(); mqtt_format_ = settings.mqtt_format(); // single, nested or ha
// if we're on auto mode, register this first one we find as we may find multiple // if we're on auto mode, register this thermostat if it has a device id of 0x10 or 0x17
// or if its the master thermostat we defined // or if its the master thermostat we defined
if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT)) || (master_thermostat == device_id)) { // see https://github.com/proddy/EMS-ESP/issues/362#issuecomment-629628161
if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT) && ((device_id == 0x10) || (device_id == 0x17)))
|| (master_thermostat == device_id)) {
EMSESP::actual_master_thermostat(device_id); EMSESP::actual_master_thermostat(device_id);
DEBUG_LOG(F("Registering new thermostat with device ID 0x%02X (as the master)"), device_id); DEBUG_LOG(F("Registering new thermostat with device ID 0x%02X (as the master)"), device_id);
init_mqtt();
// MQTT callbacks
register_mqtt_topic("thermostat_cmd", std::bind(&Thermostat::thermostat_cmd, this, _1));
register_mqtt_topic("thermostat_cmd_temp", std::bind(&Thermostat::thermostat_cmd_temp, this, _1));
register_mqtt_topic("thermostat_cmd_mode", std::bind(&Thermostat::thermostat_cmd_mode, this, _1));
} else { } else {
DEBUG_LOG(F("Registering new thermostat with device ID 0x%02X"), device_id); DEBUG_LOG(F("Registering new thermostat with device ID 0x%02X"), device_id);
} }
} }
// for the master thermostat initialize the MQTT subscribes
void Thermostat::init_mqtt() {
register_mqtt_topic("thermostat_cmd", std::bind(&Thermostat::thermostat_cmd, this, _1)); // generic commands
// if the MQTT format type is ha then send the config to HA (via the mqtt discovery service)
// for each of the heating circuits
if (mqtt_format_ == Settings::MQTT_format::HA) {
for (uint8_t hc = 0; hc < monitor_typeids.size(); hc++) {
std::string topic(100, '\0'); // e.g homeassistant/climate/hc1/thermostat/config
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/hc%d/thermostat/config"), hc + 1);
// Mqtt::publish(topic.c_str()); // empty payload, this remove any previous config sent to HA
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
std::string payload(100, '\0');
snprintf_P(&payload[0], payload.capacity() + 1, PSTR("thermostat_hc%d"), hc + 1);
doc["name"] = payload; // "name": "thermostat_hc1"
doc["unique_id"] = payload; // "unique_id": "thermostat_hc1"
snprintf_P(&payload[0], payload.capacity() + 1, PSTR("homeassistant/climate/hc%d/thermostat"), hc + 1);
doc["~"] = payload; // "homeassistant/climate/hc1/thermostat"
doc["mode_cmd_t"] = "~/cmd_mode";
doc["mode_stat_t"] = "~/state";
doc["mode_stat_tpl"] = "{{value_json.mode}}";
doc["temp_cmd_t"] = "~/cmd_temp";
doc["temp_stat_t"] = "~/state";
doc["temp_stat_tpl"] = "{{value_json.seltemp}}";
doc["curr_temp_t"] = "~/state";
doc["curr_temp_tpl"] = "{{value_json.currtemp}}";
doc["min_temp"] = "5";
doc["max_temp"] = "40";
doc["temp_step"] = "0.5";
JsonArray modes = doc.createNestedArray("modes");
modes.add("off");
modes.add("heat");
modes.add("auto");
Mqtt::publish(topic.c_str(), doc, 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/hc%d/thermostat/cmd_temp"), hc + 1);
register_mqtt_topic(topic, std::bind(&Thermostat::thermostat_cmd_temp, this, _1));
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/hc%d/thermostat/cmd_mode"), hc + 1);
register_mqtt_topic(topic, std::bind(&Thermostat::thermostat_cmd_mode, this, _1));
}
} else {
// these will be prefixed with hostname and base
register_mqtt_topic("thermostat_cmd_temp", std::bind(&Thermostat::thermostat_cmd_temp, this, _1));
register_mqtt_topic("thermostat_cmd_mode", std::bind(&Thermostat::thermostat_cmd_mode, this, _1));
}
}
// only add the menu for the master thermostat // only add the menu for the master thermostat
void Thermostat::add_context_menu() { void Thermostat::add_context_menu() {
if (device_id() != EMSESP::actual_master_thermostat()) { if (device_id() != EMSESP::actual_master_thermostat()) {
@@ -292,8 +344,8 @@ void Thermostat::publish_values() {
JsonObject rootThermostat = doc.to<JsonObject>(); JsonObject rootThermostat = doc.to<JsonObject>();
JsonObject dataThermostat; JsonObject dataThermostat;
// optional, add external temp // optional, add external temp. I don't think anyone actually is interested in this
if (flags == EMS_DEVICE_FLAG_RC35) { if ((flags == EMS_DEVICE_FLAG_RC35) && (mqtt_format_ == Settings::MQTT_format::SINGLE)) {
if (dampedoutdoortemp != EMS_VALUE_INT_NOTSET) { if (dampedoutdoortemp != EMS_VALUE_INT_NOTSET) {
doc["dampedtemp"] = dampedoutdoortemp; doc["dampedtemp"] = dampedoutdoortemp;
} }
@@ -312,7 +364,7 @@ void Thermostat::publish_values() {
} }
has_data = true; has_data = true;
if (mqtt_nested_json_) { if (mqtt_format_ == Settings::MQTT_format::NESTED) {
// create nested json for each HC // create nested json for each HC
char hc_name[10]; // hc{1-4} char hc_name[10]; // hc{1-4}
strlcpy(hc_name, "hc", 10); strlcpy(hc_name, "hc", 10);
@@ -371,25 +423,54 @@ void Thermostat::publish_values() {
} }
if (hc->mode != EMS_VALUE_UINT_NOTSET) { if (hc->mode != EMS_VALUE_UINT_NOTSET) {
dataThermostat["mode"] = mode_tostring(hc->get_mode(flags)); 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_ == Settings::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)) {
hc_mode = HeatingCircuit::Mode::OFF;
} else {
hc_mode = HeatingCircuit::Mode::AUTO;
}
}
dataThermostat["mode"] = mode_tostring(hc_mode);
} }
if (hc->mode_type != EMS_VALUE_UINT_NOTSET) {
// special handling of mode type, for the RC35 replace with summer/holiday
// https://github.com/proddy/EMS-ESP/issues/373#issuecomment-619810209
if ((flags & 0x0F) == EMS_DEVICE_FLAG_RC35) {
if (hc->holiday_mode != EMS_VALUE_UINT_NOTSET) {
dataThermostat["modetype"] = F("holiday");
} else if (hc->summer_mode != EMS_VALUE_UINT_NOTSET) {
dataThermostat["modetype"] = F("summer");
} else if (hc->mode_type != EMS_VALUE_UINT_NOTSET) {
dataThermostat["modetype"] = mode_tostring(hc->get_mode_type(flags));
}
} else if (hc->mode_type != EMS_VALUE_UINT_NOTSET) {
dataThermostat["modetype"] = mode_tostring(hc->get_mode_type(flags)); dataThermostat["modetype"] = mode_tostring(hc->get_mode_type(flags));
} }
// if its not nested, send immediately
if (!mqtt_nested_json_) { // if format is single, send immediately
// if its HA send it to the special topic
if (mqtt_format_ == Settings::MQTT_format::SINGLE) {
char topic[30]; char topic[30];
char s[3]; // for formatting strings char s[3]; // for formatting strings
strlcpy(topic, "thermostat_data", 30); strlcpy(topic, "thermostat_data", 30);
strlcat(topic, Helpers::itoa(s, hc->hc_num()), 30); // append hc to topic strlcat(topic, Helpers::itoa(s, hc->hc_num()), 30); // append hc to topic
Mqtt::publish(topic, doc); Mqtt::publish(topic, doc);
return; return;
} else if (mqtt_format_ == Settings::MQTT_format::HA) {
std::string topic(100, '\0');
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/hc%d/thermostat/state"), hc->hc_num());
Mqtt::publish(topic.c_str(), doc);
return;
} }
} }
// if we're using nested json, send all in one go // if we're using nested json, send all in one go
if (mqtt_nested_json_ && has_data) { if ((mqtt_format_ == Settings::MQTT_format::NESTED) && has_data) {
Mqtt::publish("thermostat_data", doc); Mqtt::publish("thermostat_data", doc);
} }
} }
@@ -408,7 +489,7 @@ std::shared_ptr<Thermostat::HeatingCircuit> Thermostat::heating_circuit(const ui
} }
// determine which heating circuit the type ID is referring too // determine which heating circuit the type ID is referring too
// returns pointer to the HeatingCircuit // returns pointer to the HeatingCircuit or nullptr if it can't be found
std::shared_ptr<Thermostat::HeatingCircuit> Thermostat::heating_circuit(std::shared_ptr<const Telegram> telegram) { std::shared_ptr<Thermostat::HeatingCircuit> Thermostat::heating_circuit(std::shared_ptr<const Telegram> telegram) {
// look through the Monitor and Set arrays to see if there is a match // look through the Monitor and Set arrays to see if there is a match
uint8_t hc_num = 0; uint8_t hc_num = 0;
@@ -430,9 +511,9 @@ std::shared_ptr<Thermostat::HeatingCircuit> Thermostat::heating_circuit(std::sha
} }
} }
// still didn't recognize it, assume its hc 1 // still didn't recognize it, ignore it
if (hc_num == 0) { if (hc_num == 0) {
hc_num = DEFAULT_HEATING_CIRCUIT; return nullptr;
} }
// if we have the heating circuit already present, returns its object // if we have the heating circuit already present, returns its object
@@ -446,7 +527,6 @@ std::shared_ptr<Thermostat::HeatingCircuit> Thermostat::heating_circuit(std::sha
// create a new heating circuit object // create a new heating circuit object
// TODO do we need to create a new object if using emplace_back? // TODO do we need to create a new object if using emplace_back?
heating_circuits_.emplace_back(new HeatingCircuit(hc_num, monitor_typeids[hc_num - 1], set_typeids[hc_num - 1])); heating_circuits_.emplace_back(new HeatingCircuit(hc_num, monitor_typeids[hc_num - 1], set_typeids[hc_num - 1]));
return heating_circuits_.back(); return heating_circuits_.back();
} }
@@ -826,8 +906,9 @@ void Thermostat::process_RC30Set(std::shared_ptr<const Telegram> telegram) {
// type 0x3E (HC1), 0x48 (HC2), 0x52 (HC3), 0x5C (HC4) - data from the RC35 thermostat (0x10) - 16 bytes // type 0x3E (HC1), 0x48 (HC2), 0x52 (HC3), 0x5C (HC4) - data from the RC35 thermostat (0x10) - 16 bytes
void Thermostat::process_RC35Monitor(std::shared_ptr<const Telegram> telegram) { void Thermostat::process_RC35Monitor(std::shared_ptr<const Telegram> telegram) {
// exit if... // exit if the 15th byte (second from last) is 0x00, which I think is calculated flow setpoint temperature
// - the 15th byte (second from last) is 0x00, which I think is flow temp, means HC is not is use // with weather controlled RC35s this value can be zero and our setpoint temps will be incorrect
// see https://github.com/proddy/EMS-ESP/issues/373#issuecomment-627907301
if (telegram->message_data[14] == 0x00) { if (telegram->message_data[14] == 0x00) {
return; return;
} }

View File

@@ -104,10 +104,11 @@ class Thermostat : public EMSdevice {
static uuid::log::Logger logger_; static uuid::log::Logger logger_;
void console_commands(); void console_commands();
void init_mqtt();
std::string datetime_; // date and time stamp std::string datetime_; // date and time stamp
bool mqtt_nested_json_; uint8_t mqtt_format_; // single, nested or ha
// Installation parameters // Installation parameters
uint8_t ibaMainDisplay = uint8_t ibaMainDisplay =

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "2.0.0a3" #define EMSESP_APP_VERSION "2.0.0a4"