Add weather report from Open-Meteo for Passau

This commit is contained in:
Richard Zink committed 2026-10-07 08:26:48 +02:00
1 parent cc958cb7bb
commit 5ea523e6c4
1 file changed
+246 -1
+246 -1
View File
@@ -71,6 +71,77 @@
font-size: 11px;
margin-left: 4px;
}
.weather-divider {
height: 1px;
background: rgba(230, 223, 209, 0.2);
margin: 8px 0;
}
.weather-current {
display: flex;
align-items: center;
gap: 12px;
padding: 4px 0 8px 0;
border-bottom: 1px solid rgba(230, 223, 209, 0.2);
margin-bottom: 8px;
}
.weather-current-icon {
width: 36px;
height: 36px;
flex-shrink: 0;
}
.weather-current-icon svg { width: 100%; height: 100%; }
.weather-current-temp {
font-size: 22px;
font-weight: bold;
}
.weather-current-detail {
font-size: 11px;
color: #b0a89a;
line-height: 1.4;
}
.weather-day {
margin-bottom: 6px;
}
.weather-day-header {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 2px 0;
font-size: 12px;
}
.weather-day-name {
color: #e6dfd1;
font-weight: bold;
}
.weather-day-minmax {
color: #999187;
font-size: 11px;
}
.weather-day-slots {
display: flex;
justify-content: space-between;
gap: 4px;
font-size: 11px;
}
.weather-slot {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
gap: 2px;
}
.weather-slot-icon {
width: 22px;
height: 22px;
}
.weather-slot-icon svg { width: 100%; height: 100%; }
.weather-slot-label {
color: #999187;
font-size: 9px;
}
.weather-slot-temp {
color: #e6dfd1;
}
</style>
</head>
<body>
@@ -160,10 +231,13 @@
</div>
<div class="info-row">
<div class="info-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="#b0a89a" stroke-width="2"><path d="M3 12 A9 9 0 0 1 21 12 Z"/></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="#b0a89a" stroke-width="2"><path d="M3 12 A9 9 0 0 1 21 22 Z"/></svg>
</div>
<div class="info-content"><span class="info-label">Mond unter</span><span><span class="info-value" id="moonset-value">--:--</span><span class="info-sub" id="moonset-when"></span></span></div>
</div>
<div id="weather-divider" class="weather-divider"></div>
<div id="weather-current" class="weather-current"></div>
<div id="weather-forecast" class="weather-forecast"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vanta@0.5.21/dist/vanta.clouds.min.js"></script>
@@ -602,6 +676,177 @@
setTimeout(tick, 1000);
}
// ============ Weather ============
var WEATHER_LAT = 48.5667;
var WEATHER_LON = 13.4319;
var weatherData = null;
var WMO_ICONS = {
0: 'sun',
1: 'sun-cloud',
2: 'cloud-sun',
3: 'cloud',
45: 'fog',
48: 'fog',
51: 'drizzle',
53: 'drizzle',
55: 'drizzle',
56: 'drizzle',
57: 'drizzle',
61: 'rain',
63: 'rain',
65: 'rain',
66: 'rain',
67: 'rain',
71: 'snow',
73: 'snow',
75: 'snow',
77: 'snow',
80: 'rain',
81: 'rain',
82: 'rain',
85: 'snow',
86: 'snow',
95: 'thunder',
96: 'thunder',
99: 'thunder'
};
function weatherSVG(code) {
var c = '#e6dfd1';
switch (code) {
case 0:
return '<svg viewBox="0 0 24 24" fill="' + c + '"><circle cx="12" cy="12" r="5"/><g stroke="' + c + '" stroke-width="2" stroke-linecap="round"><line x1="12" y1="2" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22"/><line x1="2" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/><line x1="4.9" y1="4.9" x2="6.3" y2="6.3"/><line x1="17.7" y1="17.7" x2="19.1" y2="19.1"/><line x1="4.9" y1="19.1" x2="6.3" y2="17.7"/><line x1="17.7" y1="6.3" x2="19.1" y2="4.9"/></g></svg>';
case 1:
case 2:
return '<svg viewBox="0 0 24 24" fill="none" stroke="' + c + '" stroke-width="2"><circle cx="8" cy="8" r="3" fill="' + c + '"/><line x1="8" y1="2" x2="8" y2="3"/><line x1="2" y1="8" x2="3" y2="8"/><line x1="3.5" y1="3.5" x2="4.2" y2="4.2"/><line x1="12.5" y1="3.5" x2="11.8" y2="4.2"/><path d="M10 14 a4 4 0 0 1 4 -4 a4 4 0 0 1 4 4 a3 3 0 0 1 0 6 h-8 a3 3 0 0 1 0 -6 z" fill="#999187" stroke="none"/></svg>';
case 3:
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 18 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/></svg>';
case 45:
case 48:
return '<svg viewBox="0 0 24 24" fill="none" stroke="' + c + '" stroke-width="2"><path d="M6 18 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z" fill="#999187" stroke="' + c + '"/><line x1="8" y1="20" x2="8" y2="22"/><line x1="12" y1="20" x2="12" y2="22"/><line x1="16" y1="20" x2="16" y2="22"/></svg>';
case 51:
case 55:
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 14 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="#7c9aa1" stroke-width="1.5" stroke-linecap="round"><line x1="8" y1="17" x2="7" y2="20"/><line x1="13" y1="17" x2="12" y2="20"/><line x1="17" y1="17" x2="16" y2="20"/></g></svg>';
case 53:
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 14 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="#7c9aa1" stroke-width="1.5" stroke-linecap="round"><line x1="9" y1="16" x2="8" y2="20"/><line x1="13" y1="16" x2="12" y2="20"/></g></svg>';
case 61:
case 80:
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="#7c9aa1" stroke-width="1.5" stroke-linecap="round"><line x1="8" y1="16" x2="7" y2="21"/><line x1="13" y1="16" x2="12" y2="21"/><line x1="17" y1="16" x2="16" y2="21"/></g></svg>';
case 63:
case 81:
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="#7c9aa1" stroke-width="1.5" stroke-linecap="round"><line x1="9" y1="15" x2="7" y2="21"/><line x1="13" y1="15" x2="11" y2="21"/><line x1="17" y1="15" x2="15" y2="21"/></g></svg>';
case 65:
case 82:
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="#7c9aa1" stroke-width="1.5" stroke-linecap="round"><line x1="7" y1="15" x2="4" y2="22"/><line x1="11" y1="15" x2="8" y2="22"/><line x1="15" y1="15" x2="12" y2="22"/><line x1="19" y1="15" x2="16" y2="22"/></g></svg>';
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g fill="' + c + '"><circle cx="8" cy="18" r="1.5"/><circle cx="13" cy="19" r="1.5"/><circle cx="17" cy="18" r="1.5"/></g></svg>';
case 95:
case 96:
case 99:
return '<svg viewBox="0 0 24 24" fill="#999187"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><path d="M12 2 L8 11 L12 11 L10 18 L16 9 L12 9 Z" fill="#d6b46a"/></svg>';
default:
return '<svg viewBox="0 0 24 24" fill="#999187"><circle cx="12" cy="12" r="6"/></svg>';
}
}
function fetchWeather() {
var url = 'https://api.open-meteo.com/v1/forecast?latitude=' + WEATHER_LAT +
'&longitude=' + WEATHER_LON +
'&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m' +
'&hourly=temperature_2m,weather_code' +
'&daily=weather_code,temperature_2m_max,temperature_2m_min' +
'&timezone=Europe/Berlin&forecast_days=5';
fetch(url)
.then(function(response) { return response.json(); })
.then(function(data) {
weatherData = data;
updateWeatherDisplay();
})
.catch(function(err) {
console.log('Weather fetch failed:', err);
});
}
function findHourlyIndex(targetDate, hourlyTimeArray) {
var bestIdx = 0;
var bestDiff = Infinity;
for (var i = 0; i < hourlyTimeArray.length; i++) {
var t = new Date(hourlyTimeArray[i]);
var diff = Math.abs(t.getTime() - targetDate.getTime());
if (diff < bestDiff) {
bestDiff = diff;
bestIdx = i;
}
}
return bestIdx;
}
function updateWeatherDisplay() {
if (!weatherData) return;
var now = new Date();
var current = weatherData.current;
var hourly = weatherData.hourly;
var daily = weatherData.daily;
// Current
var currentHtml = '<div class="weather-current-icon">' + weatherSVG(current.weather_code) + '</div>' +
'<div><div class="weather-current-temp">' + Math.round(current.temperature_2m) + '°C</div>' +
'<div class="weather-current-detail">' +
'Feuchte ' + Math.round(current.relative_humidity_2m) + '% · Wind ' + Math.round(current.wind_speed_10m) + ' km/h</div></div>';
document.getElementById('weather-current').innerHTML = currentHtml;
// Forecast
var forecastHtml = '';
var dayNames = ['Heute', 'Morgen'];
for (var d = 0; d < 4; d++) {
var dayDate = new Date(daily.time[d]);
var dayLabel = d === 0 ? 'Heute' : dayNames[1] || dayDate.toLocaleDateString('de-DE', { weekday: 'short' });
if (d > 1) {
dayLabel = dayDate.toLocaleDateString('de-DE', { weekday: 'short' }) + ' ' +
(dayDate.getDate() < 10 ? '0' : '') + dayDate.getDate() + '.' +
(dayDate.getMonth() + 1 < 10 ? '0' : '') + (dayDate.getMonth() + 1) + '.';
}
// Morning (8), Noon (14), Evening (20)
var slots = [8, 14, 20];
var slotsHtml = '';
for (var i = 0; i < slots.length; i++) {
var slotDate = new Date(dayDate);
slotDate.setHours(slots[i], 0, 0, 0);
var slotIdx = findHourlyIndex(slotDate, hourly.time);
var slotCode = hourly.weather_code[slotIdx];
var slotTemp = hourly.temperature_2m[slotIdx];
var slotLabels = ['M', 'Mi', 'A'];
slotsHtml += '<div class="weather-slot">' +
'<div class="weather-slot-icon">' + weatherSVG(slotCode) + '</div>' +
'<div class="weather-slot-label">' + slotLabels[i] + '</div>' +
'<div class="weather-slot-temp">' + Math.round(slotTemp) + '°</div>' +
'</div>';
}
forecastHtml += '<div class="weather-day">' +
'<div class="weather-day-header">' +
'<span class="weather-day-name">' + dayLabel + '</span>' +
'<span class="weather-day-minmax">' + Math.round(daily.temperature_2m_min[d]) + '° / ' + Math.round(daily.temperature_2m_max[d]) + '°</span>' +
'</div>' +
'<div class="weather-day-slots">' + slotsHtml + '</div>' +
'</div>';
}
document.getElementById('weather-forecast').innerHTML = forecastHtml;
}
// Update weather every 15 minutes
fetchWeather();
setInterval(fetchWeather, 15 * 60 * 1000);
animate();
tick();
</script>