Merge pull request #8373 from yury-sannikov/8.2.0_dev_ot

OpenTherm sensor implementation
This commit is contained in:
Theo Arends 2020-05-07 14:44:30 +02:00 committed by GitHub
commit 27be030e46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1757 additions and 4 deletions

410
tasmota/OpenTherm.cpp Normal file
View File

@ -0,0 +1,410 @@
/*
OpenTherm.cpp - OpenTherm Communication Library For Arduino, ESP8266
Copyright 2018, Ihor Melnyk
*/
#include "OpenTherm.h"
OpenTherm::OpenTherm(int inPin, int outPin, bool isSlave):
status(OpenThermStatus::NOT_INITIALIZED),
inPin(inPin),
outPin(outPin),
isSlave(isSlave),
response(0),
responseStatus(OpenThermResponseStatus::NONE),
responseTimestamp(0),
handleInterruptCallback(NULL),
processResponseCallback(NULL)
{
}
void OpenTherm::begin(void(*handleInterruptCallback)(void), void(*processResponseCallback)(unsigned long, int))
{
pinMode(inPin, INPUT);
pinMode(outPin, OUTPUT);
if (handleInterruptCallback != NULL) {
this->handleInterruptCallback = handleInterruptCallback;
attachInterrupt(digitalPinToInterrupt(inPin), handleInterruptCallback, CHANGE);
}
activateBoiler();
status = OpenThermStatus::READY;
this->processResponseCallback = processResponseCallback;
}
void OpenTherm::begin(void(*handleInterruptCallback)(void))
{
begin(handleInterruptCallback, NULL);
}
bool ICACHE_RAM_ATTR OpenTherm::isReady()
{
return status == OpenThermStatus::READY;
}
int ICACHE_RAM_ATTR OpenTherm::readState() {
return digitalRead(inPin);
}
void OpenTherm::setActiveState() {
digitalWrite(outPin, LOW);
}
void OpenTherm::setIdleState() {
digitalWrite(outPin, HIGH);
}
void OpenTherm::activateBoiler() {
setIdleState();
delay(1000);
}
void OpenTherm::sendBit(bool high) {
if (high) setActiveState(); else setIdleState();
delayMicroseconds(500);
if (high) setIdleState(); else setActiveState();
delayMicroseconds(500);
}
bool OpenTherm::sendRequestAync(unsigned long request)
{
//Serial.println("Request: " + String(request, HEX));
noInterrupts();
const bool ready = isReady();
interrupts();
if (!ready)
return false;
status = OpenThermStatus::REQUEST_SENDING;
response = 0;
responseStatus = OpenThermResponseStatus::NONE;
sendBit(HIGH); //start bit
for (int i = 31; i >= 0; i--) {
sendBit(bitRead(request, i));
}
sendBit(HIGH); //stop bit
setIdleState();
status = OpenThermStatus::RESPONSE_WAITING;
responseTimestamp = micros();
return true;
}
unsigned long OpenTherm::sendRequest(unsigned long request)
{
if (!sendRequestAync(request)) return 0;
while (!isReady()) {
process();
yield();
}
return response;
}
bool OpenTherm::sendResponse(unsigned long request)
{
status = OpenThermStatus::REQUEST_SENDING;
response = 0;
responseStatus = OpenThermResponseStatus::NONE;
sendBit(HIGH); //start bit
for (int i = 31; i >= 0; i--) {
sendBit(bitRead(request, i));
}
sendBit(HIGH); //stop bit
setIdleState();
status = OpenThermStatus::READY;
return true;
}
OpenThermResponseStatus OpenTherm::getLastResponseStatus()
{
return responseStatus;
}
void ICACHE_RAM_ATTR OpenTherm::handleInterrupt()
{
if (isReady())
{
if (isSlave && readState() == HIGH) {
status = OpenThermStatus::RESPONSE_WAITING;
}
else {
return;
}
}
unsigned long newTs = micros();
if (status == OpenThermStatus::RESPONSE_WAITING) {
if (readState() == HIGH) {
status = OpenThermStatus::RESPONSE_START_BIT;
responseTimestamp = newTs;
}
else {
status = OpenThermStatus::RESPONSE_INVALID;
responseTimestamp = newTs;
}
}
else if (status == OpenThermStatus::RESPONSE_START_BIT) {
if ((newTs - responseTimestamp < 750) && readState() == LOW) {
status = OpenThermStatus::RESPONSE_RECEIVING;
responseTimestamp = newTs;
responseBitIndex = 0;
}
else {
status = OpenThermStatus::RESPONSE_INVALID;
responseTimestamp = newTs;
}
}
else if (status == OpenThermStatus::RESPONSE_RECEIVING) {
if ((newTs - responseTimestamp) > 750) {
if (responseBitIndex < 32) {
response = (response << 1) | !readState();
responseTimestamp = newTs;
responseBitIndex++;
}
else { //stop bit
status = OpenThermStatus::RESPONSE_READY;
responseTimestamp = newTs;
}
}
}
}
void OpenTherm::process()
{
noInterrupts();
OpenThermStatus st = status;
unsigned long ts = responseTimestamp;
interrupts();
if (st == OpenThermStatus::READY) return;
unsigned long newTs = micros();
if (st != OpenThermStatus::NOT_INITIALIZED && (newTs - ts) > 1000000) {
status = OpenThermStatus::READY;
responseStatus = OpenThermResponseStatus::TIMEOUT;
if (processResponseCallback != NULL) {
processResponseCallback(response, responseStatus);
}
}
else if (st == OpenThermStatus::RESPONSE_INVALID) {
status = OpenThermStatus::DELAY;
responseStatus = OpenThermResponseStatus::INVALID;
if (processResponseCallback != NULL) {
processResponseCallback(response, responseStatus);
}
}
else if (st == OpenThermStatus::RESPONSE_READY) {
status = OpenThermStatus::DELAY;
responseStatus = (isSlave ? isValidRequest(response) : isValidResponse(response)) ? OpenThermResponseStatus::SUCCESS : OpenThermResponseStatus::INVALID;
if (processResponseCallback != NULL) {
processResponseCallback(response, responseStatus);
}
}
else if (st == OpenThermStatus::DELAY) {
if ((newTs - ts) > 100000) {
status = OpenThermStatus::READY;
}
}
}
bool OpenTherm::parity(unsigned long frame) //odd parity
{
byte p = 0;
while (frame > 0)
{
if (frame & 1) p++;
frame = frame >> 1;
}
return (p & 1);
}
OpenThermMessageType OpenTherm::getMessageType(unsigned long message)
{
OpenThermMessageType msg_type = static_cast<OpenThermMessageType>((message >> 28) & 7);
return msg_type;
}
OpenThermMessageID OpenTherm::getDataID(unsigned long frame)
{
return (OpenThermMessageID)((frame >> 16) & 0xFF);
}
unsigned long OpenTherm::buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data)
{
unsigned long request = data;
if (type == OpenThermMessageType::WRITE_DATA) {
request |= 1ul << 28;
}
request |= ((unsigned long)id) << 16;
if (OpenTherm::parity(request)) request |= (1ul << 31);
return request;
}
unsigned long OpenTherm::buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data)
{
unsigned long response = data;
response |= type << 28;
response |= ((unsigned long)id) << 16;
if (OpenTherm::parity(response)) response |= (1ul << 31);
return response;
}
bool OpenTherm::isValidResponse(unsigned long response)
{
if (OpenTherm::parity(response)) return false;
byte msgType = (response << 1) >> 29;
return msgType == READ_ACK || msgType == WRITE_ACK;
}
bool OpenTherm::isValidRequest(unsigned long request)
{
if (OpenTherm::parity(request)) return false;
byte msgType = (request << 1) >> 29;
return msgType == READ_DATA || msgType == WRITE_DATA;
}
void OpenTherm::end() {
if (this->handleInterruptCallback != NULL) {
detachInterrupt(digitalPinToInterrupt(inPin));
}
}
const char *OpenTherm::statusToString(OpenThermResponseStatus status)
{
switch (status) {
case NONE: return "NONE";
case SUCCESS: return "SUCCESS";
case INVALID: return "INVALID";
case TIMEOUT: return "TIMEOUT";
default: return "UNKNOWN";
}
}
const char *OpenTherm::messageTypeToString(OpenThermMessageType message_type)
{
switch (message_type) {
case READ_DATA: return "READ_DATA";
case WRITE_DATA: return "WRITE_DATA";
case INVALID_DATA: return "INVALID_DATA";
case RESERVED: return "RESERVED";
case READ_ACK: return "READ_ACK";
case WRITE_ACK: return "WRITE_ACK";
case DATA_INVALID: return "DATA_INVALID";
case UNKNOWN_DATA_ID: return "UNKNOWN_DATA_ID";
default: return "UNKNOWN";
}
}
//building requests
unsigned long OpenTherm::buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2) {
unsigned int data = enableCentralHeating | (enableHotWater << 1) | (enableCooling << 2) | (enableOutsideTemperatureCompensation << 3) | (enableCentralHeating2 << 4);
data <<= 8;
return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Status, data);
}
unsigned long OpenTherm::buildSetBoilerTemperatureRequest(float temperature) {
unsigned int data = temperatureToData(temperature);
return buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TSet, data);
}
unsigned long OpenTherm::buildSetHotWaterTemperatureRequest(float temperature) {
unsigned int data = temperatureToData(temperature);
return buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TdhwSet, data);
}
unsigned long OpenTherm::buildGetBoilerTemperatureRequest() {
return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tboiler, 0);
}
unsigned long OpenTherm::buildSlaveConfigurationRequest() {
return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::SConfigSMemberIDcode, 0);
}
//parsing responses
bool OpenTherm::isFault(unsigned long response) {
return response & 0x1;
}
bool OpenTherm::isCentralHeatingActive(unsigned long response) {
return response & 0x2;
}
bool OpenTherm::isHotWaterActive(unsigned long response) {
return response & 0x4;
}
bool OpenTherm::isFlameOn(unsigned long response) {
return response & 0x8;
}
bool OpenTherm::isCoolingActive(unsigned long response) {
return response & 0x10;
}
bool OpenTherm::isDiagnostic(unsigned long response) {
return response & 0x40;
}
uint16_t OpenTherm::getUInt(const unsigned long response) {
const uint16_t u88 = response & 0xffff;
return u88;
}
float OpenTherm::getFloat(const unsigned long response) {
const uint16_t u88 = getUInt(response);
const float f = (u88 & 0x8000) ? -(0x10000L - u88) / 256.0f : u88 / 256.0f;
return f;
}
unsigned int OpenTherm::temperatureToData(float temperature) {
if (temperature < 0) temperature = 0;
if (temperature > 100) temperature = 100;
unsigned int data = (unsigned int)(temperature * 256);
return data;
}
//basic requests
unsigned long OpenTherm::setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2) {
return sendRequest(buildSetBoilerStatusRequest(enableCentralHeating, enableHotWater, enableCooling, enableOutsideTemperatureCompensation, enableCentralHeating2));
}
bool OpenTherm::setBoilerTemperature(float temperature) {
unsigned long response = sendRequest(buildSetBoilerTemperatureRequest(temperature));
return isValidResponse(response);
}
bool OpenTherm::setHotWaterTemperature(float temperature) {
unsigned long response = sendRequest(buildSetHotWaterTemperatureRequest(temperature));
return isValidResponse(response);
}
float OpenTherm::getBoilerTemperature() {
unsigned long response = sendRequest(buildGetBoilerTemperatureRequest());
return isValidResponse(response) ? getFloat(response) : 0;
}
float OpenTherm::getReturnTemperature() {
unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Tret, 0));
return isValidResponse(response) ? getFloat(response) : 0;
}
float OpenTherm::getModulation() {
unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0));
return isValidResponse(response) ? getFloat(response) : 0;
}
float OpenTherm::getPressure() {
unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0));
return isValidResponse(response) ? getFloat(response) : 0;
}
unsigned char OpenTherm::getFault() {
return ((sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0)) >> 8) & 0xff);
}
unsigned long OpenTherm::getSlaveConfiguration() {
return sendRequest(buildSlaveConfigurationRequest());
}

