feat: added OT option alwaysSendIndoorTemp, rename nativeHeatingControl to nativeOTC (#204)

* added option to always send the current and target indoor temp, even if the current heating mode doesnt officially need it. This is a direct fix for WeHeat heatpumps which seem to require it.

* add option to AlwaysSendIndoorTemp and rename NativeHeatingControl to
NativeOTC.

Updated locals and settings page.

* change minor order
This commit is contained in:
inediblePotato
2025-12-12 13:08:44 +01:00
committed by GitHub
parent eafb33cb6a
commit f9cb421893
12 changed files with 72 additions and 50 deletions

View File

@@ -319,7 +319,7 @@ protected:
emergencyFlags |= 0b00000010;
}
if (settings.opentherm.options.nativeHeatingControl) {
if (settings.opentherm.options.nativeOTC) {
emergencyFlags |= 0b00000100;
}
}

View File

@@ -218,7 +218,7 @@ protected:
vars.master.heating.enabled,
vars.master.dhw.enabled,
settings.opentherm.options.coolingSupport,
settings.opentherm.options.nativeHeatingControl,
settings.opentherm.options.nativeOTC,
vars.master.ch2.enabled,
summerWinterMode,
dhwBlocking,
@@ -911,7 +911,7 @@ protected:
// Update CH2 temp
if (Sensors::getAmountByType(Sensors::Type::OT_CH2_TEMP, true)) {
if (vars.master.ch2.enabled && !settings.opentherm.options.nativeHeatingControl) {
if (vars.master.ch2.enabled && !settings.opentherm.options.nativeOTC) {
if (this->updateCh2Temp()) {
float convertedCh2Temp = convertTemp(
vars.slave.ch2.currentTemp,
@@ -1209,8 +1209,8 @@ protected:
}
}
// Native heating control
if (settings.opentherm.options.nativeHeatingControl) {
// NativeOTC or if the AlwaysSendIndoorTemp option is enabled.
if ((settings.opentherm.options.nativeOTC) || (settings.opentherm.options.alwaysSendIndoorTemp)) {
// Converted current indoor temp
float convertedTemp = convertTemp(vars.master.heating.indoorTemp, settings.system.unitSystem, settings.opentherm.unitSystem);
@@ -1238,7 +1238,6 @@ protected:
}
}
// Converted target indoor temp
convertedTemp = convertTemp(vars.master.heating.targetTemp, settings.system.unitSystem, settings.opentherm.unitSystem);
@@ -1274,7 +1273,7 @@ protected:
}
// Normal heating control
if (!settings.opentherm.options.nativeHeatingControl && vars.master.heating.enabled) {
if (!settings.opentherm.options.nativeOTC && vars.master.heating.enabled) {
// Converted target heating temp
float convertedTemp = convertTemp(vars.master.heating.setpointTemp, settings.system.unitSystem, settings.opentherm.unitSystem);
@@ -1311,7 +1310,7 @@ protected:
}
// Set CH2 temp
if (!settings.opentherm.options.nativeHeatingControl && vars.master.ch2.enabled) {
if (!settings.opentherm.options.nativeOTC && vars.master.ch2.enabled) {
if (settings.opentherm.options.heatingToCh2 || settings.opentherm.options.dhwToCh2) {
// Converted target CH2 temp
float convertedTemp = convertTemp(

View File

@@ -37,7 +37,7 @@ protected:
this->indoorSensorsConnected = Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP);
//this->outdoorSensorsConnected = Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::OUTDOOR_TEMP);
if (settings.equitherm.enabled || settings.pid.enabled || settings.opentherm.options.nativeHeatingControl) {
if (settings.equitherm.enabled || settings.pid.enabled || settings.opentherm.options.nativeOTC) {
vars.master.heating.indoorTempControl = true;
vars.master.heating.minTemp = THERMOSTAT_INDOOR_MIN_TEMP;
vars.master.heating.maxTemp = THERMOSTAT_INDOOR_MAX_TEMP;
@@ -102,7 +102,7 @@ protected:
void hysteresis() {
bool useHyst = false;
if (settings.heating.hysteresis.enabled && this->indoorSensorsConnected) {
useHyst = settings.equitherm.enabled || settings.pid.enabled || settings.opentherm.options.nativeHeatingControl;
useHyst = settings.equitherm.enabled || settings.pid.enabled || settings.opentherm.options.nativeOTC;
}
if (useHyst) {
@@ -119,13 +119,13 @@ protected:
}
inline float getHeatingMinSetpointTemp() {
return settings.opentherm.options.nativeHeatingControl
return settings.opentherm.options.nativeOTC
? vars.master.heating.minTemp
: settings.heating.minTemp;
}
inline float getHeatingMaxSetpointTemp() {
return settings.opentherm.options.nativeHeatingControl
return settings.opentherm.options.nativeOTC
? vars.master.heating.maxTemp
: settings.heating.maxTemp;
}
@@ -146,7 +146,7 @@ protected:
if (vars.emergency.state) {
return settings.emergency.target;
} else if (settings.opentherm.options.nativeHeatingControl) {
} else if (settings.opentherm.options.nativeOTC) {
return settings.heating.target;
} else if (!settings.equitherm.enabled && !settings.pid.enabled) {

View File

@@ -78,7 +78,8 @@ struct Settings {
bool autoFaultReset = false;
bool autoDiagReset = false;
bool setDateAndTime = false;
bool nativeHeatingControl = false;
bool alwaysSendIndoorTemp = true;
bool nativeOTC = false;
bool immergasFix = false;
} options;
} opentherm;

View File

@@ -110,6 +110,7 @@ const char S_HYSTERESIS[] PROGMEM = "hysteresis";
const char S_ID[] PROGMEM = "id";
const char S_IGNORE_DIAG_STATE[] PROGMEM = "ignoreDiagState";
const char S_IMMERGAS_FIX[] PROGMEM = "immergasFix";
const char S_ALWAYS_SEND_INDOOR_TEMP[] PROGMEM = "alwaysSendIndoorTemp";
const char S_INDOOR_TEMP[] PROGMEM = "indoorTemp";
const char S_INDOOR_TEMP_CONTROL[] PROGMEM = "indoorTempControl";
const char S_IN_GPIO[] PROGMEM = "inGpio";
@@ -141,7 +142,7 @@ const char S_MODEL[] PROGMEM = "model";
const char S_MODULATION[] PROGMEM = "modulation";
const char S_MQTT[] PROGMEM = "mqtt";
const char S_NAME[] PROGMEM = "name";
const char S_NATIVE_HEATING_CONTROL[] PROGMEM = "nativeHeatingControl";
const char S_NATIVE_OTC[] PROGMEM = "nativeOTC";
const char S_NETWORK[] PROGMEM = "network";
const char S_NTP[] PROGMEM = "ntp";
const char S_OFFSET[] PROGMEM = "offset";

View File

@@ -468,8 +468,10 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
otOptions[FPSTR(S_AUTO_FAULT_RESET)] = src.opentherm.options.autoFaultReset;
otOptions[FPSTR(S_AUTO_DIAG_RESET)] = src.opentherm.options.autoDiagReset;
otOptions[FPSTR(S_SET_DATE_AND_TIME)] = src.opentherm.options.setDateAndTime;
otOptions[FPSTR(S_NATIVE_HEATING_CONTROL)] = src.opentherm.options.nativeHeatingControl;
otOptions[FPSTR(S_ALWAYS_SEND_INDOOR_TEMP)] = src.opentherm.options.alwaysSendIndoorTemp;
otOptions[FPSTR(S_NATIVE_OTC)] = src.opentherm.options.nativeOTC;
otOptions[FPSTR(S_IMMERGAS_FIX)] = src.opentherm.options.immergasFix;
auto mqtt = dst[FPSTR(S_MQTT)].to<JsonObject>();
mqtt[FPSTR(S_ENABLED)] = src.mqtt.enabled;
@@ -1003,11 +1005,20 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
}
}
if (src[FPSTR(S_OPENTHERM)][FPSTR(S_OPTIONS)][FPSTR(S_NATIVE_HEATING_CONTROL)].is<bool>()) {
bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_OPTIONS)][FPSTR(S_NATIVE_HEATING_CONTROL)].as<bool>();
if (src[FPSTR(S_OPENTHERM)][FPSTR(S_OPTIONS)][FPSTR(S_ALWAYS_SEND_INDOOR_TEMP)].is<bool>()) {
bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_OPTIONS)][FPSTR(S_ALWAYS_SEND_INDOOR_TEMP)].as<bool>();
if (value != dst.opentherm.options.nativeHeatingControl) {
dst.opentherm.options.nativeHeatingControl = value;
if (value != dst.opentherm.options.alwaysSendIndoorTemp) {
dst.opentherm.options.alwaysSendIndoorTemp = value;
changed = true;
}
}
if (src[FPSTR(S_OPENTHERM)][FPSTR(S_OPTIONS)][FPSTR(S_NATIVE_OTC)].is<bool>()) {
bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_OPTIONS)][FPSTR(S_NATIVE_OTC)].as<bool>();
if (value != dst.opentherm.options.nativeOTC) {
dst.opentherm.options.nativeOTC = value;
if (value) {
dst.equitherm.enabled = false;
@@ -1027,7 +1038,6 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
}
}
// mqtt
if (src[FPSTR(S_MQTT)][FPSTR(S_ENABLED)].is<bool>()) {
bool value = src[FPSTR(S_MQTT)][FPSTR(S_ENABLED)].as<bool>();
@@ -1118,7 +1128,7 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
if (src[FPSTR(S_EQUITHERM)][FPSTR(S_ENABLED)].is<bool>()) {
bool value = src[FPSTR(S_EQUITHERM)][FPSTR(S_ENABLED)].as<bool>();
if (!dst.opentherm.options.nativeHeatingControl) {
if (!dst.opentherm.options.nativeOTC) {
if (value != dst.equitherm.enabled) {
dst.equitherm.enabled = value;
changed = true;
@@ -1171,7 +1181,7 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
if (src[FPSTR(S_PID)][FPSTR(S_ENABLED)].is<bool>()) {
bool value = src[FPSTR(S_PID)][FPSTR(S_ENABLED)].as<bool>();
if (!dst.opentherm.options.nativeHeatingControl) {
if (!dst.opentherm.options.nativeOTC) {
if (value != dst.pid.enabled) {
dst.pid.enabled = value;
changed = true;
@@ -1704,7 +1714,7 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
// force check emergency target
{
float value = !src[FPSTR(S_EMERGENCY)][FPSTR(S_TARGET)].isNull() ? src[FPSTR(S_EMERGENCY)][FPSTR(S_TARGET)].as<float>() : dst.emergency.target;
bool noRegulators = !dst.opentherm.options.nativeHeatingControl;
bool noRegulators = !dst.opentherm.options.nativeOTC;
bool valid = isValidTemp(
value,
dst.system.unitSystem,
@@ -1729,7 +1739,7 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
// force check heating target
{
bool indoorTempControl = dst.equitherm.enabled || dst.pid.enabled || dst.opentherm.options.nativeHeatingControl;
bool indoorTempControl = dst.equitherm.enabled || dst.pid.enabled || dst.opentherm.options.nativeOTC;
float minTemp = indoorTempControl ? THERMOSTAT_INDOOR_MIN_TEMP : dst.heating.minTemp;
float maxTemp = indoorTempControl ? THERMOSTAT_INDOOR_MAX_TEMP : dst.heating.maxTemp;

View File

@@ -457,12 +457,13 @@
"autoFaultReset": "自动报警复位 <small>(不推荐!)</small>",
"autoDiagReset": "自动诊断复位 <small>(不推荐!)</small>",
"setDateAndTime": "同步设置锅炉日期与时间",
"immergasFix": "针对Immergas锅炉的兼容性修复"
"immergasFix": "针对Immergas锅炉的兼容性修复",
"alwaysSendIndoorTemp": "一定要给锅炉输送电流和目标温度"
},
"nativeHeating": {
"title": "原生锅炉供暖控制",
"note": "<u>注意:</u> 仅适用于锅炉需接收目标室温并自主调节载热介质温度的场景,与固件中的PIDEquithermq气候补偿功能不兼容。"
"nativeOTC": {
"title": "本地(由锅炉处理)外部温度补偿",
"note": "只有 锅炉能调节冷却液温度时才有效。未经测试。固件中不兼容PIDEquitherm的调压器。"
}
},

View File

@@ -457,12 +457,13 @@
"autoFaultReset": "Auto fault reset <small>(not recommended!)</small>",
"autoDiagReset": "Auto diag reset <small>(not recommended!)</small>",
"setDateAndTime": "Set date & time on boiler",
"immergasFix": "Fix for Immergas boilers"
"immergasFix": "Fix for Immergas boilers",
"alwaysSendIndoorTemp": "Always send current and target indoor temp to boiler"
},
"nativeHeating": {
"title": "Native heating control (boiler)",
"note": "Works <u>ONLY</u> if the boiler requires the desired room temperature and regulates the temperature of the coolant itself. Not compatible with PID and Equitherm regulators in firmware."
"nativeOTC": {
"title": "Native (handled by boiler) Outside Temp Compensation",
"note": "Works <u>ONLY</u> if the boiler regulates the temperature of the coolant itself. Untested. Not compatible with PID and Equitherm regulators in firmware."
}
},

View File

@@ -457,12 +457,13 @@
"autoFaultReset": "Ripristino automatico degli errori <small>(sconsigliato!)</small>",
"autoDiagReset": "Ripristino diagnostico automatica <small>(sconsigliato!)</small>",
"setDateAndTime": "Imposta data e ora sulla caldaia",
"immergasFix": "Fix per caldiaie Immergas"
"immergasFix": "Fix per caldiaie Immergas",
"alwaysSendIndoorTemp": "Invia sempre corrente e la temperatura interna target alla caldaia"
},
"nativeHeating": {
"title": "Controllo del riscaldamento nativo (caldaia)",
"note": "Lavora <u>SOLO</u> se la caldaia richiede la temperatura ambiente desiderata e regola autonomamente la temperatura del fluido. Non compatiblile con regolazioni PID e Equitherm del sistema."
"nativeOTC": {
"title": "Compensazione della temperatura esterna nativa (gestita dalla caldaia)",
"note": "Lavora <u>SOLO</u> se la caldaia regola la temperatura del liquido refrigerante stesso. Testate. Non compatibile con i regolatori PID ed Equitherm nel firmware."
}
},

View File

@@ -422,11 +422,12 @@
"autoFaultReset": "Automatische storingsreset <small>(niet aanbevolen!)</small>",
"autoDiagReset": "Automatische diagnosereset <small>(niet aanbevolen!)</small>",
"setDateAndTime": "Stel datum & tijd in op ketel",
"immergasFix": "Fix voor Immergas-ketels"
"immergasFix": "Fix voor Immergas-ketels",
"alwaysSendIndoorTemp": "Stuur altijd de stroom- en streeftemperatuur binnen naar de ketel"
},
"nativeHeating": {
"title": "Natuurlijke verwarmingsregeling (ketel)",
"note": "Werkt <u>ALLEEN</u> als de ketel de gewenste kamertemperatuur vereist en zelf de temperatuur van de warmtedrager regelt. Niet compatibel met PID- en Equitherm-regelaars in de firmware."
"nativeOTC": {
"title": "Native (afgehandeld door de ketel) Buitentemperatuurcompensatie",
"note": "Werkt <u>ALLEEN</u> als de ketel de temperatuur van de koelvloeistof zelf regelt. Geteste. Niet compatibel met PID- en Equitherm-regelaars in firmware"
}
},
"mqtt": {

View File

@@ -457,12 +457,13 @@
"autoFaultReset": "Автоматический сброс ошибок <small>(не рекомендуется!)</small>",
"autoDiagReset": "Автоматический сброс диагностики <small>(не рекомендуется!)</small>",
"setDateAndTime": "Устанавливать время и дату на котле",
"immergasFix": "Фикс для котлов Immergas"
"immergasFix": "Фикс для котлов Immergas",
"alwaysSendIndoorTemp": "Всегда отправляйте ток и целевую температуру в помещении в котёл"
},
"nativeHeating": {
"title": "Передать управление отоплением котлу",
"note": "Работает <u>ТОЛЬКО</u> если котел требует и принимает целевую температуру в помещении и сам регулирует температуру теплоносителя на основе встроенного режима кривых. Несовместимо с ПИД и ПЗА."
"nativeOTC": {
"title": "Нативная (обрабатывается котлом) Внешняя компенсация температуры",
"note": "Работает <u>ТОЛЬКО</u> если котёл регулирует температуру самого охлаждающей жидкости. Непроверенных. Несовместимо с регуляторами PID и Equitherm в прошивке."
}
},

View File

@@ -692,12 +692,17 @@
<span data-i18n>settings.ot.options.immergasFix</span>
</label>
<label>
<input type="checkbox" name="opentherm[options][alwaysSendIndoorTemp]" value="true">
<span data-i18n>settings.ot.options.alwaysSendIndoorTemp</span>
</label>
<hr />
<label>
<input type="checkbox" name="opentherm[options][nativeHeatingControl]" value="true">
<span data-i18n>settings.ot.nativeHeating.title</span><br />
<small data-i18n>settings.ot.nativeHeating.note</small>
<input type="checkbox" name="opentherm[options][nativeOTC]" value="true">
<span data-i18n>settings.ot.nativeOTC.title</span><br />
<small data-i18n>settings.ot.nativeOTC.note</small>
</label>
</fieldset>
</div>
@@ -1117,8 +1122,9 @@
setCheckboxValue("[name='opentherm[options][autoFaultReset]']", data.opentherm.options.autoFaultReset);
setCheckboxValue("[name='opentherm[options][autoDiagReset]']", data.opentherm.options.autoDiagReset);
setCheckboxValue("[name='opentherm[options][setDateAndTime]']", data.opentherm.options.setDateAndTime);
setCheckboxValue("[name='opentherm[options][nativeHeatingControl]']", data.opentherm.options.nativeHeatingControl);
setCheckboxValue("[name='opentherm[options][nativeOTC]']", data.opentherm.options.nativeOTC);
setCheckboxValue("[name='opentherm[options][immergasFix]']", data.opentherm.options.immergasFix);
setCheckboxValue("[name='opentherm[options][alwaysSendIndoorTemp]']", data.opentherm.options.alwaysSendIndoorTemp);
setBusy('#ot-settings-busy', '#ot-settings', false);
// MQTT
@@ -1206,7 +1212,7 @@
setBusy('#dhw-settings-busy', '#dhw-settings', false);
// Emergency mode
if (data.opentherm.options.nativeHeatingControl) {
if (data.opentherm.options.nativeOTC) {
setInputValue("[name='emergency[target]']", data.emergency.target, {
"min": data.system.unitSystem == 0 ? 5 : 41,
"max": data.system.unitSystem == 0 ? 40 : 104