Эта страница — центр управления и наблюдения за домашней IoT-системой. В отличие от дашборда с датчиками, здесь акцент сделан на статус автоматизаций, работу сервисов, аппаратные ресурсы и системную аналитику.
Домашняя автоматизация — это не только датчики и реле. Чтобы система работала стабильно, важно отслеживать:
Эта панель даёт полную картину «здоровья» всей IoT-инфраструктуры.
В начале файла включена максимально подробная диагностика:
/tmp/php_errors.logЭто помогает быстро находить проблемы при разработке и развёртывании.
Из таблицы automations извлекается правило с самым свежим last_triggered. Payload (команда) декодируется из JSON и красиво отображается в виде ключ: значение.
Для каждого активного правила (с типом daily или weekly) вычисляется точное время следующего срабатывания:
Все задачи сортируются по времени, и самая ближайшая выделяется отдельно.
Последняя запись из таблицы rpi_monitor содержит данные, собранные скриптом на самом устройстве:
Критические значения (температура >70°C, диск >90%) подсвечиваются красным.
Система анализирует системные логи и подсчитывает ключевые слова (например, «error», «offline», «battery_low»). Эти данные сохраняются в таблице log_stats и отображаются в виде счётчиков.
Через systemctl is-active проверяется статус важнейших демонов:
mosquitto.service — MQTT-брокерzigbee2mqtt.service — мост Zigbeemqtt-to-mysql.service — архиватор данныхiot-automation.service — движок правилЕсли сервис неактивен — он помечается как критический.
Простая таблица daily_resets фиксирует, был ли сегодня выполнен сброс (например, обнуление счётчика открытий двери). Это нужно для корректной работы ежедневной логики.
Каждые 30 секунд вызывается /api/live_system_data.php, который возвращает актуальные значения:
Это обновление происходит без перезагрузки страницы.
Используется бесплатный публичный API Open-Meteo для отображения текущей погоды в Москве и Калуге. Запрос включает:
Блок «API» (и потенциально другие) можно свернуть/развернуть кликом по заголовку — для удобства навигации на мобильных устройствах.
Эта панель — «кокпит» вашей IoT-системы. Она объединяет данные из базы, ОС и внешних API, чтобы вы всегда знали: всё ли работает, что случилось в последний раз и что произойдёт дальше.
Код написан с упором на прозрачность, отладку и локальное размещение — идеально подходит для тех, кто предпочитает контролировать свою умную среду без облаков и подписок.
<?php
// ЖЁСТКАЯ ОТЛАДКА
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
file_put_contents('/tmp/php_errors.log', "=== [index.php] START " . date('Y-m-d H:i:s') . " ===\n", FILE_APPEND);
function shutdownHandler() {
$error = error_get_last();
if ($error !== null) {
file_put_contents('/tmp/php_errors.log', "FATAL: " . print_r($error, true) . "\n", FILE_APPEND);
}
}
register_shutdown_function('shutdownHandler');
set_error_handler(function($errno, $errstr, $errfile, $errline) {
file_put_contents('/tmp/php_errors.log', "ERROR: $errno: $errstr in $errfile on line $errline\n", FILE_APPEND);
return false;
});
date_default_timezone_set('Europe/Moscow');
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);
// === Последнее выполненное правило ===
$stmt_last = $pdo->prepare("SELECT name, last_triggered, action_topic, action_payload FROM automations WHERE last_triggered IS NOT NULL ORDER BY last_triggered DESC LIMIT 1");
$stmt_last->execute();
$last_executed_rule = $stmt_last->fetch(PDO::FETCH_ASSOC);
if ($last_executed_rule && !empty($last_executed_rule['action_payload'])) {
$payload = json_decode($last_executed_rule['action_payload'], true);
$last_executed_rule['payload_str'] = is_array($payload)
? implode(', ', array_map(fn($k, $v) => "$k: $v", array_keys($payload), array_values($payload)))
: $last_executed_rule['action_payload'];
}
// === Ближайшие автоматизации ===
$upcoming_tasks = [];
$stmt_tasks = $pdo->prepare("
SELECT id, name, schedule_type, schedule_time, schedule_days,
action_topic, action_payload, delay_seconds, enabled,
last_triggered, last_confirmed, failure_count
FROM automations
WHERE enabled = 1 AND schedule_type != 'none'
ORDER BY
CASE WHEN schedule_type = 'daily' THEN schedule_time END ASC,
CASE WHEN schedule_type = 'weekly' THEN schedule_time END ASC
");
$stmt_tasks->execute();
$tasks = $stmt_tasks->fetchAll(PDO::FETCH_ASSOC);
foreach ($tasks as $task) {
$next_run = null;
$display_time = '';
if ($task['schedule_type'] === 'daily') {
$now = new DateTime();
$run_time = new DateTime($task['schedule_time']);
$run_time->setDate($now->format('Y'), $now->format('m'), $now->format('d'));
if ($run_time < $now) $run_time->modify('+1 day');
$next_run = $run_time;
$display_time = $task['schedule_time'];
} elseif ($task['schedule_type'] === 'weekly') {
$days_map = ['mon' => 1, 'tue' => 2, 'wed' => 3, 'thu' => 4, 'fri' => 5, 'sat' => 6, 'sun' => 0];
$now = new DateTime();
$scheduled_days = explode(',', $task['schedule_days'] ?? '');
$next_day = null;
foreach ($scheduled_days as $day) {
if (!isset($days_map[$day])) continue;
$day_num = $days_map[$day];
$current_day = (int)$now->format('N');
$days_ahead = ($day_num - $current_day + 7) % 7;
if ($days_ahead == 0 && $now->format('H:i:s') >= $task['schedule_time']) {
$days_ahead = 7;
}
$candidate = clone $now;
$candidate->modify("+{$days_ahead} days");
$candidate->setTime((int)substr($task['schedule_time'], 0, 2), (int)substr($task['schedule_time'], 3, 2), 0);
if (!$next_day || $candidate < $next_day) $next_day = $candidate;
}
$next_run = $next_day;
$display_time = $task['schedule_time'] . " (" . implode(', ', array_map(fn($d) => ucfirst(substr($d, 0, 2)), $scheduled_days)) . ")";
}
$action_desc = json_decode($task['action_payload'], true);
$payload_str = is_array($action_desc)
? implode(', ', array_map(fn($k, $v) => "$k: $v", array_keys($action_desc), array_values($action_desc)))
: $task['action_payload'];
$upcoming_tasks[] = [
'name' => $task['name'],
'time' => $display_time,
'action' => $task['action_topic'],
'payload' => $payload_str,
'next_run' => $next_run ? $next_run->format('Y-m-d H:i:s') : null,
'status' => '⏳ Ожидает',
'id' => $task['id']
];
}
if (!empty($upcoming_tasks)) {
usort($upcoming_tasks, fn($a, $b) => strtotime($a['next_run']) <=> strtotime($b['next_run']));
$next_scheduled = $upcoming_tasks[0];
} else {
$next_scheduled = null;
}
// === Ежедневный сброс ===
$stmt_check = $pdo->prepare("SELECT 1 FROM daily_resets WHERE reset_date = ?");
$stmt_check->execute([date('Y-m-d')]);
$daily_reset_today = $stmt_check->fetch() !== false;
// === RPi данные ===
$rpi_data = null;
$stmt_rpi = $pdo->query("SELECT * FROM rpi_monitor ORDER BY timestamp DESC LIMIT 1");
$rpi_data = $stmt_rpi->fetch(PDO::FETCH_ASSOC);
// === Лог-статистика ===
$log_stats = [];
$stmt_log = $pdo->prepare("SELECT keyword, count FROM log_stats WHERE date = ?");
$stmt_log->execute([date('Y-m-d')]);
$log_rows = $stmt_log->fetchAll(PDO::FETCH_ASSOC);
foreach ($log_rows as $row) {
$log_stats[$row['keyword']] = (int)$row['count'];
}
// === Статусы сервисов ===
$services_to_check = [
'iot-automation.service',
'mqtt-to-mysql.service',
'zigbee2mqtt.service',
'mosquitto.service',
];
$service_statuses = [];
foreach ($services_to_check as $service) {
$status = trim(shell_exec("systemctl is-active " . escapeshellarg($service) . " 2>&1"));
$is_active = ($status === 'active');
$service_statuses[] = [
'name' => $service,
'status' => $status,
'is_active' => $is_active,
'color' => $is_active ? 'ok' : 'critical'
];
}
} catch (Exception $e) {
die("Ошибка БД: " . htmlspecialchars($e->getMessage()));
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>⚡️ IoT Суперпанель — Система</title>
<link rel="stylesheet" href="/css/index.css">
</head>
<body>
<div style="margin-bottom:15px;">
<strong>🏠 Система</strong> |
<a href="/sensors.php">📡 Датчики и управление</a>
</div>
<h1>⚡️ IoT Суперпанель — Система</h1>
<br/>
<!-- Статус автоматизации -->
<div class="card" style="background-color: #1a1a1a; border-left: 4px solid #ffffff; margin-bottom: 20px;">
<div class="grid">
<div class="card" style="background: #2a2a2a; padding: 15px;">
<div class="title">🧠 Статус автоматизации</div>
<hr>
<div class="value"><strong>🕔 Текущее время:</strong> <span id="current-time"><?= date('Y-m-d H:i:s') ?></span></div>
<div class="value"><strong>🔄 Ежедневный сброс:</strong> <span id="daily-reset" class="<?= $daily_reset_today ? 'ok' : 'warning' ?>"><?= $daily_reset_today ? '✅ Выполнен сегодня' : '⚠️ Не выполнен' ?></span></div>
<div class="value" id="last-rule">
<?php if ($last_executed_rule): ?>
<strong>✅ Последнее правило:</strong><br>
<small><strong><?= htmlspecialchars($last_executed_rule['name']) ?></strong></small><br>
<small>Действие: <code><?= htmlspecialchars($last_executed_rule['action_topic']) ?></code></small><br>
<small>Payload: <code><?= htmlspecialchars($last_executed_rule['payload_str']) ?></code></small><br>
<small>Время: <strong><?= htmlspecialchars($last_executed_rule['last_triggered']) ?></strong></small>
<?php else: ?>
<strong>✅ Последнее правило:</strong> <span style="color: #888;">—</span>
<?php endif; ?>
</div>
<?php if ($next_scheduled): ?>
<div class="value">
<strong>⏱️ Следующее срабатывание:</strong>
<br><small>
<span style="color: #00c6ff; font-weight: bold;"><?= htmlspecialchars($next_scheduled['name']) ?></span>
в <strong><?= htmlspecialchars($next_scheduled['next_run']) ?></strong>
</small>
</div>
<?php endif; ?>
</div>
<?php if ($rpi_data): ?>
<!-- Статус железа -->
<div class="card">
<?php
$temp = floatval($rpi_data['cpu_temp'] ?? 0);
$temp_class = $temp > 70 ? 'critical' : ($temp > 60 ? 'warning' : 'ok');
$disk = floatval($rpi_data['disk_usage'] ?? 0);
$disk_class = $disk > 90 ? 'critical' : ($disk > 60 ? 'warning' : 'ok');
?>
<div class="title">🖥️ Статус железа</div>
<hr>
<div class="value">🌡️ Температура CPU: <strong class="<?= $temp_class ?>"><?= htmlspecialchars($rpi_data['cpu_temp']) ?>°C</strong></div>
<div class="value">⚡ Загрузка CPU: <strong><?= htmlspecialchars($rpi_data['cpu_load']) ?>%</strong></div>
<div class="value">🧠 Память: <strong><?= htmlspecialchars($rpi_data['mem_used']) ?>MB / <?= htmlspecialchars($rpi_data['mem_total']) ?>MB</strong></div>
<div class="value">💽 Диск: <strong class="<?= $disk_class ?>"><?= htmlspecialchars($rpi_data['disk_usage']) ?>%</strong></div>
<div class="value">🔋 Напряжение ядра: <strong><?= htmlspecialchars($rpi_data['volt_core'] ?? 'N/A') ?>V</strong></div>
<hr>
<div class="timestamp">🏠 IP: <?= htmlspecialchars($rpi_data['ip_addresses']) ?></div>
<div class="timestamp">⏱️ Uptime: <?= htmlspecialchars($rpi_data['uptime']) ?></div>
<div class="timestamp">🕒 Последнее обновление: <?= htmlspecialchars($rpi_data['timestamp']) ?></div>
</div>
<!-- Логи -->
<?php if (!empty($log_stats)): ?>
<div class="card">
<div class="title">📊 Анализ логов</div>
<hr>
<div class="value">
<?php foreach ($log_stats as $keyword => $count): ?>
<div><?= ucfirst(htmlspecialchars($keyword)) ?>: <strong><?= $count ?></strong></div>
<?php endforeach; ?>
</div>
<small class="timestamp">Обновляется каждый час</small>
</div>
<?php endif; ?>
<!-- Сервисы -->
<div class="card">
<div class="title">🔧 Активность сервисов</div>
<?php foreach ($service_statuses as $svc): ?>
<div class="value">
<strong><?= htmlspecialchars($svc['name']) ?>:</strong>
<span class="<?= $svc['color'] ?>">
<?= $svc['is_active'] ? '✅' : '❌' ?> <?= htmlspecialchars($svc['status']) ?>
</span>
</div>
<?php endforeach; ?>
<div class="timestamp">🕒 Проверено: <?= date('H:i:s') ?></div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Ближайшие автоматизации -->
<div class="card" style="background: #1a1a1a; border-left: 4px solid #ffa500; margin-bottom: 20px;">
<div class="title">⏰ Ближайшие автоматизации</div>
<hr>
<?php if (!empty($upcoming_tasks)): ?>
<div class="grid">
<?php foreach (array_slice($upcoming_tasks, 0, 6) as $task): ?>
<div class="card" style="background: #2a2a2a; padding: 15px;">
<div class="title">📅 <?= htmlspecialchars($task['name']) ?></div>
<div class="value">
<div>⏰ <strong>Время:</strong> <?= htmlspecialchars($task['time']) ?></div>
<div>🔧 <strong>Действие:</strong> <code><?= htmlspecialchars($task['action']) ?></code></div>
<div>📦 <strong>Payload:</strong> <code><?= htmlspecialchars($task['payload']) ?></code></div>
<div>⏱️ <strong>Следующий запуск:</strong><br><?= htmlspecialchars($task['next_run']) ?></div>
<div>📊 <strong>Статус:</strong> <?= htmlspecialchars($task['status']) ?></div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="value">Нет активных автоматизаций</div>
<?php endif; ?>
</div>
<!-- API -->
<div class="group-section" id="rpi-monitor">
<h2 onclick="toggleGroup(this)">
🖥️ API
<span class="arrow">▼</span>
</h2>
<div class="grid group-content" id="group-content-rpi-monitor">
<!-- Погода -->
<div class="card weather-card">
<div class="title">🌤️ Погода сейчас</div>
<div id="weather-moscow">Загрузка... Москва</div>
<div id="weather-kaluga">Загрузка... Калуга</div>
</div>
</div>
</div>
<script>
function escapeHtml(text) {
return String(text)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
function updateSystemPanel() {
fetch('/api/live_system_data.php') // ← можно вынести в отдельный API, но пока используем live_data.php
.then(r => r.json())
.then(data => {
document.getElementById('current-time').textContent = data.current_time;
const resetEl = document.getElementById('daily-reset');
if (resetEl) {
resetEl.className = data.daily_reset_today ? 'ok' : 'warning';
resetEl.textContent = data.daily_reset_today ? '✅ Выполнен сегодня' : '⚠️ Не выполнен';
}
if (data.last_executed_rule && document.getElementById('last-rule')) {
const r = data.last_executed_rule;
document.getElementById('last-rule').innerHTML = `
<strong>✅ Последнее правило:</strong><br>
<small><strong>${escapeHtml(r.name)}</strong></small><br>
<small>Действие: <code>${escapeHtml(r.action_topic)}</code></small><br>
<small>Payload: <code>${escapeHtml(r.payload_str)}</code></small><br>
<small>Время: <strong>${escapeHtml(r.last_triggered)}</strong></small>
`;
}
})
.catch(console.error);
}
async function getWeather(lat, lon, city) {
try {
const res = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true&hourly=relativehumidity_2m,pressure_msl&timezone=auto`);
const d = await res.json();
const t = d.current_weather.temperature;
const w = d.current_weather.windspeed;
const p = d.hourly.pressure_msl[0]?.toFixed(0) || '?';
const h = d.hourly.relativehumidity_2m[0] || '?';
document.getElementById(`weather-${city.toLowerCase()}`).innerHTML = `
<strong>📍 ${city}</strong>: ${t}°C | 💨 ${w} км/ч | 💧 ${h}% | 📊 ${p} гПа
`;
} catch (e) {
document.getElementById(`weather-${city.toLowerCase()}`).textContent = `⚠️ Ошибка (${city})`;
}
}
function toggleGroup(h2) {
const content = h2.nextElementSibling;
const arrow = h2.querySelector('.arrow');
const hidden = content.classList.contains('hidden');
content.classList.toggle('hidden', !hidden);
arrow.style.transform = hidden ? 'rotate(-90deg)' : 'rotate(0deg)';
}
document.addEventListener('DOMContentLoaded', () => {
getWeather(55.75, 37.62, 'Moscow');
getWeather(54.52, 36.25, 'Kaluga');
updateSystemPanel();
setInterval(updateSystemPanel, 30_000);
});
</script>
</body>
</html>
Комментарии
Пока нет комментариев. Будьте первым!