first commit

This commit is contained in:
Yurii
2022-06-26 07:59:16 +03:00
commit 72d454cf57
16 changed files with 3135 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/.vscode
/bin/*
!/bin/src.ino.bin

BIN
bin/src.ino.bin Normal file

Binary file not shown.

1280
src/HomeAssistantHelper.h Normal file

File diff suppressed because it is too large Load Diff

83
src/MainTask.h Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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() {}