Skip to content

Commit

Permalink
Historical forecasts updated
Browse files Browse the repository at this point in the history
Historical forecasts updated
  • Loading branch information
vaughanknight committed Jun 17, 2024
1 parent 8640c8c commit 880fcf7
Show file tree
Hide file tree
Showing 13 changed files with 111 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ private static EmissionsForecast ToEmissionsForecast(Location location, Forecast
}

/// <inheritdoc />
public async Task<EmissionsForecast> GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt)
public async Task<EmissionsForecast> GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt)
{
await Task.Run(() => true);
throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ internal interface IWattTimeClient
/// <summary>
/// Async method to get generated forecast at requested time and balancing authority.
/// </summary>
/// <param name="balancingAuthorityAbbreviation">Balancing authority abbreviation</param>
/// <param name="region">Balancing authority abbreviation</param>
/// <param name="requestedAt">The historical time used to fetch the most recent forecast generated as of that time.</param>
/// <returns>An <see cref="Task{Forecast}"/> which contains forecasted emissions data points or null if no Forecast generated at the requested time.</returns>
/// <exception cref="WattTimeClientException">Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes.</exception>
Task<ForecastEmissionsDataResponse?> GetForecastOnDateAsync(string balancingAuthorityAbbreviation, DateTimeOffset requestedAt);
Task<HistoricalForecastEmissionsDataResponse?> GetForecastOnDateAsync(string region, DateTimeOffset requestedAt);

/// <summary>
/// Async method to get generated forecast at requested time and balancing authority.
Expand All @@ -62,7 +62,7 @@ internal interface IWattTimeClient
/// <param name="requestedAt">The historical time used to fetch the most recent forecast generated as of that time.</param>
/// <returns>An <see cref="Task{Forecast}"/> which contains forecasted emissions data points or null if no Forecast generated at the requested time.</returns>
/// <exception cref="WattTimeClientException">Can be thrown when errors occur connecting to WattTime client. See the WattTimeClientException class for documentation of expected status codes.</exception>
Task<ForecastEmissionsDataResponse?> GetForecastOnDateAsync(RegionResponse balancingAuthority, DateTimeOffset requestedAt);
Task<HistoricalForecastEmissionsDataResponse?> GetForecastOnDateAsync(RegionResponse balancingAuthority, DateTimeOffset requestedAt);

/// <summary>
/// Async method to get the balancing authority for a given location.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,6 @@ public async Task<ForecastEmissionsDataResponse> 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<ForecastEmissionsDataResponse?>(result, _options) ?? throw new WattTimeClientException($"Error getting forecast for {region}");

return forecast;
Expand All @@ -118,32 +115,33 @@ public Task<ForecastEmissionsDataResponse> GetCurrentForecastAsync(RegionRespons
}

/// <inheritdoc/>
public async Task<ForecastEmissionsDataResponse?> GetForecastOnDateAsync(string balancingAuthorityAbbreviation, DateTimeOffset requestedAt)
public async Task<HistoricalForecastEmissionsDataResponse?> 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<string, string>()
{
{ 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<string, string>()
{
{ 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<List<ForecastEmissionsDataResponse>>(result, _options) ?? throw new WattTimeClientException($"Error getting forecasts for {balancingAuthorityAbbreviation}");
return forecasts.FirstOrDefault();
var historicalForecastResponse = await JsonSerializer.DeserializeAsync<HistoricalForecastEmissionsDataResponse>(result, _options) ?? throw new WattTimeClientException($"Error getting forecasts for {region}");
return historicalForecastResponse;
}
}

/// <inheritdoc/>
public Task<ForecastEmissionsDataResponse?> GetForecastOnDateAsync(RegionResponse balancingAuthority, DateTimeOffset requestedAt)
public Task<HistoricalForecastEmissionsDataResponse?> GetForecastOnDateAsync(RegionResponse region, DateTimeOffset requestedAt)
{
return this.GetForecastOnDateAsync(balancingAuthority.Region, requestedAt);
return this.GetForecastOnDateAsync(region.Region, requestedAt);
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GridEmissionDataPoint> Forecast { get; set; } = new List<GridEmissionDataPoint>();

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;

namespace CarbonAware.DataSources.WattTime.Model;

[Serializable]
internal record HistoricalForecastEmissionsDataResponse
{
[JsonPropertyName("data")]
public List<HistoricalEmissionsData> Data { get; set; } = new List<HistoricalEmissionsData>();


[JsonPropertyName("meta")]
public GridEmissionsMetaData Meta { get; set; } = new GridEmissionsMetaData();
}


Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public async Task<EmissionsForecast> GetCurrentCarbonIntensityForecastAsync(Loca
}

/// <inheritdoc />
public async Task<EmissionsForecast> GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt)
public async Task<EmissionsForecast> GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt)
{
this.Logger.LogInformation($"Getting carbon intensity forecast for location {location} requested at {requestedAt}");
var balancingAuthority = await this.GetBalancingAuthority(location);
Expand All @@ -100,7 +100,26 @@ public async Task<EmissionsForecast> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -174,18 +177,18 @@ public void GetCarbonIntensityForecastAsync_ThrowsWhenRegionNotFound()
this.LocationSource.Setup(l => l.ToGeopositionLocationAsync(this.DefaultLocation)).Throws<LocationConversionException>();

Assert.ThrowsAsync<LocationConversionException>(async () => await this.DataSource.GetCurrentCarbonIntensityForecastAsync(this.DefaultLocation));
Assert.ThrowsAsync<LocationConversionException>(async () => await this.DataSource.GetCarbonIntensityForecastAsync(this.DefaultLocation, new DateTimeOffset()));
Assert.ThrowsAsync<LocationConversionException>(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<ForecastEmissionsDataResponse?>(null));
this.WattTimeClient.Setup(w => w.GetForecastOnDateAsync(this.DefaultRegion, generatedAt)).Returns(Task.FromResult<HistoricalForecastEmissionsDataResponse?>(null));

// The datasource throws an exception if no forecasts are found at the requested generatedAt time.
Assert.ThrowsAsync<ArgumentException>(async () => await this.DataSource.GetCarbonIntensityForecastAsync(this.DefaultLocation, generatedAt));
Assert.ThrowsAsync<ArgumentException>(async () => await this.DataSource.GetHistoricalCarbonIntensityForecastAsync(this.DefaultLocation, generatedAt));
}

[TestCase(0, TestName = "GetCurrentCarbonIntensityForecastAsync throws for: No datapoints")]
Expand All @@ -211,15 +214,15 @@ 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;


this.WattTimeClient.Setup(w => w.GetForecastOnDateAsync(this.DefaultRegion, expectedAt)
).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);
Expand Down Expand Up @@ -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<HistoricalEmissionsData>()
{
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);
Expand Down
2 changes: 1 addition & 1 deletion src/CarbonAware/src/Interfaces/IForecastDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ internal interface IForecastDataSource
/// <param name="location">The location that should be used for getting the forecast.</param>
/// <param name="requestedAt">The historical time used to fetch the most recent forecast generated as of that time.</param>
/// <returns>A forecasted emissions object for the given location generated at the given time.</returns>
Task<EmissionsForecast> GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt);
Task<EmissionsForecast> GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt);
}
2 changes: 1 addition & 1 deletion src/CarbonAware/src/NullForecastDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace CarbonAware;

