diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e46d5cc..60b5de3 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -11,9 +11,10 @@ on: paths: - '**/*.cs' - '**/*.csproj' + 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: @@ -22,7 +23,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + os: [ubuntu-latest] steps: - name: Checkout Code diff --git a/.github/workflows/package-and-deploy.yml b/.github/workflows/package-and-deploy.yml index 445631e..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 - run: dotnet nuget push ./package/*.nupkg --api-key ${{ secrets.API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + - 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 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 diff --git a/OpenMeteo/ElevationApiResponse.cs b/OpenMeteo/ElevationApiResponse.cs new file mode 100644 index 0000000..91fee0d --- /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 input elevation so this will 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/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/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; } 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, 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/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/OpenMeteo.csproj b/OpenMeteo/OpenMeteo.csproj index f56b1ee..e4900f0 100644 --- a/OpenMeteo/OpenMeteo.csproj +++ b/OpenMeteo/OpenMeteo.csproj @@ -1,10 +1,10 @@ - netstandard2.1 + net8.0 enable - $(AssemblyName).dotnet - Open Meteo Dotnet Library + $(AssemblyName).colinnuk.dotnet + 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/OpenMeteo/OpenMeteoClient.cs b/OpenMeteo/OpenMeteoClient.cs index 5d4123f..cb8d057 100644 --- a/OpenMeteo/OpenMeteoClient.cs +++ b/OpenMeteo/OpenMeteoClient.cs @@ -14,8 +14,12 @@ 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 IOpenMeteoLogger? _logger = default!; + private readonly bool _rethrowExceptions = false; + /// /// Creates a new object and sets the neccessary variables (httpController, CultureInfo) /// @@ -24,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) /// @@ -79,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); } /// @@ -164,6 +171,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,11 +214,18 @@ 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 { - 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 }); @@ -206,8 +233,9 @@ public OpenMeteoClient() } catch (HttpRequestException e) { - Console.WriteLine(e.Message); - Console.WriteLine(e.StackTrace); + _logger?.Warning($"{nameof(OpenMeteoClient)}.GetAirQualityAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + if (_rethrowExceptions) + throw; return null; } } @@ -286,16 +314,35 @@ 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; + 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) { - Console.WriteLine(e.Message); - Console.WriteLine(e.StackTrace); + _logger?.Warning($"{nameof(OpenMeteoClient)}.GetWeatherForecastAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + if (_rethrowExceptions) + throw; return null; } @@ -305,7 +352,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 }); @@ -314,8 +364,32 @@ 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?.Warning($"{nameof(OpenMeteoClient)}.GetGeocodingDataAsync(). Message: {e.Message} StackTrace: {e.StackTrace}"); + if (_rethrowExceptions) + throw; + return null; + } + } + + private async Task GetElevationAsync(ElevationOptions options) + { + try + { + 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 }); + + return elevationData; + } + catch (HttpRequestException e) + { + _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) + throw; return null; } } @@ -563,6 +637,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/OpenMeteo/OpenMeteoClientException.cs b/OpenMeteo/OpenMeteoClientException.cs new file mode 100644 index 0000000..e6cc822 --- /dev/null +++ b/OpenMeteo/OpenMeteoClientException.cs @@ -0,0 +1,9 @@ +using System.Net; +using System.Net.Http; + +namespace OpenMeteo +{ + public class OpenMeteoClientException(string message, HttpStatusCode httpStatusCode) : HttpRequestException(message, null, httpStatusCode) + { + } +} diff --git a/OpenMeteo/WeatherModelOptions.cs b/OpenMeteo/WeatherModelOptions.cs index 0d69688..604dd92 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, @@ -125,6 +126,12 @@ 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, + ukmo_global_deterministic_10km, + ukmo_global_deterministic_2km, } } 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); + } + } +} 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 diff --git a/OpenMeteoTests/WeatherForecastTests.cs b/OpenMeteoTests/WeatherForecastTests.cs index 54e8abe..0e3301a 100644 --- a/OpenMeteoTests/WeatherForecastTests.cs +++ b/OpenMeteoTests/WeatherForecastTests.cs @@ -1,4 +1,6 @@ using System.Globalization; +using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -107,9 +109,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 +120,28 @@ 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); + 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))); } [TestMethod] - public void WeatherForecast_With_All_Options_Sync_Test() + public async Task Latitude_Longitude_No_Data_For_Selected_Forecast_Rethrows_Test() { - OpenMeteoClient client = new(); - WeatherForecastOptions options = new() + OpenMeteoClient client = new(true); + + WeatherForecastOptions options = new WeatherForecastOptions { - Hourly = HourlyOptions.All, - Daily = DailyOptions.All, - Current = CurrentOptions.All, - Minutely15 = Minutely15Options.All, + Latitude = 1, + Longitude = 1, + Models = new WeatherModelOptions(WeatherModelOptionsParameter.gfs_hrrr), }; - 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); + 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); } } }