smartBoiler

This commit is contained in:
Mit4el
2024-02-12 20:49:36 +03:00
parent d05137b4d4
commit 15de2955fb
9 changed files with 2924 additions and 0 deletions

View File

@@ -0,0 +1,812 @@
/*
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)
{
imitFlag = false;
}
void OpenTherm::begin(void (*handleInterruptCallback)(void), void (*processResponseCallback)(unsigned long, OpenThermResponseStatus))
{
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 IRAM_ATTR OpenTherm::isReady()
{
return status == OpenThermStatus::READY;
}
int IRAM_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;
// Prevent switching to other tasks as there is a delay within sendBit
#ifdef ESP32
// vTaskSuspendAll();
#endif
sendBit(HIGH); // start bit
for (int i = 31; i >= 0; i--)
{
sendBit(bitRead(request, i));
}
sendBit(HIGH); // stop bit
setIdleState();
#ifdef ESP32
// xTaskResumeAll();
#endif
status = OpenThermStatus::RESPONSE_WAITING;
responseTimestamp = micros();
if (imitFlag)
ImitationResponse(request);
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;
// Prevent switching to other tasks as there is a delay within sendBit
#ifdef ESP32
// vTaskSuspendAll();
#endif
sendBit(HIGH); // start bit
for (int i = 31; i >= 0; i--)
{
sendBit(bitRead(request, i));
}
sendBit(HIGH); // stop bit
setIdleState();
#ifdef ESP32
// xTaskResumeAll();
#endif
status = OpenThermStatus::READY;
return true;
}
unsigned long OpenTherm::getLastResponse()
{
return response;
}
OpenThermResponseStatus OpenTherm::getLastResponseStatus()
{
return responseStatus;
}
void IRAM_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
{
// Error start bit / Ошибка стартового бита
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
{
// Error Start_bit LOW 750mks / Ошибка стартового бита по тылу (нет LOW через 750мкс)
status = OpenThermStatus::RESPONSE_INVALID;
responseTimestamp = newTs;
}
}
else if (status == OpenThermStatus::RESPONSE_RECEIVING)
{
// unsigned long bitDuration = newTs - responseTimestamp;
// В новой спецификации стоповый бит не обязателен. Если не дождались, всё равно попробуем разобрать
if ((newTs - responseTimestamp) > 750 && (newTs - responseTimestamp) < 1300)
{
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 && st != OpenThermStatus::DELAY && (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;
// Error msgType (READ_ACK | WRITE_ACK) is Header
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 (parity(request))
request |= (1ul << 31);
return request;
}
unsigned long OpenTherm::buildRequestID(OpenThermMessageType type, unsigned int id, unsigned int data)
{
unsigned long request = data;
if (type == OpenThermMessageType::WRITE_DATA)
{
request |= 1ul << 28;
}
request |= ((unsigned long)id) << 16;
if (parity(request))
request |= (1ul << 31);
return request;
}
unsigned long OpenTherm::buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data)
{
unsigned long response = data;
response |= ((unsigned long)type) << 28;
response |= ((unsigned long)id) << 16;
if (parity(response))
response |= (1ul << 31);
return response;
}
bool OpenTherm::isValidResponse(unsigned long response)
{
if (parity(response))
return false;
byte msgType = (response << 1) >> 29;
return msgType == READ_ACK || msgType == WRITE_ACK;
}
bool OpenTherm::isValidRequest(unsigned long request)
{
if (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, bool enableSummerMode, bool dhwBlock)
{
unsigned int data = enableCentralHeating | (enableHotWater << 1) | (enableCooling << 2) | (enableOutsideTemperatureCompensation << 3) | (enableCentralHeating2 << 4) | (enableSummerMode << 5) | (dhwBlock << 6);
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::buildGetBoilerTemperatureRequest()
{
return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tboiler, 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
{
const uint16_t u88 = response & 0xffff;
return u88;
}
float OpenTherm::getFloat(const unsigned long response) const
{
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, bool enableSummerMode, bool dhwBlock)
{
return sendRequest(buildSetBoilerStatusRequest(enableCentralHeating, enableHotWater, enableCooling, enableOutsideTemperatureCompensation, enableCentralHeating2, enableSummerMode, dhwBlock));
}
bool OpenTherm::setBoilerTemperature(float temperature)
{
unsigned long response = sendRequest(buildSetBoilerTemperatureRequest(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;
}
bool OpenTherm::setDHWSetpoint(float temperature)
{
unsigned int data = temperatureToData(temperature);
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TdhwSet, data));
return isValidResponse(response);
}
float OpenTherm::getDHWTemperature()
{
unsigned long response = sendRequest(buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tdhw, 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);
}
int8_t flame_timer = 0;
void OpenTherm::ImitationResponse(unsigned long request)
{
// unsigned long response;
unsigned int data = getUInt(request);
OpenThermMessageType msgType;
byte ID;
OpenThermMessageID id = getDataID(request);
uint8_t flags;
switch (id)
{
case OpenThermMessageID::Status:
// Статус котла получен
msgType = OpenThermMessageType::READ_ACK;
static int8_t flame = 0;
flame_timer++;
if (flame_timer > 10)
flame = 1;
if (flame_timer > 20)
{
flame_timer = 0;
flame = 0;
}
static int8_t fault = 0;
// fault = 1 - fault;
data = (bool)fault | (true << 1) | (true << 2) | ((bool)flame << 3) | (false << 4);
break;
case OpenThermMessageID::SConfigSMemberIDcode:
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::SlaveVersion:
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::MasterVersion:
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::RelModLevel:
static float RelModLevel = 10;
// RelModLevel = RelModLevel > 100 ? 10 : RelModLevel + 1;
if (flame_timer < 11)
{
RelModLevel = 0;
}
else
{
RelModLevel = RelModLevel == 0 ? 10 : RelModLevel + 1;
}
// data = RelModLevel;
data = temperatureToData(RelModLevel);
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::Tboiler:
// Получили температуру котла
static float Tboiler = 40;
Tboiler = Tboiler > 60 ? 40 : Tboiler + 1;
data = temperatureToData(Tboiler);
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::Tdhw:
// Получили температуру ГВС
static float Tdhw = 60;
Tdhw = Tdhw > 80 ? 60 : Tdhw + 1;
data = temperatureToData(Tdhw);
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::Toutside:
// Получили внешнюю температуру
static float Toutside = -10;
Toutside = Toutside > 10 ? -10 : Toutside + 1;
data = temperatureToData(Toutside);
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::ASFflags:
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::TdhwSetUBTdhwSetLB:
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::MaxTSetUBMaxTSetLB:
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::OEMDiagnosticCode:
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::OpenThermVersionSlave:
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::CHPressure:
msgType = OpenThermMessageType::READ_ACK;
break;
break;
case OpenThermMessageID::DHWFlowRate:
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::DayTime:
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::Date:
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::Year:
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::Tret:
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::Tstorage:
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::Tcollector:
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::TflowCH2:
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::Tdhw2:
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::Texhaust:
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::TheatExchanger:
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::BoilerFanSpeed:
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::ElectricBurnerFlame:
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::BurnerStarts:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::CHPumpStarts:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::DHWPumpValveStarts:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::DHWBurnerStarts:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::BurnerOperationHours:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::CHPumpOperationHours:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::DHWPumpValveOperationHours:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::DHWBurnerOperationHours:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::RBPflags:
//
// Pre-Defined Remote Boiler Parameters
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::TdhwSet:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::TSet:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::MaxTSet:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::Hcratio:
//
if (getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::TSP:
//
// Transparent Slave Parameters
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::FHBsize:
//
// Fault History Data
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::MaxCapacityMinModLevel:
//
// Boiler Sequencer Control
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::TrOverride:
//
// Remote override room setpoint
//
msgType = OpenThermMessageType::READ_ACK;
break;
case OpenThermMessageID::RemoteOverrideFunction:
msgType = OpenThermMessageType::READ_ACK;
break;
default:
msgType = OpenThermMessageType::UNKNOWN_DATA_ID;
break;
}
response = buildResponse(msgType, id, data);
status = OpenThermStatus::RESPONSE_READY;
responseStatus = OpenThermResponseStatus::SUCCESS;
/*
if (processResponseCallback != NULL)
{
processResponseCallback(response, OpenThermResponseStatus::SUCCESS);
}
*/
}

View File

