Категории

sensors.php - как работает панель IoT-датчиков: разбор кода

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

про sensors.php датчики с графиками

Этот код реализует веб-интерфейс для отображения данных с умных датчиков (температура, влажность, освещённость и т.д.), подключённых к системе IoT. Он показывает текущие значения и рисует графики изменения параметров за последнее время.

Основные компоненты

Проект состоит из трёх частей:

Что делает PHP-часть?

1. Подключение к базе данных

Устанавливается соединение с MySQL через PDO, включается строгий режим ошибок и задаётся временная зона Europe/Moscow.

2. Выбор последних значений датчиков

Выполняется SQL-запрос, который находит самую свежую записьMAX(timestamp)). При этом отображаются только те датчики, у которых есть описание в таблице sensor_info.

3. Фильтрация по типу

Поддерживаются только датчики определённых типов: temperature, humidity, light, pm25, co2. Тип определяется из последнего сегмента MQTT-топика (например, из home/livingroom/temperature извлекается temperature).

4. Формирование данных

Для каждого датчика указываются:

5. Группировка и сортировка

Датчики сортируются по группам. Группы выводятся в заданном порядке: сначала «Дом», потом «Дача», затем «Общие». Остальные — в алфавитном порядке.

Что делает JavaScript-часть?

1. Инициализация графиков

Для каждой карточки создаётся пустой график с помощью библиотеки Chart.js. График настроен на линейный тип, без легенды и без подписей по оси X (только время).

2. Загрузка исторических данных

При загрузке страницы вызывается loadChartData(topic), который делает запрос к /api/history.php?topic=.... Сервер возвращает последние, например, 100 точек, и график заполняется ими.

3. Обновление «живых» значений

Каждую минуту вызывается updateValues(), который обращается к /api/live_data.php и получает актуальные значения всех датчиков. Эти данные подставляются в карточки без перезагрузки страницы.

4. Периодическое обновление графиков

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

Важно: Весь код ориентирован на минимальную конфигурацию — без фреймворков, без кэширования, без авторизации. Это делает его простым для понимания и развёртывания на локальном мини-ПК (например, на Raspberry Pi или ARM64-устройстве).

Для чего это нужно?

Такая панель идеально подходит для домашней системы мониторинга:

Благодаря модульной структуре (отдельные API-эндпоинты для истории и текущих данных) легко добавить новые датчики или интегрировать с умным домом.

Код можно использовать как основу для собственного self-hosted IoT-дашборда — особенно если вы предпочитаете автономные, недорогие и локальные решения.

<?php
// Только минимальная настройка
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);

    // Выбираем только датчики, у которых есть графики
    $allowed_types = ['temperature', 'humidity', 'light', 'pm25', 'co2'];
    $placeholders = str_repeat('?,', count($allowed_types) - 1) . '?';

    $stmt = $pdo->prepare("
        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
        )
        AND i.topic IS NOT NULL
    ");
    $stmt->execute();
    $rows = $stmt->fetchAll();

    $topics = [];
    foreach ($rows as $row) {
        $type = basename($row['topic']);
        if (!in_array($type, $allowed_types)) continue;

        $unit = match($type) {
            'temperature' => '°C',
            'humidity' => '%',
            'light' => 'лк',
            'pm25' => 'мкг/м³',
            'co2' => 'ppm',
            default => ''
        };

        $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'] ?? '📡'
        ];
    }

    // Группировка
    $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;
    });

} 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>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <link rel="stylesheet" href="/css/style.css">
<style>
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; margin-top: 10px; }
.group-section { margin-bottom: 40px; }
        .group-section h2 { color: #cccccc; ... }
        .grid { display: grid; ... }
        .card { background-color: #1f1f1f; ... }  /* ← ЭТОТ .card — дубль! */
        .title { font-size: 18px; ... }           /* ← дубль! */
        .value { margin-bottom: 10px; }
        .dannie {font-size: 28px;}
        .timestamp { font-size: 12px; ... }       /* ← дубль! */
        ...и всё до конца файла (включая .hidden, .arrow, .weather-card и т.д.)
</style>

</head>
<body>
    <h1>📊 Датчики с графиками</h1>

    <?php if (empty($topics)): ?>
        <p>Нет доступных датчиков с графиками.</p>
    <?php else: ?>
        <?php foreach ($groups as $groupName => $groupItems): ?>
            <div class="group-section">
                <h2><?= htmlspecialchars($groupName) ?></h2>
                <div class="grid">
                    <?php foreach ($groupItems as $data):
                        $safeId = 'sensor-' . base64_encode($data['topic']);
                    ?>
                        <div class="card">
                            <div class="title">
                                <?= $data['icon'] ?>
                                <?= htmlspecialchars($data['name']) ?>
                                <small style="color:#888">(<?= htmlspecialchars($data['room']) ?>)</small>
                            </div>
                            <div class="value"><?= htmlspecialchars($data['value']) ?> <?= htmlspecialchars($data['unit']) ?></div>
                            <div class="timestamp">🕔 <?= htmlspecialchars($data['timestamp']) ?></div>
                            <canvas id="chart-<?= htmlspecialchars($safeId) ?>" height="100"></canvas>
                        </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, "&lt;")
                .replace(/>/g, "&gt;");
        }

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

        // Обновление значений каждую минуту
        function updateValues() {
            fetch('/api/live_data.php')
                .then(r => r.json())
                .then(data => {
                    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.textContent = `${item.value} ${item.unit}`;
                        }
                        if (timeEl) {
                            timeEl.textContent = '🕔 ' + item.timestamp;
                        }
                    });
                })
                .catch(console.error);
        }

        document.addEventListener('DOMContentLoaded', () => {
            updateValues();
            setInterval(updateValues, 60_000);
            setInterval(() => Object.keys(charts).forEach(loadChartData), 60_000);
        });
    </script>
</body>
</html>

Комментарии

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

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

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

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


кто я | о блоге

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