Skip to content

Commit

Permalink
Merge pull request #40 from Green-Software-Foundation/fix-watt-time
Browse files Browse the repository at this point in the history
Fix watt time plugin
  • Loading branch information
jmcook1186 authored Feb 28, 2024
2 parents 3f243a4 + 481ccf7 commit eb42f66
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 115 deletions.
33 changes: 1 addition & 32 deletions src/__tests__/unit/lib/watt-time/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('lib/watt-time: ', () => {
geolocation: '37.7749,-122.4194',
timestamp: '2021-01-01T00:00:00Z',
duration: 1200,
'grid/carbon-intensity': 2185.332173907599,
'grid/carbon-intensity': 2096.256940667132,
},
]);
});
Expand Down Expand Up @@ -122,37 +122,6 @@ describe('lib/watt-time: ', () => {
}
});

it('throws an error if watttime api returns wrong data.', async () => {
const errorMessage =
'WattTimeGridEmissions: Did not receive data from WattTime API for the input[1] block.';
const output = WattTimeGridEmissions({
username: 'test1',
password: 'test2',
});

const inputs = [
{
geolocation: '37.7749,-122.4194',
timestamp: '2021-01-01T00:00:00Z',
duration: 3600,
},
{
geolocation: '37.7749,-122.4194',
timestamp: '2021-01-02T01:00:00Z',
duration: 3600,
},
];

expect.assertions(2);

try {
await output.execute(inputs);
} catch (error) {
expect(error).toBeInstanceOf(InputValidationError);
expect(error).toEqual(new InputValidationError(errorMessage));
}
});

