mirror of
https://github.com/Laxilef/OTGateway.git
synced 2025-12-25 17:43:35 +05:00
Compare commits
5 Commits
a2ffdb93a5
...
1.5.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd53d1ef3e | ||
|
|
3bd8010b74 | ||
|
|
6a26e27d39 | ||
|
|
8fa440810c | ||
|
|
95b18385ba |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,8 +1,13 @@
|
||||
.pio
|
||||
.vscode
|
||||
build/*.bin
|
||||
build/*
|
||||
data/*
|
||||
managed_components/*
|
||||
node_modules/*
|
||||
secrets.ini
|
||||
node_modules
|
||||
package-lock.json
|
||||
*.lock
|
||||
sdkconfig.*
|
||||
CMakeLists.txt
|
||||
!sdkconfig.defaults
|
||||
!.gitkeep
|
||||
0
build/.gitkeep
Normal file
0
build/.gitkeep
Normal file
Binary file not shown.
Binary file not shown.
0
data/static/.gitkeep
Normal file
0
data/static/.gitkeep
Normal file
@@ -16,16 +16,14 @@ public:
|
||||
float Kn = 0.0;
|
||||
float Kk = 0.0;
|
||||
float Kt = 0.0;
|
||||
float Ke = 1.3;
|
||||
|
||||
Equitherm() = default;
|
||||
|
||||
// kn, kk, kt, Ke
|
||||
Equitherm(float new_kn, float new_kk, float new_kt, float new_ke) {
|
||||
// kn, kk, kt
|
||||
Equitherm(float new_kn, float new_kk, float new_kt) {
|
||||
Kn = new_kn;
|
||||
Kk = new_kk;
|
||||
Kt = new_kt;
|
||||
Ke = new_ke;
|
||||
}
|
||||
|
||||
// лимит выходной величины
|
||||
@@ -36,7 +34,7 @@ public:
|
||||
|
||||
// возвращает новое значение при вызове
|
||||
datatype getResult() {
|
||||
datatype output = getResultN() + Kk + getResultT();
|
||||
datatype output = getResultN() + getResultK() + getResultT();
|
||||
output = constrain(output, _minOut, _maxOut); // ограничиваем выход
|
||||
return output;
|
||||
}
|
||||
@@ -44,17 +42,22 @@ public:
|
||||
private:
|
||||
unsigned short _minOut = 20, _maxOut = 90;
|
||||
|
||||
datatype getResultN()
|
||||
{
|
||||
float tempDelta = targetTemp - outdoorTemp,
|
||||
maxPoint = targetTemp - (_maxOut - targetTemp) / Kn,
|
||||
sf = (_maxOut - targetTemp) / pow(targetTemp - maxPoint, 1.0 / Ke),
|
||||
T_rad = targetTemp + sf * (tempDelta >= 0 ? pow(tempDelta, 1.0 / Ke) : -pow(-tempDelta, 1.0 / Ke));
|
||||
return T_rad > _maxOut ? _maxOut : T_rad;
|
||||
// температура контура отопления в зависимости от наружной температуры
|
||||
datatype getResultN() {
|
||||
float a = (-0.21 * Kn) - 0.06; // a = -0,21k — 0,06
|
||||
float b = (6.04 * Kn) + 1.98; // b = 6,04k + 1,98
|
||||
float c = (-5.06 * Kn) + 18.06; // с = -5,06k + 18,06
|
||||
float x = (-0.2 * outdoorTemp) + 5; // x = -0.2*t1 + 5
|
||||
return (a * x * x) + (b * x) + c; // Tn = ax2 + bx + c
|
||||
}
|
||||
|
||||
// Реакция на разницу с целевой температурой
|
||||
// поправка на желаемую комнатную температуру
|
||||
datatype getResultK() {
|
||||
return (targetTemp - 20) * Kk;
|
||||
}
|
||||
|
||||
// Расчет поправки (ошибки) термостата
|
||||
datatype getResultT() {
|
||||
return constrain((targetTemp - indoorTemp), -3, 3) * Kt;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -14,7 +14,7 @@ extra_configs = secrets.default.ini
|
||||
core_dir = .pio
|
||||
|
||||
[env]
|
||||
version = 1.5.3
|
||||
version = 1.5.4
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
bblanchon/ArduinoJson@^7.3.0
|
||||
@@ -84,7 +84,7 @@ board_build.ldscript = eagle.flash.4m1m.ld
|
||||
;platform_packages =
|
||||
; framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.5
|
||||
; framework-arduinoespressif32-libs @ https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.1/esp32-arduino-libs-idf-release_v5.1-33fbade6.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip
|
||||
platform_packages =
|
||||
board_build.partitions = esp32_partitions.csv
|
||||
lib_deps =
|
||||
@@ -164,10 +164,10 @@ board_build.ldscript = ${esp8266_defaults.board_build.ldscript}
|
||||
build_type = ${esp8266_defaults.build_type}
|
||||
build_flags =
|
||||
${esp8266_defaults.build_flags}
|
||||
-D DEFAULT_OT_IN_GPIO=4
|
||||
-D DEFAULT_OT_OUT_GPIO=5
|
||||
-D DEFAULT_OT_IN_GPIO=13
|
||||
-D DEFAULT_OT_OUT_GPIO=15
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=12
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=14
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=4
|
||||
-D DEFAULT_STATUS_LED_GPIO=2
|
||||
-D DEFAULT_OT_RX_LED_GPIO=16
|
||||
|
||||
@@ -287,21 +287,26 @@ build_flags =
|
||||
|
||||
[env:esp32_c6]
|
||||
platform = ${esp32_defaults.platform}
|
||||
framework = arduino, espidf
|
||||
platform_packages = ${esp32_defaults.platform_packages}
|
||||
board = esp32-c6-devkitm-1
|
||||
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||
lib_deps =
|
||||
${esp32_defaults.lib_deps}
|
||||
;${esp32_defaults.nimble_lib}
|
||||
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||
lib_deps = ${esp32_defaults.lib_deps}
|
||||
lib_ignore =
|
||||
${esp32_defaults.lib_ignore}
|
||||
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||
build_unflags =
|
||||
-mtext-section-literals
|
||||
build_type = ${esp32_defaults.build_type}
|
||||
build_flags =
|
||||
${esp32_defaults.build_flags}
|
||||
; Currently the NimBLE library is incompatible with ESP32 C6
|
||||
;-D USE_BLE=1
|
||||
-D USE_BLE=1
|
||||
-D DEFAULT_OT_IN_GPIO=15
|
||||
-D DEFAULT_OT_OUT_GPIO=23
|
||||
-D DEFAULT_SENSOR_OUTDOOR_GPIO=0
|
||||
-D DEFAULT_SENSOR_INDOOR_GPIO=0
|
||||
-D DEFAULT_STATUS_LED_GPIO=11
|
||||
-D DEFAULT_OT_RX_LED_GPIO=10
|
||||
|
||||
[env:otthing]
|
||||
platform = ${esp32_defaults.platform}
|
||||
|
||||
33
sdkconfig.defaults
Normal file
33
sdkconfig.defaults
Normal file
@@ -0,0 +1,33 @@
|
||||
# Source:
|
||||
# https://github.com/pioarduino/platform-espressif32/tree/main/examples/espidf-arduino-h2zero-BLE_scan
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
CONFIG_MBEDTLS_PSK_MODES=y
|
||||
CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y
|
||||
CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y
|
||||
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y
|
||||
|
||||
#
|
||||
# BT config
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
|
||||
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n
|
||||
CONFIG_BTDM_CTRL_MODE_BTDM=n
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
|
||||
#
|
||||
# Arduino Configuration
|
||||
CONFIG_AUTOSTART_ARDUINO=y
|
||||
CONFIG_ARDUINO_SELECTIVE_COMPILATION=y
|
||||
CONFIG_ARDUINO_SELECTIVE_Zigbee=n
|
||||
CONFIG_ARDUINO_SELECTIVE_Matter=n
|
||||
CONFIG_ARDUINO_SELECTIVE_WiFiProv=n
|
||||
CONFIG_ARDUINO_SELECTIVE_BLE=n
|
||||
CONFIG_ARDUINO_SELECTIVE_BluetoothSerial=n
|
||||
CONFIG_ARDUINO_SELECTIVE_SimpleBLE=n
|
||||
CONFIG_ARDUINO_SELECTIVE_RainMaker=n
|
||||
CONFIG_ARDUINO_SELECTIVE_OpenThread=n
|
||||
CONFIG_ARDUINO_SELECTIVE_Insights=n
|
||||
@@ -14,12 +14,12 @@ ap_password = otgateway123456
|
||||
sta_ssid =
|
||||
sta_password =
|
||||
|
||||
portal_login =
|
||||
portal_password =
|
||||
portal_login = admin
|
||||
portal_password = admin
|
||||
|
||||
mqtt_enabled = false
|
||||
mqtt_server =
|
||||
mqtt_port = 1883
|
||||
mqtt_user =
|
||||
mqtt_password =
|
||||
mqtt_prefix = otgateway
|
||||
mqtt_prefix = opentherm
|
||||
@@ -886,29 +886,6 @@ public:
|
||||
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) {
|
||||
JsonDocument doc;
|
||||
doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
|
||||
@@ -1238,7 +1215,7 @@ public:
|
||||
|
||||
doc[FPSTR(HA_MIN_TEMP)] = minTemp;
|
||||
doc[FPSTR(HA_MAX_TEMP)] = maxTemp;
|
||||
doc[FPSTR(HA_TEMP_STEP)] = 0.5f;
|
||||
doc[FPSTR(HA_TEMP_STEP)] = 0.1f;
|
||||
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
|
||||
doc.shrinkToFit();
|
||||
|
||||
|
||||
@@ -172,7 +172,6 @@ protected:
|
||||
etRegulator.setLimits(minTemp, maxTemp);
|
||||
etRegulator.Kn = settings.equitherm.n_factor;
|
||||
etRegulator.Kk = settings.equitherm.k_factor;
|
||||
etRegulator.Ke = settings.equitherm.e_factor;
|
||||
etRegulator.targetTemp = targetTemp;
|
||||
etRegulator.outdoorTemp = outdoorTemp;
|
||||
float etResult = etRegulator.getResult();
|
||||
|
||||
@@ -496,9 +496,8 @@ protected:
|
||||
}
|
||||
}
|
||||
|
||||
if (!rSensor.connected) {
|
||||
rSensor.connected = true;
|
||||
}
|
||||
// Mark connected
|
||||
Sensors::setConnectionStatusById(sensorId, true, true);
|
||||
|
||||
if (!this->bleLastSetDtTime[sensorId] || millis() - this->bleLastSetDtTime[sensorId] > this->bleSetDtInterval) {
|
||||
struct tm ti;
|
||||
@@ -521,7 +520,6 @@ protected:
|
||||
|
||||
this->bleLastSetDtTime[sensorId] = millis();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -981,16 +979,16 @@ protected:
|
||||
auto& rSensor = Sensors::results[sensorId];
|
||||
|
||||
if (rSensor.connected && !sSensor.enabled) {
|
||||
rSensor.connected = false;
|
||||
Sensors::setConnectionStatusById(sensorId, false, false);
|
||||
|
||||
} else if (rSensor.connected && sSensor.type == Sensors::Type::NOT_CONFIGURED) {
|
||||
rSensor.connected = false;
|
||||
Sensors::setConnectionStatusById(sensorId, false, false);
|
||||
|
||||
} else if (rSensor.connected && sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) {
|
||||
rSensor.connected = false;
|
||||
Sensors::setConnectionStatusById(sensorId, false, false);
|
||||
|
||||
} else if (sSensor.type != Sensors::Type::MANUAL && rSensor.connected && (millis() - rSensor.activityTime) > this->disconnectedTimeout) {
|
||||
rSensor.connected = false;
|
||||
Sensors::setConnectionStatusById(sensorId, false, false);
|
||||
|
||||
}/* else if (!rSensor.connected) {
|
||||
rSensor.connected = true;
|
||||
|
||||
@@ -137,7 +137,6 @@ struct Settings {
|
||||
float n_factor = 0.7f;
|
||||
float k_factor = 3.0f;
|
||||
float t_factor = 2.0f;
|
||||
float e_factor = 1.3f;
|
||||
} equitherm;
|
||||
|
||||
struct {
|
||||
|
||||
3
src/idf_component.yml
Normal file
3
src/idf_component.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
idf: ">=5.3.2"
|
||||
h2zero/esp-nimble-cpp: ">=2.2.1"
|
||||
@@ -73,7 +73,6 @@ 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_E_FACTOR[] PROGMEM = "e_factor";
|
||||
const char S_EMERGENCY[] PROGMEM = "emergency";
|
||||
const char S_ENABLED[] PROGMEM = "enabled";
|
||||
const char S_ENV[] PROGMEM = "env";
|
||||
|
||||
@@ -502,7 +502,6 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
|
||||
equitherm[FPSTR(S_ENABLED)] = src.equitherm.enabled;
|
||||
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_E_FACTOR)] = roundf(src.equitherm.e_factor, 3);
|
||||
equitherm[FPSTR(S_T_FACTOR)] = roundf(src.equitherm.t_factor, 3);
|
||||
|
||||
auto pid = dst[FPSTR(S_PID)].to<JsonObject>();
|
||||
@@ -1101,14 +1100,6 @@ 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()) {
|
||||
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_T_FACTOR)].as<float>();
|
||||
|
||||
|
||||
@@ -342,14 +342,9 @@
|
||||
"equitherm": {
|
||||
"n": "N factor",
|
||||
"k": "K factor",
|
||||
"e": "Exponent E",
|
||||
"t": {
|
||||
"title": "T factor",
|
||||
"note": "Not used if PID is enabled"
|
||||
},
|
||||
"chart": {
|
||||
"radiatorTemp": "Radiator Temperature (°C)",
|
||||
"outdoorTemp": "Outdoor Temperature (°C)"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -342,14 +342,9 @@
|
||||
"equitherm": {
|
||||
"n": "Fattore N",
|
||||
"k": "Fattore K",
|
||||
"e": "Esponente E",
|
||||
"t": {
|
||||
"title": "Fattore T",
|
||||
"note": "Non usato se PID è attivato"
|
||||
},
|
||||
"chart": {
|
||||
"radiatorTemp": "Temperatura Del Radiatore (°C)",
|
||||
"outdoorTemp": "Outdoor Temperature (°C)"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -342,14 +342,9 @@
|
||||
"equitherm": {
|
||||
"n": "Коэффициент N",
|
||||
"k": "Коэффициент K",
|
||||
"e": "Экспонента E",
|
||||
"t": {
|
||||
"title": "Коэффициент T",
|
||||
"note": "Не используется, если ПИД включен"
|
||||
},
|
||||
"chart": {
|
||||
"radiatorTemp": "Температура радиатора (°C)",
|
||||
"outdoorTemp": "Наружная температура (°C)"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -41,14 +41,18 @@
|
||||
<details open>
|
||||
<summary><b data-i18n>dashboard.section.control</b></summary>
|
||||
<div class="grid">
|
||||
<div class="thermostat" id="thermostat-heating">
|
||||
<div class="thermostat tHeat" data-purpose="heating" data-min="0" data-max="100" data-step="0.1" data-big-step="1">
|
||||
<div class="thermostat-header" data-i18n>dashboard.thermostat.heating</div>
|
||||
<div class="thermostat-temp">
|
||||
<div class="thermostat-temp-target"><span id="tHeatTargetTemp"></span> <span class="tempUnit"></span></div>
|
||||
<div class="thermostat-temp-target"><span class="targetTemp"></span> <span class="tempUnit"></span></div>
|
||||
<div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="tHeatCurrentTemp"></span> <span class="tempUnit"></span></div>
|
||||
</div>
|
||||
<div class="thermostat-minus"><button id="tHeatActionMinus" class="outline"><i class="icons-down"></i></button></div>
|
||||
<div class="thermostat-plus"><button id="tHeatActionPlus" class="outline"><i class="icons-up"></i></button></div>
|
||||
<div class="thermostat-minus">
|
||||
<button class="tAction outline" data-action="decrement"><i class="icons-down"></i></button>
|
||||
</div>
|
||||
<div class="thermostat-plus">
|
||||
<button class="tAction outline" data-action="increment"><i class="icons-up"></i></button>
|
||||
</div>
|
||||
<div class="thermostat-control">
|
||||
<input type="checkbox" role="switch" id="tHeatEnabled" value="true">
|
||||
<label htmlFor="tHeatEnabled" data-i18n>dashboard.thermostat.enable</label>
|
||||
@@ -58,21 +62,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="thermostat" id="thermostat-dhw">
|
||||
<div class="thermostat tDhw" data-purpose="dhw" data-min="0" data-max="100" data-step="1" data-big-step="5">
|
||||
<div class="thermostat-header" data-i18n>dashboard.thermostat.dhw</div>
|
||||
<div class="thermostat-temp">
|
||||
<div class="thermostat-temp-target"><span id="tDhwTargetTemp"></span> <span class="tempUnit"></span></div>
|
||||
<div class="thermostat-temp-target"><span class="targetTemp"></span> <span class="tempUnit"></span></div>
|
||||
<div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="tDhwCurrentTemp"></span> <span class="tempUnit"></span></div>
|
||||
</div>
|
||||
<div class="thermostat-minus"><button class="outline" id="tDhwActionMinus"><i class="icons-down"></i></button></div>
|
||||
<div class="thermostat-plus"><button class="outline" id="tDhwActionPlus"><i class="icons-up"></i></button></div>
|
||||
<div class="thermostat-minus">
|
||||
<button class="tAction outline" data-action="decrement"><i class="icons-down"></i></button>
|
||||
</div>
|
||||
<div class="thermostat-plus">
|
||||
<button class="tAction outline" data-action="increment"><i class="icons-up"></i></button>
|
||||
</div>
|
||||
<div class="thermostat-control">
|
||||
<input type="checkbox" role="switch" id="tDhwEnabled" value="true">
|
||||
<label htmlFor="tDhwEnabled" data-i18n>dashboard.thermostat.enable</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="notify notify-error notify-fault hidden">
|
||||
<div class="notify-icon">
|
||||
<i class="icons-error"></i>
|
||||
@@ -282,7 +290,6 @@
|
||||
<script src="/static/app.js?{BUILD_TIME}"></script>
|
||||
<script>
|
||||
let modifiedTime = null;
|
||||
let noRegulators;
|
||||
let prevSettings;
|
||||
let newSettings = {
|
||||
heating: {
|
||||
@@ -300,78 +307,69 @@
|
||||
const lang = new Lang(document.getElementById('lang'));
|
||||
lang.build();
|
||||
|
||||
document.querySelector('#tHeatActionMinus').addEventListener('click', (event) => {
|
||||
if (!prevSettings) {
|
||||
return;
|
||||
let actionTimer = null;
|
||||
let actionLongPress = false;
|
||||
document.querySelectorAll('.tAction').forEach((item) => {
|
||||
const action = item.dataset.action;
|
||||
const tContainer = item.parentNode.parentNode;
|
||||
|
||||
for (const eName of ['pointerup', 'pointercancel']) {
|
||||
item.addEventListener(eName, (event) => {
|
||||
clearInterval(actionTimer);
|
||||
|
||||
const purpose = tContainer.dataset.purpose;
|
||||
const minTemp = parseFloat(tContainer.dataset.min);
|
||||
const maxTemp = parseFloat(tContainer.dataset.max);
|
||||
const step = parseFloat(tContainer.dataset.step);
|
||||
const bigStep = parseFloat(tContainer.dataset.bigStep);
|
||||
|
||||
if (!actionLongPress && prevSettings) {
|
||||
let value = 0;
|
||||
if (action == 'increment') {
|
||||
value = step;
|
||||
|
||||
} else if (action == 'decrement') {
|
||||
value = -(step);
|
||||
}
|
||||
|
||||
newSettings[purpose].target = parseFloat(constrain(newSettings[purpose].target + value, minTemp, maxTemp).toFixed(2));
|
||||
modifiedTime = Date.now();
|
||||
setValue('.targetTemp', newSettings[purpose].target, tContainer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newSettings.heating.target -= 0.5;
|
||||
modifiedTime = Date.now();
|
||||
item.addEventListener('pointerdown', (event) => {
|
||||
if (!prevSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
let minTemp;
|
||||
if (noRegulators) {
|
||||
minTemp = prevSettings.heating.minTemp;
|
||||
} else {
|
||||
minTemp = prevSettings.system.unitSystem == 0 ? 5 : 41;
|
||||
}
|
||||
const purpose = tContainer.dataset.purpose;
|
||||
const minTemp = parseFloat(tContainer.dataset.min);
|
||||
const maxTemp = parseFloat(tContainer.dataset.max);
|
||||
const step = parseFloat(tContainer.dataset.step);
|
||||
const bigStep = parseFloat(tContainer.dataset.bigStep);
|
||||
|
||||
if (prevSettings && newSettings.heating.target < minTemp) {
|
||||
newSettings.heating.target = minTemp;
|
||||
}
|
||||
actionLongPress = false;
|
||||
actionTimer = setInterval(() => {
|
||||
if (!actionLongPress) {
|
||||
actionLongPress = true;
|
||||
}
|
||||
|
||||
setValue('#tHeatTargetTemp', newSettings.heating.target);
|
||||
});
|
||||
let value = 0;
|
||||
if (action == 'increment') {
|
||||
value = bigStep;
|
||||
|
||||
document.querySelector('#tHeatActionPlus').addEventListener('click', (event) => {
|
||||
if (!prevSettings) {
|
||||
return;
|
||||
}
|
||||
} else if (action == 'decrement') {
|
||||
value = -(bigStep);
|
||||
}
|
||||
|
||||
newSettings.heating.target += 0.5;
|
||||
modifiedTime = Date.now();
|
||||
newSettings[purpose].target = parseFloat(constrain(newSettings[purpose].target + value, minTemp, maxTemp).toFixed(2));
|
||||
modifiedTime = Date.now();
|
||||
|
||||
let maxTemp;
|
||||
if (noRegulators) {
|
||||
maxTemp = prevSettings.heating.maxTemp;
|
||||
} else {
|
||||
maxTemp = prevSettings.system.unitSystem == 0 ? 30 : 86;
|
||||
}
|
||||
|
||||
if (prevSettings && newSettings.heating.target > maxTemp) {
|
||||
newSettings.heating.target = maxTemp;
|
||||
}
|
||||
|
||||
setValue('#tHeatTargetTemp', newSettings.heating.target);
|
||||
});
|
||||
|
||||
document.querySelector('#tDhwActionMinus').addEventListener('click', (event) => {
|
||||
if (!prevSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
newSettings.dhw.target -= 1.0;
|
||||
modifiedTime = Date.now();
|
||||
|
||||
if (newSettings.dhw.target < prevSettings.dhw.minTemp) {
|
||||
newSettings.dhw.target = prevSettings.dhw.minTemp;
|
||||
}
|
||||
|
||||
setValue('#tDhwTargetTemp', newSettings.dhw.target);
|
||||
});
|
||||
|
||||
document.querySelector('#tDhwActionPlus').addEventListener('click', (event) => {
|
||||
if (!prevSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
newSettings.dhw.target += 1.0;
|
||||
modifiedTime = Date.now();
|
||||
|
||||
if (newSettings.dhw.target > prevSettings.dhw.maxTemp) {
|
||||
newSettings.dhw.target = prevSettings.dhw.maxTemp;
|
||||
}
|
||||
|
||||
setValue('#tDhwTargetTemp', newSettings.dhw.target);
|
||||
setValue('.targetTemp', newSettings[purpose].target, tContainer);
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#tHeatEnabled').addEventListener('change', (event) => {
|
||||
@@ -486,7 +484,6 @@
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
noRegulators = !result.opentherm.options.nativeHeatingControl && !result.equitherm.enabled && !result.pid.enabled;
|
||||
prevSettings = result;
|
||||
unitSystem = result.system.unitSystem;
|
||||
newSettings.heating.enabled = result.heating.enabled;
|
||||
@@ -496,17 +493,17 @@
|
||||
newSettings.dhw.target = result.dhw.target;
|
||||
|
||||
if (result.opentherm.options.dhwSupport) {
|
||||
show('#thermostat-dhw');
|
||||
show('.tDhw');
|
||||
} else {
|
||||
hide('#thermostat-dhw');
|
||||
hide('.tDhw');
|
||||
}
|
||||
|
||||
setCheckboxValue('#tHeatEnabled', result.heating.enabled);
|
||||
setCheckboxValue('#tHeatTurbo', result.heating.turbo);
|
||||
setValue('#tHeatTargetTemp', result.heating.target);
|
||||
setValue('.tHeat .targetTemp', result.heating.target);
|
||||
|
||||
setCheckboxValue('#tDhwEnabled', result.dhw.enabled);
|
||||
setValue('#tDhwTargetTemp', result.dhw.target);
|
||||
setValue('.tDhw .targetTemp', result.dhw.target);
|
||||
|
||||
setValue('.tempUnit', temperatureUnit(unitSystem));
|
||||
setValue('.pressureUnit', pressureUnit(unitSystem));
|
||||
@@ -523,20 +520,20 @@
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Response not valid');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
// Graph
|
||||
setValue('#tHeatCurrentTemp', result.master.heating.indoorTempControl
|
||||
? result.master.heating.indoorTemp
|
||||
setValue('#tHeatCurrentTemp', result.master.heating.indoorTempControl
|
||||
? result.master.heating.indoorTemp
|
||||
: result.master.heating.currentTemp
|
||||
);
|
||||
setValue('#tDhwCurrentTemp', result.master.dhw.currentTemp);
|
||||
|
||||
|
||||
|
||||
// SLAVE
|
||||
setValue('.sMemberId', result.slave.memberId);
|
||||
@@ -646,6 +643,14 @@
|
||||
setState('.mCascadeControlInput', result.master.cascadeControl.input);
|
||||
setState('.mCascadeControlOutput', result.master.cascadeControl.output);
|
||||
|
||||
const tHeat = document.querySelector('.tHeat');
|
||||
tHeat.dataset.min = result.master.heating.minTemp;
|
||||
tHeat.dataset.max = result.master.heating.maxTemp;
|
||||
|
||||
const tDhw = document.querySelector('.tDhw');
|
||||
tDhw.dataset.min = result.master.dhw.minTemp;
|
||||
tDhw.dataset.max = result.master.dhw.maxTemp;
|
||||
|
||||
setBusy('#dashboard-busy', '#dashboard-container', false);
|
||||
|
||||
} catch (error) {
|
||||
@@ -658,7 +663,7 @@
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Response not valid");
|
||||
}
|
||||
@@ -684,7 +689,7 @@
|
||||
if (!sensorNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const sData = result[sensorId];
|
||||
if (!sData.enabled || sData.purpose == 255) {
|
||||
sensorNode.classList.toggle("hidden", true);
|
||||
|
||||
@@ -261,10 +261,9 @@
|
||||
</details>
|
||||
|
||||
<hr />
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.equitherm</b></summary>
|
||||
<canvas id="equithermChart" width="400" height="200"></canvas>
|
||||
<div>
|
||||
<div id="equitherm-settings-busy" aria-busy="true"></div>
|
||||
<form action="/api/settings" id="equitherm-settings" class="hidden">
|
||||
@@ -274,7 +273,7 @@
|
||||
<span data-i18n>settings.enable</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
|
||||
<div class="grid">
|
||||
<label>
|
||||
<span data-i18n>settings.equitherm.n</span>
|
||||
@@ -286,11 +285,6 @@
|
||||
<input type="number" inputmode="decimal" name="equitherm[k_factor]" min="0" max="10" 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>
|
||||
<span data-i18n>settings.equitherm.t.title</span>
|
||||
<input type="number" inputmode="decimal" name="equitherm[t_factor]" min="0" max="10" step="0.01" required>
|
||||
@@ -305,8 +299,6 @@
|
||||
|
||||
<hr />
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b data-i18n>settings.section.pid</b></summary>
|
||||
<div>
|
||||
@@ -764,7 +756,6 @@
|
||||
</footer>
|
||||
|
||||
<script src="/static/app.js?{BUILD_TIME}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const lang = new Lang(document.getElementById('lang'));
|
||||
@@ -894,7 +885,6 @@
|
||||
setCheckboxValue("[name='equitherm[enabled]']", data.equitherm.enabled);
|
||||
setInputValue("[name='equitherm[n_factor]']", data.equitherm.n_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);
|
||||
setBusy('#equitherm-settings-busy', '#equitherm-settings', false);
|
||||
|
||||
@@ -919,9 +909,6 @@
|
||||
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);
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -974,186 +961,6 @@
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
//График
|
||||
let equithermChart;
|
||||
|
||||
async function initChart() {
|
||||
try {
|
||||
const response = await fetch("/api/settings", {
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Response not valid');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
//График переменные
|
||||
const targetTemp = result?.heating?.target ?? 24;
|
||||
const maxOut = result?.heating?.maxTemp ?? 90;
|
||||
const Kn = result?.equitherm?.n_factor ?? 1;
|
||||
const Ke = result?.equitherm?.e_factor ?? 1.3;
|
||||
const Kk = result?.equitherm?.k_factor ?? 0;
|
||||
|
||||
function calculateTRad(targetTemp, outdoorTemp, maxOut, Kn, Ke, Kk) {
|
||||
let tempDiff = targetTemp - outdoorTemp;
|
||||
if (tempDiff < 0) {
|
||||
tempDiff = 0;
|
||||
}
|
||||
const minOutside = targetTemp - (maxOut - targetTemp) / Kn;
|
||||
let base = targetTemp - minOutside;
|
||||
if (base <= 0) {
|
||||
base = 0.0001;
|
||||
}
|
||||
const c1 = (maxOut - targetTemp) / Math.pow(base, 1.0 / Ke);
|
||||
let T_rad = targetTemp + c1 * Math.pow(tempDiff, 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 };
|
||||
}
|
||||
|
||||
// Стартовые данные
|
||||
const { outdoorTemps, predictedTRad } = generateChartData(targetTemp, maxOut, Kn, Ke, Kk);
|
||||
|
||||
// Создаем график
|
||||
const ctx = document.getElementById('equithermChart').getContext('2d');
|
||||
// Create gradient for the line
|
||||
const canvasHeight = ctx.canvas.height;
|
||||
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, 0); // Adjust x1, y1, x2, y2 for direction
|
||||
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, // Use gradient instead of solid color
|
||||
borderWidth: 1,
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
pointRadius: 2, // Reduce dot size (default is 3)
|
||||
pointHoverRadius: 4,
|
||||
data: predictedTRad
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Наружная температура (°C)'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Температура Радиатора (°C)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Показ формы
|
||||
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;
|
||||
|
||||
fetch("/api/settings", {
|
||||
cache: "no-cache",
|
||||
credentials: "include"
|
||||
})
|
||||
.then(response => response.json())
|
||||
.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;
|
||||
|
||||
function calculateTRad(targetTemp, outdoorTemp, maxOut, Kn, Ke, Kk) {
|
||||
let tempDiff = targetTemp - outdoorTemp;
|
||||
if (tempDiff < 0) {
|
||||
tempDiff = 0;
|
||||
}
|
||||
const minOutside = targetTemp - (maxOut - targetTemp) / Kn;
|
||||
let base = targetTemp - minOutside;
|
||||
if (base <= 0) {
|
||||
base = 0.0001;
|
||||
}
|
||||
const c1 = (maxOut - targetTemp) / Math.pow(base, 1.0 / Ke);
|
||||
let T_rad = targetTemp + c1 * Math.pow(tempDiff, 1.0 / Ke) + Kk;
|
||||
return Math.min(T_rad, maxOut);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
equithermChart.data.labels = outdoorTemps;
|
||||
equithermChart.data.datasets[0].data = predictedTRad;
|
||||
equithermChart.update();
|
||||
})
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
|
||||
// Слушаем отправку
|
||||
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>
|
||||
</body>
|
||||
|
||||
@@ -849,4 +849,8 @@ function dec2hex(i) {
|
||||
}
|
||||
|
||||
return hex.toUpperCase();
|
||||
}
|
||||
|
||||
function constrain(amt, low, high) {
|
||||
return ((amt) < (low) ? (low) : ((amt) > (high) ? (high) : (amt)));
|
||||
}
|
||||
Reference in New Issue
Block a user