247 lines
8.6 KiB
PHP
247 lines
8.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../../contentpanel/insertion/util/news.php';
|
|
require_once __DIR__ . '/../../contentpanel/insertion/util/announcements.php';
|
|
require_once __DIR__ . '/../../../general_files/ini/runtime_env.php';
|
|
|
|
const DASHBOARD_TIMEZONE = 'Europe/Berlin';
|
|
const DASHBOARD_HA_ENTITY_ID = 'sensor.temp_temperatur';
|
|
const DASHBOARD_OPENWEATHER_LATITUDE = '52.571524';
|
|
const DASHBOARD_OPENWEATHER_LONGITUDE = '13.312494';
|
|
const DASHBOARD_NEWS_URL = '/intranet_repo/projects/contentpanel/insertion/news/png/';
|
|
|
|
function dashboard_now(): DateTimeImmutable
|
|
{
|
|
return new DateTimeImmutable('now', new DateTimeZone(DASHBOARD_TIMEZONE));
|
|
}
|
|
|
|
function dashboard_fetch_content(PDO $pdo, DateTimeImmutable $now): array
|
|
{
|
|
$now = $now->setTimezone(new DateTimeZone(DASHBOARD_TIMEZONE));
|
|
$today = $now->format('Y-m-d');
|
|
$payload = ['date' => $today, 'service' => null, 'announcements' => [], 'errors' => []];
|
|
|
|
// Unabhängige Abfragen: ein fehlerhafter Bereich blockiert den anderen nicht.
|
|
try {
|
|
$statement = $pdo->prepare(
|
|
'SELECT bibelwort, bibelvers FROM gottesdienst
|
|
WHERE bibeldatum = ? ORDER BY id ASC LIMIT 1'
|
|
);
|
|
$statement->execute([$today]);
|
|
$service = $statement->fetch(PDO::FETCH_ASSOC);
|
|
if ($service) {
|
|
// Leitgedanken-Import: bibelwort = Referenz, bibelvers = eigentlicher Text.
|
|
$payload['service'] = [
|
|
'reference' => trim((string) ($service['bibelwort'] ?? '')),
|
|
'text' => trim((string) ($service['bibelvers'] ?? '')),
|
|
];
|
|
}
|
|
} catch (Throwable $exception) {
|
|
error_log('Dashboard Bibelwort: ' . $exception->getMessage());
|
|
$payload['errors'][] = 'service';
|
|
}
|
|
|
|
try {
|
|
$statement = $pdo->prepare(
|
|
'SELECT ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort,
|
|
ansage_titel, ansage_untertitel
|
|
FROM ansagen WHERE COALESCE(ansage_datum_bis, ansage_datum_von) >= ?
|
|
ORDER BY ansage_datum_von ASC NULLS LAST, ansage_uhrzeit ASC NULLS LAST, id ASC'
|
|
);
|
|
contentpanel_cleanup_expired_announcements($pdo, $today);
|
|
$statement->execute([$today]);
|
|
foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$payload['announcements'][] = dashboard_prepare_announcement($row, $now);
|
|
}
|
|
} catch (Throwable $exception) {
|
|
error_log('Dashboard Ansagen: ' . $exception->getMessage());
|
|
$payload['errors'][] = 'announcements';
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
function dashboard_date(string $raw): ?DateTimeImmutable
|
|
{
|
|
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $raw, new DateTimeZone(DASHBOARD_TIMEZONE));
|
|
return $date && $date->format('Y-m-d') === $raw ? $date : null;
|
|
}
|
|
|
|
function dashboard_prepare_announcement(array $announcement, DateTimeImmutable $now): array
|
|
{
|
|
$now = $now->setTimezone(new DateTimeZone(DASHBOARD_TIMEZONE));
|
|
$start = substr((string) ($announcement['ansage_datum_von'] ?? ''), 0, 10);
|
|
$end = substr((string) ($announcement['ansage_datum_bis'] ?? ''), 0, 10);
|
|
$today = $now->format('Y-m-d');
|
|
$startDate = dashboard_date($start);
|
|
$endDate = dashboard_date($end);
|
|
if ($start === $today) {
|
|
$label = 'Heute';
|
|
} elseif ($start === $now->modify('+1 day')->format('Y-m-d')) {
|
|
$label = 'Morgen';
|
|
} else {
|
|
$days = [1 => 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
|
$label = $startDate ? $days[(int) $startDate->format('N')] . ' ' . $startDate->format('d.m.') : '';
|
|
}
|
|
|
|
$endLabel = $endDate && $end !== $start ? 'bis ' . $endDate->format('d.m.') : '';
|
|
$time = substr((string) ($announcement['ansage_uhrzeit'] ?? ''), 0, 5);
|
|
$place = trim((string) ($announcement['ansage_ort'] ?? ''));
|
|
|
|
return [
|
|
'date_label' => $label,
|
|
'end_label' => $endLabel,
|
|
'time' => $time,
|
|
'place' => $place,
|
|
'title' => trim((string) ($announcement['ansage_titel'] ?? '')),
|
|
'subtitle' => trim((string) ($announcement['ansage_untertitel'] ?? '')),
|
|
];
|
|
}
|
|
|
|
function dashboard_image(string $path, string $url): ?array
|
|
{
|
|
if (!is_file($path)) {
|
|
return null;
|
|
}
|
|
$size = @getimagesize($path);
|
|
if (!$size || $size[0] < 1 || $size[1] < 1) {
|
|
return null;
|
|
}
|
|
// Neu erzeugte, aber identische PNGs dürfen keinen sichtbaren Austausch auslösen.
|
|
$version = @hash_file('sha256', $path);
|
|
if ($version === false) {
|
|
return null;
|
|
}
|
|
return [
|
|
'url' => $url . '?v=' . $version,
|
|
'width' => $size[0],
|
|
'height' => $size[1],
|
|
];
|
|
}
|
|
|
|
function dashboard_list_posters(DateTimeImmutable $now, string $directory = CONTENTPANEL_NEWS_PNG_DIRECTORY): array
|
|
{
|
|
$today = $now->setTimezone(new DateTimeZone(DASHBOARD_TIMEZONE))->format('Y-m-d');
|
|
$files = glob($directory . '/*.png') ?: [];
|
|
natsort($files);
|
|
$posters = [];
|
|
foreach ($files as $file) {
|
|
$name = basename($file);
|
|
if (!preg_match('/^(.+)_page_([1-9][0-9]*)\\.png$/D', $name, $match)
|
|
|| !contentpanel_is_valid_news_file_base($match[1])) {
|
|
continue;
|
|
}
|
|
// Dieselbe inklusive Ablauffrist wie im Contentpanel. Nur filtern, nicht löschen.
|
|
$expiry = substr($match[1], 0, 10);
|
|
if (!dashboard_date($expiry) || $expiry < $today) {
|
|
continue;
|
|
}
|
|
$image = dashboard_image($file, DASHBOARD_NEWS_URL . rawurlencode($name));
|
|
if ($image) {
|
|
$title = str_replace('_', ' ', substr($match[1], 11));
|
|
$posters[] = $image + [
|
|
'document' => $match[1],
|
|
'expires' => $expiry, 'page' => (int) $match[2],
|
|
'alt' => $title . ' · Seite ' . (int) $match[2],
|
|
];
|
|
}
|
|
}
|
|
return $posters;
|
|
}
|
|
|
|
function dashboard_month_images(?string $directory = null): array
|
|
{
|
|
$directory ??= __DIR__ . '/../month';
|
|
$images = [];
|
|
foreach ([1, 2] as $page) {
|
|
$filename = 'terminuebersicht-' . $page . '.png';
|
|
$image = dashboard_image($directory . '/' . $filename, 'month/' . $filename);
|
|
if ($image) {
|
|
$images[] = $image + ['alt' => 'Monatsplan · Seite ' . $page];
|
|
}
|
|
}
|
|
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 (!isset($data['state']) || !is_numeric($data['state'])) {
|
|
throw new RuntimeException('Unerwartete Home-Assistant-Antwort.');
|
|
}
|
|
|
|
return [
|
|
'value' => (int) ceil((float) $data['state'] - 0.4),
|
|
'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;
|
|
|
|
if (!is_numeric($temperature)) {
|
|
throw new RuntimeException('Unerwartete OpenWeather-Antwort.');
|
|
}
|
|
|
|
return [
|
|
'temperature' => (int) round((float) $temperature),
|
|
];
|
|
}
|
|
|
|
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;
|
|
}
|