сохранил себе пример кода для логики в самописную систему управления iot устройствами
Если ты когда-нибудь задумывался, как умный дом сам включает свет, закрывает кран или запускает вентилятор — добро пожаловать в мир автоматизации. Сегодня я расскажу, как это работает “под капотом”, без кода, только суть. Чтобы ты мог понять — и повторить у себя, даже если пишешь всё с нуля.
Автоматизация — это правило вида:
“ЕСЛИ что-то произошло → ТО сделай что-то другое”
Примеры:
Всё просто. Но чтобы это работало — нужны три кита: Триггер, Условие и Действие.
Это “спусковой крючок”. Событие, с которого всё начинается. В системах вроде Home Assistant (HA) или твоей самописной панели — триггером может быть:
Пример для MQTT:
Ты подписываешься на топик sensors/temperature/living_room.
Как только туда приходит новое значение — триггер срабатывает.
Не всегда нужно действовать. Условие — это фильтр. “Да, событие произошло — но нам оно подходит?”
Примеры условий:
Без условия автоматизация сработает на каждое событие — даже если оно не нужно. Условие делает систему умной.
Пример:
Пришло значение “26” в топик температуры → условие “>25” → ✅ выполняем действие.
Это финальный шаг. Что система должна сделать, если триггер сработал и условие выполнено.
Примеры действий:
Пример для MQTT:
Отправить команду в топик devices/fan/living_room/set с payload {"state": "ON"}.
В отличие от крона (который проверяет раз в минуту), настоящая автоматизация работает мгновенно. Как?
Это возможно благодаря фоновому слушателю (listener), который работает 24/7 и не спит.
sensors/temperature/living_room → "26".devices/fan/living_room/set → {"state": "ON"}.Ты не обязан ставить Home Assistant. Ты можешь:
logic.php).mqtt_listener.php).mosquitto_pub — чтобы тестировать без устройств.Ты учишься на практике — и это самый ценный опыт.
Автоматизация — это сила. Но сила требует ответственности.
Когда база работает — можно усложнять:
Ты на правильном пути. Ты не просто ставишь “коробку” — ты строишь свою систему. И это круто.
Все файлы, о которых я говорил — logic.php, mqtt_listener.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>
<?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.
zigbee2mqtt/датчик_влажности).sensor_data.automations, и при выполнении условий — отправляет команды обратно в MQTT (например, devices/light/set {"state": "ON"}).Подключил Zigbee-стик, установил Zigbee2MQTT, настроил конфигурацию. Добавил датчики — они сразу начали публиковать данные в MQTT.
Настроил логин/пароль для безопасности. Все компоненты подключаются с аутентификацией.
Создал базу iot_db и таблицы:
sensor_data — для хранения значений с датчиковautomations — для хранения правил автоматизацииНаписал скрипт на Python, который:
zigbee2mqtt/#sensor_dataЗапустил его как systemd-сервис — теперь он работает 24/7 и перезапускается при падении.
Написал фоновый PHP-скрипт, который:
mosquitto_pubТоже запущен как systemd-сервис.
Создал две страницы:
logic.php — добавление/редактирование правил автоматизации. Можно указать:
sensors.php — просмотр последних данных с датчиков (последнее значение для каждого топика)Столкнулся с проблемами:
Всё починил — система теперь работает стабильно.
Я не использовал готовые системы вроде Home Assistant — я построил всё с нуля. Это дало мне полный контроль, глубокое понимание работы каждого компонента и возможность делать ровно то, что нужно — без лишнего.
Если ты читаешь это — ты тоже можешь. Начни с малого — и шаг за шагом ты построишь своё.
Умный дом — это не магия. Это код, железо и настойчивость.
Блог только запустил, все статьи генерирую через нейросеть т.к. лень, возможны ошибки. Просто чтобы вы знали и не запускали ядерный реактор по моим статьям ))
Если у вас есть вопросы, или Нашли неточность? пишите в коментах — вместе поправим и сделаем статью более качественной. Я лично объясню нюансы из практики.
Комментарии
Пока нет комментариев. Будьте первым!