Категории

Автоматизация логики в системах типа Home Assistant

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

сохранил себе пример кода для логики в самописную систему управления iot устройствами

про Автоматизация логики в системах типа Home Assistant

Если ты когда-нибудь задумывался, как умный дом сам включает свет, закрывает кран или запускает вентилятор — добро пожаловать в мир автоматизации. Сегодня я расскажу, как это работает “под капотом”, без кода, только суть. Чтобы ты мог понять — и повторить у себя, даже если пишешь всё с нуля.

🔹 Что такое автоматизация?

Автоматизация — это правило вида:

“ЕСЛИ что-то произошло → ТО сделай что-то другое”

Примеры:

Всё просто. Но чтобы это работало — нужны три кита: Триггер, Условие и Действие.

📡 1. Триггер — что запускает автоматизацию

Это “спусковой крючок”. Событие, с которого всё начинается. В системах вроде Home Assistant (HA) или твоей самописной панели — триггером может быть:

Пример для MQTT:
Ты подписываешься на топик sensors/temperature/living_room.
Как только туда приходит новое значение — триггер срабатывает.

⚖️ 2. Условие — нужно ли выполнять действие?

Не всегда нужно действовать. Условие — это фильтр. “Да, событие произошло — но нам оно подходит?”

Примеры условий:

Без условия автоматизация сработает на каждое событие — даже если оно не нужно. Условие делает систему умной.

Пример:
Пришло значение “26” в топик температуры → условие “>25” → ✅ выполняем действие.

⚡ 3. Действие — что делать дальше

Это финальный шаг. Что система должна сделать, если триггер сработал и условие выполнено.

Примеры действий:

Пример для MQTT:
Отправить команду в топик devices/fan/living_room/set с payload {"state": "ON"}.

🔄 Как это работает в реальном времени?

В отличие от крона (который проверяет раз в минуту), настоящая автоматизация работает мгновенно. Как?

Это возможно благодаря фоновому слушателю (listener), который работает 24/7 и не спит.

🧩 Пример полного цикла

  1. Датчик температуры отправляет в MQTT: sensors/temperature/living_room → "26".
  2. Триггер срабатывает → система проверяет правило: “если >25 → включить вентилятор”.
  3. Условие выполнено → система отправляет команду: devices/fan/living_room/set → {"state": "ON"}.
  4. Вентилятор включается. Лог записан. Ты доволен.

💡 Почему это круто?

🛠️ Как это сделать у себя?

Ты не обязан ставить Home Assistant. Ты можешь:

  1. Создать веб-интерфейс для правил (как мой logic.php).
  2. Написать фоновый слушатель (как мой mqtt_listener.php).
  3. Запустить его как службу — и забыть.
  4. Эмулировать события через mosquitto_pub — чтобы тестировать без устройств.

Ты учишься на практике — и это самый ценный опыт.

🔒 Важно: безопасность и надёжность

Автоматизация — это сила. Но сила требует ответственности.

🚀 Что дальше?

Когда база работает — можно усложнять:

Ты на правильном пути. Ты не просто ставишь “коробку” — ты строишь свою систему. И это круто.

📌 P.S. Для тех, кто хочет код

Все файлы, о которых я говорил — logic.php, mqtt_listener.php, примеры запуска — я выложил отдельно. Просто вставь их в проект — и автоматизация заработает.

Учись. Тестируй. Автоматизируй. Делись.

logic.php:

<?php
session_start();

if (!isset($_SESSION['iot_admin'])) {
    header('Location: /admin/sensors.php');
    exit;
}

try {
    $pdo = new PDO("mysql:host=127.0.0.1;dbname=iot_db;charset=utf8mb4", "user", "123456");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die("Ошибка подключения: " . $e->getMessage());
}


