diff --git a/myProfile.json b/myProfile.json index fd7485a8..c96b8761 100644 --- a/myProfile.json +++ b/myProfile.json @@ -212,6 +212,10 @@ "path": "src/modules/virtual/Ping", "active": true }, + { + "path": "src/modules/virtual/SolarCalc", + "active": false + }, { "path": "src/modules/virtual/Timer", "active": true @@ -374,6 +378,10 @@ "path": "src/modules/sensors/MQgas", "active": false }, + { + "path": "src/modules/sensors/NoiseAdc", + "active": false + }, { "path": "src/modules/sensors/Ntc", "active": true @@ -382,6 +390,10 @@ "path": "src/modules/sensors/Pcf8591", "active": false }, + { + "path": "src/modules/sensors/Presence", + "active": false + }, { "path": "src/modules/sensors/Pzem004t", "active": false @@ -422,6 +434,10 @@ "path": "src/modules/sensors/Sht30", "active": false }, + { + "path": "src/modules/sensors/SoftRTC", + "active": false + }, { "path": "src/modules/sensors/Sonar", "active": false @@ -548,6 +564,10 @@ "path": "src/modules/exec/Thermostat", "active": true }, + { + "path": "src/modules/exec/WakeOnLanModule", + "active": false + }, { "path": "src/modules/sensors/Ds2423", "active": false @@ -566,6 +586,10 @@ "path": "src/modules/display/Lcd2004", "active": false }, + { + "path": "src/modules/display/LedFX", + "active": false + }, { "path": "src/modules/display/Nextion", "active": false @@ -590,6 +614,10 @@ "path": "src/modules/display/TM16XX", "active": false }, + { + "path": "src/modules/display/U8g2lib", + "active": false + }, { "path": "src/modules/display/Ws2812b", "active": false diff --git a/src/modules/exec/EctoControlAdapter/AdapterCommon.h b/src/modules/exec/EctoControlAdapter/AdapterCommon.h deleted file mode 100644 index 4b6b7bca..00000000 --- a/src/modules/exec/EctoControlAdapter/AdapterCommon.h +++ /dev/null @@ -1,182 +0,0 @@ -#pragma once -#include - -#define comm_RebootAdapter 2u -#define comm_LockOutReset 3u - -struct BoilerInfo -{ - uint8_t adapterType; // тип адаптера. 000 - Opentherm 001 - eBus 010 - Navien - uint8_t boilerStatus; // состояние связи с котлом 0 - нет ответа от котла на последнюю команду 1 - есть ответ от котла на последнюю команду - uint8_t rebootStatus; // Код последней перезагрузки адаптера. 0...255 - код - uint8_t adapterHardVer; // Аппаратная версия адаптера. 0...255 - номер версии - uint8_t adapterSoftVer; // u8 Программная версия адаптера. 0...255 - номер версии - uint16_t boilerMemberCode; // 0x0021 R u16 Код производителя котла. Зависит от марки и модели котла. 0...65535 - код производителя - uint16_t boilerModelCode; // 0x0022 R u16 Код модели котла. Зависит от марки и модели котла. 0...65535 - код модели -}; - -struct BoilerStatus -{ - uint8_t burnStatus; // бит 0 - текущее состояние горелки 0 - отключена 1 - включена - uint8_t CHStatus; // бит 1 - текущее состояние отопления 0 - отключено 1 - включено - uint8_t DHWStatus; // бит 2 - текущее состояние ГВС 0 - отключено 1 - включено -}; - -// Флаги ошибок (только для котлов с интерфейсом OpenTherm) -enum FlagErrorOT //: uitn8_t -{ - er_SecviceReq, // 0: Необходимо обслуживание - er_LockOut, // 1: Котел заблокирован - er_LowWater, // 2: Низкое давление в отопительном контуре - er_FlameFault, // 3: Ошибка розжига - er_AirPresFault, // 4: Низкое давление воздуха - er_OverTem // 5: Перегрев теплоносителя в контуре -}; - -////////////////// Данные регистров второй версии адаптера котла -//////////////////// Регистры для чтения - -enum ReadDataEctoControl //: uitn16_t -{ - ecR_AdapterInfo = 0x0010, // 0x0010 R bitfields бит 2...0 - тип адаптера. 000 - Opentherm 001 - eBus 010 - Navien - // бит 3 - состояние связи с котлом 0 - нет ответа от котла на последнюю команду 1 - есть ответ от котла на последнюю команду - // u8 Код последней перезагрузки адаптера. 0...255 - код - - ecR_AdaperVersion = 0x0011, // 0x0011 R u8 Аппаратная версия адаптера. 0...255 - номер версии - // u8 Программная версия адаптера. 0...255 - номер версии - - ecR_Time = 0x0012, // 0x0012 - 0x0013 R u32 Время работы адаптера после перезагрузки 0...4294967295 - время в секундах - ecR_MinSetCH = 0x0014, // 0x0014 R u8 Нижний предел уставки теплоносителя. 0...100 - температура уставки в град. С - ecR_MaxSetCH = 0x0015, // 0x0015 R u8 Верхний предел уставки теплоносителя 0...100 - температура уставки в град. С - ecR_MinSetDHW = 0x0016, // 0x0016 R u8 Нижний предел уставки ГВС 0...100 - температура уставки в град. С - ecR_MaxSetDHW = 0x0017, // 0x0017 R u8 Верхний предел уставки ГВС 0...100 - температура уставки в град. С - ecR_TempCH = 0x0018, // 0x0018 R i16 Текущая температура теплоносителя -100...100 - температура в 0.1 гр. С - ecR_TempDHW = 0x0019, // 0x0019 R u16 Текущая температура ГВС 0...100 - температура в 0.1 гр. С - ecR_Pressure = 0x001A, // 0x001A R u8 Текущее Давление в контуре 0...50 - давление в (0,1бар) - ecR_FlowRate = 0x001B, // 0x001B R u8? Текущий расход ГВС 1...255 - расход в (0,1 л/мин) - ecR_ModLevel = 0x001C, // 0x001C R u8? Текущая модуляция горелки 0xFF - не определено 0...100 - модуляция в (%) - ecR_BoilerStatus = 0x001D, // 0x001D R bitfields бит 0 - текущее состояние горелки 0 - отключена 1 - включена - // бит 1 - текущее состояние отопления 0 - отключено 1 - включено - // бит 2 - текущее состояние ГВС 0 - отключено 1 - включено - - ecR_CodeError = 0x001E, // 0x001E R u16 Код ошибки котла (основной). Зависит от марки и модели котла. 0...65535 - код ошибки - ecR_CodeErrorExt = 0x001F, // 0x001F R u16 Код ошибки котла (дополнительный). Зависит от марки и модели котла. 0...65535 - код ошибки - ecR_TempOutside = 0x0020, // 0x0020 R s8 Температура уличного датчика котла (при егоналичии). -65…+100 температура в градусах С - ecR_MemberCode = 0x0021, // 0x0021 R u16 Код производителя котла. Зависит от марки и модели котла. 0...65535 - код производителя - ecR_ModelCode = 0x0022, // 0x0022 R u16 Код модели котла. Зависит от марки и модели котла. 0...65535 - код модели - ecR_FlagErrorOT = 0x0023 // 0x0023 R s8 Флаги ошибок (только для котлов с интерфейсом OpenTherm) - // 0: Необходимо обслуживание - // 1: Котел заблокирован - // 2: Низкое давление в отопительном контуре - // 3: Ошибка розжига - // 4: Низкое давление воздуха - // 5: Перегрев теплоносителя в контуре -}; - -////////////////////////////// WRITE ///////////// - -enum WriteDataEctoControl //: uitn16_t -{ - ecW_SetTypeConnect = 0x0030, // 0x0030 W u8 Тип внешних подключений (будет сохранено в постоянной памяти адаптера) - // 0 - адаптер подключен к котлу - // 1 - котел подключен к внешнему устройству (панель или перемычка) - - ecW_TSetCH = 0x0031, // 0x0031 W int16 Уставка теплоносителя (будет сохранено в постоянной памяти адаптера). - // Будет передана котлу при старте адаптера, пока главным устройством не - // были записаны регистры уставки температуры. - // 0...1000 - температура уставки в десятых долях градуса С (например, для - // установки 45С нужно записать число 450). Во многих котлах необходимо до - // подключения адаптера необходимо поднять температуру теплоносителя - // отопления (нажимая “+” на панели котла) для согласования диапазона - // температуры теплоносителя, в противном случае температура теплоносителя - // может не достигнуть требуемого значения. - - ecW_TSetCHFaultConn = 0x0032, // 0x0032 W int16 Уставка теплоносителя в аварийном режиме(будет сохранено в постоянной памяти адаптера). - // Будет передана котлу в случае - // отсутствия связи с главным управляющим устройством. - // 0...1000 - температура уставки в десятых долях град. С (например, - // для установки 45С нужно записать число 450) - - ecW_TSetMinCH = 0x0033, // 0x0033 W u8 Нижний предел уставки теплоносителя 0...100 - температура уставки в град. С - // Не все котлы поддерживают этот параметр. Как правило, этот - // предел не должен быть ниже аналогичного предела, - // установленного в настройках котла. - - ecW_TSetMaxCH = 0x0034, // 0x0034 W u8 Верхний предел уставки теплоносителя 0...100 - температура уставки в град. С - // Не все котлы поддерживают этот параметр. Как правило, этот - // предел не должен быть выше аналогичного предела, - // установленного в настройках котла. - - ecW_TSetMinDHW = 0x0035, // 0x0035 W u8 Нижний предел уставки ГВС 0...100 - температура уставки в град. С - // Не все котлы поддерживают этот параметр. Как правило, этот - // предел не должен быть ниже аналогичного предела, - // установленного в настройках котла. - - ecW_TSetMaxDHW = 0x0036, // 0x0036 W u8 Верхний предел уставки ГВС 0...100 - температура уставки в град. С - // Не все котлы поддерживают этот параметр. Как правило, этот - // предел не должен быть выше аналогичного предела, - // установленного в настройках котла. - - ecW_TSetDHW = 0x0037, // 0x0037 W u8 Уставка ГВС (EPROM) 0...100 - температура уставки в град. С - // Для большинства котлов эта уставка должна находиться ниже - // предела, установленного в меню самого котла, иначе она может быть - // проигнорирована котлом. Для некоторых котлов стоит устанавливать - // этот параметр равным верхнему пределу уставки ГВС. - - ecW_SetMaxModLevel = 0x0038, // 0x0038 W u8 Уставка максимальной модуляции горелки (будет сохранено в постоянной памяти адаптера) 0...100 - уровень модуляции в процентах. - // Данный параметр поддерживается не всеми котлами. Для - // электрических трехфазных котлов возможно задать только 3 - // уровня модуляции (в диапазоне 0…100%). - - ecW_SetStatusBoiler = 0x0039, // 0x0039 W bitfields бит 0 - режим контура отопления (будет сохранено в постоянной памяти адаптера) 0 - отключен 1 - включен - // бит 1 - режим контура ГВС 0 - отключен 1 - включен - // бит 2 - “второй контур”, используется только некоторыми котлами с - // интерфейсом OpenTherm и может отвечать за активацию бойлера - // косвенного нагрева или встроенной функции ГВС. 0 - отключен 1 - включен - - - ecW_Command = 0x0080 // 0x0080 W uint16 команда - // 0 - нет команды - // 1 - CH water filling (зарезервировано) - // 2 - перезагрузка адаптера - // 3 - сброс ошибок котла - // 4..65525 - зарезервировано - // При записи любой команды, кроме «нет команды», сразу же меняется - // состояние регистра «Ответ на команду» - становится 2 – идет - // обработка команжы - - -}; - -///////////////////////////////////////// Регистры состояния: - -/* -0x0040…0x006F R i16 состояние данных в регистре ($addr - 0x30) - -2 - ошибка чтения/записи в котел - -1 - регистр не поддерживается - 0- для регистра на чтение означает, что данные из котла прочитаны и - валидны. Для регистра на запись означает, что данные успешно приняты - котлом. - 1- не инициализирован. - если регистр R: чтение соответствующих данные из котла не - приводилось, - если регистр W: адаптеру не было задано значение для записи - соответствующего значения в котле -*/ - - -/* -0x0081 R int16 Ответ на команду - -32768..-6 - зарезервировано - -5 – ошибка выполнения команды - -4 – неподдерживаемая котлом команда - -3 – не поддерживаемый котлом идентификатор устройства - -2 – не поддерживается данный адаптером - -1 – не получен ответ за отведенное врекмя - 0 – команда выполнена успешно - 1 – не было команды (значение по умолчанию) - 2 – идет обработка команды (обмен данными) - 3..32767 - зарезервировано - Если команда после перезагрузки адаптера не давалась, регистр - читается как 1 - не было команды. -*/ diff --git a/src/modules/exec/EctoControlAdapter/EctoControlAdapter.cpp b/src/modules/exec/EctoControlAdapter/EctoControlAdapter.cpp index 18511cc6..bb1bdc47 100644 --- a/src/modules/exec/EctoControlAdapter/EctoControlAdapter.cpp +++ b/src/modules/exec/EctoControlAdapter/EctoControlAdapter.cpp @@ -2,813 +2,856 @@ #include "classes/IoTItem.h" #include #include - #include "ModbusEC.h" -#include "AdapterCommon.h" -// #include "Stream.h" #include +#include -// class ModbusUart; -Stream *_modbusUART = nullptr; +// Константы +constexpr uint8_t DEFAULT_DIR_PIN = 0; +constexpr uint8_t DEFAULT_MODBUS_ADDR = 0xF0; +constexpr uint8_t DEFAULT_DEVICE_TYPE = 0x14; +constexpr uint32_t DEFAULT_SERIAL_TIMEOUT = 500; +constexpr uint16_t INVALID_MODBUS_VALUE = 0x7FFF; +constexpr uint8_t MAX_RETRY_ATTEMPTS = 3; +constexpr uint16_t RETRY_DELAY_MS = 100; +constexpr uint16_t MAX_REGISTERS_PER_READ = 16; -#define UART_LINE 2 -uint8_t _DIR_PIN = 0; -// Modbus stuff -// Данные Modbus по умолчанию +// Команды адаптера +constexpr uint16_t COMMAND_REBOOT = 2; +constexpr uint16_t COMMAND_RESET_ERRORS = 3; -#define MODBUS_RX_PIN 18 // Rx pin -#define MODBUS_TX_PIN 19 // Tx pin -#define MODBUS_SERIAL_BAUD 9600 // Baud rate for esp32 and max485 communication +// Битовая маска для регистра статуса котла +struct BoilerStatusBits { + uint8_t heatingEnabled : 1; + uint8_t dhwEnabled : 1; + uint8_t secondaryCircuit : 1; + uint8_t reserved : 5; + + uint16_t toUint16() const { + return (heatingEnabled ? 0x0001 : 0) | + (dhwEnabled ? 0x0002 : 0) | + (secondaryCircuit ? 0x0004 : 0); + } +}; -void modbusPreTransmission() -{ - // delay(500); - if (_DIR_PIN) - digitalWrite(_DIR_PIN, HIGH); +// Оптимизированные структуры данных с упакованными полями +struct __attribute__((packed)) BoilerInfo { + uint8_t adapterType : 3; + bool boilerConnected : 1; + uint8_t rebootCode; + uint8_t hardwareVersion; + uint8_t softwareVersion; + uint32_t uptime; + uint16_t memberCode; + uint16_t modelCode; +}; + +struct __attribute__((packed)) BoilerStatus { + bool burnerActive : 1; + bool heatingActive : 1; + bool dhwActive : 1; + uint8_t errorFlags; + BoilerStatusBits writtenStatus; +}; + +// Регистры Modbus +enum ReadRegisters { + ecR_AdapterInfo = 0x0010, + ecR_AdapterVersion = 0x0011, + ecR_Uptime = 0x0012, + ecR_MinSetCH = 0x0014, + ecR_MaxSetCH = 0x0015, + ecR_MinSetDHW = 0x0016, + ecR_MaxSetDHW = 0x0017, + ecR_TempCH = 0x0018, + ecR_TempDHW = 0x0019, + ecR_Pressure = 0x001A, + ecR_FlowRate = 0x001B, + ecR_ModLevel = 0x001C, + ecR_BoilerStatus = 0x001D, + ecR_CodeError = 0x001E, + ecR_CodeErrorExt = 0x001F, + ecR_TempOutside = 0x0020, + ecR_MemberCode = 0x0021, + ecR_ModelCode = 0x0022, + ecR_FlagErrorOT = 0x0023, + ecR_ReadSetStatusBoiler = 0x003F, + // Регистры для чтения записанных значений (+6 от регистров записи) + ecR_ReadSetTypeConnect = 0x0036, + ecR_ReadTSetCH = 0x0037, + ecR_ReadTSetCHFaultConn = 0x0038, + ecR_ReadTSetMinCH = 0x0039, + ecR_ReadTSetMaxCH = 0x003A, + ecR_ReadTSetMinDHW = 0x003B, + ecR_ReadTSetMaxDHW = 0x003C, + ecR_ReadTSetDHW = 0x003D, + ecR_ReadSetMaxModLevel = 0x003E +}; + +enum WriteRegisters { + ecW_SetTypeConnect = 0x0030, + ecW_TSetCH = 0x0031, + ecW_TSetCHFaultConn = 0x0032, + ecW_TSetMinCH = 0x0033, + ecW_TSetMaxCH = 0x0034, + ecW_TSetMinDHW = 0x0035, + ecW_TSetMaxDHW = 0x0036, + ecW_TSetDHW = 0x0037, + ecW_SetMaxModLevel = 0x0038, + ecW_SetStatusBoiler = 0x0039, + ecW_Command = 0x0080 +}; + +Stream* _modbusUART = nullptr; +uint8_t _DIR_PIN = DEFAULT_DIR_PIN; + +// Оптимизированные функции управления направлением передачи +inline void modbusPreTransmission() { + if (_DIR_PIN) digitalWrite(_DIR_PIN, HIGH); } -// Pin 4 made low for Modbus receive mode -// Контакт 4 установлен на низком уровне для режима приема Modbus -void modbusPostTransmission() -{ - if (_DIR_PIN) - digitalWrite(_DIR_PIN, LOW); - // delay(500); +inline void modbusPostTransmission() { + if (_DIR_PIN) digitalWrite(_DIR_PIN, LOW); } -// ModbusMaster node; - -// RsEctoControl *rsEC; - -class EctoControlAdapter : public IoTItem -{ +class EctoControlAdapter : public IoTItem { private: - int _rx = MODBUS_RX_PIN; // адреса прочитаем с веба - int _tx = MODBUS_TX_PIN; - int _baud = MODBUS_SERIAL_BAUD; - String _prot = "SERIAL_8N1"; - int protocol = SERIAL_8N1; - uint8_t _addr = 0xF0; // Адрес слейва от 1 до 247 - uint8_t _type = 0x14; // Тип устройства: 0x14 – адаптер OpenTherm (вторая версия); 0x11 – адаптер OpenTherm (первая версия, снята с производства) - uint8_t _debugLevel; // Дебаг - + int _rx, _tx, _baud, _uartLine; + String _prot; + uint8_t _addr; + uint8_t _debugLevel; ModbusMaster node; - uint8_t _debug; - // Stream *_modbusUART; + BoilerInfo info = {}; + BoilerStatus status = {}; + float tCH = 0, tDHW = 0, tOut = 0; + float press = 0, flow = 0; + uint8_t modLevel = 0; + uint16_t codeError = 0, codeErrorExt = 0; + uint8_t errorFlags = 0; - BoilerInfo info; - BoilerStatus status; + // Кэш для часто запрашиваемых значений + uint32_t _lastUpdate = 0; + static constexpr uint32_t UPDATE_INTERVAL = 10000; // 10 секунд - uint16_t code; - uint16_t codeExt; - uint8_t flagErr; - float flow; - float maxSetCH; - float maxSetDHW; - float minSetCH; - float minSetDHW; - float modLevel; - float press; - float tCH; - float tDHW; - float tOut; - bool enableCH; - bool enableDHW; - bool enableCH2; - bool _isNetworkActive; - bool _mqttIsConnect; - -public: - EctoControlAdapter(String parameters) : IoTItem(parameters) - { - _DIR_PIN = 0; - _addr = jsonReadInt(parameters, "addr"); // адреса slave прочитаем с веба - _rx = jsonReadInt(parameters, "RX"); // прочитаем с веба - _tx = jsonReadInt(parameters, "TX"); - _DIR_PIN = jsonReadInt(parameters, "DIR_PIN"); - _baud = jsonReadInt(parameters, "baud"); - _prot = jsonReadStr(parameters, "protocol"); - jsonRead(parameters, "debug", _debugLevel); - - if (_prot == "SERIAL_8N1") - { - protocol = SERIAL_8N1; - } - else if (_prot == "SERIAL_8N2") - { - protocol = SERIAL_8N2; - } - - // Serial2.begin(baud-rate, protocol, RX pin, TX pin); - - _modbusUART = new HardwareSerial(UART_LINE); - - if (_debugLevel > 2) - { - SerialPrint("I", "EctoControlAdapter", "baud: " + String(_baud) + ", protocol: " + String(protocol, HEX) + ", RX: " + String(_rx) + ", TX: " + String(_tx)); - } - ((HardwareSerial *)_modbusUART)->begin(_baud, protocol, _rx, _tx); // выбираем тип протокола, скорость и все пины с веба - ((HardwareSerial *)_modbusUART)->setTimeout(200); - //_modbusUART = &serial; - node.begin(_addr, _modbusUART); - - node.preTransmission(modbusPreTransmission); - node.postTransmission(modbusPostTransmission); - - if (_DIR_PIN) - { - _DIR_PIN = _DIR_PIN; - pinMode(_DIR_PIN, OUTPUT); - digitalWrite(_DIR_PIN, LOW); - } - - // 0x14 – адаптер OpenTherm (вторая версия) - // 0x15 – адаптер eBus - // 0x16 – адаптер Navien - if (_addr > 0) - { - uint16_t type; - readFunctionModBus(0x0003, type); - type = type >> 8; - if (0x14 != type && 0x15 != type && 0x16 != type) - { - SerialPrint("E", "EctoControlAdapter", "Не подходящее устройство, type: " + String(type, HEX)); + // Оптимизированное чтение группы регистров + bool readRegisterBlock(uint16_t startReg, uint16_t* data, uint8_t count) { + if (count == 0 || count > MAX_REGISTERS_PER_READ) return false; + + uint8_t result = node.readHoldingRegisters(startReg, count); + if (result != node.ku8MBSuccess) { + if (_debugLevel > 0) { + SerialPrint("E", "Modbus", "Read block 0x" + String(startReg, HEX) + + "-0x" + String(startReg + count - 1, HEX) + + " failed, code: " + String(result, HEX)); } - getModelVersion(); - getBoilerInfo(); - getBoilerStatus(); - } - else if (_addr == 0) - { // если адреса нет, то шлем широковещательный запрос адреса - uint8_t addr = node.readAddresEctoControl(); - SerialPrint("I", "EctoControlAdapter", "readAddresEctoControl, addr: " + String(addr, HEX) + " - Enter to configuration"); - } - } - - void doByInterval() - { - if (_addr > 0) - { - // readBoilerInfo(); - getBoilerStatus(); - - getCodeError(); - getCodeErrorExt(); - if (info.adapterType == 0) - getFlagErrorOT(); - // getFlowRate(); - // getMaxSetCH(); - // getMaxSetDHW(); - // getMinSetCH(); - // getMinSetDHW(); - getModLevel(); - getPressure(); - getTempCH(); - getTempDHW(); - getTempOutside(); - } - } - - void loop() - { - // для новых версий IoTManager - IoTItem::loop(); - } - - IoTValue execute(String command, std::vector ¶m) - { - if (command == "getModelVersion") - { - getModelVersion(); - } - if (command == "getBoilerInfo") - { - getBoilerInfo(); - } - if (command == "getBoilerStatus") - { - getBoilerStatus(); - } - if (command == "getCodeError") - { - getCodeError(); - } - if (command == "getCodeErrorExt") - { - getCodeErrorExt(); - } - if (command == "getFlagErrorOT") - { - getFlagErrorOT(); - } - if (command == "getFlowRate") - { - getFlowRate(); - } - if (command == "getMaxSetCH") - { - getMaxSetCH(); - } - if (command == "getMaxSetDHW") - { - getMaxSetDHW(); - } - if (command == "getMinSetCH") - { - getMinSetCH(); - } - if (command == "getMinSetDHW") - { - getMinSetDHW(); - } - if (command == "getModLevel") - { - getModLevel(); - } - if (command == "getPressure") - { - getPressure(); - } - if (command == "getTempCH") - { - getTempCH(); - } - if (command == "getTempDHW") - { - getTempDHW(); - } - if (command == "getTempOutside") - { - getTempOutside(); - } - - if (command == "setTypeConnect") - { - setTypeConnect(param[0].valD); - } - if (command == "setTCH") - { - setTCH(param[0].valD); - } - if (command == "setTCHFaultConn") - { - setTCHFaultConn(param[0].valD); - } - if (command == "setMinCH") - { - setMinCH(param[0].valD); - } - if (command == "setMaxCH") - { - setMaxCH(param[0].valD); - } - if (command == "setMinDHW") - { - setMinDHW(param[0].valD); - } - if (command == "setMaxDHW") - { - setMaxDHW(param[0].valD); - } - if (command == "setTDHW") - { - setTDHW(param[0].valD); - } - if (command == "setMaxModLevel") - { - setMaxModLevel(param[0].valD); - } - if (command == "setStatusCH") - { - setStatusCH((bool)param[0].valD); - } - if (command == "setStatusDHW") - { - setStatusDHW((bool)param[0].valD); - } - if (command == "setStatusCH2") - { - setStatusCH2((bool)param[0].valD); - } - - if (command == "lockOutReset") - { - lockOutReset(); - } - if (command == "rebootAdapter") - { - rebootAdapter(); - } - return {}; - } - - void publishData(String widget, String status) - { - - IoTItem *tmp = findIoTItem(widget); - if (tmp) - tmp->setValue(status, true); - else - { - if (_debugLevel > 0) - SerialPrint("i", "NEW", widget + " = " + status); - } - } - - static void sendTelegramm(String msg) - { - if (tlgrmItem) - tlgrmItem->sendTelegramMsg(false, msg); - } - - ~EctoControlAdapter(){}; - - bool writeFunctionModBus(const uint16_t ®, uint16_t &data) - { - // set word 0 of TX buffer to least-significant word of counter (bits 15..0) - // node.setTransmitBuffer(1, lowWord(data)); - // set word 1 of TX buffer to most-significant word of counter (bits 31..16) - node.setTransmitBuffer(0, data); - // slave: write TX buffer to (2) 16-bit registers starting at register 0 - uint8_t result = node.writeMultipleRegisters(reg, 1); - node.clearTransmitBuffer(); - if (_debug > 2) - { - SerialPrint("I", "EctoControlAdapter", "writeSingleRegister, addr: " + String((uint8_t)_addr, HEX) + ", reg: 0x" + String(reg, HEX) + ", state: " + String(data) + " = result: " + String(result, HEX)); - } - if (result == 0) - return true; - else return false; + } + + for (uint8_t i = 0; i < count; i++) { + data[i] = node.getResponseBuffer(i); + if (_debugLevel > 2) { + SerialPrint("I", "Modbus", "Read 0x" + String(startReg + i, HEX) + + " = " + String(data[i])); + } + } + + node.clearResponseBuffer(); + return true; } - bool readFunctionModBus(const uint16_t ®, uint16_t &reading) - { - if (_addr == 0) return false; - // float retValue = 0; - if (_modbusUART) - { - // if (!addr) - // addr = _addr; - node.begin(_addr, _modbusUART); - uint8_t result; - // uint32_t reading; - - if (reg == 0x0000) - result = node.readHoldingRegisters(reg, 4); - else - result = node.readHoldingRegisters(reg, 1); - if (_debug > 2) - SerialPrint("I", "EctoControlAdapter", "readHoldingRegisters, addr: " + String(_addr, HEX) + ", reg: 0x" + String(reg, HEX) + " = result: " + String(result, HEX)); - // break; - if (result == node.ku8MBSuccess) - { - if (reg == 0x0000) - { - reading = node.getResponseBuffer(0x03); - reading = (uint16_t)((uint8_t)(reading) >> 8); - SerialPrint("I", "EctoControlAdapter", "read info, addr: " + String(_addr, HEX) + ", type: " + String(reading, HEX)); + // Оптимизированное чтение с повторными попытками + bool readWithRetry(uint16_t reg, uint16_t &reading) { + uint8_t attempts = 0; + while (attempts < MAX_RETRY_ATTEMPTS) { + if (readRegisterBlock(reg, &reading, 1)) { + if (reading != INVALID_MODBUS_VALUE) { + return true; } - else - { - reading = node.getResponseBuffer(0x00); - if (_debug > 2) - SerialPrint("I", "EctoControlAdapter", "Success, Received data, register: " + String(reg) + " = " + String(reading, HEX)); - } - node.clearResponseBuffer(); } - else - { - if (_debug > 2) - SerialPrint("E", "EctoControlAdapter", "Failed, Response Code: " + String(result, HEX)); - return false; - } - - if (reading != 0x7FFF) - return true; - else - return false; + attempts++; + delay(RETRY_DELAY_MS); + } + + if (_debugLevel > 0) { + SerialPrint("E", "Modbus", "Read reg 0x" + String(reg, HEX) + + " failed after " + String(MAX_RETRY_ATTEMPTS) + " attempts"); } return false; } - bool getModelVersion() - { - uint16_t reqData; - bool ret; - ret = readFunctionModBus(ReadDataEctoControl::ecR_MemberCode, info.boilerMemberCode); - ret = readFunctionModBus(ReadDataEctoControl::ecR_ModelCode, info.boilerModelCode); - ret = readFunctionModBus(ReadDataEctoControl::ecR_AdaperVersion, reqData); - info.adapterHardVer = highByte(reqData); - info.adapterSoftVer = lowByte(reqData); - return ret; + // Оптимизированная запись с повторными попытками + bool writeWithRetry(uint16_t reg, uint16_t data) { + uint8_t attempts = 0; + while (attempts < MAX_RETRY_ATTEMPTS) { + node.setTransmitBuffer(0, data); + uint8_t result = node.writeMultipleRegisters(reg, 1); + node.clearTransmitBuffer(); + + if (result == node.ku8MBSuccess) { + if (_debugLevel > 2) { + SerialPrint("I", "Modbus", "Write 0x" + String(reg, HEX) + + " = " + String(data)); + } + return true; + } + attempts++; + delay(RETRY_DELAY_MS); + } + + if (_debugLevel > 0) { + SerialPrint("E", "Modbus", "Write reg 0x" + String(reg, HEX) + + " failed after " + String(MAX_RETRY_ATTEMPTS) + " attempts"); + } + return false; } - bool getBoilerInfo() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_AdapterInfo, reqData); - info.adapterType = highByte(reqData) & 0xF8; - info.boilerStatus = (highByte(reqData) >> 3) & 1u; - info.rebootStatus = lowByte(reqData); - if (ret) - { - publishData("adapterType", String(info.adapterType)); - publishData("boilerStatus", String(info.boilerStatus)); - publishData("rebootStatus", String(info.rebootStatus)); + // Оптимизированная публикация данных + void publishData(const __FlashStringHelper* name, const String& value) { + IoTItem* item = findIoTItem(name); + if (item) { + item->setValue(value, true); + } else if (_debugLevel > 0) { + SerialPrint("I", "EctoControl", String(name) + " = " + value); } - return ret; } - bool getBoilerStatus() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_BoilerStatus, reqData); - status.burnStatus = (lowByte(reqData) >> 0) & 1u; - status.CHStatus = (lowByte(reqData) >> 1) & 1u; - status.DHWStatus = (lowByte(reqData) >> 2) & 1u; - if (ret) - { - publishData("burnStatus", String(status.burnStatus)); - publishData("CHStatus", String(status.CHStatus)); - publishData("DHWStatus", String(status.DHWStatus)); + + // Чтение и обработка ошибок котла + bool readBoilerErrors() { + uint16_t registers[3]; + if (!readRegisterBlock(ecR_CodeError, registers, 3)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to read boiler errors"); + return false; } - return ret; - } - bool getCodeError() - { - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_CodeError, code); - if (ret) - { - publishData("codeError", String(code)); - if (code) - sendTelegramm("EctoControlAdapter: код ошибки: " + String((int)code)); + + codeError = registers[0]; + codeErrorExt = registers[1]; + errorFlags = registers[2] & 0xFF; + + publishData(F("codeError"), String(codeError)); + publishData(F("codeErrorExt"), String(codeErrorExt)); + publishData(F("errorFlags"), String(errorFlags)); + + if (_debugLevel > 1) { + SerialPrint("I", "EctoControl", String("Boiler errors: ") + + "Main=" + String(codeError, HEX) + + ", Ext=" + String(codeErrorExt, HEX) + + ", Flags=" + String(errorFlags, HEX)); } - return ret; + + return true; } - bool getCodeErrorExt() - { - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_CodeErrorExt, codeExt); - if (ret) - { - publishData("codeErrorExt", String(codeExt)); - if (codeExt) - sendTelegramm("EctoControlAdapter: код ошибки: " + String((int)codeExt)); + + // Групповое чтение основных данных + bool readCommonData() { + uint16_t registers[9]; // Увеличено до 9 регистров + if (!readRegisterBlock(ecR_TempCH, registers, 9)) { + return false; } - return ret; + + // Обработка температуры отопления + int16_t tempCH = registers[0]; + if (tempCH != INVALID_MODBUS_VALUE) { + tCH = tempCH / 10.0f; + publishData(F("tempCH"), String(tCH)); + } + + // Обработка температуры ГВС + uint16_t tempDHW = registers[1]; + if (tempDHW != INVALID_MODBUS_VALUE) { + tDHW = tempDHW / 10.0f; + publishData(F("tempDHW"), String(tDHW)); + } + + // Обработка давления + uint16_t pressure = registers[2]; + if (pressure != INVALID_MODBUS_VALUE) { + press = (pressure & 0xFF) / 10.0f; + publishData(F("pressure"), String(press)); + } + + // Обработка расхода ГВС + uint16_t flowRate = registers[3]; + if (flowRate != INVALID_MODBUS_VALUE) { + flow = (flowRate & 0xFF) / 10.0f; + publishData(F("flowRate"), String(flow)); + } + + // Обработка уровня модуляции + uint16_t modulation = registers[4]; + if (modulation != INVALID_MODBUS_VALUE) { + modLevel = modulation & 0xFF; + publishData(F("modLevel"), String(modLevel)); + } + + // Обработка статуса котла + uint16_t statusReg = registers[5]; + status.burnerActive = statusReg & 0x01; // Бит 0: горелка + status.heatingActive = (statusReg >> 1) & 0x01; // Бит 1: отопление + status.dhwActive = (statusReg >> 2) & 0x01; // Бит 2: ГВС + + publishData(F("burner"), String(status.burnerActive)); + publishData(F("heating"), String(status.heatingActive)); + publishData(F("dhw"), String(status.dhwActive)); + + // Обработка кодов ошибок + codeError = registers[6]; + codeErrorExt = registers[7]; + + // Обработка температуры наружного воздуха + int8_t tempOut = (int8_t)(registers[8] & 0xFF); + if (tempOut != 0x7F) { + tOut = tempOut; + publishData(F("tempOut"), String(tOut)); + } + + // Чтение ошибок котла + readBoilerErrors(); + + return true; } - bool getFlagErrorOT() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_FlagErrorOT, reqData); - flagErr = lowByte(reqData); - if (ret) - { - publishData("flagErr", String(flagErr)); - switch (flagErr) - { - case 0: - sendTelegramm("EctoControlAdapter: Необходимо обслуживание!"); - break; - case 1: - sendTelegramm("EctoControlAdapter: Котёл заблокирован!"); - break; - case 2: - sendTelegramm("EctoControlAdapter: Низкое давление в отопительном контуре!"); - break; - case 3: - sendTelegramm("EctoControlAdapter: Ошибка розжига!"); - break; - case 4: - sendTelegramm("EctoControlAdapter: Низкое давления воздуха!"); - break; - case 5: - sendTelegramm("EctoControlAdapter: Перегрев теплоносителя в контуре!"); - break; - default: - break; + + // Групповое чтение настроек + bool readSettings() { + uint16_t registers[6]; + if (!readRegisterBlock(ecR_ReadSetTypeConnect, registers, 6)) { + return false; + } + + // Обработка записанных значений + publishData(F("setTypeConnect"), String(registers[0])); + publishData(F("tSetCH"), String(registers[1] / 10.0f)); + publishData(F("tSetCHFaultConn"), String(registers[2] / 10.0f)); + publishData(F("tSetMinCH"), String(registers[3] / 10.0f)); + publishData(F("tSetMaxCH"), String(registers[4] / 10.0f)); + publishData(F("tSetMinDHW"), String(registers[5])); + + // Чтение оставшихся настроек + uint16_t settings[3]; + if (readRegisterBlock(ecR_ReadTSetMaxDHW, settings, 3)) { + publishData(F("tSetMaxDHW"), String(settings[0])); + publishData(F("tSetDHW"), String(settings[1])); + publishData(F("setMaxModLevel"), String(settings[2])); + } + + return true; + } + +public: + EctoControlAdapter(String parameters) : IoTItem(parameters) { + _addr = jsonReadInt(parameters, "addr", DEFAULT_MODBUS_ADDR); + _rx = jsonReadInt(parameters, "RX", 16); + _tx = jsonReadInt(parameters, "TX", 17); + _DIR_PIN = jsonReadInt(parameters, "DIR_PIN", DEFAULT_DIR_PIN); + _baud = jsonReadInt(parameters, "baud", 19200); + _prot = jsonReadStr(parameters, "protocol", "SERIAL_8N1"); + _debugLevel = jsonReadInt(parameters, "debug", 0); + _uartLine = jsonReadInt(parameters, "UARTLine", 1); // Новый параметр выбора порта + + // Инициализация UART + if (!_modbusUART) { + _modbusUART = new HardwareSerial(_uartLine); // Используем выбранный порт + if (_modbusUART) { + ((HardwareSerial*)_modbusUART)->begin(_baud, SERIAL_8N1, _rx, _tx); + ((HardwareSerial*)_modbusUART)->setTimeout(DEFAULT_SERIAL_TIMEOUT); + } else { + SerialPrint("E", "EctoControl", "Failed to initialize UART"); } } - return ret; - } + + // Инициализация Modbus + node.begin(_addr, _modbusUART); + node.preTransmission(modbusPreTransmission); + node.postTransmission(modbusPostTransmission); - bool getFlowRate() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_FlowRate, reqData); - flow = lowByte(reqData) / 10.f; - if (ret) - publishData("flowRate", String(flow)); - return ret; - } - bool getMaxSetCH() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_MaxSetCH, reqData); - maxSetCH = (float)lowByte(reqData); - if (ret) - publishData("maxSetCH", String(maxSetCH)); - return ret; - } - bool getMaxSetDHW() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_MaxSetDHW, reqData); - maxSetDHW = (float)lowByte(reqData); - if (ret) - publishData("maxSetDHW", String(maxSetDHW)); - return ret; - } - bool getMinSetCH() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_MinSetCH, reqData); - minSetCH = (float)lowByte(reqData); - if (ret) - publishData("minSetCH", String(minSetCH)); - return ret; - } - bool getMinSetDHW() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_MinSetDHW, reqData); - minSetDHW = (float)lowByte(reqData); - if (ret) - publishData("minSetDHW", String(minSetDHW)); - return ret; - } - bool getModLevel() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_ModLevel, reqData); - modLevel = (float)lowByte(reqData); - if (ret) - publishData("modLevel", String(modLevel)); - return ret; - } - bool getPressure() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_Pressure, reqData); - press = lowByte(reqData) / 10.f; - if (ret) - publishData("press", String(press)); - return ret; - } - bool getTempCH() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_TempCH, reqData); - tCH = reqData / 10.f; - if (ret) - publishData("tCH", String(tCH)); - return ret; - } - bool getTempDHW() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_TempDHW, reqData); - tDHW = reqData / 10.f; - if (ret) - publishData("tDHW", String(tDHW)); - return ret; - } - bool getTempOutside() - { - uint16_t reqData; - bool ret = readFunctionModBus(ReadDataEctoControl::ecR_TempOutside, reqData); - tOut = (float)lowByte(reqData); - if (ret) - publishData("tOut", String(tOut)); - return ret; - } - - bool setTypeConnect(float &data) - { - bool ret = false; - uint16_t data16 = data; - if (writeFunctionModBus(ecW_SetTypeConnect, data16)) - { - // TODO запросить результат записи у адаптера - ret = true; - } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setTypeConnect"); - } - return ret; - } - bool setTCH(float &data) - { - bool ret = false; - uint16_t d16 = data * 10; - if (writeFunctionModBus(ecW_TSetCH, d16)) - { - ret = true; - } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setTCH"); + if (_DIR_PIN) { + pinMode(_DIR_PIN, OUTPUT); + digitalWrite(_DIR_PIN, LOW); } - return ret; - } - bool setTCHFaultConn(float &data) - { - bool ret = false; - uint16_t d16 = data * 10; - if (writeFunctionModBus(ecW_TSetCHFaultConn, d16)) - { - ret = true; + if (_addr >= 0) { + initializeDevice(); } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setTCHFaultConn"); - } - return ret; - } - bool setMinCH(float &data) - { - bool ret = false; - uint16_t data16 = data; - if (writeFunctionModBus(ecW_TSetMinCH, data16)) - { - ret = true; - } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setMinCH"); - } - return ret; - } - bool setMaxCH(float &data) - { - bool ret = false; - uint16_t data16 = data; - if (writeFunctionModBus(ecW_TSetMaxCH, data16)) - { - ret = true; - } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setMaxCH"); - } - return ret; - } - bool setMinDHW(float &data) - { - bool ret = false; - uint16_t data16 = data; - if (writeFunctionModBus(ecW_TSetMinDHW, data16)) - { - ret = true; - } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setMinDHW"); - } - return ret; - } - bool setMaxDHW(float &data) - { - bool ret = false; - uint16_t data16 = data; - if (writeFunctionModBus(ecW_TSetMaxDHW, data16)) - { - ret = true; - } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setMaxDHW"); - } - return ret; - } - bool setTDHW(float &data) - { - bool ret = false; - uint16_t data16 = data; - if (writeFunctionModBus(ecW_TSetDHW, data16)) - { - ret = true; - } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setTDHW"); - } - return ret; - } - bool setMaxModLevel(float &data) - { - bool ret = false; - uint16_t data16 = data; - if (writeFunctionModBus(ecW_SetMaxModLevel, data16)) - { - ret = true; - } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setMaxModLevel"); - } - return ret; } - bool setStatusCH(bool data) - { - bool ret = false; - enableCH = data; - uint16_t stat = enableCH | (enableDHW << 1) | (enableCH2 << 2); - if (writeFunctionModBus(ecW_SetStatusBoiler, stat)) - { - ret = true; + // Оптимизированная инициализация устройства + void initializeDevice() { + if (_addr > 0) { + uint16_t type = 0; + if (readWithRetry(0x0003, type)) { + type = type >> 8; + if (type != 0x14 && type != 0x15 && type != 0x16) { + SerialPrint("E", "EctoControl", "Unsupported device type: " + String(type, HEX)); + } + } else { + SerialPrint("E", "EctoControl", "Failed to read device type"); + } } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setStatusCH"); + else if (_addr == 0) { + // если адреса нет, то шлем широковещательный запрос адреса + uint8_t addr = node.readAddresEctoControl(); + SerialPrint("I", "EctoControlAdapter", "readAddresEctoControl, addr: " + String(addr) + " - Enter to configuration"); } - return ret; - } - bool setStatusDHW(bool data) - { - bool ret = false; - enableDHW = data; - uint16_t stat = enableCH | (enableDHW << 1) | (enableCH2 << 2); - if (writeFunctionModBus(ecW_SetStatusBoiler, stat)) - { - ret = true; - } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setStatusDHW"); - } - return ret; - } - bool setStatusCH2(bool data) - { - bool ret = false; - enableCH2 = data; - uint16_t stat = enableCH | (enableDHW << 1) | (enableCH2 << 2); - if (writeFunctionModBus(ecW_SetStatusBoiler, stat)) - { - ret = true; - } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, setStatusCH2"); - } - return ret; } - bool lockOutReset() - { - bool ret = false; - uint16_t d16 = comm_LockOutReset; - if (writeFunctionModBus(ecW_Command, d16)) - { - ret = true; + // Получение информации о котле + bool getBoilerInfo() { + uint16_t data[3]; + if (!readRegisterBlock(ecR_AdapterInfo, data, 3)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to read adapter info"); + return false; } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, lockOutReset"); + + info.adapterType = (data[0] >> 8) & 0x07; + info.boilerConnected = (data[0] >> 11) & 0x01; + info.rebootCode = data[0] & 0xFF; + info.hardwareVersion = data[1] >> 8; + info.softwareVersion = data[1] & 0xFF; + info.uptime = (uint32_t)data[2] << 16 | data[3]; + + uint16_t tempMemberCode; + if (!readWithRetry(ecR_MemberCode, tempMemberCode)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to read member code"); + } else { + info.memberCode = tempMemberCode; } - return ret; + + uint16_t tempModelCode; + if (!readWithRetry(ecR_ModelCode, tempModelCode)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to read model code"); + } else { + info.modelCode = tempModelCode; + } + + publishData(F("adapterType"), String(info.adapterType)); + publishData(F("boilerConnected"), String(info.boilerConnected)); + return true; } - bool rebootAdapter() - { - bool ret = false; - uint16_t d16 = comm_RebootAdapter; - if (writeFunctionModBus(ecW_Command, d16)) - { - ret = true; + + // Получение записанного статуса котла + bool getWrittenBoilerStatus() { + uint16_t regValue; + if (!readWithRetry(ecR_ReadSetStatusBoiler, regValue)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to read written boiler status"); + return false; } - else - { - if (_debug > 1) - SerialPrint("E", "EctoControlAdapter", "Failed, rebootAdapter"); + + status.writtenStatus.heatingEnabled = (regValue & 0x0001) != 0; + status.writtenStatus.dhwEnabled = (regValue & 0x0002) != 0; + status.writtenStatus.secondaryCircuit = (regValue & 0x0004) != 0; + + publishData(F("writtenHeatingEnabled"), String(status.writtenStatus.heatingEnabled)); + publishData(F("writtenDHWEnabled"), String(status.writtenStatus.dhwEnabled)); + publishData(F("writtenSecondaryCircuit"), String(status.writtenStatus.secondaryCircuit)); + + if (_debugLevel > 1) { + SerialPrint("I", "EctoControl", String("Status 0x003F: ") + + "Heating=" + status.writtenStatus.heatingEnabled + + ", DHW=" + status.writtenStatus.dhwEnabled + + ", Secondary=" + status.writtenStatus.secondaryCircuit); + } + + return true; + } + + // Установка статуса отопления + bool setHeating(bool enable) { + uint16_t currentStatus; + if (!readWithRetry(ecR_ReadSetStatusBoiler, currentStatus)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to read boiler status"); + return false; + } + + uint16_t newStatus = currentStatus & 0xFFFE; + if (enable) { + newStatus |= 0x0001; + } + + bool success = writeWithRetry(ecW_SetStatusBoiler, newStatus); + if (success) { + getWrittenBoilerStatus(); + } else { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to set heating status"); + } + return success; + } + + // Установка статуса ГВС + bool setDHW(bool enable) { + uint16_t currentStatus; + if (!readWithRetry(ecR_ReadSetStatusBoiler, currentStatus)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to read boiler status"); + return false; + } + + uint16_t newStatus = currentStatus & 0xFFFD; + if (enable) { + newStatus |= 0x0002; + } + + bool success = writeWithRetry(ecW_SetStatusBoiler, newStatus); + if (success) { + getWrittenBoilerStatus(); + } else { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to set DHW status"); + } + return success; + } + + // Установка статуса вторичного контура + bool setSecondaryCircuit(bool enable) { + uint16_t currentStatus; + if (!readWithRetry(ecR_ReadSetStatusBoiler, currentStatus)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to read boiler status"); + return false; + } + + uint16_t newStatus = currentStatus & 0xFFFB; + if (enable) { + newStatus |= 0x0004; + } + + bool success = writeWithRetry(ecW_SetStatusBoiler, newStatus); + if (success) { + getWrittenBoilerStatus(); + } else { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to set secondary circuit status"); + } + return success; + } + + // Установка общего статуса котла + bool setBoilerStatus(bool heating, bool dhw, bool secondary) { + uint16_t newStatus = 0; + if (heating) newStatus |= 0x0001; + if (dhw) newStatus |= 0x0002; + if (secondary) newStatus |= 0x0004; + + if (!writeWithRetry(ecW_SetStatusBoiler, newStatus)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to set boiler status"); + return false; + } + + // Проверяем, что статус обновился + uint16_t currentStatus; + if (readWithRetry(ecR_ReadSetStatusBoiler, currentStatus)) { + return (currentStatus & 0x0007) == newStatus; + } + return false; + } + + // Установка температуры отопления + bool setTCH(float temp) { + int16_t value = temp * 10; + if (!writeWithRetry(ecW_TSetCH, (uint16_t)value)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to set CH temperature"); + return false; + } + return true; + } + + // Установка температуры ГВС + bool setTDHW(float temp) { + uint16_t value = temp; + if (!writeWithRetry(ecW_TSetDHW, value)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to set DHW temperature"); + return false; + } + return true; + } + + // Перезагрузка адаптера + bool rebootAdapter() { + if (!writeWithRetry(ecW_Command, COMMAND_REBOOT)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to send reboot command"); + return false; + } + + uint16_t response; + if (readWithRetry(0x0081, response)) { + return response == 0; + } + return false; + } + + // Сброс ошибок котла + bool resetBoilerErrors() { + if (!writeWithRetry(ecW_Command, COMMAND_RESET_ERRORS)) { + if (_debugLevel > 0) SerialPrint("E", "EctoControl", "Failed to send reset errors command"); + return false; + } + + uint16_t response; + if (readWithRetry(0x0081, response)) { + return response == 0; + } + return false; + } + + // Оптимизированный основной цикл + void doByInterval() override { + if (_addr <= 0) return; + + uint32_t currentTime = millis(); + if (currentTime - _lastUpdate < UPDATE_INTERVAL) { + return; + } + _lastUpdate = currentTime; + + // Чтение основных данных одним запросом + if (!readCommonData()) { + return; + } + + // Чтение информации о котле + getBoilerInfo(); + + // Чтение записанных настроек + readSettings(); + + // Чтение записанного статуса + getWrittenBoilerStatus(); + } + + // Обработка команд + IoTValue execute(String command, std::vector ¶m) override { + if (command == "setHeating") { + if (param.size() && param[0].isDecimal) { + setHeating(param[0].valD); + } + } + else if (command == "setDHW") { + if (param.size() && param[0].isDecimal) { + setDHW(param[0].valD); + } + } + else if (command == "setSecondaryCircuit") { + if (param.size() && param[0].isDecimal) { + setSecondaryCircuit(param[0].valD); + } + } + else if (command == "setBoilerStatus") { + if (param.size() >= 3) { + setBoilerStatus(param[0].valD, param[1].valD, param[2].valD); + } + } + else if (command == "getBoilerInfo") { + getBoilerInfo(); + } + else if (command == "getBoilerStatus") { + // Уже читается в readCommonData() + } + else if (command == "getWrittenBoilerStatus") { + getWrittenBoilerStatus(); + } + else if (command == "getSettings") { + readSettings(); + } + else if (command == "setTCH") { + if (param.size() && param[0].isDecimal) { + setTCH(param[0].valD); + } + } + else if (command == "setTDHW") { + if (param.size() && param[0].isDecimal) { + setTDHW(param[0].valD); + } + } + else if (command == "reboot") { + rebootAdapter(); + } + else if (command == "resetErrors") { + resetBoilerErrors(); + } + else if (command == "setSetTypeConnect") { + if (param.size() && param[0].isDecimal) { + writeWithRetry(ecW_SetTypeConnect, static_cast(param[0].valD)); + } + } + else if (command == "setTSetCHFaultConn") { + if (param.size() && param[0].isDecimal) { + writeWithRetry(ecW_TSetCHFaultConn, static_cast(param[0].valD * 10)); + } + } + else if (command == "setTSetMinCH") { + if (param.size() && param[0].isDecimal) { + writeWithRetry(ecW_TSetMinCH, static_cast(param[0].valD * 10)); + } + } + else if (command == "setTSetMaxCH") { + if (param.size() && param[0].isDecimal) { + writeWithRetry(ecW_TSetMaxCH, static_cast(param[0].valD * 10)); + } + } + else if (command == "setTSetMinDHW") { + if (param.size() && param[0].isDecimal) { + writeWithRetry(ecW_TSetMinDHW, static_cast(param[0].valD)); + } + } + else if (command == "setTSetMaxDHW") { + if (param.size() && param[0].isDecimal) { + writeWithRetry(ecW_TSetMaxDHW, static_cast(param[0].valD)); + } + } + else if (command == "setSetMaxModLevel") { + if (param.size() && param[0].isDecimal) { + writeWithRetry(ecW_SetMaxModLevel, static_cast(param[0].valD)); + } + } + else if (command == "getTempCH") { + uint16_t regValue; + if (readWithRetry(ecR_TempCH, regValue)) { + IoTValue val; + val.valD = regValue / 10.0; + val.isDecimal = true; + return val; + } + } + else if (command == "getTempDHW") { + uint16_t regValue; + if (readWithRetry(ecR_TempDHW, regValue)) { + IoTValue val; + val.valD = regValue / 10.0; + val.isDecimal = true; + return val; + } + } + else if (command == "getPressure") { + uint16_t regValue; + if (readWithRetry(ecR_Pressure, regValue)) { + IoTValue val; + val.valD = (regValue & 0xFF) / 10.0; + val.isDecimal = true; + return val; + } + } + else if (command == "getFlowRate") { + uint16_t regValue; + if (readWithRetry(ecR_FlowRate, regValue)) { + IoTValue val; + val.valD = (regValue & 0xFF) / 10.0; + val.isDecimal = true; + return val; + } + } + else if (command == "getModLevel") { + uint16_t regValue; + if (readWithRetry(ecR_ModLevel, regValue)) { + IoTValue val; + val.valD = static_cast(regValue & 0xFF); + val.isDecimal = true; + return val; + } + } + else if (command == "getBoilerStatusRaw") { + uint16_t regValue; + if (readWithRetry(ecR_BoilerStatus, regValue)) { + IoTValue val; + val.valD = regValue; + val.isDecimal = true; + return val; + } + } + else if (command == "getTempOutside") { + uint16_t regValue; + if (readWithRetry(ecR_TempOutside, regValue)) { + int8_t temp = static_cast(regValue & 0xFF); + IoTValue val; + val.valD = (temp != 0x7F) ? temp : 0; + val.isDecimal = true; + return val; + } + } + else if (command == "getFlagErrorOT") { + uint16_t regValue; + if (readWithRetry(ecR_FlagErrorOT, regValue)) { + IoTValue val; + val.valD = regValue; + val.isDecimal = true; + return val; + } + } + else if (command == "getMinSetCH") { + uint16_t regValue; + if (readWithRetry(ecR_MinSetCH, regValue)) { + IoTValue val; + val.valD = regValue / 10.0; + val.isDecimal = true; + return val; + } + } + else if (command == "getMaxSetCH") { + uint16_t regValue; + if (readWithRetry(ecR_MaxSetCH, regValue)) { + IoTValue val; + val.valD = regValue / 10.0; + val.isDecimal = true; + return val; + } + } + else if (command == "getMinSetDHW") { + uint16_t regValue; + if (readWithRetry(ecR_MinSetDHW, regValue)) { + IoTValue val; + val.valD = regValue; + val.isDecimal = true; + return val; + } + } + else if (command == "getMaxSetDHW") { + uint16_t regValue; + if (readWithRetry(ecR_MaxSetDHW, regValue)) { + IoTValue val; + val.valD = regValue; + val.isDecimal = true; + return val; + } + } + else if (command == "getAdapterInfo") { + uint16_t regValue; + if (readWithRetry(ecR_AdapterInfo, regValue)) { + IoTValue val; + val.valD = regValue; + val.isDecimal = true; + return val; + } + } + else if (command == "getAdapterVersion") { + uint16_t regValue; + if (readWithRetry(ecR_AdapterVersion, regValue)) { + IoTValue val; + val.valD = regValue; + val.isDecimal = true; + return val; + } + } + else if (command == "getUptime") { + uint16_t regValue; + if (readWithRetry(ecR_Uptime, regValue)) { + IoTValue val; + val.valD = regValue; + val.isDecimal = true; + return val; + } + } + else { + SerialPrint("E", "EctoControl", "Unknown command: " + command); + } + + return {}; + } + + ~EctoControlAdapter() { + if (_modbusUART) { + ((HardwareSerial*)_modbusUART)->end(); + delete _modbusUART; + _modbusUART = nullptr; } - return ret; } }; -void *getAPI_EctoControlAdapter(String subtype, String param) -{ - - if (subtype == F("ecAdapter")) - { +void* getAPI_EctoControlAdapter(String subtype, String param) { + if (subtype == F("ecAdapter")) { return new EctoControlAdapter(param); } - { - return nullptr; - } -} + return nullptr; +} \ No newline at end of file diff --git a/src/modules/exec/EctoControlAdapter/ModbusEC.cpp b/src/modules/exec/EctoControlAdapter/ModbusEC.cpp index d9d0818e..b0644a79 100644 --- a/src/modules/exec/EctoControlAdapter/ModbusEC.cpp +++ b/src/modules/exec/EctoControlAdapter/ModbusEC.cpp @@ -1,4 +1,3 @@ - #include "ModbusEC.h" #define COUNT_BIT_AVAIL 5 @@ -11,19 +10,8 @@ ModbusMaster::ModbusMaster(void) _postTransmission = 0; } -/** -Initialize class object. - -Assigns the Modbus slave ID and serial port. -Call once class has been instantiated, typically within setup(). - -@param slave Modbus slave ID (1..255) -@param &serial reference to serial port object (Serial, Serial1, ... Serial3) -@ingroup setup -*/ void ModbusMaster::begin(uint8_t slave, Stream *serial) { - // txBuffer = (uint16_t*) calloc(ku8MaxBufferSize, sizeof(uint16_t)); _u8MBSlave = slave; _serial = serial; _u8TransmitBufferIndex = 0; @@ -42,16 +30,13 @@ void ModbusMaster::beginTransmission(uint16_t u16Address) u16TransmitBufferLength = 0; } -// eliminate this function in favor of using existing MB request functions uint8_t ModbusMaster::requestFrom(uint16_t address, uint16_t quantity) { - uint8_t read; - // clamp to buffer length + uint8_t read = 0; // if (quantity > ku8MaxBufferSize) { quantity = ku8MaxBufferSize; } - // set rx buffer iterator vars _u8ResponseBufferIndex = 0; _u8ResponseBufferLength = read; @@ -110,62 +95,21 @@ uint16_t ModbusMaster::receive(void) } } -/** -Set idle time callback function (cooperative multitasking). - -This function gets called in the idle time between transmission of data -and response from slave. Do not call functions that read from the serial -buffer that is used by ModbusMaster. Use of i2c/TWI, 1-Wire, other -serial ports, etc. is permitted within callback function. - -@see ModbusMaster::ModbusMasterTransaction() -*/ void ModbusMaster::idle(void (*idle)()) { _idle = idle; } -/** -Set pre-transmission callback function. - -This function gets called just before a Modbus message is sent over serial. -Typical usage of this callback is to enable an RS485 transceiver's -Driver Enable pin, and optionally disable its Receiver Enable pin. - -@see ModbusMaster::ModbusMasterTransaction() -@see ModbusMaster::postTransmission() -*/ void ModbusMaster::preTransmission(void (*preTransmission)()) { _preTransmission = preTransmission; } -/** -Set post-transmission callback function. - -This function gets called after a Modbus message has finished sending -(i.e. after all data has been physically transmitted onto the serial -bus). - -Typical usage of this callback is to enable an RS485 transceiver's -Receiver Enable pin, and disable its Driver Enable pin. - -@see ModbusMaster::ModbusMasterTransaction() -@see ModbusMaster::preTransmission() -*/ void ModbusMaster::postTransmission(void (*postTransmission)()) { _postTransmission = postTransmission; } -/** -Retrieve data from response buffer. - -@see ModbusMaster::clearResponseBuffer() -@param u8Index index of response buffer array (0x00..0x3F) -@return value in position u8Index of response buffer (0x0000..0xFFFF) -@ingroup buffer -*/ uint16_t ModbusMaster::getResponseBuffer(uint8_t u8Index) { if (u8Index < ku8MaxBufferSize) @@ -178,31 +122,15 @@ uint16_t ModbusMaster::getResponseBuffer(uint8_t u8Index) } } -/** -Clear Modbus response buffer. - -@see ModbusMaster::getResponseBuffer(uint8_t u8Index) -@ingroup buffer -*/ void ModbusMaster::clearResponseBuffer() { uint8_t i; - for (i = 0; i < ku8MaxBufferSize; i++) { _u16ResponseBuffer[i] = 0; } } -/** -Place data in transmit buffer. - -@see ModbusMaster::clearTransmitBuffer() -@param u8Index index of transmit buffer array (0x00..0x3F) -@param u16Value value to place in position u8Index of transmit buffer (0x0000..0xFFFF) -@return 0 on success; exception number on failure -@ingroup buffer -*/ uint8_t ModbusMaster::setTransmitBuffer(uint8_t u8Index, uint16_t u16Value) { if (u8Index < ku8MaxBufferSize) @@ -216,38 +144,15 @@ uint8_t ModbusMaster::setTransmitBuffer(uint8_t u8Index, uint16_t u16Value) } } -/** -Clear Modbus transmit buffer. - -@see ModbusMaster::setTransmitBuffer(uint8_t u8Index, uint16_t u16Value) -@ingroup buffer -*/ void ModbusMaster::clearTransmitBuffer() { uint8_t i; - for (i = 0; i < ku8MaxBufferSize; i++) { _u16TransmitBuffer[i] = 0; } } -/** -Modbus function 0x03 Read Holding Registers. - -This function code is used to read the contents of a contiguous block of -holding registers in a remote device. The request specifies the starting -register address and the number of registers. Registers are addressed -starting at zero. - -The register data in the response buffer is packed as one word per -register. - -@param u16ReadAddress address of the first holding register (0x0000..0xFFFF) -@param u16ReadQty quantity of holding registers to read (1..125, enforced by remote device) -@return 0 on success; exception number on failure -@ingroup register -*/ uint8_t ModbusMaster::readHoldingRegisters(uint16_t u16ReadAddress, uint16_t u16ReadQty) { @@ -256,18 +161,6 @@ uint8_t ModbusMaster::readHoldingRegisters(uint16_t u16ReadAddress, return ModbusMasterTransaction(ku8MBReadHoldingRegisters); } -/** -Modbus function 0x06 Write Single Register. - -This function code is used to write a single holding register in a -remote device. The request specifies the address of the register to be -written. Registers are addressed starting at zero. - -@param u16WriteAddress address of the holding register (0x0000..0xFFFF) -@param u16WriteValue value to be written to holding register (0x0000..0xFFFF) -@return 0 on success; exception number on failure -@ingroup register -*/ uint8_t ModbusMaster::writeSingleRegister(uint16_t u16WriteAddress, uint16_t u16WriteValue) { @@ -277,20 +170,6 @@ uint8_t ModbusMaster::writeSingleRegister(uint16_t u16WriteAddress, return ModbusMasterTransaction(ku8MBWriteSingleRegister); } -/** -Modbus function 0x10 Write Multiple Registers. - -This function code is used to write a block of contiguous registers (1 -to 123 registers) in a remote device. - -The requested written values are specified in the transmit buffer. Data -is packed as one word per register. - -@param u16WriteAddress address of the holding register (0x0000..0xFFFF) -@param u16WriteQty quantity of holding registers to write (1..123, enforced by remote device) -@return 0 on success; exception number on failure -@ingroup register -*/ uint8_t ModbusMaster::writeMultipleRegisters(uint16_t u16WriteAddress, uint16_t u16WriteQty) { @@ -299,7 +178,6 @@ uint8_t ModbusMaster::writeMultipleRegisters(uint16_t u16WriteAddress, return ModbusMasterTransaction(ku8MBWriteMultipleRegisters); } -// new version based on Wire.h uint8_t ModbusMaster::writeMultipleRegisters() { _u16WriteQty = _u8TransmitBufferIndex; @@ -313,6 +191,7 @@ uint8_t ModbusMaster::readAddresEctoControl() ModbusMasterTransaction(ku8MBProgRead46); return getResponseBuffer(0x00); } + uint8_t ModbusMaster::writeAddresEctoControl(uint8_t addr) { _u16WriteAddress = 0x00; @@ -321,20 +200,6 @@ uint8_t ModbusMaster::writeAddresEctoControl(uint8_t addr) return ModbusMasterTransaction(ku8MBProgWrite47); } -/* _____PRIVATE FUNCTIONS____________________________________________________ */ -/** -Modbus transaction engine. -Sequence: - - assemble Modbus Request Application Data Unit (ADU), - based on particular function called - - transmit request over selected serial port - - wait for/retrieve response - - evaluate/disassemble response - - return status (success/exception) - -@param u8MBFunction Modbus function (0x01..0xFF) -@return 0 on success; exception number on failure -*/ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) { uint8_t u8ModbusADU[24]; @@ -345,7 +210,7 @@ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) uint8_t u8BytesLeft = 8; uint8_t u8MBStatus = ku8MBSuccess; - // assemble Modbus Request Application Data Unit + // Assemble Modbus Request Application Data Unit if (u8MBFunction == ku8MBProgRead46 || u8MBFunction == ku8MBProgWrite47) { u8ModbusADU[u8ModbusADUSize++] = 0x00; @@ -382,7 +247,8 @@ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) u8ModbusADU[u8ModbusADUSize++] = lowByte(_u16WriteQty); u8ModbusADU[u8ModbusADUSize++] = lowByte(_u16WriteQty << 1); - for (i = 0; i < lowByte(_u16WriteQty); i++) + // : _u16WriteQty lowByte() + for (i = 0; i < _u16WriteQty; i++) { u8ModbusADU[u8ModbusADUSize++] = highByte(_u16TransmitBuffer[i]); u8ModbusADU[u8ModbusADUSize++] = lowByte(_u16TransmitBuffer[i]); @@ -390,47 +256,31 @@ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) break; } - // append CRC + // Append CRC u16CRC = 0xFFFF; for (i = 0; i < u8ModbusADUSize; i++) { u16CRC = crc16_update(u16CRC, u8ModbusADU[i]); } - // if (u8MBFunction == ku8MBProgRead46 || u8MBFunction == ku8MBProgWrite47) - // { - // u8ModbusADU[u8ModbusADUSize++] = highByte(u16CRC); - // u8ModbusADU[u8ModbusADUSize++] = lowByte(u16CRC); - // } - // else - // { u8ModbusADU[u8ModbusADUSize++] = lowByte(u16CRC); u8ModbusADU[u8ModbusADUSize++] = highByte(u16CRC); - // } - u8ModbusADU[u8ModbusADUSize] = 0; - // flush receive buffer before transmitting request - while (_serial->read() != -1) - ; + // Flush receive buffer before transmitting request + while (_serial->read() != -1); - // transmit request - if (_preTransmission) - { - _preTransmission(); - } + // Transmit request + if (_preTransmission) _preTransmission(); for (i = 0; i < u8ModbusADUSize; i++) { _serial->write(u8ModbusADU[i]); } u8ModbusADUSize = 0; - _serial->flush(); // flush transmit buffer - if (_postTransmission) - { - _postTransmission(); - } + _serial->flush(); + if (_postTransmission) _postTransmission(); - // loop until we run out of time or bytes, or an error occurs + // Loop until timeout or response complete u32StartTime = millis(); while (u8BytesLeft && !u8MBStatus) { @@ -441,7 +291,6 @@ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) #endif uint8_t req = _serial->read(); u8ModbusADU[u8ModbusADUSize++] = req; - Serial.print(req, HEX); u8BytesLeft--; #if __MODBUSMASTER_DEBUG__ digitalWrite(__MODBUSMASTER_DEBUG_PIN_A__, false); @@ -452,132 +301,110 @@ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) #if __MODBUSMASTER_DEBUG__ digitalWrite(__MODBUSMASTER_DEBUG_PIN_B__, true); #endif - if (_idle) - { - _idle(); - } + if (_idle) _idle(); #if __MODBUSMASTER_DEBUG__ digitalWrite(__MODBUSMASTER_DEBUG_PIN_B__, false); #endif } - // evaluate slave ID, function code once enough bytes have been read - uint8_t count; - if (u8MBFunction == ku8MBProgRead46 || u8MBFunction == ku8MBProgWrite47) - count = COUNT_BIT_AVAIL_46F; - else - count = COUNT_BIT_AVAIL; + // Evaluate response after enough bytes received + uint8_t count = (u8MBFunction == ku8MBProgRead46 || u8MBFunction == ku8MBProgWrite47) + ? COUNT_BIT_AVAIL_46F : COUNT_BIT_AVAIL; if (u8ModbusADUSize == count) { if (u8MBFunction != ku8MBProgRead46 && u8MBFunction != ku8MBProgWrite47) { - // verify response is for correct Modbus slave if (u8ModbusADU[0] != _u8MBSlave) { - // Serial.print(u8ModbusADU[0], HEX); - // Serial.print(" != "); - // Serial.println(_u8MBSlave, HEX); - u8MBStatus = ku8MBInvalidSlaveID; break; } - // verify response is for correct Modbus function code (mask exception bit 7) if ((u8ModbusADU[1] & 0x7F) != u8MBFunction) { u8MBStatus = ku8MBInvalidFunction; break; } } - // check whether Modbus exception occurred; return Modbus Exception Code + if (bitRead(u8ModbusADU[1], 7)) { u8MBStatus = u8ModbusADU[2]; break; } - // evaluate returned Modbus function code + // Determine remaining bytes based on function switch (u8ModbusADU[1]) { - case ku8MBReadHoldingRegisters: - u8BytesLeft = u8ModbusADU[2]; - break; - - case ku8MBWriteMultipleRegisters: - u8BytesLeft = 3; - break; - - case ku8MBProgRead46: - u8BytesLeft = 1; - break; - - case ku8MBProgWrite47: - u8BytesLeft = 1; - break; - - default: + case ku8MBReadHoldingRegisters: + u8BytesLeft = u8ModbusADU[2]; + break; + case ku8MBWriteMultipleRegisters: + u8BytesLeft = 3; + break; + case ku8MBProgRead46: + case ku8MBProgWrite47: + u8BytesLeft = 1; + break; + default: ; // } } + if ((millis() - u32StartTime) > ku16MBResponseTimeout) { u8MBStatus = ku8MBResponseTimedOut; } } - if (u8MBFunction != ku8MBProgRead46 && u8MBFunction != ku8MBProgWrite47) + // Verify CRC for standard functions + if (!u8MBStatus && + u8MBFunction != ku8MBProgRead46 && + u8MBFunction != ku8MBProgWrite47 && + u8ModbusADUSize >= COUNT_BIT_AVAIL) { - // verify response is large enough to inspect further - if (!u8MBStatus && u8ModbusADUSize >= COUNT_BIT_AVAIL) + u16CRC = 0xFFFF; + for (i = 0; i < (u8ModbusADUSize - 2); i++) { - // calculate CRC - u16CRC = 0xFFFF; - for (i = 0; i < (u8ModbusADUSize - 2); i++) - { - u16CRC = crc16_update(u16CRC, u8ModbusADU[i]); - } + u16CRC = crc16_update(u16CRC, u8ModbusADU[i]); + } - // verify CRC - if (!u8MBStatus && (lowByte(u16CRC) != u8ModbusADU[u8ModbusADUSize - 2] || - highByte(u16CRC) != u8ModbusADU[u8ModbusADUSize - 1])) - { - u8MBStatus = ku8MBInvalidCRC; - } + // : + if (!u8MBStatus && + ((lowByte(u16CRC) != u8ModbusADU[u8ModbusADUSize - 2]) || + (highByte(u16CRC) != u8ModbusADU[u8ModbusADUSize - 1]))) + { + u8MBStatus = ku8MBInvalidCRC; } } - // disassemble ADU into words + + // Parse response data if (!u8MBStatus) { - // evaluate returned Modbus function code switch (u8ModbusADU[1]) { - case ku8MBReadHoldingRegisters: - // load bytes into word; response bytes are ordered H, L, H, L, ... - for (i = 0; i < (u8ModbusADU[2] >> 1); i++) - { - if (i < ku8MaxBufferSize) + case ku8MBReadHoldingRegisters: + for (i = 0; i < (u8ModbusADU[2] >> 1); i++) { - _u16ResponseBuffer[i] = word(u8ModbusADU[2 * i + 3], u8ModbusADU[2 * i + 4]); + if (i < ku8MaxBufferSize) + { + _u16ResponseBuffer[i] = word(u8ModbusADU[2 * i + 3], u8ModbusADU[2 * i + 4]); + } + _u8ResponseBufferLength = i; } - - _u8ResponseBufferLength = i; - } - break; - case ku8MBProgRead46: - Serial.print("ku8MBProgRead46"); - for (i = 0; i < (u8ModbusADUSize); i++) - { - Serial.println(u8ModbusADU[i], HEX); - } - - _u16ResponseBuffer[0] = (uint16_t)u8ModbusADU[2]; - _u8ResponseBufferLength = 1; - break; + break; + + case ku8MBProgRead46: + _u16ResponseBuffer[0] = (uint16_t)u8ModbusADU[2]; + _u8ResponseBufferLength = 1; + break; } } + // Reset buffers _u8TransmitBufferIndex = 0; u16TransmitBufferLength = 0; _u8ResponseBufferIndex = 0; + return u8MBStatus; -} +} \ No newline at end of file diff --git a/src/modules/exec/EctoControlAdapter/export - Копия.json b/src/modules/exec/EctoControlAdapter/export - Копия.json new file mode 100644 index 00000000..8d8cf06b --- /dev/null +++ b/src/modules/exec/EctoControlAdapter/export - Копия.json @@ -0,0 +1,459 @@ +{ + "mark": "iotm", + "config": [ + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "boilerConnected", + "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": "burner", + "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": "heating", + "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": "dhw", + "needSave": 0, + "widget": "anydataDef", + "page": "Статус котла", + "descr": "🛁 Контур ГВС", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": "1", + "type": "Reading", + "subtype": "Variable", + "id": "tempCH", + "needSave": 0, + "widget": "anydataDef", + "page": "Котёл", + "descr": "🌡️ Теплоносителя", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": "1", + "type": "Reading", + "subtype": "Variable", + "id": "tempDHW", + "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": "setStatusCH", + "needSave": "0", + "widget": "toggle", + "page": "Управление", + "descr": "☢ Отопление (вкл/выкл)", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "setStatusDHW", + "needSave": "0", + "widget": "toggle", + "page": "Управление", + "descr": "🛁 Гвс (вкл/выкл)", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "setTCH", + "needSave": "0", + "widget": "inputDgt", + "page": "Управление", + "descr": "🌡️ Уставка отопления", + "int": "0", + "val": "0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": "1", + "type": "Reading", + "subtype": "Ds18b20", + "id": "temp", + "widget": "anydataTmp", + "page": "Термостат", + "descr": "🌡️ В помещении", + "int": "120", + "pin": "15", + "index": "0", + "addr": "28163C0D000000CF", + "round": "1" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "SetTempId", + "needSave": "1", + "widget": "inputDgt", + "page": "Термостат", + "descr": "🌡️ Желаемая", + "int": "0", + "val": "23", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "setDHW", + "needSave": "1", + "widget": "inputDgt", + "page": "Управление", + "descr": "🌡️ Уставка ГВС", + "int": "0", + "val": "0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "pressure", + "needSave": 0, + "widget": "anydataDef", + "page": "Статус котла", + "descr": "💧 Давление", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": "2" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "codeError", + "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": "lasttemp", + "needSave": 0, + "widget": "nil", + "page": "Ввод", + "descr": "Введите число", + "int": "0", + "val": "0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "auto", + "needSave": "1", + "widget": "toggle", + "page": "Термостат", + "descr": "Термостат (вкл/выкл", + "int": "0", + "val": "0", + "moduleName": "VButton" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "tempOT", + "needSave": 0, + "widget": "nil", + "page": "Ввод", + "descr": "Введите число", + "int": "0", + "val": "0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Writing", + "subtype": "TelegramLT", + "id": "tg", + "widget": "", + "page": "", + "descr": "", + "token": "", + "chatID": "", + "moduleName": "TelegramLT" + }, + { + "global": "1", + "needSave": "0", + "type": "Writing", + "subtype": "ThermostatPID", + "id": "Thermo96", + "widget": "anydataTmp", + "page": "Термостат", + "descr": "🌡️ Расчетная", + "int": "10", + "round": 1, + "map": "1024,1024,1,100", + "set_id": "SetTempId", + "term_id": "temp", + "rele": "", + "setDirection": 0, + "setLimitsMIN": "20", + "setLimitsMAX": "60", + "KP": 10, + "KI": "0.01", + "KD": "8", + "moduleName": "Thermostat" + }, + { + "global": 0, + "type": "Reading", + "subtype": "UpdateServer", + "id": "UpdateServer38", + "widget": "", + "page": "", + "descr": "", + "btn-startUpdateAll": "http://192.168.0.84:5500/iotm/esp32c6_8mb/400/", + "btn-startUpdateFS": "", + "btn-startUpdateFW": "" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "tSetCH", + "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": "tSetDHW", + "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": "writtenHeatingEnabled", + "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": "writtenDHWEnabled", + "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": "setMaxModLevel", + "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": "ecAdapter", + "id": "ecto", + "widget": "nil", + "page": "Котёл", + "descr": "Адаптер", + "int": "20", + "addr": "23", + "RX": "11", + "TX": "10", + "DIR_PIN": "0", + "baud": 19200, + "protocol": "SERIAL_8N1", + "debug": "0", + "UARTLine": "1" + } + ] +} + +scenario=>if onStart then { +tg.sendOftenMsg("E-bus http://" + getIP()); +if auto then {Thermo96.enable(1);} else {Thermo96.enable(0);} +} + +#включение/отключение отопления +if setStatusCH == 1 then ecto.setHeating(1); +if setStatusCH == 0 then ecto.setHeating(0); + +#включение/отключение ГВС +if setStatusDHW == 1 then ecto.setDHW(1); +if setStatusDHW == 0 then ecto.setDHW(0); + +#setStatusCH2 +if setStatusCH2 == 1 then ecto.setSecondaryCircuit(1); +if setStatusCH2 == 0 then ecto.setSecondaryCircuit(0); + +#Установка 🌡️ГВС +if setDHW then ecto.setTDHW (setDHW); +#Установка 🌡️ теплоносителя +if setTCH then ecto.setTCH (setTCH); + +#Включение термостата пид +if auto == 1 then Thermo96.enable(1); + +#Вылючение термостата пмд +if auto == 0 then { +Thermo96.enable(0); +tempOT = 20; +} + +if Thermo96 > 38 then tempOT = Thermo96 else tempOT = 20; + +#Сравнение температуры для предотвращения повторных данных +if tempOT != lasttemp then lasttemp = tempOT; +if lasttemp then ecto.setTCH (lasttemp); + +#считывание записанных в адаптер значений +if writtenHeatingEnabled then setStatusCH := writtenHeatingEnabled; +if writtenDHWEnabled then setStatusDHW := writtenDHWEnabled; +if tSetDHW then setDHW := tSetDHW; +if tSetCH then setTCH := tSetCH; + +#уведомления в телеграм +if codeError > 1 then tg.sendMsg("🚨 Ошибка котла F" + codeError); +if boilerConnected == 0 then tg.sendMsg("🚨 Отсутствует связь с котлом") else tg.sendMsg("Cвязь с котлом восстановлена") ; +if pressure < 0.9 then tg.sendMsg("💧 Низкое давление теплоносителя" + pressure); \ No newline at end of file diff --git a/src/modules/exec/EctoControlAdapter/modinfo.json b/src/modules/exec/EctoControlAdapter/modinfo.json index d02ab116..f7844a83 100644 --- a/src/modules/exec/EctoControlAdapter/modinfo.json +++ b/src/modules/exec/EctoControlAdapter/modinfo.json @@ -17,16 +17,17 @@ "DIR_PIN": 4, "baud": 19200, "protocol": "SERIAL_8N1", - "debug": 1 + "debug": 1, + "UARTLine": 1 } ], "about": { "authorName": "Mikhail Bubnov", "authorContact": "https://t.me/Mit4bmw", "authorGit": "https://github.com/Mit4el", - "specialThanks": "", + "specialThanks": "SeregaKi", "moduleName": "EctoControlAdapter", - "moduleVersion": "1.0", + "moduleVersion": "2.0", "usedRam": { "esp32_4mb": 15, "esp8266_4mb": 15 @@ -37,80 +38,100 @@ "title": "EctoControlAdapter", "moduleDesc": "Управление отопительным котлом через адаптер EctoControl по протоколам OpenTherm, eBUS, Navien. Посредством Modbus RTU. Разъем 4P4C: 1-Желтый(красный)+12V; 2-Белый-GND; 3-Зелёный-A; 4-Коричневый(Синий)-B", "propInfo": { - "addr": "Адрес slave, что бы узнать адрес - в конфиге адрес 0 и смотреть лог (требуется проверка)", + "addr": "Адрес slave, что бы узнать адрес - в конфиге адрес 0 и смотреть лог", "int": "Количество секунд между опросами датчика.", "RX": "Пин RX", "TX": "Пин TX", "DIR_PIN": "connect DR, RE pin of MAX485 to gpio, указать 0 если не нужен", "baud": "скорость Uart", "protocol": "Протокол Uart: SERIAL_8N1 или SERIAL_8N2", - "debug": "0 - отключить дебаг, 1 - включить вывод дебага, 2 - лог комманд, 3 - вывод modbus" + "debug": "0 - отключить дебаг, 1 - включить вывод дебага, 2 - лог комманд, 3 - вывод modbus", + "UARTLine": "Номер UART порта (0, 1 или 2), по умолчанию 1" }, "funcInfo": [ { - "name": "getModelVersion", - "descr": "Запрос модели и версии адаптера и бойлера", - "params": [] + "name": "setHeating", + "descr": "Включение/отключение контура отопления", + "params": ["1 - включить, 0 - отключить"] + }, + { + "name": "setDHW", + "descr": "Включение/отключение ГВС", + "params": ["1 - включить, 0 - отключить"] + }, + { + "name": "setSecondaryCircuit", + "descr": "Включение/отключение вторичного контура", + "params": ["1 - включить, 0 - отключить"] + }, + { + "name": "setBoilerStatus", + "descr": "Комбинированная установка статусов котла", + "params": [ + "отопление (1/0)", + "ГВС (1/0)", + "вторичный контур (1/0)" + ] }, { "name": "getBoilerInfo", - "descr": "Запрос состояния связи с котлом, типа адаптера и код перезагрузки адаптера", + "descr": "Запрос информации о котле и адаптере", "params": [] }, { - "name": "getBoilerStatus", - "descr": "Запрос состояния контуров котла и горелки", + "name": "getWrittenBoilerStatus", + "descr": "Запрос записанного статуса котла", "params": [] }, { - "name": "getCodeError", - "descr": "Код ошибки котла (основной). Зависит от марки и модели котла.", + "name": "getSettings", + "descr": "Запрос текущих настроек адаптера", "params": [] }, { - "name": "getCodeErrorExt", - "descr": "Код ошибки котла (дополнительный). Зависит от марки и модели котла.", - "params": [] + "name": "setTCH", + "descr": "Установка температуры отопления", + "params": ["температура (например 55.5)"] }, { - "name": "getFlagErrorOT", - "descr": "Стандартные флаги ошибок котла (только для котлов с интерфейсом OpenTherm)", - "params": [] + "name": "setTDHW", + "descr": "Установка температуры ГВС", + "params": ["температура (целое число)"] }, { - "name": "getFlowRate", - "descr": "Текущий расхода ГВС", - "params": [] + "name": "setSetTypeConnect", + "descr": "Установка типа подключения", + "params": ["0 - адаптер к котлу, 1 - котел к внешнему устройству"] }, { - "name": "getMaxSetCH", - "descr": "Верхний предел уставки теплоносителя", - "params": [] + "name": "setTSetCHFaultConn", + "descr": "Установка температуры отопления при аварии связи", + "params": ["температура (например 55.5)"] }, { - "name": "getMaxSetDHW", - "descr": "Верхний предел уставки ГВС", - "params": [] + "name": "setTSetMinCH", + "descr": "Установка минимальной температуры отопления", + "params": ["температура (например 35.0)"] }, { - "name": "getMinSetCH", - "descr": "Нижний предел уставки теплоносителя", - "params": [] + "name": "setTSetMaxCH", + "descr": "Установка максимальной температуры отопления", + "params": ["температура (например 85.0)"] }, { - "name": "getMinSetDHW", - "descr": "Нижний предел уставки ГВС", - "params": [] + "name": "setTSetMinDHW", + "descr": "Установка минимальной температуры ГВС", + "params": ["температура (целое число)"] }, { - "name": "getModLevel", - "descr": "Текущая модуляция горелки", - "params": [] + "name": "setTSetMaxDHW", + "descr": "Установка максимальной температуры ГВС", + "params": ["температура (целое число)"] }, { - "name": "getPressure", - "descr": "Текущее Давление в контуре", - "params": [] + "name": "setSetMaxModLevel", + "descr": "Установка максимального уровня модуляции", + "params": ["уровень (0-100)"] }, { "name": "getTempCH", @@ -122,81 +143,80 @@ "descr": "Текущая температура ГВС", "params": [] }, + { + "name": "getPressure", + "descr": "Давление в контуре", + "params": [] + }, + { + "name": "getFlowRate", + "descr": "Расход ГВС", + "params": [] + }, + { + "name": "getModLevel", + "descr": "Уровень модуляции горелки", + "params": [] + }, + { + "name": "getBoilerStatusRaw", + "descr": "Сырое значение регистра статуса", + "params": [] + }, { "name": "getTempOutside", - "descr": "Температура уличного датчика котла", + "descr": "Температура уличного датчика", "params": [] }, { - "name": "setTypeConnect", - "descr": "Установить тип внешних подключений (сохраняется в EPROM Адаптера): 0 - адаптер подключен к котлу, 1 - котел подключен к внешнему устройству (панель или перемычка)", - "params": ["Тип подключения"] - }, - { - "name": "setTCH", - "descr": "Уставка температуры теплоносителя (сохраняется в EPROM Адаптера)", - "params": ["температура передаётся до десятых градуса"] - }, - { - "name": "setTDHW", - "descr": "Уставка температуры ГВС (сохраняется в EPROM Адаптера)", - "params": ["температура передаётся до десятых градуса"] - }, - { - "name": "setTCHFaultConn", - "descr": "Уставка теплоносителя в аварийном режиме (сохраняется в EPROM Адаптера). Будет передана котлу в случае отсутствия связи адаптера с управляющим устройством", - "params": ["температура передаётся до десятых градуса"] - }, - { - "name": "setMinCH", - "descr": "Задать нижний предел уставки теплоносителя", - "params": ["температура от 0 до 100"] - }, - { - "name": "setMaxCH", - "descr": "Задать верхний предел уставки теплоносителя", - "params": ["температура от 0 до 100"] - }, - { - "name": "setMinDHW", - "descr": "Задать нижний предел уставки ГВС", - "params": ["температура от 0 до 100"] - }, - { - "name": "setMaxDHW", - "descr": "Задать верхний предел уставки ГВС", - "params": ["температура от 0 до 100"] - }, - - { - "name": "setMaxModLevel", - "descr": "Уставка максимальной модуляции горелки (сохраняется в EPROM Адаптера)", - "params": ["уровень модуляции 0-100%"] - }, - { - "name": "setStatusCH", - "descr": "Установить режим (Включить) контура отопления; 0 - отключен, 1 - включен", - "params": ["вкл/откл отопления"] - }, - { - "name": "setStatusDHW", - "descr": "Установить режим (Включить) ГВС; 0 - отключен, 1 - включен", - "params": ["вкл/откл ГВС"] - }, - { - "name": "setStatusCH2", - "descr": "Установить режим (Включить) второго контура отопления; 0 - отключен, 1 - включен. используется только некоторыми котлами с интерфейсом OpenTherm и может отвечать за активацию бойлера косвенного нагрева или встроенной функции ГВС", - "params": ["вкл/откл второго контура отопления"] - }, - { - "name": "lockOutReset", - "descr": "Сброс ошибок котла", + "name": "getFlagErrorOT", + "descr": "Флаги ошибок OpenTherm", "params": [] }, { - "name": "rebootAdapter", + "name": "getMinSetCH", + "descr": "Минимальная уставка теплоносителя", + "params": [] + }, + { + "name": "getMaxSetCH", + "descr": "Максимальная уставка теплоносителя", + "params": [] + }, + { + "name": "getMinSetDHW", + "descr": "Минимальная уставка ГВС", + "params": [] + }, + { + "name": "getMaxSetDHW", + "descr": "Максимальная уставка ГВС", + "params": [] + }, + { + "name": "getAdapterInfo", + "descr": "Информация об адаптере (регистр 0x0010)", + "params": [] + }, + { + "name": "getAdapterVersion", + "descr": "Версия адаптера (регистр 0x0011)", + "params": [] + }, + { + "name": "getUptime", + "descr": "Время работы адаптера (регистр 0x0012)", + "params": [] + }, + { + "name": "reboot", "descr": "Перезагрузка адаптера", "params": [] + }, + { + "name": "resetErrors", + "descr": "Сброс ошибок котла", + "params": [] } ] }, diff --git a/src/modules/exec/WakeOnLanModule/WakeOnLanModule.cpp b/src/modules/exec/WakeOnLanModule/WakeOnLanModule.cpp new file mode 100644 index 00000000..1fe1435f --- /dev/null +++ b/src/modules/exec/WakeOnLanModule/WakeOnLanModule.cpp @@ -0,0 +1,162 @@ +// Licensed under the Cooperative Non-Violent Public License (CNPL) +// See: https://github.com/CHE77/IoTManager-Modules/blob/main/LICENSE + +#include "Global.h" +#include "classes/IoTItem.h" + +#include +#include + +WiFiUDP UDP; // Создаем объект WiFiUDP +WakeOnLan WOL(UDP); // Используем библиотеку WakeOnLan + +class WakeOnLanModule : public IoTItem +{ +private: + String _macAddress; // MAC-адрес для пробуждения + String _SecureOn = ""; + int _port = 9; + int _repeats = 3; + bool isInitiated = false; + +public: + WakeOnLanModule(String parameters) : IoTItem(parameters) + { + jsonRead(parameters, "MAC", _macAddress); // Чтение MAC-адреса из параметров + _macAddress.replace("\"", ""); + if (!isValidMacAddress(_macAddress)) + { + SerialPrint("E", "WakeOnLan", "Settings > MAC = " + _macAddress + " is not valid", _id); + _macAddress = ""; + } + + jsonRead(parameters, "SecureOn", _SecureOn); // Чтение MAC-адреса из параметров + _SecureOn.replace("\"", ""); + if (!_SecureOn.isEmpty() && !isValidMacAddress(_SecureOn)) + { + SerialPrint("E", "WakeOnLan", "Settings > SecureOn = " + _SecureOn + " is not valid", _id); + _SecureOn = ""; + } + + jsonRead(parameters, "port", _port); // Чтение MAC-адреса из параметров + + jsonRead(parameters, "repeats", _repeats); // Чтение MAC-адреса из параметров + + WOL.setRepeat(_repeats, 100); // Repeat the packet three times with 100ms delay between + + init(); + + } + + void init() + { + if (isNetworkActive()) + { // Рассчитываем broadcast-адрес + IPAddress broadcastAddress = WOL.calculateBroadcastAddress(WiFi.localIP(), WiFi.subnetMask()); + WOL.setBroadcastAddress(broadcastAddress); // Устанавливаем broadcast-адрес + isInitiated = true; + } + } + + bool isValidMacAddress(String mac) + { + // Удаляем двоеточия, если есть + mac.replace(":", ""); + mac.toUpperCase(); + + // Должно быть ровно 12 символов (6 байт в hex) + if (mac.length() != 12) + return false; + + // Проверка, что все символы — это HEX (0-9, A-F) + for (int i = 0; i < 12; i++) + { + char c = mac.charAt(i); + if (!isHexadecimalDigit(c)) + return false; + } + + return true; + } + + void setValue(const IoTValue &Value, bool genEvent = true) + { + value = Value; + + if (value.valD == 1 && isNetworkActive()) + { + if (!isInitiated) init(); + + if (!_macAddress.isEmpty() && !_SecureOn.isEmpty()) + { + WOL.sendSecureMagicPacket(_macAddress, _SecureOn, _port); + SerialPrint("I", "WakeOnLan", "setValue, _SecureOn = " + _SecureOn, _id); + } + else if (!_macAddress.isEmpty()) + { + WOL.sendMagicPacket(_macAddress, _port); // Отправка Magic Packet + } + else + { + SerialPrint("E", "WakeOnLan", "Settings > MAC and/or SecureOn - not set or not valid", _id); + } + } + regEvent((String)(int)value.valD, "WakeOnLan", false, genEvent); + } + + IoTValue execute(String command, std::vector ¶m) + { + if (!isInitiated) init(); + + if (command == "mac") + { + String macTarget = ""; + + if (param.size() == 1 && isValidMacAddress(param[0].valS)) + { + macTarget = param[0].valS; + WOL.sendMagicPacket(macTarget); // Отправка Magic Packet + } + else if (param.size() == 2 && isValidMacAddress(param[0].valS)) + { + macTarget = param[0].valS; + int portNum = param[1].valD; + WOL.sendMagicPacket(macTarget, portNum); // Отправка Magic Packet + } + else if (param.size() == 3 && isValidMacAddress(param[0].valS) && isValidMacAddress(param[1].valS)) + { + macTarget = param[0].valS; + String secureOn = param[1].valS; + int portNum = param[2].valD; + WOL.sendSecureMagicPacket(macTarget, secureOn, portNum); + } + else + { + SerialPrint("E", "WakeOnLan", "MAC and/or SecureOn - not valid", _id); + return {}; + } + + SerialPrint("I", "WakeOnLan", "execute, Magic Packet sent to " + macTarget, _id); + } + + return {}; // команда поддерживает возвращаемое значения. Т.е. по итогу выполнения команды или общения с внешней системой, можно вернуть значение в сценарий для дальнейшей обработки + } + /* + String getValue() { + return (String)(int)value.valD; + } + */ + void doByInterval() {} +}; + +void *getAPI_WakeOnLanModule(String subtype, String param) +{ + if (subtype == F("WakeOnLanModule")) + { + return new WakeOnLanModule(param); // Используем новое имя класса + } + else + { + return nullptr; + } +} \ No newline at end of file diff --git a/src/modules/exec/WakeOnLanModule/export.json b/src/modules/exec/WakeOnLanModule/export.json new file mode 100644 index 00000000..9eec1b0f --- /dev/null +++ b/src/modules/exec/WakeOnLanModule/export.json @@ -0,0 +1,295 @@ +{ + "mark": "iotm", + "config": [ + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "ipNet", + "needSave": 0, + "widget": "anydataDef", + "page": "Network", + "descr": "IP", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Writing", + "subtype": "Cron", + "id": "cronWiFi", + "widget": "anydataDef", + "page": "Timers", + "descr": " cronWiFi", + "int": 1, + "val": "0 * * * * *", + "formatNextAlarm": "%H:%M:%S", + "needSave": 0, + "moduleName": "Cron" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "wifiConn", + "needSave": 0, + "widget": "toggle", + "page": "Network", + "descr": " Wi-Fi Connection", + "int": "0", + "val": "0", + "moduleName": "VButton" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "noWiFiCounter", + "needSave": 0, + "widget": "anydataDef", + "page": "Network", + "descr": " noWiFi Counter", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "maxNoWiFi", + "needSave": 0, + "widget": "inputDgt", + "page": "Network", + "descr": " maxNoWiFi to reboot", + "int": "0", + "val": "100", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Writing", + "subtype": "Cron", + "id": "cronPing", + "widget": "anydataDef", + "page": "Timers", + "descr": " cronPing", + "int": 1, + "val": "2 * * * * *", + "formatNextAlarm": "%H:%M:%S", + "needSave": 0, + "moduleName": "Cron" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Ping", + "id": "pingN", + "needSave": 0, + "widget": "nil", + "page": "Network", + "descr": "Internet", + "ip": "8.8.8.8", + "timeout": 5, + "interval": "1", + "data_size": 0, + "count": 0, + "tos": 0, + "moduleName": "Ping" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "noInternetCounter", + "needSave": 0, + "widget": "anydataDef", + "page": "Network", + "descr": "noInternet Counter", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "internetConn", + "needSave": 0, + "widget": "toggle", + "page": "Network", + "descr": "Internet Connection", + "int": "0", + "val": "0", + "moduleName": "VButton" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "routerFlag0", + "needSave": 0, + "widget": "nil", + "page": "nil", + "descr": "routerFlag0", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "routerFlag1", + "needSave": 0, + "widget": "nil", + "page": "nil", + "descr": "routerFlag1", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "routerFlag3", + "needSave": 0, + "widget": "nil", + "page": "nil", + "descr": "routerFlag3", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "vbtnWOLbook", + "needSave": 0, + "widget": "toggle", + "page": "WakeOnLAN", + "descr": "Wake Notebook scenario", + "int": "0", + "val": "0", + "moduleName": "VButton", + "show": true + }, + { + "global": 0, + "type": "Reading", + "subtype": "WakeOnLanModule", + "id": "wakeLanBook", + "widget": "toggle", + "page": "WakeOnLAN", + "descr": "Wake Notebook w.settings", + "MAC": "\"A0:AD:A8:A2:AF:E2\"", + "SecureOn": "", + "port": 9, + "repeats": 3, + "moduleName": "WakeOnLanModule", + "show": true + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "macInput", + "needSave": 0, + "widget": "inputTxt", + "page": "WakeOnLAN", + "descr": "MAC Input", + "int": "0", + "val": "A0:AD:A8:A2:AF:E2", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "bootDateNet", + "needSave": 0, + "widget": "anydataDef", + "page": "Time", + "descr": " Boot Date", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + } + ] +} + +scenario=>if onStart then { +ipNet = getIP() +pingN.ping("8.8.8.8") +bootDateNet=getTime() +} + + +#book + if vbtnWOLbook == 1 then { +wakeLanBook.mac(macInput) + vbtnWOLbook = 0 +} + + +###################### +if cronWiFi then ipNet = getIP() + +if ipNet == "(IP unset)" | ipNet == "192.168.4.1" then { +wifiConn = 0 +noWiFiCounter = noWiFiCounter +1 +}else { +wifiConn = 1 +noWiFiCounter = 0 +} + +if noWiFiCounter > maxNoWiFi then reboot() + +################### +if cronPing then { +pingN.ping("8.8.8.8") +} + +if pingN == 0 then { +internetConn = 0 +noInternetCounter = noInternetCounter +1 +}else { +internetConn = 1 +#noInternetCounter = 0 +noInternetCounter = noInternetCounter +1 +} diff --git a/src/modules/exec/WakeOnLanModule/modinfo.json b/src/modules/exec/WakeOnLanModule/modinfo.json new file mode 100644 index 00000000..d612c7f9 --- /dev/null +++ b/src/modules/exec/WakeOnLanModule/modinfo.json @@ -0,0 +1,48 @@ +{ + "menuSection": "executive_devices", + "configItem": [ + { + "global": 0, + "name": "WakeOnLan", + "type": "Reading", + "subtype": "WakeOnLanModule", + "id": "wakeLan", + "widget": "toggle", + "page": "Кнопки", + "descr": "Wake PC", + "MAC": "A8:A3:A5:A5:52:42", + "SecureOn": "", + "port": 9, + "repeats": 3 + } + ], + "about": { + "authorName": "Alex", + "authorContact": "https://t.me/cmche", + "authorGit": "https://github.com/CHE77/IoTManager-Modules", + "specialThanks": "", + "moduleName": "WakeOnLanModule", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "WakeOnLan", + "moduleDesc": "Пробуждение компютера по сети", + "license": "Cooperative Non-Violent Public License (CNPL)", + "propInfo": { + "MAC": "MAC адрес сетевой карты", + "SecureOn": "Секретный ключ в таком же виде как и MAC адрес. (необязательно)", + "port": "порт - по умолчнияю - 9", + "repeats": "Количество повторов команд пробужния - для надежности" + } + }, + "defActive": false, + "usedLibs": { + "esp32*": ["a7md0/WakeOnLan @ ^1.1.7"], + "esp82*": ["a7md0/WakeOnLan @ ^1.1.7"], + "bk72*": ["a7md0/WakeOnLan @ ^1.1.7"] + } +} + + diff --git a/src/modules/sensors/NoiseAdc/NoiseAdc.cpp b/src/modules/sensors/NoiseAdc/NoiseAdc.cpp new file mode 100644 index 00000000..4cdd8c0e --- /dev/null +++ b/src/modules/sensors/NoiseAdc/NoiseAdc.cpp @@ -0,0 +1,359 @@ +// Licensed under the Cooperative Non-Violent Public License (CNPL) +// See: https://github.com/CHE77/IoTManager-Modules/blob/main/LICENSE + +#include "Global.h" +#include "classes/IoTItem.h" + +extern IoTGpio IoTgpio; + +// Это файл сенсора, в нем осуществляется чтение сенсора. +// для добавления сенсора вам нужно скопировать этот файл и заменить в нем текст NoiseAdc на название вашего сенсора +// Название должно быть уникальным, коротким и отражать суть сенсора. + +// ребенок - родитель +class NoiseAdc : public IoTItem +{ +private: + //======================================================================================================= + // Секция переменных. + // Это секция где Вы можете объявлять переменные и объекты arduino библиотек, что бы + // впоследствии использовать их в loop и setup + +#ifdef ESP8266 + float adcRange = 1023.0; + const int maxSamples = 100; + int samples[100]; + float deltas[100]; // отклонения от среднего + +#else + float adcRange = 4095.0; + const int maxSamples = 200; + int samples[200]; + float deltas[200]; // отклонения от среднего +#endif + + unsigned int _pin; + unsigned int _steps; + unsigned int _period = 0; + unsigned long _lastSoundingMillis = 0; + + int sampleIndex = 0; + + float _referenceVoltage = 0.02; // калибровочный "тихий" уровень + float vcc = 3.3; + + String _parameter = ""; + + float mean = 0; + float minVal = 0; + float maxVal = 0; + float rms = 0; + float peak = 0; + float median = 0; + float minValMean = 0; + float maxValMean = 0; + float peakToPeak = 0; + float peakVoltage = 0; + float db = 0; + + bool _debug = false; + +public: + //======================================================================================================= + // setup() + // это аналог setup из arduino. Здесь вы можете выполнять методы инициализации сенсора. + // Такие как ...begin и подставлять в них параметры полученные из web интерфейса. + // Все параметры хранятся в перемененной parameters, вы можете прочитать любой параметр используя jsonRead функции: + // jsonReadStr, jsonReadBool, jsonReadInt + NoiseAdc(String parameters) : IoTItem(parameters) + { + _pin = jsonReadInt(parameters, "pin"); + _steps = jsonReadInt(parameters, "steps"); + if (_steps > maxSamples) + _steps = maxSamples; + + if (_steps < 10) + _steps = 10; + _period = _interval / _steps; + if (_debug) + SerialPrint("i", F("NoiseAdc"), "_period = " + String(_period)); + + jsonRead(parameters, F("refVoltage"), _referenceVoltage, false); + if (_referenceVoltage == 0) + _referenceVoltage = 0.01; + + _parameter = jsonReadStr(parameters, "parameter"); + jsonRead(parameters, F("debug"), _debug); + } + + //======================================================================================================= + + void doByInterval() + { + float result = analyzeSamples(_parameter); + + if (result) + { + value.valD = result; + regEvent(value.valD, "NoiseAdc", _debug, true); // обязательный вызов хотяб один + } + } + + //======================================================================================================= + // loop() + // полный аналог loop() из arduino. Нужно помнить, что все модули имеют равный поочередный доступ к центральному loop(), поэтому, необходимо следить + // за задержками в алгоритме и не создавать пауз. Кроме того, данная версия перегружает родительскую, поэтому doByInterval() отключается, если + // не повторить механизм расчета интервалов. + void loop() + { + + if (millis() > _lastSoundingMillis + _period && sampleIndex < maxSamples) + { + _lastSoundingMillis = millis(); + samples[sampleIndex++] = IoTgpio.analogRead(_pin); + } + + IoTItem::loop(); + } + + void onModuleOrder(String &key, String &value) + { + if (key == "setRefVoltage") + { + _referenceVoltage = analyzeSamples("peakVoltage"); + SerialPrint("i", F("NoiseAdc"), "User run calibration referenceVoltage " + String(_referenceVoltage)); + // TODO wtitejson to config.json????? + } + } + + IoTValue execute(String command, std::vector ¶m) + { + if (command == "parameter") + { + if (param.size() == 1 && !param[0].isDecimal) + { + + String parameter = param[0].valS.c_str(); + + float output = 0; + + if (parameter == "mean") + { + output = mean; + } + else if (parameter == "minVal") + { + output = minVal; + } + else if (parameter == "maxVal") + { + output = maxVal; + } + else if (parameter == "RMS") + { + output = rms; + } + else if (parameter == "median") + { + output = median; + } + else if (parameter == "minValMean") + { + output = minValMean; + } + else if (parameter == "maxValMean") + { + output = maxValMean; + } + else if (parameter == "peak") + { + output = peak; + } + else if (parameter == "peakToPeak") + { + output = peakToPeak; + } + else if (parameter == "peakVoltage") + { + output = peakVoltage; + } + else if (parameter == "db") + { + output = db; + } + else + { + } + + if (_debug) + SerialPrint("i", F("NoiseAdc"), "execute parameter " + String(parameter) + " = " + String(output)); + + if (output) + { + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = output; + return valTmp; + } + } + } + + return {}; // команда поддерживает возвращаемое значения. Т.е. по итогу выполнения команды или общения с внешней системой, можно вернуть значение в сценарий для дальнейшей обработки + } + + float analyzeSamples(String parameter) + { + if (sampleIndex < 5) + { + if (_debug) + SerialPrint("i", F("NoiseAdc"), "Нет данных для анализа"); + return 0; + } + + minVal = samples[0]; + maxVal = samples[0]; + float sum = 0; + for (int i = 0; i < sampleIndex; i++) + { + if (samples[i] < minVal) + minVal = samples[i]; + if (samples[i] > maxVal) + maxVal = samples[i]; + sum += samples[i]; + } + + mean = sum / sampleIndex; + + for (int i = 0; i < sampleIndex; i++) + { + deltas[i] = samples[i] - mean; + } + + rms = 0; + + minValMean = deltas[0]; + maxValMean = deltas[0]; + + peak = 0; + + for (int i = 0; i < sampleIndex; i++) + { + rms += deltas[i] * deltas[i]; + + if (deltas[i] < minValMean) + minValMean = deltas[i]; + if (deltas[i] > maxValMean) + maxValMean = deltas[i]; + + if (abs(deltas[i] > peak)) + peak = abs(deltas[i]); + } + + rms = sqrt(rms / sampleIndex); + + peakToPeak = maxValMean - minValMean; + + float sorted[maxSamples]; + memcpy(sorted, deltas, sizeof(float) * sampleIndex); + std::sort(sorted, sorted + sampleIndex); + median = (sampleIndex % 2 == 0) ? (sorted[sampleIndex / 2 - 1] + sorted[sampleIndex / 2]) / 2.0 : sorted[sampleIndex / 2]; + + peakVoltage = peakToPeak * vcc / adcRange; + db = 20.0 * log10(peakVoltage / _referenceVoltage); + if (peakVoltage < _referenceVoltage) + db = 0; + if (_debug) + { + Serial.println("== Анализ завершен =="); + Serial.print("Сэмплов: "); + Serial.println(sampleIndex); + Serial.print("Среднее (DC): "); + Serial.println(mean, 2); + Serial.print("Мин/Макс (Aбс): "); + Serial.print(minVal, 2); + Serial.print(" / "); + Serial.println(maxVal, 2); + Serial.print("RMS (AC): "); + Serial.println(rms, 2); + Serial.print("Медиана (AC): "); + Serial.println(median, 2); + Serial.print("Мин/Макс (AC): "); + Serial.print(minValMean, 2); + Serial.print(" / "); + Serial.println(maxValMean, 2); + Serial.print("Абсолютный пик (AC): "); + Serial.println(peak, 2); + Serial.print("Размах (AC): "); + Serial.println(peakToPeak, 2); + Serial.print("Размах в вольтах: "); + Serial.println(peakVoltage, 4); + Serial.print("Уровень звука ≈ dB SPL: "); + Serial.println(db, 1); + } + + sampleIndex = 0; + + if (parameter == "mean") + { + return mean; + } + else if (parameter == "minVal") + { + return minVal; + } + else if (parameter == "maxVal") + { + return maxVal; + } + else if (parameter == "RMS") + { + return rms; + } + else if (parameter == "median") + { + return median; + } + else if (parameter == "minValMean") + { + return minValMean; + } + else if (parameter == "maxValMean") + { + return maxValMean; + } + else if (parameter == "peak") + { + return peak; + } + else if (parameter == "peakToPeak") + { + return peakToPeak; + } + else if (parameter == "peakVoltage") + { + return peakVoltage; + } + else if (parameter == "db") + { + return db; + } + else + { + return 0; + } + } + + ~NoiseAdc() {}; +}; + +void *getAPI_NoiseAdc(String subtype, String param) +{ + if (subtype == F("NoiseAdc")) + { + return new NoiseAdc(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/sensors/NoiseAdc/export.json b/src/modules/sensors/NoiseAdc/export.json new file mode 100644 index 00000000..9806e3ce --- /dev/null +++ b/src/modules/sensors/NoiseAdc/export.json @@ -0,0 +1,89 @@ +{ + "mark": "iotm", + "config": [ + { + "global": 0, + "type": "Writing", + "subtype": "TelegramLT", + "id": "tg0", + "widget": "", + "page": "", + "descr": "", + "token": "1262564256457:AAGQJ6LvZ-dOGFvaerhbserhdtbzgdndaY", + "chatID": "49999999" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "valueTreashhold", + "needSave": "1", + "widget": "inputDgt", + "page": "Сенсоры", + "descr": "ADC Threshold", + "int": "0", + "val": "4095", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "vbtnListen", + "needSave": "1", + "widget": "toggle", + "page": "Сенсоры", + "descr": "Tg Alerts", + "int": "0", + "val": "0", + "moduleName": "VButton" + }, + { + "global": 0, + "type": "Writing", + "subtype": "Loging", + "id": "logNoise", + "widget": "chart2", + "page": "Графики", + "descr": "Noise", + "int": 0, + "daysSave": 5, + "daysShow": 0, + "points": 300, + "moduleName": "Loging" + }, + { + "global": 0, + "type": "Reading", + "subtype": "NoiseAdc", + "id": "noiseADC", + "widget": "anydataDef", + "page": "Сенсоры", + "descr": "NoiseAdc", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0, + "pin": "0", + "int": 10, + "avgSteps": "500", + "parameter": "peakToPeak", + "debug": 1, + "refVoltage": "0.02", + "btn-setRefVoltage": "nil", + "moduleName": "NoiseAdc" + } + ] +} + +scenario=>if onStart then tg0.sendMsg("ESP Noise reboot. IP " + getIP() ) + +if noiseADC > valueTreashhold & vbtnListen then {tg0.sendMsg("Noise level = " + noiseADC) +} + +if noiseADC then = noiseADC + + diff --git a/src/modules/sensors/NoiseAdc/modinfo.json b/src/modules/sensors/NoiseAdc/modinfo.json new file mode 100644 index 00000000..ad70d38f --- /dev/null +++ b/src/modules/sensors/NoiseAdc/modinfo.json @@ -0,0 +1,70 @@ +{ + "menuSection": "sensors", + "configItem": [ + { + "global": 0, + "name": "Анализ шума ADC", + "type": "Reading", + "subtype": "NoiseAdc", + "id": "noise", + "widget": "anydataDef", + "page": "Сенсоры", + "descr": "NoiseAdc", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0, + "pin": 0, + "int": 10, + "steps": 100, + "parameter": "peakToPeak", + "debug": 1, + "refVoltage": 0.02, + "btn-setRefVoltage": "nil" + } + ], + "about": { + "authorName": "Alex", + "authorContact": "https://t.me/cmche", + "authorGit": "https://github.com/CHE77/IoTManager-Modules", + "exampleURL": "https://iotmanager.org/wiki", + "specialThanks": "", + "moduleName": "NoiseAdc", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 10, + "esp8266_4mb": 3 + }, + "title": "Анализ шума ADC", + "moduleDesc": "Позволяет получить различные характеристики сигнала на аналоговом GPIO. В каждый промежуток времени int делает avgSteps измерений, орпеделяет характоистики ряда и выводит один параметров.", + "license": "Cooperative Non-Violent Public License (CNPL)", + "propInfo": { + "pin": "Аналоговый GPIO номер, к которому подключен датчик.", + "int": "Длительность опроса датчика и вывод показаний", + "steps": "Количество считываний в период опроса. Минимальное - 10. Максимальное - 200(esp8266), 1000(esp32). По факту оно будет несколько меньше.", + "parameter": { + "mean": "Среднее", + "minVal": "Минимальное значение ADC", + "maxVal": "Максимальное значение ADC", + "RMS": "Среднеквадратичное отклонение (от среднего)", + "median": "Медиана (от среднего)", + "minValMean": "Минимальное значение (от среднего)", + "maxValMean": "Максимальное значение (от среднего)", + "peak": "Абсолютный пик (от среднего)", + "peakToPeak": "Размах сигнала относительно среднего (AC) в отсчётах ADC", + "peakVoltage": "Максимальная амплитуда колебаний сигнала", + "db": "Приближённая оценка уровня звука в децибелах " + }, + + "debug": "Вывод отладочной информации", + "refVoltage": "Базовый уровень для сигнала (в тишине) в Вольтах", + "btn-setRefVoltage": "Замер базового уровня в тишине и вывод его в сериал" + } + }, + "defActive": false, + "usedLibs": { + "esp32*": [], + "esp82*": [], + "bk72*": [] + } +} \ No newline at end of file diff --git a/src/modules/sensors/Presence/Presence.cpp b/src/modules/sensors/Presence/Presence.cpp new file mode 100644 index 00000000..4b05df52 --- /dev/null +++ b/src/modules/sensors/Presence/Presence.cpp @@ -0,0 +1,579 @@ +// Licensed under the Cooperative Non-Violent Public License (CNPL) +// See: https://github.com/CHE77/IoTManager-Modules/blob/main/LICENSE + + +#define MQTT_MAX_PACKET_SIZE 512 // или 1024 +#include "Global.h" +#include "classes/IoTItem.h" + +#include +#define EARTH_RADIUS_KM 6371.0 // Радиус Земли в километрах + +#include "NTP.h" +#include // чтобы знать тип +extern PubSubClient mqtt; // объявляем глобальный объект + +class Presence : public IoTItem +{ +private: + String _MAC; + String _parameter; + IoTItem *tmp; + int _minutesPassed = 0; + String json = "{}"; + int orange = 0; + int red = 0; + int offline = 0; + bool dataFromNode = false; + String _topic = ""; + bool _isJson; + bool _ticker = true; + bool _debug = false; + bool sendOk = false; + float _lat_A = 0; + float _lon_A = 0; + + + struct PresenceData + { + String chargingState; + String plugState; + String connectedWifi; + String geoLocation; + float lat = 0.0; + float lon = 0.0; + unsigned long geoTimestamp = 0; + String geoTime; + String deviceName; + int batteryLevel = -1; + unsigned long currentTimestamp = 0; + String currentTime; + unsigned long nextScheduledTimestamp = 0; + String nextScheduledTime; + unsigned long nextAlarmclockTimestamp = 0; + String nextAlarmclockTime; + std::vector conditionContent; + String conditionContentString; + }; + PresenceData pdata; + +public: + Presence(String parameters) : IoTItem(parameters) + { + _parameter = jsonReadStr(parameters, "parameter"); + jsonRead(parameters, F("orange"), orange); + jsonRead(parameters, F("red"), red); + jsonRead(parameters, F("offline"), offline); + _topic = jsonReadStr(parameters, "topic"); + if (_debug) + SerialPrint("i", "Presence topic : ", _topic); + jsonRead(parameters, F("isJson"), _isJson); + // jsonRead(parameters, "addPrefix", _addPrefix); + jsonRead(parameters, F("Lat. A"), _lat_A); + jsonRead(parameters, F("Long. A"), _lon_A); + jsonRead(parameters, F("ticker"), _ticker); + jsonRead(parameters, F("debug"), _debug); + dataFromNode = false; + if (mqttIsConnect()) + { + mqtt.setBufferSize(512); + sendOk = true; + mqttSubscribeExternal(_topic); + } + } + + char *TimeToString(unsigned long t) + { + static char str[12]; + long h = t / 3600; + t = t % 3600; + int m = t / 60; + int s = t % 60; + sprintf(str, "%02ld:%02d:%02d", h, m, s); + return str; + } + + double toRadians(double degrees) + { + return degrees * M_PI / 180.0; + } + + double toDegrees(double radians) + { + return radians * 180.0 / M_PI; + } + + // Возвращает пеленг в градусах: от 0 до 360 + double calculateInitialBearing(double lat1, double lon1, double lat2, double lon2) + { + lat1 = toRadians(lat1); + lon1 = toRadians(lon1); + lat2 = toRadians(lat2); + lon2 = toRadians(lon2); + + double deltaLon = lon2 - lon1; + + double y = sin(deltaLon) * cos(lat2); + double x = cos(lat1) * sin(lat2) - + sin(lat1) * cos(lat2) * cos(deltaLon); + + double bearing = atan2(y, x); + bearing = toDegrees(bearing); + + // Приводим к диапазону 0–360 + return fmod((bearing + 360.0), 360.0); + } + + // lat и lon — в градусах + double haversineDistance(double lat1, double lon1, double lat2, double lon2) + { + double dLat = toRadians(lat2 - lat1); + double dLon = toRadians(lon2 - lon1); + + lat1 = toRadians(lat1); + lat2 = toRadians(lat2); + + double a = sin(dLat / 2) * sin(dLat / 2) + + cos(lat1) * cos(lat2) * + sin(dLon / 2) * sin(dLon / 2); + + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + + return EARTH_RADIUS_KM * c * 1000.0; + } + + void onMqttRecive(String &topic, String &msg) + { + Serial.printf("[MQTT] Topic: %s\nPayload size: %d bytes\n", topic.c_str(), msg.length()); + msg.trim(); // Убираем пробелы и переносы строк + + if (msg.indexOf("HELLO") == -1) + { + if (_debug) + SerialPrint("i", "Presence HELLO", " _1d: " + _id + " topic: " + topic + " msg: " + msg); + String dev = selectToMarkerLast(topic, "/"); + dev.toUpperCase(); + dev.replace(":", ""); + if (_topic != topic) + { + if (_debug) + { + SerialPrint("i", "Presence", topic + " not equal: " + _topic + " msg: " + msg); + } + return; + } + + if (_isJson) + { + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + DeserializationError err = deserializeJson(doc, msg); + + if (err) + { + SerialPrint("E", F("Presence"), err.f_str()); + return; + } + + JsonObject obj = doc.as(); + + if (obj.containsKey("chargingState")) + { + pdata.chargingState = obj["chargingState"].as(); + } + if (obj.containsKey("plugState")) + { + pdata.plugState = obj["plugState"].as(); + } + + if (obj.containsKey("connectedWifi")) + pdata.connectedWifi = obj["connectedWifi"].as(); + if (obj.containsKey("geoLocation")) + { + pdata.geoLocation = obj["geoLocation"].as(); + // Разбор геолокации после получения + parseGeo(pdata.geoLocation); + pdata.geoTime = getDateTimeDotFormatedFromUnix(pdata.geoTimestamp); + SerialPrint("i", "Presence", "GeoTime : " + pdata.geoTime); + } + + if (obj.containsKey("deviceName")) + pdata.deviceName = obj["deviceName"].as(); + if (obj.containsKey("batteryLevel")) + pdata.batteryLevel = obj["batteryLevel"].as(); + if (obj.containsKey("currentTimestamp")) + { + pdata.currentTimestamp = obj["currentTimestamp"].as(); + pdata.currentTime = getDateTimeDotFormatedFromUnix(pdata.currentTimestamp); + } + + if (obj.containsKey("nextScheduledTimestamp")) + { + pdata.nextScheduledTimestamp = obj["nextScheduledTimestamp"].as(); + pdata.nextScheduledTime = getDateTimeDotFormatedFromUnix(pdata.nextScheduledTimestamp); + } + + if (obj.containsKey("nextAlarmclockTimestamp")) + { + pdata.nextAlarmclockTimestamp = obj["nextAlarmclockTimestamp"].as(); + pdata.nextAlarmclockTime = getDateTimeDotFormatedFromUnix(pdata.nextAlarmclockTimestamp); + } + + if (obj.containsKey("conditionContent")) + { + JsonArray arr = obj["conditionContent"].as(); + pdata.conditionContent.clear(); + for (String s : arr) + { + pdata.conditionContent.push_back(s); + } + + String conditionStr; + for (size_t i = 0; i < pdata.conditionContent.size(); ++i) + { + conditionStr += pdata.conditionContent[i]; + if (i < pdata.conditionContent.size() - 1) + conditionStr += ", "; + } + pdata.conditionContentString = conditionStr; + } + + dataFromNode = true; + _minutesPassed = 0; + + String sensorVal; + + if (_parameter == "latitude") + { + value.isDecimal = true; + value.valD = pdata.lat; + } + else if (_parameter == "longitude") + { + value.isDecimal = true; + value.valD = pdata.lon; + } + else if (_parameter == "azimuth") + { + value.isDecimal = true; + value.valD = calculateInitialBearing(_lat_A, _lon_A, pdata.lat, pdata.lon); + } + else if (_parameter == "distance") + { + value.isDecimal = true; + value.valD = haversineDistance(_lat_A, _lon_A, pdata.lat, pdata.lon); + } + else if (_parameter == "batteryLevel") + { + value.isDecimal = true; + value.valD = pdata.batteryLevel; + } + else if (_parameter == "geoTime") + { + value.isDecimal = false; + value.valS = pdata.geoTime; + } + else if (_parameter == "geoTimestamp") + { + value.isDecimal = false; + value.valS = pdata.geoTimestamp; + } + else if (_parameter == "currentTime") + { + value.isDecimal = false; + value.valS = pdata.currentTime; + } + else if (_parameter == "nextScheduledTime") + { + value.isDecimal = false; + value.valS = pdata.nextScheduledTime; + } + else if (_parameter == "nextAlarmclockTime") + { + value.isDecimal = false; + value.valS = pdata.nextAlarmclockTime; + } + else if (_parameter == "conditionContent") + { + value.isDecimal = false; + value.valS = pdata.conditionContentString; + } + else if (obj.containsKey(_parameter) && obj[_parameter].is()) + { + sensorVal = obj[_parameter].as(); + value.isDecimal = false; + value.valS = sensorVal; + } + else + { + value.isDecimal = false; + value.valS = "parameter mismatch"; + } + + if (value.isDecimal) + { + regEvent(value.valD, F("Presence"), _debug, _ticker); + } + else + { + regEvent(value.valS, F("Presence"), _debug, _ticker); + } + + } + else + { + if (_debug) + { + SerialPrint("i", "Presence", "Received MAC: " + dev + " val=" + msg); + } + dataFromNode = true; + _minutesPassed = 0; + setValue(msg); + } + } + } + + IoTValue execute(String command, std::vector ¶m) + { + IoTValue valTmp; + + if (command == "latitude") + { + valTmp.isDecimal = true; + valTmp.valD = pdata.lat; + return valTmp; + } + else if (command == "longitude") + { + valTmp.isDecimal = true; + valTmp.valD = pdata.lon; + return valTmp; + } + else if (command == "azimuth") + { + if (param.size() == 2 && param[0].isDecimal && param[1].isDecimal) + { + valTmp.isDecimal = true; + valTmp.valD = calculateInitialBearing(param[0].valD, param[1].valD, pdata.lat, pdata.lon); + } + else + { + valTmp.isDecimal = false; + valTmp.valS = "wrong parameters"; + } + return valTmp; + } + else if (command == "distance") + { + if (param.size() == 2 && param[1].isDecimal && param[1].isDecimal) + { + valTmp.isDecimal = true; + valTmp.valD = haversineDistance(param[0].valD, param[1].valD, pdata.lat, pdata.lon); + } + else + { + valTmp.isDecimal = false; + valTmp.valS = "wrong parameters"; + } + return valTmp; + } + else if (command == "batteryLevel") + { + valTmp.isDecimal = true; + valTmp.valD = pdata.batteryLevel; + return valTmp; + } + else if (command == "geoTime") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.geoTime; + return valTmp; + } + else if (command == "geoTimestamp") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.geoTimestamp; + return valTmp; + } + else if (command == "currentTime") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.currentTime; + return valTmp; + } + else if (command == "currentTimestamp") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.currentTimestamp; + return valTmp; + } + else if (command == "nextScheduledTime") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.nextScheduledTime; + return valTmp; + } + else if (command == "nextScheduledTimestamp") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.nextScheduledTimestamp; + return valTmp; + } + else if (command == "nextAlarmclockTime") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.nextAlarmclockTime; + return valTmp; + } + else if (command == "nextAlarmclockTimestamp") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.nextAlarmclockTimestamp; + return valTmp; + } + else if (command == "conditionContent") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.conditionContentString; + return valTmp; + } + else if (command == "chargingState") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.chargingState; + return valTmp; + } + else if (command == "plugState") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.plugState; + return valTmp; + } + else if (command == "connectedWifi") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.connectedWifi; + return valTmp; + } + else if (command == "deviceName") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.deviceName; + return valTmp; + } + else if (command == "geoLocation") + { + valTmp.isDecimal = false; + valTmp.valS = pdata.geoLocation; + return valTmp; + } + else + { + valTmp.isDecimal = false; + valTmp.valS = "wrong command"; + return valTmp; + } + + return {}; + } + + String getMqttExterSub() + { + return _topic; + } + + void doByInterval() + { + _minutesPassed++; + setNewWidgetAttributes(); + if (mqttIsConnect() && !sendOk) + { + mqtt.setBufferSize(512); // ← ДО подписи + sendOk = true; + mqttSubscribeExternal(_topic); + } + } + void onMqttWsAppConnectEvent() + { + setNewWidgetAttributes(); + } + + void setNewWidgetAttributes() + { + + jsonWriteStr(json, F("info"), prettyMinutsTimeout(_minutesPassed)); + if (dataFromNode) + { + if (orange != 0 && red != 0 && offline != 0) + { + if (_minutesPassed < orange) + { + jsonWriteStr(json, F("color"), ""); + } + if (_minutesPassed >= orange && _minutesPassed < red) + { + jsonWriteStr(json, F("color"), F("orange")); // сделаем виджет оранжевым + } + if (_minutesPassed >= red && _minutesPassed < offline) + { + jsonWriteStr(json, F("color"), F("red")); // сделаем виджет красным + } + if (_minutesPassed >= offline) + { + jsonWriteStr(json, F("info"), F("offline")); + SerialPrint("i", "Presence", _id + " - offline"); + } + } + } + else + { + jsonWriteStr(json, F("info"), F("awaiting")); + } + // SerialPrint("i", "JSON", json); + sendSubWidgetsValues(_id, json); + } + + void parseGeo(const String &geo) + { + if (!geo.startsWith("geo:")) + return; + + int commaIndex = geo.indexOf(',', 4); + int semicolonIndex = geo.indexOf(';', commaIndex); + + if (commaIndex == -1 || semicolonIndex == -1) + return; + + String latStr = geo.substring(4, commaIndex); + String lonStr = geo.substring(commaIndex + 1, semicolonIndex); + + // timestamp + String tsTag = "timestamp="; + int tsIndex = geo.indexOf(tsTag); + String tsStr = (tsIndex > 0) ? geo.substring(tsIndex + tsTag.length()) : ""; + + pdata.lat = latStr.toFloat(); + pdata.lon = lonStr.toFloat(); + pdata.geoTimestamp = tsStr.toInt(); + + if (_debug) + { + SerialPrint("i", "Presence", "Lat: " + String(pdata.lat, 6)); + SerialPrint("i", "Presence", "Lon: " + String(pdata.lon, 6)); + SerialPrint("i", "Presence", "Geo timestamp: " + String(pdata.geoTimestamp)); + } + } + + ~Presence() {}; +}; + +void *getAPI_Presence(String subtype, String param) +{ + if (subtype == F("Presence")) + { + return new Presence(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/sensors/Presence/export.json b/src/modules/sensors/Presence/export.json new file mode 100644 index 00000000..409b4937 --- /dev/null +++ b/src/modules/sensors/Presence/export.json @@ -0,0 +1,372 @@ +{ + "mark": "iotm", + "config": [ + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutDistance", + "needSave": 0, + "widget": "anydataKm", + "page": "Output", + "descr": "Distance", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": "0.001", + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutAzimuth", + "needSave": 0, + "widget": "anydataСorner", + "page": "Output", + "descr": "Azimuth", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Presence", + "id": "presence", + "widget": "anydataM", + "page": "Presence", + "descr": "Distance", + "Lat. A": "47.0159", + "Long. A": "28.8448", + "parameter": "distance", + "topic": "/myPhone/status", + "isJson": 1, + "round": "0", + "orange": 60, + "red": 120, + "offline": 180, + "int": 15, + "ticker": 1, + "debug": 1, + "moduleName": "Presence" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutLat", + "needSave": 0, + "widget": "anydataСorner", + "page": "Output", + "descr": "Latitude", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutLong", + "needSave": 0, + "widget": "anydataСorner", + "page": "Output", + "descr": "Longitude", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutbatteryLevel", + "needSave": 0, + "widget": "anydataHum", + "page": "Output", + "descr": "batteryLevel", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutgeoTime", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "geoTime", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutgeoTimestamp", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "geoTimestamp", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutcurrentTime", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "currentTime", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutcurrentTimestamp", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "currentTimestamp", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutnextScheduledTime", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "nextScheduledTime", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutnextScheduledTimestamp", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "nextScheduledTimestamp", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutnextAlarmclockTime", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "nextAlarmclockTime", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutnextAlarmclockTimestamp", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "nextAlarmclockTimestamp", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutchargingState", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "chargingState", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutplugState", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "plugState", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutconnectedWifi", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "connectedWifi", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutdeviceName", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "deviceName", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutgeoLocation", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "geoLocation", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutconditionContent", + "needSave": 0, + "widget": "anydataDef", + "page": "Output", + "descr": "conditionContent", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + } + ] +} + +scenario=> if presence then { +voutDistance = presence.distance(47.0159,28.8448) +voutAzimuth = presence.azimuth(47.0159,28.8448) +voutLat = presence.latitude() +voutLong = presence.longitude() +voutbatteryLevel = presence.batteryLevel() +voutgeoTime = presence.geoTime() +voutgeoTimestamp = presence.geoTimestamp() +voutcurrentTime = presence.currentTime() +voutcurrentTimestamp = presence.currentTimestamp() +voutnextScheduledTime = presence.nextScheduledTime() +voutnextScheduledTimestamp = presence.nextScheduledTimestamp() +voutnextAlarmclockTime = presence.nextAlarmclockTime() +voutnextAlarmclockTimestamp = presence.nextAlarmclockTimestamp() +voutchargingState = presence.chargingState() +voutplugState = presence.plugState() +voutconnectedWifi = presence.connectedWifi() +voutdeviceName = presence.deviceName() +voutgeoLocation = presence.geoLocation() +voutconditionContent = presence.conditionContent() +} \ No newline at end of file diff --git a/src/modules/sensors/Presence/modinfo.json b/src/modules/sensors/Presence/modinfo.json new file mode 100644 index 00000000..fc900e39 --- /dev/null +++ b/src/modules/sensors/Presence/modinfo.json @@ -0,0 +1,199 @@ +{ + "menuSection": "sensors", + "configItem": [ + { + "global": 0, + "name": "MQTT Presence Subscriber", + "type": "Reading", + "subtype": "Presence", + "id": "presence", + "widget": "anydataM", + "page": "Presence", + "descr": "Дистанция", + "Lat. A": "47.0159", + "Long. A": "28.8448", + "parameter": "distance", + "topic": "/myPhone/status", + "isJson": 1, + "round": "0", + "orange": 60, + "red": 120, + "offline": 180, + "int": 15, + "ticker": 1, + "debug": 1 + } + ], + "about": { + "authorName": "Alex", + "authorContact": "https://t.me/cmche", + "authorGit": "https://github.com/CHE77/IoTManager-Modules", + "exampleURL": "https://iotmanager.org/wiki", + "specialThanks": "", + "moduleName": "Presence", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "MQTT Presence Subscriber", + "moduleDesc": "Модуль получения и обработки данных из Presence Publisher app - https://f-droid.org/packages/org.ostrya.presencepublisher/ https://play.google.com/store/apps/details?id=org.ostrya.presencepublisher Получает геопозицию телефона, считает пеленг и дистанцию.", + "license": "Cooperative Non-Violent Public License (CNPL)", + "propInfo": { + "Lat. A": "Широта точки отсчета (локации), в градусах", + "Long. A": "Долгота точки отсчета (локации), в градусах", + "parameter": "Параметр/ключ для получения данных из json и его производных. Совпадают с методами для сценария", + "topic": "Топик на который подписывется модуль. Должен совпадать с топиком в приложении и оканчиваться на status", + "isJson": "1 - ожидаем в топике json. Другие форматы пока не подерживаются. В приложении выберите тоже json", + "round": "Округление после запятой.", + "orange": "Количество минут после которого окрасить виджет в оранжевый цвет", + "red": "количество минут после которого окрасить виджет в красный цвет", + "offline": "Количество минут после которого отобразить что устройство offline, если все три orange red и offline поставить в ноль - то функция окраски выключится", + "int": "Интервал для изменения цвета", + "ticker": "Генерировать(1) или нет(0) события при каждом тике часов (каждые int секунд).", + "debug": "1 - выводить дополнительный лог в сериал" + }, + "retInfo": " - Согласно выбраного параметра", + "funcInfo": [ + { + "name": "latitude", + "descr": "Получить широту позиции устройства (и далее) с приложением Presence Publisher", + "params": [ + "presence.latitude()" + ] + }, + { + "name": "longitude", + "descr": "Получить долготу позиции устройства", + "params": [ + "presence.longitude()" + ] + }, + { + "name": "distance", + "descr": "Получить дистанцию до устройства", + "params": [ + "presence.distance()" + ] + }, + { + "name": "azimuth", + "descr": "Получить пеленг на устройтво", + "params": [ + "presence.azimuth()" + ] + }, + { + "name": "batteryLevel", + "descr": "Получить уровень заряда батареи устройства", + "params": [ + "presence.batteryLevel()" + ] + }, + { + "name": "geoTime", + "descr": "Получить время определения геопозии устройства", + "params": [ + "presence.geoTime()" + ] + }, + { + "name": "geoTimestamp", + "descr": "Получить UnixTime определения геопозии устройства", + "params": [ + "presence.geoTimestamp()" + ] + }, + { + "name": "currentTime", + "descr": "Получить время получения геопозиции от устройства", + "params": [ + "presence.currentTime()" + ] + }, + { + "name": "currentTimestamp", + "descr": "Получить UnixTime получения геопозиции от устройства", + "params": [ + "presence.currentTimestamp()" + ] + }, + { + "name": "nextScheduledTime", + "descr": "Получить время следующего получения геопозиции от устройства", + "params": [ + "presence.nextScheduledTime()" + ] + }, + { + "name": "nextScheduledTimestamp", + "descr": "Получить UnixTime следующего получения геопозиции от устройства", + "params": [ + "presence.nextScheduledTimestamp()" + ] + }, + { + "name": "nextAlarmclockTime", + "descr": "Получить время следующего будильника на устройтве", + "params": [ + "presence.nextAlarmclockTime()" + ] + }, + { + "name": "nextAlarmclockTimestamp", + "descr": "Получить UnixTime следующего будильника на устройтве", + "params": [ + "presence.nextAlarmclockTimestamp()" + ] + }, + { + "name": "chargingState", + "descr": "Получить статус зарядки устройства", + "params": [ + "presence.chargingState()" + ] + }, + { + "name": "plugState", + "descr": "Получить тип зарядки устройства", + "params": [ + "presence.plugState()" + ] + }, + { + "name": "connectedWifi", + "descr": "Получить Wi-Fi точку доступа к которой подключено устройство", + "params": [ + "presence.connectedWifi()" + ] + }, + { + "name": "deviceName", + "descr": "Получить имя устройства", + "params": [ + "presence.deviceName()" + ] + }, + { + "name": "geoLocation", + "descr": "Получить строку с широтой, долготой и верменем определения геопозиции", + "params": [ + "presence.geoLocation()" + ] + }, + { + "name": "conditionContent", + "descr": "Получить дополнительное условие отправки данных (геопозиции и др)", + "params": [ + "presence.conditionContent()" + ] + } + ] + }, + "defActive": false, + "usedLibs": { + "esp32*": [], + "esp82*": [], + "bk72*": [] + } +} \ No newline at end of file diff --git a/src/modules/sensors/Presence/preview.png b/src/modules/sensors/Presence/preview.png new file mode 100644 index 00000000..47ab8f1d Binary files /dev/null and b/src/modules/sensors/Presence/preview.png differ diff --git a/src/modules/sensors/Presence/widgets.json b/src/modules/sensors/Presence/widgets.json new file mode 100644 index 00000000..ad207f8e --- /dev/null +++ b/src/modules/sensors/Presence/widgets.json @@ -0,0 +1,383 @@ +[ + { + "name": "anydataRed", + "label": "Сообщение1", + "widget": "anydata", + "icon": "body", + "color": "red", + "descrColor": "red" + }, + { + "name": "anydataDgr", + "label": "Сообщение2", + "widget": "anydata", + "after": "", + "color": "red", + "icon": "walk" + }, + { + "name": "anydataDef", + "label": "Текст", + "widget": "anydata", + "after": "", + "icon": "" + }, + { + "name": "anydataVlt", + "label": "Вольты", + "widget": "anydata", + "after": "V", + "icon": "speedometer" + }, + { + "name": "anydataAmp", + "label": "Амперы", + "widget": "anydata", + "after": "A", + "icon": "speedometer" + }, + { + "name": "anydataWt", + "label": "Ватты", + "widget": "anydata", + "after": "Wt", + "icon": "speedometer", + "color": [ + { + "level": 0, + "value": "" + }, + { + "level": 200, + "value": "#009933" + }, + { + "level": 2000, + "value": "#FF9900" + }, + { + "level": 4000, + "value": "red" + } + ] + }, + { + "name": "anydataWth", + "label": "Энергия", + "widget": "anydata", + "after": "kWh", + "icon": "speedometer" + }, + { + "name": "anydataHtz", + "label": "Герцы", + "widget": "anydata", + "after": "Hz", + "icon": "speedometer" + }, + { + "name": "anydataTmp", + "label": "Температура", + "widget": "anydata", + "after": "°С", + "icon": "thermometer", + "font": "OCR A Std", + "color": [ + { + "level": -20, + "value": "#0000CC" + }, + { + "level": -10, + "value": "#0000CC" + }, + { + "level": 0, + "value": "#0000CC" + }, + { + "level": 12, + "value": "#3366FF" + }, + { + "level": 16, + "value": "#33CCFF" + }, + { + "level": 18, + "value": "#009933" + }, + { + "level": 30, + "value": "#FF9900" + }, + { + "level": 40, + "value": "red" + } + ] + }, + { + "name": "anydataMm", + "label": "Давление", + "widget": "anydata", + "after": "mm", + "icon": "speedometer" + }, + { + "name": "anydataHum", + "label": "Влажность", + "widget": "anydata", + "after": "%", + "icon": "water", + "color": "#88AADF" + }, + { + "name": "anydataTm", + "label": "Время", + "widget": "anydata", + "after": "", + "icon": "speedometer" + }, + { + "name": "button", + "label": "Кнопка", + "widget": "btn", + "size": "large", + "color": "green", + "send": "test" + }, + { + "name": "toggle", + "label": "Переключатель", + "widget": "toggle", + "icon": "", + "iconOff": "" + }, + { + "name": "chart1", + "label": "График без точек", + "widget": "chart", + "dateFormat": "HH:mm", + "maxCount": 86400, + "pointRadius": 0 + }, + { + "name": "chart2", + "label": "График с точками", + "widget": "chart", + "maxCount": 86400, + "dateFormat": "HH:mm" + }, + { + "name": "chart3", + "label": "График Дневной", + "widget": "chart", + "dateFormat": "DD.MM.YYYY", + "maxCount": 86400, + "type": "bar" + }, + { + "name": "chart4", + "label": "График Часовой", + "widget": "chart", + "dateFormat": "HH:mm", + "maxCount": 3600, + "type": "bar" + }, + { + "name": "chart5", + "label": "График двойной", + "widget": "chart", + "series": [ + "Температура, С", + "Влажность, %" + ], + "dateFormat": "HH:mm", + "maxCount": 86400, + "pointRadius": 0 + }, + { + "name": "chart6", + "label": "График тройной", + "widget": "chart", + "series": [ + "Температура, С", + "Влажность, %", + "Давление, кПа" + ], + "dateFormat": "HH:mm", + "maxCount": 86400, + "pointRadius": 0 + }, + { + "name": "fillgauge", + "label": "Бочка", + "widget": "fillgauge", + "circleColor": "#00FFFF", + "textColor": "#FFFFFF", + "waveTextColor": "#000000", + "waveColor": "#00FFFF" + }, + { + "name": "inputDate", + "label": "Ввод даты", + "widget": "input", + "size": "small", + "color": "orange", + "type": "date" + }, + { + "name": "inputDgt", + "label": "Ввод числа", + "widget": "input", + "color": "blue", + "type": "number" + }, + { + "name": "inputTxt", + "label": "Ввод текста", + "widget": "input", + "size": "small", + "color": "orange", + "type": "text" + }, + { + "name": "inputTm", + "label": "Ввод времени", + "widget": "input", + "color": "blue", + "type": "time" + }, + { + "name": "progressLine", + "label": "Статус линия", + "widget": "progress-line", + "icon": "sunny", + "max": "100", + "stroke": "10" + }, + { + "name": "progressRound", + "label": "Статус круг", + "widget": "progress-round", + "max": "100", + "stroke": "20", + "color": "#45ccce", + "background": "#777", + "semicircle": "1" + }, + { + "name": "range", + "label": "Ползунок", + "widget": "range", + "descrColor": "red", + "after": "%", + "k": 0.0977, + "min": 0, + "max": 100, + "debounce": 500 + }, + { + "name": "rangeServo", + "label": "Ползунок (Servo)", + "widget": "range", + "descrColor": "red", + "after": "°", + "k": 1, + "min": 0, + "max": 180, + "debounce": 500 + }, + { + "name": "select", + "label": "Выпадающий", + "widget": "select", + "options": [ + "Выключен", + "Включен" + ], + "status": 0 + }, + { + "name": "anydataPpm", + "label": "PPM", + "widget": "anydata", + "after": "ppm", + "icon": "speedometer" + }, + { + "name": "anydatamAmp", + "label": "миллиАмперы", + "widget": "anydata", + "after": "mAmp", + "icon": "speedometer" + }, + { + "name": "anydatamVlt", + "label": "миллиВольты", + "widget": "anydata", + "after": "mVlt", + "icon": "speedometer" + }, + { + "name": "anydatamWt", + "label": "миллиВатты", + "widget": "anydata", + "after": "mWt", + "icon": "speedometer" + }, + { + "name": "anydataKm", + "label": "Километры", + "widget": "anydata", + "after": "km", + "icon": "speedometer" + }, + { + "name": "anydataM", + "label": "Метры", + "widget": "anydata", + "after": "m", + "icon": "speedometer" + }, + { + "name": "anydataCm", + "label": "Сантиметры", + "widget": "anydata", + "after": "cm", + "icon": "speedometer" + }, + { + "name": "anydataLiter", + "label": "Литры", + "widget": "anydata", + "after": "ltr", + "icon": "speedometer" + }, + { + "name": "anydataSpeed", + "label": "мерты в секунду", + "widget": "anydata", + "after": "m/s", + "icon": "speedometer" + }, + { + "name": "anydataСorner", + "label": "угол градусов", + "widget": "anydata", + "after": "°", + "icon": "speedometer" + }, + { + "name": "anydataBar", + "label": "давление Bar", + "widget": "anydata", + "after": "Kg/cm²", + "icon": "speedometer" + }, + { + "name": "nil", + "label": "Без виджета" + } +] \ No newline at end of file diff --git a/src/modules/sensors/SoftRTC/SoftRTC.cpp b/src/modules/sensors/SoftRTC/SoftRTC.cpp new file mode 100644 index 00000000..2f1dbfba --- /dev/null +++ b/src/modules/sensors/SoftRTC/SoftRTC.cpp @@ -0,0 +1,718 @@ +// Licensed under the Cooperative Non-Violent Public License (CNPL) +// See: https://github.com/CHE77/IoTManager-Modules/blob/main/LICENSE + +#include "Global.h" +#include "classes/IoTItem.h" +#include "NTP.h" +#include "WsServer.h" + +// Подключаем нужные заголовочные файлы в зависимости от платформы +#ifdef ESP8266 +#include +#include // Для settimeofday_cb() в ESP8266 +#else +#include // Для ESP32 +#endif + +typedef enum +{ + SoftRTC_SYNC_STATUS_NOT_SET = 0, // Время не установлено + // SoftRTC_SYNC_STATUS_COULD_NOT_SET, // Время не может быть установлено + SoftRTC_SYNC_STATUS_BEFORE_SoftRTC, // Время было установлено до SoftRTC + SoftRTC_SYNC_STATUS_RESTORED, // Время восстановлено из памяти + SoftRTC_SYNC_STATUS_MANUAL, // Время установлено вручную + SoftRTC_SYNC_STATUS_FROM_BROWSER_OR_NTP, + SoftRTC_SYNC_STATUS_FROM_BROWSER, // Время (скоере всего) установлено с Браузера + SoftRTC_SYNC_STATUS_NTP_JUST, // Время только что синхронизировано через NTP + SoftRTC_SYNC_STATUS_NTP // Время синхронизировано через NTP + +} SoftRTC_sync_status_t; + +// Переменная для отслеживания источника времени +volatile int syncStatus = SoftRTC_SYNC_STATUS_NOT_SET; + +// Функция-коллбэк для NTP (разные сигнатуры для разных платформ) +#ifdef ESP8266 +/* +void timeSyncCallback() +{ + syncStatus = SoftRTC_SYNC_STATUS_NTP_JUST; +} +*/ +#else +void timeSyncCallback(struct timeval *tv) +{ + (void)tv; // Неиспользуемый параметр + syncStatus = SoftRTC_SYNC_STATUS_NTP_JUST; +} +#endif + +time_t getUnixTimeFromYMDHMS(int year, int month, int day, int hour, int minute, int second) +{ + struct tm t; + t.tm_year = year - 1900; + t.tm_mon = month - 1; + t.tm_mday = day; + t.tm_hour = hour; + t.tm_min = minute; + t.tm_sec = second; + t.tm_isdst = -1; + return mktime(&t); +} + +time_t getUnixTimeFromString(String datetime) +{ + struct tm t = {0}; + int d, m, y, h, min, s; + + const char *dt = datetime.c_str(); + + if (sscanf(dt, "%d%*c%d%*c%d %d:%d:%d", &d, &m, &y, &h, &min, &s) == 6) + { + if (y < 100) + y += (y >= 69) ? 1900 : 2000; + if (d > 31) + { + int temp = d; + d = y; + y = temp; + } + } + else + { + return -1; + } + + t.tm_mday = d; + t.tm_mon = m - 1; + t.tm_year = y - 1900; + t.tm_hour = h; + t.tm_min = min; + t.tm_sec = s; + t.tm_isdst = -1; + + return mktime(&t); +} + +String formatUnixTime(time_t unixTime) +{ + struct tm *timeInfo; + timeInfo = localtime(&unixTime); + char formattedTime[32]; + // char formattedTime[20]; + int year = timeInfo->tm_year + 1900; + int shortYear = year % 100; + + snprintf(formattedTime, sizeof(formattedTime), "%02d.%02d.%02d %02d:%02d:%02d", + timeInfo->tm_mday, timeInfo->tm_mon + 1, shortYear, + timeInfo->tm_hour, timeInfo->tm_min, timeInfo->tm_sec); + + return String(formattedTime); +} + +bool isDSTfunc(time_t utc) +{ + struct tm *timeinfo = gmtime(&utc); // UTC time → broken-down time + int year = timeinfo->tm_year + 1900; + + // Найдём последнюю дату воскресенья марта и октября + struct tm lastMarchSunday = {0}; + lastMarchSunday.tm_year = year - 1900; + lastMarchSunday.tm_mon = 2; // Март (0-based) + lastMarchSunday.tm_mday = 31; + lastMarchSunday.tm_hour = 3; // Переход в 03:00 UTC + + mktime(&lastMarchSunday); // нормализация даты + + // Ищем последнее воскресенье марта + while (lastMarchSunday.tm_wday != 0) + { // 0 = воскресенье + lastMarchSunday.tm_mday--; + mktime(&lastMarchSunday); + } + + time_t dstStart = mktime(&lastMarchSunday); // начало летнего времени + + struct tm lastOctoberSunday = {0}; + lastOctoberSunday.tm_year = year - 1900; + lastOctoberSunday.tm_mon = 9; // Октябрь + lastOctoberSunday.tm_mday = 31; + lastOctoberSunday.tm_hour = 4; // Переход в 04:00 UTC + + mktime(&lastOctoberSunday); + + while (lastOctoberSunday.tm_wday != 0) + { + lastOctoberSunday.tm_mday--; + mktime(&lastOctoberSunday); + } + + time_t dstEnd = mktime(&lastOctoberSunday); // конец летнего времени + + return utc >= dstStart && utc < dstEnd; +} + +int getWeekOfYear(time_t unixTime) +{ + struct tm *timeInfo = localtime(&unixTime); + char buffer[3]; // 2 цифры + \0 + strftime(buffer, sizeof(buffer), "%W", timeInfo); // %W — номер недели (понедельник — первый день недели) + return atoi(buffer); +} + +int getDayOfWeekNumber(time_t unixTime) +{ + struct tm *timeInfo = localtime(&unixTime); + int wday = timeInfo->tm_wday; + return (wday == 0) ? 7 : wday; // воскресенье (0) → 7 +} + +int getDaysInMonth(time_t unixTime) +{ + struct tm t = *localtime(&unixTime); + + t.tm_mday = 1; // Первый день текущего месяца + t.tm_mon += 1; // Следующий месяц + t.tm_hour = 0; t.tm_min = 0; t.tm_sec = 0; + + if (t.tm_mon > 11) { // Декабрь → январь + t.tm_mon = 0; + t.tm_year += 1; + } + + time_t firstOfNextMonth = mktime(&t); + t.tm_mon -= 1; // Возврат к текущему + t.tm_mday = 1; + time_t firstOfThisMonth = mktime(&t); + + int days = (firstOfNextMonth - firstOfThisMonth) / (60 * 60 * 24); + return days; +} + +// Возвращает день и месяц Пасхи в Григорианском календаре +void getOrthodoxEasterDate(int year, int &day, int &month) +{ + // Meeus (Julian) algorithm + int a = year % 4; + int b = year % 7; + int c = year % 19; + int d = (19 * c + 15) % 30; + int e = (2 * a + 4 * b - d + 34) % 7; + + int julianMonth = (d + e + 114) / 31; // 3 = март, 4 = апрель + int julianDay = ((d + e + 114) % 31) + 1; + + // Переходим из юлианского в григоріанский: + // * для 1900‑2099 разница = 13 дней + struct tm t {}; + t.tm_year = year - 1900; + t.tm_mon = julianMonth - 1; + t.tm_mday = julianDay + 13; // 13‑дневная разница + mktime(&t); // нормализует переход между месяцами + + day = t.tm_mday; + month = t.tm_mon + 1; +} + + +class SoftRTC : public IoTItem +{ +private: + bool _ticker = false; + bool _debug = false; + long interval = 60; + int _winterTime = 2; + int _summerTime = 3; + time_t recovered_unixTime = 0; + time_t lastNTPtimeCorrection = 0; + time_t lastUnixTimeMillis = 0; + time_t lastUnixTime = 0; + // time_t _timeZoneSeconds; + int _timezone = 0; + int lastSyncStatus = 0; + time_t before = 0; + unsigned long startMillis = 0; // Сохраняем текущее время в миллисекундах + +public: + SoftRTC(String parameters) : IoTItem(parameters) + { + _timezone = jsonReadInt(settingsFlashJson, F("timezone")); +// Устанавливаем коллбэк в зависимости от платформы +#ifdef ESP8266 + // settimeofday_cb(timeSyncCallback); +#else + sntp_set_time_sync_notification_cb(timeSyncCallback); +#endif + + _needSave = true; + _round = 0; + jsonRead(parameters, F("ticker"), _ticker); + jsonRead(parameters, F("int"), interval); + jsonRead(parameters, F("winterTime"), _winterTime); + jsonRead(parameters, F("summerTime"), _summerTime); + jsonRead(parameters, F("debug"), _debug); + + before = time(nullptr); + startMillis = millis(); // Сохраняем текущее время в миллисекундах + } + + void loop() + { + +#if defined(ESP32) + if (syncStatus == SoftRTC_SYNC_STATUS_NTP_JUST) + { + time_t ut = getSystemTime(); + String localDateTime = formatUnixTime(ut + _timezone * 60 * 60); + if (_debug) + SerialPrint("I", F("SoftRTC"), "✅ Время синхронизировано через NTP! " + (String)ut + " LT: " + localDateTime); + + lastNTPtimeCorrection = getSystemTime() - (lastUnixTime + (millis() - lastUnixTimeMillis) / 1000); + if (_debug) + SerialPrint("I", F("SoftRTC"), "lastNTPtimeCorrection: " + (String)lastNTPtimeCorrection + " сек."); + syncStatus = SoftRTC_SYNC_STATUS_NTP; + + IoTValue valTmp; + valTmp.isDecimal = false; + valTmp.valS = (String)ut; + regEvent(valTmp.valS, F("SoftRTC"), _debug, true); + } +#endif + + if (syncStatus < 4) // если вреямя обновлено из надежного источника то уже не остлеживаем его изменение + { + + time_t after = time(nullptr); + startMillis = millis() - startMillis; + + if ((abs(after - before) - int((startMillis + 500) / 1000)) > 2) // проверка на изменение вермени + { + int delta = abs(after - before - int((startMillis + 500) / 1000)); + + if (_debug){ + SerialPrint("I", F("SoftRTC"), "Замечено, что Время измененно!"); + SerialPrint("I", F("SoftRTC"), "syncStatus = " + String(syncStatus)); + SerialPrint("I", F("SoftRTC"), "lastSyncStatus = " + String(lastSyncStatus)); + } + + if (syncStatus == lastSyncStatus) + { + if (_debug) + SerialPrint("I", F("SoftRTC"), "Замечено, что Время измененно без изменения статуса"); + +#ifdef ESP8266 + syncStatus = SoftRTC_SYNC_STATUS_FROM_BROWSER_OR_NTP; + lastSyncStatus = SoftRTC_SYNC_STATUS_FROM_BROWSER_OR_NTP; + + time_t ut = getSystemTime(); + String localDateTime = formatUnixTime(ut + _timezone * 60 * 60); + if (_debug) + SerialPrint("I", F("SoftRTC"), "✅ Время синхронизировано через NTP или Browser! " + (String)ut + " LT: " + localDateTime); + + lastNTPtimeCorrection = getSystemTime() - (lastUnixTime + (millis() - lastUnixTimeMillis) / 1000); + if (_debug) + SerialPrint("I", F("SoftRTC"), "lastNTPtimeCorrection: " + (String)lastNTPtimeCorrection + " сек."); + +#else + syncStatus = SoftRTC_SYNC_STATUS_FROM_BROWSER; + lastSyncStatus = SoftRTC_SYNC_STATUS_FROM_BROWSER; +#endif + } + else + { + lastSyncStatus = syncStatus; + if (_debug) + SerialPrint("I", F("SoftRTC"), "Замечено, что Время измененно c изменением статуса"); + } + } + before = time(nullptr); + startMillis = millis(); + } + + IoTItem::loop(); + } + + void doByInterval() + { + time_t unixTime = 0; + + if (syncStatus == SoftRTC_SYNC_STATUS_RESTORED) + { + unixTime = getSystemTime(); + if (_debug) + SerialPrint("I", F("SoftRTC"), "Время восстановлено из памяти: " + (String)unixTime); + if (isNetworkActive()) + { + if (_debug) + SerialPrint("I", F("SoftRTC"), "Пробуем синхронизировать через NTP"); + synchTime(); + } + } + else if (syncStatus == SoftRTC_SYNC_STATUS_MANUAL) + { + unixTime = getSystemTime(); + if (_debug) + SerialPrint("I", F("SoftRTC"), "Время установлено вручную: " + (String)unixTime); + if (isNetworkActive()) + { + if (_debug) + SerialPrint("I", F("SoftRTC"), "Пробуем синхронизировать через NTP"); + synchTime(); + } + } +#ifdef ESP8266 + else if (syncStatus == SoftRTC_SYNC_STATUS_FROM_BROWSER_OR_NTP) + { + unixTime = getSystemTime(); + if (_debug) + SerialPrint("I", F("SoftRTC"), "Время синхронизировано через WEB или NTP: " + (String)unixTime); + } +#else + else if (syncStatus == SoftRTC_SYNC_STATUS_FROM_BROWSER) + { + unixTime = getSystemTime(); + if (_debug) + SerialPrint("I", F("SoftRTC"), "Время синхронизировано через Браузер: " + (String)unixTime); + } + else if (syncStatus == SoftRTC_SYNC_STATUS_NTP || syncStatus == SoftRTC_SYNC_STATUS_NTP_JUST) + { + unixTime = getSystemTime(); + if (_debug) + SerialPrint("I", F("SoftRTC"), "Время синхронизировано через NTP: " + (String)unixTime); + } +#endif + + else if (time(nullptr) > 100000) + { + syncStatus = SoftRTC_SYNC_STATUS_BEFORE_SoftRTC; + unixTime = getSystemTime(); + if (_debug) + SerialPrint("I", F("SoftRTC"), "Время синхронизировано через NTP до загрузки SoftRTC: " + (String)unixTime); + } + else if (syncStatus == SoftRTC_SYNC_STATUS_NOT_SET) + { + valuesFlashJson = readFile(F("values.json"), 4096); + valuesFlashJson.replace("\r\n", ""); + String valAsStr = ""; + if (jsonRead(valuesFlashJson, _id, valAsStr, _debug)) + { + recovered_unixTime = valAsStr.toInt(); + if (_debug) + SerialPrint("I", F("SoftRTC"), "Время из энергонезависимой памяти: " + (String)recovered_unixTime); + + time_t nextSecond = millis() / 1000 + 1; + while (millis() / 1000 < nextSecond) + { + } + + unixTime = recovered_unixTime + millis() / 1000 + interval / 2; + + struct timeval now = {.tv_sec = unixTime, .tv_usec = 0}; + settimeofday(&now, NULL); + + syncStatus = SoftRTC_SYNC_STATUS_RESTORED; + + String localDateTime = formatUnixTime(unixTime + _timezone * 60 * 60); + SerialPrint("I", F("SoftRTC"), "Установлено восстановленное время: " + (String)unixTime + " LT: " + localDateTime); + } + else + { + // syncStatus = SoftRTC_SYNC_STATUS_COULD_NOT_SET; + if (_debug) + SerialPrint("I", F("SoftRTC"), "Время не может быть восстановлено из памяти"); + } + } + else + { + if (_debug) + SerialPrint("I", F("SoftRTC"), "Время не установлено"); + } + + if (unixTime) + { + lastUnixTime = unixTime; + lastUnixTimeMillis = millis(); + if (_debug) + SerialPrint("I", F("SoftRTC"), "Сохраняем время: " + (String)unixTime); + value.isDecimal = false; + value.valS = (String)unixTime; + regEvent(value.valS, F("SoftRTC"), _debug, _ticker); + } + } + + void onModuleOrder(String &key, String &value) + { + if (key == "setUTime") + { + if (_debug) + SerialPrint("i", F("SoftRTC"), "Устанавливаем время: " + value); + char *stopstring; + time_t ut = strtoul(value.c_str(), &stopstring, 10); + + struct timeval now = {.tv_sec = ut, .tv_usec = 0}; + settimeofday(&now, NULL); + + syncStatus = SoftRTC_SYNC_STATUS_MANUAL; + + String localDateTime = formatUnixTime(getSystemTime() + _timezone * 60 * 60); + if (_debug) + SerialPrint("I", F("SoftRTC"), "LT: " + localDateTime); + + IoTValue valTmp; + valTmp.isDecimal = false; + valTmp.valS = (String)ut; + regEvent(valTmp.valS, F("SoftRTC"), _debug, _ticker); + } + else if (key == "setSysTime") + { + if (_debug) + SerialPrint("i", F("SoftRTC"), "Устанавливаем системное время: " + value); + + time_t ut = getUnixTimeFromString(value) - _timezone * 60 * 60; + + struct timeval now = {.tv_sec = ut, .tv_usec = 0}; + settimeofday(&now, NULL); + + String localDateTime = formatUnixTime(getSystemTime() + _timezone * 60 * 60); + if (_debug) + SerialPrint("I", F("SoftRTC"), (String)ut + " LT: " + localDateTime); + + syncStatus = SoftRTC_SYNC_STATUS_MANUAL; + + IoTValue valTmp; + valTmp.isDecimal = false; + valTmp.valS = (String)ut; + regEvent(valTmp.valS, F("SoftRTC"), _debug, _ticker); + } + } + + IoTValue execute(String command, std::vector ¶m) + { + if (command == "checkForSummer") + { + unixTime = getSystemTime(); + bool isDST = isDSTfunc(unixTime); + + int newTimeZone = isDST ? _summerTime : _winterTime; + + if (newTimeZone != _timezone) + { + jsonWriteInt_(settingsFlashJson, F("timezone"), isDST ? _summerTime : _winterTime); + syncSettingsFlashJson(); + _timezone = newTimeZone; + + if (newTimeZone == _summerTime) + { + SerialPrint("i", F("SoftRTC"), "Перешли на летнее время"); + } + else + { + SerialPrint("i", F("SoftRTC"), "Перешли на зименее время"); + } + } + + return {}; + } + else if (command == "getTime") + { + String localDateTime = formatUnixTime(getSystemTime() + _timezone * 60 * 60); + + IoTValue valTmp; + valTmp.isDecimal = false; + valTmp.valS = localDateTime; + return valTmp; + } + else if (command == "getUTShort") + { + IoTValue valTmp; + valTmp.isDecimal = true; + unixTimeShort = getSystemTime() - START_DATETIME; + valTmp.valD = unixTimeShort; + return valTmp; + } + else if (command == "setUnixTime") + { + if (param.size() == 1) + { + time_t ut = strtoul(param[0].valS.c_str(), nullptr, 10); + if (_debug) + SerialPrint("i", F("SoftRTC"), "Устанавливаем время UT: " + (String)ut); + + struct timeval now = {.tv_sec = ut, .tv_usec = 0}; + settimeofday(&now, NULL); + + String localDateTime = formatUnixTime(ut + _timezone * 60 * 60); + if (_debug) + SerialPrint("I", F("SoftRTC"), "LT: " + localDateTime); + syncStatus = SoftRTC_SYNC_STATUS_MANUAL; + + IoTValue valTmp; + valTmp.isDecimal = false; + valTmp.valS = (String)ut; + regEvent(valTmp.valS, F("SoftRTC"), _debug, _ticker); + + return {}; + } + } + else if (command == "setTimeFromYMDHMS") + { + if (param.size() == 6) + { + time_t ut = getUnixTimeFromYMDHMS(param[0].valD, param[1].valD, param[2].valD, + param[3].valD, param[4].valD, param[5].valD); + if (_debug) + SerialPrint("i", F("SoftRTC"), "Устанавливаем время YMDHMS: " + (String)ut); + + struct timeval now = {.tv_sec = ut, .tv_usec = 0}; + settimeofday(&now, NULL); + + String localDateTime = formatUnixTime(ut + _timezone * 60 * 60); + if (_debug) + SerialPrint("I", F("SoftRTC"), "LT: " + localDateTime); + syncStatus = SoftRTC_SYNC_STATUS_MANUAL; + + IoTValue valTmp; + valTmp.isDecimal = false; + valTmp.valS = (String)ut; + regEvent(valTmp.valS, F("SoftRTC"), _debug, _ticker); + + return {}; + } + } + else if (command == "lastNTPtimeCorrection") + { + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = lastNTPtimeCorrection; + return valTmp; + } + else if (command == "getWeekNumber") + { + time_t ut = getSystemTime() + _timezone * 60 * 60; // учитываем локальное смещение + int week = getWeekOfYear(ut); + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = week; + return valTmp; + } + else if (command == "getDayOfWeek") + { + time_t ut = getSystemTime() + _timezone * 60 * 60; // учёт смещения + int dayNum = getDayOfWeekNumber(ut); + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = dayNum; + return valTmp; + } + else if (command == "getYear") + { + time_t ut = getSystemTime() + _timezone * 60 * 60; // локальное смещение + struct tm *timeInfo = localtime(&ut); + int year = timeInfo->tm_year + 1900; // полный (4‑значный) год + IoTValue valTmp; + valTmp.isDecimal = true; // возвращаем число + valTmp.valD = year; + return valTmp; + } + else if (command == "getDayOfYear") + { + time_t ut = getSystemTime() + _timezone * 60 * 60; + struct tm *timeInfo = localtime(&ut); + int dayOfYear = timeInfo->tm_yday + 1; // tm_yday начинается с 0 + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = dayOfYear; + return valTmp; + } + else if (command == "getDaysInMonth") + { + time_t ut = getSystemTime() + _timezone * 60 * 60; + int days = getDaysInMonth(ut); + + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = days; + return valTmp; + } + else if (command == "getOrthodoxEaster") + { + time_t ut = getSystemTime() + _timezone * 60 * 60; + struct tm *t = localtime(&ut); + int day, month; + getOrthodoxEasterDate(t->tm_year + 1900, day, month); + + char buf[12]; + snprintf(buf, sizeof(buf), "%02d.%02d.%d", day, month, t->tm_year + 1900); + + IoTValue valTmp; + valTmp.isDecimal = false; + valTmp.valS = String(buf); + return valTmp; + } + else if (command == "isOrthodoxEaster") + { + time_t ut = getSystemTime() + _timezone * 60 * 60; + struct tm *now = localtime(&ut); + + int dEaster, mEaster; + getOrthodoxEasterDate(now->tm_year + 1900, dEaster, mEaster); + + bool todayIsEaster = (now->tm_mday == dEaster) && (now->tm_mon + 1 == mEaster); + + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = todayIsEaster ? 1 : 0; + return valTmp; + } + + + return {}; + } + + ~SoftRTC() {} +}; + +class SoftRTCsyncStatus : public IoTItem +{ +private: + int lastSyncStatus = -1; + bool _debug = false; + bool _ticker = true; + +public: + SoftRTCsyncStatus(String parameters) : IoTItem(parameters) + { + _round = 0; + jsonRead(parameters, F("ticker"), _ticker); + jsonRead(parameters, F("debug"), _debug); + } + + void loop() + { + if (syncStatus != lastSyncStatus) + { + lastSyncStatus = syncStatus; + if (_debug) + SerialPrint("i", F("SoftRTC"), "Статус синхронизации: " + (String)lastSyncStatus); + value.isDecimal = true; + value.valD = lastSyncStatus; + regEvent(value.valD, F("SoftRTCsyncStatus"), _debug, _ticker); + } + IoTItem::loop(); + } + + ~SoftRTCsyncStatus() {} +}; + +void *getAPI_SoftRTC(String subtype, String param) +{ + if (subtype == F("SoftRTC")) + { + return new SoftRTC(param); + } + else if (subtype == F("SoftRTCsyncStatus")) + { + return new SoftRTCsyncStatus(param); + } + return nullptr; +} \ No newline at end of file diff --git a/src/modules/sensors/SoftRTC/export .json b/src/modules/sensors/SoftRTC/export .json new file mode 100644 index 00000000..1bb544be --- /dev/null +++ b/src/modules/sensors/SoftRTC/export .json @@ -0,0 +1,264 @@ +{ + "mark": "iotm", + "config": [ + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutTime", + "needSave": 0, + "widget": "anydataDef", + "page": "Time", + "descr": "Local Time", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Writing", + "subtype": "Timer", + "id": "timer1", + "widget": "anydataDef", + "page": "Timers", + "descr": "Timer", + "int": 1, + "countDown": "15", + "ticker": "0", + "repeat": 1, + "needSave": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutNTPtimeCorrection", + "needSave": 0, + "widget": "anydataDef", + "page": "Time", + "descr": "Last NTP Time Correction", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutSyncStatus", + "needSave": 0, + "widget": "anydataDef", + "page": "Time", + "descr": "SoftRTC Sync Status", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "voutTimeSoft", + "needSave": 0, + "widget": "anydataDef", + "page": "Time", + "descr": "Local Time Soft", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "SoftRTCsyncStatus", + "id": "softRTCsyncStatus", + "widget": "anydataDef", + "page": "Time", + "descr": "SoftRTC Sync Status", + "ticker": "1", + "int": 10, + "round": 0, + "debug": 1 + }, + { + "global": 0, + "type": "Reading", + "subtype": "SoftRTC", + "id": "softRTC", + "widget": "anydataDef", + "page": "Timers", + "descr": "UT softRTC", + "winterTime": 2, + "summerTime": 3, + "ticker": 0, + "int": 10, + "debug": 1, + "btn-setUTime": "0", + "btn-setSysTime": "d-m-y h:m:s", + "moduleName": "SoftRTC" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "dayOfWeek", + "needSave": 0, + "widget": "anydataDef", + "page": "Days", + "descr": "Day of Week", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "weekNumber", + "needSave": 0, + "widget": "anydataDef", + "page": "Days", + "descr": " Week Number", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "dayOfWeekT", + "needSave": 0, + "widget": "anydataDef", + "page": "Days", + "descr": "Day of Week", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "getDayOfYear", + "needSave": 0, + "widget": "anydataDef", + "page": "Days", + "descr": "Day of Year", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "getDaysInMonth", + "needSave": 0, + "widget": "anydataDef", + "page": "Days", + "descr": "Days in Month", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "getOrthodoxEaster", + "needSave": 0, + "widget": "anydataDef", + "page": "Days", + "descr": "Orthodox Easter", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "isOrthodoxEasterB", + "needSave": 0, + "widget": "toggle", + "page": "Days", + "descr": "is Orthodox Easter", + "int": "0", + "val": "0", + "moduleName": "VButton" + } + ] +} + +scenario=> + + +if timer1 == 0 then { +voutTime = getTime();# не сработает если время не обновлено +voutTimeSoft = softRTC.getTime(); +dayOfWeek = softRTC.getDayOfWeek() +voutNTPtimeCorrection = softRTC.lastNTPtimeCorrection() +weekNumber = softRTC.getWeekNumber() +getDayOfYear = softRTC.getDayOfYear() +getDaysInMonth = softRTC.getDaysInMonth() +getOrthodoxEaster = softRTC.getOrthodoxEaster() +isOrthodoxEasterB = softRTC.isOrthodoxEaster() +} + +if dayOfWeek == 1 then dayOfWeekT = "Monday" +if dayOfWeek == 2 then dayOfWeekT = "Tuesday" +if dayOfWeek == 3 then dayOfWeekT = "Wednesday" +if dayOfWeek == 4 then dayOfWeekT = "Thursday" +if dayOfWeek == 5 then dayOfWeekT = "Friday" +if dayOfWeek == 6 then dayOfWeekT = "Saturday" +if dayOfWeek == 7 then dayOfWeekT = "Sunday" + +if softRTCsyncStatus == 0 then voutSyncStatus = "Not set"; +if softRTCsyncStatus == 1 then voutSyncStatus = "Set before SoftRTC"; +if softRTCsyncStatus == 2 then voutSyncStatus = "Restored"; +if softRTCsyncStatus == 3 then voutSyncStatus = "Set manually"; +if softRTCsyncStatus == 4 then voutSyncStatus = "Set with NTP or Browser"; +if softRTCsyncStatus == 5 then voutSyncStatus = "Set with Browser"; +if softRTCsyncStatus == 6 then voutSyncStatus = "Just set with NTP"; +if softRTCsyncStatus == 7 then voutSyncStatus = "Set with NTP"; + +if softRTC then {voutNTPtimeCorrection = softRTC.lastNTPtimeCorrection() +} + diff --git a/src/modules/sensors/SoftRTC/modinfo.json b/src/modules/sensors/SoftRTC/modinfo.json new file mode 100644 index 00000000..b0e81c6c --- /dev/null +++ b/src/modules/sensors/SoftRTC/modinfo.json @@ -0,0 +1,161 @@ +{ + "menuSection": "sensors", + "configItem": [ + { + "global": 0, + "name": "Часы реального времени (программные)", + "type": "Reading", + "subtype": "SoftRTC", + "id": "softRTC", + "widget": "anydataDef", + "page": "Таймеры", + "descr": "UT softRTC", + "winterTime": 2, + "summerTime": 3, + "ticker": 0, + "int": 10, + "debug": 1, + "btn-setUTime": "0", + "btn-setSysTime": "d-m-y h:m:s" + }, + { + "global": 0, + "name": "Часы реального времени (программные) Статус", + "type": "Reading", + "subtype": "SoftRTCsyncStatus", + "id": "softRTCsyncStatus", + "widget": "anydataDef", + "page": "Таймеры", + "descr": "softRTC Sync Status", + "ticker": 1, + "debug": 1 + } + ], + "about": { + "authorName": "Alex K", + "authorContact": "https://t.me/cmche", + "authorGit": "https://github.com/CHE77/IoTManager-Modules", + "exampleURL": "https://iotmanager.org/wiki", + "specialThanks": "", + "moduleName": "SoftRTC", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Часы реального времени программные", + "moduleDesc": "Позволяет хранить время в энергонезависимой памяти.", + "license": "Cooperative Non-Violent Public License (CNPL)", + "propInfo": { + "ticker": "Генерировать(1) или нет(0) события при каждом тике часов (каждые int секунд).", + "debug": "Вывод отладочной информации", + "int": "Количество секунд между сохраниеями времени. Желательно чётное.", + "btn-setUTime": "Кнопка установки времени на основе указанного unixtime", + "btn-setSysTime": "Кнопка установки времени на основе указанного в формате d-m-y h:m:s или y-m-d h:m:s" + }, + "retInfo": "Содержит сохраненное Время в энергонезависимую память", + "funcInfo": [ + { + "name": "getTime", + "descr": "Получить строковое значение времени по указанному формату.", + "params": [ + "SoftRTC.getTime()" + ] + }, + { + "name": "getUTShort", + "descr": "Получить строковое значение времени по указанному формату.", + "params": [ + "SoftRTC.getUTShort()" + ] + }, + { + "name": "setUnixTime", + "descr": "Установить время через сценарий в формате юникстайм", + "params": [ + "SoftRTC.setUnixTime(1743087041) - параметр в виде числа или строки" + ] + }, + { + "name": "setTimeFromYMDHMS", + "descr": "Установить время через сценарий в формате YYYY,MM,DD,HH,MM,SS", + "params": [ + "softRTC.setTimeFromYMDHMS(2025,01,01,12,30,30) " + ] + }, + { + "name": "lastNTPtimeCorrection", + "descr": "Вывести поправку после синхронизации по NTP", + "params": [ + " softRTC.lastNTPtimeCorrection() " + ] + }, + { + "name": "getWeekNumber", + "descr": "Вывести номер недели в году", + "params": [ + " softRTC.getWeekNumber() " + ] + }, + { + "name": "getDayOfWeek", + "descr": "Вывести день недели. Воскресенье (0) → 7", + "params": [ + " softRTC.getDayOfWeek() " + ] + }, + { + "name": "getYear", + "descr": "Вывести год", + "params": [ + " softRTC.getYear() " + ] + }, + { + "name": "getDayOfYear", + "descr": "Вывести день в году", + "params": [ + " softRTC.getDayOfYear() " + ] + }, + { + "name": "getOrthodoxEaster", + "descr": "Вывести дату Пасхи в текущем году", + "params": [ + " softRTC.getOrthodoxEaster() " + ] + }, + { + "name": "isOrthodoxEaster", + "descr": "уже Пасха?", + "params": [ + " softRTC.isOrthodoxEaster() " + ] + } + ] + , + "title2": "Часы реального времени программные Статус", + "moduleDesc2": "Позволяет получать статус синхроницации времени", + "propInfo2": { + "ticker": "Генерировать(1) или нет(0) события при каждом изменении статуса.", + "debug": "Вывод отладочной информации" + }, + "retInfo2": "Содержит статус синхроницации времени", + "Status": { + "0": "Время не установлено", + "1": "Время было установлено до SoftRTC", + "2": "Время восстановлено из памяти", + "3": "Время установлено вручную", + "4": "Время синхронизировано через NTP или через Браузер", + "5": "Время синхронизировано через Браузер", + "6": "Время только что синхронизировано через NTP", + "7": "Время синхронизировано через NTP" + } + + }, + "defActive": false, + "usedLibs": { + "esp32*": [], + "esp82*": [] + } +} \ No newline at end of file diff --git a/src/modules/virtual/SolarCalc/SolarCalc.cpp b/src/modules/virtual/SolarCalc/SolarCalc.cpp new file mode 100644 index 00000000..bc0e7680 --- /dev/null +++ b/src/modules/virtual/SolarCalc/SolarCalc.cpp @@ -0,0 +1,488 @@ +// Licensed under the Cooperative Non-Violent Public License (CNPL) +// See: https://github.com/CHE77/IoTManager-Modules/blob/main/LICENSE + +#include "Global.h" +#include "classes/IoTItem.h" +// #include +#include +#include "NTP.h" + +class SolarCalculator : public IoTItem +{ +private: + float _lat = 0; + float _long = 0; + + int year = 0; + int month = 0; + int day = 0; + int hour = 0; + int minute = 0; + int second = 0; + + unsigned long _sunriseTime = 0; + unsigned long _transitTime = 0; + unsigned long _sunsetTime = 0; + + String _parameter = ""; + bool _ticker = false; + bool _debug = false; + + // Rounded HH:mm format + char *hoursToString(double h, char *str) + { + int m = int(round(h * 60)); + int hr = (m / 60) % 24; + int mn = m % 60; + + str[0] = (hr / 10) % 10 + '0'; + str[1] = (hr % 10) + '0'; + str[2] = ':'; + str[3] = (mn / 10) % 10 + '0'; + str[4] = (mn % 10) + '0'; + str[5] = '\0'; + return str; + } + +public: + SolarCalculator(String parameters) : IoTItem(parameters) + { + jsonRead(parameters, "lat", _lat); + jsonRead(parameters, "lon", _long); + _parameter = jsonReadStr(parameters, "parameter"); + jsonRead(parameters, F("ticker"), _ticker); + jsonRead(parameters, F("debug"), _debug); + } + + void getTime() + { + unixTime = getSystemTime(); + breakEpochToTime(unixTime, _time_utc); + + day = _time_utc.day_of_month; + month = _time_utc.month; + year = _time_utc.year + 2000; // 0-25 + + hour = _time_utc.hour; + minute = _time_utc.minute; + second = _time_utc.second; + } + + void sunArcFromTransit(double longitude, double &sunArc) + { + JulianDay jd(year, month, day, hour, minute, second); + double T = calcJulianCent(jd); + + // Получаем экваториальные координаты Солнца + double ra, dec; + calcSolarCoordinates(T, ra, dec); // ra в градусах, dec тоже + + double GMST = calcGrMeanSiderealTime(jd); + double ghaSun = wrapTo360(GMST - ra); + + // LHA = GHA + долгота наблюдателя (восточная долгота положительная) + double lha = wrapTo180(ghaSun + longitude); + + // Угол от транзита: 0° — транзит, 90° — 6 часов позже и т.д. + sunArc = lha; + } + + void doByInterval() + { + if (isTimeSynch) + { + getTime(); + JulianDay jd(year, month, day, hour, minute, second); + + double azimuth, elevation; + calcHorizontalCoordinates(jd, _lat, _long, azimuth, elevation); + + value.isDecimal = true; + if (_parameter == "azimuth") + { + value.valD = azimuth; + } + else if (_parameter == "elevation") + { + value.valD = elevation; + } + else if (_parameter == "sunArcFromTransit") + { + double sunArc; + sunArcFromTransit(_long, sunArc); + value.valD = sunArc; + } + else + { + SerialPrint("E", F("SolarCalculator"), _parameter + " is not correct parameter!!!"); + return; + } + + regEvent(value.valD, F("SoftRTC"), _debug, _ticker); + } + } + + IoTValue execute(String command, std::vector ¶m) + { + if (command == "sunrise") + { + if (param.size() == 0 && isTimeSynch) + { + getTime(); + } + else if (param.size() == 3 && param[0].isDecimal && param[1].isDecimal && param[2].isDecimal) + { + year = param[2].valD; + month = param[1].valD; + day = param[0].valD; + } + else + { + SerialPrint("E", F("SolarCalculator"), "Wrong parameter!s or time is not synched!!", _id); + return {}; + } + + double transit, sunrise, sunset; + calcSunriseSunset(year, month, day, _lat, _long, transit, sunrise, sunset); + + int utc_offset = jsonReadInt(settingsFlashJson, F("timezone")); + + char str[6]; + IoTValue valTmp; + valTmp.isDecimal = false; + valTmp.valS = hoursToString(sunrise + utc_offset, str); + return valTmp; + } + else if (command == "transit") + { + if (param.size() == 0 && isTimeSynch) + { + getTime(); + } + else if (param.size() == 3 && param[0].isDecimal && param[1].isDecimal && param[2].isDecimal) + { + year = param[2].valD; + month = param[1].valD; + day = param[0].valD; + } + else + { + SerialPrint("E", F("SolarCalculator"), "Wrong parameter!s or time is not synched!!", _id); + return {}; + } + + double transit, sunrise, sunset; + calcSunriseSunset(year, month, day, _lat, _long, transit, sunrise, sunset); + + int utc_offset = jsonReadInt(settingsFlashJson, F("timezone")); + + char str[6]; + IoTValue valTmp; + valTmp.isDecimal = false; + valTmp.valS = hoursToString(transit + utc_offset, str); + return valTmp; + } + else if (command == "sunset") + { + + if (param.size() == 0 && isTimeSynch) + { + getTime(); + } + else if (param.size() == 3 && param[0].isDecimal && param[1].isDecimal && param[2].isDecimal) + { + day = param[0].valD; + month = param[1].valD; + year = param[2].valD; + } + else + { + SerialPrint("E", F("SolarCalculator"), "Wrong parameter!s or time is not synched!!", _id); + return {}; + } + + double transit, sunrise, sunset; + calcSunriseSunset(year, month, day, _lat, _long, transit, sunrise, sunset); + + int utc_offset = jsonReadInt(settingsFlashJson, F("timezone")); + + char str[6]; + IoTValue valTmp; + valTmp.isDecimal = false; + valTmp.valS = hoursToString(sunset + utc_offset, str); + return valTmp; + } + else if (command == "azimuth") + { + if (param.size() == 0 && isTimeSynch) + { + getTime(); + } + else if (param.size() > 3) + { + day = param[0].valD; + month = param[1].valD; + year = param[2].valD; + hour = param[3].valD; + minute = (param.size() > 4) ? param[4].valD : 0; + second = (param.size() > 5) ? param[5].valD : 0; + } + else + { + SerialPrint("E", F("SolarCalculator"), "Wrong parameter!s or time is not synched!!", _id); + return {}; + } + + JulianDay jd(year, month, day, hour, minute, second); + double azimuth, elevation; + calcHorizontalCoordinates(jd, _lat, _long, azimuth, elevation); + + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = azimuth; + return valTmp; + } + else if (command == "elevation") + { + if (param.size() == 0 && isTimeSynch) + { + getTime(); + } + else if (param.size() > 3) + { + day = param[0].valD; + month = param[1].valD; + year = param[2].valD; + hour = param[3].valD; + minute = (param.size() > 4) ? param[4].valD : 0; + second = (param.size() > 5) ? param[5].valD : 0; + } + else + { + SerialPrint("E", F("SolarCalculator"), "Wrong parameter!s or time is not synched!!", _id); + return {}; + } + + JulianDay jd(year, month, day, hour, minute, second); + double azimuth, elevation; + calcHorizontalCoordinates(jd, _lat, _long, azimuth, elevation); + + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = elevation; + return valTmp; + } + else if (command == "sunArcFromTransit") + { + if (param.size() == 0 && isTimeSynch) + { + getTime(); + } + else if (param.size() > 3) + { + day = param[0].valD; + month = param[1].valD; + year = param[2].valD; + hour = param[3].valD; + minute = (param.size() > 4) ? param[4].valD : 0; + second = (param.size() > 5) ? param[5].valD : 0; + } + else + { + SerialPrint("E", F("SolarCalculator"), "Wrong parameter!s or time is not synched!!", _id); + return {}; + } + + double sunArc; + sunArcFromTransit(_long, sunArc); + + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = sunArc; + return valTmp; + } + else if (command == "jd") + { + // float hours = 0; + if (param.size() == 0 && isTimeSynch) + { + getTime(); + } + else if (param.size() > 3) + { + day = param[0].valD; + month = param[1].valD; + year = param[2].valD; + hour = param[3].valD; + minute = (param.size() > 4) ? param[4].valD : 0; + second = (param.size() > 5) ? param[5].valD : 0; + } + else + { + SerialPrint("E", F("SolarCalculator"), "Wrong parameters or time is not synched!!", _id); + return {}; + } + JulianDay jd(year, month, day, hour, minute, second); + float jdJD = jd.JD; + float jdm = jd.m; + float jdSum = jdJD + jdm; + //SerialPrint("I", F("SolarCalc"), "jdJD = " + String(jdJD)); + //SerialPrint("I", F("SolarCalc"), "jdm = " + String(jdm)); + //SerialPrint("I", F("SolarCalc"), "jdSum = " + String(jdSum)); + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = jdSum; + return valTmp; + } + else if (command == "GMST") + { + if (param.size() == 0 && isTimeSynch) + { + getTime(); + } + else if (param.size() > 3) + { + day = param[0].valD; + month = param[1].valD; + year = param[2].valD; + hour = param[3].valD; + minute = (param.size() > 4) ? param[4].valD : 0; + second = (param.size() > 5) ? param[5].valD : 0; + } + else + { + SerialPrint("E", F("SolarCalculator"), "Wrong parameters or time is not synched!!", _id); + return {}; + } + JulianDay jd(year, month, day, hour, minute, second); + double GMST = calcGrMeanSiderealTime(jd); + + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = GMST; + return valTmp; + } + else if (command == "LST") + { + if (param.size() == 0 && isTimeSynch) + { + getTime(); + } + else if (param.size() > 3) + { + day = param[0].valD; + month = param[1].valD; + year = param[2].valD; + hour = param[3].valD; + minute = (param.size() > 4) ? param[4].valD : 0; + second = (param.size() > 5) ? param[5].valD : 0; + } + else + { + SerialPrint("E", F("SolarCalculator"), "Wrong parameters or time is not synched!!", _id); + return {}; + } + + JulianDay jd(year, month, day, hour, minute, second); + double GMST = calcGrMeanSiderealTime(jd); + double LST = wrapTo360(GMST + _long); + + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = LST; + return valTmp; + } + else if (command == "ra") + { + if (param.size() == 0 && isTimeSynch) + { + getTime(); + } + else if (param.size() > 3) + { + day = param[0].valD; + month = param[1].valD; + year = param[2].valD; + hour = param[3].valD; + minute = (param.size() > 4) ? param[4].valD : 0; + second = (param.size() > 5) ? param[5].valD : 0; + } + else + { + SerialPrint("E", F("SolarCalculator"), "Wrong parameters or time is not synched!!", _id); + return {}; + } + + JulianDay jd(year, month, day, hour, minute, second); + double T = calcJulianCent(jd); + double ra, dec; + calcSolarCoordinates(T, ra, dec); + + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = ra; + return valTmp; + } + else if (command == "dec") + { + if (param.size() == 0 && isTimeSynch) + { + getTime(); + } + else if (param.size() > 3) + { + day = param[0].valD; + month = param[1].valD; + year = param[2].valD; + hour = param[3].valD; + minute = (param.size() > 4) ? param[4].valD : 0; + second = (param.size() > 5) ? param[5].valD : 0; + } + else + { + SerialPrint("E", F("SolarCalculator"), "Wrong parameters or time is not synched!!", _id); + return {}; + } + + JulianDay jd(year, month, day, hour, minute, second); + double T = calcJulianCent(jd); + double ra, dec; + calcSolarCoordinates(T, ra, dec); + + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = dec; + return valTmp; + } + else if (command == "getHour" && param.size() == 1) + { // получаем час из какого-то элемента и переводим его в UTC + + int h = selectToMarker(param[0].valS, ":").toInt(); + int utc_offset = jsonReadInt(settingsFlashJson, F("timezone")); + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = h - utc_offset; + return valTmp; + } + else if (command == "getMinute" && param.size() == 1) + { // получаем минуты из какого-то элемента и переводим его в UTC + int min = selectToMarkerLast(param[0].valS, ":").toInt(); + IoTValue valTmp; + valTmp.isDecimal = true; + valTmp.valD = min; + return valTmp; + } + else + SerialPrint("E", F("SolarCalculator"), F("Unknown command or wrong parameters.")); + return {}; + } +}; + +void *getAPI_SolarCalculator(String subtype, String param) +{ + if (subtype == F("SolarCalculator")) + { + return new SolarCalculator(param); + } + return nullptr; +} diff --git a/src/modules/virtual/SolarCalc/coordinates.jpg b/src/modules/virtual/SolarCalc/coordinates.jpg new file mode 100644 index 00000000..9beae48d Binary files /dev/null and b/src/modules/virtual/SolarCalc/coordinates.jpg differ diff --git a/src/modules/virtual/SolarCalc/export.json b/src/modules/virtual/SolarCalc/export.json new file mode 100644 index 00000000..5914dbff --- /dev/null +++ b/src/modules/virtual/SolarCalc/export.json @@ -0,0 +1,455 @@ +{ + "mark": "iotm", + "config": [ + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "CalculateBtn", + "needSave": 0, + "widget": "toggle", + "page": "Sun", + "descr": "Calculate", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "RiseTimeToday", + "needSave": 0, + "widget": "anydataTm", + "page": "Sun rise", + "descr": " sunrise today", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "azimuth", + "needSave": 0, + "widget": "anydataСorner", + "page": "horizontal coordinates", + "descr": " azimuth", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "elevationTransit", + "needSave": 0, + "widget": "anydataСorner", + "page": "Sun transit", + "descr": "elevation at Transit", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "elevation", + "needSave": 0, + "widget": "anydataСorner", + "page": "horizontal coordinates", + "descr": " elevation", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "SetTimeToday", + "needSave": 0, + "widget": "anydataTm", + "page": "Sun set", + "descr": " sunset today", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "TransitTimeToday", + "needSave": 0, + "widget": "anydataTm", + "page": "Sun transit", + "descr": " transit today", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "hourRiseToday", + "needSave": 0, + "widget": "anydataDef", + "page": "Sun rise", + "descr": "Sun rise UTC hours", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "minuteRiseToday", + "needSave": 0, + "widget": "anydataDef", + "page": "Sun rise", + "descr": "Sun rise UTC minutes", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "azimuthRise", + "needSave": 0, + "widget": "anydataСorner", + "page": "Sun rise", + "descr": "azimuth at sunrise", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "azimuthTransit", + "needSave": 0, + "widget": "anydataСorner", + "page": "Sun transit", + "descr": "azimuth at transit", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "azimuthSet", + "needSave": 0, + "widget": "anydataСorner", + "page": "Sun set", + "descr": "azimuth at sunset", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "isDayTime", + "needSave": 0, + "widget": "toggle", + "page": "Sun", + "descr": "is Day Time", + "int": "0", + "val": "0", + "moduleName": "VButton" + }, + { + "global": 0, + "type": "Reading", + "subtype": "SolarCalculator", + "id": "Sun", + "widget": "anydataСorner", + "page": "by Interval", + "descr": "Azimuth", + "lat": "50.118", + "lon": "-005.478", + "parameter": "azimuth", + "int": "60", + "ticker": "1", + "debug": 1, + "moduleName": "SolarCalculator" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "sunArcFromTransit", + "needSave": 0, + "widget": "anydataСorner", + "page": "Sun transit", + "descr": "Sun Arc From Transit", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "jd", + "needSave": 0, + "widget": "anydataDef", + "page": "Raw astro data", + "descr": "jd", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "2", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "GMST", + "needSave": 0, + "widget": "anydataСorner", + "page": "Raw astro data", + "descr": " GMST", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "LST", + "needSave": 0, + "widget": "anydataСorner", + "page": "Raw astro data", + "descr": " LST", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "ra", + "needSave": 0, + "widget": "anydataСorner", + "page": "equatorial coordinates", + "descr": "ra", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "dec", + "needSave": 0, + "widget": "anydataСorner", + "page": "equatorial coordinates", + "descr": "dec", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "GHA", + "needSave": 0, + "widget": "anydataСorner", + "page": "Raw astro data", + "descr": " GHA", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "LHA", + "needSave": 0, + "widget": "anydataСorner", + "page": "Raw astro data", + "descr": "LHA", + "int": "0", + "val": "0.0", + "map": "", + "plus": 0, + "multiply": 1, + "round": "1", + "moduleName": "Variable" + }, + { + "global": 0, + "type": "Writing", + "subtype": "Timer", + "id": "timerMinute", + "widget": "anydataDef", + "page": "Timers", + "descr": "Timer 1 minite", + "int": 1, + "countDown": "60", + "ticker": "0", + "repeat": 1, + "needSave": 0, + "moduleName": "Timer" + }, + { + "global": 0, + "type": "Writing", + "subtype": "Timer", + "id": "timerDay", + "widget": "anydataDef", + "page": "Timers", + "descr": "Timer 1 Day", + "int": 1, + "countDown": "86400", + "ticker": "0", + "repeat": "1", + "needSave": 0, + "moduleName": "Timer" + }, + { + "global": 0, + "type": "Writing", + "subtype": "Cron", + "id": "cronMidnight", + "widget": "anydataDef", + "page": "Timers", + "descr": "Cron midnight", + "int": 1, + "val": "1 0 0 * * *", + "formatNextAlarm": "%H:%M:%S", + "needSave": 0, + "moduleName": "Cron" + } + ] +} + +scenario=>if timerDay == 0 | CalculateBtn == 1 then { +RiseTimeToday = Sun.sunrise() +TransitTimeToday = Sun.transit() +SetTimeToday = Sun.sunset() + +hourRiseToday = Sun.getHour(RiseTimeToday) +minuteRiseToday = Sun.getMinute(RiseTimeToday) + +if RiseTimeToday then azimuthRise = Sun.azimuth(getDay(),getMonth(),2025, Sun.getHour(RiseTimeToday) ,Sun.getMinute(RiseTimeToday)) + +if TransitTimeToday then {azimuthTransit = Sun.azimuth(getDay(),getMonth(),2025, Sun.getHour(TransitTimeToday),Sun.getMinute(TransitTimeToday)) + +elevationTransit = Sun.elevation(getDay(),getMonth(),2025, Sun.getHour(TransitTimeToday) ,Sun.getMinute(TransitTimeToday)) +} + +if SetTimeToday then azimuthSet = Sun.azimuth(getDay(),getMonth(),2025, Sun.getHour(SetTimeToday) ,Sun.getMinute(SetTimeToday)) + +CalculateBtn = 0 +} + +if timerMinute == 0 | CalculateBtn == 1 then { + +jd = Sun.jd() +GMST = Sun.GMST() +LST = Sun.LST() +ra = Sun.ra() +dec = Sun.dec() +GHA = GMST - ra +LHA = GHA - 28.8448 + +azimuth = Sun.azimuth() +elevation = Sun.elevation() +sunArcFromTransit = Sun.sunArcFromTransit() + +if elevation > 0 then isDayTime = 1 else isDayTime = 0 + +CalculateBtn = 0 +} + +if cronMidnight then CalculateBtn = 1 +if onStart then CalculateBtn = 1 \ No newline at end of file diff --git a/src/modules/virtual/SolarCalc/modinfo.json b/src/modules/virtual/SolarCalc/modinfo.json new file mode 100644 index 00000000..802a2743 --- /dev/null +++ b/src/modules/virtual/SolarCalc/modinfo.json @@ -0,0 +1,167 @@ +{ + "menuSection": "virtual_elments", + "configItem": [ + { + "global": 0, + "name": "Solar Calculator", + "type": "Reading", + "subtype": "SolarCalculator", + "id": "Sun", + "widget": "anydataCorner", + "page": "Математика", + "descr": "Azimuth", + "lat": 47.0159, + "lon": 28.8448, + "parameter": "azimuth", + "round": 1, + "int": 60, + "ticker": 1, + "debug": 0 + + } + ], + "about": { + "authorName": "Alex", + "authorContact": "https://t.me/cmche", + "authorGit": "https://github.com/CHE77/IoTManager-Modules", + "exampleURL": "https://iotmanager.org/wiki", + "specialThanks": "", + "moduleName": "SolarCalculator", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Модуль SolarCalc", + "moduleDesc": "Модуль для расчета положения Солнца в различных системах координат. Требуют обновленного времени: через интернет, RTC, SoftRTC", + "license": "Cooperative Non-Violent Public License (CNPL)", + "propInfo": { + }, + "funcInfo": [ + { + "name": "sunrise", + "descr": "Расчет времени восхода", + "params": [ + "Sun.sunrise() - на текущую дату", + "Sun.sunrise(день, месяц, год) - на заданую дату" + ] + }, + { + "name": "transit", + "descr": "Расчет времени транзита", + "params": [ + "Sun.transit() - на текущую дату", + "Sun.transit(день, месяц, год) - на заданую дату" + ] + }, + { + "name": "sunset", + "descr": "Расчет времени захода", + "params": [ + "Sun.sunset() - на текущую дату", + "Sun.sunset(день, месяц, год) - на заданую дату" + ] + }, + { + "name": "azimuth", + "descr": "Расчет азимута", + "params": [ + "Sun.azimuth() - на текущий момент времени", + "Sun.azimuth(день, месяц, год, час, минут, секунд) - на заданое веремя", + "Sun.azimuth(день, месяц, год, час, минут) - на заданое веремя", + "Sun.azimuth(день, месяц, год, час) - на заданое веремя" + ] + }, + { + "name": "elevation", + "descr": "Расчет высоты (над горизонтом)", + "params": [ + "Sun.elevation() - на текущий момент времени", + "Sun.elevation(день, месяц, год, час, минут, секунд) - на заданое веремя", + "Sun.elevation(день, месяц, год, час, минут) - на заданое веремя", + "Sun.elevation(день, месяц, год, час) - на заданое веремя" + ] + }, + { + "name": "sunArcFromTransit", + "descr": "Расчет дуги эклиптики от зенита", + "params": [ + "Sun.sunArcFromTransit() - на текущий момент времени", + "Sun.sunArcFromTransit(день, месяц, год, час, минут, секунд) - на заданое веремя", + "Sun.sunArcFromTransit(день, месяц, год, час, минут) - на заданое веремя", + "Sun.sunArcFromTransit(день, месяц, год, час) - на заданое веремя" + ] + }, + { + "name": "jd", + "descr": "Расчет Юлинаской даты", + "params": [ + "Sun.jd() - на текущий момент времени", + "Sun.jd(день, месяц, год, час, минут, секунд) - на заданое веремя", + "Sun.jd(день, месяц, год, час, минут) - на заданое веремя", + "Sun.jd(день, месяц, год, час) - на заданое веремя" + ] + }, + { + "name": "GMST", + "descr": "Расчет среднего звездного времени на меридиане Гринвича", + "params": [ + "Sun.GMST() - на текущий момент времени", + "Sun.GMST(день, месяц, год, час, минут, секунд) - на заданое веремя", + "Sun.GMST(день, месяц, год, час, минут) - на заданое веремя", + "Sun.GMST(день, месяц, год, час) - на заданое веремя" + ] + }, + { + "name": "LST", + "descr": "Расчет Местного звездного времени", + "params": [ + "Sun.LST() - на текущий момент времени", + "Sun.LST(день, месяц, год, час, минут, секунд) - на заданое веремя", + "Sun.LST(день, месяц, год, час, минут) - на заданое веремя", + "Sun.LST(день, месяц, год, час) - на заданое веремя" + ] + }, + { + "name": "ra", + "descr": "Расчет Прямого Восхождения", + "params": [ + "Sun.ra() - на текущий момент времени", + "Sun.ra(день, месяц, год, час, минут, секунд) - на заданое веремя", + "Sun.ra(день, месяц, год, час, минут) - на заданое веремя", + "Sun.ra(день, месяц, год, час) - на заданое веремя" + ] + }, + { + "name": "dec", + "descr": "Расчет Склонения", + "params": [ + "Sun.dec() - на текущий момент времени", + "Sun.dec(день, месяц, год, час, минут, секунд) - на заданое веремя", + "Sun.dec(день, месяц, год, час, минут) - на заданое веремя", + "Sun.dec(день, месяц, год, час) - на заданое веремя" + ] + }, + { + "name": "getHour", + "descr": "Получаем час из какого-то элемента и переводим его в UTC", + "params": [ + "Sun.getHour(HH:mm)" + ] + }, + { + "name": "getMinute", + "descr": "Получаем минуты из какого-то элемента", + "params": [ + "Sun.getMinute(HH:mm)" + ] + } + ] + }, + "defActive": false, + "usedLibs": { + "esp32*": ["jpb10/SolarCalculator@^2.0.1"], + "esp82*": ["jpb10/SolarCalculator@^2.0.1"], + "bk72*": ["jpb10/SolarCalculator@^2.0.1"] + } +}