Add weather widget with OpenWeatherMap integration

Implementiert vollständiges Wetter-Widget oberhalb des Webcam-Videos:

**Features:**
🌤️ **Wetter-Anzeige:**
- Temperatur (°C)
- Windgeschwindigkeit & Richtung (km/h)
- Luftdruck (hPa)
- Luftfeuchtigkeit (%)
- Wetterbeschreibung mit Emoji-Icons
- Niederschlag (Regen/Schnee) wenn vorhanden

⚙️ **Technisch:**
- OpenWeatherMap API Integration
- 5 Minuten Cache (konfigurierbar)
- Auto-Update alle X Minuten (Frontend)
- WeatherManager Klasse für Backend
- Schönes Gradient-Design mit Hover-Effekten
- Responsive für Mobile

🎛️ **Admin-Settings:**
- Wetter-Widget ein/aus
- API Key Eingabefeld + Registrierungs-Link
- Standort konfigurierbar (Stadt,Land)
- GPS-Koordinaten (Lat/Lon)
- Update-Intervall (5-60 Minuten)
- Einheiten (Metrisch/Imperial)

**Dateien:**
- WeatherManager.php: Neue Klasse für API-Calls & Caching
- SettingsManager.php: Weather Settings Defaults & Helper
- index.php: Widget HTML, CSS, JavaScript Auto-Update
- settings.json: Weather Defaults initialisiert

**Koordinaten Oberdürnten:**
Lat: 47.2833, Lon: 8.7167

