Следит за ритмом включения горелок и снижением температуры ниже среднего минимума, пишет в ТГ причину, включает аварийную лампочку. Пауза на отправку в ТГ 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");
}