From 8e7d2d6a141f1d706da249ec56960a11fd5dcca0 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Mon, 26 Feb 2024 23:31:12 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20SmartBoi?= =?UTF-8?q?ler=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/SmartBoiler/BoilerHeader.h | 44 +- src/modules/exec/SmartBoiler/OpenTherm.cpp | 812 ++++++++++ src/modules/exec/SmartBoiler/OpenTherm.h | 208 +++ src/modules/exec/SmartBoiler/SmartBoiler.cpp | 1311 +++++++++++++---- src/modules/exec/SmartBoiler/modinfo.json | 79 +- src/modules/exec/SmartBoiler/readme.txt | 2 +- src/modules/exec/SmartBoiler/smartBoiler.json | 168 +-- 7 files changed, 2167 insertions(+), 457 deletions(-) create mode 100644 src/modules/exec/SmartBoiler/OpenTherm.cpp create mode 100644 src/modules/exec/SmartBoiler/OpenTherm.h diff --git a/src/modules/exec/SmartBoiler/BoilerHeader.h b/src/modules/exec/SmartBoiler/BoilerHeader.h index cda61395..5894c879 100644 --- a/src/modules/exec/SmartBoiler/BoilerHeader.h +++ b/src/modules/exec/SmartBoiler/BoilerHeader.h @@ -3,13 +3,13 @@ #define SLAVE true #define TIMEOUT_TRESHOLD 5 -namespace _Boiler -{ +namespace _Boiler_v2 +{ // команды/установки от термостата struct SetpointBoiler { - uint8_t cmd_chEnable = 0; - uint8_t cmd_dhwEnable = 0; + bool cmd_chEnable = 0; + bool cmd_dhwEnable = 0; float TSetCH = 0; float TSetDhw = 0; } set; @@ -22,33 +22,36 @@ namespace _Boiler bool gas_fault = 0; bool air_fault = 0; bool water_overtemp = 0; - uint8_t fault_code = 0; + int 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; + bool antiFreezOn = false; + bool stateCH = 0; + bool stateDHW = 0; + bool fl_flame = 0; + int currentRele = 0; + bool 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}; + // bool r[3] = {0, 0, 0}; + int numStepOn; } state; // конфигурация котла struct ConfigBoiler { + bool autoPower = true; // если false то управление только из сценария или веба int antiFreez; - bool pump = false; // 1- наличие реле насоса СО, 0 - мы не управляем насосом СО (в протоколе ОТ нет) - bool changeRele = false; - // bool dhw = false; // 1- есть реле(трехходовой) ГВС + // 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 Команды кправления насосом от мастера не помню @@ -62,17 +65,18 @@ namespace _Boiler int gistCH; int countRele = 0; - int relePwr[3]={0,0,0}; + // int relePwr[3]={0,0,0}; int prcOnekWt = 0; // процент одного киловата из общей мощности всех тэнев, расчитывается для модуляции - // int rele2Pwr = 0; - // int rele3Pwr = 0; + // int rele2Pwr = 0; + // int rele3Pwr = 0; + int numStepDhw; + float maxKW; } conf; - - unsigned long timeout_count = 0; - int _debug = 0; + bool _debug = 0; bool _telegram = false; unsigned long ot_response = 0; + int SlaveMemberIDcode = 0; } diff --git a/src/modules/exec/SmartBoiler/OpenTherm.cpp b/src/modules/exec/SmartBoiler/OpenTherm.cpp new file mode 100644 index 00000000..8f6f7467 --- /dev/null +++ b/src/modules/exec/SmartBoiler/OpenTherm.cpp @@ -0,0 +1,812 @@ +/* +OpenTherm.cpp - OpenTherm Communication Library For Arduino, ESP8266 +Copyright 2018, Ihor Melnyk +*/ + +#include "OpenTherm.h" + +OpenTherm::OpenTherm(int inPin, int outPin, bool isSlave) : status(OpenThermStatus::NOT_INITIALIZED), + inPin(inPin), + outPin(outPin), + isSlave(isSlave), + response(0), + responseStatus(OpenThermResponseStatus::NONE), + responseTimestamp(0), + handleInterruptCallback(NULL), + processResponseCallback(NULL) +{ + imitFlag = false; +} + +void OpenTherm::begin(void (*handleInterruptCallback)(void), void (*processResponseCallback)(unsigned long, OpenThermResponseStatus)) +{ + pinMode(inPin, INPUT); + pinMode(outPin, OUTPUT); + if (handleInterruptCallback != NULL) + { + this->handleInterruptCallback = handleInterruptCallback; + attachInterrupt(digitalPinToInterrupt(inPin), handleInterruptCallback, CHANGE); + } + activateBoiler(); + status = OpenThermStatus::READY; + this->processResponseCallback = processResponseCallback; +} + +void OpenTherm::begin(void (*handleInterruptCallback)(void)) +{ + begin(handleInterruptCallback, NULL); +} + +bool IRAM_ATTR OpenTherm::isReady() +{ + return status == OpenThermStatus::READY; +} + +int IRAM_ATTR OpenTherm::readState() +{ + return digitalRead(inPin); +} + +void OpenTherm::setActiveState() +{ + digitalWrite(outPin, LOW); +} + +void OpenTherm::setIdleState() +{ + digitalWrite(outPin, HIGH); +} + +void OpenTherm::activateBoiler() +{ + setIdleState(); + delay(1000); +} + +void OpenTherm::sendBit(bool high) +{ + if (high) + setActiveState(); + else + setIdleState(); + delayMicroseconds(500); + if (high) + setIdleState(); + else + setActiveState(); + delayMicroseconds(500); +} + +bool OpenTherm::sendRequestAync(unsigned long request) +{ + // Serial.println("Request: " + String(request, HEX)); + noInterrupts(); + const bool ready = isReady(); + interrupts(); + + if (!ready) + return false; + + status = OpenThermStatus::REQUEST_SENDING; + response = 0; + responseStatus = OpenThermResponseStatus::NONE; +// Prevent switching to other tasks as there is a delay within sendBit +#ifdef ESP32 +// vTaskSuspendAll(); +#endif + sendBit(HIGH); // start bit + for (int i = 31; i >= 0; i--) + { + sendBit(bitRead(request, i)); + } + sendBit(HIGH); // stop bit + setIdleState(); +#ifdef ESP32 +// xTaskResumeAll(); +#endif + status = OpenThermStatus::RESPONSE_WAITING; + responseTimestamp = micros(); + if (imitFlag) + ImitationResponse(request); + return true; +} + +unsigned long OpenTherm::sendRequest(unsigned long request) +{ + if (!sendRequestAync(request)) + return 0; + while (!isReady()) + { + process(); + yield(); + } + return response; +} + +bool OpenTherm::sendResponse(unsigned long request) +{ + status = OpenThermStatus::REQUEST_SENDING; + response = 0; + responseStatus = OpenThermResponseStatus::NONE; + // Prevent switching to other tasks as there is a delay within sendBit +#ifdef ESP32 +// vTaskSuspendAll(); +#endif + sendBit(HIGH); // start bit + for (int i = 31; i >= 0; i--) + { + sendBit(bitRead(request, i)); + } + sendBit(HIGH); // stop bit + setIdleState(); +#ifdef ESP32 +// xTaskResumeAll(); +#endif + status = OpenThermStatus::READY; + return true; +} + +unsigned long OpenTherm::getLastResponse() +{ + return response; +} + +OpenThermResponseStatus OpenTherm::getLastResponseStatus() +{ + return responseStatus; +} + +void IRAM_ATTR OpenTherm::handleInterrupt() +{ + if (isReady()) + { + if (isSlave && readState() == HIGH) + { + status = OpenThermStatus::RESPONSE_WAITING; + } + else + { + return; + } + } + + unsigned long newTs = micros(); + if (status == OpenThermStatus::RESPONSE_WAITING) + { + if (readState() == HIGH) + { + status = OpenThermStatus::RESPONSE_START_BIT; + responseTimestamp = newTs; + } + else + { + // Error start bit / Ошибка стартового бита + status = OpenThermStatus::RESPONSE_INVALID; + responseTimestamp = newTs; + } + } + else if (status == OpenThermStatus::RESPONSE_START_BIT) + { + if ((newTs - responseTimestamp < 750) && readState() == LOW) + { + status = OpenThermStatus::RESPONSE_RECEIVING; + responseTimestamp = newTs; + responseBitIndex = 0; + } + else + { + // Error Start_bit LOW 750mks / Ошибка стартового бита по тылу (нет LOW через 750мкс) + status = OpenThermStatus::RESPONSE_INVALID; + responseTimestamp = newTs; + } + } + else if (status == OpenThermStatus::RESPONSE_RECEIVING) + { + // unsigned long bitDuration = newTs - responseTimestamp; + // В новой спецификации стоповый бит не обязателен. Если не дождались, всё равно попробуем разобрать + if ((newTs - responseTimestamp) > 750 && (newTs - responseTimestamp) < 1300) + { + if (responseBitIndex < 32) + { + response = (response << 1) | !readState(); + responseTimestamp = newTs; + responseBitIndex++; + } + else + { // stop bit + status = OpenThermStatus::RESPONSE_READY; + responseTimestamp = newTs; + } + } + } +} + +void OpenTherm::process() +{ + noInterrupts(); + OpenThermStatus st = status; + unsigned long ts = responseTimestamp; + interrupts(); + + if (st == OpenThermStatus::READY) + return; + unsigned long newTs = micros(); + if (st != OpenThermStatus::NOT_INITIALIZED && st != OpenThermStatus::DELAY && (newTs - ts) > 1000000) + { + status = OpenThermStatus::READY; + responseStatus = OpenThermResponseStatus::TIMEOUT; + if (processResponseCallback != NULL) + { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::RESPONSE_INVALID) + { + status = OpenThermStatus::DELAY; + responseStatus = OpenThermResponseStatus::INVALID; + if (processResponseCallback != NULL) + { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::RESPONSE_READY) + { + status = OpenThermStatus::DELAY; + responseStatus = (isSlave ? isValidRequest(response) : isValidResponse(response)) ? OpenThermResponseStatus::SUCCESS : OpenThermResponseStatus::INVALID; + // Error msgType (READ_ACK | WRITE_ACK) is Header + if (processResponseCallback != NULL) + { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::DELAY) + { + if ((newTs - ts) > 100000) + { + status = OpenThermStatus::READY; + } + } +} + +bool OpenTherm::parity(unsigned long frame) // odd parity +{ + byte p = 0; + while (frame > 0) + { + if (frame & 1) + p++; + frame = frame >> 1; + } + return (p & 1); +} + +OpenThermMessageType OpenTherm::getMessageType(unsigned long message) +{ + OpenThermMessageType msg_type = static_cast((message >> 28) & 7); + return msg_type; +} + +OpenThermMessageID OpenTherm::getDataID(unsigned long frame) +{ + return (OpenThermMessageID)((frame >> 16) & 0xFF); +} + +unsigned long OpenTherm::buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data) +{ + unsigned long request = data; + if (type == OpenThermMessageType::WRITE_DATA) + { + request |= 1ul << 28; + } + request |= ((unsigned long)id) << 16; + if (parity(request)) + request |= (1ul << 31); + return request; +} +unsigned long OpenTherm::buildRequestID(OpenThermMessageType type, unsigned int id, unsigned int data) +{ + unsigned long request = data; + if (type == OpenThermMessageType::WRITE_DATA) + { + request |= 1ul << 28; + } + request |= ((unsigned long)id) << 16; + if (parity(request)) + request |= (1ul << 31); + return request; +} + +unsigned long OpenTherm::buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data) +{ + unsigned long response = data; + response |= ((unsigned long)type) << 28; + response |= ((unsigned long)id) << 16; + if (parity(response)) + response |= (1ul << 31); + return response; +} + +bool OpenTherm::isValidResponse(unsigned long response) +{ + if (parity(response)) + return false; + byte msgType = (response << 1) >> 29; + return msgType == READ_ACK || msgType == WRITE_ACK; +} + +bool OpenTherm::isValidRequest(unsigned long request) +{ + if (parity(request)) + return false; + byte msgType = (request << 1) >> 29; + return msgType == READ_DATA || msgType == WRITE_DATA; +} + +void OpenTherm::end() +{ + if (this->handleInterruptCallback != NULL) + { + detachInterrupt(digitalPinToInterrupt(inPin)); + } +} + +const char *OpenTherm::statusToString(OpenThermResponseStatus status) +{ + switch (status) + { + case NONE: + return "NONE"; + case SUCCESS: + return "SUCCESS"; + case INVALID: + return "INVALID"; + case TIMEOUT: + return "TIMEOUT"; + default: + return "UNKNOWN"; + } +} + +const char *OpenTherm::messageTypeToString(OpenThermMessageType message_type) +{ + switch (message_type) + { + case READ_DATA: + return "READ_DATA"; + case WRITE_DATA: + return "WRITE_DATA"; + case INVALID_DATA: + return "INVALID_DATA"; + case RESERVED: + return "RESERVED"; + case READ_ACK: + return "READ_ACK"; + case WRITE_ACK: + return "WRITE_ACK"; + case DATA_INVALID: + return "DATA_INVALID"; + case UNKNOWN_DATA_ID: + return "UNKNOWN_DATA_ID"; + default: + return "UNKNOWN"; + } +} + +// building requests + +unsigned long OpenTherm::buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool enableSummerMode, bool dhwBlock) +{ + unsigned int data = enableCentralHeating | (enableHotWater << 1) | (enableCooling << 2) | (enableOutsideTemperatureCompensation << 3) | (enableCentralHeating2 << 4) | (enableSummerMode << 5) | (dhwBlock << 6); + data <<= 8; + return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Status, data); +} + +unsigned long OpenTherm::buildSetBoilerTemperatureRequest(float temperature) +{ + unsigned int data = temperatureToData(temperature); + return buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TSet, data); +} + +unsigned long OpenTherm::buildGetBoilerTemperatureRequest() +{ + return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tboiler, 0); +} + +// parsing responses +bool OpenTherm::isFault(unsigned long response) +{ + return response & 0x1; +} + +bool OpenTherm::isCentralHeatingActive(unsigned long response) +{ + return response & 0x2; +} + +bool OpenTherm::isHotWaterActive(unsigned long response) +{ + return response & 0x4; +} + +bool OpenTherm::isFlameOn(unsigned long response) +{ + return response & 0x8; +} + +bool OpenTherm::isCoolingActive(unsigned long response) +{ + return response & 0x10; +} + +bool OpenTherm::isDiagnostic(unsigned long response) +{ + return response & 0x40; +} + +uint16_t OpenTherm::getUInt(const unsigned long response) const +{ + const uint16_t u88 = response & 0xffff; + return u88; +} + +float OpenTherm::getFloat(const unsigned long response) const +{ + const uint16_t u88 = getUInt(response); + const float f = (u88 & 0x8000) ? -(0x10000L - u88) / 256.0f : u88 / 256.0f; + return f; +} + +unsigned int OpenTherm::temperatureToData(float temperature) +{ + if (temperature < 0) + temperature = 0; + if (temperature > 100) + temperature = 100; + unsigned int data = (unsigned int)(temperature * 256); + return data; +} + +// basic requests + +unsigned long OpenTherm::setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool enableSummerMode, bool dhwBlock) +{ + return sendRequest(buildSetBoilerStatusRequest(enableCentralHeating, enableHotWater, enableCooling, enableOutsideTemperatureCompensation, enableCentralHeating2, enableSummerMode, dhwBlock)); +} + +bool OpenTherm::setBoilerTemperature(float temperature) +{ + unsigned long response = sendRequest(buildSetBoilerTemperatureRequest(temperature)); + return isValidResponse(response); +} + +float OpenTherm::getBoilerTemperature() +{ + unsigned long response = sendRequest(buildGetBoilerTemperatureRequest()); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getReturnTemperature() +{ + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Tret, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +bool OpenTherm::setDHWSetpoint(float temperature) +{ + unsigned int data = temperatureToData(temperature); + unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TdhwSet, data)); + return isValidResponse(response); +} + +float OpenTherm::getDHWTemperature() +{ + unsigned long response = sendRequest(buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tdhw, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getModulation() +{ + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getPressure() +{ + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +unsigned char OpenTherm::getFault() +{ + return ((sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0)) >> 8) & 0xff); +} +int8_t flame_timer = 0; +void OpenTherm::ImitationResponse(unsigned long request) +{ + + // unsigned long response; + unsigned int data = getUInt(request); + OpenThermMessageType msgType; + byte ID; + OpenThermMessageID id = getDataID(request); + uint8_t flags; + + switch (id) + { + case OpenThermMessageID::Status: + // Статус котла получен + msgType = OpenThermMessageType::READ_ACK; + static int8_t flame = 0; + flame_timer++; + if (flame_timer > 10) + flame = 1; + if (flame_timer > 20) + { + flame_timer = 0; + flame = 0; + } + static int8_t fault = 0; + // fault = 1 - fault; + data = (bool)fault | (true << 1) | (true << 2) | ((bool)flame << 3) | (false << 4); + break; + case OpenThermMessageID::SConfigSMemberIDcode: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::SlaveVersion: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::MasterVersion: + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::RelModLevel: + static float RelModLevel = 10; + // RelModLevel = RelModLevel > 100 ? 10 : RelModLevel + 1; + if (flame_timer < 11) + { + RelModLevel = 0; + } + else + { + RelModLevel = RelModLevel == 0 ? 10 : RelModLevel + 1; + } + // data = RelModLevel; + data = temperatureToData(RelModLevel); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tboiler: + // Получили температуру котла + static float Tboiler = 40; + Tboiler = Tboiler > 60 ? 40 : Tboiler + 1; + data = temperatureToData(Tboiler); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tdhw: + // Получили температуру ГВС + static float Tdhw = 60; + Tdhw = Tdhw > 80 ? 60 : Tdhw + 1; + data = temperatureToData(Tdhw); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Toutside: + // Получили внешнюю температуру + static float Toutside = -10; + Toutside = Toutside > 10 ? -10 : Toutside + 1; + data = temperatureToData(Toutside); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::ASFflags: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::TdhwSetUBTdhwSetLB: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::MaxTSetUBMaxTSetLB: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::OEMDiagnosticCode: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::OpenThermVersionSlave: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::CHPressure: + msgType = OpenThermMessageType::READ_ACK; + break; + + break; + case OpenThermMessageID::DHWFlowRate: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::DayTime: + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Date: + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Year: + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + + case OpenThermMessageID::Tret: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tstorage: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tcollector: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TflowCH2: + // + msgType = OpenThermMessageType::READ_ACK; + + break; + case OpenThermMessageID::Tdhw2: + // + msgType = OpenThermMessageType::READ_ACK; + + break; + case OpenThermMessageID::Texhaust: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TheatExchanger: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::BoilerFanSpeed: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::ElectricBurnerFlame: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::BurnerStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::BurnerOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::RBPflags: + // + // Pre-Defined Remote Boiler Parameters + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TdhwSet: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::TSet: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::MaxTSet: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Hcratio: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::TSP: + // + // Transparent Slave Parameters + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::FHBsize: + // + // Fault History Data + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::MaxCapacityMinModLevel: + // + // Boiler Sequencer Control + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TrOverride: + // + // Remote override room setpoint + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::RemoteOverrideFunction: + msgType = OpenThermMessageType::READ_ACK; + break; + + default: + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + break; + } + response = buildResponse(msgType, id, data); + status = OpenThermStatus::RESPONSE_READY; + responseStatus = OpenThermResponseStatus::SUCCESS; + /* + if (processResponseCallback != NULL) + { + processResponseCallback(response, OpenThermResponseStatus::SUCCESS); + } + */ +} \ No newline at end of file diff --git a/src/modules/exec/SmartBoiler/OpenTherm.h b/src/modules/exec/SmartBoiler/OpenTherm.h new file mode 100644 index 00000000..aeea5d1b --- /dev/null +++ b/src/modules/exec/SmartBoiler/OpenTherm.h @@ -0,0 +1,208 @@ +/* +OpenTherm.h - OpenTherm Library for the ESP8266/Arduino platform +https://github.com/ihormelnyk/OpenTherm +http://ihormelnyk.com/pages/OpenTherm +Licensed under MIT license +Copyright 2018, Ihor Melnyk + +Frame Structure: +P MGS-TYPE SPARE DATA-ID DATA-VALUE +0 000 0000 00000000 00000000 00000000 +*/ + +#ifndef OpenTherm_h +#define OpenTherm_h + +#include +#include + +enum OpenThermResponseStatus : uint8_t +{ + NONE, + SUCCESS, + INVALID, + TIMEOUT +}; + +enum OpenThermMessageType : uint8_t +{ + /* Master to Slave */ + READ_DATA = B000, + READ = READ_DATA, // for backwared compatibility + WRITE_DATA = B001, + WRITE = WRITE_DATA, // for backwared compatibility + INVALID_DATA = B010, + RESERVED = B011, + /* Slave to Master */ + READ_ACK = B100, + WRITE_ACK = B101, + DATA_INVALID = B110, + UNKNOWN_DATA_ID = B111 +}; + +typedef OpenThermMessageType OpenThermRequestType; // for backwared compatibility + +enum OpenThermMessageID : uint8_t +{ + Status, // flag8 / flag8 Master and Slave Status flags. + TSet, // f8.8 Control setpoint ie CH water temperature setpoint (°C) + MConfigMMemberIDcode, // flag8 / u8 Master Configuration Flags / Master MemberID Code + SConfigSMemberIDcode, // flag8 / u8 Slave Configuration Flags / Slave MemberID Code + Command, // u8 / u8 Remote Command + ASFflags, // / OEM-fault-code flag8 / u8 Application-specific fault flags and OEM fault code + RBPflags, // flag8 / flag8 Remote boiler parameter transfer-enable & read/write flags + CoolingControl, // f8.8 Cooling control signal (%) + TsetCH2, // f8.8 Control setpoint for 2e CH circuit (°C) + TrOverride, // f8.8 Remote override room setpoint + TSP, // u8 / u8 Number of Transparent-Slave-Parameters supported by slave + TSPindexTSPvalue, // u8 / u8 Index number / Value of referred-to transparent slave parameter. + FHBsize, // u8 / u8 Size of Fault-History-Buffer supported by slave + FHBindexFHBvalue, // u8 / u8 Index number / Value of referred-to fault-history buffer entry. + MaxRelModLevelSetting, // f8.8 Maximum relative modulation level setting (%) + MaxCapacityMinModLevel, // u8 / u8 Maximum boiler capacity (kW) / Minimum boiler modulation level(%) + TrSet, // f8.8 Room Setpoint (°C) + RelModLevel, // f8.8 Relative Modulation Level (%) + CHPressure, // f8.8 Water pressure in CH circuit (bar) + DHWFlowRate, // f8.8 Water flow rate in DHW circuit. (litres/minute) + DayTime, // special / u8 Day of Week and Time of Day + Date, // u8 / u8 Calendar date + Year, // u16 Calendar year + TrSetCH2, // f8.8 Room Setpoint for 2nd CH circuit (°C) + Tr, // f8.8 Room temperature (°C) + Tboiler, // f8.8 Boiler flow water temperature (°C) + Tdhw, // f8.8 DHW temperature (°C) + Toutside, // f8.8 Outside temperature (°C) + Tret, // f8.8 Return water temperature (°C) + Tstorage, // f8.8 Solar storage temperature (°C) + Tcollector, // f8.8 Solar collector temperature (°C) + TflowCH2, // f8.8 Flow water temperature CH2 circuit (°C) + Tdhw2, // f8.8 Domestic hot water temperature 2 (°C) + Texhaust, // s16 Boiler exhaust temperature (°C) + TheatExchanger, // f8.8 Boiler heat exchanger temperature (°C) + BoilerFanSpeed, // u16 Boiler fan speed Setpiont and actual value + ElectricBurnerFlame, // f8.8?? Electric current through burner flame (mюA) + TdhwSetUBTdhwSetLB = 48, // s8 / s8 DHW setpoint upper & lower bounds for adjustment (°C) + MaxTSetUBMaxTSetLB, // s8 / s8 Max CH water setpoint upper & lower bounds for adjustment (°C) + HcratioUBHcratioLB, // s8 / s8 OTC heat curve ratio upper & lower bounds for adjustment + TdhwSet = 56, // f8.8 DHW setpoint (°C) (Remote parameter 1) + MaxTSet, // f8.8 Max CH water setpoint (°C) (Remote parameters 2) + Hcratio, // f8.8 OTC heat curve ratio (°C) (Remote parameter 3) + RemoteOverrideFunction = 100, // flag8 / - Function of manual and program changes in master and remote room setpoint. + OEMDiagnosticCode = 115, // u16 OEM-specific diagnostic/service code + BurnerStarts, // u16 Number of starts burner + CHPumpStarts, // u16 Number of starts CH pump + DHWPumpValveStarts, // u16 Number of starts DHW pump/valve + DHWBurnerStarts, // u16 Number of starts burner during DHW mode + BurnerOperationHours, // u16 Number of hours that burner is in operation (i.e. flame on) + CHPumpOperationHours, // u16 Number of hours that CH pump has been running + DHWPumpValveOperationHours, // u16 Number of hours that DHW pump has been running or DHW valve has been opened + DHWBurnerOperationHours, // u16 Number of hours that burner is in operation during DHW mode + OpenThermVersionMaster, // f8.8 The implemented version of the OpenTherm Protocol Specification in the master. + OpenThermVersionSlave, // f8.8 The implemented version of the OpenTherm Protocol Specification in the slave. + MasterVersion, // u8 / u8 Master product version number and type + SlaveVersion, // u8 / u8 Slave product version number and type +}; + +enum OpenThermStatus : uint8_t +{ + NOT_INITIALIZED, + READY, + DELAY, + REQUEST_SENDING, + RESPONSE_WAITING, + RESPONSE_START_BIT, + RESPONSE_RECEIVING, + RESPONSE_READY, + RESPONSE_INVALID +}; + +class OpenTherm +{ +public: + OpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false); + volatile OpenThermStatus status; + void begin(void (*handleInterruptCallback)(void)); + void begin(void (*handleInterruptCallback)(void), void (*processResponseCallback)(unsigned long, OpenThermResponseStatus)); + bool isReady(); + unsigned long sendRequest(unsigned long request); + bool sendResponse(unsigned long request); + bool sendRequestAync(unsigned long request); + unsigned long buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data); + unsigned long buildRequestID(OpenThermMessageType type, unsigned int id, unsigned int data); + unsigned long buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data); + unsigned long getLastResponse(); + OpenThermResponseStatus getLastResponseStatus(); + const char *statusToString(OpenThermResponseStatus status); + void handleInterrupt(); + void process(); + void end(); + + bool parity(unsigned long frame); + OpenThermMessageType getMessageType(unsigned long message); + OpenThermMessageID getDataID(unsigned long frame); + const char *messageTypeToString(OpenThermMessageType message_type); + bool isValidRequest(unsigned long request); + bool isValidResponse(unsigned long response); + + // requests + unsigned long buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false, bool enableSummerMode = false, bool dhwBlock = false); + unsigned long buildSetBoilerTemperatureRequest(float temperature); + unsigned long buildGetBoilerTemperatureRequest(); + + // responses + bool isFault(unsigned long response); + bool isCentralHeatingActive(unsigned long response); + bool isHotWaterActive(unsigned long response); + bool isFlameOn(unsigned long response); + bool isCoolingActive(unsigned long response); + bool isDiagnostic(unsigned long response); + uint16_t getUInt(const unsigned long response) const; + float getFloat(const unsigned long response) const; + unsigned int temperatureToData(float temperature); + + // basic requests + unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false, bool enableSummerMode = false, bool dhwBlock = false); + bool setBoilerTemperature(float temperature); + float getBoilerTemperature(); + float getReturnTemperature(); + bool setDHWSetpoint(float temperature); + float getDHWTemperature(); + float getModulation(); + float getPressure(); + unsigned char getFault(); + + //Имитация ответов от котла, TRUE - идет имитация ответов котла, в котел так же шлется (лучше его отключить), FALSE - штатная работа + void imitation(bool fl) {imitFlag = fl;} + +private: + bool imitFlag; + void ImitationResponse(unsigned long request); + + const int inPin; + const int outPin; + const bool isSlave; + + volatile unsigned long response; + volatile OpenThermResponseStatus responseStatus; + volatile unsigned long responseTimestamp; + volatile byte responseBitIndex; + + int readState(); + void setActiveState(); + void setIdleState(); + void activateBoiler(); + + void sendBit(bool high); + void (*handleInterruptCallback)(); + void (*processResponseCallback)(unsigned long, OpenThermResponseStatus); +}; + +#ifndef ICACHE_RAM_ATTR +#define ICACHE_RAM_ATTR +#endif + +#ifndef IRAM_ATTR +#define IRAM_ATTR ICACHE_RAM_ATTR +#endif + +#endif // OpenTherm_h diff --git a/src/modules/exec/SmartBoiler/SmartBoiler.cpp b/src/modules/exec/SmartBoiler/SmartBoiler.cpp index 15775302..ec0b74b9 100644 --- a/src/modules/exec/SmartBoiler/SmartBoiler.cpp +++ b/src/modules/exec/SmartBoiler/SmartBoiler.cpp @@ -2,10 +2,28 @@ #include "classes/IoTItem.h" #include #include "BoilerHeader.h" +#include +#include +#include "OpenTherm.h" -namespace _Boiler +#define SLAVE true +#define TIMEOUT_TRESHOLD 5 + +namespace _Boiler_v2 { - DynamicJsonDocument OpenThemData(JSON_BUFFER_SIZE / 2); + struct StepPower + { + float pwr; + std::vector listIDRele; + }; + + // std::vector vectorRele; + + std::map mapRele; + + std::map stepMap; + + // DynamicJsonDocument OpenThemData(JSON_BUFFER_SIZE / 2); IoTItem *_idPID = nullptr; IoTItem *_idTboiler = nullptr; @@ -18,31 +36,43 @@ namespace _Boiler IoTItem *_idCmdCH = nullptr; IoTItem *_idSetCH = nullptr; IoTItem *_idCtrlType = nullptr; + IoTItem *_relePump = nullptr; + + IoTItem *_idStateDHW = nullptr; + IoTItem *_idCmdDHW = nullptr; + IoTItem *_idTDhw = nullptr; + IoTItem *_idSetDHW = nullptr; + IoTItem *_releDhw = nullptr; + + float pid; + // IoTItem *rele[3]; + // IoTItem *_relePump; - int pid; - IoTItem *rele[3]; - IoTItem *relePump; - IoTItem *releDhw; IoTItem *tmp; // проверяем если пришедшее значение отличается от предыдущего регистрируем событие void publishNew(String widget, String value) { - if (OpenThemData[widget] != value) + tmp = findIoTItem(widget); + if (tmp) { - OpenThemData[widget] = value; - - tmp = findIoTItem(widget); - if (tmp) - { - tmp->setValue(value, true); - } - else - { - if (_debug > 0) - SerialPrint("new", "SmartBoiler", widget + " = " + value); - } + tmp->setValue(value, true); } + else + { + if (_debug > 0) + SerialPrint("new", "SmartBoiler", widget + " = " + value); + } + } + + // Обновит состояние реле, только если оно поменялось, что бы было меньше событий + void updateReleState(IoTItem *rele, int val) + { + if (rele) + if (val != ::atoi(rele->getValue().c_str())) + { + rele->setValue(String(val), true); + } } void sendTelegramm(String msg) @@ -54,55 +84,556 @@ namespace _Boiler } } + OpenTherm *ot_driver = nullptr; + OpenTherm *instance_OTdriver(int _RX_pin, int _TX_pin) + { + if (!ot_driver) + { + ot_driver = new OpenTherm(_RX_pin, _TX_pin, SLAVE); + // ot_driver->begin(); + } + return ot_driver; + } + + // Обработчик прерываний от ОТ + void IRAM_ATTR handleInterruptSlave() + { + if (ot_driver != nullptr) + ot_driver->handleInterrupt(); + } + + // unsigned long ot_response = 0; + // int SlaveMemberIDcode = 0; + + /* + * ========================================================================================= + * КЛАСС РАБОТЫ ПО ПРОТОКОЛУ OPENTHERM + * ========================================================================================= + */ + class OpenThermSlave : public IoTItem + { + private: + // unsigned long ts = 0; + + public: + OpenThermSlave(String parameters) : IoTItem(parameters) + { + int _RX_pin = 16; + int _TX_pin = 4; + SerialPrint("i", F("OpenThermSlave"), " START... "); + + jsonRead(parameters, "RX_pin", _RX_pin); + jsonRead(parameters, "TX_pin", _TX_pin); + jsonRead(parameters, "MemberID", SlaveMemberIDcode); + + instance_OTdriver(_RX_pin, _TX_pin); + ot_driver->begin(handleInterruptSlave, processRequest); // responseCallback + // ot_boiler = this; + } + + void configured() + { + static bool isFirstOT = true; + if (isFirstOT) + { + isFirstOT = false; + + bool haveBoiler = false; + for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) + { + if ((*it)->getSubtype() == "BoilerControl") + haveBoiler = true; + } + if (!haveBoiler) + { + SerialPrint("E", "OpenThermSlave", "Warning: BoilerControl не найден! работаем автономно по ID модулей"); + _idTboiler = findIoTItem("idTboiler"); + _idTret = findIoTItem("idTret"); + _idToutside = findIoTItem("idToutside"); + _idStateCH = findIoTItem("idStateCH"); + _idStateDHW = findIoTItem("idStateDHW"); + _idStateFlame = findIoTItem("idStateFlame"); + _idModLevel = findIoTItem("idModLevel"); + _idTDhw = findIoTItem("idTDhw"); + _idCmdCH = findIoTItem("idCmdCH"); + _idCmdDHW = findIoTItem("idCmdDHW"); + _idCtrlType = findIoTItem("idCtrlType"); + _idSetCH = findIoTItem("idSetCH"); + _idSetDHW = findIoTItem("idSetDHW"); + } + } + } + + void doByInterval() + { + configured(); + } + + // Основной цикл программы + void loop() + { + ot_driver->process(); + IoTItem::loop(); + } + + // Комманды из сценария + IoTValue execute(String command, std::vector ¶m) + { + return {}; + } + + // Обработка управления и отправка статуса + static void processStatus(unsigned int &data) + { + + uint8_t statusRequest = data >> 8; // забрали старший байт с командами мастера + set.cmd_chEnable = statusRequest & 0x1; // забрали 0 бит из этого байта - включение СО (маска 01) + + set.cmd_dhwEnable = statusRequest & 0x2; // забрали 1 бит из этого байта - включение СО (маска 10) + IoTValue val; + val.valD = set.cmd_chEnable; + if (_idCmdCH) + _idCmdCH->setValue(val, true); + val.valD = set.cmd_dhwEnable; + if (_idCmdDHW) + _idCmdDHW->setValue(val, true); + data &= 0xFF00; // старший бит не трогаем, а младший обнулили, что бы его заполнить состоянием котла и вернуть data термостату + + /* + // if (_idFail) + // state.fl_fail = ::atof(_idFail->getValue().c_str()); + if (_idStateCH) + state.stateCH = ::atoi(_idStateCH->getValue().c_str()); + if (_idStateDHW) + state.stateDHW = ::atoi(_idStateDHW->getValue().c_str()); + if (_idStateFlame) + state.fl_flame = ::atoi(_idStateFlame->getValue().c_str()); + */ + if (state.fl_fail) + data |= 0x01; // fault indication + if (state.stateCH) + data |= 0x02; // CH active + if (state.stateDHW) + data |= 0x04; // DHW active + if (state.fl_flame) + data |= 0x08; // flame on + // data |= 0x10; //cooling active + // data |= 0x20; //CH2 active + // data |= 0x40; //diagnostic/service event + // data |= 0x80; //electricity production on + } + + // обработка сброса ошибок + static void processCommand(unsigned int &data) + { + uint8_t command = data >> 8; // забрали старший байт с командами мастера + if (command == 1) + { + state.fl_fail = 0; + data |= 128; // ответ 128-255: команда выполнена + } + } + + //=================================== Обработка входящих сообщение ОТ ====================================== + static void processRequest(unsigned long request, OpenThermResponseStatus status) + { + switch (status) + { + case OpenThermResponseStatus::NONE: + if (_debug > 0) + { + SerialPrint("E", "OpenThermSlave", "Error: OpenTherm не инициализирован"); + } + break; + case OpenThermResponseStatus::INVALID: + if (_debug > 0) + { + SerialPrint("E", "OpenThermSlave", "ID:" + String(ot_driver->getDataID(request)) + " / Error: Ошибка разбора команды: " + String(request, HEX)); + // build UNKNOWN-DATAID response + unsigned long response = ot_driver->buildResponse(OpenThermMessageType::DATA_INVALID, ot_driver->getDataID(request), 0); + + // send response + ot_driver->sendResponse(response); + } + break; + case OpenThermResponseStatus::TIMEOUT: + if (_debug > 0) + { + SerialPrint("E", "OpenThermSlave", " ID: " + String(ot_driver->getDataID(request)) + " / Error: Таймаут команд от управляющего устройства (термостата)"); + } + timeout_count++; + if (timeout_count > TIMEOUT_TRESHOLD) + { + // publishNew("boilerslave", "❌"); + // publishNew("status", "не подключен"); + // setValue("ОТ не подключен", true); + timeout_count = TIMEOUT_TRESHOLD; + if (_debug > 0) + { + SerialPrint("E", "OpenThermSlave", "OpenTherm: потеря связи с управляющим устройством (термостатом) ❌"); + } + sendTelegramm(("OpenTherm: потеря связи с управляющим устройством (термостатом) ❌")); + } + break; + case OpenThermResponseStatus::SUCCESS: + timeout_count = 0; + // publishNew("boilerslave", "✅"); + // publishNew("status", "подключен"); + // setValue("ОТ подключен", true); + // sendTelegramm(("OpenTherm: котёл подключен ✅")); + // respondense_flag = true; + // ts_ = new_ts_; + HandleRequest(request); + break; + default: + break; + } + } + + // Парсинг запросов + static void HandleRequest(unsigned long request) + { + /* + if (_idCtrlType) + { + if (_idCtrlType->getValue() == "Модуляция") + conf.ctrlType = 0; + else + conf.ctrlType = 1; + } + */ + // conf.ctrlType = ::atoi(_idCtrlType->getValue().c_str()); + // unsigned long response; + unsigned int data = ot_driver->getUInt(request); + OpenThermMessageType msgType; + byte ID; + OpenThermMessageID id = ot_driver->getDataID(request); + uint8_t flags; + if (_debug > 1) + { + SerialPrint("i", "OpenThermSlave <-", String(millis()) + " ID: " + String(id) + " / requestHEX: " + String(request, HEX) + " / request: " + String(request)); + } + switch (id) + { + /*----------------------------Инициализация и конфигурация----------------------------*/ + case OpenThermMessageID::SConfigSMemberIDcode: // запрос Конфигурации Котла и SlaveMemberID + msgType = OpenThermMessageType::READ_ACK; + data = conf.dhw | (conf.ctrlType << 1) | (false << 2) | (conf.confDhw << 3) | (conf.pumpControlMaster << 4) | (false << 5); // 2-cooling, 5-CH2 + data <<= 8; + data |= SlaveMemberIDcode; + // data = (int)SlaveMemberIDcode; + break; + // case OpenThermMessageID::MConfigMMemberIDcode: // Получили Master Member ID + // msgType = OpenThermMessageType::WRITE_ACK; + // break; + // case OpenThermMessageID::SlaveVersion: // TODO вернуть версию модуля + // msgType = OpenThermMessageType::READ_ACK; + // data = (int)1; + // break; + // case OpenThermMessageID::MasterVersion: + // msgType = OpenThermMessageType::WRITE_ACK; + // break; + // case OpenThermMessageID::OpenThermVersionSlave: + // msgType = OpenThermMessageType::READ_ACK; + // break; + /*----------------------------Управление (уставки и команды)----------------------------*/ + case OpenThermMessageID::TdhwSetUBTdhwSetLB: // границы уставки ГВС + msgType = OpenThermMessageType::READ_ACK; + data |= (uint8_t)conf.minDhw; + data |= (uint8_t)conf.maxDhw << 8; + break; + case OpenThermMessageID::MaxTSetUBMaxTSetLB: // границы уставки СО + msgType = OpenThermMessageType::READ_ACK; + data |= (uint8_t)conf.minCH; + data |= (uint8_t)conf.maxCH << 8; + break; + case OpenThermMessageID::Command: // Сброс ошибок/сброс блокировки котла. Ответ: команды (не)выполнена + msgType = OpenThermMessageType::READ_ACK; + processCommand(data); + break; + case OpenThermMessageID::TdhwSet: // TODO Получили температуру ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + { + msgType = OpenThermMessageType::READ_ACK; + // if (_idSetDHW) + // set.TSetDhw = ::atof(_idSetDHW->getValue().c_str()); + data = ot_driver->temperatureToData(set.TSetDhw); + } + else + { + msgType = OpenThermMessageType::WRITE_ACK; + // processDHWSet(ot_driver->getFloat(data)); + set.TSetDhw = ot_driver->getFloat(data); + set.TSetDhw = constrain(set.TSetDhw, conf.minDhw, conf.maxDhw); + // publishNew("TDHWSet", String(set.TSetDhw)); + IoTValue val; + val.valD = set.TSetDhw; + if (_idSetDHW) + _idSetDHW->setValue(val, true); + } + break; + case OpenThermMessageID::TSet: // TODO Получили температуру СО + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + { + msgType = OpenThermMessageType::READ_ACK; + // if (_idSetCH) + // set.TSetCH = ::atof(_idSetCH->getValue().c_str()); + data = ot_driver->temperatureToData(set.TSetCH); + } + else + { + msgType = OpenThermMessageType::WRITE_ACK; + // processCHSet(ot_driver->getFloat(data)); + set.TSetCH = ot_driver->getFloat(data); + set.TSetCH = constrain(set.TSetCH, conf.minCH, conf.maxCH); + // publishNew("TCHSet", String(set.TSetCH)); + IoTValue val; + val.valD = set.TSetCH; + if (_idSetCH) + _idSetCH->setValue(val, true); + } + break; + /* case OpenThermMessageID::MaxTSet: // максимальная уставка ГВС ?????? + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Hcratio: // Коэффециент тепловой кривой + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + */ + /*----------------------------Состояние и статусы----------------------------*/ + case OpenThermMessageID::Status: // TODO Вернуть Статус котла + msgType = OpenThermMessageType::READ_ACK; + processStatus(data); + break; + case OpenThermMessageID::RelModLevel: // запрос модуляции + msgType = OpenThermMessageType::READ_ACK; + // if (_idModLevel) + // state.RelModLevel = ::atoi(_idModLevel->getValue().c_str()); + data = ot_driver->temperatureToData(state.RelModLevel); + break; + case OpenThermMessageID::Tboiler: // запрос температуры котла + msgType = OpenThermMessageType::READ_ACK; + // if (_idTboiler) + // state.Tboiler = ::atof(_idTboiler->getValue().c_str()); + data = ot_driver->temperatureToData(state.Tboiler); + break; + case OpenThermMessageID::Tdhw: // запрос температуры ГВС + msgType = OpenThermMessageType::READ_ACK; + if (_idTDhw) + { + state.Tdhw = ::atof(_idTDhw->getValue().c_str()); + data = ot_driver->temperatureToData(state.Tdhw); + } + else + { + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + } + break; + case OpenThermMessageID::Toutside: // запрос внешней температуры + msgType = OpenThermMessageType::READ_ACK; + if (_idToutside) + { + state.Toutside = ::atof(_idToutside->getValue().c_str()); + data = ot_driver->temperatureToData(state.Toutside); + } + else + { + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + } + break; + case OpenThermMessageID::ASFflags: // запрос ошибок + msgType = OpenThermMessageType::READ_ACK; + data = 0; + if (state.fl_fail) + { + data = state.fCode.service_required | (state.fCode.lockout_reset << 1) | (state.fCode.low_water_pressure << 2) | (state.fCode.gas_fault << 3) | (state.fCode.air_fault << 4) | (state.fCode.water_overtemp << 5); + data |= (uint8_t)state.fCode.fault_code << 8; + } + break; + case OpenThermMessageID::Tret: // температура обратки + msgType = OpenThermMessageType::READ_ACK; + if (_idTret) + { + state.Tret = ::atof(_idTret->getValue().c_str()); + data = ot_driver->temperatureToData(state.Tret); + } + else + { + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + } + break; + // case OpenThermMessageID::OEMDiagnosticCode: + // msgType = OpenThermMessageType::READ_ACK; + // break; + // case OpenThermMessageID::ElectricBurnerFlame: // Ток работы горелки ????? + // msgType = OpenThermMessageType::READ_ACK; + // break; + // case OpenThermMessageID::MaxCapacityMinModLevel: // максимальная мощность котла кВт и минимальная модуляция % + // msgType = OpenThermMessageType::READ_ACK; + // break; + + /*----------------------------Двусторонние информационные сообщения----------------------------*/ + /* case OpenThermMessageID::DayTime: + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Date: + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Year: + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + // ========>>>>>>>>>>> СБРОС КОЛИЧЕСТВА 0 от мастера + case OpenThermMessageID::BurnerStarts: // Количество стартов горелки + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpStarts: // Количество стартов насоса СО + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveStarts: // Количество стартов насоса/клапана ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerStarts: // Количество стартов горелки ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::BurnerOperationHours: // часы работы горелки + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpOperationHours: // часы работы горелки СО + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveOperationHours: // часы работы насоса/клапана ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerOperationHours: // часы работы горелки ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + */ + /*------------------------------------ ВСЁ ------------------------------------*/ + + default: + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + break; + } + ot_response = ot_driver->buildResponse(msgType, id, data); + ot_driver->sendResponse(ot_response); + if (_debug > 1) + { + SerialPrint("i", "OpenThermSlave ->", String(millis()) + " ID: " + String(id) + " / responseHEX: " + String(ot_response, HEX) + " / response: " + String(ot_response)); + } + } + + ~OpenThermSlave() + { + delete ot_driver; + ot_driver = nullptr; + } + }; + /* * ========================================================================================= * КЛАСС УПРАВЛЕНИЯ ГВС * ========================================================================================= */ - IoTItem *_idTdhw = nullptr; - IoTItem *_idStateDHW = nullptr; - IoTItem *_idCmdDHW = nullptr; - IoTItem *_idTDhw = nullptr; - IoTItem *_idSetDHW = nullptr; - class DHWControl : public IoTItem { private: - unsigned long ts = 0; + // 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) + jsonRead(parameters, "numStepDhw", conf.numStepDhw); + + String tmpID; + if (jsonRead(parameters, "idSetDHW", tmpID)) + _idSetDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idStateDHW", tmpID)) + _idStateDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idCmdDHW", tmpID)) + _idCmdDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idTDhw", tmpID)) + _idTDhw = findIoTItem(tmpID); + if (jsonRead(parameters, "idReleDhw", tmpID)) + _releDhw = findIoTItem(tmpID); + if (_releDhw) { SerialPrint("i", "DHWControl", "Инициализировано РЕЛЕ ГВС"); + conf.dhw = true; } - - // dhw_ctrl = this; } + /* + void configured() + { + static bool isFirstDhW = true; + if (isFirstDhW) + { + isFirstDhW = false; + String tmpID; + if (jsonRead(parameters, "idSetDHW", tmpID)) + _idSetDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idStateDHW", tmpID)) + _idStateDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idCmdDHW", tmpID)) + _idCmdDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idTDhw", tmpID)) + _idTDhw = findIoTItem(tmpID); + if (jsonRead(parameters, "idReleDhw", tmpID)) + _releDhw = findIoTItem(tmpID); + if (_releDhw){ + SerialPrint("i", "DHWControl", "Инициализировано РЕЛЕ ГВС"); + conf.dhw = true; + } + } + } + */ // ============================== ЛОГИКА РАБОТЫ КОТЛА И ВКЛЮЧЕНИЯ ТЭНОВ ====================================== // Работы котла и включения тэнов @@ -119,7 +650,7 @@ namespace _Boiler if (set.cmd_dhwEnable) { - if (releDhw) + if (_releDhw) { if (_idTDhw) state.Tdhw = ::atof(_idTDhw->getValue().c_str()); @@ -127,18 +658,20 @@ namespace _Boiler if (set.TSetDhw - state.Tdhw >= conf.gistDhw && state.Tdhw > -5 && state.Tdhw < 120) { // включаем ГВС - releDhw->setValue("1", true); + //_releDhw->setValue("1", true); + updateReleState(_releDhw, 1); state.stateDHW = 1; state.stateCH = 0; - for (uint8_t i = 0; i < conf.countRele; i++) - { - state.r[i] = true; - } - state.RelModLevel = 100; + state.numStepOn = conf.numStepDhw; + if (stepMap.find(conf.numStepDhw) != stepMap.end()) + state.RelModLevel = conf.prcOnekWt * stepMap[conf.numStepDhw].pwr; + else + state.RelModLevel = 0; } else { - releDhw->setValue("0", true); + //_releDhw->setValue("0", true); + updateReleState(_releDhw, 0); state.stateDHW = 0; state.RelModLevel = 0; } @@ -146,8 +679,9 @@ namespace _Boiler } else { - if (releDhw) - releDhw->setValue("0", true); + if (_releDhw) + //_releDhw->setValue("0", true); + updateReleState(_releDhw, 0); } // publishNew("StateDHW", String(state.stateDHW)); IoTValue val; @@ -160,6 +694,7 @@ namespace _Boiler void doByInterval() { + // configured(); } // Комманды из сценария @@ -168,21 +703,27 @@ namespace _Boiler 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 "); + if (param.size()) + { + 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 "); + if (param.size()) + { + set.cmd_dhwEnable = param[0].valD; + val.valD = set.cmd_dhwEnable; + if (_idCmdDHW) + _idCmdDHW->setValue(val, true); + SerialPrint("i", "DHWControl", "Scenario DHWEnable "); + } } return {}; @@ -201,42 +742,17 @@ namespace _Boiler class BoilerControl : public IoTItem { private: - unsigned long ts = 0; + // unsigned long ts = 0; public: BoilerControl(String parameters) : IoTItem(parameters) { SerialPrint("i", F("BoilerControl"), " START... "); - jsonRead(parameters, "LogLevel", _debug); + jsonRead(parameters, "debug", _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, "Pump", conf.pump); jsonRead(parameters, "changeRele", conf.changeRele); jsonRead(parameters, "minCH", conf.minCH); @@ -244,105 +760,287 @@ namespace _Boiler jsonRead(parameters, "gistCH", conf.gistCH); jsonRead(parameters, "antiFreez", conf.antiFreez); - configuration(); - } + jsonRead(parameters, "maxKW", conf.maxKW); + // configuration(); + state.fl_flame = state.stateCH = 0; + conf.countRele = conf.prcOnekWt = 0; + String tmpID; + if (jsonRead(parameters, "idPID", tmpID)) + _idPID = findIoTItem(tmpID); + if (jsonRead(parameters, "idTboiler", tmpID)) + _idTboiler = findIoTItem(tmpID); + if (jsonRead(parameters, "idTret", tmpID)) + _idTret = findIoTItem(tmpID); + if (jsonRead(parameters, "idToutside", tmpID)) + _idToutside = findIoTItem(tmpID); + + if (jsonRead(parameters, "idStateCH", tmpID)) + _idStateCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idStateFlame", tmpID)) + _idStateFlame = findIoTItem(tmpID); + if (jsonRead(parameters, "idModLevel", tmpID)) + _idModLevel = findIoTItem(tmpID); + if (jsonRead(parameters, "idCmdCH", tmpID)) + _idCmdCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idSetCH", tmpID)) + _idSetCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idCtrlType", tmpID)) + _idCtrlType = findIoTItem(tmpID); + if (jsonRead(parameters, "idRelePump", tmpID)) + _relePump = findIoTItem(tmpID); + if (_relePump) + SerialPrint("i", "BoilerControl", "Initialized relay pump"); + + updateStateboiler(); + } + /* + void configured() + { + static bool isFirst = true; + if (isFirst) + { + isFirst = true; + + String tmpID; + if (jsonRead(parameters, "idPID", tmpID)) + _idPID = findIoTItem(tmpID); + if (jsonRead(parameters, "idTboiler", tmpID)) + _idTboiler = findIoTItem(tmpID); + if (jsonRead(parameters, "idTret", tmpID)) + _idTret = findIoTItem(tmpID); + if (jsonRead(parameters, "idToutside", tmpID)) + _idToutside = findIoTItem(tmpID); + + if (jsonRead(parameters, "idStateCH", tmpID)) + _idStateCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idStateFlame", tmpID)) + _idStateFlame = findIoTItem(tmpID); + if (jsonRead(parameters, "idModLevel", tmpID)) + _idModLevel = findIoTItem(tmpID); + if (jsonRead(parameters, "idCmdCH", tmpID)) + _idCmdCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idSetCH", tmpID)) + _idSetCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idCtrlType", tmpID)) + _idCtrlType = findIoTItem(tmpID); + if (jsonRead(parameters, "idRelePump", tmpID)) + _relePump = findIoTItem(tmpID); + if (_relePump) + SerialPrint("i", "BoilerControl", "Initialized relay pump"); + } + } + */ // ============================== ЛОГИКА РАБОТЫ КОТЛА И ВКЛЮЧЕНИЯ ТЭНОВ ====================================== // Работы котла и включения тэнов - static void logicPowerOn() + static void + logicPowerOn() { - // TODO ВКЛЮЧЕНИЕ РЕЛЕ ПО ИХ МОЩНОСТИ (СДЕЛАТЬ ШАГИ НАГРЕВА И КОМБИНАЦИИ ВКЛБЧЕНИЯ ТЕНОВ С РАЗНОЙ МОЩНОСТЬЮ) - for (uint8_t i = 0; i < conf.countRele; i++) - { - state.r[i] = false; - } - // сейчас включаются по порядку + // TODO ВКЛЮЧЕНИЕ РЕЛЕ ПО ИХ МОЩНОСТИ (СДЕЛАТЬ ШАГИ НАГРЕВА И КОМБИНАЦИИ ВКЛБЧЕНИЯ ТЕНОВ С РАЗНОЙ МОЩНОСТЬЮ + + // state.numStepOn = 0; state.RelModLevel = 0; pid = 0; state.fl_flame = false; - if (_idPID) - pid = ::atoi(_idPID->getValue().c_str()); // обнуляем ГВС state.stateDHW = 0; + conf.countRele = mapRele.size(); // vectorRele.size(); + if (_debug > 0) + SerialPrint("I", "SmartBoiler", " countRele = " + String(conf.countRele)); + if (conf.maxKW) + { + // conf.prcOnekWt /= conf.countRele; + conf.prcOnekWt = 100 / conf.maxKW; + // SerialPrint("i", "BoilerControl", "Процент одного кВт = " + String (conf.prcOnekWt) + "%"); + } + if (conf.countRele == 1 || _idPID == nullptr) + conf.ctrlType = true; + + // IoTValue val; + // val.valD = conf.ctrlType; + if (_idCtrlType) + { + if (conf.ctrlType == 0) + _idCtrlType->setValue("Модуляция", true); + else + _idCtrlType->setValue("Вкл/Выкл", true); + } + if (_idPID) + pid = ::atof(_idPID->getValue().c_str()); + + if (conf.antiFreez > 0 && state.Tboiler < (conf.antiFreez + 5)) + { + state.antiFreezOn = true; + state.numStepOn = 1; + state.RelModLevel = conf.prcOnekWt * stepMap[1].pwr; + publishNew("status", "Анти-заморозка"); + SerialPrint("i", "BoilerControl", "Режим анти-заморозка"); + sendTelegramm("Режим анти-заморозка"); + } + else + { + if (state.antiFreezOn) + { + state.numStepOn = 0; + state.RelModLevel = 0; + state.antiFreezOn = false; + } + } + if (state.Tboiler < conf.maxCH) { - // if (dhw_ctrl) - //{ - // если есть модуль ГВС, то вызываем его логику включения тэнов - DHWControl::logicPowerDhw(); - //} - if (!state.stateDHW) // Если уже включено ГВС, то нечего смотреть на отопление + if (!state.antiFreezOn) { - if (set.cmd_chEnable) + if (conf.autoPower) { - 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 (dhw_ctrl) + //{ + // если есть модуль ГВС, то вызываем его логику включения тэнов + DHWControl::logicPowerDhw(); + //} + if (!state.stateDHW) // Если уже включено ГВС, то нечего смотреть на отопление { - if (conf.ctrlType == 0) - { // режим модуляции, это когда есть модуль ПИД и более одного реле + if (set.cmd_chEnable) + { + SerialPrint("I", "SmartBoiler", " stepMap.size = " + String(stepMap.size())); + publishNew("status", "Штатный режим"); + // включаем отопление + state.stateCH = 1; - if (pid > 0) - { - state.r[state.currentRele] = true; - state.RelModLevel = conf.prcOnekWt * conf.relePwr[state.currentRele]; + float TMAXcompare; + if (state.numStepOn == 0) + { // сейчас нагрев выключен + TMAXcompare = set.TSetCH - conf.gistCH; } - 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]); + else + { // сейчас нагрев работает + TMAXcompare = set.TSetCH; } - if (pid > 60) + + if (state.Tboiler <= TMAXcompare && state.Tboiler > -5 && state.Tboiler < 120) { - for (uint8_t i = 0; i < conf.countRele; i++) - { - state.r[i] = true; + if (conf.ctrlType == 0) + { // режим модуляции, это когда есть модуль ПИД и более одного реле + float pidStep = 100 / stepMap.size(); + if (_debug > 0) + SerialPrint("I", "SmartBoiler", " pidStep = " + String(pidStep)); + for (size_t i = 0; i < stepMap.size(); i++) + { + if (pid > 1 && pid > i * pidStep && pid <= pidStep * (i + 1)) + { + if (_debug > 0) + { + SerialPrint("I", "SmartBoiler", " Modulation, stepON = " + String(i + 1)); + SerialPrint("I", "SmartBoiler", " Modulation, pid > " + String(i * pidStep) + "; pid <= " + String(pidStep * (i + 1))); + } + state.numStepOn = i + 1; + if (stepMap.find(state.numStepOn) != stepMap.end()) + state.RelModLevel = conf.prcOnekWt * stepMap[state.numStepOn].pwr; + else + { + state.RelModLevel = 0; + SerialPrint("E", "SmartBoiler", "НЕТ ТАКОГО Шага: " + String(state.numStepOn)); + } + } + } + if (pid >= 100) + { + if (_debug > 0) + { + SerialPrint("I", "SmartBoiler", " Modulation, stepON = " + String(stepMap.size())); + SerialPrint("I", "SmartBoiler", " Modulation, pid 100% "); + } + state.numStepOn = stepMap.size(); + if (stepMap.find(state.numStepOn) != stepMap.end()) + state.RelModLevel = 100; + else + { + state.RelModLevel = 0; + SerialPrint("E", "SmartBoiler", "НЕТ ТАКОГО Шага: " + String(state.numStepOn)); + } + } } - state.RelModLevel = 100; + else + { // у нас релейный режим без ПИДа или с одним реле(тэном) + // TODO СЕЙЧАС ВКЛЮЧАЕМ ПЕРВЫЙ ШАГ НАГРЕВА, НАДО ЧТО ТО ПРИДУМАТЬ УМНОЕ + state.numStepOn = 1; + state.RelModLevel = conf.prcOnekWt * stepMap[1].pwr; + if (_debug > 0) + SerialPrint("I", "SmartBoiler", " On|Off, stepON = " + String(state.numStepOn)); + } + } + else + { + state.numStepOn = 0; + state.RelModLevel = 0; } } else - { // у нас релейный режим без ПИДа или с одним реле(тэном) - // TODO СЕЙЧАС ВКЛЮЧАЕМ ВСЕ РЕЛЕ, НАДО ЧТО ТО ПРИДУМАТЬ УМНОЕ - for (uint8_t i = 0; i < conf.countRele; i++) - { - state.r[i] = true; - } - state.RelModLevel = 100; + { + publishNew("status", "Выкл отопление"); + state.numStepOn = 0; + state.RelModLevel = 0; + state.stateCH = 0; + if (_debug > 0) + SerialPrint("I", "SmartBoiler", " Выкл отопление "); } } - else - { - state.RelModLevel = 0; - } } else { - publishNew("status", "Выкл отопление"); - state.stateCH = 0; + publishNew("status", "Ручной режим"); + if (stepMap.find(state.numStepOn) != stepMap.end()) + state.RelModLevel = conf.prcOnekWt * stepMap[state.numStepOn].pwr; + else + { + SerialPrint("E", "SmartBoiler", "НЕТ ТАКОГО Шага: " + String(state.numStepOn)); + set.cmd_chEnable = 0; + } + + if (!set.cmd_chEnable) + { + state.numStepOn = 0; + state.RelModLevel = 0; + state.stateCH = 0; + } + if (_debug > 0) + SerialPrint("I", "SmartBoiler", " Ручной режим, stepON = " + String(state.numStepOn)); } } } - if (conf.antiFreez > 0 && state.Tboiler < (conf.antiFreez + 4)) + else { - state.r[state.currentRele] = true; - state.RelModLevel = conf.prcOnekWt * conf.relePwr[state.currentRele]; - // setValue("Анти-Заморозка"); - publishNew("status", "Режим анти-заморозка"); - SerialPrint("i", "BoilerControl", "Режим анти-заморозка"); - sendTelegramm("Режим анти-заморозка"); + state.numStepOn = 0; + state.RelModLevel = 0; + state.stateCH = 0; + SerialPrint("i", "BoilerControl", "Выключились по макс. температуре, Tboiler = " + String(state.Tboiler)); } - static bool prev_flame = false; + static bool prev_flame; - if (state.RelModLevel > 0) + if (state.numStepOn > 0) + { state.fl_flame = true; // если хоть одно реле включено, то выставляем флаг горелки + // если хоть одно реле включено, то включаем насос + if (_relePump) + //_relePump->setValue("1", true); + updateReleState(_relePump, 1); + } + else + { + // если все реле выключены + if (_relePump) + { + if (state.Tboiler > conf.minCH && set.cmd_chEnable) // НО температура ещё горячая и при этом отопление включено, то включаем насос + //_relePump->setValue("1", true); + updateReleState(_relePump, 1); + else // Если температура в котле уже остыла, или отопление нам не нужно (летом нагрели воду-пусть котел сам остывает без насоса), то выключаем насос + //_relePump->setValue("0", true); + updateReleState(_relePump, 0); + } + } if (state.fl_flame && prev_flame != state.fl_flame) { @@ -357,31 +1055,71 @@ namespace _Boiler bool fl_pump = false; - // переключаем реле в соответсии с их статусами - for (uint8_t i = 0; i < conf.countRele; i++) + // onStepPower(state.numStepOn); + if (state.numStepOn > 0) { - if (rele[i]) + if (stepMap.find(state.numStepOn) == stepMap.end()) { - rele[i]->setValue(state.r[i] ? "1" : "0", true); + SerialPrint("E", "SmartBoiler", "НЕТ ТАКОГО Шага: " + String(state.numStepOn)); + state.numStepOn = 1; + } + bool releon; + if (conf.changeRele) + { + // количество реле в текущем шаге + int countOn = stepMap[state.numStepOn].listIDRele.size(); + bool crOn = false; + // перебираем все реле + int i = 0; + for (auto it = mapRele.begin(); it != mapRele.end(); it++) + { + crOn = false; + // перебираем сколько реле нам нужно включить в текущем шаге + for (size_t c = 0; c < countOn; c++) + { + // state.currentRele - с какого реле нужно начать включение + int cr = state.currentRele + c; + if (cr >= mapRele.size()) + cr = 0; + if (i == cr) + crOn = true; + } + if (crOn) + { + updateReleState(it->second, 1); + } + else + { + updateReleState(it->second, 0); + } + i++; + } + } + else + { + for (auto it = mapRele.begin(); it != mapRele.end(); it++) + { + releon = false; + for (size_t r = 0; r < stepMap[state.numStepOn].listIDRele.size(); r++) + { + if (stepMap[state.numStepOn].listIDRele[r] == it->first) + releon = true; + } + if (releon) + updateReleState(it->second, 1); + else + updateReleState(it->second, 0); + } } - 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); + for (auto it = mapRele.begin(); it != mapRele.end(); it++) + { + updateReleState(it->second, 0); } } + IoTValue val; if (_idModLevel) { @@ -399,138 +1137,144 @@ namespace _Boiler _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)); + publishNew("autoPower", String(conf.autoPower)); + + // setValue(String(stepMap[state.numStepOn].pwr)); + // 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; - // конфигурирование котла в зависимости от настроек - void configuration() + if (conf.pump) + { + _relePump = findIoTItem("_relePump"); + if (_relePump) + { + SerialPrint("i", "BoilerControl", "Initialized relay pump"); + } + } + + updateStateboiler(); + } + */ + void addStepPower(std::vector ¶m) { - state.fl_flame = state.stateCH = 0; - conf.countRele = conf.prcOnekWt = 0; - if (conf.relePwr[0]) + // int step = param[0].valD; + // float pwr = param[1].valD; + SerialPrint("i", "BoilerControl", "Add Step Power - " + param[0].valS + " , power - " + param[1].valS); + StepPower step; // = new StepPower; + step.pwr = param[1].valD; + for (size_t i = 2; i < param.size(); i++) { - rele[0] = findIoTItem("rele1"); - if (rele[0]) + step.listIDRele.push_back(param[i].valS); + tmp = findIoTItem(param[i].valS); + if (tmp) { - conf.countRele++; - SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 1-го тэна"); + // vectorRele.push_back(tmp); + mapRele[tmp->getID()] = tmp; + + SerialPrint("i", "BoilerControl", "initialized relay - " + tmp->getID()); } - } - - 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); + { + SerialPrint("E", "BoilerControl", "Error initialized relay - " + param[i].valS); + } } - - updateStateboiler(); + stepMap[(int)param[0].valD] = step; } void doByInterval() { - // updateStateboiler(); - - // Принудительно чистим данные, что бы обновился интерфейс - OpenThemData.clear(); - - if (_debug > 0) - { - SerialPrint("i", "BoilerControl", "Обновляем данные в web интерфейсе"); - } - if (_debug > 0) - { - SerialPrint("i", "BoilerControl", "memoryUsage: " + String(OpenThemData.memoryUsage())); - } + // configured(); + updateStateboiler(); + logicPowerOn(); + if (stepMap.find(state.numStepOn) != stepMap.end()) + setValue(String(stepMap[state.numStepOn].pwr)); } - // Основной цикл программы - void loop() - { - unsigned long new_ts = millis(); - int delay = 1000; - if (new_ts - ts > delay) + /* + // Основной цикл программы + void loop() { - ts = new_ts; - updateStateboiler(); - logicPowerOn(); + unsigned long new_ts = millis(); + int delay = 1000; + if (new_ts - ts > delay) + { + ts = new_ts; + updateStateboiler(); + logicPowerOn(); + } + // для новых версий IoTManager + IoTItem::loop(); } - // для новых версий IoTManager - IoTItem::loop(); - } - + */ // Комманды из сценария IoTValue execute(String command, std::vector ¶m) { 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 "); + if (param.size()) + { + 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 "); + if (param.size()) + { + set.cmd_chEnable = param[0].valD; + val.valD = set.cmd_chEnable; + if (_idCmdCH) + _idCmdCH->setValue(val, true); + SerialPrint("i", "BoilerControl", "Scenario CHEnable "); + } + } + else if (command == "addStepPower") + { + if (param.size() > 2) + { + // addstep(step, power, param); + addStepPower(param); + // SerialPrint("i", "BoilerControl", "Scenario addStep "); + } + } + else if (command == "onStepPower") + { + if (param.size()) + { + conf.autoPower = false; + // step[param[0].valD] = true; + // onStepPower(param[0].valD); + state.numStepOn = param[0].valD; + set.cmd_chEnable = true; + val.valD = set.cmd_chEnable; + + if (_idCmdCH) + _idCmdCH->setValue(val, true); + SerialPrint("i", "BoilerControl", "Scenario onStep, Hand Power On "); + } + } + else if (command == "autoPower") + { + + conf.autoPower = true; + SerialPrint("i", "BoilerControl", "Auto Power On"); } return {}; @@ -560,21 +1304,28 @@ namespace _Boiler ~BoilerControl() { + stepMap.clear(); + // vectorRele.clear(); + mapRele.clear(); } }; - } void *getAPI_SmartBoiler(String subtype, String param) { if (subtype == F("BoilerControl")) { - return new _Boiler::BoilerControl(param); + return new _Boiler_v2::BoilerControl(param); } else if (subtype == F("DHWControl")) { - return new _Boiler::DHWControl(param); + return new _Boiler_v2::DHWControl(param); } + else if (subtype == F("OpenThermSlave")) + { + return new _Boiler_v2::OpenThermSlave(param); + } + else { return nullptr; diff --git a/src/modules/exec/SmartBoiler/modinfo.json b/src/modules/exec/SmartBoiler/modinfo.json index 9be13ca2..75029d74 100644 --- a/src/modules/exec/SmartBoiler/modinfo.json +++ b/src/modules/exec/SmartBoiler/modinfo.json @@ -7,12 +7,12 @@ "type": "Reading", "subtype": "BoilerControl", "id": "boiler", - "widget": "anydataDef", + "widget": "anydataWt", "page": "Boiler", "descr": "Котёл", - "int": 60, + "int": 1, "value": "...", - "LogLevel": 0, + "debug": 0, "telegram": 1, "idPID":"PID", "idTboiler": "Tboiler", @@ -25,15 +25,13 @@ "idCmdDHW":"CmdDHW", "idSetCH":"SetCH", "idCtrlType":"CtrlType", - "rele1_Pwr": 1, - "rele2_Pwr": 2, - "rele3_Pwr": 4, "changeRele":0, - "Pump": 0, + "idRelePump": "relePump", "minCH": 35, "maxCH": 85, "gistCH": 5, - "antiFreez":10 + "antiFreez":10, + "maxKW": 24 }, { "global": 0, @@ -41,10 +39,10 @@ "type": "Reading", "subtype": "DHWControl", "id": "dhw", - "widget": "anydataDef", + "widget": "", "page": "Boiler", "descr": "Котёл", - "int": 60, + "int": 1, "value": "...", "idTdhw": "TDhw", "idReleDhw": "ReleDhw", @@ -53,7 +51,23 @@ "idSetDHW":"SetDHW", "minDhw": 20, "maxDhw": 60, - "gistDhw": 2 + "gistDhw": 2, + "numStepDhw":1 + }, + { + "global": 0, + "name": "OpenThermSlave", + "type": "Reading", + "subtype": "OpenThermSlave", + "id": "otslave", + "widget": "", + "page": "Boiler", + "descr": "Котёл", + "int": 1, + "value": "...", + "RX_pin": 13, + "TX_pin": 15, + "MemberID": 0 } ], "about": { @@ -62,28 +76,26 @@ "authorGit": "https://github.com/Mit4el", "specialThanks": "", "moduleName": "SmartBoiler", - "moduleVersion": "0.1", + "moduleVersion": "2.0", "usedRam": { "esp32_4mb": 15, "esp8266_4mb": 15 }, "subTypes": [ "BoilerControl", - "OpenThermSlave" + "OpenThermSlave", + "DHWControl" ], "title": "SmartBoiler", - "moduleDesc": "Модуль для автоматизации электрического котла. Мозги котла с внешним протоколом opentherm", + "moduleDesc": "Модуль для автоматизации электрического котла. Мозги котла с внешним протоколом opentherm. Модуль OpenThermSlave_v2 id модулй использует теже, что указаны в BoilerControl_v2. Но так же может работать автономно, если нет модуля BoilerControl_v2, он ищет модули по ID по умолчаию", "propInfo": { - "int": "Интервал отправки данных в MQTT и web интерфейс", + "int": "Интервал обработки логики и опроса внешних модулей", "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 (ноль)", + "idTret": "ID датчика температуры обратки котла, только для передачи по opentherm", + "idToutside": "ID датчика уличной температуры, только для передачи по opentherm", "Pupm": "1-есть реле насоса (ID реле должно называться relePump), 0-нет реле насоса, насос управляется котлом без нас", "minCH": "Граница установки температуры СО", "maxCH": "Граница установки температуры СО", @@ -94,7 +106,9 @@ "minDhw": "Граница установки температуры ГВС", "maxDhw": "Граница установки температуры ГВС", "changeRele":"Будет менять каждый раз при включении тэн 1->2->3->1...", - "antiFreez":"Режим анти-замерзания, Указывается температура, если опустится ниже указанной, то включится нарев один тэн и нагреет на +5гр от указанной" + "antiFreez":"Режим анти-замерзания, Указывается температура, если опустится ниже указанной, то включится нарев один тэн и нагреет на +5гр от указанной", + "maxKW": "Максимальная мощность котла при включении на поcледнем Шаге Мощности", + "numStepDhw":"На каком Шаге Мощности включать ГВС" }, "funcInfo": [ { @@ -122,7 +136,28 @@ "name": "DHWEnable", "descr": "включить / выключить ГВС", "params": [ - "dhw.DHWEnable(1) - вкл, dhw.DHWEnable(0) - выкл, " + "dhw.DHWEnable(1) - вкл, dhw.DHWEnable(0) - выкл " + ] + }, + { + "name": "addStepPower", + "descr": "Добавить Шаг Нагрева: мощность Шага кВт, ID реле на данном шаге", + "params": [ + "bolier.addStepPower(1, 3, rele1) - шаг №1 в 3kW на первом реле, bolier.addStepPower(4, 24, rele1, rele3, rele4) - шаг 4 в 24Квт на 1+3+4 реле " + ] + }, + { + "name": "onStepPower", + "descr": "включить определенный шаг нагрева, указывается номер шага, Включит Ручной Режим! ", + "params": [ + "bolier.onStepPower(2) " + ] + }, + { + "name": "autoPower", + "descr": "включить автоматический режим работы котла (по умолчанию включен) ", + "params": [ + "bolier.autoPower()" ] } ] diff --git a/src/modules/exec/SmartBoiler/readme.txt b/src/modules/exec/SmartBoiler/readme.txt index 3113d406..145d8cd4 100644 --- a/src/modules/exec/SmartBoiler/readme.txt +++ b/src/modules/exec/SmartBoiler/readme.txt @@ -34,7 +34,7 @@ 3.2 Управление модулем из сценария 3.3 есть проверка ошибок датчиков (если отвалились датчика, котел не включится) 3.4 Отправка состояния в телеграмм -3.998 3.4 TODO Автоматическая отправка состояния в модули для отображения (имена модулей в логах "new") +3.998 TODO Автоматическая отправка состояния в модули для отображения (имена модулей в логах "new") 3.999 другой функционал IoTManager ... 4 Возможность управления циркуляционным насосом diff --git a/src/modules/exec/SmartBoiler/smartBoiler.json b/src/modules/exec/SmartBoiler/smartBoiler.json index 7af83ffe..201aa6f4 100644 --- a/src/modules/exec/SmartBoiler/smartBoiler.json +++ b/src/modules/exec/SmartBoiler/smartBoiler.json @@ -1,17 +1,6 @@ { "mark": "iotm", "config": [ - { - "global": 0, - "type": "Writing", - "subtype": "TelegramLT", - "id": "tg", - "widget": "", - "page": "", - "descr": "", - "token": "", - "chatID": "" - }, { "global": 0, "type": "Reading", @@ -146,7 +135,7 @@ "subtype": "Variable", "id": "ModLevel", "needSave": 0, - "widget": "anydataDef", + "widget": "anydataHum", "page": "Состояние", "descr": "Модуляция", "int": "0", @@ -168,117 +157,6 @@ "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", @@ -295,17 +173,37 @@ "multiply": 1, "round": 0 }, + { + "global": 0, + "needSave": 0, + "type": "Writing", + "subtype": "ThermostatPID", + "id": "PID", + "widget": "anydataHum", + "page": "Котёл", + "descr": "термостат", + "int": 60, + "round": 1, + "map": "1,100,1,100", + "set_id": "SetCH", + "term_id": "Tboiler", + "term_rezerv_id": "", + "rele": "", + "KP": 5, + "KI": 50, + "KD": 1 + }, { "global": 0, "type": "Reading", "subtype": "BoilerControl", - "id": "boiler81", - "widget": "nil", - "page": "Boiler", + "id": "boiler", + "widget": "anydataWt", + "page": "Котёл", "descr": "Котёл", - "int": 60, + "int": 1, "value": "...", - "LogLevel": 0, + "debug": "0", "telegram": 1, "idPID": "PID", "idTboiler": "Tboiler", @@ -318,20 +216,22 @@ "idCmdDHW": "CmdDHW", "idSetCH": "SetCH", "idCtrlType": "CtrlType", - "rele1_Pwr": 1, - "rele2_Pwr": 2, - "rele3_Pwr": 4, "changeRele": 0, - "Pump": 0, + "idRelePump": "relePump", "minCH": 35, "maxCH": 85, "gistCH": 5, - "antiFreez": 10 + "antiFreez": 10, + "maxKW": "8" } ] } scenario=>if onStart then { - tg.sendMsg("SmartBoiler http://" + getIP()); + boiler.addStepPower(1, 3, "rele1"); +boiler.addStepPower(2, 5, "rele2", "rele3"); +boiler.addStepPower(3, 8, "rele1", "rele2", "rele3"); +#boiler.onStepPower(2); +#boiler.autoPower(); }