Этот PHP-скрипт служит централизованным API-интерфейсом для получения актуальных данных от умной IoT-системы на базе Raspberry Pi, Zigbee-датчиков и MQTT-брокера. Он агрегирует информацию из базы данных и ОС, формируя структурированный JSON-ответ, который используется веб-интерфейсом для отображения состояния системы в реальном времени.
Скрипт позволяет создать единый информационный дашборд для домашней IoT-инфраструктуры: оперативно видеть состояние датчиков, отслеживать работу автоматизаций, замечать аномалии и контролировать здоровье сервера — всё через один HTTP-запрос.
iot_db.systemctl (для проверки статусов служб).Этот эндпоинт — ядро веб-интерфейса умного дома, обеспечивающее прозрачность и контроль над распределённой IoT-системой.
<?php
date_default_timezone_set('Europe/Moscow');
header('Content-Type: application/json; charset=utf8mb4');
// Отключаем отладку — чтобы не засорять JSON
error_reporting(0);
ini_set('display_errors', 0);
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);
// === 1. Последние данные датчиков ===
$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' => '%',
'voltage' => 'mV',
default => ''
};
if (!in_array($type, [
'valve', 'temperature', 'humidity', 'light', 'pm25', 'co2', 'status',
'voltage', 'current', 'power', 'contact', 'vibration','illuminance', 'presence', 'battery'
])) continue;
$display_value = $row['value'] ?? 'Нет данных';
if ($type === 'presence') {
$is_presence = ($display_value == 'true' || $display_value == '1' || strtolower($display_value) === 'on');
$display_value = $is_presence ? '🚶 Движение' : '❌ Тихо';
}
$topics[] = [
'topic' => $row['topic'],
'value' => $display_value,
// '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'] ?? '📡'
];
}
// === 2. Статус последнего и следующего правила ===
$last_executed_rule = null;
$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_row = $stmt_last->fetch(PDO::FETCH_ASSOC);
if ($last_row) {
$payload = json_decode($last_row['action_payload'], true);
$last_executed_rule = [
'name' => $last_row['name'],
'last_triggered' => $last_row['last_triggered'],
'action_topic' => $last_row['action_topic'],
'payload_str' => is_array($payload)
? implode(', ', array_map(fn($k, $v) => "$k: $v", array_keys($payload), array_values($payload)))
: $last_row['action_payload']
];
}
// Проверка ежедневного сброса
$daily_reset_today = false;
$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;
// === 3. Ближайшая запланированная задача ===
$next_scheduled = null;
$stmt_tasks = $pdo->prepare("
SELECT
id, name, schedule_type, schedule_time, schedule_days,
action_topic, action_payload, delay_seconds
FROM automations
WHERE enabled = 1 AND schedule_type != 'none'
ORDER BY created_at
");
$stmt_tasks->execute();
$tasks = $stmt_tasks->fetchAll(PDO::FETCH_ASSOC);
$upcoming = [];
foreach ($tasks as $task) {
$now = new DateTime();
$next_run = null;
if ($task['schedule_type'] === 'daily') {
$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;
} elseif ($task['schedule_type'] === 'weekly') {
$days_map = ['mon' => 1, 'tue' => 2, 'wed' => 3, 'thu' => 4, 'fri' => 5, 'sat' => 6, 'sun' => 0];
$scheduled_days = explode(',', $task['schedule_days'] ?? '');
foreach ($scheduled_days as $day) {
if (!isset($days_map[$day])) continue;
$day_num = $days_map[$day];
$current_day = (int)$now->format('N');
$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_run || $candidate < $next_run) {
$next_run = $candidate;
}
}
}
if ($next_run) {
$upcoming[] = [
'name' => $task['name'],
'next_run' => $next_run->format('Y-m-d H:i:s'),
'time' => $task['schedule_time'],
'delay' => (int)$task['delay_seconds']
];
}
}
if (!empty($upcoming)) {
usort($upcoming, fn($a, $b) => strtotime($a['next_run']) <=> strtotime($b['next_run']));
$next_scheduled = $upcoming[0];
}
// === 4. Тренды (тревоги) ===
$trend_alerts = [];
$monitor_topics = [
'zigbee2mqtt/0x00124b003548b510/temperature',
'zigbee2mqtt/bedroom_sensor/temperature',
'zigbee2mqtt/0x00124b003548b510/battery',
'zigbee2mqtt/bedroom_sensor/battery',
'zigbee2mqtt/0xa4c1385d1ba39457/voltage',
'zigbee2mqtt/0x00124b003548b510/voltage',
'zigbee2mqtt/bedroom_sensor/voltage'
];
foreach ($monitor_topics as $topic) {
$stmt_hist = $pdo->prepare("
SELECT value, timestamp
FROM sensor_history
WHERE topic = ?
ORDER BY timestamp DESC
LIMIT 5
");
$stmt_hist->execute([$topic]);
$history = $stmt_hist->fetchAll(PDO::FETCH_ASSOC);
if (count($history) < 2) continue;
$latest = $history[0];
$oldest = $history[count($history) - 1];
$current_value = floatval($latest['value']);
$past_value = floatval($oldest['value']);
$time_diff_sec = strtotime($latest['timestamp']) - strtotime($oldest['timestamp']);
if ($time_diff_sec <= 0) continue;
$rate_per_min = ($current_value - $past_value) / ($time_diff_sec / 60);
$minutes = round($time_diff_sec / 60);
$alert = null;
if (strpos($topic, 'temperature') !== false && $rate_per_min > 0.6) {
$alert = "🔥 Резкий рост температуры: {$past_value}°C → {$current_value}°C за {$minutes} мин (" . number_format($rate_per_min, 1) . "°C/мин)";
} elseif (strpos($topic, 'voltage') !== false && str_contains($topic, 'bedroom') && $rate_per_min < -0.1) {
$alert = "⚡ Напряжение упало: {$past_value}V → {$current_value}V";
} elseif (strpos($topic, 'battery') !== false && $rate_per_min < -1.0) {
$alert = "🔋 Батарея быстро разряжается: {$past_value}% → {$current_value}%";
} elseif ($topic === 'zigbee2mqtt/0xa4c1385d1ba39457/voltage') {
if ($current_value < 200) {
$alert = "⚡ Критическое падение напряжения: {$current_value}V — возможна авария!";
} elseif ($rate_per_min < -10) {
$alert = "⚡ Напряжение резко упало: {$past_value}V → {$current_value}V";
}
}
if ($alert) {
$trend_alerts[] = ['message' => $alert];
}
}
// === 5. Системное время ===
$current_time = date('Y-m-d H:i:s');
// === 6. Данные RPi ===
$stmt_rpi = $pdo->query("SELECT * FROM rpi_monitor ORDER BY timestamp DESC LIMIT 1");
$rpi_data = $stmt_rpi->fetch(PDO::FETCH_ASSOC);
// === 7. Статусы сервисов ===
$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) {
$status = trim(shell_exec("systemctl is-active " . escapeshellarg($service) . " 2>&1"));
$is_active = ($status === 'active');
$service_statuses[] = [
'name' => $service,
'status' => $status,
'is_active' => $is_active,
'color' => $is_active ? 'ok' : 'critical'
];
}
// === 8. Статистика логов ===
$log_stats = [];
$stmt_log = $pdo->prepare("SELECT keyword, count FROM log_stats WHERE date = ?");
$stmt_log->execute([date('Y-m-d')]);
foreach ($stmt_log->fetchAll(PDO::FETCH_ASSOC) as $row) {
$log_stats[$row['keyword']] = (int)$row['count'];
}
// === Ответ ===
echo json_encode([
'topics' => $topics,
'last_executed_rule' => $last_executed_rule,
'next_scheduled' => $next_scheduled,
'daily_reset_today' => $daily_reset_today,
'trend_alerts' => $trend_alerts,
'rpi_data' => $rpi_data,
'service_statuses' => $service_statuses,
'log_stats' => $log_stats,
'current_time' => $current_time
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Ошибка сервера: ' . $e->getMessage()]);
}
Блог только запустил, все статьи генерирую через нейросеть т.к. лень, возможны ошибки. Просто чтобы вы знали и не запускали ядерный реактор по моим статьям ))
Если у вас есть вопросы, или Нашли неточность? пишите в коментах — вместе поправим и сделаем статью более качественной. Я лично объясню нюансы из практики.
Комментарии
Пока нет комментариев. Будьте первым!