обновлено 15.02.2026
sensors.phpФайл sensors.php отвечает за отображение текущих значений датчиков и подготовку данных для построения графиков на веб-странице. Он работает в связке с двумя таблицами в базе данных:
sensor_data — хранит актуальные (последние) значения всех топиков.sensor_info — содержит метаданные: человекочитаемое имя, комнату, группу и иконку для каждого топика.sensor_history — хранит исторические данные (для построения графиков).Устанавливается соединение с MySQL через PDO с включённым режимом исключений для отладки.
Выполняется SQL-запрос, который:
sensor_info (иначе не будет имени/комнаты).sensor_history — то есть для них ведётся запись истории и возможен график.sensor_data.Это гарантирует, что на странице отобразятся только датчики с графиками.
По последнему сегменту топика (например, voltage, temperature) определяется единица измерения:
temperature → °Chumidity, battery → %voltage → Вco2 → ppmЕсли тип неизвестен — единица остаётся пустой.
Для каждого датчика создаётся ассоциативный массив с полями:
topic — полный MQTT-топикvalue — текущее значениеtimestamp — время последнего обновленияname — имя из sensor_info или автоматическое (по типу)room — комнатаgroup — группа («Дом», «Дача» и т.д.)type — тип датчика (последняя часть топика)unit — единица измеренияicon — иконкаВсе датчики группируются по полю group. Если группа не указана или равна «Без группы», она заменяется на «Общие».
Группы выводятся в заданном порядке: сначала «Дом», затем «Дача», потом «Общие». Остальные (если появятся) — в конце.
Для каждой карточки:
<canvas> с уникальным ID для Chart.js.При загрузке страницы:
/api/history.php./api/live_data.php.Страница показывает только то, что имеет историю. Это позволяет управлять набором отображаемых датчиков централизованно — через Python-скрипт, который решает, какие топики писать в sensor_history.
TOPICS_FOR_HISTORY — и он автоматически появился на сайте.<?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);
// Получаем последние данные ТОЛЬКО для топиков, у которых есть хотя бы одна запись в истории
$stmt = $pdo->prepare("
SELECT
s.topic,
s.value,
s.timestamp as last_time,
i.name,
i.room,
i.group_name,
i.icon
FROM sensor_info i
INNER JOIN sensor_data s ON i.topic = s.topic
INNER JOIN (
SELECT topic, MAX(timestamp) as max_time
FROM sensor_data
GROUP BY topic
) latest ON s.topic = latest.topic AND s.timestamp = latest.max_time
WHERE i.topic IN (SELECT DISTINCT topic FROM sensor_history)
ORDER BY i.group_name, i.name
");
$stmt->execute();
$rows = $stmt->fetchAll();
$topics = [];
foreach ($rows as $row) {
$type = basename($row['topic']);
$unit = match($type) {
'temperature' => '°C',
'humidity' => '%',
'light', 'illuminance' => 'лк',
'pm25' => 'мкг/м³',
'co2' => 'ppm',
'voltage' => 'В',
'current' => 'А',
'power' => 'Вт',
'energy' => 'кВт·ч',
'battery' => '%',
'pressure' => 'гПа',
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; }
.value { margin-bottom: 10px; }
.dannie {font-size: 28px;}
</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>
Комментарии
Пока нет комментариев. Будьте первым!