smartBoiler

This commit is contained in:
Mit4el
2024-02-12 20:49:36 +03:00
parent d05137b4d4
commit 15de2955fb
9 changed files with 2924 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
#pragma once
#define SLAVE true
#define TIMEOUT_TRESHOLD 5
namespace _Boiler
{
// команды/установки от термостата
struct SetpointBoiler
{
uint8_t cmd_chEnable = 0;
uint8_t cmd_dhwEnable = 0;
float TSetCH = 0;
float TSetDhw = 0;
} set;
struct failCode
{
bool service_required = 0;
bool lockout_reset = 0;
bool low_water_pressure = 0;
bool gas_fault = 0;
bool air_fault = 0;
bool water_overtemp = 0;
uint8_t fault_code = 0;
};
// текущее реальное состояние котла
struct StateBoiler
{
uint8_t stateCH = 0;
uint8_t stateDHW = 0;
uint8_t fl_flame = 0;
uint8_t currentRele = 0;
uint8_t fl_fail = 0;
failCode fCode;
float RelModLevel = 0;
float Tboiler = -40;
float Tret = 0;
float Tdhw = 0;
float Toutside = 0;
bool r[3] = {0, 0, 0};
} state;
// конфигурация котла
struct ConfigBoiler
{
int antiFreez;
bool pump = false; // 1- наличие реле насоса СО, 0 - мы не управляем насосом СО (в протоколе ОТ нет)
bool changeRele = false;
// bool dhw = false; // 1- есть реле(трехходовой) ГВС
bool ctrlType = false; // 0 - модуляция, 1- вкл/выкл
bool confDhw = false; // 1 - бак, 0 - проточная //TODO ПОКА НЕ ЗНАЮ ЧТО ДЕЛАТЬ
bool pumpControlMaster = false; // в протоколе ОТ: мастер управляет насосом ????????????????????? //TODO Команды кправления насосом от мастера не помню
int minDhw;
int maxDhw;
int minCH;
int maxCH;
int gistDhw;
int gistCH;
int countRele = 0;
int relePwr[3]={0,0,0};
int prcOnekWt = 0; // процент одного киловата из общей мощности всех тэнев, расчитывается для модуляции
// int rele2Pwr = 0;
// int rele3Pwr = 0;
} conf;
unsigned long timeout_count = 0;
int _debug = 0;
bool _telegram = false;
unsigned long ot_response = 0;
}

View File

