mirror of
https://github.com/Laxilef/OTGateway.git
synced 2025-12-10 18:24:27 +05:00
first commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/.vscode
|
||||||
|
/bin/*
|
||||||
|
!/bin/src.ino.bin
|
||||||
BIN
bin/src.ino.bin
Normal file
BIN
bin/src.ino.bin
Normal file
Binary file not shown.
1280
src/HomeAssistantHelper.h
Normal file
1280
src/HomeAssistantHelper.h
Normal file
File diff suppressed because it is too large
Load Diff
83
src/MainTask.h
Normal file
83
src/MainTask.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#include "lib/MiniTask.h"
|
||||||
|
#include "SensorsTask.h"
|
||||||
|
#include "RegulatorTask.h"
|
||||||
|
|
||||||
|
extern MqttTask* tMqtt;
|
||||||
|
|
||||||
|
class MainTask : public CustomTask {
|
||||||
|
public:
|
||||||
|
MainTask(bool enabled = false, unsigned long interval = 0) : CustomTask(enabled, interval) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
//HttpServerTask* tHttpServer;
|
||||||
|
SensorsTask* tSensors;
|
||||||
|
RegulatorTask* tRegulator;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
//tHttpServer = new HttpServerTask(false);
|
||||||
|
tSensors = new SensorsTask(false, DS18B20_INTERVAL);
|
||||||
|
tRegulator = new RegulatorTask(true, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
static unsigned long lastHeapInfo = 0;
|
||||||
|
static unsigned short minFreeHeapSize = 65535;
|
||||||
|
|
||||||
|
if (eeSettings.tick()) {
|
||||||
|
INFO("Settings updated (EEPROM)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
if (!tMqtt->isEnabled()) {
|
||||||
|
tMqtt->enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (tMqtt->isEnabled()) {
|
||||||
|
tMqtt->disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
vars.states.emergency = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tSensors->isEnabled() && settings.outdoorTempSource == 2) {
|
||||||
|
tSensors->enable();
|
||||||
|
} else if (tSensors->isEnabled() && settings.outdoorTempSource != 2) {
|
||||||
|
tSensors->disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
//tHttpServer->loopWrapper();
|
||||||
|
//yield();
|
||||||
|
tSensors->loopWrapper();
|
||||||
|
yield();
|
||||||
|
tRegulator->loopWrapper();
|
||||||
|
|
||||||
|
#ifdef USE_TELNET
|
||||||
|
yield();
|
||||||
|
|
||||||
|
// anti memory leak
|
||||||
|
TelnetStream.flush();
|
||||||
|
while (TelnetStream.available() > 0) {
|
||||||
|
TelnetStream.read();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (settings.debug) {
|
||||||
|
unsigned short freeHeapSize = ESP.getFreeHeap();
|
||||||
|
unsigned short minFreeHeapSizeDiff = 0;
|
||||||
|
|
||||||
|
if (freeHeapSize < minFreeHeapSize) {
|
||||||
|
minFreeHeapSizeDiff = minFreeHeapSize - freeHeapSize;
|
||||||
|
minFreeHeapSize = freeHeapSize;
|
||||||
|
}
|
||||||
|
if (millis() - lastHeapInfo > 10000 || minFreeHeapSizeDiff > 0) {
|
||||||
|
DEBUG_F("Free heap size: %hu bytes, min: %hu bytes (diff: %hu bytes)\n", freeHeapSize, minFreeHeapSize, minFreeHeapSizeDiff);
|
||||||
|
lastHeapInfo = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*char[] getUptime() {
|
||||||
|
uint64_t = esp_timer_get_time();
|
||||||
|
}*/
|
||||||
|
};
|
||||||
474
src/MqttTask.h
Normal file
474
src/MqttTask.h
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
#include <WiFiClient.h>
|
||||||
|
#include <PubSubClient.h>
|
||||||
|
#include <netif/etharp.h>
|
||||||
|
#include "HomeAssistantHelper.h"
|
||||||
|
|
||||||
|
WiFiClient espClient;
|
||||||
|
PubSubClient client(espClient);
|
||||||
|
HomeAssistantHelper haHelper;
|
||||||
|
|
||||||
|
|
||||||
|
class MqttTask : public CustomTask {
|
||||||
|
public:
|
||||||
|
MqttTask(bool enabled = false, unsigned long interval = 0) : CustomTask(enabled, interval) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
unsigned long lastReconnectAttempt = 0;
|
||||||
|
unsigned short int reconnectAttempts = 0;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
client.setServer(settings.mqtt.server, settings.mqtt.port);
|
||||||
|
client.setCallback(__callback);
|
||||||
|
haHelper.setPrefix(settings.mqtt.prefix);
|
||||||
|
haHelper.setDeviceVersion(OT_GATEWAY_VERSION);
|
||||||
|
|
||||||
|
sprintf(buffer, CONFIG_URL, WiFi.localIP().toString().c_str());
|
||||||
|
haHelper.setDeviceConfigUrl(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if (!client.connected() && millis() - lastReconnectAttempt >= MQTT_RECONNECT_INTERVAL) {
|
||||||
|
INFO_F("Mqtt not connected, state: %i, connecting to server %s...\n", client.state(), settings.mqtt.server);
|
||||||
|
|
||||||
|
if (client.connect(settings.hostname, settings.mqtt.user, settings.mqtt.password)) {
|
||||||
|
INFO("Connected to MQTT server");
|
||||||
|
|
||||||
|
client.subscribe(getTopicPath("settings/set").c_str());
|
||||||
|
client.subscribe(getTopicPath("state/set").c_str());
|
||||||
|
publishHaEntities();
|
||||||
|
publishNonStaticHaEntities(true);
|
||||||
|
|
||||||
|
reconnectAttempts = 0;
|
||||||
|
lastReconnectAttempt = 0;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
INFO("Failed to connect to MQTT server\n");
|
||||||
|
|
||||||
|
if (!vars.states.emergency && ++reconnectAttempts >= EMERGENCY_TRESHOLD) {
|
||||||
|
vars.states.emergency = true;
|
||||||
|
INFO("Emergency mode enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
forceARP();
|
||||||
|
lastReconnectAttempt = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (client.connected()) {
|
||||||
|
if (vars.states.emergency) {
|
||||||
|
vars.states.emergency = false;
|
||||||
|
|
||||||
|
INFO("Emergency mode disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
client.loop();
|
||||||
|
bool published = publishNonStaticHaEntities();
|
||||||
|
publish(published);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void forceARP() {
|
||||||
|
struct netif* netif = netif_list;
|
||||||
|
while (netif) {
|
||||||
|
etharp_gratuitous(netif);
|
||||||
|
netif = netif->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool updateSettings(JsonDocument& doc) {
|
||||||
|
bool flag = false;
|
||||||
|
|
||||||
|
if (!doc["debug"].isNull() && doc["debug"].is<bool>()) {
|
||||||
|
settings.debug = doc["debug"].as<bool>();
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["outdoorTempSource"].isNull() && doc["outdoorTempSource"].is<int>() && doc["outdoorTempSource"] >= 0 && doc["outdoorTempSource"] <= 2) {
|
||||||
|
settings.outdoorTempSource = doc["outdoorTempSource"];
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["mqtt"]["interval"].isNull() && doc["mqtt"]["interval"].is<int>() && doc["mqtt"]["interval"] >= 1000 && doc["mqtt"]["interval"] <= 120000) {
|
||||||
|
settings.mqtt.interval = doc["mqtt"]["interval"].as<unsigned int>();
|
||||||
|
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<float>() || doc["emergency"]["target"].is<int>())) {
|
||||||
|
settings.emergency.target = round(doc["emergency"]["target"].as<float>() * 10) / 10;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["emergency"]["useEquitherm"].isNull() && doc["emergency"]["useEquitherm"].is<bool>()) {
|
||||||
|
settings.emergency.useEquitherm = doc["emergency"]["useEquitherm"].as<bool>();
|
||||||
|
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"]["target"].isNull() && (doc["heating"]["target"].is<float>() || doc["heating"]["target"].is<int>())) {
|
||||||
|
settings.heating.target = round(doc["heating"]["target"].as<float>() * 10) / 10;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["heating"]["hysteresis"].isNull() && (doc["heating"]["hysteresis"].is<float>() || doc["heating"]["hysteresis"].is<int>())) {
|
||||||
|
settings.heating.hysteresis = round(doc["heating"]["hysteresis"].as<float>() * 10) / 10;
|
||||||
|
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<int>()) {
|
||||||
|
settings.dhw.target = doc["dhw"]["target"].as<int>();
|
||||||
|
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<float>() || doc["pid"]["p_factor"].is<int>())) {
|
||||||
|
settings.pid.p_factor = round(doc["pid"]["p_factor"].as<float>() * 1000) / 1000;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["pid"]["i_factor"].isNull() && (doc["pid"]["i_factor"].is<float>() || doc["pid"]["i_factor"].is<int>())) {
|
||||||
|
settings.pid.i_factor = round(doc["pid"]["i_factor"].as<float>() * 1000) / 1000;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["pid"]["d_factor"].isNull() && (doc["pid"]["d_factor"].is<float>() || doc["pid"]["d_factor"].is<int>())) {
|
||||||
|
settings.pid.d_factor = round(doc["pid"]["d_factor"].as<float>() * 1000) / 1000;
|
||||||
|
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<float>() || doc["equitherm"]["n_factor"].is<int>())) {
|
||||||
|
settings.equitherm.n_factor = round(doc["equitherm"]["n_factor"].as<float>() * 1000) / 1000;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["equitherm"]["k_factor"].isNull() && (doc["equitherm"]["k_factor"].is<float>() || doc["equitherm"]["k_factor"].is<int>())) {
|
||||||
|
settings.equitherm.k_factor = round(doc["equitherm"]["k_factor"].as<float>() * 1000) / 1000;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["equitherm"]["t_factor"].isNull() && (doc["equitherm"]["t_factor"].is<float>() || doc["equitherm"]["t_factor"].is<int>())) {
|
||||||
|
settings.equitherm.t_factor = round(doc["equitherm"]["t_factor"].as<float>() * 1000) / 1000;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flag) {
|
||||||
|
eeSettings.update();
|
||||||
|
publish(true);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool updateVariables(const 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<int>() && doc["tuning"]["regulator"] >= 0 && doc["tuning"]["regulator"] <= 1) {
|
||||||
|
vars.tuning.regulator = doc["tuning"]["regulator"];
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["temperatures"]["indoor"].isNull() && (doc["temperatures"]["indoor"].is<float>() || doc["temperatures"]["indoor"].is<int>())) {
|
||||||
|
vars.temperatures.indoor = round(doc["temperatures"]["indoor"].as<float>() * 100) / 100;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["temperatures"]["outdoor"].isNull() && (doc["temperatures"]["outdoor"].is<float>() || doc["temperatures"]["outdoor"].is<int>()) && settings.outdoorTempSource == 1) {
|
||||||
|
vars.temperatures.outdoor = round(doc["temperatures"]["outdoor"].as<float>() * 100) / 100;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc["restart"].isNull() && doc["restart"].is<bool>() && doc["restart"]) {
|
||||||
|
eeSettings.updateNow();
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flag) {
|
||||||
|
publish(true);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void publish(bool force = false) {
|
||||||
|
static unsigned int prevPubVars = 0;
|
||||||
|
static unsigned int prevPubSettings = 0;
|
||||||
|
|
||||||
|
// publish variables and status
|
||||||
|
if (force || millis() - prevPubVars > settings.mqtt.interval) {
|
||||||
|
publishVariables(getTopicPath("state").c_str());
|
||||||
|
|
||||||
|
if (vars.states.fault) {
|
||||||
|
client.publish(getTopicPath("status").c_str(), "fault");
|
||||||
|
} else {
|
||||||
|
client.publish(getTopicPath("status").c_str(), vars.states.otStatus ? "online" : "offline");
|
||||||
|
}
|
||||||
|
|
||||||
|
forceARP();
|
||||||
|
prevPubVars = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// publish settings
|
||||||
|
if (force || millis() - prevPubSettings > settings.mqtt.interval * 10) {
|
||||||
|
publishSettings(getTopicPath("settings").c_str());
|
||||||
|
prevPubSettings = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void publishHaEntities() {
|
||||||
|
// main
|
||||||
|
haHelper.publishSelectOutdoorTempSource();
|
||||||
|
haHelper.publishSwitchDebug(false);
|
||||||
|
|
||||||
|
// emergency
|
||||||
|
haHelper.publishSwitchEmergency();
|
||||||
|
haHelper.publishNumberEmergencyTarget();
|
||||||
|
haHelper.publishSwitchEmergencyUseEquitherm();
|
||||||
|
|
||||||
|
// heating
|
||||||
|
haHelper.publishSwitchHeating(false);
|
||||||
|
//haHelper.publishNumberHeatingTarget(false);
|
||||||
|
haHelper.publishNumberHeatingHysteresis();
|
||||||
|
haHelper.publishSensorHeatingSetpoint(false);
|
||||||
|
|
||||||
|
// dhw
|
||||||
|
haHelper.publishSwitchDHW(false);
|
||||||
|
//haHelper.publishNumberDHWTarget(false);
|
||||||
|
|
||||||
|
// pid
|
||||||
|
haHelper.publishSwitchPID();
|
||||||
|
haHelper.publishNumberPIDFactorP();
|
||||||
|
haHelper.publishNumberPIDFactorI();
|
||||||
|
haHelper.publishNumberPIDFactorD();
|
||||||
|
|
||||||
|
// equitherm
|
||||||
|
haHelper.publishSwitchEquitherm();
|
||||||
|
haHelper.publishNumberEquithermFactorN();
|
||||||
|
haHelper.publishNumberEquithermFactorK();
|
||||||
|
haHelper.publishNumberEquithermFactorT();
|
||||||
|
|
||||||
|
// tuning
|
||||||
|
haHelper.publishSwitchTuning();
|
||||||
|
haHelper.publishSelectTuningRegulator();
|
||||||
|
|
||||||
|
// states
|
||||||
|
haHelper.publishBinSensorStatus();
|
||||||
|
haHelper.publishBinSensorOtStatus();
|
||||||
|
haHelper.publishBinSensorHeating();
|
||||||
|
haHelper.publishBinSensorDHW();
|
||||||
|
haHelper.publishBinSensorFlame();
|
||||||
|
haHelper.publishBinSensorFault();
|
||||||
|
haHelper.publishBinSensorDiagnostic();
|
||||||
|
haHelper.publishSensorFaultCode();
|
||||||
|
|
||||||
|
// sensors
|
||||||
|
haHelper.publishSensorModulation(false);
|
||||||
|
haHelper.publishSensorPressure(false);
|
||||||
|
|
||||||
|
// temperatures
|
||||||
|
haHelper.publishNumberIndoorTemp();
|
||||||
|
//haHelper.publishNumberOutdoorTemp();
|
||||||
|
haHelper.publishSensorHeatingTemp();
|
||||||
|
haHelper.publishSensorDHWTemp();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool publishNonStaticHaEntities(bool force = false) {
|
||||||
|
static byte _heatingMinTemp;
|
||||||
|
static byte _heatingMaxTemp;
|
||||||
|
static byte _dhwMinTemp;
|
||||||
|
static byte _dhwMaxTemp;
|
||||||
|
static bool _editableOutdoorTemp;
|
||||||
|
|
||||||
|
bool published = false;
|
||||||
|
bool isStupidMode = !settings.pid.enable && !settings.equitherm.enable;
|
||||||
|
byte heatingMinTemp = isStupidMode ? vars.parameters.heatingMinTemp : 10;
|
||||||
|
byte heatingMaxTemp = isStupidMode ? vars.parameters.heatingMaxTemp : 30;
|
||||||
|
bool editableOutdoorTemp = settings.outdoorTempSource == 1;
|
||||||
|
|
||||||
|
|
||||||
|
if (force || _heatingMinTemp != heatingMinTemp || _heatingMaxTemp != heatingMaxTemp) {
|
||||||
|
if (settings.heating.target < heatingMinTemp || settings.heating.target > heatingMaxTemp) {
|
||||||
|
settings.heating.target = constrain(settings.heating.target, heatingMinTemp, heatingMaxTemp);
|
||||||
|
}
|
||||||
|
|
||||||
|
_heatingMinTemp = heatingMinTemp;
|
||||||
|
_heatingMaxTemp = heatingMaxTemp;
|
||||||
|
|
||||||
|
haHelper.publishNumberHeatingTarget(heatingMinTemp, heatingMaxTemp, false);
|
||||||
|
haHelper.publishClimateHeating(heatingMinTemp, heatingMaxTemp);
|
||||||
|
|
||||||
|
published = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force || _dhwMinTemp != vars.parameters.dhwMinTemp || _dhwMaxTemp != vars.parameters.dhwMaxTemp) {
|
||||||
|
_dhwMinTemp = vars.parameters.dhwMinTemp;
|
||||||
|
_dhwMaxTemp = vars.parameters.dhwMaxTemp;
|
||||||
|
|
||||||
|
haHelper.publishNumberDHWTarget(vars.parameters.dhwMinTemp, vars.parameters.dhwMaxTemp, false);
|
||||||
|
haHelper.publishClimateDHW(vars.parameters.dhwMinTemp, vars.parameters.dhwMaxTemp);
|
||||||
|
|
||||||
|
published = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force || _editableOutdoorTemp != editableOutdoorTemp) {
|
||||||
|
_editableOutdoorTemp = editableOutdoorTemp;
|
||||||
|
|
||||||
|
if (editableOutdoorTemp) {
|
||||||
|
haHelper.deleteSensorOutdoorTemp();
|
||||||
|
haHelper.publishNumberOutdoorTemp();
|
||||||
|
} else {
|
||||||
|
haHelper.deleteNumberOutdoorTemp();
|
||||||
|
haHelper.publishSensorOutdoorTemp();
|
||||||
|
}
|
||||||
|
|
||||||
|
published = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return published;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool publishSettings(const char* topic) {
|
||||||
|
StaticJsonDocument<2048> doc;
|
||||||
|
|
||||||
|
doc["debug"] = settings.debug;
|
||||||
|
doc["outdoorTempSource"] = settings.outdoorTempSource;
|
||||||
|
|
||||||
|
doc["emergency"]["enable"] = settings.emergency.enable;
|
||||||
|
doc["emergency"]["target"] = settings.emergency.target;
|
||||||
|
doc["emergency"]["useEquitherm"] = settings.emergency.useEquitherm;
|
||||||
|
|
||||||
|
doc["heating"]["enable"] = settings.heating.enable;
|
||||||
|
doc["heating"]["target"] = settings.heating.target;
|
||||||
|
doc["heating"]["hysteresis"] = settings.heating.hysteresis;
|
||||||
|
|
||||||
|
doc["dhw"]["enable"] = settings.dhw.enable;
|
||||||
|
doc["dhw"]["target"] = settings.dhw.target;
|
||||||
|
|
||||||
|
doc["pid"]["enable"] = settings.pid.enable;
|
||||||
|
doc["pid"]["p_factor"] = settings.pid.p_factor;
|
||||||
|
doc["pid"]["i_factor"] = settings.pid.i_factor;
|
||||||
|
doc["pid"]["d_factor"] = settings.pid.d_factor;
|
||||||
|
|
||||||
|
doc["equitherm"]["enable"] = settings.equitherm.enable;
|
||||||
|
doc["equitherm"]["n_factor"] = settings.equitherm.n_factor;
|
||||||
|
doc["equitherm"]["k_factor"] = settings.equitherm.k_factor;
|
||||||
|
doc["equitherm"]["t_factor"] = settings.equitherm.t_factor;
|
||||||
|
|
||||||
|
client.beginPublish(topic, measureJson(doc), false);
|
||||||
|
//BufferingPrint bufferedClient(client, 32);
|
||||||
|
//serializeJson(doc, bufferedClient);
|
||||||
|
//bufferedClient.flush();
|
||||||
|
serializeJson(doc, client);
|
||||||
|
return client.endPublish();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool publishVariables(const char* topic) {
|
||||||
|
StaticJsonDocument<2048> 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["states"]["faultCode"] = vars.states.faultCode;
|
||||||
|
|
||||||
|
doc["sensors"]["modulation"] = vars.sensors.modulation;
|
||||||
|
doc["sensors"]["pressure"] = vars.sensors.pressure;
|
||||||
|
|
||||||
|
doc["temperatures"]["indoor"] = vars.temperatures.indoor;
|
||||||
|
doc["temperatures"]["outdoor"] = vars.temperatures.outdoor;
|
||||||
|
doc["temperatures"]["heating"] = vars.temperatures.heating;
|
||||||
|
doc["temperatures"]["dhw"] = vars.temperatures.dhw;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
client.beginPublish(topic, measureJson(doc), false);
|
||||||
|
//BufferingPrint bufferedClient(client, 32);
|
||||||
|
//serializeJson(doc, bufferedClient);
|
||||||
|
//bufferedClient.flush();
|
||||||
|
serializeJson(doc, client);
|
||||||
|
return client.endPublish();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string getTopicPath(const char* topic) {
|
||||||
|
return std::string(settings.mqtt.prefix) + "/" + std::string(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __callback(char* topic, byte* payload, unsigned int length) {
|
||||||
|
if (!length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.debug) {
|
||||||
|
DEBUG_F("MQTT received message\n\r Topic: %s\n\r Data: ", topic);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
DEBUG_STREAM.print((char)payload[i]);
|
||||||
|
}
|
||||||
|
DEBUG_STREAM.print("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
StaticJsonDocument<2048> doc;
|
||||||
|
DeserializationError dErr = deserializeJson(doc, (const byte*)payload, length);
|
||||||
|
if (dErr != DeserializationError::Ok || doc.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getTopicPath("state/set").compare(topic) == 0) {
|
||||||
|
updateVariables(doc);
|
||||||
|
client.publish(getTopicPath("state/set").c_str(), NULL, true);
|
||||||
|
|
||||||
|
} else if (getTopicPath("settings/set").compare(topic) == 0) {
|
||||||
|
updateSettings(doc);
|
||||||
|
client.publish(getTopicPath("settings/set").c_str(), NULL, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
542
src/OpenThermTask.h
Normal file
542
src/OpenThermTask.h
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
#include "lib/CustomOpenTherm.h"
|
||||||
|
|
||||||
|
CustomOpenTherm ot(OPENTHERM_IN_PIN, OPENTHERM_OUT_PIN);
|
||||||
|
|
||||||
|
class OpenThermTask : public CustomTask {
|
||||||
|
public:
|
||||||
|
OpenThermTask(bool enabled = false, unsigned long interval = 0) : CustomTask(enabled, interval) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void setup() {
|
||||||
|
ot.begin(handleInterrupt, responseCallback);
|
||||||
|
ot.setHandleSendRequestCallback(sendRequestCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
static byte currentHeatingTemp, currentDHWTemp = 0;
|
||||||
|
byte newHeatingTemp, newDHWTemp = 0;
|
||||||
|
unsigned long localResponse;
|
||||||
|
|
||||||
|
setMasterMemberIdCode();
|
||||||
|
DEBUG_F("Slave member id code: %u \n", vars.parameters.slaveMemberIdCode);
|
||||||
|
|
||||||
|
localResponse = ot.setBoilerStatus(
|
||||||
|
settings.heating.enable && pump,
|
||||||
|
settings.dhw.enable
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ot.isValidResponse(localResponse)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vars.states.heating = ot.isCentralHeatingActive(localResponse);
|
||||||
|
vars.states.dhw = ot.isHotWaterActive(localResponse);
|
||||||
|
vars.states.flame = ot.isFlameOn(localResponse);
|
||||||
|
vars.states.fault = ot.isFault(localResponse);
|
||||||
|
vars.states.diagnostic = ot.isDiagnostic(localResponse);
|
||||||
|
|
||||||
|
/*if (vars.dump_request.value)
|
||||||
|
{
|
||||||
|
testSupportedIDs();
|
||||||
|
vars.dump_request.value = false;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*if ( ot.isValidResponse(localResponse) ) {
|
||||||
|
vars.SlaveMemberIDcode.value = localResponse >> 0 & 0xFF;
|
||||||
|
uint8_t flags = (localResponse & 0xFFFF) >> 8 & 0xFF;
|
||||||
|
vars.dhw_present.value = flags & 0x01;
|
||||||
|
vars.control_type.value = flags & 0x02;
|
||||||
|
vars.cooling_present.value = flags & 0x04;
|
||||||
|
vars.dhw_tank_present.value = flags & 0x08;
|
||||||
|
vars.pump_control_present.value = flags & 0x10;
|
||||||
|
vars.ch2_present.value = flags & 0x20;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// Команды чтения данных котла
|
||||||
|
if (millis() - prevUpdateNonEssentialVars > 30000) {
|
||||||
|
updateSlaveParameters();
|
||||||
|
updateMasterParameters();
|
||||||
|
// crash?
|
||||||
|
DEBUG_F("Master type: %u, version: %u \n", vars.parameters.masterType, vars.parameters.masterVersion);
|
||||||
|
DEBUG_F("Slave type: %u, version: %u \n", vars.parameters.slaveType, vars.parameters.slaveVersion);
|
||||||
|
|
||||||
|
updateMinMaxDhwTemp();
|
||||||
|
updateMinMaxHeatingTemp();
|
||||||
|
if (settings.outdoorTempSource == 0) {
|
||||||
|
updateOutsideTemp();
|
||||||
|
}
|
||||||
|
if (vars.states.fault) {
|
||||||
|
updateFaultCode();
|
||||||
|
}
|
||||||
|
updatePressure();
|
||||||
|
|
||||||
|
prevUpdateNonEssentialVars = millis();
|
||||||
|
}
|
||||||
|
updateHeatingTemp();
|
||||||
|
updateDHWTemp();
|
||||||
|
updateModulationLevel();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Температура ГВС
|
||||||
|
newDHWTemp = settings.dhw.target;
|
||||||
|
if (newDHWTemp != currentDHWTemp) {
|
||||||
|
if (newDHWTemp < vars.parameters.dhwMinTemp || newDHWTemp > vars.parameters.dhwMaxTemp) {
|
||||||
|
newDHWTemp = constrain(newDHWTemp, vars.parameters.dhwMinTemp, vars.parameters.dhwMaxTemp);
|
||||||
|
}
|
||||||
|
|
||||||
|
INFO_F("Set DHW temp = %u \n", newDHWTemp);
|
||||||
|
|
||||||
|
// Записываем заданную температуру ГВС
|
||||||
|
if (ot.setDHWSetpoint(newDHWTemp)) {
|
||||||
|
currentDHWTemp = newDHWTemp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Температура отопления
|
||||||
|
if (fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001) {
|
||||||
|
INFO_F("Set heating temp = %u \n", vars.parameters.heatingSetpoint);
|
||||||
|
|
||||||
|
// Записываем заданную температуру
|
||||||
|
if (ot.setBoilerTemperature(vars.parameters.heatingSetpoint)) {
|
||||||
|
currentHeatingTemp = vars.parameters.heatingSetpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// коммутационная разность (hysteresis)
|
||||||
|
// только для pid и/или equitherm
|
||||||
|
if (settings.heating.hysteresis > 0 && !vars.states.emergency && (settings.equitherm.enable || settings.pid.enable)) {
|
||||||
|
float halfHyst = settings.heating.hysteresis / 2;
|
||||||
|
if (pump && vars.temperatures.indoor - settings.heating.target + 0.0001 >= halfHyst) {
|
||||||
|
pump = false;
|
||||||
|
} else if (!pump && vars.temperatures.indoor - settings.heating.target - 0.0001 <= -(halfHyst)) {
|
||||||
|
pump = true;
|
||||||
|
}
|
||||||
|
} else if (!pump) {
|
||||||
|
pump = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void static IRAM_ATTR handleInterrupt() {
|
||||||
|
ot.handleInterrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
void static sendRequestCallback(unsigned long request, unsigned long response, OpenThermResponseStatus status, byte attempt) {
|
||||||
|
printRequestDetail(ot.getDataID(request), status, request, response, attempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void static responseCallback(unsigned long result, OpenThermResponseStatus status) {
|
||||||
|
static byte attempt = 0;
|
||||||
|
switch (status) {
|
||||||
|
case OpenThermResponseStatus::TIMEOUT:
|
||||||
|
if (++attempt > OPENTHERM_OFFLINE_TRESHOLD) {
|
||||||
|
vars.states.otStatus = false;
|
||||||
|
attempt = OPENTHERM_OFFLINE_TRESHOLD;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OpenThermResponseStatus::SUCCESS:
|
||||||
|
attempt = 0;
|
||||||
|
vars.states.otStatus = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool pump = true;
|
||||||
|
unsigned long prevUpdateNonEssentialVars = 0;
|
||||||
|
|
||||||
|
void static printRequestDetail(OpenThermMessageID id, OpenThermResponseStatus status, unsigned long request, unsigned long response, byte attempt) {
|
||||||
|
sprintf(buffer, "OT REQUEST ID: %4d Request: %8x Response: %8x Attempt: %2d Status: %s", id, request, response, attempt, ot.statusToString(status));
|
||||||
|
if (status != OpenThermResponseStatus::SUCCESS) {
|
||||||
|
//WARN(buffer);
|
||||||
|
DEBUG(buffer);
|
||||||
|
} else {
|
||||||
|
DEBUG(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
bool getBoilerTemp()
|
||||||
|
{
|
||||||
|
unsigned long response;
|
||||||
|
return sendRequest(ot.buildGetBoilerTemperatureRequest(),response);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getDHWTemp()
|
||||||
|
{
|
||||||
|
unsigned long response;
|
||||||
|
unsigned long request = ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Tdhw, 0);
|
||||||
|
return sendRequest(request,response);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getOutsideTemp()
|
||||||
|
{
|
||||||
|
unsigned long response;
|
||||||
|
unsigned long request = ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Toutside, 0);
|
||||||
|
return sendRequest(request,response);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool setDHWTemp(float val)
|
||||||
|
{
|
||||||
|
unsigned long request = ot.buildRequest(OpenThermRequestType::WRITE, OpenThermMessageID::TdhwSet, ot.temperatureToData(val));
|
||||||
|
unsigned long response;
|
||||||
|
return sendRequest(request,response);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getFaultCode()
|
||||||
|
{
|
||||||
|
unsigned long response;
|
||||||
|
unsigned long request = ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0);
|
||||||
|
return sendRequest(request,response);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getModulationLevel() {
|
||||||
|
unsigned long response;
|
||||||
|
unsigned long request = ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0);
|
||||||
|
return sendRequest(request,response);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getPressure() {
|
||||||
|
unsigned long response;
|
||||||
|
unsigned long request = ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0);
|
||||||
|
return sendRequest(request,response);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sendRequest(unsigned long request, unsigned long& response)
|
||||||
|
{
|
||||||
|
send_newts = millis();
|
||||||
|
if (send_newts - send_ts < 200) {
|
||||||
|
// Преждем чем слать что то - надо подождать 100ms согласно специфиикации протокола ОТ
|
||||||
|
delay(200 - (send_newts - send_ts));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = ot.sendRequestAync(request);
|
||||||
|
if(!result) {
|
||||||
|
WARN("Не могу отправить запрос");
|
||||||
|
WARN("Шина " + ot.isReady() ? "готова" : "не готова");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
while (!ot.isReady())
|
||||||
|
{
|
||||||
|
ot.process();
|
||||||
|
yield(); // This is local Task yield() call which allow us to switch to another task in scheduler
|
||||||
|
}
|
||||||
|
send_ts = millis();
|
||||||
|
response = ot_response;
|
||||||
|
//printRequestDetail(ot.getDataID(request), request, response);
|
||||||
|
|
||||||
|
return true; // Response is global variable
|
||||||
|
}
|
||||||
|
|
||||||
|
void testSupportedIDs()
|
||||||
|
{
|
||||||
|
// Basic
|
||||||
|
unsigned long request;
|
||||||
|
unsigned long response;
|
||||||
|
OpenThermMessageID id;
|
||||||
|
//Command
|
||||||
|
id = OpenThermMessageID::Command;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
//ASFlags
|
||||||
|
id = OpenThermMessageID::ASFflags;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//TrOverride
|
||||||
|
id = OpenThermMessageID::TrOverride;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//TSP
|
||||||
|
id = OpenThermMessageID::TSP;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//TSPindexTSPvalue
|
||||||
|
id = OpenThermMessageID::TSPindexTSPvalue;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//FHBsize
|
||||||
|
id = OpenThermMessageID::FHBsize;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//FHBindexFHBvalue
|
||||||
|
id = OpenThermMessageID::FHBindexFHBvalue;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//MaxCapacityMinModLevel
|
||||||
|
id = OpenThermMessageID::MaxCapacityMinModLevel;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//TrSet
|
||||||
|
id = OpenThermMessageID::TrSet;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::WRITE, id, ot.temperatureToData(21));
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//RelModLevel
|
||||||
|
id = OpenThermMessageID::RelModLevel;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//CHPressure
|
||||||
|
id = OpenThermMessageID::CHPressure;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//DHWFlowRate
|
||||||
|
id = OpenThermMessageID::DHWFlowRate;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//DayTime
|
||||||
|
id = OpenThermMessageID::DayTime;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//Date
|
||||||
|
id = OpenThermMessageID::Date;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//Year
|
||||||
|
id = OpenThermMessageID::Year;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//TrSetCH2
|
||||||
|
id = OpenThermMessageID::TrSetCH2;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::WRITE, id, ot.temperatureToData(21));
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//Tr
|
||||||
|
id = OpenThermMessageID::Tr;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::WRITE, id, ot.temperatureToData(21));
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//Tret
|
||||||
|
id = OpenThermMessageID::Tret;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//Texhaust
|
||||||
|
id = OpenThermMessageID::Texhaust;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//Hcratio
|
||||||
|
id = OpenThermMessageID::Hcratio;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//RemoteOverrideFunction
|
||||||
|
id = OpenThermMessageID::RemoteOverrideFunction;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//OEMDiagnosticCode
|
||||||
|
id = OpenThermMessageID::OEMDiagnosticCode;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//BurnerStarts
|
||||||
|
id = OpenThermMessageID::BurnerStarts;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//CHPumpStarts
|
||||||
|
id = OpenThermMessageID::CHPumpStarts;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//DHWPumpValveStarts
|
||||||
|
id = OpenThermMessageID::DHWPumpValveStarts;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//DHWBurnerStarts
|
||||||
|
id = OpenThermMessageID::DHWBurnerStarts;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//BurnerOperationHours
|
||||||
|
id = OpenThermMessageID::BurnerOperationHours;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//CHPumpOperationHours
|
||||||
|
id = OpenThermMessageID::CHPumpOperationHours;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//DHWPumpValveOperationHours
|
||||||
|
id = OpenThermMessageID::DHWPumpValveOperationHours;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
|
||||||
|
//DHWBurnerOperationHours
|
||||||
|
id = OpenThermMessageID::DHWBurnerOperationHours;
|
||||||
|
request = ot.buildRequest(OpenThermRequestType::READ, id, 0);
|
||||||
|
if(sendRequest(request,response))
|
||||||
|
printRequestDetail(id, ot.getLastResponseStatus(), request, response);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
void setMasterMemberIdCode() {
|
||||||
|
//=======================================================================================
|
||||||
|
// Эта группа элементов данных определяет информацию о конфигурации как на ведомых, так
|
||||||
|
// и на главных сторонах. Каждый из них имеет группу флагов конфигурации (8 бит)
|
||||||
|
// и код MemberID (1 байт). Перед передачей информации об управлении и состоянии
|
||||||
|
// рекомендуется обмен сообщениями о допустимой конфигурации ведомого устройства
|
||||||
|
// чтения и основной конфигурации записи. Нулевой код MemberID означает клиентское
|
||||||
|
// неспецифическое устройство. Номер/тип версии продукта следует использовать в сочетании
|
||||||
|
// с "кодом идентификатора участника", который идентифицирует производителя устройства.
|
||||||
|
//=======================================================================================
|
||||||
|
|
||||||
|
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::SConfigSMemberIDcode, 0)); // 0xFFFF
|
||||||
|
if (!ot.isValidResponse(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vars.parameters.slaveMemberIdCode = response >> 0 & 0xFF;
|
||||||
|
ot.sendRequest(ot.buildRequest(OpenThermRequestType::WRITE, OpenThermMessageID::MConfigMMemberIDcode, vars.parameters.slaveMemberIdCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateMasterParameters() {
|
||||||
|
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::WRITE, OpenThermMessageID::MasterVersion, 0x013F));
|
||||||
|
if (!ot.isValidResponse(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vars.parameters.masterType = (response & 0xFFFF) >> 8;
|
||||||
|
vars.parameters.masterVersion = response & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSlaveParameters() {
|
||||||
|
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::SlaveVersion, 0));
|
||||||
|
if (!ot.isValidResponse(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vars.parameters.slaveType = (response & 0xFFFF) >> 8;
|
||||||
|
vars.parameters.slaveVersion = response & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateMinMaxDhwTemp() {
|
||||||
|
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::TdhwSetUBTdhwSetLB, 0));
|
||||||
|
|
||||||
|
if (!ot.isValidResponse(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte minTemp = response & 0xFF;
|
||||||
|
byte maxTemp = (response & 0xFFFF) >> 8;
|
||||||
|
|
||||||
|
if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) {
|
||||||
|
vars.parameters.dhwMinTemp = minTemp;
|
||||||
|
vars.parameters.dhwMaxTemp = maxTemp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateMinMaxHeatingTemp() {
|
||||||
|
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::MaxTSetUBMaxTSetLB, 0));
|
||||||
|
|
||||||
|
if (!ot.isValidResponse(response)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte minTemp = response & 0xFF;
|
||||||
|
byte maxTemp = (response & 0xFFFF) >> 8;
|
||||||
|
|
||||||
|
if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) {
|
||||||
|
vars.parameters.heatingMinTemp = minTemp;
|
||||||
|
vars.parameters.heatingMaxTemp = maxTemp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateOutsideTemp() {
|
||||||
|
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Toutside, 0));
|
||||||
|
|
||||||
|
if (ot.isValidResponse(response)) {
|
||||||
|
vars.temperatures.outdoor = ot.getFloat(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateHeatingTemp() {
|
||||||
|
unsigned long response = ot.sendRequest(ot.buildGetBoilerTemperatureRequest());
|
||||||
|
|
||||||
|
if (ot.isValidResponse(response)) {
|
||||||
|
vars.temperatures.heating = ot.getFloat(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void updateDHWTemp() {
|
||||||
|
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tdhw, 0));
|
||||||
|
|
||||||
|
if (ot.isValidResponse(response)) {
|
||||||
|
vars.temperatures.dhw = ot.getFloat(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFaultCode() {
|
||||||
|
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0));
|
||||||
|
|
||||||
|
if (ot.isValidResponse(response)) {
|
||||||
|
vars.states.faultCode = response & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateModulationLevel() {
|
||||||
|
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0));
|
||||||
|
|
||||||
|
if (ot.isValidResponse(response)) {
|
||||||
|
vars.sensors.modulation = ot.getFloat(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePressure() {
|
||||||
|
unsigned long response = ot.sendRequest(ot.buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0));
|
||||||
|
|
||||||
|
if (ot.isValidResponse(response)) {
|
||||||
|
vars.sensors.pressure = ot.getFloat(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
240
src/RegulatorTask.h
Normal file
240
src/RegulatorTask.h
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
#include "lib/Equitherm.h"
|
||||||
|
#include <GyverPID.h>
|
||||||
|
#include <PIDtuner.h>
|
||||||
|
|
||||||
|
Equitherm etRegulator;
|
||||||
|
GyverPID pidRegulator(0, 0, 0, 10000);
|
||||||
|
PIDtuner pidTuner;
|
||||||
|
|
||||||
|
class RegulatorTask : public MiniTask {
|
||||||
|
public:
|
||||||
|
RegulatorTask(bool enabled = false, unsigned long interval = 0) : MiniTask(enabled, interval) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool tunerInit = false;
|
||||||
|
byte tunerState = 0;
|
||||||
|
float prevHeatingTarget = 0;
|
||||||
|
float prevEtResult = 0;
|
||||||
|
float prevPidResult = 0;
|
||||||
|
|
||||||
|
|
||||||
|
void setup() {}
|
||||||
|
void loop() {
|
||||||
|
byte newTemp;
|
||||||
|
|
||||||
|
if (vars.states.emergency) {
|
||||||
|
newTemp = getEmergencyModeTemp();
|
||||||
|
} else {
|
||||||
|
newTemp = getNormalModeTemp();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем, если до этого не ограничило
|
||||||
|
if (newTemp < vars.parameters.heatingMinTemp || newTemp > vars.parameters.heatingMaxTemp) {
|
||||||
|
newTemp = constrain(newTemp, vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs(vars.parameters.heatingSetpoint - newTemp) + 0.0001 >= 1) {
|
||||||
|
vars.parameters.heatingSetpoint = newTemp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
byte getEmergencyModeTemp() {
|
||||||
|
byte newTemp = vars.parameters.heatingSetpoint;
|
||||||
|
|
||||||
|
// if use equitherm
|
||||||
|
if (settings.emergency.useEquitherm && settings.outdoorTempSource != 1) {
|
||||||
|
etRegulator.Kn = settings.equitherm.n_factor;
|
||||||
|
etRegulator.Kk = settings.equitherm.k_factor;
|
||||||
|
etRegulator.Kt = 0;
|
||||||
|
etRegulator.indoorTemp = 0;
|
||||||
|
etRegulator.outdoorTemp = vars.temperatures.outdoor;
|
||||||
|
|
||||||
|
etRegulator.setLimits(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
|
||||||
|
etRegulator.targetTemp = settings.emergency.target;
|
||||||
|
|
||||||
|
float etResult = etRegulator.getResult();
|
||||||
|
if (fabs(prevEtResult - etResult) + 0.0001 >= 1) {
|
||||||
|
prevEtResult = etResult;
|
||||||
|
newTemp = round(etResult);
|
||||||
|
|
||||||
|
INFO_F("New emergency equitherm result: %u (%f) \n", newTemp, etResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// default temp, manual mode
|
||||||
|
newTemp = round(settings.emergency.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newTemp;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte getNormalModeTemp() {
|
||||||
|
bool updateIntegral = false;
|
||||||
|
byte newTemp = vars.parameters.heatingSetpoint;
|
||||||
|
|
||||||
|
if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001) {
|
||||||
|
prevHeatingTarget = settings.heating.target;
|
||||||
|
updateIntegral = true;
|
||||||
|
|
||||||
|
INFO_F("New heating target: %f \n", settings.heating.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if use equitherm
|
||||||
|
if (settings.equitherm.enable) {
|
||||||
|
if (vars.tuning.enable && vars.tuning.regulator == 0) {
|
||||||
|
if (settings.pid.enable) {
|
||||||
|
settings.pid.enable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
etRegulator.Kn = tuneEquithermN(etRegulator.Kn, vars.temperatures.indoor, settings.heating.target, 300, 1800, 0.01, 1);
|
||||||
|
} else {
|
||||||
|
etRegulator.Kn = settings.equitherm.n_factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.pid.enable) {
|
||||||
|
etRegulator.Kt = 0;
|
||||||
|
etRegulator.indoorTemp = round(vars.temperatures.indoor);
|
||||||
|
etRegulator.outdoorTemp = round(vars.temperatures.outdoor);
|
||||||
|
} else {
|
||||||
|
etRegulator.Kt = settings.equitherm.t_factor;
|
||||||
|
etRegulator.indoorTemp = vars.temperatures.indoor;
|
||||||
|
etRegulator.outdoorTemp = vars.temperatures.outdoor;
|
||||||
|
}
|
||||||
|
|
||||||
|
etRegulator.setLimits(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
|
||||||
|
etRegulator.Kk = settings.equitherm.k_factor;
|
||||||
|
etRegulator.targetTemp = settings.heating.target;
|
||||||
|
|
||||||
|
float etResult = etRegulator.getResult();
|
||||||
|
if (fabs(prevEtResult - etResult) + 0.0001 >= 1) {
|
||||||
|
prevEtResult = etResult;
|
||||||
|
updateIntegral = true;
|
||||||
|
newTemp = round(etResult);
|
||||||
|
|
||||||
|
INFO_F("New equitherm result: %u (%f) \n", newTemp, etResult);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
updateIntegral = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if use pid
|
||||||
|
if (settings.pid.enable && tunerInit && (!vars.tuning.enable || vars.tuning.regulator != 1)) {
|
||||||
|
pidTuner.reset();
|
||||||
|
tunerState = 0;
|
||||||
|
tunerInit = false;
|
||||||
|
INFO(F("Tuning stopped"));
|
||||||
|
|
||||||
|
} else if (settings.pid.enable && vars.tuning.enable && vars.tuning.regulator == 1) {
|
||||||
|
if (tunerInit && pidTuner.getState() == 3) {
|
||||||
|
INFO(F("Tuning finished"));
|
||||||
|
pidTuner.debugText(&INFO_STREAM);
|
||||||
|
|
||||||
|
if (pidTuner.getAccuracy() < 90) {
|
||||||
|
WARN(F("Tuning bad result, restart..."));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
settings.pid.p_factor = pidTuner.getPID_p();
|
||||||
|
settings.pid.i_factor = pidTuner.getPID_i();
|
||||||
|
settings.pid.d_factor = pidTuner.getPID_d();
|
||||||
|
vars.tuning.enable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pidTuner.reset();
|
||||||
|
tunerState = 0;
|
||||||
|
tunerInit = false;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (!tunerInit) {
|
||||||
|
INFO(F("Tuning start"));
|
||||||
|
|
||||||
|
float step;
|
||||||
|
if (vars.temperatures.indoor - vars.temperatures.outdoor > 10) {
|
||||||
|
step = ceil(vars.parameters.heatingSetpoint / vars.temperatures.indoor * 2);
|
||||||
|
} else {
|
||||||
|
step = 5.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float startTemp = newTemp + step;
|
||||||
|
if (startTemp >= vars.parameters.heatingMaxTemp) {
|
||||||
|
startTemp = vars.parameters.heatingMaxTemp - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
INFO_F("Tuning started. Start temp: %f, step: %f \n", startTemp, step);
|
||||||
|
pidTuner.setParameters(NORMAL, startTemp, step, 20 * 60 * 1000, 0.15, 60 * 1000, 10000);
|
||||||
|
tunerInit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pidTuner.setInput(vars.temperatures.indoor);
|
||||||
|
pidTuner.compute();
|
||||||
|
|
||||||
|
if (tunerState > 0 && pidTuner.getState() != tunerState) {
|
||||||
|
INFO(F("Tuning log:"));
|
||||||
|
pidTuner.debugText(&INFO_STREAM);
|
||||||
|
tunerState = pidTuner.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
newTemp = round(pidTuner.getOutput());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.pid.enable && (!vars.tuning.enable || vars.tuning.enable && vars.tuning.regulator != 1)) {
|
||||||
|
if (updateIntegral) {
|
||||||
|
pidRegulator.integral = settings.heating.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
pidRegulator.Kp = settings.pid.p_factor;
|
||||||
|
pidRegulator.Ki = settings.pid.i_factor;
|
||||||
|
pidRegulator.Kd = settings.pid.d_factor;
|
||||||
|
|
||||||
|
pidRegulator.setLimits(vars.parameters.heatingMinTemp, vars.parameters.heatingMaxTemp);
|
||||||
|
pidRegulator.input = vars.temperatures.indoor;
|
||||||
|
pidRegulator.setpoint = settings.heating.target;
|
||||||
|
|
||||||
|
float pidResult = pidRegulator.getResultTimer();
|
||||||
|
if (abs(prevPidResult - pidResult) >= 0.5) {
|
||||||
|
prevPidResult = pidResult;
|
||||||
|
newTemp = round(pidResult);
|
||||||
|
|
||||||
|
INFO_F("New PID result: %u (%f) \n", newTemp, pidResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default temp, manual mode
|
||||||
|
if (!settings.equitherm.enable && !settings.pid.enable) {
|
||||||
|
newTemp = round(settings.heating.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newTemp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float tuneEquithermN(float ratio, float currentTemp, float setTemp, unsigned int dirtyInterval = 60, unsigned int accurateInterval = 1800, float accurateStep = 0.01, float accurateStepAfter = 1) {
|
||||||
|
static uint32_t _prevIteration = millis();
|
||||||
|
|
||||||
|
if (abs(currentTemp - setTemp) < accurateStepAfter) {
|
||||||
|
if (millis() - _prevIteration < (accurateInterval * 1000)) {
|
||||||
|
return ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTemp - setTemp > 0.1f) {
|
||||||
|
ratio -= accurateStep;
|
||||||
|
|
||||||
|
} else if (currentTemp - setTemp < -0.1f) {
|
||||||
|
ratio += accurateStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (millis() - _prevIteration < (dirtyInterval * 1000)) {
|
||||||
|
return ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
ratio = ratio * (setTemp / currentTemp);
|
||||||
|
}
|
||||||
|
|
||||||
|
_prevIteration = millis();
|
||||||
|
return ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
27
src/SensorsTask.h
Normal file
27
src/SensorsTask.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#include <microDS18B20.h>
|
||||||
|
|
||||||
|
MicroDS18B20<DS18B20_PIN> outdoorSensor;
|
||||||
|
|
||||||
|
class SensorsTask : public MiniTask {
|
||||||
|
public:
|
||||||
|
SensorsTask(bool enabled = false, unsigned long interval = 0) : MiniTask(enabled, interval) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void setup() {}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// DS18B20 sensor
|
||||||
|
if (outdoorSensor.online()) {
|
||||||
|
if (outdoorSensor.readTemp()) {
|
||||||
|
vars.temperatures.outdoor = outdoorSensor.getTemp();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
DEBUG("Invalid data from outdoor sensor (DS18B20)");
|
||||||
|
}
|
||||||
|
|
||||||
|
outdoorSensor.requestTemp();
|
||||||
|
} else {
|
||||||
|
WARN("Failed to connect to outdoor sensor (DS18B20)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
90
src/Settings.h
Normal file
90
src/Settings.h
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
struct Settings {
|
||||||
|
bool debug = false;
|
||||||
|
// 0 - boiler, 1 - manual, 2 - ds18b20
|
||||||
|
byte outdoorTempSource = 0;
|
||||||
|
char hostname[80] = "opentherm";
|
||||||
|
|
||||||
|
struct {
|
||||||
|
char server[80];
|
||||||
|
int port = 1883;
|
||||||
|
char user[32];
|
||||||
|
char password[32];
|
||||||
|
char prefix[80] = "opentherm";
|
||||||
|
unsigned int interval = 5000;
|
||||||
|
} mqtt;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool enable = true;
|
||||||
|
float target = 40.0f;
|
||||||
|
bool useEquitherm = false;
|
||||||
|
} emergency;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool enable = true;
|
||||||
|
float target = 40.0f;
|
||||||
|
float hysteresis = 0.5f;
|
||||||
|
} heating;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool enable = true;
|
||||||
|
byte target = 40;
|
||||||
|
} dhw;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool enable = false;
|
||||||
|
float p_factor = 3;
|
||||||
|
float i_factor = 0.2f;
|
||||||
|
float d_factor = 0;
|
||||||
|
} pid;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool enable = false;
|
||||||
|
float n_factor = 0.67f;
|
||||||
|
float k_factor = 1.0f;
|
||||||
|
float t_factor = 0.0f;
|
||||||
|
} equitherm;
|
||||||
|
|
||||||
|
} settings;
|
||||||
|
|
||||||
|
struct Variables {
|
||||||
|
struct {
|
||||||
|
bool enable = false;
|
||||||
|
byte regulator = 0;
|
||||||
|
} tuning;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool otStatus = false;
|
||||||
|
bool emergency = false;
|
||||||
|
bool heating = false;
|
||||||
|
bool dhw = false;
|
||||||
|
bool flame = false;
|
||||||
|
bool fault = false;
|
||||||
|
bool diagnostic = false;
|
||||||
|
byte faultCode = 0;
|
||||||
|
} states;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
float modulation = 0.0f;
|
||||||
|
float pressure = 0.0f;
|
||||||
|
} sensors;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
float indoor = 0.0f;
|
||||||
|
float outdoor = 0.0f;
|
||||||
|
float heating = 0.0f;
|
||||||
|
float dhw = 0.0f;
|
||||||
|
} temperatures;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
byte heatingMinTemp = 20;
|
||||||
|
byte heatingMaxTemp = 90;
|
||||||
|
byte heatingSetpoint = 0.0f;
|
||||||
|
byte dhwMinTemp = 30;
|
||||||
|
byte dhwMaxTemp = 60;
|
||||||
|
uint8_t slaveMemberIdCode;
|
||||||
|
uint8_t slaveType;
|
||||||
|
uint8_t slaveVersion;
|
||||||
|
uint8_t masterType;
|
||||||
|
uint8_t masterVersion;
|
||||||
|
} parameters;
|
||||||
|
} vars;
|
||||||
77
src/WifiManagerTask.h
Normal file
77
src/WifiManagerTask.h
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// #include <WiFiClient.h>
|
||||||
|
#define WM_MDNS
|
||||||
|
#include <WiFiManager.h>
|
||||||
|
//#include <ESP8266mDNS.h>
|
||||||
|
//#include <WiFiUdp.h>
|
||||||
|
|
||||||
|
// Wifimanager
|
||||||
|
WiFiManager wm;
|
||||||
|
WiFiManagerParameter *wmHostname;
|
||||||
|
WiFiManagerParameter *wmMqttServer;
|
||||||
|
WiFiManagerParameter *wmMqttPort;
|
||||||
|
WiFiManagerParameter *wmMqttUser;
|
||||||
|
WiFiManagerParameter *wmMqttPassword;
|
||||||
|
WiFiManagerParameter *wmMqttPrefix;
|
||||||
|
|
||||||
|
class WifiManagerTask : public CustomTask {
|
||||||
|
public:
|
||||||
|
WifiManagerTask(bool enabled = false, unsigned long interval = 0) : CustomTask(enabled, interval) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void setup() {
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
wm.setDebugOutput(settings.debug);
|
||||||
|
|
||||||
|
wmHostname = new WiFiManagerParameter("hostname", "Hostname", settings.hostname, 80);
|
||||||
|
wm.addParameter(wmHostname);
|
||||||
|
|
||||||
|
wmMqttServer = new WiFiManagerParameter("mqtt_server", "MQTT server", settings.mqtt.server, 80);
|
||||||
|
wm.addParameter(wmMqttServer);
|
||||||
|
|
||||||
|
//char mqttPort[6];
|
||||||
|
sprintf(buffer, "%d", settings.mqtt.port);
|
||||||
|
wmMqttPort = new WiFiManagerParameter("mqtt_port", "MQTT port", buffer, 6);
|
||||||
|
wm.addParameter(wmMqttPort);
|
||||||
|
|
||||||
|
wmMqttUser = new WiFiManagerParameter("mqtt_user", "MQTT username", settings.mqtt.user, 32);
|
||||||
|
wm.addParameter(wmMqttUser);
|
||||||
|
|
||||||
|
wmMqttPassword = new WiFiManagerParameter("mqtt_password", "MQTT password", settings.mqtt.password, 32);
|
||||||
|
wm.addParameter(wmMqttPassword);
|
||||||
|
|
||||||
|
wmMqttPrefix = new WiFiManagerParameter("mqtt_prefix", "MQTT prefix", settings.mqtt.prefix, 32);
|
||||||
|
wm.addParameter(wmMqttPrefix);
|
||||||
|
|
||||||
|
wm.setHostname(settings.hostname);
|
||||||
|
wm.setWiFiAutoReconnect(true);
|
||||||
|
wm.setConfigPortalBlocking(false);
|
||||||
|
wm.setSaveParamsCallback(saveParamsCallback);
|
||||||
|
wm.setConfigPortalTimeout(300);
|
||||||
|
wm.setDisableConfigPortal(false);
|
||||||
|
|
||||||
|
if (wm.autoConnect(AP_SSID)) {
|
||||||
|
INFO_F("Wifi connected. IP: %s, RSSI: %d\n", WiFi.localIP().toString().c_str(), WiFi.RSSI());
|
||||||
|
wm.startWebPortal();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
INFO(F("Failed to connect to WIFI, start the configuration portal..."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
wm.process();
|
||||||
|
}
|
||||||
|
|
||||||
|
void static saveParamsCallback() {
|
||||||
|
strcpy(settings.hostname, (*wmHostname).getValue());
|
||||||
|
strcpy(settings.mqtt.server, (*wmMqttServer).getValue());
|
||||||
|
settings.mqtt.port = atoi((*wmMqttPort).getValue());
|
||||||
|
strcpy(settings.mqtt.user, (*wmMqttUser).getValue());
|
||||||
|
strcpy(settings.mqtt.password, (*wmMqttPassword).getValue());
|
||||||
|
strcpy(settings.mqtt.prefix, (*wmMqttPrefix).getValue());
|
||||||
|
|
||||||
|
INFO_F("Settings\nHostname: %s, Server: %s, port: %d, user: %s, pass: %s\n", settings.hostname, settings.mqtt.server, settings.mqtt.port, settings.mqtt.user, settings.mqtt.password);
|
||||||
|
eeSettings.updateNow();
|
||||||
|
INFO(F("Settings saved"));
|
||||||
|
}
|
||||||
|
};
|
||||||
43
src/defines.h
Normal file
43
src/defines.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#define OT_GATEWAY_VERSION "1.0.4"
|
||||||
|
#define AP_SSID "OpenTherm Gateway"
|
||||||
|
//#define USE_TELNET
|
||||||
|
|
||||||
|
#define EMERGENCY_TRESHOLD 10
|
||||||
|
#define MQTT_RECONNECT_INTERVAL 5000
|
||||||
|
#define MQTT_KEEPALIVE 30
|
||||||
|
|
||||||
|
#define OPENTHERM_IN_PIN 4
|
||||||
|
#define OPENTHERM_OUT_PIN 5
|
||||||
|
#define OPENTHERM_OFFLINE_TRESHOLD 10
|
||||||
|
|
||||||
|
#define DS18B20_PIN 2
|
||||||
|
#define DS18B20_INTERVAL 1000
|
||||||
|
#define DS_CHECK_CRC true
|
||||||
|
#define DS_CRC_USE_TABLE true
|
||||||
|
|
||||||
|
#define CONFIG_URL "http://%s/"
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef USE_TELNET
|
||||||
|
#define INFO_STREAM TelnetStream
|
||||||
|
#define WARN_STREAM TelnetStream
|
||||||
|
#define ERROR_STREAM TelnetStream
|
||||||
|
#define DEBUG_STREAM if (settings.debug) TelnetStream
|
||||||
|
#define WM_DEBUG_PORT TelnetStream
|
||||||
|
#else
|
||||||
|
#define INFO_STREAM Serial
|
||||||
|
#define WARN_STREAM Serial
|
||||||
|
#define ERROR_STREAM Serial
|
||||||
|
#define DEBUG_STREAM if (settings.debug) Serial
|
||||||
|
#define WM_DEBUG_PORT Serial
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define INFO(...) INFO_STREAM.print("\r[INFO] "); INFO_STREAM.println(__VA_ARGS__);
|
||||||
|
#define INFO_F(...) INFO_STREAM.print("\r[INFO] "); INFO_STREAM.printf(__VA_ARGS__);
|
||||||
|
#define WARN(...) WARN_STREAM.print("\r[WARN] "); WARN_STREAM.println(__VA_ARGS__);
|
||||||
|
#define WARN_F(...) WARN_STREAM.print("\r[WARN] "); WARN_STREAM.printf(__VA_ARGS__);
|
||||||
|
#define ERROR(...) ERROR_STREAM.print("\r[ERROR] "); ERROR_STREAM.println(__VA_ARGS__);
|
||||||
|
#define DEBUG(...) DEBUG_STREAM.print("\r[DEBUG] "); DEBUG_STREAM.println(__VA_ARGS__);
|
||||||
|
#define DEBUG_F(...) DEBUG_STREAM.print("\r[DEBUG] "); DEBUG_STREAM.printf(__VA_ARGS__);
|
||||||
|
|
||||||
|
char buffer[120];
|
||||||
46
src/lib/CustomOpenTherm.h
Normal file
46
src/lib/CustomOpenTherm.h
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#include <OpenTherm.h>
|
||||||
|
|
||||||
|
extern SchedulerClass Scheduler;
|
||||||
|
|
||||||
|
class CustomOpenTherm : public OpenTherm {
|
||||||
|
private:
|
||||||
|
unsigned long send_ts = millis();
|
||||||
|
void(*handleSendRequestCallback)(unsigned long, unsigned long, OpenThermResponseStatus status, byte attempt);
|
||||||
|
|
||||||
|
public:
|
||||||
|
CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false) : OpenTherm(inPin, outPin, isSlave) {}
|
||||||
|
void setHandleSendRequestCallback(void(*handleSendRequestCallback)(unsigned long, unsigned long, OpenThermResponseStatus status, byte attempt)) {
|
||||||
|
this->handleSendRequestCallback = handleSendRequestCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long sendRequest(unsigned long request, byte attempts = 5, byte _attempt = 0) {
|
||||||
|
_attempt++;
|
||||||
|
while (send_ts > 0 && millis() - send_ts < 200) {
|
||||||
|
Scheduler.yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
//unsigned long response = OpenTherm::sendRequest(request);
|
||||||
|
unsigned long _response;
|
||||||
|
if (!sendRequestAync(request)) {
|
||||||
|
_response = 0;
|
||||||
|
} else {
|
||||||
|
while (!isReady()) {
|
||||||
|
Scheduler.yield();
|
||||||
|
process();
|
||||||
|
}
|
||||||
|
|
||||||
|
_response = getLastResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleSendRequestCallback != NULL) {
|
||||||
|
handleSendRequestCallback(request, _response, getLastResponseStatus(), _attempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
send_ts = millis();
|
||||||
|
if (getLastResponseStatus() == OpenThermResponseStatus::SUCCESS || _attempt >= attempts) {
|
||||||
|
return _response;
|
||||||
|
} else {
|
||||||
|
return sendRequest(request, attempts, _attempt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
45
src/lib/CustomTask.h
Normal file
45
src/lib/CustomTask.h
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
class CustomTask : public Task {
|
||||||
|
public:
|
||||||
|
CustomTask(bool enabled = false, unsigned long interval = 0) {
|
||||||
|
_enabled = enabled;
|
||||||
|
_interval = interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEnabled() {
|
||||||
|
return _enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enable() {
|
||||||
|
_enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable() {
|
||||||
|
_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setInterval(unsigned long val) {
|
||||||
|
_interval = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long getInterval() {
|
||||||
|
return _interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool _enabled = true;
|
||||||
|
unsigned long _lastRun = 0;
|
||||||
|
unsigned long _interval = 0;
|
||||||
|
|
||||||
|
bool shouldRun() {
|
||||||
|
if (!_enabled || !Task::shouldRun()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_interval > 0 && millis() - _lastRun < _interval) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastRun = millis();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
63
src/lib/Equitherm.h
Normal file
63
src/lib/Equitherm.h
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#if defined(EQUITHERM_INTEGER)
|
||||||
|
// расчёты с целыми числами
|
||||||
|
typedef int datatype;
|
||||||
|
#else
|
||||||
|
// расчёты с float числами
|
||||||
|
typedef float datatype;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class Equitherm {
|
||||||
|
public:
|
||||||
|
Equitherm() {}
|
||||||
|
|
||||||
|
// kn, kk, kt
|
||||||
|
Equitherm(float new_kn, float new_kk, float new_kt) {
|
||||||
|
Kn = new_kn;
|
||||||
|
Kk = new_kk;
|
||||||
|
Kt = new_kt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// лимит выходной величины
|
||||||
|
void setLimits(int min_output, int max_output) {
|
||||||
|
_minOut = min_output;
|
||||||
|
_maxOut = max_output;
|
||||||
|
}
|
||||||
|
|
||||||
|
datatype targetTemp = 0;
|
||||||
|
datatype indoorTemp = 0;
|
||||||
|
datatype outdoorTemp = 0;
|
||||||
|
float Kn = 0.0;
|
||||||
|
float Kk = 0.0;
|
||||||
|
float Kt = 0.0;
|
||||||
|
|
||||||
|
// возвращает новое значение при вызове
|
||||||
|
datatype getResult() {
|
||||||
|
datatype output = getResultN() + getResultK() + getResultT();
|
||||||
|
output = constrain(output, _minOut, _maxOut); // ограничиваем выход
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// температура контура отопления в зависимости от наружной температуры
|
||||||
|
datatype getResultN() {
|
||||||
|
float a = (-0.21 * Kn) - 0.06; // a = -0,21k — 0,06
|
||||||
|
float b = (6.04 * Kn) + 1.98; // b = 6,04k + 1,98
|
||||||
|
float c = (-5.06 * Kn) + 18.06; // с = -5,06k + 18,06
|
||||||
|
float x = (-0.2 * outdoorTemp) + 5; // x = -0.2*t1 + 5
|
||||||
|
return (a * x * x) + (b * x) + c; // Tn = ax2 + bx + c
|
||||||
|
}
|
||||||
|
|
||||||
|
// поправка на желаемую комнатную температуру
|
||||||
|
datatype getResultK() {
|
||||||
|
return (targetTemp - 20) * Kk;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Расчет поправки (ошибки) термостата
|
||||||
|
datatype getResultT() {
|
||||||
|
return (targetTemp - indoorTemp) * Kt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
int _minOut = 20, _maxOut = 90;
|
||||||
|
};
|
||||||
64
src/lib/MiniTask.h
Normal file
64
src/lib/MiniTask.h
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
class MiniTask {
|
||||||
|
public:
|
||||||
|
MiniTask(bool enabled = false, unsigned long interval = 0) {
|
||||||
|
_enabled = enabled;
|
||||||
|
_interval = interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEnabled() {
|
||||||
|
return _enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enable() {
|
||||||
|
_enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable() {
|
||||||
|
_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setInterval(unsigned long val) {
|
||||||
|
_interval = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long getInterval() {
|
||||||
|
return _interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
void loopWrapper() {
|
||||||
|
if (!shouldRun()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_setupDone) {
|
||||||
|
setup();
|
||||||
|
_setupDone = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
loop();
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void setup() {}
|
||||||
|
virtual void loop() {}
|
||||||
|
|
||||||
|
virtual bool shouldRun() {
|
||||||
|
if (!_enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_interval > 0 && millis() - _lastRun < _interval) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastRun = millis();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool _enabled = false;
|
||||||
|
unsigned long _interval = 0;
|
||||||
|
unsigned long _lastRun = 0;
|
||||||
|
bool _setupDone = false;
|
||||||
|
};
|
||||||
58
src/src.ino
Normal file
58
src/src.ino
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#include "defines.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <TelnetStream.h>
|
||||||
|
#include <EEManager.h>
|
||||||
|
#include <Scheduler.h>
|
||||||
|
#include "Settings.h"
|
||||||
|
|
||||||
|
EEManager eeSettings(settings, 30000);
|
||||||
|
|
||||||
|
#include "lib/CustomTask.h"
|
||||||
|
#include "WifiManagerTask.h"
|
||||||
|
#include "MqttTask.h"
|
||||||
|
#include "OpenThermTask.h"
|
||||||
|
#include "MainTask.h"
|
||||||
|
|
||||||
|
// Tasks
|
||||||
|
WifiManagerTask* tWm;
|
||||||
|
MqttTask* tMqtt;
|
||||||
|
OpenThermTask* tOt;
|
||||||
|
MainTask* tMain;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
#ifdef USE_TELNET
|
||||||
|
TelnetStream.begin();
|
||||||
|
delay(5000);
|
||||||
|
#else
|
||||||
|
Serial.begin(115200);
|
||||||
|
Serial.println("\n\n");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
EEPROM.begin(eeSettings.blockSize());
|
||||||
|
uint8_t eeSettingsResult = eeSettings.begin(0, 's');
|
||||||
|
if (eeSettingsResult == 0) {
|
||||||
|
INFO("Settings loaded");
|
||||||
|
|
||||||
|
} else if (eeSettingsResult == 1) {
|
||||||
|
INFO("Settings NOT loaded, first start");
|
||||||
|
|
||||||
|
} else if (eeSettingsResult == 2) {
|
||||||
|
INFO("Settings NOT loaded (error)");
|
||||||
|
}
|
||||||
|
|
||||||
|
tWm = new WifiManagerTask(true);
|
||||||
|
Scheduler.start(tWm);
|
||||||
|
|
||||||
|
tMqtt = new MqttTask(false);
|
||||||
|
Scheduler.start(tMqtt);
|
||||||
|
|
||||||
|
tOt = new OpenThermTask(true);
|
||||||
|
Scheduler.start(tOt);
|
||||||
|
|
||||||
|
tMain = new MainTask(true);
|
||||||
|
Scheduler.start(tMain);
|
||||||
|
|
||||||
|
Scheduler.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {}
|
||||||
Reference in New Issue
Block a user