Wetterdienst: Erweitert
This commit is contained in:
@@ -39,6 +39,8 @@ public sealed class DwdWeatherServiceTests
|
|||||||
<dwd:Forecast dwd:elementName="FF"><dwd:value>5</dwd:value></dwd:Forecast>
|
<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="FX1"><dwd:value>-</dwd:value></dwd:Forecast>
|
||||||
<dwd:Forecast dwd:elementName="RR1c"><dwd:value>1.25</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="Neff"><dwd:value>75</dwd:value></dwd:Forecast>
|
||||||
<dwd:Forecast dwd:elementName="ww"><dwd:value>61</dwd:value></dwd:Forecast>
|
<dwd:Forecast dwd:elementName="ww"><dwd:value>61</dwd:value></dwd:Forecast>
|
||||||
</kml:ExtendedData></kml:Placemark></kml:Document>
|
</kml:ExtendedData></kml:Placemark></kml:Document>
|
||||||
@@ -52,6 +54,8 @@ public sealed class DwdWeatherServiceTests
|
|||||||
Assert.Equal(18, hour.WindSpeedKmh);
|
Assert.Equal(18, hour.WindSpeedKmh);
|
||||||
Assert.Null(hour.WindGustKmh);
|
Assert.Null(hour.WindGustKmh);
|
||||||
Assert.Equal(1.3, hour.PrecipitationMm);
|
Assert.Equal(1.3, hour.PrecipitationMm);
|
||||||
|
Assert.Equal(68, hour.PrecipitationProbabilityPercent);
|
||||||
|
Assert.Equal(30, hour.SunshineDurationMinutes);
|
||||||
Assert.Equal(61, hour.WeatherCode);
|
Assert.Equal(61, hour.WeatherCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,10 @@ public sealed class DwdWeatherService(HttpClient http)
|
|||||||
WindSpeedKmh = Round(Value("FF", i) * 3.6, 1),
|
WindSpeedKmh = Round(Value("FF", i) * 3.6, 1),
|
||||||
WindGustKmh = Round(Value("FX1", i) * 3.6, 1),
|
WindGustKmh = Round(Value("FX1", i) * 3.6, 1),
|
||||||
PrecipitationMm = Round(Value("RR1c", i), 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),
|
CloudCoverPercent = Round(Value("Neff", i), 0),
|
||||||
WeatherCode = Value("ww", i) is { } code ? (int)Math.Round(code) : null,
|
WeatherCode = Value("ww", i) is { } code ? (int)Math.Round(code) : null,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ public sealed class WeatherForecastHour
|
|||||||
public double? WindSpeedKmh { get; set; }
|
public double? WindSpeedKmh { get; set; }
|
||||||
public double? WindGustKmh { get; set; }
|
public double? WindGustKmh { get; set; }
|
||||||
public double? PrecipitationMm { 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 double? CloudCoverPercent { get; set; }
|
||||||
public int? WeatherCode { get; set; }
|
public int? WeatherCode { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,27 @@ public sealed class SchoolDayWeatherSummaryTests
|
|||||||
Assert.Equal(SchoolDayWeatherKind.Sunny, summary!.Kind);
|
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]
|
[Fact]
|
||||||
public void From_AmtlicheGewitterwarnung_PraegtSymbolUndTooltip()
|
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,
|
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,
|
ValidAt = Day.ToDateTime(new TimeOnly(hour, 0)), WeatherCode = code,
|
||||||
TemperatureC = temperature, PrecipitationMm = rain, WindGustKmh = gust,
|
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? MinTemperatureC { get; init; }
|
||||||
public double? MaxTemperatureC { get; init; }
|
public double? MaxTemperatureC { get; init; }
|
||||||
public double PrecipitationMm { get; init; }
|
public double PrecipitationMm { get; init; }
|
||||||
|
public double? MaxHourlyPrecipitationProbabilityPercent { get; init; }
|
||||||
|
public double? SunshineDurationHours { get; init; }
|
||||||
public double? MaxWindGustKmh { get; init; }
|
public double? MaxWindGustKmh { get; init; }
|
||||||
public IReadOnlyList<WeatherWarning> Warnings { get; init; } = [];
|
public IReadOnlyList<WeatherWarning> Warnings { get; init; } = [];
|
||||||
|
|
||||||
@@ -38,10 +40,15 @@ public sealed class SchoolDayWeatherSummary
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var lines = new List<string> { Label };
|
var lines = new List<string> { $"{Label} · 08–16 Uhr" };
|
||||||
if (MinTemperatureC is { } min && MaxTemperatureC is { } max)
|
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 (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");
|
if (MaxWindGustKmh is { } gust) lines.Add($"Max. Böen: {gust:0.#} km/h");
|
||||||
foreach (var warning in Warnings)
|
foreach (var warning in Warnings)
|
||||||
{
|
{
|
||||||
@@ -63,7 +70,7 @@ public sealed class SchoolDayWeatherSummary
|
|||||||
var schoolHours = allHours.Where(x =>
|
var schoolHours = allHours.Where(x =>
|
||||||
{
|
{
|
||||||
var hour = Local(x.ValidAt).Hour;
|
var hour = Local(x.ValidAt).Hour;
|
||||||
return hour >= 6 && hour <= 18;
|
return hour >= 8 && hour <= 16;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
var hours = schoolHours.Count > 0 ? schoolHours : allHours;
|
var hours = schoolHours.Count > 0 ? schoolHours : allHours;
|
||||||
if (hours.Count == 0) return null;
|
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 warnings = snapshot.Warnings.Where(x => WarningTouchesDate(x, date)).ToList();
|
||||||
var codes = hours.Where(x => x.WeatherCode.HasValue).Select(x => x.WeatherCode!.Value).ToList();
|
var codes = hours.Where(x => x.WeatherCode.HasValue).Select(x => x.WeatherCode!.Value).ToList();
|
||||||
var precipitation = hours.Sum(x => x.PrecipitationMm ?? 0);
|
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 gust = hours.Where(x => x.WindGustKmh.HasValue).Select(x => x.WindGustKmh!.Value).DefaultIfEmpty().Max();
|
||||||
var hasGust = hours.Any(x => x.WindGustKmh.HasValue);
|
var hasGust = hours.Any(x => x.WindGustKmh.HasValue);
|
||||||
var warningText = string.Join(" ", warnings.SelectMany(x => new[] { x.Event, x.Headline }));
|
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,
|
MinTemperatureC = temperatures.Count > 0 ? temperatures.Min() : null,
|
||||||
MaxTemperatureC = temperatures.Count > 0 ? temperatures.Max() : null,
|
MaxTemperatureC = temperatures.Count > 0 ? temperatures.Max() : null,
|
||||||
PrecipitationMm = Math.Round(precipitation, 1, MidpointRounding.AwayFromZero),
|
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,
|
MaxWindGustKmh = hasGust ? Math.Round(gust, 1, MidpointRounding.AwayFromZero) : null,
|
||||||
Warnings = warnings,
|
Warnings = warnings,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
**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
|
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
|
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
|
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
|
eine gut sichtbare Zusammenfassung oberhalb des Rasters; mehrtägige Warnungen werden jedem
|
||||||
überlappten Schultag zugeordnet. Vergangene Tage und Wochen außerhalb der verfügbaren
|
überlappten Schultag zugeordnet. Vergangene Tage und Wochen außerhalb der verfügbaren
|
||||||
|
|||||||
Reference in New Issue
Block a user