@@ -0,0 +1,582 @@
#include "Global.h"
#include "classes/IoTItem.h"
#include <Arduino.h>
#include "BoilerHeader.h"
namespace _Boiler
{
DynamicJsonDocument OpenThemData(JSON_BUFFER_SIZE / 2);
IoTItem *_idPID = nullptr;
IoTItem *_idTboiler = nullptr;
IoTItem *_idTret = nullptr;
IoTItem *_idToutside = nullptr;
IoTItem *_idStateCH = nullptr;
IoTItem *_idStateFlame = nullptr;
IoTItem *_idModLevel = nullptr;
IoTItem *_idCmdCH = nullptr;
IoTItem *_idSetCH = nullptr;
IoTItem *_idCtrlType = nullptr;
int pid;
IoTItem *rele[3];
IoTItem *relePump;
IoTItem *releDhw;
IoTItem *tmp;
// проверяем если пришедшее значение отличается от предыдущего регистрируем событие
void publishNew(String widget, String value)
{
if (OpenThemData[widget] != value)
{
OpenThemData[widget] = value;
tmp = findIoTItem(widget);
if (tmp)
{
tmp->setValue(value, true);
}
else
{
if (_debug > 0)
SerialPrint("new", "SmartBoiler", widget + " = " + value);
}
}
}
void sendTelegramm(String msg)
{
if (_telegram == 1)
{
if (tlgrmItem)
tlgrmItem->sendTelegramMsg(false, msg);
}
}
/*
* =========================================================================================
* КЛАСС УПРАВЛЕНИЯ ГВС
* =========================================================================================
*/
IoTItem *_idTdhw = nullptr;
IoTItem *_idStateDHW = nullptr;
IoTItem *_idCmdDHW = nullptr;
IoTItem *_idTDhw = nullptr;
IoTItem *_idSetDHW = nullptr;
class DHWControl : public IoTItem
{
private:
unsigned long ts = 0;
public:
DHWControl(String parameters) : IoTItem(parameters)
{
SerialPrint("i", F("DHWControl"), " START... ");
String tmpID;
jsonRead(parameters, "idReleDhw", tmpID);
releDhw = findIoTItem(tmpID);
jsonRead(parameters, "idSetDHW", tmpID);
_idSetDHW = findIoTItem(tmpID);
jsonRead(parameters, "idStateDHW", tmpID);
_idStateDHW = findIoTItem(tmpID);
jsonRead(parameters, "idCmdDHW", tmpID);
_idCmdDHW = findIoTItem(tmpID);
jsonRead(parameters, "idTDhw", tmpID);
_idTDhw = findIoTItem(tmpID);
jsonRead(parameters, "minDhw", conf.minDhw);
jsonRead(parameters, "maxDhw", conf.maxDhw);
jsonRead(parameters, "gistDhw", conf.gistDhw);
// releDhw = findIoTItem("releDhw");
if (releDhw)
{
SerialPrint("i", "DHWControl", "Инициализировано РЕЛЕ ГВС");
}
// dhw_ctrl = this;
}
// ============================== ЛОГИКА РАБОТЫ КОТЛА И ВКЛЮЧЕНИЯ ТЭНОВ ======================================
// Работы котла и включения тэнов
static void logicPowerDhw()
{
if (_idCmdDHW)
set.cmd_dhwEnable = ::atoi(_idCmdDHW->getValue().c_str());
if (_idTDhw)
state.Tdhw = ::atof(_idTDhw->getValue().c_str());
if (_idSetDHW)
set.TSetDhw = ::atof(_idSetDHW->getValue().c_str());
if (set.cmd_dhwEnable)
{
if (releDhw)
{
if (_idTDhw)
state.Tdhw = ::atof(_idTDhw->getValue().c_str());
if (set.TSetDhw - state.Tdhw >= conf.gistDhw && state.Tdhw > -5 && state.Tdhw < 120)
{
// включаем ГВС
releDhw->setValue("1", true);
state.stateDHW = 1;
state.stateCH = 0;
for (uint8_t i = 0; i < conf.countRele; i++)
{
state.r[i] = true;
}
state.RelModLevel = 100;
}
else
{
releDhw->setValue("0", true);
state.stateDHW = 0;
state.RelModLevel = 0;
}
}
}
else
{
if (releDhw)
releDhw->setValue("0", true);
}
// publishNew("StateDHW", String(state.stateDHW));
IoTValue val;
if (_idStateDHW)
{
val.valD = state.stateDHW;
_idStateDHW->setValue(val, true);
}
}
void doByInterval()
{
}
// Комманды из сценария
IoTValue execute(String command, std::vector<IoTValue> &param)
{
IoTValue val;
if (command == "SetDHW")
{
set.TSetDhw = param[0].valD;
set.TSetDhw = constrain(set.TSetDhw, conf.minDhw, conf.maxDhw);
// publishNew("SetDHW", String(set.TSetDhw));
val.valD = set.TSetDhw;
if (_idSetDHW)
_idSetDHW->setValue(val, true);
SerialPrint("i", "DHWControl", "Scenario DHWSet ");
}
else if (command == "DHWEnable")
{
set.cmd_dhwEnable = param[0].valD;
val.valD = set.cmd_dhwEnable;
if (_idCmdDHW)
_idCmdDHW->setValue(val, true);
SerialPrint("i", "DHWControl", "Scenario DHWEnable ");
}
return {};
}
~DHWControl()
{
}
};
/*
* =========================================================================================
* КЛАСС УПРАВЛЕНИЯ КОТЛОМ
* =========================================================================================
*/
class BoilerControl : public IoTItem
{
private:
unsigned long ts = 0;
public:
BoilerControl(String parameters) : IoTItem(parameters)
{
SerialPrint("i", F("BoilerControl"), " START... ");
jsonRead(parameters, "LogLevel", _debug);
jsonRead(parameters, "telegram", _telegram);
String tmpID;
jsonRead(parameters, "idPID", tmpID);
_idPID = findIoTItem(tmpID);
jsonRead(parameters, "idTboiler", tmpID);
_idTboiler = findIoTItem(tmpID);
jsonRead(parameters, "idTret", tmpID);
_idTret = findIoTItem(tmpID);
jsonRead(parameters, "idToutside", tmpID);
_idToutside = findIoTItem(tmpID);
jsonRead(parameters, "idStateCH", tmpID);
_idStateCH = findIoTItem(tmpID);
jsonRead(parameters, "idStateFlame", tmpID);
_idStateFlame = findIoTItem(tmpID);
jsonRead(parameters, "idModLevel", tmpID);
_idModLevel = findIoTItem(tmpID);
jsonRead(parameters, "idCmdCH", tmpID);
_idCmdCH = findIoTItem(tmpID);
jsonRead(parameters, "idSetCH", tmpID);
_idSetCH = findIoTItem(tmpID);
jsonRead(parameters, "idCtrlType", tmpID);
_idCtrlType = findIoTItem(tmpID);
jsonRead(parameters, "rele1_Pwr", conf.relePwr[0]);
jsonRead(parameters, "rele2_Pwr", conf.relePwr[1]);
jsonRead(parameters, "rele3_Pwr", conf.relePwr[2]);
jsonRead(parameters, "Pump", conf.pump);
jsonRead(parameters, "changeRele", conf.changeRele);
jsonRead(parameters, "minCH", conf.minCH);
jsonRead(parameters, "maxCH", conf.maxCH);
jsonRead(parameters, "gistCH", conf.gistCH);
jsonRead(parameters, "antiFreez", conf.antiFreez);
configuration();
}
// ============================== ЛОГИКА РАБОТЫ КОТЛА И ВКЛЮЧЕНИЯ ТЭНОВ ======================================
// Работы котла и включения тэнов
static void logicPowerOn()
{
// TODO ВКЛЮЧЕНИЕ РЕЛЕ ПО ИХ МОЩНОСТИ (СДЕЛАТЬ ШАГИ НАГРЕВА И КОМБИНАЦИИ ВКЛБЧЕНИЯ ТЕНОВ С РАЗНОЙ МОЩНОСТЬЮ)
for (uint8_t i = 0; i < conf.countRele; i++)
{
state.r[i] = false;
}
// сейчас включаются по порядку
state.RelModLevel = 0;
pid = 0;
state.fl_flame = false;
if (_idPID)
pid = ::atoi(_idPID->getValue().c_str());
// обнуляем ГВС
state.stateDHW = 0;
if (state.Tboiler < conf.maxCH)
{
// if (dhw_ctrl)
//{
// если есть модуль ГВС, то вызываем его логику включения тэнов
DHWControl::logicPowerDhw();
//}
if (!state.stateDHW) // Если уже включено ГВС, то нечего смотреть на отопление
{
if (set.cmd_chEnable)
{
publishNew("status", "Штатный режим");
// включаем отопление
state.stateCH = 1;
if (state.Tboiler < (set.TSetCH + conf.gistCH) && /*set.TSetCH - state.Tboiler >= conf.gistCH &&*/ state.Tboiler > -5 && state.Tboiler < 120)
{
if (conf.ctrlType == 0)
{ // режим модуляции, это когда есть модуль ПИД и более одного реле
if (pid > 0)
{
state.r[state.currentRele] = true;
state.RelModLevel = conf.prcOnekWt * conf.relePwr[state.currentRele];
}
if (pid > 30 && pid <= 60)
{
uint8_t next = state.currentRele + 1;
if (next >= conf.countRele)
next = 0;
state.r[next] = true;
state.RelModLevel = (conf.prcOnekWt * conf.relePwr[state.currentRele]) + (conf.prcOnekWt * conf.relePwr[next]);
}
if (pid > 60)
{
for (uint8_t i = 0; i < conf.countRele; i++)
{
state.r[i] = true;
}
state.RelModLevel = 100;
}
}
else
{ // у нас релейный режим без ПИДа или с одним реле(тэном)
// TODO СЕЙЧАС ВКЛЮЧАЕМ ВСЕ РЕЛЕ, НАДО ЧТО ТО ПРИДУМАТЬ УМНОЕ
for (uint8_t i = 0; i < conf.countRele; i++)
{
state.r[i] = true;
}
state.RelModLevel = 100;
}
}
else
{
state.RelModLevel = 0;
}
}
else
{
publishNew("status", "Выкл отопление");
state.stateCH = 0;
}
}
}
if (conf.antiFreez > 0 && state.Tboiler < (conf.antiFreez + 4))
{
state.r[state.currentRele] = true;
state.RelModLevel = conf.prcOnekWt * conf.relePwr[state.currentRele];
// setValue("Анти-Заморозка");
publishNew("status", "Режим анти-заморозка");
SerialPrint("i", "BoilerControl", "Режим анти-заморозка");
sendTelegramm("Режим анти-заморозка");
}
static bool prev_flame = false;
if (state.RelModLevel > 0)
state.fl_flame = true; // если хоть одно реле включено, то выставляем флаг горелки
if (state.fl_flame && prev_flame != state.fl_flame)
{
if (conf.changeRele)
{
state.currentRele++;
if (state.currentRele >= conf.countRele)
state.currentRele = 0;
}
}
prev_flame = state.fl_flame;
bool fl_pump = false;
// переключаем реле в соответсии с их статусами
for (uint8_t i = 0; i < conf.countRele; i++)
{
if (rele[i])
{
rele[i]->setValue(state.r[i] ? "1" : "0", true);
}
if (!fl_pump) // что бы не обнулить если выстиавили true
fl_pump = state.r[i] ? true : false; // если хоть одно реле включено, то включаем насос
}
if (fl_pump)
{ // если хоть одно реле включено, то включаем насос
if (relePump)
relePump->setValue("1", true);
}
else
{
if (relePump)
{ // если все реле выключены
if (state.Tboiler > conf.minCH && set.cmd_chEnable) // НО температура ещё горячая и при этом отопление включено, то включаем насос
relePump->setValue("1", true);
else // Если температура в котле уже остыла, или отопление нам не нужно (летом нагрели воду-пусть котел сам остывает без насоса), то выключаем насос
relePump->setValue("0", true);
}
}
IoTValue val;
if (_idModLevel)
{
val.valD = state.RelModLevel;
_idModLevel->setValue(val, true);
}
if (_idStateCH)
{
val.valD = state.stateCH;
_idStateCH->setValue(val, true);
}
if (_idStateFlame)
{
val.valD = state.fl_flame;
_idStateFlame->setValue(val, true);
}
// publishNew("ModLevel", String(state.RelModLevel));
// publishNew("stateCH", String(state.stateCH));
// publishNew("controlType", String(conf.ctrlType));
// publishNew("StateFlame", String(state.fl_flame));
}
//============================== ОБЕСПЕЧЕНИЕ РАБОТЫ IoTMANAGER =====================================
// конфигурирование котла в зависимости от настроек
void configuration()
{
state.fl_flame = state.stateCH = 0;
conf.countRele = conf.prcOnekWt = 0;
if (conf.relePwr[0])
{
rele[0] = findIoTItem("rele1");
if (rele[0])
{
conf.countRele++;
SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 1-го тэна");
}
}
if (conf.relePwr[1])
{
rele[1] = findIoTItem("rele2");
if (rele[1])
{
conf.countRele++;
SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 2-го тэна");
}
}
if (conf.relePwr[2])
{
rele[2] = findIoTItem("rele3");
if (rele[2])
{
conf.countRele++;
SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 3-го тэна");
}
}
for (int i = 0; i < conf.countRele; i++)
{
conf.prcOnekWt += conf.relePwr[i];
}
if (conf.countRele && conf.prcOnekWt)
{
// conf.prcOnekWt /= conf.countRele;
conf.prcOnekWt = 100 / conf.prcOnekWt;
// SerialPrint("i", "BoilerControl", "Процент одного кВт = " + String (conf.prcOnekWt) + "%");
}
if (conf.pump)
{
relePump = findIoTItem("relePump");
if (relePump)
{
SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ Насоса");
}
}
conf.ctrlType = false;
if (conf.countRele == 1 || _idPID == nullptr)
conf.ctrlType = true;
// IoTValue val;
// val.valD = conf.ctrlType;
if (_idCtrlType)
{
if (conf.ctrlType)
_idCtrlType->setValue("Вкл/Выкл", true);
else
_idCtrlType->setValue("Модуляция", true);
}
updateStateboiler();
}
void doByInterval()
{
// updateStateboiler();
// Принудительно чистим данные, что бы обновился интерфейс
OpenThemData.clear();
if (_debug > 0)
{
SerialPrint("i", "BoilerControl", "Обновляем данные в web интерфейсе");
}
if (_debug > 0)
{
SerialPrint("i", "BoilerControl", "memoryUsage: " + String(OpenThemData.memoryUsage()));
}
}
// Основной цикл программы
void loop()
{
unsigned long new_ts = millis();
int delay = 1000;
if (new_ts - ts > delay)
{
ts = new_ts;
updateStateboiler();
logicPowerOn();
}
// для новых версий IoTManager
IoTItem::loop();
}
// Комманды из сценария
IoTValue execute(String command, std::vector<IoTValue> &param)
{
IoTValue val;
if (command == "CHSet")
{
set.TSetCH = param[0].valD;
set.TSetCH = constrain(set.TSetCH, conf.minCH, conf.maxCH);
val.valD = set.TSetCH;
if (_idSetCH)
_idSetCH->setValue(val, true);
// publishNew("SetCH", String(set.TSetCH));
SerialPrint("i", "BoilerControl", "Scenario CHSet ");
}
else if (command == "CHEnable")
{
set.cmd_chEnable = param[0].valD;
val.valD = set.cmd_chEnable;
if (_idCmdCH)
_idCmdCH->setValue(val, true);
SerialPrint("i", "BoilerControl", "Scenario CHEnable ");
}
return {};
}
// обновление данных от датчиков
void updateStateboiler()
{
if (_idTboiler)
state.Tboiler = ::atof(_idTboiler->getValue().c_str());
if (_idTret)
state.Tret = ::atof(_idTret->getValue().c_str());
if (_idToutside)
state.Toutside = ::atof(_idToutside->getValue().c_str());
// if (_idStateCH)
// state.stateCH = ::atoi(_idStateCH->getValue().c_str());
// if (_idStateFlame)
// state.fl_flame = ::atoi(_idStateFlame->getValue().c_str());
// if (_idModLevel)
// state.RelModLevel = ::atof(_idModLevel->getValue().c_str());
if (_idCmdCH)
set.cmd_chEnable = ::atoi(_idCmdCH->getValue().c_str());
if (_idSetCH)
set.TSetCH = ::atof(_idSetCH->getValue().c_str());
}
~BoilerControl()
{
}
};
}
void *getAPI_SmartBoiler(String subtype, String param)
{
if (subtype == F("BoilerControl"))
{
return new _Boiler::BoilerControl(param);
}
else if (subtype == F("DHWControl"))
{
return new _Boiler::DHWControl(param);
}
else
{
return nullptr;
}
}