193
tasmota/OpenTherm.h Normal file
View File

@ -0,0 +1,193 @@
/*
OpenTherm.h - OpenTherm Library for the ESP8266/Arduino platform
https://github.com/ihormelnyk/OpenTherm
http://ihormelnyk.com/pages/OpenTherm
Licensed under MIT license
Copyright 2018, Ihor Melnyk
Frame Structure:
P MGS-TYPE SPARE DATA-ID DATA-VALUE
0 000 0000 00000000 00000000 00000000
*/
#ifndef OpenTherm_h
#define OpenTherm_h
#include <stdint.h>
#include <Arduino.h>
enum OpenThermResponseStatus {
NONE,
SUCCESS,
INVALID,
TIMEOUT
};
enum OpenThermMessageType {
/* Master to Slave */
READ_DATA = B000,
READ = READ_DATA, // for backwared compatibility
WRITE_DATA = B001,
WRITE = WRITE_DATA, // for backwared compatibility
INVALID_DATA = B010,
RESERVED = B011,
/* Slave to Master */
READ_ACK = B100,
WRITE_ACK = B101,
DATA_INVALID = B110,
UNKNOWN_DATA_ID = B111
};
typedef OpenThermMessageType OpenThermRequestType; // for backwared compatibility
enum OpenThermMessageID {
Status, // flag8 / flag8 Master and Slave Status flags.
TSet, // f8.8 Control setpoint ie CH water temperature setpoint (°C)
MConfigMMemberIDcode, // flag8 / u8 Master Configuration Flags / Master MemberID Code
SConfigSMemberIDcode, // flag8 / u8 Slave Configuration Flags / Slave MemberID Code
Command, // u8 / u8 Remote Command
ASFflags, // / OEM-fault-code flag8 / u8 Application-specific fault flags and OEM fault code
RBPflags, // flag8 / flag8 Remote boiler parameter transfer-enable & read/write flags
CoolingControl, // f8.8 Cooling control signal (%)
TsetCH2, // f8.8 Control setpoint for 2e CH circuit (°C)
TrOverride, // f8.8 Remote override room setpoint
TSP, // u8 / u8 Number of Transparent-Slave-Parameters supported by slave
TSPindexTSPvalue, // u8 / u8 Index number / Value of referred-to transparent slave parameter.
FHBsize, // u8 / u8 Size of Fault-History-Buffer supported by slave
FHBindexFHBvalue, // u8 / u8 Index number / Value of referred-to fault-history buffer entry.
MaxRelModLevelSetting, // f8.8 Maximum relative modulation level setting (%)
MaxCapacityMinModLevel, // u8 / u8 Maximum boiler capacity (kW) / Minimum boiler modulation level(%)
TrSet, // f8.8 Room Setpoint (°C)
RelModLevel, // f8.8 Relative Modulation Level (%)
CHPressure, // f8.8 Water pressure in CH circuit (bar)
DHWFlowRate, // f8.8 Water flow rate in DHW circuit. (litres/minute)
DayTime, // special / u8 Day of Week and Time of Day
Date, // u8 / u8 Calendar date
Year, // u16 Calendar year
TrSetCH2, // f8.8 Room Setpoint for 2nd CH circuit (°C)
Tr, // f8.8 Room temperature (°C)
Tboiler, // f8.8 Boiler flow water temperature (°C)
Tdhw, // f8.8 DHW temperature (°C)
Toutside, // f8.8 Outside temperature (°C)
Tret, // f8.8 Return water temperature (°C)
Tstorage, // f8.8 Solar storage temperature (°C)
Tcollector, // f8.8 Solar collector temperature (°C)
TflowCH2, // f8.8 Flow water temperature CH2 circuit (°C)
Tdhw2, // f8.8 Domestic hot water temperature 2 (°C)
Texhaust, // s16 Boiler exhaust temperature (°C)
TdhwSetUBTdhwSetLB = 48, // s8 / s8 DHW setpoint upper & lower bounds for adjustment (°C)
MaxTSetUBMaxTSetLB, // s8 / s8 Max CH water setpoint upper & lower bounds for adjustment (°C)
HcratioUBHcratioLB, // s8 / s8 OTC heat curve ratio upper & lower bounds for adjustment
TdhwSet = 56, // f8.8 DHW setpoint (°C) (Remote parameter 1)
MaxTSet, // f8.8 Max CH water setpoint (°C) (Remote parameters 2)
Hcratio, // f8.8 OTC heat curve ratio (°C) (Remote parameter 3)
RemoteOverrideFunction = 100, // flag8 / - Function of manual and program changes in master and remote room setpoint.
OEMDiagnosticCode = 115, // u16 OEM-specific diagnostic/service code
BurnerStarts, // u16 Number of starts burner
CHPumpStarts, // u16 Number of starts CH pump
DHWPumpValveStarts, // u16 Number of starts DHW pump/valve
DHWBurnerStarts, // u16 Number of starts burner during DHW mode
BurnerOperationHours, // u16 Number of hours that burner is in operation (i.e. flame on)
CHPumpOperationHours, // u16 Number of hours that CH pump has been running
DHWPumpValveOperationHours, // u16 Number of hours that DHW pump has been running or DHW valve has been opened
DHWBurnerOperationHours, // u16 Number of hours that burner is in operation during DHW mode
OpenThermVersionMaster, // f8.8 The implemented version of the OpenTherm Protocol Specification in the master.
OpenThermVersionSlave, // f8.8 The implemented version of the OpenTherm Protocol Specification in the slave.
MasterVersion, // u8 / u8 Master product version number and type
SlaveVersion, // u8 / u8 Slave product version number and type
};
enum OpenThermStatus {
NOT_INITIALIZED,
READY,
DELAY,
REQUEST_SENDING,
RESPONSE_WAITING,
RESPONSE_START_BIT,
RESPONSE_RECEIVING,
RESPONSE_READY,
RESPONSE_INVALID
};
class OpenTherm
{
public:
OpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false);
volatile OpenThermStatus status;
void begin(void(*handleInterruptCallback)(void));
void begin(void(*handleInterruptCallback)(void), void(*processResponseCallback)(unsigned long, int));
bool isReady();
unsigned long sendRequest(unsigned long request);
bool sendResponse(unsigned long request);
bool sendRequestAync(unsigned long request);
static unsigned long buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data);
static unsigned long buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data);
OpenThermResponseStatus getLastResponseStatus();
const char *statusToString(OpenThermResponseStatus status);
void handleInterrupt();
void process();
void end();
static bool parity(unsigned long frame);
OpenThermMessageType getMessageType(unsigned long message);
OpenThermMessageID getDataID(unsigned long frame);
const char *messageTypeToString(OpenThermMessageType message_type);
bool isValidRequest(unsigned long request);
bool isValidResponse(unsigned long response);
//requests
unsigned long buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false);
unsigned long buildSetBoilerTemperatureRequest(float temperature);
unsigned long buildGetBoilerTemperatureRequest();
unsigned long buildSetHotWaterTemperatureRequest(float temperature);
unsigned long buildSlaveConfigurationRequest();
//responses
static bool isFault(unsigned long response);
static bool isCentralHeatingActive(unsigned long response);
static bool isHotWaterActive(unsigned long response);
static bool isFlameOn(unsigned long response);
static bool isCoolingActive(unsigned long response);
static bool isDiagnostic(unsigned long response);
static uint16_t getUInt(const unsigned long response);
static float getFloat(const unsigned long response);
static unsigned int temperatureToData(float temperature);
//basic requests
unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false);
bool setBoilerTemperature(float temperature);
bool setHotWaterTemperature(float temperature);
float getBoilerTemperature();
float getReturnTemperature();
float getModulation();
float getPressure();
unsigned char getFault();
unsigned long getSlaveConfiguration();
private:
const int inPin;
const int outPin;
const bool isSlave;
volatile unsigned long response;
volatile OpenThermResponseStatus responseStatus;
volatile unsigned long responseTimestamp;
volatile byte responseBitIndex;
int readState();
void setActiveState();
void setIdleState();
void activateBoiler();
void sendBit(bool high);
void(*handleInterruptCallback)();
void(*processResponseCallback)(unsigned long, int);
};
#ifndef ICACHE_RAM_ATTR
#define ICACHE_RAM_ATTR
#endif
#endif // OpenTherm_h

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_BG_BG_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_CS_CZ_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "Kalibrierung fehlerhaft"
#define D_AS3935_CAL_OK "Cap gesetzt auf:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_DE_DE_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_EL_GR_H_

