From 8475833dcee9cfca64edd4dfe849afe387572483 Mon Sep 17 00:00:00 2001 From: Yurii Date: Fri, 24 Jan 2025 01:43:52 +0300 Subject: [PATCH] feat: added deadband for pid --- src/OpenThermTask.h | 5 ++- src/RegulatorTask.h | 13 +++++- src/Settings.h | 9 ++++ src/strings.h | 6 +++ src/utils.h | 62 ++++++++++++++++++++++++++++ src_data/locales/en.json | 14 ++++++- src_data/locales/it.json | 14 ++++++- src_data/locales/ru.json | 14 ++++++- src_data/pages/settings.html | 80 +++++++++++++++++++++++++++++++----- 9 files changed, 201 insertions(+), 16 deletions(-) diff --git a/src/OpenThermTask.h b/src/OpenThermTask.h index 21e85c4..02f2d7c 100644 --- a/src/OpenThermTask.h +++ b/src/OpenThermTask.h @@ -1062,7 +1062,10 @@ protected: ); } else { - Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max heating temp")); + Log.swarningln( + FPSTR(L_OT_HEATING), F("Failed set max heating temp: %.2f (converted: %.2f)"), + vars.master.heating.setpointTemp, convertedTemp + ); } } diff --git a/src/RegulatorTask.h b/src/RegulatorTask.h index 7f63f08..cc67d39 100644 --- a/src/RegulatorTask.h +++ b/src/RegulatorTask.h @@ -196,6 +196,7 @@ protected: //if (vars.parameters.heatingEnabled) { if (settings.heating.enabled && this->indoorSensorsConnected) { pidRegulator.Kp = settings.heating.turbo ? 0.0f : settings.pid.p_factor; + pidRegulator.Ki = settings.pid.i_factor; pidRegulator.Kd = settings.pid.d_factor; pidRegulator.setLimits(settings.pid.minTemp, settings.pid.maxTemp); @@ -203,12 +204,22 @@ protected: pidRegulator.input = vars.master.heating.indoorTemp; pidRegulator.setpoint = settings.heating.target; - if (fabsf(pidRegulator.Ki - settings.pid.i_factor) >= 0.0001f) { + /*if (fabsf(pidRegulator.Ki - settings.pid.i_factor) >= 0.0001f) { pidRegulator.Ki = settings.pid.i_factor; pidRegulator.integral = 0.0f; pidRegulator.getResultNow(); Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset")); + }*/ + + float error = pidRegulator.setpoint - pidRegulator.input; + bool hasDeadband = (error > -(settings.pid.deadband.thresholdHigh)) + && (error < settings.pid.deadband.thresholdLow); + + if (hasDeadband) { + pidRegulator.Kp *= settings.pid.deadband.p_multiplier; + pidRegulator.Ki *= settings.pid.deadband.i_multiplier; + pidRegulator.Kd *= settings.pid.deadband.d_multiplier; } float pidResult = pidRegulator.getResultTimer(); diff --git a/src/Settings.h b/src/Settings.h index 88740a4..da4646c 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -115,6 +115,15 @@ struct Settings { unsigned short dt = 180; short minTemp = 0; short maxTemp = DEFAULT_HEATING_MAX_TEMP; + + struct { + bool enabled = false; + float p_multiplier = 1.0f; + float i_multiplier = 0.05f; + float d_multiplier = 0.0f; + float thresholdHigh = 0.5f; + float thresholdLow = 1.0f; + } deadband; } pid; struct { diff --git a/src/strings.h b/src/strings.h index 08d995d..ba6fab0 100644 --- a/src/strings.h +++ b/src/strings.h @@ -63,6 +63,7 @@ const char S_CRASH[] PROGMEM = "crash"; const char S_CURRENT_TEMP[] PROGMEM = "currentTemp"; const char S_DATA[] PROGMEM = "data"; 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_SUPPORT[] PROGMEM = "dhwSupport"; @@ -71,6 +72,7 @@ const char S_DIAG[] PROGMEM = "diag"; const char S_DNS[] PROGMEM = "dns"; const char S_DT[] PROGMEM = "dt"; const char S_D_FACTOR[] PROGMEM = "d_factor"; +const char S_D_MULTIPLIER[] PROGMEM = "d_multiplier"; const char S_EMERGENCY[] PROGMEM = "emergency"; const char S_ENABLED[] PROGMEM = "enabled"; const char S_ENV[] PROGMEM = "env"; @@ -108,6 +110,7 @@ const char S_INTERVAL[] PROGMEM = "interval"; const char S_INVERT_STATE[] PROGMEM = "invertState"; const char S_IP[] PROGMEM = "ip"; const char S_I_FACTOR[] PROGMEM = "i_factor"; +const char S_I_MULTIPLIER[] PROGMEM = "i_multiplier"; const char S_K_FACTOR[] PROGMEM = "k_factor"; const char S_LOGIN[] PROGMEM = "login"; const char S_LOG_LEVEL[] PROGMEM = "logLevel"; @@ -152,6 +155,7 @@ const char S_PREFIX[] PROGMEM = "prefix"; const char S_PROTOCOL_VERSION[] PROGMEM = "protocolVersion"; const char S_PURPOSE[] PROGMEM = "purpose"; const char S_P_FACTOR[] PROGMEM = "p_factor"; +const char S_P_MULTIPLIER[] PROGMEM = "p_multiplier"; const char S_REAL_SIZE[] PROGMEM = "realSize"; const char S_REASON[] PROGMEM = "reason"; const char S_RESET_DIAGNOSTIC[] PROGMEM = "resetDiagnostic"; @@ -183,6 +187,8 @@ const char S_TARGET[] PROGMEM = "target"; const char S_TARGET_TEMP[] PROGMEM = "targetTemp"; const char S_TELNET[] PROGMEM = "telnet"; const char S_TEMPERATURE[] PROGMEM = "temperature"; +const char S_THRESHOLD_HIGH[] PROGMEM = "thresholdHigh"; +const char S_THRESHOLD_LOW[] PROGMEM = "thresholdLow"; const char S_THRESHOLD_TIME[] PROGMEM = "thresholdTime"; const char S_TOTAL[] PROGMEM = "total"; const char S_TRESHOLD_TIME[] PROGMEM = "tresholdTime"; diff --git a/src/utils.h b/src/utils.h index de34a1d..7cf72e7 100644 --- a/src/utils.h +++ b/src/utils.h @@ -438,6 +438,14 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) { pid[FPSTR(S_MIN_TEMP)] = src.pid.minTemp; pid[FPSTR(S_MAX_TEMP)] = src.pid.maxTemp; + auto pidDeadband = pid[FPSTR(S_DEADBAND)].to(); + pidDeadband[FPSTR(S_ENABLED)] = src.pid.deadband.enabled; + pidDeadband[FPSTR(S_P_MULTIPLIER)] = src.pid.deadband.p_multiplier; + pidDeadband[FPSTR(S_I_MULTIPLIER)] = src.pid.deadband.i_multiplier; + pidDeadband[FPSTR(S_D_MULTIPLIER)] = src.pid.deadband.d_multiplier; + pidDeadband[FPSTR(S_THRESHOLD_HIGH)] = src.pid.deadband.thresholdHigh; + pidDeadband[FPSTR(S_THRESHOLD_LOW)] = src.pid.deadband.thresholdLow; + if (!safe) { auto externalPump = dst[FPSTR(S_EXTERNAL_PUMP)].to(); externalPump[FPSTR(S_USE)] = src.externalPump.use; @@ -1075,6 +1083,60 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false changed = true; } + if (src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_ENABLED)].is()) { + bool value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_ENABLED)].as(); + + if (value != dst.pid.deadband.enabled) { + dst.pid.deadband.enabled = value; + changed = true; + } + } + + if (!src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_P_MULTIPLIER)].isNull()) { + float value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_P_MULTIPLIER)].as(); + + if (value >= 0 && value <= 1 && fabsf(value - dst.pid.deadband.p_multiplier) > 0.0001f) { + dst.pid.deadband.p_multiplier = roundf(value, 3); + changed = true; + } + } + + if (!src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_I_MULTIPLIER)].isNull()) { + float value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_I_MULTIPLIER)].as(); + + if (value >= 0 && value <= 1 && fabsf(value - dst.pid.deadband.i_multiplier) > 0.0001f) { + dst.pid.deadband.i_multiplier = roundf(value, 3); + changed = true; + } + } + + if (!src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_D_MULTIPLIER)].isNull()) { + float value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_D_MULTIPLIER)].as(); + + if (value >= 0 && value <= 1 && fabsf(value - dst.pid.deadband.d_multiplier) > 0.0001f) { + dst.pid.deadband.d_multiplier = roundf(value, 3); + changed = true; + } + } + + if (!src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_THRESHOLD_HIGH)].isNull()) { + float value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_THRESHOLD_HIGH)].as(); + + if (value >= 0.0f && value <= 5.0f && fabsf(value - dst.pid.deadband.thresholdHigh) > 0.0001f) { + dst.pid.deadband.thresholdHigh = roundf(value, 2); + changed = true; + } + } + + if (!src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_THRESHOLD_LOW)].isNull()) { + float value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_THRESHOLD_LOW)].as(); + + if (value >= 0.0f && value <= 5.0f && fabsf(value - dst.pid.deadband.thresholdLow) > 0.0001f) { + dst.pid.deadband.thresholdLow = roundf(value, 2); + changed = true; + } + } + // heating if (src[FPSTR(S_HEATING)][FPSTR(S_ENABLED)].is()) { diff --git a/src_data/locales/en.json b/src_data/locales/en.json index 1c34587..ce55f43 100644 --- a/src_data/locales/en.json +++ b/src_data/locales/en.json @@ -347,7 +347,19 @@ "i": "I factor", "d": "D factor", "dt": "DT in seconds", - "noteMinMaxTemp": "Important: When using «Equitherm» and «PID» at the same time, the min and max temperatures limit the influence on the «Equitherm» result temperature.
Thus, if the min temperature is set to -15 and the max temperature is set to 15, then the final heat carrier setpoint will be from equitherm_result - 15 to equitherm_result + 15." + "limits": { + "title": "Limits", + "note": "Important: When using «Equitherm» and «PID» at the same time, the min and max temperatures limit the influence on the «Equitherm» result temperature.
Thus, if the min temperature is set to -15 and the max temperature is set to 15, then the final heat carrier setpoint will be from equitherm_result - 15 to equitherm_result + 15." + }, + "deadband": { + "title": "Deadband", + "note": "Deadband is a range around the target temperature where PID regulation becomes less active. Within this range, the algorithm can reduce intensity or pause adjustments to avoid overreacting to small fluctuations.

