first commit

This commit is contained in:
proddy
2018-05-14 23:16:06 +02:00
parent 58ca5c176e
commit 4ab7bb6835
23 changed files with 7838 additions and 0 deletions

13
src/LICENSE.md Normal file
View File

@@ -0,0 +1,13 @@
#### Copyright 2018 [Paul Derbsyhire](mailto:dev@derbyshire.nl). All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met :
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and / or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those of the
authors and should not be interpreted as representing official policies, either expressed
or implied, of [Paul Derbyshire](mailto:dev@derbyshire.nl).

578
src/boiler.ino Normal file
View File

@@ -0,0 +1,578 @@
/*
* Boiler Project
* Paul Derbyshire - May 2018 - https://github.com/proddy/EMS-ESP-Boiler
*
* Acknowledgments too https://github.com/susisstrolch/EMS-ESP12 and https://github.com/jeelabs/esp-link
*/
// local libraries
#include "ems.h"
#include "emsuart.h"
// private libraries
#include <ESPHelper.h>
// public libraries
#include <ArduinoJson.h>
#include <Ticker.h> // https://github.com/sstaub/Ticker
// private function prototypes
void heartbeat();
void systemCheck();
void publishValues();
void _showerColdShotStart();
void _showerColdShotStop();
// hostname is also used as the MQTT topic identifier (home/<hostname>)
#define HOSTNAME "boiler"
// Project commands for telnet
// Note: ?, *, $, ! and & are reserved
#define PROJECT_CMDS \
"s=show statistics\n\r" \
"* q=toggle Verbose telegram logging\n\r" \
"* m=publish stats to MQTT\n\r" \
"* p=toggle Poll response\n\r" \
"* T=toggle Thermostat suport on/off\n\r" \
"* S=toggle Shower Timer on/off\n\r" \
"* r [n] to request for data from EMS " \
"(33=UBAParameterWW, 18=UBAMonitorFast, 19=UBAMonitorSlow, " \
"34=UBAMonitorWWMessage, 91=RC20StatusMessage, 6=RC20Time)\n\r" \
"* t [n] set thermostat temperature to n\n\r" \
"* w [n] set boiler warm water temperature to n (min 30)\n\r" \
"* a [n] activate boiler warm water on (n=1) or off (n=0)"
// GPIOs
#define LED_RX D1 // (GPIO5 on nodemcu)
#define LED_TX D2 // (GPIO4 on nodemcu)
#define LED_ERR D3 // (GPIO0 on nodemcu)
// app specific - do not change
#define MQTT_BOILER MQTT_BASE HOSTNAME "/"
#define TOPIC_START MQTT_BOILER MQTT_TOPIC_START
#define TOPIC_THERMOSTAT MQTT_BOILER "thermostat"
#define TOPIC_SHOWERTIME MQTT_BOILER "showertime"
#define TOPIC_THERMOSTAT_TEMP MQTT_BOILER "thermostat_temp"
// all on
#define BOILER_THERMOSTAT_ENABLED 1
#define BOILER_SHOWER_ENABLED 1
#define BOILER_SHOWER_TIMER 0
// shower settings
const unsigned long SHOWER_PAUSE_TIME = 15000; // 15 seconds, max time if water is switched off & on during a shower
const unsigned long SHOWER_MIN_DURATION = 120000; // 2 minutes, before recognizing its a shower
const unsigned long SHOWER_MAX_DURATION = 420000; // 7 minutes, before trigger a shot of cold water
const unsigned long SHOWER_OFF_DURATION = 3000; // 3 seconds long for cold water
const uint8_t SHOWER_BURNPOWER_MIN = 80;
// for debugging...
//const unsigned long SHOWER_MIN_DURATION = 10000; // 10 seconds
//const unsigned long SHOWER_MAX_DURATION = 15000; // 15 seconds
typedef struct {
bool wifi_connected;
bool boiler_online;
bool shower_enabled; // true if we want to report back on shower times
bool shower_timer; // true if we want the cold water reminder
} _Boiler_Status;
typedef struct {
bool showerOn;
unsigned long timerStart; // ms
unsigned long timerPause; // ms
unsigned long duration; // ms
bool isColdShot; // true if we've just sent a jolt of cold water
} _Boiler_Shower;
// ESPHelper
netInfo homeNet = {.mqttHost = MQTT_IP,
.mqttUser = MQTT_USER,
.mqttPass = MQTT_PASS,
.mqttPort = 1883,
.ssid = WIFI_SSID,
.pass = WIFI_PASSWORD
};
ESPHelper myESP(&homeNet);
// store for overall system status
_Boiler_Status Boiler_Status;
_Boiler_Shower Boiler_Shower;
// Debugger to telnet
#define myDebug(x, ...) myESP.printf(x, ##__VA_ARGS__);
// Timers
Ticker updateHATimer(publishValues, 300000); // every 5 mins (300000) post HA values
Ticker hearbeatTimer(heartbeat, 500); // changing onboard heartbeat led every 500ms
Ticker systemCheckTimer(systemCheck, 10000); // every 10 seconds check if Boiler is online
Ticker showerResetTimer(_showerColdShotStop, SHOWER_OFF_DURATION, 1); // timer for how long we turn off the hot water
const unsigned long POLL_TIMEOUT_ERR = 10000; // if no signal from boiler for last 10 seconds, assume its offline
bool heartbeat_state = false;
const unsigned long TX_HOLD_LED_TIME = 2000; // how long to hold the Tx LED because its so quick
unsigned long timestamp; // for internal timings, via millis()
static int connectionStatus = NO_CONNECTION;
bool startMQTTsent = false;
// Show command - display stats on an 's' command
void showInfo() {
char s[10]; // for formatting floats using the _float_to_char() function
// General stats from EMS bus
myDebug("EMS Bus stats:\n");
myDebug(" Poll is %s, Shower is %s, Shower timer is %s, RxPgks=%d, TxPkgs=%d, #CrcErrors=%d",
((EMS_Sys_Status.emsPollEnabled) ? "enabled" : "disabled"),
((Boiler_Status.shower_enabled) ? "enabled" : "disabled"),
((Boiler_Status.shower_timer) ? "enabled" : "disabled"),
EMS_Sys_Status.emsRxPgks,
EMS_Sys_Status.emsTxPkgs,
EMS_Sys_Status.emxCrcErr);
myDebug(", RxStatus=");
switch (EMS_Sys_Status.emsRxStatus) {
case EMS_RX_IDLE:
myDebug("idle");
break;
case EMS_RX_ACTIVE:
myDebug("active");
break;
}
myDebug(", TxStatus=");
switch (EMS_Sys_Status.emsTxStatus) {
case EMS_TX_IDLE:
myDebug("idle");
break;
case EMS_TX_PENDING:
myDebug("pending");
break;
case EMS_TX_ACTIVE:
myDebug("active");
break;
}
myDebug(", TxAction=");
switch (EMS_TxTelegram.action) {
case EMS_TX_READ:
myDebug("read");
break;
case EMS_TX_WRITE:
myDebug("write");
break;
case EMS_TX_VALIDATE:
myDebug("validate");
break;
case EMS_TX_NONE:
myDebug("none");
break;
}
myDebug("\nBoiler stats:\n");
// UBAMonitorWWMessage & UBAParameterWW
myDebug(" Warm Water activated: %s\n", (EMS_Boiler.wWActivated ? "yes" : "no"));
myDebug(" Warm Water selected temperature: %d C\n", EMS_Boiler.wWSelTemp);
myDebug(" Warm Water circulation pump available: %s\n", (EMS_Boiler.wWCircPump ? "yes" : "no"));
myDebug(" Warm Water desired temperature: %d C\n", EMS_Boiler.wWDesiredTemp);
myDebug(" Warm Water current temperature: %s C\n", _float_to_char(s, EMS_Boiler.wWCurTmp));
myDebug(" Warm Water # starts: %d times\n", EMS_Boiler.wWStarts);
myDebug(" Warm Water active time: %d days %d hours %d minutes\n",
EMS_Boiler.wWWorkM / 1440,
(EMS_Boiler.wWWorkM % 1440) / 60,
EMS_Boiler.wWWorkM % 60);
myDebug(" Warm Water 3-way valve: %s\n", EMS_Boiler.wWHeat ? "on" : "off");
// UBAMonitorFast
myDebug(" Selected flow temperature: %d C\n", EMS_Boiler.selFlowTemp);
myDebug(" Current flow temperature: %s C\n", _float_to_char(s, EMS_Boiler.curFlowTemp));
myDebug(" Return temperature: %s C\n", _float_to_char(s, EMS_Boiler.retTemp));
myDebug(" Gas: %s\n", EMS_Boiler.burnGas ? "on" : "off"); // 0 -gas on
myDebug(" Circulating pump: %s\n", EMS_Boiler.heatPmp ? "on" : "off"); // 5 - boiler circuit pump on
myDebug(" Fan: %s\n", EMS_Boiler.fanWork ? "on" : "off"); // 2
myDebug(" Ignition: %s\n", EMS_Boiler.ignWork ? "on" : "off"); // 3
myDebug(" Circulation pump: %s\n", EMS_Boiler.wWCirc ? "on" : "off"); // 7
myDebug(" Burner max power: %d %%\n", EMS_Boiler.selBurnPow);
myDebug(" Burner current power: %d %%\n", EMS_Boiler.curBurnPow);
myDebug(" Flame current: %s uA\n", _float_to_char(s, EMS_Boiler.flameCurr));
myDebug(" System pressure: %s bar\n", _float_to_char(s, EMS_Boiler.sysPress));
// UBAMonitorSlow
myDebug(" Outside temperature: %s C\n", _float_to_char(s, EMS_Boiler.extTemp));
myDebug(" Boiler temperature: %s C\n", _float_to_char(s, EMS_Boiler.boilTemp));
myDebug(" Pump modulation: %d %%\n", EMS_Boiler.pumpMod);
myDebug(" # burner restarts: %d\n", EMS_Boiler.burnStarts);
myDebug(" Total burner operating time: %d days %d hours %d minutes\n",
EMS_Boiler.burnWorkMin / 1440,
(EMS_Boiler.burnWorkMin % 1440) / 60,
EMS_Boiler.burnWorkMin % 60);
myDebug(" Total heat operating time: %d days %d hours %d minutes\n",
EMS_Boiler.heatWorkMin / 1440,
(EMS_Boiler.heatWorkMin % 1440) / 60,
EMS_Boiler.heatWorkMin % 60);
// Thermostat stats
if (EMS_Sys_Status.emsThermostatEnabled) {
myDebug("Thermostat stats:\n Thermostat time is %02d:%02d:%02d %d/%d/%d\n",
EMS_Thermostat.hour,
EMS_Thermostat.minute,
EMS_Thermostat.second,
EMS_Thermostat.day,
EMS_Thermostat.month,
EMS_Thermostat.year + 2000);
myDebug(" Setpoint room temperature is %s C and current room temperature is %s C\n",
_float_to_char(s, EMS_Thermostat.setpoint_roomTemp),
_float_to_char(s, EMS_Thermostat.curr_roomTemp));
}
if (Boiler_Status.shower_enabled) {
// show the Shower Info
myDebug("Shower stats:\n Shower is %s\n", (Boiler_Shower.showerOn ? "on" : "off"));
char s[70];
uint8_t sec = (uint8_t)((Boiler_Shower.duration / 1000) % 60);
uint8_t min = (uint8_t)((Boiler_Shower.duration / (1000 * 60)) % 60);
sprintf(s, " Last shower duration was %d minutes and %d %s\n", min, sec, (sec == 1) ? "second" : "seconds");
myDebug(s);
}
myDebug("\n");
}
// send values to HA via MQTT
void publishValues() {
// only send values if we actually have them
if (((int)EMS_Thermostat.curr_roomTemp == (int)0) || ((int)EMS_Thermostat.setpoint_roomTemp == (int)0)) {
return;
}
myDebug("Publishing data to MQTT topics\n");
// build a JSON with the current temp and selected temp from the Thermostat
StaticJsonBuffer<200> jsonBuffer;
JsonObject & root = jsonBuffer.createObject();
root["currtemp"] = (String)EMS_Thermostat.curr_roomTemp;
root["seltemp"] = (String)EMS_Thermostat.setpoint_roomTemp;
char data[100];
root.printTo(data, root.measureLength() + 1);
myESP.publish(TOPIC_THERMOSTAT, data);
}
// extra commands options for telnet debug window
void myDebugCallback() {
char * cmd = myESP.consoleGetLastCommand();
bool b;
switch (cmd[0]) {
case 's':
showInfo();
break;
case 'p':
b = !ems_getPoll();
ems_setPoll(b);
break;
case 'm':
publishValues();
break;
case 'r': // read command for Boiler or Thermostat
ems_doReadCommand((uint8_t)strtol(&cmd[2], 0, 16));
break;
case 't': // set thermostat temp
ems_setThermostatTemp(strtof(&cmd[2], 0));
break;
case 'w': // set warm water temp
ems_setWarmWaterTemp((uint8_t)strtol(&cmd[2], 0, 10));
break;
case 'q': // quiet
b = !ems_getLogVerbose();
ems_setLogVerbose(b);
enableHeartbeat(b);
break;
case 'a': // set ww activate on or off
if ((cmd[2] - '0') == 1) {
ems_setWarmWaterActivated(true);
} else if ((cmd[2] - '0') == 0) {
ems_setWarmWaterActivated(false);
}
break;
case 'T': // toggle Thermostat
b = !ems_getThermostatEnabled();
ems_setThermostatEnabled(b);
break;
case 'S': // toggle Shower timer support
Boiler_Status.shower_enabled = !Boiler_Status.shower_enabled;
myDebug("Shower timer is %s\n", Boiler_Status.shower_enabled ? "enabled" : "disabled");
break;
default:
myDebug("Unknown command '%c'. Use ? for help.\n", cmd[0]);
break;
}
}
// MQTT Callback to handle incoming/outgoing changes
void MQTTcallback(char * topic, byte * payload, uint8_t length) {
// check if start is received, if so return boottime - defined in ESPHelper.h
if (strcmp(topic, TOPIC_START) == 0) {
payload[length] = '\0'; // add null terminator
myDebug("MQTT topic boottime: %s\n", payload);
myESP.setBoottime((char *)payload);
return;
}
// thermostat_temp
if (strcmp(topic, TOPIC_THERMOSTAT_TEMP) == 0) {
float f = strtof((char *)payload, 0);
char s[10];
myDebug("MQTT topic: thermostat_temp value %s\n", _float_to_char(s, f));
ems_setThermostatTemp(f);
return;
}
}
// WifiCallback, called when a WiFi connect has successfully been established
void WIFIcallback() {
Boiler_Status.wifi_connected = true;
// turn off the LEDs since we've finished the boot loading
digitalWrite(LED_RX, LOW);
digitalWrite(LED_TX, LOW);
digitalWrite(LED_ERR, LOW);
// when finally we're all set up, we can fire up the uart (this will enable the interrupts)
emsuart_init();
}
void _initBoiler() {
// default settings
ems_setThermostatEnabled(BOILER_THERMOSTAT_ENABLED);
Boiler_Status.shower_enabled = BOILER_SHOWER_ENABLED;
Boiler_Status.shower_timer = BOILER_SHOWER_TIMER;
// init boiler
Boiler_Status.wifi_connected = false;
Boiler_Status.boiler_online = true; // assume we have a connection, it will be checked in the loop() anyway
// init shower
Boiler_Shower.timerStart = 0;
Boiler_Shower.timerPause = 0;
Boiler_Shower.duration = 0;
Boiler_Shower.isColdShot = false;
}
//
// SETUP
// Note: we don't init the UART here as we should wait until everything is loaded first. It's done in loop()
//
void setup() {
// set pin for LEDs - start up with all lit up while we sort stuff out
pinMode(LED_RX, OUTPUT);
pinMode(LED_TX, OUTPUT);
pinMode(LED_ERR, OUTPUT);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_RX, HIGH);
digitalWrite(LED_TX, HIGH);
digitalWrite(LED_ERR, HIGH);
// Timers
updateHATimer.start();
systemCheckTimer.start();
// set up Wifi, MQTT, Telnet
myESP.setWifiCallback(WIFIcallback);
myESP.setMQTTCallback(MQTTcallback);
myESP.addSubscription(TOPIC_START);
myESP.addSubscription(TOPIC_THERMOSTAT);
myESP.addSubscription(TOPIC_THERMOSTAT_TEMP);
myESP.addSubscription(TOPIC_SHOWERTIME);
myESP.consoleSetHelpProjectsCmds(PROJECT_CMDS);
myESP.consoleSetCallBackProjectCmds(myDebugCallback);
myESP.begin(HOSTNAME);
// init ems stats
ems_init();
// init Boiler specific params
_initBoiler();
// heartbeat, only if setting is enabled
enableHeartbeat(ems_getLogVerbose());
}
// flash ERR LEDs
// Using a faster way to write to pins as digitalWrite does a lot of overhead like pin checking & disabling interrupts
void showLEDs() {
// update Ticker
hearbeatTimer.update();
// hearbeat timer, using internal LED on board
if (hearbeatTimer.counter() == 20)
hearbeatTimer.interval(200);
if (hearbeatTimer.counter() == 80)
hearbeatTimer.interval(1000);
if (ems_getLogVerbose()) {
// ERR LED
if (!Boiler_Status.boiler_online) {
WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + 4, (1 << LED_ERR)); // turn on
EMS_Sys_Status.emsRxStatus = EMS_RX_IDLE;
EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE;
} else {
WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + 8, (1 << LED_ERR)); // turn off
}
// Rx LED
WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + ((EMS_Sys_Status.emsRxStatus == EMS_RX_IDLE) ? 8 : 4), (1 << LED_RX));
// Tx LED
// because sends are quick, if we did a recent send show the LED for a short while
uint64_t t = (timestamp - EMS_Sys_Status.emsLastTx);
WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + ((t < TX_HOLD_LED_TIME) ? 4 : 8), (1 << LED_TX));
}
}
// heartbeat callback to light up the LED, called via Ticker
void heartbeat() {
digitalWrite(LED_BUILTIN, heartbeat_state);
heartbeat_state = !heartbeat_state;
}
// enables or disables the heartbeat LED
void enableHeartbeat(bool on) {
heartbeat_state = (on) ? LOW : HIGH;
heartbeat();
if (on)
hearbeatTimer.resume();
else
hearbeatTimer.pause();
}
// do a healthcheck every now and then to see if we connections
void systemCheck() {
Boiler_Status.boiler_online = ((timestamp - EMS_Sys_Status.emsLastPoll) < POLL_TIMEOUT_ERR);
if (!Boiler_Status.boiler_online) {
myDebug("Error! Boiler unreachable. Please check connection. Retrying in 10 seconds.\n");
}
}
// turn off hot water to send a shot of cold
void _showerColdShotStart() {
myDebug("Shower: exceeded max shower time, doing a shot of cold...\n");
ems_setWarmWaterActivated(false);
Boiler_Shower.isColdShot = true;
}
// turn back on the hot water for the shower
void _showerColdShotStop() {
if (Boiler_Shower.isColdShot) {
myDebug("Shower: turning back hot shower water.\n");
ems_setWarmWaterActivated(true);
Boiler_Shower.isColdShot = false;
}
}
//
// Main loop
//
void loop() {
// my myESP to maintain the wifi, mqtt and debugging
yield();
connectionStatus = myESP.loop();
timestamp = millis();
// Timers
updateHATimer.update();
systemCheckTimer.update();
// update the Rx Tx and ERR LEDs
showLEDs();
// do not continue unless we have a wifi connection
if (connectionStatus < WIFI_ONLY) {
myDebug("Waiting to connect to wifi...\n");
return;
}
// if first time connected to MQTT, send welcome start message
// which will send all the state values from HA back to the clock via MQTT and return the boottime
if ((!startMQTTsent) && (connectionStatus == FULL_CONNECTION)) {
myESP.sendStart();
startMQTTsent = true;
}
// if we received new data and flagged for pushing, do it
if (EMS_Sys_Status.emsRefreshed) {
EMS_Sys_Status.emsRefreshed = false;
publishValues();
}
/*
* Shower Logic
*/
if (Boiler_Status.shower_enabled) {
showerResetTimer.update(); // update Ticker
// if already in cold mode, ignore all this logic until we're out of the cold blast
if (!Boiler_Shower.isColdShot) {
// these values come from UBAMonitorFast - type 0x18) which is broadcasted every second so we're pretty accurate
Boiler_Shower.showerOn =
((EMS_Boiler.selBurnPow >= SHOWER_BURNPOWER_MIN) && (EMS_Boiler.selFlowTemp == 0) && EMS_Boiler.burnGas);
// is the shower on?
if (Boiler_Shower.showerOn) {
// if heater was off, start the timer
if (Boiler_Shower.timerStart == 0) {
Boiler_Shower.timerStart = timestamp;
Boiler_Shower.timerPause = 0; // remove any last pauses
Boiler_Shower.isColdShot = false;
Boiler_Shower.duration = 0;
myDebug("Shower: starting timer...\n");
} else {
// check if the shower has been on too long
if ((((timestamp - Boiler_Shower.timerStart) > SHOWER_MAX_DURATION) && !Boiler_Shower.isColdShot)
&& Boiler_Status.shower_timer) {
_showerColdShotStart();
showerResetTimer.start(); // start the timer for n seconds which will reset the water back to hot
}
}
} else { // shower is off
// if it just turned off, record the time as it could be a pause
if ((Boiler_Shower.timerStart != 0) && (Boiler_Shower.timerPause == 0)) {
Boiler_Shower.timerPause = timestamp;
myDebug("Shower: water has just turned off...\n");
} else {
// if shower has been off for longer than the wait time
if ((Boiler_Shower.timerPause != 0) && ((timestamp - Boiler_Shower.timerPause) > SHOWER_PAUSE_TIME)) {
// its over the wait period, so assume that the shower has finished and calculate the total time and publish
Boiler_Shower.duration = (Boiler_Shower.timerPause - Boiler_Shower.timerStart);
if (Boiler_Shower.duration > SHOWER_MIN_DURATION) {
char s[50];
sprintf(s,
"%d minutes and %d seconds",
(uint8_t)((Boiler_Shower.duration / (1000 * 60)) % 60),
(uint8_t)((Boiler_Shower.duration / 1000) % 60));
myDebug("Shower: finished, duration was %s\n", s);
myESP.publish(TOPIC_SHOWERTIME, s); // publish to HA
}
// reset
myDebug("Shower: resetting timers.\n");
Boiler_Shower.timerStart = 0;
Boiler_Shower.timerPause = 0;
_showerColdShotStop(); // turn heat back on in case its off
}
}
}
}
}
// yield to prevent watchdog from timing out
// if using delay() this is not needed, but confuses the Ticker library
yield();
}

