Страница 1 из 1

Мониторинг аварии котла + телеграмм

Добавлено: Пт дек 26, 2025 8:39 pm
Kirch
Пришлось с ИИ написать сигнализатор для своевременного включения котла, который стал неожиданно отключаться из-за стартовой горелки.
Следит за ритмом включения горелок и снижением температуры ниже среднего минимума, пишет в ТГ причину, включает аварийную лампочку. Пауза на отправку в ТГ 30 минут, что бы не спамить. По восстановлению температуры выше средней "выключает" аварийный статус, лампочку, шлет статус в ТГ и шлет график температуры как картинку. Еще добавил тревогу по прекращению данных с датчика температуры.

Код записан в новый Сценарий "sensor_alarm" и вызывается из скрипта everyminutes, который уже из ClockChime:onNewMinute.
Модуль телеграмм - стандартный в библиотеке MjD. В команды Телеграм еще добавил "Статус котла" с кодом runScript('sensor_alarm', array('force_debug' => 1)); который отправляет текущий статус на телефон по запросу.

Отладил, работает. Но если вручную сильно регулировать температуру котла, то может тоже срабатывать.
Датчик стоит на выходе горячей воды из котла.

Код: Выделить всё

// =====================================================================
// МОНИТОР КОТЛА: РИТМ И СТРАХОВКА (v6.3 - Исправленная версия)
// =====================================================================

// =====================================================================
// ОПИСАНИЕ АЛГОРИТМА РАБОТЫ СКРИПТА
// =====================================================================
// 
// Алгоритм работает по принципу двух независимых "стражей", которые следят за котлом. 
// Если хотя бы один из них фиксирует проблему, поднимается тревога.
// 
// --- СЦЕНАРИИ ТРЕВОГИ ---
// 1. Сценарий "Сбой Ритма" — умный и основной. Он анализирует цикличность работы котла.
// 2. Сценарий "Критическое Падение" — страховочный, реагирует на аномальное падение температуры.
// 
// --- ПОШАГОВАЯ ЛОГИКА РАБОТЫ ---
// 
// ШАГ 1: СБОР И ПОДГОТОВКА ДАННЫХ
//   - Загружается история температуры за период, указанный в $HISTORY_HOURS.
//   - Данные очищаются от аномальных значений (выходящих за рамки $MIN_TEMP_VALID и $MAX_TEMP_VALID).
//   - Проверяется "зависание" датчика: если новые данные не поступали дольше N минут, скрипт завершается с тревогой.
// 
// ШАГ 2: АНАЛИЗ РИТМА (умный сценарий)
//   - Определение "экватора" ($midTemp): Вычисляется средняя температура за короткий период ($ADAPTIVE_WINDOW_HOURS).
//     Этот порог нужен, чтобы точно фиксировать момент начала нагрева. Он адаптивный и "плывет" вместе с режимом котла.
//   - Поиск стартов нагрева ($risingEdges): Алгоритм находит все моменты, когда температура пересекала "экватор" снизу вверх.
//     Это и есть моменты включения котла.
//   - Расчет среднего цикла: Вычисляется среднее время между зафиксированными стартами нагрева.
//   - Прогноз и "дедлайн": На основе последнего старта и среднего цикла прогнозируется время следующего включения.
//     К этому времени добавляется допустимое опоздание ($DELAY_THRESHOLD). Если текущее время превысило этот "дедлайн",
//     а котел так и не включился, фиксируется "Сбой ритма".
// 
// ШАГ 3: АНАЛИЗ ПАДЕНИЯ (страховочный сценарий)
//   - Определение "дна" нормы ($avgLowTemp): находим минимумы по циклам и усредняем последние N минимумов.
//   - Расчет порога тревоги: От "дна" отнимается запас прочности ($DROP_MARGIN). Это и есть критический порог.
//   - Подтверждение падения: Если температура непрерывно находится ниже критического порога дольше времени,
//     указанного в $DROP_TIME_CONFIRM, фиксируется "Критическое падение".
// 
// ШАГ 4: ПРИНЯТИЕ РЕШЕНИЯ
//   - Если сработал хотя бы один из сценариев (Ритм ИЛИ Падение), активируется тревога.
//   - Тревога автоматически сбрасывается, когда котел возобновляет работу и его температура поднимается выше "экватора" ($midTemp).
// 
// =====================================================================


// =====================================================================
// НАСТРОЙКИ (ПАРАМЕТРЫ)
// =====================================================================