@@ -0,0 +1,208 @@
/*
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 : uint8_t
{
NONE,
SUCCESS,
INVALID,
TIMEOUT
};
enum OpenThermMessageType : uint8_t
{
/* 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 : uint8_t
{
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)
TheatExchanger, // f8.8 Boiler heat exchanger temperature (°C)
BoilerFanSpeed, // u16 Boiler fan speed Setpiont and actual value
ElectricBurnerFlame, // f8.8?? Electric current through burner flame (mюA)
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 : uint8_t
{
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, OpenThermResponseStatus));
bool isReady();
unsigned long sendRequest(unsigned long request);
bool sendResponse(unsigned long request);
bool sendRequestAync(unsigned long request);
unsigned long buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data);
unsigned long buildRequestID(OpenThermMessageType type, unsigned int id, unsigned int data);
unsigned long buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data);
unsigned long getLastResponse();
OpenThermResponseStatus getLastResponseStatus();
const char *statusToString(OpenThermResponseStatus status);
void handleInterrupt();
void process();
void end();
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, bool enableSummerMode = false, bool dhwBlock = false);
unsigned long buildSetBoilerTemperatureRequest(float temperature);
unsigned long buildGetBoilerTemperatureRequest();
// responses
bool isFault(unsigned long response);
bool isCentralHeatingActive(unsigned long response);
bool isHotWaterActive(unsigned long response);
bool isFlameOn(unsigned long response);
bool isCoolingActive(unsigned long response);
bool isDiagnostic(unsigned long response);
uint16_t getUInt(const unsigned long response) const;
float getFloat(const unsigned long response) const;
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 enableSummerMode = false, bool dhwBlock = false);
bool setBoilerTemperature(float temperature);
float getBoilerTemperature();
float getReturnTemperature();
bool setDHWSetpoint(float temperature);
float getDHWTemperature();
float getModulation();
float getPressure();
unsigned char getFault();
//Имитация ответов от котла, TRUE - идет имитация ответов котла, в котел так же шлется (лучше его отключить), FALSE - штатная работа
void imitation(bool fl) {imitFlag = fl;}
private:
bool imitFlag;
void ImitationResponse(unsigned long request);
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, OpenThermResponseStatus);
};
#ifndef ICACHE_RAM_ATTR
#define ICACHE_RAM_ATTR
#endif
#ifndef IRAM_ATTR
#define IRAM_ATTR ICACHE_RAM_ATTR
#endif
#endif // OpenTherm_h

View File

@@ -0,0 +1,589 @@
#include "Global.h"
#include "classes/IoTItem.h"
#include <Arduino.h>
#include "OpenTherm.h"
#define SLAVE true
#define TIMEOUT_TRESHOLD 5
namespace _OpenThermSlave
{
OpenTherm *ot_driver = nullptr;
OpenTherm *instance_OTdriver(int _RX_pin, int _TX_pin)
{
if (!ot_driver)
{
ot_driver = new OpenTherm(_RX_pin, _TX_pin, SLAVE);
// ot_driver->begin();
}
return ot_driver;
}
// Обработчик прерываний от ОТ
void IRAM_ATTR handleInterruptSlave()
{
if (ot_driver != nullptr)
ot_driver->handleInterrupt();
}
// команды/установки от термостата
struct SetpointBoiler
{
uint8_t cmd_chEnable = 0;
uint8_t cmd_dhwEnable = 0;
float TSetCH = 0;
float TSetDhw = 0;
} set;
struct failCode
{
bool service_required = 0;
bool lockout_reset = 0;
bool low_water_pressure = 0;
bool gas_fault = 0;
bool air_fault = 0;
bool water_overtemp = 0;
uint8_t fault_code = 0;
};
// текущее реальное состояние котла
struct StateBoiler
{
uint8_t stateCH = 0;
uint8_t stateDHW = 0;
uint8_t fl_flame = 0;
uint8_t fl_fail = 0;
failCode fCode;
float RelModLevel = 0;
float Tboiler = 0;
float Tret = 0;
float Tdhw = 0;
float Toutside = 0;
} state;
// конфигурация котла
struct ConfigBoiler
{
bool dhw = false; // 1- есть реле(трехходовой) ГВС
bool ctrlType = false; // 0 - модуляция, 1- вкл/выкл
bool confDhw = true; // 1 - бак, 0 - проточная //TODO ПОКА НЕ ЗНАЮ ЧТО ДЕЛАТЬ
bool pumpControlMaster = false; // в протоколе ОТ: мастер управляет насосом ????????????????????? //TODO Команды кправления насосом от мастера не помню
int minDhw;
int maxDhw;
int minCH;
int maxCH;
} conf;
// DynamicJsonDocument OpenThemData(JSON_BUFFER_SIZE / 2);
IoTItem *tmp;
IoTItem *_idTboiler = nullptr;
IoTItem *_idTret = nullptr;
IoTItem *_idToutside = nullptr;
IoTItem *_idStateCH = nullptr;
IoTItem *_idStateDHW = nullptr;
IoTItem *_idStateFlame = nullptr;
IoTItem *_idModLevel = nullptr;
IoTItem *_idTDhw = nullptr;
IoTItem *_idCmdCH = nullptr;
IoTItem *_idCmdDHW = nullptr;
IoTItem *_idSetCH = nullptr;
IoTItem *_idSetDHW = nullptr;
IoTItem *_idCtrlType = nullptr;
unsigned long timeout_count = 0;
uint8_t _debug = 0;
bool _telegram = false;
unsigned long ot_response = 0;
uint8_t SlaveMemberIDcode = 0;
void publishNew(String widget, String value)
{
tmp = findIoTItem(widget);
if (tmp)
{
tmp->setValue(value, true);
}
else
{
if (_debug > 0)
SerialPrint("new", "SmartBoiler", widget + " = " + value);
}
}
void sendTelegramm(String msg)
{
if (_telegram == 1)
{
if (tlgrmItem)
tlgrmItem->sendTelegramMsg(false, msg);
}
}
/*
* =========================================================================================
* КЛАСС РАБОТЫ ПО ПРОТОКОЛУ OPENTHERM
* =========================================================================================
*/
class OpenThermSlave : public IoTItem
{
private:
// unsigned long ts = 0;
public:
OpenThermSlave(String parameters) : IoTItem(parameters)
{
int _RX_pin = 16;
int _TX_pin = 4;
SerialPrint("i", F("OpenThermSlave"), " START... ");
jsonRead(parameters, "RX_pin", _RX_pin);
jsonRead(parameters, "TX_pin", _TX_pin);
jsonRead(parameters, "MemberID", (int &)SlaveMemberIDcode);
jsonRead(parameters, "LogLevel", (int &)_debug);
jsonRead(parameters, "telegram", _telegram);
String tmpID;
jsonRead(parameters, "idTboiler", tmpID);
_idTboiler = findIoTItem(tmpID);
jsonRead(parameters, "idTret", tmpID);
_idTret = findIoTItem(tmpID);
jsonRead(parameters, "idToutside", tmpID);
_idToutside = findIoTItem(tmpID);
jsonRead(parameters, "idStateCH", tmpID);
_idStateCH = findIoTItem(tmpID);
jsonRead(parameters, "idStateDHW", tmpID);
_idStateDHW = findIoTItem(tmpID);
jsonRead(parameters, "idStateFlame", tmpID);
_idStateFlame = findIoTItem(tmpID);
jsonRead(parameters, "idModLevel", tmpID);
_idModLevel = findIoTItem(tmpID);
jsonRead(parameters, "idTDhw", tmpID);
_idTDhw = findIoTItem(tmpID);
jsonRead(parameters, "idCmdCH", tmpID);
_idCmdCH = findIoTItem(tmpID);
jsonRead(parameters, "idCmdDHW", tmpID);
_idCmdDHW = findIoTItem(tmpID);
jsonRead(parameters, "idCtrlType", tmpID);
_idCtrlType = findIoTItem(tmpID);
jsonRead(parameters, "idSetCH", tmpID);
_idSetCH = findIoTItem(tmpID);
jsonRead(parameters, "idSetDHW", tmpID);
_idSetDHW = findIoTItem(tmpID);
jsonRead(parameters, "minCH", conf.minCH);
jsonRead(parameters, "maxCH", conf.maxCH);
jsonRead(parameters, "minDhw", conf.minDhw);
jsonRead(parameters, "maxDhw", conf.maxDhw);
instance_OTdriver(_RX_pin, _TX_pin);
ot_driver->begin(handleInterruptSlave, processRequest); // responseCallback
// ot_boiler = this;
}
void doByInterval()
{
}
// Основной цикл программы
void loop()
{
ot_driver->process();
IoTItem::loop();
}
// Комманды из сценария
IoTValue execute(String command, std::vector<IoTValue> &param)
{
return {};
}
// Обработка управления и отправка статуса
static void processStatus(unsigned int &data)
{
uint8_t statusRequest = data >> 8; // забрали старший байт с командами мастера
set.cmd_chEnable = statusRequest & 0x1; // забрали 0 бит из этого байта - включение СО (маска 01)
set.cmd_dhwEnable = statusRequest & 0x2; // забрали 1 бит из этого байта - включение СО (маска 10)
IoTValue val;
val.valD = set.cmd_chEnable;
_idCmdCH->setValue(val, true);
val.valD = set.cmd_dhwEnable;
_idCmdDHW->setValue(val, true);
data &= 0xFF00; // старший бит не трогаем, а младший обнулили, что бы его заполнить состоянием котла и вернуть data термостату
// if (_idFail)
// state.fl_fail = ::atof(_idFail->getValue().c_str());
if (_idStateCH)
state.stateCH = ::atoi(_idStateCH->getValue().c_str());
if (_idStateDHW)
state.stateDHW = ::atoi(_idStateDHW->getValue().c_str());
if (_idStateFlame)
state.fl_flame = ::atoi(_idStateFlame->getValue().c_str());
if (state.fl_fail)
data |= 0x01; // fault indication
if (state.stateCH)
data |= 0x02; // CH active
if (state.stateDHW)
data |= 0x04; // DHW active
if (state.fl_flame)
data |= 0x08; // flame on
// data |= 0x10; //cooling active
// data |= 0x20; //CH2 active
// data |= 0x40; //diagnostic/service event
// data |= 0x80; //electricity production on
}
// обработка сброса ошибок
static void processCommand(unsigned int &data)
{
uint8_t command = data >> 8; // забрали старший байт с командами мастера
if (command == 1)
{
state.fl_fail = 0;
data |= 128; // ответ 128-255: команда выполнена
}
}
//=================================== Обработка входящих сообщение ОТ ======================================
static void processRequest(unsigned long request, OpenThermResponseStatus status)
{
switch (status)
{
case OpenThermResponseStatus::NONE:
if (_debug > 0)
{
SerialPrint("E", "OpenThermSlave", "Error: OpenTherm не инициализирован");
}
break;
case OpenThermResponseStatus::INVALID:
if (_debug > 0)
{
SerialPrint("E", "OpenThermSlave", "ID:" + String(ot_driver->getDataID(request)) + " / Error: Ошибка разбора команды: " + String(request, HEX));
// build UNKNOWN-DATAID response
unsigned long response = ot_driver->buildResponse(OpenThermMessageType::DATA_INVALID, ot_driver->getDataID(request), 0);
// send response
ot_driver->sendResponse(response);
}
break;
case OpenThermResponseStatus::TIMEOUT:
if (_debug > 0)
{
SerialPrint("E", "OpenThermSlave", " ID: " + String(ot_driver->getDataID(request)) + " / Error: Таймаут команд от управляющего устройства (термостата)");
}
timeout_count++;
if (timeout_count > TIMEOUT_TRESHOLD)
{
publishNew("boilerslave", "");
// publishNew("status", "не подключен");
timeout_count = TIMEOUT_TRESHOLD;
sendTelegramm(("OpenTherm: потеря связи с управляющим устройством (термостатом) ❌"));
}
break;
case OpenThermResponseStatus::SUCCESS:
timeout_count = 0;
publishNew("boilerslave", "");
// publishNew("status", "подключен");
// sendTelegramm(("OpenTherm: котёл подключен ✅"));
// respondense_flag = true;
// ts_ = new_ts_;
HandleRequest(request);
break;
default:
break;
}
}
// Парсинг запросов
static void HandleRequest(unsigned long request)
{
if (_idCtrlType)
conf.ctrlType = ::atoi(_idCtrlType->getValue().c_str());
// unsigned long response;
unsigned int data = ot_driver->getUInt(request);
OpenThermMessageType msgType;
byte ID;
OpenThermMessageID id = ot_driver->getDataID(request);
uint8_t flags;
if (_debug > 2)
{
SerialPrint("i", "OpenThermSlave <-", String(millis()) + " ID: " + String(id) + " / requestHEX: " + String(request, HEX) + " / request: " + String(request));
}
switch (id)
{
/*----------------------------Инициализация и конфигурация----------------------------*/
case OpenThermMessageID::SConfigSMemberIDcode: // запрос Конфигурации Котла и SlaveMemberID
msgType = OpenThermMessageType::READ_ACK;
data = conf.dhw | (conf.ctrlType << 1) | (false << 2) | (conf.confDhw << 3) | (conf.pumpControlMaster << 4) | (false << 5); // 2-cooling, 5-CH2
data <<= 8;
data |= SlaveMemberIDcode;
// data = (int)SlaveMemberIDcode;
break;
// case OpenThermMessageID::MConfigMMemberIDcode: // Получили Master Member ID
// msgType = OpenThermMessageType::WRITE_ACK;
// break;
// case OpenThermMessageID::SlaveVersion: // TODO вернуть версию модуля
// msgType = OpenThermMessageType::READ_ACK;
// data = (int)1;
// break;
// case OpenThermMessageID::MasterVersion:
// msgType = OpenThermMessageType::WRITE_ACK;
// break;
// case OpenThermMessageID::OpenThermVersionSlave:
// msgType = OpenThermMessageType::READ_ACK;
// break;
/*----------------------------Управление (уставки и команды)----------------------------*/
case OpenThermMessageID::TdhwSetUBTdhwSetLB: // границы уставки ГВС
msgType = OpenThermMessageType::READ_ACK;
data |= (uint8_t)conf.minDhw;
data |= (uint8_t)conf.maxDhw << 8;
break;
case OpenThermMessageID::MaxTSetUBMaxTSetLB: // границы уставки СО
msgType = OpenThermMessageType::READ_ACK;
data |= (uint8_t)conf.minCH;
data |= (uint8_t)conf.maxCH << 8;
break;
case OpenThermMessageID::Command: // Сброс ошибок/сброс блокировки котла. Ответ: команды (не)выполнена
msgType = OpenThermMessageType::READ_ACK;
processCommand(data);
break;
case OpenThermMessageID::TdhwSet: // TODO Получили температуру ГВС
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
{
msgType = OpenThermMessageType::READ_ACK;
if (_idSetDHW)
set.TSetDhw = ::atof(_idSetDHW->getValue().c_str());
data = ot_driver->temperatureToData(set.TSetDhw);
}
else
{
msgType = OpenThermMessageType::WRITE_ACK;
// processDHWSet(ot_driver->getFloat(data));
set.TSetDhw = ot_driver->getFloat(data);
set.TSetDhw = constrain(set.TSetDhw, conf.minDhw, conf.maxDhw);
// publishNew("TDHWSet", String(set.TSetDhw));
IoTValue val;
val.valD = set.TSetDhw;
_idSetDHW->setValue(val, true);
}
break;
case OpenThermMessageID::TSet: // TODO Получили температуру СО
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
{
msgType = OpenThermMessageType::READ_ACK;
if (_idSetCH)
set.TSetCH = ::atof(_idSetCH->getValue().c_str());
data = ot_driver->temperatureToData(set.TSetCH);
}
else
{
msgType = OpenThermMessageType::WRITE_ACK;
// processCHSet(ot_driver->getFloat(data));
set.TSetCH = ot_driver->getFloat(data);
set.TSetCH = constrain(set.TSetCH, conf.minCH, conf.maxCH);
// publishNew("TCHSet", String(set.TSetCH));
IoTValue val;
val.valD = set.TSetCH;
_idSetCH->setValue(val, true);
}
break;
/* case OpenThermMessageID::MaxTSet: // максимальная уставка ГВС ??????
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::Hcratio: // Коэффециент тепловой кривой
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
*/
/*----------------------------Состояние и статусы----------------------------*/
case OpenThermMessageID::Status: // TODO Вернуть Статус котла
msgType = OpenThermMessageType::READ_ACK;
processStatus(data);
break;
case OpenThermMessageID::RelModLevel: // запрос модуляции
msgType = OpenThermMessageType::READ_ACK;
if (_idModLevel)
state.RelModLevel = ::atoi(_idModLevel->getValue().c_str());
data = ot_driver->temperatureToData(state.RelModLevel);
break;
case OpenThermMessageID::Tboiler: // запрос температуры котла
msgType = OpenThermMessageType::READ_ACK;
if (_idTboiler)
state.Tboiler = ::atof(_idTboiler->getValue().c_str());
data = ot_driver->temperatureToData(state.Tboiler);
break;
case OpenThermMessageID::Tdhw: // запрос температуры ГВС
msgType = OpenThermMessageType::READ_ACK;
if (_idTDhw)
{
state.Tdhw = ::atof(_idTDhw->getValue().c_str());
data = ot_driver->temperatureToData(state.Tdhw);
}
else
{
msgType = OpenThermMessageType::UNKNOWN_DATA_ID;
}
break;
case OpenThermMessageID::Toutside: // запрос внешней температуры
msgType = OpenThermMessageType::READ_ACK;
if (_idToutside)
{
state.Toutside = ::atof(_idToutside->getValue().c_str());
data = ot_driver->temperatureToData(state.Toutside);
}
else
{
msgType = OpenThermMessageType::UNKNOWN_DATA_ID;
}
break;
case OpenThermMessageID::ASFflags: // запрос ошибок
msgType = OpenThermMessageType::READ_ACK;
data = 0;
if (state.fl_fail)
{
data = state.fCode.service_required | (state.fCode.lockout_reset << 1) | (state.fCode.low_water_pressure << 2) | (state.fCode.gas_fault << 3) | (state.fCode.air_fault << 4) | (state.fCode.water_overtemp << 5);
data |= (uint8_t)state.fCode.fault_code << 8;
}
break;
case OpenThermMessageID::Tret: // температура обратки
msgType = OpenThermMessageType::READ_ACK;
if (_idTret)
{
state.Tret = ::atof(_idTret->getValue().c_str());
data = ot_driver->temperatureToData(state.Tret);
}
else
{
msgType = OpenThermMessageType::UNKNOWN_DATA_ID;
}
break;
// case OpenThermMessageID::OEMDiagnosticCode:
// msgType = OpenThermMessageType::READ_ACK;
// break;
// case OpenThermMessageID::ElectricBurnerFlame: // Ток работы горелки ?????
// msgType = OpenThermMessageType::READ_ACK;
// break;
// case OpenThermMessageID::MaxCapacityMinModLevel: // максимальная мощность котла кВт и минимальная модуляция %
// msgType = OpenThermMessageType::READ_ACK;
// break;
/*----------------------------Двусторонние информационные сообщения----------------------------*/
/* case OpenThermMessageID::DayTime:
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::Date:
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::Year:
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
// ========>>>>>>>>>>> СБРОС КОЛИЧЕСТВА 0 от мастера
case OpenThermMessageID::BurnerStarts: // Количество стартов горелки
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::CHPumpStarts: // Количество стартов насоса СО
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::DHWPumpValveStarts: // Количество стартов насоса/клапана ГВС
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::DHWBurnerStarts: // Количество стартов горелки ГВС
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::BurnerOperationHours: // часы работы горелки
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::CHPumpOperationHours: // часы работы горелки СО
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::DHWPumpValveOperationHours: // часы работы насоса/клапана ГВС
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
case OpenThermMessageID::DHWBurnerOperationHours: // часы работы горелки ГВС
if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA)
msgType = OpenThermMessageType::READ_ACK;
else
msgType = OpenThermMessageType::WRITE_ACK;
break;
*/
/*------------------------------------ ВСЁ ------------------------------------*/
default:
msgType = OpenThermMessageType::UNKNOWN_DATA_ID;
break;
}
ot_response = ot_driver->buildResponse(msgType, id, data);
ot_driver->sendResponse(ot_response);
if (_debug > 2)
{
SerialPrint("i", "OpenThermSlave ->", String(millis()) + " ID: " + String(id) + " / responseHEX: " + String(ot_response, HEX) + " / response: " + String(ot_response));
}
}
~OpenThermSlave()
{
delete ot_driver;
ot_driver = nullptr;
}
};
}
void *getAPI_OpenThermSlave(String subtype, String param)
{
if (subtype == F("OpenThermSlave"))
{
return new _OpenThermSlave::OpenThermSlave(param);
}
else
{
return nullptr;
}
}