727
src/ems.cpp Normal file
View File

@@ -0,0 +1,727 @@
/*
* ems.cpp
* handles all the EMS messages
* Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler
*/
#include "ems.h"
#include "emsuart.h"
#include <Arduino.h>
#include <ESPHelper.h>
#include <TimeLib.h>
_EMS_Sys_Status EMS_Sys_Status; // EMS Status
_EMS_TxTelegram EMS_TxTelegram; // Empty buffer for sending telegrams
// call back for handling Types
#define MAX_TYPECALLBACK 11
const _EMS_Types EMS_Types[MAX_TYPECALLBACK] =
{ {EMS_ID_BOILER, EMS_TYPE_UBAMonitorFast, "UBAMonitorFast", 36, _process_UBAMonitorFast},
{EMS_ID_BOILER, EMS_TYPE_UBAMonitorSlow, "UBAMonitorSlow", 28, _process_UBAMonitorSlow},
{EMS_ID_BOILER, EMS_TYPE_UBAMonitorWWMessage, "UBAMonitorWWMessage", 10, _process_UBAMonitorWWMessage},
{EMS_ID_BOILER, EMS_TYPE_UBAParameterWW, "UBAParameterWW", 10, _process_UBAParameterWW},
{EMS_ID_BOILER, EMS_TYPE_UBATotalUptimeMessage, "UBATotalUptimeMessage", 30, NULL},
{EMS_ID_BOILER, EMS_TYPE_UBAMaintenanceSettingsMessage, "UBAMaintenanceSettingsMessage", 30, NULL},
{EMS_ID_BOILER, EMS_TYPE_UBAParametersMessage, "UBAParametersMessage", 30, NULL},
{EMS_ID_BOILER, EMS_TYPE_UBAMaintenanceStatusMessage, "UBAMaintenanceStatusMessage", 30, NULL},
{EMS_ID_THERMOSTAT, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", 3, _process_RC20StatusMessage},
{EMS_ID_THERMOSTAT, EMS_TYPE_RC20Time, "RC20Time", 20, _process_RC20Time},
{EMS_ID_THERMOSTAT, EMS_TYPE_RC20Temperature, "RC20Temperature", 10, _process_RC20Temperature}
};
// reserve space for the data we collect from the Boiler and Thermostat
_EMS_Boiler EMS_Boiler;
_EMS_Thermostat EMS_Thermostat;
// CRC lookup table with poly 12 for faster checking
const uint8_t ems_crc_table[] =
{ 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, 0x22, 0x24,
0x26, 0x28, 0x2A, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E, 0x40, 0x42, 0x44, 0x46, 0x48, 0x4A,
0x4C, 0x4E, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5A, 0x5C, 0x5E, 0x60, 0x62, 0x64, 0x66, 0x68, 0x6A, 0x6C, 0x6E, 0x70,
0x72, 0x74, 0x76, 0x78, 0x7A, 0x7C, 0x7E, 0x80, 0x82, 0x84, 0x86, 0x88, 0x8A, 0x8C, 0x8E, 0x90, 0x92, 0x94, 0x96,
0x98, 0x9A, 0x9C, 0x9E, 0xA0, 0xA2, 0xA4, 0xA6, 0xA8, 0xAA, 0xAC, 0xAE, 0xB0, 0xB2, 0xB4, 0xB6, 0xB8, 0xBA, 0xBC,
0xBE, 0xC0, 0xC2, 0xC4, 0xC6, 0xC8, 0xCA, 0xCC, 0xCE, 0xD0, 0xD2, 0xD4, 0xD6, 0xD8, 0xDA, 0xDC, 0xDE, 0xE0, 0xE2,
0xE4, 0xE6, 0xE8, 0xEA, 0xEC, 0xEE, 0xF0, 0xF2, 0xF4, 0xF6, 0xF8, 0xFA, 0xFC, 0xFE, 0x19, 0x1B, 0x1D, 0x1F, 0x11,
0x13, 0x15, 0x17, 0x09, 0x0B, 0x0D, 0x0F, 0x01, 0x03, 0x05, 0x07, 0x39, 0x3B, 0x3D, 0x3F, 0x31, 0x33, 0x35, 0x37,
0x29, 0x2B, 0x2D, 0x2F, 0x21, 0x23, 0x25, 0x27, 0x59, 0x5B, 0x5D, 0x5F, 0x51, 0x53, 0x55, 0x57, 0x49, 0x4B, 0x4D,
0x4F, 0x41, 0x43, 0x45, 0x47, 0x79, 0x7B, 0x7D, 0x7F, 0x71, 0x73, 0x75, 0x77, 0x69, 0x6B, 0x6D, 0x6F, 0x61, 0x63,
0x65, 0x67, 0x99, 0x9B, 0x9D, 0x9F, 0x91, 0x93, 0x95, 0x97, 0x89, 0x8B, 0x8D, 0x8F, 0x81, 0x83, 0x85, 0x87, 0xB9,
0xBB, 0xBD, 0xBF, 0xB1, 0xB3, 0xB5, 0xB7, 0xA9, 0xAB, 0xAD, 0xAF, 0xA1, 0xA3, 0xA5, 0xA7, 0xD9, 0xDB, 0xDD, 0xDF,
0xD1, 0xD3, 0xD5, 0xD7, 0xC9, 0xCB, 0xCD, 0xCF, 0xC1, 0xC3, 0xC5, 0xC7, 0xF9, 0xFB, 0xFD, 0xFF, 0xF1, 0xF3, 0xF5,
0xF7, 0xE9, 0xEB, 0xED, 0xEF, 0xE1, 0xE3, 0xE5, 0xE7
};
extern ESPHelper myESP;
#define myDebug(x, ...) myESP.printf(x, ##__VA_ARGS__);
// constants timers
const uint64_t RX_READ_TIMEOUT = 5000; // in ms. 5 seconds timeout for read replies
const uint8_t RX_READ_TIMEOUT_COUNT = 4; // 4 retries before timeout
uint8_t emsLastRxCount = 0;
// init stats and counters and buffers
void ems_init() {
// overall status
EMS_Sys_Status.emsRxPgks = 0;
EMS_Sys_Status.emsTxPkgs = 0;
EMS_Sys_Status.emxCrcErr = 0;
EMS_Sys_Status.emsRxStatus = EMS_RX_IDLE;
EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE;
EMS_Sys_Status.emsLastPoll = 0;
EMS_Sys_Status.emsLastRx = 0;
EMS_Sys_Status.emsLastTx = 0;
EMS_Sys_Status.emsRefreshed = false;
EMS_Sys_Status.emsPollEnabled = false; // start up with Poll disabled
EMS_Sys_Status.emsThermostatEnabled = true; // there is a RCxx thermostat active
EMS_Sys_Status.emsLogVerbose = false; // Verbose logging is off
EMS_Thermostat.hour = 0;
EMS_Thermostat.minute = 0;
EMS_Thermostat.second = 0;
EMS_Thermostat.day = 0;
EMS_Thermostat.month = 0;
EMS_Thermostat.year = 0;
EMS_Boiler.wWActivated = false; // Warm Water activated
EMS_Boiler.wWSelTemp = 0; // Warm Water selected temperature
EMS_Boiler.wWCircPump = false; // Warm Water circulation pump Available
EMS_Boiler.wWDesiredTemp = 0; // Warm Water desired temperature
// UBAMonitorFast
EMS_Boiler.selFlowTemp = 0; // Selected flow temperature
EMS_Boiler.curFlowTemp = -1; // Current flow temperature
EMS_Boiler.retTemp = -1; // Return temperature
EMS_Boiler.burnGas = false; // Gas on/off
EMS_Boiler.fanWork = false; // Fan on/off
EMS_Boiler.ignWork = false; // Ignition on/off
EMS_Boiler.heatPmp = false; // Circulating pump on/off
EMS_Boiler.wWHeat = false; // 3-way valve on WW
EMS_Boiler.wWCirc = false; // Circulation on/off
EMS_Boiler.selBurnPow = 0; // Burner max power
EMS_Boiler.curBurnPow = 0; // Burner current power
EMS_Boiler.flameCurr = -1; // Flame current in micro amps
EMS_Boiler.sysPress = -1; // System pressure
// UBAMonitorSlow
EMS_Boiler.extTemp = -1; // Outside temperature
EMS_Boiler.boilTemp = -1; // Boiler temperature
EMS_Boiler.pumpMod = 0; // Pump modulation
EMS_Boiler.burnStarts = 0; // # burner restarts
EMS_Boiler.burnWorkMin = 0; // Total burner operating time
EMS_Boiler.heatWorkMin = 0; // Total heat operating time
// UBAMonitorWWMessage
EMS_Boiler.wWCurTmp = -1; // Warm Water current temperature:
EMS_Boiler.wWStarts = 0; // Warm Water # starts
EMS_Boiler.wWWorkM = 0; // Warm Water # minutes
EMS_Boiler.wWOneTime = false; // Warm Water one time function on/off
// init the Tx package
_initTxBuffer();
}
// init Tx Buffer
void _initTxBuffer() {
EMS_TxTelegram.length = 0;
EMS_TxTelegram.type = 0;
EMS_TxTelegram.dest = 0;
EMS_TxTelegram.offset = 0;
EMS_TxTelegram.length = 0;
EMS_TxTelegram.type_validate = 0;
EMS_TxTelegram.action = EMS_TX_NONE;
EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE;
emsLastRxCount = 0;
}
// Getters and Setters for parameters
void ems_setPoll(bool b) {
EMS_Sys_Status.emsPollEnabled = b;
myDebug("EMS Bus Poll is %s\n", EMS_Sys_Status.emsPollEnabled ? "enabled" : "disabled");
}
bool ems_getPoll() {
return EMS_Sys_Status.emsPollEnabled;
}
bool ems_getThermostatEnabled() {
return EMS_Sys_Status.emsThermostatEnabled;
}
void ems_setThermostatEnabled(bool b) {
EMS_Sys_Status.emsThermostatEnabled = b;
myDebug("Thermostat is %s\n", EMS_Sys_Status.emsThermostatEnabled ? "enabled" : "disabled");
}
bool ems_getLogVerbose() {
return EMS_Sys_Status.emsLogVerbose;
}
void ems_setLogVerbose(bool b) {
EMS_Sys_Status.emsLogVerbose = b;
myDebug("Verbose logging is %s.\n", EMS_Sys_Status.emsLogVerbose ? "on" : "off");
}
/*
* Calculate CRC checksum using lookup table
* len is length of data in bytes (including the CRC byte at end)
*/
uint8_t _crcCalculator(uint8_t * data, uint8_t len) {
uint8_t crc = 0;
// read data and stop before the CRC
for (uint8_t i = 0; i < len - 1; i++) {
crc = ems_crc_table[crc];
crc ^= data[i];
}
return crc;
}
// debug print a telegram to telnet console
// len is length in bytes including the CRC
void _debugPrintTelegram(const char * prefix, uint8_t * data, uint8_t len, const char * color) {
if (!EMS_Sys_Status.emsLogVerbose)
return;
bool crcok = (data[len - 1] == _crcCalculator(data, len));
if (crcok) {
myDebug(color)
} else {
myDebug(COLOR_RED);
}
time_t currentTime = now();
myDebug("[%02d:%02d:%02d] %s len=%02d, data: ", hour(currentTime), minute(currentTime), second(currentTime), prefix, len);
for (int i = 0; i < len; i++) {
myDebug("%02x ", data[i]);
}
myDebug("(%s) %s\n", crcok ? "OK" : "BAD", 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);
EMS_Sys_Status.emsTxStatus = EMS_TX_ACTIVE;
emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length);
EMS_Sys_Status.emsTxPkgs++;
EMS_Sys_Status.emsLastTx = millis();
// if it was a write command, check if we need to do a new read to validate the results
if (EMS_TxTelegram.action == EMS_TX_WRITE) {
// straight after the write check to see if we have to followup with a read to verify the write worked
// we do this by re-submitting the same telegram but this time as a write command (type,offset,length are still valid)
if (EMS_TxTelegram.type_validate != 0) {
EMS_TxTelegram.dest = EMS_TxTelegram.dest | 0x80; // add the 7th bit make sure its a read
_buildTxTelegram(1); // get a single value back, which is one byte
}
EMS_TxTelegram.action = EMS_TX_VALIDATE; // will start the validate next time a telegram is received
} else {
EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE; // nothing to send
}
}
/*
* parse the telegram message
* length is only data bytes, excluding the BRK
* Read commands are asynchronous as they're handled by the interrupt
* When we receive a Poll Request we need to send quickly
*/
void ems_parseTelegram(uint8_t * telegram, uint8_t length) {
// if we're waiting on a reponse from a read and it hasn't come, try again
if ((EMS_Sys_Status.emsTxStatus != EMS_TX_PENDING)
&& ((EMS_TxTelegram.action == EMS_TX_READ) || (EMS_TxTelegram.action == EMS_TX_VALIDATE))
&& ((millis() - EMS_Sys_Status.emsLastTx) > RX_READ_TIMEOUT)) {
if (emsLastRxCount++ >= RX_READ_TIMEOUT_COUNT) {
// give up
myDebug("Error! no send acknowledgement. Giving up.\n");
_initTxBuffer();
} else {
myDebug("Didn't receive acknowledgement so resending (attempt #%d/%d)...\n",
emsLastRxCount,
RX_READ_TIMEOUT_COUNT);
EMS_Sys_Status.emsTxStatus = EMS_TX_PENDING; // set to pending will trigger sending the same package again
}
}
// check if we just received one byte
// it could be a Poll request from the boiler which is 0x8B (0x0B | 0x80 to set 7th bit)
// or a return code like 0x01 or 0x04 from the last Write command
if (length == 1) {
uint8_t value = telegram[0]; // 1st byte
// check first for Poll
if (value == (EMS_ID_ME | 0x80)) {
// set the timestamp of the last poll, we use this to see if we have a connection to the boiler
EMS_Sys_Status.emsLastPoll = millis();
// do we have something to send? if so send it
if (EMS_Sys_Status.emsTxStatus == EMS_TX_PENDING) {
_ems_sendTelegram();
} else {
// nothing to send so just send a poll acknowledgement back
if (EMS_Sys_Status.emsPollEnabled) {
emsaurt_tx_poll();
}
}
// check if we're waiting on a response after a recent Write, which is a return code (0x01 for success or 0x04 for error)
} else if (EMS_TxTelegram.action == EMS_TX_WRITE) {
// TODO: need to tidy this piece up!
// a response from UBA after a write should be within a specific time period <100ms
/*
if (value == 0x01) {
emsaurt_tx_poll(); // send a poll acknowledgement
myDebug("Receiver of last write acknowledged OK, ready to validate...\n");
EMS_TxTelegram.action = EMS_TX_VALIDATE;
} else if (value == 0x04) {
EMS_TxTelegram.action = EMS_TX_NONE;
myDebug("Return value from write FAILED.\n");
emsaurt_tx_poll(); // send a poll acknowledgement
}
*/
}
return; // all done here, quit. if we haven't processes anything its a poll but not for us
}
// 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);
return;
}
// Assume at this point we have something that vaguely resembles a telegram
// see if we got a telegram as [src] [dest] [type] [offset] [data] [crc]
// so is at least 6 bytes long and the CRC checks out (which is last byte)
uint8_t crc = _crcCalculator(telegram, length);
if (telegram[length - 1] != crc) {
EMS_Sys_Status.emxCrcErr++;
_debugPrintTelegram("Corrupt telegram:", telegram, length, COLOR_RED);
} else {
// go and do the magic
_processType(telegram, length);
}
}
/*
* decipher the telegram packet
* length is only data bytes, excluding the BRK
*/
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
// if its an echo of ourselves from the master, ignore
if (src == EMS_ID_ME) {
_debugPrintTelegram("Telegram echo:", telegram, length, COLOR_BLUE);
}
// header
uint8_t dest = telegram[1];
uint8_t type = telegram[2];
uint8_t * data = telegram + 4; // data block starts at position 5
// for building a debug message
char s[100];
char color_s[20];
char src_s[20];
char dest_s[20];
// scan through known types
int i = 0;
bool typeFound = false;
while (i < MAX_TYPECALLBACK) {
if ((EMS_Types[i].src == src) && (EMS_Types[i].type == type)) {
// we have a match
typeFound = true;
// call callback to fetch the values from the telegram
// ignoring the return value for now
if ((EMS_Types[i].processType_cb) != (void *)NULL) {
(void)EMS_Types[i].processType_cb(data, length);
}
break;
}
i++;
}
if (src == EMS_ID_BOILER) {
strcpy(src_s, "Boiler");
} else if (src == EMS_ID_THERMOSTAT) {
strcpy(src_s, "Thermostat");
}
// was it sent specifically to us?
if (dest == EMS_ID_ME) {
strcpy(dest_s, "telegram for us");
strcpy(color_s, COLOR_YELLOW);
// did we actually ask for it from an earlier read/write request?
// note when we issue a read command the responder (dest) has to return a telegram back immediately
if ((EMS_TxTelegram.action == EMS_TX_READ) && (EMS_TxTelegram.type == type) && typeFound) {
// yes we were expecting this one one
EMS_Sys_Status.emsRxPgks++; // increment rx counter
EMS_Sys_Status.emsLastRx = millis();
EMS_TxTelegram.action = EMS_TX_NONE;
emsLastRxCount = 0; // reset retry count
}
// send Acknowledgement back to free the bus
emsaurt_tx_poll();
} else if (dest == EMS_ID_NONE) {
// it's probably just a broadcast
strcpy(dest_s, "broadcast");
strcpy(color_s, COLOR_GREEN);
} else {
// for someone else
strcpy(dest_s, "(not for us)");
strcpy(color_s, COLOR_MAGENTA);
}
// debug print
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);
}
// check to see if we're waiting on a specific value, either from a recent read or by chance a broadcast
// and then do the comparison
if ((EMS_TxTelegram.action == EMS_TX_VALIDATE) && (EMS_TxTelegram.type == type)) {
uint8_t offset;
// we're waiting for a read on our last write to compare the values
// if we have a special telegram package we sent to validate use that for comparison, otherwise assume its a simple 1-byte read
if (EMS_TxTelegram.type_validate == EMS_ID_NONE) {
offset = EMS_TxTelegram.offset; // special, use the offset from the last send
} else {
offset = 0;
}
// get the data at the position we wrote too
// do compare, when validating we always return a single value
if (EMS_TxTelegram.checkValue == data[offset]) {
myDebug("Last write operation successful (value=%d, offset=%d)\n", EMS_TxTelegram.checkValue, offset);
EMS_Sys_Status.emsRefreshed = true; // flag this so values are sent back to HA via MQTT
EMS_TxTelegram.action = EMS_TX_NONE; // no more sends
} else {
myDebug("Last write operation failed. (value=%d, got=%d, offset=%d)\n",
EMS_TxTelegram.checkValue,
data[offset],
offset);
}
}
}
/*
* UBAParameterWW - type 0x33 - warm water parameters
*/
bool _process_UBAParameterWW(uint8_t * data, uint8_t length) {
EMS_Boiler.wWSelTemp = data[2];
EMS_Boiler.wWActivated = (data[1] == 0xFF);
EMS_Boiler.wWCircPump = (data[6] == 0xFF);
EMS_Boiler.wWDesiredTemp = data[8];
return true;
}
/*
* UBAMonitorWWMessage - type 0x34 - warm water monitor. 19 bytes long
*/
bool _process_UBAMonitorWWMessage(uint8_t * data, uint8_t length) {
EMS_Boiler.wWCurTmp = _toFloat(1, data);
EMS_Boiler.wWStarts = _toLong(13, data);
EMS_Boiler.wWWorkM = _toLong(10, data);
EMS_Boiler.wWOneTime = bitRead(data[5], 1);
return true;
}
/*
* UBAMonitorFast - type 0x18 - central heating monitor part 1 (25 bytes long)
*/
bool _process_UBAMonitorFast(uint8_t * data, uint8_t length) {
EMS_Boiler.selFlowTemp = data[0];
EMS_Boiler.curFlowTemp = _toFloat(1, data);
EMS_Boiler.retTemp = _toFloat(13, data);
uint8_t v = data[7];
EMS_Boiler.burnGas = bitRead(v, 0);
EMS_Boiler.fanWork = bitRead(v, 2);
EMS_Boiler.ignWork = bitRead(v, 3);
EMS_Boiler.heatPmp = bitRead(v, 5);
EMS_Boiler.wWHeat = bitRead(v, 6);
EMS_Boiler.wWCirc = bitRead(v, 7);
EMS_Boiler.selBurnPow = data[3];
EMS_Boiler.curBurnPow = data[4];
EMS_Boiler.flameCurr = _toFloat(15, data);
if (data[17] == 0xFF) { // missing value for system pressure
EMS_Boiler.sysPress = 0;
} else {
EMS_Boiler.sysPress = (((float)data[17]) / (float)10);
}
return true;
}
/*
* UBAMonitorSlow - type 0x19 - central heating monitor part 2 (27 bytes long)
*/
bool _process_UBAMonitorSlow(uint8_t * data, uint8_t length) {
EMS_Boiler.extTemp = _toFloat(0, data); // 0x8000 if not available
EMS_Boiler.boilTemp = _toFloat(2, data); // 0x8000 if not available
EMS_Boiler.pumpMod = data[13];
EMS_Boiler.burnStarts = _toLong(10, data);
EMS_Boiler.burnWorkMin = _toLong(13, data);
EMS_Boiler.heatWorkMin = _toLong(19, data);
return true;
}
/*
* RC20StatusMessage - type 0x91 - data from the RC20 thermostat (0x17) - 15 bytes long
*/
bool _process_RC20StatusMessage(uint8_t * data, uint8_t length) {
EMS_Thermostat.setpoint_roomTemp = ((float)data[1]) / (float)2;
EMS_Thermostat.curr_roomTemp = _toFloat(2, data);
// set the updated flag to trigger a send back to HA
EMS_Sys_Status.emsRefreshed = true;
return true;
}
/*
* RC20Temperature - type 0xa8 - set temp value from the RC20 thermostat (0x17)
* Special case as we only want to store the value after a write command
*/
bool _process_RC20Temperature(uint8_t * data, uint8_t length) {
// only interested in the single byte response we send to validate a temp change
if (length == 6) {
EMS_Thermostat.setpoint_roomTemp = ((float)data[0]) / (float)2;
EMS_Sys_Status.emsRefreshed = true; // set the updated flag to trigger a send back to HA
}
return true;
}
/*
* process_RC20Time - type 0x06 - date and time from the RC20 thermostat (0x17) - 14 bytes long
*/
bool _process_RC20Time(uint8_t * data, uint8_t length) {
EMS_Thermostat.hour = data[2];
EMS_Thermostat.minute = data[4];
EMS_Thermostat.second = data[5];
EMS_Thermostat.day = data[3];
EMS_Thermostat.month = data[1];
EMS_Thermostat.year = data[0];
// now we have the time, set our ESP code to it - wil be replaced with NTP
setTime(EMS_Thermostat.hour,
EMS_Thermostat.minute,
EMS_Thermostat.second,
EMS_Thermostat.day,
EMS_Thermostat.month,
EMS_Thermostat.year + 2000);
return true;
}
/*
* Build the telegram, which includes a single byte followed by the CRC at the end
*/
void _buildTxTelegram(uint8_t data_value) {
// header
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
// data
EMS_TxTelegram.data[4] = data_value; // value, can be size
// crc
EMS_TxTelegram.data[5] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length);
EMS_Sys_Status.emsTxStatus = EMS_TX_PENDING; // armed and ready to send
}
/*
* Send a command to Tx to Read from another device
* Read commands when sent must to responded too by the destination (target) immediately
* usually within a 10ms window
*/
void ems_doReadCommand(uint8_t type) {
if (type == EMS_TYPE_NONE)
return; // not a valid type, quit
// check if there is already something in the queue
if (EMS_Sys_Status.emsTxStatus == EMS_TX_PENDING) { // send is already pending
myDebug("Cannot write - already a telegram pending send.\n");
return;
}
uint8_t dest, size;
// scan through known types
bool typeFound = false;
int i = 0;
while (i < MAX_TYPECALLBACK) {
if (EMS_Types[i].type == type) {
typeFound = true; // we have a match
// call callback to fetch the values from the telegram
dest = EMS_Types[i].src;
size = EMS_Types[i].size;
break;
}
i++;
}
// for adhoc calls use default values
if (!typeFound) {
dest = EMS_ID_BOILER; // default is boiler
size = 1;
myDebug("Requesting type (0x%02x) from dest 0x%02x for %d bytes\n", type, dest, size);
} else {
myDebug("Requesting type %s(0x%02x) from dest 0x%02x for %d bytes\n", EMS_Types[i].typeString, type, dest, size);
}
EMS_TxTelegram.action = EMS_TX_READ; // read command
EMS_TxTelegram.dest = dest | 0x80; // set 7th bit to indicate a read
EMS_TxTelegram.offset = 0; // 0 for all data
EMS_TxTelegram.length = 6; // is always 6 bytes long (including CRC at end)
EMS_TxTelegram.type = type;
_buildTxTelegram(size); // we send the # bytes we want back
}
/*
* Set the temperature of the thermostat
*/
void ems_setThermostatTemp(float temperature) {
// check if there is already something in the queue
if (EMS_Sys_Status.emsTxStatus == EMS_TX_PENDING) { // send is already pending
myDebug("Cannot write - already a telegram pending send.\n");
return;
}
char s[10];
myDebug("Setting thermostat temperature to %s C\n", _float_to_char(s, temperature));
EMS_TxTelegram.action = EMS_TX_WRITE; // write command
EMS_TxTelegram.dest = EMS_ID_THERMOSTAT;
EMS_TxTelegram.type = EMS_TYPE_RC20Temperature;
EMS_TxTelegram.offset = 0x1C; // manual setpoint temperature
EMS_TxTelegram.length = 6; // includes CRC
EMS_TxTelegram.checkValue = (uint8_t)((float)temperature * (float)2); // value to compare against. must be a single int
// post call is RC20StatusMessage to fetch temps and send to HA
EMS_TxTelegram.type_validate = EMS_TYPE_RC20Temperature;
_buildTxTelegram(EMS_TxTelegram.checkValue);
}
/*
* Set the warm water temperature
*/
void ems_setWarmWaterTemp(uint8_t temperature) {
// check if there is already something in the queue
if (EMS_Sys_Status.emsTxStatus == EMS_TX_PENDING) { // send is already pending
myDebug("Cannot write - already a telegram pending send.\n");
return;
}
myDebug("Setting boiler warm water temperature to %d C\n", temperature);
EMS_TxTelegram.action = EMS_TX_WRITE; // write command
EMS_TxTelegram.dest = EMS_ID_BOILER;
EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW;
EMS_TxTelegram.offset = 0x02; // Temperature
EMS_TxTelegram.length = 6; // includes CRC
EMS_TxTelegram.checkValue = temperature; // value to compare against. must be a single int
EMS_TxTelegram.type_validate = EMS_ID_NONE; // this means don't send and we'll pick up the data from the next broadcast
_buildTxTelegram(temperature);
}
/*
* Activate / De-activate the Warm Water
* true = on, false = off
*/
void ems_setWarmWaterActivated(bool activated) {
// check if there is already something in the queue
if (EMS_Sys_Status.emsTxStatus == EMS_TX_PENDING) { // send is already pending
myDebug("Cannot write - already a telegram pending send.\n");
return;
}
myDebug("Setting boiler warm water to %s\n", activated ? "on" : "off");
EMS_TxTelegram.action = EMS_TX_WRITE; // write command
EMS_TxTelegram.dest = EMS_ID_BOILER;
EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW;
EMS_TxTelegram.offset = 0x01; // WW activation
EMS_TxTelegram.length = 6; // includes CRC
EMS_TxTelegram.type_validate = EMS_ID_NONE; // this means don't send and we'll pick up the data from the next broadcast
if (activated) {
EMS_TxTelegram.checkValue = 0xFF; // the EMS value for on
_buildTxTelegram(0xFF);
} else {
EMS_TxTelegram.checkValue = 0x00; // the EMS value for off
_buildTxTelegram(0x00);
}
}
/*
* Helper functions for formatting and converting floats
*/
// function to turn a telegram int (2 bytes) to a float
float _toFloat(uint8_t i, uint8_t * data) {
if ((data[i] == 0x80) && (data[i + 1] == 0)) // 0x8000 is used when sensor is missing
return (float)-1;
return ((float)(((data[i] << 8) + data[i + 1]))) / 10;
}
// function to turn a telegram long (3 bytes) to a long int
uint16_t _toLong(uint8_t i, uint8_t * data) {
return (((data[i]) << 16) + ((data[i + 1]) << 8) + (data[i + 2]));
}
// convert float to char
char * _float_to_char(char * a, float f, uint8_t precision) {
long p[] = {0, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
char * ret = a;
// check for 0x8000 (sensor missing)
if (f == -1) {
strcpy(ret, "<n/a>");
} else {
long whole = (long)f;
itoa(whole, a, 10);
while (*a != '\0')
a++;
*a++ = '.';
long decimal = abs((long)((f - whole) * p[precision]));
itoa(decimal, a, 10);
}
return ret;
}

191
src/ems.h Normal file
View File

@@ -0,0 +1,191 @@
/*
* Header file for EMS.cpp
*/
#ifndef __EMS_H
#define __EMS_H
#include <Arduino.h>
// EMS IDs
#define EMS_ID_THERMOSTAT 0x17 // x17=RC20, x10=RC30 (Moduline 300)
#define EMS_ID_NONE 0x00 // Fixed - used as a dest in broadcast messages
#define EMS_ID_BOILER 0x08 // Fixed - also known as MC10.
#define EMS_ID_ME 0x0B // Fixed - our device, hardcoded as "Service Key"
// EMS Telegram Types
#define EMS_TYPE_NONE 0x00 // none
#define EMS_TYPE_UBAMonitorFast 0x18 // is an automatic monitor broadcast
#define EMS_TYPE_UBAMonitorSlow 0x19 // is an automatic monitor broadcast
#define EMS_TYPE_UBAMonitorWWMessage 0x34 // is an automatic monitor broadcast
#define EMS_TYPE_UBAParameterWW 0x33 // is an automatic monitor broadcast
#define EMS_TYPE_UBATotalUptimeMessage 0x14
#define EMS_TYPE_UBAMaintenanceSettingsMessage 0x15
#define EMS_TYPE_UBAParametersMessage 0x16
#define EMS_TYPE_UBAMaintenanceStatusMessage 0x1c
// EMS Telegram types from Thermostat
// types 1A and 35 and used for errors from Thermostat
#define EMS_TYPE_RC20StatusMessage 0x91
#define EMS_TYPE_RC20Time 0x06 // is an automatic monitor broadcast
#define EMS_TYPE_RC20Temperature 0xA8
#define EMS_TYPE_RCOutdoorTempMessage 0xa3 // we can ignore
#define EMS_TX_MAXBUFFERSIZE 128 // max size of the buffer. packets are 32 bits
/* EMS UART transfer status */
typedef enum {
EMS_RX_IDLE,
EMS_RX_ACTIVE // Rx package is being sent
} _EMS_RX_STATUS;
typedef enum {
EMS_TX_IDLE,
EMS_TX_PENDING, // got Tx package to send, waiting for next Poll to send
EMS_TX_ACTIVE // Tx package being sent, no break sent
} _EMS_TX_STATUS;
typedef enum {
EMS_TX_NONE,
EMS_TX_READ, // doing a read request
EMS_TX_WRITE, // doing a write request
EMS_TX_VALIDATE // do a validate after a write
} _EMS_TX_ACTION;
// status/counters since last power on
typedef struct {
_EMS_RX_STATUS emsRxStatus;
_EMS_TX_STATUS emsTxStatus;
uint16_t emsRxPgks; // received
uint16_t emsTxPkgs; // sent
uint16_t emxCrcErr; // CRC errors
bool emsPollEnabled; // flag enable the response to poll messages
bool emsThermostatEnabled; // if there is a RCxx thermostat active
bool emsLogVerbose; // Verbose logging
unsigned long emsLastPoll; // in ms, last time we received a poll
unsigned long emsLastRx; // timings
unsigned long emsLastTx; // timings
bool emsRefreshed; // fresh data, needs to be pushed out to MQTT
} _EMS_Sys_Status;
// The Tx send package
typedef struct {
_EMS_TX_ACTION action; // read or write
uint8_t dest;
uint8_t type;
uint8_t offset;
uint8_t length;
uint8_t checkValue; // value to validate against
uint8_t type_validate; // type to call after a successful Write command
uint8_t data[EMS_TX_MAXBUFFERSIZE];
} _EMS_TxTelegram;
/*
* Telegram package defintions
*/
typedef struct {
// UBAParameterWW
bool wWActivated; // Warm Water activated
uint8_t wWSelTemp; // Warm Water selected temperature
bool wWCircPump; // Warm Water circulation pump Available
uint8_t wWDesiredTemp; // Warm Water desired temperature
// UBAMonitorFast
uint8_t selFlowTemp; // Selected flow temperature
float curFlowTemp; // Current flow temperature
float retTemp; // Return temperature
bool burnGas; // Gas on/off
bool fanWork; // Fan on/off
bool ignWork; // Ignition on/off
bool heatPmp; // Circulating pump on/off
bool wWHeat; // 3-way valve on WW
bool wWCirc; // Circulation on/off
uint8_t selBurnPow; // Burner max power
uint8_t curBurnPow; // Burner current power
float flameCurr; // Flame current in micro amps
float sysPress; // System pressure
// UBAMonitorSlow
float extTemp; // Outside temperature
float boilTemp; // Boiler temperature
uint8_t pumpMod; // Pump modulation
uint16_t burnStarts; // # burner restarts
uint16_t burnWorkMin; // Total burner operating time
uint16_t heatWorkMin; // Total heat operating time
// UBAMonitorWWMessage
float wWCurTmp; // Warm Water current temperature:
uint32_t wWStarts; // Warm Water # starts
uint32_t wWWorkM; // Warm Water # minutes
bool wWOneTime; // Warm Water one time function on/off
} _EMS_Boiler;
// RC20 data
typedef struct {
float setpoint_roomTemp;
float curr_roomTemp;
uint8_t hour;
uint8_t minute;
uint8_t second;
uint8_t day;
uint8_t month;
uint8_t year;
} _EMS_Thermostat;
// call back function signature
typedef bool (*EMS_processType_cb)(uint8_t * data, uint8_t length);
// Definition for each type, including the relative callback function
typedef struct {
uint8_t src;
uint8_t type;
const char typeString[50];
uint8_t size; // size of telegram, excluding the 4-byte header and crc
EMS_processType_cb processType_cb;
} _EMS_Types;
// function definitions
extern void ems_parseTelegram(uint8_t * telegram, uint8_t len);
void ems_init();
void ems_doReadCommand(uint8_t type);
void ems_setThermostatTemp(float temp);
void ems_setWarmWaterTemp(uint8_t temperature);
void ems_setWarmWaterActivated(bool activated);
void ems_setPoll(bool b);
bool ems_getPoll();
bool ems_getThermostatEnabled();
void ems_setThermostatEnabled(bool b);
bool ems_getLogVerbose();
void ems_setLogVerbose(bool b);
// private functions
uint8_t _crcCalculator(uint8_t * data, uint8_t len);
void _processType(uint8_t * telegram, uint8_t length);
void _initTxBuffer();
void _buildTxTelegram(uint8_t data_value);
void _debugPrintPackage(const char * prefix, uint8_t * data, uint8_t len, const char * color);
// callbacks per type
bool _process_UBAMonitorFast(uint8_t * data, uint8_t length);
bool _process_UBAMonitorSlow(uint8_t * data, uint8_t length);
bool _process_UBAMonitorWWMessage(uint8_t * data, uint8_t length);
bool _process_UBAParameterWW(uint8_t * data, uint8_t length);
bool _process_RC20StatusMessage(uint8_t * data, uint8_t length);
bool _process_RC20Time(uint8_t * data, uint8_t length);
bool _process_RC20Temperature(uint8_t * data, uint8_t length);
// helper functions
float _toFloat(uint8_t i, uint8_t * data);
uint16_t _toLong(uint8_t i, uint8_t * data);
char * _float_to_char(char * a, float f, uint8_t precision = 1);
// global so can referenced in other classes
extern _EMS_Sys_Status EMS_Sys_Status;
extern _EMS_TxTelegram EMS_TxTelegram;
extern _EMS_Boiler EMS_Boiler;
extern _EMS_Thermostat EMS_Thermostat;
#endif

182
src/emsuart.cpp Normal file
View File

@@ -0,0 +1,182 @@
/*
* emsuart.cpp
* The low level UART code for ESP8266
* Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler
*/
#include "emsuart.h"
#include "ems.h"
#include "ets_sys.h"
#include "osapi.h"
#include <Arduino.h>
#include <user_interface.h>
_EMSRxBuf * pEMSRxBuf;
_EMSRxBuf * paEMSRxBuf[EMS_MAXBUFFERS];
uint8_t emsRxBufIdx = 0;
// queues
os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen];
//
// Main interrupt handler
// Important: do not use ICACHE_FLASH_ATTR !
//
static void emsuart_rx_intr_handler(void * para) {
static uint16_t length;
static uint8_t uart_buffer[EMS_MAXBUFFERSIZE];
// is a new buffer? if so init the thing
if (EMS_Sys_Status.emsRxStatus == EMS_RX_IDLE) {
EMS_Sys_Status.emsRxStatus = EMS_RX_ACTIVE; // status set to active
length = 0;
}
// fill IRQ buffer, by emptying FIFO
if (U0IS & ((1 << UIFF) | (1 << UITO) | (1 << UIBD))) {
// get data from Rx
while ((USS(EMSUART_UART) >> USRXC) & 0xFF) {
uart_buffer[length++] = USF(EMSUART_UART);
}
// clear Rx FIFO full and Rx FIFO timeout interrupts
U0IC = (1 << UIFF);
U0IC = (1 << UITO);
}
// BREAK detection = End of EMS data block
if (USIS(EMSUART_UART) & ((1 << UIBD))) {
// disable all interrupts and clear them
ETS_UART_INTR_DISABLE();
U0IC = (1 << UIBD); // INT clear the BREAK detect interrupt
// copy data into transfer buffer
pEMSRxBuf->writePtr = length;
os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, length);
// set the status flag stating BRK has been received and we can start a new package
EMS_Sys_Status.emsRxStatus = EMS_RX_IDLE;
// call emsuart_recvTask() at next opportunity
system_os_post(EMSUART_recvTaskPrio, 0, 0);
// re-enable UART interrupts
ETS_UART_INTR_ENABLE();
}
}
/*
* system task triggered on BRK interrupt
* Read commands are all asynchronous
*/
static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events) {
// get next free EMS Receive buffer
_EMSRxBuf * pCurrent = pEMSRxBuf;
pEMSRxBuf = paEMSRxBuf[++emsRxBufIdx % EMS_MAXBUFFERS];
// transmit EMS buffer, excluding the BRK
if (pCurrent->writePtr > 1) {
ems_parseTelegram((uint8_t *)pCurrent->buffer, (pCurrent->writePtr) - 1);
}
}
/*
* init UART0
* This is low level ESP8266 code to manually configure the UART driver
*/
void ICACHE_FLASH_ATTR emsuart_init() {
ETS_UART_INTR_DISABLE();
ETS_UART_INTR_ATTACH(NULL, NULL);
// allocate and preset EMS Receive buffers
for (int i = 0; i < EMS_MAXBUFFERS; i++) {
_EMSRxBuf * p = (_EMSRxBuf *)malloc(sizeof(_EMSRxBuf));
paEMSRxBuf[i] = p;
}
pEMSRxBuf = paEMSRxBuf[0]; // preset EMS Rx Buffer
// pin settings
PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0TXD_U);
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0RXD);
PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0RXD_U);
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD);
// set 9600, 8 bits, no parity check, 1 stop bit
USD(EMSUART_UART) = (ESP8266_CLOCK / EMSUART_BAUD);
USC0(EMSUART_UART) = EMSUART_CONFIG; // 8N1
// flush everything left over in buffer, this clears both rx and tx FIFOs
uint32_t tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask
USC0(EMSUART_UART) |= (tmp); // set bits
USC0(EMSUART_UART) &= ~(tmp); // clear bits
// conf 1 params
// UCTOE = RX TimeOut enable (1)
// UCTOT = RX TimeOut Threshold (7bit) = want this when no more data after 2 characters. (default was 2)
// UCFFT = RX FIFO Full Threshold (7 bit) = want this to be 31 for 32 bytes of buffer. (default was 127).
USC1(EMSUART_UART) = 0; // reset config first
USC1(EMSUART_UART) = (31 << UCFFT) | (0x02 << UCTOT) | (1 << UCTOE); // enable interupts
// set interrupts for triggers
USIC(EMSUART_UART) = 0xffff; // clear all interupts
USIE(EMSUART_UART) = 0; // disable all interrupts
// enable rx break, fifo full and timeout.
// not frame error UIFR or overflow UIOF. Frame errors are too frequent
// and overflow never happens because our buffer is only max 32 bytes
USIE(EMSUART_UART) = (1 << UIBD) | (1 << UIFF) | (1 << UITO);
// set up interrupt callbacks for Rx and Tx
system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen);
//system_os_task(emsuart_sendTask, sendTaskPrio, sendTaskQueue, sendTaskQueueLen);
// disable esp debug which will go to Tx and mess up the line
// system_set_os_print(0); // https://github.com/espruino/Espruino/issues/655
ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, NULL);
ETS_UART_INTR_ENABLE();
// when all ready swap RX and TX to use GPIO13 (D7) and GPIO15 (D8) respectively
system_uart_swap();
}
/*
* Send a BRK signal
* Which is a 11-bit set of zero's (11 cycles)
*/
void ICACHE_FLASH_ATTR emsuart_tx_brk() {
// must make sure Tx FIFO is empty
while (((USS(EMSUART_UART) >> USTXC) & 0xff) != 0)
;
uint32_t tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask
USC0(EMSUART_UART) |= (tmp); // set bits
USC0(EMSUART_UART) &= ~(tmp); // clear bits
// To create a 11-bit <BRK> we set TXD_BRK bit so the break signal will automatically be sent when the tx fifo is empty
USC0(EMSUART_UART) |= (1 << UCBRK); // set bit
delayMicroseconds(EMS_TX_BRK_WAIT);
USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear bit
}
/*
* Send to tx, ending with a BRK
*/
void ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) {
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
}
emsuart_tx_brk();
}
/*
* Send the Poll (our ID) to Tx as a single byte and ending with a <BRK>
*/
void ICACHE_FLASH_ATTR emsaurt_tx_poll() {
USF(EMSUART_UART) = EMS_ID_ME;
emsuart_tx_brk();
}

