Skip to content

Commit

Permalink
Merge pull request #6 from colinnuk/model_update_timestamps
Browse files Browse the repository at this point in the history
Add method to get weather forecast model update timestamps from OpenMeteo
  • Loading branch information
colinnuk authored Oct 19, 2024
2 parents 02abbc5 + c953107 commit cbbffe8
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 15 deletions.
12 changes: 12 additions & 0 deletions OpenMeteo/MetadataApiModel.cs
Original file line number Diff line number Diff line change
@@ -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; }
}

11 changes: 11 additions & 0 deletions OpenMeteo/MetadataModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

namespace OpenMeteo;
public record MetadataModel(
DateTime DataEndTime,
DateTime LastRunAvailabilityTime,
DateTime LastRunInitialisationTime,
DateTime LastRunModificationTime,
int TemporalResolutionSeconds,
int UpdateIntervalSeconds
);
27 changes: 27 additions & 0 deletions OpenMeteo/MetadataNameHelper.cs
Original file line number Diff line number Diff line change
@@ -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)
};
}
2 changes: 1 addition & 1 deletion OpenMeteo/OpenMeteo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" Version="6.0.5" />
<PackageReference Include="System.Text.Json" Version="6.0.10" />
</ItemGroup>

</Project>
43 changes: 36 additions & 7 deletions OpenMeteo/OpenMeteoClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

/// <summary>
/// If set to true, exceptions from the OpenMeteo API will be rethrown. Default is false.
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -245,6 +246,33 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey)
return QueryElevationAsync(latitude, longitude).GetAwaiter().GetResult();
}

public async Task<MetadataModel> 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<MetadataApiModel>(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<AirQuality?> GetAirQualityAsync(AirQualityOptions options)
{
try
Expand All @@ -254,7 +282,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey)
HttpResponseMessage response = await httpController.Client.GetAsync(url);
response.EnsureSuccessStatusCode();

AirQuality? airQuality = await JsonSerializer.DeserializeAsync<AirQuality>(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
AirQuality? airQuality = await JsonSerializer.DeserializeAsync<AirQuality>(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions);
return airQuality;
}
catch (HttpRequestException e)
Expand All @@ -265,6 +293,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey)
return null;
}
}

private async Task<WeatherForecast?> GetWeatherForecastAsync(WeatherForecastOptions options)
{
try
Expand All @@ -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<WeatherForecast>(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
WeatherForecast? weatherForecast = await JsonSerializer.DeserializeAsync<WeatherForecast>(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions);
return weatherForecast;
}

Expand All @@ -283,7 +312,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey)
{
try
{
error = await JsonSerializer.DeserializeAsync<ErrorResponse>(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
error = await JsonSerializer.DeserializeAsync<ErrorResponse>(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions);
}
catch (Exception e)
{
Expand Down Expand Up @@ -313,7 +342,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey)
HttpResponseMessage response = await httpController.Client.GetAsync(url);
response.EnsureSuccessStatusCode();

GeocodingApiResponse? geocodingData = await JsonSerializer.DeserializeAsync<GeocodingApiResponse>(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
GeocodingApiResponse? geocodingData = await JsonSerializer.DeserializeAsync<GeocodingApiResponse>(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions);

return geocodingData;
}
Expand All @@ -335,7 +364,7 @@ public OpenMeteoClient(IOpenMeteoLogger logger, string apiKey)
HttpResponseMessage response = await httpController.Client.GetAsync(url);
response.EnsureSuccessStatusCode();

ElevationApiResponse? elevationData = await JsonSerializer.DeserializeAsync<ElevationApiResponse>(await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
ElevationApiResponse? elevationData = await JsonSerializer.DeserializeAsync<ElevationApiResponse>(await response.Content.ReadAsStreamAsync(), _jsonSerializerOptions);

return elevationData;
}
Expand Down
7 changes: 7 additions & 0 deletions OpenMeteo/UrlFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand Down
10 changes: 3 additions & 7 deletions OpenMeteo/WeatherModelOptions.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -31,19 +27,19 @@ public class WeatherModelOptions : IEnumerable<WeatherModelOptionsParameter>, IC

public WeatherModelOptions(WeatherModelOptionsParameter parameter)
{
_parameter = new List<WeatherModelOptionsParameter>();
_parameter = [];
Add(parameter);
}

public WeatherModelOptions(WeatherModelOptionsParameter[] parameter)
{
_parameter = new List<WeatherModelOptionsParameter>();
_parameter = [];
Add(parameter);
}

public WeatherModelOptions()
{
_parameter = new List<WeatherModelOptionsParameter>();
_parameter = [];
}

public WeatherModelOptionsParameter this[int index]
Expand Down
44 changes: 44 additions & 0 deletions OpenMeteoTests/MetadataTests.cs
Original file line number Diff line number Diff line change
@@ -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);

}
}
53 changes: 53 additions & 0 deletions OpenMeteoTests/UrlFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down

0 comments on commit cbbffe8

Please # to comment.