View File

@@ -0,0 +1,111 @@
{
"menuSection": "executive_devices",
"configItem": [
{
"global": 0,
"name": "OpenThermSlave",
"type": "Reading",
"subtype": "OpenThermSlave",
"id": "otslave",
"widget": "",
"page": "Boiler",
"descr": "Котёл",
"int": 60,
"value": "...",
"RX_pin": 13,
"TX_pin": 15,
"LogLevel": 0,
"telegram": 1,
"MemberID": 0,
"confDhw":0,
"minCH": 35,
"maxCH": 85,
"minDhw": 30,
"maxDhw": 60,
"idTboiler": "Tboiler",
"idTret": "Tret",
"idToutside": "Toutside",
"idTDhw":"TDhw",
"idStateCH":"StateCH",
"idStateDHW":"StateDHW",
"idStateFlame":"StateFlame",
"idModLevel":"ModLevel",
"idCmdCH":"CmdCH",
"idCmdDHW":"CmdDHW",
"idSetCH":"SetCH",
"idSetDHW":"SetDHW",
"idCtrlType":"CtrlType"
}
],
"about": {
"authorName": "Mikhail Bubnov",
"authorContact": "https://t.me/Mit4bmw",
"authorGit": "https://github.com/Mit4el",
"specialThanks": "",
"moduleName": "OpenThermSlave",
"moduleVersion": "0.1",
"usedRam": {
"esp32_4mb": 15,
"esp8266_4mb": 15
},
"title": "OpenThermSlave",
"moduleDesc": "Модуль для автоматизации электрического котла. Мозги котла с внешним протоколом opentherm",
"propInfo": {
"int": "Интервал отправки данных в MQTT и web интерфейс",
"telegram": "1- Будет отправлять в телеграмм оповещения при ошибках котла и пропаже сигнала от котла, остальные необходимо реализовывать через сценарий",
"MemberID": "SlaveMemberIDcode - код производителя котла, кем притворится котёл;) Менять в большинстве случаев не надо",
"idPID":"ID модуля ПИД регулятора, для расчета модуляции и включения тэнов в зависимости от температуры теплоносителя, в модуле TCHSet будет уставка СО, создать TCHSet и указать его в модуле ПИД",
"idTboiler": "ID датчика температуры подачи котла",
"idTret": "ID датчика температуры обратки котла",
"idToutside": "ID датчика уличной температуры",
"rele1_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele1",
"rele2_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele2, если нет, то 0 (ноль)",
"rele3_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele3, если нет, то 0 (ноль)",
"Pupm": "1-есть реле насоса (ID реле должно называться relePump), 0-нет реле насоса, насос управляется котлом без нас",
"minCH": "Граница установки температуры СО",
"maxCH": "Граница установки температуры СО",
"gistCH": "Гистерезис СО - нагрев СО включится если температура теплоносителя ниже уставки на указанные градусы (CHSet = 45гр, gistCH = 5гр, нагрев включится когда idTboiler = 40гр)",
"idTdhw": "ID датчика температуры ГВС, например в датчик в БКН",
"idReleDhw":"ID реле трехходового крана ГВС",
"gistDhw": "Гистерезис ГВС - нагрев ГВС включится если температура воды ниже уставки на указанные градусы",
"minDhw": "Граница установки температуры ГВС",
"maxDhw": "Граница установки температуры ГВС"
},
"funcInfo": [
{
"name": "CHSet",
"descr": "Установить целевую температуру СО",
"params": [
"тепмература СО (подачи) - bolier.CHSet(60)"
]
},
{
"name": "CHEnable",
"descr": "включить / выключить отопление",
"params": [
"bolier.CHEnable(1) - вкл, bolier.CHEnable(0) - выкл, "
]
},
{
"name": "DHWSet",
"descr": "Установить целевую температуру ГВС",
"params": [
"тепмература ГВС - dhw.DHWSet(40)"
]
},
{
"name": "DHWEnable",
"descr": "включить / выключить ГВС",
"params": [
"dhw.DHWEnable(1) - вкл, dhw.DHWEnable(0) - выкл, "
]
}
]
},
"defActive": true,
"usedLibs": {
"esp32_4mb3f": [],
"esp32*": [],
"esp82*": []
}
}

