diff --git a/README.md b/README.md index 07c07aa9c..d06d27996 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Use the telnet client to inform you of all activity and errors real-time. This i ![Telnet](doc/telnet/telnet_example.jpg) -If you type 'v 2' and Enter, it will toggle verbose logging showing you more detailed messages. I use ANSI colors with white text for info messages, green for well formatted telegram packages (which have validated CRC checks), red for corrupt packages and yellow for send responses. +If you type 'v 3' and Enter, it will toggle verbose logging showing you more detailed messages. I use ANSI colors with white text for info messages, green for well formatted telegram packages (which have validated CRC checks), red for corrupt packages and yellow for send responses. ![Telnet](doc/telnet/telnet_verbose.PNG) @@ -81,15 +81,16 @@ To see the current values of the Boiler and its parameters type 's' and hit Ente Commands can be issued directly to the EMS bus typing in a letter followed by an optional parameter and pressing Enter. Supported commands are: -- **r** to send a read command to all devices to fetch values. The 2nd parameter is the type. For example 'r 33' will request type UBAParameterWW and bring back the Warm Water temperatures (not the heating) from the Boiler. You can issue any type here. -- **t** set the thermostat temperature to the given celsius value +- **r** to send a read command to the boiler. The 2nd parameter is the type. For example 'b 33' will request type UBAParameterWW and bring back the Warm Water temperatures from the Boiler. +- **t** is similar, but to send a read command to the thermostat. +- **T** set the thermostat temperature to the given celsius value - **w** to adjust the temperature of the warm water from the boiler - **a** to turn the warm water on and off - **h** to list all the recognized EMS types -- **p** to toggle the Polling response on/off. It's not necessary to have Polling enabled to work. I use this for debugging purposes. -- **m** to set the thermostat mode to manual or auto. -- **T** to toggle thermostat readings on/off +- **p** to toggle the Polling response on/off (note it's not necessary to have Polling enabled to work) +- **m** to set the thermostat mode to manual or auto - **S** to toggle the Shower Timer functionality on/off +- **A** to toggle the Shower Timer Alert functionality on/off **Disclaimer: be careful when sending values to the boiler. If in doubt you can always reset the boiler to its original factory settings by following the instructions in the user guide. On my **Nefit Trendline HRC30** that is done by holding down the Home and Menu buttons simultaneously for a few seconds, selecting factory settings from the scroll menu and lastly pressing the Reset button.** @@ -144,7 +145,7 @@ Each device has a unique ID. The Boiler has an ID of 0x08 (type MC10) and also referred to as the Bus Master or UBA. -My thermostat, which is a* Moduline 300* uses the RC20 protocol and has an ID 0x17. If you're using an RC30 or RC35 type thermostat such as the newer Moduline 300s or 400s use 0x10 and make adjustments in the code as appropriate. bbqkees did a nice write-up on his github page [here](https://github.com/bbqkees/Nefit-Buderus-EMS-bus-Arduino-Domoticz/blob/master/README.md). +My thermostat, which is a* Moduline 300* uses the RC30 protocol and has an ID 0x17. If you're using a RC35 type thermostat such as the newer Moduline 300s or 400s use 0x10 and make adjustments in the code as appropriate. bbqkees did a nice write-up on his github page [here](https://github.com/bbqkees/Nefit-Buderus-EMS-bus-Arduino-Domoticz/blob/master/README.md). Our circuit acts as a service key and thus uses an ID 0x0B. This ID is reserved for special devices intended for installation engineers for maintenance work. @@ -183,7 +184,7 @@ Refer to the code in `ems.cpp` for further explanation on how to parse these mes Telegram packets can only be sent after the Boiler sends a poll to the sending device. The response can be a read command to request data or a write command to send data. At the end of the transmission a poll response is sent from the client (` `) to say we're all done and free up the bus for other clients. -When doing a request to read data the `[src]` is our device (0x0B) and the `[dest]` has it's 7-bit set. Say we were requesting data from the thermostat we would use `[dest] = 0x97` since RC20 has an ID of 0x17. +When doing a request to read data the `[src]` is our device (0x0B) and the `[dest]` has it's 7-bit set. Say we were requesting data from the thermostat we would use `[dest] = 0x97` since RC30 has an ID of 0x17. When doing a write request, the 7th bit is masked in the `[dest]`. After this write request the destination device will send either a single byte 0x01 for success or 0x04 for fail. @@ -218,23 +219,26 @@ The code is built on the Arduino framework and is dependent on these external li | Boiler (0x08) | 0x14 | UBATotalUptimeMessage | | | Boiler (0x08) | 0x15 | UBAMaintenanceSettingsMessage | | | Boiler (0x08) | 0x16 | UBAParametersMessage | | -| Thermostat (0x17) | 0xA8 | RC20Temperature | sets operating modes | +| Thermostat (0x17) | 0xA8 | RC20Temperature | sets operating modes for a RC20 & RC30 | | Thermostat (0x17) | 0x02 | Version | reads Version major/minor | +| Thermostat (0x18) | 0x0A | EasyTemperature | thermostat monitor for an TC100/Easy | In `boiler.ino` you can make calls to automatically send these read commands. See the function *regularUpdates()* #### Supporting other Thermostats types -The code is originally designed for a Moduline300 (RC20) thermostat. +The code is originally designed for a Moduline300 (RC30) thermostat. To adjust for a RC35 first change `EMS_ID_THERMOSTAT` in `ems.cpp`. A RC35 thermostat has 4 heating circuits and to read the values use different Monitor type IDs (e.g. 0x3E, 0x48, etc). The mode (0=night, 1=day, 2=holiday) is the first byte of the telegram and the temperature is the value of the 2nd byte divided by 2. Then to set temperature values use the Working Mode with type IDs (0x3D, 0x47,0x51 and 0x5B) respectively. Set the offset (byte 4 of the header) to determine which temperature you're changing; 1 for night, 2 for day and 3 for holiday. The data value is the desired temperature multiplied by 2 as a single byte. -I will add further support for the other thermostats (such as the Nefit Easy) as soon as I can get my hands on a physical device. I do however welcome contribtions to this code repository which is essentially the purpose of GitHub. By inspecting the telegram packets and looking up the codes in the German wiki (and with lots of trial and error) it is possible to easily extend the existing functions to support other EMS devices. +There is limited support for an Nefit Easy TC100) thermostat. The current room temperature and setpoint temperature can be read. What still needs fixing is +- reading the mode (manual vs clock) +- setting the temperature ### Customizing The Code - To configure for your thermostat and specific boiler settings, modify `my_config.h`. Here you can - - set the thermostat type. The default ID is 0x17 for an RC20 Moduline 300. + - set the thermostat type. The default ID is 0x17 for an RC30 Moduline 300. - set flags for enabled/disabling functionality such as `BOILER_THERMOSTAT_ENABLED`, `BOILER_SHOWER_ENABLED` and `BOILER_SHOWER_TIMER`. - Set WIFI and MQTT settings, instead of doing this in `platformio.ini` - To add new handlers for EMS data types, first create a callback function and add to the `EMS_Types` array at the top of the file `ems.cpp` and modify `ems.h` diff --git a/doc/telnet/telnet_example.jpg b/doc/telnet/telnet_example.jpg index d745155b4..afaaf4a5d 100644 Binary files a/doc/telnet/telnet_example.jpg and b/doc/telnet/telnet_example.jpg differ diff --git a/doc/telnet/telnet_stats.PNG b/doc/telnet/telnet_stats.PNG index 1feaa582c..f37c1f6ff 100644 Binary files a/doc/telnet/telnet_stats.PNG and b/doc/telnet/telnet_stats.PNG differ diff --git a/doc/telnet/telnet_verbose.PNG b/doc/telnet/telnet_verbose.PNG index 0a9d5a698..4a8099155 100644 Binary files a/doc/telnet/telnet_verbose.PNG and b/doc/telnet/telnet_verbose.PNG differ diff --git a/espurna/boiler-espurna.ino b/espurna/boiler-espurna.ino index 164050179..95e073e7e 100644 --- a/espurna/boiler-espurna.ino +++ b/espurna/boiler-espurna.ino @@ -455,7 +455,7 @@ void _boilerLoop() { last_boilersend = millis(); // get 0x33 WW values manually - ems_doReadCommand(EMS_TYPE_UBAParameterWW); + ems_doReadCommand(EMS_TYPE_UBAParameterWW, EMS_ID_BOILER); #if MQTT_SUPPORT // send MQTT diff --git a/src/ESPHelper.cpp b/src/ESPHelper.cpp index 810718806..b73ff1338 100644 --- a/src/ESPHelper.cpp +++ b/src/ESPHelper.cpp @@ -666,7 +666,7 @@ void ESPHelper::consoleHandle() { // Set callback of sketch function to process project messages void ESPHelper::consoleSetCallBackProjectCmds(command_t * cmds, uint8_t count, void (*callback)()) { _helpProjectCmds = cmds; // command list - _helpProjectCmds_count = count; // numiber of commands + _helpProjectCmds_count = count; // number of commands _consoleCallbackProjectCmds = callback; // external function to handle commands } @@ -742,26 +742,29 @@ size_t ESPHelper::write(uint8_t character) { // Show help of commands void ESPHelper::consoleShowHelp() { - String help = "********************************\n\r* Remote Telnet Command Center " - "*\n\r********************************\n\r"; + String help = "**********************************************\n\r* Remote Telnet Command Center & Log Monitor " + "*\n\r**********************************************\n\r"; help += "* Device hostname: " + WiFi.hostname() + "\tIP: " + WiFi.localIP().toString() + "\tMAC address: " + WiFi.macAddress() + "\n\r"; help += "* Connected to WiFi AP: " + WiFi.SSID() + "\n\r"; help += "* Boot time: "; help.concat(_boottime); - help += "\n\r* Free Heap RAM: "; + help += "\n\r* Free RAM: "; help.concat(ESP.getFreeHeap()); help += " bytes\n\r"; - help += "*\n\r* Commands:\n\r* ?=this help, q=quit telnet, $=show used memory, !=reboot, &=suspend all " + help += "*\n\r* Commands:\n\r* ?=this help, q=quit telnet, $=show free memory, !=reboot, &=suspend all " "notifications\n\r"; + char s[100]; + // print custom commands if available if (_consoleCallbackProjectCmds) { for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { - //for (uint8_t i = 0; i < 5; i++) { help += FPSTR("* "); help += FPSTR(_helpProjectCmds[i].key); - help += FPSTR(" "); + for (int j = 0; j < (8 - strlen(_helpProjectCmds[i].key)); j++) { // padding + help += FPSTR(" "); + } help += FPSTR(_helpProjectCmds[i].description); help += FPSTR("\n\r"); } @@ -812,7 +815,7 @@ void ESPHelper::consoleProcessCommand() { telnetClient.println("* Closing telnet connection..."); telnetClient.stop(); } else if (cmd == '$') { - telnetClient.print("* Free Heap RAM (bytes): "); + telnetClient.print("* Free RAM (bytes): "); telnetClient.println(ESP.getFreeHeap()); } else if (cmd == '!') { resetESP(); diff --git a/src/ESPHelper.h b/src/ESPHelper.h index 95cd2aadf..cf079779c 100644 --- a/src/ESPHelper.h +++ b/src/ESPHelper.h @@ -74,7 +74,7 @@ typedef struct { } subscription; typedef struct { - char key[5]; + char key[10]; char description[400]; } command_t; diff --git a/src/boiler.ino b/src/boiler.ino index 797887510..0ef7fef55 100644 --- a/src/boiler.ino +++ b/src/boiler.ino @@ -21,18 +21,28 @@ // timers, all values are in seconds #define PUBLISHVALUES_TIME 300 // every 5 mins post HA values -#define SYSTEMCHECK_TIME 10 // every 10 seconds check if Boiler is online and execute other requests -#define REGULARUPDATES_TIME 60 // every minute a call is made, so for our 2 calls theres a write cmd every 30seconds -#define HEARTBEAT_TIME 1 // every second blink heartbeat LED -#define MAX_MANUAL_CALLS 2 // number of ems reads we do during the fetch cycle (in regularUpdates) +Ticker publishValuesTimer; + +#define SYSTEMCHECK_TIME 10 // every 10 seconds check if Boiler is online and execute other requests +Ticker systemCheckTimer; + +#define REGULARUPDATES_TIME 60 // every minute a call is made, so for our 2 calls theres a write cmd every 30seconds +Ticker regularUpdatesTimer; + +#define HEARTBEAT_TIME 1 // every second blink heartbeat LED +Ticker heartbeatTimer; + +// thermostat scan - for debugging +Ticker scanThermostat; +#define SCANTHERMOSTAT_TIME 4 +uint8_t scanThermostat_count; -Ticker publishValuesTimer; -Ticker systemCheckTimer; -Ticker regularUpdatesTimer; -Ticker heartbeatTimer; Ticker showerColdShotStopTimer; uint8_t regularUpdatesCount = 0; +#define MAX_MANUAL_CALLS 2 // number of ems reads we do during the fetch cycle (in regularUpdates) + + // GPIOs #define LED_HEARTBEAT LED_BUILTIN // onboard LED @@ -108,20 +118,24 @@ netInfo homeNet = {.mqttHost = MQTT_IP, ESPHelper myESP(&homeNet); -command_t PROGMEM project_cmds[] = {{"s", "show statistics"}, - {"h", "list EMS telegram type ids with supported logic"}, - {"P", "publish all stat to MQTT"}, - {"v", "[n] set logging (0=none, 1=basic, 2=verbose)"}, - {"p", "toggle EMS Poll response on/off"}, - {"T", "toggle Thermostat monitoring on/off"}, - {"S", "toggle Shower timer on/off"}, - {"A", "toggle shower Alert on/off"}, - {"r", "[n] send EMS request (n=any telegram type id. Use 'h' for suppported types)"}, - {"t", "[n] set thermostat temperature"}, - {"m", "[n] set thermostat mode (1=manual, 2=auto)"}, - {"w", "[n] set boiler warm water temperature (min 30)"}, - {"a", "[n] boiler warm water (1=on, 2=off)"}, - {"x", "[n] experimental (warning: for debugging only!)"}}; +command_t PROGMEM project_cmds[] = { + + {"v [n]", "set logging (0=none, 1=basic, 2=thermostat only, 3=verbose)"}, + {"s", "show statistics"}, + {"h", "list supported EMS telegram type IDs"}, + {"P", "publish all stat to MQTT"}, + {"p", "toggle EMS Poll response on/off"}, + {"S", "toggle Shower timer on/off"}, + {"A", "toggle shower Alert on/off"}, + {"b [xx]", "boiler request (xx=telegram type ID)"}, + {"w [nn]", "set boiler warm water temperature (min 30)"}, + {"a [n]", "boiler warm water (1=on, 2=off)"}, + {"t [xx]", "thermostat request (xx=telegram type ID)"}, + {"T [xx]", "set thermostat temperature"}, + {"m [n]", "set thermostat mode (1=manual, 2=auto)"}, + {"x [xx]", "experimental code for debugging."} + +}; // calculates size of an 2d array at compile time template @@ -244,12 +258,14 @@ void showInfo() { // General stats from EMS bus myDebug("%sEMS-ESP-Boiler system stats:%s\n", COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug(" System Logging is set to "); + myDebug(" System logging is set to "); _EMS_SYS_LOGGING sysLog = ems_getLogging(); if (sysLog == EMS_SYS_LOGGING_BASIC) { myDebug("Basic"); } else if (sysLog == EMS_SYS_LOGGING_VERBOSE) { myDebug("Verbose"); + } else if (sysLog == EMS_SYS_LOGGING_THERMOSTAT) { + myDebug("Thermostat only"); } else { myDebug("None"); } @@ -432,7 +448,7 @@ void publishValues(bool force) { if ((previousBoilerPublishCRC != checksum) || force) { previousBoilerPublishCRC = checksum; - if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { + if (ems_getLogging() == EMS_SYS_LOGGING_VERBOSE) { myDebug("Publishing boiler data via MQTT\n"); } @@ -443,7 +459,7 @@ void publishValues(bool force) { // see if the heating or hot tap water has changed, if so send // last_boilerActive stores heating in bit 1 and tap water in bit 2 if (last_boilerActive != ((EMS_Boiler.tapwaterActive << 1) + EMS_Boiler.heatingActive)) { - if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { + if (ems_getLogging() == EMS_SYS_LOGGING_VERBOSE) { myDebug("Publishing hot water and heating state via MQTT\n"); } myESP.publish(TOPIC_BOILER_TAPWATER_ACTIVE, EMS_Boiler.tapwaterActive == 1 ? "1" : "0"); @@ -486,7 +502,7 @@ void publishValues(bool force) { if ((previousThermostatPublishCRC != checksum) || force) { previousThermostatPublishCRC = checksum; - if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { + if (ems_getLogging() == EMS_SYS_LOGGING_VERBOSE) { myDebug("Publishing thermostat data via MQTT\n"); } @@ -512,30 +528,50 @@ void set_showerAlert() { // extra commands options for telnet debug window void myDebugCallback() { - char * cmd = myESP.consoleGetLastCommand(); - bool b; + char * cmd = myESP.consoleGetLastCommand(); + uint8_t len = strlen(cmd); + bool b; + // look for single letter commands + if (len == 1) { + switch (cmd[0]) { + case 's': + showInfo(); + break; + case 'p': + b = !ems_getPoll(); + ems_setPoll(b); + break; + case 'P': + //myESP.logger(LOG_HA, "Force publish values"); + publishValues(true); + break; + case 'h': // show type handlers + ems_printAllTypes(); + break; + case 'S': // toggle Shower timer support + Boiler_Status.shower_timer = !Boiler_Status.shower_timer; + myESP.publish(TOPIC_SHOWER_TIMER, Boiler_Status.shower_timer ? "1" : "0"); + break; + case 'A': // toggle Shower alert + Boiler_Status.shower_alert = !Boiler_Status.shower_alert; + myESP.publish(TOPIC_SHOWER_ALERT, Boiler_Status.shower_alert ? "1" : "0"); + break; + default: + myDebug("Unknown command. Use ? for help.\n"); + break; + } + return; + } + + if (len < 2) + return; + + // for commands with parameters, assume command is just one letter switch (cmd[0]) { - case 's': - showInfo(); - break; - case 'p': - b = !ems_getPoll(); - ems_setPoll(b); - break; - case 'P': - //myESP.logger(LOG_HA, "Force publish values"); - publishValues(true); - break; - case 'r': // read command for Boiler or Thermostat - ems_doReadCommand((uint8_t)strtol(&cmd[2], 0, 16)); - break; - case 't': // set thermostat temp + case 'T': // set thermostat temp ems_setThermostatTemp(strtof(&cmd[2], 0)); break; - case 'h': // show type handlers - ems_printAllTypes(); - break; case 'm': // set thermostat mode if ((cmd[2] - '0') == 1) ems_setThermostatMode(1); @@ -555,26 +591,31 @@ void myDebugCallback() { else if ((cmd[2] - '0') == 0) ems_setWarmWaterActivated(false); break; - case 'x': // experimental code for debugging - use with caution! + case 'b': // boiler read command + ems_doReadCommand((uint8_t)strtol(&cmd[2], 0, 16), EMS_ID_BOILER); + break; + case 't': // thermostat command + ems_doReadCommand((uint8_t)strtol(&cmd[2], 0, 16), EMS_ID_THERMOSTAT); + break; + case 'x': // experimental, not displayed! + myDebug("Calling experimental...\n"); + ems_setLogging(EMS_SYS_LOGGING_VERBOSE); ems_setExperimental((uint8_t)strtol(&cmd[2], 0, 16)); // takes HEX param break; - case 'T': // toggle Thermostat - b = !ems_getThermostatEnabled(); - ems_setThermostatEnabled(b); - Boiler_Status.thermostat_enabled = b; - break; - case 'S': // toggle Shower timer support - Boiler_Status.shower_timer = !Boiler_Status.shower_timer; - myESP.publish(TOPIC_SHOWER_TIMER, Boiler_Status.shower_timer ? "1" : "0"); - break; - case 'A': // toggle Shower alert - Boiler_Status.shower_alert = !Boiler_Status.shower_alert; - myESP.publish(TOPIC_SHOWER_ALERT, Boiler_Status.shower_alert ? "1" : "0"); + case 'U': // thermostat scan + myDebug("Doing a scan on thermostat IDs\n"); + ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); + publishValuesTimer.detach(); + systemCheckTimer.detach(); + regularUpdatesTimer.detach(); + scanThermostat_count = (uint8_t)strtol(&cmd[2], 0, 16); + scanThermostat.attach(SCANTHERMOSTAT_TIME, do_scanThermostat); break; default: - myDebug("Unknown command '%c'. Use ? for help.\n", cmd[0]); + myDebug("Unknown command. Use ? for help.\n"); break; } + return; } // MQTT Callback to handle incoming/outgoing changes @@ -805,6 +846,13 @@ void heartbeat() { } } +// Thermostat scan +void do_scanThermostat() { + //myDebug("Scanning %d..\n", scanThermostat_count); + ems_doReadCommand(scanThermostat_count, EMS_ID_THERMOSTAT); + scanThermostat_count++; +} + // do a healthcheck every now and then to see if we connections void do_systemCheck() { // first do a system check to see if there is still a connection to the EMS @@ -826,7 +874,7 @@ void regularUpdates() { // force get the thermostat data which are not usually automatically broadcasted ems_getThermostatTemps(); } else if (cycle == 1) { - ems_doReadCommand(EMS_TYPE_UBAParameterWW); // get Warm Water values + ems_doReadCommand(EMS_TYPE_UBAParameterWW, EMS_ID_BOILER); // get Warm Water values } } } @@ -880,7 +928,7 @@ void loop() { #ifndef NO_TX if (Boiler_Status.boiler_online) { // now that we're connected lets get some data from the EMS - ems_doReadCommand(EMS_TYPE_UBAParameterWW); + ems_doReadCommand(EMS_TYPE_UBAParameterWW, EMS_ID_BOILER); ems_setWarmWaterActivated(true); // make sure warm water if activated, in case it got stuck with the shower alert } else { myDebugLog("Boot: can't connect to EMS."); @@ -911,20 +959,27 @@ void loop() { Boiler_Shower.doingColdShot = false; Boiler_Shower.duration = 0; Boiler_Shower.showerOn = false; +#ifdef SHOWER_TEST myDebugLog("Shower: hot water on..."); +#endif } else { // hot water has been on for a while // first check to see if hot water has been on long enough to be recognized as a Shower/Bath if (!Boiler_Shower.showerOn && (timestamp - Boiler_Shower.timerStart) > SHOWER_MIN_DURATION) { Boiler_Shower.showerOn = true; +#ifdef SHOWER_TEST + myDebugLog("Shower: hot water still running, starting shower timer"); +#endif } // check if the shower has been on too long else if ((((timestamp - Boiler_Shower.timerStart) > SHOWER_MAX_DURATION) && !Boiler_Shower.doingColdShot) && Boiler_Status.shower_alert) { myESP.sendHACommand(TOPIC_SHOWER_ALARM); +#ifdef SHOWER_TEST myDebugLog("Shower: exceeded max shower time"); +#endif _showerColdShotStart(); } } @@ -932,7 +987,9 @@ void loop() { // if it just turned off, record the time as it could be a short pause if ((Boiler_Shower.timerStart != 0) && (Boiler_Shower.timerPause == 0)) { Boiler_Shower.timerPause = timestamp; +#ifdef SHOWER_TEST myDebugLog("Shower: hot water turned off"); +#endif } // if shower has been off for longer than the wait time @@ -964,8 +1021,10 @@ void loop() { } } +#ifdef SHOWER_TEST // reset everything myDebugLog("Shower: resetting timers"); +#endif Boiler_Shower.timerStart = 0; Boiler_Shower.timerPause = 0; Boiler_Shower.showerOn = false; diff --git a/src/ems.cpp b/src/ems.cpp index 32c824b49..07182a2ac 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -44,6 +44,7 @@ bool _process_RC20Temperature(uint8_t * data, uint8_t length); bool _process_RCTempMessage(uint8_t * data, uint8_t length); bool _process_Version(uint8_t * data, uint8_t length); bool _process_SetPoints(uint8_t * data, uint8_t length); +bool _process_EasyTemperature(uint8_t * data, uint8_t length); const _EMS_Types EMS_Types[] = { @@ -59,6 +60,7 @@ const _EMS_Types EMS_Types[] = { {EMS_ID_THERMOSTAT, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage}, {EMS_ID_THERMOSTAT, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, {EMS_ID_THERMOSTAT, EMS_TYPE_RC20Temperature, "RC20Temperature", _process_RC20Temperature}, + {EMS_ID_THERMOSTAT, EMS_TYPE_EasyTemperature, "EasyTemperature", _process_EasyTemperature}, {EMS_ID_THERMOSTAT, EMS_TYPE_RCTempMessage, "RCTempMessage", _process_RCTempMessage}, {EMS_ID_THERMOSTAT, EMS_TYPE_Version, "Version", _process_Version}, {EMS_ID_THERMOSTAT, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints} @@ -111,7 +113,7 @@ void ems_init() { EMS_Sys_Status.emsLogging = EMS_SYS_LOGGING_NONE; // Verbose logging is off // thermostat - EMS_Thermostat.type = EMS_ID_THERMOSTAT; // type, see ems.h + EMS_Thermostat.type = EMS_ID_THERMOSTAT; // type, see my_config.h EMS_Thermostat.hour = 0; EMS_Thermostat.minute = 0; EMS_Thermostat.second = 0; @@ -206,12 +208,14 @@ void ems_setLogging(_EMS_SYS_LOGGING loglevel) { if (loglevel <= EMS_SYS_LOGGING_VERBOSE) { EMS_Sys_Status.emsLogging = loglevel; myDebug("System Logging is set to "); - if (loglevel == EMS_SYS_LOGGING_BASIC) { + if (loglevel == EMS_SYS_LOGGING_NONE) { + myDebug("None\n"); + } else if (loglevel == EMS_SYS_LOGGING_BASIC) { myDebug("Basic\n"); } else if (loglevel == EMS_SYS_LOGGING_VERBOSE) { myDebug("Verbose\n"); - } else { - myDebug("None\n"); + } else if (loglevel == EMS_SYS_LOGGING_THERMOSTAT) { + myDebug("Thermostat only\n"); } } } @@ -247,10 +251,10 @@ uint16_t _toLong(uint8_t i, uint8_t * data) { // debugging only - print out all handled types void ems_printAllTypes() { - myDebug("These %d telegram type ids are recognized:\n", _EMS_Types_max); + myDebug("These %d telegram type IDs are recognized:\n", _EMS_Types_max); for (uint8_t i = 0; i < _EMS_Types_max; i++) { - myDebug(" %s:\ttype %02x (%s)\n", + myDebug(" %s:\ttype ID %02X (%s)\n", EMS_Types[i].src == EMS_ID_THERMOSTAT ? "Thermostat" : "Boiler", EMS_Types[i].type, EMS_Types[i].typeString); @@ -281,20 +285,26 @@ void _debugPrintTelegram(const char * prefix, uint8_t * data, uint8_t len, const if (EMS_Sys_Status.emsLogging != EMS_SYS_LOGGING_VERBOSE) return; - myDebug("%s%s len=%d, telegram: ", color, prefix, len); + myDebug("%s%s telegram: ", color, prefix); for (int i = 0; i < len; i++) { - myDebug("%02x ", data[i]); + myDebug("%02X ", data[i]); } - myDebug("%s\n", COLOR_RESET); + + myDebug("(len %d)%s\n", len, COLOR_RESET); } // send the contents of the Tx buffer void _ems_sendTelegram() { // only send when Tx is not busy - _debugPrintTelegram(((EMS_TxTelegram.action == EMS_TX_WRITE) ? "Sending write telegram:" : "Sending read telegram:"), - EMS_TxTelegram.data, - EMS_TxTelegram.length, - COLOR_CYAN); + char s[50]; + + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { + sprintf(s, + "Sending %s to 0x%02X:", + ((EMS_TxTelegram.action == EMS_TX_WRITE) ? "write" : "read"), + EMS_TxTelegram.dest & 0x7F); + _debugPrintTelegram(s, EMS_TxTelegram.data, EMS_TxTelegram.length, COLOR_CYAN); + } EMS_Sys_Status.emsTxStatus = EMS_TX_ACTIVE; emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); @@ -335,7 +345,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { _initTxBuffer(); } else { if (EMS_Sys_Status.emsLogging != EMS_SYS_LOGGING_NONE) { - myDebug("Didn't receive acknowledgement from the 0x%02x, so resending (attempt #%d/%d)...\n", + myDebug("Didn't receive acknowledgement from the 0x%X, so resending (attempt #%d/%d)...\n", EMS_TxTelegram.type, emsLastRxCount, RX_READ_TIMEOUT_COUNT); @@ -386,8 +396,8 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // ignore anything that doesn't resemble a proper telegram package // minimal is 5 bytes, excluding CRC at the end - if ((length < 5)) { - _debugPrintTelegram("Noisy data:", telegram, length, COLOR_MAGENTA); + if (length < 5) { + _debugPrintTelegram("Noisy data:", telegram, length, COLOR_RED); return; } @@ -410,7 +420,8 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { */ void _processType(uint8_t * telegram, uint8_t length) { // extract the 4-byte header information - uint8_t src = telegram[0] & 0x7F; // remove 8th bit as we deal with both reads and writes + // removing 8th bit as we deal with both reads and writes + uint8_t src = telegram[0] & 0x7F; // if its an echo of ourselves from the master, ignore if (src == EMS_ID_ME) { @@ -419,7 +430,7 @@ void _processType(uint8_t * telegram, uint8_t length) { } // header - uint8_t dest = telegram[1]; + uint8_t dest = telegram[1] & 0x7F; // remove 8th bit uint8_t type = telegram[2]; uint8_t * data = telegram + 4; // data block starts at position 5 @@ -432,6 +443,7 @@ void _processType(uint8_t * telegram, uint8_t length) { // we have a match typeFound = true; // call callback to fetch the values from the telegram + // data block is sent, which starts with the 5th byte of the telegram // return value tells us if we need to force send values back to MQTT if ((EMS_Types[i].processType_cb) != (void *)NULL) { EMS_Sys_Status.emsRefreshed = EMS_Types[i].processType_cb(data, length); @@ -458,7 +470,16 @@ void _processType(uint8_t * telegram, uint8_t length) { } // print debug messages - if (EMS_Sys_Status.emsLogging != EMS_SYS_LOGGING_NONE) { + // special case for only thermostat + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_THERMOSTAT) { + if ((src == EMS_ID_THERMOSTAT) && (dest == EMS_ID_ME)) { + myDebug("Thermostat -> me, type 0x%02X telegram: ", type); + for (int i = 0; i < length; i++) { + myDebug("%02X ", telegram[i]); + } + myDebug("\n"); + } + } else if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { char color_s[20]; char src_s[20]; char dest_s[20]; @@ -466,32 +487,38 @@ void _processType(uint8_t * telegram, uint8_t length) { // set source string if (src == EMS_ID_BOILER) { - strcpy(src_s, "Boiler"); + strcpy(src_s, "Boiler -> "); } else if (src == EMS_ID_THERMOSTAT) { - strcpy(src_s, "Thermostat"); + strcpy(src_s, "Thermostat -> "); } else { - strcpy(src_s, ""); + sprintf(src_s, "0x%02X -> ", src); } // set destination string if (dest == EMS_ID_ME) { - strcpy(dest_s, "telegram for us"); + strcpy(dest_s, "me"); strcpy(color_s, COLOR_YELLOW); } else if (dest == EMS_ID_NONE) { // it's probably just a broadcast - strcpy(dest_s, "broadcast"); + strcpy(dest_s, "all"); strcpy(color_s, COLOR_GREEN); + } else if (dest == EMS_ID_BOILER) { + strcpy(dest_s, "Boiler"); + strcpy(color_s, COLOR_MAGENTA); + } else if (dest == EMS_ID_THERMOSTAT) { + strcpy(dest_s, "Thermostat"); + strcpy(color_s, COLOR_MAGENTA); } else { - // for someone else - strcpy(dest_s, "(not for us)"); + sprintf(dest_s, "0x%02X", dest); strcpy(color_s, COLOR_MAGENTA); } + // and print - sprintf(s, "%s %s, type 0x%02x", src_s, dest_s, type); + sprintf(s, "%s%s, type 0x%02X", src_s, dest_s, type); _debugPrintTelegram(s, telegram, length, color_s); if (typeFound) { - myDebug("<--- %s(0x%02x) received\n", EMS_Types[i].typeString, type); + myDebug("<--- %s(0x%02X) received\n", EMS_Types[i].typeString, type); } } @@ -521,13 +548,13 @@ void _processType(uint8_t * telegram, uint8_t length) { // look up the ID and fetch string int i = ems_findType(EMS_TxTelegram.type); if (i != -1) { - myDebug("---> %s(0x%02x) sent with value %d at offset %d ", + myDebug("---> %s(0x%02X) sent with value %d at offset %d ", EMS_Types[i].typeString, type, EMS_TxTelegram.checkValue, offset); } else { - myDebug("---> ?(0x%02x) sent with value %d at offset %d ", type, EMS_TxTelegram.checkValue, offset); + myDebug("---> ?(0x%02X) sent with value %d at offset %d ", type, EMS_TxTelegram.checkValue, offset); } if (EMS_TxTelegram.checkValue == data[offset]) { @@ -545,7 +572,7 @@ void _processType(uint8_t * telegram, uint8_t length) { bool _checkWriteQueueFull() { if (EMS_Sys_Status.emsTxStatus == EMS_TX_PENDING) { // send is already pending if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { - myDebug("Delaying write command as there is already a telegram (type 0x%02x) in the queue\n", + myDebug("Delaying write command as there is already a telegram (type 0x%02X) in the queue\n", EMS_TxTelegram.type); } return true; // something in queue @@ -656,6 +683,17 @@ bool _process_RC20StatusMessage(uint8_t * data, uint8_t length) { return true; // triggers a send the values back to Home Assistant via MQTT } +/* + * EasyTemperature - type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long + * The Easy has a digital precision of its floats to 2 decimal places, so values is divided by 100 + */ +bool _process_EasyTemperature(uint8_t * data, uint8_t length) { + EMS_Thermostat.curr_roomTemp = ((float)(((data[8] << 8) + data[9]))) / 100; + EMS_Thermostat.setpoint_roomTemp = ((float)(((data[10] << 8) + data[11]))) / 100; + + return true; // triggers a send the values back to Home Assistant via MQTT +} + /* * RC20Temperature - type 0xa8 - for set temp value and mode from the RC20 thermostat (0x17) * received only after requested @@ -717,7 +755,7 @@ bool _process_SetPoints(uint8_t * data, uint8_t length) { uint8_t hk_power = data[1]; uint8_t ww_power = data[2]; - if (EMS_Sys_Status.emsLogging != EMS_SYS_LOGGING_NONE) { + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { myDebug("UBASetPoint: SetPoint=%d, hk_power=%d ww_power=%d\n", setpoint, hk_power, ww_power); } @@ -776,7 +814,9 @@ void _buildTxTelegram(uint8_t data_value) { */ void ems_getThermostatTemps() { if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_RC20) { - ems_doReadCommand(EMS_TYPE_RC20Temperature); + ems_doReadCommand(EMS_TYPE_RC20Temperature, EMS_ID_THERMOSTAT); + } else if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_EASY) { + ems_doReadCommand(EMS_TYPE_EasyTemperature, EMS_ID_THERMOSTAT); } } @@ -784,21 +824,22 @@ void ems_getThermostatTemps() { * Send a command to UART Tx to Read from another device * Read commands when sent must respond by the destination (target) immediately (or within 10ms) */ -void ems_doReadCommand(uint8_t type) { +void ems_doReadCommand(uint8_t type, uint8_t dest) { if (type == EMS_TYPE_NONE) return; // not a valid type, quit if (_checkWriteQueueFull()) return; // check if there is already something in the queue - int i = ems_findType(type); - uint8_t dest = (i == -1 ? EMS_ID_BOILER : EMS_Types[i].src); // default is Boiler + // see if its a known type + int i = ems_findType(type); + // uint8_t dest = (i == -1 ? EMS_ID_BOILER : EMS_Types[i].src); // default is Boiler - if (ems_getLogging() != EMS_SYS_LOGGING_NONE) { + if ((ems_getLogging() == EMS_SYS_LOGGING_BASIC) || (ems_getLogging() == EMS_SYS_LOGGING_VERBOSE)) { if (i == -1) { - myDebug("Requesting type (0x%02x) from dest 0x%02x\n", type, dest); + myDebug("Requesting type (0x%02X) from dest 0x%02X\n", type, dest); } else { - myDebug("Requesting type %s(0x%02x) from dest 0x%02x\n", EMS_Types[i].typeString, type, dest); + myDebug("Requesting type %s(0x%02X) from dest 0x%02X\n", EMS_Types[i].typeString, type, dest); } } @@ -818,17 +859,33 @@ void ems_setThermostatTemp(float temperature) { if (_checkWriteQueueFull()) return; // check if there is already something in the queue - myDebug("Setting new thermostat temperature\n"); + EMS_TxTelegram.action = EMS_TX_WRITE; + EMS_TxTelegram.dest = EMS_ID_THERMOSTAT; - EMS_TxTelegram.action = EMS_TX_WRITE; - EMS_TxTelegram.dest = EMS_ID_THERMOSTAT; - EMS_TxTelegram.type = EMS_TYPE_RC20Temperature; - EMS_TxTelegram.offset = EMS_OFFSET_RC20Temperature_temp; - EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; - EMS_TxTelegram.checkValue = (uint8_t)((float)temperature * (float)2); // value to compare against. must be a single int + if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_RC20) { + myDebug("Setting new thermostat temperature\n"); + + // RC20 + EMS_TxTelegram.type = EMS_TYPE_RC20Temperature; + EMS_TxTelegram.offset = EMS_OFFSET_RC20Temperature_temp; + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; + EMS_TxTelegram.checkValue = + (uint8_t)((float)temperature * (float)2); // value to compare against. must be a single int + + // post call is back to EMS_TYPE_RC20Temperature to fetch temps and send to HA + EMS_TxTelegram.type_validate = EMS_OFFSET_RC20Temperature_temp; + + } else if (EMS_Thermostat.type == EMS_ID_THERMOSTAT_EASY) { + myDebug("Setting new thermostat temperature on an Easy - not working\n"); + + EMS_TxTelegram.type = EMS_TYPE_EasyTemperature; + EMS_TxTelegram.offset = 11; + EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; + EMS_TxTelegram.checkValue = 0; + + EMS_TxTelegram.type_validate = EMS_ID_NONE; + } - // post call is back to EMS_TYPE_RC20Temperature to fetch temps and send to HA - EMS_TxTelegram.type_validate = EMS_OFFSET_RC20Temperature_temp; _buildTxTelegram(EMS_TxTelegram.checkValue); } @@ -902,15 +959,34 @@ void ems_setExperimental(uint8_t value) { if (_checkWriteQueueFull()) return; // check if there is already something in the queue - myDebug("Sending experimental code, value=%02x\n", value); + /* + EMS_TxTelegram.action = EMS_TX_READ; // read command + EMS_TxTelegram.dest = EMS_ID_THERMOSTAT | 0x80; // set 7th bit to indicate a read + EMS_TxTelegram.offset = 0; // 0 for all data + EMS_TxTelegram.length = 8; + EMS_TxTelegram.type = 0xF0; - EMS_TxTelegram.action = EMS_TX_WRITE; - EMS_TxTelegram.dest = EMS_ID_BOILER; - EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW; - EMS_TxTelegram.offset = 6; - EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; - EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't force a send to check the value but do it during next broadcast + EMS_TxTelegram.data[0] = EMS_ID_ME; // src + EMS_TxTelegram.data[1] = EMS_TxTelegram.dest; // dest + EMS_TxTelegram.data[2] = EMS_TxTelegram.type; // type + EMS_TxTelegram.data[3] = EMS_TxTelegram.offset; //offset - EMS_TxTelegram.checkValue = value; - _buildTxTelegram(value); + // EMS Plus test + // Sending read to 0x18: telegram: 0B 98 F0 00 01 B9 63 DB (len 8) + + EMS_TxTelegram.data[0] = EMS_ID_ME; // src + EMS_TxTelegram.data[1] = EMS_TxTelegram.dest; // dest + EMS_TxTelegram.data[2] = 0xF0; // marker + EMS_TxTelegram.data[3] = EMS_TxTelegram.offset; // offset + EMS_TxTelegram.data[4] = 0x01; // hi byte + EMS_TxTelegram.data[5] = 0xB9; // low byte + + // data: + EMS_TxTelegram.data[6] = 99; // max length + + // crc: + EMS_TxTelegram.data[7] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); + + EMS_Sys_Status.emsTxStatus = EMS_TX_PENDING; // armed and ready to send + */ } diff --git a/src/ems.h b/src/ems.h index 91b5e0c49..5a2088a46 100644 --- a/src/ems.h +++ b/src/ems.h @@ -44,7 +44,8 @@ #define EMS_TYPE_RCTime 0x06 // is an automatic thermostat broadcast #define EMS_TYPE_RCTempMessage 0xA3 // is an automatic thermostat broadcast #define EMS_TYPE_RC20Temperature 0xA8 -#define EMS_TYPE_Version 0x02 // version of the UBA controller +#define EMS_TYPE_EasyTemperature 0x0A // reading values on an Easy Thermostat +#define EMS_TYPE_Version 0x02 // version of the UBA controller (boiler) // Offsets for specific values in a telegram, per type, used for validation #define EMS_OFFSET_RC20Temperature_temp 0x1C // thermostat set temp @@ -77,8 +78,13 @@ typedef enum { EMS_TX_VALIDATE // do a validate after a write } _EMS_TX_ACTION; -/* EMS UART logging */ -typedef enum { EMS_SYS_LOGGING_NONE, EMS_SYS_LOGGING_BASIC, EMS_SYS_LOGGING_VERBOSE } _EMS_SYS_LOGGING; +/* EMS logging */ +typedef enum { + EMS_SYS_LOGGING_NONE, // no messages + EMS_SYS_LOGGING_BASIC, // only basic read/write messages + EMS_SYS_LOGGING_THERMOSTAT, // only telegrams sent from thermostat + EMS_SYS_LOGGING_VERBOSE // everything +} _EMS_SYS_LOGGING; // status/counters since last power on typedef struct { @@ -194,7 +200,7 @@ typedef struct { // function definitions extern void ems_parseTelegram(uint8_t * telegram, uint8_t len); void ems_init(); -void ems_doReadCommand(uint8_t type); +void ems_doReadCommand(uint8_t type, uint8_t dest); void ems_setThermostatTemp(float temp); void ems_setThermostatMode(uint8_t mode); diff --git a/src/my_config.h b/src/my_config.h index 4281cc354..86d5f0f57 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -26,7 +26,8 @@ #define BOILER_SHOWER_ALERT 0 // send alert if showetime exceeded // define here the Thermostat type. see ems.h for options -#define EMS_ID_THERMOSTAT EMS_ID_THERMOSTAT_RC20 // your thermostat ID +//#define EMS_ID_THERMOSTAT EMS_ID_THERMOSTAT_RC20 // your thermostat ID +#define EMS_ID_THERMOSTAT EMS_ID_THERMOSTAT_EASY // trigger settings to determine if hot tap water or the heating is active #define EMS_BOILER_BURNPOWER_TAPWATER 100