Категории

index_ajax.php главная страница IoT-системы

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

про iot первая страница вывода датчиков на монитор

Этот файл — главная страница моей IoT-панели. Он не просто отображает данные, а собирает их из разных источников: базы данных, системных команд и MQTT-истории. Всё это превращается в удобный интерфейс с карточками, графиками и предупреждениями.

Основные задачи index_ajax.php

  1. Подключается к базе данных (iot_db) и забирает актуальные значения датчиков.
  2. Показывает карточки устройств: температура, влажность, напряжение, состояние клапанов и т.д.
  3. Отображает автоматизации: запланированные задачи (каждый день/неделю) и триггерные правила (реагируют на события).
  4. Анализирует историю: ищет резкие изменения (например, рост температуры или падение напряжения) и показывает алерты.
  5. Мониторит Raspberry Pi: показывает температуру CPU, загрузку, диск, IP и статус сервисов.
  6. Рисует графики для датчиков (температура, влажность и т.д.) через Chart.js, получая данные из API.
  7. Обновляется каждые 30 секунд — чтобы данные были свежими.
  8. Позволяет управлять устройствами через кнопки «ВКЛ/ВЫКЛ» (отправляет команды в MQTT).

Как работает логика внутри файла

1. Подключение к БД и выборка данных

Файл начинается с подключения к MySQL:

new PDO("mysql:host=127.0.0.1;dbname=iot_db;charset=utf8mb4", "iot_user", "123456")

Затем он делает запрос к таблице sensor_data, чтобы получить последнее значение каждого датчика:

SELECT s1.topic, s1.value, s1.timestamp, i.name, i.room, i.group_name, i.icon
FROM sensor_data s1
LEFT JOIN sensor_info i ON s1.topic = i.topic
WHERE s1.timestamp = (SELECT MAX(s2.timestamp) FROM sensor_data s2 WHERE s2.topic = s1.topic)

Эти данные используются для создания карточек устройств.

2. Получение автоматизаций

Файл запрашивает две группы правил из таблицы automations:

3. Анализ резких изменений (Trend Alerts)

Скрипт проверяет историю по ключевым топикам (температура, батарея, напряжение) и ищет аномалии:

Эти алерты появляются в разделе «📈 Анализ тенденций».

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

Файл берёт последние данные из таблицы rpi_monitor и показывает:

5. Отображение карточек устройств

Каждое устройство выводится в виде карточки с:

Карточки группируются по комнатам и типам устройств.

6. Управление устройствами через JavaScript

Когда вы нажимаете «ВКЛ» на карточке клапана:

  1. JavaScript формирует новый топик (заменяет /status на /set).
  2. Отправляет GET-запрос к /admin/send_mqtt.php?topic=...&payload=....
  3. Скрипт публикует команду в MQTT.
  4. Устройство меняет состояние → новое значение попадает в БД → карточка обновляется.

7. Графики с помощью Chart.js

Для каждого датчика (температура, влажность и т.д.) создаётся canvas-элемент. При загрузке страницы JavaScript запрашивает историю через /api/history.php и рисует график.

Что ещё есть в файле

Что дальше?

Эта статья — описание основной страницы. В будущем планирую написать подробные статьи про:

Каждую такую статью можно будет читать независимо, но эта — даст общую картину.


Обновлено:

