Log Filer

Tilbage til liste

temp_graph.txt

<?php // temp_graph.php - Integreret version med virkende farver + ny sampling // Opdateret 05. april 2026 error_reporting(E_ALL); ini_set('display_errors', 1); date_default_timezone_set('Europe/Copenhagen'); require_once '/home/skjoldpe/silkeborg.skjoldpetersen.dk/config.php'; $conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME); $conn->set_charset("utf8mb4"); if ($conn->connect_error) { die("Connection failed: " . $conn->connect_error); } $conn->query("SET time_zone = '+02:00'"); // GET params $interval = $_GET['interval'] ?? '1h'; $min_temp = (float)($_GET['min_temp'] ?? 0); $max_temp = (float)($_GET['max_temp'] ?? 60); $start_date = $_GET['start_date'] ?? ''; $end_date = $_GET['end_date'] ?? ''; // Intervals med sampling $intervals = [ '5min' => ['sql' => '5 MINUTE', 'sec' => 300, 'label' => '5 min', 'sample_min' => 2], '10min' => ['sql' => '10 MINUTE', 'sec' => 600, 'label' => '10 min', 'sample_min' => 2], '30min' => ['sql' => '30 MINUTE', 'sec' => 1800, 'label' => '30 min', 'sample_min' => 2], '1h' => ['sql' => '1 HOUR', 'sec' => 3600, 'label' => '1 time', 'sample_min' => 2], '2h' => ['sql' => '2 HOUR', 'sec' => 7200, 'label' => '2 timer', 'sample_min' => 2], '3h' => ['sql' => '3 HOUR', 'sec' => 10800, 'label' => '3 timer', 'sample_min' => 2], '6h' => ['sql' => '6 HOUR', 'sec' => 21600, 'label' => '6 timer', 'sample_min' => 2], '12h' => ['sql' => '12 HOUR', 'sec' => 43200, 'label' => '12 timer', 'sample_min' => 15], '18h' => ['sql' => '18 HOUR', 'sec' => 64800, 'label' => '18 timer', 'sample_min' => 15], '1d' => ['sql' => '1 DAY', 'sec' => 86400, 'label' => '1 dag', 'sample_min' => 15], '2d' => ['sql' => '2 DAY', 'sec' => 172800, 'label' => '2 dage', 'sample_min' => 15], '7d' => ['sql' => '7 DAY', 'sec' => 604800, 'label' => '7 dage', 'sample_min' => 60], '14d' => ['sql' => '14 DAY', 'sec' => 1209600,'label' => '14 dage', 'sample_min' => 60], '30d' => ['sql' => '30 DAY', 'sec' => 2592000,'label' => '30 dage', 'sample_min' => 120] ]; $interval_config = $intervals[$interval] ?? $intervals['1h']; $sql_interval = $interval_config['sql']; $interval_sec = $interval_config['sec']; $sample_minutes = $interval_config['sample_min']; $is_multi_day = (!empty($start_date) && !empty($end_date)) || $interval_sec >= 86400; // Date filter $date_filter = ''; $params = []; $types = ''; $start_time_ms = null; $current_time_ms = null; if (!empty($start_date) && !empty($end_date)) { $date_filter = 't.timestamp BETWEEN ? AND ?'; $params[] = $start_date . ' 00:00:00'; $params[] = $end_date . ' 23:59:59'; $types .= 'ss'; $start_time_dt = new DateTime($start_date); $end_time_dt = new DateTime($end_date); $start_time_ms = $start_time_dt->getTimestamp() * 1000; $current_time_ms = $end_time_dt->getTimestamp() * 1000 + 86399000; } else { $date_filter = 't.timestamp >= NOW() - INTERVAL ' . $sql_interval; } // Dynamisk sampling $group_by_time = ($sample_minutes == 2) ? "DATE_FORMAT(CAST(t.tid AS DATETIME), '%Y-%m-%d %H:%i:00')" : "DATE_FORMAT(DATE_ADD('1970-01-01', INTERVAL FLOOR(UNIX_TIMESTAMP(t.tid) / 900) * 900 SECOND), '%Y-%m-%d %H:%i:00')"; $sql = " SELECT $group_by_time AS bucket_tid, t.board_id, t.sensor_id, AVG(t.temp) AS avg_temp, s.sensor_name FROM temp_sensors_v1_1 t LEFT JOIN sensor_config_v1_1 s ON t.board_id = s.board_id AND t.sensor_id = s.sensor_id WHERE " . $date_filter; if ($min_temp > 0) { $sql .= ' AND t.temp >= ?'; $params[] = $min_temp; $types .= 'd'; } if ($max_temp < 1000) { $sql .= ' AND t.temp <= ?'; $params[] = $max_temp; $types .= 'd'; } $sql .= " GROUP BY bucket_tid, t.board_id, t.sensor_id HAVING avg_temp IS NOT NULL ORDER BY bucket_tid ASC"; $stmt = $conn->prepare($sql); if (!empty($params)) $stmt->bind_param($types, ...$params); $stmt->execute(); $result = $stmt->get_result(); $raw_data = []; while ($row = $result->fetch_assoc()) { $raw_data[] = $row; } $stmt->close(); // === ÆNDRING: Byg sensor_data og begræns til de 4 seneste sensorer === $sensor_data = []; $colors = ['#007BFF', '#28A745', '#DC3545', '#FFC107']; // kun 4 farver da vi max viser 4 $i = 0; // Loop gennem rådata og byg sensor_data foreach ($raw_data as $row) { $key = ($row['sensor_name'] ?? $row['sensor_id']) . ' (' . $row['board_id'] . ')'; if (!isset($sensor_data[$key])) { $sensor_data[$key] = [ 'label' => $key, 'data' => [], 'color' => $colors[$i % count($colors)] ]; $i++; } // RETTELSE HER: Brug 'bucket_tid' og 'avg_temp' if (!empty($row['bucket_tid'])) { $timestamp = strtotime($row['bucket_tid']) * 1000; if ($timestamp > 0) { $sensor_data[$key]['data'][] = [ 'x' => $timestamp, 'y' => (float)$row['avg_temp'] ]; } } } // Begræns til de 4 seneste sensorer (baseret på nyeste måling) if (!empty($sensor_data)) { foreach ($sensor_data as $key => &$sensor) { if (!empty($sensor['data'])) { $lastPoint = end($sensor['data']); $sensor['last_time'] = $lastPoint['x']; } else { $sensor['last_time'] = 0; } } unset($sensor); // Sorter efter seneste tidspunkt (nyeste først) uasort($sensor_data, function($a, $b) { return $b['last_time'] <=> $a['last_time']; }); // Behold kun de 4 nyeste sensorer $sensor_data = array_slice($sensor_data, 0, 4, true); } // === SLUT ÆNDRING === $last_temps_sql = "SELECT s.sensor_name, t.board_id, t.sensor_id, t.temp, t.tid FROM temp_sensors_v1_1 t LEFT JOIN sensor_config_v1_1 s ON t.board_id = s.board_id AND t.sensor_id = s.sensor_id WHERE t.timestamp = (SELECT MAX(t2.timestamp) FROM temp_sensors_v1_1 t2 WHERE t2.board_id = t.board_id AND t2.sensor_id = t.sensor_id)"; $last_temps_result = $conn->query($last_temps_sql); $last_temps = []; while ($row = $last_temps_result->fetch_assoc()) { $name = $row['sensor_name'] ?: $row['sensor_id']; $key = $name . ' (' . $row['board_id'] . ')'; $last_temps[$key] = ['temp' => (float)$row['temp'], 'tid' => $row['tid']]; } $filtered_last_temps = $last_temps; // Sorter listen så de sensorer, der er i grafen ($sensor_data), kommer først uksort($filtered_last_temps, function($a, $b) use ($sensor_data, $filtered_last_temps) { $a_in_graph = isset($sensor_data[$a]); $b_in_graph = isset($sensor_data[$b]); if ($a_in_graph && !$b_in_graph) return -1; // $a skal op if (!$a_in_graph && $b_in_graph) return 1; // $b skal op // Nu virker denne linje, fordi $filtered_last_temps er med i "use" return strtotime($filtered_last_temps[$b]['tid']) <=> strtotime($filtered_last_temps[$a]['tid']); }); // Time bounds if (!$start_time_ms) { $now = new DateTime(); $start_time_dt = clone $now; $start_time_dt->sub(new DateInterval('PT' . ($interval_sec / 60) . 'M')); $start_time_ms = $start_time_dt->getTimestamp() * 1000; $current_time_ms = $now->getTimestamp() * 1000; } $conn->close(); $default_colors = ['#007BFF', '#28A745', '#DC3545', '#FFC107', '#6F42C1', '#FD7E14', '#20C997', '#E83E8C']; $min_options = [-10, 0, 5, 10, 15, 20, 25, 30, 35]; $max_options = array_merge([20, 30], range(40, 120, 10), range(120, 171, 10)); ?> <!DOCTYPE html> <html lang="da"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Temperatur Graf</title> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script> <style> .navbar-brand { font-weight: bold; } .navbar { background-color: #007bff; } .nav-link { color: white !important; } .nav-link:hover { color: #ffc107 !important; } body { background-color: #f8f9fa; } .card { box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .card-boks { width: 300px; } .chart-container { position: relative; height: 500px; margin-top: 10px; } .no-data { text-align: center; color: #6c757d; font-style: italic; } .last-temp-item { display: flex; align-items: flex-start; margin-bottom: 5px; flex-wrap: wrap; gap: 5px; } .last-temp-checkbox { margin-right: 3px; flex-shrink: 0; } .last-temp-name { flex-grow: 1; margin-left: 5px; word-wrap: break-word; white-space: normal; font-size: 0.9em; min-width: 0; } .last-temp-temp { margin-right: 10px; font-weight: bold; flex-shrink: 0; } .last-temp-color { width: 25px; height: 20px; border: 1px solid #ccc; flex-shrink: 0; } .temp-buttons { display: flex; flex-wrap: wrap; gap: 1px; } .temp-btn { padding: 5px 8px; min-width: 50px; font-size: 0.8em; } .date-inputs { display: flex; align-items: center; gap: 5px; margin-top: 2px; flex-wrap: nowrap; } .date-input { width: 130px; flex-shrink: 0; } .date-label { font-size: 0.9em; color: #6c757d; white-space: nowrap; flex-shrink: 0; } .interval-buttons { display: flex; flex-wrap: wrap; gap: 2px; justify-content: flex-start; } .interval-btn { padding: 4px 6px; min-width: 55px; font-size: 0.75em; } @media (max-width: 768px) { .temp-buttons { justify-content: center; } .date-inputs { flex-wrap: wrap; gap: 2px; } .date-input { width: 110px; } .interval-btn { min-width: 35px; } .last-temp-item { justify-content: flex-start; } .chart-container { height: 400px; } } </style> </head> <body> <?php include 'menu.php'; ?> <div class="container-fluid mt-2"> <!-- <h1 class="text-center mb-2">Temperatur Graf</h1> --> <!-- Din originale kontrol-række --> <div class="row mb-0"> <div class="col-md-4"> <label class="form-label">Tidsinterval</label> <div class="interval-buttons"> <?php foreach ($intervals as $key => $config): ?> <a href="?interval=<?= $key ?>&min_temp=<?= $min_temp ?>&max_temp=<?= $max_temp ?>&start_date=<?= $start_date ?>&end_date=<?= $end_date ?>" class="btn btn-outline-primary interval-btn <?= $interval === $key ? 'active' : '' ?>"> <?= $config['label'] ?> </a> <?php endforeach; ?> </div> <div class="date-inputs"> <input type="date" class="form-control date-input" id="start_date" value="<?= htmlspecialchars($start_date) ?>" onchange="updateDates()"> <span class="date-label">til</span> <input type="date" class="form-control date-input" id="end_date" value="<?= htmlspecialchars($end_date) ?>" onchange="updateDates()"> <button type="button" class="btn btn-secondary btn-sm" onclick="clearDates()">Ryd</button> </div> </div> <div class="col-md-4"> <label class="form-label">Min Temp (°C)</label> <div class="temp-buttons"> <?php foreach ($min_options as $opt): ?> <a href="?interval=<?= $interval ?>&min_temp=<?= $opt ?>&max_temp=<?= $max_temp ?>&start_date=<?= $start_date ?>&end_date=<?= $end_date ?>" class="btn btn-outline-primary temp-btn <?= $min_temp == $opt ? 'active' : '' ?>"> <?= $opt ?> </a> <?php endforeach; ?> </div> </div> <div class="col-md-4"> <label class="form-label">Max Temp (°C)</label> <div class="temp-buttons"> <?php foreach ($max_options as $opt): ?> <a href="?interval=<?= $interval ?>&min_temp=<?= $min_temp ?>&max_temp=<?= $opt ?>&start_date=<?= $start_date ?>&end_date=<?= $end_date ?>" class="btn btn-outline-primary temp-btn <?= $max_temp == $opt ? 'active' : '' ?>"> <?= $opt ?> </a> <?php endforeach; ?> </div> </div> </div> <div class="row"> <div class="col-md-9" style="border: 0px solid black; width: 70%;"> <div class="card"> <div class="card-header">Temperatur Graf</div> <div class="card-body"> <div class="chart-container"> <?php if (empty($sensor_data)): ?> <div class="no-data">Ingen data i det valgte interval.</div> <?php else: ?> <canvas id="tempChart"></canvas> <?php endif; ?> </div> </div> </div> </div> <div class="col-md-3" style="border: 0px solid black; width: 30%;"> <div class="card"> <div class="card-header">Sidste Temperaturer (24t)</div> <div class="card-body"> <div id="lastTempsList"> <?php foreach ($filtered_last_temps as $key => $data): // En sensor er KUN offline, hvis den IKKE er på grafen OG er over 24 timer gammel $is_on_graph = isset($sensor_data[$key]); $is_too_old = (strtotime($data['tid']) < strtotime('-24 hours')); $is_offline = (!$is_on_graph && $is_too_old); $text_style = $is_offline ? 'color: #adb5bd; font-style: italic; border: 0px solid black; font-size: 10px;' : ''; ?> <div class="last-temp-item" style="<?= $text_style ?>"> <input type="checkbox" id="toggle_<?= htmlspecialchars($key, ENT_QUOTES) ?>" class="last-temp-checkbox" checked onchange="toggleDataset('<?= htmlspecialchars($key, ENT_QUOTES) ?>')"> <span class="last-temp-name"> <?= htmlspecialchars($key) ?> <?php if($is_offline): ?> <small>(Offline)</small> <?php endif; ?> </span> <span class="last-temp-temp"><?= number_format($data['temp'], 1) ?>°C</span> <input type="color" id="color_<?= htmlspecialchars($key, ENT_QUOTES) ?>" class="last-temp-color" onchange="changeColor('<?= htmlspecialchars($key, ENT_QUOTES) ?>', this.value)"> </div> <?php endforeach; ?> </div> </div> </div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script> const defaultColors = <?= json_encode($default_colors) ?>; let chart; let colorMap = JSON.parse(localStorage.getItem('sensorColors')) || {}; const isMultiDay = <?= $is_multi_day ? 'true' : 'false' ?>; // Initialize colors from localStorage or defaults + sæt farver i picker <?php $js_i = 0; foreach ($sensor_data as $key => $sensor): ?> // Ret her også if (!colorMap['<?= addslashes($key) ?>']) { colorMap['<?= addslashes($key) ?>'] = defaultColors[<?= $js_i ?> % defaultColors.length]; } const colorInput<?= $js_i ?> = document.getElementById('color_<?= addslashes($key) ?>'); if (colorInput<?= $js_i ?>) colorInput<?= $js_i ?>.value = colorMap['<?= addslashes($key) ?>']; <?php $js_i++; endforeach; ?> <?php if (!empty($sensor_data)): ?> const ctx = document.getElementById('tempChart').getContext('2d'); // Datasets – kun de 4 seneste sensorer (sortér efter nyeste måling) const datasets = [ <?php $i = 0; foreach ($sensor_data as $key => $data): ?> { label: '<?= addslashes($key) ?>', data: <?= json_encode($data['data']) ?>, borderColor: colorMap['<?= addslashes($key) ?>'] || defaultColors[<?= $i ?> % defaultColors.length], backgroundColor: colorMap['<?= addslashes($key) ?>'] || defaultColors[<?= $i ?> % defaultColors.length], tension: 0.1, fill: false, pointRadius: <?= $sample_minutes === 15 ? '0' : '2' ?>, borderWidth: 2, hidden: false }<?= $i < count($sensor_data) - 1 ? ',' : '' ?> <?php $i++; endforeach; ?> ]; chart = new Chart(ctx, { type: 'line', data: { datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { type: 'time', time: { unit: 'minute', displayFormats: { minute: 'HH:mm' } }, min: <?= $start_time_ms ?>, max: <?= $current_time_ms ?>, ticks: { callback: function(value) { const date = new Date(value); const timeStr = date.toLocaleTimeString('da-DK', { hour: '2-digit', minute: '2-digit' }); if (isMultiDay) { const dateStr = date.toLocaleDateString('da-DK', { day: '2-digit', month: 'short' }); return timeStr + '\n' + dateStr; } return timeStr; } } }, y: { title: { display: true, text: 'Temperatur (°C)' }, min: <?= $min_temp ?>, max: <?= $max_temp ?> } }, plugins: { tooltip: { callbacks: { label: function(context) { return context.dataset.label + ': ' + context.parsed.y.toFixed(1) + '°C'; } } }, legend: { display: false } }, interaction: { mode: 'nearest', axis: 'x', intersect: false } } }); <?php endif; ?> function toggleDataset(key) { if (!chart) return; const index = chart.data.datasets.findIndex(ds => ds.label === key); if (index > -1) { const meta = chart.getDatasetMeta(index); meta.hidden = !meta.hidden; chart.update(); } } function changeColor(key, color) { if (!chart) return; colorMap[key] = color; localStorage.setItem('sensorColors', JSON.stringify(colorMap)); const index = chart.data.datasets.findIndex(ds => ds.label === key); if (index > -1) { chart.data.datasets[index].borderColor = color; chart.data.datasets[index].backgroundColor = color; chart.update(); } } function updateDates() { const start = document.getElementById('start_date').value; const end = document.getElementById('end_date').value; localStorage.setItem('graphStartDate', start); localStorage.setItem('graphEndDate', end); const url = new URL(window.location); url.searchParams.set('start_date', start); url.searchParams.set('end_date', end); window.location = url; } function clearDates() { localStorage.removeItem('graphStartDate'); localStorage.removeItem('graphEndDate'); const url = new URL(window.location); url.searchParams.delete('start_date'); url.searchParams.delete('end_date'); window.location = url; } </script> </body> </html>
Viser de seneste 1000 linjer. Genindlæs