diff --git a/.gitignore b/.gitignore index 94be47d..584ce9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .pio .vscode +.PVS-Studio build/* data/* managed_components/* diff --git a/README.md b/README.md index a5f681b..a1d9634 100644 --- a/README.md +++ b/README.md @@ -71,5 +71,10 @@ All available information and instructions can be found in the wiki: * [Connection](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#connection) * [Leds on board](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#leds-on-board) -___ -This project is tested with BrowserStack. +## Gratitude +* To the developers of the libraries used: [OpenTherm Library](https://github.com/ihormelnyk/opentherm_library), [ESP8266Scheduler](https://github.com/nrwiersma/ESP8266Scheduler), [ArduinoJson](https://github.com/bblanchon/ArduinoJson), [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino), [ArduinoMqttClient](https://github.com/arduino-libraries/ArduinoMqttClient), [ESPTelnet](https://github.com/LennartHennigs/ESPTelnet), [FileData](https://github.com/GyverLibs/FileData), [GyverPID](https://github.com/GyverLibs/GyverPID), [GyverBlinker](https://github.com/GyverLibs/GyverBlinker), [FileData](https://github.com/GyverLibs/FileData), [OneWireNg](https://github.com/pstolarz/OneWireNg) & [OneWire](https://github.com/PaulStoffregen/OneWire) +* To the [PlatformIO](https://platformio.org/) Team +* To the team and contributors of the [pioarduino](https://github.com/pioarduino/platform-espressif32) project +* To the [BrowserStack](https://www.browserstack.com/) team. This project is tested with BrowserStack. +* To the [PVS-Studio](https://pvs-studio.com/pvs-studio/?utm_source=website&utm_medium=github&utm_campaign=open_source) - static analyzer for C, C++, C#, and Java code. +* And of course to the contributors for their contribution to the development of the project! \ No newline at end of file diff --git a/lib/CustomOpenTherm/CustomOpenTherm.h b/lib/CustomOpenTherm/CustomOpenTherm.h index 389cfe5..82f59c1 100644 --- a/lib/CustomOpenTherm/CustomOpenTherm.h +++ b/lib/CustomOpenTherm/CustomOpenTherm.h @@ -106,6 +106,11 @@ public: return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::RemoteRequest); } + static bool isCh2Active(unsigned long response) + { + return response & 0x20; + } + static bool isValidResponseId(unsigned long response, OpenThermMessageID id) { byte responseId = (response >> 16) & 0xFF; diff --git a/platformio.ini b/platformio.ini index f2e2727..b574a42 100644 --- a/platformio.ini +++ b/platformio.ini @@ -14,7 +14,7 @@ extra_configs = secrets.default.ini core_dir = .pio [env] -version = 1.5.4 +version = 1.5.5 framework = arduino lib_deps = bblanchon/ArduinoJson@^7.3.0 @@ -291,6 +291,11 @@ framework = arduino, espidf platform_packages = ${esp32_defaults.platform_packages} board = esp32-c6-devkitm-1 board_build.partitions = ${esp32_defaults.board_build.partitions} +board_build.embed_txtfiles = + managed_components/espressif__esp_insights/server_certs/https_server.crt + managed_components/espressif__esp_rainmaker/server_certs/rmaker_mqtt_server.crt + managed_components/espressif__esp_rainmaker/server_certs/rmaker_claim_service_server.crt + managed_components/espressif__esp_rainmaker/server_certs/rmaker_ota_server.crt lib_deps = ${esp32_defaults.lib_deps} lib_ignore = ${esp32_defaults.lib_ignore} diff --git a/src/MainTask.h b/src/MainTask.h index 65aba81..af02b60 100644 --- a/src/MainTask.h +++ b/src/MainTask.h @@ -29,6 +29,7 @@ protected: enum class PumpStartReason {NONE, HEATING, ANTISTUCK}; Blinker* blinker = nullptr; + unsigned long miscRunned = 0; unsigned long lastHeapInfo = 0; unsigned int minFreeHeap = 0; unsigned int minMaxFreeBlockHeap = 0; @@ -42,6 +43,8 @@ protected: bool telnetStarted = false; bool emergencyDetected = false; unsigned long emergencyFlipTime = 0; + bool freezeDetected = false; + unsigned long freezeDetectedTime = 0; #if defined(ARDUINO_ARCH_ESP32) const char* getTaskName() override { @@ -150,17 +153,16 @@ protected: Sensors::setConnectionStatusByType(Sensors::Type::MANUAL, false, false); } - this->yield(); - this->emergency(); + this->yield(); + if (this->misc()) { + this->yield(); + } this->ledStatus(); - this->cascadeControl(); - this->externalPump(); - this->yield(); - // telnet if (this->telnetStarted) { + this->yield(); telnetStream->loop(); this->yield(); } @@ -179,14 +181,27 @@ protected: // heap info this->heap(); + } + bool misc() { + if (millis() - this->miscRunned < 1000) { + return false; + } - // restart + // restart if required if (this->restartSignalReceived && millis() - this->restartSignalReceivedTime > 15000) { this->restartSignalReceived = false; ESP.restart(); } + + this->heating(); + this->emergency(); + this->cascadeControl(); + this->externalPump(); + this->miscRunned = millis(); + + return true; } void heap() { @@ -228,6 +243,65 @@ protected: } } + void heating() { + // freeze protection + if (!settings.heating.enabled) { + float lowTemp = 255.0f; + uint8_t availableSensors = 0; + + if (Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP)) { + auto value = Sensors::getMeanValueByPurpose(Sensors::Purpose::INDOOR_TEMP, Sensors::ValueType::PRIMARY); + if (value < lowTemp) { + lowTemp = value; + } + + availableSensors++; + } + + if (Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::HEATING_TEMP)) { + auto value = Sensors::getMeanValueByPurpose(Sensors::Purpose::HEATING_TEMP, Sensors::ValueType::PRIMARY); + if (value < lowTemp) { + lowTemp = value; + } + + availableSensors++; + } + + if (Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::HEATING_RETURN_TEMP)) { + auto value = Sensors::getMeanValueByPurpose(Sensors::Purpose::HEATING_RETURN_TEMP, Sensors::ValueType::PRIMARY); + if (value < lowTemp) { + lowTemp = value; + } + + availableSensors++; + } + + if (availableSensors && lowTemp <= settings.heating.freezeProtection.lowTemp) { + if (!this->freezeDetected) { + this->freezeDetected = true; + this->freezeDetectedTime = millis(); + + } else if (millis() - this->freezeDetectedTime > (settings.heating.freezeProtection.thresholdTime * 1000)) { + this->freezeDetected = false; + settings.heating.enabled = true; + fsSettings.update(); + + Log.sinfoln( + FPSTR(L_MAIN), + F("Heating turned on by freeze protection, current low temp: %.2f, threshold: %hhu"), + lowTemp, settings.heating.freezeProtection.lowTemp + ); + } + + } else if (this->freezeDetected) { + this->freezeDetected = false; + } + + } else if (this->freezeDetected) { + this->freezeDetected = false; + } + } + void emergency() { // flags uint8_t emergencyFlags = 0b00000000; diff --git a/src/OpenThermTask.h b/src/OpenThermTask.h index f37bc80..4b7543b 100644 --- a/src/OpenThermTask.h +++ b/src/OpenThermTask.h @@ -169,12 +169,15 @@ protected: // Heating settings vars.master.heating.enabled = this->isReady() - && (settings.heating.enabled || vars.emergency.state) + && settings.heating.enabled && vars.cascadeControl.input - && !vars.master.heating.blocking; + && !vars.master.heating.blocking + && !vars.master.heating.overheat; // DHW settings - vars.master.dhw.enabled = settings.opentherm.options.dhwSupport && settings.dhw.enabled; + vars.master.dhw.enabled = settings.opentherm.options.dhwSupport + && settings.dhw.enabled + && !vars.master.dhw.overheat; vars.master.dhw.targetTemp = settings.dhw.target; // CH2 settings @@ -205,6 +208,12 @@ protected: summerWinterMode = vars.master.heating.enabled == summerWinterMode; } + // DHW blocking + bool dhwBlocking = settings.opentherm.options.dhwBlocking; + if (settings.opentherm.options.dhwStateAsDhwBlocking) { + dhwBlocking = vars.master.dhw.enabled == dhwBlocking; + } + unsigned long response = this->instance->setBoilerStatus( vars.master.heating.enabled, vars.master.dhw.enabled, @@ -212,7 +221,7 @@ protected: settings.opentherm.options.nativeHeatingControl, vars.master.ch2.enabled, summerWinterMode, - settings.opentherm.options.dhwBlocking, + dhwBlocking, statusLb ); @@ -228,6 +237,7 @@ protected: vars.slave.dhw.active = settings.opentherm.options.dhwSupport ? CustomOpenTherm::isHotWaterActive(response) : false; vars.slave.flame = CustomOpenTherm::isFlameOn(response); vars.slave.cooling = CustomOpenTherm::isCoolingActive(response); + vars.slave.ch2.active = CustomOpenTherm::isCh2Active(response); vars.slave.fault.active = CustomOpenTherm::isFault(response); if (!settings.opentherm.options.ignoreDiagState) { @@ -238,9 +248,9 @@ protected: } Log.snoticeln( - FPSTR(L_OT), F("Received boiler status. Heating: %hhu; DHW: %hhu; flame: %hhu; cooling: %hhu; fault: %hhu; diag: %hhu"), + FPSTR(L_OT), F("Received boiler status. Heating: %hhu; DHW: %hhu; flame: %hhu; cooling: %hhu; channel 2: %hhu; fault: %hhu; diag: %hhu"), vars.slave.heating.active, vars.slave.dhw.active, - vars.slave.flame, vars.slave.cooling, vars.slave.fault.active, vars.slave.diag.active + vars.slave.flame, vars.slave.cooling, vars.slave.ch2.active, vars.slave.fault.active, vars.slave.diag.active ); } @@ -1307,6 +1317,84 @@ protected: } } } + + + // Heating overheat control + if (settings.heating.overheatProtection.highTemp > 0 && settings.heating.overheatProtection.lowTemp > 0) { + float highTemp = convertTemp( + max({ + vars.slave.heating.currentTemp, + vars.slave.heating.returnTemp, + vars.slave.heatExchangerTemp + }), + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + if (vars.master.heating.overheat) { + if ((float) settings.heating.overheatProtection.lowTemp - highTemp + 0.0001f >= 0.0f) { + vars.master.heating.overheat = false; + + Log.sinfoln( + FPSTR(L_OT_HEATING), F("Overheating not detected. Current high temp: %.2f, threshold (low): %hhu"), + highTemp, settings.heating.overheatProtection.lowTemp + ); + } + + } else if (vars.slave.heating.active) { + if (highTemp - (float) settings.heating.overheatProtection.highTemp + 0.0001f >= 0.0f) { + vars.master.heating.overheat = true; + + Log.swarningln( + FPSTR(L_OT_HEATING), F("Overheating detected! Current high temp: %.2f, threshold (high): %hhu"), + highTemp, settings.heating.overheatProtection.highTemp + ); + } + } + + } else if (vars.master.heating.overheat) { + vars.master.heating.overheat = false; + } + + // DHW overheat control + if (settings.dhw.overheatProtection.highTemp > 0 && settings.dhw.overheatProtection.lowTemp > 0) { + float highTemp = convertTemp( + max({ + vars.slave.heating.currentTemp, + vars.slave.heating.returnTemp, + vars.slave.heatExchangerTemp, + vars.slave.dhw.currentTemp, + vars.slave.dhw.currentTemp2, + vars.slave.dhw.returnTemp + }), + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + if (vars.master.dhw.overheat) { + if ((float) settings.dhw.overheatProtection.lowTemp - highTemp + 0.0001f >= 0.0f) { + vars.master.dhw.overheat = false; + + Log.sinfoln( + FPSTR(L_OT_DHW), F("Overheating not detected. Current high temp: %.2f, threshold (low): %hhu"), + highTemp, settings.dhw.overheatProtection.lowTemp + ); + } + + } else if (vars.slave.dhw.active) { + if (highTemp - (float) settings.dhw.overheatProtection.highTemp + 0.0001f >= 0.0f) { + vars.master.dhw.overheat = true; + + Log.swarningln( + FPSTR(L_OT_DHW), F("Overheating detected! Current high temp: %.2f, threshold (high): %hhu"), + highTemp, settings.dhw.overheatProtection.highTemp + ); + } + } + + } else if (vars.master.dhw.overheat) { + vars.master.dhw.overheat = false; + } } void initialize() { diff --git a/src/Sensors.h b/src/Sensors.h index 4937d79..7b0cc61 100644 --- a/src/Sensors.h +++ b/src/Sensors.h @@ -138,7 +138,7 @@ public: } uint8_t amount = 0; - for (uint8_t id = 0; id < getMaxSensorId(); id++) { + for (uint8_t id = 0; id <= getMaxSensorId(); id++) { if (settings[id].type == type && (!onlyEnabled || settings[id].enabled)) { amount++; } @@ -152,7 +152,7 @@ public: return 0; } - for (uint8_t id = 0; id < getMaxSensorId(); id++) { + for (uint8_t id = 0; id <= getMaxSensorId(); id++) { if (strcmp(settings[id].name, name) == 0) { return id; } @@ -167,7 +167,7 @@ public: } String refObjectId; - for (uint8_t id = 0; id < getMaxSensorId(); id++) { + for (uint8_t id = 0; id <= getMaxSensorId(); id++) { Sensors::makeObjectId(refObjectId, settings[id].name); if (refObjectId.equals(objectId)) { return id; @@ -247,7 +247,7 @@ public: uint8_t updated = 0; // read sensors data for current instance - for (uint8_t sensorId = 0; sensorId < getMaxSensorId(); sensorId++) { + for (uint8_t sensorId = 0; sensorId <= getMaxSensorId(); sensorId++) { auto& sSensor = settings[sensorId]; // only target & valid sensors @@ -311,7 +311,7 @@ public: uint8_t updated = 0; // read sensors data for current instance - for (uint8_t sensorId = 0; sensorId < getMaxSensorId(); sensorId++) { + for (uint8_t sensorId = 0; sensorId <= getMaxSensorId(); sensorId++) { auto& sSensor = settings[sensorId]; // only target & valid sensors @@ -340,7 +340,7 @@ public: float value = 0.0f; uint8_t amount = 0; - for (uint8_t id = 0; id < getMaxSensorId(); id++) { + for (uint8_t id = 0; id <= getMaxSensorId(); id++) { auto& sSensor = settings[id]; auto& rSensor = results[id]; @@ -366,7 +366,7 @@ public: return 0; } - for (uint8_t id = 0; id < getMaxSensorId(); id++) { + for (uint8_t id = 0; id <= getMaxSensorId(); id++) { if (settings[id].purpose == purpose && results[id].connected) { return true; } diff --git a/src/Settings.h b/src/Settings.h index 1442534..c718a4d 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -71,6 +71,7 @@ struct Settings { bool heatingToCh2 = false; bool dhwToCh2 = false; bool dhwBlocking = false; + bool dhwStateAsDhwBlocking = false; bool maxTempSyncWithTargetTemp = true; bool getMinMaxTemp = true; bool ignoreDiagState = false; @@ -107,6 +108,16 @@ struct Settings { byte minTemp = DEFAULT_HEATING_MIN_TEMP; byte maxTemp = DEFAULT_HEATING_MAX_TEMP; uint8_t maxModulation = 100; + + struct { + uint8_t highTemp = 95; + uint8_t lowTemp = 90; + } overheatProtection; + + struct { + uint8_t lowTemp = 10; + unsigned short thresholdTime = 600; + } freezeProtection; } heating; struct { @@ -115,6 +126,11 @@ struct Settings { byte minTemp = DEFAULT_DHW_MIN_TEMP; byte maxTemp = DEFAULT_DHW_MAX_TEMP; uint8_t maxModulation = 100; + + struct { + uint8_t highTemp = 95; + uint8_t lowTemp = 90; + } overheatProtection; } dhw; struct { @@ -280,6 +296,7 @@ struct Variables { bool blocking = false; bool enabled = false; bool indoorTempControl = false; + bool overheat = false; float setpointTemp = 0.0f; float targetTemp = 0.0f; float currentTemp = 0.0f; @@ -292,6 +309,7 @@ struct Variables { struct { bool enabled = false; + bool overheat = false; float targetTemp = 0.0f; float currentTemp = 0.0f; float returnTemp = 0.0f; @@ -391,6 +409,7 @@ struct Variables { } dhw; struct { + bool active = false; bool enabled = false; float targetTemp = 0.0f; float currentTemp = 0.0f; diff --git a/src/strings.h b/src/strings.h index b924369..7d86f5a 100644 --- a/src/strings.h +++ b/src/strings.h @@ -68,6 +68,7 @@ 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_STATE_AS_DHW_BLOCKING[] PROGMEM = "dhwStateAsDhwBlocking"; const char S_DHW_SUPPORT[] PROGMEM = "dhwSupport"; const char S_DHW_TO_CH2[] PROGMEM = "dhwToCh2"; const char S_DIAG[] PROGMEM = "diag"; @@ -84,6 +85,7 @@ const char S_EXPONENT[] PROGMEM = "exponent"; const char S_EXTERNAL_PUMP[] PROGMEM = "externalPump"; const char S_FACTOR[] PROGMEM = "factor"; const char S_FAULT[] PROGMEM = "fault"; +const char S_FREEZE_PROTECTION[] PROGMEM = "freezeProtection"; const char S_FILTERING[] PROGMEM = "filtering"; const char S_FILTERING_FACTOR[] PROGMEM = "filteringFactor"; const char S_FLAGS[] PROGMEM = "flags"; @@ -99,6 +101,7 @@ const char S_HEATING[] PROGMEM = "heating"; const char S_HEATING_TO_CH2[] PROGMEM = "heatingToCh2"; const char S_HEATING_STATE_TO_SUMMER_WINTER_MODE[] PROGMEM = "heatingStateToSummerWinterMode"; const char S_HIDDEN[] PROGMEM = "hidden"; +const char S_HIGH_TEMP[] PROGMEM = "highTemp"; const char S_HOME_ASSISTANT_DISCOVERY[] PROGMEM = "homeAssistantDiscovery"; const char S_HOSTNAME[] PROGMEM = "hostname"; const char S_HUMIDITY[] PROGMEM = "humidity"; @@ -117,6 +120,7 @@ const char S_I_FACTOR[] PROGMEM = "i_factor"; const char S_I_MULTIPLIER[] PROGMEM = "i_multiplier"; const char S_LOGIN[] PROGMEM = "login"; const char S_LOG_LEVEL[] PROGMEM = "logLevel"; +const char S_LOW_TEMP[] PROGMEM = "lowTemp"; const char S_MAC[] PROGMEM = "mac"; const char S_MASTER[] PROGMEM = "master"; const char S_MAX[] PROGMEM = "max"; @@ -148,6 +152,8 @@ const char S_OPTIONS[] PROGMEM = "options"; const char S_OUTDOOR_TEMP[] PROGMEM = "outdoorTemp"; const char S_OUT_GPIO[] PROGMEM = "outGpio"; const char S_OUTPUT[] PROGMEM = "output"; +const char S_OVERHEAT[] PROGMEM = "overheat"; +const char S_OVERHEAT_PROTECTION[] PROGMEM = "overheatProtection"; const char S_PASSWORD[] PROGMEM = "password"; const char S_PID[] PROGMEM = "pid"; const char S_PORT[] PROGMEM = "port"; diff --git a/src/utils.h b/src/utils.h index b2404e6..cfaada9 100644 --- a/src/utils.h +++ b/src/utils.h @@ -461,6 +461,7 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) { otOptions[FPSTR(S_HEATING_TO_CH2)] = src.opentherm.options.heatingToCh2; otOptions[FPSTR(S_DHW_TO_CH2)] = src.opentherm.options.dhwToCh2; otOptions[FPSTR(S_DHW_BLOCKING)] = src.opentherm.options.dhwBlocking; + otOptions[FPSTR(S_DHW_STATE_AS_DHW_BLOCKING)] = src.opentherm.options.dhwStateAsDhwBlocking; otOptions[FPSTR(S_MAX_TEMP_SYNC_WITH_TARGET_TEMP)] = src.opentherm.options.maxTempSyncWithTargetTemp; otOptions[FPSTR(S_GET_MIN_MAX_TEMP)] = src.opentherm.options.getMinMaxTemp; otOptions[FPSTR(S_IGNORE_DIAG_STATE)] = src.opentherm.options.ignoreDiagState; @@ -495,6 +496,14 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) { heating[FPSTR(S_MAX_TEMP)] = src.heating.maxTemp; heating[FPSTR(S_MAX_MODULATION)] = src.heating.maxModulation; + auto heatingOverheatProtection = heating[FPSTR(S_OVERHEAT_PROTECTION)].to(); + heatingOverheatProtection[FPSTR(S_HIGH_TEMP)] = src.heating.overheatProtection.highTemp; + heatingOverheatProtection[FPSTR(S_LOW_TEMP)] = src.heating.overheatProtection.lowTemp; + + auto freezeProtection = heating[FPSTR(S_FREEZE_PROTECTION)].to(); + freezeProtection[FPSTR(S_LOW_TEMP)] = src.heating.freezeProtection.lowTemp; + freezeProtection[FPSTR(S_THRESHOLD_TIME)] = src.heating.freezeProtection.thresholdTime; + auto dhw = dst[FPSTR(S_DHW)].to(); dhw[FPSTR(S_ENABLED)] = src.dhw.enabled; dhw[FPSTR(S_TARGET)] = roundf(src.dhw.target, 1); @@ -502,6 +511,10 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) { dhw[FPSTR(S_MAX_TEMP)] = src.dhw.maxTemp; dhw[FPSTR(S_MAX_MODULATION)] = src.dhw.maxModulation; + auto dhwOverheatProtection = dhw[FPSTR(S_OVERHEAT_PROTECTION)].to(); + dhwOverheatProtection[FPSTR(S_HIGH_TEMP)] = src.dhw.overheatProtection.highTemp; + dhwOverheatProtection[FPSTR(S_LOW_TEMP)] = src.dhw.overheatProtection.lowTemp; + auto equitherm = dst[FPSTR(S_EQUITHERM)].to(); equitherm[FPSTR(S_ENABLED)] = src.equitherm.enabled; equitherm[FPSTR(S_SLOPE)] = roundf(src.equitherm.slope, 3); @@ -924,6 +937,15 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_OPTIONS)][FPSTR(S_DHW_STATE_AS_DHW_BLOCKING)].is()) { + bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_OPTIONS)][FPSTR(S_DHW_STATE_AS_DHW_BLOCKING)].as(); + + if (value != dst.opentherm.options.dhwStateAsDhwBlocking) { + dst.opentherm.options.dhwStateAsDhwBlocking = value; + changed = true; + } + } + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_OPTIONS)][FPSTR(S_MAX_TEMP_SYNC_WITH_TARGET_TEMP)].is()) { bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_OPTIONS)][FPSTR(S_MAX_TEMP_SYNC_WITH_TARGET_TEMP)].as(); @@ -1342,6 +1364,49 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } + if (!src[FPSTR(S_HEATING)][FPSTR(S_OVERHEAT_PROTECTION)][FPSTR(S_HIGH_TEMP)].isNull()) { + unsigned char value = src[FPSTR(S_HEATING)][FPSTR(S_OVERHEAT_PROTECTION)][FPSTR(S_HIGH_TEMP)].as(); + + if (isValidTemp(value, dst.system.unitSystem, 0.0f, 100.0f) && value != dst.heating.overheatProtection.highTemp) { + dst.heating.overheatProtection.highTemp = value; + changed = true; + } + } + + if (!src[FPSTR(S_HEATING)][FPSTR(S_OVERHEAT_PROTECTION)][FPSTR(S_LOW_TEMP)].isNull()) { + unsigned char value = src[FPSTR(S_HEATING)][FPSTR(S_OVERHEAT_PROTECTION)][FPSTR(S_LOW_TEMP)].as(); + + if (isValidTemp(value, dst.system.unitSystem, 0.0f, 99.0f) && value != dst.heating.overheatProtection.lowTemp) { + dst.heating.overheatProtection.lowTemp = value; + changed = true; + } + } + + if (dst.heating.overheatProtection.highTemp < dst.heating.overheatProtection.lowTemp) { + dst.heating.overheatProtection.highTemp = dst.heating.overheatProtection.lowTemp; + changed = true; + } + + if (!src[FPSTR(S_HEATING)][FPSTR(S_FREEZE_PROTECTION)][FPSTR(S_LOW_TEMP)].isNull()) { + unsigned short value = src[FPSTR(S_HEATING)][FPSTR(S_FREEZE_PROTECTION)][FPSTR(S_LOW_TEMP)].as(); + + if (isValidTemp(value, dst.system.unitSystem, 1, 30) && value != dst.heating.freezeProtection.lowTemp) { + dst.heating.freezeProtection.lowTemp = value; + changed = true; + } + } + + if (!src[FPSTR(S_HEATING)][FPSTR(S_FREEZE_PROTECTION)][FPSTR(S_THRESHOLD_TIME)].isNull()) { + unsigned short value = src[FPSTR(S_HEATING)][FPSTR(S_FREEZE_PROTECTION)][FPSTR(S_THRESHOLD_TIME)].as(); + + if (value >= 30 && value <= 1800) { + if (value != dst.heating.freezeProtection.thresholdTime) { + dst.heating.freezeProtection.thresholdTime = value; + changed = true; + } + } + } + // dhw if (src[FPSTR(S_DHW)][FPSTR(S_ENABLED)].is()) { @@ -1385,6 +1450,29 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } + if (!src[FPSTR(S_DHW)][FPSTR(S_OVERHEAT_PROTECTION)][FPSTR(S_HIGH_TEMP)].isNull()) { + unsigned char value = src[FPSTR(S_DHW)][FPSTR(S_OVERHEAT_PROTECTION)][FPSTR(S_HIGH_TEMP)].as(); + + if (isValidTemp(value, dst.system.unitSystem, 0.0f, 100.0f) && value != dst.dhw.overheatProtection.highTemp) { + dst.dhw.overheatProtection.highTemp = value; + changed = true; + } + } + + if (!src[FPSTR(S_DHW)][FPSTR(S_OVERHEAT_PROTECTION)][FPSTR(S_LOW_TEMP)].isNull()) { + unsigned char value = src[FPSTR(S_DHW)][FPSTR(S_OVERHEAT_PROTECTION)][FPSTR(S_LOW_TEMP)].as(); + + if (isValidTemp(value, dst.system.unitSystem, 0.0f, 99.0f) && value != dst.dhw.overheatProtection.lowTemp) { + dst.dhw.overheatProtection.lowTemp = value; + changed = true; + } + } + + if (dst.dhw.overheatProtection.highTemp < dst.dhw.overheatProtection.lowTemp) { + dst.dhw.overheatProtection.highTemp = dst.dhw.overheatProtection.lowTemp; + changed = true; + } + if (!safe) { // external pump @@ -2016,6 +2104,7 @@ void varsToJson(const Variables& src, JsonVariant dst) { mHeating[FPSTR(S_ENABLED)] = src.master.heating.enabled; mHeating[FPSTR(S_BLOCKING)] = src.master.heating.blocking; mHeating[FPSTR(S_INDOOR_TEMP_CONTROL)] = src.master.heating.indoorTempControl; + mHeating[FPSTR(S_OVERHEAT)] = src.master.heating.overheat; mHeating[FPSTR(S_SETPOINT_TEMP)] = roundf(src.master.heating.setpointTemp, 2); mHeating[FPSTR(S_TARGET_TEMP)] = roundf(src.master.heating.targetTemp, 2); mHeating[FPSTR(S_CURRENT_TEMP)] = roundf(src.master.heating.currentTemp, 2); @@ -2027,6 +2116,7 @@ void varsToJson(const Variables& src, JsonVariant dst) { auto mDhw = master[FPSTR(S_DHW)].to(); mDhw[FPSTR(S_ENABLED)] = src.master.dhw.enabled; + mDhw[FPSTR(S_OVERHEAT)] = src.master.dhw.overheat; mDhw[FPSTR(S_TARGET_TEMP)] = roundf(src.master.dhw.targetTemp, 2); mDhw[FPSTR(S_CURRENT_TEMP)] = roundf(src.master.dhw.currentTemp, 2); mDhw[FPSTR(S_RETURN_TEMP)] = roundf(src.master.dhw.returnTemp, 2); diff --git a/src_data/locales/en.json b/src_data/locales/en.json index 31fb86c..5bd698c 100644 --- a/src_data/locales/en.json +++ b/src_data/locales/en.json @@ -117,6 +117,7 @@ "mHeatEnabled": "Heating enabled", "mHeatBlocking": "Heating blocked", + "mHeatOverheat": "Heating overheat", "sHeatActive": "Heating active", "mHeatSetpointTemp": "Heating setpoint temp", "mHeatTargetTemp": "Heating target temp", @@ -126,6 +127,7 @@ "mHeatOutdoorTemp": "Heating, outdoor temp", "mDhwEnabled": "DHW enabled", + "mDhwOverheat": "DHW overheat", "sDhwActive": "DHW active", "mDhwTargetTemp": "DHW target temp", "mDhwCurrTemp": "DHW current temp", @@ -302,6 +304,24 @@ "max": "Maximum temperature" }, "maxModulation": "Max modulation level", + "ohProtection": { + "title": "Overheating protection", + "desc": "Note: This feature can be useful if the built-in boiler overheating protection does not work or does not work correctly and the heat carrier boils. To disable, set 0 as high and low temperature.", + "highTemp": { + "title": "High temperature threshold", + "note": "Threshold at which the burner will be forcibly switched off" + }, + "lowTemp": { + "title": "Low temperature threshold", + "note": "Threshold at which the burner can be turned on again" + } + }, + "freezeProtection": { + "title": "Freeze protection", + "desc": "Heating will be forced to turn on if the heat carrier or indoor temperature drops below Low temperature during Waiting time.", + "lowTemp": "Low temperature threshold", + "thresholdTime": "Waiting time (sec)" + }, "portal": { "login": "Login", @@ -410,7 +430,8 @@ }, "options": { - "desc": "Options", + "title": "Options (additional settings)", + "desc": "Options can change the logic of the boiler. Not all options are documented in the protocol, so the same option can have different effects on different boilers.
Note: There is no need to change anything if everything works well.", "dhwSupport": "DHW support", "coolingSupport": "Cooling support", "summerWinterMode": "Summer/winter mode", @@ -419,6 +440,7 @@ "heatingToCh2": "Duplicate heating to CH2", "dhwToCh2": "Duplicate DHW to CH2", "dhwBlocking": "DHW blocking", + "dhwStateAsDhwBlocking": "DHW state as DHW blocking", "maxTempSyncWithTargetTemp": "Sync max heating temp with target temp", "getMinMaxTemp": "Get min/max temp from boiler", "ignoreDiagState": "Ignore diag state", diff --git a/src_data/locales/it.json b/src_data/locales/it.json index 3a34983..1619849 100644 --- a/src_data/locales/it.json +++ b/src_data/locales/it.json @@ -117,6 +117,7 @@ "mHeatEnabled": "Riscaldamento attivato", "mHeatBlocking": "Riscaldamento bloccato", + "mHeatOverheat": "Riscaldamento surriscaldamento", "sHeatActive": "Riscaldamento attivo", "mHeatSetpointTemp": "Temp riscaldamento impostato", "mHeatTargetTemp": "Target Temp caldaia", @@ -126,6 +127,7 @@ "mHeatOutdoorTemp": "Riscaldamento, temp esterna", "mDhwEnabled": "ACS attivata", + "mDhwOverheat": "ACS surriscaldamento", "sDhwActive": "ACS attiva", "mDhwTargetTemp": "ACS temp impostata", "mDhwCurrTemp": "ACS temp attuale", @@ -302,6 +304,24 @@ "max": "Temperatura massima" }, "maxModulation": "Max livello modulazione", + "ohProtection": { + "title": "Protezione contro il surriscaldamento", + "desc": "Nota: questa funzione può essere utile se la protezione contro il surriscaldamento integrata nella caldaia non funziona o non funziona correttamente e il fluido termovettore bolle. Per disattivarla, impostare 0 come temperatura alta e bassa.", + "highTemp": { + "title": "Soglia di temperatura alta", + "note": "Soglia alla quale il bruciatore verrà spento forzatamente" + }, + "lowTemp": { + "title": "Soglia di temperatura bassa", + "note": "Soglia alla quale il bruciatore può essere riacceso" + } + }, + "freezeProtection": { + "title": "Protezione antigelo", + "desc": "Il riscaldamento verrà attivato forzatamente se la temperatura del vettore di calore o interna scende al di sotto della temperatura minima durante il tempo di attesa.", + "lowTemp": "Soglia di temperatura minima", + "thresholdTime": "Tempo di attesa (sec)" + }, "portal": { "login": "Login", @@ -410,7 +430,8 @@ }, "options": { - "desc": "Opzioni", + "title": "Opzioni (impostazioni aggiuntive)", + "desc": "Le opzioni possono modificare la logica della caldaia. Non tutte le opzioni sono documentate nel protocollo, quindi la stessa opzione può avere effetti diversi su caldaie diverse.
Nota: Non è necessario modificare nulla se tutto funziona correttamente.", "dhwSupport": "Supporto ACS", "coolingSupport": "Supporto rafferscamento", "summerWinterMode": "Modalità Estate/inverno", @@ -419,6 +440,7 @@ "heatingToCh2": "Riproduci riscaldamento su CH2", "dhwToCh2": "Riproduci ACS su CH2", "dhwBlocking": "Bloccare ACS", + "dhwStateAsDhwBlocking": "Stato ACS come bloccare ACS", "maxTempSyncWithTargetTemp": "Sincronizza la temperatura massima di riscaldamento con la temperatura target", "getMinMaxTemp": "Prendi temp min/max dalla caldaia", "ignoreDiagState": "Ignora lo stato diagnostico", diff --git a/src_data/locales/ru.json b/src_data/locales/ru.json index d41f1bd..468a9de 100644 --- a/src_data/locales/ru.json +++ b/src_data/locales/ru.json @@ -117,6 +117,7 @@ "mHeatEnabled": "Отопление", "mHeatBlocking": "Блокировка отопления", + "mHeatOverheat": "Отопление, перегрев", "sHeatActive": "Активность отопления", "mHeatSetpointTemp": "Отопление, уставка", "mHeatTargetTemp": "Отопление, целевая температура", @@ -126,6 +127,7 @@ "mHeatOutdoorTemp": "Отопление, наружная темп.", "mDhwEnabled": "ГВС", + "mDhwOverheat": "ГВС, перегрев", "sDhwActive": "Активность ГВС", "mDhwTargetTemp": "ГВС, целевая температура", "mDhwCurrTemp": "ГВС, текущая температура", @@ -302,6 +304,24 @@ "max": "Макс. температура" }, "maxModulation": "Макс. уровень модуляции", + "ohProtection": { + "title": "Защита от перегрева", + "desc": "Примечание: Эта функция может быть полезна, если встроенная защита от перегрева котла не срабатывает или срабатывает некорректно и теплоноситель закипает. Для отключения установите 0 в качестве верхнего и нижнего порога температуры.", + "highTemp": { + "title": "Верхний порог температуры", + "note": "Порог, при котором горелка будет принудительно отключена" + }, + "lowTemp": { + "title": "Нижний порог температуры", + "note": "Порог, при котором горелка может быть включена снова" + } + }, + "freezeProtection": { + "title": "Защита от замерзания", + "desc": "Отопление будет принудительно включено, если темп. теплоносителя или внутренняя темп. опустится ниже нижнего порога в течение времени ожидания.", + "lowTemp": "Нижний порог температуры", + "thresholdTime": "Время ожидания (сек)" + }, "portal": { "login": "Логин", @@ -410,7 +430,8 @@ }, "options": { - "desc": "Опции", + "title": "Опции (дополнительные настройки)", + "desc": "Опции могут менять логику работы котла. Не все опции задокументированы в протоколе, поэтому одна и та же опция может иметь разный эффект на разных котлах.
Примечание: Нет необходимости что-то менять, если всё работает хорошо.", "dhwSupport": "Поддержка ГВС", "coolingSupport": "Поддержка охлаждения", "summerWinterMode": "Летний/зимний режим", @@ -419,6 +440,7 @@ "heatingToCh2": "Дублировать параметры отопления в канал 2", "dhwToCh2": "Дублировать параметры ГВС в канал 2", "dhwBlocking": "DHW blocking", + "dhwStateAsDhwBlocking": "DHW blocking в качестве состояния ГВС", "maxTempSyncWithTargetTemp": "Синхронизировать макс. темп. отопления с целевой темп.", "getMinMaxTemp": "Получать мин. и макс. температуру от котла", "ignoreDiagState": "Игнорировать состояние диагностики", diff --git a/src_data/pages/dashboard.html b/src_data/pages/dashboard.html index 55bf986..0c83cc4 100644 --- a/src_data/pages/dashboard.html +++ b/src_data/pages/dashboard.html @@ -184,6 +184,10 @@ dashboard.states.mHeatBlocking + + dashboard.states.mHeatOverheat + + dashboard.states.sHeatActive @@ -218,6 +222,10 @@ dashboard.states.mDhwEnabled + + dashboard.states.mDhwOverheat + + dashboard.states.sDhwActive @@ -611,6 +619,11 @@ result.master.heating.blocking ? "red" : "green" ); setState('.mHeatIndoorTempControl', result.master.heating.indoorTempControl); + setStatus( + '.mHeatOverheat', + result.master.heating.overheat ? "success" : "error", + result.master.heating.overheat ? "red" : "green" + ); setValue('.mHeatSetpointTemp', result.master.heating.setpointTemp); setValue('.mHeatTargetTemp', result.master.heating.targetTemp); setValue('.mHeatCurrTemp', result.master.heating.currentTemp); @@ -621,6 +634,11 @@ setValue('.mHeatMaxTemp', result.master.heating.maxTemp); setState('.mDhwEnabled', result.master.dhw.enabled); + setStatus( + '.mDhwOverheat', + result.master.dhw.overheat ? "success" : "error", + result.master.dhw.overheat ? "red" : "green" + ); setValue('.mDhwTargetTemp', result.master.dhw.targetTemp); setValue('.mDhwCurrTemp', result.master.dhw.currentTemp); setValue('.mDhwRetTemp', result.master.dhw.returnTemp); diff --git a/src_data/pages/settings.html b/src_data/pages/settings.html index a1d058a..6617de3 100644 --- a/src_data/pages/settings.html +++ b/src_data/pages/settings.html @@ -207,6 +207,50 @@ +
+ +
+ settings.ohProtection.title + +
+ + + +
+ + settings.ohProtection.desc +
+ +
+ +
+ settings.freezeProtection.title + +
+ + + +
+ + settings.freezeProtection.desc +
+ +
+ @@ -236,6 +280,30 @@ +
+ +
+ settings.ohProtection.title + +
+ + + +
+ + settings.ohProtection.desc +
+ +
+ @@ -506,92 +574,107 @@ -
- settings.ot.options.desc +
+ settings.ot.options.title - +
+
+ settings.ot.options.desc +
- +
+ - + - + - + - + - + - + - + - + - + - + - + - + - + -
- -
+ + + +
+ + +
+ + + +
@@ -999,6 +1082,7 @@ setCheckboxValue("[name='opentherm[options][heatingToCh2]']", data.opentherm.options.heatingToCh2); setCheckboxValue("[name='opentherm[options][dhwToCh2]']", data.opentherm.options.dhwToCh2); setCheckboxValue("[name='opentherm[options][dhwBlocking]']", data.opentherm.options.dhwBlocking); + setCheckboxValue("[name='opentherm[options][dhwStateAsDhwBlocking]']", data.opentherm.options.dhwStateAsDhwBlocking); setCheckboxValue("[name='opentherm[options][maxTempSyncWithTargetTemp]']", data.opentherm.options.maxTempSyncWithTargetTemp); setCheckboxValue("[name='opentherm[options][getMinMaxTemp]']", data.opentherm.options.getMinMaxTemp); setCheckboxValue("[name='opentherm[options][ignoreDiagState]']", data.opentherm.options.ignoreDiagState); @@ -1055,6 +1139,19 @@ setInputValue("[name='heating[hysteresis]']", data.heating.hysteresis); setInputValue("[name='heating[turboFactor]']", data.heating.turboFactor); setInputValue("[name='heating[maxModulation]']", data.heating.maxModulation); + setInputValue("[name='heating[overheatProtection][highTemp]']", data.heating.overheatProtection.highTemp, { + "min": 0, + "max": data.system.unitSystem == 0 ? 100 : 212 + }); + setInputValue("[name='heating[overheatProtection][lowTemp]']", data.heating.overheatProtection.lowTemp, { + "min": 0, + "max": data.system.unitSystem == 0 ? 99 : 211 + }); + setInputValue("[name='heating[freezeProtection][lowTemp]']", data.heating.freezeProtection.lowTemp, { + "min": data.system.unitSystem == 0 ? 1 : 34, + "max": data.system.unitSystem == 0 ? 30 : 86 + }); + setInputValue("[name='heating[freezeProtection][thresholdTime]']", data.heating.freezeProtection.thresholdTime); setBusy('#heating-settings-busy', '#heating-settings', false); // DHW @@ -1067,14 +1164,21 @@ "max": data.system.unitSystem == 0 ? 100 : 212 }); setInputValue("[name='dhw[maxModulation]']", data.dhw.maxModulation); + setInputValue("[name='dhw[overheatProtection][highTemp]']", data.dhw.overheatProtection.highTemp, { + "min": 0, + "max": data.system.unitSystem == 0 ? 100 : 212 + }); + setInputValue("[name='dhw[overheatProtection][lowTemp]']", data.dhw.overheatProtection.lowTemp, { + "min": 0, + "max": data.system.unitSystem == 0 ? 99 : 211 + }); setBusy('#dhw-settings-busy', '#dhw-settings', false); // Emergency mode - setInputValue("[name='emergency[tresholdTime]']", data.emergency.tresholdTime); if (data.opentherm.options.nativeHeatingControl) { setInputValue("[name='emergency[target]']", data.emergency.target, { "min": data.system.unitSystem == 0 ? 5 : 41, - "max": data.system.unitSystem == 0 ? 40 : 86 + "max": data.system.unitSystem == 0 ? 40 : 104 }); } else { @@ -1083,7 +1187,7 @@ "max": data.heating.maxTemp, }); } - + setInputValue("[name='emergency[tresholdTime]']", data.emergency.tresholdTime); setBusy('#emergency-settings-busy', '#emergency-settings', false); // Equitherm diff --git a/src_data/styles/app.css b/src_data/styles/app.css index 7a5e002..8e7ffe5 100644 --- a/src_data/styles/app.css +++ b/src_data/styles/app.css @@ -3,6 +3,10 @@ --pico-block-spacing-vertical: calc(var(--pico-spacing) * 0.75); --pico-block-spacing-horizontal: calc(var(--pico-spacing) * 0.75); } + + .logo { + font-size: 1.2rem; + } } @media (min-width: 768px) { @@ -10,6 +14,10 @@ --pico-block-spacing-vertical: var(--pico-spacing); --pico-block-spacing-horizontal: var(--pico-spacing); } + + .logo { + font-size: 1.25rem; + } } @media (min-width: 1024px) { @@ -17,6 +25,10 @@ --pico-block-spacing-vertical: calc(var(--pico-spacing) * 1.25); --pico-block-spacing-horizontal: calc(var(--pico-spacing) * 1.25); } + + .logo { + font-size: 1.25rem; + } } @media (min-width: 1280px) { @@ -25,6 +37,10 @@ --pico-block-spacing-horizontal: calc(var(--pico-spacing) * 1.5); } + .logo { + font-size: 1.3rem; + } + .container { max-width: 1000px; } @@ -36,6 +52,10 @@ --pico-block-spacing-horizontal: calc(var(--pico-spacing) * 1.75); } + .logo { + font-size: 1.3rem; + } + .container { max-width: 1000px; } @@ -111,7 +131,7 @@ tr.network:hover { border-radius: var(--pico-border-radius); color: var(--pico-code-kbd-color); font-weight: bolder; - font-size: 1.3rem; + /*font-size: 1.3rem;*/ font-family: var(--pico-font-family-monospace); }