<?php
// ЖЕСТКАЯ ОТЛАДКА
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// Логируем ВСЁ
file_put_contents('/tmp/php_errors.log', "=== 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');
// ... остальной код ...
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 = $pdo->query("
        SELECT s1.topic, s1.value, s1.timestamp as last_time, i.name, i.room, i.group_name, i.icon
        FROM sensor_data s1
        LEFT JOIN sensor_info i ON s1.topic = i.topic
        WHERE s1.timestamp = (
            SELECT MAX(s2.timestamp)
            FROM sensor_data s2
            WHERE s2.topic = s1.topic
        )
        GROUP BY s1.topic
    ");
    $rows = $stmt->fetchAll();
    $topics = [];
    foreach ($rows as $row) {
        $type = basename($row['topic']);
        if ($type === 'status') $type = 'valve';
        $unit = match($type) {
            'temperature' => '°C',
            'humidity' => '%',
            'light' => 'лк',
            'pm25' => 'мкг/м³',
            'co2' => 'ppm',
            'water_pressure' => 'бар',
            'valve' => 'Вкл/Выкл',
            'current' => 'мА',
    'illuminance' => 'лк',        // 💡 Освещенность
    'presence' => '',             // 🚶 Движение (пустая строка - будет "Да/Нет")
    'battery' => '%',       // 📶 Качество связи

            default => ''
        };
        // Убираем жёсткую фильтрацию — оставляем всё, что есть в базе
        // Но чтобы не сломать UI, оставим проверку только на безопасные типы что показывать в карточках на главной
        if (!in_array($type, ['valve', 'temperature', 'humidity', 'light', 'pm25', 'co2', 'status', 'current', 'power', 'contact', 'vibration', 'illuminance', 'presence', 'battery'])) {
            continue;
        }
        $topics[] = [
            'topic' => $row['topic'],
            'value' => $row['value'] ?? 'Нет данных',
            'timestamp' => $row['last_time'] ?? '—',
            'name' => $row['name'] ?: ucfirst($type),
            'room' => $row['room'] ?: 'Неизвестная комната',
            'group' => $row['group_name'] ?: 'Без группы',
            'type' => $type,
            'unit' => $unit,
            'icon' => $row['icon'] ?? '📡'
        ];
    }



// === Получаем активные автоматизации (задачи) ===
$upcoming_tasks = [];
try {
    $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 = '';
        $status = '⏳ Ожидает';

        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();
            $run_time = new DateTime($task['schedule_time']);

            // Разбиваем дни недели
            $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'); // 1=Пн, 7=Вс

                $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),
                    (int)substr($task['schedule_time'], 6, 2)
                );

                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);
        if (is_array($action_desc)) {
            $payload_str = implode(', ', array_map(fn($k, $v) => "$k: $v", array_keys($action_desc), array_values($action_desc)));
        } else {
            $payload_str = $task['action_payload'];
        }

        // Добавляем задержку, если есть
        $delay_text = $task['delay_seconds'] > 0 ? " (через {$task['delay_seconds']} сек)" : '';

        // Формируем задачу
        $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' => $status,
            'delay' => $task['delay_seconds'],
            'id' => $task['id']
        ];
    }

} catch (PDOException $e) {
    error_log("Ошибка получения автоматизаций: " . $e->getMessage());
}

// === Определяем ближайшее запланированное правило ===
$next_scheduled = null;
if (!empty($upcoming_tasks)) {
    usort($upcoming_tasks, fn($a, $b) => strtotime($a['next_run']) <=> strtotime($b['next_run']));
    $next_scheduled = $upcoming_tasks[0];
}

// === Получаем статистику логов за сегодня ===
$log_stats = [];
try {
    $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'];
    }
} catch (PDOException $e) {
    error_log("Ошибка получения статистики логов: " . $e->getMessage());
}