it('throws an error when wrong `geolocation` is provided.', async () => {
const errorMessage =
"\"geolocation\" parameter is 'geolocation' should be a comma separated string of 'latitude' and 'longitude'. Error code: invalid_string.";
Expand Down
28 changes: 14 additions & 14 deletions src/lib/watt-time/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ WattTime plugin provides a way to calculate emissions for a given time in a spec

The plugin is based on the WattTime API. The plugin uses the following inputs:

- geolocation: Location of the software system (latitude in decimal degrees, longitude in decimal degrees). "latitude,longitude"
- timestamp: Timestamp of the recorded event (2021-01-01T00:00:00Z) RFC3339
- duration: Duration of the recorded event in seconds (3600)
- `geolocation`: Location of the software system (latitude in decimal degrees, longitude in decimal degrees). "latitude,longitude"
- `timestamp`: Timestamp of the recorded event (2021-01-01T00:00:00Z) RFC3339
- `duration`: Duration of the recorded event in seconds (3600)

## Implementation

Expand Down Expand Up @@ -44,9 +44,9 @@ WattTime API requires activation of subscription before usage. Please refer to t

**Required Parameters:**

- timestamp: Timestamp of the recorded event (2021-01-01T00:00:00Z) RFC3339
- geolocation: Location of the software system (latitude in decimal degrees, longitude in decimal degrees). "latitude,longitude"
- duration: Duration of the recorded event in seconds (3600)
- `timestamp`: Timestamp of the recorded event (2021-01-01T00:00:00Z) RFC3339
- `geolocation`: Location of the software system (latitude in decimal degrees, longitude in decimal degrees). "latitude,longitude"
- `duration`: Duration of the recorded event in seconds (3600)

### Typescript Usage

Expand All @@ -69,9 +69,9 @@ const inputs = [
const results = output.execute(inputs);
```

### IMPL Usage
### manifest Usage

#### Environment Variable based configuration for IMPL
#### Environment Variable based configuration for manifest

```yaml
# environment variable config , prefix the environment variables with "ENV" to load them inside the plugin.
Expand All @@ -82,7 +82,7 @@ global-config:
password: ENV_WATT_TIME_PASSWORD
```
#### Static configuration for IMPL
#### Static configuration for manifest
```yaml
inputs:
Expand All @@ -91,7 +91,7 @@ inputs:
duration: 3600
```
## Example impl
## Example manifest
```yaml
name: watt-time
Expand All @@ -100,7 +100,7 @@ tags:
initialize:
plugins:
watt-time:
plugin: WattTimeGridEmissions
method: WattTimeGridEmissions
path: '@grnsft/if-unofficial-plugins'
global-config:
username: username
Expand All @@ -121,10 +121,10 @@ You can run this by passing it to `if`. Run impact using the following command r
```sh
npm i -g @grnsft/if
npm i -g @grnsft/if-unofficial-plugins
if --impl ./examples/impls/test/watt-time.yml --ompl ./examples/ompls/watt-time.yml
if --manifest ./examples/manifests/test/watt-time.yml --output ./examples/outputs/watt-time.yml
```

## Position and effects in the impl:
## Position and effects in the manifest:

- Technically, WattTime plugin sets (or overwrites any preconfigured value of) the _grid/carbon-intensity_ attribute.
- Technically, WattTime plugin sets (or overwrites any preconfigured value of) the `grid/carbon-intensity` attribute.
- As such, it should be positioned before the _sci-o_ plugin, if such a plugin is used.
103 changes: 34 additions & 69 deletions src/lib/watt-time/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import {z} from 'zod';
import {ERRORS} from '../../util/errors';
import {buildErrorMessage} from '../../util/helpers';

import {ConfigParams, KeyValuePair, ModelParams} from '../../types/common';
import {ConfigParams, KeyValuePair, PluginParams} from '../../types/common';
import {PluginInterface} from '../../interfaces';
import {validate} from '../../util/validations';

import {WattAuthType, WattTimeParams} from './types';
import {WattTimeParams} from './types';
import {WattTimeAPI} from './watt-time-api';

const {InputValidationError} = ERRORS;
Expand All @@ -25,28 +25,21 @@ export const WattTimeGridEmissions = (
*/
const initializeAuthentication = async () => {
const extractedParams = extractParamsFromConfig();
const safeConfig: WattAuthType = validateConfig(extractedParams);
const safeConfig = validateConfig(extractedParams);

await wattTimeAPI.authenticate(safeConfig);
};

/**
* Calculates the average emission.
*/
const execute = async (inputs: ModelParams[]): Promise<ModelParams[]> => {
const execute = async (inputs: PluginParams[]) => {
await initializeAuthentication();
validateInputs(inputs);

const wattTimeData = await getWattTimeData(inputs);

return inputs.map((input, index) => {
const inputStart = dayjs(input.timestamp);
const inputEnd = inputStart.add(input.duration, 'seconds');
const data = getWattTimeDataForDuration(
wattTimeData,
inputStart,
inputEnd
);
const data = getWattTimeDataForDuration(wattTimeData);

if (data.length === 0) {
throw new InputValidationError(
Expand All @@ -57,10 +50,27 @@ export const WattTimeGridEmissions = (
}

const totalEmission = data.reduce((a: number, b: number) => a + b, 0);
input['grid/carbon-intensity'] = totalEmission / data.length;

return input;
return {
...input,
'grid/carbon-intensity': totalEmission / data.length,
};
});
};

const validateInput = (input: PluginParams) => {
const schema = z.object({
duration: z.number(),
timestamp: z.string(),
geolocation: z
.string()
.regex(new RegExp('^\\d{1,3}\\.\\d+,-\\d{1,3}\\.\\d+$'), {
message:
"'geolocation' should be a comma separated string of 'latitude' and 'longitude'",
}),
});

return validate<z.infer<typeof schema>>(schema, input);
};

/**
Expand All @@ -70,85 +80,39 @@ export const WattTimeGridEmissions = (
* convert to g/KWh by multiplying by 1000. (1Kg = 1000g)
* hence each other cancel out and g/KWh is the same as kg/MWh
*/
const getWattTimeDataForDuration = (
wattTimeData: KeyValuePair[],
inputStart: dayjs.Dayjs,
inputEnd: dayjs.Dayjs
) => {
const getWattTimeDataForDuration = (wattTimeData: KeyValuePair[]) => {
const kgMWh = 0.45359237;

return wattTimeData.reduce((accumulator, data) => {
if (
!dayjs(data.point_time).isBefore(inputStart) &&
!dayjs(data.point_time).isAfter(inputEnd) &&
dayjs(data.point_time).format() !== dayjs(inputEnd).format()
) {
accumulator.push(data.value / kgMWh);
}
accumulator.push(data.value / kgMWh);

return accumulator;
}, []);
};

/**
* Validates inputs for geolocation, latitude and longitude.
*/
const validateInputs = (inputs: ModelParams[]) => {
inputs.forEach((input, index) => {
if ('geolocation' in input) {
const {latitude, longitude} = parseLocation(input);

if (isNaN(latitude) || isNaN(longitude)) {
throw new InputValidationError(
errorBuilder({
message: `'latitude' or 'longitude' from input[${index}] is not a number`,
})
);
}
}
});
};

/**
* Parses the geolocation string from the input data to extract latitude and longitude.
* Throws an InputValidationError if the geolocation string is invalid.
*/
const parseLocation = (
input: ModelParams
geolocation: string
): {
latitude: number;
longitude: number;
} => {
const safeInput = Object.assign(input, validateSingleInput(input));
const [latitude, longitude] = safeInput['geolocation'].split(',');
const [latitude, longitude] = geolocation.split(',');

return {latitude: parseFloat(latitude), longitude: parseFloat(longitude)};
};

/**
* Validates single input.
*/
const validateSingleInput = (input: ModelParams) => {
const schema = z.object({
geolocation: z
.string()
.regex(new RegExp('^\\d{1,3}\\.\\d+,-\\d{1,3}\\.\\d+$'), {
message:
"'geolocation' should be a comma separated string of 'latitude' and 'longitude'",
}),
});

return validate<z.infer<typeof schema>>(schema, input);
};

/**
* Retrieves data from the WattTime API based on the provided inputs.
* Determines the start time and fetch duration from the inputs, and parses the geolocation.
* Fetches data from the WattTime API for the entire duration and returns the sorted data.
*/
const getWattTimeData = async (inputs: ModelParams[]) => {
const getWattTimeData = async (inputs: PluginParams[]) => {
const {startTime, fetchDuration} = calculateStartDurationTime(inputs);

const {latitude, longitude} = parseLocation(inputs[0]);
const {latitude, longitude} = parseLocation(inputs[0].geolocation);

const params: WattTimeParams = {
latitude,
Expand All @@ -168,14 +132,15 @@ export const WattTimeGridEmissions = (
*
*/
const calculateStartDurationTime = (
inputs: ModelParams[]
inputs: PluginParams[]
): {
startTime: string;
fetchDuration: number;
} => {
const {startTime, endtime} = inputs.reduce(
(acc, input) => {
const {duration, timestamp} = input;
const safeInput = validateInput(input);
const {duration, timestamp} = safeInput;
const dayjsTimestamp = dayjs(timestamp);
const startTime = dayjsTimestamp.isBefore(acc.startTime)
? dayjsTimestamp
Expand Down

0 comments on commit eb42f66

Please sign in to comment.