View File

@@ -0,0 +1,78 @@
#pragma once
#define SLAVE true
#define TIMEOUT_TRESHOLD 5
namespace _Boiler
{
// команды/установки от термостата
struct SetpointBoiler
{
uint8_t cmd_chEnable = 0;
uint8_t cmd_dhwEnable = 0;
float TSetCH = 0;
float TSetDhw = 0;
} set;
struct failCode
{
bool service_required = 0;
bool lockout_reset = 0;
bool low_water_pressure = 0;
bool gas_fault = 0;
bool air_fault = 0;
bool water_overtemp = 0;
uint8_t fault_code = 0;
};
// текущее реальное состояние котла
struct StateBoiler
{
uint8_t stateCH = 0;
uint8_t stateDHW = 0;
uint8_t fl_flame = 0;
uint8_t currentRele = 0;
uint8_t fl_fail = 0;
failCode fCode;
float RelModLevel = 0;
float Tboiler = -40;
float Tret = 0;
float Tdhw = 0;
float Toutside = 0;
bool r[3] = {0, 0, 0};
} state;
// конфигурация котла
struct ConfigBoiler
{
int antiFreez;
bool pump = false; // 1- наличие реле насоса СО, 0 - мы не управляем насосом СО (в протоколе ОТ нет)
bool changeRele = false;
// bool dhw = false; // 1- есть реле(трехходовой) ГВС
bool ctrlType = false; // 0 - модуляция, 1- вкл/выкл
bool confDhw = false; // 1 - бак, 0 - проточная //TODO ПОКА НЕ ЗНАЮ ЧТО ДЕЛАТЬ
bool pumpControlMaster = false; // в протоколе ОТ: мастер управляет насосом ????????????????????? //TODO Команды кправления насосом от мастера не помню
int minDhw;
int maxDhw;
int minCH;
int maxCH;
int gistDhw;
int gistCH;
int countRele = 0;
int relePwr[3]={0,0,0};
int prcOnekWt = 0; // процент одного киловата из общей мощности всех тэнев, расчитывается для модуляции
// int rele2Pwr = 0;
// int rele3Pwr = 0;
} conf;
unsigned long timeout_count = 0;
int _debug = 0;
bool _telegram = false;
unsigned long ot_response = 0;
}