// === Получаем последние данные Raspberry Pi ===
$rpi_data = null;
try {
    $stmt_rpi = $pdo->query("
        SELECT * FROM rpi_monitor
        ORDER BY timestamp DESC
        LIMIT 1
    ");
    $rpi_data = $stmt_rpi->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
    error_log("Ошибка получения данных RPi: " . $e->getMessage());
}

// === Проверка статусов системных сервисов ===
$services_to_check = [
    'iot-automation.service',
    'mqtt-to-mysql.service',
    'zigbee2mqtt.service',
    'iot-mqtt-listener.service',
    'mosquitto.service',
];

$service_statuses = [];

foreach ($services_to_check as $service) {
    $command = "systemctl is-active " . escapeshellarg($service) . " 2>&1";
    $status = trim(shell_exec($command));
    $is_active = ($status === 'active');

    $service_statuses[] = [
        'name' => $service,
        'status' => $status,
        'is_active' => $is_active,
        'color' => $is_active ? 'ok' : 'critical'
    ];
}






    // === Статус автоматизации ===
    $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'];
    }

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

} catch (Exception $e) {
    die("Ошибка БД: " . htmlspecialchars($e->getMessage()));
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta charset="UTF-8">
    <title>⚡️ IoT Суперпанель</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        body { font-family: 'Segoe UI', sans-serif; background-color: #121212; color: #ffffff; margin: 0; padding: 20px; line-height: 1.6; }
        h1 { color: #00c6ff; font-size: 24px; margin-bottom: 30px; text-align: center; }
        .group-section { margin-bottom: 40px; }
        .group-section h2 { color: #cccccc; font-size: 20px; border-left: 4px solid #00c6ff; padding-left: 10px; margin-bottom: 20px; cursor: pointer; background-color: #1f1f1f; padding: 10px; border-radius: 6px; }
        .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; margin-top: 10px; }
        .card { background-color: #1f1f1f; border-radius: 12px; padding: 20px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4); }
        .title { font-size: 18px; font-weight: bold; display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
        .value { margin-bottom: 10px; }
        .dannie {font-size: 28px;}
        .timestamp { font-size: 12px; color: #aaaaaa; margin-bottom: 15px; display: block; }
        .status-indicator { display: inline-block; width: 12px; height: 12px; border-radius: 50%; vertical-align: middle; margin-right: 10px; }
        .status-on { background-color: #4caf50; }
        .status-off { background-color: #f44336; }
        .controls { display: flex; gap: 10px; margin-top: 15px; }
        .btn { padding: 10px 16px; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 14px; }
        .btn-on { background-color: #00c6ff; color: #000; }
        .btn-off { background-color: #333; color: #ccc; }
        canvas { width: 100% !important; height: auto !important; margin-top: 15px; border-radius: 8px; background-color: #111; }
        .hidden { display: none; }
        .arrow { float: right; transition: transform 0.3s ease; }
        .weather-card { background-color: #2c2c2c; margin-bottom: 30px; border-left: 4px solid #00c6ff; padding: 15px; border-radius: 8px; }
        .critical { color: #e74c3c; font-weight: bold; }
        .warning { color: #f39c12; }
        .ok { color: #2ecc71; }
    </style>
</head>
<body>
<h1>⚡️ IoT Суперпанель</h1>

<!-- Статус автоматизации -->
<div class="card" style="background-color: #1a1a1a; border-left: 4px solid #ffffff; margin-bottom: 20px;">
    <div class="title">🧠 Статус автоматизации</div>
    <hr>
<div class="grid">
<div class="card" style="background: #2a2a2a; padding: 15px;">
    <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>
</div>
</div>
<?php endif; ?>
    <div class="timestamp">Обновляется каждые 30 сек</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>

<!-- Raspberry Pi Monitoring -->
<?php if ($rpi_data): ?>
<div class="group-section" id="rpi-monitor">
    <h2 onclick="toggleGroup(this)">
        🖥️ Raspberry Pi Статус
        <span class="arrow">▼</span>
    </h2>
    <div class="grid group-content" id="group-content-rpi-monitor">

        <!-- Карточка: Статус железа -->
        <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">📊 Анализ логов (<?= date('Y-m-d') ?>)</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 class="card weather-card">
    <div class="title">🌤️ Погода сейчас</div>
    <div id="weather-moscow">Загрузка... Москва</div>
    <div id="weather-kaluga">Загрузка... Калуга</div>
</div>


<!-- Датчики -->
<?php if (empty($topics)): ?>
    <p>Нет доступных датчиков.</p>
<?php else:
    // Улучшенная группировка - объединяем "Без группы" с "Общие"
    $groups = [];
    foreach ($topics as $data) {
        $groupName = $data['group'];
        if (empty($groupName) || $groupName === 'Без группы') {
            $groupName = 'Общие';
        }
        if (!isset($groups[$groupName])) $groups[$groupName] = [];
        $groups[$groupName][] = $data;
    }

    // Сортируем группы в нужном порядке
    $preferredOrder = ['Дом', 'Дача', 'Общие'];
    uksort($groups, function($a, $b) use ($preferredOrder) {
        $posA = array_search($a, $preferredOrder);
        $posB = array_search($b, $preferredOrder);
        if ($posA === false) $posA = 999;
        if ($posB === false) $posB = 999;
        return $posA - $posB;
    });

    foreach ($groups as $groupName => $groupItems): ?>
        <div class="group-section">
            <h2 onclick="toggleGroup(this)">
                <?= htmlspecialchars($groupName) ?>
                <span class="arrow">▼</span>
            </h2>
            <div class="grid group-content">
                <?php foreach ($groupItems as $data):
                    $safeId = 'sensor-' . base64_encode($data['topic']);
                ?>
                    <div class="card" id="<?= htmlspecialchars($safeId) ?>">
                        <div class="title">
                            <?= $data['icon'] ?>
                            <?= htmlspecialchars($data['name']) ?>
                            <small style="color:#888">(<?= htmlspecialchars($data['room']) ?>)</small>
                        </div>

                        <!-- 👇 БЛОК ОТОБРАЖЕНИЯ ЗНАЧЕНИЯ -->
                        <?php if ($data['type'] === 'valve' && !str_contains($data['topic'], '/set/')): ?>
                            <div class="value dannie">
                                <span class="status-indicator status-<?= strtolower($data['value']) === 'on' ? 'on' : 'off' ?>"></span>
                                <?= htmlspecialchars($data['value']) ?>
                            </div>
                            <div class="controls">
                                <button class="btn btn-on" onclick="sendCommand('<?= rawurlencode($data['topic']) ?>', 'ON')">ВКЛ</button>
                                <button class="btn btn-off" onclick="sendCommand('<?= rawurlencode($data['topic']) ?>', 'OFF')">ВЫКЛ</button>
                            </div>
                        <?php else: ?>
                                <div class="value dannie"><?= htmlspecialchars($data['value']) ?> <?= htmlspecialchars($data['unit']) ?></div>

                        <?php endif; ?>
                        <div class="timestamp">🕔 <?= htmlspecialchars($data['timestamp']) ?></div>
<!--графики что показывать-->
                        <?php if (in_array($data['type'], ['temperature', 'humidity', 'light', 'pm25', 'co2'])): ?>
                            <canvas id="chart-<?= htmlspecialchars($safeId) ?>" height="100"></canvas>
                        <?php endif; ?>
                    </div>
                <?php endforeach; ?>
            </div>
        </div>





    <?php endforeach; ?>
<?php endif; ?>
<script>
// Графики
const charts = {};
document.querySelectorAll('canvas[id^="chart-"]').forEach(canvas => {
    const safeId = canvas.id.replace('chart-', '');
    const topic = atob(safeId.replace('sensor-', ''));

    const chart = new Chart(canvas.getContext('2d'), {
        type: 'line',
        data: {
            labels: [],
            datasets: [{
                label: '',
                data: [],
                borderColor: '#00c6ff',
                backgroundColor: 'rgba(0, 198, 255, 0.1)',
                fill: true,
                tension: 0.4,
                pointRadius: 0
            }]
        },
        options: {
            responsive: true,
            plugins: { legend: false },
            scales: { x: { display: false }, y: { beginAtZero: false } },
            animation: false
        }
    });
    charts[topic] = chart;
    loadChartData(topic);
});


function loadChartData(topic) {
    fetch('/api/history.php?topic=' + encodeURIComponent(topic))
        .then(r => r.json())
        .then(data => {
            if (!Array.isArray(data) || data.length < 2) return;
            const labels = data.map(p => p.timestamp.split(' ')[1]);
            const values = data.map(p => parseFloat(p.value)).filter(v => !isNaN(v));
            if (values.length === 0) return;
            const chart = charts[topic];
            if (chart) {
                chart.data.labels = labels;
                chart.data.datasets[0].data = values;
                chart.update('none');
            }
        });
}

function escapeHtml(text) {
    return String(text)
        .replace(/&/g, "&amp;")
        .replace(/</g, "<")
        .replace(/>/g, ">");
}

function getSafeId(topic) {
    return 'sensor-' + btoa(topic);
}

// Обновление панели
function updatePanel() {
    fetch('/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) {
                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>
                `;
            }
            // Датчики
            data.topics.forEach(item => {
                const id = getSafeId(item.topic);
                const card = document.getElementById(id);
                if (!card) return;
                const valueEl = card.querySelector('.value');
                const timeEl = card.querySelector('.timestamp');
                if (valueEl && !valueEl.querySelector('.controls')) {
                    if (item.type === 'valve') {
                        const on = item.value.toLowerCase() === 'on';
                        valueEl.innerHTML = `<span class="status-indicator status-${on ? 'on' : 'off'}"></span>${escapeHtml(item.value)}`;
                    } else {
                        valueEl.textContent = `${item.value} ${item.unit}`;
                    }
                }
                if (timeEl) timeEl.textContent = '🕔 ' + item.timestamp;
            });
        })
        .catch(console.error);
function updatePanel() {
    console.log('=== START UPDATE ===', new Date().toLocaleTimeString());
    fetch('/api/live_data.php')
        .then(r => {
            console.log('Response status:', r.status);
            return r.json();
        })
        .then(data => {
            console.log('Received data:', data);
            console.log('Topics count:', data.topics.length);

            // Логируем конкретные датчики
            data.topics.forEach(item => {
                if (item.topic.includes('illuminance') || item.topic.includes('humidity')) {
                    console.log('Sensor:', item.topic, 'Value:', item.value, 'Time:', item.timestamp);
                }
            });

            // Остальная обработка...
            data.topics.forEach(item => {
                const id = getSafeId(item.topic);
                const card = document.getElementById(id);
                if (!card) {
                    console.log('Card not found for:', item.topic);
                    return;
                }
                // ... остальной код ...
            });
        })
        .catch(e => console.error('Update error:', e));
}
}

function sendCommand(topic, cmd) {
    const setTopic = decodeURIComponent(topic).replace('/status', '/set');
    fetch(`/admin/send_mqtt.php?topic=${encodeURIComponent(setTopic)}&payload=${encodeURIComponent(JSON.stringify({state: cmd}))}`)
        .then(r => r.text())
        .then(() => alert('Команда отправлена'))
        .catch(e => alert('Ошибка: ' + e));
}

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)';
}

// Погода
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})`;
    }
}

// Запуск
document.addEventListener('DOMContentLoaded', () => {
    getWeather(55.75, 37.62, 'Moscow');
    getWeather(54.52, 36.25, 'Kaluga');
    updatePanel();
    setInterval(updatePanel, 60_000);
    setInterval(() => Object.keys(charts).forEach(loadChartData), 60_000);
});
</script>
</body>
</html>

Обновлено:

буду обновлять по мере изменения файла, как минимум стили надо в отдельный файл а то жирный код получился

style.css

/* style.css */

:root {
  --bg-color: #1a1a1a;
  --card-bg: #2d2d2d;
  --accent-color: #00c6ff;
  --accent-gradient-start: #00c6ff;
  --accent-gradient-end: #0072ff;
  --text-color: #f5f5f5;
  --switch-bg-off: #444;
  --switch-bg-on: #00c6ff;
  --border-radius: 16px;
  --grid-gap: 1.5rem;
  --padding: 1.5rem;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background-color: var(--bg-color);
  color: var(--text-color);
  line-height: 1.6;
}

header {
  background: linear-gradient(90deg, var(--accent-gradient-start), var(--accent-gradient-end));
  color: white;
  padding: 1.5rem 2rem;
  text-align: center;
  font-size: 1.8rem;
  font-weight: bold;
}

main {
  max-width: 1400px;
  margin: 2rem auto;
  padding: var(--padding);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--grid-gap);
}

.card {
  background: var(--card-bg);
  border-radius: var(--border-radius);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
  padding: 1.5rem;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.card:hover {
  transform: translateY(-6px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}

.title {
  font-weight: bold;
  margin-bottom: 0.5rem;
  display: flex;
  align-items: center;
  font-size: 1.2rem;
}

.icon {
  font-size: 1.5rem;
  margin-right: 0.5rem;
  color: var(--accent-color);
}

.value {
  font-size: 1.6rem;
  margin-top: 0.5rem;
  color: white;
}

.topic {
  font-size: 0.8rem;
  color: #aaa;
  margin-top: 0.5rem;
}

form {
  margin-top: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
}

label {
  font-size: 0.95rem;
}

input[type="number"] {
  width: 100%;
  padding: 0.5rem;
  border: none;
  border-radius: 8px;
  background: #444;
  color: white;
  font-size: 1rem;
}

button {
  padding: 0.6rem;
  font-size: 1rem;
  background: var(--accent-color);
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.3s ease;
}

button:hover {
  background: #00aaff;
}

.switch {
  position: relative;
  display: inline-block;
  width: 50px;
  height: 24px;
}

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.slider {
  position: absolute;
  cursor: pointer;
  top: 0; left: 0;
  right: 0; bottom: 0;
  background-color: var(--switch-bg-off);
  transition: .4s;
  border-radius: 34px;
}

.slider:before {
  position: absolute;
  content: "";
  height: 20px;
  width: 20px;
  left: 2px;
  bottom: 2px;
  background-color: white;
  transition: .4s;
  border-radius: 50%;
}

input:checked + .slider {
  background-color: var(--switch-bg-on);
}

input:checked + .slider:before {
  transform: translateX(26px);
}

footer {
  text-align: center;
  padding: 2rem 1rem;
  font-size: 0.9rem;
  color: #666;
  margin-top: 2rem;
}

.timestamp {
    font-size: 0.8rem;
    color: #aaa;
    margin-top: 0.3rem;
}

.delete-button {
    display: inline-block;
    margin-top: 1rem;
    padding: 0.4rem 0.8rem;
    background-color: #4169E1;
    color: white;
    text-decoration: none;
    border-radius: 6px;
    font-size: 0.9rem;
    transition: background-color 0.3s ease;
}

.delete-button:hover {
    background-color: #e60000;
}

.delete-button:active {
    background-color: #cc0000;
}

.delete-button::selection {
    background: #ff4d4d;
}



.card.humidity .icon::before { content: "\1F4A7"; } /* Капля воды */
.card.temperature .icon::before { content: "\2103"; } /* °C */
.card.light .icon::before { content: "\1F506"; } /* 💡 */
.card.door .icon::before { content: "\1F6AA"; } /* 🚪 */
.card.motion .icon::before { content: "\1F6E3"; } /* 🛣️ */
.card.power .icon::before { content: "\26A1"; } /* ⚡ */

.group-divider {
    width: 100%;
    height: 24px;
    margin: 30px 0;
    background-color: #f5f5f5;
    border-top: 1px solid #ddd;
    position: relative;
}

/* Опционально: подпись внутри разделителя */
.group-divider::after {
    content: "🌿 Конец группы";
    display: block;
    text-align: center;
    font-size: 0.9em;
    color: #888;
    margin-top: -16px;
    background: white;
    width: 120px;
    padding: 0 10px;
    margin-left: auto;
    margin-right: auto;
}

Комментарии

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

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

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

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

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

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


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

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