fix: crash on unavailable source api #4
@@ -215,6 +215,18 @@ int jpegDrawCallback(JPEGDRAW *pDraw) {
|
|||||||
return 1; // Continue decoding
|
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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
Serial.println("E-Ink Display Initialization");
|
Serial.println("E-Ink Display Initialization");
|
||||||
@@ -513,27 +525,36 @@ void fetchAndDisplayImage() {
|
|||||||
Serial.println("Image displayed successfully.");
|
Serial.println("Image displayed successfully.");
|
||||||
} else {
|
} else {
|
||||||
Serial.println("Failed to open JPEG image");
|
Serial.println("Failed to open JPEG image");
|
||||||
|
clearDisplay();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Serial.println("Failed to read entire image.");
|
Serial.println("Failed to read entire image.");
|
||||||
|
clearDisplay();
|
||||||
}
|
}
|
||||||
free(buffer);
|
free(buffer);
|
||||||
} else {
|
} else {
|
||||||
Serial.println("Failed to allocate buffer!");
|
Serial.println("Failed to allocate buffer!");
|
||||||
|
clearDisplay();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Serial.println("Content length unknown or invalid.");
|
Serial.println("Content length unknown or invalid.");
|
||||||
|
clearDisplay();
|
||||||
}
|
}
|
||||||
} else if (httpCode == HTTPC_ERROR_CONNECTION_REFUSED) {
|
} else if (httpCode == HTTPC_ERROR_CONNECTION_REFUSED) {
|
||||||
Serial.println("Connection refused - server may be down");
|
Serial.println("Connection refused - server may be down");
|
||||||
|
clearDisplay();
|
||||||
} else if (httpCode == HTTPC_ERROR_CONNECTION_LOST) {
|
} else if (httpCode == HTTPC_ERROR_CONNECTION_LOST) {
|
||||||
Serial.println("Connection lost during request");
|
Serial.println("Connection lost during request");
|
||||||
|
clearDisplay();
|
||||||
} else if (httpCode == HTTPC_ERROR_NO_HTTP_SERVER) {
|
} else if (httpCode == HTTPC_ERROR_NO_HTTP_SERVER) {
|
||||||
Serial.println("No HTTP server found");
|
Serial.println("No HTTP server found");
|
||||||
|
clearDisplay();
|
||||||
} else if (httpCode == HTTPC_ERROR_NOT_CONNECTED) {
|
} else if (httpCode == HTTPC_ERROR_NOT_CONNECTED) {
|
||||||
Serial.println("Not connected to server");
|
Serial.println("Not connected to server");
|
||||||
|
clearDisplay();
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("HTTP GET failed, error: %d\n", httpCode);
|
Serial.printf("HTTP GET failed, error: %d\n", httpCode);
|
||||||
|
clearDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
http.end();
|
http.end();
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ public static class ContractExtensions
|
|||||||
/// A <see cref="WeatherInformation"/> object populated with current weather, aurora probability, and forecast information.
|
/// A <see cref="WeatherInformation"/> object populated with current weather, aurora probability, and forecast information.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public static WeatherInformation ToContract(
|
public static WeatherInformation ToContract(
|
||||||
this WeatherData weather,
|
this WeatherData? weather,
|
||||||
string locationName,
|
string? locationName,
|
||||||
AuroraForecastApiResponse? auroraForecast,
|
AuroraForecastApiResponse? auroraForecast,
|
||||||
List<Models.Forecast> forecasts)
|
List<Models.Forecast>? forecasts)
|
||||||
{
|
{
|
||||||
return new WeatherInformation
|
return new WeatherInformation
|
||||||
{
|
{
|
||||||
@@ -58,23 +58,23 @@ public static class ContractExtensions
|
|||||||
},
|
},
|
||||||
AuroraProbability = new Probability
|
AuroraProbability = new Probability
|
||||||
{
|
{
|
||||||
Date = auroraForecast.Date,
|
Date = auroraForecast?.Date ?? DateTime.Now,
|
||||||
Calculated = new CalculatedProbability
|
Calculated = new CalculatedProbability
|
||||||
{
|
{
|
||||||
Value = auroraForecast.Probability.Calculated.Value,
|
Value = auroraForecast?.Probability.Calculated.Value ?? 0,
|
||||||
Colour = auroraForecast.Probability.Calculated.Colour,
|
Colour = auroraForecast?.Probability.Calculated.Colour ?? string.Empty,
|
||||||
Lat = auroraForecast.Probability.Calculated.Lat,
|
Lat = auroraForecast?.Probability.Calculated.Lat ?? 0,
|
||||||
Long = auroraForecast.Probability.Calculated.Long
|
Long = auroraForecast?.Probability.Calculated.Long ?? 0
|
||||||
},
|
},
|
||||||
Colour = auroraForecast.Probability.Colour,
|
Colour = auroraForecast?.Probability.Colour ?? string.Empty,
|
||||||
Value = auroraForecast.Probability.Value,
|
Value = auroraForecast?.Probability.Value.ToString() ?? "No data",
|
||||||
HighestProbability = new Highest
|
HighestProbability = new Highest
|
||||||
{
|
{
|
||||||
Colour = auroraForecast.Probability.Highest.Colour,
|
Colour = auroraForecast?.Probability.Highest.Colour ?? string.Empty,
|
||||||
Lat = auroraForecast.Probability.Highest.Lat,
|
Lat = auroraForecast?.Probability.Highest.Lat ?? 0,
|
||||||
Long = auroraForecast.Probability.Highest.Long,
|
Long = auroraForecast?.Probability.Highest.Long ?? 0,
|
||||||
Value = auroraForecast.Probability.Highest.Value,
|
Value = auroraForecast?.Probability.Highest.Value ?? 0,
|
||||||
Date = auroraForecast.Probability.Highest.Date
|
Date = auroraForecast?.Probability.Highest.Date ?? DateTime.Now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
22
HomeApi/Extensions/MediatorExtensions.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace HomeApi.Extensions;
|
||||||
|
|
||||||
|
public static class MediatorExtensions
|
||||||
|
{
|
||||||
|
public static async Task<T?> TrySendAsync<T>(
|
||||||
|
this IMediator mediator,
|
||||||
|
IRequest<T> request,
|
||||||
|
CancellationToken cancellationToken) where T : class
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await mediator.Send(request, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { throw; }
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
|
|||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
HomeApi/Extensions/ServiceCallExtensions.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace HomeApi.Extensions;
|
||||||
|
|
||||||
|
public static class ServiceCallExtensions
|
||||||
|
{
|
||||||
|
public static async Task<TResult?> TryCallAsync<TService, TResult>(
|
||||||
|
this TService service,
|
||||||
|
Func<TService, Task<TResult>> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Dynamic;
|
using System.Dynamic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using HomeApi.Extensions;
|
||||||
using HomeApi.Models.Configuration;
|
using HomeApi.Models.Configuration;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -22,8 +23,8 @@ public static class ImageGeneration
|
|||||||
|
|
||||||
public async Task<Stream> Handle(Command request, CancellationToken cancellationToken)
|
public async Task<Stream> Handle(Command request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var weather = await mediator.Send(new Weather.Command(), cancellationToken);
|
var weather = await mediator.TrySendAsync(new Weather.Command(), cancellationToken);
|
||||||
var departureBoard = await mediator.Send(new DepartureBoard.Command(), cancellationToken);
|
var departureBoard = await mediator.TrySendAsync(new DepartureBoard.Command(), cancellationToken);
|
||||||
|
|
||||||
var model = new Models.Image
|
var model = new Models.Image
|
||||||
{
|
{
|
||||||
@@ -31,8 +32,6 @@ public static class ImageGeneration
|
|||||||
TimeTable = departureBoard
|
TimeTable = departureBoard
|
||||||
};
|
};
|
||||||
|
|
||||||
if(weather is null)
|
|
||||||
throw new Exception("Weather data not found");
|
|
||||||
|
|
||||||
var engine = new RazorLightEngineBuilder()
|
var engine = new RazorLightEngineBuilder()
|
||||||
.SetOperatingAssembly(Assembly.GetExecutingAssembly())
|
.SetOperatingAssembly(Assembly.GetExecutingAssembly())
|
||||||
@@ -59,7 +58,7 @@ public static class ImageGeneration
|
|||||||
{
|
{
|
||||||
var browserFetcher = new BrowserFetcher();
|
var browserFetcher = new BrowserFetcher();
|
||||||
await browserFetcher.DownloadAsync();
|
await browserFetcher.DownloadAsync();
|
||||||
var browser = await Puppeteer.LaunchAsync(new LaunchOptions
|
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
|
||||||
{
|
{
|
||||||
Headless = true,
|
Headless = true,
|
||||||
Args = ["--disable-gpu"]
|
Args = ["--disable-gpu"]
|
||||||
|
|||||||
@@ -35,16 +35,27 @@ public static class Weather
|
|||||||
|
|
||||||
public async Task<WeatherInformation> Handle(Command request, CancellationToken cancellationToken)
|
public async Task<WeatherInformation> Handle(Command request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var coordinates = await _geocodingService.GetCoordinatesAsync(_apiConfiguration.DefaultCity);
|
var coordinates = await _geocodingService.TryCallAsync(service =>
|
||||||
if (coordinates is null)
|
service.GetCoordinatesAsync(_apiConfiguration.DefaultCity),
|
||||||
throw new Exception("Coordinates not found");
|
_logger,
|
||||||
|
"Failed to get coordinates"
|
||||||
|
);
|
||||||
|
|
||||||
var aurora = await _auroraService.GetAuroraForecastAsync(coordinates.Lat, coordinates.Lon);
|
var aurora = await _auroraService.TryCallAsync(service =>
|
||||||
var weather = await _weatherService.GetWeatherAsync(coordinates.Lat, coordinates.Lon);
|
service.GetAuroraForecastAsync(coordinates?.Lat ?? "0.00", coordinates?.Lon ?? "0.00"),
|
||||||
|
_logger,
|
||||||
|
"Failed to get aurora forecast"
|
||||||
|
);
|
||||||
|
|
||||||
var forecasts = weather.Forecast.Forecastday.Select(day => day.ToForecast()).ToList();
|
var weather = await _weatherService.TryCallAsync(service =>
|
||||||
|
service.GetWeatherAsync(coordinates?.Lat ?? string.Empty, coordinates?.Lon ?? string.Empty),
|
||||||
|
_logger,
|
||||||
|
"Failed to get weather data"
|
||||||
|
);
|
||||||
|
|
||||||
return weather.ToContract(coordinates.Name, aurora, forecasts);
|
var forecasts = weather?.Forecast.Forecastday.Select(day => day.ToForecast()).ToList();
|
||||||
|
|
||||||
|
return weather?.ToContract(coordinates?.Name, aurora, forecasts) ?? new WeatherInformation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,9 +14,15 @@ public class WeatherService(IWeatherClient weatherApi, IOptions<ApiConfiguration
|
|||||||
{
|
{
|
||||||
private readonly ApiConfiguration _apiConfig = options.Value;
|
private readonly ApiConfiguration _apiConfig = options.Value;
|
||||||
|
|
||||||
public Task<WeatherData> GetWeatherAsync(string lat, string lon)
|
public Task<WeatherData> GetWeatherAsync(string? lat, string? lon)
|
||||||
{
|
{
|
||||||
var location = $"{lat},{lon}";
|
var location = $"{lat},{lon}";
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(lat) || string.IsNullOrEmpty(lon))
|
||||||
|
{
|
||||||
|
location = _apiConfig.DefaultCity;
|
||||||
|
}
|
||||||
|
|
||||||
return weatherApi.GetForecastAsync(_apiConfig.Keys.Weather, location);
|
return weatherApi.GetForecastAsync(_apiConfig.Keys.Weather, location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,6 @@ namespace HomeApi.Models;
|
|||||||
|
|
||||||
public class Image
|
public class Image
|
||||||
{
|
{
|
||||||
public WeatherInformation Weather { get; set; }
|
public WeatherInformation? Weather { get; set; }
|
||||||
public List<TimeTable> TimeTable { get; set; }
|
public List<TimeTable>? TimeTable { get; set; }
|
||||||
}
|
}
|
||||||
153
HomeApi/Models/Response/YrWeatherForecastResponse.cs
Normal file
@@ -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<double> 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> 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; }
|
||||||
|
}
|
||||||
@@ -1,24 +1,26 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
using HomeApi.Models.Response;
|
using HomeApi.Models.Response;
|
||||||
|
|
||||||
namespace HomeApi.Models;
|
namespace HomeApi.Models;
|
||||||
|
|
||||||
public class WeatherInformation
|
public class WeatherInformation
|
||||||
{
|
{
|
||||||
public string CityName { get; set; } = string.Empty;
|
public string CityName { get; set; } = "Not defined";
|
||||||
public Current Current { get; set; } = new();
|
public Current Current { get; set; } = new();
|
||||||
public List<Forecast> Forecast { get; set; }
|
public List<Forecast> Forecast { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Current
|
public class Current
|
||||||
{
|
{
|
||||||
public string Date { get; set; }
|
public string Date { get; set; } = string.Empty;
|
||||||
public double Feelslike { get; set; }
|
public double Feelslike { get; set; } = 0;
|
||||||
public int IsDay { get; set; }
|
public int IsDay { get; set; } = 1;
|
||||||
public double WindPerMeterSecond { get; set; } = 0;
|
public double WindPerMeterSecond { get; set; } = 0;
|
||||||
public double WindGustPerMeterSecond { get; set; } = 0;
|
public double WindGustPerMeterSecond { get; set; } = 0;
|
||||||
public double Temperature { get; set; } = 0;
|
public double Temperature { get; set; } = 0;
|
||||||
public string LastUpdated { get; set; } = string.Empty;
|
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 string WindDirection { get; set; } = string.Empty;
|
||||||
public Location WeatherDataLocation { get; set; } = new();
|
public Location WeatherDataLocation { get; set; } = new();
|
||||||
public AirQuality AirQuality { get; set; }
|
public AirQuality AirQuality { get; set; }
|
||||||
@@ -28,72 +30,72 @@ public class Current
|
|||||||
|
|
||||||
public class Location
|
public class Location
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; } = string.Empty;
|
||||||
public string Region { get; set; }
|
public string Region { get; set; } = string.Empty;
|
||||||
public string Country { get; set; }
|
public string Country { get; set; } = string.Empty;
|
||||||
public double Lat { get; set; }
|
public double Lat { get; set; } = 0;
|
||||||
public double Lon { get; set; }
|
public double Lon { get; set; } = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Probability
|
public class Probability
|
||||||
{
|
{
|
||||||
public DateTime Date { get; set; }
|
public DateTime Date { get; set; } = DateTime.Now;
|
||||||
public CalculatedProbability Calculated { get; set; } // my location?
|
public CalculatedProbability Calculated { get; set; } = new();
|
||||||
public string Colour { get; set; }
|
public string Colour { get; set; } = string.Empty;
|
||||||
public int Value { get; set; }
|
public string Value { get; set; } = string.Empty;
|
||||||
public Highest HighestProbability { get; set; }
|
public Highest HighestProbability { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Highest
|
public class Highest
|
||||||
{
|
{
|
||||||
public DateTime Date { get; set; }
|
public DateTime Date { get; set; } = DateTime.Now;
|
||||||
public string Colour { get; set; }
|
public string Colour { get; set; } = string.Empty;
|
||||||
public double Lat { get; set; }
|
public double Lat { get; set; } = 0;
|
||||||
public double Long { get; set; }
|
public double Long { get; set; } = 0;
|
||||||
public int Value { get; set; }
|
public int Value { get; set; } = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Forecast
|
public class Forecast
|
||||||
{
|
{
|
||||||
public string Date { get; set; }
|
public string Date { get; set; } = string.Empty;
|
||||||
public double MinTempC { get; set; }
|
public double MinTempC { get; set; } = 0;
|
||||||
public double MaxTempC { get; set; }
|
public double MaxTempC { get; set; } = 0;
|
||||||
public string DayIcon { get; set; }
|
public string DayIcon { get; set; } = string.Empty;
|
||||||
public WeatherSummary? Day { get; set; }
|
public WeatherSummary? Day { get; set; } = null;
|
||||||
public WeatherSummary? Night { get; set; }
|
public WeatherSummary? Night { get; set; } = null;
|
||||||
public Astro Astro { get; set; }
|
public Astro Astro { get; set; } = new();
|
||||||
public int IconCode { get; set; }
|
public int IconCode { get; set; } = 0;
|
||||||
|
|
||||||
public int ChanceOfRain { get; set; }
|
public int ChanceOfRain { get; set; } = 0;
|
||||||
}
|
}
|
||||||
public class Astro
|
public class Astro
|
||||||
{
|
{
|
||||||
public string Sunrise { get; set; }
|
public string Sunrise { get; set; } = string.Empty;
|
||||||
public string Sunset { get; set; }
|
public string Sunset { get; set; } = string.Empty;
|
||||||
public string Moonrise { get; set; }
|
public string Moonrise { get; set; } = string.Empty;
|
||||||
public string Moonset { get; set; }
|
public string Moonset { get; set; } = string.Empty;
|
||||||
public string Moon_Phase { get; set; }
|
public string Moon_Phase { get; set; } = string.Empty;
|
||||||
public double? Moon_Illumination { get; set; }
|
public double? Moon_Illumination { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AirQuality
|
public class AirQuality
|
||||||
{
|
{
|
||||||
public double Co { get; set; }
|
public double Co { get; set; } = 0;
|
||||||
public double No2 { get; set; }
|
public double No2 { get; set; } = 0;
|
||||||
public double O3 { get; set; }
|
public double O3 { get; set; } = 0;
|
||||||
public double So2 { get; set; }
|
public double So2 { get; set; } = 0;
|
||||||
public double Pm2_5 { get; set; }
|
public double Pm2_5 { get; set; } = 0;
|
||||||
|
The Value property type changed from int to string, but line 531 in the view still uses .Value% which expects a numeric type. This will cause a runtime error when trying to append % to a string. The Value property type changed from int to string, but line 531 in the view still uses .Value% which expects a numeric type. This will cause a runtime error when trying to append % to a string.
```suggestion
public int Value { get; set; } = 0;
```
|
|||||||
public double Pm10 { get; set; }
|
public double Pm10 { get; set; } = 0;
|
||||||
public int Us_Epa_Index { get; set; }
|
public int Us_Epa_Index { get; set; } = 0;
|
||||||
public int Gb_Defra_Index { get; set; }
|
public int Gb_Defra_Index { get; set; } = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WeatherSummary
|
public class WeatherSummary
|
||||||
{
|
{
|
||||||
public string ConditionText { get; set; }
|
public string ConditionText { get; set; } = string.Empty;
|
||||||
public string ConditionIcon { get; set; }
|
public string ConditionIcon { get; set; } = string.Empty;
|
||||||
public double AvgTempC { get; set; }
|
public double AvgTempC { get; set; } = 0;
|
||||||
public double AvgFeelslikeC { get; set; }
|
public double AvgFeelslikeC { get; set; } = 0;
|
||||||
public int TotalChanceOfRain { get; set; }
|
public int TotalChanceOfRain { get; set; } = 0;
|
||||||
public int TotalChanceOfSnow { get; set; }
|
public int TotalChanceOfSnow { get; set; } = 0;
|
||||||
}
|
}
|
||||||
@@ -43,8 +43,11 @@
|
|||||||
return "fa-question-circle";
|
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);
|
var highestAqi = Math.Max(data.Us_Epa_Index, data.Gb_Defra_Index);
|
||||||
|
|
||||||
return highestAqi switch
|
return highestAqi switch
|
||||||
@@ -487,12 +490,14 @@
|
|||||||
<div class="current-weather">
|
<div class="current-weather">
|
||||||
|
If Model.Weather is null, this will display '°C' without any temperature value. Consider using null-conditional operator for the entire expression: @(Model.Weather?.Current.Temperature)°C or provide a fallback message. If Model.Weather is null, this will display '°C' without any temperature value. Consider using null-conditional operator for the entire expression: @(Model.Weather?.Current.Temperature)°C or provide a fallback message.
```suggestion
<div class="temp-main">
@(Model.Weather?.Current.Temperature != null ? $"{Model.Weather.Current.Temperature}°C" : "N/A")
</div>
```
|
|||||||
<div class="location">
|
<div class="location">
|
||||||
<i class="fas fa-map-marker-alt location-icon"></i>
|
<i class="fas fa-map-marker-alt location-icon"></i>
|
||||||
<span class="location-text">@Model.Weather.CityName</span>
|
<span class="location-text">@(Model.Weather?.CityName ?? "Failed to fetch data")</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="current-temp">
|
<div class="current-temp">
|
||||||
<div class="temp-main">@Model.Weather.Current.Temperature°C</div>
|
<div class="temp-main">
|
||||||
<div class="feels-like">Feels like @Model.Weather.Current.Feelslike°C</div>
|
@(Model.Weather?.Current.Temperature != null ? $"{Model.Weather.Current.Temperature}°C" : "N/A")
|
||||||
|
</div>
|
||||||
|
<div class="feels-like">Feels like @Model.Weather?.Current.Feelslike°C</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="weather-details">
|
<div class="weather-details">
|
||||||
@@ -501,7 +506,7 @@
|
|||||||
<i class="fas fa-cloud cloud-icon"></i>
|
<i class="fas fa-cloud cloud-icon"></i>
|
||||||
<span>Clouds</span>
|
<span>Clouds</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="detail-value">@Model.Weather.Current.Cloud%</span>
|
<span class="detail-value">@Model?.Weather?.Current.Cloud%</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
@@ -509,7 +514,7 @@
|
|||||||
<i class="fas fa-wind wind-icon"></i>
|
<i class="fas fa-wind wind-icon"></i>
|
||||||
<span>Wind</span>
|
<span>Wind</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="detail-value">@Model.Weather.Current.WindPerMeterSecond.ToString("0.##") m/s @Model.Weather.Current.WindDirection</span>
|
<span class="detail-value">@Model?.Weather?.Current.WindPerMeterSecond.ToString("0.##") m/s @Model?.Weather?.Current.WindDirection</span>
|
||||||
|
Similar to the temperature issue, if Model.Weather is null, this will display 'Feels like °C' without any temperature value. Consider using null-conditional operator for the entire expression. Similar to the temperature issue, if Model.Weather is null, this will display 'Feels like °C' without any temperature value. Consider using null-conditional operator for the entire expression.
```suggestion
<div class="feels-like">
Feels like @(Model.Weather?.Current?.Feelslike != null ? Model.Weather.Current.Feelslike + "°C" : "N/A")
</div>
```
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
@@ -517,7 +522,7 @@
|
|||||||
<i class="fas fa-chart-line activity-icon"></i>
|
<i class="fas fa-chart-line activity-icon"></i>
|
||||||
<span>Gusts</span>
|
<span>Gusts</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="detail-value">@Model.Weather.Current.WindGustPerMeterSecond.ToString("0.##") m/s</span>
|
<span class="detail-value">@Model?.Weather?.Current.WindGustPerMeterSecond.ToString("0.##") m/s</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
@@ -525,21 +530,21 @@
|
|||||||
<i class="fas fa-star aurora-icon"></i>
|
<i class="fas fa-star aurora-icon"></i>
|
||||||
<span>Aurora</span>
|
<span>Aurora</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="detail-value">@Model.Weather.Current.AuroraProbability.Value%</span>
|
<span class="detail-value">@Model?.Weather?.Current.AuroraProbability.Value%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="air-quality-badge">
|
<div class="air-quality-badge">
|
||||||
Air Quality: @GetAirQualityStatus(Model.Weather.Current.AirQuality)
|
Air Quality: @GetAirQualityStatus(Model?.Weather?.Current.AirQuality)
|
||||||
</div>
|
</div>
|
||||||
|
If any part of the chain is null, this will display just '%' without the cloud value. Consider wrapping the entire expression in parentheses with a fallback: @(Model?.Weather?.Current.Cloud ?? "--")% If any part of the chain is null, this will display just '%' without the cloud value. Consider wrapping the entire expression in parentheses with a fallback: @(Model?.Weather?.Current.Cloud ?? "--")%
```suggestion
<span class="detail-value">@((Model?.Weather?.Current.Cloud ?? "--")%)</span>
```
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Middle Column - Forecast -->
|
<!-- Middle Column - Forecast -->
|
||||||
<div class="forecast-section">
|
<div class="forecast-section">
|
||||||
<h3 class="section-title">@Model.Weather.Forecast.Count-Day Forecast</h3>
|
<h3 class="section-title">@Model?.Weather?.Forecast.Count-Day Forecast</h3>
|
||||||
|
|
||||||
<div class="forecast-grid">
|
<div class="forecast-grid">
|
||||||
@foreach (var day in Model.Weather.Forecast)
|
@foreach (var day in Model?.Weather?.Forecast ?? Enumerable.Empty<Forecast>())
|
||||||
{
|
{
|
||||||
<div class="forecast-card">
|
<div class="forecast-card">
|
||||||
<div class="forecast-date">@GetDayStatus(day.Date)</div>
|
<div class="forecast-date">@GetDayStatus(day.Date)</div>
|
||||||
@@ -559,7 +564,7 @@
|
|||||||
<i class="fas fa-sun sunrise-icon"></i>
|
<i class="fas fa-sun sunrise-icon"></i>
|
||||||
<span>Sunrise</span>
|
<span>Sunrise</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="sun-moon-value">@Model.Weather.Forecast[0].Astro.Sunrise</span>
|
<span class="sun-moon-value">@Model?.Weather?.Forecast[0].Astro.Sunrise</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sun-moon-item">
|
<div class="sun-moon-item">
|
||||||
@@ -567,7 +572,7 @@
|
|||||||
<i class="fas fa-sun sunset-icon"></i>
|
<i class="fas fa-sun sunset-icon"></i>
|
||||||
<span>Sunset</span>
|
<span>Sunset</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="sun-moon-value">@Model.Weather.Forecast[0].Astro.Sunset</span>
|
<span class="sun-moon-value">@Model?.Weather?.Forecast[0].Astro.Sunset</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sun-moon-item">
|
<div class="sun-moon-item">
|
||||||
@@ -575,7 +580,7 @@
|
|||||||
<i class="fas fa-moon moon-icon"></i>
|
<i class="fas fa-moon moon-icon"></i>
|
||||||
<span>Moonrise</span>
|
<span>Moonrise</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="sun-moon-value">@Model.Weather.Forecast[0].Astro.Moonrise</span>
|
<span class="sun-moon-value">@Model?.Weather?.Forecast[0].Astro.Moonrise</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sun-moon-item">
|
<div class="sun-moon-item">
|
||||||
@@ -583,12 +588,12 @@
|
|||||||
<i class="fas fa-moon moon-icon"></i>
|
<i class="fas fa-moon moon-icon"></i>
|
||||||
<span>Moonset</span>
|
<span>Moonset</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="sun-moon-value">@Model.Weather.Forecast[0].Astro.Moonset</span>
|
<span class="sun-moon-value">@Model?.Weather?.Forecast[0].Astro.Moonset</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sun-moon-item">
|
<div class="sun-moon-item">
|
||||||
<span class="moon-phase-label">Moon phase</span>
|
<span class="moon-phase-label">Moon phase</span>
|
||||||
<span class="sun-moon-value">@Model.Weather.Forecast[0].Astro.Moon_Illumination%</span>
|
<span class="sun-moon-value">@Model?.Weather?.Forecast[0].Astro.Moon_Illumination%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -598,7 +603,7 @@
|
|||||||
<h3 class="section-title">Upcoming Departures</h3>
|
<h3 class="section-title">Upcoming Departures</h3>
|
||||||
|
|
||||||
<div class="transport-list">
|
<div class="transport-list">
|
||||||
@foreach (var transport in Model.TimeTable.Take(5))
|
@foreach (var transport in Model?.TimeTable?.Take(5) ?? Enumerable.Empty<TimeTable>())
|
||||||
{
|
{
|
||||||
|
|
||||||
var departureTime = DateTime.Parse(transport.DepartureTime);
|
var departureTime = DateTime.Parse(transport.DepartureTime);
|
||||||
|
|||||||
Using a bare catch clause without specifying exception type is generally discouraged as it catches all exceptions including system exceptions. Consider catching specific exception types or at least log the exception for debugging purposes.