View File

@@ -0,0 +1,582 @@
#include "Global.h"
#include "classes/IoTItem.h"
#include <Arduino.h>
#include "BoilerHeader.h"
namespace _Boiler
{
DynamicJsonDocument OpenThemData(JSON_BUFFER_SIZE / 2);
IoTItem *_idPID = nullptr;
IoTItem *_idTboiler = nullptr;
IoTItem *_idTret = nullptr;
IoTItem *_idToutside = nullptr;
IoTItem *_idStateCH = nullptr;
IoTItem *_idStateFlame = nullptr;
IoTItem *_idModLevel = nullptr;
IoTItem *_idCmdCH = nullptr;
IoTItem *_idSetCH = nullptr;
IoTItem *_idCtrlType = nullptr;
int pid;
IoTItem *rele[3];
IoTItem *relePump;
IoTItem *releDhw;
IoTItem *tmp;
// проверяем если пришедшее значение отличается от предыдущего регистрируем событие
void publishNew(String widget, String value)
{
if (OpenThemData[widget] != value)
{
OpenThemData[widget] = value;
tmp = findIoTItem(widget);
if (tmp)
{
tmp->setValue(value, true);
}
else
{
if (_debug > 0)
SerialPrint("new", "SmartBoiler", widget + " = " + value);
}
}
}
void sendTelegramm(String msg)
{
if (_telegram == 1)
{
if (tlgrmItem)
tlgrmItem->sendTelegramMsg(false, msg);
}
}
/*
* =========================================================================================
* КЛАСС УПРАВЛЕНИЯ ГВС
* =========================================================================================
*/
IoTItem *_idTdhw = nullptr;
IoTItem *_idStateDHW = nullptr;
IoTItem *_idCmdDHW = nullptr;
IoTItem *_idTDhw = nullptr;
IoTItem *_idSetDHW = nullptr;
class DHWControl : public IoTItem
{
private:
unsigned long ts = 0;
public:
DHWControl(String parameters) : IoTItem(parameters)
{
SerialPrint("i", F("DHWControl"), " START... ");
String tmpID;
jsonRead(parameters, "idReleDhw", tmpID);
releDhw = findIoTItem(tmpID);
jsonRead(parameters, "idSetDHW", tmpID);
_idSetDHW = findIoTItem(tmpID);
jsonRead(parameters, "idStateDHW", tmpID);
_idStateDHW = findIoTItem(tmpID);
jsonRead(parameters, "idCmdDHW", tmpID);
_idCmdDHW = findIoTItem(tmpID);
jsonRead(parameters, "idTDhw", tmpID);
_idTDhw = findIoTItem(tmpID);
jsonRead(parameters, "minDhw", conf.minDhw);
jsonRead(parameters, "maxDhw", conf.maxDhw);
jsonRead(parameters, "gistDhw", conf.gistDhw);
// releDhw = findIoTItem("releDhw");
if (releDhw)
{
SerialPrint("i", "DHWControl", "Инициализировано РЕЛЕ ГВС");
}
// dhw_ctrl = this;
}
// ============================== ЛОГИКА РАБОТЫ КОТЛА И ВКЛЮЧЕНИЯ ТЭНОВ ======================================
// Работы котла и включения тэнов
static void logicPowerDhw()
{
if (_idCmdDHW)
set.cmd_dhwEnable = ::atoi(_idCmdDHW->getValue().c_str());
if (_idTDhw)
state.Tdhw = ::atof(_idTDhw->getValue().c_str());
if (_idSetDHW)
set.TSetDhw = ::atof(_idSetDHW->getValue().c_str());
if (set.cmd_dhwEnable)
{
if (releDhw)
{
if (_idTDhw)
state.Tdhw = ::atof(_idTDhw->getValue().c_str());
if (set.TSetDhw - state.Tdhw >= conf.gistDhw && state.Tdhw > -5 && state.Tdhw < 120)
{
// включаем ГВС
releDhw->setValue("1", true);
state.stateDHW = 1;
state.stateCH = 0;
for (uint8_t i = 0; i < conf.countRele; i++)
{
state.r[i] = true;
}
state.RelModLevel = 100;
}
else
{
releDhw->setValue("0", true);
state.stateDHW = 0;
state.RelModLevel = 0;
}
}
}
else
{
if (releDhw)
releDhw->setValue("0", true);
}
// publishNew("StateDHW", String(state.stateDHW));
IoTValue val;
if (_idStateDHW)
{
val.valD = state.stateDHW;
_idStateDHW->setValue(val, true);
}
}
void doByInterval()
{
}
// Комманды из сценария
IoTValue execute(String command, std::vector<IoTValue> &param)
{
IoTValue val;
if (command == "SetDHW")
{
set.TSetDhw = param[0].valD;
set.TSetDhw = constrain(set.TSetDhw, conf.minDhw, conf.maxDhw);
// publishNew("SetDHW", String(set.TSetDhw));
val.valD = set.TSetDhw;
if (_idSetDHW)
_idSetDHW->setValue(val, true);
SerialPrint("i", "DHWControl", "Scenario DHWSet ");
}
else if (command == "DHWEnable")
{
set.cmd_dhwEnable = param[0].valD;
val.valD = set.cmd_dhwEnable;
if (_idCmdDHW)
_idCmdDHW->setValue(val, true);
SerialPrint("i", "DHWControl", "Scenario DHWEnable ");
}
return {};
}
~DHWControl()
{
}
};
/*
* =========================================================================================
* КЛАСС УПРАВЛЕНИЯ КОТЛОМ
* =========================================================================================
*/
class BoilerControl : public IoTItem
{
private:
unsigned long ts = 0;
public:
BoilerControl(String parameters) : IoTItem(parameters)
{
SerialPrint("i", F("BoilerControl"), " START... ");
jsonRead(parameters, "LogLevel", _debug);
jsonRead(parameters, "telegram", _telegram);
String tmpID;
jsonRead(parameters, "idPID", tmpID);
_idPID = findIoTItem(tmpID);
jsonRead(parameters, "idTboiler", tmpID);
_idTboiler = findIoTItem(tmpID);
jsonRead(parameters, "idTret", tmpID);
_idTret = findIoTItem(tmpID);
jsonRead(parameters, "idToutside", tmpID);
_idToutside = findIoTItem(tmpID);
jsonRead(parameters, "idStateCH", tmpID);
_idStateCH = findIoTItem(tmpID);
jsonRead(parameters, "idStateFlame", tmpID);
_idStateFlame = findIoTItem(tmpID);
jsonRead(parameters, "idModLevel", tmpID);
_idModLevel = findIoTItem(tmpID);
jsonRead(parameters, "idCmdCH", tmpID);
_idCmdCH = findIoTItem(tmpID);
jsonRead(parameters, "idSetCH", tmpID);
_idSetCH = findIoTItem(tmpID);
jsonRead(parameters, "idCtrlType", tmpID);
_idCtrlType = findIoTItem(tmpID);
jsonRead(parameters, "rele1_Pwr", conf.relePwr[0]);
jsonRead(parameters, "rele2_Pwr", conf.relePwr[1]);
jsonRead(parameters, "rele3_Pwr", conf.relePwr[2]);
jsonRead(parameters, "Pump", conf.pump);
jsonRead(parameters, "changeRele", conf.changeRele);
jsonRead(parameters, "minCH", conf.minCH);
jsonRead(parameters, "maxCH", conf.maxCH);
jsonRead(parameters, "gistCH", conf.gistCH);
jsonRead(parameters, "antiFreez", conf.antiFreez);
configuration();
}
// ============================== ЛОГИКА РАБОТЫ КОТЛА И ВКЛЮЧЕНИЯ ТЭНОВ ======================================
// Работы котла и включения тэнов
static void logicPowerOn()
{
// TODO ВКЛЮЧЕНИЕ РЕЛЕ ПО ИХ МОЩНОСТИ (СДЕЛАТЬ ШАГИ НАГРЕВА И КОМБИНАЦИИ ВКЛБЧЕНИЯ ТЕНОВ С РАЗНОЙ МОЩНОСТЬЮ)
for (uint8_t i = 0; i < conf.countRele; i++)
{
state.r[i] = false;
}
// сейчас включаются по порядку
state.RelModLevel = 0;
pid = 0;
state.fl_flame = false;
if (_idPID)
pid = ::atoi(_idPID->getValue().c_str());
// обнуляем ГВС
state.stateDHW = 0;
if (state.Tboiler < conf.maxCH)
{
// if (dhw_ctrl)
//{
// если есть модуль ГВС, то вызываем его логику включения тэнов
DHWControl::logicPowerDhw();
//}
if (!state.stateDHW) // Если уже включено ГВС, то нечего смотреть на отопление
{
if (set.cmd_chEnable)
{
publishNew("status", "Штатный режим");
// включаем отопление
state.stateCH = 1;
if (state.Tboiler < (set.TSetCH + conf.gistCH) && /*set.TSetCH - state.Tboiler >= conf.gistCH &&*/ state.Tboiler > -5 && state.Tboiler < 120)
{
if (conf.ctrlType == 0)
{ // режим модуляции, это когда есть модуль ПИД и более одного реле
if (pid > 0)
{
state.r[state.currentRele] = true;
state.RelModLevel = conf.prcOnekWt * conf.relePwr[state.currentRele];
}
if (pid > 30 && pid <= 60)
{
uint8_t next = state.currentRele + 1;
if (next >= conf.countRele)
next = 0;
state.r[next] = true;
state.RelModLevel = (conf.prcOnekWt * conf.relePwr[state.currentRele]) + (conf.prcOnekWt * conf.relePwr[next]);
}
if (pid > 60)
{
for (uint8_t i = 0; i < conf.countRele; i++)
{
state.r[i] = true;
}
state.RelModLevel = 100;
}
}
else
{ // у нас релейный режим без ПИДа или с одним реле(тэном)
// TODO СЕЙЧАС ВКЛЮЧАЕМ ВСЕ РЕЛЕ, НАДО ЧТО ТО ПРИДУМАТЬ УМНОЕ
for (uint8_t i = 0; i < conf.countRele; i++)
{
state.r[i] = true;
}
state.RelModLevel = 100;
}
}
else
{
state.RelModLevel = 0;
}
}
else
{
publishNew("status", "Выкл отопление");
state.stateCH = 0;
}
}
}
if (conf.antiFreez > 0 && state.Tboiler < (conf.antiFreez + 4))
{
state.r[state.currentRele] = true;
state.RelModLevel = conf.prcOnekWt * conf.relePwr[state.currentRele];
// setValue("Анти-Заморозка");
publishNew("status", "Режим анти-заморозка");
SerialPrint("i", "BoilerControl", "Режим анти-заморозка");
sendTelegramm("Режим анти-заморозка");
}
static bool prev_flame = false;
if (state.RelModLevel > 0)
state.fl_flame = true; // если хоть одно реле включено, то выставляем флаг горелки
if (state.fl_flame && prev_flame != state.fl_flame)
{
if (conf.changeRele)
{
state.currentRele++;
if (state.currentRele >= conf.countRele)
state.currentRele = 0;
}
}
prev_flame = state.fl_flame;
bool fl_pump = false;
// переключаем реле в соответсии с их статусами
for (uint8_t i = 0; i < conf.countRele; i++)
{
if (rele[i])
{
rele[i]->setValue(state.r[i] ? "1" : "0", true);
}
if (!fl_pump) // что бы не обнулить если выстиавили true
fl_pump = state.r[i] ? true : false; // если хоть одно реле включено, то включаем насос
}
if (fl_pump)
{ // если хоть одно реле включено, то включаем насос
if (relePump)
relePump->setValue("1", true);
}
else
{
if (relePump)
{ // если все реле выключены
if (state.Tboiler > conf.minCH && set.cmd_chEnable) // НО температура ещё горячая и при этом отопление включено, то включаем насос
relePump->setValue("1", true);
else // Если температура в котле уже остыла, или отопление нам не нужно (летом нагрели воду-пусть котел сам остывает без насоса), то выключаем насос
relePump->setValue("0", true);
}
}
IoTValue val;
if (_idModLevel)
{
val.valD = state.RelModLevel;
_idModLevel->setValue(val, true);
}
if (_idStateCH)
{
val.valD = state.stateCH;
_idStateCH->setValue(val, true);
}
if (_idStateFlame)
{
val.valD = state.fl_flame;
_idStateFlame->setValue(val, true);
}
// publishNew("ModLevel", String(state.RelModLevel));
// publishNew("stateCH", String(state.stateCH));
// publishNew("controlType", String(conf.ctrlType));
// publishNew("StateFlame", String(state.fl_flame));
}
//============================== ОБЕСПЕЧЕНИЕ РАБОТЫ IoTMANAGER =====================================
// конфигурирование котла в зависимости от настроек
void configuration()
{
state.fl_flame = state.stateCH = 0;
conf.countRele = conf.prcOnekWt = 0;
if (conf.relePwr[0])
{
rele[0] = findIoTItem("rele1");
if (rele[0])
{
conf.countRele++;
SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 1-го тэна");
}
}
if (conf.relePwr[1])
{
rele[1] = findIoTItem("rele2");
if (rele[1])
{
conf.countRele++;
SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 2-го тэна");
}
}
if (conf.relePwr[2])
{
rele[2] = findIoTItem("rele3");
if (rele[2])
{
conf.countRele++;
SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 3-го тэна");
}
}
for (int i = 0; i < conf.countRele; i++)
{
conf.prcOnekWt += conf.relePwr[i];
}
if (conf.countRele && conf.prcOnekWt)
{
// conf.prcOnekWt /= conf.countRele;
conf.prcOnekWt = 100 / conf.prcOnekWt;
// SerialPrint("i", "BoilerControl", "Процент одного кВт = " + String (conf.prcOnekWt) + "%");
}
if (conf.pump)
{
relePump = findIoTItem("relePump");
if (relePump)
{
SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ Насоса");
}
}
conf.ctrlType = false;
if (conf.countRele == 1 || _idPID == nullptr)
conf.ctrlType = true;
// IoTValue val;
// val.valD = conf.ctrlType;
if (_idCtrlType)
{
if (conf.ctrlType)
_idCtrlType->setValue("Вкл/Выкл", true);
else
_idCtrlType->setValue("Модуляция", true);
}
updateStateboiler();
}
void doByInterval()
{
// updateStateboiler();
// Принудительно чистим данные, что бы обновился интерфейс
OpenThemData.clear();
if (_debug > 0)
{
SerialPrint("i", "BoilerControl", "Обновляем данные в web интерфейсе");
}
if (_debug > 0)
{
SerialPrint("i", "BoilerControl", "memoryUsage: " + String(OpenThemData.memoryUsage()));
}
}
// Основной цикл программы
void loop()
{
unsigned long new_ts = millis();
int delay = 1000;
if (new_ts - ts > delay)
{
ts = new_ts;
updateStateboiler();
logicPowerOn();
}
// для новых версий IoTManager
IoTItem::loop();
}
// Комманды из сценария
IoTValue execute(String command, std::vector<IoTValue> &param)
{
IoTValue val;
if (command == "CHSet")
{
set.TSetCH = param[0].valD;
set.TSetCH = constrain(set.TSetCH, conf.minCH, conf.maxCH);
val.valD = set.TSetCH;
if (_idSetCH)
_idSetCH->setValue(val, true);
// publishNew("SetCH", String(set.TSetCH));
SerialPrint("i", "BoilerControl", "Scenario CHSet ");
}
else if (command == "CHEnable")
{
set.cmd_chEnable = param[0].valD;
val.valD = set.cmd_chEnable;
if (_idCmdCH)
_idCmdCH->setValue(val, true);
SerialPrint("i", "BoilerControl", "Scenario CHEnable ");
}
return {};
}
// обновление данных от датчиков
void updateStateboiler()
{
if (_idTboiler)
state.Tboiler = ::atof(_idTboiler->getValue().c_str());
if (_idTret)
state.Tret = ::atof(_idTret->getValue().c_str());
if (_idToutside)
state.Toutside = ::atof(_idToutside->getValue().c_str());
// if (_idStateCH)
// state.stateCH = ::atoi(_idStateCH->getValue().c_str());
// if (_idStateFlame)
// state.fl_flame = ::atoi(_idStateFlame->getValue().c_str());
// if (_idModLevel)
// state.RelModLevel = ::atof(_idModLevel->getValue().c_str());
if (_idCmdCH)
set.cmd_chEnable = ::atoi(_idCmdCH->getValue().c_str());
if (_idSetCH)
set.TSetCH = ::atof(_idSetCH->getValue().c_str());
}
~BoilerControl()
{
}
};
}
void *getAPI_SmartBoiler(String subtype, String param)
{
if (subtype == F("BoilerControl"))
{
return new _Boiler::BoilerControl(param);
}
else if (subtype == F("DHWControl"))
{
return new _Boiler::DHWControl(param);
}
else
{
return nullptr;
}
}

