Merge branch 'v2a8' of https://github.com/proddy/EMS-ESP into v2

This commit is contained in:
MichaelDvP
2020-05-28 08:25:36 +02:00
36 changed files with 857 additions and 532 deletions

View File

@@ -41,7 +41,7 @@ Note: Version 2.0 is not backward compatible with v1.0. The File system structur
common commands available in all contexts: common commands available in all contexts:
exit exit
help help
log [level] [trace ID] log [level] [raw] [trace ID]
su su
(top level) (top level)
@@ -124,9 +124,9 @@ thermostat
### **Known issues, bugs and improvements currently working on** ### **Known issues, bugs and improvements currently working on**
``` ```
TODO when doing show, should we sort the ems devices? TODO ESP32 - when saving SPIFFS the UART stop and restart() functions need to flush queue to avoid miss fires
TODO network issues with ESP8266 - can take a while to get an IP address. DNS issue?
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 sometimes with tx_mode 0 there are a few CRC errors due to collision when waiting for a BRK signal. 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.
``` ```
@@ -152,4 +152,6 @@ TODO See if it's easier to use timers instead of millis() timers, using https://
``` ```
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 the web code which has the Captive AP and better wifi reconnect logic. Use IPV6 and NTP from lwip2
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.
TODO when doing show in telnet, should we sort the ems devices?
``` ```

View File

@@ -26,15 +26,25 @@ uint64_t get_uptime_ms() {
static uint32_t high_millis = 0; static uint32_t high_millis = 0;
static uint32_t low_millis = 0; static uint32_t low_millis = 0;
uint32_t now_millis = ::millis(); if (get_uptime() < low_millis) {
if (now_millis < low_millis) {
high_millis++; high_millis++;
} }
low_millis = now_millis; low_millis = get_uptime();
return ((uint64_t)high_millis << 32) | low_millis; return ((uint64_t)high_millis << 32) | low_millis;
} }
// added by proddy
static uint32_t now_millis; // added by proddy
void set_uptime() {
now_millis = ::millis();
}
uint32_t get_uptime() {
return now_millis;
}
} // namespace uuid } // namespace uuid

View File

