mirror of
https://github.com/Laxilef/OTGateway.git
synced 2025-12-10 18:24:27 +05:00
* Added new OT parameters * Improved compatibility with the boiler ITALTHERM TIME MAX 30F * Refactoring min/max temp * Fix port forwarding: disable captive portal after connecting to wifi * Compatible with WOKWI emulator * upd README
485 lines
16 KiB
C++
485 lines
16 KiB
C++
#include <new>
|
||
#include <CustomOpenTherm.h>
|
||
|
||
CustomOpenTherm* ot;
|
||
|
||
class OpenThermTask : public Task {
|
||
public:
|
||
OpenThermTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {}
|
||
|
||
void static IRAM_ATTR handleInterrupt() {
|
||
ot->handleInterrupt();
|
||
}
|
||
|
||
protected:
|
||
const char* getTaskName() {
|
||
return "OpenTherm";
|
||
}
|
||
|
||
int getTaskCore() {
|
||
return 1;
|
||
}
|
||
|
||
void setup() {
|
||
ot = new CustomOpenTherm(settings.opentherm.inPin, settings.opentherm.outPin);
|
||
|
||
ot->setHandleSendRequestCallback(this->sendRequestCallback);
|
||
ot->begin(OpenThermTask::handleInterrupt, this->responseCallback);
|
||
|
||
ot->setYieldCallback([](void* self) {
|
||
static_cast<OpenThermTask*>(self)->delay(10);
|
||
}, this);
|
||
|
||
#ifdef LED_OT_RX_PIN
|
||
pinMode(LED_OT_RX_PIN, OUTPUT);
|
||
#endif
|
||
}
|
||
|
||
void loop() {
|
||
static byte currentHeatingTemp, currentDHWTemp = 0;
|
||
unsigned long localResponse;
|
||
|
||
if (setMasterMemberIdCode()) {
|
||
DEBUG_F("Slave member id code: %u\r\n", vars.parameters.slaveMemberIdCode);
|
||
DEBUG_F("Master member id code: %u\r\n", settings.opentherm.memberIdCode > 0 ? settings.opentherm.memberIdCode : vars.parameters.slaveMemberIdCode);
|
||
|
||
} else {
|
||
WARN("Slave member id failed");
|
||
}
|
||
|
||
bool heatingEnabled = (vars.states.emergency || settings.heating.enable) && pump && isReady();
|
||
bool heatingCh2Enabled = settings.opentherm.heatingCh2Enabled;
|
||
if (settings.opentherm.heatingCh1ToCh2) {
|
||
heatingCh2Enabled = heatingEnabled;
|
||
}
|
||
|
||
localResponse = ot->setBoilerStatus(
|
||
heatingEnabled,
|
||
settings.opentherm.dhwPresent && settings.dhw.enable,
|
||
false,
|
||
false,
|
||
heatingCh2Enabled,
|
||
settings.opentherm.summerWinterMode,
|
||
false
|
||
);
|
||
|
||
if (!ot->isValidResponse(localResponse)) {
|
||
WARN_F("Invalid response after setBoilerStatus: %s\r\n", ot->statusToString(ot->getLastResponseStatus()));
|
||
return;
|
||
}
|
||
|
||
if (vars.parameters.heatingEnabled != heatingEnabled) {
|
||
vars.parameters.heatingEnabled = heatingEnabled;
|
||
INFO_F("Heating enabled: %s\r\n", heatingEnabled ? "on\0" : "off\0");
|
||
}
|
||
|
||
vars.states.heating = ot->isCentralHeatingActive(localResponse);
|
||
vars.states.dhw = settings.opentherm.dhwPresent ? ot->isHotWaterActive(localResponse) : false;
|
||
vars.states.flame = ot->isFlameOn(localResponse);
|
||
vars.states.fault = ot->isFault(localResponse);
|
||
vars.states.diagnostic = ot->isDiagnostic(localResponse);
|
||
|
||
setMaxModulationLevel(heatingEnabled ? 100 : 0);
|
||
yield();
|
||
|
||
// Команды чтения данных котла
|
||
if (millis() - prevUpdateNonEssentialVars > 60000) {
|
||
updateSlaveParameters();
|
||
updateMasterParameters();
|
||
|
||
DEBUG_F("Master type: %u, version: %u\r\n", vars.parameters.masterType, vars.parameters.masterVersion);
|
||
DEBUG_F("Slave type: %u, version: %u\r\n", vars.parameters.slaveType, vars.parameters.slaveVersion);
|
||
|
||
if (settings.opentherm.dhwPresent) {
|
||
if (updateMinMaxDhwTemp()) {
|
||
if (settings.dhw.minTemp < vars.parameters.dhwMinTemp || settings.dhw.maxTemp > vars.parameters.dhwMaxTemp) {
|
||
settings.dhw.minTemp = vars.parameters.dhwMinTemp;
|
||
settings.dhw.maxTemp = vars.parameters.dhwMaxTemp;
|
||
}
|
||
|
||
} else {
|
||
WARN("Failed get min/max DHW temp");
|
||
}
|
||
}
|
||
|
||
if (updateMinMaxHeatingTemp()) {
|
||
if (settings.heating.minTemp < vars.parameters.heatingMinTemp || settings.heating.maxTemp > vars.parameters.heatingMaxTemp) {
|
||
settings.heating.minTemp = vars.parameters.heatingMinTemp;
|
||
settings.heating.maxTemp = vars.parameters.heatingMaxTemp;
|
||
|
||
} else {
|
||
WARN("Failed get min/max heating temp");
|
||
}
|
||
}
|
||
|
||
// force
|
||
setMaxHeatingTemp(settings.heating.maxTemp);
|
||
|
||
if (settings.sensors.outdoor.type == 0) {
|
||
updateOutsideTemp();
|
||
}
|
||
|
||
if (vars.states.fault) {
|
||
updateFaultCode();
|
||
ot->sendBoilerReset();
|
||
}
|
||
|
||
if (vars.states.diagnostic) {
|
||
ot->sendServiceReset();
|
||
}
|
||
|
||
prevUpdateNonEssentialVars = millis();
|
||
yield();
|
||
}
|
||
|
||
updatePressure();
|
||
if ((settings.opentherm.dhwPresent && settings.dhw.enable) || settings.heating.enable || heatingEnabled) {
|
||
updateModulationLevel();
|
||
|
||
} else {
|
||
vars.sensors.modulation = 0;
|
||
}
|
||
yield();
|
||
|
||
if (settings.opentherm.dhwPresent) {
|
||
updateDHWTemp();
|
||
} else {
|
||
vars.temperatures.dhw = 0;
|
||
}
|
||
|
||
updateHeatingTemp();
|
||
yield();
|
||
|
||
//
|
||
// Температура ГВС
|
||
byte newDHWTemp = settings.dhw.target;
|
||
if (settings.opentherm.dhwPresent && settings.dhw.enable && (needSetDhwTemp() || newDHWTemp != currentDHWTemp)) {
|
||
if (newDHWTemp < settings.dhw.minTemp || newDHWTemp > settings.dhw.maxTemp) {
|
||
newDHWTemp = constrain(newDHWTemp, settings.dhw.minTemp, settings.dhw.maxTemp);
|
||
}
|
||
|
||
INFO_F("Set DHW temp = %u\r\n", newDHWTemp);
|
||
|
||
// Записываем заданную температуру ГВС
|
||
if (ot->setDHWSetpoint(newDHWTemp)) {
|
||
currentDHWTemp = newDHWTemp;
|
||
dhwSetTempTime = millis();
|
||
|
||
} else {
|
||
WARN("Failed set DHW temp");
|
||
}
|
||
}
|
||
|
||
//
|
||
// Температура отопления
|
||
if (heatingEnabled && (needSetHeatingTemp() || fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001)) {
|
||
INFO_F("Setting heating temp = %u \n", vars.parameters.heatingSetpoint);
|
||
|
||
// Записываем заданную температуру
|
||
if (ot->setBoilerTemperature(vars.parameters.heatingSetpoint)) {
|
||
currentHeatingTemp = vars.parameters.heatingSetpoint;
|
||
heatingSetTempTime = millis();
|
||
|
||
} else {
|
||
WARN("Failed set heating temp");
|
||
}
|
||
|
||
if (settings.opentherm.heatingCh1ToCh2) {
|
||
if (!ot->setBoilerTemperature2(vars.parameters.heatingSetpoint)) {
|
||
WARN("Failed set ch2 heating temp");
|
||
}
|
||
}
|
||
}
|
||
|
||
// коммутационная разность (hysteresis)
|
||
// только для pid и/или equitherm
|
||
if (settings.heating.hysteresis > 0 && !vars.states.emergency && (settings.equitherm.enable || settings.pid.enable)) {
|
||
float halfHyst = settings.heating.hysteresis / 2;
|
||
if (pump && vars.temperatures.indoor - settings.heating.target + 0.0001 >= halfHyst) {
|
||
pump = false;
|
||
|
||
} else if (!pump && vars.temperatures.indoor - settings.heating.target - 0.0001 <= -(halfHyst)) {
|
||
pump = true;
|
||
}
|
||
|
||
} else if (!pump) {
|
||
pump = true;
|
||
}
|
||
}
|
||
|
||
void static sendRequestCallback(unsigned long request, unsigned long response, OpenThermResponseStatus status, byte attempt) {
|
||
printRequestDetail(ot->getDataID(request), status, request, response, attempt);
|
||
}
|
||
|
||
void static responseCallback(unsigned long result, OpenThermResponseStatus status) {
|
||
static byte attempt = 0;
|
||
|
||
switch (status) {
|
||
case OpenThermResponseStatus::TIMEOUT:
|
||
if (vars.states.otStatus && ++attempt > OPENTHERM_OFFLINE_TRESHOLD) {
|
||
vars.states.otStatus = false;
|
||
attempt = OPENTHERM_OFFLINE_TRESHOLD;
|
||
}
|
||
break;
|
||
|
||
case OpenThermResponseStatus::SUCCESS:
|
||
attempt = 0;
|
||
if (!vars.states.otStatus) {
|
||
vars.states.otStatus = true;
|
||
}
|
||
|
||
#ifdef LED_OT_RX_PIN
|
||
{
|
||
digitalWrite(LED_OT_RX_PIN, true);
|
||
unsigned long ts = millis();
|
||
while (millis() - ts < 2) {}
|
||
digitalWrite(LED_OT_RX_PIN, false);
|
||
}
|
||
#endif
|
||
break;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
protected:
|
||
unsigned short readyTime = 60000;
|
||
unsigned short dhwSetTempInterval = 60000;
|
||
unsigned short heatingSetTempInterval = 60000;
|
||
|
||
bool pump = true;
|
||
unsigned long prevUpdateNonEssentialVars = 0;
|
||
unsigned long startupTime = millis();
|
||
unsigned long dhwSetTempTime = 0;
|
||
unsigned long heatingSetTempTime = 0;
|
||
|
||
|
||
bool isReady() {
|
||
return millis() - startupTime > readyTime;
|
||
}
|
||
|
||
bool needSetDhwTemp() {
|
||
return millis() - dhwSetTempTime > dhwSetTempInterval;
|
||
}
|
||
|
||
bool needSetHeatingTemp() {
|
||
return millis() - heatingSetTempTime > heatingSetTempInterval;
|
||
}
|
||
|
||
void static printRequestDetail(OpenThermMessageID id, OpenThermResponseStatus status, unsigned long request, unsigned long response, byte attempt) {
|
||
sprintf(buffer, "OT REQUEST ID: %4d Request: %8lx Response: %8lx Attempt: %2d Status: %s", id, request, response, attempt, ot->statusToString(status));
|
||
if (status != OpenThermResponseStatus::SUCCESS) {
|
||
//WARN(buffer);
|
||
DEBUG(buffer);
|
||
} else {
|
||
DEBUG(buffer);
|
||
}
|
||
}
|
||
|
||
bool setMasterMemberIdCode() {
|
||
//=======================================================================================
|
||
// Эта группа элементов данных определяет информацию о конфигурации как на ведомых, так
|
||
// и на главных сторонах. Каждый из них имеет группу флагов конфигурации (8 бит)
|
||
// и код MemberID (1 байт). Перед передачей информации об управлении и состоянии
|
||
// рекомендуется обмен сообщениями о допустимой конфигурации ведомого устройства
|
||
// чтения и основной конфигурации записи. Нулевой код MemberID означает клиентское
|
||
// неспецифическое устройство. Номер/тип версии продукта следует использовать в сочетании
|
||
// с "кодом идентификатора участника", который идентифицирует производителя устройства.
|
||
//=======================================================================================
|
||
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::SConfigSMemberIDcode, 0)); // 0xFFFF
|
||
if (ot->isValidResponse(response)) {
|
||
vars.parameters.slaveMemberIdCode = response & 0xFF;
|
||
|
||
/*uint8_t flags = (response & 0xFFFF) >> 8;
|
||
DEBUG_F(
|
||
"MasterMemberIdCode:\r\n DHW present: %u\r\n Control type: %u\r\n Cooling configuration: %u\r\n DHW configuration: %u\r\n Pump control: %u\r\n CH2 present: %u\r\n Remote water filling function: %u\r\n Heat/cool mode control: %u\r\n Slave MemberID Code: %u\r\n",
|
||
flags & 0x01,
|
||
flags & 0x02,
|
||
flags & 0x04,
|
||
flags & 0x08,
|
||
flags & 0x10,
|
||
flags & 0x20,
|
||
flags & 0x40,
|
||
flags & 0x80,
|
||
response & 0xFF
|
||
);*/
|
||
|
||
} else if (settings.opentherm.memberIdCode <= 0) {
|
||
return false;
|
||
}
|
||
|
||
response = ot->sendRequest(ot->buildRequest(
|
||
OpenThermRequestType::WRITE,
|
||
OpenThermMessageID::MConfigMMemberIDcode,
|
||
settings.opentherm.memberIdCode > 0 ? settings.opentherm.memberIdCode : vars.parameters.slaveMemberIdCode
|
||
));
|
||
|
||
return ot->isValidResponse(response);
|
||
}
|
||
|
||
bool setMaxModulationLevel(byte value) {
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::WRITE, OpenThermMessageID::MaxRelModLevelSetting, (unsigned int)(value * 256)));
|
||
|
||
return ot->isValidResponse(response);
|
||
}
|
||
|
||
bool setOpenThermVersionMaster() {
|
||
unsigned long response;
|
||
response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::OpenThermVersionSlave, 0));
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
// INFO_F("Opentherm version slave: %f\n", ot->getFloat(response));
|
||
|
||
response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::WRITE_DATA, OpenThermMessageID::OpenThermVersionMaster, response));
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
// INFO_F("Opentherm version master: %f\n", ot->getFloat(response));
|
||
|
||
return true;
|
||
}
|
||
|
||
bool updateMasterParameters() {
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::WRITE, OpenThermMessageID::MasterVersion, 0x013F));
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
|
||
vars.parameters.masterType = (response & 0xFFFF) >> 8;
|
||
vars.parameters.masterVersion = response & 0xFF;
|
||
|
||
return true;
|
||
}
|
||
|
||
bool updateSlaveParameters() {
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::SlaveVersion, 0));
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
|
||
vars.parameters.slaveType = (response & 0xFFFF) >> 8;
|
||
vars.parameters.slaveVersion = response & 0xFF;
|
||
|
||
return true;
|
||
}
|
||
|
||
bool updateMinMaxDhwTemp() {
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::TdhwSetUBTdhwSetLB, 0));
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
|
||
byte minTemp = response & 0xFF;
|
||
byte maxTemp = (response & 0xFFFF) >> 8;
|
||
|
||
if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) {
|
||
vars.parameters.dhwMinTemp = minTemp;
|
||
vars.parameters.dhwMaxTemp = maxTemp;
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
bool updateMinMaxHeatingTemp() {
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::MaxTSetUBMaxTSetLB, 0));
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
|
||
byte minTemp = response & 0xFF;
|
||
byte maxTemp = (response & 0xFFFF) >> 8;
|
||
|
||
if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) {
|
||
vars.parameters.heatingMinTemp = minTemp;
|
||
vars.parameters.heatingMaxTemp = maxTemp;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
bool setMaxHeatingTemp(byte value) {
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::MaxTSet, ot->temperatureToData(value)));
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool updateOutsideTemp() {
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Toutside, 0));
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
|
||
vars.temperatures.outdoor = ot->getFloat(response) + settings.sensors.outdoor.offset;
|
||
return true;
|
||
}
|
||
|
||
bool updateHeatingTemp() {
|
||
unsigned long response = ot->sendRequest(ot->buildGetBoilerTemperatureRequest());
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
|
||
vars.temperatures.heating = ot->getFloat(response);
|
||
return true;
|
||
}
|
||
|
||
|
||
bool updateDHWTemp() {
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermMessageType::READ, OpenThermMessageID::Tdhw, 0));
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
|
||
vars.temperatures.dhw = ot->getFloat(response);
|
||
return true;
|
||
}
|
||
|
||
bool updateFaultCode() {
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0));
|
||
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
|
||
vars.states.faultCode = response & 0xFF;
|
||
return true;
|
||
}
|
||
|
||
bool updateModulationLevel() {
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0));
|
||
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
|
||
float modulation = ot->f88(response);
|
||
if (!vars.states.flame) {
|
||
vars.sensors.modulation = 0;
|
||
} else {
|
||
vars.sensors.modulation = modulation;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool updatePressure() {
|
||
unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0));
|
||
|
||
if (!ot->isValidResponse(response)) {
|
||
return false;
|
||
}
|
||
|
||
vars.sensors.pressure = ot->getFloat(response);
|
||
return true;
|
||
}
|
||
};
|