View File

@@ -0,0 +1,136 @@
{
"menuSection": "executive_devices",
"configItem": [
{
"global": 0,
"name": "BoilerControl",
"type": "Reading",
"subtype": "BoilerControl",
"id": "boiler",
"widget": "anydataDef",
"page": "Boiler",
"descr": "Котёл",
"int": 60,
"value": "...",
"LogLevel": 0,
"telegram": 1,
"idPID":"PID",
"idTboiler": "Tboiler",
"idTret": "Tret",
"idToutside": "Toutside",
"idStateCH":"StateCH",
"idStateFlame":"StateFlame",
"idModLevel":"ModLevel",
"idCmdCH":"CmdCH",
"idCmdDHW":"CmdDHW",
"idSetCH":"SetCH",
"idCtrlType":"CtrlType",
"rele1_Pwr": 1,
"rele2_Pwr": 2,
"rele3_Pwr": 4,
"changeRele":0,
"Pump": 0,
"minCH": 35,
"maxCH": 85,
"gistCH": 5,
"antiFreez":10
},
{
"global": 0,
"name": "DHWControl",
"type": "Reading",
"subtype": "DHWControl",
"id": "dhw",
"widget": "anydataDef",
"page": "Boiler",
"descr": "Котёл",
"int": 60,
"value": "...",
"idTdhw": "TDhw",
"idReleDhw": "ReleDhw",
"idCmdDHW":"CmdDHW",
"idStateDHW":"StateDHW",
"idSetDHW":"SetDHW",
"minDhw": 20,
"maxDhw": 60,
"gistDhw": 2
}
],
"about": {
"authorName": "Mikhail Bubnov",
"authorContact": "https://t.me/Mit4bmw",
"authorGit": "https://github.com/Mit4el",
"specialThanks": "",
"moduleName": "SmartBoiler",
"moduleVersion": "0.1",
"usedRam": {
"esp32_4mb": 15,
"esp8266_4mb": 15
},
"subTypes": [
"BoilerControl",
"OpenThermSlave"
],
"title": "SmartBoiler",
"moduleDesc": "Модуль для автоматизации электрического котла. Мозги котла с внешним протоколом opentherm",
"propInfo": {
"int": "Интервал отправки данных в MQTT и web интерфейс",
"telegram": "1- Будет отправлять в телеграмм оповещения при ошибках котла и пропаже сигнала от котла, остальные необходимо реализовывать через сценарий",
"MemberID": "SlaveMemberIDcode - код производителя котла, кем притворится котёл;) Менять в большинстве случаев не надо",
"idPID":"ID модуля ПИД регулятора, для расчета модуляции и включения тэнов в зависимости от температуры теплоносителя, в модуле TCHSet будет уставка СО, создать TCHSet и указать его в модуле ПИД",
"idTboiler": "ID датчика температуры подачи котла",
"idTret": "ID датчика температуры обратки котла",
"idToutside": "ID датчика уличной температуры",
"rele1_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele1",
"rele2_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele2, если нет, то 0 (ноль)",
"rele3_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele3, если нет, то 0 (ноль)",
"Pupm": "1-есть реле насоса (ID реле должно называться relePump), 0-нет реле насоса, насос управляется котлом без нас",
"minCH": "Граница установки температуры СО",
"maxCH": "Граница установки температуры СО",
"gistCH": "Гистерезис СО - нагрев СО включится если температура теплоносителя ниже уставки на указанные градусы (CHSet = 45гр, gistCH = 5гр, нагрев включится когда idTboiler = 40гр)",
"idTdhw": "ID датчика температуры ГВС, например в датчик в БКН",
"idReleDhw":"ID реле трехходового крана ГВС",
"gistDhw": "Гистерезис ГВС - нагрев ГВС включится если температура воды ниже уставки на указанные градусы",
"minDhw": "Граница установки температуры ГВС",
"maxDhw": "Граница установки температуры ГВС",
"changeRele":"Будет менять каждый раз при включении тэн 1->2->3->1...",
"antiFreez":"Режим анти-замерзания, Указывается температура, если опустится ниже указанной, то включится нарев один тэн и нагреет на +5гр от указанной"
},
"funcInfo": [
{
"name": "CHSet",
"descr": "Установить целевую температуру СО",
"params": [
"тепмература СО (подачи) - bolier.CHSet(60)"
]
},
{
"name": "CHEnable",
"descr": "включить / выключить отопление",
"params": [
"bolier.CHEnable(1) - вкл, bolier.CHEnable(0) - выкл, "
]
},
{
"name": "SetDHW",
"descr": "Установить целевую температуру ГВС",
"params": [
"тепмература ГВС - dhw.SetDHW(40)"
]
},
{
"name": "DHWEnable",
"descr": "включить / выключить ГВС",
"params": [
"dhw.DHWEnable(1) - вкл, dhw.DHWEnable(0) - выкл, "
]
}
]
},
"defActive": true,
"usedLibs": {
"esp32_4mb3f": [],
"esp32*": [],
"esp82*": []
}
}

