From 2cf6021e6d1ed9369847a7f79ed375fb3e6d28dc Mon Sep 17 00:00:00 2001 From: Sebastian Jensen Date: Tue, 22 Jul 2025 21:18:43 +0200 Subject: [PATCH 1/7] Add air pollution data models Introduces AirPolutionComponents, AirPolutionEntry, AirPolutionIndex, and AirPolutionRoot classes to represent air quality data, pollutant concentrations, AQI, and API response structure. These models enable structured deserialization and handling of air pollution data from the OpenWeatherMap API. --- .../Models/AirPolutionComponents.cs | 59 +++++++++++++++++++ .../Models/AirPolutionEntry.cs | 39 ++++++++++++ .../Models/AirPolutionIndex.cs | 16 +++++ .../Models/AirPolutionRoot.cs | 23 ++++++++ 4 files changed, 137 insertions(+) create mode 100644 src/OpenWeatherMapSharp/Models/AirPolutionComponents.cs create mode 100644 src/OpenWeatherMapSharp/Models/AirPolutionEntry.cs create mode 100644 src/OpenWeatherMapSharp/Models/AirPolutionIndex.cs create mode 100644 src/OpenWeatherMapSharp/Models/AirPolutionRoot.cs diff --git a/src/OpenWeatherMapSharp/Models/AirPolutionComponents.cs b/src/OpenWeatherMapSharp/Models/AirPolutionComponents.cs new file mode 100644 index 0000000..d3ee3e7 --- /dev/null +++ b/src/OpenWeatherMapSharp/Models/AirPolutionComponents.cs @@ -0,0 +1,59 @@ +using System.Text.Json.Serialization; + +namespace OpenWeatherMapSharp.Models +{ + /// + /// Represents the concentration values + /// of various air pollutants (in μg/m³). + /// + public class AirPolutionComponents + { + /// + /// Carbon monoxide concentration. + /// + [JsonPropertyName("co")] + public double CarbonMonoxide { get; set; } + + /// + /// Nitric oxide concentration. + /// + [JsonPropertyName("no")] + public double NitrogenMonoxide { get; set; } + + /// + /// Nitrogen dioxide concentration. + /// + [JsonPropertyName("no2")] + public double NitrogenDioxide { get; set; } + + /// + /// Ozone concentration. + /// + [JsonPropertyName("o3")] + public double Ozone { get; set; } + + /// + /// Sulfur dioxide concentration. + /// + [JsonPropertyName("so2")] + public double SulfurDioxide { get; set; } + + /// + /// Fine particulate matter (PM2.5) concentration. + /// + [JsonPropertyName("pm2_5")] + public double FineParticlesMatter { get; set; } + + /// + /// Coarse particulate matter (PM10) concentration. + /// + [JsonPropertyName("pm10")] + public double CoarseParticulateMatter { get; set; } + + /// + /// Ammonia concentration. + /// + [JsonPropertyName("nh3")] + public double Ammonia { get; set; } + } +} diff --git a/src/OpenWeatherMapSharp/Models/AirPolutionEntry.cs b/src/OpenWeatherMapSharp/Models/AirPolutionEntry.cs new file mode 100644 index 0000000..1d222d4 --- /dev/null +++ b/src/OpenWeatherMapSharp/Models/AirPolutionEntry.cs @@ -0,0 +1,39 @@ +using OpenWeatherMapSharp.Utils; +using System; +using System.Text.Json.Serialization; + +namespace OpenWeatherMapSharp.Models +{ + /// + /// Represents a single entry of air quality data, + /// including AQI, components, and timestamp. + /// + public class AirPolutionEntry + { + /// + /// Main air quality index (AQI). + /// + [JsonPropertyName("main")] + public AirPolutionIndex AQI { get; set; } + + /// + /// Concentrations of individual air components. + /// + [JsonPropertyName("components")] + public AirPolutionComponents Components { get; set; } + + /// + /// Timestamp of the measurement (Unix time in seconds). + /// + [JsonPropertyName("dt")] + public long DateUnix { get; set; } + + /// + /// Timestamp of the weather data as a + /// UTC . + /// + [JsonIgnore] + public DateTime Date + => DateUnix.ToDateTime(); + } +} diff --git a/src/OpenWeatherMapSharp/Models/AirPolutionIndex.cs b/src/OpenWeatherMapSharp/Models/AirPolutionIndex.cs new file mode 100644 index 0000000..c0315f3 --- /dev/null +++ b/src/OpenWeatherMapSharp/Models/AirPolutionIndex.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace OpenWeatherMapSharp.Models +{ + /// + /// Represents the air quality index (AQI) value. + /// + public class AirPolutionIndex + { + /// + /// Air Quality Index (1 = Good, 5 = Very Poor). + /// + [JsonPropertyName("aqi")] + public int Index { get; set; } + } +} diff --git a/src/OpenWeatherMapSharp/Models/AirPolutionRoot.cs b/src/OpenWeatherMapSharp/Models/AirPolutionRoot.cs new file mode 100644 index 0000000..bfc3a5a --- /dev/null +++ b/src/OpenWeatherMapSharp/Models/AirPolutionRoot.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenWeatherMapSharp.Models +{ + /// + /// Root object representing the air quality response from the API. + /// + public class AirPolutionRoot + { + /// + /// Geographic coordinates of the measurement location. + /// + [JsonPropertyName("coord")] + public Coordinates Coordinates { get; set; } + + /// + /// List of air polution measurements. + /// + [JsonPropertyName("list")] + public List Entries { get; set; } + } +} From f19834f8fff3974b54b93f717fa7c290d36bea5a Mon Sep 17 00:00:00 2001 From: Sebastian Jensen Date: Tue, 22 Jul 2025 21:18:53 +0200 Subject: [PATCH 2/7] Return UTC time in ToDateTime extension method Changed the ToDateTime method to return universal time (UTC) instead of local time when converting a Unix timestamp. This ensures consistent time representation across different environments. --- src/OpenWeatherMapSharp/Utils/LongExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenWeatherMapSharp/Utils/LongExtensions.cs b/src/OpenWeatherMapSharp/Utils/LongExtensions.cs index df55bee..d2ef150 100644 --- a/src/OpenWeatherMapSharp/Utils/LongExtensions.cs +++ b/src/OpenWeatherMapSharp/Utils/LongExtensions.cs @@ -17,12 +17,12 @@ internal static class LongExtensions /// The Unix timestamp to convert. /// /// A object representing - /// the local time. + /// the universal time. /// internal static DateTime ToDateTime(this long unixTimeStamp) { DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - return dateTime.AddSeconds(unixTimeStamp).ToLocalTime(); + return dateTime.AddSeconds(unixTimeStamp).ToUniversalTime(); } } } From 1a06c8754411fdafa11e6e90a2d0332265c702ff Mon Sep 17 00:00:00 2001 From: Sebastian Jensen Date: Tue, 22 Jul 2025 21:19:00 +0200 Subject: [PATCH 3/7] Add DateTime to Unix timestamp extension Introduces DateTimeExtensions with a ToUnixTimestamp method for converting DateTime objects to Unix timestamps in seconds. Ensures conversion to UTC before calculation. --- .../Utils/DateTimeExtensions.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/OpenWeatherMapSharp/Utils/DateTimeExtensions.cs diff --git a/src/OpenWeatherMapSharp/Utils/DateTimeExtensions.cs b/src/OpenWeatherMapSharp/Utils/DateTimeExtensions.cs new file mode 100644 index 0000000..fda0aff --- /dev/null +++ b/src/OpenWeatherMapSharp/Utils/DateTimeExtensions.cs @@ -0,0 +1,27 @@ +using System; + +namespace OpenWeatherMapSharp.Utils +{ + /// + /// Provides extension methods for converting + /// a to a Unix timestamps. + /// + internal static class DateTimeExtensions + { + /// + /// Converts a DateTime object to a Unix timestamp (seconds since 1970-01-01T00:00:00Z). + /// + /// The DateTime to convert. + /// Should be in UTC or convertible to UTC. + /// The Unix timestamp in seconds. + public static long ToUnixTimestamp(this DateTime dateTime) + { + // Ensure the DateTime is in UTC + var utcDateTime = dateTime.Kind == DateTimeKind.Utc + ? dateTime + : dateTime.ToUniversalTime(); + + return new DateTimeOffset(utcDateTime).ToUnixTimeSeconds(); + } + } +} From e601066c15764aabc0de18decdb6c4ab290b4fdc Mon Sep 17 00:00:00 2001 From: Sebastian Jensen Date: Tue, 22 Jul 2025 21:19:09 +0200 Subject: [PATCH 4/7] Add air pollution API URIs to Statics class Introduces static URI strings for air pollution data, forecast, and history endpoints in the Statics utility class. This enables easier access to OpenWeatherMap air pollution APIs by geographic coordinates. --- src/OpenWeatherMapSharp/Utils/Statics.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/OpenWeatherMapSharp/Utils/Statics.cs b/src/OpenWeatherMapSharp/Utils/Statics.cs index 32129ef..d0eb36a 100644 --- a/src/OpenWeatherMapSharp/Utils/Statics.cs +++ b/src/OpenWeatherMapSharp/Utils/Statics.cs @@ -17,6 +17,9 @@ private static readonly string ForecastBaseUri private static readonly string GeocodeBaseUri = $"{BaseUri}/geo/1.0"; + private static readonly string AirPollutionBaseUri + = $"{BaseUri}/data/2.5/air_pollution"; + /// /// Weather by geographic coordinates (latitude, longitude). @@ -80,5 +83,26 @@ public static readonly string GeocodeZipUri /// public static readonly string GeocodeReverseUri = GeocodeBaseUri + "/reverse?lat={0}&lon={1}&limit={2}&appid={3}"; + + /// + /// Air pollution data by geographic coordinates. + /// Format: lat={0}&lon={1}&appid={2} + /// + public static readonly string AirPollutionCoordinatesUri + = AirPollutionBaseUri + "?lat={0}&lon={1}&appid={2}"; + + /// + /// Air pollution forecast by geographic coordinates. + /// Format: lat={0}&lon={1}&start={2}&end={3}&appid={4} + /// + public static readonly string AirPollutionCoordinatesForecastUri + = AirPollutionBaseUri + "/forecast?lat={0}&lon={1}&appid={2}"; + + /// + /// Air pollution history by geographic coordinates. + /// Format: lat={0}&lon={1}&start={2}&end={3}&appid={4} + /// + public static readonly string AirPollutionCoordinatesHistoryUri + = AirPollutionBaseUri + "/history?lat={0}&lon={1}&start={2}&end={3}&appid={4}"; } } From 4c2ad74cb7a073f2078966aef316cb8937706673 Mon Sep 17 00:00:00 2001 From: Sebastian Jensen Date: Tue, 22 Jul 2025 21:19:34 +0200 Subject: [PATCH 5/7] Add air pollution data methods to service interface Introduces methods to retrieve current, forecast, and historical air pollution data in IOpenWeatherMapService and implements them in OpenWeatherMapService. This extends the API coverage to include air pollution endpoints. --- .../IOpenWeatherMapService.cs | 34 +++++++++++++++++++ .../OpenWeatherMapService.cs | 21 ++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/OpenWeatherMapSharp/IOpenWeatherMapService.cs b/src/OpenWeatherMapSharp/IOpenWeatherMapService.cs index d0acf45..14ce7ca 100644 --- a/src/OpenWeatherMapSharp/IOpenWeatherMapService.cs +++ b/src/OpenWeatherMapSharp/IOpenWeatherMapService.cs @@ -121,5 +121,39 @@ Task>> GetLocationByLatLonAsync( double latitude, double longitude, int limit = 5); + + /// + /// Retrieves current air pollution data for a specific location. + /// + /// Latitude of the location. + /// Longitude of the location. + /// Current air pollution data wrapped in a service response. + Task> GetAirPolutionAsync( + double latitude, + double longitude); + + /// + /// Retrieves forecasted air pollution data for the coming days for a specific location. + /// + /// Latitude of the location. + /// Longitude of the location. + /// Air pollution forecast data wrapped in a service response. + Task> GetAirPolutionForecastAsync( + double latitude, + double longitude); + + /// + /// Retrieves historical air pollution data for a specific location and time range. + /// + /// Latitude of the location. + /// Longitude of the location. + /// Start of the time range (UTC). + /// End of the time range (UTC). + /// Historical air pollution data wrapped in a service response. + Task> GetAirPolutionHistoryAsync( + double latitude, + double longitude, + DateTime start, + DateTime end); } } diff --git a/src/OpenWeatherMapSharp/OpenWeatherMapService.cs b/src/OpenWeatherMapSharp/OpenWeatherMapService.cs index 463ba19..1a9ab05 100644 --- a/src/OpenWeatherMapSharp/OpenWeatherMapService.cs +++ b/src/OpenWeatherMapSharp/OpenWeatherMapService.cs @@ -148,5 +148,26 @@ public async Task>> GetLocationB string url = string.Format(Statics.GeocodeReverseUri, latitude, longitude, limit, _apiKey); return await HttpService.GetDataAsync>(url); } + + /// + public async Task> GetAirPolutionAsync(double latitude, double longitude) + { + string url = string.Format(Statics.AirPollutionCoordinatesUri, latitude, longitude, _apiKey); + return await HttpService.GetDataAsync(url); + } + + /// + public async Task> GetAirPolutionForecastAsync(double latitude, double longitude) + { + string url = string.Format(Statics.AirPollutionCoordinatesForecastUri, latitude, longitude, _apiKey); + return await HttpService.GetDataAsync(url); + } + + /// + public async Task> GetAirPolutionHistoryAsync(double latitude, double longitude, DateTime start, DateTime end) + { + string url = string.Format(Statics.AirPollutionCoordinatesHistoryUri, latitude, longitude, start.ToUnixTimestamp(), end.ToUnixTimestamp(), _apiKey); + return await HttpService.GetDataAsync(url); + } } } From 0dff555c05ea5177fa739eb1c3ad21610821d1e1 Mon Sep 17 00:00:00 2001 From: Sebastian Jensen Date: Tue, 22 Jul 2025 21:20:01 +0200 Subject: [PATCH 6/7] Add air pollution API tests to service tests Added unit tests for GetAirPollutionAsync, GetAirPollutionForecastAsync, and GetAirPollutionHistoryAsync methods in OpenWeatherMapService. These tests verify valid responses, future forecast entries, and historical data within a specified time range. --- .../OpenWeatherMapServiceTests.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/OpenWeatherMapSharp.UnitTests/OpenWeatherMapServiceTests.cs b/tests/OpenWeatherMapSharp.UnitTests/OpenWeatherMapServiceTests.cs index a751675..e21aebc 100644 --- a/tests/OpenWeatherMapSharp.UnitTests/OpenWeatherMapServiceTests.cs +++ b/tests/OpenWeatherMapSharp.UnitTests/OpenWeatherMapServiceTests.cs @@ -265,4 +265,80 @@ OpenWeatherMapServiceResponse response Assert.False(response.IsSuccess); Assert.NotNull(response.Error); } + + [Fact] + public async Task GetAirPollution_ShouldReturnValidResponse() + { + // Arrange + OpenWeatherMapService service = new(OPENWEATHERMAPAPIKEY); + double latitude = 48.89; + double longitude = 8.69; + + // Act + OpenWeatherMapServiceResponse response = + await service.GetAirPolutionAsync(latitude, longitude); + + // Assert + Assert.NotNull(response); + Assert.True(response.IsSuccess); + Assert.NotNull(response.Response); + Assert.Null(response.Error); + + var entry = response.Response.Entries.FirstOrDefault(); + Assert.NotNull(entry); + Assert.InRange(entry.Components.CoarseParticulateMatter, 0, 1000); // basic plausibility check + } + + [Fact] + public async Task GetAirPollutionForecast_ShouldReturnFutureEntries() + { + // Arrange + OpenWeatherMapService service = new(OPENWEATHERMAPAPIKEY); + double latitude = 48.89; + double longitude = 8.69; + + // Act + OpenWeatherMapServiceResponse response = + await service.GetAirPolutionForecastAsync(latitude, longitude); + + // Assert + Assert.NotNull(response); + Assert.True(response.IsSuccess); + Assert.NotNull(response.Response); + Assert.Null(response.Error); + + var first = response.Response.Entries.FirstOrDefault(); + Assert.NotNull(first); + Assert.True(first.Date > DateTime.UtcNow.AddHours(-1)); + } + + [Fact] + public async Task GetAirPollutionHistory_ShouldReturnEntriesInTimeRange() + { + // Arrange + OpenWeatherMapService service = new(OPENWEATHERMAPAPIKEY); + double latitude = 48.89; + double longitude = 8.69; + + // Use a valid past time range (e.g. 3–2 days ago) + DateTime end = DateTime.UtcNow.AddDays(-2); + DateTime start = end.AddHours(-24); + + // Act + OpenWeatherMapServiceResponse response = + await service.GetAirPolutionHistoryAsync(latitude, longitude, start, end); + + // Assert + Assert.NotNull(response); + Assert.True(response.IsSuccess); + Assert.NotNull(response.Response); + Assert.Null(response.Error); + Assert.NotEmpty(response.Response.Entries); + + // Ensure all entries are within the time range + foreach (var entry in response.Response.Entries) + { + Assert.InRange(entry.Date, start, end); + } + } } \ No newline at end of file From 91775fd92d4654dcc04e4d8d370f96894de31209 Mon Sep 17 00:00:00 2001 From: Sebastian Jensen Date: Tue, 22 Jul 2025 21:23:47 +0200 Subject: [PATCH 7/7] Add air pollution data display to console app Fetches air pollution data using OpenWeatherMap API and displays AQI and pollutant levels in a new panel. Includes AQI meaning mapping and error handling for data retrieval. --- .../OpenWeatherMapSharp.Console/Program.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/samples/OpenWeatherMapSharp.Console/Program.cs b/samples/OpenWeatherMapSharp.Console/Program.cs index 62001ac..dfb4545 100644 --- a/samples/OpenWeatherMapSharp.Console/Program.cs +++ b/samples/OpenWeatherMapSharp.Console/Program.cs @@ -87,4 +87,49 @@ OpenWeatherMapServiceResponse weatherResponse }; AnsiConsole.Write(weatherPanel); +// == AIR POLLUTION == +OpenWeatherMapServiceResponse airPollutionResponse + = await openWeatherMapService.GetAirPolutionAsync(geolocation.Latitude, geolocation.Longitude); + +if (!airPollutionResponse.IsSuccess || airPollutionResponse.Response is not AirPolutionRoot airQuality || airQuality.Entries.Count == 0) +{ + AnsiConsole.MarkupLine("[bold red]Unfortunately I can't retrieve air pollution data. Please try again.[/]"); + return; +} + +AirPolutionEntry pollution = airQuality.Entries.First(); + +// Map AQI to meaning +string GetAqiMeaning(int aqi) => aqi switch +{ + 1 => "[green]Good[/]", + 2 => "[yellow]Fair[/]", + 3 => "[orange1]Moderate[/]", + 4 => "[red]Poor[/]", + 5 => "[maroon]Very Poor[/]", + _ => "[grey]Unknown[/]" +}; + +// == AIR POLLUTION PANEL == +List pollutionMarkupList = +[ + new($"[red]Air Quality Index (AQI): [/]{pollution.AQI.Index} ({GetAqiMeaning(pollution.AQI.Index)})"), + new("-----"), + new($"[red]PM2.5: [/]{pollution.Components.FineParticlesMatter:0.00} µg/m³"), + new($"[red]PM10: [/]{pollution.Components.CoarseParticulateMatter:0.00} µg/m³"), + new($"[red]O₃ (Ozone): [/]{pollution.Components.Ozone:0.00} µg/m³"), + new($"[red]NOâ‚‚ (Nitrogen Dioxide): [/]{pollution.Components.NitrogenDioxide:0.00} µg/m³"), + new($"[red]SOâ‚‚ (Sulfur Dioxide): [/]{pollution.Components.SulfurDioxide:0.00} µg/m³"), + new($"[red]CO (Carbon Monoxide): [/]{pollution.Components.CarbonMonoxide:0.00} µg/m³"), + new($"[red]NH₃ (Ammonia): [/]{pollution.Components.Ammonia:0.00} µg/m³") +]; + +Panel pollutionPanel = new(new Rows(pollutionMarkupList)) +{ + Header = new PanelHeader("Air Pollution"), + Width = 120 +}; +AnsiConsole.Write(pollutionPanel); + + Console.ReadLine();