Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix watt time plugin #40

Merged
merged 3 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading