↩️ Назад

Категории

mqtt_listener.php скрипт сервис автоматизации iot-automation.service

23.11.2025 | коды из категории: IOT умный дом

обновлено 20.02.2026 (еще одну проблему с дублями попробор) Добавлено проверка состояния, чтобы не засорять эфир командами. Перед отправкой команды читаtn актуальное состояние устройства из базы данных и сравнивать его с желаемым. Если всё совпадает — пропускаем отправку.

Скрипт /var/www/html/iot/public_html/admin/mqtt_listener.php

Назначение: Движок автоматизаций. Постоянно проверяет таблицу sensor_data и выполняет правила из таблицы automations при срабатывании условий (по триггеру или по расписанию).

Основные функции

Что было изменено (исправлено)

  1. Исправлена ошибка SQL: Unknown column 'id'

    Исходный код пытался выполнить:

    SELECT value FROM sensor_data WHERE topic = ? ORDER BY id DESC LIMIT 1

    Но в таблице sensor_data нет столбца id (используется PRIMARY KEY (topic)).

    Исправление: запрос заменён на:

    SELECT value FROM sensor_data WHERE topic = ?

    Так как для каждого топика хранится только одно (последнее) значение — сортировка не нужна.

  2. Временно отключена логика in_progress

    Движок пытался помечать правила как «в обработке», но в таблице automations отсутствовал столбец in_progress, что вызывало скрытые ошибки и блокировку правил.

    Исправление: блоки кода, работающие с in_progress, закомментированы до добавления соответствующего столбца в БД.

  3. Добавлена защита от повторного срабатывания (частично)

    Хотя полная защита через in_progress отключена, движок использует поле last_triggered для логгирования времени срабатывания. В будущем можно будет добавить проверку «не чаще чем раз в N секунд» на его основе.

Как запускается

Работает как systemd-сервис iot-automation.service, запускается при старте системы и постоянно опрашивает базу данных с интервалом 1 секунда.

Важно!

Этот скрипт — не веб-интерфейс, а фоновый обработчик. Веб-интерфейс (logic.php) только редактирует правила. Без запущенного mqtt_listener.php автоматизации не работают.

<?php

// ============================================
// ПОДКЛЮЧЕНИЕ К БАЗЕ ДАННЫХ
// ============================================
try {
    // Создаем соединение с MySQL базой данных
    // Хост: 127.0.0.1 (локальный), база: iot_db, пользователь: mazzick, пароль: 09540056
    $pdo = new PDO("mysql:host=127.0.0.1;dbname=iot_db;charset=utf8mb4", "mazzick", "09540056");
    
    // Включаем режим исключений для ошибок SQL
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    // Если не удалось подключиться - выводим ошибку и завершаем скрипт
    die("❌ Ошибка подключения к БД: " . $e->getMessage() . "\n");
}

echo "🧠 IoT Automation Engine запущен (режим: только БД + расписание).\n";
// Устанавливаем часовой пояс для корректной работы с временем
date_default_timezone_set('Europe/Moscow');
echo "🌍 Установлен часовой пояс: " . date_default_timezone_get() . "\n";

// ============================================
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ (состояние системы)
// ============================================
$rules = [];                    // Все активные правила автоматизации
$lastRuleReload = 0;            // Время последней загрузки правил
$lastScheduleCheck = 0;          // Время последней проверки расписания
$scheduled = [];                 // Отложенные действия (с задержкой)
$pendingConfirmations = [];       // Ожидающие подтверждения команды
$ruleById = [];                  // Индекс правил по ID для быстрого доступа

$lastTriggeredMinute = [];        // Время последнего срабатывания по минутам (устаревшее)
$conditionStartTime = [];         // Время начала выполнения условия (для длительных условий)
$sensorData = [];                 // Последние значения с сенсоров (ключ=топик, значение=данные)
$lastTriggeredTime = [];           // Время последнего срабатывания правила
$lastTriggerState = [];           // Последнее состояние для каждого правила (чтобы избежать повторов)
$lastActionTime = [];             // Время последнего действия