// --- ОБЩИЕ НАСТРОЙКИ И ОБЪЕКТЫ СИСТЕМЫ "УМНЫЙ ДОМ" ---
$DEBUG_MODE       = false;  // Режим отладки. true = писать подробный лог и слать тех. данные в Telegram каждый запуск.
// !!! НОВАЯ ПРОВЕРКА: Проверяем, был ли передан параметр 'force_debug' при вызове скрипта.
// Это позволяет принудительно включить режим отладки для одного конкретного запуска.
if (isset($params['force_debug']) && $params['force_debug']) {
    $DEBUG_MODE = true;
}
$SEND_TO_TELEGRAM = true;  // Глобальный "рубильник" для всех сообщений в Telegram.

$sensor_obj  = 'Sensor_temp12';      // Имя объекта, с которого берется температура (источник данных).
$monitor_obj = 'Sensor_general05';   // Имя объекта-хранилища. Он нужен, чтобы хранить состояние тревоги (alarmActive) 
                                     // и время последней тревоги между запусками скрипта.
$relay_obj   = 'Relay27';            // Имя объекта-исполнителя. Это реле, которое будет мигать 
                                     // (или включать сирену/лампу) при тревоге.

// --- СЦЕНАРИЙ "РИТМ": Анализ цикличности работы котла ---
// Эта группа настроек отвечает за "умную" детекцию сбоя, основанную на ожидаемом поведении котла.

$HISTORY_HOURS   = 2;    // ГЛУБИНА АНАЛИЗА (часы). Сколько часов истории температуры нужно загрузить для анализа. 
                         // Это "память" алгоритма. Чем больше значение, тем стабильнее средний цикл, 
                         // но тем медленнее реакция на изменение режима работы. 2-4 часа - оптимально.

$DELAY_THRESHOLD = 5;    // ДОПУСТИМОЕ ОПОЗДАНИЕ (минуты). На сколько минут котел может "опоздать" с включением 
                         // относительно своего среднего ритма, прежде чем будет поднята тревога. 
                         // Это запас времени на случай небольших отклонений в работе.

$DEBOUNCE_SEC    = 180;  // ЗАЩИТА ОТ ДРЕБЕЗГА (секунды). Минимальный интервал между двумя фиксациями "старта нагрева". 
                         // Нужно, чтобы исключить ложные срабатывания, если температура колеблется 
                         // на границе средней температуры. 3 минуты (180 сек) - хороший выбор.

// --- СЦЕНАРИЙ "ПАДЕНИЕ": Страховка от критического падения температуры ---
// Эта группа настроек отвечает за "страховочный" сценарий, который сработает, даже если ритм определить не удалось.

$DROP_MARGIN       = 5.0;  // ЗАПАС ПРОЧНОСТИ (°C). На сколько градусов температура должна упасть НИЖЕ 
                           // своего обычного минимального значения за последние часы, чтобы это считалось 
                           // началом критического падения. Это отступ от нормальной работы.

$DROP_TIME_CONFIRM = 3;    // ВРЕМЯ ПОДТВЕРЖДЕНИЯ ПАДЕНИЯ (минуты). Сколько минут температура должна НЕПРЕРЫВНО 
                           // находиться ниже критического порога, чтобы сработала тревога. 
                           // Это защита от ложных тревог из-за кратковременных провалов.

// --- ФИЛЬТР ДАННЫХ И НОВЫЕ ПАРАМЕТРЫ ---
// Эти параметры нужны для очистки истории и новых проверок.

$MIN_TEMP_VALID = 0.1;   // МИНИМАЛЬНО ДОПУСТИМАЯ ТЕМПЕРАТУРА. Все значения ниже этого будут отброшены.
$MAX_TEMP_VALID = 100.0; // МАКСИМАЛЬНО ДОПУСТИМАЯ ТЕМПЕРАТУРА. Все значения выше этого будут отброшены.

$NO_DATA_THRESHOLD    = 10;   // ПОРОГ ЗАВИСАНИЯ ДАТЧИКА (минуты). Если с момента последнего полученного значения
                              // прошло больше этого времени, будет отправлена тревога о зависании датчика.

$ADAPTIVE_WINDOW_HOURS = 1;    // ДЛИНА АДАПТИВНОГО ОКНА (час). Длина "короткого" периода для вычисления 
                              // актуальной средней температуры ($midTemp). Позволяет алгоритму быстро 
                              // адаптироваться к изменению погодных условий.

