diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs index 01e494bbd..acfc1339e 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/ElectricityMapsDataSource.cs @@ -72,7 +72,7 @@ private static EmissionsForecast ToEmissionsForecast(Location location, Forecast } /// - public async Task GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) + public async Task GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) { await Task.Run(() => true); throw new NotImplementedException(); diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/IWattTimeClient.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/IWattTimeClient.cs index b07f96cd8..73d432861 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/IWattTimeClient.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/IWattTimeClient.cs @@ -49,11 +49,11 @@ internal interface IWattTimeClient /// /// Async method to get generated forecast at requested time and balancing authority. /// - /// Balancing authority abbreviation + /// Balancing authority abbreviation /// The historical time used to fetch the most recent forecast generated as of that time. /// An which contains forecasted emissions data points or null if no Forecast generated at the requested time. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task GetForecastOnDateAsync(string balancingAuthorityAbbreviation, DateTimeOffset requestedAt); + Task GetForecastOnDateAsync(string region, DateTimeOffset requestedAt); /// /// Async method to get generated forecast at requested time and balancing authority. @@ -62,7 +62,7 @@ internal interface IWattTimeClient /// The historical time used to fetch the most recent forecast generated as of that time. /// An which contains forecasted emissions data points or null if no Forecast generated at the requested time. /// Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes. - Task GetForecastOnDateAsync(RegionResponse balancingAuthority, DateTimeOffset requestedAt); + Task GetForecastOnDateAsync(RegionResponse balancingAuthority, DateTimeOffset requestedAt); /// /// Async method to get the balancing authority for a given location. diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClient.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClient.cs index 8e02c7349..2d0b50951 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClient.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClient.cs @@ -103,9 +103,6 @@ public async Task GetCurrentForecastAsync(string var result = await this.MakeRequestGetStreamAsync(Paths.Forecast, parameters, tags); - var sr = new StreamReader(result); - var s = sr.ReadToEnd(); - var forecast = await JsonSerializer.DeserializeAsync(result, _options) ?? throw new WattTimeClientException($"Error getting forecast for {region}"); return forecast; @@ -118,32 +115,33 @@ public Task GetCurrentForecastAsync(RegionRespons } /// - public async Task GetForecastOnDateAsync(string balancingAuthorityAbbreviation, DateTimeOffset requestedAt) + public async Task GetForecastOnDateAsync(string region, DateTimeOffset requestedAt) { - _log.LogInformation($"Requesting forecast from balancingAuthority {balancingAuthorityAbbreviation} generated at {requestedAt}."); + _log.LogInformation($"Requesting forecast from balancingAuthority {region} generated at {requestedAt}."); var parameters = new Dictionary() { - { QueryStrings.Region, balancingAuthorityAbbreviation }, + { QueryStrings.Region, region }, + { QueryStrings.SignalType, SignalTypes.co2_moer }, { QueryStrings.StartTime, requestedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) }, { QueryStrings.EndTime, requestedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) } }; var tags = new Dictionary() { - { QueryStrings.Region, balancingAuthorityAbbreviation } + { QueryStrings.Region, region } }; - using (var result = await this.MakeRequestGetStreamAsync(Paths.Forecast, parameters, tags)) + using (var result = await this.MakeRequestGetStreamAsync(Paths.ForecastHistorical, parameters, tags)) { - var forecasts = await JsonSerializer.DeserializeAsync>(result, _options) ?? throw new WattTimeClientException($"Error getting forecasts for {balancingAuthorityAbbreviation}"); - return forecasts.FirstOrDefault(); + var historicalForecastResponse = await JsonSerializer.DeserializeAsync(result, _options) ?? throw new WattTimeClientException($"Error getting forecasts for {region}"); + return historicalForecastResponse; } } /// - public Task GetForecastOnDateAsync(RegionResponse balancingAuthority, DateTimeOffset requestedAt) + public Task GetForecastOnDateAsync(RegionResponse region, DateTimeOffset requestedAt) { - return this.GetForecastOnDateAsync(balancingAuthority.Region, requestedAt); + return this.GetForecastOnDateAsync(region.Region, requestedAt); } /// diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/Paths.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/Paths.cs index 61c3092f9..8704d8bfb 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/Paths.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/Paths.cs @@ -4,6 +4,8 @@ internal class Paths { public const string Data = "historical"; public const string Forecast = "forecast"; + + public const string ForecastHistorical = "forecast/historical"; public const string BalancingAuthorityFromLocation = "region-from-loc"; public const string Login = "login"; public const string Historical = "historical"; diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalEmissionsData.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalEmissionsData.cs new file mode 100644 index 000000000..5c82f01a6 --- /dev/null +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalEmissionsData.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace CarbonAware.DataSources.WattTime.Model; + +[Serializable] +internal class HistoricalEmissionsData +{ + [JsonPropertyName("generated_at")] + public DateTimeOffset GeneratedAt { get; set; } = DateTimeOffset.MinValue; + + [JsonPropertyName("forecast")] + public List Forecast { get; set; } = new List(); + +} + diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalForecastEmissionsDataResponse.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalForecastEmissionsDataResponse.cs new file mode 100644 index 000000000..212e8170a --- /dev/null +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/HistoricalForecastEmissionsDataResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CarbonAware.DataSources.WattTime.Model; + +[Serializable] +internal record HistoricalForecastEmissionsDataResponse +{ + [JsonPropertyName("data")] + public List Data { get; set; } = new List(); + + + [JsonPropertyName("meta")] + public GridEmissionsMetaData Meta { get; set; } = new GridEmissionsMetaData(); +} + + diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs index c879df636..39a114777 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/WattTimeDataSource.cs @@ -87,7 +87,7 @@ public async Task GetCurrentCarbonIntensityForecastAsync(Loca } /// - public async Task GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) + public async Task GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) { this.Logger.LogInformation($"Getting carbon intensity forecast for location {location} requested at {requestedAt}"); var balancingAuthority = await this.GetBalancingAuthority(location); @@ -100,7 +100,26 @@ public async Task GetCarbonIntensityForecastAsync(Location lo throw ex; } // keep input from the user. - return ForecastToEmissionsForecast(forecast, location, requestedAt); + return HistoricalForecastToEmissionsForecast(forecast, location, requestedAt); + } + + private EmissionsForecast HistoricalForecastToEmissionsForecast(HistoricalForecastEmissionsDataResponse historicalForecast, Location location, DateTimeOffset requestedAt) + { + var duration = GetDurationFromGridEmissionDataPoints(historicalForecast.Data[0].Forecast); + var forecastData = historicalForecast.Data[0].Forecast.Select(e => new EmissionsData() + { + Location = historicalForecast.Meta.Region, + Rating = ConvertMoerToGramsPerKilowattHour(e.Value), + Time = e.PointTime, + Duration = duration + }); + var emissionsForecast = new EmissionsForecast() + { + GeneratedAt = historicalForecast.Data[0].GeneratedAt, + Location = location, + ForecastData = forecastData + }; + return emissionsForecast; } private EmissionsForecast ForecastToEmissionsForecast(ForecastEmissionsDataResponse forecast, Location location, DateTimeOffset requestedAt) diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeClientTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeClientTests.cs index a79ca8b56..52d244a9f 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeClientTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeClientTests.cs @@ -252,12 +252,12 @@ public async Task GetForecastOnDateAsync_DeserializesExpectedResponse() var overloadedForecast = await client.GetForecastOnDateAsync(ba, new DateTimeOffset(2022, 4, 22, 0, 0, 0, TimeSpan.Zero)); Assert.AreEqual(forecastResponse!.Meta.GeneratedAt, overloadedForecast!.Meta.GeneratedAt); - Assert.AreEqual(forecastResponse.Data.First(), overloadedForecast.Data.First()); + Assert.AreEqual(forecastResponse.Data[0].Forecast.First(), overloadedForecast.Data[0].Forecast.First()); Assert.AreEqual(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), forecastResponse.Meta.GeneratedAt); Assert.AreEqual("region", forecastResponse.Meta.Region); - var forecastDataPoint = forecastResponse.Data.ToList().First(); + var forecastDataPoint = forecastResponse.Data[0].Forecast.ToList().First(); Assert.AreEqual(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), forecastDataPoint.PointTime); Assert.AreEqual("999.99", forecastDataPoint.Value.ToString("0.00", CultureInfo.InvariantCulture)); //Format float to avoid precision issues Assert.AreEqual("1.0", forecastDataPoint.Version); diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs index 0ddf84dcb..4e9942efb 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/WattTimeDataSourceTests.cs @@ -127,6 +127,9 @@ public async Task GetCarbonIntensityForecastAsync_ReturnsResultsWhenRecordsFound var forecastResponse = GenerateForecastResponse(2, value: lbsPerMwhEmissions); forecastResponse.Meta.GeneratedAt = generatedAt; + var historicalForecastResponse = GenerateHistoricalForecastResponse(2, value: lbsPerMwhEmissions); + historicalForecastResponse.Meta.GeneratedAt = generatedAt; + EmissionsForecast result; if (getCurrentForecast) @@ -140,10 +143,10 @@ public async Task GetCarbonIntensityForecastAsync_ReturnsResultsWhenRecordsFound else { this.WattTimeClient.Setup(w => w.GetForecastOnDateAsync(this.DefaultRegion, generatedAt) - ).ReturnsAsync(() => forecastResponse); + ).ReturnsAsync(() => historicalForecastResponse); // Act - result = await this.DataSource.GetCarbonIntensityForecastAsync(this.DefaultLocation, generatedAt); + result = await this.DataSource.GetHistoricalCarbonIntensityForecastAsync(this.DefaultLocation, generatedAt); } // Assert @@ -174,18 +177,18 @@ public void GetCarbonIntensityForecastAsync_ThrowsWhenRegionNotFound() this.LocationSource.Setup(l => l.ToGeopositionLocationAsync(this.DefaultLocation)).Throws(); Assert.ThrowsAsync(async () => await this.DataSource.GetCurrentCarbonIntensityForecastAsync(this.DefaultLocation)); - Assert.ThrowsAsync(async () => await this.DataSource.GetCarbonIntensityForecastAsync(this.DefaultLocation, new DateTimeOffset())); + Assert.ThrowsAsync(async () => await this.DataSource.GetHistoricalCarbonIntensityForecastAsync(this.DefaultLocation, new DateTimeOffset())); } [Test] - public void GetCarbonIntensityForecastAsync_ThrowsWhenNoForecastFoundForReuqestedTime() + public void GetHistoricalCarbonIntensityForecastAsync_ThrowsWhenNoForecastFoundForReuqestedTime() { var generatedAt = new DateTimeOffset(); - this.WattTimeClient.Setup(w => w.GetForecastOnDateAsync(this.DefaultRegion, generatedAt)).Returns(Task.FromResult(null)); + this.WattTimeClient.Setup(w => w.GetForecastOnDateAsync(this.DefaultRegion, generatedAt)).Returns(Task.FromResult(null)); // The datasource throws an exception if no forecasts are found at the requested generatedAt time. - Assert.ThrowsAsync(async () => await this.DataSource.GetCarbonIntensityForecastAsync(this.DefaultLocation, generatedAt)); + Assert.ThrowsAsync(async () => await this.DataSource.GetHistoricalCarbonIntensityForecastAsync(this.DefaultLocation, generatedAt)); } [TestCase(0, TestName = "GetCurrentCarbonIntensityForecastAsync throws for: No datapoints")] @@ -211,7 +214,7 @@ public async Task GetCarbonIntensityForecastAsync_RequiredAtRounded(string reque var requestedAt = DateTimeOffset.Parse(requested); var expectedAt = DateTimeOffset.Parse(expected); - var forecastResponse = GenerateForecastResponse(2, startTime: requestedAt); + var forecastResponse = GenerateHistoricalForecastResponse(2, startTime: requestedAt); forecastResponse.Meta.GeneratedAt = expectedAt; @@ -219,7 +222,7 @@ public async Task GetCarbonIntensityForecastAsync_RequiredAtRounded(string reque ).ReturnsAsync(() => forecastResponse); // Act - var result = await this.DataSource.GetCarbonIntensityForecastAsync(this.DefaultLocation, requestedAt); + var result = await this.DataSource.GetHistoricalCarbonIntensityForecastAsync(this.DefaultLocation, requestedAt); // Assert Assert.IsNotNull(result); @@ -305,6 +308,31 @@ private GridEmissionsDataResponse GenerateGridEmissionsResponse(int numberOfData return response; } + private HistoricalForecastEmissionsDataResponse GenerateHistoricalForecastResponse(int numberOfDatapoints, float value = 10, DateTimeOffset startTime = default) + { + var data = GenerateDataPoints(numberOfDatapoints, value, startTime); + var meta = new GridEmissionsMetaData() + { + Region = this.DefaultRegion.Region, + SignalType = SignalTypes.co2_moer + }; + + var response = new HistoricalForecastEmissionsDataResponse() + { + Data = new List() + { + new HistoricalEmissionsData() + { + Forecast = data, + GeneratedAt = DateTimeOffset.Now + } + }, + Meta = meta + }; + + return response; + } + private ForecastEmissionsDataResponse GenerateForecastResponse(int numberOfDatapoints, float value = 10, DateTimeOffset startTime = default) { var data = GenerateDataPoints(numberOfDatapoints, value, startTime); diff --git a/src/CarbonAware/src/Interfaces/IForecastDataSource.cs b/src/CarbonAware/src/Interfaces/IForecastDataSource.cs index cd8d06c08..964b0e18e 100644 --- a/src/CarbonAware/src/Interfaces/IForecastDataSource.cs +++ b/src/CarbonAware/src/Interfaces/IForecastDataSource.cs @@ -14,5 +14,5 @@ internal interface IForecastDataSource /// The location that should be used for getting the forecast. /// The historical time used to fetch the most recent forecast generated as of that time. /// A forecasted emissions object for the given location generated at the given time. - Task GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt); + Task GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt); } \ No newline at end of file diff --git a/src/CarbonAware/src/NullForecastDataSource.cs b/src/CarbonAware/src/NullForecastDataSource.cs index 7b4288dbd..9492e3d31 100644 --- a/src/CarbonAware/src/NullForecastDataSource.cs +++ b/src/CarbonAware/src/NullForecastDataSource.cs @@ -4,7 +4,7 @@ namespace CarbonAware; internal class NullForecastDataSource : IForecastDataSource { - public Task GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) + public Task GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt) { throw new ArgumentException("ForecastDataSource is not configured"); } diff --git a/src/GSF.CarbonAware/src/Handlers/ForecastHandler.cs b/src/GSF.CarbonAware/src/Handlers/ForecastHandler.cs index adc40f4de..fa8b413ab 100644 --- a/src/GSF.CarbonAware/src/Handlers/ForecastHandler.cs +++ b/src/GSF.CarbonAware/src/Handlers/ForecastHandler.cs @@ -73,7 +73,7 @@ public async Task GetForecastByDateAsync(string location, Dat { parameters.SetRequiredProperties(PropertyName.SingleLocation, PropertyName.Requested); parameters.Validate(); - var forecast = await _forecastDataSource.GetCarbonIntensityForecastAsync(parameters.SingleLocation, parameters.Requested); + var forecast = await _forecastDataSource.GetHistoricalCarbonIntensityForecastAsync(parameters.SingleLocation, parameters.Requested); var emissionsForecast = ProcessAndValidateForecast(forecast, parameters); return emissionsForecast; } diff --git a/src/GSF.CarbonAware/test/Handlers/ForecastHandlerTests.cs b/src/GSF.CarbonAware/test/Handlers/ForecastHandlerTests.cs index d0057fe48..d87733311 100644 --- a/src/GSF.CarbonAware/test/Handlers/ForecastHandlerTests.cs +++ b/src/GSF.CarbonAware/test/Handlers/ForecastHandlerTests.cs @@ -207,7 +207,7 @@ private static Mock CreateForecastByDateDataSource(global:: { var datasource = new Mock(); datasource - .Setup(x => x.GetCarbonIntensityForecastAsync(It.IsAny(), requested)) + .Setup(x => x.GetHistoricalCarbonIntensityForecastAsync(It.IsAny(), requested)) .ReturnsAsync(data); return datasource; @@ -221,7 +221,7 @@ private static Mock SetupMockDataSourceThatThrows() .ThrowsAsync(new CarbonAware.Exceptions.CarbonAwareException("")); datasource - .Setup(x => x.GetCarbonIntensityForecastAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.GetHistoricalCarbonIntensityForecastAsync(It.IsAny(), It.IsAny())) .ThrowsAsync(new CarbonAware.Exceptions.CarbonAwareException("", It.IsAny())); return datasource;