View File

@@ -0,0 +1,136 @@
{
"menuSection": "executive_devices",
"configItem": [
{
"global": 0,
"name": "BoilerControl",
"type": "Reading",
"subtype": "BoilerControl",
"id": "boiler",
"widget": "anydataDef",
"page": "Boiler",
"descr": "Котёл",
"int": 60,
"value": "...",
"LogLevel": 0,
"telegram": 1,
"idPID":"PID",
"idTboiler": "Tboiler",
"idTret": "Tret",
"idToutside": "Toutside",
"idStateCH":"StateCH",
"idStateFlame":"StateFlame",
"idModLevel":"ModLevel",
"idCmdCH":"CmdCH",
"idCmdDHW":"CmdDHW",
"idSetCH":"SetCH",
"idCtrlType":"CtrlType",
"rele1_Pwr": 1,
"rele2_Pwr": 2,
"rele3_Pwr": 4,
"changeRele":0,
"Pump": 0,
"minCH": 35,
"maxCH": 85,
"gistCH": 5,
"antiFreez":10
},
{
"global": 0,
"name": "DHWControl",
"type": "Reading",
"subtype": "DHWControl",
"id": "dhw",
"widget": "anydataDef",
"page": "Boiler",
"descr": "Котёл",
"int": 60,
"value": "...",
"idTdhw": "TDhw",
"idReleDhw": "ReleDhw",
"idCmdDHW":"CmdDHW",
"idStateDHW":"StateDHW",
"idSetDHW":"SetDHW",
"minDhw": 20,
"maxDhw": 60,
"gistDhw": 2
}
],
"about": {
"authorName": "Mikhail Bubnov",
"authorContact": "https://t.me/Mit4bmw",
"authorGit": "https://github.com/Mit4el",
"specialThanks": "",
"moduleName": "SmartBoiler",
"moduleVersion": "0.1",
"usedRam": {
"esp32_4mb": 15,
"esp8266_4mb": 15
},
"subTypes": [
"BoilerControl",
"OpenThermSlave"
],
"title": "SmartBoiler",
"moduleDesc": "Модуль для автоматизации электрического котла. Мозги котла с внешним протоколом opentherm",
"propInfo": {
"int": "Интервал отправки данных в MQTT и web интерфейс",
"telegram": "1- Будет отправлять в телеграмм оповещения при ошибках котла и пропаже сигнала от котла, остальные необходимо реализовывать через сценарий",
"MemberID": "SlaveMemberIDcode - код производителя котла, кем притворится котёл;) Менять в большинстве случаев не надо",
"idPID":"ID модуля ПИД регулятора, для расчета модуляции и включения тэнов в зависимости от температуры теплоносителя, в модуле TCHSet будет уставка СО, создать TCHSet и указать его в модуле ПИД",
"idTboiler": "ID датчика температуры подачи котла",
"idTret": "ID датчика температуры обратки котла",
"idToutside": "ID датчика уличной температуры",
"rele1_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele1",
"rele2_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele2, если нет, то 0 (ноль)",
"rele3_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele3, если нет, то 0 (ноль)",
"Pupm": "1-есть реле насоса (ID реле должно называться relePump), 0-нет реле насоса, насос управляется котлом без нас",
"minCH": "Граница установки температуры СО",
"maxCH": "Граница установки температуры СО",
"gistCH": "Гистерезис СО - нагрев СО включится если температура теплоносителя ниже уставки на указанные градусы (CHSet = 45гр, gistCH = 5гр, нагрев включится когда idTboiler = 40гр)",
"idTdhw": "ID датчика температуры ГВС, например в датчик в БКН",
"idReleDhw":"ID реле трехходового крана ГВС",
"gistDhw": "Гистерезис ГВС - нагрев ГВС включится если температура воды ниже уставки на указанные градусы",
"minDhw": "Граница установки температуры ГВС",
"maxDhw": "Граница установки температуры ГВС",
"changeRele":"Будет менять каждый раз при включении тэн 1->2->3->1...",
"antiFreez":"Режим анти-замерзания, Указывается температура, если опустится ниже указанной, то включится нарев один тэн и нагреет на +5гр от указанной"
},
"funcInfo": [
{
"name": "CHSet",
"descr": "Установить целевую температуру СО",
"params": [
"тепмература СО (подачи) - bolier.CHSet(60)"
]
},
{
"name": "CHEnable",
"descr": "включить / выключить отопление",
"params": [
"bolier.CHEnable(1) - вкл, bolier.CHEnable(0) - выкл, "
]
},
{
"name": "SetDHW",
"descr": "Установить целевую температуру ГВС",
"params": [
"тепмература ГВС - dhw.SetDHW(40)"
]
},
{
"name": "DHWEnable",
"descr": "включить / выключить ГВС",
"params": [
"dhw.DHWEnable(1) - вкл, dhw.DHWEnable(0) - выкл, "
]
}
]
},
"defActive": true,
"usedLibs": {
"esp32_4mb3f": [],
"esp32*": [],
"esp82*": []
}
}

