From 6820460915613af46ab8d928d62df15048142627 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Wed, 20 Nov 2024 21:13:12 +0300 Subject: [PATCH] update Thermostat and Loging --- src/modules/exec/Thermostat/GyverPID.h | 171 +++++++++ src/modules/exec/Thermostat/Thermostat.cpp | 194 +++++++--- src/modules/exec/Thermostat/modinfo.json | 37 +- src/modules/virtual/Loging/Loging.cpp | 274 ++++++++++---- src/modules/virtual/Loging/modinfo.json | 111 +++--- src/modules/virtual/LogingDaily/modinfo.json | 93 ++--- .../virtual/LogingHourly/LogingHourly.cpp | 339 ++++++++++++++++++ src/modules/virtual/LogingHourly/modinfo.json | 50 +++ 8 files changed, 1045 insertions(+), 224 deletions(-) create mode 100644 src/modules/exec/Thermostat/GyverPID.h create mode 100644 src/modules/virtual/LogingHourly/LogingHourly.cpp create mode 100644 src/modules/virtual/LogingHourly/modinfo.json diff --git a/src/modules/exec/Thermostat/GyverPID.h b/src/modules/exec/Thermostat/GyverPID.h new file mode 100644 index 00000000..5adddeea --- /dev/null +++ b/src/modules/exec/Thermostat/GyverPID.h @@ -0,0 +1,171 @@ +/* + GyverPID - библиотека PID регулятора для Arduino + Документация: https://alexgyver.ru/gyverpid/ + GitHub: https://github.com/GyverLibs/GyverPID + Возможности: + - Время одного расчёта около 70 мкс + - Режим работы по величине или по её изменению (для интегрирующих процессов) + - Возвращает результат по встроенному таймеру или в ручном режиме + - Встроенные калибровщики коэффициентов + - Режим работы по ошибке и по ошибке измерения + - Встроенные оптимизаторы интегральной суммы + + AlexGyver, alex@alexgyver.ru + https://alexgyver.ru/ + MIT License + + Версии: + v1.1 - убраны дефайны + v1.2 - возвращены дефайны + v1.3 - вычисления ускорены, библиотека облегчена + v2.0 - логика работы чуть переосмыслена, код улучшен, упрощён и облегчён + v2.1 - integral вынесен в public + v2.2 - оптимизация вычислений + v2.3 - добавлен режим PID_INTEGRAL_WINDOW + v2.4 - реализация внесена в класс + v3.0 + - Добавлен режим оптимизации интегральной составляющей (см. доку) + - Добавлены автоматические калибровщики коэффициентов (см. примеры и доку) + v3.1 - исправлен режиме ON_RATE, добавлено автоограничение инт. суммы + v3.2 - чуть оптимизации, добавлена getResultNow + v3.3 - в тюнерах можно передать другой обработчик класса Stream для отладки +*/ + +#ifndef GyverPID_h +#define GyverPID_h +#include + +#if defined(PID_INTEGER) // расчёты с целыми числами +typedef int datatype; +#else // расчёты с float числами +typedef float datatype; +#endif + +#define NORMAL 0 +#define REVERSE 1 +#define ON_ERROR 0 +#define ON_RATE 1 + +class GyverPID +{ +public: + // ==== datatype это float или int, в зависимости от выбранного (см. пример integer_calc) ==== + GyverPID() {} + + // kp, ki, kd, dt + GyverPID(float new_kp, float new_ki, float new_kd, int new_dt = 60) + { + setDt(new_dt); + Kp = new_kp; + Ki = new_ki; + Kd = new_kd; + prevInput = 0; + integral = 0; + output = 0; + } + + // направление регулирования: NORMAL (0) или REVERSE (1) + void setDirection(boolean direction) + { + _direction = direction; + } + + // режим: работа по входной ошибке ON_ERROR (0) или по изменению ON_RATE (1) + void setMode(boolean mode) + { + _mode = mode; + } + + // лимит выходной величины (например для ШИМ ставим 0-255) + void setLimits(int min_output, int max_output) + { + _minOut = min_output; + _maxOut = max_output; + } + + // установка времени дискретизации (для getResultTimer) + void setDt(int new_dt) + { + _dt_s = new_dt; + _dt = new_dt * 1000; + } + + datatype setpoint = 0; // заданная величина, которую должен поддерживать регулятор + datatype input = 0; // сигнал с датчика (например температура, которую мы регулируем) + datatype output = 0; // выход с регулятора на управляющее устройство (например величина ШИМ или угол поворота серво) + float Kp = 0.0; // коэффициент P + float Ki = 0.0; // коэффициент I + float Kd = 0.0; // коэффициент D + float integral = 0.0; // интегральная сумма + + // возвращает новое значение при вызове (если используем свой таймер с периодом dt!) + datatype getResult() + { + datatype error = setpoint - input; // ошибка регулирования + datatype delta_input = prevInput - input; // изменение входного сигнала за dt + prevInput = input; // запомнили предыдущее + if (_direction) + { // смена направления + error = -error; + delta_input = -delta_input; + } + output = _mode ? 0 : (error * Kp); // пропорциональая составляющая + output += delta_input * Kd / _dt_s; // дифференциальная составляющая +#if (PID_INTEGRAL_WINDOW > 0) + // ЭКСПЕРИМЕНТАЛЬНЫЙ РЕЖИМ ИНТЕГРАЛЬНОГО ОКНА + if (++t >= PID_INTEGRAL_WINDOW) + t = 0; // перемотка t + integral -= errors[t]; // вычитаем старое + errors[t] = error * Ki * _dt_s; // запоминаем в массив + integral += errors[t]; // прибавляем новое +#else + integral += error * Ki * _dt_s; // обычное суммирование инт. суммы +#endif + +#ifdef PID_OPTIMIZED_I + // ЭКСПЕРИМЕНТАЛЬНЫЙ РЕЖИМ ОГРАНИЧЕНИЯ ИНТЕГРАЛЬНОЙ СУММЫ + output = constrain(output, _minOut, _maxOut); + if (Ki != 0) + integral = constrain(integral, (_minOut - output) / (Ki * _dt_s), (_maxOut - output) / (Ki * _dt_s)); +#endif + + if (_mode) + integral += delta_input * Kp; // режим пропорционально скорости + integral = constrain(integral, _minOut, _maxOut); // ограничиваем инт. сумму + output += integral; // интегральная составляющая + output = constrain(output, _minOut, _maxOut); // ограничиваем выход + return output; + } + + // возвращает новое значение не ранее, чем через dt миллисекунд (встроенный таймер с периодом dt) + datatype getResultTimer() + { + if (millis() - pidTimer >= _dt) + { + pidTimer = millis(); + getResult(); + } + return output; + } + + // посчитает выход по реальному прошедшему времени между вызовами функции + datatype getResultNow() + { + setDt(millis() - pidTimer); + pidTimer = millis(); + return getResult(); + } + +private: + int16_t _dt = 100; // время итерации в мс + float _dt_s = 0.1; // время итерации в с + boolean _mode = 0, _direction = 0; + int _minOut = 0, _maxOut = 255; + datatype prevInput = 0; + uint32_t pidTimer = 0; +#if (PID_INTEGRAL_WINDOW > 0) + datatype errors[PID_INTEGRAL_WINDOW]; + int t = 0; +#endif +}; +#endif \ No newline at end of file diff --git a/src/modules/exec/Thermostat/Thermostat.cpp b/src/modules/exec/Thermostat/Thermostat.cpp index 8d3c9260..d91000ab 100644 --- a/src/modules/exec/Thermostat/Thermostat.cpp +++ b/src/modules/exec/Thermostat/Thermostat.cpp @@ -1,5 +1,6 @@ #include "Global.h" #include "classes/IoTItem.h" +#include "GyverPID.h" extern IoTGpio IoTgpio; @@ -15,6 +16,7 @@ private: float sp, pv, pv2; String interim; int enable = 1; + int _direction = 0; public: ThermostatGIST(String parameters) : IoTItem(parameters) @@ -24,6 +26,7 @@ public: jsonRead(parameters, "term_rezerv_id", _term_rezerv_id); jsonRead(parameters, "gist", _gist); jsonRead(parameters, "rele", _rele); + jsonRead(parameters, "direction", _direction); } void doByInterval() @@ -55,13 +58,31 @@ public: { tmp = findIoTItem(_rele); if (tmp) - tmp->setValue("0", true); + { + if (_direction) + { + tmp->setValue("0", true); + } + else + { + tmp->setValue("1", true); + } + } } if (pv <= sp - _gist && enable) { tmp = findIoTItem(_rele); if (tmp) - tmp->setValue("1", true); + { + if (_direction) + { + tmp->setValue("1", true); + } + else + { + tmp->setValue("0", true); + } + } } } else @@ -86,13 +107,31 @@ public: { tmp = findIoTItem(_rele); if (tmp) - tmp->setValue("0", true); + { + if (_direction) + { + tmp->setValue("0", true); + } + else + { + tmp->setValue("1", true); + } + } } if (pv2 <= sp - _gist && enable) { tmp = findIoTItem(_rele); if (tmp) - tmp->setValue("1", true); + { + if (_direction) + { + tmp->setValue("1", true); + } + else + { + tmp->setValue("0", true); + } + } } } else @@ -144,23 +183,40 @@ public: return {}; } - ~ThermostatGIST(){}; + ~ThermostatGIST() {}; }; +GyverPID *regulator = nullptr; +GyverPID *instanceregulator(float _KP, float _KI, float _KD, int interval, boolean setDirection, int setLimitsMIN, int setLimitsMAX) +{ + if (!regulator) + { // Если библиотека ранее инициализировалась, то просто вернем указатель + // Инициализируем библиотеку + regulator = new GyverPID(_KP, _KI, _KD, interval); // коэф. П, коэф. И, коэф. Д, период дискретизации dt (с) + regulator->setDirection(setDirection); // направление регулирования (NORMAL/REVERSE). ПО УМОЛЧАНИЮ СТОИТ NORMAL + regulator->setLimits(setLimitsMIN, setLimitsMAX); // пределы. ПО УМОЛЧАНИЮ СТОЯТ 0 И 100 + SerialPrint("i", F("ThermostatPID"), " _KP:" + String(_KP) + " _KI:" + String(_KI) + " _KD:" + String(_KD) + " interval:" + String(interval) + " _setLimitsMIN:" + String(setLimitsMIN) + " _setLimitsMAX:" + String(setLimitsMAX) + " Direction:" + String(setDirection)); + // GyverPID regulator(_KP, _KI, _KD, interval); + } + return regulator; +} + class ThermostatPID : public IoTItem { private: - String _set_id; // заданная температура - String _term_id; // термометр - String _term_rezerv_id; // резервный термометр - float _int, _KP, _KI, _KD, sp, pv, + String _set_id; // заданная температура + String _term_id; // термометр + boolean _setDirection; + + float _int, _KP, _KI, _KD, + sp, pv, pv_last = 0, // предыдущая температура ierr = 0, // интегральная погрешность dt = 0; // время между измерениями String _rele; // реле String interim; int enable = 1; - long interval; + int interval, _setLimitsMIN, _setLimitsMAX; IoTItem *tmp; int releState = 0; @@ -169,20 +225,29 @@ public: { jsonRead(parameters, "set_id", _set_id); jsonRead(parameters, "term_id", _term_id); - jsonRead(parameters, "term_rezerv_id", _term_rezerv_id); jsonRead(parameters, "int", _int); jsonRead(parameters, "KP", _KP); jsonRead(parameters, "KI", _KI); jsonRead(parameters, "KD", _KD); jsonRead(parameters, F("int"), interval); - interval = interval * 1000; // интервал проверки в сек jsonRead(parameters, "rele", _rele); + + // GyverPID + jsonRead(parameters, "setDirection", _setDirection); + jsonRead(parameters, "setLimitsMIN", _setLimitsMIN); + jsonRead(parameters, "setLimitsMAX", _setLimitsMAX); + + // в процессе работы можно менять коэффициенты + // instanceregulator(_KP, _KI, _KD, interval)->Kp = _KP; + // instanceregulator(_KP, _KI, _KD, interval)->Ki = _KI; + // instanceregulator(_KP, _KI, _KD, interval)->Kd = _KD; } protected: //=============================================================== // Вычисляем температуру контура отпления, коэффициенты ПИД регулятора //=============================================================== + /* float pid(float sp, float pv, float pv_last, float &ierr, float dt) { float Kc = _KP; // K / %Heater 5 @@ -217,11 +282,10 @@ protected: // выход регулятора, он же уставка для ID-1 (температура теплоносителя контура СО котла) op = constrain(op, oplo, ophi); } - ierr = I; return op; } - +*/ void doByInterval() { @@ -239,32 +303,24 @@ protected: interim = tmp->getValue(); pv = ::atof(interim.c_str()); } - if (pv < -40 && pv > 120 && !pv) // Решаем что ошибка датчика + if (enable) { - if (_term_rezerv_id != "") - { - tmp = findIoTItem(_term_rezerv_id); // используем резервный - if (tmp) - { - interim = tmp->getValue(); - pv = ::atof(interim.c_str()); - if (pv < -40 && pv > 120 && !pv) - pv = 0; - } - else - pv = 0; - } - else - pv = 0; - } - if (sp && pv) - { - // value.valD = pid(sp, pv, pv_last, ierr, _int); - // value.valS = (String)(int)value.valD; - regEvent(pid(sp, pv, pv_last, ierr, _int), "ThermostatPID", false, true); + // regEvent(pid(sp, pv, pv_last, ierr, _int), "ThermostatPID", false, true); + // instanceregulator(_KP, _KI, _KD, interval,_setDirection,_setLimitsMIN,_setLimitsMAX)->setDirection(_setDirection); // направление регулирования (NORMAL/REVERSE). ПО УМОЛЧАНИЮ СТОИТ NORMAL + // instanceregulator(_KP, _KI, _KD, interval,_setDirection,_setLimitsMIN,_setLimitsMAX)->setLimits(_setLimitsMIN, _setLimitsMAX); // пределы. ПО УМОЛЧАНИЮ СТОЯТ 0 И 100 + // instanceregulator(_KP, _KI, _KD, interval)->setMode(1); + instanceregulator(_KP, _KI, _KD, interval, _setDirection, _setLimitsMIN, _setLimitsMAX)->setpoint = sp; + instanceregulator(_KP, _KI, _KD, interval, _setDirection, _setLimitsMIN, _setLimitsMAX)->input = pv; + value.valD = instanceregulator(_KP, _KI, _KD, interval, _setDirection, _setLimitsMIN, _setLimitsMAX)->getResult(); + SerialPrint("i", F("ThermostatPID"), " _KP:" + String(_KP) + " _KI:" + String(_KI) + " _KD:" + String(_KD) + " interval:" + String(interval) + " _setLimitsMIN:" + String(_setLimitsMIN) + " _setLimitsMAX:" + String(_setLimitsMAX) + " Direction:" + String(_setDirection)); + SerialPrint("i", F("ThermostatPID"), "setpoint: " + String(sp) + " input: " + String(pv)); + regEvent(value.valD, "ThermostatPID", false, true); } else - regEvent(0, "ThermostatPID", false, true); + { + value.valD = 0; + regEvent(value.valD, "ThermostatPID", false, true); + } pv_last = pv; } @@ -280,14 +336,14 @@ protected: currentMillis = millis(); difference = currentMillis - prevMillis; - if (_rele != "" && enable && value.valD * interval / 100000 > difference / 1000 && releState == 0) + if (_rele != "" && enable && value.valD * interval / 100 > difference / 1000 && releState == 0) { releState = 1; tmp = findIoTItem(_rele); if (tmp) tmp->setValue("1", true); } - if (_rele != "" && enable && value.valD * interval / 100000 < difference / 1000 && releState == 1) + if (_rele != "" && enable && value.valD * interval / 100 < difference / 1000 && releState == 1) { releState = 0; tmp = findIoTItem(_rele); @@ -295,7 +351,7 @@ protected: tmp->setValue("0", true); } - if (difference >= interval) + if (difference >= interval * 1000) { prevMillis = millis(); this->doByInterval(); @@ -311,6 +367,32 @@ protected: if (param.size()) { enable = param[0].valD; + if (enable == 0) + { + delete regulator; + regulator = nullptr; + // instanceregulator(_KP, _KI, _KD, interval, _setDirection, _setLimitsMIN, _setLimitsMAX); + } + } + } + if (command == "setLimitsMIN") + { + if (param.size()) + { + _setLimitsMIN = param[0].valD; + // delete regulator; + // regulator = nullptr; + // instanceregulator(_KP, _KI, _KD, interval, _setDirection, _setLimitsMIN, _setLimitsMAX); + } + } + if (command == "setLimitsMAX") + { + if (param.size()) + { + _setLimitsMAX = param[0].valD; + // delete regulator; + // regulator = nullptr; + // instanceregulator(_KP, _KI, _KD, interval, _setDirection, _setLimitsMIN, _setLimitsMAX); } } if (command == "KP") @@ -318,6 +400,9 @@ protected: if (param.size()) { _KP = param[0].valD; + delete regulator; + regulator = nullptr; + instanceregulator(_KP, _KI, _KD, interval, _setDirection, _setLimitsMIN, _setLimitsMAX); } } if (command == "KI") @@ -325,6 +410,9 @@ protected: if (param.size()) { _KI = param[0].valD; + delete regulator; + regulator = nullptr; + instanceregulator(_KP, _KI, _KD, interval, _setDirection, _setLimitsMIN, _setLimitsMAX); } } if (command == "KD") @@ -332,12 +420,30 @@ protected: if (param.size()) { _KD = param[0].valD; + delete regulator; + regulator = nullptr; + instanceregulator(_KP, _KI, _KD, interval, _setDirection, _setLimitsMIN, _setLimitsMAX); + } + } + + if (command == "setDirection") + { + if (param.size()) + { + _setDirection = param[0].valD; + delete regulator; + regulator = nullptr; + instanceregulator(_KP, _KI, _KD, interval, _setDirection, _setLimitsMIN, _setLimitsMAX); } } } return {}; } - ~ThermostatPID(){}; + ~ThermostatPID() + { + delete regulator; + regulator = nullptr; + }; }; class ThermostatETK : public IoTItem @@ -395,7 +501,7 @@ protected: } } - ~ThermostatETK(){}; + ~ThermostatETK() {}; }; class ThermostatETK2 : public IoTItem @@ -471,7 +577,7 @@ protected: } } - ~ThermostatETK2(){}; + ~ThermostatETK2() {}; }; void *getAPI_Thermostat(String subtype, String param) @@ -495,4 +601,4 @@ void *getAPI_Thermostat(String subtype, String param) //} return nullptr; -} +} \ No newline at end of file diff --git a/src/modules/exec/Thermostat/modinfo.json b/src/modules/exec/Thermostat/modinfo.json index fa0a274c..fe3ab759 100644 --- a/src/modules/exec/Thermostat/modinfo.json +++ b/src/modules/exec/Thermostat/modinfo.json @@ -16,8 +16,9 @@ "set_id": "", "term_id": "", "term_rezerv_id": "", - "gist": 0.3, - "rele": "" + "gist": 0.1, + "rele": "", + "direction": 0 }, { "global": 0, @@ -31,14 +32,16 @@ "descr": "термостат", "int": 60, "round": 1, - "map": "1,100,1,100", + "map": "1024,1024,1,100", "set_id": "", "term_id": "", - "term_rezerv_id": "", "rele": "", - "KP": 5.0, - "KI": 50, - "KD": 1.0 + "setDirection": 0, + "setLimitsMIN": 0, + "setLimitsMAX": 100, + "KP": 10, + "KI": 0.02, + "KD": 8 }, { "global": 0, @@ -75,11 +78,11 @@ ], "about": { "authorName": "AVAKS", - "authorContact": "https://t.me/@avaks_dev", + "authorContact": "https://t.me/@avaks", "authorGit": "https://github.com/avaksru", "specialThanks": "@Serghei63 за работу PID с обычным реле, Serg помощь в тестировании и устранении ошибок", "moduleName": "Thermostat", - "moduleVersion": "1", + "moduleVersion": "3", "usedRam": { "esp32_4mb": 15, "esp8266_4mb": 15 @@ -129,10 +132,24 @@ "params": [ "thermostat.KD(1) - задает значение коэффициента" ] + }, + { + "name": "setLimitsMIN / setLimitsMAX", + "descr": " лимит выходной величины (например для ШИМ ставим 0-255).", + "params": [ + "thermostat.setLimitsMIN(1) - задает минимальное значение PID" + ] + }, + { + "name": "setDirection", + "descr": "направление регулирования: NORMAL (0) или REVERSE (1).", + "params": [ + "thermostat.setDirection(1) - задает реверсное регулирование" + ] } ] }, - "defActive": false, + "defActive": true, "usedLibs": { "esp32*": [], "esp82*": [] diff --git a/src/modules/virtual/Loging/Loging.cpp b/src/modules/virtual/Loging/Loging.cpp index 513cf241..9196c5cb 100644 --- a/src/modules/virtual/Loging/Loging.cpp +++ b/src/modules/virtual/Loging/Loging.cpp @@ -5,8 +5,9 @@ void *getAPI_Date(String params); -class Loging : public IoTItem { - private: +class Loging : public IoTItem +{ +private: String logid; String id; String tmpValue; @@ -14,6 +15,8 @@ class Loging : public IoTItem { int _publishType = -2; int _wsNum = -1; + int days = 1; + int daysShow = 1; int points; // int keepdays; @@ -25,45 +28,58 @@ class Loging : public IoTItem { // long interval; - public: - Loging(String parameters) : IoTItem(parameters) { +public: + Loging(String parameters) : IoTItem(parameters) + { jsonRead(parameters, F("logid"), logid); jsonRead(parameters, F("id"), id); jsonRead(parameters, F("points"), points); - if (points > 300) { + if (points > 300) + { points = 300; SerialPrint("E", F("Loging"), "'" + id + "' user set more points than allowed, value reset to 300"); } long interval; - jsonRead(parameters, F("int"), interval); // в минутах + jsonRead(parameters, F("int"), interval); // в минутах setInterval(interval * 60); // jsonRead(parameters, F("keepdays"), keepdays, false); + jsonRead(parameters, F("daysSave"), days); + days = days * 86400; + jsonRead(parameters, F("daysShow"), daysShow); + daysShow = daysShow * 86400; + // создадим экземпляр класса даты dateIoTItem = (IoTItem *)getAPI_Date("{\"id\": \"" + id + "-date\",\"int\":\"20\",\"subtype\":\"date\"}"); IoTItems.push_back(dateIoTItem); SerialPrint("I", F("Loging"), "created date instance " + id); } - void doByInterval() { + void doByInterval() + { // если объект логгирования не был создан - if (!isItemExist(logid)) { + if (!isItemExist(logid)) + { SerialPrint("E", F("Loging"), "'" + id + "' loging object not exist, return"); return; } String value = getItemValue(logid); // если значение логгирования пустое - if (value == "") { + if (value == "") + { SerialPrint("E", F("Loging"), "'" + id + "' loging value is empty, return"); return; } // если время не было получено из интернета - if (!isTimeSynch) { + if (!isTimeSynch) + { SerialPrint("E", F("Loging"), "'" + id + "' Сant loging - time not synchronized, return"); return; } + if (hasDayChanged()) + deleteOldFile(); regEvent(value, F("Loging")); @@ -76,13 +92,17 @@ class Loging : public IoTItem { String filePath = readDataDB(id); // если данные о файле отсутствуют, создадим новый - if (filePath == "failed" || filePath == "") { + if (filePath == "failed" || filePath == "") + { SerialPrint("E", F("Loging"), "'" + id + "' file path not found, start create new file"); createNewFileWithData(logData); return; - } else { + } + else + { // если файл все же есть но был создан не сегодня, то создаем сегодняшний - if (getTodayDateDotFormated() != getDateDotFormatedFromUnix(getFileUnixLocalTime(filePath))) { + if (getTodayDateDotFormated() != getDateDotFormatedFromUnix(getFileUnixLocalTime(filePath))) + { SerialPrint("E", F("Loging"), "'" + id + "' file too old, start create new file"); createNewFileWithData(logData); return; @@ -95,29 +115,38 @@ class Loging : public IoTItem { SerialPrint("i", F("Loging"), "'" + id + "' " + "lines = " + String(lines) + ", size = " + String(size)); // если количество строк до заданной величины и дата не менялась - if (lines <= points && !hasDayChanged()) { + if (lines <= points && !hasDayChanged()) + { // просто добавим в существующий файл новые данные addNewDataToExistingFile(filePath, logData); // если больше или поменялась дата то создадим следующий файл - } else { + } + else + { createNewFileWithData(logData); } // запускаем процедуру удаления старых файлов если память переполняется deleteLastFile(); } - void SetDoByInterval(String valse) { + void SetDoByInterval(String valse) + { String value = valse; // если значение логгирования пустое - if (value == "") { + if (value == "") + { SerialPrint("E", F("LogingEvent"), "'" + id + "' loging value is empty, return"); return; } // если время не было получено из интернета - if (!isTimeSynch) { + if (!isTimeSynch) + { SerialPrint("E", F("LogingEvent"), "'" + id + "' Сant loging - time not synchronized, return"); return; } + if (hasDayChanged()) + deleteOldFile(); + regEvent(value, F("LogingEvent")); String logData; jsonWriteInt(logData, "x", unixTime, false); @@ -126,13 +155,17 @@ class Loging : public IoTItem { String filePath = readDataDB(id); // если данные о файле отсутствуют, создадим новый - if (filePath == "failed" || filePath == "") { + if (filePath == "failed" || filePath == "") + { SerialPrint("E", F("LogingEvent"), "'" + id + "' file path not found, start create new file"); createNewFileWithData(logData); return; - } else { + } + else + { // если файл все же есть но был создан не сегодня, то создаем сегодняшний - if (getTodayDateDotFormated() != getDateDotFormatedFromUnix(getFileUnixLocalTime(filePath))) { + if (getTodayDateDotFormated() != getDateDotFormatedFromUnix(getFileUnixLocalTime(filePath))) + { SerialPrint("E", F("LogingEvent"), "'" + id + "' file too old, start create new file"); createNewFileWithData(logData); return; @@ -145,32 +178,39 @@ class Loging : public IoTItem { SerialPrint("i", F("LogingEvent"), "'" + id + "' " + "lines = " + String(lines) + ", size = " + String(size)); // если количество строк до заданной величины и дата не менялась - if (lines <= points && !hasDayChanged()) { + if (lines <= points && !hasDayChanged()) + { // просто добавим в существующий файл новые данные addNewDataToExistingFile(filePath, logData); // если больше или поменялась дата то создадим следующий файл - } else { + } + else + { createNewFileWithData(logData); } // запускаем процедуру удаления старых файлов если память переполняется deleteLastFile(); } - void createNewFileWithData(String &logData) { + void createNewFileWithData(String &logData) + { logData = logData + ","; - String path = "/lg/" + id + "/" + String(unixTimeShort) + ".txt"; // создадим путь вида /lg/id/133256622333.txt + String path = "/lg/" + id + "/" + String(unixTimeShort) + ".txt"; // создадим путь вида /lg/id/133256622333.txt // создадим пустой файл - if (writeEmptyFile(path) != "success") { + if (writeEmptyFile(path) != "success") + { SerialPrint("E", F("Loging"), "'" + id + "' file writing error, return"); return; } // запишем в него данные - if (addFile(path, logData) != "success") { + if (addFile(path, logData) != "success") + { SerialPrint("E", F("Loging"), "'" + id + "' data writing error, return"); return; } // запишем путь к нему в базу данных - if (saveDataDB(id, path) != "success") { + if (saveDataDB(id, path) != "success") + { SerialPrint("E", F("Loging"), "'" + id + "' db file writing error, return"); return; } @@ -181,9 +221,11 @@ class Loging : public IoTItem { #endif } - void addNewDataToExistingFile(String &path, String &logData) { + void addNewDataToExistingFile(String &path, String &logData) + { logData = logData + ","; - if (addFile(path, logData) != "success") { + if (addFile(path, logData) != "success") + { SerialPrint("i", F("Loging"), "'" + id + "' file writing error, return"); return; }; @@ -195,11 +237,14 @@ class Loging : public IoTItem { } // данная функция уже перенесена в ядро и будет удалена в последствии - bool hasDayChanged() { + bool hasDayChanged() + { bool changed = false; String currentDate = getTodayDateDotFormated(); - if (!firstTimeInit) { - if (prevDate != currentDate) { + if (!firstTimeInit) + { + if (prevDate != currentDate) + { changed = true; SerialPrint("i", F("NTP"), F("Change day event")); #if defined(ESP8266) @@ -214,7 +259,8 @@ class Loging : public IoTItem { return changed; } - void publishValue() { + void publishValue() + { String dir = "/lg/" + id; filesList = getFilesList(dir); @@ -224,75 +270,111 @@ class Loging : public IoTItem { bool noData = true; - while (filesList.length()) { + while (filesList.length()) + { String path = selectToMarker(filesList, ";"); - - path = "/lg/" + id + path; + path = dir + path; f++; unsigned long fileUnixTimeLocal = getFileUnixLocalTime(path); unsigned long reqUnixTime = strDateToUnix(getItemValue(id + "-date")); - if (fileUnixTimeLocal > reqUnixTime && fileUnixTimeLocal < reqUnixTime + 86400) { + + if (fileUnixTimeLocal > reqUnixTime - daysShow && fileUnixTimeLocal < reqUnixTime + 86400) + { noData = false; String json = getAdditionalJson(); - if (_publishType == TO_MQTT) { + if (_publishType == TO_MQTT) + { publishChartFileToMqtt(path, id, calculateMaxCount()); - } else if (_publishType == TO_WS) { + } + else if (_publishType == TO_WS) + { sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); - } else if (_publishType == TO_MQTT_WS) { + } + else if (_publishType == TO_MQTT_WS) + { sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); publishChartFileToMqtt(path, id, calculateMaxCount()); } SerialPrint("i", F("Loging"), String(f) + ") " + path + ", " + getDateTimeDotFormatedFromUnix(fileUnixTimeLocal) + ", sent"); - } else { + } + else + { SerialPrint("i", F("Loging"), String(f) + ") " + path + ", " + getDateTimeDotFormatedFromUnix(fileUnixTimeLocal) + ", skipped"); } + /* + //------------ delete old files ---------------- + String pathTodel = path; + pathTodel.replace("/", ""); + pathTodel.replace(".txt", ""); + int pathTodel_ = pathTodel.toInt(); + if (pathTodel_ < unixTimeShort - days) + { + removeFile(path); + SerialPrint("i", "Loging!!!!!!", path + " => old files been clean"); + } + if (pathTodel_ < unixTimeShort - 5184000) + { + removeFile(path); + SerialPrint("i", "Loging!!!!!!", path + " => > 2 month files been clean"); + } + //------------ delete old files ---------------- + */ filesList = deleteBeforeDelimiter(filesList, ";"); } // если данных нет отправляем пустой грфик - if (noData) { + if (noData) + { clearValue(); } } - String getAdditionalJson() { + String getAdditionalJson() + { String topic = mqttRootDevice + "/" + id; String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\"}"; return json; } - void publishChartToWsSinglePoint(String value) { + void publishChartToWsSinglePoint(String value) + { String topic = mqttRootDevice + "/" + id; String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + "}]}"; sendStringToWs("chartb", json, -1); } - void clearValue() { + void clearValue() + { String topic = mqttRootDevice + "/" + id; String json = "{\"maxCount\":0,\"topic\":\"" + topic + "\",\"status\":[]}"; sendStringToWs("chartb", json, -1); } - void clearHistory() { + void clearHistory() + { String dir = "/lg/" + id; cleanDirectory(dir); } - void deleteLastFile() { + void deleteLastFile() + { IoTFSInfo tmp = getFSInfo(); SerialPrint("i", "Loging", String(tmp.freePer) + " % free flash remaining"); - if (tmp.freePer <= 50.00) { + if (tmp.freePer <= 10.00) + { String dir = "/lg/" + id; filesList = getFilesList(dir); int i = 0; - while (filesList.length()) { + while (filesList.length()) + { String path = selectToMarker(filesList, ";"); path = dir + path; i++; - if (i == 1) { + if (i == 1) + { removeFile(path); SerialPrint("!", "Loging", String(i) + ") " + path + " => oldest files been deleted"); return; @@ -303,7 +385,37 @@ class Loging : public IoTItem { } } - void setPublishDestination(int publishType, int wsNum) { + void deleteOldFile() + { + String dir = "/lg/" + id; + filesList = getFilesList(dir); + int i = 0; + while (filesList.length()) + { + String path = selectToMarker(filesList, ";"); + String pathTodel = path; + pathTodel.replace("/", ""); + pathTodel.replace(".txt", ""); + int pathTodel_ = pathTodel.toInt(); + path = dir + path; + i++; + if (pathTodel_ < unixTimeShort - days) + { + removeFile(path); + SerialPrint("i", "Loging!!!!!!", String(i) + ") " + path + " => old files been clean"); + } + if (pathTodel_ < unixTimeShort - 5184000) + { + removeFile(path); + SerialPrint("i", "Loging!!!!!!", String(i) + ") " + path + " => > 2 month files been clean"); + } + + filesList = deleteBeforeDelimiter(filesList, ";"); + } + } + + void setPublishDestination(int publishType, int wsNum) + { _publishType = publishType; _wsNum = wsNum; } @@ -323,11 +435,13 @@ class Loging : public IoTItem { // } // } - void regEvent(const String &value, const String &consoleInfo, bool error = false, bool genEvent = true) { + void regEvent(const String &value, const String &consoleInfo, bool error = false, bool genEvent = true) + { String userDate = getItemValue(id + "-date"); String currentDate = getTodayDateDotFormated(); // отправляем в график данные только когда выбран сегодняшний день - if (userDate == currentDate) { + if (userDate == currentDate) + { // generateEvent(_id, value); // publishStatusMqtt(_id, value); @@ -341,7 +455,8 @@ class Loging : public IoTItem { // путь вида: /lg/log/1231231.txt unsigned long getFileUnixLocalTime(String path) { return gmtTimeToLocal(selectToMarkerLast(deleteToMarkerLast(path, "."), "/").toInt() + START_DATETIME); } - void setValue(const IoTValue &Value, bool genEvent = true) { + void setValue(const IoTValue &Value, bool genEvent = true) + { value = Value; this->SetDoByInterval(String(value.valD)); SerialPrint("i", "Loging", "setValue:" + String(value.valD)); @@ -349,37 +464,48 @@ class Loging : public IoTItem { } }; -void *getAPI_Loging(String subtype, String param) { - if (subtype == F("Loging")) { +void *getAPI_Loging(String subtype, String param) +{ + if (subtype == F("Loging")) + { return new Loging(param); - } else { + } + else + { return nullptr; } } -class Date : public IoTItem { - private: +class Date : public IoTItem +{ +private: bool firstTime = true; - public: +public: String id; - Date(String parameters) : IoTItem(parameters) { + Date(String parameters) : IoTItem(parameters) + { jsonRead(parameters, F("id"), id); value.isDecimal = false; } - void setValue(const String &valStr, bool genEvent = true) { + void setValue(const String &valStr, bool genEvent = true) + { value.valS = valStr; setValue(value, genEvent); } - void setValue(const IoTValue &Value, bool genEvent = true) { + void setValue(const IoTValue &Value, bool genEvent = true) + { value = Value; regEvent(value.valS, "", false, genEvent); // отправка данных при изменении даты - for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { - if ((*it)->getSubtype() == "Loging") { - if ((*it)->getID() == selectToMarker(id, "-")) { + for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) + { + if ((*it)->getSubtype() == "Loging") + { + if ((*it)->getID() == selectToMarker(id, "-")) + { (*it)->setPublishDestination(TO_MQTT_WS, -1); (*it)->publishValue(); } @@ -387,14 +513,18 @@ class Date : public IoTItem { } } - void setTodayDate() { + void setTodayDate() + { setValue(getTodayDateDotFormated()); - SerialPrint("E", F("Loging"), "today date set " + getTodayDateDotFormated()); + SerialPrint("i", F("Loging"), "today date set " + getTodayDateDotFormated()); } - void doByInterval() { - if (isTimeSynch) { - if (firstTime) { + void doByInterval() + { + if (isTimeSynch) + { + if (firstTime) + { setTodayDate(); firstTime = false; } diff --git a/src/modules/virtual/Loging/modinfo.json b/src/modules/virtual/Loging/modinfo.json index 61b2ad2d..65b5fef8 100644 --- a/src/modules/virtual/Loging/modinfo.json +++ b/src/modules/virtual/Loging/modinfo.json @@ -1,56 +1,63 @@ { - "menuSection": "virtual_elments", - "configItem": [ - { - "global": 0, - "name": "График", - "type": "Writing", - "subtype": "Loging", - "id": "log", - "widget": "chart2", - "page": "Графики", - "descr": "Температура", - "num": 1, - "int": 5, - "logid": "t", - "points": 300 - }, - { - "global": 0, - "name": "График по событию", - "type": "Writing", - "subtype": "Loging", - "id": "log", - "widget": "chart2", - "page": "Графики", - "descr": "Температура", - "int": 0, - "num": 1, - "points": 300 - } - ], - "about": { - "authorName": "Dmitry Borisenko", - "authorContact": "https://t.me/Dmitry_Borisenko", - "authorGit": "https://github.com/DmitryBorisenko33", - "specialThanks": "@itsid1 @Valiuhaaa Serg", - "moduleName": "Loging", - "moduleVersion": "3.0", - "usedRam": { - "esp32_4mb": 15, - "esp8266_4mb": 15 - }, - "title": "Логирование в график", - "moduleDesc": "Расширение позволяющее логировать любую величину в график. Графики доступны в мобильном приложении и в веб интерфейсе. Данные графиков хранятся в встроенной памяти esp. В окне ввода даты можно выбирать день, историю которого вы хотите посмотреть. Старые файлы будут удаляться автоматически после того как объем оставшейся flesh памяти устройства будет менее 20 процентов", - "propInfo": { - "int": "Интервал логирования в мнутах, рекомендуется для esp8266 использоать интервал не менее 5-ти минут", - "logid": "ID величины которую будем логировать", - "points": "Максимальное количество точек в одном файле, может быть не более 300. Не рекомендуется менять этот параметр" - } + "menuSection": "virtual_elments", + "configItem": [ + { + "global": 0, + "name": "График", + "type": "Writing", + "subtype": "Loging", + "id": "log", + "widget": "chart2", + "page": "Графики", + "descr": "Температура", + "num": 1, + "int": 5, + "logid": "t", + "daysSave": 5, + "daysShow": 0, + "points": 300 }, - "defActive": true, - "usedLibs": { - "esp32*": [], - "esp82*": [] + { + "global": 0, + "name": "График по событию", + "type": "Writing", + "subtype": "Loging", + "id": "log", + "widget": "chart2", + "page": "Графики", + "descr": "Температура", + "int": 0, + "num": 1, + "daysSave": 5, + "daysShow": 0, + "points": 300 } + ], + "about": { + "authorName": "Dmitry Borisenko", + "authorContact": "https://t.me/Dmitry_Borisenko", + "authorGit": "https://github.com/DmitryBorisenko33", + "specialThanks": "@itsid1 @Valiuhaaa Serg", + "moduleName": "Loging", + "moduleVersion": "4.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Логирование в график", + "moduleDesc": "Расширение позволяющее логировать любую величину в график. Графики доступны в мобильном приложении и в веб интерфейсе. Данные графиков хранятся в встроенной памяти esp. В окне ввода даты можно выбирать день, историю которого вы хотите посмотреть. Старые файлы будут удаляться автоматически после того как объем оставшейся flesh памяти устройства будет менее 20 процентов", + "propInfo": { + "int": "Интервал логирования в мнутах, рекомендуется для esp8266 использоать интервал не менее 5-ти минут", + "logid": "ID величины которую будем логировать", + "points": "Максимальное количество точек в одном файле, может быть не более 300. Не рекомендуется менять этот параметр", + "daysSave": "Количество дней за которое надо хранить график", + "daysShow": "За какое количество дней отображать график при открытии" + } + }, + "defActive": true, + "usedLibs": { + "esp32*": [], + "esp82*": [], + "bk72*": [] + } } \ No newline at end of file diff --git a/src/modules/virtual/LogingDaily/modinfo.json b/src/modules/virtual/LogingDaily/modinfo.json index 1bfbc10d..3fd3e1ea 100644 --- a/src/modules/virtual/LogingDaily/modinfo.json +++ b/src/modules/virtual/LogingDaily/modinfo.json @@ -1,49 +1,50 @@ { - "menuSection": "virtual_elments", - "configItem": [ - { - "global": 0, - "name": "График дневного расхода", - "type": "Writing", - "subtype": "LogingDaily", - "id": "log", - "widget": "chart3", - "page": "Графики", - "descr": "Температура", - "num": 1, - "int": 1, - "logid": "t", - "points": 200, - "telegram": 0, - "test": 0, - "btn-defvalue": 0, - "btn-reset": "nil" - } - ], - "about": { - "authorName": "Dmitry Borisenko", - "authorContact": "https://t.me/Dmitry_Borisenko", - "authorGit": "https://github.com/DmitryBorisenko33", - "specialThanks": "@itsid1 @Valiuhaaa Serg", - "moduleName": "LogingDaily", - "moduleVersion": "3.1", - "usedRam": { - "esp32_4mb": 15, - "esp8266_4mb": 15 - }, - "title": "График дневного расхода", - "moduleDesc": "Расширение позволяющее логировать накопительные величины и видеть их дневное изменение. Графики доступны в мобильном приложении и в веб интерфейсе. Данные графиков хранятся в встроенной памяти esp", - "propInfo": { - "int": "Интервал логирования в мнутах, частота проверки смены суток в минутах. Не рекомендуется менять", - "logid": "ID накопительной величины которую будем логировать", - "points": "Максимальное количество точек", - "telegram": "График будет отправлять в телеграм репорт с расходами каждый день", - "test": "Параметр необходим для разработчиков. Режим тестирования. График будет обновляться не раз в сутки, а кадый заданный в int интервал." - } - }, - "defActive": true, - "usedLibs": { - "esp32*": [], - "esp82*": [] + "menuSection": "virtual_elments", + "configItem": [ + { + "global": 0, + "name": "График дневного расхода", + "type": "Writing", + "subtype": "LogingDaily", + "id": "log", + "widget": "chart3", + "page": "Графики", + "descr": "Температура", + "num": 1, + "int": 1, + "logid": "t", + "points": 365, + "telegram": 0, + "test": 0, + "btn-defvalue": 0, + "btn-reset": "nil" } + ], + "about": { + "authorName": "Dmitry Borisenko", + "authorContact": "https://t.me/Dmitry_Borisenko", + "authorGit": "https://github.com/DmitryBorisenko33", + "specialThanks": "@itsid1 @Valiuhaaa Serg", + "moduleName": "LogingDaily", + "moduleVersion": "3.1", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "График дневного расхода", + "moduleDesc": "Расширение позволяющее логировать накопительные величины и видеть их дневное изменение. Графики доступны в мобильном приложении и в веб интерфейсе. Данные графиков хранятся в встроенной памяти esp", + "propInfo": { + "int": "Интервал логирования в мнутах, частота проверки смены суток в минутах. Не рекомендуется менять", + "logid": "ID накопительной величины которую будем логировать", + "points": "Максимальное количество точек", + "telegram": "График будет отправлять в телеграм репорт с расходами каждый день", + "test": "Параметр необходим для разработчиков. Режим тестирования. График будет обновляться не раз в сутки, а кадый заданный в int интервал." + } + }, + "defActive": true, + "usedLibs": { + "esp32*": [], + "esp82*": [], + "bk72*": [] + } } \ No newline at end of file diff --git a/src/modules/virtual/LogingHourly/LogingHourly.cpp b/src/modules/virtual/LogingHourly/LogingHourly.cpp new file mode 100644 index 00000000..4d4ed935 --- /dev/null +++ b/src/modules/virtual/LogingHourly/LogingHourly.cpp @@ -0,0 +1,339 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include "ESPConfiguration.h" +#include "NTP.h" + +class LogingHourly : public IoTItem +{ +private: + String logid; + String id; + String filesList = ""; + + String descr; + + int _publishType = -2; + int _wsNum = -1; + + int points; + + int testMode; + + int telegram; + + IoTItem *dateIoTItem; + + // String prevDate = ""; + String prevHourly = ""; + bool firstTimeInit = true; + + // long interval; + +public: + LogingHourly(String parameters) : IoTItem(parameters) + { + jsonRead(parameters, F("logid"), logid); + jsonRead(parameters, F("id"), id); + jsonRead(parameters, F("points"), points); + jsonRead(parameters, F("test"), testMode); + jsonRead(parameters, F("telegram"), telegram); + jsonRead(parameters, F("descr"), descr); + + long interval; + + jsonRead(parameters, F("int"), interval); + interval = interval * 1000 * 60; // приводим к милисекундам + } + + void doByInterval() + { + if (hasHourlyChanged() || testMode == 1) + { + execute(); + } + } + + void execute() + { + // если объект логгирования не был создан + if (!isItemExist(logid)) + { + SerialPrint("E", F("LogingHourly"), "'" + id + "' LogingHourly object not exist, return"); + return; + } + + String value = getItemValue(logid); + + // если значение логгирования пустое + if (value == "") + { + SerialPrint("E", F("LogingHourly"), "'" + id + "' LogingHourly value is empty, return"); + return; + } + + // если время не было получено из интернета + if (!isTimeSynch) + { + SerialPrint("E", F("LogingHourly"), "'" + id + "' Cant LogingHourly - time not synchronized, return"); + return; + } + + String logData; + + float currentValue = value.toFloat(); + // прочитаем предудущее значение + float prevValue = readDataDB(id + "-v").toFloat(); + // сохраним в базу данных текущее значение, понадобится в следующие час + saveDataDB(id + "-v", value); + + float difference = currentValue - prevValue; + + if (telegram == 1) + { + String msg = descr + ": total " + String(currentValue) + ", consumed " + String(difference); + for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) + { + if ((*it)->getSubtype() == "TelegramLT" || "Telegram") + { + (*it)->sendTelegramMsg(false, msg); + } + } + } + + // jsonWriteInt(logData, "x", unixTime - 120); + jsonWriteInt(logData, "x", unixTime - 120); + jsonWriteFloat(logData, "y1", difference); + + // прочитаем путь к файлу последнего сохранения + String filePath = readDataDB(id); + + // если данные о файле отсутствуют, создадим новый + if (filePath == "failed" || filePath == "") + { + SerialPrint("E", F("LogingHourly"), "'" + id + "' file path not found, start create new file"); + createNewFileWithData(logData); + return; + } + + // считаем количество строк и определяем размер файла + size_t size = 0; + int lines = countJsonObj(filePath, size); + SerialPrint("i", F("LogingHourly"), "'" + id + "' " + "lines = " + String(lines) + ", size = " + String(size)); + + // если количество строк до заданной величины и час и дата не менялась + // if (lines <= points && !hasHourlyChanged()) { + if (lines <= points) + { + // просто добавим в существующий файл новые данные + addNewDataToExistingFile(filePath, logData); + } + else + { + String file = readFile(filePath, 2000); + file = deleteBeforeDelimiter(file, "},"); + writeFile(filePath, file); + addNewDataToExistingFile(filePath, logData); + } + } + + void createNewFileWithData(String &logData) + { + logData = logData + ","; + + String path = "/lgh/" + id + "/" + id + ".txt"; // создадим путь вида /lgd/id/id.txt + // создадим пустой файл + if (writeEmptyFile(path) != "success") + { + SerialPrint("E", F("LogingHourly"), "'" + id + "' file writing error, return"); + return; + } + + // запишем в него данные + if (addFile(path, logData) != "success") + { + SerialPrint("E", F("LogingHourly"), "'" + id + "' data writing error, return"); + return; + } + // запишем путь к нему в базу данных + if (saveDataDB(id, path) != "success") + { + SerialPrint("E", F("LogingHourly"), "'" + id + "' db file writing error, return"); + return; + } + SerialPrint("i", F("LogingHourly"), "'" + id + "' file created http://" + WiFi.localIP().toString() + path); + } + + void addNewDataToExistingFile(String &path, String &logData) + { + logData = logData + ","; + if (addFile(path, logData) != "success") + { + SerialPrint("i", F("LogingHourly"), "'" + id + "' file writing error, return"); + return; + }; + SerialPrint("i", F("LogingHourly"), "'" + id + "' LogingHourly in file http://" + WiFi.localIP().toString() + path); + } + const String getTimeLocal_hh() + { + char buf[32]; + sprintf(buf, "%02d", _time_local.hour); + return String(buf); + } + + bool hasHourlyChanged() + { + bool changed = false; + String currentHourly = getTimeLocal_hh(); + if (!firstTimeInit) + { + if (prevHourly != currentHourly) + { + changed = true; + SerialPrint("i", F("NTP"), F("Change hourly event")); +#if defined(ESP8266) + FileFS.gc(); +#endif +#if defined(ESP32) +#endif + } + } + if (isTimeSynch) + firstTimeInit = false; + prevHourly = currentHourly; + return changed; + } + + bool hasDayChanged() + { + bool changed = false; + String currentDate = getTodayDateDotFormated(); + if (!firstTimeInit) + { + if (prevDate != currentDate) + { + changed = true; + SerialPrint("i", F("NTP"), F("Change day event")); +#if defined(ESP8266) + FileFS.gc(); +#endif +#if defined(ESP32) +#endif + } + } + if (isTimeSynch) + firstTimeInit = false; + prevDate = currentDate; + return changed; + } + + void publishValue() + { + String dir = "/lgh/" + id; + filesList = getFilesList(dir); + + SerialPrint("i", F("LogingHourly"), "file list: " + filesList); + + int f = 0; + + while (filesList.length()) + { + String path = selectToMarker(filesList, ";"); + + path = "/lgh/" + id + path; + + f++; + String json = getAdditionalJson(); + if (_publishType == TO_MQTT) + { + publishChartFileToMqtt(path, id, calculateMaxCount()); + } + else if (_publishType == TO_WS) + { + sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); + } + else if (_publishType == TO_MQTT_WS) + { + publishChartFileToMqtt(path, id, calculateMaxCount()); + sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); + } + SerialPrint("i", F("LogingHourly"), String(f) + ") " + path + ", sent"); + + filesList = deleteBeforeDelimiter(filesList, ";"); + } + } + + String getAdditionalJson() + { + String topic = mqttRootDevice + "/" + id; + String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\"}"; + return json; + } + + void clearHistory() + { + String dir = "/lgh/" + id; + cleanDirectory(dir); + } + + // void publishChartToWsSinglePoint(String value) { + // String topic = mqttRootDevice + "/" + id; + // String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + "}]}"; + // String pk = "/string/chart.json|" + json; + // standWebSocket.broadcastTXT(pk); + // } + + void setPublishDestination(int publishType, int wsNum = -1) + { + _publishType = publishType; + _wsNum = wsNum; + } + + String getValue() + { + return ""; + } + /* + void loop() { + if (enableDoByInt) { + currentMillis = millis(); + difference = currentMillis - prevMillis; + if (difference >= interval) { + prevMillis = millis(); + this->doByInterval(); + } + } + } + */ + // просто максимальное количество точек + int calculateMaxCount() + { + // return 1440;//1440 + return 3600; // 1440 + } + + void onModuleOrder(String &key, String &value) + { + if (key == "defvalue") + { + saveDataDB(id + "-v", value); + SerialPrint("i", F("LogingHourly"), "User set default value: " + value); + } + else if (key == "reset") + { + clearHistory(); + SerialPrint("i", F("LogingHourly"), F("User clean chart history")); + } + } +}; + +void *getAPI_LogingHourly(String subtype, String param) +{ + if (subtype == F("LogingHourly")) + { + return new LogingHourly(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/virtual/LogingHourly/modinfo.json b/src/modules/virtual/LogingHourly/modinfo.json new file mode 100644 index 00000000..bcb18142 --- /dev/null +++ b/src/modules/virtual/LogingHourly/modinfo.json @@ -0,0 +1,50 @@ +{ + "menuSection": "virtual_elments", + "configItem": [ + { + "global": 0, + "name": "График часового расхода", + "type": "Writing", + "subtype": "LogingHourly", + "id": "logh", + "widget": "chart3", + "page": "Графики", + "descr": "Расход в час", + "num": 1, + "int": 1, + "logid": "", + "points": 24, + "telegram": 0, + "test": 0, + "btn-defvalue": 0, + "btn-reset": "nil" + } + ], + "about": { + "authorName": "AVAKS", + "authorContact": "https://t.me/avaks", + "authorGit": "https://github.com/avaksru", + "specialThanks": "@itsid1 @Valiuhaaa Serg @Serghei63", + "moduleName": "LogingHourly", + "moduleVersion": "2", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "График часового расхода", + "moduleDesc": "Расширение позволяющее логировать накопительные величины и видеть их часовое изменение. Графики доступны в мобильном приложении и в веб интерфейсе. Данные графиков хранятся в встроенной памяти esp", + "propInfo": { + "int": "Интервал логирования в мнутах, частота проверки смены часа в минутах. Не рекомендуется менять", + "logid": "ID накопительной величины которую будем логировать", + "points": "Максимальное количество точек", + "telegram": "График будет отправлять в телеграм репорт с расходами каждый час", + "test": "Параметр необходим для разработчиков. Режим тестирования. График будет обновляться не раз в час, а кадый заданный в int интервал." + } + }, + "defActive": true, + "usedLibs": { + "esp32*": [], + "esp82*": [], + "bk72*": [] + } +} \ No newline at end of file