For instance, with a target temperature of 22°, a lower threshold of 1.0, and an upper threshold of 0.5, the deadband operates between 21° and 22.5°. If the I coefficient is 0.0005 and the I multiplier is 0.05, then within the deadband, the I coefficient becomes: 0.0005 * 0.05 = 0.000025", + "p_multiplier": "Multiplier for P factor", + "i_multiplier": "Multiplier for I factor", + "d_multiplier": "Multiplier for D factor", + "thresholdHigh": "Threshold high", + "thresholdLow": "Threshold low" + } }, "ot": { diff --git a/src_data/locales/it.json b/src_data/locales/it.json index e3bd749..6907e9c 100644 --- a/src_data/locales/it.json +++ b/src_data/locales/it.json @@ -347,7 +347,19 @@ "i": "Fattore I", "d": "Fattore D", "dt": "DT in secondi", - "noteMinMaxTemp": "Importante: Quando usi «Equitherm» e «PID» allo stesso tempo, i limiti della temperatura min e max influenzano il risultato della temperatura «Equitherm».
Thus, se la temperatura minima è impostata a -15 e la massima a 15, il riscaldamento finale sarà impostato fra equitherm_result - 15 a equitherm_result + 15." + "limits": { + "title": "Limiti", + "note": "Importante: Quando usi «Equitherm» e «PID» allo stesso tempo, i limiti della temperatura min e max influenzano il risultato della temperatura «Equitherm».
Thus, se la temperatura minima è impostata a -15 e la massima a 15, il riscaldamento finale sarà impostato fra equitherm_result - 15 a equitherm_result + 15." + }, + "deadband": { + "title": "Zona morta (Deadband)", + "note": "La zona morta è un intervallo intorno alla temperatura target in cui la regolazione PID diventa meno attiva. In questo intervallo, l'algoritmo può ridurre l'intensità o interrompere gli aggiustamenti per evitare di reagire eccessivamente a piccole fluttuazioni.