View File

@ -794,4 +794,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_EN_GB_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_ES_ES_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_FR_FR_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_HE_HE_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_HU_HU_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibrazione fallita"
#define D_AS3935_CAL_OK "calibrazione impostata a:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_IT_IT_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_KO_KO_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_NL_NL_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_PL_PL_D_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_PT_BR_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_PT_PT_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_RO_RO_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_RU_RU_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_SK_SK_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_SV_SE_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_TR_TR_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_UK_UA_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_ZH_CN_H_

View File

@ -793,4 +793,8 @@
#define D_AS3935_CAL_FAIL "calibration failed"
#define D_AS3935_CAL_OK "calibration set to:"
//xsns_68_opentherm.ino
#define D_SENSOR_BOILER_OT_RX "OpenTherm RX"
#define D_SENSOR_BOILER_OT_TX "OpenTherm TX"
#endif // _LANGUAGE_ZH_TW_H_

View File

@ -666,6 +666,7 @@
#define USE_TASMOTA_SLAVE_FLASH_SPEED 57600 // Usually 57600 for 3.3V variants and 115200 for 5V variants
#define USE_TASMOTA_SLAVE_SERIAL_SPEED 57600 // Depends on the sketch that is running on the Uno/Pro Mini
//#define USE_OPENTHERM // Use OpenTherm implementation
// -- End of general directives -------------------
/*********************************************************************************************\

View File

@ -482,7 +482,10 @@ struct {
uint8_t shutter_startrelay[MAX_SHUTTERS]; // E84
uint8_t pcf8574_config[MAX_PCF8574]; // E88
uint8_t free_e8c[4]; // E8C
uint8_t ot_hot_water_setpoint; // E8C
uint8_t ot_boiler_setpoint; // E8D
uint8_t ot_flags; // E8E
uint8_t free_e8f[1]; // E8F
uint16_t dimmer_hw_min; // E90
uint16_t dimmer_hw_max; // E92

View File

@ -554,7 +554,9 @@ void GetFeatures(void)
#ifdef USE_PING
feature6 |= 0x00000080; // xdrv_38_ping.ino
#endif
#ifdef USE_THERMOSTAT
feature6 |= 0x00000200; // xsns_68_opentherm.ino
#endif
// feature6 |= 0x00000100;
// feature6 |= 0x00000200;
// feature6 |= 0x00000400;

View File

@ -230,6 +230,8 @@ enum UserSelectablePins {
GPIO_ELECTRIQ_MOODL_TX, // ElectriQ iQ-wifiMOODL Serial TX
GPIO_AS3935,
GPIO_PMS5003_TX, // Plantower PMS5003 Serial interface
GPIO_BOILER_OT_RX, // OpenTherm Boiler RX pin
GPIO_BOILER_OT_TX, // OpenTherm Boiler TX pin
GPIO_SENSOR_END };
// Programmer selectable GPIO functionality
@ -317,7 +319,8 @@ const char kSensorNames[] PROGMEM =
D_SENSOR_CC1101_GDO0 "|" D_SENSOR_CC1101_GDO2 "|"
D_SENSOR_HRXL_RX "|"
D_SENSOR_ELECTRIQ_MOODL "|"
D_SENSOR_AS3935 "|" D_SENSOR_PMS5003_TX
D_SENSOR_AS3935 "|" D_SENSOR_PMS5003_TX "|"
D_SENSOR_BOILER_OT_RX "|" D_SENSOR_BOILER_OT_TX
;
const char kSensorNamesFixed[] PROGMEM =
@ -665,6 +668,10 @@ const uint8_t kGpioNiceList[] PROGMEM = {
#ifdef USE_AS3935
GPIO_AS3935,
#endif
#ifdef USE_OPENTHERM
GPIO_BOILER_OT_RX,
GPIO_BOILER_OT_TX,
#endif
};
/********************************************************************************************/