// Параметры для расчета средней нижней по минимумам
$CYCLES_FOR_AVG = 5;
$MIN_RISE_DELTA = 0.2; // ИСПРАВЛЕНО: было 0.2, теперь используется переменная

// ===== ИНИЦИАЛИЗАЦИЯ TELEGRAM =====
$GLOBALS['telegram_module'] = null;
if ($SEND_TO_TELEGRAM) {
    include_once(DIR_MODULES . 'telegram/telegram.class.php');
    if (class_exists('telegram')) {
        $GLOBALS['telegram_module'] = new telegram();
    }
}

// ===== ФУНКЦИЯ ОТПРАВКИ =====
function sendMessage($text) {
    $SEND_TO_TELEGRAM = isset($GLOBALS['SEND_TO_TELEGRAM']) ? $GLOBALS['SEND_TO_TELEGRAM'] : true;
    $telegram_module = isset($GLOBALS['telegram_module']) ? $GLOBALS['telegram_module'] : null;
    if (!$SEND_TO_TELEGRAM || !is_object($telegram_module)) {
        return;
    }
    if (method_exists($telegram_module, 'sendMessageToAdmin')) {
        $telegram_module->sendMessageToAdmin($text);
    }
}

/**
 * Создание и отправка PNG-графика температуры.
 * Принимает нормализованный $history (массив элементов ['VALUE','ADDED']),
 * имя датчика, текущую температуру и длительность в часах (для подписи).
 * Возвращает true при успешной отправке.
 */