**Setup für User:**
1. Gratis Account auf openweathermap.org erstellen
2. API Key im Admin-Panel einfügen
3. Fertig! Widget zeigt Live-Wetter an
This commit is contained in:
Claude
2026-01-22 18:50:16 +00:00
parent 5b8200a4ff
commit 7eda2fbbe8
4 changed files with 544 additions and 2 deletions
+38
View File
@@ -73,6 +73,16 @@ class SettingsManager {
'meta_description' => '',
'meta_keywords' => ''
],
// Weather Widget
'weather' => [
'enabled' => true,
'api_key' => '',
'location' => 'Oberdürnten,CH',
'lat' => '47.2833',
'lon' => '8.7167',
'update_interval' => 5, // Minuten
'units' => 'metric' // metric (Celsius) oder imperial (Fahrenheit)
],
'last_updated' => null,
'updated_by' => null
];
@@ -239,4 +249,32 @@ class SettingsManager {
public function getMetaKeywords() {
return $this->get('seo.meta_keywords') ?? '';
}
// Weather Helper
public function isWeatherEnabled() {
return $this->get('weather.enabled') === true;
}
public function getWeatherApiKey() {
return $this->get('weather.api_key') ?? '';
}
public function getWeatherLocation() {
return $this->get('weather.location') ?? 'Oberdürnten,CH';
}
public function getWeatherCoords() {
return [
'lat' => $this->get('weather.lat') ?? '47.2833',
'lon' => $this->get('weather.lon') ?? '8.7167'
];
}
public function getWeatherUpdateInterval() {
return $this->get('weather.update_interval') ?? 5;
}
public function getWeatherUnits() {
return $this->get('weather.units') ?? 'metric';
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
/**
* WeatherManager - Holt und cached Wetterdaten von OpenWeatherMap
*/
class WeatherManager {
private $settingsManager;
private $cacheFile = 'weather_cache.json';
private $cacheTime = 300; // 5 Minuten in Sekunden
public function __construct($settingsManager) {
$this->settingsManager = $settingsManager;
}
/**
* Holt aktuelle Wetterdaten (cached)
*/
public function getCurrentWeather() {
// Prüfe ob Weather aktiviert ist
if (!$this->settingsManager->isWeatherEnabled()) {
return null;
}
// Prüfe API Key
$apiKey = $this->settingsManager->getWeatherApiKey();
if (empty($apiKey)) {
return ['error' => 'API Key fehlt'];
}
// Prüfe Cache
$cached = $this->getCache();
if ($cached !== null) {
return $cached;
}
// Hole frische Daten von API
$coords = $this->settingsManager->getWeatherCoords();
$units = $this->settingsManager->getWeatherUnits();
$url = "https://api.openweathermap.org/data/2.5/weather?lat={$coords['lat']}&lon={$coords['lon']}&units={$units}&appid={$apiKey}&lang=de";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$response) {
return ['error' => 'API Fehler'];
}
$data = json_decode($response, true);
if (!$data || !isset($data['main'])) {
return ['error' => 'Ungültige API Antwort'];
}
// Formatiere Daten
$weather = [
'temp' => round($data['main']['temp'], 1),
'feels_like' => round($data['main']['feels_like'], 1),
'humidity' => $data['main']['humidity'],
'pressure' => $data['main']['pressure'],
'wind_speed' => round($data['wind']['speed'] * 3.6, 1), // m/s -> km/h
'wind_deg' => $data['wind']['deg'] ?? 0,
'wind_direction' => $this->getWindDirection($data['wind']['deg'] ?? 0),
'clouds' => $data['clouds']['all'] ?? 0,
'description' => ucfirst($data['weather'][0]['description'] ?? 'Unbekannt'),
'icon' => $data['weather'][0]['icon'] ?? '01d',
'rain_1h' => $data['rain']['1h'] ?? 0,
'snow_1h' => $data['snow']['1h'] ?? 0,
'location' => $data['name'] ?? $this->settingsManager->getWeatherLocation(),
'timestamp' => time()
];
// Cache speichern
$this->saveCache($weather);
return $weather;
}
/**
* Wandelt Windrichtung (Grad) in Kompassrichtung um
*/
private function getWindDirection($deg) {
$directions = ['N', 'NNO', 'NO', 'ONO', 'O', 'OSO', 'SO', 'SSO', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
$index = round($deg / 22.5) % 16;
return $directions[$index];
}
/**
* Holt Daten aus Cache (wenn noch gültig)
*/
private function getCache() {
if (!file_exists($this->cacheFile)) {
return null;
}
$content = file_get_contents($this->cacheFile);
$data = json_decode($content, true);
if (!$data || !isset($data['timestamp'])) {
return null;
}
// Update-Intervall aus Settings holen (in Minuten)
$updateInterval = $this->settingsManager->getWeatherUpdateInterval() * 60; // Minuten -> Sekunden
// Prüfe ob Cache noch gültig
if (time() - $data['timestamp'] < $updateInterval) {
return $data;
}
return null;
}
/**
* Speichert Daten im Cache
*/
private function saveCache($data) {
$json = json_encode($data, JSON_PRETTY_PRINT);
file_put_contents($this->cacheFile, $json, LOCK_EX);
}
/**
* Gibt Wetter-Icon-Emoji zurück
*/
public function getWeatherEmoji($iconCode) {
$map = [
'01d' => '☀️', '01n' => '🌙',
'02d' => '⛅', '02n' => '☁️',
'03d' => '☁️', '03n' => '☁️',
'04d' => '☁️', '04n' => '☁️',
'09d' => '🌧️', '09n' => '🌧️',
'10d' => '🌦️', '10n' => '🌧️',
'11d' => '⛈️', '11n' => '⛈️',
'13d' => '❄️', '13n' => '❄️',
'50d' => '🌫️', '50n' => '🌫️'
];
return $map[$iconCode] ?? '🌤️';
}
/**
* AJAX Handler für Wetter-Updates
*/
public function handleAjax() {
if ($_SERVER['REQUEST_METHOD'] !== 'GET') return;
if (!isset($_GET['weather_action'])) return;
header('Content-Type: application/json');
if ($_GET['weather_action'] === 'get') {
$weather = $this->getCurrentWeather();
echo json_encode(['success' => true, 'data' => $weather]);
exit;
}
}
}
+337 -2
View File
@@ -4,12 +4,17 @@ use PHPMailer\PHPMailer\Exception;
require __DIR__ . '/vendor/autoload.php';
require_once 'SettingsManager.php';
require_once 'WeatherManager.php';
// SettingsManager initialisieren
$settingsManager = new SettingsManager();
// AJAX-Handler für Settings (VOR anderen Ausgaben!)
// WeatherManager initialisieren
$weatherManager = new WeatherManager($settingsManager);
// AJAX-Handler für Settings und Weather (VOR anderen Ausgaben!)
$settingsManager->handleAjax();
$weatherManager->handleAjax();
if (isset($_GET['download_video'])) {
$videoDir = './videos/';
@@ -1264,6 +1269,67 @@ class AdminManager {
$output .= '</div>';
$output .= '</div>'; // settings-group
// Weather Settings
$output .= '<div class="settings-group">';
$output .= '<h4>🌤️ Wetter-Widget</h4>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Wetter-Widget anzeigen</span>';
$output .= '<div class="setting-input">';
$output .= '<label class="toggle-switch">';
$output .= '<input type="checkbox" id="setting-weather-enabled" ' . ($settingsManager->get('weather.enabled') ? 'checked' : '') . '>';
$output .= '<span class="toggle-slider"></span>';
$output .= '</label>';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">OpenWeatherMap API Key <a href="https://openweathermap.org/api" target="_blank" style="font-size:11px;">(kostenlos registrieren)</a></span>';
$output .= '<div class="setting-input">';
$output .= '<input type="text" id="setting-weather-api-key" class="text-input" placeholder="Dein API Key hier einfügen..." value="' . htmlspecialchars($settingsManager->get('weather.api_key')) . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Standort (Stadt,Land)</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="text" id="setting-weather-location" class="text-input" placeholder="Oberdürnten,CH" value="' . htmlspecialchars($settingsManager->get('weather.location')) . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Latitude (Breitengrad)</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="text" id="setting-weather-lat" class="text-input" placeholder="47.2833" value="' . htmlspecialchars($settingsManager->get('weather.lat')) . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Longitude (Längengrad)</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="text" id="setting-weather-lon" class="text-input" placeholder="8.7167" value="' . htmlspecialchars($settingsManager->get('weather.lon')) . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Update-Intervall (Minuten)</span>';
$output .= '<div class="setting-input">';
$output .= '<input type="number" id="setting-weather-interval" class="number-input" min="5" max="60" value="' . $settingsManager->get('weather.update_interval') . '">';
$output .= '</div>';
$output .= '</div>';
$output .= '<div class="setting-row">';
$output .= '<span class="setting-label">Einheit</span>';
$output .= '<div class="setting-input">';
$output .= '<select id="setting-weather-units" class="select-input">';
$currentUnits = $settingsManager->get('weather.units');
$output .= '<option value="metric" ' . ($currentUnits === 'metric' ? 'selected' : '') . '>Metrisch (°C, km/h)</option>';
$output .= '<option value="imperial" ' . ($currentUnits === 'imperial' ? 'selected' : '') . '>Imperial (°F, mph)</option>';
$output .= '</select>';
$output .= '</div>';
$output .= '</div>';
$output .= '</div>'; // settings-group
$output .= '</div>'; // admin-settings-panel
// Bestehender Admin-Content
@@ -2065,6 +2131,90 @@ button[type="submit"]:hover { background-color: #45a049; }
.delete-btn { background-color: #ff4136; color: white; border: none; padding: 5px 10px; cursor: pointer; font-size: 0.8em; margin-left: 10px; border-radius: 3px; }
/* Weather Widget */
.weather-widget {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
animation: weatherFadeIn 0.5s ease;
}
.weather-widget.weather-error {
background: linear-gradient(135deg, #f44336 0%, #e91e63 100%);
color: white;
font-weight: bold;
justify-content: center;
}
.weather-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
padding: 10px 15px;
background: rgba(255, 255, 255, 0.15);
border-radius: 8px;
backdrop-filter: blur(10px);
min-width: 120px;
transition: transform 0.3s ease, background 0.3s ease;
}
.weather-item:hover {
transform: translateY(-3px);
background: rgba(255, 255, 255, 0.25);
}
.weather-icon {
font-size: 32px;
line-height: 1;
}
.weather-value {
font-size: 18px;
font-weight: bold;
color: white;
white-space: nowrap;
}
.weather-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.weather-description {
flex: 1 1 auto;
min-width: 180px;
}
.weather-description .weather-value {
font-size: 16px;
text-align: center;
}
@keyframes weatherFadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.weather-widget {
gap: 10px;
padding: 15px 10px;
}
.weather-item {
min-width: 90px;
padding: 8px 10px;
}
.weather-icon {
font-size: 24px;
}
.weather-value {
font-size: 14px;
}
.weather-label {
font-size: 10px;
}
}
/* Guestbook */
.guestbook-entry { background-color: #f9f9f9; border-left: 5px solid #4CAF50; margin-bottom: 20px; padding: 15px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
@@ -2498,6 +2648,53 @@ body.theme-neo footer {
<!-- WEBCAM SECTION -->
<section id="webcams" class="section">
<div class="container">
<!-- WEATHER WIDGET -->
<?php if ($settingsManager->isWeatherEnabled()): ?>
<?php
$weather = $weatherManager->getCurrentWeather();
if ($weather && !isset($weather['error'])):
?>
<div id="weather-widget" class="weather-widget">
<div class="weather-item weather-temp">
<span class="weather-icon">🌡️</span>
<span class="weather-value"><?php echo $weather['temp']; ?>°C</span>
<span class="weather-label">Temperatur</span>
</div>
<div class="weather-item weather-wind">
<span class="weather-icon">💨</span>
<span class="weather-value"><?php echo $weather['wind_speed']; ?> km/h <?php echo $weather['wind_direction']; ?></span>
<span class="weather-label">Wind</span>
</div>
<div class="weather-item weather-pressure">
<span class="weather-icon">🔽</span>
<span class="weather-value"><?php echo $weather['pressure']; ?> hPa</span>
<span class="weather-label">Luftdruck</span>
</div>
<div class="weather-item weather-humidity">
<span class="weather-icon">💧</span>
<span class="weather-value"><?php echo $weather['humidity']; ?>%</span>
<span class="weather-label">Luftfeuchtigkeit</span>
</div>
<div class="weather-item weather-description">
<span class="weather-icon"><?php echo $weatherManager->getWeatherEmoji($weather['icon']); ?></span>
<span class="weather-value"><?php echo $weather['description']; ?></span>
<span class="weather-label"><?php echo $weather['location']; ?></span>
</div>
<?php if ($weather['rain_1h'] > 0 || $weather['snow_1h'] > 0): ?>
<div class="weather-item weather-precipitation">
<span class="weather-icon"><?php echo $weather['rain_1h'] > 0 ? '🌧️' : '❄️'; ?></span>
<span class="weather-value"><?php echo $weather['rain_1h'] > 0 ? $weather['rain_1h'] : $weather['snow_1h']; ?> mm</span>
<span class="weather-label">Niederschlag</span>
</div>
<?php endif; ?>
</div>
<?php elseif ($weather && isset($weather['error'])): ?>
<div class="weather-widget weather-error">
<span>⚠️ Wetterdaten nicht verfügbar: <?php echo $weather['error']; ?></span>
</div>
<?php endif; ?>
<?php endif; ?>
<!-- VIDEO PLAYER -->
<div class="video-container" style="border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.15);">
<?php echo $webcamManager->displayWebcam(); ?>
@@ -3262,6 +3459,25 @@ const AdminSettings = {
case 'seo.meta_keywords':
this.showNotification('SEO Meta gespeichert (wirksam bei Reload)', 'success');
break;
// Weather Settings
case 'weather.enabled':
const weatherWidget = document.getElementById('weather-widget');
if (weatherWidget) {
weatherWidget.style.display = boolValue ? 'flex' : 'none';
}
this.showNotification('Wetter-Widget ' + (boolValue ? 'aktiviert' : 'deaktiviert'), 'success');
break;
case 'weather.api_key':
case 'weather.location':
case 'weather.lat':
case 'weather.lon':
case 'weather.units':
this.showNotification('Wetter-Einstellung gespeichert (Reload empfohlen)', 'success');
break;
case 'weather.update_interval':
this.showNotification('Update-Intervall: ' + value + ' Minuten (Reload empfohlen)', 'success');
break;
}
},
@@ -3359,6 +3575,35 @@ const AdminSettings = {
document.getElementById('setting-meta-keywords')?.addEventListener('change', (e) => {
this.updateSetting('seo.meta_keywords', e.target.value);
});
// Weather Settings
document.getElementById('setting-weather-enabled')?.addEventListener('change', (e) => {
this.updateSetting('weather.enabled', e.target.checked);
});
document.getElementById('setting-weather-api-key')?.addEventListener('change', (e) => {
this.updateSetting('weather.api_key', e.target.value);
});
document.getElementById('setting-weather-location')?.addEventListener('change', (e) => {
this.updateSetting('weather.location', e.target.value);
});
document.getElementById('setting-weather-lat')?.addEventListener('change', (e) => {
this.updateSetting('weather.lat', e.target.value);
});
document.getElementById('setting-weather-lon')?.addEventListener('change', (e) => {
this.updateSetting('weather.lon', e.target.value);
});
document.getElementById('setting-weather-interval')?.addEventListener('change', (e) => {
this.updateSetting('weather.update_interval', parseInt(e.target.value));
});
document.getElementById('setting-weather-units')?.addEventListener('change', (e) => {
this.updateSetting('weather.units', e.target.value);
});
},
showNotification: function(message, type) {
@@ -3597,7 +3842,97 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
</script>
<!-- WEATHER AUTO-UPDATE -->
<script>
const WeatherUpdater = {
updateInterval: <?php echo $settingsManager->getWeatherUpdateInterval() * 60 * 1000; ?>, // Minuten -> Millisekunden
init: function() {
if (!document.getElementById('weather-widget')) return;
// Update alle X Minuten
setInterval(() => this.updateWeather(), this.updateInterval);
},
updateWeather: function() {
fetch(window.location.href + '?weather_action=get')
.then(r => r.json())
.then(data => {
if (data.success && data.data && !data.data.error) {
this.renderWeather(data.data);
}
})
.catch(err => console.error('Weather update error:', err));
},
renderWeather: function(weather) {
const widget = document.getElementById('weather-widget');
if (!widget) return;
const rainSnow = weather.rain_1h > 0 || weather.snow_1h > 0;
const precipIcon = weather.rain_1h > 0 ? '🌧️' : '❄️';
const precipValue = weather.rain_1h > 0 ? weather.rain_1h : weather.snow_1h;
widget.innerHTML = `
<div class="weather-item weather-temp">
<span class="weather-icon">🌡️</span>
<span class="weather-value">${weather.temp}°C</span>
<span class="weather-label">Temperatur</span>
</div>
<div class="weather-item weather-wind">
<span class="weather-icon">💨</span>
<span class="weather-value">${weather.wind_speed} km/h ${weather.wind_direction}</span>
<span class="weather-label">Wind</span>
</div>
<div class="weather-item weather-pressure">
<span class="weather-icon">🔽</span>
<span class="weather-value">${weather.pressure} hPa</span>
<span class="weather-label">Luftdruck</span>
</div>
<div class="weather-item weather-humidity">
<span class="weather-icon">💧</span>
<span class="weather-value">${weather.humidity}%</span>
<span class="weather-label">Luftfeuchtigkeit</span>
</div>
<div class="weather-item weather-description">
<span class="weather-icon">${this.getWeatherEmoji(weather.icon)}</span>
<span class="weather-value">${weather.description}</span>
<span class="weather-label">${weather.location}</span>
</div>
${rainSnow ? `
<div class="weather-item weather-precipitation">
<span class="weather-icon">${precipIcon}</span>
<span class="weather-value">${precipValue} mm</span>
<span class="weather-label">Niederschlag</span>
</div>
` : ''}
`;
// Fade-in Animation
widget.style.animation = 'weatherFadeIn 0.5s ease';
},
getWeatherEmoji: function(iconCode) {
const map = {
'01d': '☀️', '01n': '🌙',
'02d': '⛅', '02n': '☁️',
'03d': '☁️', '03n': '☁️',
'04d': '☁️', '04n': '☁️',
'09d': '🌧️', '09n': '🌧️',
'10d': '🌦️', '10n': '🌧️',
'11d': '⛈️', '11n': '⛈️',
'13d': '❄️', '13n': '❄️',
'50d': '🌫️', '50n': '🌫️'
};
return map[iconCode] || '🌤️';
}
};
document.addEventListener('DOMContentLoaded', function() {
WeatherUpdater.init();
});
</script>
</body>
</html>
+9
View File
@@ -46,6 +46,15 @@
"meta_description": "",
"meta_keywords": ""
},
"weather": {
"enabled": true,
"api_key": "",
"location": "Oberdürnten,CH",
"lat": "47.2833",
"lon": "8.7167",
"update_interval": 5,
"units": "metric"
},
"last_updated": null,
"updated_by": null
}