From abddf2521ea1b0c50e674d1653ffe96aeb0de401 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Mon, 11 Mar 2024 18:47:51 -0700 Subject: [PATCH 01/34] feat: Add ECMWF 0.25 IFS and AIFS models --- OpenMeteo/WeatherModelOptions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OpenMeteo/WeatherModelOptions.cs b/OpenMeteo/WeatherModelOptions.cs index cb955e8..0d69688 100644 --- a/OpenMeteo/WeatherModelOptions.cs +++ b/OpenMeteo/WeatherModelOptions.cs @@ -104,6 +104,8 @@ public enum WeatherModelOptionsParameter { best_match, ecmwf_ifs04, + ecmwf_ifs025, + ecmwf_aifs025, metno_nordic, gfs_seamless, gfs_global, From c5a963659e1889988dc8c6d96462e9fa574895a0 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Mon, 11 Mar 2024 19:56:48 -0700 Subject: [PATCH 02/34] feat: Add Hourly units for ECMWF 0.25 models --- OpenMeteo/Hourly.cs | 112 +++++++++++++++++++++++++++++++++++++++ OpenMeteo/HourlyUnits.cs | 112 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) diff --git a/OpenMeteo/Hourly.cs b/OpenMeteo/Hourly.cs index 95fe2b7..d25879e 100644 --- a/OpenMeteo/Hourly.cs +++ b/OpenMeteo/Hourly.cs @@ -437,6 +437,118 @@ public class Hourly public float?[]? Geopotential_height_250hPa_ecmwf_ifs04 { get; set; } public float?[]? Geopotential_height_200hPa_ecmwf_ifs04 { get; set; } public float?[]? Geopotential_height_50hPa_ecmwf_ifs04 { get; set; } + public float?[]? Temperature_2m_ecmwf_ifs025 { get; set; } + public float?[]? Precipitation_ecmwf_ifs025 { get; set; } + public float?[]? Snowfall_ecmwf_ifs025 { get; set; } + public int?[]? Weathercode_ecmwf_ifs025 { get; set; } + public float?[]? Pressure_msl_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_low_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_mid_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_high_ecmwf_ifs025 { get; set; } + public float?[]? Windspeed_10m_ecmwf_ifs025 { get; set; } + public int?[]? Winddirection_10m_ecmwf_ifs025 { get; set; } + public float?[]? Temperature_1000hPa_ecmwf_ifs025 { get; set; } + public float?[]? Temperature_925hPa_ecmwf_ifs025 { get; set; } + public float?[]? Temperature_850hPa_ecmwf_ifs025 { get; set; } + public float?[]? Temperature_700hPa_ecmwf_ifs025 { get; set; } + public float?[]? Temperature_500hPa_ecmwf_ifs025 { get; set; } + public float?[]? Temperature_300hPa_ecmwf_ifs025 { get; set; } + public float?[]? Temperature_250hPa_ecmwf_ifs025 { get; set; } + public float?[]? Temperature_200hPa_ecmwf_ifs025 { get; set; } + public float?[]? Temperature_50hPa_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_1000hPa_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_925hPa_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_850hPa_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_700hPa_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_500hPa_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_300hPa_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_250hPa_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_200hPa_ecmwf_ifs025 { get; set; } + public int?[]? Cloudcover_50hPa_ecmwf_ifs025 { get; set; } + public float?[]? Windspeed_1000hPa_ecmwf_ifs025 { get; set; } + public float?[]? Windspeed_925hPa_ecmwf_ifs025 { get; set; } + public float?[]? Windspeed_850hPa_ecmwf_ifs025 { get; set; } + public float?[]? Windspeed_700hPa_ecmwf_ifs025 { get; set; } + public float?[]? Windspeed_500hPa_ecmwf_ifs025 { get; set; } + public float?[]? Windspeed_300hPa_ecmwf_ifs025 { get; set; } + public float?[]? Windspeed_250hPa_ecmwf_ifs025 { get; set; } + public float?[]? Windspeed_200hPa_ecmwf_ifs025 { get; set; } + public float?[]? Windspeed_50hPa_ecmwf_ifs025 { get; set; } + public int?[]? Winddirection_1000hPa_ecmwf_ifs025 { get; set; } + public int?[]? Winddirection_925hPa_ecmwf_ifs025 { get; set; } + public int?[]? Winddirection_850hPa_ecmwf_ifs025 { get; set; } + public int?[]? Winddirection_700hPa_ecmwf_ifs025 { get; set; } + public int?[]? Winddirection_500hPa_ecmwf_ifs025 { get; set; } + public int?[]? Winddirection_300hPa_ecmwf_ifs025 { get; set; } + public int?[]? Winddirection_250hPa_ecmwf_ifs025 { get; set; } + public int?[]? Winddirection_200hPa_ecmwf_ifs025 { get; set; } + public int?[]? Winddirection_50hPa_ecmwf_ifs025 { get; set; } + public float?[]? Geopotential_height_1000hPa_ecmwf_ifs025 { get; set; } + public float?[]? Geopotential_height_925hPa_ecmwf_ifs025 { get; set; } + public float?[]? Geopotential_height_850hPa_ecmwf_ifs025 { get; set; } + public float?[]? Geopotential_height_700hPa_ecmwf_ifs025 { get; set; } + public float?[]? Geopotential_height_500hPa_ecmwf_ifs025 { get; set; } + public float?[]? Geopotential_height_300hPa_ecmwf_ifs025 { get; set; } + public float?[]? Geopotential_height_250hPa_ecmwf_ifs025 { get; set; } + public float?[]? Geopotential_height_200hPa_ecmwf_ifs025 { get; set; } + public float?[]? Geopotential_height_50hPa_ecmwf_ifs025 { get; set; } + public float?[]? Temperature_2m_ecmwf_aifs025 { get; set; } + public float?[]? Precipitation_ecmwf_aifs025 { get; set; } + public float?[]? Snowfall_ecmwf_aifs025 { get; set; } + public int?[]? Weathercode_ecmwf_aifs025 { get; set; } + public float?[]? Pressure_msl_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_low_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_mid_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_high_ecmwf_aifs025 { get; set; } + public float?[]? Windspeed_10m_ecmwf_aifs025 { get; set; } + public int?[]? Winddirection_10m_ecmwf_aifs025 { get; set; } + public float?[]? Temperature_1000hPa_ecmwf_aifs025 { get; set; } + public float?[]? Temperature_925hPa_ecmwf_aifs025 { get; set; } + public float?[]? Temperature_850hPa_ecmwf_aifs025 { get; set; } + public float?[]? Temperature_700hPa_ecmwf_aifs025 { get; set; } + public float?[]? Temperature_500hPa_ecmwf_aifs025 { get; set; } + public float?[]? Temperature_300hPa_ecmwf_aifs025 { get; set; } + public float?[]? Temperature_250hPa_ecmwf_aifs025 { get; set; } + public float?[]? Temperature_200hPa_ecmwf_aifs025 { get; set; } + public float?[]? Temperature_50hPa_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_1000hPa_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_925hPa_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_850hPa_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_700hPa_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_500hPa_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_300hPa_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_250hPa_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_200hPa_ecmwf_aifs025 { get; set; } + public int?[]? Cloudcover_50hPa_ecmwf_aifs025 { get; set; } + public float?[]? Windspeed_1000hPa_ecmwf_aifs025 { get; set; } + public float?[]? Windspeed_925hPa_ecmwf_aifs025 { get; set; } + public float?[]? Windspeed_850hPa_ecmwf_aifs025 { get; set; } + public float?[]? Windspeed_700hPa_ecmwf_aifs025 { get; set; } + public float?[]? Windspeed_500hPa_ecmwf_aifs025 { get; set; } + public float?[]? Windspeed_300hPa_ecmwf_aifs025 { get; set; } + public float?[]? Windspeed_250hPa_ecmwf_aifs025 { get; set; } + public float?[]? Windspeed_200hPa_ecmwf_aifs025 { get; set; } + public float?[]? Windspeed_50hPa_ecmwf_aifs025 { get; set; } + public int?[]? Winddirection_1000hPa_ecmwf_aifs025 { get; set; } + public int?[]? Winddirection_925hPa_ecmwf_aifs025 { get; set; } + public int?[]? Winddirection_850hPa_ecmwf_aifs025 { get; set; } + public int?[]? Winddirection_700hPa_ecmwf_aifs025 { get; set; } + public int?[]? Winddirection_500hPa_ecmwf_aifs025 { get; set; } + public int?[]? Winddirection_300hPa_ecmwf_aifs025 { get; set; } + public int?[]? Winddirection_250hPa_ecmwf_aifs025 { get; set; } + public int?[]? Winddirection_200hPa_ecmwf_aifs025 { get; set; } + public int?[]? Winddirection_50hPa_ecmwf_aifs025 { get; set; } + public float?[]? Geopotential_height_1000hPa_ecmwf_aifs025 { get; set; } + public float?[]? Geopotential_height_925hPa_ecmwf_aifs025 { get; set; } + public float?[]? Geopotential_height_850hPa_ecmwf_aifs025 { get; set; } + public float?[]? Geopotential_height_700hPa_ecmwf_aifs025 { get; set; } + public float?[]? Geopotential_height_500hPa_ecmwf_aifs025 { get; set; } + public float?[]? Geopotential_height_300hPa_ecmwf_aifs025 { get; set; } + public float?[]? Geopotential_height_250hPa_ecmwf_aifs025 { get; set; } + public float?[]? Geopotential_height_200hPa_ecmwf_aifs025 { get; set; } + public float?[]? Geopotential_height_50hPa_ecmwf_aifs025 { get; set; } public float?[]? Temperature_2m_gfs_seamless { get; set; } public int?[]? Relativehumidity_2m_gfs_seamless { get; set; } public float?[]? Dewpoint_2m_gfs_seamless { get; set; } diff --git a/OpenMeteo/HourlyUnits.cs b/OpenMeteo/HourlyUnits.cs index 929cb6b..55f18b1 100644 --- a/OpenMeteo/HourlyUnits.cs +++ b/OpenMeteo/HourlyUnits.cs @@ -437,6 +437,118 @@ public class HourlyUnits public string? Geopotential_height_250hPa_ecmwf_ifs04 { get; set; } public string? Geopotential_height_200hPa_ecmwf_ifs04 { get; set; } public string? Geopotential_height_50hPa_ecmwf_ifs04 { get; set; } + public string? Temperature_2m_ecmwf_ifs025 { get; set; } + public string? Precipitation_ecmwf_ifs025 { get; set; } + public string? Snowfall_ecmwf_ifs025 { get; set; } + public string? Weathercode_ecmwf_ifs025 { get; set; } + public string? Pressure_msl_ecmwf_ifs025 { get; set; } + public string? Cloudcover_ecmwf_ifs025 { get; set; } + public string? Cloudcover_low_ecmwf_ifs025 { get; set; } + public string? Cloudcover_mid_ecmwf_ifs025 { get; set; } + public string? Cloudcover_high_ecmwf_ifs025 { get; set; } + public string? Windspeed_10m_ecmwf_ifs025 { get; set; } + public string? Winddirection_10m_ecmwf_ifs025 { get; set; } + public string? Temperature_1000hPa_ecmwf_ifs025 { get; set; } + public string? Temperature_925hPa_ecmwf_ifs025 { get; set; } + public string? Temperature_850hPa_ecmwf_ifs025 { get; set; } + public string? Temperature_700hPa_ecmwf_ifs025 { get; set; } + public string? Temperature_500hPa_ecmwf_ifs025 { get; set; } + public string? Temperature_300hPa_ecmwf_ifs025 { get; set; } + public string? Temperature_250hPa_ecmwf_ifs025 { get; set; } + public string? Temperature_200hPa_ecmwf_ifs025 { get; set; } + public string? Temperature_50hPa_ecmwf_ifs025 { get; set; } + public string? Cloudcover_1000hPa_ecmwf_ifs025 { get; set; } + public string? Cloudcover_925hPa_ecmwf_ifs025 { get; set; } + public string? Cloudcover_850hPa_ecmwf_ifs025 { get; set; } + public string? Cloudcover_700hPa_ecmwf_ifs025 { get; set; } + public string? Cloudcover_500hPa_ecmwf_ifs025 { get; set; } + public string? Cloudcover_300hPa_ecmwf_ifs025 { get; set; } + public string? Cloudcover_250hPa_ecmwf_ifs025 { get; set; } + public string? Cloudcover_200hPa_ecmwf_ifs025 { get; set; } + public string? Cloudcover_50hPa_ecmwf_ifs025 { get; set; } + public string? Windspeed_1000hPa_ecmwf_ifs025 { get; set; } + public string? Windspeed_925hPa_ecmwf_ifs025 { get; set; } + public string? Windspeed_850hPa_ecmwf_ifs025 { get; set; } + public string? Windspeed_700hPa_ecmwf_ifs025 { get; set; } + public string? Windspeed_500hPa_ecmwf_ifs025 { get; set; } + public string? Windspeed_300hPa_ecmwf_ifs025 { get; set; } + public string? Windspeed_250hPa_ecmwf_ifs025 { get; set; } + public string? Windspeed_200hPa_ecmwf_ifs025 { get; set; } + public string? Windspeed_50hPa_ecmwf_ifs025 { get; set; } + public string? Winddirection_1000hPa_ecmwf_ifs025 { get; set; } + public string? Winddirection_925hPa_ecmwf_ifs025 { get; set; } + public string? Winddirection_850hPa_ecmwf_ifs025 { get; set; } + public string? Winddirection_700hPa_ecmwf_ifs025 { get; set; } + public string? Winddirection_500hPa_ecmwf_ifs025 { get; set; } + public string? Winddirection_300hPa_ecmwf_ifs025 { get; set; } + public string? Winddirection_250hPa_ecmwf_ifs025 { get; set; } + public string? Winddirection_200hPa_ecmwf_ifs025 { get; set; } + public string? Winddirection_50hPa_ecmwf_ifs025 { get; set; } + public string? Geopotential_height_1000hPa_ecmwf_ifs025 { get; set; } + public string? Geopotential_height_925hPa_ecmwf_ifs025 { get; set; } + public string? Geopotential_height_850hPa_ecmwf_ifs025 { get; set; } + public string? Geopotential_height_700hPa_ecmwf_ifs025 { get; set; } + public string? Geopotential_height_500hPa_ecmwf_ifs025 { get; set; } + public string? Geopotential_height_300hPa_ecmwf_ifs025 { get; set; } + public string? Geopotential_height_250hPa_ecmwf_ifs025 { get; set; } + public string? Geopotential_height_200hPa_ecmwf_ifs025 { get; set; } + public string? Geopotential_height_50hPa_ecmwf_ifs025 { get; set; } + public string? Temperature_2m_ecmwf_aifs025 { get; set; } + public string? Precipitation_ecmwf_aifs025 { get; set; } + public string? Snowfall_ecmwf_aifs025 { get; set; } + public string? Weathercode_ecmwf_aifs025 { get; set; } + public string? Pressure_msl_ecmwf_aifs025 { get; set; } + public string? Cloudcover_ecmwf_aifs025 { get; set; } + public string? Cloudcover_low_ecmwf_aifs025 { get; set; } + public string? Cloudcover_mid_ecmwf_aifs025 { get; set; } + public string? Cloudcover_high_ecmwf_aifs025 { get; set; } + public string? Windspeed_10m_ecmwf_aifs025 { get; set; } + public string? Winddirection_10m_ecmwf_aifs025 { get; set; } + public string? Temperature_1000hPa_ecmwf_aifs025 { get; set; } + public string? Temperature_925hPa_ecmwf_aifs025 { get; set; } + public string? Temperature_850hPa_ecmwf_aifs025 { get; set; } + public string? Temperature_700hPa_ecmwf_aifs025 { get; set; } + public string? Temperature_500hPa_ecmwf_aifs025 { get; set; } + public string? Temperature_300hPa_ecmwf_aifs025 { get; set; } + public string? Temperature_250hPa_ecmwf_aifs025 { get; set; } + public string? Temperature_200hPa_ecmwf_aifs025 { get; set; } + public string? Temperature_50hPa_ecmwf_aifs025 { get; set; } + public string? Cloudcover_1000hPa_ecmwf_aifs025 { get; set; } + public string? Cloudcover_925hPa_ecmwf_aifs025 { get; set; } + public string? Cloudcover_850hPa_ecmwf_aifs025 { get; set; } + public string? Cloudcover_700hPa_ecmwf_aifs025 { get; set; } + public string? Cloudcover_500hPa_ecmwf_aifs025 { get; set; } + public string? Cloudcover_300hPa_ecmwf_aifs025 { get; set; } + public string? Cloudcover_250hPa_ecmwf_aifs025 { get; set; } + public string? Cloudcover_200hPa_ecmwf_aifs025 { get; set; } + public string? Cloudcover_50hPa_ecmwf_aifs025 { get; set; } + public string? Windspeed_1000hPa_ecmwf_aifs025 { get; set; } + public string? Windspeed_925hPa_ecmwf_aifs025 { get; set; } + public string? Windspeed_850hPa_ecmwf_aifs025 { get; set; } + public string? Windspeed_700hPa_ecmwf_aifs025 { get; set; } + public string? Windspeed_500hPa_ecmwf_aifs025 { get; set; } + public string? Windspeed_300hPa_ecmwf_aifs025 { get; set; } + public string? Windspeed_250hPa_ecmwf_aifs025 { get; set; } + public string? Windspeed_200hPa_ecmwf_aifs025 { get; set; } + public string? Windspeed_50hPa_ecmwf_aifs025 { get; set; } + public string? Winddirection_1000hPa_ecmwf_aifs025 { get; set; } + public string? Winddirection_925hPa_ecmwf_aifs025 { get; set; } + public string? Winddirection_850hPa_ecmwf_aifs025 { get; set; } + public string? Winddirection_700hPa_ecmwf_aifs025 { get; set; } + public string? Winddirection_500hPa_ecmwf_aifs025 { get; set; } + public string? Winddirection_300hPa_ecmwf_aifs025 { get; set; } + public string? Winddirection_250hPa_ecmwf_aifs025 { get; set; } + public string? Winddirection_200hPa_ecmwf_aifs025 { get; set; } + public string? Winddirection_50hPa_ecmwf_aifs025 { get; set; } + public string? Geopotential_height_1000hPa_ecmwf_aifs025 { get; set; } + public string? Geopotential_height_925hPa_ecmwf_aifs025 { get; set; } + public string? Geopotential_height_850hPa_ecmwf_aifs025 { get; set; } + public string? Geopotential_height_700hPa_ecmwf_aifs025 { get; set; } + public string? Geopotential_height_500hPa_ecmwf_aifs025 { get; set; } + public string? Geopotential_height_300hPa_ecmwf_aifs025 { get; set; } + public string? Geopotential_height_250hPa_ecmwf_aifs025 { get; set; } + public string? Geopotential_height_200hPa_ecmwf_aifs025 { get; set; } + public string? Geopotential_height_50hPa_ecmwf_aifs025 { get; set; } public string? Temperature_2m_gfs_seamless { get; set; } public string? Relativehumidity_2m_gfs_seamless { get; set; } public string? Dewpoint_2m_gfs_seamless { get; set; } From cbd5d2cd7a5b5ebf4fbb927c3182d39f99997022 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Wed, 13 Mar 2024 08:36:18 -0700 Subject: [PATCH 03/34] fix: Reduce number of calls made to the open-meteo API in the tests - as we are hitting a limit imposed by them --- OpenMeteoTests/WeatherForecastTests.cs | 42 +++++--------------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/OpenMeteoTests/WeatherForecastTests.cs b/OpenMeteoTests/WeatherForecastTests.cs index 54e8abe..70f47af 100644 --- a/OpenMeteoTests/WeatherForecastTests.cs +++ b/OpenMeteoTests/WeatherForecastTests.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -107,9 +108,8 @@ public async Task WeatherForecast_With_String_And_Options_Test() } [TestMethod] - public async Task WeatherForecast_With_All_Options_Test() + public void WeatherForecast_With_All_Options_Test() { - OpenMeteoClient client = new(); WeatherForecastOptions options = new() { Hourly = HourlyOptions.All, @@ -119,39 +119,11 @@ public async Task WeatherForecast_With_All_Options_Test() Minutely15 = Minutely15Options.All }; - var res = await client.QueryAsync(options); - - Assert.IsNotNull(res); - Assert.IsNotNull(res.Hourly); - Assert.IsNotNull(res.HourlyUnits); - Assert.IsNotNull(res.Daily); - Assert.IsNotNull(res.DailyUnits); - Assert.IsNotNull(res.Hourly.Cloudcover_1000hPa_best_match); - Assert.IsNotNull(res.Current); - Assert.IsNotNull(res.Minutely15); - } - - [TestMethod] - public void WeatherForecast_With_All_Options_Sync_Test() - { - OpenMeteoClient client = new(); - WeatherForecastOptions options = new() - { - Hourly = HourlyOptions.All, - Daily = DailyOptions.All, - Current = CurrentOptions.All, - Minutely15 = Minutely15Options.All, - }; - - var res = client.Query(options); - - Assert.IsNotNull(res); - Assert.IsNotNull(res.Hourly); - Assert.IsNotNull(res.HourlyUnits); - Assert.IsNotNull(res.Daily); - Assert.IsNotNull(res.DailyUnits); - Assert.IsNotNull(res.Current); - Assert.IsNotNull(res.Minutely15); + Assert.IsTrue(HourlyOptions.All.Parameter.All(p => options.Hourly.Parameter.Contains(p))); + Assert.IsTrue(DailyOptions.All.Parameter.All(p => options.Daily.Parameter.Contains(p))); + Assert.IsTrue(WeatherModelOptions.All.Parameter.All(p => options.Models.Parameter.Contains(p))); + Assert.IsTrue(CurrentOptions.All.Parameter.All(p => options.Current.Parameter.Contains(p))); + Assert.IsTrue(Minutely15Options.All.Parameter.All(p => options.Minutely15.Parameter.Contains(p))); } } } From 748665f9a84dfde8b61eff90b5801a8308a628e2 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Wed, 13 Mar 2024 09:08:28 -0700 Subject: [PATCH 04/34] feat: Add elevation API & tests --- OpenMeteo/ElevationApiResponse.cs | 12 ++++++++ OpenMeteo/ElevationOptions.cs | 21 +++++++++++++ OpenMeteo/OpenMeteoClient.cs | 50 +++++++++++++++++++++++++++++++ OpenMeteoTests/ElevationTests.cs | 33 ++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 OpenMeteo/ElevationApiResponse.cs create mode 100644 OpenMeteo/ElevationOptions.cs create mode 100644 OpenMeteoTests/ElevationTests.cs diff --git a/OpenMeteo/ElevationApiResponse.cs b/OpenMeteo/ElevationApiResponse.cs new file mode 100644 index 0000000..97ae763 --- /dev/null +++ b/OpenMeteo/ElevationApiResponse.cs @@ -0,0 +1,12 @@ +namespace OpenMeteo +{ + /// + /// Elevation API response + /// + public class ElevationApiResponse + { + /// + /// Elevation array in meters - this library currently only supports 1 elevation so this ill always be a single value array + public float[]? Elevation { get; set; } + } +} diff --git a/OpenMeteo/ElevationOptions.cs b/OpenMeteo/ElevationOptions.cs new file mode 100644 index 0000000..70459f8 --- /dev/null +++ b/OpenMeteo/ElevationOptions.cs @@ -0,0 +1,21 @@ +namespace OpenMeteo +{ + internal class ElevationOptions + { + public ElevationOptions(float latitude, float longitude) + { + Latitude = latitude; + Longitude = longitude; + } + + /// + /// Geographical WGS84 coordinate of the location + /// + public float Latitude { get; set; } + + /// + /// Geographical WGS84 coordinate of the location + /// + public float Longitude { get; set; } + } +} diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index 5d4123f..a470854 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -14,6 +14,7 @@ public class OpenMeteoClient private readonly string _weatherApiUrl = "https://api.open-meteo.com/v1/forecast"; private readonly string _geocodeApiUrl = "https://geocoding-api.open-meteo.com/v1/search"; private readonly string _airQualityApiUrl = "https://air-quality-api.open-meteo.com/v1/air-quality"; + private readonly string _elevationApiUrl = "https://api.open-meteo.com/v1/elevation"; private readonly HttpController httpController; /// @@ -164,6 +165,19 @@ public OpenMeteoClient() return (response.Locations[0].Latitude, response.Locations[0].Longitude); } + /// + /// Performs one GET-Request to Open-Meteo Elevation API + /// + /// Latitude + /// Longitude + /// + public async Task QueryElevationAsync(float latitude, float longitude) + { + ElevationOptions elevationOptions = new ElevationOptions(latitude, longitude); + + return await GetElevationAsync(elevationOptions); + } + public WeatherForecast? Query(WeatherForecastOptions options) { return QueryAsync(options).GetAwaiter().GetResult(); @@ -194,6 +208,11 @@ public OpenMeteoClient() return QueryAsync(options).GetAwaiter().GetResult(); } + public ElevationApiResponse? QueryElevation(float latitude, float longitude) + { + return QueryElevationAsync(latitude, longitude).GetAwaiter().GetResult(); + } + private async Task GetAirQualityAsync(AirQualityOptions options) { try @@ -320,6 +339,25 @@ public string WeathercodeToString(int weathercode) } } + private async Task GetElevationAsync(ElevationOptions options) + { + try + { + HttpResponseMessage response = await httpController.Client.GetAsync(MergeUrlWithOptions(_elevationApiUrl, options)); + response.EnsureSuccessStatusCode(); + + ElevationApiResponse? elevationData = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + + return elevationData; + } + catch (HttpRequestException e) + { + Console.WriteLine($"Can't find elevation for latitude {options.Latitude} & longitude {options.Longitude}. Please make sure that they are valid."); + Console.WriteLine(e.Message); + return null; + } + } + private string MergeUrlWithOptions(string url, WeatherForecastOptions? options) { if (options == null) return url; @@ -563,6 +601,18 @@ private string MergeUrlWithOptions(string url, AirQualityOptions options) return uri.ToString(); } + + private string MergeUrlWithOptions(string url, ElevationOptions options) + { + if (options == null) return url; + + UriBuilder uri = new UriBuilder(url) + { + Query = $"?latitude={options.Latitude.ToString(CultureInfo.InvariantCulture)}&longitude={options.Longitude.ToString(CultureInfo.InvariantCulture)}" + }; + + return uri.ToString(); + } } } diff --git a/OpenMeteoTests/ElevationTests.cs b/OpenMeteoTests/ElevationTests.cs new file mode 100644 index 0000000..1658731 --- /dev/null +++ b/OpenMeteoTests/ElevationTests.cs @@ -0,0 +1,33 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenMeteo; +using System.Threading.Tasks; + +namespace OpenMeteoTests +{ + [TestClass] + public class ElevationTests + { + private static readonly float Latitude = 52.5235f; + private static readonly float Longitude = 13.4115f; + + [TestMethod] + public async Task Elevation_Async_Test() + { + OpenMeteoClient client = new(); + var res = await client.QueryElevationAsync(Latitude, Longitude); + + Assert.IsNotNull(res); + Assert.AreEqual(res.Elevation.Length, 1); + } + + [TestMethod] + public void Elevation_Sync_Test() + { + OpenMeteoClient client = new(); + var res = client.QueryElevation(Latitude, Longitude); + + Assert.IsNotNull(res); + Assert.AreEqual(res.Elevation.Length, 1); + } + } +} From 6973bfbb46abdb66aa11143225c87ee4ec52739f Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Wed, 13 Mar 2024 09:16:27 -0700 Subject: [PATCH 05/34] fix: comment typos --- OpenMeteo/ElevationApiResponse.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenMeteo/ElevationApiResponse.cs b/OpenMeteo/ElevationApiResponse.cs index 97ae763..91fee0d 100644 --- a/OpenMeteo/ElevationApiResponse.cs +++ b/OpenMeteo/ElevationApiResponse.cs @@ -6,7 +6,7 @@ public class ElevationApiResponse { /// - /// Elevation array in meters - this library currently only supports 1 elevation so this ill always be a single value array + /// Elevation array in meters - this library currently only supports 1 input elevation so this will always be a single value array public float[]? Elevation { get; set; } } } From a56afca1beb7749e8062a085072be6deb2db3947 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Thu, 21 Mar 2024 19:44:21 -0700 Subject: [PATCH 06/34] Update package-and-deploy.yml --- .github/workflows/package-and-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/package-and-deploy.yml b/.github/workflows/package-and-deploy.yml index 445631e..b12b0cb 100644 --- a/.github/workflows/package-and-deploy.yml +++ b/.github/workflows/package-and-deploy.yml @@ -52,4 +52,4 @@ jobs: run: dotnet pack ./OpenMeteo/OpenMeteo.csproj --configuration release -o:package /p:PackageVersion=${{ steps.gitversion.outputs.AssemblySemVer }} - name: Push generated package to NuGet.org - run: dotnet nuget push ./package/*.nupkg --api-key ${{ secrets.API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + run: dotnet nuget push ./package/*.nupkg --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/colinnuk/index.json --skip-duplicate From 47aa2358fb40f3841c87c7934e0a0c8f8423fd8f Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Thu, 21 Mar 2024 19:45:09 -0700 Subject: [PATCH 07/34] Delete .github/workflows/test-dev-build.yml --- .github/workflows/test-dev-build.yml | 49 ---------------------------- 1 file changed, 49 deletions(-) delete mode 100644 .github/workflows/test-dev-build.yml diff --git a/.github/workflows/test-dev-build.yml b/.github/workflows/test-dev-build.yml deleted file mode 100644 index a40ef54..0000000 --- a/.github/workflows/test-dev-build.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Test Development Build - -on: - push: - branches-ignore: - - 'master' - paths: - - '**/*.cs' - - '**/*.csproj' - pull_request: - branches-ignore: - - 'master' - paths: - - '**/*.cs' - - '**/*.csproj' - - -env: - DOTNET_VERSION: '6.x.x' # The .NET SDK version to use - -jobs: - test: - - name: test-ubuntu - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Setup .NET Core - uses: actions/setup-dotnet@v2 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: Caching dependencies - uses: actions/cache@v3 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - ${{ runner.os }}-nuget - - - name: Install dependencies - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore - - - name: Test - run: dotnet test --no-restore --verbosity normal From 0eb818b40394d41c2bb5162dfb2b31f6022cf022 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Thu, 21 Mar 2024 19:57:59 -0700 Subject: [PATCH 08/34] Update build-and-test.yml Only need one OS --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e46d5cc..6ca34f2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -22,7 +22,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + os: [ubuntu-latest] steps: - name: Checkout Code From a8d10e6132f9729f04ba29a1994feebfb1ce3ffe Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Thu, 21 Mar 2024 19:58:21 -0700 Subject: [PATCH 09/34] Update package-and-deploy.yml --- .github/workflows/package-and-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/package-and-deploy.yml b/.github/workflows/package-and-deploy.yml index b12b0cb..89874ef 100644 --- a/.github/workflows/package-and-deploy.yml +++ b/.github/workflows/package-and-deploy.yml @@ -51,5 +51,5 @@ jobs: - name: Package nuget run: dotnet pack ./OpenMeteo/OpenMeteo.csproj --configuration release -o:package /p:PackageVersion=${{ steps.gitversion.outputs.AssemblySemVer }} - - name: Push generated package to NuGet.org + - name: Push generated package to Github Packages run: dotnet nuget push ./package/*.nupkg --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/colinnuk/index.json --skip-duplicate From e94bb0ae532974b293b8b77337d41912536d2bee Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Thu, 21 Mar 2024 19:59:34 -0700 Subject: [PATCH 10/34] Update build-and-test.yml --- .github/workflows/build-and-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6ca34f2..8886509 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -11,6 +11,7 @@ on: paths: - '**/*.cs' - '**/*.csproj' + workflow_dispatch: env: DOTNET_VERSION: '6.x.x' # The .NET SDK version to use From b00e7790edd459b1832176a67b90550bb3e806f1 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Thu, 21 Mar 2024 20:18:41 -0700 Subject: [PATCH 11/34] Update OpenMeteo.csproj --- OpenMeteo/OpenMeteo.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenMeteo/OpenMeteo.csproj b/OpenMeteo/OpenMeteo.csproj index f56b1ee..c37b69c 100644 --- a/OpenMeteo/OpenMeteo.csproj +++ b/OpenMeteo/OpenMeteo.csproj @@ -3,7 +3,7 @@ netstandard2.1 enable - $(AssemblyName).dotnet + $(AssemblyName).colinnuk.dotnet Open Meteo Dotnet Library 0.0.3 AlienDwarf From 19dace5ac56cbadfe1f1fe151792d891c1b7cc20 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Thu, 21 Mar 2024 21:33:55 -0700 Subject: [PATCH 12/34] Handle the OpenMeteo error responses more accurately --- OpenMeteo/ErrorResponse.cs | 13 +++++ OpenMeteo/IOpenMeteoLogger.cs | 16 ++++++ OpenMeteo/OpenMeteoClient.cs | 70 +++++++++++++++++--------- OpenMeteo/OpenMeteoClientException.cs | 15 ++++++ OpenMeteoTests/WeatherForecastTests.cs | 18 +++++++ 5 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 OpenMeteo/ErrorResponse.cs create mode 100644 OpenMeteo/IOpenMeteoLogger.cs create mode 100644 OpenMeteo/OpenMeteoClientException.cs diff --git a/OpenMeteo/ErrorResponse.cs b/OpenMeteo/ErrorResponse.cs new file mode 100644 index 0000000..4de35a5 --- /dev/null +++ b/OpenMeteo/ErrorResponse.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace OpenMeteo +{ + internal class ErrorResponse + { + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + [JsonPropertyName("error")] + public bool Error { get; set; } + } +} diff --git a/OpenMeteo/IOpenMeteoLogger.cs b/OpenMeteo/IOpenMeteoLogger.cs new file mode 100644 index 0000000..b260c73 --- /dev/null +++ b/OpenMeteo/IOpenMeteoLogger.cs @@ -0,0 +1,16 @@ +namespace OpenMeteo +{ + /// + /// Specifies a simple interface for a logger. + /// Usage of this interface is optional. + /// To use this, create your own implementation of this interface which you can easily setup + /// to call your own logging framework. + /// + public interface IOpenMeteoLogger + { + void Information(string message); + void Warning(string message); + void Error(string message); + void Debug(string message); + } +} \ No newline at end of file diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index a470854..d316b34 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -17,6 +17,9 @@ public class OpenMeteoClient private readonly string _elevationApiUrl = "https://api.open-meteo.com/v1/elevation"; private readonly HttpController httpController; + private readonly IOpenMeteoLogger? _logger = default!; + private readonly bool _rethrowExceptions = false; + /// /// Creates a new object and sets the neccessary variables (httpController, CultureInfo) /// @@ -25,6 +28,16 @@ public OpenMeteoClient() httpController = new HttpController(); } + /// + /// Creates a new object and sets the neccessary variables (httpController, CultureInfo) + /// + public OpenMeteoClient(bool rethrowExceptions, IOpenMeteoLogger? logger = null) + { + httpController = new HttpController(); + _logger = logger; + _rethrowExceptions = rethrowExceptions; + } + /// /// Performs two GET-Requests (first geocoding api for latitude,longitude, then weather forecast) /// @@ -80,14 +93,7 @@ public OpenMeteoClient() /// Awaitable Task containing WeatherForecast or NULL public async Task QueryAsync(WeatherForecastOptions options) { - try - { - return await GetWeatherForecastAsync(options); - } - catch (Exception) - { - return null; - } + return await GetWeatherForecastAsync(options); } /// @@ -217,7 +223,9 @@ public OpenMeteoClient() { try { - HttpResponseMessage response = await httpController.Client.GetAsync(MergeUrlWithOptions(_airQualityApiUrl, options)); + var url = MergeUrlWithOptions(_airQualityApiUrl, options); + _logger?.Debug($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). URL: {url}"); + HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); AirQuality? airQuality = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); @@ -225,8 +233,9 @@ public OpenMeteoClient() } catch (HttpRequestException e) { - Console.WriteLine(e.Message); - Console.WriteLine(e.StackTrace); + _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + if (_rethrowExceptions) + throw; return null; } } @@ -305,16 +314,23 @@ public string WeathercodeToString(int weathercode) { try { - HttpResponseMessage response = await httpController.Client.GetAsync(MergeUrlWithOptions(_weatherApiUrl, options)); - response.EnsureSuccessStatusCode(); + var url = MergeUrlWithOptions(_weatherApiUrl, options); + _logger?.Debug($"{nameof(OpenMeteoClient)}.GetElevationAsync(). URL: {url}"); + HttpResponseMessage response = await httpController.Client.GetAsync(url); + if(response.IsSuccessStatusCode) + { + WeatherForecast? weatherForecast = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + return weatherForecast; + } - WeatherForecast? weatherForecast = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); - return weatherForecast; + var error = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + throw new OpenMeteoClientException(error?.Reason ?? "Exception in OpenMeteoClient", response.StatusCode); } catch (HttpRequestException e) { - Console.WriteLine(e.Message); - Console.WriteLine(e.StackTrace); + _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + if (_rethrowExceptions) + throw; return null; } @@ -324,7 +340,10 @@ public string WeathercodeToString(int weathercode) { try { - HttpResponseMessage response = await httpController.Client.GetAsync(MergeUrlWithOptions(_geocodeApiUrl, options)); + + var url = MergeUrlWithOptions(_geocodeApiUrl, options); + _logger?.Debug($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). URL: {url}"); + HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); GeocodingApiResponse? geocodingData = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); @@ -333,8 +352,9 @@ public string WeathercodeToString(int weathercode) } catch (HttpRequestException e) { - Console.WriteLine("Can't find " + options.Name + ". Please make sure that the name is valid."); - Console.WriteLine(e.Message); + _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + if (_rethrowExceptions) + throw; return null; } } @@ -343,7 +363,9 @@ public string WeathercodeToString(int weathercode) { try { - HttpResponseMessage response = await httpController.Client.GetAsync(MergeUrlWithOptions(_elevationApiUrl, options)); + var url = MergeUrlWithOptions(_elevationApiUrl, options); + _logger?.Debug($"{nameof(OpenMeteoClient)}.GetElevationAsync(). URL: {url}"); + HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); ElevationApiResponse? elevationData = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); @@ -352,8 +374,10 @@ public string WeathercodeToString(int weathercode) } catch (HttpRequestException e) { - Console.WriteLine($"Can't find elevation for latitude {options.Latitude} & longitude {options.Longitude}. Please make sure that they are valid."); - Console.WriteLine(e.Message); + _logger?.Warning($"Can't find elevation for latitude {options.Latitude} & longitude {options.Longitude}. Please make sure that they are valid."); + _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetElevationAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + if (_rethrowExceptions) + throw; return null; } } diff --git a/OpenMeteo/OpenMeteoClientException.cs b/OpenMeteo/OpenMeteoClientException.cs new file mode 100644 index 0000000..6c70617 --- /dev/null +++ b/OpenMeteo/OpenMeteoClientException.cs @@ -0,0 +1,15 @@ +using System.Net; +using System.Net.Http; + +namespace OpenMeteo +{ + public class OpenMeteoClientException : HttpRequestException + { + public HttpStatusCode StatusCode { get; } + + public OpenMeteoClientException(string message, HttpStatusCode httpStatusCode) : base(message) + { + StatusCode = httpStatusCode; + } + } +} diff --git a/OpenMeteoTests/WeatherForecastTests.cs b/OpenMeteoTests/WeatherForecastTests.cs index 70f47af..0e3301a 100644 --- a/OpenMeteoTests/WeatherForecastTests.cs +++ b/OpenMeteoTests/WeatherForecastTests.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -125,5 +126,22 @@ public void WeatherForecast_With_All_Options_Test() Assert.IsTrue(CurrentOptions.All.Parameter.All(p => options.Current.Parameter.Contains(p))); Assert.IsTrue(Minutely15Options.All.Parameter.All(p => options.Minutely15.Parameter.Contains(p))); } + + [TestMethod] + public async Task Latitude_Longitude_No_Data_For_Selected_Forecast_Rethrows_Test() + { + OpenMeteoClient client = new(true); + + WeatherForecastOptions options = new WeatherForecastOptions + { + Latitude = 1, + Longitude = 1, + Models = new WeatherModelOptions(WeatherModelOptionsParameter.gfs_hrrr), + }; + + var ex = await Assert.ThrowsExceptionAsync(async () => await client.QueryAsync(options)); + Assert.AreEqual(System.Net.HttpStatusCode.BadRequest, ex.StatusCode); + Assert.AreEqual("No data is available for this location", ex.Message); + } } } From 0cee91b7d187f74af2abbb63c274446035727e0b Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Thu, 21 Mar 2024 21:40:38 -0700 Subject: [PATCH 13/34] Add models --- OpenMeteo/WeatherModelOptions.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/OpenMeteo/WeatherModelOptions.cs b/OpenMeteo/WeatherModelOptions.cs index 0d69688..0748120 100644 --- a/OpenMeteo/WeatherModelOptions.cs +++ b/OpenMeteo/WeatherModelOptions.cs @@ -125,6 +125,10 @@ public enum WeatherModelOptionsParameter meteofrance_arpege_world, meteofrance_arpege_europe, meteofrance_arome_france, - meteofrance_arome_france_hd + meteofrance_arome_france_hd, + bom_access_global, + arpae_cosmo_2i, + arpae_cosmo_2i_ruc, + arpae_cosmo_5m } } From 111b33ee140b043658705df3551b3a95421961ab Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Fri, 22 Mar 2024 06:14:56 -0700 Subject: [PATCH 14/34] Small tweak to error handling --- OpenMeteo/OpenMeteoClient.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index d316b34..15f85d5 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -233,7 +233,7 @@ public OpenMeteoClient(bool rethrowExceptions, IOpenMeteoLogger? logger = null) } catch (HttpRequestException e) { - _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + _logger?.Error($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) throw; return null; @@ -323,12 +323,24 @@ public string WeathercodeToString(int weathercode) return weatherForecast; } - var error = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + ErrorResponse? error = null; + if((int)response.StatusCode >= 400 && (int)response.StatusCode < 500) + { + try + { + error = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + } + catch (Exception e) + { + _logger?.Error($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Unable to deserialise error response. This exception will be thrown. Message: {e.Message} StackTrace: {e.StackTrace}"); + } + } + throw new OpenMeteoClientException(error?.Reason ?? "Exception in OpenMeteoClient", response.StatusCode); } - catch (HttpRequestException e) + catch (Exception e) { - _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + _logger?.Error($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) throw; return null; @@ -352,7 +364,7 @@ public string WeathercodeToString(int weathercode) } catch (HttpRequestException e) { - _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + _logger?.Error($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) throw; return null; From 2fd56d233b2e18162bd0d83817e215f9cb00fc3d Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Fri, 5 Apr 2024 14:12:55 -0700 Subject: [PATCH 15/34] Add gfs_graphcast025 --- OpenMeteo/WeatherModelOptions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/OpenMeteo/WeatherModelOptions.cs b/OpenMeteo/WeatherModelOptions.cs index 0748120..0d9d94b 100644 --- a/OpenMeteo/WeatherModelOptions.cs +++ b/OpenMeteo/WeatherModelOptions.cs @@ -110,6 +110,7 @@ public enum WeatherModelOptionsParameter gfs_seamless, gfs_global, gfs_hrrr, + gfs_graphcast025, jma_seamless, jma_msm, jma_gsm, From 4ada501717c0a3a38a716e3af1c012f1c3a4e064 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Tue, 9 Apr 2024 13:16:24 -0700 Subject: [PATCH 16/34] Change to only log errors if we have set to rethrow exceptions --- OpenMeteo/OpenMeteoClient.cs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index 15f85d5..c3b2a63 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -232,10 +232,13 @@ public OpenMeteoClient(bool rethrowExceptions, IOpenMeteoLogger? logger = null) return airQuality; } catch (HttpRequestException e) - { - _logger?.Error($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + { if (_rethrowExceptions) + { + _logger?.Error($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); throw; + } + _logger?.Warning($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); return null; } } @@ -340,9 +343,13 @@ public string WeathercodeToString(int weathercode) } catch (Exception e) { - _logger?.Error($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + if (_rethrowExceptions) + { + _logger?.Error($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); throw; + } + _logger?.Warning($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); return null; } @@ -364,9 +371,12 @@ public string WeathercodeToString(int weathercode) } catch (HttpRequestException e) { - _logger?.Error($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) + { + _logger?.Error($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); throw; + } + _logger?.Warning($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); return null; } } @@ -387,9 +397,12 @@ public string WeathercodeToString(int weathercode) catch (HttpRequestException e) { _logger?.Warning($"Can't find elevation for latitude {options.Latitude} & longitude {options.Longitude}. Please make sure that they are valid."); - _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetElevationAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) + { + _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetElevationAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); throw; + } + _logger?.Warning($"Error in {nameof(OpenMeteoClient)}.GetElevationAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); return null; } } From 8ad1b3fa51356de5d45b13990ae2a2e333c7d02f Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Tue, 9 Apr 2024 16:18:34 -0700 Subject: [PATCH 17/34] Revert "Change to only log errors if we have set to rethrow exceptions" This reverts commit 4ada501717c0a3a38a716e3af1c012f1c3a4e064. --- OpenMeteo/OpenMeteoClient.cs | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index c3b2a63..15f85d5 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -232,13 +232,10 @@ public OpenMeteoClient(bool rethrowExceptions, IOpenMeteoLogger? logger = null) return airQuality; } catch (HttpRequestException e) - { + { + _logger?.Error($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) - { - _logger?.Error($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); throw; - } - _logger?.Warning($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); return null; } } @@ -343,13 +340,9 @@ public string WeathercodeToString(int weathercode) } catch (Exception e) { - + _logger?.Error($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) - { - _logger?.Error($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); throw; - } - _logger?.Warning($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); return null; } @@ -371,12 +364,9 @@ public string WeathercodeToString(int weathercode) } catch (HttpRequestException e) { + _logger?.Error($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) - { - _logger?.Error($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); throw; - } - _logger?.Warning($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); return null; } } @@ -397,12 +387,9 @@ public string WeathercodeToString(int weathercode) catch (HttpRequestException e) { _logger?.Warning($"Can't find elevation for latitude {options.Latitude} & longitude {options.Longitude}. Please make sure that they are valid."); + _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetElevationAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) - { - _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetElevationAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); throw; - } - _logger?.Warning($"Error in {nameof(OpenMeteoClient)}.GetElevationAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); return null; } } From 0f46876681fd55cbeba1c8da56562502b47b981b Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Tue, 9 Apr 2024 16:19:45 -0700 Subject: [PATCH 18/34] Change errors to warnings - client code will have to log this if it needs to --- OpenMeteo/OpenMeteoClient.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index 15f85d5..cb8d057 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -233,7 +233,7 @@ public OpenMeteoClient(bool rethrowExceptions, IOpenMeteoLogger? logger = null) } catch (HttpRequestException e) { - _logger?.Error($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + _logger?.Warning($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) throw; return null; @@ -340,7 +340,7 @@ public string WeathercodeToString(int weathercode) } catch (Exception e) { - _logger?.Error($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + _logger?.Warning($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) throw; return null; @@ -364,7 +364,7 @@ public string WeathercodeToString(int weathercode) } catch (HttpRequestException e) { - _logger?.Error($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + _logger?.Warning($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) throw; return null; @@ -387,7 +387,7 @@ public string WeathercodeToString(int weathercode) catch (HttpRequestException e) { _logger?.Warning($"Can't find elevation for latitude {options.Latitude} & longitude {options.Longitude}. Please make sure that they are valid."); - _logger?.Error($"Error in {nameof(OpenMeteoClient)}.GetElevationAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + _logger?.Warning($"Error in {nameof(OpenMeteoClient)}.GetElevationAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); if (_rethrowExceptions) throw; return null; From 3b47c1a8fcd0793877ba55cdfc90dfb5f2e30a56 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Tue, 30 Apr 2024 15:43:14 -0700 Subject: [PATCH 19/34] Upgrade lib to dotnet 8 --- .github/workflows/build-and-test.yml | 2 +- OpenMeteo/HttpController.cs | 9 ++++++--- OpenMeteo/OpenMeteo.csproj | 6 +++--- OpenMeteoTests/OpenMeteoTests.csproj | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 8886509..60b5de3 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -14,7 +14,7 @@ on: workflow_dispatch: env: - DOTNET_VERSION: '6.x.x' # The .NET SDK version to use + DOTNET_VERSION: '8.x.x' # The .NET SDK version to use jobs: build-and-test: diff --git a/OpenMeteo/HttpController.cs b/OpenMeteo/HttpController.cs index d563ead..cd48cd9 100644 --- a/OpenMeteo/HttpController.cs +++ b/OpenMeteo/HttpController.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Net.Http; namespace OpenMeteo @@ -15,7 +13,12 @@ internal class HttpController public HttpController() { - _httpClient = new HttpClient(); + var socketHttpHandler = new SocketsHttpHandler() + { + PooledConnectionLifetime = TimeSpan.FromMinutes(4), + }; + _httpClient = new HttpClient(socketHttpHandler); + _httpClient.DefaultRequestHeaders.Accept.Clear(); _httpClient.DefaultRequestHeaders.Accept.Add( new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json") diff --git a/OpenMeteo/OpenMeteo.csproj b/OpenMeteo/OpenMeteo.csproj index c37b69c..e4900f0 100644 --- a/OpenMeteo/OpenMeteo.csproj +++ b/OpenMeteo/OpenMeteo.csproj @@ -1,10 +1,10 @@ - netstandard2.1 + net8.0 enable $(AssemblyName).colinnuk.dotnet - Open Meteo Dotnet Library + Open Meteo Dotnet 8 Library 0.0.3 AlienDwarf @@ -17,7 +17,7 @@ 0.0.1 0.0.1 LICENSE - A .Net Standard 2.1 library for the Open-Meteo.com API. + A DotNet 8 library for the Open-Meteo.com API. True snupkg False diff --git a/OpenMeteoTests/OpenMeteoTests.csproj b/OpenMeteoTests/OpenMeteoTests.csproj index 17432e8..15a682d 100644 --- a/OpenMeteoTests/OpenMeteoTests.csproj +++ b/OpenMeteoTests/OpenMeteoTests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false From 4dc5c111e5bb8e7571d7098aa950f318e10bb0e9 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Tue, 30 Apr 2024 16:03:35 -0700 Subject: [PATCH 20/34] Add pressure height params --- OpenMeteo/HourlyOptions.cs | 98 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/OpenMeteo/HourlyOptions.cs b/OpenMeteo/HourlyOptions.cs index 7d25e3b..d23e707 100644 --- a/OpenMeteo/HourlyOptions.cs +++ b/OpenMeteo/HourlyOptions.cs @@ -158,11 +158,25 @@ public enum HourlyOptionsParameter temperature_950hPa, temperature_925hPa, temperature_900hPa, + temperature_875hPa, temperature_850hPa, + temperature_825hPa, temperature_800hPa, + temperature_775hPa, + temperature_750hPa, + temperature_725hPa, temperature_700hPa, + temperature_675hPa, + temperature_650hPa, + temperature_625hPa, temperature_600hPa, + temperature_575hPa, + temperature_550hPa, + temperature_525hPa, temperature_500hPa, + temperature_475hPa, + temperature_450hPa, + temperature_425hPa, temperature_400hPa, temperature_300hPa, temperature_250hPa, @@ -177,11 +191,25 @@ public enum HourlyOptionsParameter dewpoint_950hPa, dewpoint_925hPa, dewpoint_900hPa, + dewpoint_875hPa, dewpoint_850hPa, + dewpoint_825hPa, dewpoint_800hPa, + dewpoint_775hPa, + dewpoint_750hPa, + dewpoint_725hPa, dewpoint_700hPa, + dewpoint_675hPa, + dewpoint_650hPa, + dewpoint_625hPa, dewpoint_600hPa, + dewpoint_575hPa, + dewpoint_550hPa, + dewpoint_525hPa, dewpoint_500hPa, + dewpoint_475hPa, + dewpoint_450hPa, + dewpoint_425hPa, dewpoint_400hPa, dewpoint_300hPa, dewpoint_250hPa, @@ -196,11 +224,25 @@ public enum HourlyOptionsParameter relativehumidity_950hPa, relativehumidity_925hPa, relativehumidity_900hPa, + relativehumidity_875hPa, relativehumidity_850hPa, + relativehumidity_825hPa, relativehumidity_800hPa, + relativehumidity_775hPa, + relativehumidity_750hPa, + relativehumidity_725hPa, relativehumidity_700hPa, + relativehumidity_675hPa, + relativehumidity_650hPa, + relativehumidity_625hPa, relativehumidity_600hPa, + relativehumidity_575hPa, + relativehumidity_550hPa, + relativehumidity_525hPa, relativehumidity_500hPa, + relativehumidity_475hPa, + relativehumidity_450hPa, + relativehumidity_425hPa, relativehumidity_400hPa, relativehumidity_300hPa, relativehumidity_250hPa, @@ -215,11 +257,25 @@ public enum HourlyOptionsParameter cloudcover_950hPa, cloudcover_925hPa, cloudcover_900hPa, + cloudcover_875hPa, cloudcover_850hPa, + cloudcover_825hPa, cloudcover_800hPa, + cloudcover_775hPa, + cloudcover_750hPa, + cloudcover_725hPa, cloudcover_700hPa, + cloudcover_675hPa, + cloudcover_650hPa, + cloudcover_625hPa, cloudcover_600hPa, + cloudcover_575hPa, + cloudcover_550hPa, + cloudcover_525hPa, cloudcover_500hPa, + cloudcover_475hPa, + cloudcover_450hPa, + cloudcover_425hPa, cloudcover_400hPa, cloudcover_300hPa, cloudcover_250hPa, @@ -234,11 +290,25 @@ public enum HourlyOptionsParameter windspeed_950hPa, windspeed_925hPa, windspeed_900hPa, + windspeed_875hPa, windspeed_850hPa, + windspeed_825hPa, windspeed_800hPa, + windspeed_775hPa, + windspeed_750hPa, + windspeed_725hPa, windspeed_700hPa, + windspeed_675hPa, + windspeed_650hPa, + windspeed_625hPa, windspeed_600hPa, + windspeed_575hPa, + windspeed_550hPa, + windspeed_525hPa, windspeed_500hPa, + windspeed_475hPa, + windspeed_450hPa, + windspeed_425hPa, windspeed_400hPa, windspeed_300hPa, windspeed_250hPa, @@ -253,11 +323,25 @@ public enum HourlyOptionsParameter winddirection_950hPa, winddirection_925hPa, winddirection_900hPa, + winddirection_875hPa, winddirection_850hPa, + winddirection_825hPa, winddirection_800hPa, + winddirection_775hPa, + winddirection_750hPa, + winddirection_725hPa, winddirection_700hPa, + winddirection_675hPa, + winddirection_650hPa, + winddirection_625hPa, winddirection_600hPa, + winddirection_575hPa, + winddirection_550hPa, + winddirection_525hPa, winddirection_500hPa, + winddirection_475hPa, + winddirection_450hPa, + winddirection_425hPa, winddirection_400hPa, winddirection_300hPa, winddirection_250hPa, @@ -272,11 +356,25 @@ public enum HourlyOptionsParameter geopotential_height_950hPa, geopotential_height_925hPa, geopotential_height_900hPa, + geopotential_height_875hPa, geopotential_height_850hPa, + geopotential_height_825hPa, geopotential_height_800hPa, + geopotential_height_775hPa, + geopotential_height_750hPa, + geopotential_height_725hPa, geopotential_height_700hPa, + geopotential_height_675hPa, + geopotential_height_650hPa, + geopotential_height_625hPa, geopotential_height_600hPa, + geopotential_height_575hPa, + geopotential_height_550hPa, + geopotential_height_525hPa, geopotential_height_500hPa, + geopotential_height_475hPa, + geopotential_height_450hPa, + geopotential_height_425hPa, geopotential_height_400hPa, geopotential_height_300hPa, geopotential_height_250hPa, From 91f9d4fdb6af0c08f1120e1f8d7353232c0a93d4 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Tue, 30 Apr 2024 17:45:26 -0700 Subject: [PATCH 21/34] Add new pressure level options --- OpenMeteo/Hourly.cs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/OpenMeteo/Hourly.cs b/OpenMeteo/Hourly.cs index d25879e..321f6b9 100644 --- a/OpenMeteo/Hourly.cs +++ b/OpenMeteo/Hourly.cs @@ -67,10 +67,21 @@ public class Hourly public float?[]? Temperature_950hPa { get; set; } public float?[]? Temperature_925hPa { get; set; } public float?[]? Temperature_900hPa { get; set; } + public float?[]? Temperature_875hPa { get; set; } public float?[]? Temperature_850hPa { get; set; } + public float?[]? Temperature_825hPa { get; set; } public float?[]? Temperature_800hPa { get; set; } + public float?[]? Temperature_775hPa { get; set; } + public float?[]? Temperature_750hPa { get; set; } + public float?[]? Temperature_725hPa { get; set; } public float?[]? Temperature_700hPa { get; set; } + public float?[]? Temperature_675hPa { get; set; } + public float?[]? Temperature_650hPa { get; set; } + public float?[]? Temperature_625hPa { get; set; } public float?[]? Temperature_600hPa { get; set; } + public float?[]? Temperature_575hPa { get; set; } + public float?[]? Temperature_550hPa { get; set; } + public float?[]? Temperature_525hPa { get; set; } public float?[]? Temperature_500hPa { get; set; } public float?[]? Temperature_400hPa { get; set; } public float?[]? Temperature_300hPa { get; set; } @@ -105,10 +116,21 @@ public class Hourly public int?[]? Relativehumidity_950hPa { get; set; } public int?[]? Relativehumidity_925hPa { get; set; } public int?[]? Relativehumidity_900hPa { get; set; } + public int?[]? Relativehumidity_875hPa { get; set; } public int?[]? Relativehumidity_850hPa { get; set; } + public int?[]? Relativehumidity_825hPa { get; set; } public int?[]? Relativehumidity_800hPa { get; set; } + public int?[]? Relativehumidity_775hPa { get; set; } + public int?[]? Relativehumidity_750hPa { get; set; } + public int?[]? Relativehumidity_725hPa { get; set; } public int?[]? Relativehumidity_700hPa { get; set; } + public int?[]? Relativehumidity_675hPa { get; set; } + public int?[]? Relativehumidity_650hPa { get; set; } + public int?[]? Relativehumidity_625hPa { get; set; } public int?[]? Relativehumidity_600hPa { get; set; } + public int?[]? Relativehumidity_575hPa { get; set; } + public int?[]? Relativehumidity_550hPa { get; set; } + public int?[]? Relativehumidity_525hPa { get; set; } public int?[]? Relativehumidity_500hPa { get; set; } public int?[]? Relativehumidity_400hPa { get; set; } public int?[]? Relativehumidity_300hPa { get; set; } @@ -124,10 +146,21 @@ public class Hourly public int?[]? Cloudcover_950hPa { get; set; } public int?[]? Cloudcover_925hPa { get; set; } public int?[]? Cloudcover_900hPa { get; set; } + public int?[]? Cloudcover_875hPa { get; set; } public int?[]? Cloudcover_850hPa { get; set; } + public int?[]? Cloudcover_825hPa { get; set; } public int?[]? Cloudcover_800hPa { get; set; } + public int?[]? Cloudcover_775hPa { get; set; } + public int?[]? Cloudcover_750hPa { get; set; } + public int?[]? Cloudcover_725hPa { get; set; } public int?[]? Cloudcover_700hPa { get; set; } + public int?[]? Cloudcover_675hPa { get; set; } + public int?[]? Cloudcover_650hPa { get; set; } + public int?[]? Cloudcover_625hPa { get; set; } public int?[]? Cloudcover_600hPa { get; set; } + public int?[]? Cloudcover_575hPa { get; set; } + public int?[]? Cloudcover_550hPa { get; set; } + public int?[]? Cloudcover_525hPa { get; set; } public int?[]? Cloudcover_500hPa { get; set; } public int?[]? Cloudcover_400hPa { get; set; } public int?[]? Cloudcover_300hPa { get; set; } @@ -181,10 +214,21 @@ public class Hourly public float?[]? Geopotential_height_950hPa { get; set; } public float?[]? Geopotential_height_925hPa { get; set; } public float?[]? Geopotential_height_900hPa { get; set; } + public float?[]? Geopotential_height_875hPa { get; set; } public float?[]? Geopotential_height_850hPa { get; set; } + public float?[]? Geopotential_height_825hPa { get; set; } public float?[]? Geopotential_height_800hPa { get; set; } + public float?[]? Geopotential_height_775hPa { get; set; } + public float?[]? Geopotential_height_750hPa { get; set; } + public float?[]? Geopotential_height_725hPa { get; set; } public float?[]? Geopotential_height_700hPa { get; set; } + public float?[]? Geopotential_height_675hPa { get; set; } + public float?[]? Geopotential_height_650hPa { get; set; } + public float?[]? Geopotential_height_625hPa { get; set; } public float?[]? Geopotential_height_600hPa { get; set; } + public float?[]? Geopotential_height_575hPa { get; set; } + public float?[]? Geopotential_height_550hPa { get; set; } + public float?[]? Geopotential_height_525hPa { get; set; } public float?[]? Geopotential_height_500hPa { get; set; } public float?[]? Geopotential_height_400hPa { get; set; } public float?[]? Geopotential_height_300hPa { get; set; } From cffdca273af961979a9b99cd343c584db600a7b3 Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Tue, 30 Apr 2024 17:49:52 -0700 Subject: [PATCH 22/34] Fix warning --- OpenMeteo/OpenMeteoClientException.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/OpenMeteo/OpenMeteoClientException.cs b/OpenMeteo/OpenMeteoClientException.cs index 6c70617..e6cc822 100644 --- a/OpenMeteo/OpenMeteoClientException.cs +++ b/OpenMeteo/OpenMeteoClientException.cs @@ -3,13 +3,7 @@ namespace OpenMeteo { - public class OpenMeteoClientException : HttpRequestException + public class OpenMeteoClientException(string message, HttpStatusCode httpStatusCode) : HttpRequestException(message, null, httpStatusCode) { - public HttpStatusCode StatusCode { get; } - - public OpenMeteoClientException(string message, HttpStatusCode httpStatusCode) : base(message) - { - StatusCode = httpStatusCode; - } } } From 6b91bdef0c62efa0d220144aae036d865e6dd442 Mon Sep 17 00:00:00 2001 From: colinnuk Date: Mon, 26 Aug 2024 18:11:46 -0700 Subject: [PATCH 23/34] Add UK Met Office models --- OpenMeteo/WeatherModelOptions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OpenMeteo/WeatherModelOptions.cs b/OpenMeteo/WeatherModelOptions.cs index 0d9d94b..604dd92 100644 --- a/OpenMeteo/WeatherModelOptions.cs +++ b/OpenMeteo/WeatherModelOptions.cs @@ -130,6 +130,8 @@ public enum WeatherModelOptionsParameter bom_access_global, arpae_cosmo_2i, arpae_cosmo_2i_ruc, - arpae_cosmo_5m + arpae_cosmo_5m, + ukmo_global_deterministic_10km, + ukmo_global_deterministic_2km, } } From c1ac2f1f21bec6d2868788582423954d339b6dd4 Mon Sep 17 00:00:00 2001 From: colinnuk Date: Mon, 26 Aug 2024 18:17:45 -0700 Subject: [PATCH 24/34] Fix UK model name --- OpenMeteo/WeatherModelOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenMeteo/WeatherModelOptions.cs b/OpenMeteo/WeatherModelOptions.cs index 604dd92..d3472b4 100644 --- a/OpenMeteo/WeatherModelOptions.cs +++ b/OpenMeteo/WeatherModelOptions.cs @@ -132,6 +132,6 @@ public enum WeatherModelOptionsParameter arpae_cosmo_2i_ruc, arpae_cosmo_5m, ukmo_global_deterministic_10km, - ukmo_global_deterministic_2km, + ukmo_uk_deterministic_2km, } } From 4456015020c2d91fec3b49f1107c1685928721e7 Mon Sep 17 00:00:00 2001 From: colinnuk Date: Thu, 29 Aug 2024 06:58:41 -0700 Subject: [PATCH 25/34] Add apiKey to the constructor and remove rethrowExceptions from the constructor (this will be set by a setter method instead) --- OpenMeteo/OpenMeteoClient.cs | 47 +++++++++++++++++++++----- OpenMeteoTests/WeatherForecastTests.cs | 8 +++-- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index cb8d057..45bf524 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -18,7 +18,13 @@ public class OpenMeteoClient private readonly HttpController httpController; private readonly IOpenMeteoLogger? _logger = default!; - private readonly bool _rethrowExceptions = false; + private readonly string _apiKey = string.Empty; + + /// + /// If set to true, exceptions from the OpenMeteo API will be rethrown. Default is false. + /// + /// + public bool RethrowExceptions { get; set; } = false; /// /// Creates a new object and sets the neccessary variables (httpController, CultureInfo) @@ -29,13 +35,36 @@ public OpenMeteoClient() } /// - /// Creates a new object and sets the neccessary variables (httpController, CultureInfo) + /// Creates a new object with a logger /// - public OpenMeteoClient(bool rethrowExceptions, IOpenMeteoLogger? logger = null) + /// An object which implements an interface that can be used for logging from this class + public OpenMeteoClient(IOpenMeteoLogger logger) { httpController = new HttpController(); _logger = logger; - _rethrowExceptions = rethrowExceptions; + } + + /// + /// Creates a new object with a logger and an API key + /// + /// The API key to use the customer OpenMeteo URLs such as https://customer-api.open-meteo.com + public OpenMeteoClient(string apiKey) + { + httpController = new HttpController(); + _apiKey = apiKey; + } + + /// + /// Creates a new object with a logger and an API key + /// + /// An object which implements an interface that can be used for logging from this class + /// The API key to use the customer OpenMeteo URLs such as https://customer-api.open-meteo.com + + public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) + { + httpController = new HttpController(); + _logger = logger; + _apiKey = apiKey; } /// @@ -45,7 +74,7 @@ public OpenMeteoClient(bool rethrowExceptions, IOpenMeteoLogger? logger = null) /// If successful returns an awaitable Task containing WeatherForecast or NULL if request failed public async Task QueryAsync(string location) { - GeocodingOptions geocodingOptions = new GeocodingOptions(location); + GeocodingOptions geocodingOptions = new(location); // Get location Information GeocodingApiResponse? response = await GetGeocodingDataAsync(geocodingOptions); @@ -234,7 +263,7 @@ public OpenMeteoClient(bool rethrowExceptions, IOpenMeteoLogger? logger = null) catch (HttpRequestException e) { _logger?.Warning($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); - if (_rethrowExceptions) + if (RethrowExceptions) throw; return null; } @@ -341,7 +370,7 @@ public string WeathercodeToString(int weathercode) catch (Exception e) { _logger?.Warning($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); - if (_rethrowExceptions) + if (RethrowExceptions) throw; return null; } @@ -365,7 +394,7 @@ public string WeathercodeToString(int weathercode) catch (HttpRequestException e) { _logger?.Warning($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); - if (_rethrowExceptions) + if (RethrowExceptions) throw; return null; } @@ -388,7 +417,7 @@ public string WeathercodeToString(int weathercode) { _logger?.Warning($"Can't find elevation for latitude {options.Latitude} & longitude {options.Longitude}. Please make sure that they are valid."); _logger?.Warning($"Error in {nameof(OpenMeteoClient)}.GetElevationAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); - if (_rethrowExceptions) + if (RethrowExceptions) throw; return null; } diff --git a/OpenMeteoTests/WeatherForecastTests.cs b/OpenMeteoTests/WeatherForecastTests.cs index 0e3301a..1b944ec 100644 --- a/OpenMeteoTests/WeatherForecastTests.cs +++ b/OpenMeteoTests/WeatherForecastTests.cs @@ -1,6 +1,5 @@ using System.Globalization; using System.Linq; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -130,9 +129,12 @@ public void WeatherForecast_With_All_Options_Test() [TestMethod] public async Task Latitude_Longitude_No_Data_For_Selected_Forecast_Rethrows_Test() { - OpenMeteoClient client = new(true); + OpenMeteoClient client = new() + { + RethrowExceptions = true + }; - WeatherForecastOptions options = new WeatherForecastOptions + WeatherForecastOptions options = new() { Latitude = 1, Longitude = 1, From 0ed740fa19cfeafc657bdb668fc5a29c4f1b3ff3 Mon Sep 17 00:00:00 2001 From: colinnuk Date: Thu, 29 Aug 2024 07:05:21 -0700 Subject: [PATCH 26/34] Move the WeatherCode method to its own class --- OpenMeteo/OpenMeteoClient.cs | 71 ------------------------ OpenMeteo/WeatherCodeHelper.cs | 44 +++++++++++++++ OpenMeteoTests/OpenMeteoClientTests.cs | 27 --------- OpenMeteoTests/WeatherCodeHelperTests.cs | 26 +++++++++ 4 files changed, 70 insertions(+), 98 deletions(-) create mode 100644 OpenMeteo/WeatherCodeHelper.cs delete mode 100644 OpenMeteoTests/OpenMeteoClientTests.cs create mode 100644 OpenMeteoTests/WeatherCodeHelperTests.cs diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index 45bf524..8368acf 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -268,77 +268,6 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) return null; } } - - /// - /// Converts a given weathercode to it's string representation - /// - /// - /// Weathercode string representation - public string WeathercodeToString(int weathercode) - { - switch (weathercode) - { - case 0: - return "Clear sky"; - case 1: - return "Mainly clear"; - case 2: - return "Partly cloudy"; - case 3: - return "Overcast"; - case 45: - return "Fog"; - case 48: - return "Depositing rime Fog"; - case 51: - return "Light drizzle"; - case 53: - return "Moderate drizzle"; - case 55: - return "Dense drizzle"; - case 56: - return "Light freezing drizzle"; - case 57: - return "Dense freezing drizzle"; - case 61: - return "Slight rain"; - case 63: - return "Moderate rain"; - case 65: - return "Heavy rain"; - case 66: - return "Light freezing rain"; - case 67: - return "Heavy freezing rain"; - case 71: - return "Slight snow fall"; - case 73: - return "Moderate snow fall"; - case 75: - return "Heavy snow fall"; - case 77: - return "Snow grains"; - case 80: - return "Slight rain showers"; - case 81: - return "Moderate rain showers"; - case 82: - return "Violent rain showers"; - case 85: - return "Slight snow showers"; - case 86: - return "Heavy snow showers"; - case 95: - return "Thunderstorm"; - case 96: - return "Thunderstorm with light hail"; - case 99: - return "Thunderstorm with heavy hail"; - default: - return "Invalid weathercode"; - } - } - private async Task GetWeatherForecastAsync(WeatherForecastOptions options) { try diff --git a/OpenMeteo/WeatherCodeHelper.cs b/OpenMeteo/WeatherCodeHelper.cs new file mode 100644 index 0000000..22e1f7a --- /dev/null +++ b/OpenMeteo/WeatherCodeHelper.cs @@ -0,0 +1,44 @@ +namespace OpenMeteo; +public static class WeatherCodeHelper +{ + /// + /// Converts a given weathercode to it's string representation + /// + /// + /// Weathercode string representation + public static string WeathercodeToString(int weathercode) + { + return weathercode 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", + 56 => "Light freezing drizzle", + 57 => "Dense freezing drizzle", + 61 => "Slight rain", + 63 => "Moderate rain", + 65 => "Heavy rain", + 66 => "Light freezing rain", + 67 => "Heavy freezing rain", + 71 => "Slight snow fall", + 73 => "Moderate snow fall", + 75 => "Heavy snow fall", + 77 => "Snow grains", + 80 => "Slight rain showers", + 81 => "Moderate rain showers", + 82 => "Violent rain showers", + 85 => "Slight snow showers", + 86 => "Heavy snow showers", + 95 => "Thunderstorm", + 96 => "Thunderstorm with light hail", + 99 => "Thunderstorm with heavy hail", + _ => "Invalid weathercode", + }; + } +} diff --git a/OpenMeteoTests/OpenMeteoClientTests.cs b/OpenMeteoTests/OpenMeteoClientTests.cs deleted file mode 100644 index 68254d7..0000000 --- a/OpenMeteoTests/OpenMeteoClientTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenMeteo; - -namespace OpenMeteoTests -{ - [TestClass] - public class OpenMeteoClientTests - { - [TestMethod] - public void Weather_Codes_To_String_Tests() - { - OpenMeteoClient client = new OpenMeteoClient(); - int[] testWeatherCodes = { 0, 1, 2, 3, 51, 53, 96, 99, 100 }; - foreach (var weatherCode in testWeatherCodes) - { - string weatherCodeString = client.WeathercodeToString(weatherCode); - Assert.IsInstanceOfType(weatherCodeString, typeof(string)); - - if (weatherCode == 0) - Assert.AreEqual("Clear sky", weatherCodeString); - - if (weatherCode == 100) - Assert.AreEqual("Invalid weathercode", weatherCodeString); - } - } - } -} \ No newline at end of file diff --git a/OpenMeteoTests/WeatherCodeHelperTests.cs b/OpenMeteoTests/WeatherCodeHelperTests.cs new file mode 100644 index 0000000..676412f --- /dev/null +++ b/OpenMeteoTests/WeatherCodeHelperTests.cs @@ -0,0 +1,26 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenMeteo; + +namespace OpenMeteoTests +{ + [TestClass] + public class WeatherCodeHelperTests + { + [DataTestMethod] + [DataRow(0, "Clear sky")] + [DataRow(1, "Mainly clear")] + [DataRow(2, "Partly cloudy")] + [DataRow(3, "Overcast")] + [DataRow(51, "Light drizzle")] + [DataRow(53, "Moderate drizzle")] + [DataRow(96, "Thunderstorm with light hail")] + [DataRow(99, "Thunderstorm with heavy hail")] + [DataRow(100, "Invalid weathercode")] + public void Weather_Codes_To_String_Tests(int weatherCode, string expectedString) + { + string weatherCodeString = WeatherCodeHelper.WeathercodeToString(weatherCode); + Assert.IsInstanceOfType(weatherCodeString, typeof(string)); + Assert.AreEqual(expectedString, weatherCodeString); + } + } +} \ No newline at end of file From 7e802f04bf86d1e37d640905a9039245086ee37b Mon Sep 17 00:00:00 2001 From: colinnuk Date: Thu, 29 Aug 2024 08:30:34 -0700 Subject: [PATCH 27/34] Refactor URL builder code into its own class and support apiKeys --- OpenMeteo/ElevationOptions.cs | 2 +- OpenMeteo/OpenMeteoClient.cs | 276 +----------------------------- OpenMeteo/UrlFactory.cs | 245 ++++++++++++++++++++++++++ OpenMeteoTests/UrlFactoryTests.cs | 123 +++++++++++++ 4 files changed, 376 insertions(+), 270 deletions(-) create mode 100644 OpenMeteo/UrlFactory.cs create mode 100644 OpenMeteoTests/UrlFactoryTests.cs diff --git a/OpenMeteo/ElevationOptions.cs b/OpenMeteo/ElevationOptions.cs index 70459f8..2d0b449 100644 --- a/OpenMeteo/ElevationOptions.cs +++ b/OpenMeteo/ElevationOptions.cs @@ -1,6 +1,6 @@ namespace OpenMeteo { - internal class ElevationOptions + public class ElevationOptions { public ElevationOptions(float latitude, float longitude) { diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index 8368acf..835f621 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -2,7 +2,6 @@ using System.Net.Http; using System.Threading.Tasks; using System.Text.Json; -using System.Globalization; namespace OpenMeteo { @@ -11,14 +10,9 @@ namespace OpenMeteo /// public class OpenMeteoClient { - private readonly string _weatherApiUrl = "https://api.open-meteo.com/v1/forecast"; - private readonly string _geocodeApiUrl = "https://geocoding-api.open-meteo.com/v1/search"; - private readonly string _airQualityApiUrl = "https://air-quality-api.open-meteo.com/v1/air-quality"; - private readonly string _elevationApiUrl = "https://api.open-meteo.com/v1/elevation"; private readonly HttpController httpController; - + private readonly UrlFactory _urlFactory = new(); private readonly IOpenMeteoLogger? _logger = default!; - private readonly string _apiKey = string.Empty; /// /// If set to true, exceptions from the OpenMeteo API will be rethrown. Default is false. @@ -51,7 +45,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger) public OpenMeteoClient(string apiKey) { httpController = new HttpController(); - _apiKey = apiKey; + _urlFactory = new UrlFactory(apiKey); } /// @@ -64,7 +58,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { httpController = new HttpController(); _logger = logger; - _apiKey = apiKey; + _urlFactory = new UrlFactory(apiKey); } /// @@ -252,7 +246,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { try { - var url = MergeUrlWithOptions(_airQualityApiUrl, options); + var url = _urlFactory.GetUrlWithOptions(options); _logger?.Debug($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). URL: {url}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); @@ -272,7 +266,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { try { - var url = MergeUrlWithOptions(_weatherApiUrl, options); + var url = _urlFactory.GetUrlWithOptions(options); _logger?.Debug($"{nameof(OpenMeteoClient)}.GetElevationAsync(). URL: {url}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); if(response.IsSuccessStatusCode) @@ -311,7 +305,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) try { - var url = MergeUrlWithOptions(_geocodeApiUrl, options); + var url = _urlFactory.GetUrlWithOptions(options); _logger?.Debug($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). URL: {url}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); @@ -333,7 +327,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { try { - var url = MergeUrlWithOptions(_elevationApiUrl, options); + var url = _urlFactory.GetUrlWithOptions(options); _logger?.Debug($"{nameof(OpenMeteoClient)}.GetElevationAsync(). URL: {url}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); @@ -351,262 +345,6 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) return null; } } - - private string MergeUrlWithOptions(string url, WeatherForecastOptions? options) - { - if (options == null) return url; - - UriBuilder uri = new UriBuilder(url); - bool isFirstParam = false; - - // If no query given, add '?' to start the query string - if (uri.Query == string.Empty) - { - uri.Query = "?"; - - // isFirstParam becomes true because the query string is new - isFirstParam = true; - } - - // Add the properties - - // Begin with Latitude and Longitude since they're required - if (isFirstParam) - uri.Query += "latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); - else - uri.Query += "&latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); - - uri.Query += "&longitude=" + options.Longitude.ToString(CultureInfo.InvariantCulture); - - uri.Query += "&temperature_unit=" + options.Temperature_Unit.ToString(); - uri.Query += "&windspeed_unit=" + options.Windspeed_Unit.ToString(); - uri.Query += "&precipitation_unit=" + options.Precipitation_Unit.ToString(); - if (options.Timezone != string.Empty) - uri.Query += "&timezone=" + options.Timezone; - - uri.Query += "&timeformat=" + options.Timeformat.ToString(); - - uri.Query += "&past_days=" + options.Past_Days; - - if (options.Start_date != string.Empty) - uri.Query += "&start_date=" + options.Start_date; - if (options.End_date != string.Empty) - uri.Query += "&end_date=" + options.End_date; - - // Now we iterate through hourly and daily - - // Hourly - if (options.Hourly.Count > 0) - { - bool firstHourlyElement = true; - uri.Query += "&hourly="; - - foreach (var option in options.Hourly) - { - if (firstHourlyElement) - { - uri.Query += option.ToString(); - firstHourlyElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - // Daily - if (options.Daily.Count > 0) - { - bool firstDailyElement = true; - uri.Query += "&daily="; - foreach (var option in options.Daily) - { - if (firstDailyElement) - { - uri.Query += option.ToString(); - firstDailyElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - // 0.2.0 Weather models - // cell_selection - uri.Query += "&cell_selection=" + options.Cell_Selection; - - // Models - if (options.Models.Count > 0) - { - bool firstModelsElement = true; - uri.Query += "&models="; - foreach (var option in options.Models) - { - if (firstModelsElement) - { - uri.Query += option.ToString(); - firstModelsElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - // new current parameter - if (options.Current.Count > 0) - { - bool firstCurrentElement = true; - uri.Query += "¤t="; - foreach (var option in options.Current) - { - if (firstCurrentElement) - { - uri.Query += option.ToString(); - firstCurrentElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - // new minutely_15 parameter - if (options.Minutely15.Count > 0) - { - bool firstMinutelyElement = true; - uri.Query += "&minutely_15="; - foreach (var option in options.Minutely15) - { - if (firstMinutelyElement) - { - uri.Query += option.ToString(); - firstMinutelyElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - return uri.ToString(); - } - - /// - /// Combines a given url with an options object to create a url for GET requests - /// - /// url+queryString - private string MergeUrlWithOptions(string url, GeocodingOptions options) - { - if (options == null) return url; - - UriBuilder uri = new UriBuilder(url); - bool isFirstParam = false; - - // If no query given, add '?' to start the query string - if (uri.Query == string.Empty) - { - uri.Query = "?"; - - // isFirstParam becomes true because the query string is new - isFirstParam = true; - } - - // Now we check every property and set the value, if neccessary - if (isFirstParam) - uri.Query += "name=" + options.Name; - else - uri.Query += "&name=" + options.Name; - - if(options.Count >0) - uri.Query += "&count=" + options.Count; - - if (options.Format != string.Empty) - uri.Query += "&format=" + options.Format; - - if (options.Language != string.Empty) - uri.Query += "&language=" + options.Language; - - return uri.ToString(); - } - - /// - /// Combines a given url with an options object to create a url for GET requests - /// - /// url+queryString - private string MergeUrlWithOptions(string url, AirQualityOptions options) - { - if (options == null) return url; - - UriBuilder uri = new UriBuilder(url); - bool isFirstParam = false; - - // If no query given, add '?' to start the query string - if (uri.Query == string.Empty) - { - uri.Query = "?"; - - // isFirstParam becomes true because the query string is new - isFirstParam = true; - } - - // Now we check every property and set the value, if neccessary - if (isFirstParam) - uri.Query += "latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); - else - uri.Query += "&latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); - - uri.Query += "&longitude=" + options.Longitude.ToString(CultureInfo.InvariantCulture); - - if (options.Domains != string.Empty) - uri.Query += "&domains=" + options.Domains; - - if (options.Timeformat != string.Empty) - uri.Query += "&timeformat=" + options.Timeformat; - - if (options.Timezone != string.Empty) - uri.Query += "&timezone=" + options.Timezone; - - // Finally add hourly array - if (options.Hourly.Count >= 0) - { - bool firstHourlyElement = true; - uri.Query += "&hourly="; - - foreach (var option in options.Hourly) - { - if (firstHourlyElement) - { - uri.Query += option.ToString(); - firstHourlyElement = false; - } - else - { - uri.Query += "," + option.ToString(); - } - } - } - - return uri.ToString(); - } - - private string MergeUrlWithOptions(string url, ElevationOptions options) - { - if (options == null) return url; - - UriBuilder uri = new UriBuilder(url) - { - Query = $"?latitude={options.Latitude.ToString(CultureInfo.InvariantCulture)}&longitude={options.Longitude.ToString(CultureInfo.InvariantCulture)}" - }; - - return uri.ToString(); - } } } diff --git a/OpenMeteo/UrlFactory.cs b/OpenMeteo/UrlFactory.cs new file mode 100644 index 0000000..a5cd343 --- /dev/null +++ b/OpenMeteo/UrlFactory.cs @@ -0,0 +1,245 @@ +using System; +using System.Globalization; + +namespace OpenMeteo; +public class UrlFactory +{ + private readonly string _weatherApiUrl = "api.open-meteo.com/v1/forecast"; + private readonly string _geocodeApiUrl = "geocoding-api.open-meteo.com/v1/search"; + private readonly string _airQualityApiUrl = "air-quality-api.open-meteo.com/v1/air-quality"; + private readonly string _elevationApiUrl = "api.open-meteo.com/v1/elevation"; + private readonly string _customerApiUrlFragment = "customer-"; + + private readonly string _apiKey = string.Empty; + + public UrlFactory() + { + } + + public UrlFactory(string apiKey) + { + _apiKey = apiKey; + } + + public string GetUrlWithOptions(WeatherForecastOptions options) + { + UriBuilder uri = new(GetBaseUrl(_weatherApiUrl)); + + // Add the properties + // Begin with Latitude and Longitude since they're required + uri.Query = "latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); + uri.Query += "&longitude=" + options.Longitude.ToString(CultureInfo.InvariantCulture); + + uri.Query += "&temperature_unit=" + options.Temperature_Unit.ToString(); + uri.Query += "&windspeed_unit=" + options.Windspeed_Unit.ToString(); + uri.Query += "&precipitation_unit=" + options.Precipitation_Unit.ToString(); + if (options.Timezone != string.Empty) + uri.Query += "&timezone=" + options.Timezone; + + uri.Query += "&timeformat=" + options.Timeformat.ToString(); + + uri.Query += "&past_days=" + options.Past_Days; + + if (options.Start_date != string.Empty) + uri.Query += "&start_date=" + options.Start_date; + if (options.End_date != string.Empty) + uri.Query += "&end_date=" + options.End_date; + + // Now we iterate through hourly and daily + + // Hourly + if (options.Hourly.Count > 0) + { + bool firstHourlyElement = true; + uri.Query += "&hourly="; + + foreach (var option in options.Hourly) + { + if (firstHourlyElement) + { + uri.Query += option.ToString(); + firstHourlyElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + // Daily + if (options.Daily.Count > 0) + { + bool firstDailyElement = true; + uri.Query += "&daily="; + foreach (var option in options.Daily) + { + if (firstDailyElement) + { + uri.Query += option.ToString(); + firstDailyElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + // 0.2.0 Weather models + // cell_selection + uri.Query += "&cell_selection=" + options.Cell_Selection; + + // Models + if (options.Models.Count > 0) + { + bool firstModelsElement = true; + uri.Query += "&models="; + foreach (var option in options.Models) + { + if (firstModelsElement) + { + uri.Query += option.ToString(); + firstModelsElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + // new current parameter + if (options.Current.Count > 0) + { + bool firstCurrentElement = true; + uri.Query += "¤t="; + foreach (var option in options.Current) + { + if (firstCurrentElement) + { + uri.Query += option.ToString(); + firstCurrentElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + // new minutely_15 parameter + if (options.Minutely15.Count > 0) + { + bool firstMinutelyElement = true; + uri.Query += "&minutely_15="; + foreach (var option in options.Minutely15) + { + if (firstMinutelyElement) + { + uri.Query += option.ToString(); + firstMinutelyElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + SetApiKeyIfNeeded(uri); + return uri.ToString(); + } + + /// + /// Combines a given url with an options object to create a url for GET requests + /// + /// url+queryString + public string GetUrlWithOptions(GeocodingOptions options) + { + UriBuilder uri = new(GetBaseUrl(_geocodeApiUrl)); + + // Now we check every property and set the value, if neccessary + uri.Query = "name=" + options.Name; + + if (options.Count > 0) + uri.Query += "&count=" + options.Count; + + if (options.Format != string.Empty) + uri.Query += "&format=" + options.Format; + + if (options.Language != string.Empty) + uri.Query += "&language=" + options.Language; + + SetApiKeyIfNeeded(uri); + return uri.ToString(); + } + + /// + /// Combines a given url with an options object to create a url for GET requests + /// + /// url+queryString + public string GetUrlWithOptions(AirQualityOptions options) + { + UriBuilder uri = new(GetBaseUrl(_airQualityApiUrl)); + + // Now we check every property and set the value, if neccessary + uri.Query += "latitude=" + options.Latitude.ToString(CultureInfo.InvariantCulture); + uri.Query += "&longitude=" + options.Longitude.ToString(CultureInfo.InvariantCulture); + + if (options.Domains != string.Empty) + uri.Query += "&domains=" + options.Domains; + + if (options.Timeformat != string.Empty) + uri.Query += "&timeformat=" + options.Timeformat; + + if (options.Timezone != string.Empty) + uri.Query += "&timezone=" + options.Timezone; + + // Finally add hourly array + if (options.Hourly.Count >= 0) + { + bool firstHourlyElement = true; + uri.Query += "&hourly="; + + foreach (var option in options.Hourly) + { + if (firstHourlyElement) + { + uri.Query += option.ToString(); + firstHourlyElement = false; + } + else + { + uri.Query += "," + option.ToString(); + } + } + } + + SetApiKeyIfNeeded(uri); + return uri.ToString(); + } + + public string GetUrlWithOptions(ElevationOptions options) + { + UriBuilder uri = new(GetBaseUrl(_elevationApiUrl)) + { + Query = $"latitude={options.Latitude.ToString(CultureInfo.InvariantCulture)}&longitude={options.Longitude.ToString(CultureInfo.InvariantCulture)}" + }; + + SetApiKeyIfNeeded(uri); + return uri.ToString(); + } + + private void SetApiKeyIfNeeded(UriBuilder uri) + { + if (!string.IsNullOrEmpty(_apiKey)) + uri.Query += $"&apikey={_apiKey}"; + } + + private string GetBaseUrl(string url) + { + var prependCustomerIfHasApiKey = string.IsNullOrEmpty(_apiKey) ? string.Empty : _customerApiUrlFragment; + return $"https://{prependCustomerIfHasApiKey}{url}"; + } +} diff --git a/OpenMeteoTests/UrlFactoryTests.cs b/OpenMeteoTests/UrlFactoryTests.cs new file mode 100644 index 0000000..9fef65f --- /dev/null +++ b/OpenMeteoTests/UrlFactoryTests.cs @@ -0,0 +1,123 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenMeteo; + +namespace OpenMeteoTests +{ + [TestClass] + public class UrlFactoryTests + { + [TestMethod] + public void GetUrlWithOptions_WeatherForecastOptions_Test() + { + var factory = new UrlFactory(); + var url = factory.GetUrlWithOptions(GetWeatherForecastOptions()); + + var expectedUrl = "https://api.open-meteo.com:443/v1/forecast?latitude=40.7128&longitude=-74.006&temperature_unit=celsius&windspeed_unit=kmh&precipitation_unit=mm&timezone=America/New_York&timeformat=iso8601&past_days=2&start_date=2023-01-01&end_date=2023-01-02&hourly=temperature_2m,windspeed_10m&daily=temperature_2m_max,temperature_2m_min&cell_selection=nearest&models=gfs_hrrr,gfs_global¤t=temperature_2m&minutely_15=precipitation"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_GeocodingOptions_Test() + { + var factory = new UrlFactory(); + var url = factory.GetUrlWithOptions(GetGeocodingOptions()); + + var expectedUrl = "https://geocoding-api.open-meteo.com:443/v1/search?name=New York&count=100&format=json&language=en"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_AirQualityOptions_Test() + { + var factory = new UrlFactory(); + var url = factory.GetUrlWithOptions(GetAirQualityOptions()); + + var expectedUrl = "https://air-quality-api.open-meteo.com:443/v1/air-quality?latitude=40.7128&longitude=-74.006&domains=global&timeformat=iso8601&timezone=America/New_York&hourly=pm10,pm2_5"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_ElevationOptions_Test() + { + var factory = new UrlFactory(); + var url = factory.GetUrlWithOptions(GetElevationOptions()); + + var expectedUrl = "https://api.open-meteo.com:443/v1/elevation?latitude=40.7128&longitude=-74.006"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_WeatherForecastOptions_WithApiKey_Test() + { + var factory = new UrlFactory("testApiKey"); + var url = factory.GetUrlWithOptions(GetWeatherForecastOptions()); + + var expectedUrl = "https://customer-api.open-meteo.com:443/v1/forecast?latitude=40.7128&longitude=-74.006&temperature_unit=celsius&windspeed_unit=kmh&precipitation_unit=mm&timezone=America/New_York&timeformat=iso8601&past_days=2&start_date=2023-01-01&end_date=2023-01-02&hourly=temperature_2m,windspeed_10m&daily=temperature_2m_max,temperature_2m_min&cell_selection=nearest&models=gfs_hrrr,gfs_global¤t=temperature_2m&minutely_15=precipitation&apikey=testApiKey"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_GeocodingOptions_WithApiKey_Test() + { + var factory = new UrlFactory("testApiKey"); + var url = factory.GetUrlWithOptions(GetGeocodingOptions()); + + var expectedUrl = "https://customer-geocoding-api.open-meteo.com:443/v1/search?name=New York&count=100&format=json&language=en&apikey=testApiKey"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_AirQualityOptions_WithApiKey_Test() + { + var factory = new UrlFactory("testApiKey"); + var url = factory.GetUrlWithOptions(GetAirQualityOptions()); + + var expectedUrl = "https://customer-air-quality-api.open-meteo.com:443/v1/air-quality?latitude=40.7128&longitude=-74.006&domains=global&timeformat=iso8601&timezone=America/New_York&hourly=pm10,pm2_5&apikey=testApiKey"; + Assert.AreEqual(expectedUrl, url); + } + + [TestMethod] + public void GetUrlWithOptions_ElevationOptions_WithApiKey_Test() + { + var factory = new UrlFactory("testApiKey"); + var url = factory.GetUrlWithOptions(GetElevationOptions()); + + var expectedUrl = "https://customer-api.open-meteo.com:443/v1/elevation?latitude=40.7128&longitude=-74.006&apikey=testApiKey"; + Assert.AreEqual(expectedUrl, url); + } + + private static WeatherForecastOptions GetWeatherForecastOptions() => new() + { + Latitude = 40.7128f, + Longitude = -74.006f, + Temperature_Unit = TemperatureUnitType.celsius, + Windspeed_Unit = WindspeedUnitType.kmh, + Precipitation_Unit = PrecipitationUnitType.mm, + Timezone = "America/New_York", + Timeformat = TimeformatType.iso8601, + Past_Days = 2, + Start_date = "2023-01-01", + End_date = "2023-01-02", + Hourly = new HourlyOptions([HourlyOptionsParameter.temperature_2m, HourlyOptionsParameter.windspeed_10m]), + Daily = new DailyOptions([DailyOptionsParameter.temperature_2m_max, DailyOptionsParameter.temperature_2m_min]), + Cell_Selection = CellSelectionType.nearest, + Models = new WeatherModelOptions([WeatherModelOptionsParameter.gfs_hrrr, WeatherModelOptionsParameter.gfs_global]), + Current = new CurrentOptions([CurrentOptionsParameter.temperature_2m]), + Minutely15 = new Minutely15Options([Minutely15OptionsParameter.precipitation]) + }; + + private static GeocodingOptions GetGeocodingOptions() => new("New York"); + + private static AirQualityOptions GetAirQualityOptions() => new() + { + Latitude = 40.7128f, + Longitude = -74.006f, + Domains = "global", + Timeformat = "iso8601", + Timezone = "America/New_York", + Hourly = new AirQualityOptions.HourlyOptions([AirQualityOptions.HourlyOptionsParameter.pm10, AirQualityOptions.HourlyOptionsParameter.pm2_5]) + }; + + private static ElevationOptions GetElevationOptions() => new(40.7128f, -74.006f); + } +} From 1ac61f7af1037d08f95af0ef504fcfbee6c2a465 Mon Sep 17 00:00:00 2001 From: colinnuk Date: Fri, 30 Aug 2024 06:27:20 -0700 Subject: [PATCH 28/34] Remove APIKEY from logs --- OpenMeteo/OpenMeteoClient.cs | 12 ++++++------ OpenMeteo/UrlFactory.cs | 5 +++++ OpenMeteoTests/UrlFactoryTests.cs | 24 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index 835f621..7471d3c 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -127,7 +127,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) /// Awaitable Task containing WeatherForecast or NULL public async Task QueryAsync(float latitude, float longitude) { - WeatherForecastOptions options = new WeatherForecastOptions + WeatherForecastOptions options = new() { Latitude = latitude, Longitude = longitude, @@ -171,7 +171,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) /// public async Task GetLocationDataAsync(string location) { - GeocodingOptions geocodingOptions = new GeocodingOptions(location); + GeocodingOptions geocodingOptions = new(location); return await GetLocationDataAsync(geocodingOptions); } @@ -247,7 +247,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) try { var url = _urlFactory.GetUrlWithOptions(options); - _logger?.Debug($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). URL: {url}"); + _logger?.Debug($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). URL: {_urlFactory.SanitiseUrl(url)}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); @@ -267,7 +267,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) try { var url = _urlFactory.GetUrlWithOptions(options); - _logger?.Debug($"{nameof(OpenMeteoClient)}.GetElevationAsync(). URL: {url}"); + _logger?.Debug($"{nameof(OpenMeteoClient)}.GetElevationAsync(). URL: {_urlFactory.SanitiseUrl(url)}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); if(response.IsSuccessStatusCode) { @@ -306,7 +306,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { var url = _urlFactory.GetUrlWithOptions(options); - _logger?.Debug($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). URL: {url}"); + _logger?.Debug($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). URL: {_urlFactory.SanitiseUrl(url)}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); @@ -328,7 +328,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) try { var url = _urlFactory.GetUrlWithOptions(options); - _logger?.Debug($"{nameof(OpenMeteoClient)}.GetElevationAsync(). URL: {url}"); + _logger?.Debug($"{nameof(OpenMeteoClient)}.GetElevationAsync(). URL: {_urlFactory.SanitiseUrl(url)}"); HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); diff --git a/OpenMeteo/UrlFactory.cs b/OpenMeteo/UrlFactory.cs index a5cd343..8dc5b28 100644 --- a/OpenMeteo/UrlFactory.cs +++ b/OpenMeteo/UrlFactory.cs @@ -21,6 +21,11 @@ public UrlFactory(string apiKey) _apiKey = apiKey; } + public string SanitiseUrl(string url) + { + return string.IsNullOrEmpty(_apiKey) ? url : url.Replace(_apiKey, "APIKEY"); + } + public string GetUrlWithOptions(WeatherForecastOptions options) { UriBuilder uri = new(GetBaseUrl(_weatherApiUrl)); diff --git a/OpenMeteoTests/UrlFactoryTests.cs b/OpenMeteoTests/UrlFactoryTests.cs index 9fef65f..5730e4d 100644 --- a/OpenMeteoTests/UrlFactoryTests.cs +++ b/OpenMeteoTests/UrlFactoryTests.cs @@ -86,6 +86,30 @@ public void GetUrlWithOptions_ElevationOptions_WithApiKey_Test() Assert.AreEqual(expectedUrl, url); } + [TestMethod] + public void SanitiseUrl_WithApiKey_Test() + { + var factory = new UrlFactory("testApiKey"); + var url = "https://api.open-meteo.com/v1/forecast?apikey=testApiKey"; + var sanitisedUrl = factory.SanitiseUrl(url); + + var expectedUrl = "https://api.open-meteo.com/v1/forecast?apikey=APIKEY"; + Assert.AreEqual(expectedUrl, sanitisedUrl); + } + + [TestMethod] + public void SanitiseUrl_WithNoApiKey_Test() + { + var factory = new UrlFactory(); + var url = "https://api.open-meteo.com/v1/forecast"; + var sanitisedUrl = factory.SanitiseUrl(url); + + var expectedUrl = "https://api.open-meteo.com/v1/forecast"; + Assert.AreEqual(expectedUrl, sanitisedUrl); + } + + + private static WeatherForecastOptions GetWeatherForecastOptions() => new() { Latitude = 40.7128f, From ee909973c7504470bff65a5a173209e7bfc4fb5c Mon Sep 17 00:00:00 2001 From: colinnuk Date: Fri, 30 Aug 2024 07:36:50 -0700 Subject: [PATCH 29/34] Add log to confirm API Key is being used --- OpenMeteo/OpenMeteoClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index 7471d3c..b51f063 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -58,6 +58,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { httpController = new HttpController(); _logger = logger; + _logger?.Information($"{nameof(OpenMeteoClient)} Initialised with API Key starting with: {apiKey.Substring(0, 2)}"); _urlFactory = new UrlFactory(apiKey); } From 11aa7f40d5303fa2508f0a8a63f78e9a928582cf Mon Sep 17 00:00:00 2001 From: colinnuk Date: Fri, 30 Aug 2024 07:49:35 -0700 Subject: [PATCH 30/34] Fix issue when passing in empty string - if string is empty then it'll be as if no apikey was used --- OpenMeteo/OpenMeteoClient.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index b51f063..edd37fa 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -58,8 +58,10 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { httpController = new HttpController(); _logger = logger; - _logger?.Information($"{nameof(OpenMeteoClient)} Initialised with API Key starting with: {apiKey.Substring(0, 2)}"); _urlFactory = new UrlFactory(apiKey); + + if (!string.IsNullOrEmpty(apiKey)) + _logger?.Information($"{nameof(OpenMeteoClient)} Initialised with API key starting with: {apiKey[..2]}"); } /// From 02abbc54a9202f48bcad9cf409050b47d70986ea Mon Sep 17 00:00:00 2001 From: Colin Noakes Date: Fri, 30 Aug 2024 07:53:36 -0700 Subject: [PATCH 31/34] Update package-and-deploy.yml --- .github/workflows/package-and-deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/package-and-deploy.yml b/.github/workflows/package-and-deploy.yml index 89874ef..13fd26d 100644 --- a/.github/workflows/package-and-deploy.yml +++ b/.github/workflows/package-and-deploy.yml @@ -1,6 +1,7 @@ name: Package, Deploy and create new release on: + workflow_dispatch: push: branches: [ master ] paths: From 1dfb30b016e9240fad08aaf90b98138b878189c7 Mon Sep 17 00:00:00 2001 From: colinnuk Date: Sat, 19 Oct 2024 15:48:37 -0700 Subject: [PATCH 32/34] Upgrade vulnerable package --- OpenMeteo/OpenMeteo.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenMeteo/OpenMeteo.csproj b/OpenMeteo/OpenMeteo.csproj index e4900f0..ccb0f56 100644 --- a/OpenMeteo/OpenMeteo.csproj +++ b/OpenMeteo/OpenMeteo.csproj @@ -43,7 +43,7 @@ - + From c953107d38d01f23b4061e8da5d7ad01c4adf1bf Mon Sep 17 00:00:00 2001 From: colinnuk Date: Sat, 19 Oct 2024 16:42:29 -0700 Subject: [PATCH 33/34] Add a method to get the weather forecast metadata --- OpenMeteo/MetadataApiModel.cs | 12 +++++++ OpenMeteo/MetadataModel.cs | 11 +++++++ OpenMeteo/MetadataNameHelper.cs | 27 ++++++++++++++++ OpenMeteo/OpenMeteoClient.cs | 43 +++++++++++++++++++++---- OpenMeteo/UrlFactory.cs | 7 ++++ OpenMeteo/WeatherModelOptions.cs | 10 ++---- OpenMeteoTests/MetadataTests.cs | 44 +++++++++++++++++++++++++ OpenMeteoTests/UrlFactoryTests.cs | 53 +++++++++++++++++++++++++++++++ 8 files changed, 193 insertions(+), 14 deletions(-) create mode 100644 OpenMeteo/MetadataApiModel.cs create mode 100644 OpenMeteo/MetadataModel.cs create mode 100644 OpenMeteo/MetadataNameHelper.cs create mode 100644 OpenMeteoTests/MetadataTests.cs diff --git a/OpenMeteo/MetadataApiModel.cs b/OpenMeteo/MetadataApiModel.cs new file mode 100644 index 0000000..858fc58 --- /dev/null +++ b/OpenMeteo/MetadataApiModel.cs @@ -0,0 +1,12 @@ +namespace OpenMeteo; + +public record MetadataApiModel +{ + public long data_end_time { get; init; } + public long last_run_availability_time { get; init; } + public long last_run_initialisation_time { get; init; } + public long last_run_modification_time { get; init; } + public int temporal_resolution_seconds { get; init; } + public int update_interval_seconds { get; init; } +} + diff --git a/OpenMeteo/MetadataModel.cs b/OpenMeteo/MetadataModel.cs new file mode 100644 index 0000000..fc8c85c --- /dev/null +++ b/OpenMeteo/MetadataModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace OpenMeteo; +public record MetadataModel( + DateTime DataEndTime, + DateTime LastRunAvailabilityTime, + DateTime LastRunInitialisationTime, + DateTime LastRunModificationTime, + int TemporalResolutionSeconds, + int UpdateIntervalSeconds +); diff --git a/OpenMeteo/MetadataNameHelper.cs b/OpenMeteo/MetadataNameHelper.cs new file mode 100644 index 0000000..827f71b --- /dev/null +++ b/OpenMeteo/MetadataNameHelper.cs @@ -0,0 +1,27 @@ +using System; + +namespace OpenMeteo; +internal static class MetadataNameHelper +{ + public static string GetPrefixForWeatherModel(WeatherModelOptionsParameter weatherModel) => weatherModel switch + { + WeatherModelOptionsParameter.ecmwf_ifs025 => "ecmwf_ifs025", + WeatherModelOptionsParameter.ecmwf_aifs025 => "ecmwf_aifs025", + WeatherModelOptionsParameter.icon_global => "dwd_icon", + WeatherModelOptionsParameter.icon_eu => "dwd_icon_eu", + WeatherModelOptionsParameter.icon_d2 => "dwd_icon_d2", + WeatherModelOptionsParameter.meteofrance_arpege_world => "meteofrance_arpege_world025", + WeatherModelOptionsParameter.meteofrance_arpege_europe => "meteofrance_arpege_europe", + WeatherModelOptionsParameter.meteofrance_arome_france => "meteofrance_arome_france0025", + WeatherModelOptionsParameter.ukmo_uk_deterministic_2km => "ukmo_uk_deterministic_2km", + WeatherModelOptionsParameter.ukmo_global_deterministic_10km => "ukmo_global_deterministic_10km", + WeatherModelOptionsParameter.gfs_global => "ncep_gfs013", + WeatherModelOptionsParameter.gfs_graphcast025 => "ncep_gfs_graphcast025", + WeatherModelOptionsParameter.gfs_hrrr => "ncep_hrrr_conus", + WeatherModelOptionsParameter.gem_global => "cmc_gem_gdps", + WeatherModelOptionsParameter.gem_hrdps_continental => "cmc_gem_hrdps", + WeatherModelOptionsParameter.gem_regional => "cmc_gem_rdps", + WeatherModelOptionsParameter.jma_gsm => "jma_gsm", + _ => throw new ArgumentOutOfRangeException(nameof(weatherModel), weatherModel, null) + }; +} diff --git a/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index edd37fa..39ca1c9 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -13,6 +13,7 @@ public class OpenMeteoClient private readonly HttpController httpController; private readonly UrlFactory _urlFactory = new(); private readonly IOpenMeteoLogger? _logger = default!; + private readonly JsonSerializerOptions _jsonSerializerOptions = new() { PropertyNameCaseInsensitive = true }; /// /// If set to true, exceptions from the OpenMeteo API will be rethrown. Default is false. @@ -78,7 +79,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) if (response == null || response.Locations == null) return null; - WeatherForecastOptions options = new WeatherForecastOptions + WeatherForecastOptions options = new() { Latitude = response.Locations[0].Latitude, Longitude = response.Locations[0].Longitude, @@ -101,7 +102,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) if (response == null || response?.Locations == null) return null; - WeatherForecastOptions weatherForecastOptions = new WeatherForecastOptions + WeatherForecastOptions weatherForecastOptions = new() { Latitude = response.Locations[0].Latitude, Longitude = response.Locations[0].Longitude, @@ -245,6 +246,33 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) return QueryElevationAsync(latitude, longitude).GetAwaiter().GetResult(); } + public async Task QueryWeatherForecastMetadata(WeatherModelOptionsParameter weatherModel) + { + try + { + var url = _urlFactory.GetWeatherForecastMetadataUrl(weatherModel); + _logger?.Debug($"{nameof(OpenMeteoClient)}.GetWeatherForecastMetadata(). URL: {_urlFactory.SanitiseUrl(url)}"); + HttpResponseMessage response = await httpController.Client.GetAsync(url); + response.EnsureSuccessStatusCode(); + + MetadataApiModel? meta = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions); + return ConvertMetadataModel(meta ?? throw new OpenMeteoClientException("No metadata found", response.StatusCode)); + } + catch (HttpRequestException e) + { + _logger?.Warning($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + throw; + } + } + + private static MetadataModel ConvertMetadataModel(MetadataApiModel apiModel) => new( + DateTimeOffset.FromUnixTimeSeconds(apiModel.data_end_time).UtcDateTime, + DateTimeOffset.FromUnixTimeSeconds(apiModel.last_run_availability_time).UtcDateTime, + DateTimeOffset.FromUnixTimeSeconds(apiModel.last_run_initialisation_time).UtcDateTime, + DateTimeOffset.FromUnixTimeSeconds(apiModel.last_run_modification_time).UtcDateTime, + apiModel.temporal_resolution_seconds, + apiModel.update_interval_seconds); + private async Task GetAirQualityAsync(AirQualityOptions options) { try @@ -254,7 +282,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); - AirQuality? airQuality = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + AirQuality? airQuality = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions); return airQuality; } catch (HttpRequestException e) @@ -265,6 +293,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) return null; } } + private async Task GetWeatherForecastAsync(WeatherForecastOptions options) { try @@ -274,7 +303,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) HttpResponseMessage response = await httpController.Client.GetAsync(url); if(response.IsSuccessStatusCode) { - WeatherForecast? weatherForecast = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + WeatherForecast? weatherForecast = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions); return weatherForecast; } @@ -283,7 +312,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) { try { - error = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + error = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions); } catch (Exception e) { @@ -313,7 +342,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); - GeocodingApiResponse? geocodingData = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + GeocodingApiResponse? geocodingData = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions); return geocodingData; } @@ -335,7 +364,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey) HttpResponseMessage response = await httpController.Client.GetAsync(url); response.EnsureSuccessStatusCode(); - ElevationApiResponse? elevationData = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + ElevationApiResponse? elevationData = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions); return elevationData; } diff --git a/OpenMeteo/UrlFactory.cs b/OpenMeteo/UrlFactory.cs index 8dc5b28..fa534f2 100644 --- a/OpenMeteo/UrlFactory.cs +++ b/OpenMeteo/UrlFactory.cs @@ -8,6 +8,7 @@ public class UrlFactory private readonly string _geocodeApiUrl = "geocoding-api.open-meteo.com/v1/search"; private readonly string _airQualityApiUrl = "air-quality-api.open-meteo.com/v1/air-quality"; private readonly string _elevationApiUrl = "api.open-meteo.com/v1/elevation"; + private readonly string _metadataFileFragment = "/static/meta.json"; private readonly string _customerApiUrlFragment = "customer-"; private readonly string _apiKey = string.Empty; @@ -236,6 +237,12 @@ public string GetUrlWithOptions(ElevationOptions options) return uri.ToString(); } + public string GetWeatherForecastMetadataUrl(WeatherModelOptionsParameter weatherModel) + { + var metadataBaseUrl = $"api.open-meteo.com/data/{MetadataNameHelper.GetPrefixForWeatherModel(weatherModel)}{_metadataFileFragment}"; + return GetBaseUrl(metadataBaseUrl); + } + private void SetApiKeyIfNeeded(UriBuilder uri) { if (!string.IsNullOrEmpty(_apiKey)) diff --git a/OpenMeteo/WeatherModelOptions.cs b/OpenMeteo/WeatherModelOptions.cs index d3472b4..c90bacc 100644 --- a/OpenMeteo/WeatherModelOptions.cs +++ b/OpenMeteo/WeatherModelOptions.cs @@ -1,10 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; namespace OpenMeteo { @@ -31,19 +27,19 @@ public class WeatherModelOptions : IEnumerable, IC public WeatherModelOptions(WeatherModelOptionsParameter parameter) { - _parameter = new List(); + _parameter = []; Add(parameter); } public WeatherModelOptions(WeatherModelOptionsParameter[] parameter) { - _parameter = new List(); + _parameter = []; Add(parameter); } public WeatherModelOptions() { - _parameter = new List(); + _parameter = []; } public WeatherModelOptionsParameter this[int index] diff --git a/OpenMeteoTests/MetadataTests.cs b/OpenMeteoTests/MetadataTests.cs new file mode 100644 index 0000000..69ce514 --- /dev/null +++ b/OpenMeteoTests/MetadataTests.cs @@ -0,0 +1,44 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenMeteo; +using System; +using System.Threading.Tasks; + +namespace OpenMeteoTests; + +[TestClass] +public class MetadataTests +{ + [DataTestMethod] + [DataRow(WeatherModelOptionsParameter.ecmwf_ifs025)] + [DataRow(WeatherModelOptionsParameter.ecmwf_aifs025)] + [DataRow(WeatherModelOptionsParameter.icon_global)] + [DataRow(WeatherModelOptionsParameter.icon_eu)] + [DataRow(WeatherModelOptionsParameter.icon_d2)] + [DataRow(WeatherModelOptionsParameter.meteofrance_arpege_world)] + [DataRow(WeatherModelOptionsParameter.meteofrance_arpege_europe)] + [DataRow(WeatherModelOptionsParameter.meteofrance_arome_france)] + [DataRow(WeatherModelOptionsParameter.ukmo_uk_deterministic_2km)] + [DataRow(WeatherModelOptionsParameter.ukmo_global_deterministic_10km)] + [DataRow(WeatherModelOptionsParameter.gfs_global)] + [DataRow(WeatherModelOptionsParameter.gfs_graphcast025)] + [DataRow(WeatherModelOptionsParameter.gfs_hrrr)] + [DataRow(WeatherModelOptionsParameter.gem_global)] + [DataRow(WeatherModelOptionsParameter.gem_hrdps_continental)] + [DataRow(WeatherModelOptionsParameter.gem_regional)] + [DataRow(WeatherModelOptionsParameter.jma_gsm)] + public async Task Metadata_Async_Test(WeatherModelOptionsParameter model) + { + var historicalDateTime = DateTime.UtcNow.AddDays(-2); + OpenMeteoClient client = new(); + var res = await client.QueryWeatherForecastMetadata(model); + + Assert.IsNotNull(res); + Assert.IsTrue(res.DataEndTime > historicalDateTime); + Assert.IsTrue(res.LastRunInitialisationTime > historicalDateTime); + Assert.IsTrue(res.LastRunAvailabilityTime > historicalDateTime); + Assert.IsTrue(res.LastRunModificationTime > historicalDateTime); + Assert.IsTrue(res.UpdateIntervalSeconds > 0); + Assert.IsTrue(res.TemporalResolutionSeconds > 0); + + } +} diff --git a/OpenMeteoTests/UrlFactoryTests.cs b/OpenMeteoTests/UrlFactoryTests.cs index 5730e4d..f82e49c 100644 --- a/OpenMeteoTests/UrlFactoryTests.cs +++ b/OpenMeteoTests/UrlFactoryTests.cs @@ -108,7 +108,60 @@ public void SanitiseUrl_WithNoApiKey_Test() Assert.AreEqual(expectedUrl, sanitisedUrl); } + [DataTestMethod] + [DataRow(WeatherModelOptionsParameter.ecmwf_ifs025, "ecmwf_ifs025")] + [DataRow(WeatherModelOptionsParameter.ecmwf_aifs025, "ecmwf_aifs025")] + [DataRow(WeatherModelOptionsParameter.icon_global, "dwd_icon")] + [DataRow(WeatherModelOptionsParameter.icon_eu, "dwd_icon_eu")] + [DataRow(WeatherModelOptionsParameter.icon_d2, "dwd_icon_d2")] + [DataRow(WeatherModelOptionsParameter.meteofrance_arpege_world, "meteofrance_arpege_world025")] + [DataRow(WeatherModelOptionsParameter.meteofrance_arpege_europe, "meteofrance_arpege_europe")] + [DataRow(WeatherModelOptionsParameter.meteofrance_arome_france, "meteofrance_arome_france0025")] + [DataRow(WeatherModelOptionsParameter.ukmo_uk_deterministic_2km, "ukmo_uk_deterministic_2km")] + [DataRow(WeatherModelOptionsParameter.ukmo_global_deterministic_10km, "ukmo_global_deterministic_10km")] + [DataRow(WeatherModelOptionsParameter.gfs_global, "ncep_gfs013")] + [DataRow(WeatherModelOptionsParameter.gfs_graphcast025, "ncep_gfs_graphcast025")] + [DataRow(WeatherModelOptionsParameter.gfs_hrrr, "ncep_hrrr_conus")] + [DataRow(WeatherModelOptionsParameter.gem_global, "cmc_gem_gdps")] + [DataRow(WeatherModelOptionsParameter.gem_hrdps_continental, "cmc_gem_hrdps")] + [DataRow(WeatherModelOptionsParameter.gem_regional, "cmc_gem_rdps")] + [DataRow(WeatherModelOptionsParameter.jma_gsm, "jma_gsm")] + public void GetWeatherForecastMetadataUrl_WithNoApiKey_Test(WeatherModelOptionsParameter weatherModel, string expectedName) + { + var factory = new UrlFactory(); + var url = factory.GetWeatherForecastMetadataUrl(weatherModel); + + var expectedUrl = $"https://api.open-meteo.com/data/{expectedName}/static/meta.json"; + Assert.AreEqual(expectedUrl, url); + } + + [DataTestMethod] + [DataRow(WeatherModelOptionsParameter.ecmwf_ifs025, "ecmwf_ifs025")] + [DataRow(WeatherModelOptionsParameter.ecmwf_aifs025, "ecmwf_aifs025")] + [DataRow(WeatherModelOptionsParameter.icon_global, "dwd_icon")] + [DataRow(WeatherModelOptionsParameter.icon_eu, "dwd_icon_eu")] + [DataRow(WeatherModelOptionsParameter.icon_d2, "dwd_icon_d2")] + [DataRow(WeatherModelOptionsParameter.meteofrance_arpege_world, "meteofrance_arpege_world025")] + [DataRow(WeatherModelOptionsParameter.meteofrance_arpege_europe, "meteofrance_arpege_europe")] + [DataRow(WeatherModelOptionsParameter.meteofrance_arome_france, "meteofrance_arome_france0025")] + [DataRow(WeatherModelOptionsParameter.ukmo_uk_deterministic_2km, "ukmo_uk_deterministic_2km")] + [DataRow(WeatherModelOptionsParameter.ukmo_global_deterministic_10km, "ukmo_global_deterministic_10km")] + [DataRow(WeatherModelOptionsParameter.gfs_global, "ncep_gfs013")] + [DataRow(WeatherModelOptionsParameter.gfs_graphcast025, "ncep_gfs_graphcast025")] + [DataRow(WeatherModelOptionsParameter.gfs_hrrr, "ncep_hrrr_conus")] + [DataRow(WeatherModelOptionsParameter.gem_global, "cmc_gem_gdps")] + [DataRow(WeatherModelOptionsParameter.gem_hrdps_continental, "cmc_gem_hrdps")] + [DataRow(WeatherModelOptionsParameter.gem_regional, "cmc_gem_rdps")] + [DataRow(WeatherModelOptionsParameter.jma_gsm, "jma_gsm")] + public void GetWeatherForecastMetadataUrl_WithApiKey_Test(WeatherModelOptionsParameter weatherModel, string expectedName) + { + var factory = new UrlFactory("testApiKey"); + var url = factory.GetWeatherForecastMetadataUrl(weatherModel); + + var expectedUrl = $"https://customer-api.open-meteo.com/data/{expectedName}/static/meta.json"; + Assert.AreEqual(expectedUrl, url); + } private static WeatherForecastOptions GetWeatherForecastOptions() => new() { From c24224c2c88391b0bde9fcbf7bfaf6cc62232dc9 Mon Sep 17 00:00:00 2001 From: colinnuk Date: Sun, 20 Oct 2024 15:28:58 -0700 Subject: [PATCH 34/34] Add NCEP NBM model --- OpenMeteo/MetadataNameHelper.cs | 1 + OpenMeteo/WeatherModelOptions.cs | 1 + OpenMeteoTests/MetadataTests.cs | 1 + OpenMeteoTests/UrlFactoryTests.cs | 2 ++ 4 files changed, 5 insertions(+) diff --git a/OpenMeteo/MetadataNameHelper.cs b/OpenMeteo/MetadataNameHelper.cs index 827f71b..6508e9a 100644 --- a/OpenMeteo/MetadataNameHelper.cs +++ b/OpenMeteo/MetadataNameHelper.cs @@ -18,6 +18,7 @@ internal static class MetadataNameHelper WeatherModelOptionsParameter.gfs_global => "ncep_gfs013", WeatherModelOptionsParameter.gfs_graphcast025 => "ncep_gfs_graphcast025", WeatherModelOptionsParameter.gfs_hrrr => "ncep_hrrr_conus", + WeatherModelOptionsParameter.ncep_nbm_conus => "ncep_nbm_conus", WeatherModelOptionsParameter.gem_global => "cmc_gem_gdps", WeatherModelOptionsParameter.gem_hrdps_continental => "cmc_gem_hrdps", WeatherModelOptionsParameter.gem_regional => "cmc_gem_rdps", diff --git a/OpenMeteo/WeatherModelOptions.cs b/OpenMeteo/WeatherModelOptions.cs index c90bacc..328856f 100644 --- a/OpenMeteo/WeatherModelOptions.cs +++ b/OpenMeteo/WeatherModelOptions.cs @@ -107,6 +107,7 @@ public enum WeatherModelOptionsParameter gfs_global, gfs_hrrr, gfs_graphcast025, + ncep_nbm_conus, jma_seamless, jma_msm, jma_gsm, diff --git a/OpenMeteoTests/MetadataTests.cs b/OpenMeteoTests/MetadataTests.cs index 69ce514..8c53bfa 100644 --- a/OpenMeteoTests/MetadataTests.cs +++ b/OpenMeteoTests/MetadataTests.cs @@ -22,6 +22,7 @@ public class MetadataTests [DataRow(WeatherModelOptionsParameter.gfs_global)] [DataRow(WeatherModelOptionsParameter.gfs_graphcast025)] [DataRow(WeatherModelOptionsParameter.gfs_hrrr)] + [DataRow(WeatherModelOptionsParameter.ncep_nbm_conus)] [DataRow(WeatherModelOptionsParameter.gem_global)] [DataRow(WeatherModelOptionsParameter.gem_hrdps_continental)] [DataRow(WeatherModelOptionsParameter.gem_regional)] diff --git a/OpenMeteoTests/UrlFactoryTests.cs b/OpenMeteoTests/UrlFactoryTests.cs index f82e49c..5f893ea 100644 --- a/OpenMeteoTests/UrlFactoryTests.cs +++ b/OpenMeteoTests/UrlFactoryTests.cs @@ -122,6 +122,7 @@ public void SanitiseUrl_WithNoApiKey_Test() [DataRow(WeatherModelOptionsParameter.gfs_global, "ncep_gfs013")] [DataRow(WeatherModelOptionsParameter.gfs_graphcast025, "ncep_gfs_graphcast025")] [DataRow(WeatherModelOptionsParameter.gfs_hrrr, "ncep_hrrr_conus")] + [DataRow(WeatherModelOptionsParameter.ncep_nbm_conus, "ncep_nbm_conus")] [DataRow(WeatherModelOptionsParameter.gem_global, "cmc_gem_gdps")] [DataRow(WeatherModelOptionsParameter.gem_hrdps_continental, "cmc_gem_hrdps")] [DataRow(WeatherModelOptionsParameter.gem_regional, "cmc_gem_rdps")] @@ -150,6 +151,7 @@ public void GetWeatherForecastMetadataUrl_WithNoApiKey_Test(WeatherModelOptionsP [DataRow(WeatherModelOptionsParameter.gfs_global, "ncep_gfs013")] [DataRow(WeatherModelOptionsParameter.gfs_graphcast025, "ncep_gfs_graphcast025")] [DataRow(WeatherModelOptionsParameter.gfs_hrrr, "ncep_hrrr_conus")] + [DataRow(WeatherModelOptionsParameter.ncep_nbm_conus, "ncep_nbm_conus")] [DataRow(WeatherModelOptionsParameter.gem_global, "cmc_gem_gdps")] [DataRow(WeatherModelOptionsParameter.gem_hrdps_continental, "cmc_gem_hrdps")] [DataRow(WeatherModelOptionsParameter.gem_regional, "cmc_gem_rdps")]