@@ -21,6 +21,7 @@
namespace uuid { namespace uuid {
void loop() { void loop() {
set_uptime(); // added by proddy
get_uptime_ms(); get_uptime_ms();
} }

View File

@@ -86,6 +86,9 @@ void loop();
*/ */
uint64_t get_uptime_ms(); uint64_t get_uptime_ms();
uint32_t get_uptime(); // added by proddy
void set_uptime();
} // namespace uuid } // namespace uuid
#endif #endif

View File

@@ -2,9 +2,9 @@
; For EMS-ESP ; For EMS-ESP
[platformio] [platformio]
;default_envs = esp8266 default_envs = esp8266
; default_envs = esp32 ; default_envs = esp32
default_envs = esp32_d1 ;default_envs = esp32_d1
# override any settings with your own local ones in pio_local.ini # override any settings with your own local ones in pio_local.ini
extra_configs = pio_local.ini extra_configs = pio_local.ini
@@ -34,7 +34,7 @@ build_flags = -std=c++11 -Os -fno-exceptions
-D ARDUINOJSON_USE_LONG_LONG=0 -D ARDUINOJSON_USE_LONG_LONG=0
-D BEARSSL_SSL_BASIC -D BEARSSL_SSL_BASIC
-D PROGMEM_WWW -D PROGMEM_WWW
-D UUID_TELNET_HAVE_WIFICLIENT_NODELAY=0 -D UUID_TELNET_HAVE_WIFICLIENT_NODELAY=1
libs_core = libs_core =
ArduinoJson ArduinoJson
@@ -59,18 +59,18 @@ check_flags =
clangtidy: --checks=-*,clang-analyzer-*,performance-* clangtidy: --checks=-*,clang-analyzer-*,performance-*
; USB upload ; USB upload
upload_protocol = esptool ; upload_protocol = esptool
; example ports for OSX ; example ports for OSX
;upload_port = /dev/cu.wchusbserial14403 ;upload_port = /dev/cu.wchusbserial14403
;upload_port = /dev/cu.usbserial-1440 ;upload_port = /dev/cu.usbserial-1440
;upload_port = /dev/cu.SLAB_USBtoUART ;upload_port = /dev/cu.SLAB_USBtoUART
; OTA upload ; OTA upload
; upload_protocol = espota upload_protocol = espota
; upload_flags = upload_flags =
; --port=8266 --port=8266
; --auth=neo --auth=neo
; upload_port = ems-esp.local upload_port = 192.168.0.20
[env:esp8266] [env:esp8266]
build_type = release build_type = release

View File

@@ -46,7 +46,7 @@ uuid::log::Logger Boiler::logger_{F_(logger_name), uuid::log::Facility::CONSOLE}
Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand) Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) { : EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
DEBUG_LOG(F("Registering new Boiler with device ID 0x%02X"), device_id); LOG_DEBUG(F("Registering new Boiler with device ID 0x%02X"), device_id);
// the telegram handlers... // the telegram handlers...
register_telegram_type(0x18, F("UBAMonitorFast"), true, std::bind(&Boiler::process_UBAMonitorFast, this, _1)); register_telegram_type(0x18, F("UBAMonitorFast"), true, std::bind(&Boiler::process_UBAMonitorFast, this, _1));
@@ -91,7 +91,7 @@ void Boiler::boiler_cmd(const char * message) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc; StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message); DeserializationError error = deserializeJson(doc, message);
if (error) { if (error) {
DEBUG_LOG(F("MQTT error: payload %s, error %s"), message, error.c_str()); LOG_DEBUG(F("MQTT error: payload %s, error %s"), message, error.c_str());
return; return;
} }
const char * command = doc["cmd"]; const char * command = doc["cmd"];
@@ -312,7 +312,7 @@ void Boiler::publish_values() {
} }
#ifdef EMSESP_DEBUG #ifdef EMSESP_DEBUG
DEBUG_LOG(F("[DEBUG] Performing a boiler publish")); LOG_DEBUG(F("[DEBUG] Performing a boiler publish"));
#endif #endif
// if we have data, publish it // if we have data, publish it
@@ -675,31 +675,31 @@ void Boiler::process_UBAMaintenanceSettings(std::shared_ptr<const Telegram> tele
// Set the warm water temperature 0x33 // Set the warm water temperature 0x33
void Boiler::set_warmwater_temp(const uint8_t temperature) { void Boiler::set_warmwater_temp(const uint8_t temperature) {
logger_.info(F("Setting boiler warm water temperature to %d C"), temperature); LOG_INFO(F("Setting boiler warm water temperature to %d C"), temperature);
write_command(EMS_TYPE_UBAParameterWW, 2, temperature); write_command(EMS_TYPE_UBAParameterWW, 2, temperature);
} }
// flow temp // flow temp
void Boiler::set_flow_temp(const uint8_t temperature) { void Boiler::set_flow_temp(const uint8_t temperature) {
logger_.info(F("Setting boiler flow temperature to %d C"), temperature); LOG_INFO(F("Setting boiler flow temperature to %d C"), temperature);
write_command(EMS_TYPE_UBASetPoints, 0, temperature); write_command(EMS_TYPE_UBASetPoints, 0, temperature);
} }
// 1=hot, 2=eco, 3=intelligent // 1=hot, 2=eco, 3=intelligent
void Boiler::set_warmwater_mode(const uint8_t comfort) { void Boiler::set_warmwater_mode(const uint8_t comfort) {
if (comfort == 1) { if (comfort == 1) {
logger_.info(F("Setting boiler warm water to hot")); LOG_INFO(F("Setting boiler warm water to hot"));
} else if (comfort == 2) { } else if (comfort == 2) {
logger_.info(F("Setting boiler warm water to eco")); LOG_INFO(F("Setting boiler warm water to eco"));
} else if (comfort == 3) { } else if (comfort == 3) {
logger_.info(F("Setting boiler warm water to intelligent")); LOG_INFO(F("Setting boiler warm water to intelligent"));
} }
write_command(EMS_TYPE_UBAParameterWW, 9, comfort); write_command(EMS_TYPE_UBAParameterWW, 9, comfort);
} }
// turn on/off warm water // turn on/off warm water
void Boiler::set_warmwater_activated(const bool activated) { void Boiler::set_warmwater_activated(const bool activated) {
logger_.info(F("Setting boiler warm water %s"), activated ? "on" : "off"); LOG_INFO(F("Setting boiler warm water %s"), activated ? "on" : "off");
uint8_t value; uint8_t value;
// https://github.com/proddy/EMS-ESP/issues/268 // https://github.com/proddy/EMS-ESP/issues/268
@@ -715,7 +715,7 @@ void Boiler::set_warmwater_activated(const bool activated) {
// true = on, false = off // true = on, false = off
// Note: Using the type 0x1D to put the boiler into Test mode. This may be shown on the boiler with a flashing 'T' // Note: Using the type 0x1D to put the boiler into Test mode. This may be shown on the boiler with a flashing 'T'
void Boiler::set_tapwarmwater_activated(const bool activated) { void Boiler::set_tapwarmwater_activated(const bool activated) {
logger_.info(F("Setting boiler warm tap water %s"), activated ? "on" : "off"); LOG_INFO(F("Setting boiler warm tap water %s"), activated ? "on" : "off");
uint8_t message_data[EMS_MAX_TELEGRAM_MESSAGE_LENGTH]; uint8_t message_data[EMS_MAX_TELEGRAM_MESSAGE_LENGTH];
for (uint8_t i = 0; i < sizeof(message_data); i++) { for (uint8_t i = 0; i < sizeof(message_data); i++) {
message_data[i] = 0x00; message_data[i] = 0x00;
@@ -743,14 +743,14 @@ void Boiler::set_tapwarmwater_activated(const bool activated) {
// true = on, false = off // true = on, false = off
// See also https://github.com/proddy/EMS-ESP/issues/341#issuecomment-596245458 for Junkers // See also https://github.com/proddy/EMS-ESP/issues/341#issuecomment-596245458 for Junkers
void Boiler::set_warmwater_onetime(const bool activated) { void Boiler::set_warmwater_onetime(const bool activated) {
logger_.info(F("Setting boiler warm water OneTime loading %s"), activated ? "on" : "off"); LOG_INFO(F("Setting boiler warm water OneTime loading %s"), activated ? "on" : "off");
write_command(EMS_TYPE_UBAFlags, 0, (activated ? 0x22 : 0x02)); write_command(EMS_TYPE_UBAFlags, 0, (activated ? 0x22 : 0x02));
} }
// Activate / De-activate circulation of warm water 0x35 // Activate / De-activate circulation of warm water 0x35
// true = on, false = off // true = on, false = off
void Boiler::set_warmwater_circulation(const bool activated) { void Boiler::set_warmwater_circulation(const bool activated) {
logger_.info(F("Setting boiler warm water circulation %s"), activated ? "on" : "off"); LOG_INFO(F("Setting boiler warm water circulation %s"), activated ? "on" : "off");
write_command(EMS_TYPE_UBAFlags, 1, (activated ? 0x22 : 0x02)); write_command(EMS_TYPE_UBAFlags, 1, (activated ? 0x22 : 0x02));
} }

View File

@@ -221,8 +221,9 @@ void Console::load_standard_commands(unsigned int context) {
context, context,
CommandFlags::USER, CommandFlags::USER,
flash_string_vector{F_(log)}, flash_string_vector{F_(log)},
flash_string_vector{F_(log_level_optional), F_(traceid_optional)}, flash_string_vector{F_(log_level_optional), F_(trace_format_optional), F_(traceid_optional)},
[](Shell & shell, const std::vector<std::string> & arguments) { [](Shell & shell, const std::vector<std::string> & arguments) {
uint16_t watch_id;
if (!arguments.empty()) { if (!arguments.empty()) {
uuid::log::Level level; uuid::log::Level level;
@@ -233,15 +234,34 @@ void Console::load_standard_commands(unsigned int context) {
return; return;
} }
// see if we have extra argument, for trace // trace logic
uint16_t watch_id = 0; // no watch ID set if (level == uuid::log::Level::TRACE) {
if ((arguments.size() == 2) && (level == uuid::log::Level::TRACE)) { watch_id = LOG_TRACE_WATCH_NONE; // no watch ID set
watch_id = Helpers::hextoint(arguments[1].c_str()); if (arguments.size() > 1) {
shell.printfln(("Tracing only telegrams that match a device ID or telegram type of 0x%02X"), watch_id); // next argument is raw or full
if (arguments[1] == read_flash_string(F_(raw))) {
emsesp::EMSESP::trace_raw(true);
} else if (arguments[1] == read_flash_string(F_(full))) {
emsesp::EMSESP::trace_raw(false);
} }
emsesp::EMSESP::trace_watch_id(watch_id);
// get the watch_id if its set
if (arguments.size() == 3) {
emsesp::EMSESP::trace_watch_id(Helpers::hextoint(arguments[2].c_str()));
} }
}
}
}
// print out logging settings
shell.printfln(F_(log_level_fmt), uuid::log::format_level_uppercase(shell.log_level())); shell.printfln(F_(log_level_fmt), uuid::log::format_level_uppercase(shell.log_level()));
watch_id = emsesp::EMSESP::trace_watch_id();
if (watch_id == LOG_TRACE_WATCH_NONE) {
shell.printfln(F("Tracing all telegrams"));
} else {
shell.printfln(F("Tracing only telegrams that match a device ID or telegram type of 0x%02X"), watch_id);
}
shell.printfln(F_(trace_raw_fmt), emsesp::EMSESP::trace_raw() ? uuid::read_flash_string(F_(on)).c_str() : uuid::read_flash_string(F_(off)).c_str());
}, },
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> std::vector<std::string> { [](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> std::vector<std::string> {
return uuid::log::levels_lowercase(); return uuid::log::levels_lowercase();
@@ -384,6 +404,8 @@ EMSESPStreamConsole::~EMSESPStreamConsole() {
#endif #endif
ptys_[pty_] = false; ptys_[pty_] = false;
ptys_.shrink_to_fit(); ptys_.shrink_to_fit();
EMSESPShell::commands->remove_all_commands();
} }
} }
@@ -414,7 +436,7 @@ void Console::start() {
// note, this must be started after the network/wifi for ESP32 otherwise it'll crash // note, this must be started after the network/wifi for ESP32 otherwise it'll crash
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
telnet_.start(); telnet_.start();
telnet_.default_write_timeout(1000); // in ms, socket timeout 1 second // telnet_.default_write_timeout(1000); // in ms, socket timeout 1 second
#endif #endif
} }

View File

@@ -41,8 +41,12 @@ using uuid::log::Level;
// clang-format off // clang-format off
#define DEBUG_LOG(...) if (logger_.enabled(Level::DEBUG)) {logger_.debug(__VA_ARGS__);} #define LOG_DEBUG(...) if (logger_.enabled(Level::DEBUG)) {logger_.debug(__VA_ARGS__);}
#define TRACE_LOG(...) if (logger_.enabled(Level::TRACE)) {logger_.trace(__VA_ARGS__);} #define LOG_TRACE(...) if (logger_.enabled(Level::TRACE)) {logger_.trace(__VA_ARGS__);}
#define LOG_INFO(...) logger_.info(__VA_ARGS__)
#define LOG_NOTICE(...) logger_.notice(__VA_ARGS__)
#define LOG_WARNING(...) logger_.warning(__VA_ARGS__)
#define LOG_ERROR(...) logger_.err(__VA_ARGS__)
#define MAKE_PSTR(string_name, string_literal) static const char __pstr__##string_name[] __attribute__((__aligned__(sizeof(int)))) PROGMEM = string_literal; #define MAKE_PSTR(string_name, string_literal) static const char __pstr__##string_name[] __attribute__((__aligned__(sizeof(int)))) PROGMEM = string_literal;
#define MAKE_PSTR_WORD(string_name) MAKE_PSTR(string_name, #string_name) #define MAKE_PSTR_WORD(string_name) MAKE_PSTR(string_name, #string_name)
@@ -84,6 +88,8 @@ MAKE_PSTR_WORD(debug)
MAKE_PSTR_WORD(restart) MAKE_PSTR_WORD(restart)
MAKE_PSTR_WORD(reconnect) MAKE_PSTR_WORD(reconnect)
MAKE_PSTR_WORD(format) MAKE_PSTR_WORD(format)
MAKE_PSTR_WORD(raw)
MAKE_PSTR_WORD(full)
// context menus // context menus
MAKE_PSTR_WORD(mqtt) MAKE_PSTR_WORD(mqtt)
@@ -96,7 +102,9 @@ MAKE_PSTR(asterisks, "********")
MAKE_PSTR(n_mandatory, "<n>") MAKE_PSTR(n_mandatory, "<n>")
MAKE_PSTR(n_optional, "[n]") MAKE_PSTR(n_optional, "[n]")
MAKE_PSTR(traceid_optional, "[trace ID]") MAKE_PSTR(traceid_optional, "[trace ID]")
MAKE_PSTR(bool_mandatory, "<on | off>") MAKE_PSTR(trace_raw_fmt, "Displaying raw bytes = %s")
MAKE_PSTR(trace_format_optional, "[full|raw]")
MAKE_PSTR(bool_mandatory, "<on|off>")
MAKE_PSTR(typeid_mandatory, "<type ID>") MAKE_PSTR(typeid_mandatory, "<type ID>")
MAKE_PSTR(deviceid_mandatory, "<device ID>") MAKE_PSTR(deviceid_mandatory, "<device ID>")
MAKE_PSTR(deviceid_optional, "[device ID]") MAKE_PSTR(deviceid_optional, "[device ID]")

View File

@@ -37,7 +37,6 @@
{122, DeviceType::BOILER, F("Proline"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, {122, DeviceType::BOILER, F("Proline"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{170, DeviceType::BOILER, F("Logano GB212"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, {170, DeviceType::BOILER, F("Logano GB212"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{172, DeviceType::BOILER, F("Enviline/Compress 6000AW"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, {172, DeviceType::BOILER, F("Enviline/Compress 6000AW"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 72, DeviceType::BOILER, F("MC10"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Solar Modules - 0x30 // Solar Modules - 0x30
{ 73, DeviceType::SOLAR, F("SM10"), DeviceFlags::EMS_DEVICE_FLAG_SM10}, { 73, DeviceType::SOLAR, F("SM10"), DeviceFlags::EMS_DEVICE_FLAG_SM10},

View File

@@ -160,7 +160,7 @@ void EMSdevice::show_values(uuid::console::Shell & shell) {
// for each telegram that has the fetch value set (true) do a read request // for each telegram that has the fetch value set (true) do a read request
void EMSdevice::fetch_values() { void EMSdevice::fetch_values() {
DEBUG_LOG(F("Fetching values for device ID 0x%02X"), device_id()); LOG_DEBUG(F("Fetching values for device ID 0x%02X"), device_id());
for (const auto & tf : telegram_functions_) { for (const auto & tf : telegram_functions_) {
if (tf.fetch_) { if (tf.fetch_) {
@@ -188,7 +188,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 MQTT topic %s for device ID %02X"), topic.c_str(), this->device_id_); LOG_DEBUG(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);
} }
@@ -230,7 +230,7 @@ std::string EMSdevice::telegram_type_name(std::shared_ptr<const Telegram> telegr
bool EMSdevice::process_telegram(std::shared_ptr<const Telegram> telegram) { bool EMSdevice::process_telegram(std::shared_ptr<const Telegram> telegram) {
for (const auto & tf : telegram_functions_) { for (const auto & tf : telegram_functions_) {
if (tf.telegram_type_id_ == telegram->type_id) { if (tf.telegram_type_id_ == telegram->type_id) {
DEBUG_LOG(F("Processing %s..."), uuid::read_flash_string(tf.telegram_type_name_).c_str()); LOG_DEBUG(F("Processing %s..."), uuid::read_flash_string(tf.telegram_type_name_).c_str());
tf.process_function_(telegram); tf.process_function_(telegram);
return true; return true;
} }

View File

@@ -56,7 +56,8 @@ Shower EMSESP::shower_; // Shower logic
// static/common variables // static/common variables
uint8_t EMSESP::actual_master_thermostat_ = EMSESP_DEFAULT_MASTER_THERMOSTAT; // which thermostat leads when multiple found uint8_t EMSESP::actual_master_thermostat_ = EMSESP_DEFAULT_MASTER_THERMOSTAT; // which thermostat leads when multiple found
uint16_t EMSESP::trace_watch_id_ = 0; // for when log is TRACE uint16_t EMSESP::trace_watch_id_ = LOG_TRACE_WATCH_NONE; // for when log is TRACE. 0 means no trace set
bool EMSESP::trace_raw_ = false; // not showing raw when in trace logging
bool EMSESP::tap_water_active_ = false; // for when Boiler states we having running warm water. used in Shower() bool EMSESP::tap_water_active_ = false; // for when Boiler states we having running warm water. used in Shower()
bool EMSESP::ems_read_only_; bool EMSESP::ems_read_only_;
uint32_t EMSESP::last_fetch_ = 0; uint32_t EMSESP::last_fetch_ = 0;
@@ -332,7 +333,7 @@ void EMSESP::process_UBADevices(std::shared_ptr<const Telegram> telegram) {
// if we haven't already detected this device, request it's version details, unless its us (EMS-ESP) // if we haven't already detected this device, request it's version details, unless its us (EMS-ESP)
// when the version info is received, it will automagically add the device // when the version info is received, it will automagically add the device
if ((device_id != ems_bus_id) && !(EMSESP::device_exists(device_id))) { if ((device_id != ems_bus_id) && !(EMSESP::device_exists(device_id))) {
logger_.info(F("New EMS device detected with ID 0x%02X. Requesting version information."), device_id); LOG_DEBUG(F("New EMS device detected with ID 0x%02X. Requesting version information."), device_id);
send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id); send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id);
} }
} }
@@ -391,9 +392,11 @@ void EMSESP::process_version(std::shared_ptr<const Telegram> telegram) {
// We also check for common telgram types, like the Version(0x02) // We also check for common telgram types, like the Version(0x02)
// returns false if there are none found // returns false if there are none found
bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) { bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
// print to console if logging is TRACE if ((logger_.enabled(Level::TRACE)) && !trace_raw()) {
if ((trace_watch_id_ == 0) || (telegram->src == trace_watch_id_) || (telegram->dest == trace_watch_id_) || (telegram->type_id == trace_watch_id_)) { if ((trace_watch_id_ == LOG_TRACE_WATCH_NONE) || (telegram->src == trace_watch_id_) || (telegram->dest == trace_watch_id_)
TRACE_LOG(pretty_telegram(telegram).c_str()); || (telegram->type_id == trace_watch_id_)) {
LOG_TRACE(pretty_telegram(telegram).c_str());
}
} }
// only process broadcast telegrams or ones sent to us on request // only process broadcast telegrams or ones sent to us on request
@@ -431,7 +434,7 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
} }
if (!found) { if (!found) {
DEBUG_LOG(F("No telegram type handler found for type ID 0x%02X (src 0x%02X, dest 0x%02X)"), telegram->type_id, telegram->src, telegram->dest); LOG_DEBUG(F("No telegram type handler found for type ID 0x%02X (src 0x%02X, dest 0x%02X)"), telegram->type_id, telegram->src, telegram->dest);
} }
return found; return found;
@@ -504,7 +507,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
for (const auto & emsdevice : emsdevices) { for (const auto & emsdevice : emsdevices) {
if (emsdevice) { if (emsdevice) {
if (emsdevice->is_device_id(device_id)) { if (emsdevice->is_device_id(device_id)) {
DEBUG_LOG(F("Updating details for already existing device with ID 0x%02X"), device_id); LOG_DEBUG(F("Updating details for already existing device with ID 0x%02X"), device_id);
emsdevice->product_id(product_id); emsdevice->product_id(product_id);
emsdevice->version(version); emsdevice->version(version);
emsdevice->brand(brand); emsdevice->brand(brand);
@@ -535,10 +538,10 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
// if we don't recognize the product ID report it, but don't add it. // if we don't recognize the product ID report it, but don't add it.
if (!found) { if (!found) {
logger_.notice(F("Unrecognized EMS device with device ID 0x%02X with product ID %d. Please report on GitHub."), device_id, product_id); LOG_NOTICE(F("Unrecognized EMS device with device ID 0x%02X with product ID %d. Please report on GitHub."), device_id, product_id);
return false; // not found return false; // not found
} else { } else {
DEBUG_LOG(F("Adding new device with device ID 0x%02X with product ID %d"), device_id, product_id); LOG_DEBUG(F("Adding new device with device ID 0x%02X with product ID %d"), device_id, product_id);
// go and fetch its data, including asking for the version // go and fetch its data, including asking for the version
send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id); send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id);
fetch_device_values(device_id); fetch_device_values(device_id);
@@ -569,7 +572,6 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
// check first for echo // check first for echo
uint8_t first_value = data[0]; uint8_t first_value = data[0];
if (((first_value & 0x7F) == txservice_.ems_bus_id()) && (length > 1)) { if (((first_value & 0x7F) == txservice_.ems_bus_id()) && (length > 1)) {
DEBUG_LOG(F("Tx echo"));
return; // it's an echo return; // it's an echo
} }
@@ -579,12 +581,12 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
EMSbus::tx_waiting(false); // reset Tx wait state 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.")); LOG_DEBUG(F("Last Tx write successful. Sending read request."));
txservice_.increment_telegram_write_count(); // last tx/write was confirmed ok txservice_.increment_telegram_write_count(); // last tx/write was confirmed ok
txservice_.send_poll(); // close the bus txservice_.send_poll(); // close the bus
txservice_.post_send_query(); // send type_id to last destination txservice_.post_send_query(); // send type_id to last destination
} else if (first_value == TxService::TX_WRITE_FAIL) { } else if (first_value == TxService::TX_WRITE_FAIL) {
DEBUG_LOG(F("Last Tx write rejected by host")); LOG_DEBUG(F("Last Tx write rejected by host"));
txservice_.send_poll(); // close the bus txservice_.send_poll(); // close the bus
} else { } else {
// ignore it, it's probably a poll and we can wait for the next one // ignore it, it's probably a poll and we can wait for the next one
@@ -596,7 +598,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
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)) {
DEBUG_LOG(F("Last Tx read successful")); LOG_DEBUG(F("Last Tx read successful"));
txservice_.increment_telegram_read_count(); txservice_.increment_telegram_read_count();
txservice_.send_poll(); txservice_.send_poll();
} else { } else {
@@ -604,9 +606,9 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
// So re-send the last Tx and increment retry count // So re-send the last Tx and increment retry count
uint8_t retries = txservice_.retry_tx(); // returns 0 if exceeded count uint8_t retries = txservice_.retry_tx(); // returns 0 if exceeded count
if (retries) { if (retries) {
DEBUG_LOG(F("Last Tx read failed. Retrying #%d..."), retries); LOG_DEBUG(F("Last Tx read failed. Retrying #%d..."), retries);
} else { } else {
DEBUG_LOG(F("Last Tx read failed. Giving up")); LOG_DEBUG(F("Last Tx read failed. Giving up"));
} }
} }
} }
@@ -617,7 +619,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
// check for poll to us, if so send top message from Tx queue immediately and quit // check for poll to us, if so send top message from Tx queue immediately and quit
// if ht3 poll must be ems_bus_id else if Buderus poll must be (ems_bus_id | 0x80) // if ht3 poll must be ems_bus_id else if Buderus poll must be (ems_bus_id | 0x80)
if ((first_value ^ 0x80 ^ rxservice_.ems_mask()) == txservice_.ems_bus_id()) { 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 EMSbus::last_bus_activity(uuid::get_uptime()); // set the flag indication the EMS bus is active
txservice_.send(); txservice_.send();
} }
return; return;
@@ -635,7 +637,7 @@ void EMSESP::send_raw_telegram(const char * data) {
// sets the ems read only flag preventing any Tx from going out // sets the ems read only flag preventing any Tx from going out
void EMSESP::set_ems_read_only() { void EMSESP::set_ems_read_only() {
ems_read_only_ = Settings().ems_read_only(); ems_read_only_ = Settings().ems_read_only();
DEBUG_LOG(F("Setting EMS read-only mode to %s"), ems_read_only_ ? F("on") : F("off")); LOG_DEBUG(F("Setting EMS read-only mode to %s"), ems_read_only_ ? F("on") : F("off"));
} }
// console commands to add // console commands to add
@@ -693,17 +695,17 @@ void EMSESP::console_commands(Shell & shell, unsigned int context) {
flash_string_vector{F_(n_mandatory)}, flash_string_vector{F_(n_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) { [](Shell & shell, const std::vector<std::string> & arguments) {
uint8_t tx_mode = (arguments[0]).at(0) - '0'; uint8_t tx_mode = (arguments[0]).at(0) - '0';
if ((tx_mode > 0) && (tx_mode <= 3)) { if ((tx_mode > 0) && (tx_mode <= 4)) {
Settings settings; Settings settings;
settings.ems_tx_mode(tx_mode); settings.ems_tx_mode(tx_mode);
settings.commit(); settings.commit();
shell.printfln(F_(tx_mode_fmt), settings.ems_tx_mode()); shell.printfln(F_(tx_mode_fmt), settings.ems_tx_mode());
} else { } else {
shell.println(F("Must be 1 for EMS generic, 2 for EMS+ or 3 for HT3")); shell.println(F("Must be 1 for EMS generic, 2 for EMS+, 3 for HT3, 4 for experimental"));
} }
}, },
[](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("1")), read_flash_string(F("2")), read_flash_string(F("3"))}; return std::vector<std::string>{read_flash_string(F("1")), read_flash_string(F("2")), read_flash_string(F("3")), read_flash_string(F("4"))};
}); });
EMSESPShell::commands->add_command( EMSESPShell::commands->add_command(
@@ -787,7 +789,7 @@ void EMSESP::console_commands(Shell & shell, unsigned int context) {
// kick off the party, start all the services // kick off the party, start all the services
void EMSESP::start() { void EMSESP::start() {
// Load our libary of known devices // Load our library of known devices
device_library_ = { device_library_ = {
#include "device_library.h" #include "device_library.h"
}; };
@@ -799,25 +801,27 @@ void EMSESP::start() {
rxservice_.start(); rxservice_.start();
txservice_.start(); txservice_.start();
shower_.start(); shower_.start();
mqtt_.start();
set_ems_read_only(); // see if we have Tx disabled and set the flag set_ems_read_only(); // see if we have Tx disabled and set the flag
} }
// loop de loop // loop de loop
void EMSESP::loop() { void EMSESP::loop() {
// network returns false if an OTA is being carried out. // network returns false if an OTA is being carried out
// so we disable all services when an OTA is happening
if (network_.loop()) { if (network_.loop()) {
console_.loop(); // telnet/serial console console_.loop(); // telnet/serial console
system_.loop(); // does LED and checks system health, and syslog service system_.loop(); // does LED and checks system health, and syslog service
mqtt_.loop(); // starts mqtt, and sends out anything in the queue mqtt_.loop(); // starts mqtt, and sends out anything in the queue
rxservice_.loop(); // process what ever is in the rx queue rxservice_.loop(); // process what ever is in the rx queue
txservice_.loop(); // check that the Tx is all ok
shower_.loop(); // check for shower on/off shower_.loop(); // check for shower on/off
sensors_.loop(); // this will also send out via MQTT sensors_.loop(); // this will also send out via MQTT
// force a query on the EMS devices to fetch latest data // force a query on the EMS devices to fetch latest data at a set interval (1 min)
uint32_t currentMillis = millis(); if ((uuid::get_uptime() - last_fetch_ > EMS_FETCH_FREQUENCY)) {
if ((currentMillis - last_fetch_ > EMS_FETCH_FREQUENCY)) { last_fetch_ = uuid::get_uptime();
last_fetch_ = currentMillis;
fetch_device_values(); fetch_device_values();
} }
} }

View File

@@ -45,6 +45,8 @@
#include "boiler.h" #include "boiler.h"
#include "shower.h" #include "shower.h"
#define LOG_TRACE_WATCH_NONE 0 // no watch set
namespace emsesp { namespace emsesp {
class Shower; // forward declaration for compiler class Shower; // forward declaration for compiler
@@ -79,8 +81,6 @@ class EMSESP {
static uint8_t actual_master_thermostat(); static uint8_t actual_master_thermostat();
static void actual_master_thermostat(const uint8_t device_id); static void actual_master_thermostat(const uint8_t device_id);
static void trace_watch_id(uint16_t);
static void show_values(uuid::console::Shell & shell); static void show_values(uuid::console::Shell & shell);
static void show_devices(uuid::console::Shell & shell); static void show_devices(uuid::console::Shell & shell);
static void show_emsbus(uuid::console::Shell & shell); static void show_emsbus(uuid::console::Shell & shell);
@@ -93,10 +93,20 @@ class EMSESP {
return sensors_.devices(); return sensors_.devices();
} }
static void trace_watch_id(uint16_t id);
static uint16_t trace_watch_id() { static uint16_t trace_watch_id() {
return trace_watch_id_; return trace_watch_id_;
} }
static void trace_raw(bool trace_raw) {
trace_raw_ = trace_raw;
}
static bool trace_raw() {
return trace_raw_;
}
static bool tap_water_active() { static bool tap_water_active() {
return tap_water_active_; return tap_water_active_;
} }
@@ -151,6 +161,7 @@ class EMSESP {
static uint8_t actual_master_thermostat_; static uint8_t actual_master_thermostat_;
static uint16_t trace_watch_id_; static uint16_t trace_watch_id_;
static bool trace_raw_;
static bool tap_water_active_; static bool tap_water_active_;
static bool ems_read_only_; static bool ems_read_only_;
}; };

View File

@@ -35,7 +35,7 @@ uuid::log::Logger Heatpump::logger_{F_(logger_name), uuid::log::Facility::CONSOL
Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand) Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) { : EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
DEBUG_LOG(F("Registering new Heat Pump module with device ID 0x%02X"), device_id); LOG_DEBUG(F("Registering new Heat Pump module with device ID 0x%02X"), device_id);
// telegram handlers // telegram handlers
register_telegram_type(0x047B, F("HP1"), true, std::bind(&Heatpump::process_HPMonitor1, this, _1)); register_telegram_type(0x047B, F("HP1"), true, std::bind(&Heatpump::process_HPMonitor1, this, _1));

View File

@@ -265,7 +265,8 @@ std::string Helpers::data_to_hex(const uint8_t * data, const uint8_t length) {
} }
// takes a hex string and convert it to a 32bit number (max 8 hex digits) // takes a hex string and convert it to an unsigned 32bit number (max 8 hex digits)
// works with only positive numbers
uint32_t Helpers::hextoint(const char * hex) { uint32_t Helpers::hextoint(const char * hex) {
uint32_t val = 0; uint32_t val = 0;
while (*hex) { while (*hex) {

View File

@@ -26,20 +26,25 @@ uuid::log::Logger Mixing::logger_{F_(logger_name), uuid::log::Facility::CONSOLE}
Mixing::Mixing(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand) Mixing::Mixing(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) { : EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
DEBUG_LOG(F("Registering new Mixing module with device ID 0x%02X"), device_id); LOG_DEBUG(F("Registering new Mixing module with device ID 0x%02X"), device_id);
if (flags == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) {
if (device_id < 0x28) {
// telegram handlers 0x20 - 0x27 for HC // telegram handlers 0x20 - 0x27 for HC
register_telegram_type(0x02D7, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1)); register_telegram_type(device_id -0x20 + 0x02D7, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1));
register_telegram_type(0x02D8, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1)); } else {
register_telegram_type(0x02D9, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1));
register_telegram_type(0x02DA, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1));
// telegram handlers for warm water/DHW 0x28, 0x29 // telegram handlers for warm water/DHW 0x28, 0x29
register_telegram_type(0x0331, F("MMPLUSStatusMessage_WWC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_WWC, this, _1)); register_telegram_type(device_id - 0x28 + 0x0331, F("MMPLUSStatusMessage_WWC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_WWC, this, _1));
register_telegram_type(0x0332, F("MMPLUSStatusMessage_WWC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_WWC, this, _1)); }
}
// EMS 1.0 // EMS 1.0
if (flags == EMSdevice::EMS_DEVICE_FLAG_MM10) {
register_telegram_type(0x00AA, F("MMConfigMessage"), false, nullptr);
register_telegram_type(0x00AB, F("MMStatusMessage"), true, std::bind(&Mixing::process_MMStatusMessage, this, _1)); register_telegram_type(0x00AB, F("MMStatusMessage"), true, std::bind(&Mixing::process_MMStatusMessage, this, _1));
register_telegram_type(0x00AC, F("MMSetMessage"), false, nullptr);
}
Settings settings;
mqtt_format_ = settings.mqtt_format(); // single, nested or ha
// MQTT callbacks // MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Mixing::cmd, this, _1)); // register_mqtt_topic("cmd", std::bind(&Mixing::cmd, this, _1));
@@ -75,44 +80,65 @@ void Mixing::show_values(uuid::console::Shell & shell) {
// ideally we should group up all the mixing units together into a nested JSON but for now we'll send them individually // ideally we should group up all the mixing units together into a nested JSON but for now we'll send them individually
void Mixing::publish_values() { void Mixing::publish_values() {
DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_SMALL); DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_SMALL);
JsonObject rootMixing = doc.to<JsonObject>();
JsonObject dataMixing;
if (mqtt_format_ == Settings::MQTT_format::SINGLE) {
switch (type_) { switch (type_) {
case Type::HC: case Type::HC:
doc["type"] = F("hc"); rootMixing["type"] = F("hc");
break; break;
case Type::WWC: case Type::WWC:
doc["type"] = F("wwc"); rootMixing["type"] = F("wwc");
break; break;
case Type::NONE: case Type::NONE:
default: default:
return; return;
} }
dataMixing = rootMixing;
} else {
char hc_name[10]; // hc{1-4}
if(type_ == Type::HC) {
strlcpy(hc_name, "hc", 10);
} else {
strlcpy(hc_name, "wwc", 10);
}
char s[3]; // for formatting strings
strlcat(hc_name, Helpers::itoa(s, hc_), 10);
dataMixing = rootMixing.createNestedObject(hc_name);
}
if (flowTemp_ != EMS_VALUE_USHORT_NOTSET) { if (flowTemp_ != EMS_VALUE_USHORT_NOTSET) {
doc["flowTemp"] = (float)flowTemp_ / 10; dataMixing["flowTemp"] = (float)flowTemp_ / 10;
} }
if (pumpMod_ != EMS_VALUE_UINT_NOTSET) { if (pumpMod_ != EMS_VALUE_UINT_NOTSET) {
doc["pumpMod"] = pumpMod_; dataMixing["pumpMod"] = pumpMod_;
} }
if (status_ != EMS_VALUE_UINT_NOTSET) { if (status_ != EMS_VALUE_UINT_NOTSET) {
doc["status"] = status_; dataMixing["status"] = status_;
} }
if (flowSetTemp_ != EMS_VALUE_UINT_NOTSET) { if (flowSetTemp_ != EMS_VALUE_UINT_NOTSET) {
doc["flowSetTemp"] = flowSetTemp_; dataMixing["flowSetTemp"] = flowSetTemp_;
} }
#ifdef EMSESP_DEBUG #ifdef EMSESP_DEBUG
DEBUG_LOG(F("[DEBUG] Performing a mixing module publish")); LOG_DEBUG(F("[DEBUG] Performing a mixing module publish"));
#endif #endif
// if format is single, send immediately and quit
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, "mixing_data", 30); strlcpy(topic, "mixing_data", 30);
strlcat(topic, Helpers::itoa(s, hc_), 30); // append hc to topic strlcat(topic, Helpers::itoa(s, hc_), 30); // append hc to topic
Mqtt::publish(topic, doc); Mqtt::publish(topic, doc);
return;
}
char topic[30];
strlcpy(topic, "mixing_data", 30);
Mqtt::publish(topic, doc);
} }
// check to see if values have been updated // check to see if values have been updated

View File

@@ -63,6 +63,8 @@ class Mixing : public EMSdevice {
uint8_t status_ = EMS_VALUE_UINT_NOTSET; uint8_t status_ = EMS_VALUE_UINT_NOTSET;
uint8_t flowSetTemp_ = EMS_VALUE_UINT_NOTSET; uint8_t flowSetTemp_ = EMS_VALUE_UINT_NOTSET;
Type type_ = Type::NONE; Type type_ = Type::NONE;
uint8_t mqtt_format_; // single, nested or ha
}; };
} // namespace emsesp } // namespace emsesp

View File

@@ -27,6 +27,7 @@ MAKE_PSTR_WORD(ip)
MAKE_PSTR_WORD(nested) MAKE_PSTR_WORD(nested)
MAKE_PSTR_WORD(single) MAKE_PSTR_WORD(single)
MAKE_PSTR_WORD(ha) MAKE_PSTR_WORD(ha)
MAKE_PSTR_WORD(my)
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)
@@ -93,11 +94,11 @@ void Mqtt::reconnect() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
mqttClient_.disconnect(); mqttClient_.disconnect();
#endif #endif
DEBUG_LOG(F("Reconnecting...")); LOG_DEBUG(F("Reconnecting..."));
} }
// MQTT setup // MQTT setup
void Mqtt::start() { void Mqtt::setup() {
// exit if already initialized // exit if already initialized
if (mqtt_start_) { if (mqtt_start_) {
return; return;
@@ -142,10 +143,10 @@ void Mqtt::start() {
#endif #endif
mqtt_connecting_ = false; mqtt_connecting_ = false;
mqtt_last_connection_ = millis(); mqtt_last_connection_ = uuid::get_uptime();
mqtt_reconnect_delay_ = Mqtt::MQTT_RECONNECT_DELAY_MIN; mqtt_reconnect_delay_ = Mqtt::MQTT_RECONNECT_DELAY_MIN;
DEBUG_LOG(F("Configuring MQTT service...")); LOG_DEBUG(F("Configuring MQTT service..."));
} }
// MQTT init callbacks // MQTT init callbacks
@@ -161,28 +162,28 @@ void Mqtt::init() {
mqttClient_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { mqttClient_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) {
if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) { if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {
logger_.err(F("Disconnected from server")); LOG_DEBUG(F("Disconnected from server"));
} }
if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) { if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) {
logger_.err(F("Server identifier Rejected")); LOG_ERROR(F("Server identifier Rejected"));
} }
if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) { if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) {
logger_.err(F("Server unavailable")); LOG_ERROR(F("Server unavailable"));
} }
if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) { if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) {
logger_.err(F("Malformed credentials")); LOG_ERROR(F("Malformed credentials"));
} }
if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) { if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) {
logger_.err(F("Not authorized")); LOG_ERROR(F("Not authorized"));
} }
// Reset reconnection delay // Reset reconnection delay
mqtt_last_connection_ = millis(); mqtt_last_connection_ = uuid::get_uptime();
mqtt_connecting_ = false; mqtt_connecting_ = false;
mqtt_start_ = false; // will force a new start() mqtt_start_ = false; // will force a new start()
}); });
// mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { DEBUG_LOG(F("Subscribe ACK for PID %d"), packetId); }); // mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { LOG_DEBUG(F("Subscribe ACK for PID %d"), packetId); });
mqttClient_.onPublish([this](uint16_t packetId) { on_publish(packetId); }); mqttClient_.onPublish([this](uint16_t packetId) { on_publish(packetId); });
@@ -236,7 +237,7 @@ void Mqtt::loop() {
} }
// send out heartbeat // send out heartbeat
uint32_t currentMillis = millis(); uint32_t currentMillis = uuid::get_uptime();
if ((currentMillis - last_heartbeat_ > MQTT_HEARTBEAT_INTERVAL)) { if ((currentMillis - last_heartbeat_ > MQTT_HEARTBEAT_INTERVAL)) {
last_heartbeat_ = currentMillis; last_heartbeat_ = currentMillis;
send_heartbeat(); send_heartbeat();
@@ -258,7 +259,7 @@ void Mqtt::loop() {
} }
// We need to reconnect. Check when was the last time we tried this // We need to reconnect. Check when was the last time we tried this
if (mqtt_last_connection_ && (millis() - mqtt_last_connection_ < mqtt_reconnect_delay_)) { if (mqtt_last_connection_ && (uuid::get_uptime() - mqtt_last_connection_ < mqtt_reconnect_delay_)) {
return; return;
} }
@@ -271,8 +272,8 @@ void Mqtt::loop() {
} }
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
start(); setup();
logger_.info(F("Connecting to MQTT server...")); LOG_INFO(F("Connecting to the MQTT server..."));
mqttClient_.connect(); // Connect to the MQTT broker mqttClient_.connect(); // Connect to the MQTT broker
#endif #endif
} }
@@ -338,7 +339,7 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) {
strlcpy(message, payload, len + 1); strlcpy(message, payload, len + 1);
#ifdef EMSESP_DEBUG #ifdef EMSESP_DEBUG
DEBUG_LOG(F("Received %s => %s (length %d)"), topic, message, len); LOG_DEBUG(F("Received %s => %s (length %d)"), topic, message, len);
#endif #endif
// strip out everything until the last / // strip out everything until the last /
@@ -357,7 +358,7 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) {
} }
// if we got here we didn't find a topic match // if we got here we didn't find a topic match
DEBUG_LOG(F("No responding handler found for topic %s"), topic); LOG_DEBUG(F("No responding handler found for topic %s"), topic);
} }
// print all the topics related to a specific device_id // print all the topics related to a specific device_id
@@ -382,7 +383,7 @@ void Mqtt::show_topic_handlers(uuid::console::Shell & shell, const uint8_t devic
// and always remove from queue // and always remove from queue
void Mqtt::on_publish(uint16_t packetId) { void Mqtt::on_publish(uint16_t packetId) {
// find the MQTT message in the queue and remove it // find the MQTT message in the queue and remove it
if ((mqtt_messages_.empty()) || (mqtt_qos_ == 0)) { if (mqtt_messages_.empty()) {
return; return;
} }
@@ -394,9 +395,11 @@ void Mqtt::on_publish(uint16_t packetId) {
} }
if (mqtt_message.packet_id_ == packetId) { if (mqtt_message.packet_id_ == packetId) {
DEBUG_LOG(F("Acknowledged PID %d. Removing from queue"), packetId); #ifdef EMSESP_DEBUG
LOG_DEBUG(F("Acknowledged PID %d. Removing from queue"), packetId);
#endif
} else { } else {
DEBUG_LOG(F("Mismatch, expecting PID %d, got %d"), mqtt_message.packet_id_, packetId); LOG_DEBUG(F("Mismatch, expecting PID %d, got %d"), mqtt_message.packet_id_, packetId);
mqtt_publish_fails_++; // increment error count mqtt_publish_fails_++; // increment error count
} }
@@ -419,31 +422,26 @@ char * Mqtt::make_topic(char * result, const std::string & topic) {
return result; return result;
} }
void Mqtt::start() {
publish("status", "online", true); // say we're alive to the Last Will topic, with retain on
send_start_topic();
send_heartbeat(); // send heartbeat if enabled
}
// send online appended with the version information as JSON // send online appended with the version information as JSON
void Mqtt::send_start_topic() { void Mqtt::send_start_topic() {
StaticJsonDocument<90> doc; StaticJsonDocument<90> doc;
doc["event"] = "start"; doc["event"] = "start";
doc["version"] = Settings().app_version(); doc["version"] = Settings().app_version();
#ifndef EMSESP_STANDALONE
doc["IP"] = WiFi.localIP().toString();
#endif
publish("info", doc, false); // send with retain off publish("info", doc, false); // send with retain off
} }
// MQTT onConnect - when a connect is established // MQTT onConnect - when a connect is established
void Mqtt::on_connect() { void Mqtt::on_connect() {
DEBUG_LOG(F("MQTT connected"));
mqtt_reconnect_delay_ = Mqtt::MQTT_RECONNECT_DELAY_MIN; mqtt_reconnect_delay_ = Mqtt::MQTT_RECONNECT_DELAY_MIN;
mqtt_last_connection_ = millis(); mqtt_last_connection_ = uuid::get_uptime();
mqtt_connecting_ = false; mqtt_connecting_ = false;
LOG_INFO(F("MQTT connected"));
publish("status", "online", true); // say we're alive to the Last Will topic, with retain on
send_start_topic();
send_heartbeat(); // send heartbeat if enabled
} }
// send periodic MQTT message with system information // send periodic MQTT message with system information
@@ -462,38 +460,6 @@ void Mqtt::send_heartbeat() {
publish("heartbeat", doc, false); // send to MQTT with retain off publish("heartbeat", doc, false); // send to MQTT with retain off
} }
// add MQTT message to queue, payload is a JSON doc.
// NOTE this only prints first 255 chars
void Mqtt::queue_publish_message(const std::string & topic, const JsonDocument & payload, const bool retain) {
// can't have bogus topics, but empty payloads are ok
if (topic.empty()) {
return;
}
/*
// check for empty JSON doc - we don't like those
size_t capacity = measureJson(payload);
if (capacity <= 3) {
// DEBUG_LOG(("Empty JSON payload for topic %s. Skipping"), topic);
return;
}
*/
std::string payload_text;
serializeJson(payload, payload_text);
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());
// if the queue is full, make room but removing the last one
if (mqtt_messages_.size() >= maximum_mqtt_messages_) {
mqtt_messages_.pop_front();
}
mqtt_messages_.emplace_back(mqtt_message_id_++, std::move(message));
}
// add MQTT message to queue, payload is a string // add MQTT message to queue, payload is a string
void Mqtt::queue_publish_message(const std::string & topic, const std::string & payload, const bool retain) { void Mqtt::queue_publish_message(const std::string & topic, const std::string & payload, const bool retain) {
// can't have bogus topics, but empty payloads are ok // can't have bogus topics, but empty payloads are ok
@@ -518,7 +484,7 @@ void Mqtt::queue_subscribe_message(const std::string & topic) {
} }
auto message = std::make_shared<MqttMessage>(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()); LOG_DEBUG(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
if (mqtt_messages_.size() >= maximum_mqtt_messages_) { if (mqtt_messages_.size() >= maximum_mqtt_messages_) {
@@ -528,24 +494,27 @@ void Mqtt::queue_subscribe_message(const std::string & topic) {
mqtt_messages_.emplace_back(mqtt_message_id_++, std::move(message)); mqtt_messages_.emplace_back(mqtt_message_id_++, std::move(message));
} }
// Publish using the user's custom retain flag
void Mqtt::publish(const std::string & topic, const std::string & payload) {
publish(topic, payload, mqtt_retain_);
}
void Mqtt::publish(const std::string & topic, const JsonDocument & payload) {
publish(topic, payload, mqtt_retain_);
}
// MQTT Publish, using a specific retain flag // MQTT Publish, using a specific retain flag
void Mqtt::publish(const std::string & topic, const std::string & payload, bool retain) { void Mqtt::publish(const std::string & topic, const std::string & payload, bool retain) {
queue_publish_message(topic, payload, retain); queue_publish_message(topic, payload, retain);
} }
// Publish using the user's custom retain flag
void Mqtt::publish(const std::string & topic, const std::string & payload) {
publish(topic, payload, mqtt_retain_);
}
void Mqtt::publish(const std::string & topic, const JsonDocument & payload) {
publish(topic, payload, mqtt_retain_);
}
void Mqtt::publish(const std::string & topic, const JsonDocument & payload, bool retain) { void Mqtt::publish(const std::string & topic, const JsonDocument & payload, bool retain) {
queue_publish_message(topic, payload, retain); // convert json to string
std::string payload_text;
serializeJson(payload, payload_text);
queue_publish_message(topic, payload_text, retain);
} }
// for booleans, which get converted to string values 1 and 0
void Mqtt::publish(const std::string & topic, const bool value) { void Mqtt::publish(const std::string & topic, const bool value) {
queue_publish_message(topic, value ? "1" : "0", mqtt_retain_); queue_publish_message(topic, value ? "1" : "0", mqtt_retain_);
} }
@@ -576,7 +545,7 @@ void Mqtt::process_queue() {
// append the hostname and base to the topic, unless we're doing native HA which has a different format // 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];
// if the topic starts with "homeassistant" we leave it untouched, otherwise append ho st and base // if the topic starts with "homeassistant" we leave it untouched, otherwise append host and base
if (strncmp(message->topic.c_str(), "homeassistant/", 13) == 0) { if (strncmp(message->topic.c_str(), "homeassistant/", 13) == 0) {
strcpy(full_topic, message->topic.c_str()); strcpy(full_topic, message->topic.c_str());
} else { } else {
@@ -585,14 +554,14 @@ void Mqtt::process_queue() {
// if we're subscribing... // if we're subscribing...
if (message->operation == Operation::SUBSCRIBE) { if (message->operation == Operation::SUBSCRIBE) {
DEBUG_LOG(F("Subscribing to topic: %s"), full_topic); LOG_DEBUG(F("Subscribing to topic: %s"), full_topic);
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
uint16_t packet_id = mqttClient_.subscribe(full_topic, mqtt_qos_); uint16_t packet_id = mqttClient_.subscribe(full_topic, mqtt_qos_);
#else #else
uint16_t packet_id = 1; uint16_t packet_id = 1;
#endif #endif
if (!packet_id) { if (!packet_id) {
DEBUG_LOG(F("Error subscribing to %s, error %d"), full_topic, packet_id); LOG_DEBUG(F("Error subscribing to %s, error %d"), full_topic, packet_id);
} }
mqtt_messages_.pop_front(); // remove the message from the queue mqtt_messages_.pop_front(); // remove the message from the queue
@@ -606,25 +575,26 @@ void Mqtt::process_queue() {
return; return;
} }
// else try and publish it // else try and publish it
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
uint16_t packet_id = mqttClient_.publish(full_topic, mqtt_qos_, message->retain, message->payload.c_str()); // uint16_t packet_id = mqttClient_.publish(full_topic, mqtt_qos_, message->retain, message->payload.c_str());
uint16_t packet_id = mqttClient_.publish(full_topic, mqtt_qos_, message->retain, message->payload.c_str(), message->payload.size(), false, mqtt_message.id_);
#else #else
uint16_t packet_id = 1; uint16_t packet_id = 1;
#endif #endif
DEBUG_LOG(F("Publishing topic %s (#%02d, attempt #%d, pid %d)"), full_topic, mqtt_message.id_, mqtt_message.retry_count_ + 1, packet_id); LOG_DEBUG(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
if (mqtt_message.retry_count_ == (MQTT_PUBLISH_MAX_RETRY - 1)) { if (mqtt_message.retry_count_ == (MQTT_PUBLISH_MAX_RETRY - 1)) {
logger_.err(F("Failed to publish to %s after %d attempts"), full_topic, mqtt_message.retry_count_ + 1); LOG_ERROR(F("Failed to publish to %s after %d attempts"), full_topic, mqtt_message.retry_count_ + 1);
mqtt_publish_fails_++; // increment failure counter mqtt_publish_fails_++; // increment failure counter
mqtt_messages_.pop_front(); // delete mqtt_messages_.pop_front(); // delete
return; return;
} else { } else {
mqtt_messages_.front().retry_count_++; mqtt_messages_.front().retry_count_++;
// logger_.err(F("Failed to publish to %s. Trying again, #%d"), full_topic, mqtt_message.retry_count_ + 1); LOG_DEBUG(F("Failed to publish to %s. Trying again, #%d"), full_topic, mqtt_message.retry_count_ + 1);
DEBUG_LOG(F("Failed to publish to %s. Trying again, #%d"), full_topic, mqtt_message.retry_count_ + 1);
return; // leave on queue for next time so it gets republished return; // leave on queue for next time so it gets republished
} }
} }
@@ -633,7 +603,9 @@ void Mqtt::process_queue() {
// but add the packet_id so we can check it later // but add the packet_id so we can check it later
if (mqtt_qos_ != 0) { if (mqtt_qos_ != 0) {
mqtt_messages_.front().packet_id_ = packet_id; mqtt_messages_.front().packet_id_ = packet_id;
DEBUG_LOG(F("Setting packetID for ACK to %d"), packet_id); #ifdef EMSESP_DEBUG
LOG_DEBUG(F("Setting packetID for ACK to %d"), packet_id);
#endif
return; return;
} }
@@ -678,6 +650,8 @@ void Mqtt::console_commands(Shell & shell, unsigned int context) {
value = Settings::MQTT_format::NESTED; value = Settings::MQTT_format::NESTED;
} else if (arguments[0] == read_flash_string(F_(ha))) { } else if (arguments[0] == read_flash_string(F_(ha))) {
value = Settings::MQTT_format::HA; value = Settings::MQTT_format::HA;
} else if (arguments[0] == read_flash_string(F_(my))) {
value = Settings::MQTT_format::MY;
} else { } else {
shell.println(F("Must be single, nested or ha")); shell.println(F("Must be single, nested or ha"));
return; return;
@@ -687,7 +661,7 @@ void Mqtt::console_commands(Shell & shell, unsigned int context) {
shell.println(F("Please restart EMS-ESP")); 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_(single)), read_flash_string(F_(nested)), read_flash_string(F_(ha))}; return std::vector<std::string>{read_flash_string(F_(single)), read_flash_string(F_(nested)), read_flash_string(F_(ha)), read_flash_string(F_(my))};
}); });
EMSESPShell::commands->add_command(ShellContext::MQTT, EMSESPShell::commands->add_command(ShellContext::MQTT,
@@ -852,6 +826,8 @@ void Mqtt::console_commands(Shell & shell, unsigned int context) {
shell.printfln(F_(mqtt_format_fmt), F_(nested)); shell.printfln(F_(mqtt_format_fmt), F_(nested));
} else if (settings.mqtt_format() == Settings::MQTT_format::HA) { } else if (settings.mqtt_format() == Settings::MQTT_format::HA) {
shell.printfln(F_(mqtt_format_fmt), F_(ha)); shell.printfln(F_(mqtt_format_fmt), F_(ha));
} else if (settings.mqtt_format() == Settings::MQTT_format::MY) {
shell.printfln(F_(mqtt_format_fmt), F_(my));
} }
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());

View File

@@ -63,6 +63,7 @@ struct MqttMessage {
class Mqtt { class Mqtt {
public: public:
void loop(); void loop();
void start();
void send_heartbeat(); void send_heartbeat();
enum Operation { PUBLISH, SUBSCRIBE }; enum Operation { PUBLISH, SUBSCRIBE };
@@ -109,32 +110,28 @@ class Mqtt {
}; };
static std::deque<QueuedMqttMessage> mqtt_messages_; static std::deque<QueuedMqttMessage> mqtt_messages_;
void start();
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
static AsyncMqttClient mqttClient_; static AsyncMqttClient mqttClient_;
#endif #endif
void flush_message_queue(); void flush_message_queue();
void setup();
static constexpr size_t MAX_MQTT_MESSAGES = 50; static constexpr size_t MAX_MQTT_MESSAGES = 50;
static size_t maximum_mqtt_messages_; static size_t maximum_mqtt_messages_;
static uint16_t mqtt_message_id_; static uint16_t mqtt_message_id_;
static bool mqtt_retain_;
static constexpr uint8_t MQTT_QUEUE_MAX_SIZE = 50; static constexpr uint8_t MQTT_QUEUE_MAX_SIZE = 50;
static constexpr uint32_t MQTT_PUBLISH_WAIT = 750; // delay between sending publishes, although it should be asynchronous! static constexpr uint32_t MQTT_PUBLISH_WAIT = 750; // 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 static constexpr uint8_t MQTT_PUBLISH_MAX_RETRY = 3; // max retries for giving up on publishing
static constexpr uint8_t MQTT_KEEP_ALIVE = 60; static constexpr uint8_t MQTT_KEEP_ALIVE = 60; // 60 seconds. This could also be less, like 30 seconds
static constexpr uint32_t MQTT_RECONNECT_DELAY_MIN = 2000; // Try to reconnect in 2 seconds upon disconnection static constexpr uint32_t MQTT_RECONNECT_DELAY_MIN = 2000; // Try to reconnect in 2 seconds upon disconnection
static constexpr uint32_t MQTT_RECONNECT_DELAY_STEP = 3000; // Increase the reconnect delay in 3 seconds after each failed attempt static constexpr uint32_t MQTT_RECONNECT_DELAY_STEP = 3000; // Increase the reconnect delay in 3 seconds after each failed attempt
static constexpr uint32_t MQTT_RECONNECT_DELAY_MAX = 120000; // Set reconnect time to 2 minutes at most static constexpr uint32_t MQTT_RECONNECT_DELAY_MAX = 120000; // Set reconnect time to 2 minutes at most
static constexpr uint32_t MQTT_HEARTBEAT_INTERVAL = 60000; // in milliseconds, how often the MQTT heartbeat is sent (1 min) static constexpr uint32_t MQTT_HEARTBEAT_INTERVAL = 60000; // in milliseconds, how often the MQTT heartbeat is sent (1 min)
static bool mqtt_retain_;
static void queue_publish_message(const std::string & topic, const JsonDocument & payload, const bool retain);
static void queue_publish_message(const std::string & topic, const std::string & payload, const bool retain); static void queue_publish_message(const std::string & topic, const std::string & payload, const bool retain);
static void queue_subscribe_message(const std::string & topic); static void queue_subscribe_message(const std::string & topic);
void on_publish(uint16_t packetId); void on_publish(uint16_t packetId);

View File

@@ -36,6 +36,7 @@ void Network::start() {
WiFi.persistent(false); WiFi.persistent(false);
WiFi.disconnect(true); WiFi.disconnect(true);
WiFi.setAutoReconnect(false); WiFi.setAutoReconnect(false);
WiFi.mode(WIFI_STA);
#endif #endif
#if defined(ESP8266) #if defined(ESP8266)
@@ -71,7 +72,7 @@ void Network::sta_mode_start(WiFiEvent_t event, WiFiEventInfo_t info) {
#if defined(ESP8266) #if defined(ESP8266)
void Network::sta_mode_connected(const WiFiEventStationModeConnected & event) { void Network::sta_mode_connected(const WiFiEventStationModeConnected & event) {
logger_.info(F("Connected to %s (%02X:%02X:%02X:%02X:%02X:%02X) on channel %u"), LOG_INFO(F("Connected to %s (%02X:%02X:%02X:%02X:%02X:%02X) on channel %u"),
event.ssid.c_str(), event.ssid.c_str(),
event.bssid[0], event.bssid[0],
event.bssid[1], event.bssid[1],
@@ -86,7 +87,7 @@ void Network::sta_mode_connected(const WiFiEventStationModeConnected & event) {
} }
#elif defined(ESP32) #elif defined(ESP32)
void Network::sta_mode_connected(WiFiEvent_t event, WiFiEventInfo_t info) { void Network::sta_mode_connected(WiFiEvent_t event, WiFiEventInfo_t info) {
logger_.info(F("Connected to %s (%02X:%02X:%02X:%02X:%02X:%02X) on channel %u"), LOG_INFO(F("Connected to %s (%02X:%02X:%02X:%02X:%02X:%02X) on channel %u"),
info.connected.ssid, info.connected.ssid,
info.sta_connected.mac[0], info.sta_connected.mac[0],
info.sta_connected.mac[1], info.sta_connected.mac[1],
@@ -107,10 +108,10 @@ void Network::sta_mode_disconnected(const WiFiEventStationModeDisconnected & eve
if (event.reason == 201) { if (event.reason == 201) {
if (++disconnect_count_ == 3) { if (++disconnect_count_ == 3) {
if (System::safe_mode()) { if (System::safe_mode()) {
logger_.err(F("Failed to connect to WiFi %s after %d attempts"), event.ssid.c_str(), disconnect_count_ - 1); LOG_ERROR(F("Failed to connect to WiFi %s after %d attempts"), event.ssid.c_str(), disconnect_count_ - 1);
disconnect_count_ = 0; disconnect_count_ = 0;
} else { } else {
logger_.err(F("Failed to connect to WiFi. Rebooting into Safe mode")); LOG_ERROR(F("Failed to connect to WiFi. Rebooting into Safe mode"));
System::restart(true); // set safe mode and restart System::restart(true); // set safe mode and restart
} }
} }
@@ -118,20 +119,20 @@ void Network::sta_mode_disconnected(const WiFiEventStationModeDisconnected & eve
} }
#elif defined(ESP32) #elif defined(ESP32)
void Network::sta_mode_disconnected(WiFiEvent_t event, WiFiEventInfo_t info) { void Network::sta_mode_disconnected(WiFiEvent_t event, WiFiEventInfo_t info) {
// logger_.err(F("Failed to connect to WiFi %s, reason code %d"), info.disconnected.ssid, info.disconnected.reason); // LOG_ERROR(F("Failed to connect to WiFi %s, reason code %d"), info.disconnected.ssid, info.disconnected.reason);
} }
#endif #endif
#if defined(ESP8266) #if defined(ESP8266)
void Network::sta_mode_got_ip(const WiFiEventStationModeGotIP & event) { void Network::sta_mode_got_ip(const WiFiEventStationModeGotIP & event) {
logger_.info(F("Obtained IPv4 address %s/%s and gateway %s"), LOG_INFO(F("Obtained IPv4 address %s/%s and gateway %s"),
uuid::printable_to_string(event.ip).c_str(), uuid::printable_to_string(event.ip).c_str(),
uuid::printable_to_string(event.mask).c_str(), uuid::printable_to_string(event.mask).c_str(),
uuid::printable_to_string(event.gw).c_str()); uuid::printable_to_string(event.gw).c_str());
} }
#elif defined(ESP32) #elif defined(ESP32)
void Network::sta_mode_got_ip(WiFiEvent_t event, WiFiEventInfo_t info) { void Network::sta_mode_got_ip(WiFiEvent_t event, WiFiEventInfo_t info) {
logger_.info(F("Obtained IPv4 address %s/%s and gateway %s"), LOG_INFO(F("Obtained IPv4 address %s/%s and gateway %s"),
uuid::printable_to_string(IPAddress(info.got_ip.ip_info.ip.addr)).c_str(), uuid::printable_to_string(IPAddress(info.got_ip.ip_info.ip.addr)).c_str(),
uuid::printable_to_string(IPAddress(info.got_ip.ip_info.netmask.addr)).c_str(), uuid::printable_to_string(IPAddress(info.got_ip.ip_info.netmask.addr)).c_str(),
uuid::printable_to_string(IPAddress(info.got_ip.ip_info.gw.addr)).c_str()); uuid::printable_to_string(IPAddress(info.got_ip.ip_info.gw.addr)).c_str());
@@ -202,21 +203,21 @@ void Network::ota_setup() {
ota_->setPassword(settings.admin_password().c_str()); ota_->setPassword(settings.admin_password().c_str());
ota_->onStart([this]() { ota_->onStart([this]() {
DEBUG_LOG(F("OTA starting (send type %d)..."), ota_->getCommand()); LOG_DEBUG(F("OTA starting (send type %d)..."), ota_->getCommand());
// turn off stuff to stop interference // turn off stuff to stop interference
EMSuart::stop(); // UART stop EMSuart::stop(); // UART stop
in_ota_ = true; // set flag so all other services stop in_ota_ = true; // set flag so all other services stop
}); });
ota_->onEnd([this]() { DEBUG_LOG(F("OTA done, automatically restarting")); }); ota_->onEnd([this]() { LOG_DEBUG(F("OTA done, automatically restarting")); });
ota_->onProgress([this](unsigned int progress, unsigned int total) { ota_->onProgress([this](unsigned int progress, unsigned int total) {
/* /*
static unsigned int _progOld; static unsigned int _progOld;
unsigned int _prog = (progress / (total / 100)); unsigned int _prog = (progress / (total / 100));
if (_prog != _progOld) { if (_prog != _progOld) {
DEBUG_LOG(F("[OTA] Progress: %u%%"), _prog); LOG_DEBUG(F("[OTA] Progress: %u%%"), _prog);
_progOld = _prog; _progOld = _prog;
} }
*/ */
@@ -224,23 +225,23 @@ void Network::ota_setup() {
ota_->onError([this](ota_error_t error) { ota_->onError([this](ota_error_t error) {
if (error == OTA_AUTH_ERROR) { if (error == OTA_AUTH_ERROR) {
logger_.err(F("[OTA] Auth Failed")); LOG_ERROR(F("[OTA] Auth Failed"));
} else if (error == OTA_BEGIN_ERROR) { } else if (error == OTA_BEGIN_ERROR) {
logger_.err(F("[OTA] Begin Failed")); LOG_ERROR(F("[OTA] Begin Failed"));
} else if (error == OTA_CONNECT_ERROR) { } else if (error == OTA_CONNECT_ERROR) {
logger_.err(F("[OTA] Connect Failed")); LOG_ERROR(F("[OTA] Connect Failed"));
} else if (error == OTA_RECEIVE_ERROR) { } else if (error == OTA_RECEIVE_ERROR) {
logger_.err(F("[OTA] Receive Failed")); LOG_ERROR(F("[OTA] Receive Failed"));
} else if (error == OTA_END_ERROR) { } else if (error == OTA_END_ERROR) {
logger_.err(F("[OTA] End Failed")); LOG_ERROR(F("[OTA] End Failed"));
} else { } else {
logger_.err(F("[OTA] Error %d"), error); LOG_ERROR(F("[OTA] Error %d"), error);
}; };
}); });
// start ota service // start ota service
ota_->begin(); ota_->begin();
logger_.info(F("Listening to firmware updates on %s.local:%u"), ota_->getHostname().c_str(), OTA_PORT); LOG_INFO(F("Listening to firmware updates on %s.local:%u"), ota_->getHostname().c_str(), OTA_PORT);
#endif #endif
} }

View File

@@ -30,6 +30,23 @@ 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_format_ = Settings().mqtt_format(); mqtt_format_ = Settings().mqtt_format();
// if we're using HA MQTT Discovery, send out the config
// currently we just do this for a single sensor (sensor1)
if (mqtt_format_ == Settings::MQTT_format::HA) {
// Mqtt::publish(topic); // empty payload, this remove any previous config sent to HA
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
doc["dev_cla"] = "temperature";
doc["name"] = "ems-esp-sensor1";
doc["uniq_id"] = "ems-esp-sensor1";
doc["~"] = "homeassistant/sensor/ems-esp/external";
doc["stat_t"] = "~/state";
doc["unit_of_meas"] = "°C";
doc["val_tpl"] = "{{value_json.sensor1.temp}}";
Mqtt::publish("homeassistant/sensor/ems-esp/external/config", doc, true); // publish the config payload with retain flag
}
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
bus_.begin(SENSOR_GPIO); bus_.begin(SENSOR_GPIO);
#endif #endif
@@ -37,9 +54,11 @@ void Sensors::start() {
void Sensors::loop() { void Sensors::loop() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
uint32_t time_now = uuid::get_uptime();
if (state_ == State::IDLE) { if (state_ == State::IDLE) {
if (millis() - last_activity_ >= READ_INTERVAL_MS) { if (time_now - last_activity_ >= READ_INTERVAL_MS) {
// DEBUG_LOG(F("Read sensor temperature")); // uncomment for debug // LOG_DEBUG(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);
@@ -47,30 +66,30 @@ void Sensors::loop() {
state_ = State::READING; state_ = State::READING;
} else { } else {
// no sensors found // no sensors found
// logger_.err(F("Bus reset failed")); // uncomment for debug // LOG_ERROR(F("Bus reset failed")); // uncomment for debug
devices_.clear(); // remove all know devices incase we have a disconnect devices_.clear(); // remove all know devices incase we have a disconnect
} }
last_activity_ = millis(); last_activity_ = time_now;
} }
} else if (state_ == State::READING) { } else if (state_ == State::READING) {
if (temperature_convert_complete()) { if (temperature_convert_complete()) {
// DEBUG_LOG(F("Scanning for sensors")); // uncomment for debug // LOG_DEBUG(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_ = time_now;
} else if (millis() - last_activity_ > READ_TIMEOUT_MS) { } else if (time_now - last_activity_ > READ_TIMEOUT_MS) {
logger_.err(F("Sensor read timeout")); LOG_ERROR(F("Sensor read timeout"));
state_ = State::IDLE; state_ = State::IDLE;
last_activity_ = millis(); last_activity_ = time_now;
} }
} else if (state_ == State::SCANNING) { } else if (state_ == State::SCANNING) {
if (millis() - last_activity_ > SCAN_TIMEOUT_MS) { if (time_now - last_activity_ > SCAN_TIMEOUT_MS) {
logger_.err(F("Sensor scan timeout")); LOG_ERROR(F("Sensor scan timeout"));
state_ = State::IDLE; state_ = State::IDLE;
last_activity_ = millis(); last_activity_ = time_now;
} else { } else {
uint8_t addr[ADDR_LEN] = {0}; uint8_t addr[ADDR_LEN] = {0};
@@ -89,26 +108,26 @@ void Sensors::loop() {
/* /*
// comment out for debugging // comment out for debugging
char result[10]; char result[10];
DEBUG_LOG(F("Temp of %s = %s"), LOG_DEBUG(F("Temp of %s = %s"),
found_.back().to_string().c_str(), found_.back().to_string().c_str(),
Helpers::render_value(result, found_.back().temperature_c_, 2)); Helpers::render_value(result, found_.back().temperature_c_, 2));
*/ */
break; break;
default: default:
logger_.err(F("Unknown sensor %s"), Device(addr).to_string().c_str()); LOG_ERROR(F("Unknown sensor %s"), Device(addr).to_string().c_str());
break; break;
} }
} else { } else {
logger_.err(F("Invalid sensor %s"), Device(addr).to_string().c_str()); LOG_ERROR(F("Invalid sensor %s"), Device(addr).to_string().c_str());
} }
} else { } else {
bus_.depower(); bus_.depower();
devices_ = std::move(found_); devices_ = std::move(found_);
found_.clear(); found_.clear();
// DEBUG_LOG(F("Found %zu sensor(s). Adding them."), devices_.size()); // uncomment for debug // LOG_DEBUG(F("Found %zu sensor(s). Adding them."), devices_.size()); // uncomment for debug
state_ = State::IDLE; state_ = State::IDLE;
last_activity_ = millis(); last_activity_ = time_now;
} }
} }
} }
@@ -129,7 +148,7 @@ bool Sensors::temperature_convert_complete() {
float Sensors::get_temperature_c(const uint8_t addr[]) { float Sensors::get_temperature_c(const uint8_t addr[]) {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
if (!bus_.reset()) { if (!bus_.reset()) {
logger_.err(F("Bus reset failed before reading scratchpad from %s"), Device(addr).to_string().c_str()); LOG_ERROR(F("Bus reset failed before reading scratchpad from %s"), Device(addr).to_string().c_str());
return NAN; return NAN;
} }
@@ -140,12 +159,12 @@ float Sensors::get_temperature_c(const uint8_t addr[]) {
bus_.read_bytes(scratchpad, SCRATCHPAD_LEN); bus_.read_bytes(scratchpad, SCRATCHPAD_LEN);
if (!bus_.reset()) { if (!bus_.reset()) {
logger_.err(F("Bus reset failed after reading scratchpad from %s"), Device(addr).to_string().c_str()); LOG_ERROR(F("Bus reset failed after reading scratchpad from %s"), Device(addr).to_string().c_str());
return NAN; return NAN;
} }
if (bus_.crc8(scratchpad, SCRATCHPAD_LEN - 1) != scratchpad[SCRATCHPAD_LEN - 1]) { if (bus_.crc8(scratchpad, SCRATCHPAD_LEN - 1) != scratchpad[SCRATCHPAD_LEN - 1]) {
logger_.warning(F("Invalid scratchpad CRC: %02X%02X%02X%02X%02X%02X%02X%02X%02X from device %s"), LOG_WARNING(F("Invalid scratchpad CRC: %02X%02X%02X%02X%02X%02X%02X%02X%02X from device %s"),
scratchpad[0], scratchpad[0],
scratchpad[1], scratchpad[1],
scratchpad[2], scratchpad[2],
@@ -203,7 +222,6 @@ uint64_t Sensors::Device::id() const {
std::string Sensors::Device::to_string() const { std::string Sensors::Device::to_string() const {
std::string str(20, '\0'); std::string str(20, '\0');
snprintf_P(&str[0], snprintf_P(&str[0],
str.capacity() + 1, str.capacity() + 1,
PSTR("%02X-%04X-%04X-%04X-%02X"), PSTR("%02X-%04X-%04X-%04X-%02X"),
@@ -214,6 +232,18 @@ std::string Sensors::Device::to_string() const {
(unsigned int)(id_)&0xFF); (unsigned int)(id_)&0xFF);
return str; return str;
} }
std::string Sensors::Device::to_stringc() const {
std::string str(20, '\0');
snprintf_P(&str[0],
str.capacity() + 1,
PSTR("%02X%04X%04X%04X%02X"),
(unsigned int)(id_ >> 56) & 0xFF,
(unsigned int)(id_ >> 40) & 0xFFFF,
(unsigned int)(id_ >> 24) & 0xFFFF,
(unsigned int)(id_ >> 8) & 0xFFFF,
(unsigned int)(id_)&0xFF);
return str;
}
// send all dallas sensor values as a JSON package to MQTT // send all dallas sensor values as a JSON package to MQTT
// assumes there are devices // assumes there are devices
@@ -227,7 +257,7 @@ 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_format_ != Settings::MQTT_format::NESTED) { if (mqtt_format_ == Settings::MQTT_format::SINGLE) {
StaticJsonDocument<100> doc; StaticJsonDocument<100> doc;
for (const auto & device : devices_) { for (const auto & device : devices_) {
char s[5]; char s[5];
@@ -242,18 +272,23 @@ void Sensors::publish_values() {
} }
// group all sensors together - https://github.com/proddy/EMS-ESP/issues/327 // group all sensors together - https://github.com/proddy/EMS-ESP/issues/327
// https://arduinojson.org/v6/assistant/ // This is used for both NESTED and HA modes
// sensors = { // sensors = {
// "sensor1":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"}, // "sensor1":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"},
// "sensor2":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"}, // "sensor2":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"},
// "sensor3":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"}, // "sensor3":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"},
// "sensor4":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"} // "sensor4":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"}
// } // }
// const size_t capacity = num_devices * JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(num_devices); // const size_t capacity = num_devices * JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(num_devices);
DynamicJsonDocument doc(100 * num_devices); DynamicJsonDocument doc(100 * num_devices);
uint8_t i = 1; uint8_t i = 1;
for (const auto & device : devices_) { for (const auto & device : devices_) {
if (mqtt_format_ == Settings::MQTT_format::MY) {
char s[5];
doc[device.to_stringc()] = Helpers::render_value(s, device.temperature_c_, 2);
} else {
char sensorID[10]; // sensor{1-n} char sensorID[10]; // sensor{1-n}
strlcpy(sensorID, "sensor", 10); strlcpy(sensorID, "sensor", 10);
char s[5]; char s[5];
@@ -262,8 +297,13 @@ void Sensors::publish_values() {
dataSensor["id"] = device.to_string(); dataSensor["id"] = device.to_string();
dataSensor["temp"] = Helpers::render_value(s, device.temperature_c_, 2); dataSensor["temp"] = Helpers::render_value(s, device.temperature_c_, 2);
} }
}
if (mqtt_format_ == Settings::MQTT_format::HA) {
Mqtt::publish("homeassistant/sensor/ems-esp/external/state", doc);
} else {
Mqtt::publish("sensors", doc); Mqtt::publish("sensors", doc);
}
} }
} // namespace emsesp } // namespace emsesp

View File

@@ -46,6 +46,7 @@ class Sensors {
uint64_t id() const; uint64_t id() const;
std::string to_string() const; std::string to_string() const;
std::string to_stringc() const;
float temperature_c_ = NAN; float temperature_c_ = NAN;
@@ -66,8 +67,7 @@ class Sensors {
#if defined(ESP8266) #if defined(ESP8266)
static constexpr uint8_t SENSOR_GPIO = 14; // D5 static constexpr uint8_t SENSOR_GPIO = 14; // D5
#elif defined(ESP32) #elif defined(ESP32)
// static constexpr uint8_t SENSOR_GPIO = 14; // same position static constexpr uint8_t SENSOR_GPIO = 18; // Wemos D1-32 for compatibility D5
static constexpr uint8_t SENSOR_GPIO = 18; // for Wemos D1 32
#endif #endif
enum class State { IDLE, READING, SCANNING }; enum class State { IDLE, READING, SCANNING };
@@ -101,8 +101,8 @@ class Sensors {
bool temperature_convert_complete(); bool temperature_convert_complete();
float get_temperature_c(const uint8_t addr[]); float get_temperature_c(const uint8_t addr[]);
uint32_t last_activity_ = millis(); uint32_t last_activity_ = uuid::get_uptime();
uint32_t last_publish_ = millis(); uint32_t last_publish_ = uuid::get_uptime();
State state_ = State::IDLE; State state_ = State::IDLE;
std::vector<Device> found_; std::vector<Device> found_;
std::vector<Device> devices_; std::vector<Device> devices_;

View File

@@ -139,10 +139,10 @@ Settings::Settings() {
if (EMSESP_FS.begin(true)) { if (EMSESP_FS.begin(true)) {
#endif #endif
#endif #endif
logger_.info(F("Mounted filesystem")); LOG_INFO(F("Mounted filesystem"));
mounted_ = true; mounted_ = true;
} else { } else {
logger_.alert(F("Unable to mount filesystem")); LOG_ERROR(F("Unable to mount filesystem"));
unavailable_ = true; unavailable_ = true;
} }
} }
@@ -154,7 +154,7 @@ Settings::Settings() {
} }
if (!loaded_) { if (!loaded_) {
logger_.err(F("Failed to load settings. Using defaults")); LOG_ERROR(F("Failed to load settings. Using defaults"));
read_settings(ArduinoJson::StaticJsonDocument<0>()); read_settings(ArduinoJson::StaticJsonDocument<0>());
loaded_ = true; loaded_ = true;
} }
@@ -169,7 +169,7 @@ void Settings::commit() {
EMSuart::stop(); // temporary suspend UART because is can cause interference on the UART EMSuart::stop(); // temporary suspend UART because is can cause interference on the UART
logger_.debug(F("Saving settings")); LOG_DEBUG(F("Saving settings"));
if (write_settings(filename)) { if (write_settings(filename)) {
if (read_settings(filename, false)) { if (read_settings(filename, false)) {
write_settings(backup_filename); write_settings(backup_filename);
@@ -190,21 +190,21 @@ bool Settings::read_settings(const std::string & filename, bool load) {
auto error = ArduinoJson::deserializeMsgPack(doc, file); auto error = ArduinoJson::deserializeMsgPack(doc, file);
if (error) { if (error) {
logger_.err(F("Failed to parse settings file %s: %s"), filename.c_str(), error.c_str()); LOG_ERROR(F("Failed to parse settings file %s: %s"), filename.c_str(), error.c_str());
return false; return false;
} else { } else {
if (load) { if (load) {
logger_.info(F("Loading settings from file %s"), filename.c_str()); LOG_INFO(F("Loading settings from file %s"), filename.c_str());
read_settings(doc); read_settings(doc);
} }
return true; return true;
} }
} else { } else {
logger_.err(F("Settings file %s does not exist"), filename.c_str()); LOG_ERROR(F("Settings file %s does not exist"), filename.c_str());
return false; return false;
} }
#else #else
logger_.info(F("Loading settings from file %s (%d)"), filename.c_str(), load); LOG_INFO(F("Loading settings from file %s (%d)"), filename.c_str(), load);
return false; return false;
#endif #endif
} }
@@ -221,17 +221,17 @@ bool Settings::write_settings(const std::string & filename) {
ArduinoJson::serializeMsgPack(doc, file); ArduinoJson::serializeMsgPack(doc, file);
if (file.getWriteError()) { if (file.getWriteError()) {
logger_.alert(F("Failed to write settings file %s: %u"), filename.c_str(), file.getWriteError()); LOG_ERROR(F("Failed to write settings file %s: %u"), filename.c_str(), file.getWriteError());
return false; return false;
} else { } else {
return true; return true;
} }
} else { } else {
logger_.alert(F("Unable to open settings file %s for writing"), filename.c_str()); LOG_ERROR(F("Unable to open settings file %s for writing"), filename.c_str());
return false; return false;
} }
#else #else
logger_.debug(F("Write settings file %s"), filename.c_str()); LOG_DEBUG(F("Write settings file %s"), filename.c_str());
return false; return false;
#endif #endif
} }

View File

@@ -51,13 +51,13 @@
#define EMSESP_DEFAULT_MQTT_PORT 1884 #define EMSESP_DEFAULT_MQTT_PORT 1884
#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_FORMAT 2 // nested #define EMSESP_DEFAULT_MQTT_FORMAT 2 // 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
#define EMSESP_DEFAULT_SHOWER_ALERT false #define EMSESP_DEFAULT_SHOWER_ALERT false
#define EMSESP_DEFAULT_SYSLOG_INTERVAL 0 #define EMSESP_DEFAULT_SYSLOG_INTERVAL 0
#define EMSESP_DEFAULT_MASTER_THERMOSTAT 0 // not set #define EMSESP_DEFAULT_MASTER_THERMOSTAT 0 // 0=not set
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
#define EMSESP_DEFAULT_MQTT_PUBLISH_TIME 10 #define EMSESP_DEFAULT_MQTT_PUBLISH_TIME 10
@@ -148,7 +148,7 @@ 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 }; enum MQTT_format : uint8_t { SINGLE = 1, NESTED, HA, MY };
uint8_t mqtt_format() const; uint8_t mqtt_format() const;
void mqtt_format(const uint8_t & mqtt_format); void mqtt_format(const uint8_t & mqtt_format);

View File

@@ -36,7 +36,8 @@ void Shower::loop() {
return; return;
} }
uint32_t time_now = millis(); uint32_t time_now = uuid::get_uptime();
// if already in cold mode, ignore all this logic until we're out of the cold blast // if already in cold mode, ignore all this logic until we're out of the cold blast
if (!doing_cold_shot_) { if (!doing_cold_shot_) {
// is the hot water running? // is the hot water running?
@@ -55,7 +56,7 @@ void Shower::loop() {
if (!shower_on_ && (time_now - timer_start_) > SHOWER_MIN_DURATION) { if (!shower_on_ && (time_now - timer_start_) > SHOWER_MIN_DURATION) {
shower_on_ = true; shower_on_ = true;
Mqtt::publish("shower_active", (bool)true); Mqtt::publish("shower_active", (bool)true);
DEBUG_LOG(F("[Shower] hot water still running, starting shower timer")); LOG_DEBUG(F("[Shower] hot water still running, starting shower timer"));
} }
// check if the shower has been on too long // check if the shower has been on too long
else if ((((time_now - timer_start_) > SHOWER_MAX_DURATION) && !doing_cold_shot_) && shower_alert_) { else if ((((time_now - timer_start_) > SHOWER_MAX_DURATION) && !doing_cold_shot_) && shower_alert_) {
@@ -76,7 +77,7 @@ void Shower::loop() {
duration_ = (timer_pause_ - timer_start_ - SHOWER_OFFSET_TIME); duration_ = (timer_pause_ - timer_start_ - SHOWER_OFFSET_TIME);
if (duration_ > SHOWER_MIN_DURATION) { if (duration_ > SHOWER_MIN_DURATION) {
Mqtt::publish("shower_active", (bool)false); Mqtt::publish("shower_active", (bool)false);
DEBUG_LOG(F("[Shower] finished with duration %d"), duration_); LOG_DEBUG(F("[Shower] finished with duration %d"), duration_);
publish_values(); publish_values();
} }
} }
@@ -94,7 +95,7 @@ void Shower::loop() {
// turn back on the hot water for the shower // turn back on the hot water for the shower
void Shower::shower_alert_stop() { void Shower::shower_alert_stop() {
if (doing_cold_shot_) { if (doing_cold_shot_) {
DEBUG_LOG(F("Shower Alert stopped")); LOG_DEBUG(F("Shower Alert stopped"));
// Boiler::set_tapwarmwater_activated(true); // Boiler::set_tapwarmwater_activated(true);
doing_cold_shot_ = false; doing_cold_shot_ = false;
// showerColdShotStopTimer.detach(); // disable the timer // showerColdShotStopTimer.detach(); // disable the timer
@@ -104,7 +105,7 @@ void Shower::shower_alert_stop() {
// turn off hot water to send a shot of cold // turn off hot water to send a shot of cold
void Shower::shower_alert_start() { void Shower::shower_alert_start() {
if (shower_alert_) { if (shower_alert_) {
DEBUG_LOG(F("Shower Alert started!")); LOG_DEBUG(F("Shower Alert started!"));
// Boiler::set_tapwarmwater_activated(false); // Boiler::set_tapwarmwater_activated(false);
doing_cold_shot_ = true; doing_cold_shot_ = true;
// start the timer for n seconds which will reset the water back to hot // start the timer for n seconds which will reset the water back to hot

View File

@@ -29,7 +29,7 @@ uuid::log::Logger Solar::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand) Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) { : EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
DEBUG_LOG(F("Registering new Solar module with device ID 0x%02X"), device_id); LOG_DEBUG(F("Registering new Solar module with device ID 0x%02X"), device_id);
// telegram handlers // telegram handlers
register_telegram_type(0x0097, F("SM10Monitor"), true, std::bind(&Solar::process_SM10Monitor, this, _1)); register_telegram_type(0x0097, F("SM10Monitor"), true, std::bind(&Solar::process_SM10Monitor, this, _1));
@@ -108,7 +108,7 @@ void Solar::publish_values() {
} }
#ifdef EMSESP_DEBUG #ifdef EMSESP_DEBUG
DEBUG_LOG(F("[DEBUG] Performing a solar module publish")); LOG_DEBUG(F("[DEBUG] Performing a solar module publish"));
#endif #endif
Mqtt::publish("sm_data", doc); Mqtt::publish("sm_data", doc);

View File

@@ -60,7 +60,7 @@ void System::mqtt_commands(const char * message) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc; StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message); DeserializationError error = deserializeJson(doc, message);
if (error) { if (error) {
DEBUG_LOG(F("MQTT error: payload %s, error %s"), message, error.c_str()); LOG_DEBUG(F("MQTT error: payload %s, error %s"), message, error.c_str());
return; return;
} }
const char * command = doc["cmd"]; const char * command = doc["cmd"];
@@ -91,9 +91,9 @@ void System::restart(bool mode) {
// check for safe mode // check for safe mode
if (mode) { if (mode) {
logger_.notice("Restarting system in safe mode..."); LOG_NOTICE("Restarting system in safe mode...");
} else { } else {
logger_.notice("Restarting system..."); LOG_NOTICE("Restarting system...");
} }
Shell::loop_all(); Shell::loop_all();
@@ -140,9 +140,11 @@ void System::start() {
settings.app_version(EMSESP_APP_VERSION); settings.app_version(EMSESP_APP_VERSION);
settings.commit(); settings.commit();
logger_.info(F("System booted (EMS-ESP version %s)"), settings.app_version().c_str()); LOG_INFO(F("System booted (EMS-ESP version %s)"), settings.app_version().c_str());
pinMode(LED_GPIO, OUTPUT); // LED pin if (LED_GPIO) {
pinMode(LED_GPIO, OUTPUT); // LED pin, 0 is disabled
}
// register MQTT system commands // register MQTT system commands
Mqtt::subscribe("cmd", std::bind(&System::mqtt_commands, this, _1)); Mqtt::subscribe("cmd", std::bind(&System::mqtt_commands, this, _1));
@@ -218,19 +220,11 @@ void System::loop() {
syslog_.loop(); syslog_.loop();
#endif #endif
if (LED_GPIO) {
led_monitor(); // check status and report back using the LED led_monitor(); // check status and report back using the LED
}
system_check(); // check system health system_check(); // check system health
/*
#ifdef EMSESP_DEBUG
static uint32_t last_debug_ = 0;
if (millis() - last_debug_ >= 5000) {
last_debug_ = millis();
show_mem("loop");
}
#endif
*/
} }
void System::show_mem(const char * text) { void System::show_mem(const char * text) {
@@ -239,7 +233,7 @@ void System::show_mem(const char * text) {
#else #else
uint32_t mem = 1000; uint32_t mem = 1000;
#endif #endif
logger_.notice(F("{%s} Free mem: %ld (%d%%)"), text, mem, (100 * mem / heap_start_)); LOG_NOTICE(F("{%s} Free mem: %ld (%d%%)"), text, mem, (100 * mem / heap_start_));
} }
// sets rate of led flash // sets rate of led flash
@@ -252,9 +246,8 @@ void System::set_led_speed(uint32_t speed) {
void System::system_check() { void System::system_check() {
static uint32_t last_system_check_ = 0; static uint32_t last_system_check_ = 0;
uint32_t currentMillis = millis(); if (!last_system_check_ || ((uint32_t)(uuid::get_uptime() - last_system_check_) >= SYSTEM_CHECK_FREQUENCY)) {
if (!last_system_check_ || ((uint32_t)(currentMillis - last_system_check_) >= SYSTEM_CHECK_FREQUENCY)) { last_system_check_ = uuid::get_uptime();
last_system_check_ = currentMillis;
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
if ((WiFi.status() != WL_CONNECTED) || safe_mode()) { if ((WiFi.status() != WL_CONNECTED) || safe_mode()) {
@@ -268,23 +261,25 @@ void System::system_check() {
if (!EMSbus::bus_connected()) { if (!EMSbus::bus_connected()) {
system_healthy_ = false; system_healthy_ = false;
set_led_speed(LED_WARNING_BLINK); // flash every 1/2 second from now on set_led_speed(LED_WARNING_BLINK); // flash every 1/2 second from now on
LOG_ERROR(F("No connection to the EMS bus!"));
} else { } else {
// if it was unhealthy but now we're better, make sure the LED is solid again cos we've been healed // if it was unhealthy but now we're better, make sure the LED is solid again cos we've been healed
if (!system_healthy_) { if (!system_healthy_) {
system_healthy_ = true; system_healthy_ = true;
if (LED_GPIO) {
digitalWrite(LED_GPIO, LED_ON); // LED on, for ever digitalWrite(LED_GPIO, LED_ON); // LED on, for ever
} }
} }
} }
}
} }
// flashes the LED // flashes the LED
void System::led_monitor() { void System::led_monitor() {
static uint32_t led_last_blink_ = 0; static uint32_t led_last_blink_ = 0;
uint32_t currentMillis = millis(); if (!led_last_blink_ || (uint32_t)(uuid::get_uptime() - led_last_blink_) >= led_flash_speed_) {
if (!led_last_blink_ || (uint32_t)(currentMillis - led_last_blink_) >= led_flash_speed_) { led_last_blink_ = uuid::get_uptime();
led_last_blink_ = currentMillis;
// if bus_not_connected or network not connected, start flashing // if bus_not_connected or network not connected, start flashing
if (!system_healthy_) { if (!system_healthy_) {

View File

@@ -60,7 +60,7 @@ class System {
static void restart(bool safe_mode); static void restart(bool safe_mode);
static void restart() { static void restart() {
restart(false); // no safe mode restart(false); // default, don't boot into safe mode
} }
static void show_mem(const char * text); static void show_mem(const char * text);
@@ -74,8 +74,8 @@ class System {
static uuid::syslog::SyslogService syslog_; static uuid::syslog::SyslogService syslog_;
#endif #endif
static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 5000; // check every 5 seconds static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 10000; // check every 10 seconds
static constexpr uint32_t LED_WARNING_BLINK = 1000; // pulse to show no connection static constexpr uint32_t LED_WARNING_BLINK = 1000; // pulse to show no connection, 1 sec
static constexpr uint32_t LED_WARNING_BLINK_FAST = 100; // flash quickly for boot up sequence or safe-mode static constexpr uint32_t LED_WARNING_BLINK_FAST = 100; // flash quickly for boot up sequence or safe-mode
// internal LED // internal LED
@@ -108,6 +108,7 @@ class System {
static int reset_counter_; static int reset_counter_;
static EMSuart emsuart_; static EMSuart emsuart_;
#if defined(ESP8266) #if defined(ESP8266)
static RTCVars state_; static RTCVars state_;
#endif #endif

View File

@@ -200,9 +200,10 @@ void RxService::flush_rx_queue() {
rx_telegram_id_ = 0; rx_telegram_id_ = 0;
} }
// start and initialize the Rx incoming buffer. Not currently used. // start and initialize the Rx incoming buffer
void RxService::start() { void RxService::start() {
// DEBUG_LOG(F("RxStart")); // LOG_DEBUG(F("RxStart"));
// function not currently used
} }
// Rx loop, run as many times as you can // Rx loop, run as many times as you can
@@ -210,11 +211,10 @@ void RxService::start() {
void RxService::loop() { void RxService::loop() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
// give rx some breathing space // give rx some breathing space
uint32_t time_now = millis(); if ((uuid::get_uptime() - last_rx_check_) < RX_LOOP_WAIT) {
if ((time_now - last_rx_check_) < RX_LOOP_WAIT) {
return; return;
} }
last_rx_check_ = time_now; last_rx_check_ = uuid::get_uptime();
#endif #endif
while (!rx_telegrams_.empty()) { while (!rx_telegrams_.empty()) {
@@ -235,12 +235,13 @@ void RxService::loop() {
// add a new rx telegram object // add a new rx telegram object
// data is the whole telegram, assuming last byte holds the CRC // data is the whole telegram, assuming last byte holds the CRC
// length includes 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) {
// 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) {
TRACE_LOG(F("Rx: %s %s(BAD, CRC %02X != %02X)%s"), Helpers::data_to_hex(data, length).c_str(), COLOR_RED, data[length - 1], crc, COLOR_RESET); LOG_TRACE(F("Rx: %s %s(BAD, CRC %02X != %02X)%s"), Helpers::data_to_hex(data, length).c_str(), COLOR_RED, data[length - 1], crc, COLOR_RESET);
increment_telegram_error_count(); increment_telegram_error_count();
return; return;
} }
@@ -253,9 +254,9 @@ void RxService::add(uint8_t * data, uint8_t length) {
ems_mask(data[0]); ems_mask(data[0]);
} }
// if we're in read only mode, just dump out to console // if we're in "trace" and "raw" print out actual telegram
if (EMSESP::ems_read_only()) { if (logger_.enabled(Level::TRACE) && EMSESP::trace_raw()) {
TRACE_LOG(F("Rx: %s"), Helpers::data_to_hex(data, length).c_str()); LOG_TRACE(F("Rx: %s"), Helpers::data_to_hex(data, length).c_str());
} }
// src, dest and offset are always in fixed positions // src, dest and offset are always in fixed positions
@@ -263,7 +264,7 @@ void RxService::add(uint8_t * data, uint8_t length) {
uint8_t dest = data[1] & 0x7F; // strip MSB, don't care if its read or write for processing uint8_t dest = data[1] & 0x7F; // strip MSB, don't care if its read or write for processing
uint8_t offset = data[3]; // offset is always 4th byte uint8_t offset = data[3]; // offset is always 4th byte
uint16_t type_id; // this could be 2 bytes for ems+ uint16_t type_id = 0; // this could be 2 bytes for ems+
uint8_t * message_data; uint8_t * message_data;
uint8_t message_length; uint8_t message_length;
@@ -274,15 +275,21 @@ void RxService::add(uint8_t * data, uint8_t length) {
message_data = data + 4; // message block starts at 5th byte message_data = data + 4; // message block starts at 5th byte
message_length = length - 5; // remove 4 bytes header plus CRC message_length = length - 5; // remove 4 bytes header plus CRC
} else { } else {
// EMS 2.0 // EMS 2.0 / EMS+
if (data[2] == 0xFF) { if (data[2] == 0xFF) {
type_id = (data[4] << 8) + data[5] + 256; // check for empty data
message_data = data + 6; // message block starts at 7th position // special broadcast telegrams on ems+ have no data values, some even don't have a type ID
if (length <= 7) { if (length <= 7) {
message_length = 0; // special broadcast on ems+ have no data values message_data = data; // bogus pointer, will not be used
message_length = 0;
if (length <= 5) {
type_id = 0; // has also an empty type_id
} else {
type_id = (data[4] << 8) + data[5] + 256;
}
} else { } else {
message_length = length - 7; // remove 6 byte header plus CRC message_length = length - 7; // remove 6 byte header plus CRC
message_data = data + 6; // message block starts at 7th position
} }
} else { } else {
// its F9 or F7 // its F9 or F7
@@ -297,20 +304,28 @@ void RxService::add(uint8_t * data, uint8_t length) {
} }
} }
// if we don't have a type_id, exit
if (type_id == 0) {
return;
}
// create the telegram // create the telegram
auto telegram = std::make_shared<Telegram>(Telegram::Operation::RX, src, dest, type_id, offset, message_data, message_length); auto telegram = std::make_shared<Telegram>(Telegram::Operation::RX, src, dest, type_id, offset, message_data, message_length);
// check if queue is full // check if queue is full
if (rx_telegrams_.size() >= maximum_rx_telegrams_) { if (rx_telegrams_.size() >= MAX_RX_TELEGRAMS) {
// rx_telegrams_overflow_ = true; // rx_telegrams_overflow_ = true;
rx_telegrams_.pop_front(); rx_telegrams_.pop_front();
} }
// add to queue, with timestamp // add to queue
DEBUG_LOG(F("New Rx [#%d] telegram added, length %d"), rx_telegram_id_, message_length); LOG_DEBUG(F("New Rx [#%d] telegram, length %d"), rx_telegram_id_, message_length);
rx_telegrams_.emplace_back(rx_telegram_id_++, std::move(telegram)); rx_telegrams_.emplace_back(rx_telegram_id_++, std::move(telegram));
} }
//
// Tx CODE here
//
TxService::QueuedTxTelegram::QueuedTxTelegram(uint16_t id, std::shared_ptr<Telegram> && telegram) TxService::QueuedTxTelegram::QueuedTxTelegram(uint16_t id, std::shared_ptr<Telegram> && telegram)
: id_(id) : id_(id)
@@ -325,8 +340,6 @@ void TxService::flush_tx_queue() {
// start and initialize Tx // start and initialize Tx
void TxService::start() { void TxService::start() {
// DEBUG_LOG(F("TxStart()"));
// grab the bus ID // grab the bus ID
Settings settings; Settings settings;
ems_bus_id(settings.ems_bus_id()); ems_bus_id(settings.ems_bus_id());
@@ -336,6 +349,19 @@ void TxService::start() {
read_request(EMSdevice::EMS_TYPE_UBADevices, EMSdevice::EMS_DEVICE_ID_BOILER); read_request(EMSdevice::EMS_TYPE_UBADevices, EMSdevice::EMS_DEVICE_ID_BOILER);
} }
// Tx loop
// here we check if the Tx is not full and report an error
void TxService::loop() {
#ifndef EMSESP_STANDALONE
if ((uuid::get_uptime() - last_tx_check_) > TX_LOOP_WAIT) {
last_tx_check_ = uuid::get_uptime();
if ((tx_telegrams_.size() >= MAX_TX_TELEGRAMS - 1) && (EMSbus::bus_connected())) {
LOG_ERROR(F("Tx buffer full. Looks like Tx is not working?"));
}
}
#endif
}
// sends a 1 byte poll which is our own device ID // sends a 1 byte poll which is our own device ID
void TxService::send_poll() { void TxService::send_poll() {
EMSuart::send_poll(ems_bus_id() ^ ems_mask()); EMSuart::send_poll(ems_bus_id() ^ ems_mask());
@@ -352,9 +378,8 @@ void TxService::send() {
// if there's nothing in the queue to send // if there's nothing in the queue to send
// optionally, send back a poll and quit // optionally, send back a poll and quit
// for now I've disabled the poll
if (tx_telegrams_.empty()) { if (tx_telegrams_.empty()) {
// send_poll(); // send_poll(); // TODO commented out poll for now. should add back when stable.
return; return;
} }
@@ -431,11 +456,12 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) {
length++; // add one since we want to now include the CRC length++; // add one since we want to now include the CRC
DEBUG_LOG(F("Sending %s Tx [#%d], telegram: %s"), LOG_DEBUG(F("Sending %s Tx [#%d], telegram: %s"),
(telegram->operation == Telegram::Operation::TX_WRITE) ? F("write") : F("read"), (telegram->operation == Telegram::Operation::TX_WRITE) ? F("write") : F("read"),
tx_telegram.id_, tx_telegram.id_,
telegram->to_string(telegram_raw, length).c_str()); telegram->to_string(telegram_raw, length).c_str());
// if we're watching an ID, then always show
if ((logger_.enabled(Level::TRACE)) if ((logger_.enabled(Level::TRACE))
&& ((telegram->src == EMSESP::trace_watch_id()) || (telegram->dest == EMSESP::trace_watch_id()) || (telegram->type_id == EMSESP::trace_watch_id()))) { && ((telegram->src == EMSESP::trace_watch_id()) || (telegram->dest == EMSESP::trace_watch_id()) || (telegram->type_id == EMSESP::trace_watch_id()))) {
logger_.trace(F("Sending %s Tx [#%d], telegram: %s"), logger_.trace(F("Sending %s Tx [#%d], telegram: %s"),
@@ -446,9 +472,9 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) {
// send the telegram to the UART Tx // send the telegram to the UART Tx
EMSUART_STATUS status = EMSuart::transmit(telegram_raw, length); EMSUART_STATUS status = EMSuart::transmit(telegram_raw, length);
LOG_TRACE(F("Tx: %s"), Helpers::data_to_hex(telegram_raw, length).c_str());
if (status != EMS_TX_STATUS_OK) { if (status != EMS_TX_STATUS_OK) {
logger_.err(F("Failed to transmit Tx via UART. Error: %s"), status == EMS_TX_WTD_TIMEOUT ? F("Timeout") : F("BRK")); LOG_ERROR(F("Failed to transmit Tx via UART. Error: %s"), status == EMS_TX_WTD_TIMEOUT ? F("Timeout") : F("BRK"));
} }
tx_waiting(true); // tx now in a wait state tx_waiting(true); // tx now in a wait state
@@ -465,12 +491,13 @@ void TxService::send_telegram(const uint8_t * data, const uint8_t length) {
} }
telegram_raw[length] = calculate_crc(telegram_raw, length); // apppend CRC telegram_raw[length] = calculate_crc(telegram_raw, length); // apppend CRC
DEBUG_LOG(F("Sending Raw telegram: %s (length=%d)"), Helpers::data_to_hex(telegram_raw, length).c_str(), length); LOG_DEBUG(F("Sending Raw telegram: %s (length=%d)"), Helpers::data_to_hex(telegram_raw, length).c_str(), length);
// send the telegram to the UART Tx // send the telegram to the UART Tx
EMSUART_STATUS status = EMSuart::transmit(telegram_raw, length); EMSUART_STATUS status = EMSuart::transmit(telegram_raw, length);
LOG_TRACE(F("Tx: %s"), Helpers::data_to_hex(telegram_raw, length).c_str());
if (status != EMS_TX_STATUS_OK) { if (status != EMS_TX_STATUS_OK) {
logger_.err(F("Failed to transmit Tx via UART. Error: %s"), status == EMS_TX_WTD_TIMEOUT ? F("Timeout") : F("BRK")); LOG_ERROR(F("Failed to transmit Tx via UART. Error: %s"), status == EMS_TX_WTD_TIMEOUT ? F("Timeout") : F("BRK"));
} }
} }
@@ -478,10 +505,10 @@ void TxService::send_telegram(const uint8_t * data, const uint8_t length) {
// given some details like the destination, type, offset and message block // given some details like the destination, type, offset and message block
void TxService::add(const uint8_t operation, const uint8_t dest, const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length) { void TxService::add(const uint8_t operation, const uint8_t dest, const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length) {
auto telegram = std::make_shared<Telegram>(operation, ems_bus_id(), dest, type_id, offset, message_data, message_length); auto telegram = std::make_shared<Telegram>(operation, ems_bus_id(), dest, type_id, offset, message_data, message_length);
DEBUG_LOG(F("New Tx [#%d] telegram added, length %d"), tx_telegram_id_, message_length); LOG_DEBUG(F("New Tx [#%d] telegram, length %d"), tx_telegram_id_, message_length);
// 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 (tx_telegrams_.size() >= maximum_tx_telegrams_) { if (tx_telegrams_.size() >= MAX_TX_TELEGRAMS) {
tx_telegrams_.pop_front(); tx_telegrams_.pop_front();
} }
@@ -493,7 +520,7 @@ void TxService::add(const uint8_t operation, const uint8_t dest, const uint16_t
void TxService::add(uint8_t * data, const uint8_t length) { void TxService::add(uint8_t * data, const uint8_t length) {
uint8_t message_length = length - 4; uint8_t message_length = length - 4;
if (!message_length) { if (!message_length) {
logger_.err(F("Bad Tx telegram, too short (message length is %d)"), message_length); LOG_ERROR(F("Bad Tx telegram, too short (message length is %d)"), message_length);
return; return;
} }
@@ -507,17 +534,17 @@ void TxService::add(uint8_t * data, const uint8_t length) {
auto telegram = std::make_shared<Telegram>(Telegram::Operation::TX_RAW, src, dest, type_id, offset, message_data, message_length); auto telegram = std::make_shared<Telegram>(Telegram::Operation::TX_RAW, src, dest, type_id, offset, message_data, message_length);
// 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 (tx_telegrams_.size() >= maximum_tx_telegrams_) { if (tx_telegrams_.size() >= MAX_TX_TELEGRAMS) {
tx_telegrams_.pop_front(); tx_telegrams_.pop_front();
} }
DEBUG_LOG(F("New Tx [#%d] telegram added, length %d"), tx_telegram_id_, message_length); LOG_DEBUG(F("New Tx [#%d] telegram, length %d"), tx_telegram_id_, message_length);
tx_telegrams_.emplace_back(tx_telegram_id_++, std::move(telegram)); tx_telegrams_.emplace_back(tx_telegram_id_++, std::move(telegram));
} }
// send a Tx telegram to request data from an EMS device // send a Tx telegram to request data from an EMS device
void TxService::read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset) { void TxService::read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset) {
DEBUG_LOG(F("Tx read request to device 0x%02X for type ID 0x%02X"), dest, type_id); LOG_DEBUG(F("Tx read request to device 0x%02X for type ID 0x%02X"), dest, type_id);
uint8_t message_data[1] = {EMS_MAX_TELEGRAM_LENGTH}; // request all data, 32 bytes uint8_t message_data[1] = {EMS_MAX_TELEGRAM_LENGTH}; // request all data, 32 bytes
add(Telegram::Operation::TX_READ, dest, type_id, offset, message_data, 1); add(Telegram::Operation::TX_READ, dest, type_id, offset, message_data, 1);
@@ -582,7 +609,7 @@ uint8_t TxService::retry_tx() {
// and incoming Rx dest must be us (our ems_bus_id) // and incoming Rx dest must be us (our ems_bus_id)
// for both src and dest we strip the MSB 8th bit // for both src and dest we strip the MSB 8th bit
bool TxService::is_last_tx(const uint8_t src, const uint8_t dest) const { bool TxService::is_last_tx(const uint8_t src, const uint8_t dest) const {
// DEBUG_LOG(F("Comparing %02X=%02X , %02X,%02X"), (telegram_last_[1] & 0x7F), (src & 0x7F), (dest & 0x7F), ems_bus_id()); // LOG_DEBUG(F("Comparing %02X=%02X , %02X,%02X"), (telegram_last_[1] & 0x7F), (src & 0x7F), (dest & 0x7F), ems_bus_id());
return (((telegram_last_[1] & 0x7F) == (src & 0x7F)) && ((dest & 0x7F) == ems_bus_id())); return (((telegram_last_[1] & 0x7F) == (src & 0x7F)) && ((dest & 0x7F) == ems_bus_id()));
} }
@@ -590,7 +617,7 @@ bool TxService::is_last_tx(const uint8_t src, const uint8_t dest) const {
void TxService::post_send_query() { void TxService::post_send_query() {
if (telegram_last_post_send_query_) { if (telegram_last_post_send_query_) {
uint8_t dest = (telegram_last_[1] & 0x7F); uint8_t dest = (telegram_last_[1] & 0x7F);
DEBUG_LOG(F("Sending post validate read, type ID 0x%02X to dest 0x%02X"), telegram_last_post_send_query_, dest); LOG_DEBUG(F("Sending post validate read, type ID 0x%02X to dest 0x%02X"), telegram_last_post_send_query_, dest);
read_request(telegram_last_post_send_query_, dest, 0); // no offset read_request(telegram_last_post_send_query_, dest, 0); // no offset
} }

View File

@@ -96,7 +96,7 @@ class EMSbus {
static bool bus_connected() { static bool bus_connected() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
if ((millis() - last_bus_activity_) > EMS_BUS_TIMEOUT) { if ((uuid::get_uptime() - last_bus_activity_) > EMS_BUS_TIMEOUT) {
bus_connected_ = false; bus_connected_ = false;
} }
return bus_connected_; return bus_connected_;
@@ -202,8 +202,6 @@ class RxService : public EMSbus {
static constexpr uint32_t RX_LOOP_WAIT = 800; // delay in processing Rx queue static constexpr uint32_t RX_LOOP_WAIT = 800; // delay in processing Rx queue
uint32_t last_rx_check_ = 0; uint32_t last_rx_check_ = 0;
size_t maximum_rx_telegrams_ = MAX_RX_TELEGRAMS;
// std::atomic<bool> rx_telegrams_overflow_{false}; // std::atomic<bool> rx_telegrams_overflow_{false};
uint8_t rx_telegram_id_ = 0; // queue counter uint8_t rx_telegram_id_ = 0; // queue counter
@@ -215,7 +213,7 @@ class RxService : public EMSbus {
class TxService : public EMSbus { class TxService : public EMSbus {
public: public:
static constexpr size_t MAX_TX_TELEGRAMS = 20; static constexpr size_t MAX_TX_TELEGRAMS = 50;
static constexpr uint8_t TX_WRITE_FAIL = 4; static constexpr uint8_t TX_WRITE_FAIL = 4;
static constexpr uint8_t TX_WRITE_SUCCESS = 1; static constexpr uint8_t TX_WRITE_SUCCESS = 1;
@@ -224,6 +222,7 @@ class TxService : public EMSbus {
~TxService() = default; ~TxService() = default;
void start(); void start();
void loop();
void send(); void send();
void add(const uint8_t operation, const uint8_t dest, const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length); void add(const uint8_t operation, const uint8_t dest, const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length);
@@ -292,9 +291,11 @@ class TxService : public EMSbus {
private: private:
static constexpr uint8_t MAXIMUM_TX_RETRIES = 3; static constexpr uint8_t MAXIMUM_TX_RETRIES = 3;
size_t maximum_tx_telegrams_ = MAX_TX_TELEGRAMS;
uint8_t tx_telegram_id_ = 0; // queue counter uint8_t tx_telegram_id_ = 0; // queue counter
static constexpr uint32_t TX_LOOP_WAIT = 10000; // when to check if Tx is up and running (10 sec)
uint32_t last_tx_check_ = 0;
std::deque<QueuedTxTelegram> tx_telegrams_; std::deque<QueuedTxTelegram> tx_telegrams_;
uint8_t telegram_last_[EMS_MAX_TELEGRAM_LENGTH]; // copy of last telegram uint8_t telegram_last_[EMS_MAX_TELEGRAM_LENGTH]; // copy of last telegram

View File

@@ -7,8 +7,8 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
emsdevices.push_back(EMSFactory::add(EMSdevice::DeviceType::BOILER, EMSdevice::EMS_DEVICE_ID_BOILER, 0, "", "My Boiler", 0, 0)); emsdevices.push_back(EMSFactory::add(EMSdevice::DeviceType::BOILER, EMSdevice::EMS_DEVICE_ID_BOILER, 0, "", "My Boiler", 0, 0));
// A fake response - UBADevices(0x07) // A fake response - UBADevices(0x07)
uint8_t t0[] = {0x08, 0x00, 0x07, 0x00, 0x0B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47}; uint8_t t[] = {0x08, 0x00, 0x07, 0x00, 0x0B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47};
rxservice_.add(t0, sizeof(t0)); rxservice_.add(t, sizeof(t));
return; return;
} }
@@ -28,8 +28,8 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
rxservice_.loop(); rxservice_.loop();
// simulate getting version information back from an unknown device // simulate getting version information back from an unknown device
uint8_t u1[] = {0x09, 0x0B, 0x02, 0x00, 0x59, 0x01, 0x02, 0x56}; uint8_t t[] = {0x09, 0x0B, 0x02, 0x00, 0x59, 0x01, 0x02, 0x56};
rxservice_.add(u1, sizeof(u1)); rxservice_.add(t, sizeof(t));
rxservice_.loop(); rxservice_.loop();
return; return;
@@ -63,9 +63,9 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
// SM100Monitor - type 0x0362 EMS+ - for SM100 and SM200 // SM100Monitor - type 0x0362 EMS+ - for SM100 and SM200
// B0 0B FF 00 02 62 00 44 02 7A 80 00 80 00 80 00 80 00 80 00 80 00 00 7C 80 00 80 00 80 00 80 // B0 0B FF 00 02 62 00 44 02 7A 80 00 80 00 80 00 80 00 80 00 80 00 00 7C 80 00 80 00 80 00 80
uint8_t s1[] = {0xB0, 0x0B, 0xFF, 00, 0x02, 0x62, 00, 0x44, 0x02, 0x7A, 0x80, 00, 0x80, 0x00, 0x80, 00, uint8_t t[] = {0xB0, 0x0B, 0xFF, 00, 0x02, 0x62, 00, 0x44, 0x02, 0x7A, 0x80, 00, 0x80, 0x00, 0x80, 00,
0x80, 00, 0x80, 00, 0x80, 00, 00, 0x7C, 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 0x89}; 0x80, 00, 0x80, 00, 0x80, 00, 00, 0x7C, 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 0x89};
rxservice_.add(s1, sizeof(s1)); rxservice_.add(t, sizeof(t));
rxservice_.loop(); rxservice_.loop();
shell.loop_all(); shell.loop_all();
@@ -84,9 +84,9 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
// RCPLUSStatusMessage_HC1(0x01A5) // RCPLUSStatusMessage_HC1(0x01A5)
// 98 00 FF 00 01 A5 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03 // 98 00 FF 00 01 A5 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03
uint8_t c1[] = {0x98, 0x00, 0xFF, 0x00, 0x01, 0xA5, 0x00, 0xCF, 0x21, 0x2E, 0x00, 0x00, 0x2E, 0x24, 0x03, uint8_t t[] = {0x98, 0x00, 0xFF, 0x00, 0x01, 0xA5, 0x00, 0xCF, 0x21, 0x2E, 0x00, 0x00, 0x2E, 0x24, 0x03,
0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03, 0x13}; 0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03, 0x13};
rxservice_.add(c1, sizeof(c1)); rxservice_.add(t, sizeof(t));
rxservice_.loop(); rxservice_.loop();
shell.loop_all(); shell.loop_all();
@@ -231,7 +231,7 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
// simulate sending a read request // simulate sending a read request
// uint8_t t16[] = {0x44, 0x45, 0x46, 0x47}; // Me -> Thermostat, (0x91), telegram: 0B 17 91 05 44 45 46 47 (#data=4) // uint8_t t16[] = {0x44, 0x45, 0x46, 0x47}; // Me -> Thermostat, (0x91), telegram: 0B 17 91 05 44 45 46 47 (#data=4)
//txservice_.add(Telegram::Operation::TX_RAW, 0x17, 0x91, 0x05, t16, sizeof(t16)); // txservice_.add(Telegram::Operation::TX_RAW, 0x17, 0x91, 0x05, t16, sizeof(t16));
send_read_request(0x91, 0x17); send_read_request(0x91, 0x17);
// txservice_.show_tx_queue(); // txservice_.show_tx_queue();
@@ -240,19 +240,19 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
incoming_telegram(poll, 1); incoming_telegram(poll, 1);
// incoming Rx // incoming Rx
uint8_t t17[] = {0x17, 0x08, 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3A}; uint8_t t1[] = {0x17, 0x08, 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3A};
incoming_telegram(t17, sizeof(t17)); incoming_telegram(t1, sizeof(t1));
rxservice_.loop(); rxservice_.loop();
// Simulate adding a Poll - should send retry // Simulate adding a Poll - should send retry
incoming_telegram(poll, 1); incoming_telegram(poll, 1);
show_emsbus(shell); show_emsbus(shell);
uint8_t t18[] = {0x21, 0x22}; uint8_t t2[] = {0x21, 0x22};
send_write_request(0x91, 0x17, 0x00, t18, sizeof(t18), 0); send_write_request(0x91, 0x17, 0x00, t2, sizeof(t2), 0);
show_emsbus(shell); show_emsbus(shell);
incoming_telegram(t17, sizeof(t17)); incoming_telegram(t1, sizeof(t1));
txservice_.flush_tx_queue(); txservice_.flush_tx_queue();
@@ -324,9 +324,17 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
if (command == "rx2") { if (command == "rx2") {
// incoming Rx // incoming Rx
uint8_t t71[] = {0x1B, 0x5B, 0xFD, 0x2D, 0x9E, 0x3A, 0xB6, 0xE5, 0x02, 0x20, 0x33, 0x30, 0x32, 0x3A, 0x20, 0x5B, 0x73, uint8_t t[] = {0x1B, 0x5B, 0xFD, 0x2D, 0x9E, 0x3A, 0xB6, 0xE5, 0x02, 0x20, 0x33, 0x30, 0x32, 0x3A, 0x20, 0x5B, 0x73,
0xFF, 0xFF, 0xCB, 0xDF, 0xB7, 0xA7, 0xB5, 0x67, 0x77, 0x77, 0xE4, 0xFF, 0xFD, 0x77, 0xFF, 0xD1}; 0xFF, 0xFF, 0xCB, 0xDF, 0xB7, 0xA7, 0xB5, 0x67, 0x77, 0x77, 0xE4, 0xFF, 0xFD, 0x77, 0xFF, 0xD1};
incoming_telegram(t71, sizeof(t71)); incoming_telegram(t, sizeof(t));
return;
}
// https://github.com/proddy/EMS-ESP/issues/380#issuecomment-633663007
if (command == "rx3") {
// incoming Rx
uint8_t t[] = {0x21, 0x0B, 0xFF, 0x00, 0xDA};
incoming_telegram(t, sizeof(t));
return; return;
} }
@@ -341,8 +349,8 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
// testing the UART tx command, without a queue // testing the UART tx command, without a queue
if (command == "tx2") { if (command == "tx2") {
uint8_t tx[] = {0x0B, 0x88, 0x18, 0x00, 0x20, 0xD4}; // including CRC uint8_t t[] = {0x0B, 0x88, 0x18, 0x00, 0x20, 0xD4}; // including CRC
EMSuart::transmit(tx, sizeof(tx)); EMSuart::transmit(t, sizeof(t));
return; return;
} }

View File

@@ -130,10 +130,10 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i
if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT) && ((device_id == 0x10) || (device_id == 0x17))) if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT) && ((device_id == 0x10) || (device_id == 0x17)))
|| (master_thermostat == device_id)) { || (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); LOG_DEBUG(F("Registering new thermostat with device ID 0x%02X (as the master)"), device_id);
init_mqtt(); init_mqtt();
} else { } else {
DEBUG_LOG(F("Registering new thermostat with device ID 0x%02X"), device_id); LOG_DEBUG(F("Registering new thermostat with device ID 0x%02X"), device_id);
} }
} }
@@ -145,29 +145,32 @@ void Thermostat::init_mqtt() {
// for each of the heating circuits // for each of the heating circuits
if (mqtt_format_ == Settings::MQTT_format::HA) { if (mqtt_format_ == Settings::MQTT_format::HA) {
for (uint8_t hc = 0; hc < monitor_typeids.size(); hc++) { 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; 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" std::string hc_text(10, '\0');
doc["unique_id"] = payload; // "unique_id": "thermostat_hc1" snprintf_P(&hc_text[0], hc_text.capacity() + 1, PSTR("hc%d"), hc + 1);
doc["name"] = hc_text;
snprintf_P(&payload[0], payload.capacity() + 1, PSTR("homeassistant/climate/hc%d/thermostat"), hc + 1); doc["uniq_id"] = hc_text;
doc["~"] = payload; // "homeassistant/climate/hc1/thermostat"
doc["~"] = "homeassistant/climate/ems-esp";
doc["mode_cmd_t"] = "~/cmd_mode"; doc["mode_cmd_t"] = "~/cmd_mode";
doc["mode_stat_t"] = "~/state"; doc["mode_stat_t"] = "~/state";
doc["mode_stat_tpl"] = "{{value_json.mode}}";
doc["temp_cmd_t"] = "~/cmd_temp"; doc["temp_cmd_t"] = "~/cmd_temp";
doc["temp_stat_t"] = "~/state"; doc["temp_stat_t"] = "~/state";
doc["temp_stat_tpl"] = "{{value_json.seltemp}}";
doc["curr_temp_t"] = "~/state"; doc["curr_temp_t"] = "~/state";
doc["curr_temp_tpl"] = "{{value_json.currtemp}}";
std::string mode_str(30, '\0');
snprintf_P(&mode_str[0], 30, PSTR("{{value_json.hc%d.mode}}"), hc + 1);
doc["mode_stat_tpl"] = mode_str;
std::string seltemp_str(30, '\0');
snprintf_P(&seltemp_str[0], 30, PSTR("{{value_json.hc%d.seltemp}}"), hc + 1);
doc["temp_stat_tpl"] = seltemp_str;
std::string currtemp_str(30, '\0');
snprintf_P(&currtemp_str[0], 30, PSTR("{{value_json.hc%d.currtemp}}"), hc + 1);
doc["curr_temp_tpl"] = currtemp_str;
doc["min_temp"] = "5"; doc["min_temp"] = "5";
doc["max_temp"] = "40"; doc["max_temp"] = "40";
doc["temp_step"] = "0.5"; doc["temp_step"] = "0.5";
@@ -177,7 +180,10 @@ void Thermostat::init_mqtt() {
modes.add("heat"); modes.add("heat");
modes.add("auto"); modes.add("auto");
Mqtt::publish(topic.c_str(), doc, true); // publish the config payload with retain flag 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 + 1);
// Mqtt::publish(topic); // empty payload, this remove any previous config sent to HA
Mqtt::publish(topic, doc, true); // publish the config payload with retain flag
// subscribe to the temp and mode commands // subscribe to the temp and mode commands
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/hc%d/thermostat/cmd_temp"), hc + 1); snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/hc%d/thermostat/cmd_temp"), hc + 1);
@@ -212,7 +218,7 @@ void Thermostat::thermostat_cmd(const char * message) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc; StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message); DeserializationError error = deserializeJson(doc, message);
if (error) { if (error) {
DEBUG_LOG(F("MQTT error: payload %s, error %s"), message, error.c_str()); LOG_DEBUG(F("MQTT error: payload %s, error %s"), message, error.c_str());
return; return;
} }
@@ -342,7 +348,7 @@ void Thermostat::publish_values() {
return; return;
} }
DEBUG_LOG(F("Performing a thermostat publish (device ID 0x%02X)"), device_id()); LOG_DEBUG(F("Performing a thermostat publish (device ID 0x%02X)"), device_id());
uint8_t flags = (this->flags() & 0x0F); // specific thermostat characteristics, stripping the option bits uint8_t flags = (this->flags() & 0x0F); // specific thermostat characteristics, stripping the option bits
bool has_data = false; bool has_data = false;
@@ -352,27 +358,28 @@ void Thermostat::publish_values() {
JsonObject dataThermostat; JsonObject dataThermostat;
// optional, add external temp. I don't think anyone actually is interested in this // optional, add external temp. I don't think anyone actually is interested in this
if ((flags == EMS_DEVICE_FLAG_RC35) && (mqtt_format_ == Settings::MQTT_format::SINGLE)) { if ((flags == EMS_DEVICE_FLAG_RC35) && ((mqtt_format_ == Settings::MQTT_format::SINGLE) || (mqtt_format_ == Settings::MQTT_format::MY))) {
if (dampedoutdoortemp != EMS_VALUE_INT_NOTSET) { if (dampedoutdoortemp != EMS_VALUE_INT_NOTSET) {
doc["dampedtemp"] = dampedoutdoortemp; rootThermostat["dampedtemp"] = dampedoutdoortemp;
} }
if (tempsensor1 != EMS_VALUE_USHORT_NOTSET) { if (tempsensor1 != EMS_VALUE_USHORT_NOTSET) {
doc["tempsensor1"] = (float)tempsensor1 / 10; rootThermostat["tempsens1"] = (float)tempsensor1 / 10;
} }
if (tempsensor2 != EMS_VALUE_USHORT_NOTSET) { if (tempsensor2 != EMS_VALUE_USHORT_NOTSET) {
doc["tempsensor1"] = (float)tempsensor2 / 10; rootThermostat["tempsens2"] = (float)tempsensor2 / 10;
} }
} }
// go through all the heating circuits // go through all the heating circuits
for (const auto & hc : heating_circuits_) { for (const auto & hc : heating_circuits_) {
if ((hc->setpoint_roomTemp == EMS_VALUE_SHORT_NOTSET) || (hc->curr_roomTemp == EMS_VALUE_SHORT_NOTSET)) { // if ((hc->setpoint_roomTemp == EMS_VALUE_SHORT_NOTSET) || (hc->curr_roomTemp == EMS_VALUE_SHORT_NOTSET)) {
break; // skip this HC as we don't have the temperature values yet if (hc->heatingtype == 0) {
break; // skip this HC
} }
has_data = true; has_data = true;
// if the MQTT format is 'nested' then create the parent object hc<n> // if the MQTT format is 'nested' or 'ha' then create the parent object hc<n>
if (mqtt_format_ == Settings::MQTT_format::NESTED) { if (mqtt_format_ != Settings::MQTT_format::SINGLE) {
// 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);
@@ -400,7 +407,7 @@ void Thermostat::publish_values() {
if (hc->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) { if (hc->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) {
dataThermostat["seltemp"] = Helpers::round2((float)hc->setpoint_roomTemp / setpoint_temp_divider); dataThermostat["seltemp"] = Helpers::round2((float)hc->setpoint_roomTemp / setpoint_temp_divider);
} }
if (hc->curr_roomTemp != EMS_VALUE_SHORT_NOTSET) { if (hc->curr_roomTemp != EMS_VALUE_SHORT_NOTSET && hc->curr_roomTemp != EMS_VALUE_USHORT_NOTSET) {
dataThermostat["currtemp"] = Helpers::round2((float)hc->curr_roomTemp / curr_temp_divider); dataThermostat["currtemp"] = Helpers::round2((float)hc->curr_roomTemp / curr_temp_divider);
} }
@@ -448,10 +455,10 @@ void Thermostat::publish_values() {
// special handling of mode type, for the RC35 replace with summer/holiday // special handling of mode type, for the RC35 replace with summer/holiday
// https://github.com/proddy/EMS-ESP/issues/373#issuecomment-619810209 // https://github.com/proddy/EMS-ESP/issues/373#issuecomment-619810209
if ((flags & 0x0F) == EMS_DEVICE_FLAG_RC35) { if ((flags & 0x0F) == EMS_DEVICE_FLAG_RC35) {
if (hc->holiday_mode != EMS_VALUE_UINT_NOTSET) { if (hc->summer_mode) {
dataThermostat["modetype"] = F("holiday");
} else if (hc->summer_mode != EMS_VALUE_UINT_NOTSET) {
dataThermostat["modetype"] = F("summer"); dataThermostat["modetype"] = F("summer");
} else if (hc->holiday_mode) {
dataThermostat["modetype"] = F("holiday");
} else if (hc->mode_type != EMS_VALUE_UINT_NOTSET) { } 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));
} }
@@ -459,9 +466,7 @@ void Thermostat::publish_values() {
dataThermostat["modetype"] = mode_tostring(hc->get_mode_type(flags)); dataThermostat["modetype"] = mode_tostring(hc->get_mode_type(flags));
} }
// if format is single, send immediately and quit
// if format is single, send immediately
// if its HA send it to the special topic
if (mqtt_format_ == Settings::MQTT_format::SINGLE) { 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
@@ -469,16 +474,19 @@ void Thermostat::publish_values() {
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 (!has_data) {
return; // nothing to send
}
// if we're using nested json, send all in one go // if we're using nested json, send all in one go
if ((mqtt_format_ == Settings::MQTT_format::NESTED) && has_data) { if (mqtt_format_ == Settings::MQTT_format::NESTED) {
Mqtt::publish("thermostat_data", doc);
} else if (mqtt_format_ == Settings::MQTT_format::HA) {
Mqtt::publish("homeassistant/climate/ems-esp/state", doc);
} else if (mqtt_format_ == Settings::MQTT_format::MY) {
Mqtt::publish("thermostat_data", doc); Mqtt::publish("thermostat_data", doc);
} }
} }
@@ -635,9 +643,9 @@ std::string Thermostat::mode_tostring(uint8_t mode) const {
case HeatingCircuit::Mode::HEAT: case HeatingCircuit::Mode::HEAT:
return read_flash_string(F("heat")); return read_flash_string(F("heat"));
break; break;
case HeatingCircuit::Mode::HOLIDAY: // case HeatingCircuit::Mode::HOLIDAY:
return read_flash_string(F("holiday")); // return read_flash_string(F("holiday"));
break; // break;
case HeatingCircuit::Mode::NOFROST: case HeatingCircuit::Mode::NOFROST:
return read_flash_string(F("nofrost")); return read_flash_string(F("nofrost"));
break; break;
@@ -934,11 +942,7 @@ void Thermostat::process_RC35Monitor(std::shared_ptr<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> hc = heating_circuit(telegram); std::shared_ptr<Thermostat::HeatingCircuit> hc = heating_circuit(telegram);
// ignore if the value is 0 (see https://github.com/proddy/EMS-ESP/commit/ccc30738c00f12ae6c89177113bd15af9826b836)
if (telegram->message_data[2] != 0x00) {
telegram->read_value8(hc->setpoint_roomTemp, 2); // is * 2, force to single byte telegram->read_value8(hc->setpoint_roomTemp, 2); // is * 2, force to single byte
}
telegram->read_value(hc->curr_roomTemp, 3); // is * 10 - or 0x7D00 if thermostat is mounted on boiler telegram->read_value(hc->curr_roomTemp, 3); // is * 10 - or 0x7D00 if thermostat is mounted on boiler
telegram->read_value(hc->mode_type, 1, 1); telegram->read_value(hc->mode_type, 1, 1);
telegram->read_value(hc->summer_mode, 1, 0); telegram->read_value(hc->summer_mode, 1, 0);
@@ -998,19 +1002,19 @@ void Thermostat::process_RCTime(std::shared_ptr<const Telegram> telegram) {
// 0xA5 - Set the display settings // 0xA5 - Set the display settings
void Thermostat::set_settings_display(const uint8_t ds) { void Thermostat::set_settings_display(const uint8_t ds) {
logger_.info(F("Setting display to %d"), ds); LOG_INFO(F("Setting display to %d"), ds);
write_command(EMS_TYPE_IBASettings, 0, ds); write_command(EMS_TYPE_IBASettings, 0, ds);
} }
// 0xA5 - Set the building settings // 0xA5 - Set the building settings
void Thermostat::set_settings_building(const uint8_t bg) { void Thermostat::set_settings_building(const uint8_t bg) {
logger_.info(F("Setting building to %d"), bg); LOG_INFO(F("Setting building to %d"), bg);
write_command(EMS_TYPE_IBASettings, 6, bg); write_command(EMS_TYPE_IBASettings, 6, bg);
} }
// 0xA5 Set the language settings // 0xA5 Set the language settings
void Thermostat::set_settings_language(const uint8_t lg) { void Thermostat::set_settings_language(const uint8_t lg) {
logger_.info(F("Setting building to %d"), lg); LOG_INFO(F("Setting building to %d"), lg);
write_command(EMS_TYPE_IBASettings, 1, lg); write_command(EMS_TYPE_IBASettings, 1, lg);
} }
@@ -1037,21 +1041,21 @@ void Thermostat::set_mode(const std::string & mode, const uint8_t hc_num) {
} else if (mode_tostring(HeatingCircuit::Mode::COMFORT) == mode) { } else if (mode_tostring(HeatingCircuit::Mode::COMFORT) == mode) {
set_mode(HeatingCircuit::Mode::COMFORT, hc_num); set_mode(HeatingCircuit::Mode::COMFORT, hc_num);
} else { } else {
logger_.warning(F("Invalid mode %s. Cannot set"), mode.c_str()); LOG_WARNING(F("Invalid mode %s. Cannot set"), mode.c_str());
} }
} }
// Set the thermostat working mode // Set the thermostat working mode
void Thermostat::set_mode(const uint8_t mode, const uint8_t hc_num) { void Thermostat::set_mode(const uint8_t mode, const uint8_t hc_num) {
if (can_write()) { if (can_write()) {
logger_.warning(F("Write not supported for this model Thermostat")); LOG_WARNING(F("Write not supported for this model Thermostat"));
return; return;
} }
// get hc based on number // get hc based on number
std::shared_ptr<Thermostat::HeatingCircuit> hc = heating_circuit(hc_num); std::shared_ptr<Thermostat::HeatingCircuit> hc = heating_circuit(hc_num);
if (hc == nullptr) { if (hc == nullptr) {
logger_.warning(F("set mode: Heating Circuit %d not found or activated"), hc_num); LOG_WARNING(F("set mode: Heating Circuit %d not found or activated"), hc_num);
return; return;
} }
@@ -1127,7 +1131,7 @@ void Thermostat::set_mode(const uint8_t mode, const uint8_t hc_num) {
break; break;
} }
logger_.info(F("Setting thermostat mode to %s for heating circuit %d"), mode_tostring(mode).c_str(), hc->hc_num()); LOG_INFO(F("Setting thermostat mode to %s for heating circuit %d"), mode_tostring(mode).c_str(), hc->hc_num());
// add the write command to the Tx queue // add the write command to the Tx queue
// post validate is the corresponding monitor or set type IDs as they can differ per model // post validate is the corresponding monitor or set type IDs as they can differ per model
@@ -1138,14 +1142,14 @@ void Thermostat::set_mode(const uint8_t mode, const uint8_t hc_num) {
// Set the temperature of the thermostat // Set the temperature of the thermostat
void Thermostat::set_temperature(const float temperature, const uint8_t mode, const uint8_t hc_num) { void Thermostat::set_temperature(const float temperature, const uint8_t mode, const uint8_t hc_num) {
if (can_write()) { if (can_write()) {
logger_.warning(F("Write not supported for this model Thermostat")); LOG_WARNING(F("Write not supported for this model Thermostat"));
return; return;
} }
// get hc based on number // get hc based on number
std::shared_ptr<Thermostat::HeatingCircuit> hc = heating_circuit(hc_num); std::shared_ptr<Thermostat::HeatingCircuit> hc = heating_circuit(hc_num);
if (hc == nullptr) { if (hc == nullptr) {
logger_.warning(F("set temperature: Heating Circuit %d not found or activated for device ID 0x%02X"), hc_num, device_id()); LOG_WARNING(F("set temperature: Heating Circuit %d not found or activated for device ID 0x%02X"), hc_num, device_id());
return; return;
} }
@@ -1244,7 +1248,7 @@ void Thermostat::set_temperature(const float temperature, const uint8_t mode, co
// if we know what to send and to where, go and do it // if we know what to send and to where, go and do it
if (offset != -1) { if (offset != -1) {
char s[10]; char s[10];
logger_.info(F("Setting thermostat temperature to %s for heating circuit %d, mode %s"), LOG_INFO(F("Setting thermostat temperature to %s for heating circuit %d, mode %s"),
Helpers::render_value(s, temperature, 2), Helpers::render_value(s, temperature, 2),
hc->hc_num(), hc->hc_num(),
mode_tostring(mode).c_str()); mode_tostring(mode).c_str());

View File

@@ -16,7 +16,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
/* ESP32 UART port by Arwed Richert @ArwedL */ /*
* ESP32 UART port by @ArwedL and improved by @MichaelDvP. See https://github.com/proddy/EMS-ESP/issues/380
*/
#if defined(ESP32) #if defined(ESP32)
@@ -30,13 +32,13 @@ static intr_handle_t uart_handle;
static RingbufHandle_t buf_handle = NULL; static RingbufHandle_t buf_handle = NULL;
static uint8_t rxbuf[UART_FIFO_LEN]; static uint8_t rxbuf[UART_FIFO_LEN];
static uint8_t rxlen; static uint8_t rxlen;
bool drop_first_rx = true;
/* /*
* Task to handle the incoming data. * Task to handle the incoming data
*/ */
void EMSuart::emsuart_recvTask(void * param) { void EMSuart::emsuart_recvTask(void * param) {
while(1) { while (1) {
size_t item_size; size_t item_size;
uint8_t * telegram = (uint8_t *)xRingbufferReceive(buf_handle, &item_size, portMAX_DELAY); uint8_t * telegram = (uint8_t *)xRingbufferReceive(buf_handle, &item_size, portMAX_DELAY);
uint8_t telegramSize = item_size; uint8_t telegramSize = item_size;
@@ -50,17 +52,17 @@ void EMSuart::emsuart_recvTask(void * param) {
/* /*
* UART interrupt, on break read the fifo and put the whole telegram to ringbuffer * UART interrupt, on break read the fifo and put the whole telegram to ringbuffer
*/ */
static void IRAM_ATTR uart_intr_handle(void *arg) static void IRAM_ATTR uart_intr_handle(void * arg) {
{
if (EMS_UART.int_st.brk_det) { if (EMS_UART.int_st.brk_det) {
uint8_t rx_fifo_len = EMS_UART.status.rxfifo_cnt; uint8_t rx_fifo_len = EMS_UART.status.rxfifo_cnt;
for (rxlen = 0; rxlen < rx_fifo_len; rxlen++) { for (rxlen = 0; rxlen < rx_fifo_len; rxlen++) {
rxbuf[rxlen] = EMS_UART.fifo.rw_byte; // read all bytes into buffer rxbuf[rxlen] = EMS_UART.fifo.rw_byte; // read all bytes into buffer
} }
if ((rxlen == 2) || ((rxlen > 4) && (rxlen <= EMS_MAXBUFFERSIZE))) { if (!drop_first_rx && (rxlen == 2) || ((rxlen > 4) && (rxlen <= EMS_MAXBUFFERSIZE))) {
int baseType = 0; int baseType = 0;
xRingbufferSendFromISR(buf_handle, rxbuf, rxlen - 1, &baseType); xRingbufferSendFromISR(buf_handle, rxbuf, rxlen - 1, &baseType);
} }
drop_first_rx = false;
EMS_UART.int_clr.brk_det = 1; // clear flag EMS_UART.int_clr.brk_det = 1; // clear flag
EMS_UART.conf0.txd_brk = 0; // if it was break from sending, clear bit EMS_UART.conf0.txd_brk = 0; // if it was break from sending, clear bit
} }
@@ -76,40 +78,45 @@ void EMSuart::start(uint8_t tx_mode) {
.stop_bits = UART_STOP_BITS_1, .stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
}; };
ESP_ERROR_CHECK(uart_param_config(EMSUART_UART, &uart_config)); ESP_ERROR_CHECK(uart_param_config(EMSUART_UART, &uart_config));
ESP_ERROR_CHECK(uart_set_pin(EMSUART_UART, EMSUART_TXPIN, EMSUART_RXPIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); ESP_ERROR_CHECK(uart_set_pin(EMSUART_UART, EMSUART_TXPIN, EMSUART_RXPIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
//EMS_UART.conf1.rxfifo_full_thrhd = 127; // enough to hold the incomming telegram, should never reached //EMS_UART.conf1.rxfifo_full_thrhd = 127; // enough to hold the incoming telegram, should never reached
//EMS_UART.idle_conf.tx_brk_num = 12; // breaklenght 12 bit //EMS_UART.idle_conf.tx_brk_num = 12; // breaklength 12 bit
EMS_UART.int_ena.val = 0; // diable all intr. EMS_UART.int_ena.val = 0; // disable all intr.
EMS_UART.int_clr.val = 0xFFFFFFFF; // clear all intr. flags EMS_UART.int_clr.val = 0xFFFFFFFF; // clear all intr. flags
EMS_UART.int_ena.brk_det = 1; // activate only break
buf_handle = xRingbufferCreate(128, RINGBUF_TYPE_NOSPLIT); buf_handle = xRingbufferCreate(128, RINGBUF_TYPE_NOSPLIT);
ESP_ERROR_CHECK(uart_isr_register(EMSUART_UART,uart_intr_handle, NULL, ESP_INTR_FLAG_IRAM, &uart_handle)); ESP_ERROR_CHECK(uart_isr_register(EMSUART_UART, uart_intr_handle, NULL, ESP_INTR_FLAG_IRAM, &uart_handle));
xTaskCreate(emsuart_recvTask, "emsuart_recvTask", 2048, NULL, 12, NULL); xTaskCreate(emsuart_recvTask, "emsuart_recvTask", 2048, NULL, 12, NULL);
drop_first_rx = true;
EMS_UART.int_ena.brk_det = 1; // activate only break
} }
/* /*
* Stop, disables interrupt * Stop, disables interrupt
*/ */
void EMSuart::stop(){ void EMSuart::stop() {
EMS_UART.int_ena.val = 0; //diable all intr. EMS_UART.int_ena.val = 0; // disable all intr.
}; };
/* /*
* Restart Interrupt * Restart Interrupt
*/ */
void EMSuart::restart(){ void EMSuart::restart() {
EMS_UART.int_clr.val = 0xFFFFFFFF; // clear all intr. flags if (EMS_UART.int_st.brk_det) {
EMS_UART.int.clr.brk_det = 1; // clear break if happend
drop_first_rx = true; // and drop first frame
}
EMS_UART.int_ena.brk_det = 1; // activate only break EMS_UART.int_ena.brk_det = 1; // activate only break
}; };
/* /*
* Sends a 1-byte poll, ending with a <BRK> * Sends a 1-byte poll, ending with a <BRK>
*/ */
void EMSuart::send_poll(uint8_t data) { void EMSuart::send_poll(uint8_t data) {
EMS_UART.conf0.txd_brk = 0; // just to make sure the bit is cleared EMS_UART.conf0.txd_brk = 0; // just to make sure the bit is cleared
EMS_UART.fifo.rw_byte = data; EMS_UART.fifo.rw_byte = data;
EMS_UART.idle_conf.tx_brk_num = 12; // breaklenght 12 bit EMS_UART.idle_conf.tx_brk_num = 12; // breaklength 12 bit
EMS_UART.conf0.txd_brk = 1; // sending ends in a break EMS_UART.conf0.txd_brk = 1; // sending ends in a break
} }
@@ -128,7 +135,7 @@ EMSUART_STATUS EMSuart::transmit(uint8_t * buf, uint8_t len) {
EMS_UART.fifo.rw_byte = buf[i]; EMS_UART.fifo.rw_byte = buf[i];
} }
//uart_tx_chars(EMSUART_UART, (const char *)buf, len); //uart_tx_chars(EMSUART_UART, (const char *)buf, len);
EMS_UART.idle_conf.tx_brk_num = 12; // breaklenght 12 bit EMS_UART.idle_conf.tx_brk_num = 12; // breaklength 12 bit
EMS_UART.conf0.txd_brk = 1; // sending ends in a break EMS_UART.conf0.txd_brk = 1; // sending ends in a break
} }
return EMS_TX_STATUS_OK; return EMS_TX_STATUS_OK;

View File

@@ -27,15 +27,16 @@
#include "freertos/queue.h" #include "freertos/queue.h"
#include <driver/uart.h> #include <driver/uart.h>
#define EMS_MAXBUFFERSIZE 33 // max size of the buffer. EMS packets are max 32 bytes, plus BRK #define EMS_MAXBUFFERSIZE 33 // max size of the buffer. EMS packets are max 32 bytes, plus extra 2 for BRKs
#define EMSUART_UART UART_NUM_2 // UART 0 --> Changed to 2 for ESP32 // To do: Adapt
#define EMSUART_UART UART_NUM_2 // on the ESP32 we're using UART2
#define EMS_UART UART2 // for intr setting #define EMS_UART UART2 // for intr setting
//#define EMSUART_RXPIN 17 // To do: Adapt seems to be IO17 for ESP32 UART2 RX pin
//#define EMSUART_TXPIN 16 // To do: Adapt seems to be IO16 for ESP32 UART2 TX pin
#define EMSUART_RXPIN 23 // Wemos D1 ESP32 UART2 RX pin for compatibility
#define EMSUART_TXPIN 5 // Wemos D1 ESP32 UART2 TX pin for compatibility
#define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit #define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit
// customize the GPIO pins for RX and TX here
#define EMSUART_RXPIN 23 // 17 is UART2 RX. Use 23 for D7 on a Wemos D1-32 mini for backwards compatabilty
#define EMSUART_TXPIN 5 // 16 is UART2 TX. Use 5 for D8 on a Wemos D1-32 mini for backwards compatabilty
namespace emsesp { namespace emsesp {
typedef enum { typedef enum {

View File

@@ -25,15 +25,15 @@
namespace emsesp { namespace emsesp {
os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue
EMSuart::EMSRxBuf_t * pEMSRxBuf; EMSuart::EMSRxBuf_t * pEMSRxBuf;
EMSuart::EMSRxBuf_t * paEMSRxBuf[EMS_MAXBUFFERS]; EMSuart::EMSRxBuf_t * paEMSRxBuf[EMS_MAXBUFFERS];
uint8_t emsRxBufIdx = 0; uint8_t emsRxBufIdx = 0;
bool drop_first_rx = true;
uint8_t phantomBreak = 0;
uint8_t tx_mode_ = EMS_TXMODE_NEW;
//
// Main interrupt handler // Main interrupt handler
// Important: must not use ICACHE_FLASH_ATTR // Important: must not use ICACHE_FLASH_ATTR
//
void ICACHE_RAM_ATTR EMSuart::emsuart_rx_intr_handler(void * para) { void ICACHE_RAM_ATTR EMSuart::emsuart_rx_intr_handler(void * para) {
static uint8_t length = 0; static uint8_t length = 0;
static uint8_t uart_buffer[128]; static uint8_t uart_buffer[128];
@@ -46,12 +46,13 @@ void ICACHE_RAM_ATTR EMSuart::emsuart_rx_intr_handler(void * para) {
} }
USIE(EMSUART_UART) = 0; // disable all interrupts and clear them USIE(EMSUART_UART) = 0; // disable all interrupts and clear them
USC0(EMSUART_UART) &= ~(1 << UCBRK); // reset <BRK> from sending USC0(EMSUART_UART) &= ~(1 << UCBRK); // reset <BRK> from sending
if (length < EMS_MAXBUFFERSIZE) { // only a valid telegram if (!drop_first_rx && (length < EMS_MAXBUFFERSIZE)) { // only a valid telegram
pEMSRxBuf->length = length; pEMSRxBuf->length = length;
os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, pEMSRxBuf->length); // copy data into transfer buffer, including the BRK 0x00 at the end os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, pEMSRxBuf->length); // copy data into transfer buffer, including the BRK 0x00 at the end
system_os_post(EMSUART_recvTaskPrio, 0, 0); // call emsuart_recvTask() at next opportunity system_os_post(EMSUART_recvTaskPrio, 0, 0); // call emsuart_recvTask() at next opportunity
} }
USIC(EMSUART_UART) = (1 << UIBD); // INT clear the BREAK detect interrupt drop_first_rx = false;
USIC(EMSUART_UART) |= (1 << UIBD); // INT clear the BREAK detect interrupt
USIE(EMSUART_UART) = (1 << UIBD); // enable only rx break USIE(EMSUART_UART) = (1 << UIBD); // enable only rx break
} }
} }
@@ -67,6 +68,11 @@ void ICACHE_FLASH_ATTR EMSuart::emsuart_recvTask(os_event_t * events) {
uint8_t length = pCurrent->length; // number of bytes including the BRK at the end uint8_t length = pCurrent->length; // number of bytes including the BRK at the end
pCurrent->length = 0; pCurrent->length = 0;
if (phantomBreak) {
phantomBreak = 0;
length--; // remove phantom break from Rx buffer
}
// it's a poll or status code, single byte and ok to send on, then quit // it's a poll or status code, single byte and ok to send on, then quit
if (length == 2) { if (length == 2) {
EMSESP::incoming_telegram((uint8_t *)pCurrent->buffer, 1); EMSESP::incoming_telegram((uint8_t *)pCurrent->buffer, 1);
@@ -74,7 +80,7 @@ void ICACHE_FLASH_ATTR EMSuart::emsuart_recvTask(os_event_t * events) {
} }
// also telegrams with no data value // also telegrams with no data value
// then transmit EMS buffer, excluding the BRK, lenght is checked by irq // then transmit EMS buffer, excluding the BRK, length is checked by irq
if (length > 4) { if (length > 4) {
EMSESP::incoming_telegram((uint8_t *)pCurrent->buffer, length - 1); EMSESP::incoming_telegram((uint8_t *)pCurrent->buffer, length - 1);
} }
@@ -84,7 +90,7 @@ void ICACHE_FLASH_ATTR EMSuart::emsuart_recvTask(os_event_t * events) {
* init UART0 driver * init UART0 driver
*/ */
void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) { void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
tx_mode_ = tx_mode;
// allocate and preset EMS Receive buffers // allocate and preset EMS Receive buffers
for (int i = 0; i < EMS_MAXBUFFERS; i++) { for (int i = 0; i < EMS_MAXBUFFERS; i++) {
EMSRxBuf_t * p = (EMSRxBuf_t *)malloc(sizeof(EMSRxBuf_t)); EMSRxBuf_t * p = (EMSRxBuf_t *)malloc(sizeof(EMSRxBuf_t));
@@ -92,9 +98,6 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
} }
pEMSRxBuf = paEMSRxBuf[0]; // reset EMS Rx Buffer pEMSRxBuf = paEMSRxBuf[0]; // reset EMS Rx Buffer
//ETS_UART_INTR_DISABLE();
//ETS_UART_INTR_ATTACH(nullptr, nullptr);
// pin settings // pin settings
PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0TXD_U); PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0TXD_U);
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD); PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD);
@@ -103,24 +106,22 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
// set 9600, 8 bits, no parity check, 1 stop bit // set 9600, 8 bits, no parity check, 1 stop bit
USD(EMSUART_UART) = (UART_CLK_FREQ / EMSUART_BAUD); USD(EMSUART_UART) = (UART_CLK_FREQ / EMSUART_BAUD);
USC0(EMSUART_UART) = EMSUART_CONFIG; // 8N1 USC0(EMSUART_UART) = EMSUART_CONFIG;
// flash fifo buffers, not required since we drop the first telegram
// USC0(EMSUART_UART) |= ((1 << UCRXRST) | (1 << UCTXRST)); // set bits
// USC0(EMSUART_UART) &= ~((1 << UCRXRST) | (1 << UCTXRST)); // clear bits
USC0(EMSUART_UART) |= ((1 << UCRXRST) | (1 << UCTXRST)); // set bits // we dont use fifo-full interrupt anymore, no need to set this
USC0(EMSUART_UART) &= ~((1 << UCRXRST) | (1 << UCTXRST)); // clear bits //USC1(EMSUART_UART) = (0x7F << UCFFT); // rx buffer full
// UCFFT = RX FIFO Full Threshold (7 bit) = want this to be more than 32)
USC1(EMSUART_UART) = (0x7F << UCFFT); // rx buffer full
USIE(EMSUART_UART) = 0; // disable all interrupts USIE(EMSUART_UART) = 0; // disable all interrupts
USIC(EMSUART_UART) = 0xFFFF; // clear all interupts USIC(EMSUART_UART) = 0xFFFF; // clear all interupt flags
// set up interrupt callbacks for Rx
system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen); system_set_os_print(0); // disable esp debug which will go to Tx and mess up the line - see https://github.com/espruino/Espruino/issues/655
// disable esp debug which will go to Tx and mess up the line - see https://github.com/espruino/Espruino/issues/655 system_uart_swap(); // swap Rx and Tx pins to use GPIO13 (D7) and GPIO15 (D8) respectively
system_set_os_print(0); system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen); // set up interrupt callbacks for Rx
// swap Rx and Tx pins to use GPIO13 (D7) and GPIO15 (D8) respectively
system_uart_swap();
ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, nullptr); ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, nullptr);
drop_first_rx = true; // drop first telegram since it is incomplete
USIE(EMSUART_UART) = (1 << UIBD); // enable only rx break interrupt USIE(EMSUART_UART) = (1 << UIBD); // enable only rx break interrupt
//ETS_UART_INTR_ENABLE();
} }
/* /*
@@ -128,20 +129,30 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
* This is called prior to an OTA upload and also before a save to the filesystem to prevent conflicts * This is called prior to an OTA upload and also before a save to the filesystem to prevent conflicts
*/ */
void ICACHE_FLASH_ATTR EMSuart::stop() { void ICACHE_FLASH_ATTR EMSuart::stop() {
USIE(EMSUART_UART) = 0; // disable interrup USIE(EMSUART_UART) = 0; // disable uart interrupt
} }
/* /*
* re-start UART0 driver * re-start UART0 driver
*/ */
void ICACHE_FLASH_ATTR EMSuart::restart() { void ICACHE_FLASH_ATTR EMSuart::restart() {
USIE(EMSUART_UART) = (1 << UIBD); // enable only rx break if (USIS(EMSUART_UART) & ((1 << UIBD))) { // if we had a break
USIC(EMSUART_UART) |= (1 << UIBD); // clear the BREAK detect flag
drop_first_rx = true; // and drop first frame
} // otherwise there is the beginning of a valid telegram in the fifo
USIE(EMSUART_UART) = (1 << UIBD); // enable rx break interrupt
} }
void EMSuart::send_poll(uint8_t data) { void EMSuart::send_poll(uint8_t data) {
USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear <BRK> bit if (tx_mode_ == EMS_TXMODE_NEW) {
USC0(EMSUART_UART) &= ~(1 << UCBRK); // in doubt clear <BRK> bit
USF(EMSUART_UART) = data; USF(EMSUART_UART) = data;
USC0(EMSUART_UART) |= (1 << UCBRK); // send <BRK> at the end USC0(EMSUART_UART) |= (1 << UCBRK); // send <BRK> at the end
} else {
USF(EMSUART_UART) = data;
delayMicroseconds(EMSUART_TX_BRK_WAIT);
tx_brk(); // send <BRK>
}
} }
/* /*
@@ -149,15 +160,166 @@ void EMSuart::send_poll(uint8_t data) {
* buf contains the CRC and len is #bytes including the CRC * buf contains the CRC and len is #bytes including the CRC
*/ */
EMSUART_STATUS ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) { EMSUART_STATUS ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) {
if (len) { if (len == 0) {
return EMS_TX_STATUS_OK; // nothing to send
}
// new code from Michael. See https://github.com/proddy/EMS-ESP/issues/380
if (tx_mode_ == EMS_TXMODE_NEW) {
if ((USS(EMSUART_UART) >> USTXC) & 0xFF) { // buffer not empty
return EMS_TX_WTD_TIMEOUT;
}
USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear <BRK> bit USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear <BRK> bit
for (uint8_t i = 0; i < len ; i++) { for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i]; USF(EMSUART_UART) = buf[i]; // fill fifo buffer
} }
USC0(EMSUART_UART) |= (1 << UCBRK); // send <BRK> at the end USC0(EMSUART_UART) |= (1 << UCBRK); // send <BRK> at the end
}
return EMS_TX_STATUS_OK; return EMS_TX_STATUS_OK;
}
// EMS+ https://github.com/proddy/EMS-ESP/issues/23#
if (tx_mode_ == EMS_TXMODE_EMSPLUS) { // With extra tx delay for EMS+
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
delayMicroseconds(EMSUART_TX_BRK_WAIT);
}
tx_brk(); // send <BRK>
return EMS_TX_STATUS_OK;
}
// Junkers logic by @philrich
if (tx_mode_ == EMS_TXMODE_HT3) {
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
// just to be safe wait for tx fifo empty (still needed?)
while (((USS(EMSUART_UART) >> USTXC) & 0xff))
;
// wait until bits are sent on wire
delayMicroseconds(EMSUART_TX_WAIT_BYTE - EMSUART_TX_LAG + EMSUART_TX_WAIT_GAP); // 1760
}
tx_brk(); // send <BRK>
return EMS_TX_STATUS_OK;
}
/*
* Logic for tx_mode of 0 (EMS_TXMODE_DEFAULT)
* based on code from https://github.com/proddy/EMS-ESP/issues/103 by @susisstrolch
*
* Logic:
* we emit the whole telegram, with Rx interrupt disabled, collecting busmaster response in FIFO.
* after sending the last char we poll the Rx status until either
* - size(Rx FIFO) == size(Tx-Telegram)
* - <BRK> is detected
* At end of receive we re-enable Rx-INT and send a Tx-BRK in loopback mode.
*
* EMS-Bus error handling
* 1. Busmaster stops echoing on Tx w/o permission
* 2. Busmaster cancel telegram by sending a BRK
*
* Case 1. is handled by a watchdog counter which is reset on each
* Tx attempt. The timeout should be 20x EMSUART_BIT_TIME plus
* some smart guess for processing time on targeted EMS device.
* We set Status to EMS_TX_WTD_TIMEOUT and return
*
* Case 2. is handled via a BRK chk during transmission.
* We set Status to EMS_TX_BRK_DETECT and return
*
*/
EMSUART_STATUS result = EMS_TX_STATUS_OK;
// disable rx interrupt
// clear Rx status register, resetting the Rx FIFO and flush it
ETS_UART_INTR_DISABLE();
USC0(EMSUART_UART) |= (1 << UCRXRST);
emsuart_flush_fifos();
// send the bytes along the serial line
for (uint8_t i = 0; i < len; i++) {
uint16_t wdc = EMS_TX_TO_COUNT; // 1760
volatile uint8_t _usrxc = (USS(EMSUART_UART) >> USRXC) & 0xFF;
USF(EMSUART_UART) = buf[i]; // send each Tx byte
// wait for echo from the busmaster
while (((USS(EMSUART_UART) >> USRXC) & 0xFF) == _usrxc) {
delayMicroseconds(EMSUART_BUSY_WAIT); // burn CPU cycles...
if (--wdc == 0) {
ETS_UART_INTR_ENABLE();
return EMS_TX_WTD_TIMEOUT;
}
if (USIR(EMSUART_UART) & (1 << UIBD)) {
USIC(EMSUART_UART) = (1 << UIBD); // clear BRK detect IRQ
ETS_UART_INTR_ENABLE();
return EMS_TX_BRK_DETECT;
}
}
}
// we got the whole telegram in the Rx buffer
// on Rx-BRK (bus collision), we simply enable Rx and leave it
// otherwise we send the final Tx-BRK in the loopback and re=enable Rx-INT.
// worst case, we'll see an additional Rx-BRK...
if (result == EMS_TX_STATUS_OK) {
// neither bus collision nor timeout - send terminating BRK signal
if (!(USIS(EMSUART_UART) & (1 << UIBD))) {
// no bus collision - send terminating BRK signal
USC0(EMSUART_UART) |= (1 << UCLBE) | (1 << UCBRK); // enable loopback & set <BRK>
// wait until BRK detected...
while (!(USIR(EMSUART_UART) & (1 << UIBD))) {
delayMicroseconds(EMSUART_BIT_TIME);
}
USC0(EMSUART_UART) &= ~((1 << UCBRK) | (1 << UCLBE)); // disable loopback & clear <BRK>
USIC(EMSUART_UART) = (1 << UIBD); // clear BRK detect IRQ
phantomBreak = 1;
}
}
ETS_UART_INTR_ENABLE(); // open up the FIFO again to start receiving
return result; // send the Tx status back
} }
/*
* flush everything left over in buffer, this clears both rx and tx FIFOs
*/
void ICACHE_FLASH_ATTR EMSuart::emsuart_flush_fifos() {
uint32_t tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask
USC0(EMSUART_UART) |= (tmp); // set bits
USC0(EMSUART_UART) &= ~(tmp); // clear bits
}
/*
* Send a BRK signal
* Which is a 11-bit set of zero's (11 cycles)
*/
void ICACHE_FLASH_ATTR EMSuart::tx_brk() {
uint32_t tmp;
// must make sure Tx FIFO is empty
while (((USS(EMSUART_UART) >> USTXC) & 0xFF))
;
tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask
USC0(EMSUART_UART) |= (tmp); // set bits
USC0(EMSUART_UART) &= ~(tmp); // clear bits
// To create a 11-bit <BRK> we set TXD_BRK bit so the break signal will
// automatically be sent when the tx fifo is empty
tmp = (1 << UCBRK);
USC0(EMSUART_UART) |= (tmp); // set bit
if (tx_mode_ == EMS_TXMODE_EMSPLUS) { // EMS+ mode
delayMicroseconds(EMSUART_TX_BRK_WAIT);
} else if (tx_mode_ == EMS_TXMODE_HT3) { // junkers mode
delayMicroseconds(EMSUART_TX_WAIT_BRK - EMSUART_TX_LAG); // 1144 (11 Bits)
}
USC0(EMSUART_UART) &= ~(tmp); // clear bit
}
} // namespace emsesp } // namespace emsesp
#endif #endif

View File

@@ -28,14 +28,26 @@
#define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit #define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit
#define EMS_MAXBUFFERS 3 // buffers for circular filling to avoid collisions #define EMS_MAXBUFFERS 3 // buffers for circular filling to avoid collisions
#define EMS_MAXBUFFERSIZE 33 // max size of the buffer. EMS packets are max 32 bytes, plus extra 2 for BRK #define EMS_MAXBUFFERSIZE 33 // max size of the buffer. EMS packets are max 32 bytes, plus extra 2 for BRKs
#define EMSUART_recvTaskPrio 1 // 0, 1 or 2. 0 being the lowest #define EMSUART_recvTaskPrio 1 // 0, 1 or 2. 0 being the lowest
#define EMSUART_recvTaskQueueLen 10 // number of queued'd Rx triggers #define EMSUART_recvTaskQueueLen 10 // number of queued Rx triggers
#define EMS_TXMODE_DEFAULT 1 #define EMS_TXMODE_DEFAULT 1
#define EMS_TXMODE_EMSPLUS 2 #define EMS_TXMODE_EMSPLUS 2
#define EMS_TXMODE_HT3 3 #define EMS_TXMODE_HT3 3
#define EMS_TXMODE_NEW 4 // for michael
// LEGACY
#define EMSUART_BIT_TIME 104 // bit time @9600 baud
#define EMSUART_TX_BRK_WAIT 2070 // the BRK from Boiler master is roughly 1.039ms, so accounting for hardware lag using around 2078 (for half-duplex) - 8 (lag)
#define EMSUART_TX_WAIT_BYTE (EMSUART_BIT_TIME * 10) // Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit)
#define EMSUART_TX_WAIT_BRK (EMSUART_BIT_TIME * 11) // Time to send a BRK Signal (11 Bit)
#define EMSUART_TX_WAIT_GAP (EMSUART_BIT_TIME * 7) // Gap between to Bytes
#define EMSUART_TX_LAG 8
#define EMSUART_BUSY_WAIT (EMSUART_BIT_TIME / 8)
#define EMS_TX_TO_CHARS (2 + 20)
#define EMS_TX_TO_COUNT (EMS_TX_TO_CHARS * 8)
namespace emsesp { namespace emsesp {
@@ -64,6 +76,9 @@ class EMSuart {
private: private:
static void ICACHE_RAM_ATTR emsuart_rx_intr_handler(void * para); static void ICACHE_RAM_ATTR emsuart_rx_intr_handler(void * para);
static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events); static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events);
static void ICACHE_FLASH_ATTR emsuart_flush_fifos();
static void ICACHE_FLASH_ATTR tx_brk();
}; };
} // namespace emsesp } // namespace emsesp

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "2.0.0a6" #define EMSESP_APP_VERSION "2.0.0a8"