function createAndSendTemperatureChart($normalized, $sensor_obj, $currentTemp, $historyHours = 2) {
    if (empty($normalized) || !is_array($normalized)) {
        DebMes("ГРАФИК: Нет данных для построения");
        return false;
    }

    $width = 1000;
    $height = 420;
    $padding = 70;
    $chartWidth = $width - 2 * $padding;
    $chartHeight = $height - 2 * $padding;

    $image = imagecreatetruecolor($width, $height);
    if (!$image) {
        DebMes("ГРАФИК: Не удалось создать изображение");
        return false;
    }

    // цвета
    $white = imagecolorallocate($image, 255, 255, 255);
    $black = imagecolorallocate($image, 0, 0, 0);
    $blue  = imagecolorallocate($image, 30, 144, 255);
    $lightGray = imagecolorallocate($image, 235, 235, 235);
    $red = imagecolorallocate($image, 200, 40, 40);
    $darkBlue = imagecolorallocate($image, 0, 80, 160);

    imagefill($image, 0, 0, $white);

    // шрифт (попытаться найти)
    $font = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf';
    if (!file_exists($font)) {
        $font = '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf';
    }
    $useFont = file_exists($font);

    // фиксированный диапазон для видимости — можно менять
    $minVal = 40;
    $maxVal = 80;
    $valRange = max(0.1, $maxVal - $minVal);

    // время
    $minTime = PHP_INT_MAX;
    $maxTime = PHP_INT_MIN;
    foreach ($normalized as $p) {
        if ($p['ADDED'] < $minTime) $minTime = $p['ADDED'];
        if ($p['ADDED'] > $maxTime) $maxTime = $p['ADDED'];
    }
    $timeRange = max(1, $maxTime - $minTime);

    // Заголовок
    $title = "График температуры: {$sensor_obj}";
    if ($useFont) {
        imagettftext($image, 14, 0, $padding, 28, $darkBlue, $font, $title);
    } else {
        imagestring($image, 5, $padding, 8, $title, $darkBlue);
    }

    // Сетка Y и подписи
    $rows = 5;
    for ($i = 0; $i <= $rows; $i++) {
        $y = $padding + ($chartHeight * $i / $rows);
        imageline($image, $padding, $y, $width - $padding, $y, $lightGray);
        $value = $maxVal - ($valRange * $i / $rows);
        if ($useFont) {
            imagettftext($image, 10, 0, 8, $y + 4, $black, $font, number_format($value, 1) . "°C");
        } else {
            imagestring($image, 2, 6, $y - 6, number_format($value, 0), $black);
        }
    }

    // Сетка X и подписи (6 меток)
    $cols = 6;
    for ($i = 0; $i <= $cols; $i++) {
        $x = $padding + ($chartWidth * $i / $cols);
        imageline($image, $x, $padding, $x, $height - $padding, $lightGray);
        $t = $minTime + ($timeRange * $i / $cols);
        $label = date("d.m H:i", $t);
        if ($useFont) imagettftext($image, 9, 0, $x - 28, $height - $padding + 18, $black, $font, $label);
        else imagestring($image, 2, $x - 28, $height - $padding + 2, $label, $black);
    }

    // рамка
    imagerectangle($image, $padding, $padding, $width - $padding, $height - $padding, $black);

    // рисуем линию
    $prevX = null; $prevY = null;
    foreach ($normalized as $p) {
        $x = $padding + (($p['ADDED'] - $minTime) / $timeRange) * $chartWidth;
        $val = max($minVal, min($maxVal, $p['VALUE']));
        $y = $height - $padding - (($val - $minVal) / $valRange) * $chartHeight;

        if ($prevX !== null) {
            imagesetthickness($image, 2);
            imageline($image, (int)$prevX, (int)$prevY, (int)$x, (int)$y, $blue);
            imagesetthickness($image, 1);
        }
        $prevX = $x; $prevY = $y;
    }

    // текущая точка и подпись
    if ($prevX !== null) {
        imagefilledellipse($image, (int)$prevX, (int)$prevY, 10, 10, $red);
        $label = number_format($currentTemp, 1) . "°C";
        if ($useFont) {
            imagettftext($image, 10, 0, (int)$prevX + 12, (int)$prevY + 4, $black, $font, $label);
        } else {
            imagestring($image, 3, (int)$prevX + 12, (int)$prevY - 6, $label, $black);
        }
    }

    // инфо-строка
    $actualMin = min(array_column($normalized, 'VALUE'));
    $actualMax = max(array_column($normalized, 'VALUE'));
    $avgVal = array_sum(array_column($normalized, 'VALUE')) / count($normalized);
    $info = sprintf("Период: %dч  Мин: %.1f°C  Макс: %.1f°C  Средн: %.1f°C  Точек: %d",
        $historyHours, $actualMin, $actualMax, $avgVal, count($normalized));
    if ($useFont) imagettftext($image, 9, 0, $padding, $height - 10, $black, $font, $info);
    else imagestring($image, 2, $padding, $height - 25, $info, $black);

    // Сохраняем файл во временную папку
    $filename = "boiler_chart_" . preg_replace('/[^a-zA-Z0-9_]/', '_', $sensor_obj) . "_" . time() . ".png";
    $basePath = defined('ROOT') ? rtrim(ROOT, '/') . "/cms/cached/" : sys_get_temp_dir() . "/";
    if (!is_dir($basePath)) @mkdir($basePath, 0755, true);
    $filepath = $basePath . $filename;

    $saved = imagepng($image, $filepath);
    imagedestroy($image);

    if (!$saved) {
        DebMes("ГРАФИК: Ошибка сохранения $filepath");
        return false;
    }

    // Отправка в Telegram — используем уже созданный модуль, если есть
    $telegram_module = isset($GLOBALS['telegram_module']) && is_object($GLOBALS['telegram_module'])
        ? $GLOBALS['telegram_module']
        : null;

    if (!is_object($telegram_module)) {
        if (file_exists(DIR_MODULES . 'telegram/telegram.class.php')) {
            include_once(DIR_MODULES . 'telegram/telegram.class.php');
            if (class_exists('telegram')) $telegram_module = new telegram();
        }
    }

    if (!is_object($telegram_module)) {
        DebMes("ГРАФИК: Telegram модуль не доступен, файл сохранён: $filepath");
        return false;
    }

    $caption = sprintf("t котла %.1f°C, за %dч, мин: %.1f°C макс: %.1f°C",
        $currentTemp, $historyHours, $actualMin, $actualMax);

    $ok = false;
    if (method_exists($telegram_module, 'sendImageToAdmin')) {
        try {
            $ok = (bool)$telegram_module->sendImageToAdmin($filepath, $caption);
        } catch (Exception $e) {
            DebMes("ГРАФИК: Ошибка отправки в Telegram: " . $e->getMessage());
            $ok = false;
        }
    } else {
        DebMes("ГРАФИК: метод sendImageToAdmin не найден в telegram_module");
    }

    @unlink($filepath);
    return $ok;
}

// ===== ИНИЦИАЛИЗАЦИЯ =====
$currentTemp = (float)gg($sensor_obj . '.value');
$currentTime = time();
$alarmActive = (int)gg($monitor_obj . '.alarmActive');
$lastAlarmTs = (int)gg($monitor_obj . '.lastAlarmTime');
$lastAlarmDate = $lastAlarmTs ? date('Y-m-d H:i:s', $lastAlarmTs) : "Нет";

