diff --git a/.github/workflows/1-pr.yaml b/.github/workflows/1-pr.yaml index 581c168af..0174ad653 100644 --- a/.github/workflows/1-pr.yaml +++ b/.github/workflows/1-pr.yaml @@ -122,9 +122,11 @@ jobs: directory: ${{ env.DOTNET_SRC_DIR }} - name: Generate Open API - run: dotnet tool run swagger tofile --output ./wwwroot/api/v1/swagger.yaml --yaml ${{ env.DLL_FILE_PATH }} v1 env: + DataSources__EmissionsDataSource: Json + DataSources__Configurations__Json__Type: JSON DOTNET_ROLL_FORWARD: LatestMajor + run: dotnet tool run swagger tofile --output ./wwwroot/api/v1/swagger.yaml --yaml ${{ env.DLL_FILE_PATH }} v1 working-directory: ./src/CarbonAware.WebApi/src - name: Upload swagger artifact @@ -146,7 +148,7 @@ jobs: - name: Docker Run Container run: | - docker run -d --name runnable-container -p 8080:8080 ca-api + docker run -d --name runnable-container -e DataSources__EmissionsDataSource=Json -e DataSources__Configurations__Json__Type=JSON -p 8080:8080 ca-api docker container ls - name: Docker WGET Health Endpoint @@ -182,6 +184,8 @@ jobs: - name: Generate Open API run: dotnet tool run swagger tofile --output ./wwwroot/api/v1/swagger.yaml --yaml ${{ env.DLL_FILE_PATH }} v1 env: + DataSources__EmissionsDataSource: Json + DataSources__Configurations__Json__Type: JSON DOTNET_ROLL_FORWARD: LatestMajor - name: Upload dev artifact uses: actions/upload-artifact@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a8c52f48..7bb7e4934 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to the Carbon Aware SDK will be documented in this file. + +## [1.8] - 2025-04 + +This release consolidates the ElectricityMaps sources, introduces new CO₂-intensity signal support in WattTime, and rounds out the release with documentation improvements and critical bug fixes [https://github.com/Green-Software-Foundation/carbon-aware-sdk/labels/v1.8](https://github.com/Green-Software-Foundation/carbon-aware-sdk/labels/v1.8). + +### Added + + +- [PR #619] feat(ElectricityMaps source): unify ElectricityMaps and ElectricityMapsFree ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/619) +- [PR #614] Make documentation for forecasts signal agnostic ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/614) +- [PR #611] Support co2_aoer in WattTime ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/611) +- [PR #450] Remove JSON examples from artifacts ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/450) + +And supporting issues: + +- [#612] Documentation: Update wording about marginal signal ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/issues/612) +- [#440] [Feature Contribution]: Remove JSON examples from WebAPI container ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/issues/440) +- [#396] [Feature Contribution]: Better to throw exception when future datetime is specified to endpoints for current/historical data ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/issues/396) + +### Fixed + +- [#613] [Bug]: Electricity Maps API configuration is out of date ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/issues/613) +- [PR #623] [URGENT] Set JsonStringConverter to JSON deserializer option in WattTimeClient ](https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/623) + +For more details, checkout [https://github.com/Green-Software-Foundation/carbon-aware-sdk/issues/628](https://github.com/Green-Software-Foundation/carbon-aware-sdk/issues/628) + ## [1.7] - 2025-01 This release mainly introduces new clients libraries for NPM and Java [https://github.com/Green-Software-Foundation/carbon-aware-sdk/labels/v1.7](https://github.com/Green-Software-Foundation/carbon-aware-sdk/labels/v1.7) diff --git a/casdk-docs/docs/architecture/c-sharp-client-library.md b/casdk-docs/docs/architecture/c-sharp-client-library.md index 2b47fe4ee..b5479a215 100644 --- a/casdk-docs/docs/architecture/c-sharp-client-library.md +++ b/casdk-docs/docs/architecture/c-sharp-client-library.md @@ -48,8 +48,7 @@ The following namespaces are included: There are two main classes that represents the data fetched from the data sources (i.e `Static Json`, [WattTime](https://www.watttime.org), -[ElectricityMaps](https://www.electricitymaps.com), and -[ElectricityMapsFree](https://www.co2signal.com/)): +[ElectricityMaps](https://www.electricitymaps.com)): - `EmissionsData` - `EmissionsForecast` diff --git a/casdk-docs/docs/architecture/data-sources.md b/casdk-docs/docs/architecture/data-sources.md index c31f68b0c..df535c5e4 100644 --- a/casdk-docs/docs/architecture/data-sources.md +++ b/casdk-docs/docs/architecture/data-sources.md @@ -7,8 +7,7 @@ sidebar_position: 3 Data sources allow developers easily integrate different data providers into the carbon aware SDK ([WattTime](https://www.wattime.org), -[ElectricityMaps](https://www.electricitymaps.com/), -[ElectricityMapsFree](https://www.co2signal.com/) etc) to be made available to +[ElectricityMaps](https://www.electricitymaps.com/), etc) to be made available to all higher-level user-interfaces (WebAPI, CLI, etc), while avoiding the details of how to interact with any specific provider. diff --git a/casdk-docs/docs/architecture/decisions/0017-watt-time-signal-type.md b/casdk-docs/docs/architecture/decisions/0017-watt-time-signal-type.md new file mode 100644 index 000000000..920d22bbd --- /dev/null +++ b/casdk-docs/docs/architecture/decisions/0017-watt-time-signal-type.md @@ -0,0 +1,50 @@ +# 0017. Signal Type in WattTime v3 Data Source + +## Status + +Proposed + +## Context + +WattTime v3 API has been supported since [Carbon Aware SDK v1.5.0](https://carbon-aware-sdk.greensoftware.foundation/blog/release-v1.5). As we mentioned in [ADR-0015](https://carbon-aware-sdk.greensoftware.foundation/docs/architecture/decisions/watt-time-v3), `signal_type` has been added in each endpoints which the SDK will access since v3 API. We should be able to set following parameters to it, but it can't in Carbo Aware SDK. + +https://watttime.org/data-science/data-signals/ + +| Signal Type | Description | +|---|---| +| `co2_moer` | Marginal Operating Emissions Rate of carbon dioxide. | +| `co2_aoer` | Average Operating Emissions Rate of carbon dioxide. | + +According to [Green Software Practitioners](https://learn.greensoftware.foundation/carbon-awareness#marginal-carbon-intensity), "marginal" means the carbon intensity of the power plant that would have to be employed to meet any new demand. On the other hand, "average" means the average of all of power plants. It should be chosen by Carbon Aware SDK user because which value is needed depends on the user. + +`co2_moer` is hard-coded until Carbon Aware SDK v1.7.0 (at least). + +## Decision + +The proposal is for adding a new parameter for Signal Type in WattTime Data Source. + +## Update Changes + +We will introduce new parameter for data source configuration of WattTime as following. + +### appsettings.json + +```json +"WattTime": { + "Type": "WattTime", + "Username": "username", + "Password": "password", + "BaseURL": "https://api.watttime.org/v3/", + "SignalType": "co2_aoer" +} +``` + +### environment variable + +```bash +DataSources__Configurations__WattTime__SignalType=co2_aoer +``` + +## Green Impact + +Neutral diff --git a/casdk-docs/docs/overview/enablement.md b/casdk-docs/docs/overview/enablement.md index 01b296791..0053f859a 100644 --- a/casdk-docs/docs/overview/enablement.md +++ b/casdk-docs/docs/overview/enablement.md @@ -13,7 +13,7 @@ sidebar_position: 3 * SDK * Use cases -2. [How to Use Carbon Aware SDK](#2-how-to-use-carbon-aware-sdk) +2. [How to Use Carbon Aware SDK](#2-how-to-use-carbon-aware-sdk) 2.1 [Pre-requisites](#21-pre-requisites) * Data sources @@ -23,13 +23,13 @@ sidebar_position: 3 * Setup * Usage - 2.3 [WebAPI](#23-webapi) + 2.3 [WebAPI](#23-webapi) * Setup * Deploying with container * Deploying with Kubernetes * Usage * Calling WebAPI using CLI - * Calling WebAPI using client libraries + * Calling WebAPI using client libraries 2.4 [Configurations](#24-configurations) @@ -43,16 +43,16 @@ you different data than the WebAPI for the same query). We provide a number of different endpoints to provide the most flexibility to integrate to your environment: -* CLI +* CLI You can run the application using the [CLI](/src/CarbonAware.CLI) and refer - to more documentation [here](../tutorial-basics/carbon-aware-cli.md). + to more documentation [here](../tutorial-basics/carbon-aware-cli.md). -* WebAPI +* WebAPI You can build a container containing the [WebAPI](/src/CarbonAware.WebApi) and connect via REST requests and refer to more documentation [here](../tutorial-basics/carbon-aware-webapi.md). -* SDK +* SDK You can reference the [Carbon Aware C# Library](/src/GSF.CarbonAware) in your projects and make use of its functionalities and features. @@ -75,7 +75,6 @@ We support various data sources of carbon aware data: * [WattTime](https://www.watttime.org/) * [ElectricityMaps](https://www.electricitymaps.com/) -* [ElectricityMapsFree](https://www.co2signal.com/) * JSON file There are a few constraints to select data sources to some functions of @@ -91,7 +90,7 @@ providers into the carbon aware SDK. * Alternatively: * Docker * VSCode and its [Remote Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) -* WebAPI +* WebAPI * Docker * VSCode and its [Remote Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) @@ -127,20 +126,13 @@ VSCode Remote Containers (Dev Container). To run locally: or ```bash + export DataSources__EmissionsDataSource="ElectricityMaps" export DataSources__ForecastDataSource="ElectricityMaps" export DataSources__Configurations__ElectricityMaps__Type="ElectricityMaps" export DataSources__Configurations__ElectricityMaps__APITokenHeader="auth-token" export DataSources__Configurations__ElectricityMaps__APIToken="" ``` - or - - ```bash - export DataSources__EmissionsDataSource="ElectricityMapsFree" - export DataSources__Configurations__ElectricityMapsFree__Type="ElectricityMapsFree" - export DataSources__Configurations__ElectricityMapsFree__token="" - ``` - 4. Run the CLI using `dotnet run` The CLI will ask you to at minimum provide a `--location (-l)` parameter. @@ -155,6 +147,8 @@ parameters and short explanations. To get a list of all locations supported, you can use the Locations API, referenced in `src/CarbonAware.CLI/src/Commands/Location` and the command `.\caw locations`. +Note that you have to configure `LocationDataSourcesConfiguration` +into `appsettings.json` before running the command. Expected output: @@ -269,19 +263,13 @@ First we need to set up the GitHub repository or ```bash + export DataSources__EmissionsDataSource="ElectricityMaps" export DataSources__ForecastDataSource="ElectricityMaps" export DataSources__Configurations__ElectricityMaps__Type="ElectricityMaps" export DataSources__Configurations__ElectricityMaps__APITokenHeader="auth-token" export DataSources__Configurations__ElectricityMaps__APIToken="" ``` - or - - ```bash - export DataSources__EmissionsDataSource="ElectricityMapsFree" - export DataSources__Configurations__ElectricityMapsFree__Type="ElectricityMapsFree" - export DataSources__Configurations__ElectricityMapsFree__token="" - ``` 6. In the VSCode Terminal: 7. Change directory to: `cd src/CarbonAware.WebApi/src` @@ -452,6 +440,9 @@ endpoints, full endpoint description can be found [here](https://github.com/Gree To get a list of all locations supported, you can use the Locations API endpoint `/locations` referenced in `src/CarbonAware.WebApi/src/Controllers/LocationsController.cs`. +Note that you have to configure `LocationDataSourcesConfiguration` +into `appsettings.json` before launching WebAPI, otherwise WebAPI returns +HTTP 204 (No Content). Expected Output: @@ -474,12 +465,16 @@ Expected Output: ##### Calling the `/emissions/bylocation` endpoint In console, we can run the below command, to request data for a single location -(currently Azure region names supported) in a particular timeframe: +in a particular timeframe: ```bash curl "http://localhost:5073/emissions/bylocation?location=westus&time=2022-08-23T14%3A00&toTime=2022-08-23T14%3A30" | jq ``` +Note that region names in this example (e.g. `westus`) are defined in +[azure-regions.json](https://github.com/Green-Software-Foundation/carbon-aware-sdk/blob/dev/src/data/location-sources/azure-regions.json). +AWS region is also available in [aws-regions.json](https://github.com/Green-Software-Foundation/carbon-aware-sdk/blob/dev/src/data/data-files/aws-regions.json). + You can omit the `| jq` to get the JSON data raw and unparsed. This is a request for data in the `westus` region from the date `2022-08-23 at 14:00` to `2022-08-23 at 14:30`. (Note: semicolons `:` are encoded as `%3A` in URLs). diff --git a/casdk-docs/docs/quickstart.md b/casdk-docs/docs/quickstart.md index a500520f5..73de56591 100644 --- a/casdk-docs/docs/quickstart.md +++ b/casdk-docs/docs/quickstart.md @@ -55,19 +55,13 @@ VSCode Remote Containers (Dev Container). To run locally: or ```bash + export DataSources__EmissionsDataSource="ElectricityMaps" export DataSources__ForecastDataSource="ElectricityMaps" export DataSources__Configurations__ElectricityMaps__Type="ElectricityMaps" export DataSources__Configurations__ElectricityMaps__APITokenHeader="auth-token" export DataSources__Configurations__ElectricityMaps__APIToken="" ``` - or - - ```bash - export DataSources__EmissionsDataSource="ElectricityMapsFree" - export DataSources__Configurations__ElectricityMapsFree__Type="ElectricityMapsFree" - export DataSources__Configurations__ElectricityMapsFree__token="" - ``` 1. Run the CLI using `dotnet run` @@ -81,7 +75,8 @@ parameters and short explanations. To get a list of all locations supported, you can use the Locations API, referenced in `src/CarbonAware.CLI/src/Commands/Location` -and the command `.\caw locations`. +and the command `.\caw locations`. Note that you have to configure `LocationDataSourcesConfiguration` +into `appsettings.json` before running the command. Expected output: @@ -213,19 +208,13 @@ First we need to set up the GitHub repository or ```bash + export DataSources__EmissionsDataSource="ElectricityMaps" export DataSources__ForecastDataSource="ElectricityMaps" export DataSources__Configurations__ElectricityMaps__Type="ElectricityMaps" export DataSources__Configurations__ElectricityMaps__APITokenHeader="auth-token" export DataSources__Configurations__ElectricityMaps__APIToken="" ``` - or - - ```bash - export DataSources__EmissionsDataSource="ElectricityMapsFree" - export DataSources__Configurations__ElectricityMapsFree__Type="ElectricityMapsFree" - export DataSources__Configurations__ElectricityMapsFree__token="" - ``` 6. In the VSCode Terminal: 7. Change directory to: `cd src/CarbonAware.WebApi/src` @@ -246,6 +235,9 @@ endpoints, full endpoint description can be found here: To get a list of all locations supported, you can use the Locations API endpoint `/locations` referenced in `src/CarbonAware.WebApi/src/Controllers/LocationsController.cs`. +Note that you have to configure `LocationDataSourcesConfiguration` +into `appsettings.json` before launching WebAPI, otherwise WebAPI returns +HTTP 204 (No Content). Expected Output: @@ -269,12 +261,16 @@ Expected Output: #### Calling the `/emissions/bylocation` endpoint In console, we can run the below command, to request data for a single location -(currently Azure region names supported) in a particular timeframe: +in a particular timeframe: ```bash curl "http://localhost:5073/emissions/bylocation?location=westus&time=2022-08-23T14%3A00&toTime=2022-08-23T14%3A30" | jq ``` +Note that region names in this example (e.g. `westus`) are defined in +[azure-regions.json](https://github.com/Green-Software-Foundation/carbon-aware-sdk/blob/dev/src/data/location-sources/azure-regions.json). +AWS region is also available in [aws-regions.json](https://github.com/Green-Software-Foundation/carbon-aware-sdk/blob/dev/src/data/data-files/aws-regions.json). + You can omit the `| jq` to get the JSON data raw and unparsed. This is a request for data in the `westus` region from the date `2022-08-23 at 14:00` to `2022-08-23 at 14:30`. (Note: semicolons `:` are encoded as `%3A` in URLs). diff --git a/casdk-docs/docs/tutorial-basics/carbon-aware-webapi.md b/casdk-docs/docs/tutorial-basics/carbon-aware-webapi.md index 028db6273..8985d5a20 100644 --- a/casdk-docs/docs/tutorial-basics/carbon-aware-webapi.md +++ b/casdk-docs/docs/tutorial-basics/carbon-aware-webapi.md @@ -1,8 +1,8 @@ # Carbon Aware WebApi -The Carbon Aware SDK provides an API to get the marginal carbon intensity for a +The Carbon Aware SDK provides an API to get the carbon intensity for a given location and time period. The values reported in the Green Software -Foundation's specification for marginal carbon intensity (Grams per Kilowatt +Foundation's specification for carbon intensity (Grams per Kilowatt Hour). **_Highly Recommended_** - This user interface is best for when you can change @@ -36,9 +36,6 @@ client generation - [ElectricityMaps](#electricitymaps) - [Locations](#electricitymaps-locations) - [Exception Handling](#electricitymaps-exception-handling) - - [ElectricityMapsFree](#electricitymapsfree) - - [Locations](#locations) - - [Exception Handling](#exception-handling) ## Endpoints @@ -131,11 +128,11 @@ This endpoint fetches only the most recently generated forecast for all provided locations. It uses the "dataStartAt" and "dataEndAt" parameters to scope the forecasted data points (if available for those times). If no start or end time boundaries are provided, the entire forecast dataset is used. The scoped data -points are used to calculate average marginal carbon intensities of the -specified "windowSize" and the optimal marginal carbon intensity window is +points are used to calculate average carbon intensities of the +specified "windowSize" and the optimal carbon intensity window is identified. -The forecast data represents what the data source predicts future marginal +The forecast data represents what the data source predicts future carbon intesity values to be, not actual measured emissions data (as future values cannot be known). @@ -154,7 +151,7 @@ Parameters: current forecast data points after this time. Must be within the forecast data point timestamps. Defaults to the latest time in the forecast data. If neither `dataStartAt` nor `dataEndAt` are provided, all forecasted data - points are used in calculating the optimal marginal carbon intensity window. + points are used in calculating the optimal carbon intensity window. 4. `windowSize`: The estimated duration (in minutes) of the workload. Defaults to the duration of a single forecast data point. @@ -163,7 +160,7 @@ https:///emissions/forecasts/current?location=northeurope&dataStart ``` The response is an array of forecasts (one per requested location) with their -optimal marginal carbon intensity windows. +optimal carbon intensity windows. ```json [ @@ -202,7 +199,7 @@ optimal marginal carbon intensity windows. ### POST emissions/forecasts/batch This endpoint takes a batch of requests for historical forecast data, fetches -them, and calculates the optimal marginal carbon intensity windows for each +them, and calculates the optimal carbon intensity windows for each using the same parameters available to the '/emissions/forecasts/current' endpoint. @@ -227,7 +224,7 @@ Parameters: to the duration of a single forecast data point If neither `dataStartAt` nor `dataEndAt` are provided, all forecasted data -points are used in calculating the optimal marginal carbon intensity window. +points are used in calculating the optimal carbon intensity window. ```json [ @@ -249,7 +246,7 @@ points are used in calculating the optimal marginal carbon intensity window. ``` The response is an array of forecasts (one per requested location) with their -optimal marginal carbon intensity windows. +optimal carbon intensity windows. ```json [ @@ -310,7 +307,7 @@ https:///emissions/average-carbon-intensity?location=eastus&startTi ``` The response is a single object that contains the information about the request -and the average marginal carbon intensity +and the average carbon intensity ```json { @@ -330,7 +327,7 @@ location and time period. This endpoint only supports batching across a single location with different time boundaries. If multiple locations are provided, an error is returned. For each item in the request array, the application returns a corresponding object -containing the location, time boundaries, and average marginal carbon intensity. +containing the location, time boundaries, and average carbon intensity. Parameters: @@ -364,7 +361,7 @@ Parameters: ``` The response is an array of CarbonIntensityDTO objects which each have a -location, start time, end time, and the average marginal carbon intensity over +location, start time, end time, and the average carbon intensity over that time period. ```json @@ -491,9 +488,9 @@ specification dotnet build --configuration Release --no-restore dotnet tool run swagger tofile --output ./wwwroot/api/v1/swagger.yaml --yaml bin/Release/net8.0/CarbonAware.WebApi.dll v1 ``` -1. The `CarbonAware.WebApi/src/wwwroot/api/v1/swagger.yaml` file contains the supported +1. The `CarbonAware.WebApi/src/wwwroot/api/v1/swagger.yaml` file contains the supported OpenApi specification. -1. Use for instance [swagger editor](https://editor.swagger.io) to see and try +1. Use for instance [swagger editor](https://editor.swagger.io) to see and try the endpoint routes. ## Data Sources @@ -550,24 +547,3 @@ Data Source will forward the response code and message back to the caller. Refer to the [ElectricityMapsHttpClientException](../src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/Client/ElectricityMapsClientHttpException.cs) class for documentation on expected error codes. - -### ElectricityMapsFree - -#### Locations - -Each ElectricityMapsFree emissions data point is associated with a particular -named country code. While the ElectricityMapsFree endpoint supports calling with -lat/long geoposition as well, the result will always be a corresponding country -code. -They provide a -[route on their parent API (ElectricityMaps)](https://static.electricitymaps.com/api/docs/index.html#zones) -which can be queried to list all the country codes you have access to given your -token. - -#### Exception Handling - -If ElectricityMapsFree responds with a 4XX or 5XX status code the -ElectricityMapsFree Data Source will forward the response code and message back -to the caller. Refer to the -[ElectricityMapsFreeHttpClientException](../src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/ElectricityMapsFreeClientHttpException.cs) -class for documentation on expected error codes. diff --git a/casdk-docs/docs/tutorial-basics/containerization.md b/casdk-docs/docs/tutorial-basics/containerization.md index 8f8211e05..921c399a3 100644 --- a/casdk-docs/docs/tutorial-basics/containerization.md +++ b/casdk-docs/docs/tutorial-basics/containerization.md @@ -49,16 +49,6 @@ carbon_aware v1 6293e2528bf2 About an hour ago 230MB > carbon_aware:v1 ``` - or the [ElectricityMapsFree](https://www.co2signal.com/) provider - - ```sh - docker run --rm -p 8000:8080 \ - > -e DataSources__EmissionsDataSource="ElectricityMapsFree" \ - > -e DataSources__Configurations__ElectricityMapsFree__Type="ElectricityMapsFree" \ - > -e DataSources__Configurations__ElectricityMapsFree__token="" \ - > carbon_aware:v1 - ``` - 1. Verify that the WebApi is responding to requests using an HTTP client tool (e.g. `postman`, `curl`) diff --git a/casdk-docs/docs/tutorial-extras/carbon-aware-library.md b/casdk-docs/docs/tutorial-extras/carbon-aware-library.md index 2a8be2f3b..c5c594226 100644 --- a/casdk-docs/docs/tutorial-extras/carbon-aware-library.md +++ b/casdk-docs/docs/tutorial-extras/carbon-aware-library.md @@ -1,8 +1,8 @@ # Carbon Aware Library -The Carbon Aware SDK provides a C\# Client Library to get the marginal carbon +The Carbon Aware SDK provides a C\# Client Library to get the carbon intensity for a given location and time period. The values reported in the Green -Software Foundation's specification for marginal carbon intensity (Grams per +Software Foundation's specification for carbon intensity (Grams per Kilowatt Hour). **_Recommended_** - This user interface is best for when you need a consumable @@ -30,9 +30,6 @@ same configuration - [ElectricityMaps](#electricitymaps) - [Locations](#locations) - [Exception Handling](#exception-handling) - - [ElectricityMapsFree](#electricitymapsfree) - - [Locations](#locations) - - [Exception Handling](#exception-handling) ## EmissionsHandler Functions @@ -247,8 +244,7 @@ var data = await this._emissionsHandler.GetAverageCarbonIntensityAsync( ); ``` -The response is a single double value representing the calculated average -marginal carbon intensity g/kWh. +The response is a single double value representing the calculated average carbon intensity g/kWh. ```csharp 345.434 @@ -269,11 +265,11 @@ This function fetches only the most recently generated forecast for all provided locations. It uses the "dataStartAt" and "dataEndAt" parameters to scope the forecasted data points (if available for those times). If no start or end time boundaries are provided, the entire forecast dataset is used. The scoped data -points are used to calculate average marginal carbon intensities of the -specified "windowSize" and the optimal marginal carbon intensity window is +points are used to calculate average carbon intensities of the +specified "windowSize" and the optimal carbon intensity window is identified. -The forecast data represents what the data source predicts future marginal +The forecast data represents what the data source predicts future carbon intensity values to be, not actual measured emissions data (as future values cannot be known). @@ -292,7 +288,7 @@ Parameters: current forecast data points after this time. Must be within the forecast data point timestamps. Defaults to the latest time in the forecast data. If neither `dataStartAt` nor `dataEndAt` are provided, all forecasted data - points are used in calculating the optimal marginal carbon intensity window. + points are used in calculating the optimal carbon intensity window. 4. `windowSize`: The estimated duration (in minutes) of the workload. Defaults to the duration of a single forecast data point. @@ -306,7 +302,7 @@ var data = await this._forecastHandler.GetCurrentForecastAsync( ``` The response is an array of `EmissionsForecast` objects (one per requested -location) with their optimal marginal carbon intensity windows. +location) with their optimal carbon intensity windows. ```csharp [ @@ -347,7 +343,7 @@ location) with their optimal marginal carbon intensity windows. ### GetForecastByDateAsync This function takes a requests for historical forecast data, fetches it, and -calculates the optimal marginal carbon intensity window. This endpoint is useful +calculates the optimal carbon intensity window. This endpoint is useful for back-testing what one might have done in the past, if they had access to the current forecast at the time. @@ -367,7 +363,7 @@ Parameters: to the duration of a single forecast data point If neither `dataStartAt` nor `dataEndAt` are provided, all forecasted data -points are used in calculating the optimal marginal carbon intensity window. +points are used in calculating the optimal carbon intensity window. ```csharp var data = await this._forecastHandler.GetForecastByDateAsync( @@ -379,7 +375,7 @@ var data = await this._forecastHandler.GetForecastByDateAsync( ); ``` -The response is an `EmissionsForecast` object with the optimal marginal carbon +The response is an `EmissionsForecast` object with the optimal carbon intensity window. ```csharp @@ -470,24 +466,3 @@ Data Source will forward the response code and message back to the caller. Refer to the [ElectricityMapsHttpClientException](../src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMaps/src/Client/ElectricityMapsClientHttpException.cs) class for documentation on expected error codes. - -### ElectricityMapsFree - -#### Locations - -Each ElectricityMapsFree emissions data point is associated with a particular -named country code. While the ElectricityMapsFree endpoint supports calling with -lat/long geoposition as well, the result will always be a corresponding country -code. -They provide a -[route on their parent API (ElectricityMaps)](https://static.electricitymaps.com/api/docs/index.html#zones) -which can be queried to list all the country codes you have access to given your -token. - -#### Exception Handling - -If ElectricityMapsFree responds with a 4XX or 5XX status code the -ElectricityMapsFree Data Source will forward the response code and message back -to the caller. Refer to the -[ElectricityMapsFreeHttpClientException](../src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/ElectricityMapsFreeClientHttpException.cs) -class for documentation on expected error codes. diff --git a/casdk-docs/docs/tutorial-extras/configuration.md b/casdk-docs/docs/tutorial-extras/configuration.md index 416dce2ed..e5eb73dc4 100644 --- a/casdk-docs/docs/tutorial-extras/configuration.md +++ b/casdk-docs/docs/tutorial-extras/configuration.md @@ -9,6 +9,7 @@ - [baseUrl](#baseurl) - [Proxy](#proxy) - [WattTime Caching BalancingAuthority](#watttime-caching-balancingauthority) + - [SignalType](#signaltype) - [Json Configuration](#json-configuration) - [ElectricityMaps Configuration](#electricitymaps-configuration) - [API Token Header](#api-token-header) @@ -16,9 +17,6 @@ - [BaseUrl](#baseurl) - [Emission Factor Type](#emission-factor-type) - [Disable Estimations](#disable-estimations) - - [ElectricityMapsFree Configuration](#electricitymapsfree-configuration) - - [API Token](#api-token) - - [BaseUrl](#baseurl) - [Cache](#cache) - [CarbonAwareVars](#carbonawarevars) - [Tracing and Monitoring Configuration](#tracing-and-monitoring-configuration) @@ -30,7 +28,6 @@ - [Configuration for Emissions data Using WattTime](#configuration-for-emissions-data-using-watttime) - [Configuration for Forecast data Using ElectricityMaps](#configuration-for-forecast-data-using-electricitymaps) - [Configuration for Emissions data using ElectricityMaps and Forecast data using WattTime](#configuration-for-emissions-data-using-electricitymaps-and-forecast-data-using-watttime) - - [Configuration for Emissions data using ElectricityMapsFree and Forecast data using WattTime](#configuration-for-emissions-data-using-electricitymapsfree-and-forecast-data-using-watttime) - [Configuration For Emissions data Using JSON](#configuration-for-emissions-data-using-json) - [Configuration Using WattTime and Defined Location Source Files](#configuration-using-watttime-and-defined-location-source-files) @@ -111,7 +108,8 @@ data provider must also be supplied. "url": "http://10.10.10.1", "username": "proxyUsername", "password": "proxyPassword" - } + }, + "SignalType": "co2_aoer" }, "ElectricityMaps": { "Type": "ElectricityMaps", @@ -187,6 +185,20 @@ recommends not caching for longer than 1 month. DataSources__Configurations__WattTime__BalancingAuthorityCacheTTL="90" ``` +#### SignalType + +WattTime supports 2 signal type. They can be set as a parameter. + +* `co2_moer`: Marginal operating emissions rate +* `co2_aoer`: Average operating emissions rate + +If values other than these are set, an error occurs. +See [WattTime documentation](https://watttime.org/data-science/data-signals/) for details. + +```bash +DataSources__Configurations__WattTime__SignalType=co2_aoer +``` + ### Json Configuration By setting @@ -296,44 +308,6 @@ See the [ElectricityMaps API Documentation](https://static.electricitymaps.com/api/docs/index.html#estimations) for more details. -### ElectricityMapsFree Configuration - -If using the ElectricityMapsFree data source, ElectricityMapsFree configuration -is required. - -**With an account token:** - -> **NOTE** The ElectricityMapsFree API does not currently support access to -> historical forecasts. This means that functionality such as the CLI -> `emissions-forecasts` > `--requested-at` flag and the API `/forecasts/batch` > -> `requestedAt` input will respond with a `NotImplemented` error. -> -> The ElectricityMapsFree API also does not currently support access to -> historical emissions data. It only supports getting the single latest -> emissions data point for the given location. -> -> If either of these restrictions are an issue, a data source that has support -> for historical forecasts, such as [WattTime](#watttime-configuration) or -> historical emissions, such as -> [ElectricityMaps](#electricitymaps-configuration) may be preferable. - -```json -{ - "token": "", - "baseUrl": "https://api.co2signal.com/v1/" -} -``` - -#### API Token - -The ElectricityMapsFree token you receive with your account. - -#### BaseUrl - -The url to use when connecting to ElectricityMapsFree. Defaults to -"https://api.co2signal.com/v1/" but can be overridden in the config if needed -(such as to enable integration testing scenarios). - ## Cache Frequent access to data sources could cause problems such as performance trouble @@ -434,7 +408,7 @@ The scraping endpoint is `/metrics` like this: http://localhost/metrics ``` -By default, the exposed data are latest ones within last 24 hours. If you would like to change the period +By default, the exposed data are latest ones within last 24 hours. If you would like to change the period in some reasones, you can configure the value like this: ```json @@ -613,28 +587,6 @@ DataSources__Configurations__WattTime__Password="wattTimePassword" } ``` -## Configuration for Emissions data using ElectricityMapsFree and Forecast data using WattTime - -```json - "DataSources": { - "EmissionsDataSource": "ElectricityMapsFree", - "ForecastDataSource": "WattTime", - "Configurations": { - "WattTime": { - "Type": "WattTime", - "Username": "username", - "Password": "password", - "BaseURL": "https://api2.watttime.org/v2/", - }, - "ElectricityMapsFree": { - "Type": "ElectricityMapsFree", - "token": "token", - "BaseURL": "https://api.co2signal.com/v1/" - } - } - } -``` - ### Configuration For Emissions data Using JSON ```json diff --git a/casdk-docs/docs/tutorial-extras/containerization.md b/casdk-docs/docs/tutorial-extras/containerization.md index 713a534d8..d8b3a6c40 100644 --- a/casdk-docs/docs/tutorial-extras/containerization.md +++ b/casdk-docs/docs/tutorial-extras/containerization.md @@ -49,16 +49,6 @@ carbon_aware v1 6293e2528bf2 About an hour ago 230MB > carbon_aware:v1 ``` - or the [ElectricityMapsFree](https://www.co2signal.com/) provider - - ```sh - docker run --rm -p 8000:80 \ - > -e DataSources__EmissionsDataSource="ElectricityMapsFree" \ - > -e DataSources__Configurations__ElectricityMapsFree__Type="ElectricityMapsFree" \ - > -e DataSources__Configurations__ElectricityMapsFree__token="" \ - > carbon_aware:v1 - ``` - 1. Verify that the WebApi is responding to requests using an HTTP client tool (e.g. `postman`, `curl`) diff --git a/casdk-docs/docs/tutorial-extras/packaging.md b/casdk-docs/docs/tutorial-extras/packaging.md index 7aef80801..78ab91dbd 100644 --- a/casdk-docs/docs/tutorial-extras/packaging.md +++ b/casdk-docs/docs/tutorial-extras/packaging.md @@ -10,7 +10,6 @@ showing how the package can be consumed. - [Included Scripts](#included-scripts) - [Running the packaging scripts](#running-the-packaging-scripts) - [SDK Configuration](#sdk-configuration) - - [ElectricityMapsFree](#electricitymapsfree) - [ElectricityMaps](#electricitymaps) - [WattTime](#watttime) - [Json](#json) @@ -24,7 +23,6 @@ The current package include 8 projects from the SDK: 1. "GSF.CarbonAware" 2. "CarbonAware" -3. "CarbonAware.DataSources.ElectricityMapsFree" 4. "CarbonAware.DataSources.ElectricityMaps" 5. "CarbonAware.DataSources.Json" 6. "CarbonAware.DataSources.Registration" @@ -67,25 +65,16 @@ Alternatively you can run in your local environment using the ## SDK Configuration -The configuration needed to connect to WattTime, ElectricityMaps, ElectricityMapsFree, or Json data sources can be managed using environment variables or appsettings. +The configuration needed to connect to WattTime, ElectricityMaps, or Json data sources can be managed using environment variables or appsettings. More information on data source configuration can be found [here](configuration.md#datasources) -### ElectricityMapsFree - -Below are the environment variables needed to set up the -**ElectricityMapsFree** data source. - -```bash -export DataSources__EmissionsDataSource=ElectricityMapsFree -export DataSources__Configurations__ElectricityMapsFree__Type=ElectricityMapsFree -export DataSources__Configurations__ElectricityMapsFree__token=[ElectricityMapsFree APIToken]` -``` ### ElectricityMaps Below are the environment variables needed to set up the **ElectricityMaps** data source. ```bash +export DataSources__EmissionsDataSource=ElectricityMaps export DataSources__ForecastDataSource=ElectricityMaps export DataSources__Configurations__ElectricityMaps__Type=ElectricityMaps export DataSources__Configurations__ElectricityMaps__APITokenHeader=[ElectricityMaps APITokenHeader] diff --git a/casdk-docs/docs/tutorial-extras/selecting-a-data-source.md b/casdk-docs/docs/tutorial-extras/selecting-a-data-source.md index 5d25823a0..8316045a4 100644 --- a/casdk-docs/docs/tutorial-extras/selecting-a-data-source.md +++ b/casdk-docs/docs/tutorial-extras/selecting-a-data-source.md @@ -5,7 +5,7 @@ sidebar_position: 1 # Selecting a Data Source The Carbon Aware SDK includes access to various data sources of carbon aware -data, including WattTime, ElectricityMaps, ElectricityMapsFree, and a custom +data, including WattTime, ElectricityMaps, and a custom JSON source. These matrices are an attempt to track what features of the Carbon Aware SDK are enabled for which data sources. @@ -14,7 +14,6 @@ Aware SDK are enabled for which data sources. - [Type of Data Sources and Configuration](#type-of-data-sources-and-configuration) - [Data Source Methods Available](#data-source-methods-available) - [Location Coverage](#location-coverage) -- [Restriction: ElectricityMaps Free Trial User](#restrictions-electricitymaps-free-trial-user) ## Type of Data Sources and Configuration @@ -22,24 +21,24 @@ In the CarbonAware SDK configuration, you can set what data source to use as the `EmissionsDataSource` and the `ForecastDataSource`. There are also certain configuration fields that must be set in order to access the raw data. -| Type | WattTime | ElectricityMaps | ElectricityMapsFree | JSON | -|-------------------------------|-----------|------------------------------------------------------------------------------------|---------------------|----------| -| Is Emissions DataSource | ✅ | ✅ | ✅ | ✅ | -| Is Forecast DataSource | ✅ | ✅ | ❌ | ❌ | -| Makes HTTP(s) call | ✅ | ✅ | ✅ | ❌ | -| Can Use Custom Data | ❌ | ❌ | ❌ | ✅ | -| Supports Trial + Full Account | ✅ | ✅ (\*[see restriction below](#restrictions-electricitymaps-free-trial-user)) | N/A | N/A | +| Type | WattTime | ElectricityMaps | JSON | +|-------------------------------|-----------|------------------------------------------------------------------------------------|----------| +| Is Emissions DataSource | ✅ | ✅ | ✅ | +| Is Forecast DataSource | ✅ | ✅ | ❌ | +| Makes HTTP(s) call | ✅ | ✅ | ❌ | +| Can Use Custom Data | ❌ | ❌ | ✅ | +| Supports Trial + Full Account | ✅ | ✅ | N/A | ## Data Source Methods Available Not all data sources support all the routes provided in the interfaces (`IEmissionsDataSource`/`IForecastDataSource`). -| Methods | WattTime | ElectricityMaps | ElectricityMapsFree | JSON | CLI Usage | Web Api Usage | SDK Usage | -| ----------------------- | :------: | :-------------: | :-----------------: | :------: | :--------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------: | -| GetCarbonIntensityAsync | ✅ | ✅ | ✅ | ✅ | `emissions` | `emissions/bylocation` or `emissions/bylocations` or `emissions/bylocations/best` or `emissions/average`‑`carbon`‑`intensity` or `emissions/average`‑`carbon`‑`intensity/batch` | `GetEmissionsDataAsync(...)` or `GetBestEmissionsDataAsync(...)` or `GetAverageCarbonIntensityDataAsync(...)` | -| GetCurrentForecastAsync | ✅ | ✅ | ❌ | ❌ | `emissions`‑`forecasts` | `forecasts/current` | `GetCurrentForecastAsync(...)` | -| GetForecastByDateAsync | ✅ | ❌ | ❌ | ❌ | `emissions`‑`forecasts` ‑‑`requested`‑`at` | `forecasts/batch` with `requestedAt` field | `GetForecastByDateAsync(...)` | +| Methods | WattTime | ElectricityMaps | JSON | CLI Usage | Web Api Usage | SDK Usage | +| ----------------------- | :------: | :-------------: | :------: | :--------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------: | +| GetCarbonIntensityAsync | ✅ | ✅ | ✅ | `emissions` | `emissions/bylocation` or `emissions/bylocations` or `emissions/bylocations/best` or `emissions/average`‑`carbon`‑`intensity` or `emissions/average`‑`carbon`‑`intensity/batch` | `GetEmissionsDataAsync(...)` or `GetBestEmissionsDataAsync(...)` or `GetAverageCarbonIntensityDataAsync(...)` | +| GetCurrentForecastAsync | ✅ | ✅ | ❌ | `emissions`‑`forecasts` | `forecasts/current` | `GetCurrentForecastAsync(...)` | +| GetForecastByDateAsync | ✅ | ❌ | ❌ | `emissions`‑`forecasts` ‑‑`requested`‑`at` | `forecasts/batch` with `requestedAt` field | `GetForecastByDateAsync(...)` | ## Location Coverage @@ -53,43 +52,7 @@ location config. relevant zone. - For `ElectricityMaps`, see their [live map app](https://app.electricitymaps.com/map) - to find the relevant zone and see current data coming in. -- For `ElectricityMapsFree`, see the Electricity Maps - [zone list](https://api.electricitymap.org/v3/zones) to find the relevant + to find the relevant zone and see current data coming in or their + [zone list](https://api.electricitymap.org/v3/zones) endpoint to find the relevant zones. -## Restrictions: free trial of ElectricityMaps - -ElectricityMaps allows new users to create a free trial for 1 month access to -the API. Free trial users have restricted access to the API and a slightly -different configuration for the SDK (see -[configuration.md](../tutorial-extras/configuration.md#electricitymaps-configuration). You can -request a free trial on the -[ElectricityMaps API Portal](https://api-portal.electricitymaps.com/). - -### Restricted Zone Access - -Free trial users only have access ~100 zones in the ElectricityMaps API. -ElectricityMaps maintains a -[frequently updated list](https://docs.google.com/document/d/e/2PACX-1vTdYp8E5E3fNogL54ICf_UxfA_rZ_RPO4WKWI4ZANPSX25jCbvHtAxc-VrJt9HymeRHFcSGWXjhVHS0/pub) -of available free trial zones that include the key, name, and country of each -zone. If you need access to other zones not included on the list, you will need -a full access product key. - -### Restricted Endpoint Access - -Free trial users only have access to seven endpoints in the ElectricityMaps API. -Of those seven, only two are currently supported as part of Carbon Aware SDK: - -1. `GET /carbon-intensity/forecast` -2. `GET /carbon-intensity/history` - -> Note: The Carbon Aware SDK is not restricting implementations to only support -> free trial users of ElectricityMaps. There may be implementations in the -> future that use endpoints that a free trial user may not be able to access and -> therefore cannot use that functionality of the SDK. - -### Restricted Call Access - -Free trial users are capped at 1,000 calls for the month of the free trial. Any -calls beyond the 1,000th call will be rejected. diff --git a/samples/casdk-demo/README.md b/samples/casdk-demo/README.md index 168cd5abd..4b221de69 100644 --- a/samples/casdk-demo/README.md +++ b/samples/casdk-demo/README.md @@ -22,8 +22,8 @@ NGINX is a reverse proxy to both Carbon Aware SDK and Swagger UI. To avoid CORS See [nginx-rp.conf](nginx-rp.conf) -/ -> Carbon Aware SDK -/swagger.yaml -> OpenAPI document provided by Carbon Aware SDK +/ -> Carbon Aware SDK +/swagger.yaml -> OpenAPI document provided by Carbon Aware SDK /swagger-ui/ -> Swagger UI for Carbon Aware SDK ## How to run @@ -53,12 +53,14 @@ See [nginx-rp.conf](nginx-rp.conf) ## Example -Run demonstration with ElectricityMapsFree datasource +Run demonstration with ElectricityMaps datasource ``` -export CASDK_DataSources__EmissionsDataSource=ElectricityMapsFree -export CASDK_DataSources__Configurations__ElectricityMapsFree__Type=ElectricityMapsFree -export CASDK_DataSources__Configurations__ElectricityMapsFree__token=YOUR_SECRET_TOKEN +export CASDK_DataSources__EmissionsDataSource=ElectricityMaps +export CASDK_DataSources__ForecastDataSource=ElectricityMaps +export CASDK_DataSources__Configurations__ElectricityMaps__Type=ElectricityMaps +export CASDK_DataSources__Configurations__ElectricityMaps__APITokenHeader = "auth-token" +export CASDK_DataSources__Configurations__ElectricityMaps__APIToken=YOUR_SECRET_TOKEN ./demo.sh start ``` diff --git a/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj b/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj index a79c9cdf0..38ea8dfd8 100644 --- a/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj +++ b/src/CarbonAware.CLI/test/integrationTests/CarbonAware.CLI.IntegrationTests.csproj @@ -21,8 +21,14 @@ Include="..\..\..\CarbonAware.DataSources\CarbonAware.DataSources.WattTime\mock\CarbonAware.DataSources.WattTime.Mocks.csproj" /> - - \ No newline at end of file + + + + + + + diff --git a/src/CarbonAware.CLI/test/integrationTests/Commands/Emissions/EmissionsCommandTests.cs b/src/CarbonAware.CLI/test/integrationTests/Commands/Emissions/EmissionsCommandTests.cs index 21473e37b..63eb86a63 100644 --- a/src/CarbonAware.CLI/test/integrationTests/Commands/Emissions/EmissionsCommandTests.cs +++ b/src/CarbonAware.CLI/test/integrationTests/Commands/Emissions/EmissionsCommandTests.cs @@ -6,12 +6,11 @@ namespace CarbonAware.CLI.IntegrationTests.Commands.Emissions; /// -/// Tests that the CLI handles and packages various responses from handlers +/// Tests that the CLI handles and packages various responses from handlers /// and data sources properly, including empty responses and exceptions. /// [TestFixture(DataSourceType.JSON)] [TestFixture(DataSourceType.WattTime)] -[TestFixture(DataSourceType.ElectricityMapsFree)] internal class EmissionsCommandTests : IntegrationTestingBase { public EmissionsCommandTests(DataSourceType dataSource) : base(dataSource) { } @@ -98,7 +97,7 @@ public async Task Emissions_InvalidStartAndEnd_ReturnsExpectedError() string expectedError = "Invalid parameters Start: Start must be before End "; // Act var exitCode = await InvokeCliAsync($"emissions -l {location} -s 2022-09-01T02:05:00Z -e 2022-09-01T01:00:00Z"); - // Whitespace characters regex + // Whitespace characters regex var regex = @"\s+"; var output = Regex.Replace(_console.Error.ToString()!, regex, " "); @@ -134,7 +133,6 @@ public async Task Emissions_BestOption_ReturnsExpectedData() [Test] public async Task Emissions_AverageOption_ReturnsExpectedData() { - IgnoreTestForDataSource($"data source does not implement the '-a' switch", DataSourceType.ElectricityMapsFree); // Arrange var start = DateTimeOffset.Parse("2022-09-01T00:00:00Z"); @@ -171,7 +169,7 @@ public async Task Average_Best_ReturnsExpectedError() // Assert Assert.AreEqual(1, exitCode); var expectedError = "Options --average and --best cannot be used together Option '-s' expects a single argument but 2 were provided. "; - // Whitespace characters regex + // Whitespace characters regex var regex = @"\s+"; var output = Regex.Replace(_console.Error.ToString()!, regex, " "); diff --git a/src/CarbonAware.CLI/test/integrationTests/Commands/EmissionsForecasts/EmissionsForecastsCommandTests.cs b/src/CarbonAware.CLI/test/integrationTests/Commands/EmissionsForecasts/EmissionsForecastsCommandTests.cs index 5daf173c8..855d6f24c 100644 --- a/src/CarbonAware.CLI/test/integrationTests/Commands/EmissionsForecasts/EmissionsForecastsCommandTests.cs +++ b/src/CarbonAware.CLI/test/integrationTests/Commands/EmissionsForecasts/EmissionsForecastsCommandTests.cs @@ -5,12 +5,11 @@ namespace CarbonAware.CLI.IntegrationTests.Commands.EmissionsForecasts; /// -/// Tests that the CLI handles and packages various responses from handlers +/// Tests that the CLI handles and packages various responses from handlers /// and data sources properly, including empty responses and exceptions. /// [TestFixture(DataSourceType.WattTime)] [TestFixture(DataSourceType.ElectricityMaps)] -//[TestFixture(DataSourceType.ElectricityMapsFree)] // Left out because these tests are not relevant for this data source internal class EmissionsForecastsCommandTests : IntegrationTestingBase { public EmissionsForecastsCommandTests(DataSourceType dataSource) : base(dataSource) { } @@ -73,14 +72,14 @@ public async Task EmissionsForecasts_StartAndEndOptions_ReturnsExpectedData() var end = start.AddHours(5); var dataStartAt = start.ToString("yyyy-MM-ddTHH:mm:ssZ"); var dataEndAt = end.ToString("yyyy-MM-ddTHH:mm:ssZ"); - + _dataSourceMocker.SetupForecastMock(); // Act var exitCode = await InvokeCliAsync($"emissions-forecasts -l {location} -s {dataStartAt} -e {dataEndAt}"); // Assert Assert.AreEqual(0, exitCode, _console.Error.ToString()); - + var jsonResults = JsonNode.Parse(_console.Out.ToString()!)!.AsArray()!; var firstResult = jsonResults.First()!.AsObject(); Assert.AreEqual(1, jsonResults.Count); diff --git a/src/CarbonAware.CLI/test/integrationTests/Commands/Location/LocationCommandTests.cs b/src/CarbonAware.CLI/test/integrationTests/Commands/Location/LocationCommandTests.cs index ff422376a..a2b625fbb 100644 --- a/src/CarbonAware.CLI/test/integrationTests/Commands/Location/LocationCommandTests.cs +++ b/src/CarbonAware.CLI/test/integrationTests/Commands/Location/LocationCommandTests.cs @@ -10,7 +10,6 @@ namespace CarbonAware.CLI.IntegrationTests.Commands.Location; [TestFixture(DataSourceType.JSON)] [TestFixture(DataSourceType.WattTime)] [TestFixture(DataSourceType.ElectricityMaps)] -[TestFixture(DataSourceType.ElectricityMapsFree)] class LocationCommandTests : IntegrationTestingBase { public LocationCommandTests(DataSourceType dataSource) : base(dataSource) { } diff --git a/src/CarbonAware.CLI/test/integrationTests/IntegrationTestingBase.cs b/src/CarbonAware.CLI/test/integrationTests/IntegrationTestingBase.cs index 1ab6b9d40..fb4a07345 100644 --- a/src/CarbonAware.CLI/test/integrationTests/IntegrationTestingBase.cs +++ b/src/CarbonAware.CLI/test/integrationTests/IntegrationTestingBase.cs @@ -1,7 +1,6 @@ using CarbonAware.DataSources.Configuration; using CarbonAware.Interfaces; using CarbonAware.DataSources.ElectricityMaps.Mocks; -using CarbonAware.DataSources.ElectricityMapsFree.Mocks; using CarbonAware.DataSources.Json.Mocks; using CarbonAware.DataSources.WattTime.Mocks; using NUnit.Framework; @@ -81,7 +80,7 @@ protected async Task InvokeCliAsync(string arguments) var standardError = new StreamWriter(Console.OpenStandardError()); standardOutput.AutoFlush = true; standardError.AutoFlush = true; - + Console.SetOut(standardOutput); Console.SetError(standardError); @@ -100,6 +99,7 @@ public void Setup() { Environment.SetEnvironmentVariable("DataSources__EmissionsDataSource", "Json"); Environment.SetEnvironmentVariable("DataSources__Configurations__Json__Type", "JSON"); + Environment.SetEnvironmentVariable("DataSources__Configurations__Json__DataFileLocation", "test-data-azure-emissions.json"); _dataSourceMocker = new JsonDataSourceMocker(); break; } @@ -115,6 +115,7 @@ public void Setup() } case DataSourceType.ElectricityMaps: { + Environment.SetEnvironmentVariable("DataSources__EmissionsDataSource", "ElectricityMaps"); Environment.SetEnvironmentVariable("DataSources__ForecastDataSource", "ElectricityMaps"); Environment.SetEnvironmentVariable("DataSources__Configurations__ElectricityMaps__Type", "ElectricityMaps"); Environment.SetEnvironmentVariable("DataSources__Configurations__ElectricityMaps__APITokenHeader", "token"); @@ -123,15 +124,6 @@ public void Setup() _dataSourceMocker = new ElectricityMapsDataSourceMocker(); break; } - case DataSourceType.ElectricityMapsFree: - { - Environment.SetEnvironmentVariable("DataSources__EmissionsDataSource", "ElectricityMapsFree"); - Environment.SetEnvironmentVariable("DataSources__Configurations__ElectricityMapsFree__Type", "ElectricityMapsFree"); - Environment.SetEnvironmentVariable("DataSources__Configurations__ElectricityMapsFree__token", "token"); - - _dataSourceMocker = new ElectricityMapsFreeDataSourceMocker(); - break; - } case DataSourceType.None: { throw new NotSupportedException($"DataSourceType {_dataSource.ToString()} not supported"); diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj deleted file mode 100644 index a8878c6ae..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - - - - diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/ElectricityMapsFreeDataSourceMocker.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/ElectricityMapsFreeDataSourceMocker.cs deleted file mode 100644 index 7ddf800b1..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/mock/ElectricityMapsFreeDataSourceMocker.cs +++ /dev/null @@ -1,79 +0,0 @@ -using CarbonAware.Interfaces; -using CarbonAware.DataSources.ElectricityMapsFree.Model; -using CarbonAware.DataSources.ElectricityMapsFree.Constants; -using System.Net; -using System.Net.Mime; -using System.Text.Json; -using WireMock.RequestBuilders; -using WireMock.ResponseBuilders; -using WireMock.Server; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Mocks; - -internal class ElectricityMapsFreeDataSourceMocker : IDataSourceMocker -{ - private readonly WireMockServer _server; - private static readonly JsonSerializerOptions _options = new JsonSerializerOptions(JsonSerializerDefaults.Web); - - - public ElectricityMapsFreeDataSourceMocker() - { - _server = WireMockServer.Start(); - Environment.SetEnvironmentVariable("DataSources__Configurations__ElectricityMapsFree__BaseURL", _server.Url!); - Initialize(); - } - - public void SetupDataMock(DateTimeOffset start, DateTimeOffset end, string location) - { - var data = new GridEmissionDataPoint - { - Disclaimer = string.Empty, - Status = "ok", - CountryCodeAbbreviation = location, - Data = new Data() { - Datetime = start, - CarbonIntensity = 100, - FossilFuelPercentage = 12.03F - }, - Units = new Units() - { - CarbonIntensity = "gCO2eq/kWh" - } - }; - - SetupResponseGivenGetRequest(Paths.Latest, data); - } - - public void SetupForecastMock() - { - throw new NotImplementedException(); - } - - public void SetupHistoricalBatchForecastMock() - { - throw new NotImplementedException(); - } - - public void Initialize() - { - // No initialization needed - return; - } - - public void Reset() => _server.Reset(); - - public void Dispose() => _server.Dispose(); - - private void SetupResponseGivenGetRequest(string path, object body) - { - var jsonBody = JsonSerializer.Serialize(body, _options); - _server - .Given(Request.Create().WithPath("/" + path).UsingGet()) - .RespondWith( - Response.Create() - .WithStatusCode(HttpStatusCode.OK) - .WithHeader("Content-Type", MediaTypeNames.Application.Json) - .WithBody(jsonBody) - ); - } -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/CarbonAware.DataSources.ElectricityMapsFree.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/CarbonAware.DataSources.ElectricityMapsFree.csproj deleted file mode 100644 index 310a6ed47..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/CarbonAware.DataSources.ElectricityMapsFree.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - net8.0 - enable - enable - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/ElectricityMapsFreeClient.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/ElectricityMapsFreeClient.cs deleted file mode 100644 index 0176b6be0..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/ElectricityMapsFreeClient.cs +++ /dev/null @@ -1,195 +0,0 @@ -using CarbonAware.DataSources.ElectricityMapsFree.Configuration; -using CarbonAware.DataSources.ElectricityMapsFree.Constants; -using CarbonAware.DataSources.ElectricityMapsFree.Model; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.Diagnostics; -using System.Net.Http.Headers; -using System.Net.Mime; -using System.Net; -using System.Text.Json; -using System.Web; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Client; - -internal class ElectricityMapsFreeClient : IElectricityMapsFreeClient -{ - private static readonly JsonSerializerOptions options = new JsonSerializerOptions(JsonSerializerDefaults.Web); - - private static readonly HttpStatusCode[] RetriableStatusCodes = new HttpStatusCode[] - { - HttpStatusCode.Unauthorized, - HttpStatusCode.Forbidden - }; - - private readonly HttpClient client; - - private IOptionsMonitor ConfigurationMonitor { get; } - - private ElectricityMapsFreeClientConfiguration Configuration => this.ConfigurationMonitor.CurrentValue; - - private static readonly ActivitySource Activity = new ActivitySource(nameof(ElectricityMapsFreeClient)); - - private ILogger Log { get; } - - public ElectricityMapsFreeClient(IHttpClientFactory factory, IOptionsMonitor configurationMonitor, ILogger log) - { - this.client = factory.CreateClient(IElectricityMapsFreeClient.NamedClient); - this.ConfigurationMonitor = configurationMonitor; - this.Log = log; - Configuration.Validate(); - this.client.BaseAddress = new Uri(this.Configuration.BaseUrl); - this.client.DefaultRequestHeaders.Accept.Clear(); - this.client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); - } - - /// - public async Task GetCurrentEmissionsAsync(string countryCodeAbbreviation) - { - Log.LogInformation("Requesting latest carbon intensity from zone {countryCode}", countryCodeAbbreviation); - - var parameters = new Dictionary() - { - { QueryStrings.countryCodeAbbreviation, countryCodeAbbreviation } - }; - - var tags = new Dictionary() - { - { QueryStrings.countryCodeAbbreviation, countryCodeAbbreviation } - }; - - var result = await this.MakeRequestAsync(Paths.Latest, parameters, tags); - var emissionData = JsonSerializer.Deserialize(result, options) ?? throw new ElectricityMapsFreeClientException($"Error getting latest carbon intensity for countryCode {countryCodeAbbreviation}"); - if (emissionData == null || emissionData.Data.CarbonIntensity == null || emissionData.Data.Datetime == null) - { - Log.LogError("Region {countryCode} is not known", countryCodeAbbreviation); - throw new ElectricityMapsFreeClientException($"Region {countryCodeAbbreviation} is not known"); - } - return emissionData; - } - - /// - public async Task GetCurrentEmissionsAsync(string latitude, string longitude) - { - Log.LogDebug("Requesting latest carbon intensity using latitude {latitude} longitude {longitude}", - latitude, longitude); - - var parameters = new Dictionary() - { - { QueryStrings.Latitude, latitude }, - { QueryStrings.Longitude, longitude } - }; - - var result = await this.MakeRequestAsync(Paths.Latest, parameters); - - var emissionData = JsonSerializer.Deserialize(result, options) ?? throw new ElectricityMapsFreeClientException($"Error getting forecast for latitude {latitude} longitude {longitude}"); - return emissionData; - } - - private async Task GetAsyncWithAuthRetry(string uriPath) - { - this.EnsureToken(); - - var response = await this.client.GetAsync(uriPath); - - if (RetriableStatusCodes.Contains(response.StatusCode)) - { - Log.LogDebug("Failed to get url {url} with status code {statusCode}. Attempting to log in again.", uriPath, response.StatusCode); - this.UpdateAuthToken(); - response = await this.client.GetAsync(uriPath); - } - - if (!response.IsSuccessStatusCode) - { - Log.LogError("Error getting data from electricityMapsFree. StatusCode: {statusCode}. Response: {response}", response.StatusCode, response); - throw new ElectricityMapsFreeClientHttpException($"Error getting data from electricityMapsFree: {response.StatusCode}", response); - } - return response; - } - - private async Task GetAsyncStringWithAuthRetry(string uriPath) - { - var response = await this.GetAsyncWithAuthRetry(uriPath); - var data = await response.Content.ReadAsStringAsync(); - return data ?? string.Empty; - } - - private async Task GetAsyncStreamWithAuthRetry(string uriPath) - { - var response = await this.GetAsyncWithAuthRetry(uriPath); - return await response.Content.ReadAsStreamAsync(); - } - - private void EnsureToken() - { - if (!this.client.DefaultRequestHeaders.Contains("auth-token")) - { - this.UpdateAuthToken(); - } - } - - private void UpdateAuthToken() - { - using (var activity = Activity.StartActivity()) - { - Log.LogInformation("Attempting to log using token {token}", this.Configuration.Token); - this.SetAuthTokenAuthenticationHeader(this.Configuration.Token!); - } - } - - internal void SetAuthTokenAuthenticationHeader(string token) - { - this.client.DefaultRequestHeaders.Add("auth-token", token); - } - - private async Task MakeRequestAsync(string path, Dictionary parameters, Dictionary? tags = null) - { - using (var activity = Activity.StartActivity()) - { - var url = BuildUrlWithQueryString(path, parameters); - - Log.LogInformation("Requesting data using url {url}", url); - - if (tags != null) - { - foreach (var kvp in tags) - { - activity?.AddTag(kvp.Key, kvp.Value); - } - } - - var result = await this.GetAsyncStringWithAuthRetry(url); - - Log.LogDebug("For query {url}, received data {result}", url, result); - - return result; - } - } - - private string BuildUrlWithQueryString(string url, IDictionary queryStringParams) - { - if (Log.IsEnabled(LogLevel.Debug)) - { - Log.LogDebug("Attempting to build a url using url {url} and query string parameters {parameters}", url, string.Join(";", queryStringParams.Select(k => $"\"{k.Key}\":\"{k.Value}\""))); - } - - // this will get a specialized namevalue collection for formatting query strings. - var query = HttpUtility.ParseQueryString(string.Empty); - - foreach (var kvp in queryStringParams) - { - query[kvp.Key] = kvp.Value; - } - - var result = $"{url}?{query}"; - - if (Log.IsEnabled(LogLevel.Debug)) - { - Log.LogDebug("Built url {result} from url {url} and query string parameters {parameters}", result, url, string.Join(";", queryStringParams.Select(k => $"\"{k.Key}\":\"{k.Value}\""))); - } - - return result; - } -} - - diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/ElectricityMapsFreeClientException.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/ElectricityMapsFreeClientException.cs deleted file mode 100644 index 3d765e64b..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/ElectricityMapsFreeClientException.cs +++ /dev/null @@ -1,11 +0,0 @@ -using CarbonAware.Interfaces; -using System; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Client; - -internal class ElectricityMapsFreeClientException : Exception -{ - public ElectricityMapsFreeClientException(string message) : base(message) - { - } -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/ElectricityMapsFreeClientHttpException.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/ElectricityMapsFreeClientHttpException.cs deleted file mode 100644 index 892ac34e1..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/ElectricityMapsFreeClientHttpException.cs +++ /dev/null @@ -1,46 +0,0 @@ -using CarbonAware.Interfaces; -using System; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Client; - -internal class ElectricityMapsFreeClientHttpException : Exception, IHttpResponseException -{ - /// - /// Creates a new instance of the class. - /// - /// The error message supplied. - /// The response object generating this exception. - public ElectricityMapsFreeClientHttpException(string message, HttpResponseMessage response) : base(message) - { - this.Response = response; - this.Status = (int)response.StatusCode; - this.Title = nameof(ElectricityMapsFreeClientHttpException); - this.Detail = message; - } - - /// - /// Gets the status code for the exception. See remarks for the status codes that can be returned. - /// - /// - /// 400: Returned when missing arguments (no country code passed or lat/lon don't map to a known country code) - /// 401: Returned when trying to access a path or location that isn't authorized for the token. - /// - public int? Status { get; } - - /// - /// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence - /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; - /// see[RFC7231], Section 3.4). - /// - public string? Title { get; } - - /// - /// A human-readable explanation specific to this occurrence of the problem. - /// - public string? Detail { get; } - - /// - /// Gets the response returned from the Electricity Maps Free call. - /// - public HttpResponseMessage? Response { get; } -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/IElectricityMapsFreeClient.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/IElectricityMapsFreeClient.cs deleted file mode 100644 index c8f0c6838..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Client/IElectricityMapsFreeClient.cs +++ /dev/null @@ -1,22 +0,0 @@ -using CarbonAware.DataSources.ElectricityMapsFree.Model; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Client; - -/// -/// An interface for interacting with the Electricity Maps Free API, also called CO2 Signal. -/// -internal interface IElectricityMapsFreeClient -{ - public const string NamedClient = "ElectricityMapsFreeClient"; - - /// - /// Async method to get the latest emission data for a given country code - /// - public Task GetCurrentEmissionsAsync(string countryCodeAbbreviation); - - /// - /// Async method to get the latest emission data for a given latitude and longitude - /// - public Task GetCurrentEmissionsAsync(string latitude, string longitude); - -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Configuration/ElectricityMapsFreeClientConfiguration.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Configuration/ElectricityMapsFreeClientConfiguration.cs deleted file mode 100644 index e31b2e2fa..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Configuration/ElectricityMapsFreeClientConfiguration.cs +++ /dev/null @@ -1,27 +0,0 @@ -using CarbonAware.Exceptions; -using CarbonAware.DataSources.ElectricityMapsFree.Constants; -using CarbonAware.DataSources.ElectricityMapsFree.Model; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Configuration; - -public class ElectricityMapsFreeClientConfiguration -{ - public const string Key = "ElectricityMapsFreeClient"; - - public string? Token { get; set; } - - public string BaseUrl { get; set; } = "https://api.co2signal.com/v1/"; - - public void Validate() - { - if (string.IsNullOrWhiteSpace(this.Token)) - { - throw new ConfigurationException($"{Key}:{nameof(this.Token)} is required for electricityMaps free."); - } - - if (!Uri.IsWellFormedUriString(this.BaseUrl, UriKind.Absolute)) - { - throw new ConfigurationException($"{Key}:{nameof(this.BaseUrl)} is not a valid absolute url."); - } - } -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Configuration/ServiceCollectionExtensions.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Configuration/ServiceCollectionExtensions.cs deleted file mode 100644 index 8156b2b87..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Configuration/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -using CarbonAware.Configuration; -using CarbonAware.DataSources.ElectricityMapsFree.Client; -using CarbonAware.Exceptions; -using CarbonAware.Interfaces; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using System.Net; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Configuration; - -internal static class ServiceCollectionExtensions -{ - public static IServiceCollection AddElectricityMapsFreeEmissionsDataSource(this IServiceCollection services, DataSourcesConfiguration dataSourcesConfig) - { - AddElectricityMapsFreeClient(services, dataSourcesConfig.EmissionsConfigurationSection()); - services.TryAddSingleton(); - return services; - } - - - private static void AddElectricityMapsFreeClient(IServiceCollection services, IConfigurationSection configSection) - { - services.Configure(c => - { - configSection.Bind(c); - }); - - var httpClientBuilder = services.AddHttpClient(IElectricityMapsFreeClient.NamedClient); - - var Proxy = configSection.GetSection("Proxy").Get(); - if (Proxy?.UseProxy == true) - { - if (String.IsNullOrEmpty(Proxy.Url)) - { - throw new ConfigurationException("Proxy Url is not configured."); - } - httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => - new HttpClientHandler() { - Proxy = new WebProxy { - Address = new Uri(Proxy.Url), - Credentials = new NetworkCredential(Proxy.Username, Proxy.Password), - BypassProxyOnLocal = true - } - } - ); - } - services.TryAddSingleton(); - } -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Constants/AuthenticationHeaderTypes.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Constants/AuthenticationHeaderTypes.cs deleted file mode 100644 index 24d723ecf..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Constants/AuthenticationHeaderTypes.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CarbonAware.DataSources.ElectricityMapsFree.Constants; - -internal class AuthenticationHeaderTypes -{ - public const string AuthToken = "auth-token"; -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Constants/Paths.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Constants/Paths.cs deleted file mode 100644 index 738b0306d..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Constants/Paths.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CarbonAware.DataSources.ElectricityMapsFree.Constants; - -internal class Paths -{ - public const string Latest = "latest"; -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Constants/QueryStrings.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Constants/QueryStrings.cs deleted file mode 100644 index a7775ca64..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Constants/QueryStrings.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace CarbonAware.DataSources.ElectricityMapsFree.Constants; - -internal class QueryStrings -{ - public const string countryCodeAbbreviation = "countryCode"; - public const string StartTime = "starttime"; - public const string EndTime = "endtime"; - public const string Latitude = "lat"; - public const string Longitude = "lon"; - public const string Username = "username"; -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/ElectricityMapsFreeDataSource.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/ElectricityMapsFreeDataSource.cs deleted file mode 100644 index d4268e352..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/ElectricityMapsFreeDataSource.cs +++ /dev/null @@ -1,115 +0,0 @@ -using CarbonAware.DataSources.ElectricityMapsFree.Client; -using CarbonAware.DataSources.ElectricityMapsFree.Model; -using CarbonAware.Interfaces; -using CarbonAware.Model; -using Microsoft.Extensions.Logging; -using System.Diagnostics; - -namespace CarbonAware.DataSources.ElectricityMapsFree; -/// -/// Represents an Electricity Maps Free data source. -/// -internal class ElectricityMapsFreeDataSource : IEmissionsDataSource -{ - public string _name => "ElectricityMapsFreeDataSource"; - - public string _description => throw new NotImplementedException(); - - public string _author => throw new NotImplementedException(); - - public string _version => throw new NotImplementedException(); - - private ILogger _logger { get; } - - private IElectricityMapsFreeClient _electricityMapsFreeClient { get; } - - private static readonly ActivitySource Activity = new ActivitySource(nameof(ElectricityMapsFreeDataSource)); - - private ILocationSource _locationSource { get; } - - /// - /// Creates a new instance of the class. - /// - /// The logger for the datasource - /// The ElectricityMapsFree Client - /// The location source to be used to convert a location name to geocoordinates. - public ElectricityMapsFreeDataSource(ILogger logger, IElectricityMapsFreeClient client, ILocationSource locationSource) - { - this._logger = logger; - this._electricityMapsFreeClient = client; - this._locationSource = locationSource; - } - - - /// - public async Task> GetCarbonIntensityAsync(IEnumerable locations, DateTimeOffset periodStartTime, DateTimeOffset periodEndTime) - { - List EmissionsDataList = new List(); - - foreach (var location in locations) - { - var emissionsDataForLocation = await GetCarbonIntensityAsync(location, periodStartTime, periodEndTime); - EmissionsDataList.AddRange(emissionsDataForLocation); - } - - return EmissionsDataList; - } - - /// - public async Task> GetCarbonIntensityAsync(Location location, DateTimeOffset periodStartTime, DateTimeOffset periodEndTime) - { - this._logger.LogInformation($"Getting current carbon intensity emissions for location {location}"); - - Location? geolocation = null; - bool coordinatesAvailable; - try - { - geolocation = await this._locationSource.ToGeopositionLocationAsync(location); - - coordinatesAvailable = geolocation.Latitude != null && geolocation.Longitude != null; - } - catch (Exception) - { - coordinatesAvailable = false; - } - - GridEmissionDataPoint gridEmissionData; - if (coordinatesAvailable && geolocation != null) - { - string latitude = geolocation.LatitudeAsCultureInvariantString(); - string longitude = geolocation.LongitudeAsCultureInvariantString(); - gridEmissionData = await this._electricityMapsFreeClient.GetCurrentEmissionsAsync(latitude, longitude); - } - else - { - gridEmissionData = await this._electricityMapsFreeClient.GetCurrentEmissionsAsync(location.Name ?? ""); - } - - List EmissionsDataList = new List(); - var emissionDateTime = gridEmissionData.Data.Datetime; - - // periodStartTime should be less than current date time because this method should not handle forecast data. - var shouldReturn = periodStartTime <= DateTimeOffset.UtcNow; - if (shouldReturn && emissionDateTime != null && periodStartTime < periodEndTime) - { - // periodEndTime would be set periodStartTime in EmissionHandler if it is not specified. - // So we can assume we should return the most recent data if they equal. - // If not, we should return the data after checking it is within specified time range. - shouldReturn = periodStartTime <= emissionDateTime && emissionDateTime < periodEndTime; - } - - if (shouldReturn) - { - var emissionData = new EmissionsData() - { - Location = location.Name ?? "", - Time = emissionDateTime ?? new DateTimeOffset(), - Rating = gridEmissionData.Data.CarbonIntensity ?? 0.0, - Duration = new TimeSpan(0, 0, 0) - }; - EmissionsDataList.Add(emissionData); - } - - return EmissionsDataList; - } -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Model/GridEmissionDataPoint.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Model/GridEmissionDataPoint.cs deleted file mode 100644 index 9528c9335..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Model/GridEmissionDataPoint.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Text.Json.Serialization; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Model; - -/// -/// An object describing the emissions for a given countryCode -/// -[Serializable] -internal record GridEmissionDataPoint -{ - - [JsonPropertyName("_disclaimer")] - public string Disclaimer { get; set; } = string.Empty; - - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; - - [JsonPropertyName("countryCode")] - public string CountryCodeAbbreviation { get; set; } = string.Empty; - - [JsonPropertyName("data")] - public Data Data { get; set; } = new Data(); - - [JsonPropertyName("units")] - public Units Units { get; set; } = new Units(); - -} - -internal record Data -{ - [JsonPropertyName("datetime")] - public DateTimeOffset? Datetime { get; set; } - - [JsonPropertyName("carbonIntensity")] - public float? CarbonIntensity { get; set; } - - [JsonPropertyName("fossilFuelPercentage")] - public float? FossilFuelPercentage { get; set; } -} - -internal record Units -{ - [JsonPropertyName("carbonIntensity")] - public string CarbonIntensity { get; set; } = string.Empty; -} diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Model/LoginResult.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Model/LoginResult.cs deleted file mode 100644 index 57da79521..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/src/Model/LoginResult.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Model; - -/// -/// Serializable object describing the electricityMap login response object. -/// -[Serializable] -internal record LoginResult -{ - /// - /// The Bearer Token used to authenticate future requests. - /// - [JsonPropertyName("Token")] - public string Token { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/CarbonAware.DataSources.ElectricityMapsFree.Tests.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/CarbonAware.DataSources.ElectricityMapsFree.Tests.csproj deleted file mode 100644 index 0375049af..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/CarbonAware.DataSources.ElectricityMapsFree.Tests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - net8.0 - enable - - false - - - - - - - - - - - - - - - - - diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Client/ElectricityMapsFreeClientTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Client/ElectricityMapsFreeClientTests.cs deleted file mode 100644 index 43df28d2e..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Client/ElectricityMapsFreeClientTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -using CarbonAware.DataSources.ElectricityMapsFree.Client; -using CarbonAware.DataSources.ElectricityMapsFree.Configuration; -using CarbonAware.DataSources.ElectricityMapsFree.Constants; -using CarbonAware.Exceptions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using Moq.Protected; -using Moq.Contrib.HttpClient; -using System.Net.Http; -using System.Threading.Tasks; -using System.Threading; -using System; -using System.Text.Json; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Tests; - -[TestFixture] -public class ElectricityMapsFreeClientTests -{ - private readonly string TestLatitude = "37.783"; - private readonly string TestLongitude = "-122.417"; - private readonly string TestCountryCode = "US-CAL-CISO"; - - private Mock Handler { get; set; } - - private IHttpClientFactory HttpClientFactory { get; set; } - - private ElectricityMapsFreeClientConfiguration Configuration { get; set; } - - private Mock> Options { get; set; } - - private Mock> Log { get; set; } - - public ElectricityMapsFreeClientTests() - { - this.Configuration = new ElectricityMapsFreeClientConfiguration() { }; - - this.Options = new Mock>(); - this.Log = new Mock>(); - - this.Options.Setup(o => o.CurrentValue).Returns(() => this.Configuration); - - this.Handler = new Mock(); - this.HttpClientFactory = Handler.CreateClientFactory(); - Mock.Get(this.HttpClientFactory).Setup(x => x.CreateClient(IElectricityMapsFreeClient.NamedClient)) - .Returns(() => - { - var client = Handler.CreateClient(); - return client; - }); - } - - [TestCase("ww.ca.u", "mytoken", TestName = "ClientInstantiation_FailsForInvalidConfig: url")] - [TestCase("https://example.com/", "", TestName = "ClientInstantiation_FailsForInvalidConfig: Token")] - public void ClientInstantiation_FailsForInvalidConfig(string baseUrl, string token) - { - // Arrange - this.Configuration = new ElectricityMapsFreeClientConfiguration() - { - Token = token, - BaseUrl = baseUrl, - }; - - // Act & Assert - Assert.Throws(() => new ElectricityMapsFreeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object)); - } - - [Test] - public void AllPublicMethods_DoNotSwallowBadProxyExceptions() - { - // Arrange - var mockHttpClientFactory = Mock.Of(); - var mockHandler = new Mock(); - this.Configuration = GetValidConfiguration(); - - // A bad proxy will throw HttpRequestException when used so we mock that here. - mockHandler.Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny() - ) - .ThrowsAsync(new HttpRequestException()); - - Mock.Get(mockHttpClientFactory) - .Setup(h => h.CreateClient(It.IsAny())) - .Returns(new HttpClient(mockHandler.Object)); - - var client = new ElectricityMapsFreeClient(mockHttpClientFactory, this.Options.Object, this.Log.Object); - - // Act & Assert - Assert.ThrowsAsync(async () => await client.GetCurrentEmissionsAsync(TestCountryCode)); - Assert.ThrowsAsync(async () => await client.GetCurrentEmissionsAsync(TestLatitude, TestLongitude)); - } - - [Test] - public void GetCurrentEmissionsAsync_ThrowJsonException_WhenBadJsonIsReturned() - { - // Arrange - this.Configuration = GetValidConfiguration(); - AddHandler_RequestResponse(r => - { - return r.RequestUri!.ToString().Contains(Paths.Latest) && r.Method == HttpMethod.Get; - }, System.Net.HttpStatusCode.OK, "This is bad json"); - - var client = new ElectricityMapsFreeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object); - - // Act & Assert - Assert.ThrowsAsync(async () => await client.GetCurrentEmissionsAsync(TestLatitude, TestLongitude)); - } - - [Test] - public async Task GetCurrentEmissionsAsync_DeserializesExpectedResponse() - { - // Arrange - this.Configuration = GetValidConfiguration(); - AddHandler_RequestResponse(r => - { - return r.RequestUri!.ToString().Contains(Paths.Latest) && r.Method == HttpMethod.Get; - }, System.Net.HttpStatusCode.OK, TestData.GetLatestCarbonIntensityDataJsonString()); - - var client = new ElectricityMapsFreeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object); - - // Act - var latestData = await client.GetCurrentEmissionsAsync(TestLatitude, TestLongitude); - - // Assert - Assert.That(latestData, Is.Not.Null); - var dataPoint = latestData.Data; - Assert.That(latestData.CountryCodeAbbreviation, Is.EqualTo(TestCountryCode)); - Assert.Multiple(() => - { - Assert.That(dataPoint.Datetime, Is.EqualTo(new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero))); - Assert.That(dataPoint.CarbonIntensity, Is.EqualTo(999)); - }); - } - - /** - * Helper to add client handler for request predicate and corresponding status code and response content - */ - private void AddHandler_RequestResponse(Predicate requestPredicate, System.Net.HttpStatusCode statusCode, string? responseContent = null) - { - if (responseContent != null) - { - this.Handler - .SetupRequest(requestPredicate) - .ReturnsResponse(statusCode, new StringContent(responseContent)); - } - else - { - this.Handler - .SetupRequest(requestPredicate) - .ReturnsResponse(statusCode); - } - } - - /** - * Provide valid values for token and baseurl - */ - static private ElectricityMapsFreeClientConfiguration GetValidConfiguration() - { - return new ElectricityMapsFreeClientConfiguration() - { - Token = "mytoken", - BaseUrl = "https://example.com/", - }; - } -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Client/TestData.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Client/TestData.cs deleted file mode 100644 index 2199e8948..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Client/TestData.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Text.Json.Nodes; -using System; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Tests; - -public static class TestData -{ - private static readonly string disclaimer = "This data is the exclusive property of Electricity Maps and/or related parties. " - + "If you're in doubt about your rights to use this data, please contact api@co2signal.com"; - private static readonly string TestZoneId1 = "US-CAL-CISO"; - private static readonly string TestZoneName1 = "California Independent System Operator"; - - public static string GetZonesAllowedJsonString() - { - var json = new JsonObject - { - [TestZoneId1] = new JsonObject - { - ["zoneName"] = TestZoneName1, - ["access"] = new JsonArray - { - "carbon-intensity/history", - "carbon-intensity/forecast", - } - } - }; - - return json.ToString(); - } - - public static string GetNoPathsSupportedJsonString() - { - var json = new JsonObject - { - [TestZoneId1] = new JsonObject - { - ["zoneName"] = TestZoneName1, - ["access"] = new JsonArray - { - } - } - }; - - return json.ToString(); - } - - public static string GetNoZonesSupportedJsonString() - { - var json = new JsonObject - { - }; - - return json.ToString(); - } - - public static string GetLatestCarbonIntensityDataJsonString() - { - var json = new JsonObject - { - ["_disclaimer"] = disclaimer, - ["status"] = "ok", - ["countryCode"] = TestZoneId1, - ["data"] = new JsonObject - { - ["datetime"] = new DateTimeOffset(2099, 1, 1, 0, 0, 0, TimeSpan.Zero), - ["carbonIntensity"] = 999, - ["fossilFuelPercentage"] = 53.33 - }, - ["units"] = new JsonObject - { - ["carbonIntensity"] = "gCO2eq/kWh" - } - }; - - return json.ToString(); - } -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Configuration/ElectricityMapsFreeClientConfigurationTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Configuration/ElectricityMapsFreeClientConfigurationTests.cs deleted file mode 100644 index 707b10982..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Configuration/ElectricityMapsFreeClientConfigurationTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -using CarbonAware.DataSources.ElectricityMapsFree.Configuration; -using CarbonAware.Exceptions; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Tests; - -[TestFixture] -public class ElectricityMapsFreeClientConfigurationTests -{ - [TestCase("faketoken", "http://example.com", TestName = "Validate does not throw: token; url")] - public void Validate_DoesNotThrow(string? tokenValue, string? url) - { - // Arrange - var config = new ElectricityMapsFreeClientConfiguration(); - if (tokenValue != null) - config.Token = tokenValue; - if (url != null) - config.BaseUrl = url; - - // Act & Assert - Assert.DoesNotThrow(() => config.Validate()); - } - - [TestCase("faketoken", "not a url", TestName = "Validate throws: value; bad url")] - [TestCase(null, "http://example.com", TestName = "Validate throws: no value; url")] - public void Validate_Throws(string? tokenValue, string? url) - { - // Arrange - var config = new ElectricityMapsFreeClientConfiguration(); - if (tokenValue != null) - config.Token = tokenValue; - if (url != null) - config.BaseUrl = url; - - // Act & Assert - Assert.Throws(() => config.Validate()); - } -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/ElectricityMapsFreeDataSourceTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/ElectricityMapsFreeDataSourceTests.cs deleted file mode 100644 index 21203ef35..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/ElectricityMapsFreeDataSourceTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -using CarbonAware.DataSources.ElectricityMapsFree.Client; -using CarbonAware.DataSources.ElectricityMapsFree.Model; -using CarbonAware.Exceptions; -using CarbonAware.Interfaces; -using CarbonAware.Model; -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using Moq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Globalization; - -namespace CarbonAware.DataSources.ElectricityMapsFree.Tests; - -[TestFixture] -public class ElectricityMapsFreeDataSourceTests -{ - private Mock> _logger { get; set; } - - private Mock _ElectricityMapsFreeClient { get; set; } - - private Mock _locationSource { get; set; } - private ElectricityMapsFreeDataSource _dataSource { get; set; } - - private static Location _defaultLocation = new Location() { Name = "eastus", Latitude = 34.123m, Longitude = 123.456m }; - - private static string _defaultLatitude => Convert.ToString(_defaultLocation.Latitude, CultureInfo.InvariantCulture) ?? ""; - private static string _defaultLongitude => Convert.ToString(_defaultLocation.Longitude, CultureInfo.InvariantCulture) ?? ""; - - public ElectricityMapsFreeDataSourceTests() - { - _logger = new Mock>(); - _ElectricityMapsFreeClient = new Mock(); - _locationSource = new Mock(); - _dataSource = new ElectricityMapsFreeDataSource(_logger.Object, _ElectricityMapsFreeClient.Object, _locationSource.Object); - } - - [TestCase(false, TestName = "GetCarbonIntensity_ReturnsResultsWhenRecordsFound without emission date time")] - [TestCase(true, TestName = "GetCarbonIntensity_ReturnsResultsWhenRecordsFound with emission date time")] - public async Task GetCarbonIntensity_ReturnsResultsWhenRecordsFound(bool withEmissionDateTime) - { - var startDate = DateTimeOffset.UtcNow.AddHours(-10); - var endDate = startDate.AddHours(1); - var expectedCarbonIntensity = 100; - - _locationSource.Setup(l => l.ToGeopositionLocationAsync(_defaultLocation)).Returns(Task.FromResult(_defaultLocation)); - - GridEmissionDataPoint emissionData = new() - { - Data = new Data() - { - Datetime = withEmissionDateTime ? startDate.AddMinutes(30) : null, - CarbonIntensity = expectedCarbonIntensity, - } - }; - - this._ElectricityMapsFreeClient.Setup(c => c.GetCurrentEmissionsAsync( - _defaultLatitude, - _defaultLongitude) - ).ReturnsAsync(emissionData); - - var result = await this._dataSource.GetCarbonIntensityAsync(new List() { _defaultLocation }, startDate, endDate); - - Assert.IsNotNull(result); - Assert.That(result.Count(), Is.EqualTo(1)); - - var first = result.First(); - Assert.IsNotNull(first); - Assert.That(first.Rating, Is.EqualTo(expectedCarbonIntensity)); - Assert.That(first.Location, Is.EqualTo(_defaultLocation.Name)); - - this._locationSource.Verify(l => l.ToGeopositionLocationAsync(_defaultLocation)); - } - - [TestCase(false, TestName = "GetCarbonIntensity_UseRegionNameWhenCoordinatesNotAvailable without emission date time")] - [TestCase(true, TestName = "GetCarbonIntensity_UseRegionNameWhenCoordinatesNotAvailable with emission date time")] - public async Task GetCarbonIntensity_UseRegionNameWhenCoordinatesNotAvailable(bool withEmissionDateTime) - { - var startDate = new DateTimeOffset(2022, 4, 18, 12, 32, 42, TimeSpan.FromHours(-6)); - var endDate = startDate.AddMinutes(1); - var expectedCarbonIntensity = 100; - - this._locationSource.Setup(l => l.ToGeopositionLocationAsync(_defaultLocation)).Throws(); - - GridEmissionDataPoint emissionData = new() - { - Data = new Data() - { - Datetime = withEmissionDateTime ? startDate.AddSeconds(30) : null, - CarbonIntensity = expectedCarbonIntensity, - } - }; - - this._ElectricityMapsFreeClient.Setup(c => c.GetCurrentEmissionsAsync(_defaultLocation.Name ?? "")).ReturnsAsync(emissionData); - - var result = await this._dataSource.GetCarbonIntensityAsync(new List() { _defaultLocation }, startDate, endDate); - - Assert.IsNotNull(result); - Assert.That(result.Count(), Is.EqualTo(1)); - - var first = result.First(); - Assert.IsNotNull(first); - Assert.That(first.Rating, Is.EqualTo(expectedCarbonIntensity)); - Assert.That(first.Location, Is.EqualTo(_defaultLocation.Name)); - - this._locationSource.Verify(l => l.ToGeopositionLocationAsync(_defaultLocation)); - this._ElectricityMapsFreeClient.Verify(l => l.GetCurrentEmissionsAsync(_defaultLocation.Name ?? "")); - } - - [Test] - public void GetCarbonIntensity_ThrowsWhenClientDoesNotKnowRegionName() - { - var startDate = new DateTimeOffset(2022, 4, 18, 12, 32, 42, TimeSpan.FromHours(-6)); - var endDate = startDate.AddMinutes(1); - - Location nonExistingLocation = new Location() { Name = "WrongLocationName", Latitude = 34.123m, Longitude = 123.456m }; - this._locationSource.Setup(l => l.ToGeopositionLocationAsync(nonExistingLocation)).Throws(); - - this._ElectricityMapsFreeClient.Setup(c => c.GetCurrentEmissionsAsync(nonExistingLocation.Name)).Throws(new ElectricityMapsFreeClientHttpException("Error", new System.Net.Http.HttpResponseMessage())); - - Assert.ThrowsAsync(async () => await this._dataSource.GetCarbonIntensityAsync(new List() { nonExistingLocation }, startDate, endDate)); - } -} \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Usings.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Usings.cs deleted file mode 100644 index cefced496..000000000 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.ElectricityMapsFree/test/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using NUnit.Framework; \ No newline at end of file diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/JsonDataSourceMocker.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/JsonDataSourceMocker.cs index 808e3df9b..b46fac141 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/JsonDataSourceMocker.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/mock/JsonDataSourceMocker.cs @@ -6,11 +6,18 @@ namespace CarbonAware.DataSources.Json.Mocks; public class JsonDataSourceMocker : IDataSourceMocker { - public JsonDataSourceMocker() { } + + public string DataFileName { get; set; } + + public JsonDataSourceMocker() { + DataFileName = "test-data-azure-emissions.json"; + } public void SetupDataMock(DateTimeOffset start, DateTimeOffset end, string location) { - string path = new JsonDataSourceConfiguration().DataFileLocation; + var config = new JsonDataSourceConfiguration(); + config.DataFileLocation = DataFileName; + string path = config.DataFileLocation; var data = new List(); DateTimeOffset pointTime = start; diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj index 7bba6e61d..aa6cf0e00 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/CarbonAware.DataSources.Json.csproj @@ -7,13 +7,6 @@ false - - - Always - - - @@ -24,4 +17,8 @@ - \ No newline at end of file + + + + + diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/Configuration/JsonDataSourceConfiguration.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/Configuration/JsonDataSourceConfiguration.cs index b54a3116d..f34387a77 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/Configuration/JsonDataSourceConfiguration.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/src/Configuration/JsonDataSourceConfiguration.cs @@ -9,7 +9,6 @@ namespace CarbonAware.DataSources.Json.Configuration; internal class JsonDataSourceConfiguration { private const string BaseDirectory = "data-sources/json"; - private const string DefaultDataFile = "test-data-azure-emissions.json"; private const string DirectoryRegExPattern = @"^(?!\.{2})[-\\/a-zA-Z_\d\.: ]*$"; private string assemblyDirectory; private string? dataFileLocation; @@ -36,7 +35,6 @@ public JsonDataSourceConfiguration() { var assemblyPath = Assembly.GetExecutingAssembly().Location; assemblyDirectory = Path.GetDirectoryName(assemblyPath)!; - DataFileLocation = DefaultDataFile; CacheJsonData = true; } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/CarbonAware.DataSources.Json.Tests.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/CarbonAware.DataSources.Json.Tests.csproj index c54355685..31cb83429 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/CarbonAware.DataSources.Json.Tests.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/CarbonAware.DataSources.Json.Tests.csproj @@ -27,4 +27,7 @@ + + + diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/JsonDataSourceConfigurationTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/JsonDataSourceConfigurationTests.cs index f63565a73..e8d0f0cc7 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/JsonDataSourceConfigurationTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/JsonDataSourceConfigurationTests.cs @@ -24,14 +24,6 @@ public void Setup() AssemblyPath = Assembly.GetExecutingAssembly().Location; } - [Test] - public void GetDefaultDataFileLocation_IsNotNull_ExpectedBaseDir() - { - Assert.That(_configuration.DataFileLocation, Is.Not.Null); - var expectedDir = Path.Combine(Path.GetDirectoryName(AssemblyPath)!, BaseDir); - Assert.That(_configuration.DataFileLocation, Contains.Substring(expectedDir)); - } - [TestCase("../newfile.json", TestName = "setting parent's dir")] [TestCase("~/newfile.json", TestName = "setting user's home dir")] [TestCase(null, TestName = "setting null filepath")] diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/JsonDataSourceTests.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/JsonDataSourceTests.cs index 68ea73e24..798d58a3c 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/JsonDataSourceTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Json/test/JsonDataSourceTests.cs @@ -111,17 +111,16 @@ public async Task GetCarbonIntensityAsync_ReturnsEmptyEmissionData() public async Task GetCarbonIntensityAsync_CacheEmissionData(bool cache) { var logger = Mock.Of>(); - var monitor = new Mock>(); + JsonDataSourceMocker dsMocker = new(); var config = new JsonDataSourceConfiguration { + DataFileLocation = dsMocker.DataFileName, CacheJsonData = cache }; monitor.Setup(m => m.CurrentValue).Returns(config); var dataSource = new JsonDataSource(logger, monitor.Object); - JsonDataSourceMocker dsMocker = new(); - var today = DateTimeOffset.Now; var todayEnd = today.AddMinutes(30); var todayLocation = new Location() { Name = "japan" }; diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj b/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj index 40ecc3ffe..87822d170 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.Registration/CarbonAware.DataSources.Registration.csproj @@ -9,8 +9,6 @@ - (); @@ -67,10 +61,6 @@ public static IServiceCollection AddDataSourceService(this IServiceCollection se services.AddElectricityMapsForecastDataSource(dataSources); break; } - case DataSourceType.ElectricityMapsFree: - { - throw new ArgumentException("ElectricityMapsFree data source is not supported for forecast data"); - } case DataSourceType.None: { services.TryAddSingleton(); @@ -82,7 +72,7 @@ public static IServiceCollection AddDataSourceService(this IServiceCollection se { throw new ConfigurationException("No data sources are configured"); } - + return services; } 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 3dba1c647..08caab921 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClient.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Client/WattTimeClient.cs @@ -10,13 +10,17 @@ using System.Net.Mime; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using System.Web; namespace CarbonAware.DataSources.WattTime.Client; internal class WattTimeClient : IWattTimeClient { - private static readonly JsonSerializerOptions _options = new JsonSerializerOptions(JsonSerializerDefaults.Web); + private static readonly JsonSerializerOptions _options = new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + Converters = { new JsonStringEnumConverter() } + }; private static readonly HttpStatusCode[] _retriableStatusCodes = new HttpStatusCode[] { @@ -64,7 +68,7 @@ public async Task GetDataAsync(string regionAbbreviat { QueryStrings.Region, regionAbbreviation }, { QueryStrings.StartTime, startTime.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) }, { QueryStrings.EndTime, endTime.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) }, - { QueryStrings.SignalType, SignalTypes.co2_moer}, + { QueryStrings.SignalType, _configuration.SignalType.ToString()}, }; var tags = new Dictionary() @@ -93,7 +97,7 @@ public async Task GetCurrentForecastAsync(string var parameters = new Dictionary() { { QueryStrings.Region, region }, - { QueryStrings.SignalType, SignalTypes.co2_moer } + { QueryStrings.SignalType, _configuration.SignalType.ToString() } }; var tags = new Dictionary() @@ -124,7 +128,7 @@ public Task GetCurrentForecastAsync(RegionRespons { QueryStrings.Region, region }, { QueryStrings.StartTime, requestedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) }, { QueryStrings.EndTime, requestedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture) }, - { QueryStrings.SignalType, SignalTypes.co2_moer } + { QueryStrings.SignalType, _configuration.SignalType.ToString() } }; var tags = new Dictionary() @@ -302,14 +306,14 @@ private async Task GetRegionFromCacheAsync(string latitude, stri { { QueryStrings.Latitude, latitude }, { QueryStrings.Longitude, longitude }, - { QueryStrings.SignalType, SignalTypes.co2_moer} + { QueryStrings.SignalType, _configuration.SignalType.ToString()} }; var tags = new Dictionary() { { QueryStrings.Latitude, latitude }, { QueryStrings.Longitude, longitude }, - { QueryStrings.SignalType, SignalTypes.co2_moer } + { QueryStrings.SignalType, _configuration.SignalType.ToString() } }; var result = await this.MakeRequestGetStreamAsync(Paths.RegionFromLocation, parameters, tags); var regionResponse = await JsonSerializer.DeserializeAsync(result, _options) ?? throw new WattTimeClientException($"Error getting Region for latitude {latitude} and longitude {longitude}"); diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs index d05f1a33a..39c025b5d 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using CarbonAware.Configuration; using CarbonAware.DataSources.WattTime.Client; +using CarbonAware.DataSources.WattTime.Constants; using CarbonAware.Exceptions; using CarbonAware.Interfaces; using Microsoft.Extensions.Configuration; @@ -24,7 +25,7 @@ public static IServiceCollection AddWattTimeEmissionsDataSource(this IServiceCol services.TryAddSingleton(); return services; } - + private static void AddDependencies(IServiceCollection services, IConfigurationSection configSection) { AddWattTimeClient(services, configSection); @@ -33,10 +34,10 @@ private static void AddDependencies(IServiceCollection services, IConfigurationS private static void AddWattTimeClient(IServiceCollection services, IConfigurationSection configSection) { - services.Configure(c => - { - configSection.Bind(c); - }); + services.AddOptions() + .Bind(configSection) + .Validate(config => Enum.IsDefined(typeof(SignalTypes), config.SignalType), "Invalid SignalType") + .ValidateOnStart(); var httpClientBuilder = services.AddHttpClient(IWattTimeClient.NamedClient); var authenticationClientBuilder = services.AddHttpClient(IWattTimeClient.NamedAuthenticationClient); diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/WattTimeClientConfiguration.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/WattTimeClientConfiguration.cs index a586523ee..5cc063db0 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/WattTimeClientConfiguration.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Configuration/WattTimeClientConfiguration.cs @@ -1,3 +1,4 @@ +using CarbonAware.DataSources.WattTime.Constants; using CarbonAware.Exceptions; using System.Text; @@ -25,6 +26,11 @@ internal class WattTimeClientConfiguration /// public string BaseUrl { get; set; } = "https://api.watttime.org/v3/"; + /// + /// Gets or sets the signal type to use: co2_moer or co2_aoer + /// + public SignalTypes SignalType { get; set; } = SignalTypes.co2_moer; + /// /// Authentication base url. This changed between v2 and v3 /// to be different to the API base url. diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/SignalTypes.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/SignalTypes.cs index 3ff905da5..e7f3a7510 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/SignalTypes.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Constants/SignalTypes.cs @@ -1,6 +1,7 @@ namespace CarbonAware.DataSources.WattTime.Constants; -internal class SignalTypes +public enum SignalTypes { - public const string co2_moer = "co2_moer"; + co2_moer, + co2_aoer } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsMetaData.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsMetaData.cs index d5abb4117..6743516b0 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsMetaData.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/GridEmissionsMetaData.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using CarbonAware.DataSources.WattTime.Constants; +using System.Text.Json.Serialization; namespace CarbonAware.DataSources.WattTime.Model; @@ -18,7 +19,7 @@ internal record GridEmissionsMetaData /// Signal Type. eg MOER /// [JsonPropertyName("signal_type")] - public string? SignalType { get; set; } + public SignalTypes? SignalType { get; set; } [JsonPropertyName("model")] public GridEmissionsModelData? Model { get; set; } diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/RegionResponse.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/RegionResponse.cs index ee334c67e..c36be2f38 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/RegionResponse.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/src/Model/RegionResponse.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using CarbonAware.DataSources.WattTime.Constants; +using System.Text.Json.Serialization; namespace CarbonAware.DataSources.WattTime.Model; @@ -18,7 +19,7 @@ internal record RegionResponse /// Signal Type /// [JsonPropertyName("signal_type")] - public string SignalType { get; set; } = string.Empty; + public SignalTypes SignalType { get; set; } /// /// Human readable name/description for the region. 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 03557348d..8ffaa341c 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeClientTests.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeClientTests.cs @@ -413,6 +413,60 @@ public async Task GetHistoricalDataAsync_RefreshesTokenWhenNoneSet() } } + [Test] + public async Task JsonStringEnumConverter_CorrectlyDeserializesEnumValues() + { + // Arrange + // Create a custom response with a specific signal_type enum value + var customResponse = new + { + meta = new + { + region = WattTimeTestData.Constants.Region, + generated_at = WattTimeTestData.Constants.GeneratedAt, + generated_at_period_seconds = 30, + signal_type = "co2_aoer", // Different from default co2_moer to verify conversion + units = "lbs_co2_per_mwh", + model = new { date = WattTimeTestData.Constants.Date, type = "binned_regression" } + }, + data = new[] + { + new { + point_time = WattTimeTestData.Constants.PointTime, + value = WattTimeTestData.Constants.Value, + frequency = WattTimeTestData.Constants.Frequency, + market = WattTimeTestData.Constants.Market, + version = WattTimeTestData.Constants.Version + } + } + }; + + var responseJson = System.Text.Json.JsonSerializer.Serialize(customResponse); + + this.AddHandlers_Auth(); + this.AddHandler_RequestResponse(r => + { + return r.RequestUri!.ToString().Contains("/v3/historical") && r.Method == HttpMethod.Get; + }, System.Net.HttpStatusCode.OK, responseJson); + + // Act + var client = new WattTimeClient(this.HttpClientFactory, this.Options.Object, this.Log.Object, this.MemoryCache); + client.SetBearerAuthenticationHeader(_DEFAULT_TOKEN_VALUE); + + var emissionsResponse = await client.GetDataAsync( + WattTimeTestData.Constants.Region, + new DateTimeOffset(2022, 4, 22, 0, 0, 0, TimeSpan.Zero), + new DateTimeOffset(2022, 4, 22, 0, 0, 0, TimeSpan.Zero)); + + // Assert + Assert.IsNotNull(emissionsResponse); + Assert.AreEqual(SignalTypes.co2_aoer, emissionsResponse.Meta.SignalType); + + // Verify other properties were correctly deserialized + Assert.AreEqual(WattTimeTestData.Constants.Region, emissionsResponse.Meta.Region); + Assert.IsTrue(emissionsResponse.Data.Count() > 0); + } + /** * Helper to add client handlers for auth checking */ diff --git a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeTestData.cs b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeTestData.cs index d46143e4f..411adb510 100644 --- a/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeTestData.cs +++ b/src/CarbonAware.DataSources/CarbonAware.DataSources.WattTime/test/Client/WattTimeTestData.cs @@ -18,7 +18,7 @@ public class Constants public static DateTime Date = new DateTime(2099, 1, 1, 0, 0, 0); public const float Value = 999.99f; public const string Version = "1.0"; - public const string SignalType = SignalTypes.co2_moer; + public const SignalTypes SignalType = SignalTypes.co2_moer; public const int Frequency = 300; } @@ -46,11 +46,11 @@ private static GridEmissionsMetaData _GetGridDataMetaResponse() Model = new GridEmissionsModelData() { Date = Constants.Date, - Type = SignalTypes.co2_moer + Type = "binned_regression" // from a response example of WattTime API: https://docs.watttime.org/#tag/GET-Historical/operation/get_historical_datapoint_v3_historical_get }, DataPointPeriodSeconds = 30, SignalType = SignalTypes.co2_moer, - Units = "co2_moer" + Units = "lbs_co2_per_mwh" // from a response example of WattTime API: https://docs.watttime.org/#tag/GET-Historical/operation/get_historical_datapoint_v3_historical_get }; return gridEmissionsMetaData; diff --git a/src/CarbonAware.LocationSources/src/CarbonAware.LocationSources.csproj b/src/CarbonAware.LocationSources/src/CarbonAware.LocationSources.csproj index 50211d65d..fabd8f96e 100644 --- a/src/CarbonAware.LocationSources/src/CarbonAware.LocationSources.csproj +++ b/src/CarbonAware.LocationSources/src/CarbonAware.LocationSources.csproj @@ -12,16 +12,13 @@ false - - - Always - - - - \ No newline at end of file + + + + + diff --git a/src/CarbonAware.LocationSources/test/CarbonAware.LocationSources.Test.csproj b/src/CarbonAware.LocationSources/test/CarbonAware.LocationSources.Test.csproj index 28dc668ef..98f8cad33 100644 --- a/src/CarbonAware.LocationSources/test/CarbonAware.LocationSources.Test.csproj +++ b/src/CarbonAware.LocationSources/test/CarbonAware.LocationSources.Test.csproj @@ -24,4 +24,8 @@ + + + + diff --git a/src/CarbonAware.WebApi/src/Controllers/CarbonAwareController.cs b/src/CarbonAware.WebApi/src/Controllers/CarbonAwareController.cs index 9bb44a307..2d31626e7 100644 --- a/src/CarbonAware.WebApi/src/Controllers/CarbonAwareController.cs +++ b/src/CarbonAware.WebApi/src/Controllers/CarbonAwareController.cs @@ -88,21 +88,21 @@ public async Task GetEmissionsDataForLocationByTime( } /// - /// Retrieves the most recent forecasted data and calculates the optimal marginal carbon intensity window. + /// Retrieves the most recent forecasted data and calculates the optimal carbon intensity window. /// /// The request object /// /// This endpoint fetches only the most recently generated forecast for all provided locations. It uses the "dataStartAt" and /// "dataEndAt" parameters to scope the forecasted data points (if available for those times). If no start or end time - /// boundaries are provided, the entire forecast dataset is used. The scoped data points are used to calculate average marginal - /// carbon intensities of the specified "windowSize" and the optimal marginal carbon intensity window is identified. + /// boundaries are provided, the entire forecast dataset is used. The scoped data points are used to calculate average + /// carbon intensities of the specified "windowSize" and the optimal carbon intensity window is identified. /// - /// The forecast data represents what the data source predicts future marginal carbon intensity values to be, + /// The forecast data represents what the data source predicts future carbon intensity values to be, /// not actual measured emissions data (as future values cannot be known). /// /// This endpoint is useful for determining if there is a more carbon-optimal time to use electricity predicted in the future. /// - /// An array of forecasts (one per requested location) with their optimal marginal carbon intensity windows. + /// An array of forecasts (one per requested location) with their optimal carbon intensity windows. /// Returns the requested forecast objects /// Returned if any of the input parameters are invalid /// Internal server error @@ -126,14 +126,14 @@ public async Task GetCurrentForecastData([FromQuery] EmissionsFor /// /// /// This endpoint takes a batch of requests for historical forecast data, fetches them, and calculates the optimal - /// marginal carbon intensity windows for each using the same parameters available to the '/emissions/forecasts/current' + /// carbon intensity windows for each using the same parameters available to the '/emissions/forecasts/current' /// endpoint. /// /// This endpoint is useful for back-testing what one might have done in the past, if they had access to the /// current forecast at the time. /// /// Array of requested forecasts. - /// An array of forecasts with their optimal marginal carbon intensity window. + /// An array of forecasts with their optimal carbon intensity window. /// Returns the requested forecast objects /// Returned if any of the input parameters are invalid /// Internal server error @@ -169,7 +169,7 @@ public async Task BatchForecastDataAsync([FromBody] IEnumerable /// The request object /// A single object that contains the location, time boundaries and average carbon intensity value. - /// Returns a single object that contains the information about the request and the average marginal carbon intensity + /// Returns a single object that contains the information about the request and the average carbon intensity /// Returned if any of the requested items are invalid /// Internal server error [Produces("application/json", "application/json; charset=utf-8")] @@ -202,11 +202,11 @@ public async Task GetAverageCarbonIntensity([FromQuery] CarbonInt /// /// /// The application only supports batching across a single location with different time boundaries. If multiple locations are provided, an error is returned. - /// For each item in the request array, the application returns a corresponding object containing the location, time boundaries, and average marginal carbon intensity. + /// For each item in the request array, the application returns a corresponding object containing the location, time boundaries, and average carbon intensity. /// - /// Array of inputs where each contains a "location", "startDate", and "endDate" for which to calculate average marginal carbon intensity. - /// An array of CarbonIntensityDTO objects which each have a location, start time, end time, and the average marginal carbon intensity over that time period. - /// Returns an array of objects where each contains location, time boundaries and the corresponding average marginal carbon intensity + /// Array of inputs where each contains a "location", "startDate", and "endDate" for which to calculate average carbon intensity. + /// An array of CarbonIntensityDTO objects which each have a location, start time, end time, and the average carbon intensity over that time period. + /// Returns an array of objects where each contains location, time boundaries and the corresponding average carbon intensity /// Returned if any of the requested items are invalid /// Internal server error [Produces("application/json", "application/json; charset=utf-8")] diff --git a/src/CarbonAware.WebApi/src/Dockerfile b/src/CarbonAware.WebApi/src/Dockerfile index 068357d4a..f0bfed06f 100644 --- a/src/CarbonAware.WebApi/src/Dockerfile +++ b/src/CarbonAware.WebApi/src/Dockerfile @@ -3,8 +3,11 @@ FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS openapi-env WORKDIR /app ENV DOTNET_ROLL_FORWARD LatestMajor COPY . ./ -RUN dotnet build CarbonAware.WebApi/src/CarbonAware.WebApi.csproj -o build +RUN dotnet build CarbonAware.WebApi/src/CarbonAware.WebApi.csproj -c Release -o build WORKDIR /app/CarbonAware.WebApi/src +# Set environment variables for dummy datasource to start "dotnet tool run" +ENV DataSources__EmissionsDataSource=Json +ENV DataSources__Configurations__Json__Type=JSON RUN dotnet tool restore && \ dotnet tool run swagger tofile --output /app/build/swagger.yaml --yaml /app/build/CarbonAware.WebApi.dll v1 @@ -17,7 +20,7 @@ ENV DOTNET_ROLL_FORWARD LatestMajor # Copy everything from source COPY . ./ # Use implicit restore to build and publish -RUN dotnet publish CarbonAware.WebApi/src/CarbonAware.WebApi.csproj -a $TARGETARCH -o publish +RUN dotnet publish CarbonAware.WebApi/src/CarbonAware.WebApi.csproj -a $TARGETARCH -c Release -o publish # Build runtime image diff --git a/src/CarbonAware.WebApi/src/Models/CarbonIntensityDTO.cs b/src/CarbonAware.WebApi/src/Models/CarbonIntensityDTO.cs index 4396c9fbe..5a7c39f2f 100644 --- a/src/CarbonAware.WebApi/src/Models/CarbonIntensityDTO.cs +++ b/src/CarbonAware.WebApi/src/Models/CarbonIntensityDTO.cs @@ -22,7 +22,7 @@ public record CarbonIntensityDTO [JsonPropertyName("endTime")] public DateTimeOffset? EndTime { get => _endTime; set => _endTime = value?.ToUniversalTime(); } - /// Value of the marginal carbon intensity in grams per kilowatt-hour. + /// Value of the carbon intensity in grams per kilowatt-hour. /// 345.434 [JsonPropertyName("carbonIntensity")] public double CarbonIntensity { get; set; } diff --git a/src/CarbonAware.WebApi/src/appsettings.json b/src/CarbonAware.WebApi/src/appsettings.json index 8208b3d19..10f68b8c8 100644 --- a/src/CarbonAware.WebApi/src/appsettings.json +++ b/src/CarbonAware.WebApi/src/appsettings.json @@ -1,14 +1,4 @@ { - "DataSources": { - "EmissionsDataSource": "test-json", - "ForecastDataSource": "", // We don't currently publish a sample test data source for forecasts. - "Configurations": { - "test-json": { - "Type": "JSON", - "DataFileLocation": "test-data-azure-emissions.json" - } - } - }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj b/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj index fd9179089..c98dc4189 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj +++ b/src/CarbonAware.WebApi/test/integrationTests/CarbonAware.WebApi.IntegrationTests.csproj @@ -22,20 +22,16 @@ - - - + + + - + - + + - \ No newline at end of file + diff --git a/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs b/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs index fc19dde48..daf63cf0b 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs +++ b/src/CarbonAware.WebApi/test/integrationTests/CarbonAwareControllerTests.cs @@ -9,13 +9,12 @@ namespace CarbonAware.WepApi.IntegrationTests; /// -/// Tests that the Web API controller handles and packages various responses from a plugin properly +/// Tests that the Web API controller handles and packages various responses from a plugin properly /// including empty responses and exceptions. /// [TestFixture(DataSourceType.JSON)] [TestFixture(DataSourceType.WattTime)] [TestFixture(DataSourceType.ElectricityMaps)] -[TestFixture(DataSourceType.ElectricityMapsFree)] class CarbonAwareControllerTests : IntegrationTestingBase { private readonly string healthURI = "/health"; @@ -181,7 +180,7 @@ public async Task BestLocations_EmptyLocationQueryString_ReturnsBadRequest(strin [Test] public async Task EmissionsForecastsCurrent_SupportedDataSources_ReturnsOk() { - IgnoreTestForDataSource($"data source does not implement '{currentForecastURI}'", DataSourceType.JSON, DataSourceType.ElectricityMapsFree); + IgnoreTestForDataSource($"data source does not implement '{currentForecastURI}'", DataSourceType.JSON); _dataSourceMocker?.SetupForecastMock(); @@ -213,8 +212,6 @@ public async Task EmissionsForecastsCurrent_SupportedDataSources_ReturnsOk() [Test] public async Task EmissionsForecastsCurrent_StartAndEndOutsideWindow_ReturnsBadRequest() { - IgnoreTestForDataSource("data source does not implement '/emissions/forecasts/current'", DataSourceType.JSON); - _dataSourceMocker?.SetupForecastMock(); var queryStrings = new Dictionary(); @@ -236,8 +233,6 @@ public async Task EmissionsForecastsCurrent_StartAndEndOutsideWindow_ReturnsBadR public async Task EmissionsForecastsCurrent_InvalidLocationQueryString_ReturnsBadRequest(string queryString, string value) { - IgnoreTestForDataSource("data source does not implement '/emissions/forecasts/current'", DataSourceType.JSON); - _dataSourceMocker?.SetupForecastMock(); var queryStrings = new Dictionary(); @@ -256,8 +251,6 @@ public async Task EmissionsForecastsCurrent_InvalidLocationQueryString_ReturnsBa [TestCase("eastus", "2021-9-1T08:30:00Z", TestName = "EmissionsForecastsBatch returns BadRequest for wrong date format")] public async Task EmissionsForecastsBatch_MissingRequiredParams_ReturnsBadRequest(string location, string requestedAt) { - IgnoreTestForDataSource("data source does not implement '/emissions/forecasts/batch'", DataSourceType.JSON); - _dataSourceMocker?.SetupForecastMock(); var forecastData = Enumerable.Range(0, 1).Select(x => new { @@ -275,7 +268,7 @@ public async Task EmissionsForecastsBatch_MissingRequiredParams_ReturnsBadReques [TestCase("2021-09-01T08:30:00Z", "2021-09-01T08:30:00Z", "2021-09-02T08:30:00Z", "westus", 3, TestName = "EmissionsForecastsBatch expects OK for multiple elements")] public async Task EmissionsForecastsBatch_SupportedDataSources_ReturnsOk(string reqAt, string start, string end, string location, int nelems) { - IgnoreTestForDataSource($"data source does not implement '{batchForecastURI}'", DataSourceType.JSON, DataSourceType.ElectricityMaps, DataSourceType.ElectricityMapsFree); + IgnoreTestForDataSource($"data source does not implement '{batchForecastURI}'", DataSourceType.JSON, DataSourceType.ElectricityMaps); var expectedRequestedAt = DateTimeOffset.Parse(reqAt); var expectedDataStartAt = DateTimeOffset.Parse(start); @@ -315,7 +308,6 @@ public async Task EmissionsForecastsBatch_SupportedDataSources_ReturnsOk(string [TestCase("2021-12-25T00:00:00+06:00", "2021-12-26T00:00:00+06:00", "westus", nameof(EmissionsMarginalCarbonIntensity_ReturnsOk) + "1", TestName = "EmissionsMarginalCarbonIntensity expects OK date only, no time")] public async Task EmissionsMarginalCarbonIntensity_ReturnsOk(string start, string end, string location, string expectedResultName) { - IgnoreTestForDataSource($"data source does not implement '{averageCarbonIntensityURI}'", DataSourceType.ElectricityMapsFree); var startDate = DateTimeOffset.Parse(start); var endDate = DateTimeOffset.Parse(end); @@ -381,8 +373,6 @@ public async Task EmissionsMarginalCarbonIntensityBatch_MissingRequiredParams_Re [TestCase("2021-12-25T00:00:00+06:00", "2021-12-26T00:00:00+06:00", "westus", 3, nameof(EmissionsMarginalCarbonIntensityBatch_SupportedDataSources_ReturnsOk) + "1", TestName = "EmissionsMarginalCarbonIntensityBatch expects OK for multiple element batch")] public async Task EmissionsMarginalCarbonIntensityBatch_SupportedDataSources_ReturnsOk(string start, string end, string location, int nelems, string expectedResultName) { - IgnoreTestForDataSource($"data source does not implement '{batchAverageCarbonIntensityURI}'", DataSourceType.ElectricityMapsFree); - var startDate = DateTimeOffset.Parse(start); var endDate = DateTimeOffset.Parse(end); _dataSourceMocker?.SetupDataMock(startDate, endDate, location); diff --git a/src/CarbonAware.WebApi/test/integrationTests/LocationsControllerTests.cs b/src/CarbonAware.WebApi/test/integrationTests/LocationsControllerTests.cs index 1a1fd79c1..db289f4cb 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/LocationsControllerTests.cs +++ b/src/CarbonAware.WebApi/test/integrationTests/LocationsControllerTests.cs @@ -7,12 +7,11 @@ namespace CarbonAware.WepApi.IntegrationTests; /// -/// Tests that the Web API controller handles locations instances +/// Tests that the Web API controller handles locations instances /// [TestFixture(DataSourceType.JSON)] [TestFixture(DataSourceType.WattTime)] [TestFixture(DataSourceType.ElectricityMaps)] -[TestFixture(DataSourceType.ElectricityMapsFree)] class LocationsControllerTests : IntegrationTestingBase { private readonly string locationsURI = "/locations"; diff --git a/src/CarbonAware.WebApi/test/integrationTests/UnconfiguredWebApiTests.cs b/src/CarbonAware.WebApi/test/integrationTests/WebApiEndpointTests.cs similarity index 77% rename from src/CarbonAware.WebApi/test/integrationTests/UnconfiguredWebApiTests.cs rename to src/CarbonAware.WebApi/test/integrationTests/WebApiEndpointTests.cs index 665b3884a..6bb55dd74 100644 --- a/src/CarbonAware.WebApi/test/integrationTests/UnconfiguredWebApiTests.cs +++ b/src/CarbonAware.WebApi/test/integrationTests/WebApiEndpointTests.cs @@ -5,15 +5,15 @@ namespace CarbonAware.WebApi.IntegrationTests; /// -/// Tests that the Web API starts without configuration. +/// Tests that static Web API endpoints. /// -[TestFixture(DataSourceType.None)] -class UnconfiguredWebApiTests : IntegrationTestingBase +[TestFixture(DataSourceType.JSON)] +class WebApiEndpointTests : IntegrationTestingBase { private readonly string healthURI = "/health"; private readonly string fakeURI = "/fake-endpoint"; - public UnconfiguredWebApiTests(DataSourceType dataSource) : base(dataSource) { } + public WebApiEndpointTests(DataSourceType dataSource) : base(dataSource) { } [Test] public async Task HealthCheck_ReturnsOK() diff --git a/src/CarbonAware/src/CarbonAware.csproj b/src/CarbonAware/src/CarbonAware.csproj index d04ce3a78..72bfe5ee3 100644 --- a/src/CarbonAware/src/CarbonAware.csproj +++ b/src/CarbonAware/src/CarbonAware.csproj @@ -28,9 +28,6 @@ - - - diff --git a/src/CarbonAwareSDK.sln b/src/CarbonAwareSDK.sln index 82672374a..3dce0b261 100644 --- a/src/CarbonAwareSDK.sln +++ b/src/CarbonAwareSDK.sln @@ -51,12 +51,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarbonAware.DataSources.Ele EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarbonAware.DataSources.ElectricityMaps.Mocks", "CarbonAware.DataSources\CarbonAware.DataSources.ElectricityMaps\mock\CarbonAware.DataSources.ElectricityMaps.Mocks.csproj", "{40155CED-4F39-485D-9AF8-4D267E236146}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarbonAware.DataSources.ElectricityMapsFree", "CarbonAware.DataSources\CarbonAware.DataSources.ElectricityMapsFree\src\CarbonAware.DataSources.ElectricityMapsFree.csproj", "{F08A1A30-EE88-4A32-9586-DC4E6410B9E2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarbonAware.DataSources.ElectricityMapsFree.Tests", "CarbonAware.DataSources\CarbonAware.DataSources.ElectricityMapsFree\test\CarbonAware.DataSources.ElectricityMapsFree.Tests.csproj", "{52100C22-4F79-4498-B413-B93C68A599C2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarbonAware.DataSources.ElectricityMapsFree.Mocks", "CarbonAware.DataSources\CarbonAware.DataSources.ElectricityMapsFree\mock\CarbonAware.DataSources.ElectricityMapsFree.Mocks.csproj", "{A7B0EF32-5AF3-4E56-AE88-627CC99BE635}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj b/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj index 6bb9fac4b..c7ad608aa 100644 --- a/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj +++ b/src/GSF.CarbonAware/src/GSF.CarbonAware.csproj @@ -50,12 +50,13 @@ - - + + + + diff --git a/src/GSF.CarbonAware/src/GSF.CarbonAware.targets b/src/GSF.CarbonAware/src/GSF.CarbonAware.targets index 752a668a6..8e17e6cf1 100644 --- a/src/GSF.CarbonAware/src/GSF.CarbonAware.targets +++ b/src/GSF.CarbonAware/src/GSF.CarbonAware.targets @@ -1,24 +1,11 @@ - - - - - - - - - - - - - - - - + + + diff --git a/src/GSF.CarbonAware/src/Handlers/IForecastHandler.cs b/src/GSF.CarbonAware/src/Handlers/IForecastHandler.cs index bdaeb48f1..383e57768 100644 --- a/src/GSF.CarbonAware/src/Handlers/IForecastHandler.cs +++ b/src/GSF.CarbonAware/src/Handlers/IForecastHandler.cs @@ -5,7 +5,7 @@ namespace GSF.CarbonAware.Handlers; public interface IForecastHandler { /// - /// Retrieves the most recent forecasted data and calculates the optimal marginal carbon intensity window. + /// Retrieves the most recent forecasted data and calculates the optimal carbon intensity window. /// /// Array of locations where the workflow is run (ex: ["eastus", "westus"]) /// Start time boundary of forecasted data points. Ignores current forecast data points before this time (ex: 2022-03-01T15:30:00Z) @@ -15,13 +15,13 @@ public interface IForecastHandler Task> GetCurrentForecastAsync(string[] locations, DateTimeOffset? dataStartAt = null, DateTimeOffset? dataEndAt = null, int? windowSize = null); /// - /// Retrieves the historical forecasted data for the given date range and calculates the optimal marginal carbon intensity window. + /// Retrieves the historical forecasted data for the given date range and calculates the optimal carbon intensity window. /// /// String location where the workflow is run (ex: "eastus") /// Start time boundary of forecasted data points. Ignores current forecast data points before this time (ex: 2022-03-01T15:30:00Z) /// End time boundary of forecasted data points. Ignores current forecast data points after this time (ex: 2022-03-01T18:30:00Z) /// The timestamp used to access the most recently generated forecast as of that time. (ex: 2022-03-01T18:30:00Z) /// The estimated duration (in minutes) of the workload. - /// An with the optimal marginal carbon intensity window. + /// An with the optimal carbon intensity window. Task GetForecastByDateAsync(string location, DateTimeOffset? dataStartAt = null, DateTimeOffset? dataEndAt = null, DateTimeOffset? requestedAt = null, int? windowSize = null); }