// ============================================
// ФУНКЦИЯ: Перезагрузка правил из БД
// ============================================
function reloadRules($pdo) {
    global $rules, $lastRuleReload, $ruleById;

    // Загружаем только активные правила (enabled = 1)
    $stmt = $pdo->prepare("SELECT * FROM automations WHERE enabled = 1");
    $stmt->execute();
    $rules = $stmt->fetchAll(PDO::FETCH_ASSOC);
    
    // Создаем индекс для быстрого доступа по ID
    $ruleById = [];
    foreach ($rules as $rule) {
        $ruleById[$rule['id']] = $rule;
    }

    $count = count($rules);
    echo "✅ Загружено $count активных правил.\n";
    if ($count === 0) {
        echo "⚠️ Нет активных правил. Добавьте в logic.php.\n";
    }

    $lastRuleReload = time(); // Запоминаем время загрузки
}

// ============================================
// ФУНКЦИЯ: Отправка команды в MQTT
// ============================================
function sendMqttCommand($topic, $payload, $ruleName = '') {
    // Формируем команду для mosquitto_pub (клиент MQTT)
    // escapeshellarg - экранирует спецсимволы для безопасного выполнения в shell
    $cmd = "/usr/bin/mosquitto_pub -h 127.0.0.1 -p 1883 -u mazzick -P 09540056 -t " . escapeshellarg($topic) . " -m " . escapeshellarg($payload) . " 2>&1";
    
    // Выполняем команду и захватываем вывод (2>&1 перенаправляет stderr в stdout)
    $output = shell_exec($cmd);
    
    if ($output) {
        // Если есть вывод - значит ошибка (обычно команда не выводит ничего при успехе)
        echo "⚠️ Ошибка отправки для правила '$ruleName': $output\n";
    } else {
        echo "✅ Успешно отправлено ($ruleName): $topic = $payload\n";
    }
}

// ============================================
// ФУНКЦИЯ: Проверка, нужно ли пропустить действие
// (если устройство уже в нужном состоянии)
// ============================================
function shouldSkipAction($rule, $pdo) {
    global $sensorData;

    // Декодируем payload действия (JSON)
    $actionPayload = json_decode($rule['action_payload'], true);
    if (!is_array($actionPayload)) return false;

    // Получаем топик состояния (убираем /set из конца)
    $stateTopic = preg_replace('/\/set$/', '', $rule['action_topic']);
    
    // Если топик не изменился - это не топик состояния
    if ($stateTopic === $rule['action_topic']) return false;

    // Если нет данных по этому топику - не можем проверить
    if (!isset($sensorData[$stateTopic])) return false;

    // Получаем текущее состояние
    $currentState = json_decode($sensorData[$stateTopic], true);
    if (!is_array($currentState)) return false;

    // Проверяем каждое поле из команды
    foreach ($actionPayload as $key => $desiredValue) {
        if (!isset($currentState[$key]) || (string)$currentState[$key] !== (string)$desiredValue) {
            return false; // Хотя бы одно поле не совпадает - нужно отправлять
        }
    }
    return true; // Все поля совпадают - можно пропустить
}