// ===== ИСТОРИЯ =====
$histStart = $currentTime - ($HISTORY_HOURS * 3600);
$history = getHistory($sensor_obj . '.value', $histStart, $currentTime);

if (!is_array($history) || count($history) === 0) {
    DebMes("КОТЕЛ: История пуста или getHistory вернуло немассив. Проверьте Keep history у свойства {$sensor_obj}.value");
    sendMessage("КОТЕЛ: ❌ История пуста! Проверьте датчик {$sensor_obj}. T={$currentTemp}°C");
    return;
}

// ===== НОРМАЛИЗАЦИЯ =====
$normalized = array();
foreach ($history as $item) {
    // Получаем значение
    $rawVal = null;
    if (isset($item['VALUE'])) $rawVal = $item['VALUE'];
    elseif (isset($item['value'])) $rawVal = $item['value'];
    elseif (isset($item['val'])) $rawVal = $item['val'];

    // Получаем время
    $rawTime = null;
    if (isset($item['ADDED'])) $rawTime = $item['ADDED'];
    elseif (isset($item['added'])) $rawTime = $item['added'];
    elseif (isset($item['TIME'])) $rawTime = $item['TIME'];
    elseif (isset($item['time'])) $rawTime = $item['time'];
    elseif (isset($item['timestamp'])) $rawTime = $item['timestamp'];

    // Приведение значения к float
    if ($rawVal === null) continue;
    $val = (float)$rawVal;

    // Приведение времени к unix секундам
    $ts = 0;
    if (is_numeric($rawTime)) {
        $ts = (int)$rawTime;
        if ($ts > 1000000000000) {
            $ts = (int)round($ts / 1000);
        } elseif ($ts > 10000000000) {
            $ts = (int)round($ts / 1000);
        }
    } else {
        $parsed = strtotime($rawTime);
        if ($parsed === false) {
            continue;
        }
        $ts = $parsed;
    }

    if ($ts <= 0) continue;

    $normalized[] = array('VALUE' => $val, 'ADDED' => $ts);
}

if (count($normalized) === 0) {
    DebMes("КОТЕЛ: После нормализации история пуста. Проверьте формат ADDED и ключи истории.");
    return;
}

// ===== ФИЛЬТРАЦИЯ И СОРТИРОВКА =====
$normalized = array_values(array_filter($normalized, function($p) use ($MIN_TEMP_VALID, $MAX_TEMP_VALID) {
    $v = (float)$p['VALUE'];
    return ($v >= $MIN_TEMP_VALID && $v <= $MAX_TEMP_VALID);
}));

if (count($normalized) < 6) {
    DebMes("КОТЕЛ: Недостаточно валидных точек после фильтрации. count=" . count($normalized));
}

usort($normalized, function($a, $b) {
    if ($a['ADDED'] == $b['ADDED']) return 0;
    return ($a['ADDED'] < $b['ADDED']) ? -1 : 1;
});

$history = $normalized;

// ===== НОВОЕ: ДЕТЕКЦИЯ ЗАВИСАНИЯ ДАТЧИКА =====
$lastPoint = end($history);
$lastPointTime = isset($lastPoint['ADDED']) ? (int)$lastPoint['ADDED'] : 0;
$timeSinceLastPoint = $currentTime - $lastPointTime;

if ($lastPointTime > 0 && $timeSinceLastPoint > ($NO_DATA_THRESHOLD * 60)) {
    $minutesAgo = round($timeSinceLastPoint / 60);
    $msg = "КОТЕЛ: ❌ Датчик '{$sensor_obj}' завис! Последние данные были {$minutesAgo} минут назад.";
    DebMes($msg);
    sendMessage($msg);
    return; // Прерываем выполнение
}

// ===== АНАЛИЗ =====
$vals = array_column($history, 'VALUE');
$hMin = min($vals);
$hMax = max($vals);

// ===== НОВОЕ: АДАПТИВНЫЙ MID (СКОЛЬЗЯЩЕЕ ОКНО) =====
$adaptiveWindowStart = $currentTime - ($ADAPTIVE_WINDOW_HOURS * 3600);
$windowHistory = array_filter($history, function($p) use ($adaptiveWindowStart) {
    return $p['ADDED'] >= $adaptiveWindowStart;
});

