mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-09 09:19:51 +03:00
603 lines
23 KiB
C++
603 lines
23 KiB
C++
/*
|
|
* Boiler Project
|
|
* Paul Derbyshire - May 2018 - https://github.com/proddy/EMS-ESP-Boiler
|
|
*
|
|
* See Readme for Acknowledgments
|
|
*/
|
|
|
|
// local libraries
|
|
#include "ESPHelper.h"
|
|
#include "ems.h"
|
|
#include "emsuart.h"
|
|
|
|
// public libraries
|
|
#include <ArduinoJson.h>
|
|
#include <Ticker.h> // https://github.com/esp8266/Arduino/tree/master/libraries/Ticker
|
|
|
|
// these are set as -D build flags. If you're not using PlatformIO hard code them
|
|
//#define WIFI_SSID "<my_ssid>"
|
|
//#define WIFI_PASSWORD "<my_password>"
|
|
//#define MQTT_IP "<broker_ip>"
|
|
//#define MQTT_USER "<broker_username>"
|
|
//#define MQTT_PASS "<broker_password>"
|
|
|
|
// private function prototypes
|
|
void heartbeat();
|
|
void systemCheck();
|
|
void publishValues();
|
|
void _showerColdShotStart();
|
|
void _showerColdShotStop();
|
|
void _toggleHeartbeat();
|
|
|
|
// timers
|
|
Ticker publishValuesTimer;
|
|
Ticker systemCheckTimer;
|
|
Ticker heartbeatTimer;
|
|
Ticker showerResetTimer;
|
|
#define publishValuesTime 300 // every 5 mins post HA values
|
|
#define systemCheckTime 10 // every 10 seconds check if Boiler is online
|
|
#define heartbeatTime 1 // every second blink heartbeat LED
|
|
|
|
// 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" \
|
|
"* P=publish stats to MQTT\n\r" \
|
|
"* p=toggle Poll response (for debugging)\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, some examples:\n\r" \
|
|
"* from Boiler: 33=UBAParameterWW, 18=UBAMonitorFast, 19=UBAMonitorSlow, 34=UBAMonitorWWMessage\n\r" \
|
|
"* from Thermostat: 91=RC20StatusMessage, A8=RC20Temperature, 6=RC20Time, 2=Version\n\r" \
|
|
"* t [n] set thermostat temperature to n\n\r" \
|
|
"* m [n] set thermostat mode (0=low, 1=manual, 2=clock)\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_TEMP MQTT_BOILER "thermostat_temp" // for received thermostat temp changes
|
|
#define TOPIC_THERMOSTAT_CURRTEMP MQTT_BOILER "thermostat_currtemp" // current temperature
|
|
#define TOPIC_THERMOSTAT_SELTEMP MQTT_BOILER "thermostat_seltemp" // selected temperature
|
|
|
|
#define TOPIC_BOILER_DATA MQTT_BOILER "boiler_data" // for sending boiler values
|
|
#define TOPIC_SHOWERTIME MQTT_BOILER "showertime" // for sending shower time results
|
|
|
|
// thermostat support, shower timing and shower alert all enabled
|
|
#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;
|
|
|
|
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, // this is the default, change if using another port
|
|
.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
|
|
const unsigned long POLL_TIMEOUT_ERR = 10000; // if no signal from boiler for last 10 seconds, assume its offline
|
|
bool heartbeatEnabled = 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");
|
|
myDebug(" Boiler pump: %s\n", EMS_Boiler.heatPmp ? "on" : "off");
|
|
myDebug(" Fan: %s\n", EMS_Boiler.fanWork ? "on" : "off");
|
|
myDebug(" Ignition: %s\n", EMS_Boiler.ignWork ? "on" : "off");
|
|
myDebug(" Circulation pump: %s\n", EMS_Boiler.wWCirc ? "on" : "off");
|
|
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\n", _float_to_char(s, EMS_Thermostat.setpoint_roomTemp));
|
|
myDebug(" Current room temperature is %s C\n", _float_to_char(s, EMS_Thermostat.curr_roomTemp));
|
|
myDebug(" Mode is set to ");
|
|
if (EMS_Thermostat.mode == 0) {
|
|
myDebug("low\n");
|
|
} else if (EMS_Thermostat.mode == 1) {
|
|
myDebug("manual\n");
|
|
} else if (EMS_Thermostat.mode == 2) {
|
|
myDebug("clock/auto\n");
|
|
} else {
|
|
myDebug("<unknown>\n");
|
|
}
|
|
}
|
|
|
|
// show the Shower Info
|
|
if (Boiler_Status.shower_enabled) {
|
|
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() {
|
|
myDebug("Publishing data to MQTT topics\n");
|
|
|
|
char s[20]; // for formatting strings
|
|
|
|
// Boiler values as one JSON object
|
|
StaticJsonBuffer<512> jsonBuffer;
|
|
char data[512];
|
|
JsonObject & root = jsonBuffer.createObject();
|
|
root["wWCurTmp"] = _float_to_char(s, EMS_Boiler.wWCurTmp);
|
|
root["wWHeat"] = EMS_Boiler.wWHeat ? "on" : "off";
|
|
root["curFlowTemp"] = _float_to_char(s, EMS_Boiler.curFlowTemp);
|
|
root["retTemp"] = _float_to_char(s, EMS_Boiler.retTemp);
|
|
root["burnGas"] = EMS_Boiler.burnGas ? "on" : "off";
|
|
root["heatPmp"] = EMS_Boiler.heatPmp ? "on" : "off";
|
|
root["fanWork"] = EMS_Boiler.fanWork ? "on" : "off";
|
|
root["ignWork"] = EMS_Boiler.ignWork ? "on" : "off";
|
|
root["wWCirc"] = EMS_Boiler.wWCirc ? "on" : "off";
|
|
root["selBurnPow"] = (String)EMS_Boiler.selBurnPow;
|
|
root["curBurnPow"] = (String)EMS_Boiler.curBurnPow;
|
|
root["sysPress"] = _float_to_char(s, EMS_Boiler.sysPress);
|
|
root["boilTemp"] = _float_to_char(s, EMS_Boiler.boilTemp);
|
|
root["pumpMod"] = (String)EMS_Boiler.pumpMod;
|
|
|
|
root.printTo(data, root.measureLength() + 1);
|
|
myESP.publish(TOPIC_BOILER_DATA, data);
|
|
|
|
if (EMS_Sys_Status.emsThermostatEnabled) {
|
|
// only send thermostat values if we actually have them
|
|
if (((int)EMS_Thermostat.curr_roomTemp == (int)0) || ((int)EMS_Thermostat.setpoint_roomTemp == (int)0)) {
|
|
return;
|
|
}
|
|
|
|
myESP.publish(TOPIC_THERMOSTAT_CURRTEMP, _float_to_char(s, EMS_Thermostat.curr_roomTemp));
|
|
myESP.publish(TOPIC_THERMOSTAT_SELTEMP, _float_to_char(s, EMS_Thermostat.setpoint_roomTemp));
|
|
}
|
|
}
|
|
|
|
// 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 'P':
|
|
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 'm': // set thermostat mode
|
|
ems_setThermostatMode(cmd[2] - '0');
|
|
break;
|
|
case 'w': // set warm water temp
|
|
ems_setWarmWaterTemp((uint8_t)strtol(&cmd[2], 0, 10));
|
|
break;
|
|
case 'q': // quiet
|
|
_toggleHeartbeat();
|
|
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;
|
|
}
|
|
}
|
|
|
|
// toggle heartbeat LED
|
|
void _toggleHeartbeat() {
|
|
bool b = !ems_getLogVerbose();
|
|
ems_setLogVerbose(b);
|
|
heartbeatEnabled = b;
|
|
digitalWrite(LED_BUILTIN, (b) ? LOW : HIGH); // set the LED
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
// Initialize the boiler settings
|
|
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
|
|
publishValuesTimer.attach(publishValuesTime, publishValues); // every 5 mins (300000) post HA values
|
|
systemCheckTimer.attach(systemCheckTime, systemCheck); // every 10 seconds check if Boiler is online
|
|
heartbeatTimer.attach(heartbeatTime, heartbeat); // every second blink heartbeat LED
|
|
|
|
// set up Wifi, MQTT, Telnet
|
|
myESP.setWifiCallback(WIFIcallback);
|
|
|
|
myESP.setMQTTCallback(MQTTcallback);
|
|
myESP.addSubscription(TOPIC_START);
|
|
myESP.addSubscription(TOPIC_THERMOSTAT_TEMP);
|
|
|
|
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
|
|
heartbeatEnabled = ems_getLogVerbose();
|
|
}
|
|
|
|
// flash LEDs
|
|
// Using a faster way to write to pins as digitalWrite does a lot of overhead like pin checking & disabling interrupts
|
|
void showLEDs() {
|
|
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() {
|
|
if (heartbeatEnabled) {
|
|
int state = digitalRead(LED_BUILTIN);
|
|
digitalWrite(LED_BUILTIN, !state);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
showerResetTimer.detach();
|
|
}
|
|
}
|
|
|
|
//
|
|
// Main loop
|
|
//
|
|
void loop() {
|
|
connectionStatus = myESP.loop();
|
|
timestamp = millis();
|
|
|
|
// 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) {
|
|
// 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 our timings are accurate enough
|
|
// and no need to fetch the values from the boiler
|
|
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();
|
|
// start the timer for n seconds which will reset the water back to hot
|
|
showerResetTimer.attach(SHOWER_OFF_DURATION, _showerColdShotStop);
|
|
}
|
|
}
|
|
} 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
|
|
yield();
|
|
}
|