* feat: new portal & network manager

* refactor: migrate from PubSubClient to ArduinoMqttClient
* refactor: migrate from EEManager to FileData
* chore: bump ESP Telnet to 2.2
* chore: bump TinyLogger to 1.1.0
This commit is contained in:
Yurii
2024-01-12 18:29:55 +03:00
parent b36e4dca42
commit ab1e9c761f
34 changed files with 4683 additions and 1125 deletions

View File

@@ -1,9 +1,9 @@
#include <Blinker.h>
extern NetworkTask* tNetwork;
extern MqttTask* tMqtt;
extern SensorsTask* tSensors;
extern OpenThermTask* tOt;
extern EEManager eeSettings;
extern FileData fsSettings, fsNetworkSettings;
#if USE_TELNET
extern ESPTelnetStream TelnetStream;
#endif
@@ -29,7 +29,6 @@ protected:
bool blinkerInitialized = false;
unsigned long firstFailConnect = 0;
unsigned long lastHeapInfo = 0;
unsigned int heapSize = 0;
unsigned int minFreeHeapSize = 0;
unsigned int minMaxFreeHeapBlockSize = 0;
unsigned long restartSignalTime = 0;
@@ -37,6 +36,9 @@ protected:
unsigned long heatingDisabledTime = 0;
byte externalPumpStartReason;
unsigned long externalPumpStartTime = 0;
#if USE_TELNET
bool telnetStarted = false;
#endif
const char* getTaskName() {
return "Main";
@@ -61,29 +63,29 @@ protected:
digitalWrite(settings.externalPump.pin, false);
}
#if defined(ARDUINO_ARCH_ESP32)
this->heapSize = ESP.getHeapSize();
#elif defined(ARDUINO_ARCH_ESP8266)
this->heapSize = 81920;
#else
this->heapSize = 99999;
#endif
this->minFreeHeapSize = heapSize;
this->minMaxFreeHeapBlockSize = heapSize;
this->minFreeHeapSize = getTotalHeap();
this->minMaxFreeHeapBlockSize = getTotalHeap();
}
void loop() {
if (eeSettings.tick()) {
Log.sinfoln("MAIN", F("Settings updated (EEPROM)"));
if (fsSettings.tick() == FD_WRITE) {
Log.sinfoln(FPSTR(L_SETTINGS), F("Updated"));
}
if (fsNetworkSettings.tick() == FD_WRITE) {
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Updated"));
}
#if USE_TELNET
if (this->telnetStarted) {
TelnetStream.loop();
}
#endif
if (vars.actions.restart) {
Log.sinfoln("MAIN", F("Restart signal received. Restart after 10 sec."));
eeSettings.updateNow();
Log.sinfoln(FPSTR(L_MAIN), F("Restart signal received. Restart after 10 sec."));
fsSettings.updateNow();
fsNetworkSettings.updateNow();
this->restartSignalTime = millis();
vars.actions.restart = false;
}
@@ -92,7 +94,14 @@ protected:
tOt->enable();
}
if (WiFi.status() == WL_CONNECTED) {
if (tNetwork->isConnected()) {
#if USE_TELNET
if (!this->telnetStarted) {
TelnetStream.begin(23, false);
this->telnetStarted = true;
}
#endif
vars.sensors.rssi = WiFi.RSSI();
if (!tMqtt->isEnabled() && strlen(settings.mqtt.server) > 0) {
@@ -111,6 +120,13 @@ protected:
}
} else {
#if USE_TELNET
if (this->telnetStarted) {
TelnetStream.stop();
this->telnetStarted = false;
}
#endif
if (tMqtt->isEnabled()) {
tMqtt->disable();
}
@@ -122,7 +138,7 @@ protected:
if (millis() - this->firstFailConnect > EMERGENCY_TIME_TRESHOLD) {
vars.states.emergency = true;
Log.sinfoln("MAIN", F("Emergency mode enabled"));
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled"));
}
}
}
@@ -150,7 +166,7 @@ protected:
}
void heap() {
unsigned int freeHeapSize = ESP.getFreeHeap();
unsigned int freeHeapSize = getFreeHeap();
#if defined(ARDUINO_ARCH_ESP32)
unsigned int maxFreeBlockSize = ESP.getMaxAllocHeap();
#else
@@ -189,9 +205,9 @@ protected:
uint8_t heapFrag = 100 - maxFreeBlockSize * 100.0 / freeHeapSize;
if (millis() - this->lastHeapInfo > 20000 || minFreeHeapSizeDiff > 0 || minMaxFreeBlockSizeDiff > 0) {
Log.sverboseln(
"MAIN",
FPSTR(L_MAIN),
F("Free heap size: %u of %u bytes (min: %u, diff: %u), max free block: %u (min: %u, diff: %u, frag: %hhu%%)"),
freeHeapSize, this->heapSize, this->minFreeHeapSize, minFreeHeapSizeDiff, maxFreeBlockSize, this->minMaxFreeHeapBlockSize, minMaxFreeBlockSizeDiff, heapFrag
freeHeapSize, getTotalHeap(), this->minFreeHeapSize, minFreeHeapSizeDiff, maxFreeBlockSize, this->minMaxFreeHeapBlockSize, minMaxFreeBlockSizeDiff, heapFrag
);
this->lastHeapInfo = millis();
}
@@ -209,7 +225,7 @@ protected:
this->blinkerInitialized = true;
}
if (WiFi.status() != WL_CONNECTED) {
if (!tNetwork->isConnected()) {
errors[errCount++] = 2;
}
@@ -270,13 +286,13 @@ protected:
}
if (!settings.externalPump.use || settings.externalPump.pin == 0) {
if (vars.externalPump.enable) {
if (vars.states.externalPump) {
if (settings.externalPump.pin != 0) {
digitalWrite(settings.externalPump.pin, false);
}
vars.externalPump.enable = false;
vars.externalPump.lastEnableTime = millis();
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
Log.sinfoln("EXTPUMP", F("Disabled: use = off"));
}
@@ -284,29 +300,29 @@ protected:
return;
}
if (vars.externalPump.enable && !this->heatingEnabled) {
if (vars.states.externalPump && !this->heatingEnabled) {
if (this->externalPumpStartReason == MainTask::REASON_PUMP_START_HEATING && millis() - this->heatingDisabledTime > ((unsigned int) settings.externalPump.postCirculationTime * 1000)) {
digitalWrite(settings.externalPump.pin, false);
vars.externalPump.enable = false;
vars.externalPump.lastEnableTime = millis();
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
Log.sinfoln("EXTPUMP", F("Disabled: expired post circulation time"));
} else if (this->externalPumpStartReason == MainTask::REASON_PUMP_START_ANTISTUCK && millis() - this->externalPumpStartTime >= ((unsigned int) settings.externalPump.antiStuckTime * 1000)) {
digitalWrite(settings.externalPump.pin, false);
vars.externalPump.enable = false;
vars.externalPump.lastEnableTime = millis();
vars.states.externalPump = false;
vars.parameters.extPumpLastEnableTime = millis();
Log.sinfoln("EXTPUMP", F("Disabled: expired anti stuck time"));
}
} else if (vars.externalPump.enable && this->heatingEnabled && this->externalPumpStartReason == MainTask::REASON_PUMP_START_ANTISTUCK) {
} else if (vars.states.externalPump && this->heatingEnabled && this->externalPumpStartReason == MainTask::REASON_PUMP_START_ANTISTUCK) {
this->externalPumpStartReason = MainTask::REASON_PUMP_START_HEATING;
} else if (!vars.externalPump.enable && this->heatingEnabled) {
vars.externalPump.enable = true;
} else if (!vars.states.externalPump && this->heatingEnabled) {
vars.states.externalPump = true;
this->externalPumpStartTime = millis();
this->externalPumpStartReason = MainTask::REASON_PUMP_START_HEATING;
@@ -314,8 +330,8 @@ protected:
Log.sinfoln("EXTPUMP", F("Enabled: heating on"));
} else if (!vars.externalPump.enable && (vars.externalPump.lastEnableTime == 0 || millis() - vars.externalPump.lastEnableTime >= ((unsigned long) settings.externalPump.antiStuckInterval * 1000))) {
vars.externalPump.enable = true;
} else if (!vars.states.externalPump && (vars.parameters.extPumpLastEnableTime == 0 || millis() - vars.parameters.extPumpLastEnableTime >= ((unsigned long) settings.externalPump.antiStuckInterval * 1000))) {
vars.states.externalPump = true;
this->externalPumpStartTime = millis();
this->externalPumpStartReason = MainTask::REASON_PUMP_START_ANTISTUCK;

View File

@@ -1,16 +1,15 @@
#include <PubSubClient.h>
#include <MqttClient.h>
#include <MqttWiFiClient.h>
#include <MqttWriter.h>
#include "HaHelper.h"
extern EEManager eeSettings;
extern FileData fsSettings;
class MqttTask : public Task {
public:
MqttTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {
this->wifiClient = new MqttWiFiClient();
this->client = new PubSubClient();
this->client = new MqttClient(this->wifiClient);
this->writer = new MqttWriter(this->client, 256);
this->haHelper = new HaHelper();
}
@@ -22,7 +21,7 @@ public:
if (this->client != nullptr) {
if (this->client->connected()) {
this->client->disconnect();
this->client->stop();
}
delete this->client;
@@ -37,9 +36,27 @@ public:
}
}
void disable() {
this->client->stop();
this->wifiClient->stop();
Task::disable();
Log.sinfoln(FPSTR(L_MQTT), F("Disabled"));
}
void enable() {
Task::enable();
Log.sinfoln(FPSTR(L_MQTT), F("Enabled"));
}
bool isConnected() {
return this->connected;
}
protected:
MqttWiFiClient* wifiClient = nullptr;
PubSubClient* client = nullptr;
MqttClient* client = nullptr;
HaHelper* haHelper = nullptr;
MqttWriter* writer = nullptr;
unsigned short readyForSendTime = 15000;
@@ -68,7 +85,7 @@ protected:
}
void setup() {
Log.sinfoln("MQTT", F("Started"));
Log.sinfoln(FPSTR(L_MQTT), F("Started"));
// wificlient settings
#ifdef ARDUINO_ARCH_ESP8266
@@ -77,19 +94,27 @@ protected:
#endif
// client settings
this->client->setClient(*this->wifiClient);
this->client->setKeepAlive(15);
//this->client->setClient(*this->wifiClient);
this->client->setKeepAliveInterval(15000);
this->client->setTxPayloadSize(256);
#ifdef ARDUINO_ARCH_ESP8266
this->client->setSocketTimeout(1);
this->client->setBufferSize(768);
this->client->setConnectionTimeout(1000);
#else
this->client->setSocketTimeout(3);
this->client->setBufferSize(1536);
this->client->setConnectionTimeout(3000);
#endif
this->client->setCallback([this] (char* topic, uint8_t* payload, unsigned int length) {
this->onMessage(topic, payload, length);
this->client->onMessage([this] (void*, size_t length) {
String topic = this->client->messageTopic();
if (!length || length > 2048 || !topic.length()) {
return;
}
uint8_t payload[length];
for (size_t i = 0; i < length && this->client->available(); i++) {
payload[i] = this->client->read();
}
this->onMessage(topic.c_str(), payload, length);
});
// writer settings
@@ -100,13 +125,13 @@ protected:
#endif
this->writer->setEventPublishCallback([this] (const char* topic, size_t written, size_t length, bool result) {
Log.straceln("MQTT", F("%s publish %u of %u bytes to topic: %s"), result ? F("Successfully") : F("Failed"), written, length, topic);
Log.straceln(FPSTR(L_MQTT), F("%s publish %u of %u bytes to topic: %s"), result ? F("Successfully") : F("Failed"), written, length, topic);
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
this->client->loop();
//this->client->poll();
this->delay(250);
});
@@ -132,7 +157,7 @@ protected:
void loop() {
if (settings.mqtt.interval > 120) {
settings.mqtt.interval = 5;
eeSettings.update();
fsSettings.update();
}
if (!this->client->connected() && this->connected) {
@@ -141,10 +166,11 @@ protected:
}
if (this->wifiClient == nullptr || (!this->client->connected() && millis() - this->lastReconnectTime >= MQTT_RECONNECT_INTERVAL)) {
Log.sinfoln("MQTT", F("Connecting to %s:%u..."), settings.mqtt.server, settings.mqtt.port);
Log.sinfoln(FPSTR(L_MQTT), F("Connecting to %s:%u..."), settings.mqtt.server, settings.mqtt.port);
this->client->setServer(settings.mqtt.server, settings.mqtt.port);
this->client->connect(settings.hostname, settings.mqtt.user, settings.mqtt.password);
this->client->setId(networkSettings.hostname);
this->client->setUsernamePassword(settings.mqtt.user, settings.mqtt.password);
this->client->connect(settings.mqtt.server, settings.mqtt.port);
this->lastReconnectTime = millis();
}
@@ -158,7 +184,7 @@ protected:
if (settings.emergency.enable && !vars.states.emergency) {
if (millis() - this->disconnectedTime > EMERGENCY_TIME_TRESHOLD) {
vars.states.emergency = true;
Log.sinfoln("MQTT", F("Emergency mode enabled"));
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode enabled"));
}
}
@@ -168,7 +194,8 @@ protected:
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
this->client->loop();
this->client->poll();
// delay for publish data
if (!this->isReadyForSend()) {
@@ -213,11 +240,11 @@ protected:
this->connectedTime = millis();
this->newConnection = true;
unsigned long downtime = (millis() - this->disconnectedTime) / 1000;
Log.sinfoln("MQTT", F("Connected (downtime: %u s.)"), downtime);
Log.sinfoln(FPSTR(L_MQTT), F("Connected (downtime: %u s.)"), downtime);
if (vars.states.emergency) {
vars.states.emergency = false;
Log.sinfoln("MQTT", F("Emergency mode disabled"));
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode disabled"));
}
this->client->subscribe(this->haHelper->getDeviceTopic("settings/set").c_str());
@@ -228,16 +255,16 @@ protected:
this->disconnectedTime = millis();
unsigned long uptime = (millis() - this->connectedTime) / 1000;
Log.swarningln("MQTT", F("Disconnected (reason: %d uptime: %u s.)"), this->client->state(), uptime);
Log.swarningln(FPSTR(L_MQTT), F("Disconnected (reason: %d uptime: %u s.)"), this->client->connectError(), uptime);
}
void onMessage(char* topic, uint8_t* payload, unsigned int length) {
void onMessage(const char* topic, uint8_t* payload, size_t length) {
if (!length) {
return;
}
if (settings.debug) {
Log.strace("MQTT.MSG", F("Topic: %s\r\n> "), topic);
Log.strace(FPSTR(L_MQTT_MSG), F("Topic: %s\r\n> "), topic);
if (Log.lock()) {
for (size_t i = 0; i < length; i++) {
if (payload[i] == 0) {
@@ -258,8 +285,12 @@ protected:
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, payload, length);
if (dErr != DeserializationError::Ok || doc.isNull()) {
Log.swarningln("MQTT.MSG", F("Error on deserialization: %s"), dErr.f_str());
if (dErr != DeserializationError::Ok) {
Log.swarningln(FPSTR(L_MQTT_MSG), F("Error on deserialization: %s"), dErr.f_str());
return;
} else if (doc.isNull() || !doc.size()) {
Log.swarningln(FPSTR(L_MQTT_MSG), F("Not valid json"));
return;
}
@@ -275,254 +306,13 @@ protected:
bool updateSettings(JsonDocument& doc) {
bool flag = false;
if (!doc["debug"].isNull() && doc["debug"].is<bool>()) {
settings.debug = doc["debug"].as<bool>();
flag = true;
}
// emergency
if (!doc["emergency"]["enable"].isNull() && doc["emergency"]["enable"].is<bool>()) {
settings.emergency.enable = doc["emergency"]["enable"].as<bool>();
flag = true;
}
if (!doc["emergency"]["target"].isNull() && doc["emergency"]["target"].is<double>()) {
if (doc["emergency"]["target"].as<double>() > 0 && doc["emergency"]["target"].as<double>() < 100) {
settings.emergency.target = MqttTask::round(doc["emergency"]["target"].as<double>(), 2);
flag = true;
}
}
if (!doc["emergency"]["useEquitherm"].isNull() && doc["emergency"]["useEquitherm"].is<bool>()) {
if (settings.sensors.outdoor.type != 1) {
settings.emergency.useEquitherm = doc["emergency"]["useEquitherm"].as<bool>();
} else {
settings.emergency.useEquitherm = false;
}
if (settings.emergency.useEquitherm && settings.emergency.usePid) {
settings.emergency.usePid = false;
}
flag = true;
}
if (!doc["emergency"]["usePid"].isNull() && doc["emergency"]["usePid"].is<bool>()) {
if (settings.sensors.indoor.type != 1) {
settings.emergency.usePid = doc["emergency"]["usePid"].as<bool>();
} else {
settings.emergency.usePid = false;
}
if (settings.emergency.usePid && settings.emergency.useEquitherm) {
settings.emergency.useEquitherm = false;
}
flag = true;
}
// heating
if (!doc["heating"]["enable"].isNull() && doc["heating"]["enable"].is<bool>()) {
settings.heating.enable = doc["heating"]["enable"].as<bool>();
flag = true;
}
if (!doc["heating"]["turbo"].isNull() && doc["heating"]["turbo"].is<bool>()) {
settings.heating.turbo = doc["heating"]["turbo"].as<bool>();
flag = true;
}
if (!doc["heating"]["target"].isNull() && doc["heating"]["target"].is<double>()) {
if (doc["heating"]["target"].as<double>() > 0 && doc["heating"]["target"].as<double>() < 100) {
settings.heating.target = MqttTask::round(doc["heating"]["target"].as<double>(), 2);
flag = true;
}
}
if (!doc["heating"]["hysteresis"].isNull() && doc["heating"]["hysteresis"].is<double>()) {
if (doc["heating"]["hysteresis"].as<double>() >= 0 && doc["heating"]["hysteresis"].as<double>() <= 5) {
settings.heating.hysteresis = MqttTask::round(doc["heating"]["hysteresis"].as<double>(), 2);
flag = true;
}
}
if (!doc["heating"]["maxModulation"].isNull() && doc["heating"]["maxModulation"].is<unsigned char>()) {
if (doc["heating"]["maxModulation"].as<unsigned char>() > 0 && doc["heating"]["maxModulation"].as<unsigned char>() <= 100) {
settings.heating.maxModulation = doc["heating"]["maxModulation"].as<unsigned char>();
flag = true;
}
}
if (!doc["heating"]["maxTemp"].isNull() && doc["heating"]["maxTemp"].is<unsigned char>()) {
if (doc["heating"]["maxTemp"].as<unsigned char>() > 0 && doc["heating"]["maxTemp"].as<unsigned char>() <= 100) {
settings.heating.maxTemp = doc["heating"]["maxTemp"].as<unsigned char>();
flag = true;
}
}
if (!doc["heating"]["minTemp"].isNull() && doc["heating"]["minTemp"].is<unsigned char>()) {
if (doc["heating"]["minTemp"].as<unsigned char>() >= 0 && doc["heating"]["minTemp"].as<unsigned char>() < 100) {
settings.heating.minTemp = doc["heating"]["minTemp"].as<unsigned char>();
flag = true;
}
}
// dhw
if (!doc["dhw"]["enable"].isNull() && doc["dhw"]["enable"].is<bool>()) {
settings.dhw.enable = doc["dhw"]["enable"].as<bool>();
flag = true;
}
if (!doc["dhw"]["target"].isNull() && doc["dhw"]["target"].is<unsigned char>()) {
if (doc["dhw"]["target"].as<unsigned char>() >= 0 && doc["dhw"]["target"].as<unsigned char>() < 100) {
settings.dhw.target = doc["dhw"]["target"].as<unsigned char>();
flag = true;
}
}
if (!doc["dhw"]["maxTemp"].isNull() && doc["dhw"]["maxTemp"].is<unsigned char>()) {
if (doc["dhw"]["maxTemp"].as<unsigned char>() > 0 && doc["dhw"]["maxTemp"].as<unsigned char>() <= 100) {
settings.dhw.maxTemp = doc["dhw"]["maxTemp"].as<unsigned char>();
flag = true;
}
}
if (!doc["dhw"]["minTemp"].isNull() && doc["dhw"]["minTemp"].is<unsigned char>()) {
if (doc["dhw"]["minTemp"].as<unsigned char>() >= 0 && doc["dhw"]["minTemp"].as<unsigned char>() < 100) {
settings.dhw.minTemp = doc["dhw"]["minTemp"].as<unsigned char>();
flag = true;
}
}
// pid
if (!doc["pid"]["enable"].isNull() && doc["pid"]["enable"].is<bool>()) {
settings.pid.enable = doc["pid"]["enable"].as<bool>();
flag = true;
}
if (!doc["pid"]["p_factor"].isNull() && doc["pid"]["p_factor"].is<double>()) {
if (doc["pid"]["p_factor"].as<double>() > 0 && doc["pid"]["p_factor"].as<double>() <= 1000) {
settings.pid.p_factor = MqttTask::round(doc["pid"]["p_factor"].as<double>(), 3);
flag = true;
}
}
if (!doc["pid"]["i_factor"].isNull() && doc["pid"]["i_factor"].is<double>()) {
if (doc["pid"]["i_factor"].as<double>() >= 0 && doc["pid"]["i_factor"].as<double>() <= 100) {
settings.pid.i_factor = MqttTask::round(doc["pid"]["i_factor"].as<double>(), 3);
flag = true;
}
}
if (!doc["pid"]["d_factor"].isNull() && doc["pid"]["d_factor"].is<double>()) {
if (doc["pid"]["d_factor"].as<double>() >= 0 && doc["pid"]["d_factor"].as<double>() <= 100000) {
settings.pid.d_factor = MqttTask::round(doc["pid"]["d_factor"].as<double>(), 1);
flag = true;
}
}
if (!doc["pid"]["dt"].isNull() && doc["pid"]["dt"].is<double>()) {
if (doc["pid"]["dt"].as<unsigned short>() >= 30 && doc["pid"]["dt"].as<unsigned short>() <= 600) {
settings.pid.dt = doc["pid"]["dt"].as<unsigned short>();
flag = true;
}
}
if (!doc["pid"]["maxTemp"].isNull() && doc["pid"]["maxTemp"].is<unsigned char>()) {
if (doc["pid"]["maxTemp"].as<unsigned char>() > 0 && doc["pid"]["maxTemp"].as<unsigned char>() <= 100 && doc["pid"]["maxTemp"].as<unsigned char>() > settings.pid.minTemp) {
settings.pid.maxTemp = doc["pid"]["maxTemp"].as<unsigned char>();
flag = true;
}
}
if (!doc["pid"]["minTemp"].isNull() && doc["pid"]["minTemp"].is<unsigned char>()) {
if (doc["pid"]["minTemp"].as<unsigned char>() >= 0 && doc["pid"]["minTemp"].as<unsigned char>() < 100 && doc["pid"]["minTemp"].as<unsigned char>() < settings.pid.maxTemp) {
settings.pid.minTemp = doc["pid"]["minTemp"].as<unsigned char>();
flag = true;
}
}
// equitherm
if (!doc["equitherm"]["enable"].isNull() && doc["equitherm"]["enable"].is<bool>()) {
settings.equitherm.enable = doc["equitherm"]["enable"].as<bool>();
flag = true;
}
if (!doc["equitherm"]["n_factor"].isNull() && doc["equitherm"]["n_factor"].is<double>()) {
if (doc["equitherm"]["n_factor"].as<double>() > 0 && doc["equitherm"]["n_factor"].as<double>() <= 10) {
settings.equitherm.n_factor = MqttTask::round(doc["equitherm"]["n_factor"].as<double>(), 3);
flag = true;
}
}
if (!doc["equitherm"]["k_factor"].isNull() && doc["equitherm"]["k_factor"].is<double>()) {
if (doc["equitherm"]["k_factor"].as<double>() >= 0 && doc["equitherm"]["k_factor"].as<double>() <= 10) {
settings.equitherm.k_factor = MqttTask::round(doc["equitherm"]["k_factor"].as<double>(), 3);
flag = true;
}
}
if (!doc["equitherm"]["t_factor"].isNull() && doc["equitherm"]["t_factor"].is<double>()) {
if (doc["equitherm"]["t_factor"].as<double>() >= 0 && doc["equitherm"]["t_factor"].as<double>() <= 10) {
settings.equitherm.t_factor = MqttTask::round(doc["equitherm"]["t_factor"].as<double>(), 3);
flag = true;
}
}
// sensors
if (!doc["sensors"]["outdoor"]["type"].isNull() && doc["sensors"]["outdoor"]["type"].is<unsigned char>()) {
if (doc["sensors"]["outdoor"]["type"].as<unsigned char>() >= 0 && doc["sensors"]["outdoor"]["type"].as<unsigned char>() <= 2) {
settings.sensors.outdoor.type = doc["sensors"]["outdoor"]["type"].as<unsigned char>();
if (settings.sensors.outdoor.type == 1) {
settings.emergency.useEquitherm = false;
}
flag = true;
}
}
if (!doc["sensors"]["outdoor"]["offset"].isNull() && doc["sensors"]["outdoor"]["offset"].is<double>()) {
if (doc["sensors"]["outdoor"]["offset"].as<double>() >= -10 && doc["sensors"]["outdoor"]["offset"].as<double>() <= 10) {
settings.sensors.outdoor.offset = MqttTask::round(doc["sensors"]["outdoor"]["offset"].as<double>(), 2);
flag = true;
}
}
if (!doc["sensors"]["indoor"]["type"].isNull() && doc["sensors"]["indoor"]["type"].is<unsigned char>()) {
if (doc["sensors"]["indoor"]["type"].as<unsigned char>() >= 1 && doc["sensors"]["indoor"]["type"].as<unsigned char>() <= 3) {
settings.sensors.indoor.type = doc["sensors"]["indoor"]["type"].as<unsigned char>();
if (settings.sensors.indoor.type == 1) {
settings.emergency.usePid = false;
}
flag = true;
}
}
if (!doc["sensors"]["indoor"]["offset"].isNull() && doc["sensors"]["indoor"]["offset"].is<double>()) {
if (doc["sensors"]["indoor"]["offset"].as<double>() >= -10 && doc["sensors"]["indoor"]["offset"].as<double>() <= 10) {
settings.sensors.indoor.offset = MqttTask::round(doc["sensors"]["indoor"]["offset"].as<double>(), 2);
flag = true;
}
}
bool changed = safeJsonToSettings(doc, settings);
doc.clear();
doc.shrinkToFit();
if (flag) {
if (changed) {
this->prevPubSettingsTime = 0;
eeSettings.update();
fsSettings.update();
return true;
}
@@ -530,54 +320,11 @@ protected:
}
bool updateVariables(JsonDocument& doc) {
bool flag = false;
if (!doc["ping"].isNull() && doc["ping"]) {
flag = true;
}
if (!doc["tuning"]["enable"].isNull() && doc["tuning"]["enable"].is<bool>()) {
vars.tuning.enable = doc["tuning"]["enable"].as<bool>();
flag = true;
}
if (!doc["tuning"]["regulator"].isNull() && doc["tuning"]["regulator"].is<unsigned char>()) {
if (doc["tuning"]["regulator"].as<unsigned char>() >= 0 && doc["tuning"]["regulator"].as<unsigned char>() <= 1) {
vars.tuning.regulator = doc["tuning"]["regulator"].as<unsigned char>();
flag = true;
}
}
if (!doc["temperatures"]["indoor"].isNull() && doc["temperatures"]["indoor"].is<double>()) {
if (settings.sensors.indoor.type == 1 && doc["temperatures"]["indoor"].as<double>() > -100 && doc["temperatures"]["indoor"].as<double>() < 100) {
vars.temperatures.indoor = MqttTask::round(doc["temperatures"]["indoor"].as<double>(), 2);
flag = true;
}
}
if (!doc["temperatures"]["outdoor"].isNull() && doc["temperatures"]["outdoor"].is<double>()) {
if (settings.sensors.outdoor.type == 1 && doc["temperatures"]["outdoor"].as<double>() > -100 && doc["temperatures"]["outdoor"].as<double>() < 100) {
vars.temperatures.outdoor = MqttTask::round(doc["temperatures"]["outdoor"].as<double>(), 2);
flag = true;
}
}
if (!doc["actions"]["restart"].isNull() && doc["actions"]["restart"].is<bool>() && doc["actions"]["restart"].as<bool>()) {
vars.actions.restart = true;
}
if (!doc["actions"]["resetFault"].isNull() && doc["actions"]["resetFault"].is<bool>() && doc["actions"]["resetFault"].as<bool>()) {
vars.actions.resetFault = true;
}
if (!doc["actions"]["resetDiagnostic"].isNull() && doc["actions"]["resetDiagnostic"].is<bool>() && doc["actions"]["resetDiagnostic"].as<bool>()) {
vars.actions.resetDiagnostic = true;
}
bool changed = jsonToVars(doc, vars);
doc.clear();
doc.shrinkToFit();
if (flag) {
if (changed) {
this->prevPubVarsTime = 0;
return true;
}
@@ -766,97 +513,15 @@ protected:
bool publishSettings(const char* topic) {
JsonDocument doc;
doc["debug"] = settings.debug;
doc["emergency"]["enable"] = settings.emergency.enable;
doc["emergency"]["target"] = MqttTask::round(settings.emergency.target, 2);
doc["emergency"]["useEquitherm"] = settings.emergency.useEquitherm;
doc["emergency"]["usePid"] = settings.emergency.usePid;
doc["heating"]["enable"] = settings.heating.enable;
doc["heating"]["turbo"] = settings.heating.turbo;
doc["heating"]["target"] = MqttTask::round(settings.heating.target, 2);
doc["heating"]["hysteresis"] = MqttTask::round(settings.heating.hysteresis, 2);
doc["heating"]["minTemp"] = settings.heating.minTemp;
doc["heating"]["maxTemp"] = settings.heating.maxTemp;
doc["heating"]["maxModulation"] = settings.heating.maxModulation;
doc["dhw"]["enable"] = settings.dhw.enable;
doc["dhw"]["target"] = settings.dhw.target;
doc["dhw"]["minTemp"] = settings.dhw.minTemp;
doc["dhw"]["maxTemp"] = settings.dhw.maxTemp;
doc["pid"]["enable"] = settings.pid.enable;
doc["pid"]["p_factor"] = MqttTask::round(settings.pid.p_factor, 3);
doc["pid"]["i_factor"] = MqttTask::round(settings.pid.i_factor, 3);
doc["pid"]["d_factor"] = MqttTask::round(settings.pid.d_factor, 1);
doc["pid"]["dt"] = settings.pid.dt;
doc["pid"]["minTemp"] = settings.pid.minTemp;
doc["pid"]["maxTemp"] = settings.pid.maxTemp;
doc["equitherm"]["enable"] = settings.equitherm.enable;
doc["equitherm"]["n_factor"] = MqttTask::round(settings.equitherm.n_factor, 3);
doc["equitherm"]["k_factor"] = MqttTask::round(settings.equitherm.k_factor, 3);
doc["equitherm"]["t_factor"] = MqttTask::round(settings.equitherm.t_factor, 3);
doc["sensors"]["outdoor"]["type"] = settings.sensors.outdoor.type;
doc["sensors"]["outdoor"]["offset"] = MqttTask::round(settings.sensors.outdoor.offset, 2);
doc["sensors"]["indoor"]["type"] = settings.sensors.indoor.type;
doc["sensors"]["indoor"]["offset"] = MqttTask::round(settings.sensors.indoor.offset, 2);
doc.shrinkToFit();
safeSettingsToJson(settings, doc);
return this->writer->publish(topic, doc, true);
}
bool publishVariables(const char* topic) {
JsonDocument doc;
doc["tuning"]["enable"] = vars.tuning.enable;
doc["tuning"]["regulator"] = vars.tuning.regulator;
doc["states"]["otStatus"] = vars.states.otStatus;
doc["states"]["heating"] = vars.states.heating;
doc["states"]["dhw"] = vars.states.dhw;
doc["states"]["flame"] = vars.states.flame;
doc["states"]["fault"] = vars.states.fault;
doc["states"]["diagnostic"] = vars.states.diagnostic;
doc["sensors"]["modulation"] = MqttTask::round(vars.sensors.modulation, 2);
doc["sensors"]["pressure"] = MqttTask::round(vars.sensors.pressure, 2);
doc["sensors"]["dhwFlowRate"] = vars.sensors.dhwFlowRate;
doc["sensors"]["faultCode"] = vars.sensors.faultCode;
doc["sensors"]["rssi"] = vars.sensors.rssi;
doc["sensors"]["uptime"] = millis() / 1000ul;
doc["temperatures"]["indoor"] = MqttTask::round(vars.temperatures.indoor, 2);
doc["temperatures"]["outdoor"] = MqttTask::round(vars.temperatures.outdoor, 2);
doc["temperatures"]["heating"] = MqttTask::round(vars.temperatures.heating, 2);
doc["temperatures"]["dhw"] = MqttTask::round(vars.temperatures.dhw, 2);
doc["parameters"]["heatingEnabled"] = vars.parameters.heatingEnabled;
doc["parameters"]["heatingMinTemp"] = vars.parameters.heatingMinTemp;
doc["parameters"]["heatingMaxTemp"] = vars.parameters.heatingMaxTemp;
doc["parameters"]["heatingSetpoint"] = vars.parameters.heatingSetpoint;
doc["parameters"]["dhwMinTemp"] = vars.parameters.dhwMinTemp;
doc["parameters"]["dhwMaxTemp"] = vars.parameters.dhwMaxTemp;
doc.shrinkToFit();
varsToJson(vars, doc);
return this->writer->publish(topic, doc, true);
}
static double round(double value, uint8_t decimals = 2) {
if (decimals == 0) {
return (int)(value + 0.001);
} else if (abs(value) < 0.00000001) {
return 0.0;
}
double multiplier = pow10(decimals);
value += 0.5 / multiplier * (value < 0 ? -1 : 1);
return (int)(value * multiplier) / multiplier;
}
};

406
src/NetworkTask.h Normal file
View File

@@ -0,0 +1,406 @@
#if defined(ARDUINO_ARCH_ESP8266)
#include <ESP8266WiFi.h>
#include "lwip/etharp.h"
#elif defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#endif
#include <Connection.h>
class NetworkTask : public Task {
public:
NetworkTask(bool _enabled = false, unsigned long _interval = 0) : Task(_enabled, _interval) {
Connection::setup(this->useDhcp);
}
NetworkTask* setHostname(const char* value) {
this->hostname = value;
return this;
}
NetworkTask* setApCredentials(const char* ssid, const char* password = nullptr, byte channel = 0) {
this->apName = ssid;
this->apPassword = password;
this->apChannel = channel;
return this;
}
NetworkTask* setStaCredentials(const char* ssid = nullptr, const char* password = nullptr, byte channel = 0) {
this->staSsid = ssid;
this->staPassword = password;
this->staChannel = channel;
return this;
}
NetworkTask* setUseDhcp(bool value) {
this->useDhcp = value;
Connection::setup(this->useDhcp);
return this;
}
NetworkTask* setStaticConfig(const char* ip, const char* gateway, const char* subnet, const char* dns) {
this->staticIp.fromString(ip);
this->staticGateway.fromString(gateway);
this->staticSubnet.fromString(subnet);
this->staticDns.fromString(dns);
return this;
}
NetworkTask* setStaticConfig(IPAddress &ip, IPAddress &gateway, IPAddress &subnet, IPAddress &dns) {
this->staticIp = ip;
this->staticGateway = gateway;
this->staticSubnet = subnet;
this->staticDns = dns;
return this;
}
bool hasStaCredentials() {
return this->staSsid != nullptr;
}
bool isConnected() {
return this->isStaEnabled() && Connection::getStatus() == Connection::Status::CONNECTED;
}
bool isConnecting() {
return this->isStaEnabled() && Connection::getStatus() == Connection::Status::CONNECTING;
}
bool isStaEnabled() {
return (WiFi.getMode() & WIFI_STA) != 0;
}
bool isApEnabled() {
return (WiFi.getMode() & WIFI_AP) != 0;
}
bool hasApClients() {
if (!this->isApEnabled()) {
return false;
}
return WiFi.softAPgetStationNum() > 0;
}
short int getRssi() {
return WiFi.RSSI();
}
IPAddress getApIp() {
return WiFi.softAPIP();
}
IPAddress getStaIp() {
return WiFi.localIP();
}
IPAddress getStaSubnet() {
return WiFi.subnetMask();
}
IPAddress getStaGateway() {
return WiFi.gatewayIP();
}
IPAddress getStaDns() {
return WiFi.dnsIP();
}
String getStaMac() {
return WiFi.macAddress();
}
const char* getStaSsid() {
return this->staSsid;
}
const char* getStaPassword() {
return this->staPassword;
}
byte getStaChannel() {
return this->staChannel;
}
bool resetWifi() {
WiFi.persistent(false);
WiFi.setAutoConnect(false);
WiFi.setAutoReconnect(false);
#ifdef ARDUINO_ARCH_ESP8266
WiFi.setSleepMode(WIFI_NONE_SLEEP);
if (wifi_softap_dhcps_status() == DHCP_STARTED) {
wifi_softap_dhcps_stop();
}
#elif defined(ARDUINO_ARCH_ESP32)
WiFi.setSleep(WIFI_PS_NONE);
#endif
WiFi.softAPdisconnect();
#ifdef ARDUINO_ARCH_ESP8266
if (wifi_station_dhcpc_status() == DHCP_STARTED) {
wifi_station_dhcpc_stop();
}
#endif
WiFi.disconnect(false, true);
return WiFi.mode(WIFI_OFF);
}
void reconnect() {
this->reconnectFlag = true;
}
bool connect(bool force = false, unsigned int timeout = 1000u) {
if (this->isConnected() && !force) {
return true;
}
if (force && !this->isApEnabled()) {
this->resetWifi();
} else {
#ifdef ARDUINO_ARCH_ESP8266
if (wifi_station_dhcpc_status() == DHCP_STARTED) {
wifi_station_dhcpc_stop();
}
#endif
WiFi.disconnect(false, true);
}
if (!this->hasStaCredentials()) {
return false;
}
this->delay(200);
#ifdef ARDUINO_ARCH_ESP32
if (this->setWifiHostname(this->hostname)) {
Log.straceln(FPSTR(L_NETWORK), F("Set hostname '%s': success"), this->hostname);
} else {
Log.serrorln(FPSTR(L_NETWORK), F("Set hostname '%s': fail"), this->hostname);
}
#endif
if (!WiFi.mode((WiFiMode_t)(WiFi.getMode() | WIFI_STA))) {
return false;
}
this->delay(200);
#ifdef ARDUINO_ARCH_ESP8266
if (this->setWifiHostname(this->hostname)) {
Log.straceln(FPSTR(L_NETWORK), F("Set hostname '%s': success"), this->hostname);
} else {
Log.serrorln(FPSTR(L_NETWORK), F("Set hostname '%s': fail"), this->hostname);
}
this->delay(200);
#endif
if (!this->useDhcp) {
WiFi.config(this->staticIp, this->staticGateway, this->staticSubnet, this->staticDns);
}
WiFi.begin(this->staSsid, this->staPassword, this->staChannel);
unsigned long beginConnectionTime = millis();
while (millis() - beginConnectionTime < timeout) {
this->delay(100);
if (WiFi.status() == WL_CONNECTED) {
return true;
}
}
return false;
}
static byte rssiToSignalQuality(short int rssi) {
return constrain(map(rssi, -100, -50, 0, 100), 0, 100);
}
protected:
const unsigned int reconnectInterval = 5000;
const unsigned int failedConnectTimeout = 30000; // 120000
const unsigned int connectionTimeout = 15000;
const unsigned int resetConnectionTimeout = 60000;
const char* hostname = "esp";
const char* apName = "ESP";
const char* apPassword = nullptr;
byte apChannel = 1;
const char* staSsid = nullptr;
const char* staPassword = nullptr;
byte staChannel = 0;
bool useDhcp = true;
IPAddress staticIp;
IPAddress staticGateway;
IPAddress staticSubnet;
IPAddress staticDns;
bool connected = false;
bool reconnectFlag = false;
unsigned long prevArpGratuitous = 0;
unsigned long prevReconnectingTime = 0;
unsigned long connectedTime = 0;
unsigned long disconnectedTime = 0;
const char* getTaskName() {
return "Wifi";
}
/*int getTaskCore() {
return 1;
}*/
int getTaskPriority() {
return 0;
}
void setup() {
this->resetWifi();
}
void loop() {
if (this->isConnected() && !this->hasStaCredentials()) {
Log.sinfoln(FPSTR(L_NETWORK), F("Reset"));
this->resetWifi();
} else if (this->isConnected() && !this->reconnectFlag) {
if (!this->connected) {
this->connectedTime = millis();
this->connected = true;
Log.sinfoln(
FPSTR(L_NETWORK),
F("Connected, downtime: %lu s., IP: %s, RSSI: %hhd"),
(millis() - this->disconnectedTime) / 1000,
WiFi.localIP().toString().c_str(),
WiFi.RSSI()
);
}
if (this->isApEnabled() && millis() - this->connectedTime > this->reconnectInterval && !this->hasApClients()) {
Log.sinfoln(FPSTR(L_NETWORK), F("Stop AP because connected, start only STA"));
WiFi.mode(WIFI_STA);
return;
}
#ifdef ARDUINO_ARCH_ESP8266
if (millis() - this->prevArpGratuitous > 60000) {
this->stationKeepAliveNow();
this->prevArpGratuitous = millis();
}
#endif
} else {
if (this->connected) {
this->disconnectedTime = millis();
this->connected = false;
Log.sinfoln(
FPSTR(L_NETWORK),
F("Disconnected, reason: %d, uptime: %lu s."),
Connection::getDisconnectReason(),
(millis() - this->connectedTime) / 1000
);
}
if (!this->hasStaCredentials() && !this->isApEnabled()) {
Log.sinfoln(FPSTR(L_NETWORK), F("No STA credentials, start AP"));
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(this->apName, this->apPassword, this->apChannel);
} else if (!this->isApEnabled() && millis() - this->disconnectedTime > this->failedConnectTimeout) {
Log.sinfoln(FPSTR(L_NETWORK), F("Disconnected for a long time, start AP"));
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(this->apName, this->apPassword, this->apChannel);
} else if (this->isConnecting() && millis() - this->prevReconnectingTime > this->resetConnectionTimeout) {
Log.swarningln(FPSTR(L_NETWORK), F("Connection timeout, reset wifi..."));
this->resetWifi();
} else if (!this->isConnecting() && (!this->prevReconnectingTime || millis() - this->prevReconnectingTime > this->reconnectInterval)) {
if (this->hasStaCredentials()) {
Log.sinfoln(FPSTR(L_NETWORK), F("Try connect..."));
this->prevReconnectingTime = millis();
this->connect(true, this->connectionTimeout);
this->reconnectFlag = false;
}
}
}
}
bool setWifiHostname(const char* hostname) {
if (!this->isHostnameValid(hostname)) {
return false;
}
if (strcmp(WiFi.getHostname(), hostname) == 0) {
return true;
}
return WiFi.setHostname(hostname);
}
#ifdef ARDUINO_ARCH_ESP8266
/**
* @brief
* https://github.com/arendst/Tasmota/blob/e6515883f0ee5451931b6280ff847b117de5a231/tasmota/tasmota_support/support_wifi.ino#L1196
*/
static void stationKeepAliveNow(void) {
for (netif* interface = netif_list; interface != nullptr; interface = interface->next) {
if (
(interface->flags & NETIF_FLAG_LINK_UP)
&& (interface->flags & NETIF_FLAG_UP)
&& interface->num == STATION_IF
&& (!ip4_addr_isany_val(*netif_ip4_addr(interface)))
) {
etharp_gratuitous(interface);
::optimistic_yield(1000);
break;
}
}
}
#endif
/**
* @brief check RFC compliance
*
* @param value
* @return true
* @return false
*/
static bool isHostnameValid(const char* value) {
size_t len = strlen(value);
if (len > 24) {
return false;
} else if (value[len - 1] == '-') {
return false;
}
for (size_t i = 0; i < len; i++) {
if (!isalnum(value[i]) && value[i] != '-') {
return false;
}
}
return true;
}
};

View File

@@ -1,13 +1,7 @@
#include <new>
#include <CustomOpenTherm.h>
CustomOpenTherm* ot;
extern EEManager eeSettings;
const char S_OT[] PROGMEM = "OT";
const char S_OT_DHW[] PROGMEM = "OT.DHW";
const char S_OT_HEATING[] PROGMEM = "OT.HEATING";
extern FileData fsSettings;
class OpenThermTask : public Task {
public:
@@ -46,7 +40,7 @@ protected:
}
void setup() {
Log.sinfoln(FPSTR(S_OT), F("Started. GPIO IN: %hhu, GPIO OUT: %hhu"), settings.opentherm.inPin, settings.opentherm.outPin);
Log.sinfoln(FPSTR(L_OT), F("Started. GPIO IN: %hhu, GPIO OUT: %hhu"), settings.opentherm.inPin, settings.opentherm.outPin);
ot->setHandleSendRequestCallback(OpenThermTask::sendRequestCallback);
ot->setYieldCallback([](void* self) {
@@ -66,32 +60,32 @@ protected:
// Not all boilers support these, only try once when the boiler becomes connected
if (updateSlaveVersion()) {
Log.straceln(FPSTR(S_OT), F("Slave version: %u, type: %u"), vars.parameters.slaveVersion, vars.parameters.slaveType);
Log.straceln(FPSTR(L_OT), F("Slave version: %u, type: %u"), vars.parameters.slaveVersion, vars.parameters.slaveType);
} else {
Log.swarningln(FPSTR(S_OT), F("Get slave version failed"));
Log.swarningln(FPSTR(L_OT), F("Get slave version failed"));
}
// 0x013F
if (setMasterVersion(0x3F, 0x01)) {
Log.straceln(FPSTR(S_OT), F("Master version: %u, type: %u"), vars.parameters.masterVersion, vars.parameters.masterType);
Log.straceln(FPSTR(L_OT), F("Master version: %u, type: %u"), vars.parameters.masterVersion, vars.parameters.masterType);
} else {
Log.swarningln(FPSTR(S_OT), F("Set master version failed"));
Log.swarningln(FPSTR(L_OT), F("Set master version failed"));
}
if (updateSlaveConfig()) {
Log.straceln(FPSTR(S_OT), F("Slave member id: %u, flags: %u"), vars.parameters.slaveMemberId, vars.parameters.slaveFlags);
Log.straceln(FPSTR(L_OT), F("Slave member id: %u, flags: %u"), vars.parameters.slaveMemberId, vars.parameters.slaveFlags);
} else {
Log.swarningln(FPSTR(S_OT), F("Get slave config failed"));
Log.swarningln(FPSTR(L_OT), F("Get slave config failed"));
}
if (setMasterConfig(settings.opentherm.memberIdCode & 0xFF, (settings.opentherm.memberIdCode & 0xFFFF) >> 8)) {
Log.straceln(FPSTR(S_OT), F("Master member id: %u, flags: %u"), vars.parameters.masterMemberId, vars.parameters.masterFlags);
Log.straceln(FPSTR(L_OT), F("Master member id: %u, flags: %u"), vars.parameters.masterMemberId, vars.parameters.masterFlags);
} else {
Log.swarningln(FPSTR(S_OT), F("Set master config failed"));
Log.swarningln(FPSTR(L_OT), F("Set master config failed"));
}
}
@@ -119,18 +113,18 @@ protected:
);
if (!ot->isValidResponse(localResponse)) {
Log.swarningln(FPSTR(S_OT), F("Invalid response after setBoilerStatus: %s"), ot->statusToString(ot->getLastResponseStatus()));
Log.swarningln(FPSTR(L_OT), F("Invalid response after setBoilerStatus: %s"), ot->statusToString(ot->getLastResponseStatus()));
}
if (vars.states.otStatus && !this->prevOtStatus) {
this->prevOtStatus = vars.states.otStatus;
Log.sinfoln(FPSTR(S_OT), F("Connected. Initializing"));
Log.sinfoln(FPSTR(L_OT), F("Connected. Initializing"));
this->initBoiler();
} else if (!vars.states.otStatus && this->prevOtStatus) {
this->prevOtStatus = vars.states.otStatus;
Log.swarningln(FPSTR(S_OT), F("Disconnected"));
Log.swarningln(FPSTR(L_OT), F("Disconnected"));
}
if (!vars.states.otStatus) {
@@ -141,7 +135,7 @@ protected:
if (vars.parameters.heatingEnabled != heatingEnabled) {
this->prevUpdateNonEssentialVars = 0;
vars.parameters.heatingEnabled = heatingEnabled;
Log.sinfoln(FPSTR(S_OT_HEATING), "%s", heatingEnabled ? F("Enabled") : F("Disabled"));
Log.sinfoln(FPSTR(L_OT_HEATING), "%s", heatingEnabled ? F("Enabled") : F("Disabled"));
}
vars.states.heating = ot->isCentralHeatingActive(localResponse);
@@ -154,18 +148,18 @@ protected:
if (millis() - this->prevUpdateNonEssentialVars > 60000) {
if (!heatingEnabled && settings.opentherm.modulationSyncWithHeating) {
if (setMaxModulationLevel(0)) {
Log.snoticeln(FPSTR(S_OT_HEATING), F("Set max modulation 0% (off)"));
Log.snoticeln(FPSTR(L_OT_HEATING), F("Set max modulation 0% (off)"));
} else {
Log.swarningln(FPSTR(S_OT_HEATING), F("Failed set max modulation 0% (off)"));
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max modulation 0% (off)"));
}
} else {
if (setMaxModulationLevel(settings.heating.maxModulation)) {
Log.snoticeln(FPSTR(S_OT_HEATING), F("Set max modulation %hhu%%"), settings.heating.maxModulation);
Log.snoticeln(FPSTR(L_OT_HEATING), F("Set max modulation %hhu%%"), settings.heating.maxModulation);
} else {
Log.swarningln(FPSTR(S_OT_HEATING), F("Failed set max modulation %hhu%%"), settings.heating.maxModulation);
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max modulation %hhu%%"), settings.heating.maxModulation);
}
}
@@ -174,24 +168,24 @@ protected:
if (updateMinMaxDhwTemp()) {
if (settings.dhw.minTemp < vars.parameters.dhwMinTemp) {
settings.dhw.minTemp = vars.parameters.dhwMinTemp;
eeSettings.update();
Log.snoticeln(FPSTR(S_OT_DHW), F("Updated min temp: %hhu"), settings.dhw.minTemp);
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_DHW), F("Updated min temp: %hhu"), settings.dhw.minTemp);
}
if (settings.dhw.maxTemp > vars.parameters.dhwMaxTemp) {
settings.dhw.maxTemp = vars.parameters.dhwMaxTemp;
eeSettings.update();
Log.snoticeln(FPSTR(S_OT_DHW), F("Updated max temp: %hhu"), settings.dhw.maxTemp);
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_DHW), F("Updated max temp: %hhu"), settings.dhw.maxTemp);
}
} else {
Log.swarningln(FPSTR(S_OT_DHW), F("Failed get min/max temp"));
Log.swarningln(FPSTR(L_OT_DHW), F("Failed get min/max temp"));
}
if (settings.dhw.minTemp >= settings.dhw.maxTemp) {
settings.dhw.minTemp = 30;
settings.dhw.maxTemp = 60;
eeSettings.update();
fsSettings.update();
}
}
@@ -200,24 +194,24 @@ protected:
if (updateMinMaxHeatingTemp()) {
if (settings.heating.minTemp < vars.parameters.heatingMinTemp) {
settings.heating.minTemp = vars.parameters.heatingMinTemp;
eeSettings.update();
Log.snoticeln(FPSTR(S_OT_HEATING), F("Updated min temp: %hhu"), settings.heating.minTemp);
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_HEATING), F("Updated min temp: %hhu"), settings.heating.minTemp);
}
if (settings.heating.maxTemp > vars.parameters.heatingMaxTemp) {
settings.heating.maxTemp = vars.parameters.heatingMaxTemp;
eeSettings.update();
Log.snoticeln(FPSTR(S_OT_HEATING), F("Updated max temp: %hhu"), settings.heating.maxTemp);
fsSettings.update();
Log.snoticeln(FPSTR(L_OT_HEATING), F("Updated max temp: %hhu"), settings.heating.maxTemp);
}
} else {
Log.swarningln(FPSTR(S_OT_HEATING), F("Failed get min/max temp"));
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed get min/max temp"));
}
if (settings.heating.minTemp >= settings.heating.maxTemp) {
settings.heating.minTemp = 20;
settings.heating.maxTemp = 90;
eeSettings.update();
fsSettings.update();
}
// force set max CH temp
@@ -258,10 +252,10 @@ protected:
if (vars.actions.resetFault) {
if (vars.states.fault) {
if (ot->sendBoilerReset()) {
Log.sinfoln(FPSTR(S_OT), F("Boiler fault reset successfully"));
Log.sinfoln(FPSTR(L_OT), F("Boiler fault reset successfully"));
} else {
Log.serrorln(FPSTR(S_OT), F("Boiler fault reset failed"));
Log.serrorln(FPSTR(L_OT), F("Boiler fault reset failed"));
}
}
@@ -272,10 +266,10 @@ protected:
if (vars.actions.resetDiagnostic) {
if (vars.states.diagnostic) {
if (ot->sendServiceReset()) {
Log.sinfoln(FPSTR(S_OT), F("Boiler diagnostic reset successfully"));
Log.sinfoln(FPSTR(L_OT), F("Boiler diagnostic reset successfully"));
} else {
Log.serrorln(FPSTR(S_OT), F("Boiler diagnostic reset failed"));
Log.serrorln(FPSTR(L_OT), F("Boiler diagnostic reset failed"));
}
}
@@ -290,7 +284,7 @@ protected:
newDhwTemp = constrain(newDhwTemp, settings.dhw.minTemp, settings.dhw.maxTemp);
}
Log.sinfoln(FPSTR(S_OT_DHW), F("Set temp = %u"), newDhwTemp);
Log.sinfoln(FPSTR(L_OT_DHW), F("Set temp = %u"), newDhwTemp);
// Записываем заданную температуру ГВС
if (ot->setDhwTemp(newDhwTemp)) {
@@ -298,12 +292,12 @@ protected:
this->dhwSetTempTime = millis();
} else {
Log.swarningln(FPSTR(S_OT_DHW), F("Failed set temp"));
Log.swarningln(FPSTR(L_OT_DHW), F("Failed set temp"));
}
if (settings.opentherm.dhwToCh2) {
if (!ot->setHeatingCh2Temp(newDhwTemp)) {
Log.swarningln(FPSTR(S_OT_DHW), F("Failed set ch2 temp"));
Log.swarningln(FPSTR(L_OT_DHW), F("Failed set ch2 temp"));
}
}
}
@@ -311,7 +305,7 @@ protected:
//
// Температура отопления
if (heatingEnabled && (needSetHeatingTemp() || fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001)) {
Log.sinfoln(FPSTR(S_OT_HEATING), F("Set temp = %u"), vars.parameters.heatingSetpoint);
Log.sinfoln(FPSTR(L_OT_HEATING), F("Set temp = %u"), vars.parameters.heatingSetpoint);
// Записываем заданную температуру
if (ot->setHeatingCh1Temp(vars.parameters.heatingSetpoint)) {
@@ -319,12 +313,12 @@ protected:
this->heatingSetTempTime = millis();
} else {
Log.swarningln(FPSTR(S_OT_HEATING), F("Failed set temp"));
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set temp"));
}
if (settings.opentherm.heatingCh1ToCh2) {
if (!ot->setHeatingCh2Temp(vars.parameters.heatingSetpoint)) {
Log.swarningln(FPSTR(S_OT_HEATING), F("Failed set ch2 temp"));
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set ch2 temp"));
}
}
}
@@ -394,7 +388,7 @@ protected:
}
static void printRequestDetail(OpenThermMessageID id, OpenThermResponseStatus status, unsigned long request, unsigned long response, byte attempt) {
Log.straceln(FPSTR(S_OT), F("OT REQUEST ID: %4d Request: %8lx Response: %8lx Attempt: %2d Status: %s"), id, request, response, attempt, ot->statusToString(status));
Log.straceln(FPSTR(L_OT), F("OT REQUEST ID: %4d Request: %8lx Response: %8lx Attempt: %2d Status: %s"), id, request, response, attempt, ot->statusToString(status));
}
bool updateSlaveConfig() {

625
src/PortalTask.h Normal file
View File

@@ -0,0 +1,625 @@
#define PORTAL_CACHE_TIME "" //"max-age=86400"
#define PORTAL_CACHE settings.debug ? nullptr : PORTAL_CACHE_TIME
#ifdef ARDUINO_ARCH_ESP8266
#include <ESP8266WebServer.h>
#include <Updater.h>
using WebServer = ESP8266WebServer;
#else
#include <WebServer.h>
#include <Update.h>
#endif
#include <BufferedWebServer.h>
#include <StaticPage.h>
#include <DynamicPage.h>
#include <UpgradeHandler.h>
#include <DNSServer.h>
extern NetworkTask* tNetwork;
extern FileData fsSettings, fsNetworkSettings;
extern MqttTask* tMqtt;
class PortalTask : public LeanTask {
public:
PortalTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {
this->webServer = new WebServer(80);
this->bufferedWebServer = new BufferedWebServer(this->webServer, 32u);
this->dnsServer = new DNSServer();
}
~PortalTask() {
if (this->bufferedWebServer != nullptr) {
delete this->bufferedWebServer;
}
if (this->webServer != nullptr) {
this->stopWebServer();
delete this->webServer;
}
if (this->dnsServer != nullptr) {
this->stopDnsServer();
delete this->dnsServer;
}
}
protected:
const unsigned int changeStateInterval = 1000;
WebServer* webServer = nullptr;
BufferedWebServer* bufferedWebServer = nullptr;
DNSServer* dnsServer = nullptr;
bool webServerEnabled = false;
bool dnsServerEnabled = false;
unsigned long webServerChangeState = 0;
unsigned long dnsServerChangeState = 0;
const char* getTaskName() {
return "Portal";
}
/*int getTaskCore() {
return 1;
}*/
int getTaskPriority() {
return 0;
}
void setup() {
this->dnsServer->setTTL(0);
this->dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
#ifdef ARDUINO_ARCH_ESP8266
this->webServer->enableETag(true);
//this->webServer->getServer().setNoDelay(true);
#endif
// index page
/*auto indexPage = (new DynamicPage("/", &LittleFS, "/index.html"))
->setTemplateFunction([](const char* var) -> String {
String result;
if (strcmp(var, "ver") == 0) {
result = PROJECT_VERSION;
}
return result;
});
this->webServer->addHandler(indexPage);*/
this->webServer->addHandler(new StaticPage("/", &LittleFS, "/index.html", PORTAL_CACHE));
// restart
this->webServer->on("/restart.html", HTTP_GET, [this]() {
if (this->isNeedAuth()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->send(401);
return;
}
}
vars.actions.restart = true;
this->webServer->sendHeader("Location", "/");
this->webServer->send(302);
});
// network settings page
auto networkPage = (new StaticPage("/network.html", &LittleFS, "/network.html", PORTAL_CACHE))
->setBeforeSendFunction([this]() {
if (this->isNeedAuth() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
return false;
}
return true;
});
this->webServer->addHandler(networkPage);
// settings page
auto settingsPage = (new StaticPage("/settings.html", &LittleFS, "/settings.html", PORTAL_CACHE))
->setBeforeSendFunction([this]() {
if (this->isNeedAuth() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
return false;
}
return true;
});
this->webServer->addHandler(settingsPage);
// upgrade page
auto upgradePage = (new StaticPage("/upgrade.html", &LittleFS, "/upgrade.html", PORTAL_CACHE))
->setBeforeSendFunction([this]() {
if (this->isNeedAuth() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
return false;
}
return true;
});
this->webServer->addHandler(upgradePage);
// OTA
auto upgradeHandler = (new UpgradeHandler("/api/upgrade"))->setCanUploadFunction([this](const String& uri) {
if (this->isNeedAuth() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->sendHeader("Connection", "close");
this->webServer->send(401);
return false;
}
return true;
})->setBeforeUpgradeFunction([](UpgradeHandler::UpgradeType type) -> bool {
return true;
})->setAfterUpgradeFunction([this](const UpgradeHandler::UpgradeResult& fwResult, const UpgradeHandler::UpgradeResult& fsResult) {
unsigned short status = 200;
if (fwResult.status == UpgradeHandler::UpgradeStatus::SUCCESS || fsResult.status == UpgradeHandler::UpgradeStatus::SUCCESS) {
vars.actions.restart = true;
} else {
status = 400;
}
String response = "{\"firmware\": {\"status\": ";
response.concat((short int) fwResult.status);
response.concat(", \"error\": \"");
response.concat(fwResult.error);
response.concat("\"}, \"filesystem\": {\"status\": ");
response.concat((short int) fsResult.status);
response.concat(", \"error\": \"");
response.concat(fsResult.error);
response.concat("\"}}");
this->webServer->send(status, "application/json", response);
});
this->webServer->addHandler(upgradeHandler);
// backup
this->webServer->on("/api/backup/save", HTTP_GET, [this]() {
if (this->isNeedAuth()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
JsonDocument networkSettingsDoc;
networkSettingsToJson(networkSettings, networkSettingsDoc);
JsonDocument settingsDoc;
settingsToJson(settings, settingsDoc);
JsonDocument doc;
doc["network"] = networkSettingsDoc;
doc["settings"] = settingsDoc;
doc.shrinkToFit();
this->webServer->sendHeader(F("Content-Disposition"), F("attachment; filename=\"backup.json\""));
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/backup/restore", HTTP_POST, [this]() {
if (this->isNeedAuth()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
String plain = this->webServer->arg(0);
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/backup/restore %d bytes: %s"), plain.length(), plain.c_str());
if (plain.length() < 2) {
this->webServer->send(406);
return;
} else if (plain.length() > 2048) {
this->webServer->send(413);
return;
}
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, plain);
plain.clear();
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
this->webServer->send(400);
return;
}
bool changed = false;
if (doc["settings"] && jsonToSettings(doc["settings"], settings)) {
fsSettings.update();
changed = true;
}
if (doc["network"] && jsonToNetworkSettings(doc["network"], networkSettings)) {
fsNetworkSettings.update();
tNetwork->setStaCredentials(networkSettings.sta.ssid, networkSettings.sta.password, networkSettings.sta.channel);
tNetwork->setUseDhcp(networkSettings.useDhcp);
tNetwork->setStaticConfig(
networkSettings.staticConfig.ip,
networkSettings.staticConfig.gateway,
networkSettings.staticConfig.subnet,
networkSettings.staticConfig.dns
);
tNetwork->reconnect();
changed = true;
}
this->webServer->send(changed ? 201 : 200);
});
// network
this->webServer->on("/api/network/settings", HTTP_GET, [this]() {
if (this->isNeedAuth()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
JsonDocument doc;
networkSettingsToJson(networkSettings, doc);
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/network/settings", HTTP_POST, [this]() {
if (this->isNeedAuth()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
String plain = this->webServer->arg(0);
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/network/settings %d bytes: %s"), plain.length(), plain.c_str());
if (plain.length() < 2) {
this->webServer->send(406);
return;
} else if (plain.length() > 512) {
this->webServer->send(413);
return;
}
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, plain);
plain.clear();
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
this->webServer->send(400);
return;
}
if (jsonToNetworkSettings(doc, networkSettings)) {
this->webServer->send(201);
fsNetworkSettings.update();
tNetwork->setStaCredentials(networkSettings.sta.ssid, networkSettings.sta.password, networkSettings.sta.channel);
tNetwork->setUseDhcp(networkSettings.useDhcp);
tNetwork->setStaticConfig(
networkSettings.staticConfig.ip,
networkSettings.staticConfig.gateway,
networkSettings.staticConfig.subnet,
networkSettings.staticConfig.dns
);
tNetwork->reconnect();
} else {
this->webServer->send(200);
}
});
this->webServer->on("/api/network/status", HTTP_GET, [this]() {
bool isConnected = tNetwork->isConnected();
JsonDocument doc;
doc["hostname"] = networkSettings.hostname;
doc["mac"] = tNetwork->getStaMac();
doc["isConnected"] = isConnected;
doc["ssid"] = tNetwork->getStaSsid();
doc["signalQuality"] = isConnected ? NetworkTask::rssiToSignalQuality(tNetwork->getRssi()) : 0;
doc["channel"] = isConnected ? tNetwork->getStaChannel() : 0;
doc["ip"] = isConnected ? tNetwork->getStaIp().toString() : "";
doc["subnet"] = isConnected ? tNetwork->getStaSubnet().toString() : "";
doc["gateway"] = isConnected ? tNetwork->getStaGateway().toString() : "";
doc["dns"] = isConnected ? tNetwork->getStaDns().toString() : "";
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/network/scan", HTTP_GET, [this]() {
if (this->isNeedAuth()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->send(401);
return;
}
}
auto apCount = WiFi.scanComplete();
if (apCount <= 0) {
WiFi.scanNetworks(true, true);
if (apCount == WIFI_SCAN_RUNNING || apCount == WIFI_SCAN_FAILED) {
this->webServer->send(202);
} else if (apCount == 0) {
this->webServer->send(200, "application/json", "[]");
} else {
this->webServer->send(500);
}
return;
}
JsonDocument doc;
for (short int i = 0; i < apCount; i++) {
String ssid = WiFi.SSID(i);
doc[i]["ssid"] = ssid;
doc[i]["signalQuality"] = NetworkTask::rssiToSignalQuality(WiFi.RSSI(i));
doc[i]["channel"] = WiFi.channel(i);
doc[i]["hidden"] = !ssid.length();
doc[i]["encryptionType"] = WiFi.encryptionType(i);
}
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
WiFi.scanNetworks(true, true);
});
// settings
this->webServer->on("/api/settings", HTTP_GET, [this]() {
if (this->isNeedAuth()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
JsonDocument doc;
settingsToJson(settings, doc);
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/settings", HTTP_POST, [this]() {
if (this->isNeedAuth()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
String plain = this->webServer->arg(0);
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/settings %d bytes: %s"), plain.length(), plain.c_str());
if (plain.length() < 2) {
this->webServer->send(406);
return;
} else if (plain.length() > 2048) {
this->webServer->send(413);
return;
}
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, plain);
plain.clear();
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
this->webServer->send(400);
return;
}
if (jsonToSettings(doc, settings)) {
fsSettings.update();
this->webServer->send(201);
} else {
this->webServer->send(200);
}
});
// vars
this->webServer->on("/api/vars", HTTP_GET, [this]() {
JsonDocument doc;
varsToJson(vars, doc);
doc["system"]["version"] = PROJECT_VERSION;
doc["system"]["buildDate"] = __DATE__ " " __TIME__;
doc["system"]["uptime"] = millis() / 1000ul;
doc["system"]["freeHeap"] = getFreeHeap();
doc["system"]["totalHeap"] = getTotalHeap();
doc["system"]["maxFreeBlockHeap"] = getMaxFreeBlockHeap();
doc["system"]["resetReason"] = getResetReason();
doc["system"]["mqttConnected"] = tMqtt->isConnected();
doc.shrinkToFit();
this->bufferedWebServer->send(200, "application/json", doc);
});
this->webServer->on("/api/vars", HTTP_POST, [this]() {
if (this->isNeedAuth()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
}
String plain = this->webServer->arg(0);
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/vars %d bytes: %s"), plain.length(), plain.c_str());
if (plain.length() < 2) {
this->webServer->send(406);
return;
} else if (plain.length() > 1024) {
this->webServer->send(413);
return;
}
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, plain);
plain.clear();
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
this->webServer->send(400);
return;
}
if (jsonToVars(doc, vars)) {
this->webServer->send(201);
} else {
this->webServer->send(200);
}
});
// not found
this->webServer->onNotFound([this]() {
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Page not found, uri: %s"), this->webServer->uri().c_str());
if (tNetwork->isApEnabled()) {
this->onCaptivePortal();
} else {
this->webServer->send(404, "text/plain", F("Page not found"));
}
});
this->webServer->serveStatic("/favicon.ico", LittleFS, "/static/favicon.ico", PORTAL_CACHE);
this->webServer->serveStatic("/static", LittleFS, "/static", PORTAL_CACHE);
}
void loop() {
// web server
if (!this->stateWebServer()) {
this->startWebServer();
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Started"));
#ifdef ARDUINO_ARCH_ESP8266
::esp_yield();
#endif
}
// dns server
if (!this->stateDnsServer() && this->stateWebServer() && tNetwork->isApEnabled() && tNetwork->hasApClients() && millis() - this->dnsServerChangeState >= this->changeStateInterval) {
this->startDnsServer();
Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Started: AP up"));
#ifdef ARDUINO_ARCH_ESP8266
::esp_yield();
#endif
} else if (this->stateDnsServer() && (!tNetwork->isApEnabled() || !this->stateWebServer()) && millis() - this->dnsServerChangeState >= this->changeStateInterval) {
this->stopDnsServer();
Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Stopped: AP down"));
#ifdef ARDUINO_ARCH_ESP8266
::esp_yield();
#endif
}
if (this->stateDnsServer()) {
this->dnsServer->processNextRequest();
#ifdef ARDUINO_ARCH_ESP8266
::esp_yield();
#endif
}
if (this->stateWebServer()) {
this->webServer->handleClient();
}
}
bool isNeedAuth() {
return !tNetwork->isApEnabled() && settings.portal.useAuth && strlen(settings.portal.password);
}
void onCaptivePortal() {
const String uri = this->webServer->uri();
if (uri.equals("/connecttest.txt")) {
this->webServer->sendHeader(F("Location"), F("http://logout.net"));
this->webServer->send(302);
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Redirect to http://logout.net with 302 code"));
} else if (uri.equals("/wpad.dat")) {
this->webServer->send(404);
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Send empty page with 404 code"));
} else if (uri.equals("/success.txt")) {
this->webServer->send(200);
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Send empty page with 200 code"));
} else {
String portalUrl = "http://" + tNetwork->getApIp().toString() + '/';
this->webServer->sendHeader("Location", portalUrl.c_str());
this->webServer->send(302);
Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Redirect to portal page with 302 code"));
}
}
bool stateWebServer() {
return this->webServerEnabled;
}
void startWebServer() {
if (this->stateWebServer()) {
return;
}
this->webServer->begin();
this->webServerEnabled = true;
this->webServerChangeState = millis();
::yield();
}
void stopWebServer() {
if (!this->stateWebServer()) {
return;
}
this->webServer->handleClient();
this->webServer->stop();
this->webServerEnabled = false;
this->webServerChangeState = millis();
::yield();
}
bool stateDnsServer() {
return this->dnsServerEnabled;
}
void startDnsServer() {
if (this->stateDnsServer()) {
return;
}
this->dnsServer->start(53, "*", tNetwork->getApIp());
this->dnsServerEnabled = true;
this->dnsServerChangeState = millis();
::yield();
}
void stopDnsServer() {
if (!this->stateDnsServer()) {
return;
}
this->dnsServer->processNextRequest();
this->dnsServer->stop();
this->dnsServerEnabled = false;
this->dnsServerChangeState = millis();
::yield();
}
};

View File

@@ -1,6 +1,35 @@
struct NetworkSettings {
char hostname[25] = HOSTNAME_DEFAULT;
bool useDhcp = true;
struct {
char ip[16] = "192.168.0.100";
char gateway[16] = "192.168.0.1";
char subnet[16] = "255.255.255.0";
char dns[16] = "192.168.0.1";
} staticConfig;
struct {
char ssid[33] = AP_SSID_DEFAULT;
char password[65] = AP_PASSWORD_DEFAULT;
byte channel = 1;
} ap;
struct {
char ssid[33] = STA_SSID_DEFAULT;
char password[65] = STA_PASSWORD_DEFAULT;
byte channel = 0;
} sta;
} networkSettings;
struct Settings {
bool debug = DEBUG_BY_DEFAULT;
char hostname[80] = "opentherm";
struct {
bool useAuth = false;
char login[13] = PORTAL_LOGIN_DEFAULT;
char password[33] = PORTAL_PASSWORD_DEFAULT;
} portal;
struct {
byte inPin = OT_IN_PIN_DEFAULT;
@@ -16,11 +45,11 @@ struct Settings {
} opentherm;
struct {
char server[80];
unsigned short port = 1883;
char user[32];
char password[32];
char prefix[80] = "opentherm";
char server[81] = MQTT_SERVER_DEFAULT;
unsigned short port = MQTT_PORT_DEFAULT;
char user[33] = MQTT_USER_DEFAULT;
char password[33] = MQTT_PASSWORD_DEFAULT;
char prefix[33] = MQTT_PREFIX_DEFAULT;
unsigned short interval = 5;
} mqtt;
@@ -107,6 +136,7 @@ struct Variables {
bool flame = false;
bool fault = false;
bool diagnostic = false;
bool externalPump = false;
} states;
struct {
@@ -124,16 +154,12 @@ struct Variables {
float dhw = 0.0f;
} temperatures;
struct {
bool enable = false;
unsigned long lastEnableTime = 0;
} externalPump;
struct {
bool heatingEnabled = false;
byte heatingMinTemp = DEFAULT_HEATING_MIN_TEMP;
byte heatingMaxTemp = DEFAULT_HEATING_MAX_TEMP;
byte heatingSetpoint = 0;
unsigned long extPumpLastEnableTime = 0;
byte dhwMinTemp = DEFAULT_DHW_MIN_TEMP;
byte dhwMaxTemp = DEFAULT_DHW_MAX_TEMP;
byte maxModulation;

View File

@@ -1,559 +0,0 @@
#define WM_MDNS
#include <WiFiManager.h>
#include <UnsignedIntParameter.h>
#include <UnsignedShortParameter.h>
#include <CheckboxParameter.h>
#include <HeaderParameter.h>
#ifdef ARDUINO_ARCH_ESP8266
extern "C" {
#include "lwip/etharp.h"
}
#endif
WiFiManager wm;
WiFiManagerParameter* wmHostname;
WiFiManagerParameter* wmMqttServer;
UnsignedShortParameter* wmMqttPort;
WiFiManagerParameter* wmMqttUser;
WiFiManagerParameter* wmMqttPassword;
WiFiManagerParameter* wmMqttPrefix;
UnsignedIntParameter* wmMqttPublishInterval;
UnsignedIntParameter* wmOtInPin;
UnsignedIntParameter* wmOtOutPin;
UnsignedIntParameter* wmOtMemberIdCode;
CheckboxParameter* wmOtDhwPresent;
CheckboxParameter* wmOtSummerWinterMode;
CheckboxParameter* wmOtHeatingCh2Enabled;
CheckboxParameter* wmOtHeatingCh1ToCh2;
CheckboxParameter* wmOtDhwToCh2;
CheckboxParameter* wmOtDhwBlocking;
CheckboxParameter* wmOtModSyncWithHeating;
UnsignedIntParameter* wmOutdoorSensorPin;
UnsignedIntParameter* wmIndoorSensorPin;
#if USE_BLE
WiFiManagerParameter* wmOutdoorSensorBleAddress;
#endif
CheckboxParameter* wmExtPumpUse;
UnsignedIntParameter* wmExtPumpPin;
UnsignedShortParameter* wmExtPumpPostCirculationTime;
UnsignedIntParameter* wmExtPumpAntiStuckInterval;
UnsignedShortParameter* wmExtPumpAntiStuckTime;
HeaderParameter* wmMqttHeader;
HeaderParameter* wmOtHeader;
HeaderParameter* wmOtFlagsHeader;
HeaderParameter* wmSensorsHeader;
HeaderParameter* wmExtPumpHeader;
extern EEManager eeSettings;
#if USE_TELNET
extern ESPTelnetStream TelnetStream;
#endif
const char S_WIFI[] PROGMEM = "WIFI";
const char S_WIFI_SETTINGS[] PROGMEM = "WIFI.SETTINGS";
class WifiManagerTask : public LeanTask {
public:
WifiManagerTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {
wmHostname = new WiFiManagerParameter("hostname", "Hostname", settings.hostname, 80);
wm.addParameter(wmHostname);
wmMqttHeader = new HeaderParameter("MQTT");
wm.addParameter(wmMqttHeader);
wmMqttServer = new WiFiManagerParameter("mqtt_server", "Server", settings.mqtt.server, 80);
wm.addParameter(wmMqttServer);
wmMqttPort = new UnsignedShortParameter("mqtt_port", "Port", settings.mqtt.port, 6);
wm.addParameter(wmMqttPort);
wmMqttUser = new WiFiManagerParameter("mqtt_user", "Username", settings.mqtt.user, 32);
wm.addParameter(wmMqttUser);
wmMqttPassword = new WiFiManagerParameter("mqtt_password", "Password", settings.mqtt.password, 32, "type=\"password\"");
wm.addParameter(wmMqttPassword);
wmMqttPrefix = new WiFiManagerParameter("mqtt_prefix", "Prefix", settings.mqtt.prefix, 32);
wm.addParameter(wmMqttPrefix);
wmMqttPublishInterval = new UnsignedIntParameter("mqtt_publish_interval", "Publish interval (sec)", settings.mqtt.interval, 3);
wm.addParameter(wmMqttPublishInterval);
wmOtHeader = new HeaderParameter("OpenTherm");
wm.addParameter(wmOtHeader);
wmOtInPin = new UnsignedIntParameter("ot_in_pin", "GPIO IN", settings.opentherm.inPin, 2);
wm.addParameter(wmOtInPin);
wmOtOutPin = new UnsignedIntParameter("ot_out_pin", "GPIO OUT", settings.opentherm.outPin, 2);
wm.addParameter(wmOtOutPin);
wmOtMemberIdCode = new UnsignedIntParameter("ot_member_id_code", "Master Member ID", settings.opentherm.memberIdCode, 5);
wm.addParameter(wmOtMemberIdCode);
wmOtFlagsHeader = new HeaderParameter("OpenTherm flags");
wm.addParameter(wmOtFlagsHeader);
wmOtDhwPresent = new CheckboxParameter("ot_dhw_present", "DHW present", settings.opentherm.dhwPresent);
wm.addParameter(wmOtDhwPresent);
wmOtSummerWinterMode = new CheckboxParameter("ot_summer_winter_mode", "Summer/winter mode", settings.opentherm.summerWinterMode);
wm.addParameter(wmOtSummerWinterMode);
wmOtHeatingCh2Enabled = new CheckboxParameter("ot_heating_ch2_enabled", "CH2 enabled", settings.opentherm.heatingCh2Enabled);
wm.addParameter(wmOtHeatingCh2Enabled);
wmOtHeatingCh1ToCh2 = new CheckboxParameter("ot_heating_ch1_to_ch2", "Heating CH1 to CH2", settings.opentherm.heatingCh1ToCh2);
wm.addParameter(wmOtHeatingCh1ToCh2);
wmOtDhwToCh2 = new CheckboxParameter("ot_dhw_to_ch2", "DHW to CH2", settings.opentherm.dhwToCh2);
wm.addParameter(wmOtDhwToCh2);
wmOtDhwBlocking = new CheckboxParameter("ot_dhw_blocking", "DHW blocking", settings.opentherm.dhwBlocking);
wm.addParameter(wmOtDhwBlocking);
wmOtModSyncWithHeating = new CheckboxParameter("ot_mod_sync_with_heating", "Modulation sync with heating", settings.opentherm.modulationSyncWithHeating);
wm.addParameter(wmOtModSyncWithHeating);
wmSensorsHeader = new HeaderParameter("Sensors");
wm.addParameter(wmSensorsHeader);
wmOutdoorSensorPin = new UnsignedIntParameter("outdoor_sensor_pin", "Outdoor sensor GPIO", settings.sensors.outdoor.pin, 2);
wm.addParameter(wmOutdoorSensorPin);
wmIndoorSensorPin = new UnsignedIntParameter("indoor_sensor_pin", "Indoor sensor GPIO", settings.sensors.indoor.pin, 2);
wm.addParameter(wmIndoorSensorPin);
#if USE_BLE
wmOutdoorSensorBleAddress = new WiFiManagerParameter("ble_address", "BLE sensor address", settings.sensors.indoor.bleAddresss, 17);
wm.addParameter(wmOutdoorSensorBleAddress);
#endif
wmExtPumpHeader = new HeaderParameter("External pump");
wm.addParameter(wmExtPumpHeader);
wmExtPumpUse = new CheckboxParameter("ext_pump_use", "Use external pump<br>", settings.externalPump.use);
wm.addParameter(wmExtPumpUse);
wmExtPumpPin = new UnsignedIntParameter("ext_pump_pin", "Relay GPIO", settings.externalPump.pin, 2);
wm.addParameter(wmExtPumpPin);
wmExtPumpPostCirculationTime = new UnsignedShortParameter("ext_pump_ps_time", "Post circulation time (min)", (settings.externalPump.postCirculationTime / 60), 5);
wm.addParameter(wmExtPumpPostCirculationTime);
wmExtPumpAntiStuckInterval = new UnsignedIntParameter("ext_pump_as_interval", "Anti stuck interval (days)", (settings.externalPump.antiStuckInterval / 86400), 7);
wm.addParameter(wmExtPumpAntiStuckInterval);
wmExtPumpAntiStuckTime = new UnsignedShortParameter("ext_pump_as_time", "Anti stuck time (min)", (settings.externalPump.antiStuckTime / 60), 5);
wm.addParameter(wmExtPumpAntiStuckTime);
}
WifiManagerTask* addTaskForDisable(AbstractTask* task) {
this->tasksForDisable.push_back(task);
return this;
}
protected:
bool connected = false;
unsigned long lastArpGratuitous = 0;
unsigned long lastReconnecting = 0;
std::vector<AbstractTask*> tasksForDisable;
const char* getTaskName() {
return "WifiManager";
}
/*int getTaskCore() {
return 1;
}*/
int getTaskPriority() {
return 0;
}
void setup() {
#ifdef WOKWI
WiFi.begin("Wokwi-GUEST", "", 6);
#endif
wm.setDebugOutput(settings.debug, (wm_debuglevel_t) WM_DEBUG_MODE);
wm.setTitle(PROJECT_NAME);
wm.setCustomHeadElement(PSTR(
"<style>"
".bheader + br {display: none;}"
".bheader {margin: 1.25em 0 0.5em 0;padding: 0;border-bottom: 2px solid #000;font-size: 1.5em;}"
"</style>"
));
wm.setCustomMenuHTML(PSTR(
"<style>.wrap h1 {display: none;} .wrap h3 {display: none;} .nh {margin: 0 0 1em 0;} .nh .logo {font-size: 1.8em; margin: 0.5em; text-align: center;} .nh .links {text-align: center;}</style>"
"<div class=\"nh\">"
"<div class=\"logo\">" PROJECT_NAME "</div>"
"<div class=\"links\"><a href=\"" PROJECT_REPO "\" target=\"_blank\">Repo</a> | <a href=\"" PROJECT_REPO "/issues\" target=\"_blank\">Issues</a> | <a href=\"" PROJECT_REPO "/releases\" target=\"_blank\">Releases</a> | <small>v" PROJECT_VERSION " (" __DATE__ ")</small></div>"
"</div>"
));
std::vector<const char *> menu = {"custom", "wifi", "param", "sep", "info", "update", "restart"};
wm.setMenu(menu);
//wm.setCleanConnect(true);
wm.setRestorePersistent(false);
wm.setHostname(settings.hostname);
wm.setWiFiAutoReconnect(false);
wm.setAPClientCheck(true);
wm.setConfigPortalBlocking(false);
wm.setSaveParamsCallback(saveParamsCallback);
wm.setPreOtaUpdateCallback([this] {
for (AbstractTask* task : this->tasksForDisable) {
if (task->isEnabled()) {
task->disable();
}
}
});
wm.setConfigPortalTimeout(wm.getWiFiIsSaved() ? 180 : 0);
wm.setDisableConfigPortal(false);
wm.autoConnect(AP_SSID, AP_PASSWORD);
}
void loop() {
if (connected && WiFi.status() != WL_CONNECTED) {
connected = false;
if (wm.getWebPortalActive()) {
wm.stopWebPortal();
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
}
/*wm.setCaptivePortalEnable(true);
if (!wm.getConfigPortalActive()) {
wm.startConfigPortal(AP_SSID, AP_PASSWORD);
}*/
#if USE_TELNET
TelnetStream.stop();
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
#endif
Log.sinfoln(FPSTR(S_WIFI), F("Disconnected"));
}
if (WiFi.status() != WL_CONNECTED && !wm.getConfigPortalActive()) {
if (millis() - this->lastReconnecting > 5000) {
Log.sinfoln(FPSTR(S_WIFI), F("Reconnecting..."));
WiFi.reconnect();
this->lastReconnecting = millis();
}
}
if (!connected && WiFi.status() == WL_CONNECTED) {
connected = true;
wm.setConfigPortalTimeout(180);
if (wm.getConfigPortalActive()) {
wm.stopConfigPortal();
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
}
wm.setCaptivePortalEnable(false);
if (!wm.getWebPortalActive()) {
wm.startWebPortal();
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
}
#if USE_TELNET
TelnetStream.begin(23, false);
#ifdef ARDUINO_ARCH_ESP8266
::yield();
#endif
#endif
Log.sinfoln(FPSTR(S_WIFI), F("Connected. IP: %s, RSSI: %hhd"), WiFi.localIP().toString().c_str(), WiFi.RSSI());
}
#ifdef ARDUINO_ARCH_ESP8266
if (connected && millis() - lastArpGratuitous > 60000) {
stationKeepAliveNow();
lastArpGratuitous = millis();
::yield();
}
#endif
wm.process();
}
static void saveParamsCallback() {
bool changed = false;
bool needRestart = false;
if (strcmp(wmHostname->getValue(), settings.hostname) != 0) {
changed = true;
needRestart = true;
strcpy(settings.hostname, wmHostname->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New hostname: %s"), settings.hostname);
}
if (strcmp(wmMqttServer->getValue(), settings.mqtt.server) != 0) {
changed = true;
strcpy(settings.mqtt.server, wmMqttServer->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.server: %s"), settings.mqtt.server);
}
if (wmMqttPort->getValue() != settings.mqtt.port) {
changed = true;
settings.mqtt.port = wmMqttPort->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.port: %hu"), settings.mqtt.port);
}
if (strcmp(wmMqttUser->getValue(), settings.mqtt.user) != 0) {
changed = true;
strcpy(settings.mqtt.user, wmMqttUser->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.user: %s"), settings.mqtt.user);
}
if (strcmp(wmMqttPassword->getValue(), settings.mqtt.password) != 0) {
changed = true;
strcpy(settings.mqtt.password, wmMqttPassword->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.password: %s"), settings.mqtt.password);
}
if (strcmp(wmMqttPrefix->getValue(), settings.mqtt.prefix) != 0) {
changed = true;
strcpy(settings.mqtt.prefix, wmMqttPrefix->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.prefix: %s"), settings.mqtt.prefix);
}
if (wmMqttPublishInterval->getValue() != settings.mqtt.interval) {
changed = true;
settings.mqtt.interval = wmMqttPublishInterval->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New mqtt.interval: %du"), settings.mqtt.interval);
}
if (wmOtInPin->getValue() != settings.opentherm.inPin) {
changed = true;
needRestart = true;
settings.opentherm.inPin = wmOtInPin->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.inPin: %hhu"), settings.opentherm.inPin);
}
if (wmOtOutPin->getValue() != settings.opentherm.outPin) {
changed = true;
needRestart = true;
settings.opentherm.outPin = wmOtOutPin->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.outPin: %hhu"), settings.opentherm.outPin);
}
if (wmOtMemberIdCode->getValue() != settings.opentherm.memberIdCode) {
changed = true;
settings.opentherm.memberIdCode = wmOtMemberIdCode->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.memberIdCode: %du"), settings.opentherm.memberIdCode);
}
if (wmOtDhwPresent->getCheckboxValue() != settings.opentherm.dhwPresent) {
changed = true;
settings.opentherm.dhwPresent = wmOtDhwPresent->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.dhwPresent: %s"), settings.opentherm.dhwPresent ? "on" : "off");
}
if (wmOtSummerWinterMode->getCheckboxValue() != settings.opentherm.summerWinterMode) {
changed = true;
settings.opentherm.summerWinterMode = wmOtSummerWinterMode->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.summerWinterMode: %s"), settings.opentherm.summerWinterMode ? "on" : "off");
}
if (wmOtHeatingCh2Enabled->getCheckboxValue() != settings.opentherm.heatingCh2Enabled) {
changed = true;
settings.opentherm.heatingCh2Enabled = wmOtHeatingCh2Enabled->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh2Enabled: %s"), settings.opentherm.heatingCh2Enabled ? "on" : "off");
if (settings.opentherm.heatingCh1ToCh2) {
settings.opentherm.heatingCh1ToCh2 = false;
wmOtHeatingCh1ToCh2->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh1ToCh2: %s"), settings.opentherm.heatingCh1ToCh2 ? "on" : "off");
}
if (settings.opentherm.dhwToCh2) {
settings.opentherm.dhwToCh2 = false;
wmOtDhwToCh2->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.dhwToCh2: %s"), settings.opentherm.dhwToCh2 ? "on" : "off");
}
}
if (wmOtHeatingCh1ToCh2->getCheckboxValue() != settings.opentherm.heatingCh1ToCh2) {
changed = true;
settings.opentherm.heatingCh1ToCh2 = wmOtHeatingCh1ToCh2->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh1ToCh2: %s"), settings.opentherm.heatingCh1ToCh2 ? "on" : "off");
if (settings.opentherm.heatingCh2Enabled) {
settings.opentherm.heatingCh2Enabled = false;
wmOtHeatingCh2Enabled->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh2Enabled: %s"), settings.opentherm.heatingCh2Enabled ? "on" : "off");
}
if (settings.opentherm.dhwToCh2) {
settings.opentherm.dhwToCh2 = false;
wmOtDhwToCh2->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.dhwToCh2: %s"), settings.opentherm.dhwToCh2 ? "on" : "off");
}
}
if (wmOtDhwToCh2->getCheckboxValue() != settings.opentherm.dhwToCh2) {
changed = true;
settings.opentherm.dhwToCh2 = wmOtDhwToCh2->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.dhwToCh2: %s"), settings.opentherm.dhwToCh2 ? "on" : "off");
if (settings.opentherm.heatingCh2Enabled) {
settings.opentherm.heatingCh2Enabled = false;
wmOtHeatingCh2Enabled->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh2Enabled: %s"), settings.opentherm.heatingCh2Enabled ? "on" : "off");
}
if (settings.opentherm.heatingCh1ToCh2) {
settings.opentherm.heatingCh1ToCh2 = false;
wmOtHeatingCh1ToCh2->setValue(false);
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.heatingCh1ToCh2: %s"), settings.opentherm.heatingCh1ToCh2 ? "on" : "off");
}
}
if (wmOtDhwBlocking->getCheckboxValue() != settings.opentherm.dhwBlocking) {
changed = true;
settings.opentherm.dhwBlocking = wmOtDhwBlocking->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.dhwBlocking: %s"), settings.opentherm.dhwBlocking ? "on" : "off");
}
if (wmOtModSyncWithHeating->getCheckboxValue() != settings.opentherm.modulationSyncWithHeating) {
changed = true;
settings.opentherm.modulationSyncWithHeating = wmOtModSyncWithHeating->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New opentherm.modulationSyncWithHeating: %s"), settings.opentherm.modulationSyncWithHeating ? "on" : "off");
}
if (wmOutdoorSensorPin->getValue() != settings.sensors.outdoor.pin) {
changed = true;
needRestart = true;
settings.sensors.outdoor.pin = wmOutdoorSensorPin->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New sensors.outdoor.pin: %hhu"), settings.sensors.outdoor.pin);
}
if (wmIndoorSensorPin->getValue() != settings.sensors.indoor.pin) {
changed = true;
needRestart = true;
settings.sensors.indoor.pin = wmIndoorSensorPin->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New sensors.indoor.pin: %hhu"), settings.sensors.indoor.pin);
}
#if USE_BLE
if (strcmp(wmOutdoorSensorBleAddress->getValue(), settings.sensors.indoor.bleAddresss) != 0) {
changed = true;
strcpy(settings.sensors.indoor.bleAddresss, wmOutdoorSensorBleAddress->getValue());
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New sensors.indoor.bleAddresss: %s"), settings.sensors.indoor.bleAddresss);
}
#endif
if (wmExtPumpUse->getCheckboxValue() != settings.externalPump.use) {
changed = true;
settings.externalPump.use = wmExtPumpUse->getCheckboxValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New externalPump.use: %s"), settings.externalPump.use ? "on" : "off");
}
if (wmExtPumpPin->getValue() != settings.externalPump.pin) {
changed = true;
needRestart = true;
settings.externalPump.pin = wmExtPumpPin->getValue();
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New externalPump.pin: %hhu"), settings.externalPump.pin);
}
if ((wmExtPumpPostCirculationTime->getValue() * 60) != settings.externalPump.postCirculationTime) {
changed = true;
settings.externalPump.postCirculationTime = wmExtPumpPostCirculationTime->getValue() * 60;
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New externalPump.postCirculationTime: %hu"), settings.externalPump.postCirculationTime);
}
if ((wmExtPumpAntiStuckInterval->getValue() * 86400) != settings.externalPump.antiStuckInterval) {
changed = true;
settings.externalPump.antiStuckInterval = wmExtPumpAntiStuckInterval->getValue() * 86400;
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New externalPump.antiStuckInterval: %du"), settings.externalPump.antiStuckInterval);
}
if ((wmExtPumpAntiStuckTime->getValue() * 60) != settings.externalPump.antiStuckTime) {
changed = true;
settings.externalPump.antiStuckTime = wmExtPumpAntiStuckTime->getValue() * 60;
Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New externalPump.antiStuckTime: %hu"), settings.externalPump.antiStuckTime);
}
if (!changed) {
return;
} else if (needRestart) {
vars.actions.restart = true;
}
eeSettings.update();
}
#ifdef ARDUINO_ARCH_ESP8266
/**
* @brief
* https://github.com/arendst/Tasmota/blob/e6515883f0ee5451931b6280ff847b117de5a231/tasmota/tasmota_support/support_wifi.ino#L1196
*/
static void stationKeepAliveNow(void) {
for (netif* interface = netif_list; interface != nullptr; interface = interface->next) {
if (
(interface->flags & NETIF_FLAG_LINK_UP)
&& (interface->flags & NETIF_FLAG_UP)
&& interface->num == STATION_IF
&& (!ip4_addr_isany_val(*netif_ip4_addr(interface)))
) {
etharp_gratuitous(interface);
break;
}
}
}
#endif
};

View File

@@ -1,8 +1,6 @@
#define PROJECT_NAME "OpenTherm Gateway"
#define PROJECT_VERSION "1.4.0-rc.5"
#define PROJECT_VERSION "1.4.0-rc.9"
#define PROJECT_REPO "https://github.com/Laxilef/OTGateway"
#define AP_SSID "OpenTherm Gateway"
#define AP_PASSWORD "otgateway123456"
#define EMERGENCY_TIME_TRESHOLD 120000
#define MQTT_RECONNECT_INTERVAL 15000
@@ -38,10 +36,58 @@
#define USE_BLE false
#endif
#ifndef HOSTNAME_DEFAULT
#define HOSTNAME_DEFAULT "opentherm"
#endif
#ifndef AP_SSID_DEFAULT
#define AP_SSID_DEFAULT "OpenTherm Gateway"
#endif
#ifndef AP_PASSWORD_DEFAULT
#define AP_PASSWORD_DEFAULT "otgateway123456"
#endif
#ifndef STA_SSID_DEFAULT
#define STA_SSID_DEFAULT ""
#endif
#ifndef STA_PASSWORD_DEFAULT
#define STA_PASSWORD_DEFAULT ""
#endif
#ifndef DEBUG_BY_DEFAULT
#define DEBUG_BY_DEFAULT false
#endif
#ifndef PORTAL_LOGIN_DEFAULT
#define PORTAL_LOGIN_DEFAULT ""
#endif
#ifndef PORTAL_PASSWORD_DEFAULT
#define PORTAL_PASSWORD_DEFAULT ""
#endif
#ifndef MQTT_SERVER_DEFAULT
#define MQTT_SERVER_DEFAULT ""
#endif
#ifndef MQTT_PORT_DEFAULT
#define MQTT_PORT_DEFAULT 1883
#endif
#ifndef MQTT_USER_DEFAULT
#define MQTT_USER_DEFAULT ""
#endif
#ifndef MQTT_PASSWORD_DEFAULT
#define MQTT_PASSWORD_DEFAULT ""
#endif
#ifndef MQTT_PREFIX_DEFAULT
#define MQTT_PREFIX_DEFAULT "opentherm"
#endif
#ifndef OT_IN_PIN_DEFAULT
#define OT_IN_PIN_DEFAULT 0
#endif

View File

@@ -1,9 +1,12 @@
#include <Arduino.h>
#include "defines.h"
#include "strings.h"
#include <ArduinoJson.h>
#include <EEManager.h>
#include <FileData.h>
#include <LittleFS.h>
#include <TinyLogger.h>
#include "Settings.h"
#include <utils.h>
#if USE_TELNET
#include "ESPTelnetStream.h"
@@ -19,29 +22,34 @@
#include <Task.h>
#include <LeanTask.h>
#include "WifiManagerTask.h"
#include "NetworkTask.h"
#include "MqttTask.h"
#include "OpenThermTask.h"
#include "SensorsTask.h"
#include "RegulatorTask.h"
#include "PortalTask.h"
#include "MainTask.h"
// Vars
EEManager eeSettings(settings, 60000);
FileData fsNetworkSettings(&LittleFS, "/network.conf", 'n', &networkSettings, sizeof(networkSettings), 1000);
FileData fsSettings(&LittleFS, "/settings.conf", 's', &settings, sizeof(settings), 60000);
#if USE_TELNET
ESPTelnetStream TelnetStream;
ESPTelnetStream TelnetStream;
#endif
// Tasks
WifiManagerTask* tWm;
NetworkTask* tNetwork;
MqttTask* tMqtt;
OpenThermTask* tOt;
SensorsTask* tSensors;
RegulatorTask* tRegulator;
PortalTask* tPortal;
MainTask* tMain;
void setup() {
LittleFS.begin();
Log.setLevel(TinyLogger::Level::VERBOSE);
Log.setServiceTemplate("\033[1m[%s]\033[22m");
Log.setLevelTemplate("\033[1m[%s]\033[22m");
@@ -52,46 +60,87 @@ void setup() {
int sec = time % 60;
int min = time % 3600 / 60;
int hour = time / 3600;
return tm{sec, min, hour};
});
#if USE_SERIAL
Serial.begin(115200);
Serial.println("\n\n");
Log.addStream(&Serial);
Serial.begin(115200);
Log.addStream(&Serial);
#endif
#if USE_TELNET
TelnetStream.setKeepAliveInterval(500);
Log.addStream(&TelnetStream);
TelnetStream.setKeepAliveInterval(500);
Log.addStream(&TelnetStream);
#endif
EEPROM.begin(eeSettings.blockSize());
uint8_t eeSettingsResult = eeSettings.begin(0, 's');
if (eeSettingsResult == 0) {
Log.sinfoln("MAIN", F("Settings loaded"));
Log.print("\n\n\r");
if (strcmp(SETTINGS_VALID_VALUE, settings.validationValue) != 0) {
Log.swarningln("MAIN", F("Settings not valid, reset and restart..."));
eeSettings.reset();
delay(5000);
ESP.restart();
}
// network settings
switch (fsNetworkSettings.read()) {
case FD_FS_ERR:
Log.swarningln(FPSTR(L_NETWORK_SETTINGS), F("Filesystem error, load default"));
break;
case FD_FILE_ERR:
Log.swarningln(FPSTR(L_NETWORK_SETTINGS), F("Bad data, load default"));
break;
case FD_WRITE:
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Not found, load default"));
break;
case FD_ADD:
case FD_READ:
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Loaded"));
break;
default:
break;
}
} else if (eeSettingsResult == 1) {
Log.sinfoln("MAIN", F("Settings NOT loaded, first start"));
// settings
switch (fsSettings.read()) {
case FD_FS_ERR:
Log.swarningln(FPSTR(L_SETTINGS), F("Filesystem error, load default"));
break;
case FD_FILE_ERR:
Log.swarningln(FPSTR(L_SETTINGS), F("Bad data, load default"));
break;
case FD_WRITE:
Log.sinfoln(FPSTR(L_SETTINGS), F("Not found, load default"));
break;
case FD_ADD:
case FD_READ:
Log.sinfoln(FPSTR(L_SETTINGS), F("Loaded"));
} else if (eeSettingsResult == 2) {
Log.serrorln("MAIN", F("Settings NOT loaded (error)"));
if (strcmp(SETTINGS_VALID_VALUE, settings.validationValue) != 0) {
Log.swarningln(FPSTR(L_SETTINGS), F("Not valid, set default and restart..."));
fsSettings.reset();
delay(5000);
ESP.restart();
}
break;
default:
break;
}
Log.setLevel(settings.debug ? TinyLogger::Level::VERBOSE : TinyLogger::Level::INFO);
tWm = new WifiManagerTask(true, 0);
Scheduler.start(tWm);
tMqtt = new MqttTask(false, 100);
tNetwork = (new NetworkTask(true, 500))
->setHostname(networkSettings.hostname)
->setStaCredentials(
#ifdef WOKWI
"Wokwi-GUEST", nullptr, 6
#else
strlen(networkSettings.sta.ssid) ? networkSettings.sta.ssid : nullptr,
strlen(networkSettings.sta.password) ? networkSettings.sta.password : nullptr,
networkSettings.sta.channel
#endif
)->setApCredentials(
strlen(networkSettings.ap.ssid) ? networkSettings.ap.ssid : nullptr,
strlen(networkSettings.ap.password) ? networkSettings.ap.password : nullptr,
networkSettings.ap.channel
);
Scheduler.start(tNetwork);
tMqtt = new MqttTask(false, 500);
Scheduler.start(tMqtt);
tOt = new OpenThermTask(false, 1000);
@@ -103,21 +152,17 @@ void setup() {
tRegulator = new RegulatorTask(true, 10000);
Scheduler.start(tRegulator);
tPortal = new PortalTask(true, 0);
Scheduler.start(tPortal);
tMain = new MainTask(true, 100);
Scheduler.start(tMain);
tWm
->addTaskForDisable(tMain)
->addTaskForDisable(tMqtt)
->addTaskForDisable(tOt)
->addTaskForDisable(tSensors)
->addTaskForDisable(tRegulator);
Scheduler.begin();
}
void loop() {
#if defined(ARDUINO_ARCH_ESP32)
vTaskDelete(NULL);
#endif
#if defined(ARDUINO_ARCH_ESP32)
vTaskDelete(NULL);
#endif
}

23
src/strings.h Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#ifndef PROGMEM
#define PROGMEM
#endif
const char L_SETTINGS[] PROGMEM = "SETTINGS";
const char L_NETWORK[] PROGMEM = "NETWORK";
const char L_NETWORK_SETTINGS[] PROGMEM = "NETWORK.SETTINGS";
const char L_PORTAL_WEBSERVER[] PROGMEM = "PORTAL.WEBSERVER";
const char L_PORTAL_DNSSERVER[] PROGMEM = "PORTAL.DNSSERVER";
const char L_PORTAL_CAPTIVE[] PROGMEM = "PORTAL.CAPTIVE";
const char L_MAIN[] PROGMEM = "MAIN";
const char L_MQTT[] PROGMEM = "MQTT";
const char L_MQTT_MSG[] PROGMEM = "MQTT.MSG";
const char L_OT[] PROGMEM = "OT";
const char L_OT_DHW[] PROGMEM = "OT.DHW";
const char L_OT_HEATING[] PROGMEM = "OT.HEATING";
const char L_SENSORS_OUTDOOR[] PROGMEM = "SENSORS.OUTDOOR";
const char L_SENSORS_INDOOR[] PROGMEM = "SENSORS.INDOOR";
const char L_SENSORS_BLE[] PROGMEM = "SENSORS.BLE";
const char L_REGULATOR[] PROGMEM = "REGULATOR";
const char L_REGULATOR_PID[] PROGMEM = "REGULATOR.PID";
const char L_REGULATOR_EQUITHERM[] PROGMEM = "REGULATOR.EQUITHERM";

939
src/utils.h Normal file
View File

@@ -0,0 +1,939 @@
#include <Arduino.h>
double roundd(double value, uint8_t decimals = 2) {
if (decimals == 0) {
return (int)(value + 0.5);
} else if (abs(value) < 0.00000001) {
return 0.0;
}
double multiplier = pow10(decimals);
value += 0.5 / multiplier * (value < 0 ? -1 : 1);
return (int)(value * multiplier) / multiplier;
}
size_t getFreeHeap() {
return ESP.getFreeHeap();
}
size_t getTotalHeap() {
#if defined(ARDUINO_ARCH_ESP32)
return ESP.getHeapSize();
#elif defined(ARDUINO_ARCH_ESP8266)
return 81920;
#else
return 99999;
#endif
}
size_t getMaxFreeBlockHeap() {
#if defined(ARDUINO_ARCH_ESP32)
return ESP.getMaxAllocHeap();
#else
return ESP.getMaxFreeBlockSize();
#endif
}
String getResetReason() {
String value;
#if defined(ARDUINO_ARCH_ESP8266)
value = ESP.getResetReason();
#elif defined(ARDUINO_ARCH_ESP32)
switch(esp_reset_reason()) {
case ESP_RST_POWERON:
value = F("Reset due to power-on event");
break;
case ESP_RST_EXT:
value = F("Reset by external pin");
break;
case ESP_RST_SW:
value = F("Software reset via esp_restart");
break;
case ESP_RST_PANIC:
value = F("Software reset due to exception/panic");
break;
case ESP_RST_INT_WDT:
value = F("Reset (software or hardware) due to interrupt watchdog");
break;
case ESP_RST_TASK_WDT:
value = F("Reset due to task watchdog");
break;
case ESP_RST_WDT:
value = F("Reset due to other watchdogs");
break;
case ESP_RST_DEEPSLEEP:
value = F("Reset after exiting deep sleep mode");
break;
case ESP_RST_BROWNOUT:
value = F("Brownout reset (software or hardware)");
break;
case ESP_RST_SDIO:
value = F("Reset over SDIO");
break;
case ESP_RST_UNKNOWN:
default:
value = F("unknown");
break;
}
#else
value = F("unknown");
#endif
return value;
}
void networkSettingsToJson(const NetworkSettings& src, JsonVariant dst) {
dst["hostname"] = src.hostname;
dst["useDhcp"] = src.useDhcp;
dst["staticConfig"]["ip"] = src.staticConfig.ip;
dst["staticConfig"]["gateway"] = src.staticConfig.gateway;
dst["staticConfig"]["subnet"] = src.staticConfig.subnet;
dst["staticConfig"]["dns"] = src.staticConfig.dns;
dst["ap"]["ssid"] = src.ap.ssid;
dst["ap"]["password"] = src.ap.password;
dst["ap"]["channel"] = src.ap.channel;
dst["sta"]["ssid"] = src.sta.ssid;
dst["sta"]["password"] = src.sta.password;
dst["sta"]["channel"] = src.sta.channel;
//dst.shrinkToFit();
}
bool jsonToNetworkSettings(const JsonVariantConst src, NetworkSettings& dst) {
bool changed = false;
// hostname
if (!src["hostname"].isNull()) {
String value = src["hostname"].as<String>();
if (value.length() < sizeof(dst.hostname)) {
strcpy(dst.hostname, value.c_str());
changed = true;
}
}
// use dhcp
if (src["useDhcp"].is<bool>()) {
dst.useDhcp = src["useDhcp"].as<bool>();
changed = true;
}
// static config
if (!src["staticConfig"]["ip"].isNull()) {
String value = src["staticConfig"]["ip"].as<String>();
if (value.length() < sizeof(dst.staticConfig.ip)) {
strcpy(dst.staticConfig.ip, value.c_str());
changed = true;
}
}
if (!src["staticConfig"]["gateway"].isNull()) {
String value = src["staticConfig"]["gateway"].as<String>();
if (value.length() < sizeof(dst.staticConfig.gateway)) {
strcpy(dst.staticConfig.gateway, value.c_str());
changed = true;
}
}
if (!src["staticConfig"]["subnet"].isNull()) {
String value = src["staticConfig"]["subnet"].as<String>();
if (value.length() < sizeof(dst.staticConfig.subnet)) {
strcpy(dst.staticConfig.subnet, value.c_str());
changed = true;
}
}
if (!src["staticConfig"]["dns"].isNull()) {
String value = src["staticConfig"]["dns"].as<String>();
if (value.length() < sizeof(dst.staticConfig.dns)) {
strcpy(dst.staticConfig.dns, value.c_str());
changed = true;
}
}
// ap
if (!src["ap"]["ssid"].isNull()) {
String value = src["ap"]["ssid"].as<String>();
if (value.length() < sizeof(dst.ap.ssid)) {
strcpy(dst.ap.ssid, value.c_str());
changed = true;
}
}
if (!src["ap"]["password"].isNull()) {
String value = src["ap"]["password"].as<String>();
if (value.length() < sizeof(dst.ap.password)) {
strcpy(dst.ap.password, value.c_str());
changed = true;
}
}
if (!src["ap"]["channel"].isNull()) {
unsigned char value = src["ap"]["channel"].as<unsigned char>();
if (value >= 0 && value < 12) {
dst.ap.channel = value;
changed = true;
}
}
// ap
if (!src["sta"]["ssid"].isNull()) {
String value = src["sta"]["ssid"].as<String>();
if (value.length() < sizeof(dst.sta.ssid)) {
strcpy(dst.sta.ssid, value.c_str());
changed = true;
}
}
if (!src["sta"]["password"].isNull()) {
String value = src["sta"]["password"].as<String>();
if (value.length() < sizeof(dst.sta.password)) {
strcpy(dst.sta.password, value.c_str());
changed = true;
}
}
if (!src["sta"]["channel"].isNull()) {
unsigned char value = src["sta"]["channel"].as<unsigned char>();
if (value >= 0 && value < 12) {
dst.sta.channel = value;
changed = true;
}
}
return changed;
}
void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
dst["debug"] = src.debug;
if (!safe) {
dst["portal"]["useAuth"] = src.portal.useAuth;
dst["portal"]["login"] = src.portal.login;
dst["portal"]["password"] = src.portal.password;
dst["opentherm"]["inPin"] = src.opentherm.inPin;
dst["opentherm"]["outPin"] = src.opentherm.outPin;
dst["opentherm"]["memberIdCode"] = src.opentherm.memberIdCode;
dst["opentherm"]["dhwPresent"] = src.opentherm.dhwPresent;
dst["opentherm"]["summerWinterMode"] = src.opentherm.summerWinterMode;
dst["opentherm"]["heatingCh2Enabled"] = src.opentherm.heatingCh2Enabled;
dst["opentherm"]["heatingCh1ToCh2"] = src.opentherm.heatingCh1ToCh2;
dst["opentherm"]["dhwToCh2"] = src.opentherm.dhwToCh2;
dst["opentherm"]["dhwBlocking"] = src.opentherm.dhwBlocking;
dst["opentherm"]["modulationSyncWithHeating"] = src.opentherm.modulationSyncWithHeating;
dst["mqtt"]["server"] = src.mqtt.server;
dst["mqtt"]["port"] = src.mqtt.port;
dst["mqtt"]["user"] = src.mqtt.user;
dst["mqtt"]["password"] = src.mqtt.password;
dst["mqtt"]["prefix"] = src.mqtt.prefix;
dst["mqtt"]["interval"] = src.mqtt.interval;
}
dst["emergency"]["enable"] = src.emergency.enable;
dst["emergency"]["target"] = roundd(src.emergency.target, 2);
dst["emergency"]["useEquitherm"] = src.emergency.useEquitherm;
dst["emergency"]["usePid"] = src.emergency.usePid;
dst["heating"]["enable"] = src.heating.enable;
dst["heating"]["turbo"] = src.heating.turbo;
dst["heating"]["target"] = roundd(src.heating.target, 2);
dst["heating"]["hysteresis"] = roundd(src.heating.hysteresis, 2);
dst["heating"]["minTemp"] = src.heating.minTemp;
dst["heating"]["maxTemp"] = src.heating.maxTemp;
dst["heating"]["maxModulation"] = src.heating.maxModulation;
dst["dhw"]["enable"] = src.dhw.enable;
dst["dhw"]["target"] = src.dhw.target;
dst["dhw"]["minTemp"] = src.dhw.minTemp;
dst["dhw"]["maxTemp"] = src.dhw.maxTemp;
dst["pid"]["enable"] = src.pid.enable;
dst["pid"]["p_factor"] = roundd(src.pid.p_factor, 3);
dst["pid"]["i_factor"] = roundd(src.pid.i_factor, 3);
dst["pid"]["d_factor"] = roundd(src.pid.d_factor, 1);
dst["pid"]["dt"] = src.pid.dt;
dst["pid"]["minTemp"] = src.pid.minTemp;
dst["pid"]["maxTemp"] = src.pid.maxTemp;
dst["equitherm"]["enable"] = src.equitherm.enable;
dst["equitherm"]["n_factor"] = roundd(src.equitherm.n_factor, 3);
dst["equitherm"]["k_factor"] = roundd(src.equitherm.k_factor, 3);
dst["equitherm"]["t_factor"] = roundd(src.equitherm.t_factor, 3);
dst["sensors"]["outdoor"]["type"] = src.sensors.outdoor.type;
dst["sensors"]["outdoor"]["pin"] = src.sensors.outdoor.pin;
dst["sensors"]["outdoor"]["offset"] = roundd(src.sensors.outdoor.offset, 2);
dst["sensors"]["indoor"]["type"] = src.sensors.indoor.type;
dst["sensors"]["indoor"]["pin"] = src.sensors.indoor.pin;
dst["sensors"]["indoor"]["bleAddresss"] = src.sensors.indoor.bleAddresss;
dst["sensors"]["indoor"]["offset"] = roundd(src.sensors.indoor.offset, 2);
if (!safe) {
dst["externalPump"]["use"] = src.externalPump.use;
dst["externalPump"]["pin"] = src.externalPump.pin;
dst["externalPump"]["postCirculationTime"] = roundd(src.externalPump.postCirculationTime / 60, 0);
dst["externalPump"]["antiStuckInterval"] = roundd(src.externalPump.antiStuckInterval / 86400, 0);
dst["externalPump"]["antiStuckTime"] = roundd(src.externalPump.antiStuckTime / 60, 0);
}
//dst.shrinkToFit();
}
void safeSettingsToJson(const Settings& src, JsonVariant dst) {
settingsToJson(src, dst, true);
}
bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false) {
bool changed = false;
if (src["debug"].is<bool>()) {
dst.debug = src["debug"].as<bool>();
changed = true;
}
if (!safe) {
// portal
if (src["portal"]["useAuth"].is<bool>()) {
dst.portal.useAuth = src["portal"]["useAuth"].as<bool>();
changed = true;
}
if (!src["portal"]["login"].isNull()) {
String value = src["portal"]["login"].as<String>();
if (value.length() < sizeof(dst.portal.login)) {
strcpy(dst.portal.login, value.c_str());
changed = true;
}
}
if (!src["portal"]["password"].isNull()) {
String value = src["portal"]["password"].as<String>();
if (value.length() < sizeof(dst.portal.password)) {
strcpy(dst.portal.password, value.c_str());
changed = true;
}
}
// opentherm
if (!src["opentherm"]["inPin"].isNull()) {
unsigned char value = src["opentherm"]["inPin"].as<unsigned char>();
if (value >= 0 && value < 50) {
dst.opentherm.inPin = value;
changed = true;
}
}
if (!src["opentherm"]["outPin"].isNull()) {
unsigned char value = src["opentherm"]["outPin"].as<unsigned char>();
if (value >= 0 && value < 50) {
dst.opentherm.outPin = value;
changed = true;
}
}
if (!src["opentherm"]["memberIdCode"].isNull()) {
unsigned int value = src["opentherm"]["memberIdCode"].as<unsigned int>();
if (value >= 0 && value < 65536) {
dst.opentherm.memberIdCode = value;
changed = true;
}
}
if (src["opentherm"]["dhwPresent"].is<bool>()) {
dst.opentherm.dhwPresent = src["opentherm"]["dhwPresent"].as<bool>();
changed = true;
}
if (src["opentherm"]["summerWinterMode"].is<bool>()) {
dst.opentherm.summerWinterMode = src["opentherm"]["summerWinterMode"].as<bool>();
changed = true;
}
if (src["opentherm"]["heatingCh2Enabled"].is<bool>()) {
dst.opentherm.heatingCh2Enabled = src["opentherm"]["heatingCh2Enabled"].as<bool>();
if (dst.opentherm.heatingCh2Enabled) {
dst.opentherm.heatingCh1ToCh2 = false;
dst.opentherm.dhwToCh2 = false;
}
changed = true;
}
if (src["opentherm"]["heatingCh1ToCh2"].is<bool>()) {
dst.opentherm.heatingCh1ToCh2 = src["opentherm"]["heatingCh1ToCh2"].as<bool>();
if (dst.opentherm.heatingCh1ToCh2) {
dst.opentherm.heatingCh2Enabled = false;
dst.opentherm.dhwToCh2 = false;
}
changed = true;
}
if (src["opentherm"]["dhwToCh2"].is<bool>()) {
dst.opentherm.dhwToCh2 = src["opentherm"]["dhwToCh2"].as<bool>();
if (dst.opentherm.dhwToCh2) {
dst.opentherm.heatingCh2Enabled = false;
dst.opentherm.heatingCh1ToCh2 = false;
}
changed = true;
}
if (src["opentherm"]["dhwBlocking"].is<bool>()) {
dst.opentherm.dhwBlocking = src["opentherm"]["dhwBlocking"].as<bool>();
changed = true;
}
if (src["opentherm"]["modulationSyncWithHeating"].is<bool>()) {
dst.opentherm.modulationSyncWithHeating = src["opentherm"]["modulationSyncWithHeating"].as<bool>();
changed = true;
}
// mqtt
if (!src["mqtt"]["server"].isNull()) {
String value = src["mqtt"]["server"].as<String>();
if (value.length() < sizeof(dst.mqtt.server)) {
strcpy(dst.mqtt.server, value.c_str());
changed = true;
}
}
if (!src["mqtt"]["port"].isNull()) {
unsigned short value = src["mqtt"]["port"].as<unsigned short>();
if (value >= 0 && value <= 65536) {
dst.mqtt.port = value;
changed = true;
}
}
if (!src["mqtt"]["user"].isNull()) {
String value = src["mqtt"]["user"].as<String>();
if (value.length() < sizeof(dst.mqtt.user)) {
strcpy(dst.mqtt.user, value.c_str());
changed = true;
}
}
if (!src["mqtt"]["password"].isNull()) {
String value = src["mqtt"]["password"].as<String>();
if (value.length() < sizeof(dst.mqtt.password)) {
strcpy(dst.mqtt.password, value.c_str());
changed = true;
}
}
if (!src["mqtt"]["prefix"].isNull()) {
String value = src["mqtt"]["prefix"].as<String>();
if (value.length() < sizeof(dst.mqtt.prefix)) {
strcpy(dst.mqtt.prefix, value.c_str());
changed = true;
}
}
if (!src["mqtt"]["interval"].isNull()) {
unsigned short value = src["mqtt"]["interval"].as<unsigned short>();
if (value >= 3 && value <= 60) {
dst.mqtt.interval = value;
changed = true;
}
}
}
// emergency
if (src["emergency"]["enable"].is<bool>()) {
dst.emergency.enable = src["emergency"]["enable"].as<bool>();
changed = true;
}
if (!src["emergency"]["target"].isNull()) {
double value = src["emergency"]["target"].as<double>();
if (value > 0 && value < 100) {
dst.emergency.target = roundd(value, 2);
changed = true;
}
}
if (src["emergency"]["useEquitherm"].is<bool>()) {
if (dst.sensors.outdoor.type != 1) {
dst.emergency.useEquitherm = src["emergency"]["useEquitherm"].as<bool>();
} else {
dst.emergency.useEquitherm = false;
}
if (dst.emergency.useEquitherm && dst.emergency.usePid) {
dst.emergency.usePid = false;
}
changed = true;
}
if (src["emergency"]["usePid"].is<bool>()) {
if (dst.sensors.indoor.type != 1) {
dst.emergency.usePid = src["emergency"]["usePid"].as<bool>();
} else {
dst.emergency.usePid = false;
}
if (dst.emergency.usePid && dst.emergency.useEquitherm) {
dst.emergency.useEquitherm = false;
}
changed = true;
}
// heating
if (src["heating"]["enable"].is<bool>()) {
dst.heating.enable = src["heating"]["enable"].as<bool>();
changed = true;
}
if (src["heating"]["turbo"].is<bool>()) {
dst.heating.turbo = src["heating"]["turbo"].as<bool>();
changed = true;
}
if (!src["heating"]["target"].isNull()) {
double value = src["heating"]["target"].as<double>();
if (value > 0 && value < 100) {
dst.heating.target = roundd(value, 2);
changed = true;
}
}
if (!src["heating"]["hysteresis"].isNull()) {
double value = src["heating"]["hysteresis"].as<double>();
if (value >= 0 && value <= 5) {
dst.heating.hysteresis = roundd(value, 2);
changed = true;
}
}
if (!src["heating"]["minTemp"].isNull()) {
unsigned char value = src["heating"]["minTemp"].as<unsigned char>();
if (value >= vars.parameters.heatingMinTemp && value <= vars.parameters.heatingMaxTemp) {
dst.heating.minTemp = value;
changed = true;
}
}
if (!src["heating"]["maxTemp"].isNull()) {
unsigned char value = src["heating"]["maxTemp"].as<unsigned char>();
if (value >= vars.parameters.heatingMinTemp && value <= vars.parameters.heatingMaxTemp) {
dst.heating.maxTemp = value;
changed = true;
}
}
if (!src["heating"]["maxModulation"].isNull()) {
unsigned char value = src["heating"]["maxModulation"].as<unsigned char>();
if (value > 0 && value <= 100) {
dst.heating.maxModulation = value;
changed = true;
}
}
// dhw
if (src["dhw"]["enable"].is<bool>()) {
dst.dhw.enable = src["dhw"]["enable"].as<bool>();
changed = true;
}
if (!src["dhw"]["target"].isNull()) {
unsigned char value = src["dhw"]["target"].as<unsigned char>();
if (value >= 0 && value < 100) {
dst.dhw.target = value;
changed = true;
}
}
if (!src["dhw"]["minTemp"].isNull()) {
unsigned char value = src["dhw"]["minTemp"].as<unsigned char>();
if (value >= vars.parameters.dhwMinTemp && value <= vars.parameters.dhwMaxTemp) {
dst.dhw.minTemp = value;
changed = true;
}
}
if (!src["dhw"]["maxTemp"].isNull()) {
unsigned char value = src["dhw"]["maxTemp"].as<unsigned char>();
if (value >= vars.parameters.dhwMinTemp && value <= vars.parameters.dhwMaxTemp) {
dst.dhw.maxTemp = value;
changed = true;
}
}
// pid
if (src["pid"]["enable"].is<bool>()) {
dst.pid.enable = src["pid"]["enable"].as<bool>();
changed = true;
}
if (!src["pid"]["p_factor"].isNull()) {
double value = src["pid"]["p_factor"].as<double>();
if (value > 0 && value <= 1000) {
dst.pid.p_factor = roundd(value, 3);
changed = true;
}
}
if (!src["pid"]["i_factor"].isNull()) {
double value = src["pid"]["i_factor"].as<double>();
if (value >= 0 && value <= 100) {
dst.pid.i_factor = roundd(value, 3);
changed = true;
}
}
if (!src["pid"]["d_factor"].isNull()) {
double value = src["pid"]["d_factor"].as<double>();
if (value >= 0 && value <= 100000) {
dst.pid.d_factor = roundd(value, 1);
changed = true;
}
}
if (!src["pid"]["dt"].isNull()) {
unsigned short value = src["pid"]["dt"].as<unsigned short>();
if (value >= 30 && value <= 600) {
dst.pid.dt = value;
changed = true;
}
}
if (!src["pid"]["maxTemp"].isNull()) {
unsigned char value = src["pid"]["maxTemp"].as<unsigned char>();
if (value > 0 && value <= 100 && value > dst.pid.minTemp) {
dst.pid.maxTemp = value;
changed = true;
}
}
if (!src["pid"]["minTemp"].isNull()) {
unsigned char value = src["pid"]["minTemp"].as<unsigned char>();
if (value >= 0 && value < 100 && value < dst.pid.maxTemp) {
dst.pid.minTemp = value;
changed = true;
}
}
// equitherm
if (src["equitherm"]["enable"].is<bool>()) {
dst.equitherm.enable = src["equitherm"]["enable"].as<bool>();
changed = true;
}
if (!src["equitherm"]["n_factor"].isNull()) {
double value = src["equitherm"]["n_factor"].as<double>();
if (value > 0 && value <= 10) {
dst.equitherm.n_factor = roundd(value, 3);
changed = true;
}
}
if (!src["equitherm"]["k_factor"].isNull()) {
double value = src["equitherm"]["k_factor"].as<double>();
if (value >= 0 && value <= 10) {
dst.equitherm.k_factor = roundd(value, 3);
changed = true;
}
}
if (!src["equitherm"]["t_factor"].isNull()) {
double value = src["equitherm"]["t_factor"].as<double>();
if (value >= 0 && value <= 10) {
dst.equitherm.t_factor = roundd(value, 3);
changed = true;
}
}
// sensors
if (!src["sensors"]["outdoor"]["type"].isNull()) {
unsigned char value = src["sensors"]["outdoor"]["type"].as<unsigned char>();
if (value >= 0 && value <= 2) {
dst.sensors.outdoor.type = value;
if (dst.sensors.outdoor.type == 1) {
dst.emergency.useEquitherm = false;
}
changed = true;
}
}
if (!src["sensors"]["outdoor"]["pin"].isNull()) {
unsigned char value = src["sensors"]["outdoor"]["pin"].as<unsigned char>();
if (value >= 0 && value <= 50) {
dst.sensors.outdoor.pin = value;
changed = true;
}
}
if (!src["sensors"]["outdoor"]["offset"].isNull()) {
double value = src["sensors"]["outdoor"]["offset"].as<double>();
if (value >= -10 && value <= 10) {
dst.sensors.outdoor.offset = roundd(value, 2);
changed = true;
}
}
if (!src["sensors"]["indoor"]["type"].isNull()) {
unsigned char value = src["sensors"]["indoor"]["type"].as<unsigned char>();
if (value >= 1 && value <= 3) {
dst.sensors.indoor.type = value;
if (dst.sensors.indoor.type == 1) {
dst.emergency.usePid = false;
}
changed = true;
}
}
if (!src["sensors"]["indoor"]["pin"].isNull()) {
unsigned char value = src["sensors"]["indoor"]["pin"].as<unsigned char>();
if (value >= 0 && value <= 50) {
dst.sensors.indoor.pin = value;
changed = true;
}
}
#if USE_BLE
if (!src["sensors"]["indoor"]["bleAddresss"].isNull()) {
String value = src["sensors"]["indoor"]["bleAddresss"].as<String>();
if (value.length() < sizeof(dst.sensors.indoor.bleAddresss)) {
strcpy(dst.sensors.indoor.bleAddresss, value.c_str());
changed = true;
}
}
#endif
if (!src["sensors"]["indoor"]["offset"].isNull()) {
double value = src["sensors"]["indoor"]["offset"].as<double>();
if (value >= -10 && value <= 10) {
dst.sensors.indoor.offset = roundd(value, 2);
changed = true;
}
}
if (!safe) {
// external pump
if (src["externalPump"]["use"].is<bool>()) {
dst.externalPump.use = src["externalPump"]["use"].as<bool>();
changed = true;
}
if (!src["externalPump"]["pin"].isNull()) {
unsigned char value = src["externalPump"]["pin"].as<unsigned char>();
if (value >= 0 && value <= 50) {
dst.externalPump.pin = value;
changed = true;
}
}
if (!src["externalPump"]["postCirculationTime"].isNull()) {
unsigned short value = src["externalPump"]["postCirculationTime"].as<unsigned short>();
if (value >= 0 && value <= 120) {
dst.externalPump.postCirculationTime = value * 60;
changed = true;
}
}
if (!src["externalPump"]["antiStuckInterval"].isNull()) {
unsigned int value = src["externalPump"]["antiStuckInterval"].as<unsigned int>();
if (value >= 0 && value <= 366) {
dst.externalPump.antiStuckInterval = value * 86400;
changed = true;
}
}
if (!src["externalPump"]["antiStuckTime"].isNull()) {
unsigned short value = src["externalPump"]["antiStuckTime"].as<unsigned short>();
if (value >= 0 && value <= 20) {
dst.externalPump.antiStuckTime = value * 60;
changed = true;
}
}
}
return changed;
}
bool safeJsonToSettings(const JsonVariantConst src, Settings& dst) {
return jsonToSettings(src, dst, true);
}
void varsToJson(const Variables& src, JsonVariant dst) {
dst["tuning"]["enable"] = src.tuning.enable;
dst["tuning"]["regulator"] = src.tuning.regulator;
dst["states"]["otStatus"] = src.states.otStatus;
dst["states"]["emergency"] = src.states.emergency;
dst["states"]["heating"] = src.states.heating;
dst["states"]["dhw"] = src.states.dhw;
dst["states"]["flame"] = src.states.flame;
dst["states"]["fault"] = src.states.fault;
dst["states"]["diagnostic"] = src.states.diagnostic;
dst["states"]["externalPump"] = src.states.externalPump;
dst["sensors"]["modulation"] = roundd(src.sensors.modulation, 2);
dst["sensors"]["pressure"] = roundd(src.sensors.pressure, 2);
dst["sensors"]["dhwFlowRate"] = src.sensors.dhwFlowRate;
dst["sensors"]["faultCode"] = src.sensors.faultCode;
dst["sensors"]["rssi"] = src.sensors.rssi;
dst["sensors"]["uptime"] = millis() / 1000ul;
dst["temperatures"]["indoor"] = roundd(src.temperatures.indoor, 2);
dst["temperatures"]["outdoor"] = roundd(src.temperatures.outdoor, 2);
dst["temperatures"]["heating"] = roundd(src.temperatures.heating, 2);
dst["temperatures"]["dhw"] = roundd(src.temperatures.dhw, 2);
dst["parameters"]["heatingEnabled"] = src.parameters.heatingEnabled;
dst["parameters"]["heatingMinTemp"] = src.parameters.heatingMinTemp;
dst["parameters"]["heatingMaxTemp"] = src.parameters.heatingMaxTemp;
dst["parameters"]["heatingSetpoint"] = src.parameters.heatingSetpoint;
dst["parameters"]["dhwMinTemp"] = src.parameters.dhwMinTemp;
dst["parameters"]["dhwMaxTemp"] = src.parameters.dhwMaxTemp;
//dst.shrinkToFit();
}
bool jsonToVars(const JsonVariantConst src, Variables& dst) {
bool changed = false;
// tuning
if (src["tuning"]["enable"].is<bool>()) {
dst.tuning.enable = src["tuning"]["enable"].as<bool>();
changed = true;
}
if (!src["tuning"]["regulator"].isNull()) {
unsigned char value = src["tuning"]["regulator"].as<unsigned char>();
if (value >= 0 && value <= 1) {
dst.tuning.regulator = value;
changed = true;
}
}
// temperatures
if (!src["temperatures"]["indoor"].isNull()) {
double value = src["temperatures"]["indoor"].as<double>();
if (settings.sensors.indoor.type == 1 && value > -100 && value < 100) {
dst.temperatures.indoor = roundd(value, 2);
changed = true;
}
}
if (!src["temperatures"]["outdoor"].isNull()) {
double value = src["temperatures"]["outdoor"].as<double>();
if (settings.sensors.outdoor.type == 1 && value > -100 && value < 100) {
dst.temperatures.outdoor = roundd(value, 2);
changed = true;
}
}
// actions
if (src["actions"]["restart"].is<bool>() && src["actions"]["restart"].as<bool>()) {
dst.actions.restart = true;
}
if (src["actions"]["resetFault"].is<bool>() && src["actions"]["resetFault"].as<bool>()) {
dst.actions.resetFault = true;
}
if (src["actions"]["resetDiagnostic"].is<bool>() && src["actions"]["resetDiagnostic"].as<bool>()) {
dst.actions.resetDiagnostic = true;
}
return changed;
}