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