// ============================================
// ФУНКЦИЯ: Оценка условия правила
// (для расписания и сложных условий)
// ============================================
function evaluateRuleCondition($rule, $pdo) {
    global $conditionStartTime, $sensorData;

    $ruleId = $rule['id'];
    $duration = (int)($rule['condition_duration'] ?? 0);

    // Определяем тип условия
    $isSimple = !empty($rule['condition_operator']) && !empty($rule['condition_value']);
    $isScript = !empty($rule['condition_script']);

    // Если нет условия - считаем что условие выполнено
    if (!$isSimple && !$isScript) {
        unset($conditionStartTime[$ruleId]);
        return true;
    }

    // Внутренняя функция для проверки простого условия (>, <, == и т.д.)
    $checkSimple = function($triggerTopic, $operator, $value) use (&$sensorData) {
        if (empty($triggerTopic) || !isset($sensorData[$triggerTopic])) return false;
        $actual = $sensorData[$triggerTopic];
        $numActual = is_numeric($actual) ? (float)$actual : null;
        $numValue = is_numeric($value) ? (float)$value : null;

        switch ($operator) {
            case '>':  return $numActual !== null && $numValue !== null ? $numActual > $numValue : $actual > $value;
            case '<':  return $numActual !== null && $numValue !== null ? $numActual < $numValue : $actual < $value;
            case '==': return $actual == $value;
            case '!=': return $actual != $value;
            case '>=': return $numActual !== null && $numValue !== null ? $numActual >= $numValue : $actual >= $value;
            case '<=': return $numActual !== null && $numValue !== null ? $numActual <= $numValue : $actual <= $value;
            default:   return false;
        }
    };

    $currentMet = false;

    // Проверяем простое условие
    if ($isSimple) {
        $currentMet = $checkSimple($rule['trigger_topic'], $rule['condition_operator'], $rule['condition_value']);
    } 
    // Проверяем скриптовое условие
    elseif ($isScript) {
        try {
            $script = $rule['condition_script'];
            
            // Заменяем sensor.название на $sensorData['название']
            $script = preg_replace('/\bsensor\.([a-zA-Z0-9_]+)\b/', '$sensorData[\'$1\']', $script);
            $script = preg_replace('/\bsensor\[([\'"])([a-zA-Z0-9_]+)\1\]/', '$sensorData[\'$2\']', $script);
            $script = str_replace('data', '$data', $script);

            // Проверка безопасности: только разрешенные символы
            if (!preg_match('/^[a-zA-Z0-9_\[\]\.\(\)\+\-\*\/\!\&\|\s\=\>\<\,\{\}\'\"]+$/', $script)) {
                throw new Exception("Недопустимые символы");
            }
            
            // Запрещенные опасные функции
            $forbidden = ['eval', 'exec', 'system', 'shell_exec', 'passthru', 'assert', 'include', 'require'];
            foreach ($forbidden as $word) {
                if (stripos($script, $word) !== false) {
                    throw new Exception("Запрещённая функция: " . $word);
                }
            }
            
            // Сохраняем скрипт во временный файл и подключаем его
            // (безопаснее чем eval())
            $tmpFile = sys_get_temp_dir() . '/eval_' . uniqid() . '.php';
            file_put_contents($tmpFile, "<?php return (" . $script . ");");
            $result = include $tmpFile;
            unlink($tmpFile); // Удаляем временный файл
            
            $currentMet = (bool)$result;
        } catch (Exception $e) {
            echo "❌ Ошибка в скриптовом условии '" . $rule['name'] . "': " . $e->getMessage() . "\n";
            $currentMet = false;
        }
    }

    // Если нет длительности условия - сразу возвращаем результат
    if ($duration <= 0) {
        unset($conditionStartTime[$ruleId]);
        return $currentMet;
    }

    // Обработка длительности условия (условие должно выполняться X секунд)
    if ($currentMet) {
        if (!isset($conditionStartTime[$ruleId])) {
            // Условие только что стало истинным - начинаем отсчет
            $conditionStartTime[$ruleId] = time();
            echo "⏳ Условие для правила '" . $rule['name'] . "' стало истинным. Начинаем отсчет $duration сек.\n";
        } else {
            $elapsed = time() - $conditionStartTime[$ruleId];
            if ($elapsed >= $duration) {
                // Условие выполнялось достаточно долго
                unset($conditionStartTime[$ruleId]);
                echo "✅ Условие для правила '" . $rule['name'] . "' выполнялось $duration сек — можно срабатывать!\n";
                return true;
            } else {
                echo "⏳ Условие выполняется уже $elapsed сек из $duration для правила '" . $rule['name'] . "'.\n";
                return false;
            }
        }
    } else {
        // Условие перестало выполняться - сбрасываем отсчет
        if (isset($conditionStartTime[$ruleId])) {
            echo "⚠️ Условие для правила '" . $rule['name'] . "' перестало выполняться — отсчет сброшен.\n";
            unset($conditionStartTime[$ruleId]);
        }
        return false;
    }
}