View File

@@ -0,0 +1,71 @@
Модуль для автоматизации электрического котла.
(Описание модуля SmartBoiler/modinfo.json)
0 TODO Сделать чтобы при связи моих модулей промежуточные итемы можно было не создавать,
но если их создать там отобразятся актуальные значения
1 Управления котлом с 1-3 тэнами
1.1 указывается количество тэнов от 1 до 3х
1.2 задается мощность каждого тэна, должна быть по возрастающей
1.3 задание гистерезиса включения тэнов (ограничиваем в том числе и с ПИД)
1.4 задание минимальной и максимальной температуры теплоносителя (минимальную автоматически не поддерживает пока, просто проверяет вхождение в диапазон)
1.5 задание минимальной и максимальной температуры ГВС (минимальную автоматически не поддерживает пока, просто проверяет вхождение в диапазон)
1.6 реализация тремя отдельными элементами:
BoilerControl - Основная логика котла, по сути раздутый термостат
DHWControl - Логика управления нагрева ГВС
OpenThermSlave - Обеспечения протоокла взаимодействи OpenTherm
1.7 Смена тэна периодически (по флагу из конфигурации)
1.8 Если отвалились датчика, котел не включится
1.9 TODO режим антизамерзания
1.998 TODO поддержание минимальной температуры СО и ГВС
2 Поддержание температуры теплоносителя
2.1 Теплоноситель нагреется до заданной целевой и поддерживает её по гистерезису (если упала на гист. то включит нагрев)
2.2 ПИД расчитывает только количество тэнов для включения (0-30% = первый тэн, 30-60% = 1 и 2 тэны, 60-100% все тэны)
2.3 в зависимости от включенных тэнов и их мощности показывается процент модуляции.
2.4 Если не добавлен модуль ПИД, то включает все тэны
2.5 Если указан всего один тэн, то модуль ПИД не нужно создавать
2.998 TODO возможно использовать ПИД для периодического включения одного тэна
2.999 TODO сделать больше ступеней включения с различными комбинациями тэнов
3 Управление из IoTManager
3.1 Возможность управления по комнатному термостату в том числе с другой ESP
3.2 Управление модулем из сценария
3.3 есть проверка ошибок датчиков (если отвалились датчика, котел не включится)
3.4 Отправка состояния в телеграмм
3.998 3.4 TODO Автоматическая отправка состояния в модули для отображения (имена модулей в логах "new")
3.999 другой функционал IoTManager ...
4 Возможность управления циркуляционным насосом
4.1 насос включается всегда, если включено хотя бы один из тэнов
4.2 если включено по ГВС, насос отключается сразу как ГВС нагрелся до целевой
4.3 если включено по СО, насос отключается когда теплоноситель остынет до минимальной температуры СО
4.999 TODO Сделать управление выбегом насоса
5 Возможность управления ГВС при наличии 3-х ходового крана с БКН (бойлер косвенного нагрева)
5.1 ГВС работает только при указании реле 3-х ходового крана (а иначе как?)
5.2 ГВС имеет приоритет над СО
5.3 при нагреве ГВС котел включается на полную мощность (все тэны)
5.4 ГВС нагреет до заданной целевой и поддерживает её по гистерезису (если упала на гист. то включит ГВС)
5.5 Температура подачи котла при этом не должны превысить минимальную температуру СО (что бы не перегревал тенплоноситель, пока греется БКН)
6 Возможность управления по OpenTherm
6.1 в схему платы управления необходимо добавить часть OpenThermSlave и 24В
6.2 возможность управления любым OpenTherm адаптером/термостатом
6.3 задание целевой температуры теплоносителя
6.4 задание целевой температуры ГВС
6.5 команды включения СО и ГВС
6.6 отправка статуса Управляющему устройству OpenTherm
6.999 TODO Явного приоритета OpenTherm над другим управлением нет, а надо сделать. Сделать настройку "OpenTherm главне сценария" (сейчас команды выполняются ото всех по мере поступления)
7. Название модулей в которые автоматически отправится информации при их наличии (простто добавить в конфигурацию с указанным именем)
controlType - Тип управления тэнами: 0 - модуляция, 1- вкл/выкл
CHEnable - Состояние включения СО (не нагрев, а включение режима) 0 - выкл, 1- вкл
isFlame - Состояние нагрева/горелки (включенных тэнов) 0 - выкл, 1- вкл
RelModLevel - Уровень модуляции, в процентах в зависимости мощности включенных тэнов от их общего количества
TDHWSet - Установленная в котле целевая температура ГВС (из Сценария или OpenTherm)
TCHSet - Установленная в котле целевая температура СО (из Сценария или OpenTherm)
DHWEnable Состояние включения ГВС (не нагрев, а включение режима) 0 - выкл, 1- вкл
boilerslave - Состояние подключения к Управляющему устройству OpenTherm, значком ❌ ✅
status - Состояние подключения к Управляющему устройству OpenTherm, строкой: "не подключен" / "подключен"

