Log Filer
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