// ============================================
// ФУНКЦИЯ: Выполнение действия правила
// ============================================
function handleAction($rule) {
    global $scheduled, $pendingConfirmations, $ruleById, $pdo, $lastActionTime;

    // Обновляем время последнего срабатывания в БД
    try {
        $stmtLog = $pdo->prepare("UPDATE automations SET last_triggered = NOW() WHERE id = ?");
        $stmtLog->execute([$rule['id']]);
        echo "📝 Записано время срабатывания для правила ID " . $rule['id'] . "\n";
        $logId = logAutomationEvent($rule, false);
    } catch (Exception $e) {
        echo "⚠️ Не удалось обновить last_triggered: " . $e->getMessage() . "\n";
        $logId = logAutomationEvent($rule, false);
    }

    $delay = (int)$rule['delay_seconds'];
    
    if ($delay > 0) {
        // Действие с задержкой - добавляем в очередь
        $scheduled[] = [
            'execute_at' => time() + $delay,
            'topic' => $rule['action_topic'],
            'payload' => $rule['action_payload'],
            'rule_name' => $rule['name']
        ];
        echo "⏱ Запланировано на " . date('H:i', time() + $delay) . "\n";
    } else {
        // Немедленное действие
        if (shouldSkipAction($rule, $pdo)) {
            echo "⏭️ Пропускаем действие (устройство уже в нужном состоянии): " . $rule['name'] . "\n";
        } else {
            sendMqttCommand($rule['action_topic'], $rule['action_payload'], $rule['name']);
            $lastActionTime[$rule['id']] = time(); // Запоминаем время действия

            // ========== МЕХАНИЗМ ПОДТВЕРЖДЕНИЯ ==========
            // Получаем топик состояния (без /set)
            $actionTopic = $rule['action_topic'];
            $stateTopic = preg_replace('/\/set$/', '', $actionTopic);
            
            echo "📡 Для правила '{$rule['name']}':\n";
            echo "   Команда: $actionTopic\n";
            echo "   Ожидаем состояние в: $stateTopic\n";

            // Добавляем в список ожидающих подтверждения
            if (!isset($pendingConfirmations[$stateTopic])) {
                $pendingConfirmations[$stateTopic] = [];
            }

            $pendingConfirmations[$stateTopic][$rule['id']] = [
                'rule_id' => $rule['id'],
                'log_id' => $logId,
                'expected_payload' => $rule['action_payload'],
                'rule_name' => $rule['name']
            ];
            echo "⏳ Ожидаю подтверждения в топике: $stateTopic\n";
            // ========== КОНЕЦ МЕХАНИЗМА ПОДТВЕРЖДЕНИЯ ==========

            if (!empty($rule['persistent'])) {
                echo "🔁 Правило не удалено (галочка 'Повторять' активна).\n";
            }
        }
    }
}

