format('Y-m-d'); $serviceStatement = $pdo->prepare( 'SELECT bibelwort, bibelvers, eingang_nummer, eingang_titel FROM gottesdienst WHERE bibeldatum = ?' ); $serviceStatement->execute([$today]); $service = $serviceStatement->fetch(PDO::FETCH_ASSOC) ?: []; $announcementStatement = $pdo->prepare( 'SELECT ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel FROM ansagen WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ? ORDER BY ansage_datum_von ASC' ); $announcementStatement->execute([$today, $today]); $announcements = $announcementStatement->fetchAll(PDO::FETCH_ASSOC); foreach ($announcements as &$announcement) { $announcement = dashboard_prepare_announcement($announcement, $now); } unset($announcement); contentpanel_cleanup_expired_news(); return [ 'date_label' => dashboard_date_label($now), 'service' => [ 'bibelwort' => (string) ($service['bibelwort'] ?? 'Bibelwort nicht eingetragen'), 'bibelvers' => (string) ($service['bibelvers'] ?? 'Bibelvers nicht gefunden.'), 'eingang_nummer' => (string) ($service['eingang_nummer'] ?? 'N/A'), 'eingang_titel' => (string) ($service['eingang_titel'] ?? 'Eingangslied nicht gefunden'), ], 'announcements' => $announcements, 'posters' => dashboard_list_posters(), 'month_images' => dashboard_month_images(), ]; } function dashboard_date_label(DateTimeImmutable $date): string { static $months = [ 1 => 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember', ]; return sprintf('%02d. %s', (int) $date->format('d'), $months[(int) $date->format('n')]); } function dashboard_prepare_announcement(array $announcement, DateTimeImmutable $now): array { $startRaw = substr((string) ($announcement['ansage_datum_von'] ?? ''), 0, 10); $endRaw = substr((string) ($announcement['ansage_datum_bis'] ?? ''), 0, 10); $timeRaw = substr((string) ($announcement['ansage_uhrzeit'] ?? ''), 0, 5); $startDate = DateTimeImmutable::createFromFormat('!Y-m-d', $startRaw, $now->getTimezone()); $tomorrow = $now->setTime(0, 0)->modify('+1 day'); if ($startDate && $startDate->format('Y-m-d') === $now->format('Y-m-d')) { $dateLabel = 'Heute'; } elseif ($startDate && $startDate->format('Y-m-d') === $tomorrow->format('Y-m-d')) { $dateLabel = 'Morgen'; } elseif ($startDate) { static $weekdays = [1 => 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; $dateLabel = $weekdays[(int) $startDate->format('N')] . ' ' . $startDate->format('d.m.'); } else { $dateLabel = $startRaw; } $endLabel = ''; if ($endRaw !== '') { $endDate = DateTimeImmutable::createFromFormat('!Y-m-d', $endRaw, $now->getTimezone()); $endLabel = $endDate ? 'bis ' . $endDate->format('d.m.') : 'bis ' . $endRaw; } $meta = []; if ($timeRaw !== '') { $meta[] = $timeRaw . ' Uhr'; } $place = trim((string) ($announcement['ansage_ort'] ?? '')); if ($place !== '') { $meta[] = $place; } return [ 'date_label' => $dateLabel, 'end_label' => $endLabel, 'title' => (string) ($announcement['ansage_titel'] ?? ''), 'subtitle' => (string) ($announcement['ansage_untertitel'] ?? ''), 'meta' => implode(', ', $meta), ]; } function dashboard_list_posters(): array { $files = glob(CONTENTPANEL_NEWS_PNG_DIRECTORY . '/*.png') ?: []; natsort($files); $posters = []; foreach ($files as $file) { if (!is_file($file)) { continue; } $basename = basename($file); $posters[] = [ 'url' => DASHBOARD_NEWS_URL . rawurlencode($basename) . '?v=' . (int) filemtime($file), 'alt' => 'Aushang', ]; } return array_values($posters); } function dashboard_month_images(): array { $images = []; foreach ([1, 2] as $page) { $filename = 'terminuebersicht-' . $page . '.png'; $path = __DIR__ . '/../month/' . $filename; $version = is_file($path) ? (int) filemtime($path) : 0; $images[] = [ 'page' => $page, 'url' => 'month/' . $filename . '?v=' . $version, ]; } return $images; } function dashboard_fetch_indoor_temperature(): array { $baseUrl = rtrim(intranet_env('HA_BASE_URL'), '/'); $token = intranet_env('HA_TOKEN'); $url = $baseUrl . '/api/states/' . rawurlencode(DASHBOARD_HA_ENTITY_ID); $data = dashboard_http_json($url, ['Authorization: Bearer ' . $token]); if (!array_key_exists('state', $data)) { throw new RuntimeException('Unerwartete Home-Assistant-Antwort.'); } $value = $data['state']; if (is_numeric($value)) { $value = round((float) $value, 1); } return [ 'value' => $value, 'unit' => (string) ($data['attributes']['unit_of_measurement'] ?? '°C'), ]; } function dashboard_fetch_outdoor_weather(): array { $query = http_build_query([ 'lat' => DASHBOARD_OPENWEATHER_LATITUDE, 'lon' => DASHBOARD_OPENWEATHER_LONGITUDE, 'appid' => intranet_env('OPENWEATHER_API_KEY'), 'units' => 'metric', 'lang' => 'de', ], '', '&', PHP_QUERY_RFC3986); $data = dashboard_http_json('https://api.openweathermap.org/data/2.5/weather?' . $query); $temperature = $data['main']['temp'] ?? null; $icon = (string) ($data['weather'][0]['icon'] ?? ''); if (!is_numeric($temperature)) { throw new RuntimeException('Unerwartete OpenWeather-Antwort.'); } return [ 'temperature' => (int) round((float) $temperature), 'icon' => dashboard_weather_kind($icon), ]; } function dashboard_weather_kind(string $icon): string { return match ($icon) { '04d', '04n', '03d', '03n' => 'cloudy', '02d' => 'cloudy_day', '02n' => 'cloudy_night', '01d' => 'sun', '01n' => 'moon', '09d', '09n', '10n' => 'rain_shower', '10d' => 'rain', '11d', '11n' => 'thunder', '13d', '13n' => 'snowy', '50d', '50n' => 'mist', default => 'cloudy', }; } function dashboard_http_json(string $url, array $headers = []): array { $handle = curl_init($url); if ($handle === false) { throw new RuntimeException('HTTP-Anfrage konnte nicht initialisiert werden.'); } curl_setopt_array($handle, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => array_merge(['Accept: application/json'], $headers), CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_TIMEOUT => 5, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, ]); try { $response = curl_exec($handle); $status = (int) curl_getinfo($handle, CURLINFO_HTTP_CODE); $error = curl_error($handle); } finally { curl_close($handle); } if ($response === false || $status < 200 || $status >= 300) { throw new RuntimeException($error !== '' ? $error : 'HTTP ' . $status); } try { $data = json_decode($response, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $exception) { throw new RuntimeException('Ungültige JSON-Antwort.', 0, $exception); } if (!is_array($data)) { throw new RuntimeException('Unerwartete JSON-Antwort.'); } return $data; }