View File

@@ -0,0 +1,71 @@
Модуль для автоматизации электрического котла.
(Описание модуля SmartBoiler/modinfo.json)
0 TODO Сделать чтобы при связи моих модулей промежуточные итемы можно было не создавать,
но если их создать там отобразятся актуальные значения
1 Управления котлом с 1-3 тэнами
1.1 указывается количество тэнов от 1 до 3х
1.2 задается мощность каждого тэна, должна быть по возрастающей
1.3 задание гистерезиса включения тэнов (ограничиваем в том числе и с ПИД)
1.4 задание минимальной и максимальной температуры теплоносителя (минимальную автоматически не поддерживает пока, просто проверяет вхождение в диапазон)
1.5 задание минимальной и максимальной температуры ГВС (минимальную автоматически не поддерживает пока, просто проверяет вхождение в диапазон)
1.6 реализация тремя отдельными элементами:
BoilerControl - Основная логика котла, по сути раздутый термостат
DHWControl - Логика управления нагрева ГВС
OpenThermSlave - Обеспечения протоокла взаимодействи OpenTherm
1.7 Смена тэна периодически (по флагу из конфигурации)
1.8 Если отвалились датчика, котел не включится
1.9 TODO режим антизамерзания
1.998 TODO поддержание минимальной температуры СО и ГВС
2 Поддержание температуры теплоносителя
2.1 Теплоноситель нагреется до заданной целевой и поддерживает её по гистерезису (если упала на гист. то включит нагрев)
2.2 ПИД расчитывает только количество тэнов для включения (0-30% = первый тэн, 30-60% = 1 и 2 тэны, 60-100% все тэны)
2.3 в зависимости от включенных тэнов и их мощности показывается процент модуляции.
2.4 Если не добавлен модуль ПИД, то включает все тэны
2.5 Если указан всего один тэн, то модуль ПИД не нужно создавать
2.998 TODO возможно использовать ПИД для периодического включения одного тэна
2.999 TODO сделать больше ступеней включения с различными комбинациями тэнов
3 Управление из IoTManager
3.1 Возможность управления по комнатному термостату в том числе с другой ESP
3.2 Управление модулем из сценария
3.3 есть проверка ошибок датчиков (если отвалились датчика, котел не включится)
3.4 Отправка состояния в телеграмм
3.998 3.4 TODO Автоматическая отправка состояния в модули для отображения (имена модулей в логах "new")
3.999 другой функционал IoTManager ...
4 Возможность управления циркуляционным насосом
4.1 насос включается всегда, если включено хотя бы один из тэнов
4.2 если включено по ГВС, насос отключается сразу как ГВС нагрелся до целевой
4.3 если включено по СО, насос отключается когда теплоноситель остынет до минимальной температуры СО
4.999 TODO Сделать управление выбегом насоса
5 Возможность управления ГВС при наличии 3-х ходового крана с БКН (бойлер косвенного нагрева)
5.1 ГВС работает только при указании реле 3-х ходового крана (а иначе как?)
5.2 ГВС имеет приоритет над СО
5.3 при нагреве ГВС котел включается на полную мощность (все тэны)
5.4 ГВС нагреет до заданной целевой и поддерживает её по гистерезису (если упала на гист. то включит ГВС)
5.5 Температура подачи котла при этом не должны превысить минимальную температуру СО (что бы не перегревал тенплоноситель, пока греется БКН)
6 Возможность управления по OpenTherm
6.1 в схему платы управления необходимо добавить часть OpenThermSlave и 24В
6.2 возможность управления любым OpenTherm адаптером/термостатом
6.3 задание целевой температуры теплоносителя
6.4 задание целевой температуры ГВС
6.5 команды включения СО и ГВС
6.6 отправка статуса Управляющему устройству OpenTherm
6.999 TODO Явного приоритета OpenTherm над другим управлением нет, а надо сделать. Сделать настройку "OpenTherm главне сценария" (сейчас команды выполняются ото всех по мере поступления)
7. Название модулей в которые автоматически отправится информации при их наличии (простто добавить в конфигурацию с указанным именем)
controlType - Тип управления тэнами: 0 - модуляция, 1- вкл/выкл
CHEnable - Состояние включения СО (не нагрев, а включение режима) 0 - выкл, 1- вкл
isFlame - Состояние нагрева/горелки (включенных тэнов) 0 - выкл, 1- вкл
RelModLevel - Уровень модуляции, в процентах в зависимости мощности включенных тэнов от их общего количества
TDHWSet - Установленная в котле целевая температура ГВС (из Сценария или OpenTherm)
TCHSet - Установленная в котле целевая температура СО (из Сценария или OpenTherm)
DHWEnable Состояние включения ГВС (не нагрев, а включение режима) 0 - выкл, 1- вкл
boilerslave - Состояние подключения к Управляющему устройству OpenTherm, значком ❌ ✅
status - Состояние подключения к Управляющему устройству OpenTherm, строкой: "не подключен" / "подключен"

