Этот PHP-скрипт представляет собой автономный движок автоматизации для системы умного дома на базе MQTT и реляционной базы данных (MySQL). Он постоянно работает в фоне, проверяет условия срабатывания правил и выполняет действия (публикации в MQTT), если условия выполнены.
automations.in_progress у ежедневных правил.Скрипт работает в бесконечном цикле и каждые 5 секунд:
sensor_data.mosquitto_pub в командной строке.automations — правила автоматизации с полями:
id, nameenabled, persistentschedule_type (none/daily/weekly)schedule_time, schedule_daystrigger_topic, condition_operator, condition_valuecondition_script, condition_durationaction_topic, action_payload, delay_secondsconfirmation_topic, dependency_topic, dependency_valuein_progress, last_triggeredsensor_data — данные сенсоров:
id, topic, value, timestampdaily_resets — служебная таблица для отслеживания ежедневного сброса.Этот движок идеально подходит для локального сервера умного дома без облака, где важна автономность, безопасность и гибкость логики.
<?php
// Подключение к БД
try {
$pdo = new PDO("mysql:host=127.0.0.1;dbname=iot_db;charset=utf8mb4", "iot_user", "123456");
$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() . "
";
// Глобальные переменные
$rules = [];
$lastScheduleCheck = 0;
$scheduled = []; // отложенные действия
$pendingConfirmations = []; // ожидание подтверждений
$ruleById = [];
$lastDailyReset = 0; // ← добавь эту строку
$lastTriggeredMinute = []; // ← добавьте эту строку
$conditionStartTime = [];
// Функция перезагрузки правил из БД
function reloadRules($pdo) {
global $rules, $lastRuleReload, $ruleById;
$stmt = $pdo->prepare("SELECT * FROM automations WHERE enabled = 1");
$stmt->execute();
$rules = $stmt->fetchAll(PDO::FETCH_ASSOC);
$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 = '') {
$cmd = "/usr/bin/mosquitto_pub -h 127.0.0.1 -p 1883 -u iot_user -P 123456 -t " . escapeshellarg($topic) . " -m " . escapeshellarg($payload) . " 2>&1";
$output = shell_exec($cmd);
if ($output) {
echo "⚠️ Ошибка отправки для правила '$ruleName': $output\n";
} else {
echo "✅ Успешно отправлено ($ruleName): $topic = $payload\n";
}
}
// Функция проверки условия по БД
// ========== Функция оценки условия правила (поддерживает simple/script) ==========
function evaluateRuleCondition($rule, $pdo) {
global $conditionStartTime; // ← Подключаем глобальную переменную
$ruleId = $rule['id'];
$duration = (int)($rule['condition_duration'] ?? 0);
// Если условие не задано — считаем, что оно истинно и длится 0 секунд (т.е. мгновенно)
// Если условие не задано — считаем, что оно истинно и длится 0 секунд (т.е. мгновенно)
// Условие считается "не заданным", если:
// - для простого типа: оба поля operator и value пустые
// - для скриптового типа: поле script пустое
$isSimpleConditionDefined = !empty($rule['condition_operator']) && !empty($rule['condition_value']);
$isScriptConditionDefined = !empty($rule['condition_script']);
if (!$isSimpleConditionDefined && !$isScriptConditionDefined) {
// Сбрасываем таймер, если условие не задано
unset($conditionStartTime[$ruleId]);
return true;
}
// Получаем последние данные всех сенсоров для скриптов
$stmt = $pdo->query("SELECT topic, value FROM sensor_data ORDER BY id DESC");
$rows = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$sensorData = [];
foreach ($rows as $topic => $rawValue) {
$decoded = json_decode($rawValue, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded) && isset($decoded['state'])) {
$sensorData[$topic] = $decoded['state'];
} else {
$sensorData[$topic] = $rawValue;
}
}
// Функция для проверки простого условия
$checkSimpleCondition = function($triggerTopic, $operator, $value) use ($sensorData) {
if (empty($triggerTopic)) return false;
if (!isset($sensorData[$triggerTopic])) return false;
$actualValue = $sensorData[$triggerTopic];
$numActual = is_numeric($actualValue) ? (float)$actualValue : null;
$numValue = is_numeric($value) ? (float)$value : null;
switch ($operator) {
case '>': return $numActual !== null && $numValue !== null ? $numActual > $numValue : $actualValue > $value;
case '<': return $numActual !== null && $numValue !== null ? $numActual < $numValue : $actualValue < $value;
case '==': return $actualValue == $value;
case '!=': return $actualValue != $value;
case '>=': return $numActual !== null && $numValue !== null ? $numActual >= $numValue : $actualValue >= $value;
case '<=': return $numActual !== null && $numValue !== null ? $numActual <= $numValue : $actualValue <= $value;
default: return false;
}
};
// Проверяем условие
$currentConditionMet = false;
// Простое условие
if (!empty($rule['condition_operator']) && !empty($rule['condition_value'])) {
$currentConditionMet = $checkSimpleCondition($rule['trigger_topic'], $rule['condition_operator'], $rule['condition_value']);
}
// Скриптовое условие
else if (!empty($rule['condition_script'])) {
try {
$script = $rule['condition_script'];
$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);
}
}
$tmpFile = sys_get_temp_dir() . '/eval_' . uniqid() . '.php';
file_put_contents($tmpFile, "<?php return (" . $script . ");");
$result = null;
try {
$result = include $tmpFile;
} catch (Exception $e) {
unlink($tmpFile);
throw new Exception("Ошибка в выражении: " . $e->getMessage());
}
unlink($tmpFile);
$currentConditionMet = (bool)$result;
} catch (Exception $e) {
echo "❌ Ошибка в скриптовом условии правила '" . $rule['name'] . "': " . $e->getMessage() . "\n";
$currentConditionMet = false;
}
}
// Логика для condition_duration
if ($duration <= 0) {
// Если длительность 0 или не задана — возвращаем результат сразу
unset($conditionStartTime[$ruleId]); // Сбрасываем таймер
return $currentConditionMet;
}
if ($currentConditionMet) {
// Условие ВЫПОЛНЯЕТСЯ прямо сейчас
if (!isset($conditionStartTime[$ruleId])) {
// Это первый момент, когда условие стало true — запоминаем время
$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;
}
return false; // fallback
}
function checkCondition($pdo, $triggerTopic, $operator, $value) {
try {
$stmt = $pdo->prepare("SELECT value FROM sensor_data WHERE topic = ? ORDER BY id DESC LIMIT 1");
$stmt->execute([$triggerTopic]);
$rawValue = $stmt->fetchColumn();
if ($rawValue === false) {
echo "⚠️ Нет данных для топика: $triggerTopic\n";
return false;
}
$decoded = json_decode($rawValue, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded) && isset($decoded['state'])) {
$actualValue = $decoded['state'];
} else {
$actualValue = $rawValue;
}
$numActual = is_numeric($actualValue) ? (float)$actualValue : null;
$numValue = is_numeric($value) ? (float)$value : null;
switch ($operator) {
case '>': return $numActual !== null && $numValue !== null ? $numActual > $numValue : $actualValue > $value;
case '<': return $numActual !== null && $numValue !== null ? $numActual < $numValue : $actualValue < $value;
case '==': return $actualValue == $value;
case '!=': return $actualValue != $value;
case '>=': return $numActual !== null && $numValue !== null ? $numActual >= $numValue : $actualValue >= $value;
case '<=': return $numActual !== null && $numValue !== null ? $numActual <= $numValue : $actualValue <= $value;
default: return false;
}
} catch (Exception $e) {
echo "❌ Ошибка проверки условия для '$triggerTopic': " . $e->getMessage() . "\n";
return false;
}
}
// Инициализация
reloadRules($pdo);
echo "✅ Готов! Проверяю правила по расписанию и по БД...\n";
function resetDailyRules($pdo) {
global $pendingConfirmations, $ruleById;
// Сбрасываем in_progress для всех daily-правил
$stmt = $pdo->prepare("UPDATE automations SET in_progress = 0 WHERE schedule_type = 'daily'");
$stmt->execute();
// Записываем факт сброса
$stmt_log = $pdo->prepare("INSERT IGNORE INTO daily_resets (reset_date) VALUES (?)");
$stmt_log->execute([date('Y-m-d')]);
// Очищаем ожидания подтверждений для daily-правил
foreach ($ruleById as $rule) {
if ($rule['schedule_type'] === 'daily' && !empty($rule['confirmation_topic']) && isset($pendingConfirmations[$rule['confirmation_topic']][$rule['id']])) {
unset($pendingConfirmations[$rule['confirmation_topic']][$rule['id']]);
// Если больше никто не ждёт этот топик — удаляем его
if (empty($pendingConfirmations[$rule['confirmation_topic']])) {
unset($pendingConfirmations[$rule['confirmation_topic']]);
}
}
}
echo "🔄 Ежедневный сброс: сняты флаги in_progress и очищены ожидания подтверждений для daily-правил.\n";
}
// Основной цикл — без подписки!
while (true) {
// 🌅 Ежедневный сброс в 00:00:05
$currentHour = (int)date('H');
$currentMinute = (int)date('i');
$currentSecond = (int)date('s');
if ($currentHour === 0 && $currentMinute === 0 && $currentSecond >= 5 && time() - $lastDailyReset > 60) {
resetDailyRules($pdo);
$lastDailyReset = time();
}
// 🔁 Перезагружаем правила КАЖДЫЕ 5 СЕКУНД — чтобы мгновенно реагировать на изменения в БД
reloadRules($pdo);
// 🕔 Проверяем расписание каждые 10 секунд
if (time() - $lastScheduleCheck >= 10) {
$lastScheduleCheck = time();
$currentTime = date('H:i'); // ← Только часы и минуты!
echo "🔍 DEBUG: Текущее время для проверки расписания: $currentTime
";
$currentDay = strtolower(date('D')); // mon, tue, ..., sun
foreach ($rules as $rule) {
if ($rule['schedule_type'] === 'none') continue;
// Проверка зависимости (если есть)
if ($rule['dependency_topic'] && $rule['dependency_value'] !== null) {
$stmt = $pdo->prepare("SELECT value FROM sensor_data WHERE topic = ? ORDER BY timestamp DESC LIMIT 1");
$stmt->execute([$rule['dependency_topic']]);
$rawValue = $stmt->fetchColumn();
if ($rawValue === false) continue;
$dependencyValue = $rawValue;
$json = json_decode($rawValue, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($json) && isset($json['state'])) {
$dependencyValue = $json['state'];
}
if ((string)$dependencyValue !== (string)$rule['dependency_value']) continue;
}
// Проверка времени и дня
$shouldTrigger = false;
if ($rule['schedule_type'] === 'daily' && substr($rule['schedule_time'], 0, 5) === $currentTime) {
$shouldTrigger = true;
} elseif ($rule['schedule_type'] === 'weekly') {
$allowedDays = explode(',', $rule['schedule_days']);
if (substr($rule['schedule_time'], 0, 5) === $currentTime && in_array($currentDay, $allowedDays)) {
$shouldTrigger = true;
}
}
if ($shouldTrigger) {
$currentMinuteKey = date('Y-m-d H:i');
$ruleKey = $rule['id'] . '_' . $currentMinuteKey;
$duration = (int)($rule['condition_duration'] ?? 0);
// Защита от дублирования РАБОТАЕТ ТОЛЬКО для мгновенных условий (duration = 0)
if ($duration <= 0 && isset($lastTriggeredMinute[$ruleKey])) {
echo "🕒 Правило '" . $rule['name'] . "' уже сработало в эту минуту — пропускаем.\n";
continue;
}
echo "⏰ Сработало расписание: " . $rule['name'] . " (" . $rule['schedule_type'] . ")
";
if (evaluateRuleCondition($rule, $pdo)) {
echo "✅ Условие выполнено — выполняем действие.
";
handleAction($rule);
// Запоминаем, что правило сработало в эту минуту
$lastTriggeredMinute[$ruleKey] = time();
} else {
echo "❌ Условие НЕ выполнено — пропускаем правило.
";
}
}
} // <-- Закрываем foreach ($rules as $rule) для расписания
} // <-- Закрываем if (time() - $lastScheduleCheck >= 10)
// 🔍 Проверяем ВСЕ правила по БД (не по MQTT!)
foreach ($rules as $rule) {
if ($rule['schedule_type'] !== 'none') continue; // пропускаем по расписанию — они уже обработаны
// 🚦 Пропускаем, если уже в обработке
if (isset($rule['in_progress']) && !empty($rule['in_progress'])) {
echo "🚧 Правило '" . $rule['name'] . "' пропущено — уже в обработке.\n";
continue;
}
// Проверка зависимости
if ($rule['dependency_topic'] && $rule['dependency_value'] !== null) {
$stmt = $pdo->prepare("SELECT value FROM sensor_data WHERE topic = ? ORDER BY timestamp DESC LIMIT 1");
$stmt->execute([$rule['dependency_topic']]);
$rawValue = $stmt->fetchColumn();
if ($rawValue === false) {
continue;
}
$dependencyValue = $rawValue;
$json = json_decode($rawValue, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($json) && isset($json['state'])) {
$dependencyValue = $json['state'];
}
if ((string)$dependencyValue !== (string)$rule['dependency_value']) {
continue;
}
}
// Проверка условия по БД
if (checkCondition($pdo, $rule['trigger_topic'], $rule['condition_operator'], $rule['condition_value'])) {
echo "🎉 Сработало правило: " . $rule['name'] . "\n";
handleAction($rule);
}
}
// ✅✅ Проверяем, пришли ли подтверждения
checkPendingConfirmations($pdo);
// ⏱ Выполняем отложенные действия
global $scheduled;
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);
// 💤 Ждём 5 секунд перед следующей проверкой
sleep(5);
}
// Общая функция выполнения действия (с поддержкой delay и confirmation)
function handleAction($rule) {
global $scheduled, $pendingConfirmations, $ruleById, $pdo;
// 📝 Обновляем время последнего срабатывания
try {
$stmtLog = $pdo->prepare("UPDATE automations SET last_triggered = NOW() WHERE id = ?");
$stmtLog->execute([$rule['id']]);
echo "📝 Записано время срабатывания для правила ID " . $rule['id'] . "\n";
} catch (Exception $e) {
echo "⚠️ Не удалось обновить last_triggered: " . $e->getMessage() . "\n";
}
// 🚦 Помечаем правило как "в обработке" — чтобы не срабатывало повторно
if (empty($rule['in_progress'])) { // на всякий случай — если уже помечено, не обновляем
$stmt = $pdo->prepare("UPDATE automations SET in_progress = 1 WHERE id = ?");
$stmt->execute([$rule['id']]);
echo "🚦 Правило '" . $rule['name'] . "' помечено как in_progress.\n";
}
$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 {
sendMqttCommand($rule['action_topic'], $rule['action_payload'], $rule['name']);
// Если нужно ждать подтверждения
if (!empty($rule['confirmation_topic'])) {
$expectedState = null;
$actionPayloadDecoded = json_decode($rule['action_payload'], true);
if (is_array($actionPayloadDecoded) && isset($actionPayloadDecoded['state'])) {
$expectedState = $actionPayloadDecoded['state'];
} else {
$expectedState = 'any';
}
if (!isset($pendingConfirmations[$rule['confirmation_topic']])) {
$pendingConfirmations[$rule['confirmation_topic']] = [];
}
$pendingConfirmations[$rule['confirmation_topic']][$rule['id']] = $expectedState;
echo "⏳ Ожидаю подтверждения в топике: " . $rule['confirmation_topic'] . " (ожидается: " . $expectedState . ")\n";
} else {
echo "✅ Правило '" . $rule['name'] . "' выполнено (без подтверждения).
";
// 💥 Удаляем только если галочка "Повторять" НЕ стоит
if (empty($rule['persistent'])) {
$stmt = $pdo->prepare("DELETE FROM automations WHERE id = ?");
$stmt->execute([$rule['id']]);
echo "🗑️ Правило '" . $rule['name'] . "' удалено после выполнения.
";
unset($ruleById[$rule['id']]); // чистим кеш
} else {
echo "🔁 Правило не удалено (галочка 'Повторять' активна).
";
}
}
}
}
// Проверка ожидающих подтверждений через БД
function checkPendingConfirmations($pdo) {
global $pendingConfirmations, $ruleById;
if (empty($pendingConfirmations)) return;
foreach ($pendingConfirmations as $topic => $ruleList) {
// Получаем последнее значение из БД по топику подтверждения
$stmt = $pdo->prepare("SELECT value FROM sensor_data WHERE topic = ? ORDER BY id DESC LIMIT 1");
$stmt->execute([$topic]);
$rawValue = $stmt->fetchColumn();
if ($rawValue === false) continue; // Нет данных — ждём дальше
// У тебя в БД просто "ON" или "OFF" — не JSON, так что оставляем как есть
$actualValue = $rawValue;
// Проверяем каждое правило, которое ждёт подтверждения по этому топику
foreach ($ruleList as $ruleId => $expectedState) {
$rule = $ruleById[$ruleId] ?? null;
if (!$rule) continue;
// Сравниваем: если ожидали "ON", а в БД "ON" — значит, подтверждение получено!
if ((string)$actualValue === (string)$expectedState) {
echo "✅✅ Подтверждение получено для правила: " . $rule['name'] . " — состояние: " . $actualValue . "\n";
// ✅✅ Удаляем только если галочка "Повторять" НЕ стоит
if (empty($rule['persistent'])) {
$stmtDel = $pdo->prepare("DELETE FROM automations WHERE id = ?");
$stmtDel->execute([$ruleId]);
echo "🗑️ Правило '" . $rule['name'] . "' удалено после подтверждения.
";
// Удаляем из кеша, чтобы не было ошибок
unset($ruleById[$ruleId]);
} else {
echo "🔁 Правило не удалено после подтверждения (галочка 'Повторять' активна).
";
}
unset($pendingConfirmations[$topic][$ruleId]); // Удаляем из ожидания
}
}
// Если больше никто не ждёт этот топик — удаляем его из списка
if (empty($pendingConfirmations[$topic])) {
unset($pendingConfirmations[$topic]);
}
}
}
Блог только запустил, все статьи генерирую через нейросеть т.к. лень, возможны ошибки. Просто чтобы вы знали и не запускали ядерный реактор по моим статьям ))
Если у вас есть вопросы, или Нашли неточность? пишите в коментах — вместе поправим и сделаем статью более качественной. Я лично объясню нюансы из практики.
Комментарии
Пока нет комментариев. Будьте первым!