From 15de2955fbe42cf30106b643081000a518005369 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Mon, 12 Feb 2024 20:49:36 +0300 Subject: [PATCH] smartBoiler --- src/modules/exec/OpenThermSlave/OpenTherm.cpp | 812 ++++++++++++++++++ src/modules/exec/OpenThermSlave/OpenTherm.h | 208 +++++ .../exec/OpenThermSlave/OpenThermSlave.cpp | 589 +++++++++++++ src/modules/exec/OpenThermSlave/modinfo.json | 111 +++ src/modules/exec/SmartBoiler/BoilerHeader.h | 78 ++ src/modules/exec/SmartBoiler/SmartBoiler.cpp | 582 +++++++++++++ src/modules/exec/SmartBoiler/modinfo.json | 136 +++ src/modules/exec/SmartBoiler/readme.txt | 71 ++ src/modules/exec/SmartBoiler/smartBoiler.json | 337 ++++++++ 9 files changed, 2924 insertions(+) create mode 100644 src/modules/exec/OpenThermSlave/OpenTherm.cpp create mode 100644 src/modules/exec/OpenThermSlave/OpenTherm.h create mode 100644 src/modules/exec/OpenThermSlave/OpenThermSlave.cpp create mode 100644 src/modules/exec/OpenThermSlave/modinfo.json create mode 100644 src/modules/exec/SmartBoiler/BoilerHeader.h create mode 100644 src/modules/exec/SmartBoiler/SmartBoiler.cpp create mode 100644 src/modules/exec/SmartBoiler/modinfo.json create mode 100644 src/modules/exec/SmartBoiler/readme.txt create mode 100644 src/modules/exec/SmartBoiler/smartBoiler.json diff --git a/src/modules/exec/OpenThermSlave/OpenTherm.cpp b/src/modules/exec/OpenThermSlave/OpenTherm.cpp new file mode 100644 index 00000000..8f6f7467 --- /dev/null +++ b/src/modules/exec/OpenThermSlave/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/OpenThermSlave/OpenTherm.h b/src/modules/exec/OpenThermSlave/OpenTherm.h new file mode 100644 index 00000000..aeea5d1b --- /dev/null +++ b/src/modules/exec/OpenThermSlave/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/OpenThermSlave/OpenThermSlave.cpp b/src/modules/exec/OpenThermSlave/OpenThermSlave.cpp new file mode 100644 index 00000000..59357de9 --- /dev/null +++ b/src/modules/exec/OpenThermSlave/OpenThermSlave.cpp @@ -0,0 +1,589 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include +#include "OpenTherm.h" + +#define SLAVE true +#define TIMEOUT_TRESHOLD 5 + +namespace _OpenThermSlave +{ + 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(); + } + + // команды/установки от термостата + 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 fl_fail = 0; + failCode fCode; + float RelModLevel = 0; + float Tboiler = 0; + float Tret = 0; + float Tdhw = 0; + float Toutside = 0; + } state; + + // конфигурация котла + struct ConfigBoiler + { + bool dhw = false; // 1- есть реле(трехходовой) ГВС + bool ctrlType = false; // 0 - модуляция, 1- вкл/выкл + bool confDhw = true; // 1 - бак, 0 - проточная //TODO ПОКА НЕ ЗНАЮ ЧТО ДЕЛАТЬ + bool pumpControlMaster = false; // в протоколе ОТ: мастер управляет насосом ????????????????????? //TODO Команды кправления насосом от мастера не помню + + int minDhw; + int maxDhw; + int minCH; + int maxCH; + + } conf; + + // DynamicJsonDocument OpenThemData(JSON_BUFFER_SIZE / 2); + + IoTItem *tmp; + + IoTItem *_idTboiler = nullptr; + IoTItem *_idTret = nullptr; + IoTItem *_idToutside = nullptr; + IoTItem *_idStateCH = nullptr; + IoTItem *_idStateDHW = nullptr; + IoTItem *_idStateFlame = nullptr; + IoTItem *_idModLevel = nullptr; + IoTItem *_idTDhw = nullptr; + IoTItem *_idCmdCH = nullptr; + IoTItem *_idCmdDHW = nullptr; + IoTItem *_idSetCH = nullptr; + IoTItem *_idSetDHW = nullptr; + IoTItem *_idCtrlType = nullptr; + + unsigned long timeout_count = 0; + + uint8_t _debug = 0; + bool _telegram = false; + unsigned long ot_response = 0; + uint8_t SlaveMemberIDcode = 0; + + + void publishNew(String widget, String 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); + } + } + + /* + * ========================================================================================= + * КЛАСС РАБОТЫ ПО ПРОТОКОЛУ 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", (int &)SlaveMemberIDcode); + + jsonRead(parameters, "LogLevel", (int &)_debug); + jsonRead(parameters, "telegram", _telegram); + + String 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, "idStateDHW", tmpID); + _idStateDHW = findIoTItem(tmpID); + jsonRead(parameters, "idStateFlame", tmpID); + _idStateFlame = findIoTItem(tmpID); + jsonRead(parameters, "idModLevel", tmpID); + _idModLevel = findIoTItem(tmpID); + jsonRead(parameters, "idTDhw", tmpID); + _idTDhw = findIoTItem(tmpID); + jsonRead(parameters, "idCmdCH", tmpID); + _idCmdCH = findIoTItem(tmpID); + jsonRead(parameters, "idCmdDHW", tmpID); + _idCmdDHW = findIoTItem(tmpID); + jsonRead(parameters, "idCtrlType", tmpID); + _idCtrlType = findIoTItem(tmpID); + + jsonRead(parameters, "idSetCH", tmpID); + _idSetCH = findIoTItem(tmpID); + jsonRead(parameters, "idSetDHW", tmpID); + _idSetDHW = findIoTItem(tmpID); + + jsonRead(parameters, "minCH", conf.minCH); + jsonRead(parameters, "maxCH", conf.maxCH); + jsonRead(parameters, "minDhw", conf.minDhw); + jsonRead(parameters, "maxDhw", conf.maxDhw); + + instance_OTdriver(_RX_pin, _TX_pin); + ot_driver->begin(handleInterruptSlave, processRequest); // responseCallback +// ot_boiler = this; + } + + void doByInterval() + { + } + + // Основной цикл программы + 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; + _idCmdCH->setValue(val, true); + val.valD = set.cmd_dhwEnable; + _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", "не подключен"); + timeout_count = TIMEOUT_TRESHOLD; + sendTelegramm(("OpenTherm: потеря связи с управляющим устройством (термостатом) ❌")); + } + break; + case OpenThermResponseStatus::SUCCESS: + timeout_count = 0; + publishNew("boilerslave", "✅"); + // publishNew("status", "подключен"); + // sendTelegramm(("OpenTherm: котёл подключен ✅")); + // respondense_flag = true; + // ts_ = new_ts_; + HandleRequest(request); + break; + default: + break; + } + } + + // Парсинг запросов + static void HandleRequest(unsigned long request) + { + if (_idCtrlType) + 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 > 2) + { + 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; + _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; + _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 > 2) + { + SerialPrint("i", "OpenThermSlave ->", String(millis()) + " ID: " + String(id) + " / responseHEX: " + String(ot_response, HEX) + " / response: " + String(ot_response)); + } + } + + ~OpenThermSlave() + { + delete ot_driver; + ot_driver = nullptr; + } + }; +} + + +void *getAPI_OpenThermSlave(String subtype, String param) +{ + if (subtype == F("OpenThermSlave")) + { + return new _OpenThermSlave::OpenThermSlave(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/exec/OpenThermSlave/modinfo.json b/src/modules/exec/OpenThermSlave/modinfo.json new file mode 100644 index 00000000..2125a60f --- /dev/null +++ b/src/modules/exec/OpenThermSlave/modinfo.json @@ -0,0 +1,111 @@ +{ + "menuSection": "executive_devices", + "configItem": [ + { + "global": 0, + "name": "OpenThermSlave", + "type": "Reading", + "subtype": "OpenThermSlave", + "id": "otslave", + "widget": "", + "page": "Boiler", + "descr": "Котёл", + "int": 60, + "value": "...", + "RX_pin": 13, + "TX_pin": 15, + "LogLevel": 0, + "telegram": 1, + "MemberID": 0, + "confDhw":0, + "minCH": 35, + "maxCH": 85, + "minDhw": 30, + "maxDhw": 60, + "idTboiler": "Tboiler", + "idTret": "Tret", + "idToutside": "Toutside", + "idTDhw":"TDhw", + "idStateCH":"StateCH", + "idStateDHW":"StateDHW", + "idStateFlame":"StateFlame", + "idModLevel":"ModLevel", + "idCmdCH":"CmdCH", + "idCmdDHW":"CmdDHW", + "idSetCH":"SetCH", + "idSetDHW":"SetDHW", + "idCtrlType":"CtrlType" + } + ], + "about": { + "authorName": "Mikhail Bubnov", + "authorContact": "https://t.me/Mit4bmw", + "authorGit": "https://github.com/Mit4el", + "specialThanks": "", + "moduleName": "OpenThermSlave", + "moduleVersion": "0.1", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "OpenThermSlave", + "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": "Граница установки температуры ГВС" + }, + "funcInfo": [ + { + "name": "CHSet", + "descr": "Установить целевую температуру СО", + "params": [ + "тепмература СО (подачи) - bolier.CHSet(60)" + ] + }, + { + "name": "CHEnable", + "descr": "включить / выключить отопление", + "params": [ + "bolier.CHEnable(1) - вкл, bolier.CHEnable(0) - выкл, " + ] + }, + { + "name": "DHWSet", + "descr": "Установить целевую температуру ГВС", + "params": [ + "тепмература ГВС - dhw.DHWSet(40)" + ] + }, + { + "name": "DHWEnable", + "descr": "включить / выключить ГВС", + "params": [ + "dhw.DHWEnable(1) - вкл, dhw.DHWEnable(0) - выкл, " + ] + } + ] + }, + "defActive": true, + "usedLibs": { + "esp32_4mb3f": [], + "esp32*": [], + "esp82*": [] + } +} \ No newline at end of file diff --git a/src/modules/exec/SmartBoiler/BoilerHeader.h b/src/modules/exec/SmartBoiler/BoilerHeader.h new file mode 100644 index 00000000..cda61395 --- /dev/null +++ b/src/modules/exec/SmartBoiler/BoilerHeader.h @@ -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; +} diff --git a/src/modules/exec/SmartBoiler/SmartBoiler.cpp b/src/modules/exec/SmartBoiler/SmartBoiler.cpp new file mode 100644 index 00000000..15775302 --- /dev/null +++ b/src/modules/exec/SmartBoiler/SmartBoiler.cpp @@ -0,0 +1,582 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include +#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 ¶m) + { + 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 ¶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 "); + } + 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; + } +} diff --git a/src/modules/exec/SmartBoiler/modinfo.json b/src/modules/exec/SmartBoiler/modinfo.json new file mode 100644 index 00000000..9be13ca2 --- /dev/null +++ b/src/modules/exec/SmartBoiler/modinfo.json @@ -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*": [] + } +} \ No newline at end of file diff --git a/src/modules/exec/SmartBoiler/readme.txt b/src/modules/exec/SmartBoiler/readme.txt new file mode 100644 index 00000000..3113d406 --- /dev/null +++ b/src/modules/exec/SmartBoiler/readme.txt @@ -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, строкой: "не подключен" / "подключен" diff --git a/src/modules/exec/SmartBoiler/smartBoiler.json b/src/modules/exec/SmartBoiler/smartBoiler.json new file mode 100644 index 00000000..7af83ffe --- /dev/null +++ b/src/modules/exec/SmartBoiler/smartBoiler.json @@ -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()); + }