обновлено 15.02.2026
Логика работы файла sensors.php
Файл sensors.php отвечает за отображение текущих значений датчиков и подготовку данных для построения графиков на веб-странице. Он работает в связке с двумя таблицами в базе данных:
sensor_data— хранит актуальные (последние) значения всех топиков.sensor_info— содержит метаданные: человекочитаемое имя, комнату, группу и иконку для каждого топика.sensor_history— хранит исторические данные (для построения графиков).
Основные функции и этапы работы
1. Подключение к базе данных
Устанавливается соединение с MySQL через PDO с включённым режимом исключений для отладки.
2. Выборка только тех датчиков, у которых есть история
Выполняется SQL-запрос, который:
- Берёт только те топики, что присутствуют в таблице
sensor_info(иначе не будет имени/комнаты). - Фильтрует только те топики, которые есть в
sensor_history— то есть для них ведётся запись истории и возможен график. - Для каждого такого топика выбирается самая свежая запись из
sensor_data.
Это гарантирует, что на странице отобразятся только датчики с графиками.
3. Определение единицы измерения
По последнему сегменту топика (например, voltage, temperature) определяется единица измерения:
temperature→ °Chumidity,battery→ %voltage→ Вco2→ ppm- и т.д.
Если тип неизвестен — единица остаётся пустой.
4. Формирование данных для отображения
Для каждого датчика создаётся ассоциативный массив с полями:
topic— полный MQTT-топикvalue— текущее значениеtimestamp— время последнего обновленияname— имя изsensor_infoили автоматическое (по типу)room— комнатаgroup— группа («Дом», «Дача» и т.д.)type— тип датчика (последняя часть топика)unit— единица измеренияicon— иконка
5. Группировка по категориям
Все датчики группируются по полю group. Если группа не указана или равна «Без группы», она заменяется на «Общие».
6. Сортировка групп
Группы выводятся в заданном порядке: сначала «Дом», затем «Дача», потом «Общие». Остальные (если появятся) — в конце.
7. Генерация HTML и JavaScript
Для каждой карточки:
- Отображается имя, комната, текущее значение и время.
- Создаётся элемент
<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", "Abudfv09540056");
$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>
Комментарии
Пока нет комментариев. Будьте первым!