From 32b136d4cc85261790fa1f2c9c8060d1e49db797 Mon Sep 17 00:00:00 2001 From: SocksOnHead Date: Thu, 14 Aug 2025 17:55:40 +0200 Subject: [PATCH] fix: crash on unavailable source api (#4) * fix: crash on unavailable source api the screen not updating because of errors * Change temperature format if null Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../INFOSCREEN_WITH_INTERVAL.ino | 21 +++ HomeApi/Extensions/ContractExtensions.cs | 30 ++-- HomeApi/Extensions/MediatorExtensions.cs | 22 +++ HomeApi/Extensions/ServiceCallExtensions.cs | 23 +++ HomeApi/Handlers/ImageGeneration.cs | 9 +- HomeApi/Handlers/Weather.cs | 27 +++- HomeApi/Integration/WeatherService.cs | 8 +- HomeApi/Models/ImageGeneration.cs | 4 +- .../Response/YrWeatherForecastResponse.cs | 153 ++++++++++++++++++ HomeApi/Models/WeatherInformation.cs | 98 +++++------ HomeApi/wwwroot/index.cshtml | 39 +++-- 11 files changed, 338 insertions(+), 96 deletions(-) create mode 100644 HomeApi/Extensions/MediatorExtensions.cs create mode 100644 HomeApi/Extensions/ServiceCallExtensions.cs create mode 100644 HomeApi/Models/Response/YrWeatherForecastResponse.cs diff --git a/Esp32_Code/INFOSCREEN_WITH_INTERVAL/INFOSCREEN_WITH_INTERVAL.ino b/Esp32_Code/INFOSCREEN_WITH_INTERVAL/INFOSCREEN_WITH_INTERVAL.ino index 20c96bc..eddbf6b 100644 --- a/Esp32_Code/INFOSCREEN_WITH_INTERVAL/INFOSCREEN_WITH_INTERVAL.ino +++ b/Esp32_Code/INFOSCREEN_WITH_INTERVAL/INFOSCREEN_WITH_INTERVAL.ino @@ -215,6 +215,18 @@ int jpegDrawCallback(JPEGDRAW *pDraw) { return 1; // Continue decoding } +void clearDisplay() { + // Clear both layers of the display + Paint_SelectImage(BlackImage); + Paint_Clear(WHITE); + Paint_SelectImage(RYImage); + Paint_Clear(WHITE); + + // Send clear command to the display + EPD_7IN5B_V2_Display(BlackImage, RYImage); + Serial.println("Display cleared."); +} + void setup() { Serial.begin(115200); Serial.println("E-Ink Display Initialization"); @@ -513,27 +525,36 @@ void fetchAndDisplayImage() { Serial.println("Image displayed successfully."); } else { Serial.println("Failed to open JPEG image"); + clearDisplay(); } } else { Serial.println("Failed to read entire image."); + clearDisplay(); } free(buffer); } else { Serial.println("Failed to allocate buffer!"); + clearDisplay(); } } else { Serial.println("Content length unknown or invalid."); + clearDisplay(); } } else if (httpCode == HTTPC_ERROR_CONNECTION_REFUSED) { Serial.println("Connection refused - server may be down"); + clearDisplay(); } else if (httpCode == HTTPC_ERROR_CONNECTION_LOST) { Serial.println("Connection lost during request"); + clearDisplay(); } else if (httpCode == HTTPC_ERROR_NO_HTTP_SERVER) { Serial.println("No HTTP server found"); + clearDisplay(); } else if (httpCode == HTTPC_ERROR_NOT_CONNECTED) { Serial.println("Not connected to server"); + clearDisplay(); } else { Serial.printf("HTTP GET failed, error: %d\n", httpCode); + clearDisplay(); } http.end(); diff --git a/HomeApi/Extensions/ContractExtensions.cs b/HomeApi/Extensions/ContractExtensions.cs index bae680a..0ff7c36 100644 --- a/HomeApi/Extensions/ContractExtensions.cs +++ b/HomeApi/Extensions/ContractExtensions.cs @@ -18,10 +18,10 @@ public static class ContractExtensions /// A object populated with current weather, aurora probability, and forecast information. /// public static WeatherInformation ToContract( - this WeatherData weather, - string locationName, + this WeatherData? weather, + string? locationName, AuroraForecastApiResponse? auroraForecast, - List forecasts) + List? forecasts) { return new WeatherInformation { @@ -58,23 +58,23 @@ public static class ContractExtensions }, AuroraProbability = new Probability { - Date = auroraForecast.Date, + Date = auroraForecast?.Date ?? DateTime.Now, Calculated = new CalculatedProbability { - Value = auroraForecast.Probability.Calculated.Value, - Colour = auroraForecast.Probability.Calculated.Colour, - Lat = auroraForecast.Probability.Calculated.Lat, - Long = auroraForecast.Probability.Calculated.Long + Value = auroraForecast?.Probability.Calculated.Value ?? 0, + Colour = auroraForecast?.Probability.Calculated.Colour ?? string.Empty, + Lat = auroraForecast?.Probability.Calculated.Lat ?? 0, + Long = auroraForecast?.Probability.Calculated.Long ?? 0 }, - Colour = auroraForecast.Probability.Colour, - Value = auroraForecast.Probability.Value, + Colour = auroraForecast?.Probability.Colour ?? string.Empty, + Value = auroraForecast?.Probability.Value.ToString() ?? "No data", HighestProbability = new Highest { - Colour = auroraForecast.Probability.Highest.Colour, - Lat = auroraForecast.Probability.Highest.Lat, - Long = auroraForecast.Probability.Highest.Long, - Value = auroraForecast.Probability.Highest.Value, - Date = auroraForecast.Probability.Highest.Date + Colour = auroraForecast?.Probability.Highest.Colour ?? string.Empty, + Lat = auroraForecast?.Probability.Highest.Lat ?? 0, + Long = auroraForecast?.Probability.Highest.Long ?? 0, + Value = auroraForecast?.Probability.Highest.Value ?? 0, + Date = auroraForecast?.Probability.Highest.Date ?? DateTime.Now } } }, diff --git a/HomeApi/Extensions/MediatorExtensions.cs b/HomeApi/Extensions/MediatorExtensions.cs new file mode 100644 index 0000000..4700de2 --- /dev/null +++ b/HomeApi/Extensions/MediatorExtensions.cs @@ -0,0 +1,22 @@ +using MediatR; + +namespace HomeApi.Extensions; + +public static class MediatorExtensions +{ + public static async Task TrySendAsync( + this IMediator mediator, + IRequest request, + CancellationToken cancellationToken) where T : class + { + try + { + return await mediator.Send(request, cancellationToken); + } + catch (OperationCanceledException) { throw; } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/HomeApi/Extensions/ServiceCallExtensions.cs b/HomeApi/Extensions/ServiceCallExtensions.cs new file mode 100644 index 0000000..86b3277 --- /dev/null +++ b/HomeApi/Extensions/ServiceCallExtensions.cs @@ -0,0 +1,23 @@ +namespace HomeApi.Extensions; + +public static class ServiceCallExtensions +{ + public static async Task TryCallAsync( + this TService service, + Func> action, + ILogger logger, + string errorMessage) + where TResult : class? + { + try + { + return await action(service); + } + catch (OperationCanceledException) { throw; } + catch (Exception exception) + { + logger.LogError(exception, errorMessage); + return null; + } + } +} \ No newline at end of file diff --git a/HomeApi/Handlers/ImageGeneration.cs b/HomeApi/Handlers/ImageGeneration.cs index 22a88f2..31887f7 100644 --- a/HomeApi/Handlers/ImageGeneration.cs +++ b/HomeApi/Handlers/ImageGeneration.cs @@ -1,5 +1,6 @@ using System.Dynamic; using System.Reflection; +using HomeApi.Extensions; using HomeApi.Models.Configuration; using MediatR; using Microsoft.Extensions.Options; @@ -22,8 +23,8 @@ public static class ImageGeneration public async Task Handle(Command request, CancellationToken cancellationToken) { - var weather = await mediator.Send(new Weather.Command(), cancellationToken); - var departureBoard = await mediator.Send(new DepartureBoard.Command(), cancellationToken); + var weather = await mediator.TrySendAsync(new Weather.Command(), cancellationToken); + var departureBoard = await mediator.TrySendAsync(new DepartureBoard.Command(), cancellationToken); var model = new Models.Image { @@ -31,8 +32,6 @@ public static class ImageGeneration TimeTable = departureBoard }; - if(weather is null) - throw new Exception("Weather data not found"); var engine = new RazorLightEngineBuilder() .SetOperatingAssembly(Assembly.GetExecutingAssembly()) @@ -59,7 +58,7 @@ public static class ImageGeneration { var browserFetcher = new BrowserFetcher(); await browserFetcher.DownloadAsync(); - var browser = await Puppeteer.LaunchAsync(new LaunchOptions + await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true, Args = ["--disable-gpu"] diff --git a/HomeApi/Handlers/Weather.cs b/HomeApi/Handlers/Weather.cs index 07ab89d..0617a93 100644 --- a/HomeApi/Handlers/Weather.cs +++ b/HomeApi/Handlers/Weather.cs @@ -35,16 +35,27 @@ public static class Weather public async Task Handle(Command request, CancellationToken cancellationToken) { - var coordinates = await _geocodingService.GetCoordinatesAsync(_apiConfiguration.DefaultCity); - if (coordinates is null) - throw new Exception("Coordinates not found"); + var coordinates = await _geocodingService.TryCallAsync(service => + service.GetCoordinatesAsync(_apiConfiguration.DefaultCity), + _logger, + "Failed to get coordinates" + ); + + var aurora = await _auroraService.TryCallAsync(service => + service.GetAuroraForecastAsync(coordinates?.Lat ?? "0.00", coordinates?.Lon ?? "0.00"), + _logger, + "Failed to get aurora forecast" + ); + + var weather = await _weatherService.TryCallAsync(service => + service.GetWeatherAsync(coordinates?.Lat ?? string.Empty, coordinates?.Lon ?? string.Empty), + _logger, + "Failed to get weather data" + ); - var aurora = await _auroraService.GetAuroraForecastAsync(coordinates.Lat, coordinates.Lon); - var weather = await _weatherService.GetWeatherAsync(coordinates.Lat, coordinates.Lon); + var forecasts = weather?.Forecast.Forecastday.Select(day => day.ToForecast()).ToList(); - var forecasts = weather.Forecast.Forecastday.Select(day => day.ToForecast()).ToList(); - - return weather.ToContract(coordinates.Name, aurora, forecasts); + return weather?.ToContract(coordinates?.Name, aurora, forecasts) ?? new WeatherInformation(); } } } \ No newline at end of file diff --git a/HomeApi/Integration/WeatherService.cs b/HomeApi/Integration/WeatherService.cs index a7d2e64..04e0e64 100644 --- a/HomeApi/Integration/WeatherService.cs +++ b/HomeApi/Integration/WeatherService.cs @@ -14,9 +14,15 @@ public class WeatherService(IWeatherClient weatherApi, IOptions GetWeatherAsync(string lat, string lon) + public Task GetWeatherAsync(string? lat, string? lon) { var location = $"{lat},{lon}"; + + if (string.IsNullOrEmpty(lat) || string.IsNullOrEmpty(lon)) + { + location = _apiConfig.DefaultCity; + } + return weatherApi.GetForecastAsync(_apiConfig.Keys.Weather, location); } } \ No newline at end of file diff --git a/HomeApi/Models/ImageGeneration.cs b/HomeApi/Models/ImageGeneration.cs index e5b1350..09df7f4 100644 --- a/HomeApi/Models/ImageGeneration.cs +++ b/HomeApi/Models/ImageGeneration.cs @@ -2,6 +2,6 @@ namespace HomeApi.Models; public class Image { - public WeatherInformation Weather { get; set; } - public List TimeTable { get; set; } + public WeatherInformation? Weather { get; set; } + public List? TimeTable { get; set; } } \ No newline at end of file diff --git a/HomeApi/Models/Response/YrWeatherForecastResponse.cs b/HomeApi/Models/Response/YrWeatherForecastResponse.cs new file mode 100644 index 0000000..14e6480 --- /dev/null +++ b/HomeApi/Models/Response/YrWeatherForecastResponse.cs @@ -0,0 +1,153 @@ +using System.Text.Json.Serialization; + +namespace HomeApi.Models.Response; + +public class Data +{ + [JsonPropertyName("instant")] + public Instant Instant { get; set; } + + [JsonPropertyName("next_12_hours")] + public Next12Hours Next12Hours { get; set; } + + [JsonPropertyName("next_1_hours")] + public Next1Hours Next1Hours { get; set; } + + [JsonPropertyName("next_6_hours")] + public Next6Hours Next6Hours { get; set; } +} + +public class Details +{ + [JsonPropertyName("air_pressure_at_sea_level")] + public double AirPressureAtSeaLevel { get; set; } + + [JsonPropertyName("air_temperature")] + public double AirTemperature { get; set; } + + [JsonPropertyName("cloud_area_fraction")] + public double CloudAreaFraction { get; set; } + + [JsonPropertyName("relative_humidity")] + public double RelativeHumidity { get; set; } + + [JsonPropertyName("wind_from_direction")] + public double WindFromDirection { get; set; } + + [JsonPropertyName("wind_speed")] + public double WindSpeed { get; set; } + + [JsonPropertyName("precipitation_amount")] + public double PrecipitationAmount { get; set; } +} + +public class Geometry +{ + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("coordinates")] + public List Coordinates { get; set; } +} + +public class Instant +{ + [JsonPropertyName("details")] + public Details Details { get; set; } +} + +public class Meta +{ + [JsonPropertyName("updated_at")] + public DateTime UpdatedAt { get; set; } + + [JsonPropertyName("units")] + public Units Units { get; set; } +} + +public class Next12Hours +{ + [JsonPropertyName("summary")] + public Summary Summary { get; set; } + + [JsonPropertyName("details")] + public Details Details { get; set; } +} + +public class Next1Hours +{ + [JsonPropertyName("summary")] + public Summary Summary { get; set; } + + [JsonPropertyName("details")] + public Details Details { get; set; } +} + +public class Next6Hours +{ + [JsonPropertyName("summary")] + public Summary Summary { get; set; } + + [JsonPropertyName("details")] + public Details Details { get; set; } +} + +public class Properties +{ + [JsonPropertyName("meta")] + public Meta Meta { get; set; } + + [JsonPropertyName("timeseries")] + public List Timeseries { get; set; } +} + +public class YrWeatherForecastResponse +{ + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("geometry")] + public Geometry Geometry { get; set; } + + [JsonPropertyName("properties")] + public Properties Properties { get; set; } +} + +public class Summary +{ + [JsonPropertyName("symbol_code")] + public string SymbolCode { get; set; } +} + +public class Timeseries +{ + [JsonPropertyName("time")] + public DateTime Time { get; set; } + + [JsonPropertyName("data")] + public Data Data { get; set; } +} + +public class Units +{ + [JsonPropertyName("air_pressure_at_sea_level")] + public string AirPressureAtSeaLevel { get; set; } + + [JsonPropertyName("air_temperature")] + public string AirTemperature { get; set; } + + [JsonPropertyName("cloud_area_fraction")] + public string CloudAreaFraction { get; set; } + + [JsonPropertyName("precipitation_amount")] + public string PrecipitationAmount { get; set; } + + [JsonPropertyName("relative_humidity")] + public string RelativeHumidity { get; set; } + + [JsonPropertyName("wind_from_direction")] + public string WindFromDirection { get; set; } + + [JsonPropertyName("wind_speed")] + public string WindSpeed { get; set; } +} \ No newline at end of file diff --git a/HomeApi/Models/WeatherInformation.cs b/HomeApi/Models/WeatherInformation.cs index 3a04298..5084769 100644 --- a/HomeApi/Models/WeatherInformation.cs +++ b/HomeApi/Models/WeatherInformation.cs @@ -1,24 +1,26 @@ +#nullable disable + using HomeApi.Models.Response; namespace HomeApi.Models; public class WeatherInformation { - public string CityName { get; set; } = string.Empty; + public string CityName { get; set; } = "Not defined"; public Current Current { get; set; } = new(); public List Forecast { get; set; } } public class Current { - public string Date { get; set; } - public double Feelslike { get; set; } - public int IsDay { get; set; } + public string Date { get; set; } = string.Empty; + public double Feelslike { get; set; } = 0; + public int IsDay { get; set; } = 1; public double WindPerMeterSecond { get; set; } = 0; public double WindGustPerMeterSecond { get; set; } = 0; public double Temperature { get; set; } = 0; public string LastUpdated { get; set; } = string.Empty; - public int Cloud { get; set; } + public int Cloud { get; set; } = 0; public string WindDirection { get; set; } = string.Empty; public Location WeatherDataLocation { get; set; } = new(); public AirQuality AirQuality { get; set; } @@ -28,72 +30,72 @@ public class Current public class Location { - public string Name { get; set; } - public string Region { get; set; } - public string Country { get; set; } - public double Lat { get; set; } - public double Lon { get; set; } + public string Name { get; set; } = string.Empty; + public string Region { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + public double Lat { get; set; } = 0; + public double Lon { get; set; } = 0; } public class Probability { - public DateTime Date { get; set; } - public CalculatedProbability Calculated { get; set; } // my location? - public string Colour { get; set; } - public int Value { get; set; } - public Highest HighestProbability { get; set; } + public DateTime Date { get; set; } = DateTime.Now; + public CalculatedProbability Calculated { get; set; } = new(); + public string Colour { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public Highest HighestProbability { get; set; } = new(); } public class Highest { - public DateTime Date { get; set; } - public string Colour { get; set; } - public double Lat { get; set; } - public double Long { get; set; } - public int Value { get; set; } + public DateTime Date { get; set; } = DateTime.Now; + public string Colour { get; set; } = string.Empty; + public double Lat { get; set; } = 0; + public double Long { get; set; } = 0; + public int Value { get; set; } = 0; } public class Forecast { - public string Date { get; set; } - public double MinTempC { get; set; } - public double MaxTempC { get; set; } - public string DayIcon { get; set; } - public WeatherSummary? Day { get; set; } - public WeatherSummary? Night { get; set; } - public Astro Astro { get; set; } - public int IconCode { get; set; } + public string Date { get; set; } = string.Empty; + public double MinTempC { get; set; } = 0; + public double MaxTempC { get; set; } = 0; + public string DayIcon { get; set; } = string.Empty; + public WeatherSummary? Day { get; set; } = null; + public WeatherSummary? Night { get; set; } = null; + public Astro Astro { get; set; } = new(); + public int IconCode { get; set; } = 0; - public int ChanceOfRain { get; set; } + public int ChanceOfRain { get; set; } = 0; } public class Astro { - public string Sunrise { get; set; } - public string Sunset { get; set; } - public string Moonrise { get; set; } - public string Moonset { get; set; } - public string Moon_Phase { get; set; } + public string Sunrise { get; set; } = string.Empty; + public string Sunset { get; set; } = string.Empty; + public string Moonrise { get; set; } = string.Empty; + public string Moonset { get; set; } = string.Empty; + public string Moon_Phase { get; set; } = string.Empty; public double? Moon_Illumination { get; set; } } public class AirQuality { - public double Co { get; set; } - public double No2 { get; set; } - public double O3 { get; set; } - public double So2 { get; set; } - public double Pm2_5 { get; set; } - public double Pm10 { get; set; } - public int Us_Epa_Index { get; set; } - public int Gb_Defra_Index { get; set; } + public double Co { get; set; } = 0; + public double No2 { get; set; } = 0; + public double O3 { get; set; } = 0; + public double So2 { get; set; } = 0; + public double Pm2_5 { get; set; } = 0; + public double Pm10 { get; set; } = 0; + public int Us_Epa_Index { get; set; } = 0; + public int Gb_Defra_Index { get; set; } = 0; } public class WeatherSummary { - public string ConditionText { get; set; } - public string ConditionIcon { get; set; } - public double AvgTempC { get; set; } - public double AvgFeelslikeC { get; set; } - public int TotalChanceOfRain { get; set; } - public int TotalChanceOfSnow { get; set; } + public string ConditionText { get; set; } = string.Empty; + public string ConditionIcon { get; set; } = string.Empty; + public double AvgTempC { get; set; } = 0; + public double AvgFeelslikeC { get; set; } = 0; + public int TotalChanceOfRain { get; set; } = 0; + public int TotalChanceOfSnow { get; set; } = 0; } \ No newline at end of file diff --git a/HomeApi/wwwroot/index.cshtml b/HomeApi/wwwroot/index.cshtml index 67a163a..55056c8 100644 --- a/HomeApi/wwwroot/index.cshtml +++ b/HomeApi/wwwroot/index.cshtml @@ -43,8 +43,11 @@ return "fa-question-circle"; } - private string GetAirQualityStatus(AirQuality data) + private string GetAirQualityStatus(AirQuality? data) { + if(data == null) + return "No Data"; + var highestAqi = Math.Max(data.Us_Epa_Index, data.Gb_Defra_Index); return highestAqi switch @@ -487,12 +490,14 @@
- @Model.Weather.CityName + @(Model.Weather?.CityName ?? "Failed to fetch data")
-
@Model.Weather.Current.Temperature°C
-
Feels like @Model.Weather.Current.Feelslike°C
+
+ @(Model.Weather?.Current.Temperature != null ? $"{Model.Weather.Current.Temperature}°C" : "N/A") +
+
Feels like @Model.Weather?.Current.Feelslike°C
@@ -501,7 +506,7 @@ Clouds
- @Model.Weather.Current.Cloud% + @Model?.Weather?.Current.Cloud%
@@ -509,7 +514,7 @@ Wind
- @Model.Weather.Current.WindPerMeterSecond.ToString("0.##") m/s @Model.Weather.Current.WindDirection + @Model?.Weather?.Current.WindPerMeterSecond.ToString("0.##") m/s @Model?.Weather?.Current.WindDirection
@@ -517,7 +522,7 @@ Gusts
- @Model.Weather.Current.WindGustPerMeterSecond.ToString("0.##") m/s + @Model?.Weather?.Current.WindGustPerMeterSecond.ToString("0.##") m/s
@@ -525,21 +530,21 @@ Aurora
- @Model.Weather.Current.AuroraProbability.Value% + @Model?.Weather?.Current.AuroraProbability.Value%
- Air Quality: @GetAirQualityStatus(Model.Weather.Current.AirQuality) + Air Quality: @GetAirQualityStatus(Model?.Weather?.Current.AirQuality)
-

@Model.Weather.Forecast.Count-Day Forecast

+

@Model?.Weather?.Forecast.Count-Day Forecast

- @foreach (var day in Model.Weather.Forecast) + @foreach (var day in Model?.Weather?.Forecast ?? Enumerable.Empty()) {
@GetDayStatus(day.Date)
@@ -559,7 +564,7 @@ Sunrise
- @Model.Weather.Forecast[0].Astro.Sunrise + @Model?.Weather?.Forecast[0].Astro.Sunrise
@@ -567,7 +572,7 @@ Sunset
- @Model.Weather.Forecast[0].Astro.Sunset + @Model?.Weather?.Forecast[0].Astro.Sunset
@@ -575,7 +580,7 @@ Moonrise
- @Model.Weather.Forecast[0].Astro.Moonrise + @Model?.Weather?.Forecast[0].Astro.Moonrise
@@ -583,12 +588,12 @@ Moonset
- @Model.Weather.Forecast[0].Astro.Moonset + @Model?.Weather?.Forecast[0].Astro.Moonset
Moon phase - @Model.Weather.Forecast[0].Astro.Moon_Illumination% + @Model?.Weather?.Forecast[0].Astro.Moon_Illumination%
@@ -598,7 +603,7 @@

Upcoming Departures

- @foreach (var transport in Model.TimeTable.Take(5)) + @foreach (var transport in Model?.TimeTable?.Take(5) ?? Enumerable.Empty()) { var departureTime = DateTime.Parse(transport.DepartureTime);