diff --git a/src/HaHelper.h b/src/HaHelper.h index 3b6f9ba..88a5d4d 100644 --- a/src/HaHelper.h +++ b/src/HaHelper.h @@ -443,6 +443,28 @@ public: return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("heating_turbo")).c_str(), doc); } + bool publishSwitchHeatingHysteresis(bool enabledByDefault = true) { + JsonDocument doc; + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; + doc[FPSTR(HA_UNIQUE_ID)] = this->getUniqueIdWithPrefix(F("heating_hysteresis")); + doc[FPSTR(HA_DEFAULT_ENTITY_ID)] = this->getEntityIdWithPrefix(FPSTR(HA_ENTITY_SWITCH), F("heating_hysteresis")); + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_NAME)] = F("Use heating hysteresis"); + doc[FPSTR(HA_ICON)] = F("mdi:altimeter"); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); + doc[FPSTR(HA_STATE_ON)] = true; + doc[FPSTR(HA_STATE_OFF)] = false; + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.hysteresis.enabled }}"); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); + doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"heating\": {\"hysteresis\" : {\"enabled\" : true}}}"); + doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"heating\": {\"hysteresis\" : {\"enabled\" : false}}}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; + doc.shrinkToFit(); + + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("heating_hysteresis")).c_str(), doc); + } + bool publishInputHeatingHysteresis(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { JsonDocument doc; doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); @@ -462,9 +484,9 @@ public: doc[FPSTR(HA_NAME)] = F("Heating hysteresis"); doc[FPSTR(HA_ICON)] = F("mdi:altimeter"); doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.hysteresis|float(0)|round(2) }}"); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.hysteresis.value|float(0)|round(2) }}"); doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); - doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"heating\": {\"hysteresis\" : {{ value }}}}"); + doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"heating\": {\"hysteresis\" : {\"value\" : {{ value }}}}}"); doc[FPSTR(HA_MIN)] = 0; doc[FPSTR(HA_MAX)] = 15; doc[FPSTR(HA_STEP)] = 0.01f; diff --git a/src/MqttTask.h b/src/MqttTask.h index b629d78..b9f6132 100644 --- a/src/MqttTask.h +++ b/src/MqttTask.h @@ -486,6 +486,7 @@ protected: void publishHaEntities() { // heating this->haHelper->publishSwitchHeatingTurbo(false); + this->haHelper->publishSwitchHeatingHysteresis(); this->haHelper->publishInputHeatingHysteresis(settings.system.unitSystem); this->haHelper->publishInputHeatingTurboFactor(false); this->haHelper->publishInputHeatingMinTemp(settings.system.unitSystem); diff --git a/src/OpenThermTask.h b/src/OpenThermTask.h index 1dbb93b..c609597 100644 --- a/src/OpenThermTask.h +++ b/src/OpenThermTask.h @@ -171,7 +171,7 @@ protected: vars.master.heating.enabled = this->isReady() && settings.heating.enabled && vars.cascadeControl.input - && !vars.master.heating.blocking + && (!vars.master.heating.blocking || settings.heating.hysteresis.action != HysteresisAction::DISABLE_HEATING) && !vars.master.heating.overheat; // DHW settings diff --git a/src/RegulatorTask.h b/src/RegulatorTask.h index 4334df7..43df116 100644 --- a/src/RegulatorTask.h +++ b/src/RegulatorTask.h @@ -57,12 +57,23 @@ protected: this->turbo(); this->hysteresis(); - vars.master.heating.targetTemp = settings.heating.target; - vars.master.heating.setpointTemp = roundf(constrain( - this->getHeatingSetpointTemp(), - this->getHeatingMinSetpointTemp(), - this->getHeatingMaxSetpointTemp() - ), 0); + if (vars.master.heating.blocking && settings.heating.hysteresis.action == HysteresisAction::SET_ZERO_TARGET) { + vars.master.heating.targetTemp = 0.0f; + vars.master.heating.setpointTemp = 0.0f; + + // tick if PID enabled + if (settings.pid.enabled) { + this->getHeatingSetpointTemp(); + } + + } else { + vars.master.heating.targetTemp = settings.heating.target; + vars.master.heating.setpointTemp = roundf(constrain( + this->getHeatingSetpointTemp(), + this->getHeatingMinSetpointTemp(), + this->getHeatingMaxSetpointTemp() + ), 0); + } Sensors::setValueByType( Sensors::Type::HEATING_SETPOINT_TEMP, vars.master.heating.setpointTemp, @@ -90,15 +101,15 @@ protected: void hysteresis() { bool useHyst = false; - if (settings.heating.hysteresis > 0.01f && this->indoorSensorsConnected) { + if (settings.heating.hysteresis.enabled && this->indoorSensorsConnected) { useHyst = settings.equitherm.enabled || settings.pid.enabled || settings.opentherm.options.nativeHeatingControl; } if (useHyst) { - if (!vars.master.heating.blocking && vars.master.heating.indoorTemp - settings.heating.target + 0.0001f >= settings.heating.hysteresis) { + if (!vars.master.heating.blocking && vars.master.heating.indoorTemp - settings.heating.target + 0.0001f >= settings.heating.hysteresis.value) { vars.master.heating.blocking = true; - } else if (vars.master.heating.blocking && vars.master.heating.indoorTemp - settings.heating.target - 0.0001f <= -(settings.heating.hysteresis)) { + } else if (vars.master.heating.blocking && vars.master.heating.indoorTemp - settings.heating.target - 0.0001f <= -(settings.heating.hysteresis.value)) { vars.master.heating.blocking = false; } diff --git a/src/Settings.h b/src/Settings.h index c09fb89..43c3871 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -103,12 +103,17 @@ struct Settings { bool enabled = true; bool turbo = false; float target = DEFAULT_HEATING_TARGET_TEMP; - float hysteresis = 0.5f; float turboFactor = 7.5f; uint8_t minTemp = DEFAULT_HEATING_MIN_TEMP; uint8_t maxTemp = DEFAULT_HEATING_MAX_TEMP; uint8_t maxModulation = 100; + struct { + bool enabled = true; + float value = 0.5f; + HysteresisAction action = HysteresisAction::DISABLE_HEATING; + } hysteresis; + struct { uint8_t highTemp = 95; uint8_t lowTemp = 90; diff --git a/src/defines.h b/src/defines.h index f5e0de9..89443da 100644 --- a/src/defines.h +++ b/src/defines.h @@ -163,4 +163,9 @@ enum class UnitSystem : uint8_t { IMPERIAL = 1 }; +enum class HysteresisAction : uint8_t { + DISABLE_HEATING = 0, + SET_ZERO_TARGET = 1 +}; + char buffer[255]; \ No newline at end of file diff --git a/src/strings.h b/src/strings.h index 961721e..510c392 100644 --- a/src/strings.h +++ b/src/strings.h @@ -34,6 +34,7 @@ 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"; diff --git a/src/utils.h b/src/utils.h index 2c7b64a..c3d5dfc 100644 --- a/src/utils.h +++ b/src/utils.h @@ -490,7 +490,9 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) { heating[FPSTR(S_ENABLED)] = src.heating.enabled; heating[FPSTR(S_TURBO)] = src.heating.turbo; heating[FPSTR(S_TARGET)] = roundf(src.heating.target, 2); - heating[FPSTR(S_HYSTERESIS)] = roundf(src.heating.hysteresis, 3); + 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; @@ -1314,15 +1316,41 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src[FPSTR(S_HEATING)][FPSTR(S_HYSTERESIS)].isNull()) { - float value = src[FPSTR(S_HEATING)][FPSTR(S_HYSTERESIS)].as(); + if (src[FPSTR(S_HEATING)][FPSTR(S_HYSTERESIS)][FPSTR(S_ENABLED)].is()) { + bool value = src[FPSTR(S_HEATING)][FPSTR(S_HYSTERESIS)][FPSTR(S_ENABLED)].as(); - if (value >= 0.0f && value <= 15.0f && fabsf(value - dst.heating.hysteresis) > 0.0001f) { - dst.heating.hysteresis = roundf(value, 2); + if (value != dst.heating.hysteresis.enabled) { + dst.heating.hysteresis.enabled = value; changed = true; } } + if (!src[FPSTR(S_HEATING)][FPSTR(S_HYSTERESIS)][FPSTR(S_VALUE)].isNull()) { + float value = src[FPSTR(S_HEATING)][FPSTR(S_HYSTERESIS)][FPSTR(S_VALUE)].as(); + + if (value >= 0.0f && value <= 15.0f && fabsf(value - dst.heating.hysteresis.value) > 0.0001f) { + dst.heating.hysteresis.value = roundf(value, 2); + changed = true; + } + } + + 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 10a3a6b..33f1269 100644 --- a/src_data/locales/cn.json +++ b/src_data/locales/cn.json @@ -356,7 +356,16 @@ }, "heating": { - "hyst": "滞后值(单位:度)", + "hyst": { + "title": "滞回", + "desc": "滞回有助于维持设定的室内温度(在使用«Equitherm»和/或«PID»时)。强制禁用加热当current indoor > target + value,启用加热当current indoor < (target - value)。", + "value": "值 (以度为单位)", + "action": { + "title": "行动", + "disableHeating": "禁用加热", + "set0target": "设置空目标" + } + }, "turboFactor": "Turbo 模式系数" }, diff --git a/src_data/locales/en.json b/src_data/locales/en.json index f460b79..732a5f7 100644 --- a/src_data/locales/en.json +++ b/src_data/locales/en.json @@ -356,7 +356,16 @@ }, "heating": { - "hyst": "Hysteresis (in degrees)", + "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" + } + }, "turboFactor": "Turbo mode coeff." }, diff --git a/src_data/locales/it.json b/src_data/locales/it.json index a9a0227..3f295f2 100644 --- a/src_data/locales/it.json +++ b/src_data/locales/it.json @@ -356,7 +356,16 @@ }, "heating": { - "hyst": "Isteresi (in gradi)", + "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" + } + }, "turboFactor": "Turbo mode coeff." }, diff --git a/src_data/locales/nl.json b/src_data/locales/nl.json index 65ff5a3..5940579 100644 --- a/src_data/locales/nl.json +++ b/src_data/locales/nl.json @@ -327,7 +327,16 @@ } }, "heating": { - "hyst": "Hysterese (in graden)", + "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" + } + }, "turboFactor": "Turbomodus coëff." }, "emergency": { diff --git a/src_data/locales/ru.json b/src_data/locales/ru.json index 09b1b4d..ae70304 100644 --- a/src_data/locales/ru.json +++ b/src_data/locales/ru.json @@ -356,7 +356,16 @@ }, "heating": { - "hyst": "Гистерезис (в градусах)", + "hyst": { + "title": "Гистерезис", + "desc": "Гистерезис полезен для поддержания заданной внутр. темп. (при использовании «Equitherm» и/или «PID»). Принудительно откл. отопление, когда current indoor > target + value, и вкл. отопление, когда current indoor < (target - value).", + "value": "Значение (в градусах)", + "action": { + "title": "Действие", + "disableHeating": "Отключить отопление", + "set0target": "Установить 0 в качестве целевой темп." + } + }, "turboFactor": "Коэфф. турбо режима" }, diff --git a/src_data/pages/settings.html b/src_data/pages/settings.html index 04f3dbc..8630dbc 100644 --- a/src_data/pages/settings.html +++ b/src_data/pages/settings.html @@ -193,21 +193,48 @@
- - + +
- +
+ +
+ settings.heating.hyst.title + +
+
+ +
+ +
+ + + +
+
+ + settings.heating.hyst.desc +

@@ -1138,7 +1165,9 @@ "min": data.system.unitSystem == 0 ? 1 : 33, "max": data.system.unitSystem == 0 ? 100 : 212 }); - setInputValue("[name='heating[hysteresis]']", data.heating.hysteresis); + 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); setInputValue("[name='heating[overheatProtection][highTemp]']", data.heating.overheatProtection.highTemp, {