Категории

index.php - IoT Суперпанель — системный мониторинг: как устроен код (упрощенная версия)

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

про главная страница iot системы

Эта страница — центр управления и наблюдения за домашней IoT-системой. В отличие от дашборда с датчиками, здесь акцент сделан на статус автоматизаций, работу сервисов, аппаратные ресурсы и системную аналитику.

Зачем это нужно?

Домашняя автоматизация — это не только датчики и реле. Чтобы система работала стабильно, важно отслеживать:

Эта панель даёт полную картину «здоровья» всей IoT-инфраструктуры.

Как устроен PHP-код?

1. Жёсткая отладка

В начале файла включена максимально подробная диагностика:

Это помогает быстро находить проблемы при разработке и развёртывании.

2. Статус последней выполненной автоматизации

Из таблицы automations извлекается правило с самым свежим last_triggered. Payload (команда) декодируется из JSON и красиво отображается в виде ключ: значение.

3. Расчёт ближайших запусков

Для каждого активного правила (с типом daily или weekly) вычисляется точное время следующего срабатывания:

Все задачи сортируются по времени, и самая ближайшая выделяется отдельно.

4. Мониторинг Raspberry Pi

Последняя запись из таблицы rpi_monitor содержит данные, собранные скриптом на самом устройстве:

Критические значения (температура >70°C, диск >90%) подсвечиваются красным.

5. Статистика по логам

Система анализирует системные логи и подсчитывает ключевые слова (например, «error», «offline», «battery_low»). Эти данные сохраняются в таблице log_stats и отображаются в виде счётчиков.

6. Проверка системных сервисов

Через systemctl is-active проверяется статус важнейших демонов:

Если сервис неактивен — он помечается как критический.

7. Ежедневный сброс

Простая таблица daily_resets фиксирует, был ли сегодня выполнен сброс (например, обнуление счётчика открытий двери). Это нужно для корректной работы ежедневной логики.

Как работает JavaScript-часть?

1. Обновление системных данных

Каждые 30 секунд вызывается /api/live_system_data.php, который возвращает актуальные значения:

Это обновление происходит без перезагрузки страницы.

2. Погода через Open-Meteo

Используется бесплатный публичный API Open-Meteo для отображения текущей погоды в Москве и Калуге. Запрос включает:

3. Сворачиваемые секции

Блок «API» (и потенциально другие) можно свернуть/развернуть кликом по заголовку — для удобства навигации на мобильных устройствах.

Важно: Страница не требует аутентификации, поэтому её следует защищать на уровне веб-сервера (например, через HTTP Basic Auth или доступ только из локальной сети).

Итог

Эта панель — «кокпит» вашей 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, "&amp;")
                .replace(/</g, "&lt;")
                .replace(/>/g, "&gt;");
        }

        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}&current_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>

Комментарии

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

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

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

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


кто я | о блоге

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