diff --git a/src/OpenThermTask.h b/src/OpenThermTask.h index 7f49d2a..7d3074c 100644 --- a/src/OpenThermTask.h +++ b/src/OpenThermTask.h @@ -182,7 +182,8 @@ protected: && !vars.master.heating.overheat; // DHW settings - vars.master.dhw.enabled = settings.opentherm.options.dhwSupport + vars.master.dhw.enabled = this->isReady() + && settings.opentherm.options.dhwSupport && settings.dhw.enabled && !vars.master.dhw.overheat; vars.master.dhw.targetTemp = settings.dhw.target; @@ -190,7 +191,7 @@ protected: // CH2 settings vars.master.ch2.enabled = settings.opentherm.options.ch2AlwaysEnabled || (settings.opentherm.options.heatingToCh2 && vars.master.heating.enabled) - || (settings.opentherm.options.dhwToCh2 && settings.opentherm.options.dhwSupport && settings.dhw.enabled); + || (settings.opentherm.options.dhwToCh2 && vars.master.dhw.enabled); if (settings.opentherm.options.heatingToCh2) { vars.master.ch2.targetTemp = !settings.opentherm.options.nativeOTC @@ -211,6 +212,7 @@ protected: statusLb = 0xCA; } + // Mode flags follow the channel request, before applying its disabling action. // Summer/winter mode bool summerWinterMode = settings.opentherm.options.summerWinterMode; if (settings.opentherm.options.heatingStateToSummerWinterMode) { @@ -224,11 +226,11 @@ protected: } unsigned long response = this->instance->setBoilerStatus( - vars.master.heating.enabled, - vars.master.dhw.enabled, + this->getSlaveChannelEnabled(vars.master.heating.enabled, settings.opentherm.heatingDisablingAction), + settings.opentherm.options.dhwSupport && this->getSlaveChannelEnabled(vars.master.dhw.enabled, settings.opentherm.dhwDisablingAction), settings.opentherm.options.coolingSupport, settings.opentherm.options.nativeOTC, - vars.master.ch2.enabled, + this->getSlaveChannelEnabled(vars.master.ch2.enabled, settings.opentherm.ch2DisablingAction), summerWinterMode, dhwBlocking, statusLb @@ -1222,16 +1224,14 @@ protected: // Update DHW temp - if (vars.master.dhw.enabled) { + if (settings.opentherm.options.dhwSupport) { // Target dhw temp - const float& targetTemp = vars.master.dhw.targetTemp; + const float targetTemp = this->getSlaveChannelSetpoint( + vars.master.dhw.enabled, vars.master.dhw.targetTemp, settings.opentherm.dhwDisablingAction + ); // Converted target dhw temp - const float convertedTemp = convertTemp( - targetTemp, - settings.system.unitSystem, - settings.opentherm.unitSystem - ); + const float convertedTemp = this->convertSetpointTemp(targetTemp); // Set DHW temp if (this->needSetDhwTemp(convertedTemp)) { @@ -1328,19 +1328,16 @@ protected: // Set heating temp { // Target heating temp - float targetTemp = 0.0f; - if (vars.master.heating.enabled) { - targetTemp = !settings.opentherm.options.nativeOTC - ? vars.master.heating.setpointTemp - : vars.master.heating.targetTemp; - } + const float targetTemp = this->getSlaveChannelSetpoint( + vars.master.heating.enabled, + settings.opentherm.options.nativeOTC + ? vars.master.heating.targetTemp + : vars.master.heating.setpointTemp, + settings.opentherm.heatingDisablingAction + ); // Converted target heating temp - const float convertedTemp = convertTemp( - targetTemp, - settings.system.unitSystem, - settings.opentherm.unitSystem - ); + const float convertedTemp = this->convertSetpointTemp(targetTemp); if (this->needSetHeatingTemp(convertedTemp)) { // Set max heating temp @@ -1375,18 +1372,15 @@ protected: } // Set CH2 temp - if (settings.opentherm.options.heatingToCh2 || settings.opentherm.options.dhwToCh2) { + if (settings.opentherm.options.heatingToCh2 || settings.opentherm.options.dhwToCh2 + || (!vars.master.ch2.enabled && settings.opentherm.ch2DisablingAction != ChannelDisablingAction::OFF)) { // Target CH2 heating temp - const float targetTemp = vars.master.ch2.enabled - ? vars.master.ch2.targetTemp - : 0.0f; + const float targetTemp = this->getSlaveChannelSetpoint( + vars.master.ch2.enabled, vars.master.ch2.targetTemp, settings.opentherm.ch2DisablingAction + ); // Converted target CH2 temp - const float convertedTemp = convertTemp( - targetTemp, - settings.system.unitSystem, - settings.opentherm.unitSystem - ); + const float convertedTemp = this->convertSetpointTemp(targetTemp); if (this->needSetCh2Temp(convertedTemp)) { if (this->setCh2Temp(convertedTemp)) { @@ -1556,6 +1550,27 @@ protected: return millis() - this->instanceCreatedTime > this->readyTime; } + bool getSlaveChannelEnabled(const bool enabled, const ChannelDisablingAction action) { + return enabled || action == ChannelDisablingAction::ZERO_SETPOINT; + } + + float getSlaveChannelSetpoint(const bool enabled, const float temperature, const ChannelDisablingAction action) { + if (!enabled && action != ChannelDisablingAction::OFF) { + return 0.0f; + } + + return temperature; + } + + float convertSetpointTemp(const float temperature) { + // Preserve the no-demand zero for TSet/TsetCH2 when converting units. + if (temperature == 0.0f) { + return 0.0f; + } + + return convertTemp(temperature, settings.system.unitSystem, settings.opentherm.unitSystem); + } + bool needSetRoomSetpoint(const float target) { return !this->roomSetTempTime || millis() - this->roomSetTempTime > this->heatingSetTempInterval diff --git a/src/Settings.h b/src/Settings.h index ccd1a91..68be7a6 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -58,6 +58,9 @@ struct Settings { uint8_t rxLedGpio = DEFAULT_OT_RX_LED_GPIO; uint8_t memberId = 0; uint8_t flags = 0; + ChannelDisablingAction heatingDisablingAction = ChannelDisablingAction::OFF_AND_ZERO_SETPOINT; + ChannelDisablingAction dhwDisablingAction = ChannelDisablingAction::OFF_AND_ZERO_SETPOINT; + ChannelDisablingAction ch2DisablingAction = ChannelDisablingAction::OFF_AND_ZERO_SETPOINT; float minPower = 0.0f; float maxPower = 0.0f; @@ -113,7 +116,6 @@ struct Settings { struct { bool enabled = true; float value = 0.5f; - HysteresisAction action = HysteresisAction::DISABLE_HEATING; } hysteresis; struct { diff --git a/src/defines.h b/src/defines.h index d710c90..8a56039 100644 --- a/src/defines.h +++ b/src/defines.h @@ -163,9 +163,10 @@ enum class UnitSystem : uint8_t { IMPERIAL = 1 }; -enum class HysteresisAction : uint8_t { - DISABLE_HEATING = 0, - SET_ZERO_TARGET = 1 +enum class ChannelDisablingAction : uint8_t { + OFF = 0, + ZERO_SETPOINT = 1, + OFF_AND_ZERO_SETPOINT = 2 }; char buffer[255]; diff --git a/src/strings.h b/src/strings.h index c3ade14..8353618 100644 --- a/src/strings.h +++ b/src/strings.h @@ -35,7 +35,6 @@ const char L_CASCADE_OUTPUT[] PROGMEM = "CASCADE.OUTPUT"; const char L_EXTPUMP[] PROGMEM = "EXTPUMP"; -const char S_ACTION[] PROGMEM = "action"; const char S_ACTIONS[] PROGMEM = "actions"; const char S_ACTIVE[] PROGMEM = "active"; const char S_ADDRESS[] PROGMEM = "address"; @@ -55,6 +54,7 @@ const char S_BUILD[] PROGMEM = "build"; const char S_CASCADE_CONTROL[] PROGMEM = "cascadeControl"; const char S_CHANNEL[] PROGMEM = "channel"; const char S_CH2_ALWAYS_ENABLED[] PROGMEM = "ch2AlwaysEnabled"; +const char S_CH2_DISABLING_ACTION[] PROGMEM = "ch2DisablingAction"; const char S_CHIP[] PROGMEM = "chip"; const char S_CODE[] PROGMEM = "code"; const char S_COMMIT[] PROGMEM = "commit"; @@ -71,6 +71,7 @@ const char S_DATE[] PROGMEM = "date"; const char S_DEADBAND[] PROGMEM = "deadband"; const char S_DHW[] PROGMEM = "dhw"; const char S_DHW_BLOCKING[] PROGMEM = "dhwBlocking"; +const char S_DHW_DISABLING_ACTION[] PROGMEM = "dhwDisablingAction"; const char S_DHW_STATE_AS_DHW_BLOCKING[] PROGMEM = "dhwStateAsDhwBlocking"; const char S_DHW_SUPPORT[] PROGMEM = "dhwSupport"; const char S_DHW_TO_CH2[] PROGMEM = "dhwToCh2"; @@ -102,6 +103,7 @@ const char S_GET_MIN_MAX_TEMP[] PROGMEM = "getMinMaxTemp"; const char S_GPIO[] PROGMEM = "gpio"; const char S_HEAP[] PROGMEM = "heap"; const char S_HEATING[] PROGMEM = "heating"; +const char S_HEATING_DISABLING_ACTION[] PROGMEM = "heatingDisablingAction"; const char S_HEATING_TO_CH2[] PROGMEM = "heatingToCh2"; const char S_HEATING_STATE_TO_SUMMER_WINTER_MODE[] PROGMEM = "heatingStateToSummerWinterMode"; const char S_HIDDEN[] PROGMEM = "hidden"; diff --git a/src/utils.h b/src/utils.h index b3e626a..eb1a0b7 100644 --- a/src/utils.h +++ b/src/utils.h @@ -444,6 +444,9 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) { opentherm[FPSTR(S_RX_LED_GPIO)] = src.opentherm.rxLedGpio; opentherm[FPSTR(S_MEMBER_ID)] = src.opentherm.memberId; opentherm[FPSTR(S_FLAGS)] = src.opentherm.flags; + opentherm[FPSTR(S_HEATING_DISABLING_ACTION)] = static_cast(src.opentherm.heatingDisablingAction); + opentherm[FPSTR(S_DHW_DISABLING_ACTION)] = static_cast(src.opentherm.dhwDisablingAction); + opentherm[FPSTR(S_CH2_DISABLING_ACTION)] = static_cast(src.opentherm.ch2DisablingAction); opentherm[FPSTR(S_MIN_POWER)] = roundf(src.opentherm.minPower, 2); opentherm[FPSTR(S_MAX_POWER)] = roundf(src.opentherm.maxPower, 2); @@ -489,7 +492,6 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) { heating[FPSTR(S_TARGET)] = roundf(src.heating.target, 2); heating[FPSTR(S_HYSTERESIS)][FPSTR(S_ENABLED)] = src.heating.hysteresis.enabled; heating[FPSTR(S_HYSTERESIS)][FPSTR(S_VALUE)] = roundf(src.heating.hysteresis.value, 3); - heating[FPSTR(S_HYSTERESIS)][FPSTR(S_ACTION)] = static_cast(src.heating.hysteresis.action); heating[FPSTR(S_TURBO_FACTOR)] = roundf(src.heating.turboFactor, 3); heating[FPSTR(S_MIN_TEMP)] = src.heating.minTemp; heating[FPSTR(S_MAX_TEMP)] = src.heating.maxTemp; @@ -570,6 +572,29 @@ inline void safeSettingsToJson(const Settings& src, JsonVariant dst) { settingsToJson(src, dst, true); } +bool jsonToChannelDisablingAction(const JsonVariantConst src, ChannelDisablingAction& dst) { + if (!src.is()) { + return false; + } + + const uint8_t value = src.as(); + switch (value) { + case static_cast(ChannelDisablingAction::OFF): + case static_cast(ChannelDisablingAction::ZERO_SETPOINT): + case static_cast(ChannelDisablingAction::OFF_AND_ZERO_SETPOINT): + if (static_cast(dst) != value) { + dst = static_cast(value); + return true; + } + break; + + default: + break; + } + + return false; +} + bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false) { bool changed = false; @@ -822,6 +847,18 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } + if (jsonToChannelDisablingAction(src[FPSTR(S_OPENTHERM)][FPSTR(S_HEATING_DISABLING_ACTION)], dst.opentherm.heatingDisablingAction)) { + changed = true; + } + + if (jsonToChannelDisablingAction(src[FPSTR(S_OPENTHERM)][FPSTR(S_DHW_DISABLING_ACTION)], dst.opentherm.dhwDisablingAction)) { + changed = true; + } + + if (jsonToChannelDisablingAction(src[FPSTR(S_OPENTHERM)][FPSTR(S_CH2_DISABLING_ACTION)], dst.opentherm.ch2DisablingAction)) { + changed = true; + } + if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_MIN_POWER)].isNull()) { float value = src[FPSTR(S_OPENTHERM)][FPSTR(S_MIN_POWER)].as(); @@ -1332,23 +1369,6 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src[FPSTR(S_HEATING)][FPSTR(S_HYSTERESIS)][FPSTR(S_ACTION)].isNull()) { - uint8_t value = src[FPSTR(S_HEATING)][FPSTR(S_HYSTERESIS)][FPSTR(S_ACTION)].as(); - - switch (value) { - case static_cast(HysteresisAction::DISABLE_HEATING): - case static_cast(HysteresisAction::SET_ZERO_TARGET): - if (static_cast(dst.heating.hysteresis.action) != value) { - dst.heating.hysteresis.action = static_cast(value); - changed = true; - } - break; - - default: - break; - } - } - if (!src[FPSTR(S_HEATING)][FPSTR(S_TURBO_FACTOR)].isNull()) { float value = src[FPSTR(S_HEATING)][FPSTR(S_TURBO_FACTOR)].as(); diff --git a/src_data/locales/cn.json b/src_data/locales/cn.json index d6e15ef..8879ce7 100644 --- a/src_data/locales/cn.json +++ b/src_data/locales/cn.json @@ -352,12 +352,7 @@ "hyst": { "title": "滞回", "desc": "滞回有助于维持设定的室内温度(在使用«Equitherm»和/或«PID»时)。强制禁用加热当current indoor > target + value,启用加热当current indoor < (target - value)。", - "value": "值 (以度为单位)", - "action": { - "title": "行动", - "disableHeating": "禁用加热", - "set0target": "设置空目标" - } + "value": "值 (以度为单位)" }, "turboFactor": "Turbo 模式系数", "indoorTempAvgType": { @@ -420,6 +415,16 @@ } }, "ot": { + "disablingAction": { + "title": "回路关闭方式", + "desc": "无论因何原因关闭回路,包括过热,均采用所选的关闭方式。
某些锅炉会在关闭回路时停止循环泵,此时可尝试将目标温度设为 0,以使循环泵的延时运转功能正常工作。", + "heating": "采暖回路", + "dhw": "生活热水回路", + "ch2": "回路 2", + "off": "仅关闭回路", + "zeroSetpoint": "将目标温度设为 0", + "offAndZeroSetpoint": "关闭回路并将目标温度设为 0" + }, "advanced": "高级设置", "inGpio": "In GPIO", "outGpio": "Out GPIO", diff --git a/src_data/locales/en.json b/src_data/locales/en.json index c668f33..38d85f0 100644 --- a/src_data/locales/en.json +++ b/src_data/locales/en.json @@ -352,12 +352,7 @@ "hyst": { "title": "Hysteresis", "desc": "Hysteresis is useful for maintaining a set indoor temp (when using «Equitherm» and/or «PID»). Forces disable heating when current indoor > target + value and enable heating when current indoor < (target - value).", - "value": "Value (in degrees)", - "action": { - "title": "Action", - "disableHeating": "Disable heating", - "set0target": "Set null target" - } + "value": "Value (in degrees)" }, "turboFactor": "Turbo mode coeff.", "indoorTempAvgType": { @@ -420,6 +415,16 @@ } }, "ot": { + "disablingAction": { + "title": "Circuit disabling action", + "desc": "The selected action applies whenever the circuit is disabled, including overheating.
Some boilers stop the circulation pump when the circuit is disabled. In this case, you can try setting the target temperature to 0 so that the pump overrun works correctly.", + "heating": "Heating circuit", + "dhw": "DHW circuit", + "ch2": "Circuit 2", + "off": "Disable circuit only", + "zeroSetpoint": "Set the target temperature to 0", + "offAndZeroSetpoint": "Disable circuit and set the target temperature to 0" + }, "advanced": "Advanced Settings", "inGpio": "In GPIO", "outGpio": "Out GPIO", diff --git a/src_data/locales/it.json b/src_data/locales/it.json index 58f4bdb..cd95f33 100644 --- a/src_data/locales/it.json +++ b/src_data/locales/it.json @@ -352,12 +352,7 @@ "hyst": { "title": "Isteresi", "desc": "L'isteresi è utile per mantenere una temperatura interna impostata (quando si utilizza «Equitherm» e/o «PID»). Forza la disabilitazione del riscaldamento quando current indoor > target + value e abilita il riscaldamento quando current indoor < (target - value).", - "value": "Valore (in gradi)", - "action": { - "title": "Azione", - "disableHeating": "Disabilita riscaldamento", - "set0target": "Imposta target nullo" - } + "value": "Valore (in gradi)" }, "turboFactor": "Turbo mode coeff.", "indoorTempAvgType": { @@ -420,6 +415,16 @@ } }, "ot": { + "disablingAction": { + "title": "Azione di disattivazione dei circuiti", + "desc": "L'azione selezionata si applica a ogni disattivazione del circuito, anche in caso di surriscaldamento.
Alcune caldaie arrestano la pompa di circolazione quando il circuito viene disattivato; in questo caso si può provare a impostare la temperatura desiderata a 0, affinché la post-circolazione della pompa funzioni correttamente.", + "heating": "Circuito di riscaldamento", + "dhw": "Circuito ACS", + "ch2": "Circuito 2", + "off": "Disattiva solo il circuito", + "zeroSetpoint": "Imposta la temperatura desiderata a 0", + "offAndZeroSetpoint": "Disattiva il circuito e imposta la temperatura desiderata a 0" + }, "advanced": "Impostazioni avanzate", "inGpio": "In GPIO", "outGpio": "Out GPIO", diff --git a/src_data/locales/nl.json b/src_data/locales/nl.json index 84f9ddc..a358439 100644 --- a/src_data/locales/nl.json +++ b/src_data/locales/nl.json @@ -352,12 +352,7 @@ "hyst": { "title": "Hysterese", "desc": "Hysterese is nuttig voor het handhaven van een ingestelde binnentemperatuur (bij gebruik van «Equitherm» en/of «PID»). Forceert uitschakelen van verwarming wanneer current indoor > target + value en inschakelen van verwarming wanneer current indoor < (target - value).", - "value": "Waarde (in graden)", - "action": { - "title": "Actie", - "disableHeating": "Verwarming uitschakelen", - "set0target": "Stel null target in" - } + "value": "Waarde (in graden)" }, "turboFactor": "Turbomodus coëff.", "indoorTempAvgType": { @@ -420,6 +415,16 @@ } }, "ot": { + "disablingAction": { + "title": "Actie bij uitschakelen van circuits", + "desc": "De gekozen actie geldt bij elke uitschakeling van het circuit, ook bij oververhitting.
Sommige ketels schakelen de circulatiepomp uit wanneer het circuit wordt uitgeschakeld. In dat geval kunt u proberen de doeltemperatuur op 0 in te stellen, zodat de pompnaloop correct werkt.", + "heating": "Verwarmingscircuit", + "dhw": "Warmtapwatercircuit", + "ch2": "Circuit 2", + "off": "Alleen het circuit uitschakelen", + "zeroSetpoint": "Doeltemperatuur op 0 instellen", + "offAndZeroSetpoint": "Circuit uitschakelen en doeltemperatuur op 0 instellen" + }, "advanced": "Geavanceerde instellingen", "inGpio": "In GPIO", "outGpio": "Uit GPIO", diff --git a/src_data/locales/ru.json b/src_data/locales/ru.json index a66d086..b52ed79 100644 --- a/src_data/locales/ru.json +++ b/src_data/locales/ru.json @@ -352,12 +352,7 @@ "hyst": { "title": "Гистерезис", "desc": "Гистерезис полезен для поддержания заданной внутр. темп. (при использовании «ПЗА» и/или «ПИД»). Принудительно откл. отопление, когда current indoor > target + value, и вкл. отопление, когда current indoor < (target - value).", - "value": "Значение (в градусах)", - "action": { - "title": "Действие", - "disableHeating": "Отключить отопление", - "set0target": "Установить 0 в качестве целевой темп." - } + "value": "Значение (в градусах)" }, "turboFactor": "Коэфф. турбо режима", "indoorTempAvgType": { @@ -420,6 +415,16 @@ } }, "ot": { + "disablingAction": { + "title": "Действие отключения контуров", + "desc": "Выбранное действие применяется при любой причине отключения, включая перегрев.
Некоторые котлы отключают циркуляционный насос при отключении контура, в этом случае можно попробовать установить 0 в качестве целевой температуры, чтобы выбег насоса работал корректно.", + "heating": "Контур отопления", + "dhw": "Контур ГВС", + "ch2": "Контур 2", + "off": "Только отключить контур", + "zeroSetpoint": "Установить 0 в качестве целевой температуры", + "offAndZeroSetpoint": "Отключить контур и установить 0 в качестве целевой температуры" + }, "advanced": "Дополнительные настройки", "inGpio": "Вход GPIO", "outGpio": "Выход GPIO", diff --git a/src_data/pages/settings.html b/src_data/pages/settings.html index 376d311..5571a4a 100644 --- a/src_data/pages/settings.html +++ b/src_data/pages/settings.html @@ -239,13 +239,6 @@ - @@ -621,6 +614,81 @@ +
+ +
+ settings.ot.disablingAction.title + +
+
+ settings.ot.disablingAction.desc +
+ +
+ settings.ot.disablingAction.heating + + + + + + +
+ +
+ +
+ settings.ot.disablingAction.dhw + + + + + + +
+ +
+ +
+ settings.ot.disablingAction.ch2 + + + + + + +
+
+
+ +
+
settings.ot.options.title @@ -1124,6 +1192,9 @@ setInputValue("[name='opentherm[flags]']", data.opentherm.flags); setInputValue("[name='opentherm[minPower]']", data.opentherm.minPower); setInputValue("[name='opentherm[maxPower]']", data.opentherm.maxPower); + setRadioValue("[name='opentherm[heatingDisablingAction]']", data.opentherm.heatingDisablingAction); + setRadioValue("[name='opentherm[dhwDisablingAction]']", data.opentherm.dhwDisablingAction); + setRadioValue("[name='opentherm[ch2DisablingAction]']", data.opentherm.ch2DisablingAction); setCheckboxValue("[name='opentherm[options][dhwSupport]']", data.opentherm.options.dhwSupport); setCheckboxValue("[name='opentherm[options][coolingSupport]']", data.opentherm.options.coolingSupport); setCheckboxValue("[name='opentherm[options][summerWinterMode]']", data.opentherm.options.summerWinterMode); @@ -1190,7 +1261,6 @@ }); setCheckboxValue("[name='heating[hysteresis][enabled]']", data.heating.hysteresis.enabled); setInputValue("[name='heating[hysteresis][value]']", data.heating.hysteresis.value); - setSelectValue("[name='heating[hysteresis][action]']", data.heating.hysteresis.action); setInputValue("[name='heating[turboFactor]']", data.heating.turboFactor); setInputValue("[name='heating[maxModulation]']", data.heating.maxModulation); setSelectValue("[name='heating[indoorTempAvgType]']", data.heating.indoorTempAvgType); @@ -1402,4 +1472,4 @@ }); - \ No newline at end of file +