diff --git a/assets/adapter_schematic_o.png b/assets/adapter_schematic_o.png new file mode 100644 index 0000000..b1f5f04 Binary files /dev/null and b/assets/adapter_schematic_o.png differ diff --git a/assets/connection.png b/assets/connection.png new file mode 100644 index 0000000..0c3b2be Binary files /dev/null and b/assets/connection.png differ diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 0000000..da8e14c --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,2 @@ +OTGATEWAY BOILER CONTROL GATEWAY + diff --git a/build/otgateway.ino.bin b/build/otgateway.ino.bin index b7bd512..b3f2431 100644 Binary files a/build/otgateway.ino.bin and b/build/otgateway.ino.bin differ diff --git a/src/HomeAssistantHelper.h b/src/HomeAssistantHelper.h index 4b4ac93..c00a723 100644 --- a/src/HomeAssistantHelper.h +++ b/src/HomeAssistantHelper.h @@ -447,7 +447,7 @@ public: doc["command_topic"] = _prefix + "/settings/set"; doc["command_template"] = "{\"pid\": {\"p_factor\" : {{ value }}}}"; doc["min"] = 0.001; - doc["max"] = 3; + doc["max"] = 10; doc["step"] = 0.001; client.beginPublish((F("homeassistant/number/") + _prefix + "/pid_p_factor/config").c_str(), measureJson(doc), true); @@ -478,7 +478,7 @@ public: doc["command_topic"] = _prefix + "/settings/set"; doc["command_template"] = "{\"pid\": {\"i_factor\" : {{ value }}}}"; doc["min"] = 0; - doc["max"] = 3; + doc["max"] = 10; doc["step"] = 0.001; client.beginPublish((F("homeassistant/number/") + _prefix + "/pid_i_factor/config").c_str(), measureJson(doc), true); @@ -509,7 +509,7 @@ public: doc["command_topic"] = _prefix + "/settings/set"; doc["command_template"] = "{\"pid\": {\"d_factor\" : {{ value }}}}"; doc["min"] = 0; - doc["max"] = 3; + doc["max"] = 10; doc["step"] = 0.001; client.beginPublish((F("homeassistant/number/") + _prefix + "/pid_d_factor/config").c_str(), measureJson(doc), true); @@ -617,6 +617,8 @@ public: bool publishNumberEquithermFactorT(bool enabledByDefault = true) { StaticJsonDocument<1536> doc; + doc["availability"]["topic"] = _prefix + F("/settings"); + doc["availability"]["value_template"] = F("{{ iif(value_json.pid.enable, 'offline', 'online') }}"); doc["device"]["identifiers"][0] = _prefix; doc["device"]["sw_version"] = _deviceVersion; doc["device"]["manufacturer"] = _deviceManufacturer; diff --git a/src/MqttTask.h b/src/MqttTask.h index 3627ce6..75f8936 100644 --- a/src/MqttTask.h +++ b/src/MqttTask.h @@ -218,6 +218,10 @@ protected: } if (!doc["restart"].isNull() && doc["restart"].is() && doc["restart"]) { + DEBUG("Received restart message..."); + Scheduler.delay(10000); + DEBUG("Restart..."); + eeSettings.updateNow(); ESP.restart(); } diff --git a/src/RegulatorTask.h b/src/RegulatorTask.h index 41d1fc4..d9ed965 100644 --- a/src/RegulatorTask.h +++ b/src/RegulatorTask.h @@ -13,6 +13,7 @@ public: protected: bool tunerInit = false; byte tunerState = 0; + byte tunerRegulator = 0; float prevHeatingTarget = 0; float prevEtResult = 0; float prevPidResult = 0; @@ -24,8 +25,19 @@ protected: if (vars.states.emergency) { newTemp = getEmergencyModeTemp(); + } else { - newTemp = getNormalModeTemp(); + if ( vars.tuning.enable || tunerInit ) { + newTemp = getTuningModeTemp(); + + if ( newTemp == 0 ) { + vars.tuning.enable = false; + } + } + + if ( !vars.tuning.enable ) { + newTemp = getNormalModeTemp(); + } } // Ограничиваем, если до этого не ограничило @@ -40,175 +52,194 @@ protected: byte getEmergencyModeTemp() { - byte newTemp = vars.parameters.heatingSetpoint; + float newTemp = 0; // if use equitherm if (settings.emergency.useEquitherm && settings.outdoorTempSource != 1) { - etRegulator.Kn = settings.equitherm.n_factor; - etRegulator.Kk = settings.equitherm.k_factor; - etRegulator.Kt = 0; - etRegulator.indoorTemp = 0; - etRegulator.outdoorTemp = vars.temperatures.outdoor; + float etResult = getEquithermTemp(); - etRegulator.setLimits(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp); - etRegulator.targetTemp = settings.emergency.target; - - float etResult = etRegulator.getResult(); - if (fabs(prevEtResult - etResult) + 0.0001 >= 1) { + if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) { prevEtResult = etResult; - newTemp = round(etResult); + newTemp += etResult; - INFO_F("New emergency equitherm result: %u (%f) \n", newTemp, etResult); + INFO_F("[REGULATOR][EQUITHERM] New emergency result: %u (%f) \n", (byte) round(etResult), etResult); + + } else { + newTemp += prevEtResult; } } else { // default temp, manual mode - newTemp = round(settings.emergency.target); + newTemp = settings.emergency.target; } - return newTemp; + return round(newTemp); } byte getNormalModeTemp() { - bool updateIntegral = false; - byte newTemp = vars.parameters.heatingSetpoint; + float newTemp = 0; if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001) { prevHeatingTarget = settings.heating.target; - updateIntegral = true; - INFO_F("New heating target: %f \n", settings.heating.target); + INFO_F("[REGULATOR] New target: %f \n", settings.heating.target); } // if use equitherm if (settings.equitherm.enable) { - if (vars.tuning.enable && vars.tuning.regulator == 0) { - if (settings.pid.enable) { - settings.pid.enable = false; - } + float etResult = getEquithermTemp(); - etRegulator.Kn = tuneEquithermN(etRegulator.Kn, vars.temperatures.indoor, settings.heating.target, 300, 1800, 0.01, 1); - } else { - etRegulator.Kn = settings.equitherm.n_factor; - } - - if (settings.pid.enable) { - etRegulator.Kt = 0; - etRegulator.indoorTemp = round(vars.temperatures.indoor); - etRegulator.outdoorTemp = round(vars.temperatures.outdoor); - } else { - etRegulator.Kt = settings.equitherm.t_factor; - etRegulator.indoorTemp = vars.temperatures.indoor; - etRegulator.outdoorTemp = vars.temperatures.outdoor; - } - - etRegulator.setLimits(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp); - etRegulator.Kk = settings.equitherm.k_factor; - etRegulator.targetTemp = settings.heating.target; - - float etResult = etRegulator.getResult(); - if (fabs(prevEtResult - etResult) + 0.0001 >= 1) { + if (fabs(prevEtResult - etResult) + 0.0001 >= 0.5) { prevEtResult = etResult; - updateIntegral = true; - newTemp = round(etResult); + newTemp += etResult; - INFO_F("New equitherm result: %u (%f) \n", newTemp, etResult); + INFO_F("[REGULATOR][EQUITHERM] New result: %u (%f) \n", (byte) round(etResult), etResult); } else { - updateIntegral = false; + newTemp += prevEtResult; } } // if use pid - if (settings.pid.enable && tunerInit && (!vars.tuning.enable || vars.tuning.regulator != 1)) { - pidTuner.reset(); - tunerState = 0; - tunerInit = false; - INFO(F("Tuning stopped")); + if (settings.pid.enable) { + float pidResult = getPidTemp(); - } else if (settings.pid.enable && vars.tuning.enable && vars.tuning.regulator == 1) { - if (tunerInit && pidTuner.getState() == 3) { - INFO(F("Tuning finished")); - pidTuner.debugText(&INFO_STREAM); + if (fabs(prevPidResult - pidResult) + 0.0001 >= 0.5) { + prevPidResult = pidResult; + newTemp += pidResult; - if (pidTuner.getAccuracy() < 90) { - WARN(F("Tuning bad result, restart...")); - - } else { - settings.pid.p_factor = pidTuner.getPID_p(); - settings.pid.i_factor = pidTuner.getPID_i(); - settings.pid.d_factor = pidTuner.getPID_d(); - vars.tuning.enable = false; - } - - pidTuner.reset(); - tunerState = 0; - tunerInit = false; + INFO_F("[REGULATOR][PID] New result: %u (%f) \n", (byte) round(pidResult), pidResult); } else { - if (!tunerInit) { - INFO(F("Tuning start")); - - float step; - if (vars.temperatures.indoor - vars.temperatures.outdoor > 10) { - step = ceil(vars.parameters.heatingSetpoint / vars.temperatures.indoor * 2); - } else { - step = 5.0f; - } - - float startTemp = newTemp + step; - if (startTemp >= vars.parameters.heatingMaxTemp) { - startTemp = vars.parameters.heatingMaxTemp - 10; - } - - INFO_F("Tuning started. Start temp: %f, step: %f \n", startTemp, step); - pidTuner.setParameters(NORMAL, startTemp, step, 20 * 60 * 1000, 0.15, 60 * 1000, 10000); - tunerInit = true; - } - - pidTuner.setInput(vars.temperatures.indoor); - pidTuner.compute(); - - if (tunerState > 0 && pidTuner.getState() != tunerState) { - INFO(F("Tuning log:")); - pidTuner.debugText(&INFO_STREAM); - tunerState = pidTuner.getState(); - } - - newTemp = round(pidTuner.getOutput()); - } - } - - if (settings.pid.enable && (!vars.tuning.enable || vars.tuning.enable && vars.tuning.regulator != 1)) { - if (updateIntegral) { - pidRegulator.integral = settings.heating.target; - } - - pidRegulator.Kp = settings.pid.p_factor; - pidRegulator.Ki = settings.pid.i_factor; - pidRegulator.Kd = settings.pid.d_factor; - - pidRegulator.setLimits(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp); - pidRegulator.input = vars.temperatures.indoor; - pidRegulator.setpoint = settings.heating.target; - - float pidResult = pidRegulator.getResultTimer(); - if (abs(prevPidResult - pidResult) >= 0.5) { - prevPidResult = pidResult; - newTemp = round(pidResult); - - INFO_F("New PID result: %u (%f) \n", newTemp, pidResult); + newTemp += prevPidResult; } } // default temp, manual mode if (!settings.equitherm.enable && !settings.pid.enable) { - newTemp = round(settings.heating.target); + newTemp = settings.heating.target; } - return newTemp; + return round(newTemp); } + byte getTuningModeTemp() { + if ( tunerInit && (!vars.tuning.enable || vars.tuning.regulator != tunerRegulator) ) { + if ( tunerRegulator == 0 ) { + pidTuner.reset(); + } + + tunerInit = false; + tunerRegulator = 0; + tunerState = 0; + INFO(F("[REGULATOR][TUNING] Stopped")); + } + + if ( !vars.tuning.enable ) { + return 0; + } + + + if ( vars.tuning.regulator == 0 ) { + // @TODO дописать + INFO(F("[REGULATOR][TUNING][EQUITHERM] Not implemented")); + return 0; + + } else if ( vars.tuning.regulator == 1 ) { + // PID tuner + float defaultTemp = settings.equitherm.enable ? getEquithermTemp() : settings.heating.target; + + if (tunerInit && pidTuner.getState() == 3) { + INFO(F("[REGULATOR][TUNING][PID] Finished")); + pidTuner.debugText(&INFO_STREAM); + + pidTuner.reset(); + tunerInit = false; + tunerRegulator = 0; + tunerState = 0; + + if (pidTuner.getAccuracy() < 90) { + WARN(F("[REGULATOR][TUNING][PID] Bad result, try again...")); + + } else { + settings.pid.p_factor = pidTuner.getPID_p(); + settings.pid.i_factor = pidTuner.getPID_i(); + settings.pid.d_factor = pidTuner.getPID_d(); + + return 0; + } + } + + if (!tunerInit) { + INFO(F("[REGULATOR][TUNING][PID] Start...")); + + float step; + if (vars.temperatures.indoor - vars.temperatures.outdoor > 10) { + step = ceil(vars.parameters.heatingSetpoint / vars.temperatures.indoor * 2); + } else { + step = 5.0f; + } + + float startTemp = step; + INFO_F("[REGULATOR][TUNING][PID] Started. Start value: %f, step: %f \n", startTemp, step); + pidTuner.setParameters(NORMAL, startTemp, step, 20 * 60 * 1000, 0.15, 60 * 1000, 10000); + tunerInit = true; + tunerRegulator = 1; + } + + pidTuner.setInput(vars.temperatures.indoor); + pidTuner.compute(); + + if (tunerState > 0 && pidTuner.getState() != tunerState) { + INFO(F("[REGULATOR][TUNING][PID] Log:")); + pidTuner.debugText(&INFO_STREAM); + tunerState = pidTuner.getState(); + } + + return round(defaultTemp + pidTuner.getOutput()); + + } else { + return 0; + } + } + + float getEquithermTemp() { + if ( vars.states.emergency ) { + etRegulator.Kt = 0; + etRegulator.indoorTemp = 0; + etRegulator.outdoorTemp = vars.temperatures.outdoor; + + } else if (settings.pid.enable) { + etRegulator.Kt = 0; + etRegulator.indoorTemp = round(vars.temperatures.indoor); + etRegulator.outdoorTemp = round(vars.temperatures.outdoor); + + } else { + etRegulator.Kt = settings.equitherm.t_factor; + etRegulator.indoorTemp = vars.temperatures.indoor; + etRegulator.outdoorTemp = vars.temperatures.outdoor; + } + + etRegulator.setLimits(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp); + etRegulator.Kn = settings.equitherm.n_factor; + // etRegulator.Kn = tuneEquithermN(etRegulator.Kn, vars.temperatures.indoor, settings.heating.target, 300, 1800, 0.01, 1); + etRegulator.Kk = settings.equitherm.k_factor; + etRegulator.targetTemp = vars.states.emergency ? settings.emergency.target : settings.heating.target; + + return etRegulator.getResult(); + } + + float getPidTemp() { + pidRegulator.Kp = settings.pid.p_factor; + pidRegulator.Ki = settings.pid.i_factor; + pidRegulator.Kd = settings.pid.d_factor; + + pidRegulator.setLimits(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp); + pidRegulator.input = vars.temperatures.indoor; + pidRegulator.setpoint = settings.heating.target; + + return pidRegulator.getResultTimer(); + } float tuneEquithermN(float ratio, float currentTemp, float setTemp, unsigned int dirtyInterval = 60, unsigned int accurateInterval = 1800, float accurateStep = 0.01, float accurateStepAfter = 1) { static uint32_t _prevIteration = millis(); diff --git a/src/SensorsTask.h b/src/SensorsTask.h index 405e6e2..c6d5913 100644 --- a/src/SensorsTask.h +++ b/src/SensorsTask.h @@ -7,21 +7,39 @@ public: SensorsTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {} protected: + float filteredOutdoorTemp = 0; + bool emptyOutdoorTemp = true; void setup() {} void loop() { // DS18B20 sensor if (outdoorSensor.online()) { if (outdoorSensor.readTemp()) { - vars.temperatures.outdoor = outdoorSensor.getTemp(); + float rawTemp = outdoorSensor.getTemp(); + INFO_F("[SENSORS][DS18B20] Raw temp: %f \n", rawTemp); + + if ( emptyOutdoorTemp ) { + filteredOutdoorTemp = rawTemp; + emptyOutdoorTemp = false; + + } else { + filteredOutdoorTemp += (rawTemp - filteredOutdoorTemp) * OUTDOOR_SENSOR_FILTER_K; + } + + filteredOutdoorTemp = floor(filteredOutdoorTemp * 100) / 100; + + if ( fabs(vars.temperatures.outdoor - filteredOutdoorTemp) > 0.099 ) { + vars.temperatures.outdoor = filteredOutdoorTemp; + INFO_F("[SENSORS][DS18B20] New temp: %f \n", filteredOutdoorTemp); + } } else { - DEBUG("Invalid data from outdoor sensor (DS18B20)"); + ERROR("[SENSORS][DS18B20] Invalid data from sensor"); } outdoorSensor.requestTemp(); } else { - WARN("Failed to connect to outdoor sensor (DS18B20)"); + ERROR("[SENSORS][DS18B20] Failed to connect to sensor"); } } }; \ No newline at end of file diff --git a/src/WifiManagerTask.h b/src/WifiManagerTask.h index 7b493d2..d5ef20a 100644 --- a/src/WifiManagerTask.h +++ b/src/WifiManagerTask.h @@ -19,7 +19,7 @@ public: protected: void setup() { - WiFi.mode(WIFI_STA); + //WiFi.mode(WIFI_STA); wm.setDebugOutput(settings.debug); wmHostname = new WiFiManagerParameter("hostname", "Hostname", settings.hostname, 80); @@ -28,7 +28,6 @@ protected: wmMqttServer = new WiFiManagerParameter("mqtt_server", "MQTT server", settings.mqtt.server, 80); wm.addParameter(wmMqttServer); - //char mqttPort[6]; sprintf(buffer, "%d", settings.mqtt.port); wmMqttPort = new WiFiManagerParameter("mqtt_port", "MQTT port", buffer, 6); wm.addParameter(wmMqttPort); @@ -42,6 +41,9 @@ protected: wmMqttPrefix = new WiFiManagerParameter("mqtt_prefix", "MQTT prefix", settings.mqtt.prefix, 32); wm.addParameter(wmMqttPrefix); + //wm.setCleanConnect(true); + wm.setRestorePersistent(false); + wm.setHostname(settings.hostname); wm.setWiFiAutoReconnect(true); wm.setConfigPortalBlocking(false); @@ -49,16 +51,24 @@ protected: wm.setConfigPortalTimeout(300); wm.setDisableConfigPortal(false); - if (wm.autoConnect(AP_SSID)) { - INFO_F("Wifi connected. IP: %s, RSSI: %d\n", WiFi.localIP().toString().c_str(), WiFi.RSSI()); - wm.startWebPortal(); - - } else { - INFO(F("Failed to connect to WIFI, start the configuration portal...")); - } + wm.autoConnect(AP_SSID); } void loop() { + if (connected && WiFi.status() != WL_CONNECTED) { + connected = false; + INFO("[wifi] Disconnected"); + + } else if (!connected && WiFi.status() == WL_CONNECTED) { + connected = true; + + INFO_F("[wifi] Connected. IP address: %s, RSSI: %d\n", WiFi.localIP().toString().c_str(), WiFi.RSSI()); + } + + if (WiFi.status() == WL_CONNECTED && !wm.getWebPortalActive() && !wm.getConfigPortalActive()) { + wm.startWebPortal(); + } + wm.process(); } @@ -74,4 +84,6 @@ protected: eeSettings.updateNow(); INFO(F("Settings saved")); } + + bool connected = false; }; \ No newline at end of file diff --git a/src/defines.h b/src/defines.h index 5f3140f..5b20709 100644 --- a/src/defines.h +++ b/src/defines.h @@ -1,4 +1,4 @@ -#define OT_GATEWAY_VERSION "1.0.5" +#define OT_GATEWAY_VERSION "1.0.7" #define AP_SSID "OpenTherm Gateway" #define USE_TELNET @@ -11,7 +11,8 @@ #define OPENTHERM_OFFLINE_TRESHOLD 10 #define DS18B20_PIN 2 -#define DS18B20_INTERVAL 1000 +#define DS18B20_INTERVAL 5000 +#define OUTDOOR_SENSOR_FILTER_K 0.15 #define DS_CHECK_CRC true #define DS_CRC_USE_TABLE true