= ($cache['retryAt'] ?? 0)) { try { $data = dashboard_http_json('https://v6.bvg.transport.rest/stops/' . DASHBOARD_BVG_STOP . '/departures?' . 'duration=120&results=30&bus=true&suburban=false&subway=false&tram=false' . '&ferry=false&express=false®ional=false&remarks=false&language=de&pretty=false'); if (!isset($data['departures']) || !is_array($data['departures'])) { throw new RuntimeException('Unerwartete BVG-Antwort.'); } $now = microtime(true); $cache = ['departures' => array_values(array_filter($data['departures'], static fn(array $d): bool => ($d['line']['product'] ?? '') === 'bus' && in_array($d['stop']['id'] ?? '', [DASHBOARD_BVG_STOP, '900000' . substr(DASHBOARD_BVG_STOP, 3)], true))), 'updatedAt' => (int) round($now * 1000), 'retryAt' => min($now + 12, (floor($now / 60) + 1) * 60), 'unavailable' => false]; } catch (Throwable $error) { // Letzte Antwort behalten; auch bei 429/503 nicht sofort erneut anfragen. error_log('Dashboard BVG: ' . $error->getMessage()); $cache['unavailable'] = true; $cache['retryAt'] = microtime(true) + 30; } rewind($file); $json = json_encode($cache, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); if (!ftruncate($file, 0) || fwrite($file, $json) !== strlen($json) || !fflush($file)) { throw new RuntimeException('BVG-Cache konnte nicht gespeichert werden.'); } } return $cache + ['departures' => [], 'updatedAt' => 0]; } finally { fclose($file); } }