View File

@@ -0,0 +1,337 @@
{
"mark": "iotm",
"config": [
{
"global": 0,
"type": "Writing",
"subtype": "TelegramLT",
"id": "tg",
"widget": "",
"page": "",
"descr": "",
"token": "",
"chatID": ""
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "Tboiler",
"needSave": 0,
"widget": "inputDgt",
"page": "Котёл",
"descr": "Датчик котла",
"int": "0",
"val": "20",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "SetCH",
"needSave": 0,
"widget": "inputDgt",
"page": "Котёл",
"descr": "Уставка СО",
"int": "0",
"val": "60",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "CtrlType",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": " Тип управления",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "CmdCH",
"needSave": 0,
"widget": "toggle",
"page": "Котёл",
"descr": " ВКЛ СО",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "StateCH",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": "Состояние СО",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "StateFlame",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": "Состояние Нагрева",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "rele1",
"needSave": 0,
"widget": "toggle",
"page": "Котёл",
"descr": "rele1",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "rele2",
"needSave": 0,
"widget": "toggle",
"page": "Котёл",
"descr": "rele2",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "rele3",
"needSave": 0,
"widget": "toggle",
"page": "Котёл",
"descr": "rele3",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "ModLevel",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": "Модуляция",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "relePump",
"needSave": 0,
"widget": "toggle",
"page": "Котёл",
"descr": "Реле цирк.насоса",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "ReleDhw",
"needSave": 0,
"widget": "toggle",
"page": "ГВС",
"descr": "3-хходовой",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "VButton",
"id": "CmdDHW",
"needSave": 0,
"widget": "toggle",
"page": "ГВС",
"descr": " ВКЛ ГВС",
"int": "0",
"val": "0"
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "TDhw",
"needSave": 0,
"widget": "inputDgt",
"page": "ГВС",
"descr": "Датчик БКН",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "SetDHW",
"needSave": 0,
"widget": "inputDgt",
"page": "ГВС",
"descr": "Уставка ГВС",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "StateDHW",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": "Состояние ГВС",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "DHWControl",
"id": "dhw96",
"widget": "nil",
"page": "Boiler",
"descr": "Котёл",
"int": 60,
"value": "...",
"idTDhw": "TDhw",
"idReleDhw": "ReleDhw",
"idCmdDHW": "CmdDHW",
"idStateDHW": "StateDHW",
"idSetDHW": "SetDHW",
"minDhw": 20,
"maxDhw": 60,
"gistDhw": 2
},
{
"global": 0,
"needSave": 0,
"type": "Writing",
"subtype": "ThermostatPID",
"id": "PID",
"widget": "anydataHum",
"page": "Котёл",
"descr": "термостат ПИД",
"int": "10",
"round": 1,
"map": "0,100,0,100",
"set_id": "SetCH",
"term_id": "Tboiler",
"term_rezerv_id": "",
"rele": "",
"KP": 5,
"KI": 50,
"KD": 1
},
{
"global": 0,
"type": "Reading",
"subtype": "Variable",
"id": "status",
"needSave": 0,
"widget": "anydataDef",
"page": "Состояние",
"descr": " Состояние",
"int": "0",
"val": "0.0",
"map": "1024,1024,1,100",
"plus": 0,
"multiply": 1,
"round": 0
},
{
"global": 0,
"type": "Reading",
"subtype": "BoilerControl",
"id": "boiler81",
"widget": "nil",
"page": "Boiler",
"descr": "Котёл",
"int": 60,
"value": "...",
"LogLevel": 0,
"telegram": 1,
"idPID": "PID",
"idTboiler": "Tboiler",
"idTret": "Tret",
"idToutside": "Toutside",
"idStateCH": "StateCH",
"idStateFlame": "StateFlame",
"idModLevel": "ModLevel",
"idCmdCH": "CmdCH",
"idCmdDHW": "CmdDHW",
"idSetCH": "SetCH",
"idCtrlType": "CtrlType",
"rele1_Pwr": 1,
"rele2_Pwr": 2,
"rele3_Pwr": 4,
"changeRele": 0,
"Pump": 0,
"minCH": 35,
"maxCH": 85,
"gistCH": 5,
"antiFreez": 10
}
]
}
scenario=>if onStart then
{
tg.sendMsg("SmartBoiler http://" + getIP());
}