Wetterdienst: Erweitert

This commit is contained in:
2026-08-23 21:45:25 +02:00
parent e76884a58e
commit 83a430f8ee
6 changed files with 60 additions and 7 deletions
@@ -39,6 +39,8 @@ public sealed class DwdWeatherServiceTests
<dwd:Forecast dwd:elementName="FF"><dwd:value>5</dwd:value></dwd:Forecast>
<dwd:Forecast dwd:elementName="FX1"><dwd:value>-</dwd:value></dwd:Forecast>
<dwd:Forecast dwd:elementName="RR1c"><dwd:value>1.25</dwd:value></dwd:Forecast>
<dwd:Forecast dwd:elementName="R101"><dwd:value>67.6</dwd:value></dwd:Forecast>
<dwd:Forecast dwd:elementName="SunD1"><dwd:value>1800</dwd:value></dwd:Forecast>
<dwd:Forecast dwd:elementName="Neff"><dwd:value>75</dwd:value></dwd:Forecast>
<dwd:Forecast dwd:elementName="ww"><dwd:value>61</dwd:value></dwd:Forecast>
</kml:ExtendedData></kml:Placemark></kml:Document>
@@ -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);
}
+4
View File
@@ -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,
});
+4
View File
@@ -42,6 +42,10 @@ public sealed class WeatherForecastHour
public double? WindSpeedKmh { get; set; }
public double? WindGustKmh { get; set; }
public double? PrecipitationMm { get; set; }
/// <summary>Stündliche MOSMIX-Wahrscheinlichkeit für mehr als 0,1 mm Niederschlag.</summary>
public double? PrecipitationProbabilityPercent { get; set; }
/// <summary>MOSMIX-Sonnenscheindauer der letzten Stunde, in Minuten.</summary>
public double? SunshineDurationMinutes { get; set; }
public double? CloudCoverPercent { get; set; }
public int? WeatherCode { get; set; }
}
@@ -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("0816 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,
};
}
@@ -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<WeatherWarning> Warnings { get; init; } = [];
@@ -38,10 +40,15 @@ public sealed class SchoolDayWeatherSummary
{
get
{
var lines = new List<string> { Label };
var lines = new List<string> { $"{Label} · 0816 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,
};
+5 -2
View File
@@ -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 0618 Uhr je Tag auf ein kleines Symbol im
Wochenüberblick verdichtet die Stundenwerte von 0816 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