if (count($windowHistory) > 2) {
    $windowVals = array_column($windowHistory, 'VALUE');
    // ИСПРАВЛЕНИЕ 1: Критическая ошибка синтаксиса
    $midTemp = (min($windowVals) + max($windowVals)) / 2.0;
} else {
    // Если в окне мало данных, используем общую историю
    $midTemp = ($hMin + $hMax) / 2.0;
}

// NOTE: исправление потенциальной ошибки: если $windowVals не корректен, обеспечить midTemp корректным
if (!isset($midTemp) || !is_numeric($midTemp)) {
    $midTemp = ($hMin + $hMax) / 2.0;
}

// ===== НОВЫЙ БЛОК: РАСЧЁТ СРЕДНЕЙ НИЖНЕЙ ПО МИНИМУМАМ ЦИКЛОВ =====
// Сначала собираем плоский массив температур T из history
$T = array_column($history, 'VALUE');

// По предложенному тобой алгоритму:
$cycleMins = [];

for ($i = 2; $i < count($T) - 2; $i++) {
    if (
        $T[$i-1] > $T[$i] &&
        $T[$i]   < $T[$i+1] &&
        // ИСПРАВЛЕНИЕ 2: Используем переменную из настроек вместо константы
        ($T[$i+1] - $T[$i]) > $MIN_RISE_DELTA 
    ) {
        $cycleMins[] = $T[$i];
    }
}

// Если мало минимумов — делаем fallback (используем локальные минимумы старого алгоритма)
if (count($cycleMins) < 2) {
    $localMinima = array();
    for ($i = 2; $i < count($history) - 2; $i++) {
        $v = $history[$i]['VALUE'];
        if ($v < $history[$i-1]['VALUE'] && $v < $history[$i+1]['VALUE']) {
            $localMinima[] = $v;
        }
    }
    // если нашли локальные minima и cycleMins пуст — заполним
    if (count($cycleMins) == 0 && count($localMinima) > 0) {
        $cycleMins = $localMinima;
    }
}

// Усреднение последних N минимумов
$N = $CYCLES_FOR_AVG;
$avgLowTemp = $hMin; // безопасный дефолт
if (count($cycleMins) > 0) {
    $recent = array_slice($cycleMins, -$N);
    if (count($recent) > 0) {
        $avgLowTemp = array_sum($recent) / count($recent);
    }
}

// ===== РИТМ: Поиск взлетов (Rising Edges) =====
$risingEdges = array();
$prevVal = $history[0]['VALUE'];
$prevTime = $history[0]['ADDED'];

for ($i = 1; $i < count($history); $i++) {
    $currVal = (float)$history[$i]['VALUE'];
    $currTime = (int)$history[$i]['ADDED'];

    if ($prevVal < $midTemp && $currVal >= $midTemp && ($currVal - $prevVal) > 0.01) {
        $lastEdge = end($risingEdges);
        $lastEdgeTs = $lastEdge ? (int)$lastEdge : 0;
        if (empty($risingEdges) || (($currTime - $lastEdgeTs) > $DEBOUNCE_SEC)) {
            $risingEdges[] = $currTime;
        }
    }
    $prevVal = $currVal;
    $prevTime = $currTime;
}

// Расчёт параметров цикла
$avgCycleMinutes = 0.0;
$lastRiseTime = 0;
$nextExpectedRise = 0;
$deadline = 0;
$cycleDetected = false;

if (count($risingEdges) >= 2) {
    $intervals = array();
    for ($i = 1; $i < count($risingEdges); $i++) {
        $intervals[] = $risingEdges[$i] - $risingEdges[$i-1];
    }
    
    // ИСПРАВЛЕНИЕ 5a: Защита от деления на ноль
    if (count($intervals) > 0) {
        $avgSeconds = array_sum($intervals) / count($intervals);
        $avgCycleMinutes = round($avgSeconds / 60.0, 1);

        $lastRiseTime = end($risingEdges);
        $nextExpectedRise = (int)round($lastRiseTime + $avgSeconds);
        $deadline = $nextExpectedRise + ($DELAY_THRESHOLD * 60);
        $cycleDetected = true;
    }
}

// ===== ПАДЕНИЕ: Анализ =====
$dropThreshold = $avgLowTemp - $DROP_MARGIN;
$dropAlarmInfo = "Норма (T > Порога)";