// ============================================
// ФУНКЦИЯ: Логирование событий автоматизации
// ============================================
function logAutomationEvent($rule, $skipped = false, $reason = null, $expectedState = null) {
    global $pdo;

    try {
        // Извлекаем ожидаемое состояние из payload если не передано
        if ($expectedState === null && !empty($rule['action_payload'])) {
            $payloadDecoded = json_decode($rule['action_payload'], true);
            if (is_array($payloadDecoded)) {
                // Для Zigbee2MQTT обычно ищем state или другие ключи
                if (isset($payloadDecoded['state'])) {
                    $expectedState = $payloadDecoded['state'];
                } elseif (isset($payloadDecoded['state_l1'])) {
                    $expectedState = $payloadDecoded['state_l1'];
                } elseif (isset($payloadDecoded['brightness'])) {
                    $expectedState = 'brightness:' . $payloadDecoded['brightness'];
                } else {
                    // Если не нашли конкретное поле, сохраняем весь JSON как строку
                    $expectedState = $rule['action_payload'];
                }
            } else {
                $expectedState = $rule['action_payload'];
            }
        }

        // Вставляем запись в лог
        $stmt = $pdo->prepare("
            INSERT INTO automation_log 
            (rule_id, rule_name, action_topic, action_payload, triggered_at, skipped, reason, expected_state)
            VALUES (?, ?, ?, ?, NOW(), ?, ?, ?)
        ");
        $stmt->execute([
            $rule['id'],
            $rule['name'],
            $rule['action_topic'],
            $rule['action_payload'],
            $skipped ? 1 : 0,
            $reason,
            $expectedState
        ]);
        
        // Возвращаем ID вставленной записи
        return $pdo->lastInsertId();
    } catch (Exception $e) {
        error_log("⚠️ Не удалось записать в automation_log: " . $e->getMessage());
        return null;
    }
}

// ============================================
// ФУНКЦИЯ: Проверка ожидающих подтверждений
// ============================================
function checkPendingConfirmations($pdo) {
    global $pendingConfirmations, $ruleById, $sensorData;

    if (empty($pendingConfirmations)) {
        return;
    }

    echo "\n🔍 Проверка подтверждений...\n";
    
    foreach ($pendingConfirmations as $stateTopic => $confirmations) {
        if (!isset($sensorData[$stateTopic])) {
            echo "   ⏳ Нет данных в топике: $stateTopic\n";
            continue;
        }
        
        $currentState = $sensorData[$stateTopic];
        $currentStateDecoded = json_decode($currentState, true);
        
        echo "   📊 Топик $stateTopic = $currentState\n";
        
        foreach ($confirmations as $ruleId => $data) {
            $rule = $ruleById[$ruleId] ?? null;
            if (!$rule) {
                unset($pendingConfirmations[$stateTopic][$ruleId]);
                continue;
            }
            
            $logId = $data['log_id'];
            $expectedPayload = $data['expected_payload'];
            $expectedDecoded = json_decode($expectedPayload, true);
            
            // Проверяем подтверждение
            $confirmed = false;
            
            // СЛУЧАЙ 1: Пришел JSON
            if (is_array($currentStateDecoded)) {
                echo "   📦 Получен JSON формат\n";
                if (is_array($expectedDecoded)) {
                    // Проверяем каждое поле из команды
                    $matchedFields = 0;
                    foreach ($expectedDecoded as $key => $expectedValue) {
                        if (isset($currentStateDecoded[$key])) {
                            $currentValue = $currentStateDecoded[$key];
                            if ((string)$currentValue === (string)$expectedValue) {
                                $matchedFields++;
                                echo "      ✅ Поле $key совпадает: $expectedValue\n";
                            } else {
                                echo "      ❌ Поле $key: ожидалось $expectedValue, получено $currentValue\n";
                            }
                        }
                    }
                    $confirmed = ($matchedFields === count($expectedDecoded));
                }
            } 
            // СЛУЧАЙ 2: Пришла простая строка
            else {
                echo "   📄 Получен простой формат: '$currentState'\n";
                
                // Пробуем извлечь ожидаемое значение из JSON команды
                if (is_array($expectedDecoded)) {
                    // Берем первое значение из JSON команды
                    $expectedValue = reset($expectedDecoded);
                    echo "      Ожидаемое значение из JSON: '$expectedValue'\n";
                    
                    // Сравниваем строки (убираем кавычки)
                    $currentTrimmed = trim($currentState, "\"' \t\n\r\0\x0B");
                    $expectedTrimmed = trim($expectedValue, "\"' \t\n\r\0\x0B");
                    
                    $confirmed = ($currentTrimmed === $expectedTrimmed);
                    echo "      Сравнение: '$currentTrimmed' vs '$expectedTrimmed' = " . ($confirmed ? "✅" : "❌") . "\n";
                } else {
                    // Если команда была простой строкой
                    $confirmed = (trim($currentState) === trim($expectedPayload));
                }
            }
            
            // Обновляем лог
            $stmt = $pdo->prepare("
                UPDATE automation_log 
                SET confirmed = ?, 
                    confirmed_at = NOW(), 
                    actual_state = ? 
                WHERE id = ?
            ");
            $stmt->execute([$confirmed ? 1 : 0, $currentState, $logId]);
            
            if ($confirmed) {
                echo "   ✅✅ ПОДТВЕРЖДЕНО: {$rule['name']}\n";
                unset($pendingConfirmations[$stateTopic][$ruleId]);
                
                // Если правило не повторяющееся - удаляем
                if (empty($rule['persistent'])) {
                    $stmtDel = $pdo->prepare("DELETE FROM automations WHERE id = ?");
                    $stmtDel->execute([$ruleId]);
                    echo "   🗑️ Правило удалено\n";
                    unset($ruleById[$ruleId]);
                }
            } else {
                echo "   ⏳ ОЖИДАЕМ: {$rule['name']}\n";
            }
        }
        
        // Очищаем пустые массивы
        if (empty($pendingConfirmations[$stateTopic])) {
            unset($pendingConfirmations[$stateTopic]);
        }
    }
}

// ============================================
// ИНИЦИАЛИЗАЦИЯ
// ============================================
reloadRules($pdo);
echo "✅ Готов! Проверяю правила по расписанию и по БД...\n";

// ============================================
// ОСНОВНОЙ ЦИКЛ (бесконечный)
// ============================================
while (true) {


    // ========== ПЕРЕЗАГРУЗКА ПРАВИЛ ==========
    reloadRules($pdo);

    // ========== ЗАГРУЗКА ПОСЛЕДНИХ ДАННЫХ СЕНСОРОВ ==========
    try {
        // Пытаемся использовать оконную функцию ROW_NUMBER() (для MySQL 8+)
        $stmt = $pdo->prepare("
            SELECT topic, value
            FROM (
                SELECT topic, value, ROW_NUMBER() OVER (PARTITION BY topic ORDER BY timestamp DESC) as rn
                FROM sensor_data
            ) t
            WHERE rn = 1
        ");
        $stmt->execute();
    } catch (PDOException $e) {
        // Если оконные функции не поддерживаются (старый MySQL) - используем JOIN
        $stmt = $pdo->query("
            SELECT s1.topic, s1.value
            FROM sensor_data s1
            LEFT JOIN sensor_data s2
              ON s1.topic = s2.topic AND s1.timestamp < s2.timestamp
            WHERE s2.topic IS NULL
        ");
    }

    // Загружаем данные в массив $sensorData
    $sensorData = [];
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        $topic = $row['topic'];
        $rawValue = $row['value'];
        $decoded = json_decode($rawValue, true);
        // Если это JSON с полем state - извлекаем только state для удобства
        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded) && isset($decoded['state'])) {
            $sensorData[$topic] = $decoded['state'];
        } else {
            $sensorData[$topic] = $rawValue;
        }
    }

    // ========== ОТЛАДКА ДВИЖЕНИЯ ==========
    if (isset($sensorData['zigbee2mqtt/motion_sensor/presence'])) {
        $p = $sensorData['zigbee2mqtt/motion_sensor/presence'];
        echo "🧪 PRESENCE = [$p]\n";
        if ((string)$p === '1') {
            echo "✅ ДВИЖЕНИЕ ОБНАРУЖЕНО!\n";
        }
    }

    // ========== ПРОВЕРКА РАСПИСАНИЯ ==========
    if (time() - $lastScheduleCheck >= 1) { // Проверяем каждую секунду
        $lastScheduleCheck = time();
        $currentTime = date('H:i:s');
        echo "🔍 DEBUG: Текущее время для проверки расписания: $currentTime\n";
        $currentDay = strtolower(date('D')); // Пн, Вт, Ср и т.д.

        foreach ($rules as $rule) {
            if ($rule['schedule_type'] === 'none') continue; // Пропускаем не расписания

            // Проверяем зависимость (если есть)
            if (!empty($rule['dependency_topic']) && isset($sensorData[$rule['dependency_topic']])) {
                if ((string)$sensorData[$rule['dependency_topic']] !== (string)$rule['dependency_value']) {
                    continue; // Зависимость не выполнена
                }
            }

            $scheduleTime = $rule['schedule_time'];
            if ($currentTime >= $scheduleTime) {
                // Проверяем, не срабатывало ли уже сегодня
                $lastTriggered = $rule['last_triggered'];
                $lastDate = $lastTriggered ? date('Y-m-d', strtotime($lastTriggered)) : null;
                $today = date('Y-m-d');
                if ($lastDate === $today) continue;

                // Для недельного расписания проверяем день недели
                if ($rule['schedule_type'] === 'weekly') {
                    $allowedDays = explode(',', $rule['schedule_days']);
                    if (!in_array($currentDay, $allowedDays)) continue;
                }

                echo "⏰ Сработало расписание: " . $rule['name'] . "\n";
                if (evaluateRuleCondition($rule, $pdo)) {
                    echo "✅ Условие выполнено — выполняем действие.\n";
                    handleAction($rule);
                } else {
                    echo "❌ Условие НЕ выполнено — пропускаем правило.\n";
                }
            }
        }
    }

    // ========== ПРОВЕРКА ТРИГГЕРНЫХ ПРАВИЛ (по изменению данных) ==========
    foreach ($rules as $rule) {
        if ($rule['schedule_type'] !== 'none') continue; // Только не расписания

        // Проверка зависимости
        if (!empty($rule['dependency_topic']) && isset($sensorData[$rule['dependency_topic']])) {
            if ((string)$sensorData[$rule['dependency_topic']] !== (string)$rule['dependency_value']) {
                continue;
            }
        }

        // Проверка триггера
        if (!isset($sensorData[$rule['trigger_topic']])) continue;
        $actual = $sensorData[$rule['trigger_topic']];

        // Проверяем условие
        $condMet = false;
        switch ($rule['condition_operator']) {
            case '==': $condMet = ((string)$actual === (string)$rule['condition_value']); break;
            case '!=': $condMet = ((string)$actual !== (string)$rule['condition_value']); break;
            case '>':  $condMet = ((float)$actual > (float)$rule['condition_value']); break;
            case '<':  $condMet = ((float)$actual < (float)$rule['condition_value']); break;
            case '>=': $condMet = ((float)$actual >= (float)$rule['condition_value']); break;
            case '<=': $condMet = ((float)$actual <= (float)$rule['condition_value']); break;
            default: $condMet = false;
        }

        if ($condMet) {
            // Проверяем, изменилось ли состояние
            $ruleKey = $rule['id'] . '_' . $rule['trigger_topic'];
            $previousState = $lastTriggerState[$ruleKey] ?? null;
            
            // Проверяем минимальный интервал (60 секунд)
            $lastTime = $lastActionTime[$rule['id']] ?? 0;
            $timeSinceLastAction = time() - $lastTime;
            
            // Срабатываем только если состояние изменилось И прошло больше 60 секунд
            if ($condMet && ($previousState === null || $previousState !== $actual) && $timeSinceLastAction >= 60) {
                $lastTriggerState[$ruleKey] = $actual;
                
                if (shouldSkipAction($rule, $pdo)) {
                    echo "⏭️ Устройство уже в нужном состоянии — пропускаем правило: " . $rule['name'] . "\n";
                } else {
                    echo "🎉 Сработало правило: " . $rule['name'] . "\n";
                    handleAction($rule);
                }
            } elseif ($condMet && $timeSinceLastAction < 60) {
                echo "⏱️ Слишком часто! Правило '" . $rule['name'] . "' прошло только " . $timeSinceLastAction . " сек\n";
            }
        } else {
            // Если условие перестало выполняться, сбрасываем запомненное состояние
            $ruleKey = $rule['id'] . '_' . $rule['trigger_topic'];
            if (isset($lastTriggerState[$ruleKey])) {
                unset($lastTriggerState[$ruleKey]);
            }
        }
    }

    // ========== ПРОВЕРКА ПОДТВЕРЖДЕНИЙ ==========
    checkPendingConfirmations($pdo);

    // ========== ВЫПОЛНЕНИЕ ОТЛОЖЕННЫХ ДЕЙСТВИЙ ==========
    foreach ($scheduled as $i => $task) {
        if (time() >= $task['execute_at']) {
            echo "⏰ Выполняю отложенное действие: " . $task['rule_name'] . "\n";
            sendMqttCommand($task['topic'], $task['payload'], $task['rule_name']);
            unset($scheduled[$i]);
        }
    }
    $scheduled = array_values($scheduled); // Переиндексируем массив

    // ========== ПАУЗА 1 СЕКУНДА ==========
    sleep(1);
}



Категории:

Категории

Комментарии

Пока нет комментариев. Будьте первым!

Оставить комментарий

← Назад к списку

Посетителей сегодня: 0
о блоге | карта блога

© Digital Specialist | Не являемся сотрудниками Google, Яндекса и NASA