Этот код реализует веб-интерфейс для отображения данных с умных датчиков (температура, влажность, освещённость и т.д.), подключённых к системе IoT. Он показывает текущие значения и рисует графики изменения параметров за последнее время.
Проект состоит из трёх частей:
sensor_data) и метаинформацию о них (sensor_info).
Устанавливается соединение с MySQL через PDO, включается строгий режим ошибок и задаётся временная зона Europe/Moscow.
Выполняется SQL-запрос, который находит самую свежую записьMAX(timestamp)). При этом отображаются только те датчики, у которых есть описание в таблице sensor_info.
Поддерживаются только датчики определённых типов: temperature, humidity, light, pm25, co2. Тип определяется из последнего сегмента MQTT-топика (например, из home/livingroom/temperature извлекается temperature).
Для каждого датчика указываются:
sensor_info)Датчики сортируются по группам. Группы выводятся в заданном порядке: сначала «Дом», потом «Дача», затем «Общие». Остальные — в алфавитном порядке.
Для каждой карточки создаётся пустой график с помощью библиотеки Chart.js. График настроен на линейный тип, без легенды и без подписей по оси X (только время).
При загрузке страницы вызывается loadChartData(topic), который делает запрос к /api/history.php?topic=.... Сервер возвращает последние, например, 100 точек, и график заполняется ими.
Каждую минуту вызывается updateValues(), который обращается к /api/live_data.php и получает актуальные значения всех датчиков. Эти данные подставляются в карточки без перезагрузки страницы.
Раз в минуту графики тоже обновляются — подгружаются свежие данные, чтобы отображать динамику в реальном времени.
Такая панель идеально подходит для домашней системы мониторинга:
Благодаря модульной структуре (отдельные 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, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
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>
Комментарии
Пока нет комментариев. Будьте первым!