View File

@ -125,6 +125,8 @@ enum UserSelectablePins {
GPIO_WEBCAM_PSCLK,
GPIO_WEBCAM_HSD,
GPIO_WEBCAM_PSRCS,
GPIO_BOILER_OT_RX, // OpenTherm Boiler RX pin
GPIO_BOILER_OT_TX, // OpenTherm Boiler TX pin
GPIO_SENSOR_END };
enum ProgramSelectablePins {
@ -211,7 +213,8 @@ const char kSensorNames[] PROGMEM =
D_GPIO_WEBCAM_VSYNC "|" D_GPIO_WEBCAM_HREF "|" D_GPIO_WEBCAM_PCLK "|"
D_GPIO_WEBCAM_PSCLK "|"
D_GPIO_WEBCAM_HSD "|"
D_GPIO_WEBCAM_PSRCS
D_GPIO_WEBCAM_PSRCS "|"
D_SENSOR_BOILER_OT_RX "|" D_SENSOR_BOILER_OT_TX
;
const char kSensorNamesFixed[] PROGMEM =
@ -548,6 +551,10 @@ const uint16_t kGpioNiceList[] PROGMEM = {
AGPIO(GPIO_WEBCAM_PSRCS),
#endif
#ifdef USE_OPENTHERM
AGPIO(GPIO_BOILER_OT_RX),
AGPIO(GPIO_BOILER_OT_TX),
#endif
};
//********************************************************************************************

View File

@ -0,0 +1,598 @@
/*
xsns_68_opentherm.ino - OpenTherm protocol support for Tasmota
Copyright (C) 2020 Yuriy Sannikov
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <OpenTherm.h>
#ifdef USE_OPENTHERM
#define XSNS_68 68
// Hot water and boiler parameter ranges
#define OT_HOT_WATER_MIN 23
#define OT_HOT_WATER_MAX 55
#define OT_BOILER_MIN 40
#define OT_BOILER_MAX 85
#define OT_HOT_WATER_DEFAULT 36;
#define OT_BOILER_DEFAULT 85;
// Seconds before OT will make an attempt to connect to the boiler after connection error
#define SNS_OT_DISCONNECT_COOLDOWN_SECONDS 10
// Count of the OpenThermSettingsFlags
#define OT_FLAGS_COUNT 6
enum OpenThermSettingsFlags
{
// If set, central heating on/off state follows diagnostic indication bit(6), however
// EnableCentralHeating flag has a priority over it
EnableCentralHeatingOnDiagnostics = 0x01,
// If set, DHW is on after restart.
EnableHotWater = 0x02,
// If set, keep CH always on after restart. If off, follows the EnableCentralHeatingOnDiagnostics rule
EnableCentralHeating = 0x04,
EnableCooling = 0x08,
EnableTemperatureCompensation = 0x10,
EnableCentralHeating2 = 0x20,
};
enum OpenThermConnectionStatus
{
OTC_NONE, // OT not initialized
OTC_DISCONNECTED, // OT communication timed out
OTC_CONNECTING, // Connecting after start or from DISCONNECTED state
OTC_HANDSHAKE, // Wait for the handshake response
OTC_READY, // Last Known Good response state is SUCCESS and no requests are in flight
OTC_INFLIGHT // Request sent, waiting from the response
};
OpenThermConnectionStatus sns_ot_connection_status = OpenThermConnectionStatus::OTC_NONE;
uint8_t sns_ot_disconnect_cooldown = 0;
OpenTherm *sns_ot_master = NULL;
// Has valid values if connection status is READY or INFLIGHT
typedef struct OT_BOILER_STATUS_T
{
// Boiler fault code
uint8_t m_fault_code;
// Boiler OEM fault code
uint8_t m_oem_fault_code;
// Boilder OEM Diagnostics code
uint16_t m_oem_diag_code;
// OpenTherm ID(3) response.
uint8_t m_slave_flags;
// OpenTherm ID(1) codes. Should be used to display state
unsigned long m_slave_raw_status;
// Desired boiler states
bool m_enableCentralHeating;
bool m_enableHotWater;
bool m_enableCooling;
bool m_enableOutsideTemperatureCompensation;
bool m_enableCentralHeating2;
// Some boilers has an input for the heat request. When short, heat is requested
// OT ID(0) bit 6 may indicate state of the Heat Request input
// By enabling this bit we will set m_enableCentralHeating to true when OT ID(0) bit 6 is set.
// This enables to use external mechanical thermostat to enable heating.
// Some of the use cases might be setting an emergency temperature to prevent freezing
// in case of the software thermostat failure.
bool m_useDiagnosticIndicationAsHeatRequest;
// Hot Water temperature
float m_hotWaterSetpoint_read;
// Flame Modulation
float m_flame_modulation_read;
// Boiler Temperature
float m_boiler_temperature_read;
// Boiler desired values
float m_boilerSetpoint;
float m_hotWaterSetpoint;
} OT_BOILER_STATUS;
OT_BOILER_STATUS sns_ot_boiler_status;
const char *sns_opentherm_connection_stat_to_str(int status)
{
switch (status)
{
case OpenThermConnectionStatus::OTC_NONE:
return "NONE";
case OpenThermConnectionStatus::OTC_DISCONNECTED:
return "FAULT";
case OpenThermConnectionStatus::OTC_CONNECTING:
return "CONNECTING";
case OpenThermConnectionStatus::OTC_HANDSHAKE:
return "HANDSHAKE";
case OpenThermConnectionStatus::OTC_READY:
return "READY";
case OpenThermConnectionStatus::OTC_INFLIGHT:
return "BUSY";
default:
return "UNKNOWN";
}
}
void sns_opentherm_init_boiler_status()
{
memset(&sns_ot_boiler_status, 0, sizeof(OT_BOILER_STATUS));
// Settings
sns_ot_boiler_status.m_useDiagnosticIndicationAsHeatRequest = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableCentralHeatingOnDiagnostics;
sns_ot_boiler_status.m_enableHotWater = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableHotWater;
sns_ot_boiler_status.m_enableCentralHeating = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableCentralHeating;
sns_ot_boiler_status.m_enableCooling = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableCooling;
sns_ot_boiler_status.m_enableOutsideTemperatureCompensation = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableTemperatureCompensation;
sns_ot_boiler_status.m_enableCentralHeating2 = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableCentralHeating2;
sns_ot_boiler_status.m_boilerSetpoint = (float)Settings.ot_boiler_setpoint;
sns_ot_boiler_status.m_hotWaterSetpoint = (float)Settings.ot_hot_water_setpoint;
sns_ot_boiler_status.m_fault_code = 0;
sns_ot_boiler_status.m_oem_fault_code = 0;
sns_ot_boiler_status.m_oem_diag_code = 0;
sns_ot_boiler_status.m_hotWaterSetpoint_read = 0;
sns_ot_boiler_status.m_flame_modulation_read = 0;
sns_ot_boiler_status.m_boiler_temperature_read = 0;
}
void ICACHE_RAM_ATTR sns_opentherm_handleInterrupt()
{
sns_ot_master->handleInterrupt();
}
void sns_opentherm_processResponseCallback(unsigned long response, int st)
{
OpenThermResponseStatus status = (OpenThermResponseStatus)st;
AddLog_P2(LOG_LEVEL_DEBUG_MORE,
PSTR("[OTH]: Processing response. Status=%s, Response=0x%lX"),
sns_ot_master->statusToString(status), response);
if (sns_ot_connection_status == OpenThermConnectionStatus::OTC_HANDSHAKE)
{
return sns_ot_process_handshake(response, st);
}
switch (status)
{
case OpenThermResponseStatus::SUCCESS:
if (sns_ot_master->isValidResponse(response))
{
sns_opentherm_process_success_response(&sns_ot_boiler_status, response);
}
sns_ot_connection_status = OpenThermConnectionStatus::OTC_READY;
break;
case OpenThermResponseStatus::INVALID:
sns_opentherm_check_retry_request();
sns_ot_connection_status = OpenThermConnectionStatus::OTC_READY;
break;
// Timeout may indicate not valid/supported command or connection error
// In this case we do reconnect.
// If this command will timeout multiple times, it will be excluded from the rotation later on
// after couple of failed attempts. See sns_opentherm_check_retry_request logic
case OpenThermResponseStatus::TIMEOUT:
sns_opentherm_check_retry_request();
sns_ot_connection_status = OpenThermConnectionStatus::OTC_DISCONNECTED;
break;
}
}
bool sns_opentherm_Init()
{
if (PinUsed(GPIO_BOILER_OT_RX) && PinUsed(GPIO_BOILER_OT_TX))
{
sns_ot_master = new OpenTherm(Pin(GPIO_BOILER_OT_RX), Pin(GPIO_BOILER_OT_TX));
sns_ot_master->begin(sns_opentherm_handleInterrupt, sns_opentherm_processResponseCallback);
sns_ot_connection_status = OpenThermConnectionStatus::OTC_CONNECTING;
sns_opentherm_init_boiler_status();
return true;
}
return false;
// !warning, sns_opentherm settings are not ready at this point
}
void sns_opentherm_stat(bool json)
{
if (!sns_ot_master)
{
return;
}
const char *statusStr = sns_opentherm_connection_stat_to_str(sns_ot_connection_status);
if (json)
{
ResponseAppend_P(PSTR(",\"OPENTHERM\":{"));
ResponseAppend_P(PSTR("\"conn\":\"%s\","), statusStr);
ResponseAppend_P(PSTR("\"settings\":%d,"), Settings.ot_flags);
sns_opentherm_dump_telemetry();
ResponseJsonEnd();
#ifdef USE_WEBSERVER
}
else
{
WSContentSend_P(PSTR("{s}OpenTherm status{m}%s (0x%X){e}"), statusStr, (int)sns_ot_boiler_status.m_slave_flags);
if (sns_ot_connection_status < OpenThermConnectionStatus::OTC_READY)
{
return;
}
WSContentSend_P(PSTR("{s}Std/OEM Fault Codes{m}%d / %d{e}"),
(int)sns_ot_boiler_status.m_fault_code,
(int)sns_ot_boiler_status.m_oem_fault_code);
WSContentSend_P(PSTR("{s}OEM Diagnostic Code{m}%d{e}"),
(int)sns_ot_boiler_status.m_oem_diag_code);
WSContentSend_P(PSTR("{s}Hot Water Setpoint{m}%d{e}"),
(int)sns_ot_boiler_status.m_hotWaterSetpoint_read);
WSContentSend_P(PSTR("{s}Flame Modulation{m}%d{e}"),
(int)sns_ot_boiler_status.m_flame_modulation_read);
WSContentSend_P(PSTR("{s}Boiler Temp/Setpnt{m}%d / %d{e}"),
(int)sns_ot_boiler_status.m_boiler_temperature_read,
(int)sns_ot_boiler_status.m_boilerSetpoint);
if (OpenTherm::isCentralHeatingActive(sns_ot_boiler_status.m_slave_raw_status))
{
WSContentSend_P(PSTR("{s}Central Heating is ACTIVE{m}{e}"));
}
if (sns_ot_boiler_status.m_enableHotWater)
{
WSContentSend_P(PSTR("{s}Hot Water is Enabled{m}{e}"));
}
if (OpenTherm::isHotWaterActive(sns_ot_boiler_status.m_slave_raw_status))
{
WSContentSend_P(PSTR("{s}Hot Water is ACTIVE{m}{e}"));
}
if (OpenTherm::isFlameOn(sns_ot_boiler_status.m_slave_raw_status))
{
WSContentSend_P(PSTR("{s}Flame is ACTIVE{m}{e}"));
}
if (sns_ot_boiler_status.m_enableCooling)
{
WSContentSend_P(PSTR("{s}Cooling is Enabled{m}{e}"));
}
if (OpenTherm::isCoolingActive(sns_ot_boiler_status.m_slave_raw_status))
{
WSContentSend_P(PSTR("{s}Cooling is ACTIVE{m}{e}"));
}
if (OpenTherm::isDiagnostic(sns_ot_boiler_status.m_slave_raw_status))
{
WSContentSend_P(PSTR("{s}Diagnostic Indication{m}{e}"));
}
#endif // USE_WEBSERVER
}
}
void sns_ot_start_handshake()
{
if (!sns_ot_master)
{
return;
}
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("[OTH]: perform handshake"));
sns_ot_master->sendRequestAync(
OpenTherm::buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::SConfigSMemberIDcode, 0));
sns_ot_connection_status = OpenThermConnectionStatus::OTC_HANDSHAKE;
}
void sns_ot_process_handshake(unsigned long response, int st)
{
OpenThermResponseStatus status = (OpenThermResponseStatus)st;
if (status != OpenThermResponseStatus::SUCCESS || !sns_ot_master->isValidResponse(response))
{
AddLog_P2(LOG_LEVEL_ERROR,
PSTR("[OTH]: getSlaveConfiguration failed. Status=%s"),
sns_ot_master->statusToString(status));
sns_ot_connection_status = OpenThermConnectionStatus::OTC_DISCONNECTED;
return;
}
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("[OTH]: getLastResponseStatus SUCCESS. Slave Cfg: %lX"), response);
sns_ot_boiler_status.m_slave_flags = (response & 0xFF00) >> 8;
sns_ot_connection_status = OpenThermConnectionStatus::OTC_READY;
}
void sns_opentherm_CheckSettings(void)
{
bool settingsValid = true;
settingsValid &= Settings.ot_hot_water_setpoint >= OT_HOT_WATER_MIN;
settingsValid &= Settings.ot_hot_water_setpoint <= OT_HOT_WATER_MAX;
settingsValid &= Settings.ot_boiler_setpoint >= OT_BOILER_MIN;
settingsValid &= Settings.ot_boiler_setpoint <= OT_BOILER_MAX;
if (!settingsValid)
{
Settings.ot_hot_water_setpoint = OT_HOT_WATER_DEFAULT;
Settings.ot_boiler_setpoint = OT_BOILER_DEFAULT;
Settings.ot_flags =
OpenThermSettingsFlags::EnableCentralHeatingOnDiagnostics |
OpenThermSettingsFlags::EnableHotWater;
}
}
/*********************************************************************************************\
* Command Processing
\*********************************************************************************************/
const char *sns_opentherm_flag_text(uint8_t mode)
{
switch ((OpenThermSettingsFlags)mode)
{
case OpenThermSettingsFlags::EnableCentralHeatingOnDiagnostics:
return "CHOD";
case OpenThermSettingsFlags::EnableHotWater:
return "DHW";
case OpenThermSettingsFlags::EnableCentralHeating:
return "CH";
case OpenThermSettingsFlags::EnableCooling:
return "COOL";
case OpenThermSettingsFlags::EnableTemperatureCompensation:
return "OTC";
case OpenThermSettingsFlags::EnableCentralHeating2:
return "CH2";
default:
return "?";
}
}
uint8_t sns_opentherm_parse_flag(char *flag)
{
if (!strncmp(flag, "CHOD", 4))
{
return OpenThermSettingsFlags::EnableCentralHeatingOnDiagnostics;
}
else if (!strncmp(flag, "COOL", 4))
{
return OpenThermSettingsFlags::EnableCooling;
}
else if (!strncmp(flag, "DHW", 3))
{
return OpenThermSettingsFlags::EnableHotWater;
}
else if (!strncmp(flag, "OTC", 3))
{
return OpenThermSettingsFlags::EnableTemperatureCompensation;
}
else if (!strncmp(flag, "CH2", 3))
{
return OpenThermSettingsFlags::EnableCentralHeating2;
}
else if (!strncmp(flag, "CH", 2))
{
return OpenThermSettingsFlags::EnableCentralHeating;
}
return 0;
}
uint8_t sns_opentherm_read_flags(char *data, uint32_t len)
{
uint8_t tokens = 1;
for (int i = 0; i < len; ++i)
{
if (data[i] == ',')
{
++tokens;
}
}
uint8_t result = 0;
char sub_string[XdrvMailbox.data_len + 1];
for (int i = 1; i <= tokens; ++i)
{
char *flag = subStr(sub_string, data, ",", i);
if (!flag)
{
break;
}
result |= sns_opentherm_parse_flag(flag);
}
return result;
}
#define D_PRFX_OTHERM "ot_"
// set the boiler temperature (CH). Sutable for the PID app.
// After restart will use the defaults from the settings
#define D_CMND_OTHERM_BOILER_SETPOINT "tboiler"
// set hot water (DHW) temperature. Do not write it in the flash memory.
// suitable for the temporary changes
#define D_CMND_OTHERM_DHW_SETPOINT "twater"
// This command will save CH and DHW setpoints into the settings. Those values will be used after system restart
// The reason to separate set and save is to reduce flash memory write count, especially if boiler temperature is controlled
// by the PID thermostat
#define D_CMND_OTHERM_SAVE_SETTINGS "save_setpoints"
// Get or set flags
// EnableCentralHeatingOnDiagnostics -> CHOD
// EnableHotWater -> DHW
// EnableCentralHeating -> CH
// EnableCooling -> COOL
// EnableTemperatureCompensation -> OTC
// EnableCentralHeating2 -> CH2
#define D_CMND_OTHERM_FLAGS "flags"
// Get/Set boiler status m_enableCentralHeating value. It's equivalent of the EnableCentralHeating settings
// flag value, however, this command does not update the settings.
// Usefull to buld automations
// Please note, if you set it to "0" and EnableCentralHeatingOnDiagnostics is set
// boiler will follow the Diagnostics bit and won't turn CH off. When Diagnostics bit cleared,
// and "ot_ch" is "1", boiler will keep heating
#define D_CMND_SET_CENTRAL_HEATING_ENABLED "ch"
const char kOpenThermCommands[] PROGMEM = D_PRFX_OTHERM "|" D_CMND_OTHERM_BOILER_SETPOINT "|" D_CMND_OTHERM_DHW_SETPOINT
"|" D_CMND_OTHERM_SAVE_SETTINGS "|" D_CMND_OTHERM_FLAGS "|" D_CMND_SET_CENTRAL_HEATING_ENABLED;
void (*const OpenThermCommands[])(void) PROGMEM = {
&sns_opentherm_boiler_setpoint_cmd,
&sns_opentherm_hot_water_setpoint_cmd,
&sns_opentherm_save_settings_cmd,
&sns_opentherm_flags_cmd,
&sns_opentherm_set_central_heating_cmd};
void sns_opentherm_cmd(void) { }
void sns_opentherm_boiler_setpoint_cmd(void)
{
bool query = strlen(XdrvMailbox.data) == 0;
if (!query)
{
sns_ot_boiler_status.m_boilerSetpoint = atof(XdrvMailbox.data);
}
ResponseCmndFloat(sns_ot_boiler_status.m_boilerSetpoint, Settings.flag2.temperature_resolution);
}
void sns_opentherm_hot_water_setpoint_cmd(void)
{
bool query = strlen(XdrvMailbox.data) == 0;
if (!query)
{
sns_ot_boiler_status.m_hotWaterSetpoint = atof(XdrvMailbox.data);
}
ResponseCmndFloat(sns_ot_boiler_status.m_hotWaterSetpoint, Settings.flag2.temperature_resolution);
}
void sns_opentherm_save_settings_cmd(void)
{
Settings.ot_hot_water_setpoint = (uint8_t)sns_ot_boiler_status.m_hotWaterSetpoint;
Settings.ot_boiler_setpoint = (uint8_t)sns_ot_boiler_status.m_boilerSetpoint;
ResponseCmndDone();
}
void sns_opentherm_flags_cmd(void)
{
bool query = strlen(XdrvMailbox.data) == 0;
if (!query)
{
// Set flags value
Settings.ot_flags = sns_opentherm_read_flags(XdrvMailbox.data, XdrvMailbox.data_len);
// Reset boiler status to apply settings
sns_opentherm_init_boiler_status();
}
bool addComma = false;
mqtt_data[0] = 0;
for (int pos = 0; pos < OT_FLAGS_COUNT; ++pos)
{
int mask = 1 << pos;
int mode = Settings.ot_flags & (uint8_t)mask;
if (mode > 0)
{
if (addComma)
{
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s,"), mqtt_data);
}
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s%s"), mqtt_data, sns_opentherm_flag_text(mode));
addComma = true;
}
}
}
void sns_opentherm_set_central_heating_cmd(void)
{
bool query = strlen(XdrvMailbox.data) == 0;
if (!query)
{
sns_ot_boiler_status.m_enableCentralHeating = atoi(XdrvMailbox.data);
}
ResponseCmndNumber(sns_ot_boiler_status.m_enableCentralHeating ? 1 : 0);
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
bool Xsns68(uint8_t function)
{
bool result = false;
if (FUNC_INIT == function)
{
if (sns_opentherm_Init())
{
sns_opentherm_CheckSettings();
}
}
if (!sns_ot_master)
{
return result;
}
switch (function)
{
case FUNC_LOOP:
sns_ot_master->process();
break;
case FUNC_EVERY_100_MSECOND:
if (sns_ot_connection_status == OpenThermConnectionStatus::OTC_READY && sns_ot_master->isReady())
{
unsigned long request = sns_opentherm_get_next_request(&sns_ot_boiler_status);
if (-1 != request)
{
sns_ot_master->sendRequestAync(request);
sns_ot_connection_status = OpenThermConnectionStatus::OTC_INFLIGHT;
}
}
break;
case FUNC_EVERY_SECOND:
if (sns_ot_connection_status == OpenThermConnectionStatus::OTC_DISCONNECTED)
{
// If disconnected, wait for the SNS_OT_DISCONNECT_COOLDOWN_SECONDS before the handshake
if (sns_ot_disconnect_cooldown == 0)
{
sns_ot_disconnect_cooldown = SNS_OT_DISCONNECT_COOLDOWN_SECONDS;
}
else if (--sns_ot_disconnect_cooldown == 0)
{
sns_ot_connection_status = OpenThermConnectionStatus::OTC_CONNECTING;
}
}
else if (sns_ot_connection_status == OpenThermConnectionStatus::OTC_CONNECTING)
{
sns_ot_start_handshake();
}
break;
case FUNC_COMMAND:
result = DecodeCommand(kOpenThermCommands, OpenThermCommands);
break;
case FUNC_JSON_APPEND:
sns_opentherm_stat(1);
break;
#ifdef USE_WEBSERVER
case FUNC_WEB_SENSOR:
sns_opentherm_stat(0);
break;
#endif // USE_WEBSERVER
}
return result;
}
#endif // USE_OPENTHERM

View File

@ -0,0 +1,440 @@
/*
xsns_68_opentherm_protocol.ino - OpenTherm protocol support for Tasmota
Copyright (C) 2020 Yuriy Sannikov
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "OpenTherm.h"
#ifdef USE_OPENTHERM
// Temperature tolerance. If temperature setpoint difference is less than the value,
// OT (1)(Control setpoint) command will be skipped
#define OPENTHERM_BOILER_SETPOINT_TOLERANCE 1.0
typedef union {
uint8_t m_flags;
struct
{
uint8_t notSupported : 1; // If set, boiler does not support this command
uint8_t supported : 1; // Set if at least one response were successfull
uint8_t retryCount : 2; // Retry counter before notSupported flag being set
};
} OpenThermParamFlags;
typedef union {
float m_float;
uint8_t m_u8;
uint16_t m_u16;
unsigned long m_ul;
bool m_bool;
} ResponseStorage;
typedef struct OpenThermCommandT
{
const char *m_command_name;
uint8_t m_command_code;
OpenThermParamFlags m_flags;
ResponseStorage m_results[2];
unsigned long (*m_ot_make_request)(OpenThermCommandT *self, OT_BOILER_STATUS_T *boilerStatus);
void (*m_ot_parse_response)(OpenThermCommandT *self, OT_BOILER_STATUS_T *boilerStatus, unsigned long response);
void (*m_ot_appent_telemetry)(OpenThermCommandT *self);
} OpenThermCommand;
OpenThermCommand sns_opentherm_commands[] = {
{// Get/Set Slave Status Flags
.m_command_name = "SLAVE",
.m_command_code = 0,
// OpenTherm ID(0) should never go into the notSupported state due to some connectivity issues
// otherwice it may lose boiler control
.m_flags = {.supported = 1},
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_set_slave_flags,
.m_ot_parse_response = sns_opentherm_parse_slave_flags,
.m_ot_appent_telemetry = sns_opentherm_tele_slave_flags},
{// Set boiler temperature
.m_command_name = "BTMP",
.m_command_code = 0,
// OpenTherm ID(1) also should never go into the notSupported state due to some connectivity issues
.m_flags = {.supported = 1},
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_set_boiler_temperature,
.m_ot_parse_response = sns_opentherm_parse_set_boiler_temperature,
.m_ot_appent_telemetry = sns_opentherm_tele_boiler_temperature},
{// Set Hot Water temperature
.m_command_name = "HWTMP",
.m_command_code = 0,
// OpenTherm ID(56) may not be supported
.m_flags = 0,
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_set_boiler_dhw_temperature,
.m_ot_parse_response = sns_opentherm_parse_boiler_dhw_temperature,
.m_ot_appent_telemetry = sns_opentherm_tele_boiler_dhw_temperature},
{// Read Application-specific fault flags and OEM fault code
.m_command_name = "ASFF",
.m_command_code = 0,
.m_flags = 0,
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_get_flags,
.m_ot_parse_response = sns_opentherm_parse_flags,
.m_ot_appent_telemetry = sns_opentherm_tele_flags},
{// Read An OEM-specific diagnostic/service code
.m_command_name = "OEMD",
.m_command_code = 0,
.m_flags = 0,
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_get_oem_diag,
.m_ot_parse_response = sns_opentherm_parse_oem_diag,
.m_ot_appent_telemetry = sns_opentherm_tele_oem_diag},
{// Read Flame modulation
.m_command_name = "FLM",
.m_command_code = (uint8_t)OpenThermMessageID::RelModLevel,
.m_flags = 0,
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_get_generic_float,
.m_ot_parse_response = sns_opentherm_parse_flame_modulation,
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
{// Read Boiler Temperature
.m_command_name = "TB",
.m_command_code = (uint8_t)OpenThermMessageID::Tboiler,
.m_flags = 0,
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_get_generic_float,
.m_ot_parse_response = sns_opentherm_parse_boiler_temperature,
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
{// Read DHW temperature
.m_command_name = "TDHW",
.m_command_code = (uint8_t)OpenThermMessageID::Tdhw,
.m_flags = 0,
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_get_generic_float,
.m_ot_parse_response = sns_opentherm_parse_generic_float,
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
{// Read Outside temperature
.m_command_name = "TOUT",
.m_command_code = (uint8_t)OpenThermMessageID::Toutside,
.m_flags = 0,
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_get_generic_float,
.m_ot_parse_response = sns_opentherm_parse_generic_float,
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
{// Read Return water temperature
.m_command_name = "TRET",
.m_command_code = (uint8_t)OpenThermMessageID::Tret,
.m_flags = 0,
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_get_generic_float,
.m_ot_parse_response = sns_opentherm_parse_generic_float,
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
{// Read DHW setpoint
.m_command_name = "DHWS",
.m_command_code = (uint8_t)OpenThermMessageID::TdhwSet,
.m_flags = 0,
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_get_generic_float,
.m_ot_parse_response = sns_opentherm_parse_dhw_setpoint,
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
{// Read max CH water setpoint
.m_command_name = "TMAX",
.m_command_code = (uint8_t)OpenThermMessageID::MaxTSet,
.m_flags = 0,
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
.m_ot_make_request = sns_opentherm_get_generic_float,
.m_ot_parse_response = sns_opentherm_parse_generic_float,
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
};
/////////////////////////////////// Process Slave Status Flags & Control //////////////////////////////////////////////////
unsigned long sns_opentherm_set_slave_flags(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *status)
{
bool centralHeatingIsOn = status->m_enableCentralHeating;
if (status->m_useDiagnosticIndicationAsHeatRequest) {
centralHeatingIsOn |= OpenTherm::isDiagnostic(status->m_slave_raw_status);
}
if (self->m_results[1].m_bool != centralHeatingIsOn) {
AddLog_P2(LOG_LEVEL_INFO,
PSTR("[OTH]: Central Heating transitioning from %s to %s"),
self->m_results[1].m_bool ? "on" : "off",
status->m_enableCentralHeating ? "on" : "off");
}
self->m_results[1].m_bool = centralHeatingIsOn;
unsigned int data = centralHeatingIsOn |
(status->m_enableHotWater << 1) |
(status->m_enableCooling << 2) |
(status->m_enableOutsideTemperatureCompensation << 3) |
(status->m_enableCentralHeating2 << 4);
data <<= 8;
return OpenTherm::buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Status, data);
}
void sns_opentherm_parse_slave_flags(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
{
boilerStatus->m_slave_raw_status = response;
self->m_results[0].m_ul = response;
}
#define OT_FLAG_TO_ON_OFF(status, flag) ((((status) & (flag)) != 0) ? 1 : 0)
void sns_opentherm_tele_slave_flags(struct OpenThermCommandT *self)
{
unsigned long st = self->m_results[0].m_ul;
ResponseAppend_P(PSTR("{\"FAULT\":%d,\"CH\":%d,\"DHW\":%d,\"FL\":%d,\"COOL\":%d,\"CH2\":%d,\"DIAG\":%d,\"RAW\":%lu}"),
OT_FLAG_TO_ON_OFF(st, 0x01),
OT_FLAG_TO_ON_OFF(st, 0x02),
OT_FLAG_TO_ON_OFF(st, 0x04),
OT_FLAG_TO_ON_OFF(st, 0x08),
OT_FLAG_TO_ON_OFF(st, 0x10),
OT_FLAG_TO_ON_OFF(st, 0x20),
OT_FLAG_TO_ON_OFF(st, 0x40),
st);
}
/////////////////////////////////// Set Boiler Temperature //////////////////////////////////////////////////
unsigned long sns_opentherm_set_boiler_temperature(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *status)
{
// Assuming some boilers might write setpoint temperature into the Flash memory
// Having PID controlled appliance may produce a lot of small fluctuations in the setpoint value
// wearing out Boiler flash memory.
float diff = abs(status->m_boilerSetpoint - self->m_results[0].m_float);
// Ignore small changes in the boiler setpoint temperature
if (diff < OPENTHERM_BOILER_SETPOINT_TOLERANCE)
{
return -1;
}
AddLog_P2(LOG_LEVEL_INFO,
PSTR("[OTH]: Setting Boiler Temp. Old: %d, New: %d"),
(int)self->m_results[0].m_float,
(int)status->m_boilerSetpoint);
self->m_results[0].m_float = status->m_boilerSetpoint;
unsigned int data = OpenTherm::temperatureToData(status->m_boilerSetpoint);
return OpenTherm::buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TSet, data);
}
void sns_opentherm_parse_set_boiler_temperature(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
{
self->m_results[1].m_float = OpenTherm::getFloat(response);
}
void sns_opentherm_tele_boiler_temperature(struct OpenThermCommandT *self)
{
char requested[FLOATSZ];
dtostrfd(self->m_results[0].m_float, Settings.flag2.temperature_resolution, requested);
char actual[FLOATSZ];
dtostrfd(self->m_results[1].m_float, Settings.flag2.temperature_resolution, actual);
// indicate fault if tepmerature demand and actual setpoint are greater then tolerance
bool isFault = abs(self->m_results[1].m_float - self->m_results[0].m_float) > OPENTHERM_BOILER_SETPOINT_TOLERANCE;
ResponseAppend_P(PSTR("{\"FAULT\":%d,\"REQ\":%s,\"ACT\": %s}"),
(int)isFault,
requested,
actual);
}
/////////////////////////////////// Set Domestic Hot Water Temperature //////////////////////////////////////////////////
unsigned long sns_opentherm_set_boiler_dhw_temperature(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *status)
{
// The same consideration as for the boiler temperature
float diff = abs(status->m_hotWaterSetpoint - self->m_results[0].m_float);
// Ignore small changes in the boiler setpoint temperature
if (diff < OPENTHERM_BOILER_SETPOINT_TOLERANCE)
{
return -1;
}
AddLog_P2(LOG_LEVEL_INFO,
PSTR("[OTH]: Setting Hot Water Temp. Old: %d, New: %d"),
(int)self->m_results[0].m_float,
(int)status->m_hotWaterSetpoint);
self->m_results[0].m_float = status->m_hotWaterSetpoint;
unsigned int data = OpenTherm::temperatureToData(status->m_hotWaterSetpoint);
return OpenTherm::buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TdhwSet, data);
}
void sns_opentherm_parse_boiler_dhw_temperature(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
{
self->m_results[1].m_float = OpenTherm::getFloat(response);
}
void sns_opentherm_tele_boiler_dhw_temperature(struct OpenThermCommandT *self)
{
char requested[FLOATSZ];
dtostrfd(self->m_results[0].m_float, Settings.flag2.temperature_resolution, requested);
char actual[FLOATSZ];
dtostrfd(self->m_results[1].m_float, Settings.flag2.temperature_resolution, actual);
ResponseAppend_P(PSTR("{\"REQ\":%s,\"ACT\": %s}"),
requested,
actual);
}
/////////////////////////////////// App Specific Fault Flags //////////////////////////////////////////////////
unsigned long sns_opentherm_get_flags(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *)
{
return OpenTherm::buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0);
}
void sns_opentherm_parse_flags(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
{
uint8_t fault_code = (response >> 8) & 0xFF;
uint8_t oem_fault_code = response & 0xFF;
boilerStatus->m_fault_code = fault_code;
boilerStatus->m_oem_fault_code = fault_code;
self->m_results[0].m_u8 = fault_code;
self->m_results[1].m_u8 = oem_fault_code;
}
void sns_opentherm_tele_flags(struct OpenThermCommandT *self)
{
ResponseAppend_P(PSTR("{\"FC\":%d,\"OFC\":%d}"),
(int)self->m_results[0].m_u8,
(int)self->m_results[1].m_u8);
}
/////////////////////////////////// OEM Diag Code //////////////////////////////////////////////////
unsigned long sns_opentherm_get_oem_diag(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *)
{
return OpenTherm::buildRequest(OpenThermRequestType::READ, OpenThermMessageID::OEMDiagnosticCode, 0);
}
void sns_opentherm_parse_oem_diag(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
{
uint16_t diag_code = (uint16_t)response & 0xFFFF;
boilerStatus->m_oem_diag_code = diag_code;
self->m_results[0].m_u16 = diag_code;
}
void sns_opentherm_tele_oem_diag(struct OpenThermCommandT *self)
{
ResponseAppend_P(PSTR("%d"), (int)self->m_results[0].m_u16);
}
/////////////////////////////////// Generic Single Float /////////////////////////////////////////////////
unsigned long sns_opentherm_get_generic_float(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *)
{
return OpenTherm::buildRequest(OpenThermRequestType::READ, (OpenThermMessageID)self->m_command_code, 0);
}
void sns_opentherm_parse_generic_float(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
{
self->m_results[0].m_float = OpenTherm::getFloat(response);
}
void sns_opentherm_tele_generic_float(struct OpenThermCommandT *self)
{
char str[FLOATSZ];
dtostrfd(self->m_results[0].m_float, Settings.flag2.temperature_resolution, str);
ResponseAppend_P(PSTR("%s"), str);
}
/////////////////////////////////// Specific Floats Rerports to the /////////////////////////////////////////////////
void sns_opentherm_parse_dhw_setpoint(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
{
self->m_results[0].m_float = OpenTherm::getFloat(response);
boilerStatus->m_hotWaterSetpoint_read = self->m_results[0].m_float;
}
void sns_opentherm_parse_flame_modulation(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
{
self->m_results[0].m_float = OpenTherm::getFloat(response);
boilerStatus->m_flame_modulation_read = self->m_results[0].m_float;
}
void sns_opentherm_parse_boiler_temperature(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
{
self->m_results[0].m_float = OpenTherm::getFloat(response);
boilerStatus->m_boiler_temperature_read = self->m_results[0].m_float;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
#define SNS_OT_COMMANDS_COUNT (sizeof(sns_opentherm_commands) / sizeof(OpenThermCommand))
int sns_opentherm_current_command = SNS_OT_COMMANDS_COUNT;
unsigned long sns_opentherm_get_next_request(struct OT_BOILER_STATUS_T *boilerStatus)
{
// get next and loop the command
if (++sns_opentherm_current_command >= SNS_OT_COMMANDS_COUNT)
{
sns_opentherm_current_command = 0;
}
struct OpenThermCommandT *cmd = &sns_opentherm_commands[sns_opentherm_current_command];
// Return error if command known as not supported
if (cmd->m_flags.notSupported)
{
return -1;
}
// Retrurn OT compatible request
return cmd->m_ot_make_request(cmd, boilerStatus);
}
void sns_opentherm_check_retry_request()
{
if (sns_opentherm_current_command >= SNS_OT_COMMANDS_COUNT)
{
return;
}
struct OpenThermCommandT *cmd = &sns_opentherm_commands[sns_opentherm_current_command];
bool canRetry = ++cmd->m_flags.retryCount < 3;
// In case of last retry and if this command never respond successfully, set notSupported flag
if (!canRetry && !cmd->m_flags.supported)
{
cmd->m_flags.notSupported = true;
AddLog_P2(LOG_LEVEL_ERROR,
PSTR("[OTH]: command %s is not supported by the boiler. Last status: %s"),
cmd->m_command_name,
sns_ot_master->statusToString(sns_ot_master->getLastResponseStatus()));
}
}
void sns_opentherm_process_success_response(struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
{
if (sns_opentherm_current_command >= SNS_OT_COMMANDS_COUNT)
{
return;
}
struct OpenThermCommandT *cmd = &sns_opentherm_commands[sns_opentherm_current_command];
// mark command as supported
cmd->m_flags.supported = true;
cmd->m_ot_parse_response(cmd, boilerStatus, response);
}
void sns_opentherm_dump_telemetry()
{
bool add_coma = false;
for (int i = 0; i < SNS_OT_COMMANDS_COUNT; ++i)
{
struct OpenThermCommandT *cmd = &sns_opentherm_commands[i];
if (!cmd->m_flags.supported)
{
continue;
}
ResponseAppend_P(PSTR("%s\"%s\":"), add_coma ? "," : "", cmd->m_command_name);
cmd->m_ot_appent_telemetry(cmd);
add_coma = true;
}
}
#endif