From 83a430f8ee99d0c0a6374c8f9718efca83b26875 Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Sun, 23 Aug 2026 21:45:25 +0200 Subject: [PATCH] Wetterdienst: Erweitert --- LehrerApp.Api.Tests/DwdWeatherServiceTests.cs | 4 +++ LehrerApp.Api/DwdWeatherService.cs | 4 +++ LehrerApp.Core/Models/Weather.cs | 4 +++ .../SchoolDayWeatherSummaryTests.cs | 27 +++++++++++++++++-- .../Planning/SchoolDayWeatherSummary.cs | 21 ++++++++++++--- TODO.md | 7 +++-- 6 files changed, 60 insertions(+), 7 deletions(-) diff --git a/LehrerApp.Api.Tests/DwdWeatherServiceTests.cs b/LehrerApp.Api.Tests/DwdWeatherServiceTests.cs index 7722cbe..633408d 100644 --- a/LehrerApp.Api.Tests/DwdWeatherServiceTests.cs +++ b/LehrerApp.Api.Tests/DwdWeatherServiceTests.cs @@ -39,6 +39,8 @@ public sealed class DwdWeatherServiceTests 5 - 1.25 + 67.6 + 1800 75 61 @@ -52,6 +54,8 @@ public sealed class DwdWeatherServiceTests Assert.Equal(18, hour.WindSpeedKmh); Assert.Null(hour.WindGustKmh); Assert.Equal(1.3, hour.PrecipitationMm); + Assert.Equal(68, hour.PrecipitationProbabilityPercent); + Assert.Equal(30, hour.SunshineDurationMinutes); Assert.Equal(61, hour.WeatherCode); } diff --git a/LehrerApp.Api/DwdWeatherService.cs b/LehrerApp.Api/DwdWeatherService.cs index 6f6aa45..5ba3833 100644 --- a/LehrerApp.Api/DwdWeatherService.cs +++ b/LehrerApp.Api/DwdWeatherService.cs @@ -150,6 +150,10 @@ public sealed class DwdWeatherService(HttpClient http) WindSpeedKmh = Round(Value("FF", i) * 3.6, 1), WindGustKmh = Round(Value("FX1", i) * 3.6, 1), PrecipitationMm = Round(Value("RR1c", i), 1), + // Beide Zusatzwerte stammen aus derselben MOSMIX-L-Datei. R101 bezeichnet die + // Wahrscheinlichkeit für >0,1 mm in der letzten Stunde, SunD1 liegt in Sekunden vor. + PrecipitationProbabilityPercent = Round(Value("R101", i), 0), + SunshineDurationMinutes = Round(Value("SunD1", i) / 60, 0), CloudCoverPercent = Round(Value("Neff", i), 0), WeatherCode = Value("ww", i) is { } code ? (int)Math.Round(code) : null, }); diff --git a/LehrerApp.Core/Models/Weather.cs b/LehrerApp.Core/Models/Weather.cs index 93927f3..28db93f 100644 --- a/LehrerApp.Core/Models/Weather.cs +++ b/LehrerApp.Core/Models/Weather.cs @@ -42,6 +42,10 @@ public sealed class WeatherForecastHour public double? WindSpeedKmh { get; set; } public double? WindGustKmh { get; set; } public double? PrecipitationMm { get; set; } + /// Stündliche MOSMIX-Wahrscheinlichkeit für mehr als 0,1 mm Niederschlag. + public double? PrecipitationProbabilityPercent { get; set; } + /// MOSMIX-Sonnenscheindauer der letzten Stunde, in Minuten. + public double? SunshineDurationMinutes { get; set; } public double? CloudCoverPercent { get; set; } public int? WeatherCode { get; set; } } diff --git a/LehrerApp.Desktop.Tests/SchoolDayWeatherSummaryTests.cs b/LehrerApp.Desktop.Tests/SchoolDayWeatherSummaryTests.cs index a453f81..b86ed96 100644 --- a/LehrerApp.Desktop.Tests/SchoolDayWeatherSummaryTests.cs +++ b/LehrerApp.Desktop.Tests/SchoolDayWeatherSummaryTests.cs @@ -41,6 +41,27 @@ public sealed class SchoolDayWeatherSummaryTests Assert.Equal(SchoolDayWeatherKind.Sunny, summary!.Kind); } + [Fact] + public void From_VerwendetFuerKennzahlenNurAchtBisSechzehnUhr() + { + var snapshot = Snapshot( + Hour(7, temperature: 5, rain: 3, probability: 95, sunshineMinutes: 0), + Hour(8, temperature: 12, probability: 20, sunshineMinutes: 30), + Hour(16, temperature: 17, probability: 65, sunshineMinutes: 45), + Hour(17, temperature: 25, rain: 4, probability: 99, sunshineMinutes: 60)); + + var summary = SchoolDayWeatherSummary.From(snapshot, Day); + + Assert.Equal(12, summary!.MinTemperatureC); + Assert.Equal(17, summary.MaxTemperatureC); + Assert.Equal(0, summary.PrecipitationMm); + Assert.Equal(65, summary.MaxHourlyPrecipitationProbabilityPercent); + Assert.Equal(1.3, summary.SunshineDurationHours); + Assert.Contains("08–16 Uhr", summary.Tooltip); + Assert.Contains("Regenwahrscheinlichkeit: bis 65 % (stündlich)", summary.Tooltip); + Assert.Contains($"Sonnenschein: {summary.SunshineDurationHours:0.#} h", summary.Tooltip); + } + [Fact] public void From_AmtlicheGewitterwarnung_PraegtSymbolUndTooltip() { @@ -93,10 +114,12 @@ public sealed class SchoolDayWeatherSummaryTests }; private static WeatherForecastHour Hour(int hour, int? code = 0, double? temperature = 20, - double? rain = 0, double? gust = 20, double? cloud = 20) => new() + double? rain = 0, double? gust = 20, double? cloud = 20, + double? probability = null, double? sunshineMinutes = null) => new() { ValidAt = Day.ToDateTime(new TimeOnly(hour, 0)), WeatherCode = code, TemperatureC = temperature, PrecipitationMm = rain, WindGustKmh = gust, - CloudCoverPercent = cloud, + CloudCoverPercent = cloud, PrecipitationProbabilityPercent = probability, + SunshineDurationMinutes = sunshineMinutes, }; } diff --git a/LehrerApp.Desktop/ViewModels/Planning/SchoolDayWeatherSummary.cs b/LehrerApp.Desktop/ViewModels/Planning/SchoolDayWeatherSummary.cs index dce154a..2287b65 100644 --- a/LehrerApp.Desktop/ViewModels/Planning/SchoolDayWeatherSummary.cs +++ b/LehrerApp.Desktop/ViewModels/Planning/SchoolDayWeatherSummary.cs @@ -13,6 +13,8 @@ public sealed class SchoolDayWeatherSummary public double? MinTemperatureC { get; init; } public double? MaxTemperatureC { get; init; } public double PrecipitationMm { get; init; } + public double? MaxHourlyPrecipitationProbabilityPercent { get; init; } + public double? SunshineDurationHours { get; init; } public double? MaxWindGustKmh { get; init; } public IReadOnlyList Warnings { get; init; } = []; @@ -38,10 +40,15 @@ public sealed class SchoolDayWeatherSummary { get { - var lines = new List { Label }; + var lines = new List { $"{Label} · 08–16 Uhr" }; if (MinTemperatureC is { } min && MaxTemperatureC is { } max) - lines.Add(Math.Abs(max - min) < 0.05 ? $"{min:0.#} °C" : $"{min:0.#}–{max:0.#} °C"); + lines.Add("Temperatur: " + (Math.Abs(max - min) < 0.05 + ? $"{min:0.#} °C" : $"{min:0.#}–{max:0.#} °C")); + if (MaxHourlyPrecipitationProbabilityPercent is { } probability) + lines.Add($"Regenwahrscheinlichkeit: bis {probability:0} % (stündlich)"); if (PrecipitationMm >= 0.05) lines.Add($"Niederschlag: {PrecipitationMm:0.#} mm"); + if (SunshineDurationHours is { } sunshine) + lines.Add($"Sonnenschein: {sunshine:0.#} h"); if (MaxWindGustKmh is { } gust) lines.Add($"Max. Böen: {gust:0.#} km/h"); foreach (var warning in Warnings) { @@ -63,7 +70,7 @@ public sealed class SchoolDayWeatherSummary var schoolHours = allHours.Where(x => { var hour = Local(x.ValidAt).Hour; - return hour >= 6 && hour <= 18; + return hour >= 8 && hour <= 16; }).ToList(); var hours = schoolHours.Count > 0 ? schoolHours : allHours; if (hours.Count == 0) return null; @@ -71,6 +78,10 @@ public sealed class SchoolDayWeatherSummary var warnings = snapshot.Warnings.Where(x => WarningTouchesDate(x, date)).ToList(); var codes = hours.Where(x => x.WeatherCode.HasValue).Select(x => x.WeatherCode!.Value).ToList(); var precipitation = hours.Sum(x => x.PrecipitationMm ?? 0); + var probabilities = hours.Where(x => x.PrecipitationProbabilityPercent.HasValue) + .Select(x => x.PrecipitationProbabilityPercent!.Value).ToList(); + var sunshineMinutes = hours.Where(x => x.SunshineDurationMinutes.HasValue) + .Select(x => x.SunshineDurationMinutes!.Value).ToList(); var gust = hours.Where(x => x.WindGustKmh.HasValue).Select(x => x.WindGustKmh!.Value).DefaultIfEmpty().Max(); var hasGust = hours.Any(x => x.WindGustKmh.HasValue); var warningText = string.Join(" ", warnings.SelectMany(x => new[] { x.Event, x.Headline })); @@ -95,6 +106,10 @@ public sealed class SchoolDayWeatherSummary MinTemperatureC = temperatures.Count > 0 ? temperatures.Min() : null, MaxTemperatureC = temperatures.Count > 0 ? temperatures.Max() : null, PrecipitationMm = Math.Round(precipitation, 1, MidpointRounding.AwayFromZero), + MaxHourlyPrecipitationProbabilityPercent = probabilities.Count > 0 + ? Math.Round(probabilities.Max(), 0, MidpointRounding.AwayFromZero) : null, + SunshineDurationHours = sunshineMinutes.Count > 0 + ? Math.Round(sunshineMinutes.Sum() / 60, 1, MidpointRounding.AwayFromZero) : null, MaxWindGustKmh = hasGust ? Math.Round(gust, 1, MidpointRounding.AwayFromZero) : null, Warnings = warnings, }; diff --git a/TODO.md b/TODO.md index 8dc0830..05ba8eb 100644 --- a/TODO.md +++ b/TODO.md @@ -753,9 +753,12 @@ weiterhin keine automatisierte Testsuite (kein PHP-Testframework im Projekt). **Nachtrag Wetter im Wochenstundenplan:** Die MOSMIX-Ausgabe des Servers umfasst nun die vollständige Prognosespanne bis +240 Stunden statt nur 72 Stunden. Der schreibgeschützte - Wochenüberblick verdichtet die Stundenwerte von 06–18 Uhr je Tag auf ein kleines Symbol im + Wochenüberblick verdichtet die Stundenwerte von 08–16 Uhr je Tag auf ein kleines Symbol im Tageskopf (Priorität Gewitter → Sturm ab 62 km/h Böen → Regen → bewölkt → sonnig). Der - Tooltip nennt Temperaturspanne, Niederschlag, maximale Böen und betroffene Warnungen. + Tooltip nennt Temperaturspanne, die höchste stündliche Regenwahrscheinlichkeit (`R101`), + Niederschlagsmenge, aufsummierte Sonnenscheindauer (`SunD1`), maximale Böen und betroffene + Warnungen. Alle Werte stammen aus derselben bereits geladenen MOSMIX-L-Datei; ein UV-Index + wird bewusst nicht geschätzt, weil MOSMIX ihn nicht direkt enthält. Amtliche DWD-Warnungen und Vorabinformationen erhalten zusätzlich ein Warnsymbol am Tag und eine gut sichtbare Zusammenfassung oberhalb des Rasters; mehrtägige Warnungen werden jedem überlappten Schultag zugeordnet. Vergangene Tage und Wochen außerhalb der verfügbaren