// Считаем непрерывный период времени в конце истории, где VALUE < dropThreshold
$revHist = array_reverse($history);
$belowStartTs = null;
$belowEndTs = null;
foreach ($revHist as $p) {
    if ($p['VALUE'] < $dropThreshold) {
        $ts = (int)$p['ADDED'];
        if ($belowEndTs === null) $belowEndTs = $ts;
        $belowStartTs = $ts;
    } else {
        break;
    }
}

$belowDurationSec = 0;
if ($belowStartTs !== null && $belowEndTs !== null) {
    $belowDurationSec = max(0, $belowEndTs - $belowStartTs);
}

// Если сейчас ниже порога - считаем таймер подтверждения
if ($currentTemp < $dropThreshold) {
    $neededSec = $DROP_TIME_CONFIRM * 60;
    $remainingSec = $neededSec - $belowDurationSec;
    if ($remainingSec <= 0) {
        $dropAlarmInfo = "УЖЕ СРАБОТАЛО";
    } else {
        $dropAlarmInfo = date('H:i:s', $currentTime + $remainingSec) . " (через " . ceil($remainingSec/60) . " мин)";
    }
}

// ===== ПРОВЕРКА УСЛОВИЙ =====
$failRhythm = false;
$failDrop = false;

if ($cycleDetected) {
    if ($currentTime > $deadline && $currentTemp < $midTemp) {
        $failRhythm = true;
    }
}

if ($belowDurationSec >= ($DROP_TIME_CONFIRM * 60)) {
    $failDrop = true;
}

// ===== ЛОГИРОВАНИЕ =====
if ($DEBUG_MODE) {
    $statusText = ($alarmActive || $failRhythm || $failDrop) ? "Тревога" : "Наблюдение";

    $rhythmLast = $lastRiseTime ? date('Y-m-d H:i:s', $lastRiseTime) : "Нет данных";
    $rhythmNext = $nextExpectedRise ? date('Y-m-d H:i:s', $nextExpectedRise) : "Нет данных";
    $rhythmDeadline = $deadline ? date('Y-m-d H:i:s', $deadline) : "Нет данных";

    // Лог MajorDomo (как в рабочей версии)
    $logMsg = "\n=== МОНИТОР КОТЛА ===";
    $logMsg .= "\nТекущий статус: $statusText";
    $logMsg .= "\nСценарий \"ритм\":";
    $logMsg .= " средняя t " . round($midTemp, 1) . "°C (адапт.),"; // Добавлено "(адапт.)" для ясности
    $logMsg .= " средний цикл " . $avgCycleMinutes . " мин,";
    $logMsg .= " последнее событие пересечения $rhythmLast,";
    $logMsg .= " ожидаемое время следующего события $rhythmNext,";
    $logMsg .= " время тревоги если события не будет $rhythmDeadline,";
    $logMsg .= " дата-время последней тревоги $lastAlarmDate.";
    $logMsg .= "\n(РisingEdges count=" . count($risingEdges) . ")";

    $logMsg .= "\nСценарий \"падение\":";
    $logMsg .= " средняя нижняя t " . round($avgLowTemp, 1) . "°C (мин $hMin),";
    $logMsg .= " уставка/разброс от t " . $DROP_MARGIN . "°C,";
    $logMsg .= " температура тревоги " . round($dropThreshold, 1) . "°C,";
    $logMsg .= " время тревоги если температура будет ниже тревожной: $dropAlarmInfo.";
    $logMsg .= "\n(Время ниже порога sec=" . $belowDurationSec . ")";

    DebMes($logMsg);

    // Telegram (красивый формат)
    $telegramMsg = " <b>МОНИТОР КОТЛА</b>\n\n";
    $telegramMsg .= " <b>Статус:</b> $statusText\n\n";
    $telegramMsg .= " <b>Температура:</b> " . round($currentTemp, 1) . "°C\n";
    $telegramMsg .= " <b>Диапазон (общий):</b> " . round($hMin, 1) . " … " . round($hMax, 1) . "°C\n";
    $telegramMsg .= "⚖️ <b>Средняя (адапт.):</b> " . round($midTemp, 1) . "°C\n\n";
    
    $telegramMsg .= "⏱ <b>Ритм нагрева:</b>\n";
    $telegramMsg .= "  • Циклов: " . count($risingEdges) . "\n";
    $telegramMsg .= "  • Средний: " . $avgCycleMinutes . " мин\n";
    $telegramMsg .= "  • Последний: " . ($lastRiseTime ? date('H:i', $lastRiseTime) : 'Нет') . "\n";
    $telegramMsg .= "  • Следующий: " . ($nextExpectedRise ? date('H:i', $nextExpectedRise) : 'Нет') . "\n";
    $telegramMsg .= "  • Дедлайн: " . ($deadline ? date('H:i', $deadline) : 'Нет') . "\n\n";
    
    $telegramMsg .= " <b>Падение:</b>\n";
    $telegramMsg .= "  • Порог: " . round($dropThreshold, 1) . "°C\n";
    $telegramMsg .= "  • Ниже: " . (int)$belowDurationSec . " сек\n";
    $telegramMsg .= "  • Статус: $dropAlarmInfo\n\n";
    
    $statusRhythm = $failRhythm ? "❌ СБОЙ" : "✅ OK";
    $statusDrop = $failDrop ? "❌ СБОЙ" : "✅ OK";
    $telegramMsg .= " <b>Проверки:</b>\n";
    $telegramMsg .= "  • Ритм: $statusRhythm\n";
    $telegramMsg .= "  • Падение: $statusDrop";
    
    sendMessage($telegramMsg);

    // Отправить график при DEBUG
    $chartSentDebug = createAndSendTemperatureChart($history, $sensor_obj, $currentTemp, $HISTORY_HOURS);
    if ($chartSentDebug) {
        DebMes("КОТЕЛ: DEBUG - график отправлен");
    } else {
        DebMes("КОТЕЛ: DEBUG - не удалось отправить график");
    }
}

