Как я создал умный дом без облаков: от датчиков до автоматических инсайтов
Многие считают, что «умный дом» — это Alexa, Google Home или платформы вроде Home Assistant. Но настоящий умный дом — это когда он не ждёт команды, а сам говорит: «Здесь проблема». В этой статье — пошаговый гайд, как я сделал систему на Raspberry Pi, которая анализирует данные, выявляет риски и пишет идеи каждые 5 минут — без интернета, без нейросетей, без подписок.
можно и в админку добавить, но если вы шарите то лучше вручную прописывать условия
Создать локальную систему, которая:
Простая схема из четырёх компонентов:
Создадим две таблицы — для данных и для идей.
CREATE TABLE IF NOT EXISTS sensor_data (
id INT AUTO_INCREMENT PRIMARY KEY,
topic VARCHAR(255) NOT NULL,
value TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS insights (
id INT AUTO_INCREMENT PRIMARY KEY,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Таблица automations — хранит все триггеры и расписания.
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 'Значение для условия',
condition_duration INT DEFAULT 0 COMMENT 'Условие должно соблюдаться N секунд (0 = мгновенно)',
action_topic VARCHAR(255) NOT NULL COMMENT 'Топик для действия',
action_payload TEXT NOT NULL COMMENT 'Payload для отправки',
schedule_type ENUM('none','daily','weekly') DEFAULT 'none' COMMENT 'Тип расписания',
enabled TINYINT(1) DEFAULT 1 COMMENT 'Активно ли правило',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_triggered TIMESTAMP NULL,
failure_count INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Добавим правило: «Если температура > 45°C — напомни о перегреве».
INSERT INTO automations (
name,
trigger_topic,
condition_operator,
condition_value,
schedule_type,
enabled
) VALUES (
'Перегрев контактов',
'sensors/temperature/kitchen',
'>',
'45',
'none',
1
);
Вот минимальная версия скрипта, который читает данные и генерирует идеи. Он работает на PHP + PDO.
<?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());
}
// === Получаем последние данные по ключевым топикам ===
$topics = [
'sensors/temperature/kitchen', // температура рядом с контактами
'sensors/voltage/power_supply', // напряжение питания
'sensors/resistance/contact_kitchen', // сопротивление контактов (если есть датчик)
'sensors/current/water_valve', // ток через клапан
'zigbee2mqtt/0x00124b003548b510/battery', // батарейка датчика
];
$data = [];
foreach ($topics as $topic) {
$stmt = $pdo->prepare("
SELECT topic, value, timestamp
FROM sensor_data
WHERE topic = ?
ORDER BY timestamp DESC
LIMIT 1
");
$stmt->execute([$topic]);
if ($row = $stmt->fetch()) {
$data[$topic] = [
'value' => $row['value'],
'timestamp' => $row['timestamp']
];
} else {
$data[$topic] = ['value' => null, 'timestamp' => '—'];
}
}
// === Формируем “инсайт” на основе правил ===
$insights = [];
// 🔥 ПОЖАР / ПЕРЕГРЕВ
if (
isset($data['sensors/temperature/kitchen']['value']) &&
$data['sensors/temperature/kitchen']['value'] > 45
) {
$insights[] = "⚠️ **Перегрев!** Температура у контактов: {$data['sensors/temperature/kitchen']['value']}°C — возможно, плохой контакт или короткое замыкание.";
}
// ⚡ ПЛОХОЙ КОНТАКТ — РОСТ СОПРОТИВЛЕНИЯ
if (
isset($data['sensors/resistance/contact_kitchen']['value']) &&
$data['sensors/resistance/contact_kitchen']['value'] > 50 // Ом — пример для плохого контакта
) {
$insights[] = "⚠️ **Плохой контакт!** Сопротивление: {$data['sensors/resistance/contact_kitchen']['value']} Ом — выше нормы (обычно < 5 Ом). Проверьте соединения!";
}
// 🔋 БАТАРЕЙКА НИЗКАЯ
if (
isset($data['zigbee2mqtt/0x00124b003548b510/battery']['value']) &&
$data['zigbee2mqtt/0x00124b003548b510/battery']['value'] < 20
) {
$insights[] = "🔋 **Низкий заряд батареи:** {$data['zigbee2mqtt/0x00124b003548b510/battery']['value']}%. Замените батарейку — датчик может отключиться.";
}
// ⚠️ НАПРЯЖЕНИЕ ПАДАЕТ
if (
isset($data['sensors/voltage/power_supply']['value']) &&
$data['sensors/voltage/power_supply']['value'] < 10
) {
$insights[] = "⚡ **Низкое напряжение:** {$data['sensors/voltage/power_supply']['value']} В — возможно, проблема с блоком питания или перегрузка сети.";
}
// 💧 ПРИСУТСТВИЕ ТОКА ПРИ ЗАКРЫТОМ КРАНЕ
if (
isset($data['sensors/current/water_valve']['value']) &&
$data['sensors/current/water_valve']['value'] > 0.1 // 100 мА при закрытом клапане — плохо
) {
$insights[] = "💧 **Ток при закрытом кране:** {$data['sensors/current/water_valve']['value']} А — возможна утечка или залипание контакта клапана.";
}
// === Генерация "идеи" раз в 5 минут ===
$last_insight_time = null;
$stmt = $pdo->query("SELECT MAX(created_at) as last FROM insights ORDER BY created_at DESC LIMIT 1");
if ($row = $stmt->fetch()) {
$last_insight_time = $row['last'];
}
$now = new DateTime();
$five_min_ago = (new DateTime())->modify('-5 minutes');
$should_generate = !$last_insight_time || $last_insight_time < $five_min_ago->format('Y-m-d H:i:s');
// Если пора — генерим новую идею
if ($should_generate && !empty($insights)) {
$idea = "🔧 Автоматический анализ системы:\n";
foreach ($insights as $i) {
$idea .= "- " . preg_replace('/<[^>]*>/', '', $i) . "\n"; // чистим HTML
}
$stmt = $pdo->prepare("INSERT INTO insights (content, created_at) VALUES (?, NOW())");
$stmt->execute([$idea]);
$insights[] = "🧠 **Новая идея от системы:**\n" . $idea;
}
// === Получаем последние 10 идей ===
$stmt = $pdo->query("SELECT content, created_at FROM insights ORDER BY created_at DESC LIMIT 10");
$history = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>🧠 Умный анализ — Идеи для тебя</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #121212; color: #fff; padding: 30px; }
h1 { color: #00c6ff; text-align: center; margin-bottom: 40px; }
.card { background: #1f1f1f; padding: 20px; border-radius: 10px; margin-bottom: 20px; border-left: 4px solid #00c6ff; }
.alert { background: #2c1a1a; border-left: 4px solid #e74c3c; padding: 15px; margin: 15px 0; }
.info { background: #1a2c2c; border-left: 4px solid #00c6ff; padding: 15px; margin: 15px 0; }
code { background: #333; padding: 3px 6px; border-radius: 4px; }
.btn { background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin-top: 10px; }
.btn:hover { background: #0056b3; }
.timestamp { color: #aaa; font-size: 0.9em; margin-top: 10px; }
pre { background: #000; padding: 15px; border-radius: 8px; overflow-x: auto; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>🧠 Умный анализ — Идеи для тебя</h1>
<div class="card">
<h2>🔥 Активные риски (обнаружено)</h2>
<?php if (empty($insights)): ?>
<p style="color: #888;">Все в порядке — рисков не обнаружено.</p>
<?php else: ?>
<?php foreach ($insights as $insight): ?>
<?php if (strpos($insight, 'Новая идея') !== false): ?>
<div class="info">
<?= nl2br(htmlspecialchars($insight)) ?>
</div>
<?php else: ?>
<div class="alert">
<?= $insight ?>
</div>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
</div>
<div class="card">
<h2>📜 История идей (последние 10)</h2>
<?php if (empty($history)): ?>
<p style="color: #888;">Пока нет исторических идей.</p>
<?php else: ?>
<?php foreach ($history as $item): ?>
<div style="margin-bottom: 15px;">
<pre><?= htmlspecialchars($item['content']) ?></pre>
<div class="timestamp">📅 <?= htmlspecialchars($item['created_at']) ?></div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<button class="btn" onclick="location.href='/admin/insights.php?refresh=1'">🔄 Обновить</button>
<form method="post" action="/admin/logout.php" style="margin-top: 40px;">
<button type="submit">🚪 Выйти</button>
</form>
<script>
setTimeout(() => {
location.reload();
}, 300000); // обновлять каждые 5 минут
</script>
</body>
</html>
Чтобы протестировать систему — отправь MQTT-сообщения вручную:
mosquitto_pub -t "sensors/temperature/kitchen" -m "52"
mosquitto_pub -t "sensors/resistance/contact_kitchen" -m "65"
mosquitto_pub -t "zigbee2mqtt/0x00124b003548b510/battery" -m "15"
mosquitto_pub -t "sensors/current/water_valve" -m "0.15"
После этого открой http://raspberrypi/admin/insights.php — ты увидишь предупреждения.
Чтобы страница обновлялась каждые 5 минут — добавь в конец файла insights.php:
Система теперь выводит:
⚠️ Перегрев! Температура у контактов: 52°C — возможно, плохой контакт.
⚠️ Плохой контакт! Сопротивление: 65 Ом — выше нормы (<5 Ом).
🔋 Низкий заряд батареи: 15%. Замените батарейку.
🧠 Новая идея от системы:
🔧 Автоматический анализ системы:
- Перегрев! Температура у контактов: 52°C — возможно, плохой контакт.
- Плохой контакт! Сопротивление: 65 Ом — выше нормы (<5 Ом).
- Низкий заряд батареи: 15%. Замените батарейку.
Ты не нуждаешься в нейросетях, чтобы предсказать пожар. Тебе нужно знать:
Это не AI. Это инженерное мышление. И оно работает лучше, чем любая LLM, потому что оно знает контекст твоего дома — а не обучено на общих данных.
Теперь мой дом не просто «умный» — он заботливый.
Блог только запустил, все статьи генерирую через нейросеть т.к. лень, возможны ошибки. Просто чтобы вы знали и не запускали ядерный реактор по моим статьям ))
Если у вас есть вопросы, или Нашли неточность? пишите в коментах — вместе поправим и сделаем статью более качественной. Я лично объясню нюансы из практики.
Комментарии
Пока нет комментариев. Будьте первым!