View File

@@ -0,0 +1,337 @@
{
"mark": "iotm",
"config": [
{
"global": 0,
"type": "Writing",
"subtype": "TelegramLT",
"id": "tg",
"widget": "",
"page": "",
"descr": "",
"token": "",
"chatID": ""
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "Tboiler",
"needSave": 0,
"widget": "inputDgt",
"page": "Котёл",
"descr": "Датчик котла",
"int": "0",
"val": "20",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "SetCH",
"needSave": 0,
"widget": "inputDgt",
"page": "Котёл",
"descr": "Уставка СО",
"int": "0",
"val": "60",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "CtrlType",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": " Тип управления",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "CmdCH",
"needSave": 0,
"widget": "toggle",
"page": "Котёл",
"descr": " ВКЛ СО",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "StateCH",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": "Состояние СО",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "StateFlame",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": "Состояние Нагрева",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "rele1",
"needSave": 0,
"widget": "toggle",
"page": "Котёл",
"descr": "rele1",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "rele2",
"needSave": 0,
"widget": "toggle",
"page": "Котёл",
"descr": "rele2",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "rele3",
"needSave": 0,
"widget": "toggle",
"page": "Котёл",
"descr": "rele3",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "ModLevel",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": "Модуляция",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "relePump",
"needSave": 0,
"widget": "toggle",
"page": "Котёл",
"descr": "Реле цирк.насоса",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "ReleDhw",
"needSave": 0,
"widget": "toggle",
"page": "ГВС",
"descr": "3-хходовой",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "CmdDHW",
"needSave": 0,
"widget": "toggle",
"page": "ГВС",
"descr": " ВКЛ ГВС",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "TDhw",
"needSave": 0,
"widget": "inputDgt",
"page": "ГВС",
"descr": "Датчик БКН",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "SetDHW",
"needSave": 0,
"widget": "inputDgt",
"page": "ГВС",
"descr": "Уставка ГВС",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "StateDHW",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": "Состояние ГВС",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "DHWControl",
"id": "dhw96",
"widget": "nil",
"page": "Boiler",
"descr": "Котёл",
"int": 60,
"value": "...",
"idTDhw": "TDhw",
"idReleDhw": "ReleDhw",
"idCmdDHW": "CmdDHW",
"idStateDHW": "StateDHW",
"idSetDHW": "SetDHW",
"minDhw": 20,
"maxDhw": 60,
"gistDhw": 2
},
{
"global": 0,
"needSave": 0,
"type": "Writing",
"subtype": "ThermostatPID",
"id": "PID",
"widget": "anydataHum",
"page": "Котёл",
"descr": "термостат ПИД",
"int": "10",
"round": 1,
"map": "0,100,0,100",
"set_id": "SetCH",
"term_id": "Tboiler",
"term_rezerv_id": "",
"rele": "",
"KP": 5,
"KI": 50,
"KD": 1
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "status",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": " Состояние",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "BoilerControl",
"id": "boiler81",
"widget": "nil",
"page": "Boiler",
"descr": "Котёл",
"int": 60,
"value": "...",
"LogLevel": 0,
"telegram": 1,
"idPID": "PID",
"idTboiler": "Tboiler",
"idTret": "Tret",
"idToutside": "Toutside",
"idStateCH": "StateCH",
"idStateFlame": "StateFlame",
"idModLevel": "ModLevel",
"idCmdCH": "CmdCH",
"idCmdDHW": "CmdDHW",
"idSetCH": "SetCH",
"idCtrlType": "CtrlType",
"rele1_Pwr": 1,
"rele2_Pwr": 2,
"rele3_Pwr": 4,
"changeRele": 0,
"Pump": 0,
"minCH": 35,
"maxCH": 85,
"gistCH": 5,
"antiFreez": 10
}
]
}
scenario=>if onStart then
{
tg.sendMsg("SmartBoiler http://" + getIP());
}