$pdo->exec("
    CREATE TABLE IF NOT EXISTS automations (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(255) NOT NULL COMMENT 'Название правила',
        trigger_topic VARCHAR(255) NOT NULL COMMENT 'Топик-триггер',
        condition_operator ENUM('>', '<', '==', '!=') NOT NULL DEFAULT '==',
        condition_value VARCHAR(50) NOT NULL COMMENT 'Значение для условия',
        action_topic VARCHAR(255) NOT NULL COMMENT 'Топик для действия',
        action_payload TEXT NOT NULL COMMENT 'Payload для отправки',
        confirmation_topic VARCHAR(255) NULL COMMENT 'Топик для подтверждения выполнения (например, состояние устройства)',
        delay_seconds INT DEFAULT 0 COMMENT 'Задержка в секундах',
        schedule_type ENUM('none', 'daily', 'weekly') DEFAULT 'none' COMMENT 'Тип расписания',
        schedule_time TIME NULL COMMENT 'Время срабатывания',
        schedule_days SET('mon','tue','wed','thu','fri','sat','sun') DEFAULT NULL COMMENT 'Дни недели',
        enabled TINYINT(1) DEFAULT 1 COMMENT 'Активно ли правило',
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
$error = '';
$success = '';

// Обработка сохранения правила
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save'])) {
    $name = trim($_POST['name'] ?? '');
    $trigger_topic = trim($_POST['trigger_topic'] ?? '');
    $condition_operator = $_POST['condition_operator'] ?? '==';
    $condition_value = trim($_POST['condition_value'] ?? '');
    $action_topic = trim($_POST['action_topic'] ?? '');
    $action_payload = trim($_POST['action_payload'] ?? '');
    $delay_seconds = (int)($_POST['delay_seconds'] ?? 0);
    $schedule_type = $_POST['schedule_type'] ?? 'none';
    $schedule_time = $_POST['schedule_time'] ?: null;
    $schedule_days = isset($_POST['schedule_days']) ? implode(',', $_POST['schedule_days']) : null;
    $enabled = isset($_POST['enabled']) ? 1 : 0;

    if (!$name || (!$trigger_topic && $schedule_type === 'none') || !$action_topic || !$action_payload) {
        $error = "Название, действие и payload обязательны. Для триггеров — топик и условие.";
    } else {
        try {
$stmt = $pdo->prepare("
    INSERT INTO automations (
        name, trigger_topic, condition_operator, condition_value,
        action_topic, action_payload, confirmation_topic, delay_seconds,
        schedule_type, schedule_time, schedule_days, enabled
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ON DUPLICATE KEY UPDATE
        name = VALUES(name),
        trigger_topic = VALUES(trigger_topic),
        condition_operator = VALUES(condition_operator),
        condition_value = VALUES(condition_value),
        action_topic = VALUES(action_topic),
        action_payload = VALUES(action_payload),
        confirmation_topic = VALUES(confirmation_topic),
        delay_seconds = VALUES(delay_seconds),
        schedule_type = VALUES(schedule_type),
        schedule_time = VALUES(schedule_time),
        schedule_days = VALUES(schedule_days),
        enabled = VALUES(enabled)
");

$stmt->execute([
    $name, $trigger_topic, $condition_operator, $condition_value,
    $action_topic, $action_payload, $_POST['confirmation_topic'] ?? null, $delay_seconds,
    $schedule_type, $schedule_time, $schedule_days, $enabled
]);





            $success = "Правило успешно сохранено!";
        } catch (Exception $e) {
            $error = "Ошибка сохранения: " . $e->getMessage();
        }
    }
}

// Обработка удаления
if (isset($_GET['delete'])) {
    $id = (int)$_GET['delete'];
    $stmt = $pdo->prepare("DELETE FROM automations WHERE id = ?");
    $stmt->execute([$id]);
    header("Location: logic.php");
    exit;
}

$stmt = $pdo->query("SELECT DISTINCT topic FROM sensor_data ORDER BY topic");
$topics = $stmt->fetchAll(PDO::FETCH_COLUMN);

$stmt = $pdo->query("SELECT * FROM automations ORDER BY created_at DESC");
$rules = $stmt->fetchAll(PDO::FETCH_ASSOC);

?>

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>🧠 Логика автоматизации</title>
    <style>
        body { font-family: Arial; padding: 20px; background: #f5f5f5; }
        h1 { color: #333; }
        .form-box { background: white; padding: 20px; border-radius: 8px; margin-bottom: 30px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
        label { display: block; margin-top: 10px; }
        input, select, textarea { width: 100%; padding: 8px; margin-top: 5px; }
        input[type="checkbox"] { width: auto; }
        .schedule-days label { display: inline-block; margin-right: 10px; }
        button, input[type="submit"] { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background: #0056b3; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; background: white; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
        th, td { padding: 12px; border: 1px solid #ddd; text-align: left; }
        th { background: #f0f0f0; }
        .status-enabled { color: green; }
        .status-disabled { color: red; }
        .actions a { margin-right: 10px; text-decoration: none; }
        .message { padding: 10px; margin: 10px 0; border-radius: 4px; }
        .error { background: #f8d7da; color: #721c24; }
        .success { background: #d4edda; color: #155724; }
    </style>
</head>
<body>

<h1>🧠 Логика автоматизации</h1>

<?php if ($error): ?>
    <div class="message error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($success): ?>
    <div class="message success"><?= htmlspecialchars($success) ?></div>
<?php endif; ?>

<div class="form-box">
    <h2>➕ Добавить / Редактировать правило</h2>
    <form method="post">
        <label>Название правила<br>
            <input type="text" name="name" required placeholder="Например: Включить свет в 19:00">
        </label>

        <label>Тип активации<br>
            <select name="schedule_type" onchange="toggleSchedule(this.value)">
                <option value="none">По триггеру (MQTT)</option>
                <option value="daily">Каждый день в заданное время</option>
                <option value="weekly">По дням недели</option>
            </select>
        </label>

        <div id="triggerSection">
            <label>Топик-триггер (что слушаем)<br>
                <input list="topics" name="trigger_topic" placeholder="sensors/temperature/living_room">
                <datalist id="topics">
                    <?php foreach ($topics as $topic): ?>
                        <option value="<?= htmlspecialchars($topic) ?>">
                    <?php endforeach; ?>
                </datalist>
            </label>

            <label>Условие<br>
                <select name="condition_operator" style="width: auto; margin-right: 10px;">
                    <option value=">">></option>
                    <option value="<"><</option>
                    <option value="==" selected>=</option>
                    <option value="!=">!=</option>
                </select>
                <input type="text" name="condition_value" placeholder="25" style="width: 60%;">
            </label>
        </div>
        <div id="scheduleSection" style="display:none;">
            <label>Время срабатывания (HH:MM:SS)<br>
                <input type="time" name="schedule_time" step="1">
            </label>

            <div id="weeklyDays" style="display:none; margin-top:10px;">
                <label>Дни недели<br>
                    <div class="schedule-days">
                        <?php
                        $days = ['mon'=>'Пн', 'tue'=>'Вт', 'wed'=>'Ср', 'thu'=>'Чт', 'fri'=>'Пт', 'sat'=>'Сб', 'sun'=>'Вс'];
                        foreach ($days as $k => $v): ?>
                            <label>
                                <input type="checkbox" name="schedule_days[]" value="<?= $k ?>">
                                <?= $v ?>
                            </label>
                        <?php endforeach; ?>
                    </div>
                </label>
            </div>
        </div>

        <label>Топик для действия (куда отправить)<br>
            <input list="topics" name="action_topic" required placeholder="devices/light/living_room/set">
        </label>

        <label>Payload (что отправить)<br>
            <textarea name="action_payload" rows="2" required placeholder='{"state": "ON"}'>{"state": "ON"}</textarea>
        </label>
        <label>Топик подтверждения (опционально)<br>
            <input list="topics" name="confirmation_topic" placeholder="devices/valve/state или zigbee2mqtt/0x1234/state">
            <small>Устройство должно опубликовать сюда состояние после выполнения действия. Если не указано — правило считается выполненным сразу.</smal>
        </label>


        <label>Задержка перед действием (сек)<br>
            <input type="number" name="delay_seconds" value="0" min="0">
        </label>

        <label>
            <input type="checkbox" name="enabled" checked> Активно
        </label>

        <br><br>
        <input type="submit" name="save" value="💾 Сохранить правило">
    </form>
</div>

<h2>📋 Все правила</h2>
<table>
    <thead>
        <tr>
            <th>ID</th>
            <th>Название</th>
            <th>Тип</th>
            <th>Триггер/Время</th>
            <th>Действие</th>
            <th>Активно</th>
            <th>Управление</th>
        </tr>
    </thead>
    <tbody>
        <?php foreach ($rules as $rule): ?>
            <tr>
                <td><?= $rule['id'] ?></td>
                <td><?= htmlspecialchars($rule['name']) ?></td>
                <td>
                    <?php
                    $type = $rule['schedule_type'];
                    if ($type === 'none') echo 'Триггер';
                    elseif ($type === 'daily') echo 'Ежедневно';
                    elseif ($type === 'weekly') echo 'По дням';
                    ?>
                </td>
                <td>
                    <?php if ($rule['schedule_type'] !== 'none'): ?>
                        <?= $rule['schedule_time'] ?>
                        <?php if ($rule['schedule_type'] === 'weekly' && $rule['schedule_days']): ?>
                            (<?= $rule['schedule_days'] ?>)
                        <?php endif; ?>
                    <?php else: ?>
                        <code><?= htmlspecialchars($rule['trigger_topic']) ?></code><br>
                        <?= htmlspecialchars($rule['condition_operator']) ?> <?= htmlspecialchars($rule['condition_value']) ?>
                    <?php endif; ?>
                </td>
                <td>
                    <b>Топик:</b> <code><?= htmlspecialchars($rule['action_topic']) ?></code><br>
                    <b>Payload:</b> <code><?= htmlspecialchars($rule['action_payload']) ?></code><br>
                    <?php if ($rule['delay_seconds'] > 0): ?>
                        <b>Задержка:</b> <?= $rule['delay_seconds'] ?> сек
                    <?php endif; ?>
                </td>
                <td class="<?= $rule['enabled'] ? 'status-enabled' : 'status-disabled' ?>">
                    <?= $rule['enabled'] ? '✅ Да' : '❌ Нет' ?>
                </td>
                <td>
                    <a href="#" onclick="fillForm(
                        '<?= addslashes($rule['name']) ?>',
                        '<?= addslashes($rule['trigger_topic']) ?>',
                        '<?= addslashes($rule['condition_operator']) ?>',
                        '<?= addslashes($rule['condition_value']) ?>',
                        '<?= addslashes($rule['action_topic']) ?>',
                        '<?= addslashes($rule['action_payload']) ?>',
                        <?= (int)$rule['delay_seconds'] ?>,
                        '<?= addslashes($rule['schedule_type']) ?>',
                        '<?= addslashes($rule['schedule_time'] ?: '') ?>',
                        '<?= addslashes($rule['schedule_days'] ?: '') ?>',
                        <?= $rule['enabled'] ? 'true' : 'false' ?>
                    )">✏️ Ред.</a>
                    <a href="?delete=<?= $rule['id'] ?>" onclick="return confirm('Удалить правило?')">🗑️ Удалить</a>
                </td>
            </tr>
        <?php endforeach; ?>
    </tbody>
</table>

<script>
function toggleSchedule(type) {
    const triggerSection = document.getElementById('triggerSection');
    const scheduleSection = document.getElementById('scheduleSection');
    const weeklyDays = document.getElementById('weeklyDays');

    if (type === 'none') {
        triggerSection.style.display = 'block';
        scheduleSection.style.display = 'none';
    } else {
        triggerSection.style.display = 'none';
        scheduleSection.style.display = 'block';
        weeklyDays.style.display = (type === 'weekly') ? 'block' : 'none';
    }
}

function fillForm(name, trigger_topic, condition_operator, condition_value, action_topic, action_payload, delay_seconds, schedule_type, schedule_time, schedule_days_str, enabled) {
    document.querySelector("[name='name']").value = name;
    document.querySelector("[name='trigger_topic']").value = trigger_topic;
    document.querySelector("[name='condition_operator']").value = condition_operator;
    document.querySelector("[name='condition_value']").value = condition_value;
    document.querySelector("[name='action_topic']").value = action_topic;
    document.querySelector("[name='action_payload']").value = action_payload;
    document.querySelector("[name='delay_seconds']").value = delay_seconds;
    document.querySelector("[name='schedule_type']").value = schedule_type;
    document.querySelector("[name='schedule_time']").value = schedule_time;

    // Чекбоксы дней
    const checkboxes = document.querySelectorAll("[name='schedule_days[]']");
    const selectedDays = schedule_days_str ? schedule_days_str.split(',') : [];
    checkboxes.forEach(cb => {
        cb.checked = selectedDays.includes(cb.value);
    });

    document.querySelector("[name='enabled']").checked = enabled;
    toggleSchedule(schedule_type);
    window.scrollTo(0, 0);
}
</script>

<form method="post" action="/admin/logout.php" style="margin-top: 30px;">
    <button type="submit">🚪 Выйти</button>
</form>

</body>
</html>

mqtt_listener.php

<?php

// Подключение к БД
try {
    $pdo = new PDO("mysql:host=127.0.0.1;dbname=iot_db;charset=utf8mb4", "user", "123456");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die("❌ Ошибка подключения к БД: " . $e->getMessage() . "\n");
}

echo "🧠 MQTT Listener запущен. Загружаю активные правила...\n";

// Глобальные переменные состояния
$rules = [];
$topicToRules = [];
$topics = [];
$process = null;
$pipes = null;
$scheduled = []; // отложенные действия по delay_seconds
//новое
$pendingConfirmations = []; // [confirmation_topic => [rule_id => expected_state, ...]]
$ruleById = [];             // для быстрого доступа к правилу по ID
//
$lastRuleReload = 0;
$lastScheduleCheck = 0;

// Функция перезагрузки правил
function reloadRules($pdo) {
    global $rules, $topicToRules, $topics, $lastRuleReload;

    $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;
}

    $topicToRules = [];
    $topics = [];

    foreach ($rules as $rule) {
        $topic = $rule['trigger_topic'];
        if (!isset($topicToRules[$topic])) {
            $topicToRules[$topic] = [];
            $topics[] = $topic;
        }
        $topicToRules[$topic][] = $rule;
    }

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

    $lastRuleReload = time();
    return [$rules, $topicToRules, $topics];
}

// Функция запуска mosquitto_sub
function startMqttSubscriber($topics) {
    if (empty($topics)) {
        throw new Exception("Нет топиков для подписки");
    }

    $topicArgs = implode(' ', array_map('escapeshellarg', $topics));
//    $cmd = "mosquitto_sub -h localhost -t $topicArgs -v";
$cmd = "mosquitto_sub -h 127.0.0.1 -p 1883 -u user -P 123456 -t $topicArgs -v 2>&1";

    echo "📡 Запускаю подписку: $cmd\n";

    $process = proc_open($cmd, [
        0 => ['pipe', 'r'],  // stdin
        1 => ['pipe', 'w'],  // stdout
        2 => ['pipe', 'w']   // stderr
    ], $pipes);

    if (!is_resource($process)) {
        throw new Exception("Не удалось запустить mosquitto_sub");
    }

    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);

    return [$process, $pipes];
}

// Функция проверки условия
function checkCondition($payload, $operator, $value) {
    $decoded = json_decode($payload, true);
    if (is_array($decoded)) {
        if (isset($decoded['state'])) {
            $payload = $decoded['state'];
        } else {
            foreach ($decoded as $v) {
                if (is_scalar($v)) {
                    $payload = $v;
                    break;
                }
            }
        }
    }

    $numPayload = is_numeric($payload) ? (float)$payload : null;
    $numValue = is_numeric($value) ? (float)$value : null;

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

// Функция отправки команды
function sendMqttCommand($topic, $payload, $ruleName = '') {
//    $cmd = "mosquitto_pub -h localhost -t " . escapeshellarg($topic) . " -m " . escapeshellarg($payload) . " 2>&1";
$cmd = "mosquitto_pub -h 127.0.0.1 -p 1883 -u 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";
    }
}

// Инициализация
[$rules, $topicToRules, $topics] = reloadRules($pdo);

// Запуск подписчика
try {
    [$process, $pipes] = startMqttSubscriber($topics);
} catch (Exception $e) {
    echo "❌ Ошибка: " . $e->getMessage() . "\n";
    exit(1);
}

echo "✅ Готов! Ловлю события и проверяю расписание...\n";

// Основной цикл
while (true) {

    // 🔁 Перезагружаем правила каждые 5 минут
    if (time() - $lastRuleReload > 300) {
        echo "\n🔄 Автоматическая перезагрузка правил...\n";
        [$rules, $topicToRules, $topics] = reloadRules($pdo);

        // Перезапускаем подписку, если список топиков изменился
        $currentTopics = [];
        foreach ($rules as $rule) {
            $currentTopics[$rule['trigger_topic']] = true;
        }
        $topicsChanged = false;
        foreach ($topics as $t) if (!isset($currentTopics[$t])) $topicsChanged = true;
        foreach ($currentTopics as $t => $v) if (!in_array($t, $topics)) $topicsChanged = true;

        if ($topicsChanged) {
            echo "🔁 Топики изменились — перезапускаю подписку...\n";
            if (is_resource($process)) {
                proc_terminate($process);
                proc_close($process);
            }
            [$process, $pipes] = startMqttSubscriber($topics);
        }
    }

    // 🕔 Проверяем расписание раз в минуту
    if (time() - $lastScheduleCheck > 60) {
        $lastScheduleCheck = time();
        $currentTime = date('H:i:s'); // например, "19:00:00"
        $currentDay = strtolower(date('D')); // 'mon', 'tue' и т.д.

        foreach ($rules as $rule) {
            if ($rule['schedule_type'] === 'none') continue;

            $shouldTrigger = false;

            if ($rule['schedule_type'] === 'daily' && $rule['schedule_time'] === $currentTime) {
                $shouldTrigger = true;
            } elseif ($rule['schedule_type'] === 'weekly' && $rule['schedule_time'] === $currentTime) {
                $allowedDays = explode(',', $rule['schedule_days']);
                if (in_array($currentDay, $allowedDays)) {
                    $shouldTrigger = true;
                }
            }

            if ($shouldTrigger) {
                echo "⏰ Сработало расписание: " . $rule['name'] . " в " . $currentTime . "\n";
                $delay = (int)$rule['delay_seconds'];
                if ($delay > 0) {
                    $executeAt = time() + $delay;
                    global $scheduled;
                    $scheduled[] = [
                        'execute_at' => $executeAt,
                        'topic' => $rule['action_topic'],
                        'payload' => $rule['action_payload'],
                        'rule_name' => $rule['name']
                    ];
                    echo "⏱ Запланировано на " . date('H:i:s', $executeAt) . "\n";
                } else {
                    sendMqttCommand($rule['action_topic'], $rule['action_payload'], $rule['name']);

// Если нужно ждать подтверждения
if (!empty($rule['confirmation_topic'])) {
    // Определяем, какое состояние ожидаем — посылали {"state": "ON"} → ждём "ON"
    $expectedState = null;
    $actionPayloadDecoded = json_decode($rule['action_payload'], true);
    if (is_array($actionPayloadDecoded) && isset($actionPayloadDecoded['state'])) {
        $expectedState = $actionPayloadDecoded['state'];
    } else {
        // Если не JSON или нет state — ждём любое непустое сообщение
        $expectedState = 'any';
    }

    // Добавляем в список ожидания
    if (!isset($pendingConfirmations[$rule['confirmation_topic']])) {
        $pendingConfirmations[$rule['confirmation_topic']] = [];
        // Подписываемся на новый топик, если нужно
        if (!in_array($rule['confirmation_topic'], $topics)) {
            $topics[] = $rule['confirmation_topic'];
            $topicsChanged = true;
        }
    }
    $pendingConfirmations[$rule['confirmation_topic']][$rule['id']] = $expectedState;

    echo "⏳ Ожидаю подтверждения в топике: " . $rule['confirmation_topic'] . " (ожидается: " . $expectedState . ")\n";
} else {
    echo "✅ Правило '" . $rule['name'] . "' выполнено (без подтверждения).\n";
}
                }
            }
        }
    }

    // ⏱ Выполняем отложенные действия (по delay_seconds)
    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); // переиндексация

    // 📥 Читаем MQTT
    if (is_resource($process)) {
        $status = proc_get_status($process);
        if (!$status['running']) {
            echo "⚠️ mosquitto_sub завершился. Перезапуск...\n";
            proc_close($process);
            sleep(3);
            try {
                [$process, $pipes] = startMqttSubscriber($topics);
            } catch (Exception $e) {
                echo "❌ Не удалось перезапустить: " . $e->getMessage() . "\n";
                sleep(10);
            }
            continue;
        }

        // Чтение stdout
        while ($output = fgets($pipes[1])) {
            $output = trim($output);
            $firstSpace = strpos($output, ' ');
            if ($firstSpace === false) continue;

            $topic = substr($output, 0, $firstSpace);
            $payload = substr($output, $firstSpace + 1);

            echo "📥 Получено: $topic = $payload\n";

// Проверяем, не является ли это подтверждением выполнения
if (isset($pendingConfirmations[$topic])) {
    foreach ($pendingConfirmations[$topic] as $ruleId => $expectedState) {
        $rule = $ruleById[$ruleId] ?? null;
        if (!$rule) continue;

        $confirmed = false;

        if ($expectedState === 'any') {
            $confirmed = !empty($payload);
        } else {
            // Пытаемся распарсить как JSON
            $decoded = json_decode($payload, true);
            if (is_array($decoded) && isset($decoded['state']) && $decoded['state'] == $expectedState) {
                $confirmed = true;
            } elseif ($payload == $expectedState) {
                $confirmed = true;
            }
        }

        if ($confirmed) {
            echo "✅✅ Подтверждение получено для правила: " . $rule['name'] . " — состояние: " . $payload . "\n";
            unset($pendingConfirmations[$topic][$ruleId]);

            // Если больше никто не ждёт этого топика — можно отписаться (опционально)
            if (empty($pendingConfirmations[$topic])) {
                unset($pendingConfirmations[$topic]);
            }
        }
    }
}


            if (isset($topicToRules[$topic])) {
                foreach ($topicToRules[$topic] as $rule) {
                    if ($rule['schedule_type'] !== 'none') continue; // по времени — не триггер

                    if (checkCondition($payload, $rule['condition_operator'], $rule['condition_value'])) {
                        echo "🎉 Сработало правило: " . $rule['name'] . "\n";
                        $delay = (int)$rule['delay_seconds'];
                        if ($delay > 0) {
                            $executeAt = time() + $delay;
                            $scheduled[] = [
                                'execute_at' => $executeAt,
                                'topic' => $rule['action_topic'],
                                'payload' => $rule['action_payload'],
                                'rule_name' => $rule['name']
                            ];
                            echo "⏱ Запланировано на " . date('H:i:s', $executeAt) . "\n";
                        } else {
sendMqttCommand($rule['action_topic'], $rule['action_payload'], $rule['name']);

// Если нужно ждать подтверждения
if (!empty($rule['confirmation_topic'])) {
    // Определяем, какое состояние ожидаем — посылали {"state": "ON"} → ждём "ON"
    $expectedState = null;
    $actionPayloadDecoded = json_decode($rule['action_payload'], true);
    if (is_array($actionPayloadDecoded) && isset($actionPayloadDecoded['state'])) {
        $expectedState = $actionPayloadDecoded['state'];
    } else {
        // Если не JSON или нет state — ждём любое непустое сообщение
        $expectedState = 'any';
    }

    // Добавляем в список ожидания
    if (!isset($pendingConfirmations[$rule['confirmation_topic']])) {
        $pendingConfirmations[$rule['confirmation_topic']] = [];
        // Подписываемся на новый топик, если нужно
        if (!in_array($rule['confirmation_topic'], $topics)) {
            $topics[] = $rule['confirmation_topic'];
            $topicsChanged = true;
        }
    }
    $pendingConfirmations[$rule['confirmation_topic']][$rule['id']] = $expectedState;

    echo "⏳ Ожидаю подтверждения в топике: " . $rule['confirmation_topic'] . " (ожидается: " . $expectedState . ")\n";
} else {
    echo "✅ Правило '" . $rule['name'] . "' выполнено (без подтверждения).\n";
}
                        }
                    } else {
                        echo "⏸️ Правило '" . $rule['name'] . "' — условие не выполнено.\n";
                    }
                }
            }
        }

        // Чтение stderr
        while ($error = fgets($pipes[2])) {
            echo "❌ MQTT Error: " . trim($error) . "\n";
        }
    }

    usleep(100000); // 0.1 сек
}

Эта статья — отчёт о том, как я собрал с нуля систему умного дома на Raspberry Pi, используя только открытые технологии, PHP, Python и MQTT. Никаких Home Assistant, Node-RED или облачных сервисов — только мои руки, мозги и Linux.

🔧 Что я хотел сделать

📦 Используемые технологии

🏗️ Архитектура системы

  1. Zigbee2MQTT — получает данные от датчиков и публикует их в MQTT-топики (например, zigbee2mqtt/датчик_влажности).
  2. Mosquitto — брокер, который пересылает сообщения всем подписчикам.
  3. Python-логгер — подписывается на топики, парсит JSON и сохраняет данные в MySQL-таблицу sensor_data.
  4. PHP-демон автоматизации — слушает те же топики, сверяет с правилами из таблицы automations, и при выполнении условий — отправляет команды обратно в MQTT (например, devices/light/set {"state": "ON"}).
  5. Веб-интерфейс на PHP — позволяет добавлять/редактировать правила, просматривать данные с датчиков.
  6. Systemd — запускает Python-логгер и PHP-демон при загрузке системы.

📝 Основные этапы разработки

1. Установка и настройка Zigbee2MQTT

Подключил Zigbee-стик, установил Zigbee2MQTT, настроил конфигурацию. Добавил датчики — они сразу начали публиковать данные в MQTT.

2. Настройка Mosquitto с авторизацией

Настроил логин/пароль для безопасности. Все компоненты подключаются с аутентификацией.

3. Создание базы данных

Создал базу iot_db и таблицы:

4. Python-логгер: MQTT → MySQL

Написал скрипт на Python, который:

Запустил его как systemd-сервис — теперь он работает 24/7 и перезапускается при падении.

5. PHP-демон автоматизации

Написал фоновый PHP-скрипт, который:

Тоже запущен как systemd-сервис.

6. Веб-интерфейс на PHP

Создал две страницы:

7. Тестирование и отладка

Столкнулся с проблемами:

Всё починил — система теперь работает стабильно.

✅ Что умеет система

🚀 Планы на будущее

💡 Вывод

Я не использовал готовые системы вроде Home Assistant — я построил всё с нуля. Это дало мне полный контроль, глубокое понимание работы каждого компонента и возможность делать ровно то, что нужно — без лишнего.

Если ты читаешь это — ты тоже можешь. Начни с малого — и шаг за шагом ты построишь своё.

Умный дом — это не магия. Это код, железо и настойчивость.

Теги: #PHP #MQTT #IoT #автоматизация #самоделка #умный_дом #home_assistant #триггер #условие #действие #логика #своими_руками #блог_айтишника #умная_автоматизация #без_хомасистента

Комментарии

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

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

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

Важно: Блог-эксперимент

Блог только запустил, все статьи генерирую через нейросеть т.к. лень, возможны ошибки. Просто чтобы вы знали и не запускали ядерный реактор по моим статьям ))
Если у вас есть вопросы, или Нашли неточность? пишите в коментах — вместе поправим и сделаем статью более качественной. Я лично объясню нюансы из практики.

Посетителей сегодня: 0


кто я | книга | контакты без контактов

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