Ad esempio, con una temperatura target di 22°, una soglia inferiore di 1.0 e una soglia superiore di 0.5, la zona morta opera tra 21° e 22.5°. Se il coefficiente I è 0.0005 e il moltiplicatore I è 0.05, allora nella zona morta, il coefficiente I diventa: 0.0005 * 0.05 = 0.000025", + "p_multiplier": "Moltiplicatore P", + "i_multiplier": "Moltiplicatore I", + "d_multiplier": "Moltiplicatore D", + "thresholdHigh": "Soglia superiore", + "thresholdLow": "Soglia inferiore" + } }, "ot": { diff --git a/src_data/locales/ru.json b/src_data/locales/ru.json index f4aa603..aed040a 100644 --- a/src_data/locales/ru.json +++ b/src_data/locales/ru.json @@ -347,7 +347,19 @@ "i": "Коэффициент I", "d": "Коэффициент D", "dt": "DT (сек)", - "noteMinMaxTemp": "Важно: При использовании «ПЗА» и «ПИД» одновременно, мин. и макс. температура ограничивает влияние на расчётную температуру «ПЗА».
Таким образом, если мин. температура задана как -15, а макс. как 15, то конечная температура теплоносителя будет от equitherm_result - 15 до equitherm_result + 15." + "limits": { + "title": "Лимиты", + "note": "Важно: При использовании «ПЗА» и «ПИД» одновременно, мин. и макс. температура ограничивает влияние на расчётную температуру «ПЗА».
Таким образом, если мин. температура задана как -15, а макс. как 15, то конечная температура теплоносителя будет от equitherm_result - 15 до equitherm_result + 15." + }, + "deadband": { + "title": "Мертвая зона (Deadband)", + "note": "Deadband - это мёртвая зона вокруг целевой температуры, в которой PID-регулирование становится менее активным. В этом диапазоне алгоритм может снижать интенсивность или полностью прекращать корректировку температуры, чтобы избежать излишней чувствительности к небольшим колебаниям.