36
src/emsuart.h Normal file
View File

@@ -0,0 +1,36 @@
/*
* emsuart.h
* Header file for emsuart.cpp
* Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler
*/
#ifndef __EMSUART_H
#define __EMSUART_H
#include <Arduino.h>
#define EMSUART_UART 0 // UART 0 - there is only one on the esp8266
#define EMSUART_CONFIG 0x1c // 8N1 (8 bits, no stop bits, 1 parity)
#define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit
#define EMS_MAXBUFFERS 4 // 4 buffers for circular filling to avoid collisions
#define EMS_MAXBUFFERSIZE 128 // max size of the buffer. packets are 32 bits
// for how long we drop the Tx signal to create a 11-bit Break of zeros
// At 9600 baud, 11 bits will be 1144 microseconds
// the BRK from UBA is roughly 1.039ms, so accounting for hardware lag use 2078 (for half-duplex) - 8 (lag)
#define EMS_TX_BRK_WAIT 2070
#define EMSUART_recvTaskPrio 1
#define EMSUART_recvTaskQueueLen 64
typedef struct {
int16_t writePtr;
uint8_t buffer[EMS_MAXBUFFERSIZE];
} _EMSRxBuf;
void ICACHE_FLASH_ATTR emsuart_init();
void ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len);
void ICACHE_FLASH_ATTR emsaurt_tx_poll();
void ICACHE_FLASH_ATTR emsuart_tx_brk();
#endif