// --------------------- УПРАВЛЕНИЕ АВАРИЕЙ ---------------------------
if ($failRhythm || $failDrop) {
    $reason = $failRhythm ? "Сбой ритма" : "Падение температуры";

    if (!$alarmActive) {
        sg($monitor_obj . '.alarmActive', 1);
        sg($monitor_obj . '.lastAlarmTime', time());
        sg($monitor_obj . '.lastMsgTime', time());

	    $logMessage = "КОТЕЛ: АВАРИЯ! Причина: $reason. Текущая температура: {$currentTemp}°C";
    	DebMes($logMessage); // <<-- ДОБАВЛЕНА СТРОКА ДЛЯ ЛОГА

        say("Внимание! Авария котла. $reason", 5);
        // ИСПРАВЛЕНИЕ 5b: Опечатка в иконке
        sendMessage("🚨 <b>АВАРИЯ КОТЛА</b>\n\nПричина: $reason\nТемпература: {$currentTemp}°C");

        // Отправить график при входе в аварию (один раз при первом срабатывании)
        $chartSentAlarm = createAndSendTemperatureChart($history, $sensor_obj, $currentTemp, $HISTORY_HOURS);
        if ($chartSentAlarm) {
            DebMes("КОТЕЛ: АВАРИЯ - график отправлен");
        } else {
            DebMes("КОТЕЛ: АВАРИЯ - не удалось отправить график");
        }
    }

    // Мигание
    cm($relay_obj . '.turnOn');
    SetTimeOut("boiler_blink_" . time(), "cm('" . $relay_obj . ".turnOff');", 50);

    // Напоминание
    $lastMsg = (int)gg($monitor_obj . '.lastMsgTime');
    if ($alarmActive && (time() - $lastMsg > 1800)) {
        say("Котел в аварии. T=$currentTemp", 2);
        sendMessage("⏰ <b>КОТЕЛ В АВАРИИ</b>\n\nТемпература: {$currentTemp}°C");
        sg($monitor_obj . '.lastMsgTime', time());
    }

} elseif ($alarmActive && $currentTemp >= $midTemp) {
    // Сброс тревоги
    sg($monitor_obj . '.alarmActive', 0);
    cm($relay_obj . '.turnOff');

    $msg = "Котел восстановлен. T=$currentTemp";
    DebMes("КОТЕЛ: РАБОТА ВОССТАНОВЛЕНА. Текущая температура: {$currentTemp}°C"); // <<-- ДОБАВЛЕНА СТРОКА ДЛЯ ЛОГА
    
    say($msg, 2);
    sendMessage("✅ <b>КОТЕЛ ВОССТАНОВЛЕН</b>\n\nТемпература: {$currentTemp}°C");
}

Re: Мониторинг аварии котла + телеграмм

Добавлено: Вс дек 28, 2025 6:34 am
AK1
С газовым котлом не надо баловаться.
Надо вызвать грамотного спеца и восстановить нормальную работу котла.
Сигнализатор неисправности полезен.
Но только сигнализатор.