alpha 0.2 - add solar, mixing, heatpump

This commit is contained in:
Paul
2020-05-11 23:00:53 +02:00
parent 30b502a1e4
commit 3801042ea5
34 changed files with 745 additions and 280 deletions

104
README.md
View File

@@ -1,6 +1,8 @@
# EMS-ESP version 2.0 (alpha)
*Warning: this is a snapshot the EMS-ESP2 development in still in early stages of development and not for production!*
*Warning: this is a snapshot from my EMS-ESP2 development repo and still in early stages of it's development. Not all features have been fully tested. Use at your own risk!*
Note: Version 2.0 is not backward compatible with v1.0. The File system structure is different. When coming from version 1.9.x its best if you first erase the flash on the ESP and upload using USB. `esptool.py erase_flash` will clean the flash and `esptool.py -p COM6 -b 921600 write_flash 0x00000 firmware.bin` is an example of how to upload the firmware over USB.
---
@@ -8,9 +10,9 @@
### **Design & Coding principles**
- The code can be built and run without an ESP microcontroller, which helps with testing and simulating handling of telegrams.
- The code can be built and run without an ESP microcontroller, which is useful with testing and simulating handling of telegrams. Make sure you have GNU make and g++ installed and use 'make' to build the image and execute the file `emsesp` (on linux).
- I used C++11 containers where I could (std::string, std::deque, std::list, std::multimap etc).
- The core is based off the great libraries from @nomis and adopted his general design pattens such as making everything as asynchronous as possible so that no one operation should starve another operation of it's time to execute (https://isocpp.org/wiki/faq/ctors#static-init-order).
- The core is based off the great libraries from @nomis' and I adopted his general design pattens such as making everything as asynchronous as possible so that no one operation should starve another operation of it's time to execute (https://isocpp.org/wiki/faq/ctors#static-init-order).
- All EMS devices (e.g. boiler, thermostat, solar modules etc) are derived from a factory base class and each class handles its own registering of telegram and mqtt handlers. This makes the EMS device code easier to manage and extend with new telegrams types and features.
- Built to work with both EMS8266 and ESP32.
@@ -26,29 +28,111 @@
* `su` to switch to Admin which enables more commands such as most of the `set` commands. The default password is "neo". When in Admin mode the command prompt switches from `$` to `#`.
* `log` sets the logging. `log off` disables logging. Use `log trace` to see the telegram traffic and `log debug` for very verbose logging. To watch a specific telegram ID or device ID use `log trace [id]`.
- There is no "serial mode" anymore. When the Wifi cannot connect to the SSID it will automatically enter a "safe" mode where the Serial console is activated. Note Serial is always available on the ESP32 because it has 2 UARTs.
- There is no "serial mode" anymore like with version 1.9. When the Wifi cannot connect to the SSID it will automatically enter a "safe" mode where the Serial console is activated, baud 115200. Note Serial is always available on the ESP32 because it has 2 UARTs.
- LED behaves like in 1.9. A solid LED means good connection and EMS data is coming in. A slow pulse means either the WiFi or the EMS bus is not connected. A very fast pulse is when the system is booting up and configuring itself.
- on a new install you will want to enter `su` and then go to the `system` context. Use `set wifi ...` to set the network up. Then go to the `mqtt` context to set the mqtt up.
# Full Console
```
common commands available in all contexts:
exit
help
log [level] [trace ID]
su
top level/root
refresh
show
show version
ems (is a menu)
mqtt (is a menu)
system (is a menu)
boiler (is a menu)
thermostat (is a menu)
ems
scan devices [deep]
send telegram <"XX XX ...">
set
set bus_id <device ID>
set read_only <on | off>
set tx_mode <n>
show
show devices
show emsbus
mqtt
publish
reconnect
set
set base <name>
set enabled <on | off>
set heartbeat <on | off>
set ip <IP address>
set nested_json <on | off>
set password
set publish_time <seconds>
set qos <n>
set username [name]
show
system
format
passwd
restart
set
set hostname <name>
set syslog host [IP address]
set syslog level [level]
set syslog mark [seconds]
set wifi password
set wifi ssid <name>
show
test <name>
wifi disconnect
wifi reconnect
wifi scan
boiler
change comfort <hot|eco|intelligent>
change flowtemp <degrees>
change wwactive <on | off>
change wwcirculation <on | off>
change wwonetime <on | off>
change wwtemp <degrees>
read <type ID>
set
set shower alert <on | off>
set shower timer <on | off>
show
thermostat
change mode <mode> [heating circuit]
change temp <degrees> [heating circuit]
read <type ID>
set
set master [device ID]
show
```
----------
### **Fixes and Improvements to work on now**
### **Known issues, bugs and improvements currently working on**
```
TODO figure out why sometimes telnet on ESP32 (and sometimes ESP8266) has slow response times.
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 console auto-complete with 'set' command in the system context is not showing all commands, only the hostname.
TODO sometimes get an error after a Tx send when first booting up. timeout perhaps?
```
### **Features to add next**
```
TODO finish porting over code for Solar, Mixing and Heat pump.
TODO implement 0xE9 telegram type for EMS+ boilers. This was a request #382 (https://github.com/proddy/EMS-ESP/issues/382)
TODO validate 0xE9 with data from Koen. (https://github.com/proddy/EMS-ESP/issues/382)
```
### **To tidy up in code later**
@@ -62,7 +146,7 @@ TODO add real unit tests using platformio's test bed (https://docs.platformio.or
TODO See if it's easier to use timers instead of millis() timers, using https://github.com/esp8266/Arduino/blob/master/libraries/esp8266/examples/BlinkPolledTimeout/BlinkPolledTimeout.ino
```
### **These features to add**
### **These features to add next**
```
TODO merge in the web code

View File

@@ -79,10 +79,17 @@ void Commands::add_command(unsigned int context,
}
// added by proddy
// note we should really iterate and free up the lambda code and any flashstrings
void Commands::remove_all_commands() {
commands_.clear();
}
// added by proddy
// note we should really iterate and free up the lambda code and any flashstrings
void Commands::remove_context_commands(unsigned int context) {
commands_.erase(context);
/*
/*
auto commands = commands_.equal_range(context);
for (auto command_it = commands.first; command_it != commands.second; command_it++) {
shell.printf("Got: ");

View File

@@ -57,9 +57,8 @@ Shell::~Shell() {
}
void Shell::start() {
// Added by proddy - default log level
#ifdef EMSESP_DEBUG
uuid::log::Logger::register_handler(this, uuid::log::Level::NOTICE); // was debug
uuid::log::Logger::register_handler(this, uuid::log::Level::DEBUG); // added by proddy
#else
uuid::log::Logger::register_handler(this, uuid::log::Level::NOTICE);
#endif

View File

@@ -128,7 +128,6 @@ void Shell::print_all_available_commands() {
command_line.escape_initial_parameters(name.size());
name.clear();
arguments.clear();
print(" "); // added by proddy
println(command_line.to_string(maximum_command_line_length()));
});
*/

View File

@@ -1313,6 +1313,7 @@ class Commands {
void for_each_available_command(Shell & shell, apply_function f) const;
void remove_context_commands(unsigned int context); // added by proddy
void remove_all_commands(); // added by proddy
private:
/**

View File

@@ -66,35 +66,12 @@ namespace uuid {
#define COLOR_BRIGHT_CYAN "\x1B[0;96m"
#define COLOR_BRIGHT_WHITE "\x1B[0;97m"
#define COLOR_UNDERLINE "\x1B[4m"
/*
Background Black: \u001b[40m
Background Red: \u001b[41m
Background Green: \u001b[42m
Background Yellow: \u001b[43m
Background Blue: \u001b[44m
Background Magenta: \u001b[45m
Background Cyan: \u001b[46m
Background White: \u001b[47m
With the bright versions being:
Background Bright Black: \u001b[40;1m
Background Bright Red: \u001b[41;1m
Background Bright Green: \u001b[42;1m
Background Bright Yellow: \u001b[43;1m
Background Bright Blue: \u001b[44;1m
Background Bright Magenta: \u001b[45;1m
Background Bright Cyan: \u001b[46;1m
Background Bright White: \u001b[47;1m
*/
#define COLOR_BRIGHT_RED_BACKGROUND "\x1B[41;1m"
namespace log {
/**
* Severity level of log messages. Proddy added a VERBOSE
* Severity level of log messages. Proddy switches trace & debug
*
* @since 1.0.0
*/
@@ -340,7 +317,7 @@ class Logger {
*
* @since 1.0.0
*/
static constexpr size_t MAX_LOG_LENGTH = 255; // proddy note, kept at 255
static constexpr size_t MAX_LOG_LENGTH = 255;
/**
* Create a new logger with the given name and logging facility.

View File

@@ -21,7 +21,7 @@ extra_configs = pio_local.ini
;debug_flags = -DDEBUG_ESP_PORT=Serial -DDEBUG_ESP_CORE -DDEBUG_ESP_SSL -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM
debug_flags =
-D EMSESP_DEBUG
; -D EMSESP_DEBUG
; -D EMSESP_SAFE_MODE
; -D ENABLE_CORS -D CORS_ORIGIN=\"http://localhost:3000\"
@@ -57,13 +57,14 @@ upload_protocol = esptool
; upload_flags =
; --port=8266
; --auth=neo
; upload_port = ems-esp2.local
; upload_port = ems-esp.local
[env:esp8266]
build_type = release
platform = espressif8266 ; https://github.com/platformio/platform-espressif8266/releases
;platform = espressif8266@2.4.0 ; Arduino core 2.6.3
board = esp12e
; board = esp12e
board = d1_mini ; https://github.com/platformio/platform-espressif8266/blob/master/boards/d1_mini.json
lib_deps = ${common.libs_core} ${common.libs_esp8266}
board_build.f_cpu = 160000000L ; 160MHz
;board_build.ldscript = eagle.flash.4m1m.ld ; 1019 KB sketch, 1000 KB SPIFFS. 4KB EEPROM, 4KB RFCAL, 12KB WIFI stack, 2052 KB OTA & buffer

View File

@@ -49,21 +49,24 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
DEBUG_LOG(F("Registering new Boiler with device ID 0x%02X"), device_id);
// the telegram handlers...
register_telegram_type(0x18, F("UBAMonitorFast"), true, std::bind(&Boiler::process_UBAMonitorFast, this, _1)); // 0x18
register_telegram_type(0x19, F("UBAMonitorSlow"), true, std::bind(&Boiler::process_UBAMonitorSlow, this, _1)); // 0x19
register_telegram_type(0x34, F("UBAMonitorWW"), false, std::bind(&Boiler::process_UBAMonitorWW, this, _1)); // 0x34
register_telegram_type(0x1C, F("UBAMaintenanceStatus"), false, std::bind(&Boiler::process_UBAMaintenanceStatus, this, _1)); // 0x1C
register_telegram_type(0x2A, F("MC10Status"), false, std::bind(&Boiler::process_MC10Status, this, _1)); // 0x2A
register_telegram_type(0x33, F("UBAParameterWW"), true, std::bind(&Boiler::process_UBAParameterWW, this, _1)); // 0x33
register_telegram_type(0x14, F("UBATotalUptime"), true, std::bind(&Boiler::process_UBATotalUptime, this, _1)); // 0x14
register_telegram_type(0x35, F("UBAFlags"), false, std::bind(&Boiler::process_UBAFlags, this, _1)); // 0x35
register_telegram_type(0x15, F("UBAMaintenanceSettings"), false, std::bind(&Boiler::process_UBAMaintenanceSettings, this, _1)); // 0x15
register_telegram_type(0x16, F("UBAParameters"), true, std::bind(&Boiler::process_UBAParameters, this, _1)); // 0x16
register_telegram_type(0x1A, F("UBASetPoints"), false, std::bind(&Boiler::process_UBASetPoints, this, _1)); // 0x1A
register_telegram_type(0xD1, F("UBAOutdoorTemp"), false, std::bind(&Boiler::process_UBAOutdoorTemp, this, _1)); // 0xD1
register_telegram_type(0xE4, F("UBAMonitorFastPlus"), false, std::bind(&Boiler::process_UBAMonitorFastPlus, this, _1)); // 0xE4
register_telegram_type(0xE5, F("UBAMonitorSlowPlus"), false, std::bind(&Boiler::process_UBAMonitorSlowPlus, this, _1)); // 0xE5
register_telegram_type(0xE9, F("UBADHWStatus"), false, std::bind(&Boiler::process_UBADHWStatus, this, _1)); // 0xE9
register_telegram_type(0x18, F("UBAMonitorFast"), true, std::bind(&Boiler::process_UBAMonitorFast, this, _1));
register_telegram_type(0x19, F("UBAMonitorSlow"), true, std::bind(&Boiler::process_UBAMonitorSlow, this, _1));
register_telegram_type(0x34, F("UBAMonitorWW"), false, std::bind(&Boiler::process_UBAMonitorWW, this, _1));
register_telegram_type(0x1C, F("UBAMaintenanceStatus"), false, std::bind(&Boiler::process_UBAMaintenanceStatus, this, _1));
register_telegram_type(0x2A, F("MC10Status"), false, std::bind(&Boiler::process_MC10Status, this, _1));
register_telegram_type(0x33, F("UBAParameterWW"), true, std::bind(&Boiler::process_UBAParameterWW, this, _1));
register_telegram_type(0x14, F("UBATotalUptime"), true, std::bind(&Boiler::process_UBATotalUptime, this, _1));
register_telegram_type(0x35, F("UBAFlags"), false, std::bind(&Boiler::process_UBAFlags, this, _1));
register_telegram_type(0x15, F("UBAMaintenanceSettings"), false, std::bind(&Boiler::process_UBAMaintenanceSettings, this, _1));
register_telegram_type(0x16, F("UBAParameters"), true, std::bind(&Boiler::process_UBAParameters, this, _1));
register_telegram_type(0x1A, F("UBASetPoints"), false, std::bind(&Boiler::process_UBASetPoints, this, _1));
register_telegram_type(0xD1, F("UBAOutdoorTemp"), false, std::bind(&Boiler::process_UBAOutdoorTemp, this, _1));
register_telegram_type(0xE4, F("UBAMonitorFastPlus"), false, std::bind(&Boiler::process_UBAMonitorFastPlus, this, _1));
register_telegram_type(0xE5, F("UBAMonitorSlowPlus"), false, std::bind(&Boiler::process_UBAMonitorSlowPlus, this, _1));
register_telegram_type(0xE9, F("UBADHWStatus"), false, std::bind(&Boiler::process_UBADHWStatus, this, _1));
register_telegram_type(0xE3, F("HeatPumpMonitor1"), true, std::bind(&Boiler::process_HPMonitor1, this, _1));
register_telegram_type(0xE5, F("HeatPumpMonitor2"), true, std::bind(&Boiler::process_HPMonitor2, this, _1));
// MQTT callbacks
register_mqtt_topic("boiler_cmd", std::bind(&Boiler::boiler_cmd, this, _1));
@@ -73,6 +76,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_mqtt_topic("boiler_cmd_wwtemp", std::bind(&Boiler::boiler_cmd_wwtemp, this, _1));
}
// add submenu context
void Boiler::add_context_menu() {
EMSESPShell::commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
@@ -83,8 +87,8 @@ void Boiler::add_context_menu() {
});
}
// boiler_cmd topic
void Boiler::boiler_cmd(const char * message) {
// convert JSON and get the command
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message);
if (error) {
@@ -157,148 +161,155 @@ void Boiler::boiler_cmd_wwtemp(const char * message) {
void Boiler::publish_values() {
const size_t capacity = JSON_OBJECT_SIZE(47); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/
DynamicJsonDocument doc(capacity);
JsonObject rootBoiler = doc.to<JsonObject>();
char s[10]; // for formatting strings
if (wWComfort_ == 0x00) {
rootBoiler["wWComfort"] = "Hot";
doc["wWComfort"] = "Hot";
} else if (wWComfort_ == 0xD8) {
rootBoiler["wWComfort"] = "Eco";
doc["wWComfort"] = "Eco";
} else if (wWComfort_ == 0xEC) {
rootBoiler["wWComfort"] = "Intelligent";
doc["wWComfort"] = "Intelligent";
}
if (wWSelTemp_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["wWSelTemp"] = wWSelTemp_;
doc["wWSelTemp"] = wWSelTemp_;
}
if (wWDisinfectTemp_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["wWDisinfectionTemp"] = wWDisinfectTemp_;
doc["wWDisinfectionTemp"] = wWDisinfectTemp_;
}
if (selFlowTemp_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["selFlowTemp"] = selFlowTemp_;
doc["selFlowTemp"] = selFlowTemp_;
}
if (selBurnPow_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["selBurnPow"] = selBurnPow_;
doc["selBurnPow"] = selBurnPow_;
}
if (curBurnPow_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["curBurnPow"] = curBurnPow_;
doc["curBurnPow"] = curBurnPow_;
}
if (pumpMod_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["pumpMod"] = pumpMod_;
doc["pumpMod"] = pumpMod_;
}
if (wWCircPump_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["wWCircPump"] = wWCircPump_;
doc["wWCircPump"] = wWCircPump_;
}
if (wWCircPumpType_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["wWCiPuType"] = wWCircPumpType_;
doc["wWCiPuType"] = wWCircPumpType_;
}
if (wWCircPumpMode_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["wWCiPuMode"] = wWCircPumpMode_;
doc["wWCiPuMode"] = wWCircPumpMode_;
}
if (extTemp_ != EMS_VALUE_SHORT_NOTSET) {
rootBoiler["outdoorTemp"] = (float)extTemp_ / 10;
doc["outdoorTemp"] = (float)extTemp_ / 10;
}
if (wWCurTmp_ != EMS_VALUE_USHORT_NOTSET) {
rootBoiler["wWCurTmp"] = (float)wWCurTmp_ / 10;
doc["wWCurTmp"] = (float)wWCurTmp_ / 10;
}
if (wWCurFlow_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["wWCurFlow"] = (float)wWCurFlow_ / 10;
doc["wWCurFlow"] = (float)wWCurFlow_ / 10;
}
if (curFlowTemp_ != EMS_VALUE_USHORT_NOTSET) {
rootBoiler["curFlowTemp"] = (float)curFlowTemp_ / 10;
doc["curFlowTemp"] = (float)curFlowTemp_ / 10;
}
if (retTemp_ != EMS_VALUE_USHORT_NOTSET) {
rootBoiler["retTemp"] = (float)retTemp_ / 10;
doc["retTemp"] = (float)retTemp_ / 10;
}
if (switchTemp_ != EMS_VALUE_USHORT_NOTSET) {
rootBoiler["switchTemp"] = (float)switchTemp_ / 10;
doc["switchTemp"] = (float)switchTemp_ / 10;
}
if (sysPress_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["sysPress"] = (float)sysPress_ / 10;
doc["sysPress"] = (float)sysPress_ / 10;
}
if (boilTemp_ != EMS_VALUE_USHORT_NOTSET) {
rootBoiler["boilTemp"] = (float)boilTemp_ / 10;
doc["boilTemp"] = (float)boilTemp_ / 10;
}
if (wwStorageTemp1_ != EMS_VALUE_USHORT_NOTSET) {
rootBoiler["wwStorageTemp1"] = (float)wwStorageTemp1_ / 10;
doc["wwStorageTemp1"] = (float)wwStorageTemp1_ / 10;
}
if (wwStorageTemp2_ != EMS_VALUE_USHORT_NOTSET) {
rootBoiler["wwStorageTemp2"] = (float)wwStorageTemp2_ / 10;
doc["wwStorageTemp2"] = (float)wwStorageTemp2_ / 10;
}
if (exhaustTemp_ != EMS_VALUE_USHORT_NOTSET) {
rootBoiler["exhaustTemp"] = (float)exhaustTemp_ / 10;
doc["exhaustTemp"] = (float)exhaustTemp_ / 10;
}
if (wWActivated_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["wWActivated"] = Helpers::render_value(s, wWActivated_, EMS_VALUE_BOOL);
doc["wWActivated"] = Helpers::render_value(s, wWActivated_, EMS_VALUE_BOOL);
}
if (wWOneTime_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["wWOnetime"] = Helpers::render_value(s, wWOneTime_, EMS_VALUE_BOOL);
doc["wWOnetime"] = Helpers::render_value(s, wWOneTime_, EMS_VALUE_BOOL);
}
if (wWDesinfecting_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["wWDesinfecting"] = Helpers::render_value(s, wWDesinfecting_, EMS_VALUE_BOOL);
doc["wWDesinfecting"] = Helpers::render_value(s, wWDesinfecting_, EMS_VALUE_BOOL);
}
if (wWReadiness_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["wWReady"] = Helpers::render_value(s, wWReadiness_, EMS_VALUE_BOOL);
doc["wWReady"] = Helpers::render_value(s, wWReadiness_, EMS_VALUE_BOOL);
}
if (wWRecharging_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["wWRecharge"] = Helpers::render_value(s, wWRecharging_, EMS_VALUE_BOOL);
doc["wWRecharge"] = Helpers::render_value(s, wWRecharging_, EMS_VALUE_BOOL);
}
if (wWTemperatureOK_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["wWTempOK"] = Helpers::render_value(s, wWTemperatureOK_, EMS_VALUE_BOOL);
doc["wWTempOK"] = Helpers::render_value(s, wWTemperatureOK_, EMS_VALUE_BOOL);
}
if (wWCirc_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["wWCirc"] = Helpers::render_value(s, wWCirc_, EMS_VALUE_BOOL);
doc["wWCirc"] = Helpers::render_value(s, wWCirc_, EMS_VALUE_BOOL);
}
if (burnGas_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["burnGas"] = Helpers::render_value(s, burnGas_, EMS_VALUE_BOOL);
doc["burnGas"] = Helpers::render_value(s, burnGas_, EMS_VALUE_BOOL);
}
if (flameCurr_ != EMS_VALUE_USHORT_NOTSET) {
rootBoiler["flameCurr"] = (float)(int16_t)flameCurr_ / 10;
doc["flameCurr"] = (float)(int16_t)flameCurr_ / 10;
}
if (heatPmp_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["heatPmp"] = Helpers::render_value(s, heatPmp_, EMS_VALUE_BOOL);
doc["heatPmp"] = Helpers::render_value(s, heatPmp_, EMS_VALUE_BOOL);
}
if (fanWork_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["fanWork"] = Helpers::render_value(s, fanWork_, EMS_VALUE_BOOL);
doc["fanWork"] = Helpers::render_value(s, fanWork_, EMS_VALUE_BOOL);
}
if (ignWork_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["ignWork"] = Helpers::render_value(s, ignWork_, EMS_VALUE_BOOL);
doc["ignWork"] = Helpers::render_value(s, ignWork_, EMS_VALUE_BOOL);
}
if (wWHeat_ != EMS_VALUE_BOOL_NOTSET) {
rootBoiler["wWHeat"] = Helpers::render_value(s, wWHeat_, EMS_VALUE_BOOL);
doc["wWHeat"] = Helpers::render_value(s, wWHeat_, EMS_VALUE_BOOL);
}
if (heating_temp_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["heating_temp"] = heating_temp_;
doc["heating_temp"] = heating_temp_;
}
if (pump_mod_max_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["pump_mod_max"] = pump_mod_max_;
doc["pump_mod_max"] = pump_mod_max_;
}
if (pump_mod_min_ != EMS_VALUE_UINT_NOTSET) {
rootBoiler["pump_mod_min"] = pump_mod_min_;
doc["pump_mod_min"] = pump_mod_min_;
}
if (wWStarts_ != EMS_VALUE_ULONG_NOTSET) {
rootBoiler["wWStarts"] = wWStarts_;
doc["wWStarts"] = wWStarts_;
}
if (wWWorkM_ != EMS_VALUE_ULONG_NOTSET) {
rootBoiler["wWWorkM"] = wWWorkM_;
doc["wWWorkM"] = wWWorkM_;
}
if (UBAuptime_ != EMS_VALUE_ULONG_NOTSET) {
rootBoiler["UBAuptime"] = UBAuptime_;
doc["UBAuptime"] = UBAuptime_;
}
if (burnStarts_ != EMS_VALUE_ULONG_NOTSET) {
rootBoiler["burnStarts"] = burnStarts_;
doc["burnStarts"] = burnStarts_;
}
if (burnWorkMin_ != EMS_VALUE_ULONG_NOTSET) {
rootBoiler["burnWorkMin"] = burnWorkMin_;
doc["burnWorkMin"] = burnWorkMin_;
}
if (heatWorkMin_ != EMS_VALUE_ULONG_NOTSET) {
rootBoiler["heatWorkMin"] = heatWorkMin_;
doc["heatWorkMin"] = heatWorkMin_;
}
if (serviceCode_ != EMS_VALUE_USHORT_NOTSET) {
rootBoiler["ServiceCode"] = serviceCodeChar_;
rootBoiler["ServiceCodeNumber"] = serviceCode_;
doc["serviceCode"] = serviceCodeChar_;
doc["serviceCodeNumber"] = serviceCode_;
}
// heatpump specific
if (hpModulation_ != EMS_VALUE_UINT_NOTSET) {
doc["pumpmodulation"] = hpModulation_;
}
if (hpSpeed_ != EMS_VALUE_UINT_NOTSET) {
doc["pumpspeed"] = hpSpeed_;
}
#ifdef EMSESP_DEBUG
@@ -410,6 +421,14 @@ void Boiler::show_values(uuid::console::Shell & shell) {
shell.printfln(F(" Total UBA working time: %d days %d hours %d minutes"), UBAuptime_ / 1440, (UBAuptime_ % 1440) / 60, UBAuptime_ % 60);
}
if (hpModulation_ != EMS_VALUE_UINT_NOTSET) {
print_value(shell, F("Heat Pump modulation"), F_(percent), Helpers::render_value(buffer, hpModulation_, 1));
}
if (hpSpeed_ != EMS_VALUE_UINT_NOTSET) {
print_value(shell, F("Heat Pump speed"), F_(percent), Helpers::render_value(buffer, hpSpeed_, 1));
}
shell.println();
}
@@ -583,6 +602,39 @@ void Boiler::process_UBAMonitorSlowPlus(std::shared_ptr<const Telegram> telegram
telegram->read_value(pumpMod_, 25); // or is it switchTemp ?
}
/*
* Type 0xE3 - HeatPump Monitor 1
*/
void Boiler::process_HPMonitor1(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(hpModulation_, 13);
}
/*
* Type 0xE5 - HeatPump Monitor 2
*/
void Boiler::process_HPMonitor2(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(hpSpeed_, 25);
}
// 0xE9 - DHW Status
// e.g. 08 00 E9 00 37 01 F6 01 ED 00 00 00 00 41 3C 00 00 00 00 00 00 00 00 00 00 00 00 37 00 00 00 (CRC=77) #data=27
void Boiler::process_UBADHWStatus(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(wWSetTmp_, 0);
telegram->read_value(wWCurTmp_, 1);
telegram->read_value(wWCurTmp2_, 3);
telegram->read_value(wWWorkM_, 17);
telegram->read_value(wWStarts_, 14);
telegram->read_value(wWOneTime_, 12, 2);
telegram->read_value(wWDesinfecting_, 12, 3);
telegram->read_value(wWReadiness_, 12, 4);
telegram->read_value(wWRecharging_, 13, 4);
telegram->read_value(wWTemperatureOK_, 13, 5);
telegram->read_value(wWActivated_, 20);
telegram->read_value(wWCircPump_, 13, 2);
telegram->read_value(wWSelTemp_, 10);
telegram->read_value(wWDisinfectTemp_, 9);
}
/**
* UBAOutdoorTemp - type 0xD1 - external temperature EMS+
*/
@@ -593,11 +645,6 @@ void Boiler::process_UBAOutdoorTemp(std::shared_ptr<const Telegram> telegram) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
// 0xE9 - DHW Status
void Boiler::process_UBADHWStatus(std::shared_ptr<const Telegram> telegram) {
}
// UBASetPoint 0x1A
// not yet implemented
void Boiler::process_UBASetPoints(std::shared_ptr<const Telegram> telegram) {

View File

@@ -32,9 +32,6 @@
#include "helpers.h"
#include "mqtt.h"
MAKE_PSTR(logger_name2, "boiler")
namespace emsesp {
class Boiler : public EMSdevice {
@@ -122,6 +119,10 @@ class Boiler : public EMSdevice {
uint8_t tap_water_active_ = EMS_VALUE_BOOL_NOTSET; // Hot tap water is on/off
uint8_t heating_active_ = EMS_VALUE_BOOL_NOTSET; // Central heating is on/off
// heatpump boilers
uint8_t hpModulation_ = EMS_VALUE_UINT_NOTSET; // heatpump modulation in %
uint8_t hpSpeed_ = EMS_VALUE_UINT_NOTSET; // speed 0-100 %
void process_UBAParameterWW(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorFast(std::shared_ptr<const Telegram> telegram);
void process_UBATotalUptime(std::shared_ptr<const Telegram> telegram);
@@ -136,7 +137,10 @@ class Boiler : public EMSdevice {
void process_MC10Status(std::shared_ptr<const Telegram> telegram);
void process_UBAMaintenanceStatus(std::shared_ptr<const Telegram> telegram);
void process_UBAMaintenanceSettings(std::shared_ptr<const Telegram> telegram);
void process_UBADHWStatus(std::shared_ptr<const Telegram> telegram);
void process_HPMonitor1(std::shared_ptr<const Telegram> telegram);
void process_HPMonitor2(std::shared_ptr<const Telegram> telegram);
void check_active();

View File

@@ -50,6 +50,10 @@ void EMSESPShell::stopped() {
logger().log(LogLevel::INFO, LogFacility::AUTH, F("Admin session closed on console %s"), console_name().c_str());
}
logger().log(LogLevel::INFO, LogFacility::CONSOLE, F("User session closed on console %s"), console_name().c_str());
// remove all custom contexts
commands->remove_all_commands();
_console_commands_loaded = false; // make sure they got loaded next time a console is opened
}
// show welcome banner
@@ -87,7 +91,9 @@ void EMSESPShell::add_console_commands() {
return;
}
commands->remove_context_commands(ShellContext::MAIN); // just in case, remove everything
// just in case, remove everything
// commands->remove_context_commands(ShellContext::MAIN);
commands->remove_all_commands();
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
@@ -96,7 +102,7 @@ void EMSESPShell::add_console_commands() {
shell.printfln(F("Refreshing console and fetching device data"));
_console_commands_loaded = false;
add_console_commands();
EMSESP::fetch_all_values();
EMSESP::fetch_device_values();
});
commands->add_command(ShellContext::MAIN,
@@ -119,7 +125,7 @@ void EMSESPShell::add_console_commands() {
});
/*
* add the submenus...
* add the submenu contexts...
*/
// MQTT
@@ -150,7 +156,7 @@ void EMSESPShell::add_console_commands() {
});
// add all the context menus for the connected devices
// this assumes they have been loaded
// this assumes they devices have been detected and registered
EMSESP::add_context_menu();
enter_custom_context(ShellContext::MAIN); // add su, exit and help
@@ -223,7 +229,7 @@ void EMSESPShell::enter_custom_context(unsigned int context) {
uint16_t watch_id = 0; // no watch ID set
if ((arguments.size() == 2) && (level == uuid::log::Level::TRACE)) {
watch_id = Helpers::hextoint(arguments[1].c_str());
shell.printfln(("Tracing only telegrams that match a device/telegram type ID of 0x%02X"), watch_id);
shell.printfln(("Tracing only telegrams that match a device ID/telegram type ID of 0x%02X"), watch_id);
}
emsesp::EMSESP::trace_watch_id(watch_id);
}
@@ -405,10 +411,10 @@ void Console::start() {
}
// always start the telnet service
// default idle is 10 minutes, default write timeout is 0 (automatic)
// note, this must be started after the network/wifi for ESP32 otherwise it'll crash
#ifndef EMSESP_STANDALONE
telnet_.start();
// default idle is 10 minutes, default write timeout is 0 (automatic)
// telnet_.default_write_timeout(1000); // in ms, socket timeout 1 second
#endif
}
@@ -422,6 +428,8 @@ void Console::loop() {
#endif
Shell::loop_all();
// delay(0); // in EMS-ESP 1.9.5 this helped with stability
}
} // namespace emsesp

View File

@@ -52,7 +52,9 @@ using uuid::log::Level;
// clang-format on
// common words
#ifdef EMSESP_DEBUG
MAKE_PSTR_WORD(test)
#endif
MAKE_PSTR_WORD(exit)
MAKE_PSTR_WORD(help)
MAKE_PSTR_WORD(settings)

View File

@@ -18,7 +18,7 @@
#include "controller.h"
MAKE_PSTR_WORD(controller)
// MAKE_PSTR_WORD(controller)
namespace emsesp {

View File

@@ -38,27 +38,27 @@
{170, DeviceType::BOILER, F("Logano GB212"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{172, DeviceType::BOILER, F("Enviline/Compress 6000AW"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Solar Modules - type 0x30
// Solar Modules - 0x30
{ 73, DeviceType::SOLAR, F("SM10"), DeviceFlags::EMS_DEVICE_FLAG_SM10},
{163, DeviceType::SOLAR, F("SM100"), DeviceFlags::EMS_DEVICE_FLAG_SM100},
{164, DeviceType::SOLAR, F("SM200"), DeviceFlags::EMS_DEVICE_FLAG_SM100},
{101, DeviceType::SOLAR, F("ISM1"), DeviceFlags::EMS_DEVICE_FLAG_SM100},
{162, DeviceType::SOLAR, F("SM50"), DeviceFlags::EMS_DEVICE_FLAG_SM100},
// Mixing Modules - type 0x20 or 0x21
// Mixing Modules - 0x20 / 0x21
{160, DeviceType::MIXING, F("MM100"), DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{161, DeviceType::MIXING, F("MM200"), DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{ 69, DeviceType::MIXING, F("MM10"), DeviceFlags::EMS_DEVICE_FLAG_MM10},
{159, DeviceType::MIXING, F("MM50"), DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
// Heat Pumps - type 0x38
// Heat Pumps - 0x38
{252, DeviceType::HEATPUMP, F("HP Module"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{200, DeviceType::HEATPUMP, F("HP Module"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Switches - type 0x11
// Switches - 0x11
{ 71, DeviceType::SWITCH, F("WM10"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x11
// Controllers - type 0x09 and 0x10
// Controllers - 0x09 / 0x10
{ 68, DeviceType::CONTROLLER, F("BC10/RFM20"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{218, DeviceType::CONTROLLER, F("M200/RFM200"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x50
{190, DeviceType::CONTROLLER, F("BC10"), DeviceFlags::EMS_DEVICE_FLAG_NONE},// 0x09
@@ -71,21 +71,20 @@
{230, DeviceType::CONTROLLER, F("BC Base"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{207, DeviceType::CONTROLLER, F("Sense II/CS200"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x10
// Connect devices - type 0x02
// Connect devices - 0x02
{205, DeviceType::CONNECT, F("Moduline Easy Connect"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x02
{206, DeviceType::CONNECT, F("Easy Connect"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x02
{171, DeviceType::CONNECT, F("OpenTherm Converter"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x02
// Gateways - type 0x48 and sometimes 0x18?
// Gateways - 0x48 / 0x18
{189, DeviceType::GATEWAY, F("KM200"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x48
{ 94, DeviceType::GATEWAY, F("RC"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x18
// Thermostat - not currently supporting write operations
// Thermostat - not currently supporting write operations, like the Easy/100 types - 0x18
{202, DeviceType::THERMOSTAT, F("Logamatic TC100/Moduline Easy"), DeviceFlags::EMS_DEVICE_FLAG_EASY | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write
{203, DeviceType::THERMOSTAT, F("EasyControl CT200"), DeviceFlags::EMS_DEVICE_FLAG_EASY | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write
{157, DeviceType::THERMOSTAT, F("RC200/CW100"), DeviceFlags::EMS_DEVICE_FLAG_RC100}, // 0x18
// Thermostat - Buderus/Nefit specific
// Thermostat - Common for Buderus/Nefit/Bosch specific - 0x17 / 0x10 / 0x18
{ 79, DeviceType::THERMOSTAT, F("RC10/Moduline 100"), DeviceFlags::EMS_DEVICE_FLAG_RC10},// 0x17
{ 80, DeviceType::THERMOSTAT, F("Moduline 200"), DeviceFlags::EMS_DEVICE_FLAG_RC10}, // 0x17
{ 77, DeviceType::THERMOSTAT, F("RC20/Moduline 300"), DeviceFlags::EMS_DEVICE_FLAG_RC20},// 0x17
@@ -93,14 +92,15 @@
{ 78, DeviceType::THERMOSTAT, F("Moduline 400"), DeviceFlags::EMS_DEVICE_FLAG_RC30}, // 0x10
{ 86, DeviceType::THERMOSTAT, F("RC35"), DeviceFlags::EMS_DEVICE_FLAG_RC35}, // 0x10
{ 93, DeviceType::THERMOSTAT, F("RC20RF"), DeviceFlags::EMS_DEVICE_FLAG_RC20}, // 0x19
{157, DeviceType::THERMOSTAT, F("RC200/CW100"), DeviceFlags::EMS_DEVICE_FLAG_RC100}, // 0x18
{158, DeviceType::THERMOSTAT, F("RC300/RC310/Moduline 3000/CW400/Sense II"), DeviceFlags::EMS_DEVICE_FLAG_RC300}, // 0x10
{165, DeviceType::THERMOSTAT, F("RC100/Moduline 1000/1010"), DeviceFlags::EMS_DEVICE_FLAG_RC100}, // 0x18, 0x38
// Thermostat - Sieger
// Thermostat - Sieger - 0x10 / 0x17
{ 76, DeviceType::THERMOSTAT, F("ES73"), DeviceFlags::EMS_DEVICE_FLAG_RC35}, // 0x10
{113, DeviceType::THERMOSTAT, F("ES72"), DeviceFlags::EMS_DEVICE_FLAG_RC20_2}, // 0x17
// Thermostat - Junkers - all 0x10
// Thermostat - Junkers - 0x10
{105, DeviceType::THERMOSTAT, F("FW100"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{106, DeviceType::THERMOSTAT, F("FW200"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{107, DeviceType::THERMOSTAT, F("FR100"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_2}, // older model

View File

@@ -23,7 +23,6 @@ MAKE_PSTR_WORD(tx_mode)
MAKE_PSTR_WORD(read_only)
MAKE_PSTR_WORD(emsbus)
MAKE_PSTR_WORD(devices)
MAKE_PSTR_WORD(deep)
MAKE_PSTR_WORD(send)
MAKE_PSTR_WORD(telegram)
@@ -56,7 +55,7 @@ Network EMSESP::network_; // WiFi
Shower EMSESP::shower_; // Shower logic
// static/common variables
uint8_t EMSESP::actual_master_thermostat_ = EMSESP_DEFAULT_NOTSET; // 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
bool EMSESP::tap_water_active_ = false; // for when Boiler states we having running warm water. used in Shower()
bool EMSESP::ems_read_only_;
@@ -66,19 +65,16 @@ bool EMSESP::ems_read_only_;
#endif
// for each associated EMS device go and request data values
void EMSESP::fetch_all_values() {
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
emsdevice->fetch_values();
}
}
void EMSESP::fetch_device_values() {
fetch_device_values(0); // fetch all
}
// for a specific EMS device go and request data values
// or if device_id is 0 it will fetch from all known devices
void EMSESP::fetch_device_values(const uint8_t device_id) {
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
if (emsdevice->is_device_id(device_id)) {
if ((device_id == 0) || emsdevice->is_device_id(device_id)) {
emsdevice->fetch_values();
return;
}
@@ -478,7 +474,7 @@ void EMSESP::show_devices(uuid::console::Shell & shell) {
shell.println();
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
shell.printf(F("%s%s%s"), COLOR_BOLD_ON, emsdevice->to_string().c_str(), COLOR_BOLD_OFF);
shell.printf(F("%s: %s"), emsdevice->device_type_name().c_str(), emsdevice->to_string().c_str());
if ((emsdevice->device_type() == EMSdevice::DeviceType::THERMOSTAT) && (emsdevice->device_id() == actual_master_thermostat())) {
shell.printf(F(" ** master device **"));
}
@@ -584,7 +580,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
txservice_.send_poll(); // close the bus
} else {
#ifdef EMSESP_DEBUG
logger_.err(F("Waiting for Tx ACK (1 or 4) but got 0x%02X"), first_value);
logger_.err(F("Expecting Tx ACK (1/4) but got 0x%02X. Tx:%s"), first_value, txservice_.last_tx_to_string().c_str());
#endif
}
@@ -611,7 +607,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
}
// check for poll, if so, send Tx from the queue immediately
// 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 ((length == 1) && ((first_value ^ 0x80 ^ rxservice_.ems_mask()) == txservice_.ems_bus_id())) {
EMSbus::last_bus_activity(millis()); // set the flag indication the EMS bus is active
txservice_.send();
@@ -729,7 +725,9 @@ void EMSESP::console_commands() {
CommandFlags::USER,
flash_string_vector{F_(send), F_(telegram)},
flash_string_vector{F_(data_mandatory)},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments) { EMSESP::send_raw_telegram(arguments.front().c_str()); });
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments) {
EMSESP::send_raw_telegram(arguments.front().c_str());
});
EMSESPShell::commands->add_command(ShellContext::EMS,
CommandFlags::USER,
@@ -784,7 +782,6 @@ void EMSESP::start() {
system_.start();
network_.start();
console_.start();
mqtt_.start();
sensors_.start();
rxservice_.start();
txservice_.start();

View File

@@ -54,7 +54,6 @@ class EMSESP {
static void start();
static void loop();
static void fetch_all_values();
static void publish_all_values();
#ifdef EMSESP_DEBUG
@@ -114,13 +113,14 @@ class EMSESP {
static void console_commands();
static void fetch_device_values(const uint8_t device_id);
static void fetch_device_values();
private:
EMSESP() = delete;
static uuid::log::Logger logger_;
static void fetch_device_values(const uint8_t device_id);
static bool add_device(const uint8_t device_id, const uint8_t product_id, std::string & version, const uint8_t brand);
static std::string device_tostring(const uint8_t device_id);

View File

@@ -18,7 +18,14 @@
#include "heatpump.h"
MAKE_PSTR_WORD(heatpump)
// MAKE_PSTR_WORD(heatpump)
/*
example telegrams 0x32B, 0x37B
"38 10 FF 00 03 7B 08 24 00 4B",
"38 10 FF 00 03 2B 00 C7 07 C3 01",
"38 10 FF 00 03 2B 00 D1 08 2A 01",
*/
namespace emsesp {
@@ -28,20 +35,25 @@ 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)
: 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);
// telegram handlers
// register_telegram_type(EMS_TYPE_XX, "XX", false, std::bind(&Heatpump::process_XX, this, _1));
register_telegram_type(0x047B, F("HP1"), true, std::bind(&Heatpump::process_HPMonitor1, this, _1));
register_telegram_type(0x042B, F("HP2"), true, std::bind(&Heatpump::process_HPMonitor2, this, _1));
// MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Heatpump::cmd, this, _1));
}
// context submenu
void Heatpump::add_context_menu() {
}
// display all values into the shell console
void Heatpump::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // always call this to show header
shell.println();
}
// publish values via MQTT
@@ -57,4 +69,25 @@ bool Heatpump::updated_values() {
void Heatpump::console_commands() {
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
/*
* Type 0x42B- HeatPump Monitor 1
* e.g. "38 10 FF 00 03 2B 00 D1 08 2A 01"
*/
void Heatpump::process_HPMonitor1(std::shared_ptr<const Telegram> telegram) {
// still to implement
}
/*
* Type 0x47B - HeatPump Monitor 2
* e.g. "38 10 FF 00 03 7B 08 24 00 4B"
*/
void Heatpump::process_HPMonitor2(std::shared_ptr<const Telegram> telegram) {
// still to implement
}
#pragma GCC diagnostic pop
} // namespace emsesp

View File

@@ -45,6 +45,9 @@ class Heatpump : public EMSdevice {
static uuid::log::Logger logger_;
void console_commands();
void process_HPMonitor1(std::shared_ptr<const Telegram> telegram);
void process_HPMonitor2(std::shared_ptr<const Telegram> telegram);
};
} // namespace emsesp

View File

@@ -18,7 +18,7 @@
#include "mixing.h"
MAKE_PSTR_WORD(mixing)
// MAKE_PSTR_WORD(mixing)
namespace emsesp {
@@ -28,24 +28,74 @@ 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)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
// telegram handlers
// register_telegram_type(EMS_TYPE_XX, "XX", false, std::bind(&Mixing::process_XX, this, _1));
DEBUG_LOG(F("Registering new Mixing module with device ID 0x%02X"), device_id);
// 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(0x02D8, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1));
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
register_telegram_type(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
register_telegram_type(0x00AB, F("MMStatusMessage"), true, std::bind(&Mixing::process_MMStatusMessage, this, _1));
// MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Mixing::cmd, this, _1));
}
// add context submenu
void Mixing::add_context_menu() {
}
// display all values into the shell console
void Mixing::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // always call this to show header
char buffer[10]; // used for formatting
shell.printfln(F(" Circuit #: %d"), hc_);
print_value(shell, F("Current flow temperature"), F_(degrees), Helpers::render_value(buffer, flowTemp_, 10));
print_value(shell, F("Setpoint flow temperature"), F_(degrees), Helpers::render_value(buffer, flowSetTemp_, 1));
print_value(shell, F("Current pump modulation"), Helpers::render_value(buffer, pumpMod_, 1));
print_value(shell, F("Current valve status"), Helpers::render_value(buffer, valveStatus_, 1));
shell.println();
}
// publish values via MQTT
// 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() {
DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_SMALL);
if (flowTemp_ != EMS_VALUE_USHORT_NOTSET) {
doc["flowTemp"] = (float)flowTemp_ / 10;
}
if (pumpMod_ != EMS_VALUE_UINT_NOTSET) {
doc["pumpMod"] = pumpMod_;
}
if (valveStatus_ != EMS_VALUE_UINT_NOTSET) {
doc["valveStatus_"] = valveStatus_;
}
if (flowSetTemp_ != EMS_VALUE_UINT_NOTSET) {
doc["flowSetTemp_"] = flowSetTemp_;
}
#ifdef EMSESP_DEBUG
DEBUG_LOG(F("[DEBUG] Performing a mixing module publish"));
#endif
char topic[30];
char s[3]; // for formatting strings
strlcpy(topic, "mixing_data", 30);
strlcat(topic, Helpers::itoa(s, hc_), 30); // append hc to topic
Mqtt::publish(topic, doc);
}
// check to see if values have been updated
@@ -57,4 +107,27 @@ bool Mixing::updated_values() {
void Mixing::console_commands() {
}
// 0x02D7, 0x02D8 etc...
void Mixing::process_MMPLUSStatusMessage_HC(std::shared_ptr<const Telegram> telegram) {
hc_ = 0x02D8 - telegram->type_id; // determine which circuit this is
telegram->read_value(flowTemp_, 3); // isd * 10
telegram->read_value(pumpMod_, 5);
telegram->read_value(valveStatus_, 2);
}
// Mixing module warm water loading - 0x0331, 0x0332
void Mixing::process_MMPLUSStatusMessage_WWC(std::shared_ptr<const Telegram> telegram) {
hc_ = 0x0332 - telegram->type_id; // determine which circuit this is. There are max 2.
telegram->read_value(flowTemp_, 0); // isd * 10
telegram->read_value(pumpMod_, 2);
telegram->read_value(valveStatus_, 11);
}
void Mixing::process_MMStatusMessage(std::shared_ptr<const Telegram> telegram) {
hc_ = 1; // fixed
telegram->read_value(flowTemp_, 1); // isd * 10
telegram->read_value(pumpMod_, 3);
telegram->read_value(valveStatus_, 0);
}
} // namespace emsesp

View File

@@ -45,6 +45,17 @@ class Mixing : public EMSdevice {
static uuid::log::Logger logger_;
void console_commands();
void process_MMPLUSStatusMessage_HC(std::shared_ptr<const Telegram> telegram);
void process_MMPLUSStatusMessage_WWC(std::shared_ptr<const Telegram> telegram);
void process_MMStatusMessage(std::shared_ptr<const Telegram> telegram);
private:
uint16_t hc_ = EMS_VALUE_USHORT_NOTSET;
uint16_t flowTemp_ = EMS_VALUE_USHORT_NOTSET;
uint8_t pumpMod_ = EMS_VALUE_UINT_NOTSET;
uint8_t valveStatus_ = EMS_VALUE_UINT_NOTSET;
uint8_t flowSetTemp_ = EMS_VALUE_UINT_NOTSET;
};
} // namespace emsesp

View File

@@ -47,11 +47,9 @@ MAKE_PSTR(logger_name, "mqtt")
namespace emsesp {
// exposing static stuff to compiler/linker
#ifndef EMSESP_STANDALONE
AsyncMqttClient Mqtt::mqttClient_;
#endif
std::vector<Mqtt::MQTTFunction> Mqtt::mqtt_functions_;
bool Mqtt::mqtt_retain_;
uint8_t Mqtt::mqtt_qos_;
@@ -59,6 +57,7 @@ std::string Mqtt::mqtt_hostname_; // copy of hostname
std::string Mqtt::mqtt_base_;
uint16_t Mqtt::mqtt_publish_fails_ = 0;
size_t Mqtt::maximum_mqtt_messages_ = Mqtt::MAX_MQTT_MESSAGES;
bool Mqtt::force_publish_ = false;
uint16_t Mqtt::mqtt_message_id_ = 0;
std::deque<Mqtt::QueuedMqttMessage> Mqtt::mqtt_messages_;
char will_topic_[Mqtt::MQTT_TOPIC_MAX_SIZE]; // because MQTT library keeps only char pointer
@@ -87,20 +86,22 @@ void Mqtt::flush_message_queue() {
}
// restart MQTT services
void Mqtt ::reconnect() {
void Mqtt::reconnect() {
#ifndef EMSESP_STANDALONE
mqttClient_.disconnect();
#endif
DEBUG_LOG(F("Reconnecting..."));
// flush_message_queue(); // clean the queues
// start();
}
// MQTT setup
void Mqtt::start() {
// get some settings and store them locally. This is also because the asyncmqtt library uses references
// exit if already initialized
if (mqtt_start_) {
return;
}
mqtt_start_ = true;
// get some settings and store them locally. This is also because the asyncmqtt library uses references for char *
Settings settings;
mqtt_enabled_ = settings.mqtt_enabled();
mqtt_hostname_ = settings.hostname();
@@ -119,9 +120,7 @@ void Mqtt::start() {
mqtt_enabled_ = false;
}
if (!mqtt_init_) {
init(); // set up call backs. only done once.
}
#ifdef EMSESP_STANDALONE
mqtt_enabled_ = true; // force it on for debugging standalone
@@ -141,17 +140,24 @@ void Mqtt::start() {
mqtt_connecting_ = false;
mqtt_last_connection_ = millis();
mqtt_reconnect_delay_ = Mqtt::MQTT_RECONNECT_DELAY_MIN;
DEBUG_LOG(F("Configuring MQTT service..."));
}
// MQTT init callbacks
// This should only be executed once
void Mqtt::init() {
if (mqtt_init_) {
return;
}
mqtt_init_ = true;
#ifndef EMSESP_STANDALONE
mqttClient_.onConnect([this](bool sessionPresent) { on_connect(); });
mqttClient_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) {
if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {
logger_.err(F("Cannot connect to server"));
logger_.err(F("Disconnected from server"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) {
logger_.err(F("Server identifier Rejected"));
@@ -169,6 +175,7 @@ void Mqtt::init() {
// Reset reconnection delay
mqtt_last_connection_ = millis();
mqtt_connecting_ = false;
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); });
@@ -179,8 +186,6 @@ void Mqtt::init() {
on_message(topic, payload, len);
});
#endif
mqtt_init_ = true;
}
Mqtt::MQTTFunction::MQTTFunction(uint8_t device_id, const std::string && topic, mqtt_function_p mqtt_function)
@@ -192,7 +197,6 @@ Mqtt::MQTTFunction::MQTTFunction(uint8_t device_id, const std::string && topic,
// subscribe to an MQTT topic, and store the associated callback function
void Mqtt::subscribe(const uint8_t device_id, const std::string & topic, mqtt_function_p cb) {
mqtt_functions_.emplace_back(device_id, std::move(topic), cb); // register a call back function for a specific telegram type
queue_subscribe_message(topic); // add subscription to queue
}
@@ -201,7 +205,9 @@ void Mqtt::subscribe(const std::string & topic, mqtt_function_p cb) {
subscribe(0, topic, cb); // no device_id needed, if generic to EMS-ESP
}
// Main loops. Checks for connection, sends out top item on publish queue
// Main MQTT loop
// Checks for connection, establishes a connection if not
// sends out top item on publish queue
void Mqtt::loop() {
// exit if MQTT is not enabled, there is no WIFI or we're still in the MQTT connection process
#ifndef EMSESP_STANDALONE
@@ -214,6 +220,13 @@ void Mqtt::loop() {
// if we're already connected....
if (connected()) {
if (force_publish_) {
force_publish_ = false;
send_heartbeat(); // create a heartbeat payload
EMSESP::publish_all_values(); // add sensors and mqtt to queue
publish_all_queue(); // publish everything on queue
}
// send out heartbeat
uint32_t currentMillis = millis();
if ((currentMillis - last_heartbeat_ > MQTT_HEARTBEAT_INTERVAL)) {
@@ -227,7 +240,7 @@ void Mqtt::loop() {
EMSESP::publish_all_values();
}
// publish top item from MQTT queue - this happens every 200ms to stop flooding
// publish top item from MQTT queue to stop flooding
if ((uint32_t)(currentMillis - last_mqtt_poll_) > MQTT_PUBLISH_WAIT) {
last_mqtt_poll_ = currentMillis;
publish_queue();
@@ -236,24 +249,23 @@ void Mqtt::loop() {
return;
}
// We need to reconnect. Check when was the last time we tried.
if (millis() - mqtt_last_connection_ < mqtt_reconnect_delay_) {
// We need to reconnect. Check when was the last time we tried this
if (mqtt_last_connection_ && (millis() - mqtt_last_connection_ < mqtt_reconnect_delay_)) {
return;
}
mqtt_connecting_ = true; // we're doing a connection
mqtt_connecting_ = true; // we're doing a connection now
// Increase the reconnect delay
// Increase the reconnect delay for next time
mqtt_reconnect_delay_ += MQTT_RECONNECT_DELAY_STEP;
if (mqtt_reconnect_delay_ > MQTT_RECONNECT_DELAY_MAX) {
mqtt_reconnect_delay_ = MQTT_RECONNECT_DELAY_MAX;
}
// Connect to the MQTT broker
logger_.info(F("Connecting to MQTT server..."));
#ifndef EMSESP_STANDALONE
mqttClient_.connect();
start();
logger_.info(F("Connecting to MQTT server..."));
mqttClient_.connect(); // Connect to the MQTT broker
#endif
}
@@ -270,7 +282,7 @@ void Mqtt::show_mqtt(uuid::console::Shell & shell) {
return;
}
shell.printfln(F("MQTT queue (%d items):"), mqtt_messages_.size());
shell.printfln(F("MQTT queue (%d messages):"), mqtt_messages_.size());
for (const auto & message : mqtt_messages_) {
auto content = message.content_;
@@ -400,14 +412,11 @@ char * Mqtt::make_topic(char * result, const std::string & topic) {
// send online appended with the version information as JSON
void Mqtt::send_start_topic() {
Settings settings;
StaticJsonDocument<200> doc; // length is actually about 60
JsonObject payload = doc.to<JsonObject>();
payload["event"] = "start";
payload["version"] = settings.app_version();
StaticJsonDocument<90> doc;
doc["event"] = "start";
doc["version"] = Settings().app_version();
#ifndef EMSESP_STANDALONE
payload["IP"] = WiFi.localIP().toString();
doc["IP"] = WiFi.localIP().toString();
#endif
publish("info", doc, false); // send with retain off
@@ -434,13 +443,12 @@ void Mqtt::send_heartbeat() {
return;
}
StaticJsonDocument<100> doc; // length is 76
JsonObject rootHeartbeat = doc.to<JsonObject>();
StaticJsonDocument<90> doc;
rootHeartbeat["rssid"] = Network::wifi_quality();
rootHeartbeat["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
rootHeartbeat["freemem"] = System::free_mem();
rootHeartbeat["mqttpublishfails"] = mqtt_publish_fails_;
doc["rssid"] = Network::wifi_quality();
doc["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
doc["freemem"] = System::free_mem();
doc["mqttpublishfails"] = mqtt_publish_fails_;
publish("heartbeat", doc, false); // send to MQTT with retain off
}
@@ -529,6 +537,13 @@ void Mqtt::publish(const char * topic, const bool value) {
queue_publish_message(topic, value ? "1" : "0", mqtt_retain_);
}
// publish all queued messages to MQTT
void Mqtt::publish_all_queue() {
while (!mqtt_messages_.empty()) {
publish_queue();
}
}
// take top from queue and try and publish it
// assumes there is an MQTT connection
void Mqtt::publish_queue() {
@@ -576,12 +591,14 @@ void Mqtt::publish_queue() {
if (packet_id == 0) {
// it failed. if we retried n times, give up. remove from queue
if (mqtt_message.retry_count_ == (MQTT_PUBLISH_MAX_RETRY - 1)) {
DEBUG_LOG(F("Failed to publish to %s after %d attemps, payload %s"), full_topic, mqtt_message.retry_count_ + 1, message->payload.c_str());
logger_.err(F("Failed to publish to %s after %d attempts"), full_topic, mqtt_message.retry_count_ + 1);
mqtt_publish_fails_++; // increment failure counter
mqtt_messages_.pop_front(); // delete
return;
} else {
mqtt_messages_.front().retry_count_++;
// logger_.err(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
}
}
@@ -728,11 +745,11 @@ void Mqtt::console_commands() {
});
EMSESPShell::commands->add_command(ShellContext::MQTT,
CommandFlags::ADMIN,
CommandFlags::USER,
flash_string_vector{F_(publish)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
[=](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
shell.printfln(F("Publishing all values to MQTT"));
EMSESP::publish_all_values();
force_publish_ = true;
});
EMSESPShell::commands->add_command(ShellContext::MQTT,

View File

@@ -39,9 +39,9 @@
#include <uuid/log.h>
#define EMSESP_MAX_JSON_SIZE 800 // for json docs from ems devices
#define EMSESP_MAX_JSON_SIZE_SMALL 200 // for smaller json docs from ems devices
#define EMSESP_MAX_JSON_SIZE_MEDIUM 200 // for smaller json docs from ems devices
#define EMSESP_MAX_JSON_SIZE_SMALL 200 // for smaller json docs
#define EMSESP_MAX_JSON_SIZE_MEDIUM 800 // for smaller json docs from ems devices
#define EMSESP_MAX_JSON_SIZE_LARGE 1500 // for large json docs from ems devices, like boiler or thermostat data
namespace emsesp {
@@ -61,7 +61,6 @@ struct MqttMessage {
class Mqtt {
public:
void start();
void loop();
void send_heartbeat();
@@ -108,6 +107,7 @@ class Mqtt {
};
static std::deque<QueuedMqttMessage> mqtt_messages_;
void start();
#ifndef EMSESP_STANDALONE
static AsyncMqttClient mqttClient_;
@@ -120,8 +120,8 @@ class Mqtt {
static uint16_t mqtt_message_id_;
static constexpr uint8_t MQTT_QUEUE_MAX_SIZE = 50;
static constexpr uint32_t MQTT_PUBLISH_WAIT = 200; // delay between sending publishes, although it should be asynchronous!
static constexpr uint8_t MQTT_PUBLISH_MAX_RETRY = 4; // max retries for giving up on publishing
static constexpr uint32_t MQTT_PUBLISH_WAIT = 750; // delay between sending publishes, although it should be asynchronous!
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 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
@@ -140,6 +140,7 @@ class Mqtt {
void on_connect();
static char * make_topic(char * result, const std::string & topic);
void publish_queue();
void publish_all_queue();
void send_start_topic();
static void reconnect();
void init();
@@ -157,27 +158,29 @@ class Mqtt {
};
static std::vector<MQTTFunction> mqtt_functions_; // list of mqtt callbacks for all devices
static std::string mqtt_hostname_;
static std::string mqtt_base_;
static uint8_t mqtt_qos_;
static uint16_t mqtt_publish_fails_;
uint32_t mqtt_last_connection_;
uint32_t mqtt_last_connection_ = 0;
uint32_t mqtt_reconnect_delay_ = MQTT_RECONNECT_DELAY_MIN;
bool mqtt_init_ = false;
bool mqtt_connecting_;
bool mqtt_start_ = false;
bool mqtt_connecting_ = false;
uint16_t mqtt_publish_time_;
uint32_t last_heartbeat_ = 0;
uint32_t last_mqtt_poll_ = 0;
uint32_t last_publish_ = 0;
static bool force_publish_;
// settings
static std::string mqtt_hostname_;
static std::string mqtt_base_;
static uint8_t mqtt_qos_;
std::string mqtt_ip_;
std::string mqtt_user_;
std::string mqtt_password_;
bool mqtt_enabled_;
bool mqtt_enabled_ = true; // start off assuming we want to connect
bool mqtt_heartbeat_;
uint16_t mqtt_port_;
};

View File

@@ -219,25 +219,31 @@ void Sensors::publish_values() {
// if we're not using nested JSON, send each sensor out seperately
// sensor1, sensor2 etc...
// e.g. {"sensor1":{"id":"28-EA41-9497-0E03-5F","temp":"21.81"}}
// e.g. sensor_1 = {"temp":20.2}
if (!mqtt_nestedjson_) {
StaticJsonDocument<100> doc; // length of payload is 60 bytes
JsonObject sensors = doc.to<JsonObject>();
StaticJsonDocument<20> doc;
for (const auto & device : devices_) {
char s[5];
sensors["temp"] = Helpers::render_value(s, device.temperature_c_, 2);
doc["temp"] = Helpers::render_value(s, device.temperature_c_, 2);
char topic[60]; // sensors{1-n}
strlcpy(topic, "sensor_", 50); // create topic
strlcat(topic, device.to_string().c_str(), 50);
Mqtt::publish(topic, doc);
doc.clear(); // clear json doc so we can reuse the buffer again
}
return;
}
// group all sensors together - https://github.com/proddy/EMS-ESP/issues/327
const size_t capacity = JSON_OBJECT_SIZE(num_devices * 60); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/
DynamicJsonDocument doc(capacity);
JsonObject sensors = doc.to<JsonObject>();
// https://arduinojson.org/v6/assistant/
// sensors = {
// "sensor1":{"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"},
// "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);
DynamicJsonDocument doc(100 * num_devices);
uint8_t i = 1;
for (const auto & device : devices_) {
@@ -245,7 +251,7 @@ void Sensors::publish_values() {
strlcpy(sensorID, "sensor", 10);
char s[5];
strlcat(sensorID, Helpers::itoa(s, i++), 10);
JsonObject dataSensor = sensors.createNestedObject(sensorID);
JsonObject dataSensor = doc.createNestedObject(sensorID);
dataSensor["id"] = device.to_string();
dataSensor["temp"] = Helpers::render_value(s, device.temperature_c_, 2);
}

View File

@@ -34,13 +34,13 @@ namespace emsesp {
EMSESP_SETTINGS_SIMPLE(std::string, "", wifi_password, "", (), "") \
EMSESP_SETTINGS_CUSTOM(std::string, "", syslog_host, "", (), "") \
EMSESP_SETTINGS_ENUM(uuid::log::Level, "", syslog_level, "", (), uuid::log::Level::OFF) \
EMSESP_SETTINGS_SIMPLE(unsigned long, "", syslog_mark_interval, "", (), 0) \
EMSESP_SETTINGS_SIMPLE(unsigned long, "", syslog_mark_interval, "", (), EMSESP_DEFAULT_SYSLOG_INTERVAL) \
EMSESP_SETTINGS_SIMPLE(uint8_t, "", ems_bus_id, "", (), EMSESP_DEFAULT_BUS_ID) \
EMSESP_SETTINGS_SIMPLE(uint8_t, "", ems_tx_mode, "", (), EMSESP_DEFAULT_TX_MODE) \
EMSESP_SETTINGS_SIMPLE(bool, "", ems_read_only, "", (), false) \
EMSESP_SETTINGS_SIMPLE(bool, "", shower_timer, "", (), false) \
EMSESP_SETTINGS_SIMPLE(bool, "", shower_alert, "", (), false) \
EMSESP_SETTINGS_SIMPLE(uint8_t, "", master_thermostat, "", (), EMSESP_DEFAULT_NOTSET) \
EMSESP_SETTINGS_SIMPLE(bool, "", ems_read_only, "", (), EMSESP_DEFAULT_EMS_READ_ONLY) \
EMSESP_SETTINGS_SIMPLE(bool, "", shower_timer, "", (), EMSESP_DEFAULT_SHOWER_TIMER) \
EMSESP_SETTINGS_SIMPLE(bool, "", shower_alert, "", (), EMSESP_DEFAULT_SHOWER_ALERT) \
EMSESP_SETTINGS_SIMPLE(uint8_t, "", master_thermostat, "", (), EMSESP_DEFAULT_MASTER_THERMOSTAT) \
EMSESP_SETTINGS_SIMPLE(uint16_t, "", mqtt_publish_time, "", (), EMSESP_DEFAULT_MQTT_PUBLISH_TIME) \
EMSESP_SETTINGS_SIMPLE(std::string, "", mqtt_ip, "", (), "") \
EMSESP_SETTINGS_SIMPLE(std::string, "", mqtt_user, "", (), "") \

View File

@@ -41,13 +41,11 @@
#include "version.h"
#include "console.h"
// defaults
#define EMSESP_DEFAULT_HOSTNAME "ems-esp2" // TODO rename to ems-esp
// default settings - these can be customized from within the application
#define EMSESP_DEFAULT_HOSTNAME "ems-esp"
#define EMSESP_DEFAULT_ADMIN_PASSWORD "neo"
#define EMSESP_DEFAULT_BUS_ID 0x0B
#define EMSESP_DEFAULT_TX_MODE 1
#define EMSESP_DEFAULT_NOTSET 0
#define EMSESP_DEFAULT_MQTT_ENABLED true
#define EMSESP_DEFAULT_MQTT_BASE "home"
#define EMSESP_DEFAULT_MQTT_PORT 1883
@@ -55,6 +53,11 @@
#define EMSESP_DEFAULT_MQTT_RETAIN false
#define EMSESP_DEFAULT_MQTT_NESTEDJSON true
#define EMSESP_DEFAULT_MQTT_HEARTBEAT true
#define EMSESP_DEFAULT_EMS_READ_ONLY false
#define EMSESP_DEFAULT_SHOWER_TIMER false
#define EMSESP_DEFAULT_SHOWER_ALERT false
#define EMSESP_DEFAULT_SYSLOG_INTERVAL 0
#define EMSESP_DEFAULT_MASTER_THERMOSTAT 0 // not set
#ifndef EMSESP_STANDALONE
#define EMSESP_DEFAULT_MQTT_PUBLISH_TIME 10

View File

@@ -115,10 +115,9 @@ void Shower::shower_alert_start() {
// Publish shower data
// returns true if added to MQTT queue went ok
void Shower::publish_values() {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
JsonObject rootShower = doc.to<JsonObject>();
rootShower["shower_timer"] = shower_timer_ ? "1" : "0";
rootShower["shower_alert"] = shower_alert_ ? "1" : "0";
StaticJsonDocument<90> doc;
doc["shower_timer"] = shower_timer_ ? "1" : "0";
doc["shower_alert"] = shower_alert_ ? "1" : "0";
// only publish shower duration if there is a value
char s[50];
@@ -126,9 +125,9 @@ void Shower::publish_values() {
char buffer[16] = {0};
strlcpy(s, Helpers::itoa(buffer, (uint8_t)((duration_ / (1000 * 60)) % 60), 10), 50);
strlcat(s, " minutes and ", 50);
strlcat(s, Helpers::itoa(buffer,(uint8_t)((duration_ / 1000) % 60), 10), 50);
strlcat(s, Helpers::itoa(buffer, (uint8_t)((duration_ / 1000) % 60), 10), 50);
strlcat(s, " seconds", 50);
rootShower["duration"] = s;
doc["duration"] = s;
}
Mqtt::publish("shower_data", doc);

View File

@@ -18,7 +18,9 @@
#include "solar.h"
MAKE_PSTR_WORD(solar)
// MAKE_PSTR_WORD(solar)
MAKE_PSTR(kwh, "kWh")
MAKE_PSTR(wh, "Wh")
namespace emsesp {
@@ -28,24 +30,91 @@ 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)
: 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);
// telegram handlers
// register_telegram_type(EMS_TYPE_XX, "XX", false, std::bind(&Solar::process_XX, this, _1));
register_telegram_type(0x0097, F("SM10Monitor"), true, std::bind(&Solar::process_SM10Monitor, this, _1));
register_telegram_type(0x0362, F("SM100Monitor"), true, std::bind(&Solar::process_SM100Monitor, this, _1));
register_telegram_type(0x0364, F("SM100Status"), false, std::bind(&Solar::process_SM100Status, this, _1));
register_telegram_type(0x036A, F("SM100Status2"), false, std::bind(&Solar::process_SM100Status2, this, _1));
register_telegram_type(0x038E, F("SM100Energy"), false, std::bind(&Solar::process_SM100Energy, this, _1));
register_telegram_type(0x0003, F("ISM1StatusMessage"), true, std::bind(&Solar::process_ISM1StatusMessage, this, _1));
register_telegram_type(0x0001, F("ISM1Set"), false, std::bind(&Solar::process_ISM1Set, this, _1));
// MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Solar::cmd, this, _1));
}
// context submenu
void Solar::add_context_menu() {
}
// display all values into the shell console
void Solar::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // always call this to show header
char buffer[10]; // used for formatting
print_value(shell, F("Collector temperature (TS1)"), F_(degrees), Helpers::render_value(buffer, collectorTemp_, 10));
print_value(shell, F("Bottom temperature (TS2)"), F_(degrees), Helpers::render_value(buffer, bottomTemp_, 10));
print_value(shell, F("Bottom temperature (TS5)"), F_(degrees), Helpers::render_value(buffer, bottomTemp2_, 10));
print_value(shell, F("Pump modulation"), F_(percent), Helpers::render_value(buffer, pumpModulation_, 1));
print_value(shell, F("Valve (VS2) status"), Helpers::render_value(buffer, valveStatus_, EMS_VALUE_BOOL));
print_value(shell, F("Pump (PS1) active"), Helpers::render_value(buffer, pump_, EMS_VALUE_BOOL));
if (pumpWorkMin_ != EMS_VALUE_ULONG_NOTSET) {
shell.printfln(F(" Pump working time: %d days %d hours %d minutes"), pumpWorkMin_ / 1440, (pumpWorkMin_ % 1440) / 60, pumpWorkMin_ % 60);
}
print_value(shell, F("Energy last hour"), F_(wh), Helpers::render_value(buffer, energyLastHour_, 10));
print_value(shell, F("Energy today"), F_(wh), Helpers::render_value(buffer, energyToday_, 0)); // no division
print_value(shell, F("Energy total"), F_(kwh), Helpers::render_value(buffer, energyTotal_, 10));
shell.println();
}
// publish values via MQTT
void Solar::publish_values() {
DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_MEDIUM);
char s[10]; // for formatting strings
if (collectorTemp_ != EMS_VALUE_SHORT_NOTSET) {
doc["collectortemp"] = (float)collectorTemp_ / 10;
}
if (bottomTemp_ != EMS_VALUE_SHORT_NOTSET) {
doc["bottomtemp"] = (float)bottomTemp_ / 10;
}
if (bottomTemp2_ != EMS_VALUE_SHORT_NOTSET) {
doc["bottomtemp2"] = (float)bottomTemp2_ / 10;
}
if (pumpModulation_ != EMS_VALUE_INT_NOTSET) {
doc["pumpmodulation"] = pumpModulation_;
}
if (pump_ != EMS_VALUE_BOOL_NOTSET) {
doc["pump"] = Helpers::render_value(s, pump_, EMS_VALUE_BOOL);
}
if (valveStatus_ != EMS_VALUE_BOOL_NOTSET) {
doc["valvestatus"] = Helpers::render_value(s, valveStatus_, EMS_VALUE_BOOL);
}
if (pumpWorkMin_ != EMS_VALUE_ULONG_NOTSET) {
doc["pumpWorkMin"] = (float)pumpWorkMin_;
}
if (energyLastHour_ != EMS_VALUE_ULONG_NOTSET) {
doc["energylasthour"] = (float)energyLastHour_ / 10;
}
if (energyToday_ != EMS_VALUE_ULONG_NOTSET) {
doc["energytoday"] = energyToday_;
}
if (energyTotal_ != EMS_VALUE_ULONG_NOTSET) {
doc["energytotal"] = (float)energyTotal_ / 10;
}
#ifdef EMSESP_DEBUG
DEBUG_LOG(F("[DEBUG] Performing a solar module publish"));
#endif
Mqtt::publish("sm_data", doc);
}
// check to see if values have been updated
@@ -57,4 +126,78 @@ bool Solar::updated_values() {
void Solar::console_commands() {
}
// SM10Monitor - type 0x97
void Solar::process_SM10Monitor(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(collectorTemp_, 2); // collector temp from SM10, is *10
telegram->read_value(bottomTemp_, 5); // bottom temp from SM10, is *10
telegram->read_value(pumpModulation_, 4); // modulation solar pump
telegram->read_value(pump_, 7, 1);
}
/*
* SM100Monitor - type 0x0162 EMS+ - for SM100 and SM200
* e.g. 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
* e.g, 30 00 FF 00 02 62 01 AC
* 30 00 FF 18 02 62 80 00
* 30 00 FF 00 02 62 01 A1 - for bottom temps
* bytes 0+1 = TS1 Temperature sensor for collector
* bytes 2+3 = TS2 Temperature sensor bottom cylinder 1
* bytes 16+17 = TS5 Temperature sensor bottom cylinder 2
*/
void Solar::process_SM100Monitor(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(collectorTemp_, 0); // is *10
telegram->read_value(bottomTemp_, 2); // is *10
telegram->read_value(bottomTemp2_, 16); // is *10
}
/*
* SM100Status - type 0x0264 EMS+ for pump modulation - for SM100 and SM200
* e.g. 30 00 FF 09 02 64 64 = 100%
* 30 00 FF 09 02 64 1E = 30%
*/
void Solar::process_SM100Status(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(pumpModulation_, 9);
}
/*
* SM100Status2 - type 0x026A EMS+ for pump on/off at offset 0x0A - for SM100 and SM200
* e.g. B0 00 FF 00 02 6A 03 03 03 03 01 03 03 03 03 03 01 03
* byte 4 = VS2 3-way valve for cylinder 2 : test=01, on=04 and off=03
* byte 10 = PS1 Solar circuit pump for collector array 1: test=01, on=04 and off=03
*/
void Solar::process_SM100Status2(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(valveStatus_, 4, 2); // on if bit 2 set
telegram->read_value(pump_, 10, 2); // on if bit 2 set
}
/*
* SM100Energy - type 0x028E EMS+ for energy readings
* e.g. 30 00 FF 00 02 8E 00 00 00 00 00 00 06 C5 00 00 76 35
*/
void Solar::process_SM100Energy(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(energyLastHour_, 0); // last hour / 10 in Wh
telegram->read_value(energyToday_, 4); // todays in Wh
telegram->read_value(energyTotal_, 8); // total / 10 in kWh
}
/*
* Junkers ISM1 Solar Module - type 0x0003 EMS+ for energy readings
* e.g. B0 00 FF 00 00 03 32 00 00 00 00 13 00 D6 00 00 00 FB D0 F0
*/
void Solar::process_ISM1StatusMessage(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(collectorTemp_, 4); // Collector Temperature
telegram->read_value(bottomTemp_, 6); // Temperature Bottom of Solar Boiler
telegram->read_value(energyLastHour_, 2); // Solar Energy produced in last hour - is * 10 and handled in ems-esp.cpp
telegram->read_value(pump_, 8, 0); // Solar pump on (1) or off (0)
telegram->read_value(pumpWorkMin_, 10);
}
/*
* Junkers ISM1 Solar Module - type 0x0001 EMS+ for setting values
* e.g. 90 30 FF 06 00 01 50
*/
void Solar::process_ISM1Set(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(setpoint_maxBottomTemp_, 6);
}
} // namespace emsesp

View File

@@ -45,6 +45,26 @@ class Solar : public EMSdevice {
static uuid::log::Logger logger_;
void console_commands();
int16_t collectorTemp_ = EMS_VALUE_SHORT_NOTSET; // collector temp (TS1)
int16_t bottomTemp_ = EMS_VALUE_SHORT_NOTSET; // bottom temp (TS2)
int16_t bottomTemp2_ = EMS_VALUE_SHORT_NOTSET; // bottom temp cylinder 2 (TS5)
uint8_t pumpModulation_ = EMS_VALUE_UINT_NOTSET; // modulation solar pump
uint8_t pump_ = EMS_VALUE_BOOL_NOTSET; // pump active
uint8_t valveStatus_ = EMS_VALUE_BOOL_NOTSET; // valve status (VS2)
int16_t setpoint_maxBottomTemp_ = EMS_VALUE_SHORT_NOTSET; // setpoint for maximum collector temp
uint32_t energyLastHour_ = EMS_VALUE_ULONG_NOTSET;
uint32_t energyToday_ = EMS_VALUE_ULONG_NOTSET;
uint32_t energyTotal_ = EMS_VALUE_ULONG_NOTSET;
uint32_t pumpWorkMin_ = EMS_VALUE_ULONG_NOTSET; // Total solar pump operating time
void process_SM10Monitor(std::shared_ptr<const Telegram> telegram);
void process_SM100Monitor(std::shared_ptr<const Telegram> telegram);
void process_SM100Status(std::shared_ptr<const Telegram> telegram);
void process_SM100Status2(std::shared_ptr<const Telegram> telegram);
void process_SM100Energy(std::shared_ptr<const Telegram> telegram);
void process_ISM1StatusMessage(std::shared_ptr<const Telegram> telegram);
void process_ISM1Set(std::shared_ptr<const Telegram> telegram);
};
} // namespace emsesp

View File

@@ -58,7 +58,6 @@ int System::reset_counter_;
// handle generic system related MQTT commands
void System::mqtt_commands(const char * message) {
// convert JSON and get the command
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message);
if (error) {

View File

@@ -602,4 +602,9 @@ void TxService::post_send_query() {
}
}
// returns details of the last Tx message that was sent (for debugging)
std::string TxService::last_tx_to_string() const {
return Helpers::data_to_hex(telegram_last_, telegram_last_length_);
}
} // namespace emsesp

View File

@@ -82,7 +82,6 @@ class Telegram {
void read_value8(int16_t & param, const uint8_t index) const;
void read_value(int8_t & param, const uint8_t index) const;
private:
int8_t _getDataPosition(const uint8_t index) const;
};
@@ -288,6 +287,9 @@ class TxService : public EMSbus {
return tx_telegrams_;
}
std::string last_tx_to_string() const;
private:
static constexpr uint8_t MAXIMUM_TX_RETRIES = 3;

View File

@@ -37,6 +37,28 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
return;
}
if (command == "solar") {
shell.printfln(F("Testing Solar"));
rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS);
std::string version("1.2.3");
add_device(0x30, 163, version, EMSdevice::Brand::BUDERUS); // SM100
rxservice_.loop();
// 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
uint8_t s1[] = {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};
rxservice_.add(s1, sizeof(s1));
rxservice_.loop();
shell.loop_all();
return;
}
if (command == "cr100") {
shell.printfln(F("Testing CR100"));
@@ -259,7 +281,7 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
strcpy(payload, "{\"cmd\":\"temp\",\"hc\":2,\"data\":22}");
mqtt_.incoming(topic, payload);
strcpy(topic, "home/ems-esp2/cmd");
strcpy(topic, "home/ems-esp/cmd");
strcpy(payload, "restart");
mqtt_.incoming(topic, payload);

View File

@@ -118,7 +118,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i
// if we're on auto mode, register this first one we find as we may find multiple
// or if its the master thermostat we defined
if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_NOTSET)) || (master_thermostat == device_id)) {
if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT)) || (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);
@@ -150,7 +150,6 @@ void Thermostat::add_context_menu() {
// general MQTT command for controlling thermostat
// e.g. { "cmd":"daytemp2", "data": 20 }
void Thermostat::thermostat_cmd(const char * message) {
// convert JSON and get the command
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message);
if (error) {
@@ -296,18 +295,22 @@ void Thermostat::publish_values() {
// optional, add external temp
if (flags == EMS_DEVICE_FLAG_RC35) {
if (dampedoutdoortemp != EMS_VALUE_INT_NOTSET) {
rootThermostat["dampedtemp"] = dampedoutdoortemp;
doc["dampedtemp"] = dampedoutdoortemp;
}
if (tempsensor1 != EMS_VALUE_USHORT_NOTSET) {
rootThermostat["tempsensor1"] = (float)tempsensor1 / 10;
doc["tempsensor1"] = (float)tempsensor1 / 10;
}
if (tempsensor2 != EMS_VALUE_USHORT_NOTSET) {
rootThermostat["tempsensor1"] = (float)tempsensor2 / 10;
doc["tempsensor1"] = (float)tempsensor2 / 10;
}
}
// go through all the heating circuits
for (const auto & hc : heating_circuits_) {
// only send if we have an actual setpoint temp temperature values
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
}
has_data = true;
if (mqtt_nested_json_) {
// create nested json for each HC
@@ -367,8 +370,12 @@ void Thermostat::publish_values() {
dataThermostat["designtemp"] = hc->designtemp;
}
if (hc->mode != EMS_VALUE_UINT_NOTSET) {
dataThermostat["mode"] = mode_tostring(hc->get_mode(flags));
}
if (hc->mode_type != EMS_VALUE_UINT_NOTSET) {
dataThermostat["modetype"] = mode_tostring(hc->get_mode_type(flags));
}
// if its not nested, send immediately
if (!mqtt_nested_json_) {
@@ -376,18 +383,14 @@ void Thermostat::publish_values() {
char s[3]; // for formatting strings
strlcpy(topic, "thermostat_data", 30);
strlcat(topic, Helpers::itoa(s, hc->hc_num()), 30); // append hc to topic
char data[EMSESP_MAX_JSON_SIZE];
serializeJson(doc, data);
Mqtt::publish(topic, data);
Mqtt::publish(topic, doc);
return;
}
}
// if we're using nested json, send all in one go
if (mqtt_nested_json_ && has_data) {
char data[EMSESP_MAX_JSON_SIZE];
serializeJson(doc, data);
Mqtt::publish("thermostat_data", data);
Mqtt::publish("thermostat_data", doc);
}
}
@@ -1164,7 +1167,7 @@ void Thermostat::console_commands() {
[](Shell & shell, const std::vector<std::string> & arguments) {
uint8_t value;
if (arguments.empty()) {
value = EMSESP_DEFAULT_NOTSET;
value = EMSESP_DEFAULT_MASTER_THERMOSTAT;
} else {
value = Helpers::hextoint(arguments.front().c_str());
}

View File

@@ -37,17 +37,15 @@ uint8_t tx_mode_ = EMS_TXMODE_DEFAULT;
// Important: must not use ICACHE_FLASH_ATTR
//
void ICACHE_RAM_ATTR EMSuart::emsuart_rx_intr_handler(void * para) {
static uint8_t length;
static uint8_t length = 0;
static bool rx_idle_ = true;
static uint8_t uart_buffer[EMS_MAXBUFFERSIZE + 2];
// TODO check if need UART Rx idle/busy
/*
// is a new buffer? if so init the thing for a new telegram
if (EMS_Sys_Status.emsRxStatus == EMS_RX_STATUS_IDLE) {
EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_BUSY; // status set to busy
if (rx_idle_) {
rx_idle_ = false; // status set to busy
length = 0;
}
*/
// fill IRQ buffer, by emptying Rx FIFO
if (USIS(EMSUART_UART) & ((1 << UIFF) | (1 << UITO) | (1 << UIBD))) {
@@ -68,8 +66,7 @@ void ICACHE_RAM_ATTR EMSuart::emsuart_rx_intr_handler(void * para) {
pEMSRxBuf->length = (length > EMS_MAXBUFFERSIZE) ? EMS_MAXBUFFERSIZE : length;
os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, pEMSRxBuf->length); // copy data into transfer buffer, including the BRK 0x00 at the end
length = 0;
// EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; // TODO check set the status flag stating BRK has been received and we can start a new package
rx_idle_ = true; // check set the status flag stating BRK has been received and we can start a new package
ETS_UART_INTR_ENABLE(); // re-enable UART interrupts
system_os_post(EMSUART_recvTaskPrio, 0, 0); // call emsuart_recvTask() at next opportunity

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "2.0.0a1"
#define EMSESP_APP_VERSION "2.0.0a2"