internal class NullForecastDataSource : IForecastDataSource
{
public Task<EmissionsForecast> GetCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt)
public Task<EmissionsForecast> GetHistoricalCarbonIntensityForecastAsync(Location location, DateTimeOffset requestedAt)
{
throw new ArgumentException("ForecastDataSource is not configured");
}
Expand Down
2 changes: 1 addition & 1 deletion src/GSF.CarbonAware/src/Handlers/ForecastHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public async Task<EmissionsForecast> 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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/GSF.CarbonAware/test/Handlers/ForecastHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ private static Mock<IForecastDataSource> CreateForecastByDateDataSource(global::
{
var datasource = new Mock<IForecastDataSource>();
datasource
.Setup(x => x.GetCarbonIntensityForecastAsync(It.IsAny<Location>(), requested))
.Setup(x => x.GetHistoricalCarbonIntensityForecastAsync(It.IsAny<Location>(), requested))
.ReturnsAsync(data);

return datasource;
Expand All @@ -221,7 +221,7 @@ private static Mock<IForecastDataSource> SetupMockDataSourceThatThrows()
.ThrowsAsync(new CarbonAware.Exceptions.CarbonAwareException(""));

datasource
.Setup(x => x.GetCarbonIntensityForecastAsync(It.IsAny<Location>(), It.IsAny<DateTimeOffset>()))
.Setup(x => x.GetHistoricalCarbonIntensityForecastAsync(It.IsAny<Location>(), It.IsAny<DateTimeOffset>()))
.ThrowsAsync(new CarbonAware.Exceptions.CarbonAwareException("", It.IsAny<Exception>()));

return datasource;
Expand Down

0 comments on commit 880fcf7

Please sign in to comment.