Например, при целевой температуре 22°, нижнем пороге 1.0 и верхнем 0.5, deadband активен в диапазоне от 21° до 22.5°. Если коэфф. I=0.0005, а множитель I=0.05, то при включении мертвой зоны коэфф. I будет равен: 0.0005 * 0.05 = 0.000025", + "p_multiplier": "Множитель для коэф. P", + "i_multiplier": "Множитель для коэф. I", + "d_multiplier": "Множитель для коэф. D", + "thresholdHigh": "Верхний порог", + "thresholdLow": "Нижний порог" + } }, "ot": { diff --git a/src_data/pages/settings.html b/src_data/pages/settings.html index d84c327..3b1470d 100644 --- a/src_data/pages/settings.html +++ b/src_data/pages/settings.html @@ -313,19 +313,71 @@
-
- +
+ settings.pid.limits.title - -
+
+
+ + + +
- settings.pid.noteMinMaxTemp + settings.pid.limits.note +
+ + +
+ +
+ settings.pid.deadband.title + +
+
+ +
+ +
+ + + + + +
+ +
+ + + +
+ + settings.pid.deadband.note +
+
@@ -823,6 +875,12 @@ "min": (data.system.unitSystem == 0 ? 0 : 33), "max": (data.system.unitSystem == 0 ? 100 : 212) }); + setCheckboxValue("[name='pid[deadband][enabled]']", data.pid.deadband.enabled); + setInputValue("[name='pid[deadband][p_multiplier]']", data.pid.deadband.p_multiplier); + setInputValue("[name='pid[deadband][i_multiplier]']", data.pid.deadband.i_multiplier); + setInputValue("[name='pid[deadband][d_multiplier]']", data.pid.deadband.d_multiplier); + setInputValue("[name='pid[deadband][thresholdHigh]']", data.pid.deadband.thresholdHigh); + setInputValue("[name='pid[deadband][thresholdLow]']", data.pid.deadband.thresholdLow); setBusy('#pid-settings-busy', '#pid-settings', false); };