mirror of
https://github.com/Laxilef/OTGateway.git
synced 2025-12-26 01:53:35 +05:00
Compare commits
20 Commits
passive_bl
...
7e6b2ee6f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e6b2ee6f7 | ||
|
|
63d3b0ceed | ||
|
|
291b4cbd65 | ||
|
|
c9119c042f | ||
|
|
1bd9e22602 | ||
|
|
82d8b7ed8d | ||
|
|
8b88d133d4 | ||
|
|
bae540d67a | ||
|
|
68514d3c9f | ||
|
|
c3b127c868 | ||
|
|
b7334b5f3e | ||
|
|
7c2059d7c6 | ||
|
|
66ced5f8d0 | ||
|
|
ba255c1bd1 | ||
|
|
54095892e1 | ||
|
|
5dd76c9168 | ||
|
|
1f72286be7 | ||
|
|
40767fbb99 | ||
|
|
cb34e073a7 | ||
|
|
8ba73093af |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
.pio
|
.pio
|
||||||
.vscode
|
.vscode
|
||||||
build/*.bin
|
build/*.bin
|
||||||
|
build/*.elf
|
||||||
data/*
|
data/*
|
||||||
secrets.ini
|
secrets.ini
|
||||||
node_modules
|
node_modules
|
||||||
|
|||||||
@@ -16,14 +16,16 @@ public:
|
|||||||
float Kn = 0.0;
|
float Kn = 0.0;
|
||||||
float Kk = 0.0;
|
float Kk = 0.0;
|
||||||
float Kt = 0.0;
|
float Kt = 0.0;
|
||||||
|
float Ke = 1.3;
|
||||||
|
|
||||||
Equitherm() = default;
|
Equitherm() = default;
|
||||||
|
|
||||||
// kn, kk, kt
|
// kn, kk, kt, Ke
|
||||||
Equitherm(float new_kn, float new_kk, float new_kt) {
|
Equitherm(float new_kn, float new_kk, float new_kt, float new_ke) {
|
||||||
Kn = new_kn;
|
Kn = new_kn;
|
||||||
Kk = new_kk;
|
Kk = new_kk;
|
||||||
Kt = new_kt;
|
Kt = new_kt;
|
||||||
|
Ke = new_ke;
|
||||||
}
|
}
|
||||||
|
|
||||||
// лимит выходной величины
|
// лимит выходной величины
|
||||||
@@ -34,7 +36,7 @@ public:
|
|||||||
|
|
||||||
// возвращает новое значение при вызове
|
// возвращает новое значение при вызове
|
||||||
datatype getResult() {
|
datatype getResult() {
|
||||||
datatype output = getResultN() + getResultK() + getResultT();
|
datatype output = getResultN() + Kk + getResultT();
|
||||||
output = constrain(output, _minOut, _maxOut); // ограничиваем выход
|
output = constrain(output, _minOut, _maxOut); // ограничиваем выход
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
@@ -42,21 +44,16 @@ public:
|
|||||||
private:
|
private:
|
||||||
unsigned short _minOut = 20, _maxOut = 90;
|
unsigned short _minOut = 20, _maxOut = 90;
|
||||||
|
|
||||||
// температура контура отопления в зависимости от наружной температуры
|
datatype getResultN()
|
||||||
datatype getResultN() {
|
{
|
||||||
float a = (-0.21 * Kn) - 0.06; // a = -0,21k — 0,06
|
float tempDelta = targetTemp - outdoorTemp,
|
||||||
float b = (6.04 * Kn) + 1.98; // b = 6,04k + 1,98
|
maxPoint = targetTemp - (_maxOut - targetTemp) / Kn,
|
||||||
float c = (-5.06 * Kn) + 18.06; // с = -5,06k + 18,06
|
sf = (_maxOut - targetTemp) / pow(targetTemp - maxPoint, 1.0 / Ke),
|
||||||
float x = (-0.2 * outdoorTemp) + 5; // x = -0.2*t1 + 5
|
T_rad = targetTemp + sf * (tempDelta >= 0 ? pow(tempDelta, 1.0 / Ke) : -pow(-tempDelta, 1.0 / Ke));
|
||||||
return (a * x * x) + (b * x) + c; // Tn = ax2 + bx + c
|
return T_rad > _maxOut ? _maxOut : T_rad;
|
||||||
}
|
}
|
||||||
|
|
||||||
// поправка на желаемую комнатную температуру
|
// Реакция на разницу с целевой температурой
|
||||||
datatype getResultK() {
|
|
||||||
return (targetTemp - 20) * Kk;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Расчет поправки (ошибки) термостата
|
|
||||||
datatype getResultT() {
|
datatype getResultT() {
|
||||||
return constrain((targetTemp - indoorTemp), -3, 3) * Kt;
|
return constrain((targetTemp - indoorTemp), -3, 3) * Kt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -876,8 +876,8 @@ public:
|
|||||||
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.k_factor|float(0)|round(2) }}");
|
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.k_factor|float(0)|round(2) }}");
|
||||||
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
|
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
|
||||||
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"k_factor\" : {{ value }}}}");
|
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"k_factor\" : {{ value }}}}");
|
||||||
doc[FPSTR(HA_MIN)] = 0;
|
doc[FPSTR(HA_MIN)] = -15;
|
||||||
doc[FPSTR(HA_MAX)] = 10;
|
doc[FPSTR(HA_MAX)] = 15;
|
||||||
doc[FPSTR(HA_STEP)] = 0.01f;
|
doc[FPSTR(HA_STEP)] = 0.01f;
|
||||||
doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX);
|
doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX);
|
||||||
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
|
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
|
||||||
@@ -886,6 +886,29 @@ public:
|
|||||||
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_k_factor")).c_str(), doc);
|
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_k_factor")).c_str(), doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool publishInputEquithermFactorE(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("equitherm_e"));
|
||||||
|
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("Equitherm Exponent E");
|
||||||
|
doc[FPSTR(HA_ICON)] = F("mdi:alpha-e-circle-outline");
|
||||||
|
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str();
|
||||||
|
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.e_factor|float(0)|round(2) }}");
|
||||||
|
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
|
||||||
|
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"e_factor\" : {{ value }}}}");
|
||||||
|
doc[FPSTR(HA_MIN)] = 1;
|
||||||
|
doc[FPSTR(HA_MAX)] = 2;
|
||||||
|
doc[FPSTR(HA_STEP)] = 0.01f;
|
||||||
|
doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX);
|
||||||
|
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
|
||||||
|
doc.shrinkToFit();
|
||||||
|
|
||||||
|
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_e_factor")).c_str(), doc);
|
||||||
|
}
|
||||||
|
|
||||||
bool publishInputEquithermFactorT(bool enabledByDefault = true) {
|
bool publishInputEquithermFactorT(bool enabledByDefault = true) {
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
|
doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ protected:
|
|||||||
etRegulator.setLimits(minTemp, maxTemp);
|
etRegulator.setLimits(minTemp, maxTemp);
|
||||||
etRegulator.Kn = settings.equitherm.n_factor;
|
etRegulator.Kn = settings.equitherm.n_factor;
|
||||||
etRegulator.Kk = settings.equitherm.k_factor;
|
etRegulator.Kk = settings.equitherm.k_factor;
|
||||||
|
etRegulator.Ke = settings.equitherm.e_factor;
|
||||||
etRegulator.targetTemp = targetTemp;
|
etRegulator.targetTemp = targetTemp;
|
||||||
etRegulator.outdoorTemp = outdoorTemp;
|
etRegulator.outdoorTemp = outdoorTemp;
|
||||||
float etResult = etRegulator.getResult();
|
float etResult = etRegulator.getResult();
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ struct Settings {
|
|||||||
float n_factor = 0.7f;
|
float n_factor = 0.7f;
|
||||||
float k_factor = 3.0f;
|
float k_factor = 3.0f;
|
||||||
float t_factor = 2.0f;
|
float t_factor = 2.0f;
|
||||||
|
float e_factor = 1.3f;
|
||||||
} equitherm;
|
} equitherm;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const char S_DNS[] PROGMEM = "dns";
|
|||||||
const char S_DT[] PROGMEM = "dt";
|
const char S_DT[] PROGMEM = "dt";
|
||||||
const char S_D_FACTOR[] PROGMEM = "d_factor";
|
const char S_D_FACTOR[] PROGMEM = "d_factor";
|
||||||
const char S_D_MULTIPLIER[] PROGMEM = "d_multiplier";
|
const char S_D_MULTIPLIER[] PROGMEM = "d_multiplier";
|
||||||
|
const char S_E_FACTOR[] PROGMEM = "e_factor";
|
||||||
const char S_EMERGENCY[] PROGMEM = "emergency";
|
const char S_EMERGENCY[] PROGMEM = "emergency";
|
||||||
const char S_ENABLED[] PROGMEM = "enabled";
|
const char S_ENABLED[] PROGMEM = "enabled";
|
||||||
const char S_ENV[] PROGMEM = "env";
|
const char S_ENV[] PROGMEM = "env";
|
||||||
|
|||||||
@@ -502,6 +502,7 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
|
|||||||
equitherm[FPSTR(S_ENABLED)] = src.equitherm.enabled;
|
equitherm[FPSTR(S_ENABLED)] = src.equitherm.enabled;
|
||||||
equitherm[FPSTR(S_N_FACTOR)] = roundf(src.equitherm.n_factor, 3);
|
equitherm[FPSTR(S_N_FACTOR)] = roundf(src.equitherm.n_factor, 3);
|
||||||
equitherm[FPSTR(S_K_FACTOR)] = roundf(src.equitherm.k_factor, 3);
|
equitherm[FPSTR(S_K_FACTOR)] = roundf(src.equitherm.k_factor, 3);
|
||||||
|
equitherm[FPSTR(S_E_FACTOR)] = roundf(src.equitherm.e_factor, 3);
|
||||||
equitherm[FPSTR(S_T_FACTOR)] = roundf(src.equitherm.t_factor, 3);
|
equitherm[FPSTR(S_T_FACTOR)] = roundf(src.equitherm.t_factor, 3);
|
||||||
|
|
||||||
auto pid = dst[FPSTR(S_PID)].to<JsonObject>();
|
auto pid = dst[FPSTR(S_PID)].to<JsonObject>();
|
||||||
@@ -1100,6 +1101,14 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_E_FACTOR)].isNull()) {
|
||||||
|
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_E_FACTOR)].as<float>();
|
||||||
|
|
||||||
|
if (value >= 1 && value <= 2 && fabsf(value - dst.equitherm.e_factor) > 0.0001f) {
|
||||||
|
dst.equitherm.e_factor = roundf(value, 3);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_T_FACTOR)].isNull()) {
|
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_T_FACTOR)].isNull()) {
|
||||||
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_T_FACTOR)].as<float>();
|
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_T_FACTOR)].as<float>();
|
||||||
|
|
||||||
|
|||||||
@@ -342,9 +342,21 @@
|
|||||||
"equitherm": {
|
"equitherm": {
|
||||||
"n": "N factor",
|
"n": "N factor",
|
||||||
"k": "K factor",
|
"k": "K factor",
|
||||||
|
"e": "Exponent E",
|
||||||
"t": {
|
"t": {
|
||||||
"title": "T factor",
|
"title": "T factor",
|
||||||
"note": "Not used if PID is enabled"
|
"note": "Not used if PID is enabled"
|
||||||
|
},
|
||||||
|
"calibration": "2 points Calibration",
|
||||||
|
"obs1_outdoor": "Warm Point Outdoor",
|
||||||
|
"obs1_radiator": "Warm Point Radiator",
|
||||||
|
"obs2_outdoor": "Cold Point Outdoor",
|
||||||
|
"obs2_radiator": "Cold Point Radiator",
|
||||||
|
"calibrate": "Calibrate",
|
||||||
|
|
||||||
|
"chart": {
|
||||||
|
"radiatorTemp": "Radiator Temperature (°C)",
|
||||||
|
"outdoorTemp": "Outdoor Temperature (°C)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -342,9 +342,20 @@
|
|||||||
"equitherm": {
|
"equitherm": {
|
||||||
"n": "Fattore N",
|
"n": "Fattore N",
|
||||||
"k": "Fattore K",
|
"k": "Fattore K",
|
||||||
|
"e": "Esponente E",
|
||||||
"t": {
|
"t": {
|
||||||
"title": "Fattore T",
|
"title": "Fattore T",
|
||||||
"note": "Non usato se PID è attivato"
|
"note": "Non usato se PID è attivato"
|
||||||
|
},
|
||||||
|
"calibration": "Calibrazione a 2 punti",
|
||||||
|
"obs1_outdoor": "Punto caldo esterno",
|
||||||
|
"obs1_radiator": "Punto caldo (radiatore)",
|
||||||
|
"obs2_outdoor": "Punto freddo esterno",
|
||||||
|
"obs2_radiator": "Punto freddo (radiatore)",
|
||||||
|
"calibrate": "Calibra",
|
||||||
|
"chart": {
|
||||||
|
"radiatorTemp": "Temperatura Del Radiatore (°C)",
|
||||||
|
"outdoorTemp": "Outdoor Temperature (°C)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -342,9 +342,20 @@
|
|||||||
"equitherm": {
|
"equitherm": {
|
||||||
"n": "Коэффициент N",
|
"n": "Коэффициент N",
|
||||||
"k": "Коэффициент K",
|
"k": "Коэффициент K",
|
||||||
|
"e": "Экспонента E",
|
||||||
"t": {
|
"t": {
|
||||||
"title": "Коэффициент T",
|
"title": "Коэффициент T",
|
||||||
"note": "Не используется, если ПИД включен"
|
"note": "Не используется, если ПИД включен"
|
||||||
|
},
|
||||||
|
"calibration": "Калибровка по двум точкам",
|
||||||
|
"obs1_outdoor": "Теплая точка (снаружи)",
|
||||||
|
"obs1_radiator": "Теплая точка (радиатор)",
|
||||||
|
"obs2_outdoor": "Холодная точка (снаружи)",
|
||||||
|
"obs2_radiator": "Холодная точка (радиатор)",
|
||||||
|
"calibrate": "Калибровать",
|
||||||
|
"chart": {
|
||||||
|
"radiatorTemp": "Температура радиатора (°C)",
|
||||||
|
"outdoorTemp": "Наружная температура (°C)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -264,6 +264,7 @@
|
|||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b data-i18n>settings.section.equitherm</b></summary>
|
<summary><b data-i18n>settings.section.equitherm</b></summary>
|
||||||
|
<canvas id="equithermChart" width="400" height="200"></canvas>
|
||||||
<div>
|
<div>
|
||||||
<div id="equitherm-settings-busy" aria-busy="true"></div>
|
<div id="equitherm-settings-busy" aria-busy="true"></div>
|
||||||
<form action="/api/settings" id="equitherm-settings" class="hidden">
|
<form action="/api/settings" id="equitherm-settings" class="hidden">
|
||||||
@@ -277,12 +278,18 @@
|
|||||||
<div class="grid">
|
<div class="grid">
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n>settings.equitherm.n</span>
|
<span data-i18n>settings.equitherm.n</span>
|
||||||
<input type="number" inputmode="decimal" name="equitherm[n_factor]" min="0.001" max="10" step="0.001" required>
|
<input type="number" inputmode="decimal" name="equitherm[n_factor]" min="0.001" max="10" step="0.001"
|
||||||
|
required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n>settings.equitherm.k</span>
|
<span data-i18n>settings.equitherm.k</span>
|
||||||
<input type="number" inputmode="decimal" name="equitherm[k_factor]" min="0" max="10" step="0.01" required>
|
<input type="number" inputmode="decimal" name="equitherm[k_factor]" min="-15" max="5" step="0.01" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span data-i18n>settings.equitherm.e</span>
|
||||||
|
<input type="number" inputmode="decimal" name="equitherm[e_factor]" min="1" max="2" step="0.01" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
@@ -291,7 +298,31 @@
|
|||||||
<small data-i18n>settings.equitherm.t.note</small>
|
<small data-i18n>settings.equitherm.t.note</small>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<legend><b data-i18n>settings.equitherm.calibration</b></legend>
|
||||||
|
<div class="grid">
|
||||||
|
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span data-i18n>settings.equitherm.obs1_outdoor</span>
|
||||||
|
<input type="number" inputmode="decimal" name="equitherm[obs1_outdoor]" min="-50" max="50" step="0.1">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span data-i18n>settings.equitherm.obs1_radiator</span>
|
||||||
|
<input type="number" inputmode="decimal" name="equitherm[obs1_radiator]" min="10" max="90" step="0.1">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span data-i18n>settings.equitherm.obs2_outdoor</span>
|
||||||
|
<input type="number" inputmode="decimal" name="equitherm[obs2_outdoor]" min="-50" max="50" step="0.1">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span data-i18n>settings.equitherm.obs2_radiator</span>
|
||||||
|
<input type="number" inputmode="decimal" name="equitherm[obs2_radiator]" min="10" max="90" step="0.1">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="calibrateEquitherm" data-i18n>settings.equitherm.calibrate</button>
|
||||||
<button type="submit" data-i18n>button.save</button>
|
<button type="submit" data-i18n>button.save</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,6 +330,8 @@
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b data-i18n>settings.section.pid</b></summary>
|
<summary><b data-i18n>settings.section.pid</b></summary>
|
||||||
<div>
|
<div>
|
||||||
@@ -756,6 +789,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/static/app.js?{BUILD_TIME}"></script>
|
<script src="/static/app.js?{BUILD_TIME}"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const lang = new Lang(document.getElementById('lang'));
|
const lang = new Lang(document.getElementById('lang'));
|
||||||
@@ -885,6 +919,7 @@
|
|||||||
setCheckboxValue("[name='equitherm[enabled]']", data.equitherm.enabled);
|
setCheckboxValue("[name='equitherm[enabled]']", data.equitherm.enabled);
|
||||||
setInputValue("[name='equitherm[n_factor]']", data.equitherm.n_factor);
|
setInputValue("[name='equitherm[n_factor]']", data.equitherm.n_factor);
|
||||||
setInputValue("[name='equitherm[k_factor]']", data.equitherm.k_factor);
|
setInputValue("[name='equitherm[k_factor]']", data.equitherm.k_factor);
|
||||||
|
setInputValue("[name='equitherm[e_factor]']", data.equitherm.e_factor);
|
||||||
setInputValue("[name='equitherm[t_factor]']", data.equitherm.t_factor);
|
setInputValue("[name='equitherm[t_factor]']", data.equitherm.t_factor);
|
||||||
setBusy('#equitherm-settings-busy', '#equitherm-settings', false);
|
setBusy('#equitherm-settings-busy', '#equitherm-settings', false);
|
||||||
|
|
||||||
@@ -909,6 +944,9 @@
|
|||||||
setInputValue("[name='pid[deadband][thresholdHigh]']", data.pid.deadband.thresholdHigh);
|
setInputValue("[name='pid[deadband][thresholdHigh]']", data.pid.deadband.thresholdHigh);
|
||||||
setInputValue("[name='pid[deadband][thresholdLow]']", data.pid.deadband.thresholdLow);
|
setInputValue("[name='pid[deadband][thresholdLow]']", data.pid.deadband.thresholdLow);
|
||||||
setBusy('#pid-settings-busy', '#pid-settings', false);
|
setBusy('#pid-settings-busy', '#pid-settings', false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -961,6 +999,258 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//График
|
||||||
|
let equithermChart;
|
||||||
|
|
||||||
|
async function fetchSettings() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/settings", {
|
||||||
|
cache: "no-cache",
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Response not valid");
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateTRad(targetTemp, outdoorTemp, maxOut, Kn, Ke, Kk) {
|
||||||
|
let tempDelta = targetTemp - outdoorTemp;
|
||||||
|
const maxPoint = targetTemp - (maxOut - targetTemp) / Kn;
|
||||||
|
let base = targetTemp - maxPoint;
|
||||||
|
if (base <= 0) {
|
||||||
|
base = 0.0001;
|
||||||
|
}
|
||||||
|
const sf = (maxOut - targetTemp) / Math.pow(base, 1.0 / Ke);
|
||||||
|
let T_rad =
|
||||||
|
targetTemp +
|
||||||
|
sf * (tempDelta >= 0 ? Math.pow(tempDelta, 1.0 / Ke) : -Math.pow(-tempDelta, 1.0 / Ke)) +
|
||||||
|
Kk;
|
||||||
|
return Math.min(T_rad, maxOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateChartData(targetTemp, maxOut, Kn, Ke, Kk) {
|
||||||
|
const outdoorTemps = [];
|
||||||
|
const predictedTRad = [];
|
||||||
|
for (let temp = 25; temp >= -30; temp -= 1) {
|
||||||
|
outdoorTemps.push(temp);
|
||||||
|
predictedTRad.push(calculateTRad(targetTemp, temp, maxOut, Kn, Ke, Kk).toFixed(1));
|
||||||
|
}
|
||||||
|
return { outdoorTemps, predictedTRad };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChart(outdoorTemps, predictedTRad, obsPoints = []) {
|
||||||
|
const ctx = document.getElementById("equithermChart").getContext("2d");
|
||||||
|
const canvasHeight = ctx.canvas.height;
|
||||||
|
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, 0);
|
||||||
|
gradient.addColorStop(0, "rgba(75, 192, 192, 1)");
|
||||||
|
gradient.addColorStop(0.5, "rgba(255, 99, 132, 1)");
|
||||||
|
equithermChart = new Chart(ctx, {
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: outdoorTemps,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Температура Радиатора (°C)",
|
||||||
|
borderColor: gradient,
|
||||||
|
borderWidth: 1,
|
||||||
|
fill: false,
|
||||||
|
tension: 0.1,
|
||||||
|
pointRadius: 2,
|
||||||
|
pointHoverRadius: 4,
|
||||||
|
data: predictedTRad
|
||||||
|
},
|
||||||
|
// Точки калибровки
|
||||||
|
{
|
||||||
|
label: "Наблюдаемые точки",
|
||||||
|
type: "scatter",
|
||||||
|
data: obsPoints,
|
||||||
|
backgroundColor: "gold",
|
||||||
|
borderColor: "gold",
|
||||||
|
pointRadius: 6,
|
||||||
|
pointHoverRadius: 8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
interaction: {
|
||||||
|
mode: "nearest",
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
position: "nearest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Наружная температура (°C)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Температура Радиатора (°C)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initChart() {
|
||||||
|
try {
|
||||||
|
const result = await fetchSettings();
|
||||||
|
const { heating, equitherm } = result;
|
||||||
|
const targetTemp = heating?.target ?? 24;
|
||||||
|
const maxOut = heating?.maxTemp ?? 90;
|
||||||
|
const Kn = equitherm?.n_factor ?? 1;
|
||||||
|
const Ke = equitherm?.e_factor ?? 1.3;
|
||||||
|
const Kk = equitherm?.k_factor ?? 0;
|
||||||
|
const { outdoorTemps, predictedTRad } = generateChartData(targetTemp, maxOut, Kn, Ke, Kk);
|
||||||
|
createChart(outdoorTemps, predictedTRad);
|
||||||
|
document.getElementById("equitherm-settings-busy").classList.add("hidden");
|
||||||
|
document.getElementById("equitherm-settings").classList.remove("hidden");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChart(formData) {
|
||||||
|
if (!equithermChart) return;
|
||||||
|
fetchSettings()
|
||||||
|
.then(result => {
|
||||||
|
const targetTemp = result?.heating?.target ?? 24;
|
||||||
|
const maxOut = result?.heating?.maxTemp ?? 90;
|
||||||
|
const Kn = parseFloat(formData.get("equitherm[n_factor]")) || 1;
|
||||||
|
const Ke = parseFloat(formData.get("equitherm[e_factor]")) || 1.3;
|
||||||
|
const Kk = parseFloat(formData.get("equitherm[k_factor]")) || 0;
|
||||||
|
const { outdoorTemps, predictedTRad } = generateChartData(targetTemp, maxOut, Kn, Ke, Kk);
|
||||||
|
// Точки калибровки
|
||||||
|
let obsPoints = [];
|
||||||
|
const obs1Out = parseFloat(formData.get("equitherm[obs1_outdoor]"));
|
||||||
|
const obs1Rad = parseFloat(formData.get("equitherm[obs1_radiator]"));
|
||||||
|
const obs2Out = parseFloat(formData.get("equitherm[obs2_outdoor]"));
|
||||||
|
const obs2Rad = parseFloat(formData.get("equitherm[obs2_radiator]"));
|
||||||
|
if (!isNaN(obs1Out) && !isNaN(obs1Rad) && !isNaN(obs2Out) && !isNaN(obs2Rad)) {
|
||||||
|
obsPoints.push({ x: obs1Out, y: obs1Rad });
|
||||||
|
obsPoints.push({ x: obs2Out, y: obs2Rad });
|
||||||
|
}
|
||||||
|
equithermChart.data.labels = outdoorTemps;
|
||||||
|
equithermChart.data.datasets[0].data = predictedTRad;
|
||||||
|
if (equithermChart.data.datasets.length > 1) {
|
||||||
|
equithermChart.data.datasets[1].data = obsPoints;
|
||||||
|
}
|
||||||
|
equithermChart.update();
|
||||||
|
})
|
||||||
|
.catch(error => console.log(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Калибровка
|
||||||
|
async function fetchAndCalibrate() {
|
||||||
|
try {
|
||||||
|
const result = await fetchSettings();
|
||||||
|
const { heating, equitherm } = result;
|
||||||
|
const targetTemp = heating?.target ?? 24;
|
||||||
|
const maxOut = heating?.maxTemp ?? 90;
|
||||||
|
const Ke = equitherm?.e_factor ?? 1.3;
|
||||||
|
|
||||||
|
const form = document.getElementById("equitherm-settings");
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
const obs1Out = parseFloat(formData.get("equitherm[obs1_outdoor]"));
|
||||||
|
const obs1Rad = parseFloat(formData.get("equitherm[obs1_radiator]"));
|
||||||
|
const obs2Out = parseFloat(formData.get("equitherm[obs2_outdoor]"));
|
||||||
|
const obs2Rad = parseFloat(formData.get("equitherm[obs2_radiator]"));
|
||||||
|
|
||||||
|
if (isNaN(obs1Out) || isNaN(obs1Rad) || isNaN(obs2Out) || isNaN(obs2Rad)) {
|
||||||
|
alert("Please enter valid observation points.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем чтобы наружн. меньше целевой
|
||||||
|
const diff1 = targetTemp - obs1Out;
|
||||||
|
const diff2 = targetTemp - obs2Out;
|
||||||
|
if (diff1 <= 0 || diff2 <= 0) {
|
||||||
|
alert("Outdoor temperature must be below the target temperature.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A
|
||||||
|
const denominator = Math.pow(diff2, 1 / Ke) - Math.pow(diff1, 1 / Ke);
|
||||||
|
if (denominator === 0) {
|
||||||
|
alert("Calibration error: Denominator is zero.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const A = (obs2Rad - obs1Rad) / denominator;
|
||||||
|
|
||||||
|
// Считаем N из A
|
||||||
|
const N = Math.pow(A / Math.pow(maxOut - targetTemp, 1 - 1 / Ke), Ke);
|
||||||
|
|
||||||
|
// Считаем K по первой точке
|
||||||
|
const K = obs1Rad - targetTemp - A * Math.pow(diff1, 1 / Ke);
|
||||||
|
|
||||||
|
// Обновляем поля
|
||||||
|
const nField = document.querySelector("input[name='equitherm[n_factor]']");
|
||||||
|
const kField = document.querySelector("input[name='equitherm[k_factor]']");
|
||||||
|
const eField = document.querySelector("input[name='equitherm[e_factor]']");
|
||||||
|
|
||||||
|
nField.value = N.toFixed(3);
|
||||||
|
kField.value = K.toFixed(2);
|
||||||
|
|
||||||
|
// Триггерим Change
|
||||||
|
nField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
kField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
eField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
|
||||||
|
// Обновляем график
|
||||||
|
updateChart(new FormData(form));
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error during calibration:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Кнопка
|
||||||
|
const calibrateButton = document.getElementById("calibrateEquitherm");
|
||||||
|
calibrateButton.addEventListener("click", fetchAndCalibrate);
|
||||||
|
|
||||||
|
// Слушаем отправку
|
||||||
|
const form = document.getElementById('equitherm-settings');
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
updateChart(formData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Слушаем кнопку сохранить
|
||||||
|
const equithermSection = document.querySelector('details');
|
||||||
|
const saveButton = equithermSection.querySelector('button[data-i18n="button.save"]');
|
||||||
|
saveButton.addEventListener('click', () => {
|
||||||
|
const form = document.getElementById('equitherm-settings');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
updateChart(formData);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Следим за изменениями в полях
|
||||||
|
document.querySelectorAll('#equitherm-settings input').forEach(input => {
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
const form = document.getElementById('equitherm-settings');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
updateChart(formData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// инициализируем график
|
||||||
|
initChart();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user