#include extern FileData fsSettings; class OpenThermTask : public Task { public: OpenThermTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {} ~OpenThermTask() { delete this->instance; } protected: const unsigned short readyTime = 60000; const unsigned short dhwSetTempInterval = 60000; const unsigned short heatingSetTempInterval = 60000; const unsigned int initializingInterval = 3600000; CustomOpenTherm* instance = nullptr; unsigned long instanceCreatedTime = 0; byte instanceInGpio = 0; byte instanceOutGpio = 0; bool isInitialized = false; unsigned long initializedTime = 0; unsigned int initializedMemberIdCode = 0; bool pump = true; unsigned long lastSuccessResponse = 0; unsigned long prevUpdateNonEssentialVars = 0; unsigned long dhwSetTempTime = 0; unsigned long heatingSetTempTime = 0; byte configuredRxLedGpio = GPIO_IS_NOT_CONFIGURED; byte configuredFaultStateGpio = GPIO_IS_NOT_CONFIGURED; bool faultState = false; #if defined(ARDUINO_ARCH_ESP32) const char* getTaskName() override { return "OpenTherm"; } BaseType_t getTaskCore() override { return 1; } int getTaskPriority() override { return 5; } #endif void setup() { if (settings.system.unitSystem != UnitSystem::METRIC) { vars.parameters.heatingMinTemp = convertTemp(vars.parameters.heatingMinTemp, UnitSystem::METRIC, settings.system.unitSystem); vars.parameters.heatingMaxTemp = convertTemp(vars.parameters.heatingMaxTemp, UnitSystem::METRIC, settings.system.unitSystem); vars.parameters.dhwMinTemp = convertTemp(vars.parameters.dhwMinTemp, UnitSystem::METRIC, settings.system.unitSystem); vars.parameters.dhwMaxTemp = convertTemp(vars.parameters.dhwMaxTemp, UnitSystem::METRIC, settings.system.unitSystem); } // delete instance if (this->instance != nullptr) { delete this->instance; this->instance = nullptr; Log.sinfoln(FPSTR(L_OT), F("Stopped")); } if (!GPIO_IS_VALID(settings.opentherm.inGpio) || !GPIO_IS_VALID(settings.opentherm.outGpio)) { Log.swarningln(FPSTR(L_OT), F("Not started. GPIO IN: %hhu or GPIO OUT: %hhu is not valid"), settings.opentherm.inGpio, settings.opentherm.outGpio); return; } // create instance this->instance = new CustomOpenTherm(settings.opentherm.inGpio, settings.opentherm.outGpio); // flags this->instanceCreatedTime = millis(); this->instanceInGpio = settings.opentherm.inGpio; this->instanceOutGpio = settings.opentherm.outGpio; this->isInitialized = false; Log.sinfoln(FPSTR(L_OT), F("Started. GPIO IN: %hhu, GPIO OUT: %hhu"), settings.opentherm.inGpio, settings.opentherm.outGpio); this->instance->setAfterSendRequestCallback([this](unsigned long request, unsigned long response, OpenThermResponseStatus status, byte attempt) { Log.straceln( FPSTR(L_OT), F("ID: %4d Request: %8lx Response: %8lx Attempt: %2d Status: %s"), CustomOpenTherm::getDataID(request), request, response, attempt, CustomOpenTherm::statusToString(status) ); if (status == OpenThermResponseStatus::SUCCESS) { this->lastSuccessResponse = millis(); if (this->configuredRxLedGpio != GPIO_IS_NOT_CONFIGURED) { digitalWrite(this->configuredRxLedGpio, HIGH); delayMicroseconds(2000); digitalWrite(this->configuredRxLedGpio, LOW); } } }); this->instance->setYieldCallback([this]() { this->delay(25); }); this->instance->begin(); } void loop() { static float currentHeatingTemp = 0.0f; static float currentDhwTemp = 0.0f; if (this->instanceInGpio != settings.opentherm.inGpio || this->instanceOutGpio != settings.opentherm.outGpio) { this->setup(); } else if (this->initializedMemberIdCode != settings.opentherm.memberIdCode || millis() - this->initializedTime > this->initializingInterval) { this->isInitialized = false; } if (this->instance == nullptr) { this->delay(5000); return; } // RX LED GPIO setup if (settings.opentherm.rxLedGpio != this->configuredRxLedGpio) { if (this->configuredRxLedGpio != GPIO_IS_NOT_CONFIGURED) { digitalWrite(this->configuredRxLedGpio, LOW); } if (GPIO_IS_VALID(settings.opentherm.rxLedGpio)) { this->configuredRxLedGpio = settings.opentherm.rxLedGpio; pinMode(this->configuredRxLedGpio, OUTPUT); digitalWrite(this->configuredRxLedGpio, LOW); } else if (this->configuredRxLedGpio != GPIO_IS_NOT_CONFIGURED) { this->configuredRxLedGpio = GPIO_IS_NOT_CONFIGURED; } } // Fault state setup if (settings.opentherm.faultStateGpio != this->configuredFaultStateGpio) { if (this->configuredFaultStateGpio != GPIO_IS_NOT_CONFIGURED) { digitalWrite(this->configuredFaultStateGpio, LOW); } if (GPIO_IS_VALID(settings.opentherm.faultStateGpio)) { this->configuredFaultStateGpio = settings.opentherm.faultStateGpio; this->faultState = false ^ settings.opentherm.invertFaultState ? HIGH : LOW; pinMode(this->configuredFaultStateGpio, OUTPUT); digitalWrite( this->configuredFaultStateGpio, this->faultState ); } else if (this->configuredFaultStateGpio != GPIO_IS_NOT_CONFIGURED) { this->configuredFaultStateGpio = GPIO_IS_NOT_CONFIGURED; } } bool heatingEnabled = (vars.states.emergency || settings.heating.enable) && this->pump && this->isReady(); bool heatingCh2Enabled = settings.opentherm.heatingCh2Enabled; if (settings.opentherm.heatingCh1ToCh2) { heatingCh2Enabled = heatingEnabled; } else if (settings.opentherm.dhwToCh2) { heatingCh2Enabled = settings.opentherm.dhwPresent && settings.dhw.enable; } // Set boiler status LB // Some boilers require this, although this is against protocol uint8_t statusLb = 0; // Immergas fix // https://arduino.ru/forum/programmirovanie/termostat-opentherm-na-esp8266?page=15#comment-649392 if (settings.opentherm.immergasFix) { statusLb = 0xCA; } unsigned long response = this->instance->setBoilerStatus( heatingEnabled, settings.opentherm.dhwPresent && settings.dhw.enable, false, settings.opentherm.nativeHeatingControl, heatingCh2Enabled, settings.opentherm.summerWinterMode, settings.opentherm.dhwBlocking, statusLb ); if (!CustomOpenTherm::isValidResponse(response)) { Log.swarningln(FPSTR(L_OT), F("Invalid response after setBoilerStatus: %s"), CustomOpenTherm::statusToString(this->instance->getLastResponseStatus())); } if (!vars.states.otStatus && millis() - this->lastSuccessResponse < 1150) { Log.sinfoln(FPSTR(L_OT), F("Connected")); vars.states.otStatus = true; } else if (vars.states.otStatus && millis() - this->lastSuccessResponse > 1150) { Log.swarningln(FPSTR(L_OT), F("Disconnected")); vars.states.otStatus = false; this->isInitialized = false; } // If boiler is disconnected, no need try setting other OT stuff if (!vars.states.otStatus) { vars.states.heating = false; vars.states.dhw = false; vars.states.flame = false; vars.states.fault = false; vars.states.diagnostic = false; // Force fault state = on if (this->configuredFaultStateGpio != GPIO_IS_NOT_CONFIGURED) { bool fState = true ^ settings.opentherm.invertFaultState ? HIGH : LOW; if (fState != this->faultState) { this->faultState = fState; digitalWrite(this->configuredFaultStateGpio, this->faultState); } } return; } if (!this->isInitialized) { Log.sinfoln(FPSTR(L_OT), F("Initializing...")); this->isInitialized = true; this->initializedTime = millis(); this->initializedMemberIdCode = settings.opentherm.memberIdCode; this->initialize(); } if (vars.parameters.heatingEnabled != heatingEnabled) { this->prevUpdateNonEssentialVars = 0; vars.parameters.heatingEnabled = heatingEnabled; Log.sinfoln(FPSTR(L_OT_HEATING), "%s", heatingEnabled ? F("Enabled") : F("Disabled")); } vars.states.heating = CustomOpenTherm::isCentralHeatingActive(response); vars.states.dhw = settings.opentherm.dhwPresent ? CustomOpenTherm::isHotWaterActive(response) : false; vars.states.flame = CustomOpenTherm::isFlameOn(response); vars.states.fault = CustomOpenTherm::isFault(response); vars.states.diagnostic = CustomOpenTherm::isDiagnostic(response); // Fault state if (this->configuredFaultStateGpio != GPIO_IS_NOT_CONFIGURED) { bool fState = vars.states.fault ^ settings.opentherm.invertFaultState ? HIGH : LOW; if (fState != this->faultState) { this->faultState = fState; digitalWrite(this->configuredFaultStateGpio, this->faultState); } } // These parameters will be updated every minute if (millis() - this->prevUpdateNonEssentialVars > 60000) { if (this->updateMinModulationLevel()) { Log.straceln(FPSTR(L_OT), F("Min modulation: %u%%, boiler max power: %u kW"), vars.parameters.minModulation, vars.sensors.maxPower); if (vars.parameters.minModulation > settings.heating.maxModulation) { settings.heating.maxModulation = vars.parameters.minModulation; fsSettings.update(); Log.snoticeln(FPSTR(L_OT_DHW), F("Updated min modulation: %hhu"), settings.heating.maxModulation); } } else { Log.swarningln(FPSTR(L_OT), F("Failed get min modulation & max power")); } if (!heatingEnabled && settings.opentherm.modulationSyncWithHeating) { if (this->setMaxModulationLevel(0)) { Log.snoticeln(FPSTR(L_OT_HEATING), F("Set max modulation 0% (off)")); } else { Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max modulation 0% (off)")); } } else { if (this->setMaxModulationLevel(settings.heating.maxModulation)) { Log.snoticeln(FPSTR(L_OT_HEATING), F("Set max modulation %hhu%%"), settings.heating.maxModulation); } else { Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max modulation %hhu%%"), settings.heating.maxModulation); } } // Get DHW min/max temp (if necessary) if (settings.opentherm.dhwPresent && settings.opentherm.getMinMaxTemp) { if (this->updateMinMaxDhwTemp()) { if (settings.dhw.minTemp < vars.parameters.dhwMinTemp) { settings.dhw.minTemp = vars.parameters.dhwMinTemp; fsSettings.update(); Log.snoticeln(FPSTR(L_OT_DHW), F("Updated min temp: %hhu"), settings.dhw.minTemp); } if (settings.dhw.maxTemp > vars.parameters.dhwMaxTemp) { settings.dhw.maxTemp = vars.parameters.dhwMaxTemp; fsSettings.update(); Log.snoticeln(FPSTR(L_OT_DHW), F("Updated max temp: %hhu"), settings.dhw.maxTemp); } } else { vars.parameters.dhwMinTemp = convertTemp(DEFAULT_DHW_MIN_TEMP, UnitSystem::METRIC, settings.system.unitSystem); vars.parameters.dhwMaxTemp = convertTemp(DEFAULT_DHW_MAX_TEMP, UnitSystem::METRIC, settings.system.unitSystem); Log.swarningln(FPSTR(L_OT_DHW), F("Failed get min/max temp")); } if (settings.dhw.minTemp >= settings.dhw.maxTemp) { settings.dhw.minTemp = vars.parameters.dhwMinTemp; settings.dhw.maxTemp = vars.parameters.dhwMaxTemp; fsSettings.update(); } } // Get heating min/max temp if (settings.opentherm.getMinMaxTemp) { if (this->updateMinMaxHeatingTemp()) { if (settings.heating.minTemp < vars.parameters.heatingMinTemp) { settings.heating.minTemp = vars.parameters.heatingMinTemp; fsSettings.update(); Log.snoticeln(FPSTR(L_OT_HEATING), F("Updated min temp: %hhu"), settings.heating.minTemp); } if (settings.heating.maxTemp > vars.parameters.heatingMaxTemp) { settings.heating.maxTemp = vars.parameters.heatingMaxTemp; fsSettings.update(); Log.snoticeln(FPSTR(L_OT_HEATING), F("Updated max temp: %hhu"), settings.heating.maxTemp); } } else { vars.parameters.heatingMinTemp = convertTemp(DEFAULT_HEATING_MIN_TEMP, UnitSystem::METRIC, settings.system.unitSystem); vars.parameters.heatingMaxTemp = convertTemp(DEFAULT_HEATING_MAX_TEMP, UnitSystem::METRIC, settings.system.unitSystem); Log.swarningln(FPSTR(L_OT_HEATING), F("Failed get min/max temp")); } } if (settings.heating.minTemp >= settings.heating.maxTemp) { settings.heating.minTemp = vars.parameters.heatingMinTemp; settings.heating.maxTemp = vars.parameters.heatingMaxTemp; fsSettings.update(); } // Get fault code (if necessary) if (vars.states.fault) { this->updateFaultCode(); } else if (vars.sensors.faultCode != 0) { vars.sensors.faultCode = 0; } // Get diagnostic code (if necessary) if (vars.states.fault || vars.states.diagnostic) { this->updateDiagCode(); } else if (vars.sensors.diagnosticCode != 0) { vars.sensors.diagnosticCode = 0; } // If filtering is disabled, then it is enough to // update these parameters once a minute if (!settings.opentherm.filterNumValues.enable) { // Get outdoor temp (if necessary) if (settings.sensors.outdoor.type == SensorType::BOILER) { this->updateOutdoorTemp(); } // Get pressure this->updatePressure(); } this->prevUpdateNonEssentialVars = millis(); } // Get current modulation level (if necessary) if (vars.states.flame && this->updateModulationLevel()) { vars.sensors.currentPower = vars.sensors.maxPower > 0 ? vars.sensors.maxPower * (vars.sensors.modulation / 100) : 0; Log.sverboseln( FPSTR(L_OT_DHW), F("Current modulation level: %.2f%%, power: %.2f of %hhu kW"), vars.sensors.modulation, vars.sensors.currentPower, vars.sensors.maxPower ); } else { vars.sensors.modulation = 0; vars.sensors.currentPower = 0; } // Update DHW sensors (if necessary) if (settings.opentherm.dhwPresent) { this->updateDhwTemp(); this->updateDhwFlowRate(); } else { vars.temperatures.dhw = 0.0f; vars.sensors.dhwFlowRate = 0.0f; } // Get current heating temp this->updateHeatingTemp(); // Get heating return temp this->updateHeatingReturnTemp(); // Get exhaust temp this->updateExhaustTemp(); // If filtering is enabled, these parameters // must be updated every time. if (settings.opentherm.filterNumValues.enable) { // Get outdoor temp (if necessary) if (settings.sensors.outdoor.type == SensorType::BOILER) { this->updateOutdoorTemp(); } // Get pressure this->updatePressure(); } // Fault reset action if (vars.actions.resetFault) { if (vars.states.fault) { if (this->instance->sendBoilerReset()) { Log.sinfoln(FPSTR(L_OT), F("Boiler fault reset successfully")); } else { Log.serrorln(FPSTR(L_OT), F("Boiler fault reset failed")); } } vars.actions.resetFault = false; } // Diag reset action if (vars.actions.resetDiagnostic) { if (vars.states.diagnostic) { if (this->instance->sendServiceReset()) { Log.sinfoln(FPSTR(L_OT), F("Boiler diagnostic reset successfully")); } else { Log.serrorln(FPSTR(L_OT), F("Boiler diagnostic reset failed")); } } vars.actions.resetDiagnostic = false; } // Update DHW temp if (settings.opentherm.dhwPresent && settings.dhw.enable && (this->needSetDhwTemp() || fabs(settings.dhw.target - currentDhwTemp) > 0.0001f)) { float convertedTemp = convertTemp(settings.dhw.target, settings.system.unitSystem, settings.opentherm.unitSystem); Log.sinfoln(FPSTR(L_OT_DHW), F("Set temp: %.2f (converted: %.2f)"), settings.dhw.target, convertedTemp); // Set DHW temp if (this->instance->setDhwTemp(convertedTemp)) { currentDhwTemp = settings.dhw.target; this->dhwSetTempTime = millis(); } else { Log.swarningln(FPSTR(L_OT_DHW), F("Failed set temp")); } // Set DHW temp to CH2 if (settings.opentherm.dhwToCh2) { if (!this->instance->setHeatingCh2Temp(convertedTemp)) { Log.swarningln(FPSTR(L_OT_DHW), F("Failed set CH2 temp")); } } } // Native heating control if (settings.opentherm.nativeHeatingControl) { // Set current indoor temp float indoorTemp = 0.0f; float convertedTemp = 0.0f; if (!vars.states.emergency || settings.sensors.indoor.type != SensorType::MANUAL) { indoorTemp = vars.temperatures.indoor; convertedTemp = convertTemp(indoorTemp, settings.system.unitSystem, settings.opentherm.unitSystem); } Log.sinfoln(FPSTR(L_OT_HEATING), F("Set current indoor temp: %.2f (converted: %.2f)"), indoorTemp, convertedTemp); if (!this->instance->setRoomTemp(convertedTemp)) { Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set current indoor temp")); } // Set target indoor temp if (this->needSetHeatingTemp() || fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001f) { convertedTemp = convertTemp(vars.parameters.heatingSetpoint, settings.system.unitSystem, settings.opentherm.unitSystem); Log.sinfoln(FPSTR(L_OT_HEATING), F("Set target indoor temp: %.2f (converted: %.2f)"), vars.parameters.heatingSetpoint, convertedTemp); if (this->instance->setRoomSetpoint(convertedTemp)) { currentHeatingTemp = vars.parameters.heatingSetpoint; this->heatingSetTempTime = millis(); } else { Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set target indoor temp")); } // Set target temp to CH2 if (settings.opentherm.heatingCh1ToCh2) { if (!this->instance->setRoomSetpointCh2(convertedTemp)) { Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set target indoor temp to CH2")); } } } // force enable pump if (!this->pump) { this->pump = true; } } else { // Update heating temp if (heatingEnabled && (this->needSetHeatingTemp() || fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001f)) { float convertedTemp = convertTemp(vars.parameters.heatingSetpoint, settings.system.unitSystem, settings.opentherm.unitSystem); Log.sinfoln(FPSTR(L_OT_HEATING), F("Set temp: %.2f (converted: %.2f)"), vars.parameters.heatingSetpoint, convertedTemp); // Set max heating temp if (this->setMaxHeatingTemp(convertedTemp)) { currentHeatingTemp = vars.parameters.heatingSetpoint; this->heatingSetTempTime = millis(); } else { Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max heating temp")); } // Set heating temp if (this->instance->setHeatingCh1Temp(convertedTemp)) { currentHeatingTemp = vars.parameters.heatingSetpoint; this->heatingSetTempTime = millis(); } else { Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set CH1 temp")); } // Set heating temp to CH2 if (settings.opentherm.heatingCh1ToCh2) { if (!this->instance->setHeatingCh2Temp(convertedTemp)) { Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set CH2 temp")); } } } // Hysteresis // Only if enabled PID or/and Equitherm if (settings.heating.hysteresis > 0 && (!vars.states.emergency || settings.emergency.usePid) && (settings.equitherm.enable || settings.pid.enable)) { float halfHyst = settings.heating.hysteresis / 2; if (this->pump && vars.temperatures.indoor - settings.heating.target + 0.0001f >= halfHyst) { this->pump = false; } else if (!this->pump && vars.temperatures.indoor - settings.heating.target - 0.0001f <= -(halfHyst)) { this->pump = true; } } else if (!this->pump) { this->pump = true; } } } void initialize() { // Not all boilers support these, only try once when the boiler becomes connected if (this->updateSlaveVersion()) { Log.straceln(FPSTR(L_OT), F("Slave version: %u, type: %u"), vars.parameters.slaveVersion, vars.parameters.slaveType); } else { Log.swarningln(FPSTR(L_OT), F("Get slave version failed")); } // 0x013F if (this->setMasterVersion(0x3F, 0x01)) { Log.straceln(FPSTR(L_OT), F("Master version: %u, type: %u"), vars.parameters.masterVersion, vars.parameters.masterType); } else { Log.swarningln(FPSTR(L_OT), F("Set master version failed")); } if (this->updateSlaveOtVersion()) { Log.straceln(FPSTR(L_OT), F("Slave OT version: %f"), vars.parameters.slaveOtVersion); } else { Log.swarningln(FPSTR(L_OT), F("Get slave OT version failed")); } if (this->setMasterOtVersion(2.2f)) { Log.straceln(FPSTR(L_OT), F("Master OT version: %f"), vars.parameters.masterOtVersion); } else { Log.swarningln(FPSTR(L_OT), F("Set master OT version failed")); } if (this->updateSlaveConfig()) { Log.straceln(FPSTR(L_OT), F("Slave member id: %u, flags: %u"), vars.parameters.slaveMemberId, vars.parameters.slaveFlags); } else { Log.swarningln(FPSTR(L_OT), F("Get slave config failed")); } if (this->setMasterConfig(settings.opentherm.memberIdCode & 0xFF, (settings.opentherm.memberIdCode & 0xFFFF) >> 8)) { Log.straceln(FPSTR(L_OT), F("Master member id: %u, flags: %u"), vars.parameters.masterMemberId, vars.parameters.masterFlags); } else { Log.swarningln(FPSTR(L_OT), F("Set master config failed")); } } bool isReady() { return millis() - this->instanceCreatedTime > this->readyTime; } bool needSetDhwTemp() { return millis() - this->dhwSetTempTime > this->dhwSetTempInterval; } bool needSetHeatingTemp() { return millis() - this->heatingSetTempTime > this->heatingSetTempInterval; } bool updateSlaveConfig() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::SConfigSMemberIDcode, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } vars.parameters.slaveMemberId = response & 0xFF; vars.parameters.slaveFlags = (response & 0xFFFF) >> 8; /*uint8_t flags = (response & 0xFFFF) >> 8; Log.straceln( "OT", 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 Raw: %u"), (bool) (flags & 0x01), (bool) (flags & 0x02), (bool) (flags & 0x04), (bool) (flags & 0x08), (bool) (flags & 0x10), (bool) (flags & 0x20), (bool) (flags & 0x40), (bool) (flags & 0x80), response & 0xFF, response );*/ return true; } /** * @brief Set the Master Config * From slave member id code: * id: slave.memberIdCode & 0xFF, * flags: (slave.memberIdCode & 0xFFFF) >> 8 * @param id * @param flags * @param force * @return true * @return false */ bool setMasterConfig(uint8_t id, uint8_t flags, bool force = false) { //uint8_t configId = settings.opentherm.memberIdCode & 0xFF; //uint8_t configFlags = (settings.opentherm.memberIdCode & 0xFFFF) >> 8; vars.parameters.masterMemberId = (force || id || settings.opentherm.memberIdCode > 65535) ? id : vars.parameters.slaveMemberId; vars.parameters.masterFlags = (force || flags || settings.opentherm.memberIdCode > 65535) ? flags : vars.parameters.slaveFlags; unsigned int request = (unsigned int) vars.parameters.masterMemberId | (unsigned int) vars.parameters.masterFlags << 8; // if empty request if (!request) { return true; } unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::WRITE_DATA, OpenThermMessageID::MConfigMMemberIDcode, request )); return CustomOpenTherm::isValidResponse(response); } bool setMaxModulationLevel(byte value) { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::WRITE_DATA, OpenThermMessageID::MaxRelModLevelSetting, CustomOpenTherm::toFloat(value) )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } vars.parameters.maxModulation = CustomOpenTherm::getFloat(response); return true; } bool updateSlaveOtVersion() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::OpenThermVersionSlave, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } vars.parameters.slaveOtVersion = CustomOpenTherm::getFloat(response); return true; } bool setMasterOtVersion(float version) { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::WRITE_DATA, OpenThermMessageID::OpenThermVersionMaster, CustomOpenTherm::toFloat(version) )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } vars.parameters.masterOtVersion = CustomOpenTherm::getFloat(response); return true; } bool updateSlaveVersion() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::SlaveVersion, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } vars.parameters.slaveVersion = response & 0xFF; vars.parameters.slaveType = (response & 0xFFFF) >> 8; return true; } bool setMasterVersion(uint8_t version, uint8_t type) { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::WRITE_DATA, OpenThermMessageID::MasterVersion, (unsigned int) version | (unsigned int) type << 8 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } vars.parameters.masterVersion = response & 0xFF; vars.parameters.masterType = (response & 0xFFFF) >> 8; return true; } bool updateMinMaxDhwTemp() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::TdhwSetUBTdhwSetLB, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } byte minTemp = response & 0xFF; byte maxTemp = (response & 0xFFFF) >> 8; if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) { vars.parameters.dhwMinTemp = convertTemp(minTemp, settings.opentherm.unitSystem, settings.system.unitSystem); vars.parameters.dhwMaxTemp = convertTemp(maxTemp, settings.opentherm.unitSystem, settings.system.unitSystem); return true; } return false; } bool updateMinMaxHeatingTemp() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::MaxTSetUBMaxTSetLB, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } byte minTemp = response & 0xFF; byte maxTemp = (response & 0xFFFF) >> 8; if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) { vars.parameters.heatingMinTemp = convertTemp(minTemp, settings.opentherm.unitSystem, settings.system.unitSystem); vars.parameters.heatingMaxTemp = convertTemp(maxTemp, settings.opentherm.unitSystem, settings.system.unitSystem); return true; } return false; } bool setMaxHeatingTemp(byte value) { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermMessageType::WRITE_DATA, OpenThermMessageID::MaxTSet, CustomOpenTherm::temperatureToData(value) )); return CustomOpenTherm::isValidResponse(response); } bool updateOutdoorTemp() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::Toutside, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } float value = settings.sensors.outdoor.offset + convertTemp( CustomOpenTherm::getFloat(response), settings.opentherm.unitSystem, settings.system.unitSystem ); if (settings.opentherm.filterNumValues.enable && fabs(vars.temperatures.outdoor) >= 0.1f) { vars.temperatures.outdoor += (value - vars.temperatures.outdoor) * settings.opentherm.filterNumValues.factor; } else { vars.temperatures.outdoor = value; } return true; } bool updateExhaustTemp() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::Texhaust, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } float value = (float) CustomOpenTherm::getInt(response); if (!isValidTemp(value, settings.opentherm.unitSystem, -40, 500)) { return false; } value = convertTemp( value, settings.opentherm.unitSystem, settings.system.unitSystem ); if (settings.opentherm.filterNumValues.enable && fabs(vars.temperatures.exhaust) >= 0.1f) { vars.temperatures.exhaust += (value - vars.temperatures.exhaust) * settings.opentherm.filterNumValues.factor; } else { vars.temperatures.exhaust = value; } return true; } bool updateHeatingTemp() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermMessageType::READ_DATA, OpenThermMessageID::Tboiler, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } float value = CustomOpenTherm::getFloat(response); if (value <= 0) { return false; } value = convertTemp( value, settings.opentherm.unitSystem, settings.system.unitSystem ); if (settings.opentherm.filterNumValues.enable && fabs(vars.temperatures.heating) >= 0.1f) { vars.temperatures.heating += (value - vars.temperatures.heating) * settings.opentherm.filterNumValues.factor; } else { vars.temperatures.heating = value; } return true; } bool updateHeatingReturnTemp() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermMessageType::READ_DATA, OpenThermMessageID::Tret, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } float value = convertTemp( CustomOpenTherm::getFloat(response), settings.opentherm.unitSystem, settings.system.unitSystem ); if (settings.opentherm.filterNumValues.enable && fabs(vars.temperatures.heatingReturn) >= 0.1f) { vars.temperatures.heatingReturn += (value - vars.temperatures.heatingReturn) * settings.opentherm.filterNumValues.factor; } else { vars.temperatures.heatingReturn = value; } return true; } bool updateDhwTemp() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermMessageType::READ_DATA, OpenThermMessageID::Tdhw, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } float value = CustomOpenTherm::getFloat(response); if (value <= 0) { return false; } value = convertTemp( value, settings.opentherm.unitSystem, settings.system.unitSystem ); if (settings.opentherm.filterNumValues.enable && fabs(vars.temperatures.dhw) >= 0.1f) { vars.temperatures.dhw += (value - vars.temperatures.dhw) * settings.opentherm.filterNumValues.factor; } else { vars.temperatures.dhw = value; } return true; } bool updateDhwFlowRate() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermMessageType::READ_DATA, OpenThermMessageID::DHWFlowRate, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } float value = CustomOpenTherm::getFloat(response); if (value < 0 || value > convertVolume(16, UnitSystem::METRIC, settings.opentherm.unitSystem)) { return false; } value = convertVolume( value * settings.opentherm.dhwFlowRateFactor, settings.opentherm.unitSystem, settings.system.unitSystem ); vars.sensors.dhwFlowRate = value > 0.09f ? value : 0.0f; return true; } bool updateFaultCode() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::ASFflags, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { vars.sensors.faultCode = 0; return false; } vars.sensors.faultCode = response & 0xFF; return true; } bool updateDiagCode() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::OEMDiagnosticCode, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { vars.sensors.diagnosticCode = 0; return false; } vars.sensors.diagnosticCode = CustomOpenTherm::getUInt(response); return true; } bool updateModulationLevel() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::RelModLevel, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } float value = CustomOpenTherm::getFloat(response); if (settings.opentherm.filterNumValues.enable && fabs(vars.sensors.modulation) >= 0.1f) { vars.sensors.modulation += (value - vars.sensors.modulation) * settings.opentherm.filterNumValues.factor; } else { vars.sensors.modulation = value; } return true; } bool updateMinModulationLevel() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::MaxCapacityMinModLevel, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } byte minModulation = response & 0xFF; byte maxPower = (response & 0xFFFF) >> 8; vars.parameters.minModulation = minModulation; vars.sensors.maxPower = maxPower; return true; } bool updatePressure() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::CHPressure, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; } float value = CustomOpenTherm::getFloat(response); if (value < 0 || value > convertPressure(5, UnitSystem::METRIC, settings.opentherm.unitSystem)) { return false; } value = convertPressure( value * settings.opentherm.pressureFactor, settings.opentherm.unitSystem, settings.system.unitSystem ); if (settings.opentherm.filterNumValues.enable && fabs(vars.sensors.pressure) >= 0.1f) { vars.sensors.pressure += (value - vars.sensors.pressure) * settings.opentherm.filterNumValues.factor; } else { vars.sensors.pressure = value; } return true; } };