Innovenergy_trunk/csharp/App/Backend/Services/WeatherService.cs

179 lines
6.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Flurl.Http;
using Newtonsoft.Json;
namespace InnovEnergy.App.Backend.Services;
public static class WeatherService
{
public record DailyWeather(
string Date,
double TempMin,
double TempMax,
double SunshineHours,
double PrecipitationMm,
string Description
);
private static readonly Dictionary<string, (double Lat, double Lon)> GeoCache = new();
/// <summary>
/// Returns a 7-day weather forecast for the given city, or null on any failure.
/// </summary>
public static async Task<List<DailyWeather>?> GetForecastAsync(string? city, string? country, string? region = null)
{
if (string.IsNullOrWhiteSpace(city))
return null;
try
{
var coords = await GeocodeAsync(city, region);
if (coords == null)
return null;
var (lat, lon) = coords.Value;
return await FetchForecastAsync(lat, lon);
}
catch (Exception ex)
{
Console.Error.WriteLine($"[WeatherService] Error fetching forecast for '{city}': {ex.Message}");
return null;
}
}
/// <summary>
/// Formats a forecast list into a compact text block for AI prompt injection.
/// </summary>
public static string FormatForPrompt(List<DailyWeather> forecast)
{
var lines = forecast.Select(d =>
{
var date = DateTime.Parse(d.Date);
var dayName = date.ToString("ddd dd MMM");
return $"- {dayName}: {d.TempMin:F0}{d.TempMax:F0}°C, {d.Description}, {d.SunshineHours:F1}h sunshine, {d.PrecipitationMm:F0}mm rain";
});
return "WEATHER FORECAST (next 7 days):\n" + string.Join("\n", lines);
}
/// <summary>
/// Extracts a geocodable city name from a Location field that may contain a full address.
/// Handles Swiss formats like "Street 5, 8604 Volketswil" → "Volketswil"
/// Also tries the Region field as fallback.
/// </summary>
private static IEnumerable<string> ExtractSearchTerms(string city, string? region)
{
// If it contains a comma, try the part after the last comma (often "PostalCode City")
if (city.Contains(','))
{
var afterComma = city.Split(',').Last().Trim();
// Strip leading postal code (digits) → "8604 Volketswil" → "Volketswil"
var withoutPostal = System.Text.RegularExpressions.Regex.Replace(afterComma, @"^\d+\s*", "").Trim();
if (!string.IsNullOrEmpty(withoutPostal))
yield return withoutPostal;
if (!string.IsNullOrEmpty(afterComma))
yield return afterComma;
}
// Try the raw value as-is
yield return city;
// Fallback to Region
if (!string.IsNullOrWhiteSpace(region))
yield return region;
}
private static async Task<(double Lat, double Lon)?> GeocodeAsync(string city, string? region = null)
{
if (GeoCache.TryGetValue(city, out var cached))
return cached;
foreach (var term in ExtractSearchTerms(city, region))
{
var url = $"https://geocoding-api.open-meteo.com/v1/search?name={Uri.EscapeDataString(term)}&count=1&language=en";
var json = await url.GetStringAsync();
var data = JsonConvert.DeserializeObject<dynamic>(json);
if (data?.results != null && data.results.Count > 0)
{
var lat = (double)data.results[0].latitude;
var lon = (double)data.results[0].longitude;
GeoCache[city] = (lat, lon);
Console.WriteLine($"[WeatherService] Geocoded '{city}' (matched on '{term}') → ({lat:F4}, {lon:F4})");
return (lat, lon);
}
}
Console.WriteLine($"[WeatherService] Could not geocode location: '{city}'");
return null;
}
private static async Task<List<DailyWeather>?> FetchForecastAsync(double lat, double lon)
{
var url = $"https://api.open-meteo.com/v1/forecast"
+ $"?latitude={lat}&longitude={lon}"
+ "&daily=temperature_2m_max,temperature_2m_min,sunshine_duration,precipitation_sum,weathercode"
+ "&timezone=Europe/Zurich&forecast_days=7";
var json = await url.GetStringAsync();
var data = JsonConvert.DeserializeObject<dynamic>(json);
if (data?.daily == null)
return null;
var dates = data.daily.time;
var tempMax = data.daily.temperature_2m_max;
var tempMin = data.daily.temperature_2m_min;
var sun = data.daily.sunshine_duration;
var precip = data.daily.precipitation_sum;
var codes = data.daily.weathercode;
var forecast = new List<DailyWeather>();
for (int i = 0; i < dates.Count; i++)
{
forecast.Add(new DailyWeather(
Date: (string)dates[i],
TempMin: (double)tempMin[i],
TempMax: (double)tempMax[i],
SunshineHours: Math.Round((double)sun[i] / 3600.0, 1),
PrecipitationMm: (double)precip[i],
Description: WeatherCodeToDescription((int)codes[i])
));
}
Console.WriteLine($"[WeatherService] Fetched {forecast.Count}-day forecast.");
return forecast;
}
private static string WeatherCodeToDescription(int code) => code switch
{
0 => "Clear sky",
1 => "Mainly clear",
2 => "Partly cloudy",
3 => "Overcast",
45 => "Fog",
48 => "Depositing rime fog",
51 => "Light drizzle",
53 => "Moderate drizzle",
55 => "Dense drizzle",
61 => "Slight rain",
63 => "Moderate rain",
65 => "Heavy rain",
66 => "Light freezing rain",
67 => "Heavy freezing rain",
71 => "Slight snow",
73 => "Moderate snow",
75 => "Heavy snow",
77 => "Snow grains",
80 => "Slight showers",
81 => "Moderate showers",
82 => "Violent showers",
85 => "Slight snow showers",
86 => "Heavy snow showers",
95 => "Thunderstorm",
96 => "Thunderstorm with slight hail",
99 => "Thunderstorm with heavy hail",
_ => "Unknown"
};
}