diff --git a/src/HaHelper.h b/src/HaHelper.h index 69ba800..3f9e390 100644 --- a/src/HaHelper.h +++ b/src/HaHelper.h @@ -439,6 +439,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->getObjectIdWithPrefix(F("heating_hysteresis")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + 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(); @@ -458,9 +480,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 7578f72..645ef3a 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 fd5e1d8..4b8dc67 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 f768a54..062d70f 100644 --- a/src/RegulatorTask.h +++ b/src/RegulatorTask.h @@ -59,12 +59,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, @@ -92,15 +103,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 1fded9f..c1a0005 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 74e2eaf..5611ed8 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 ac4ffa2..2082c84 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; @@ -1303,15 +1305,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/pages/settings.html b/src_data/pages/settings.html index 49f6327..51fff96 100644 --- a/src_data/pages/settings.html +++ b/src_data/pages/settings.html @@ -192,21 +192,48 @@
- - + +
- +
+ +
+ settings.heating.hyst.title + +
+
+ +
+ +
+ + + +
+
+ + settings.heating.hyst.desc +

@@ -956,7 +983,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, {