Skip to content

Commit

Permalink
Merge pull request #53 from Green-Software-Foundation/watt-time-region
Browse files Browse the repository at this point in the history
Enhance Watt-time plugin to use WT-REGION-ID
  • Loading branch information
narekhovhannisyan authored Mar 12, 2024
2 parents 4699ea2 + 82cde69 commit 25bf096
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 106 deletions.
73 changes: 23 additions & 50 deletions src/__mocks__/watt-time/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import * as DATA from './data.json';

export function getMockResponse(url: string) {
switch (url) {
case 'https://api2.watttime.org/v2/login':
case 'https://api.watttime.org/login':
if (
process.env.WATT_TIME_USERNAME === 'test1' &&
process.env.WATT_TIME_PASSWORD === 'test2'
process.env.WATT_TIME_USERNAME &&
['test1', 'invalidData1', 'fetchError1'].includes(
process.env.WATT_TIME_USERNAME
) &&
process.env.WATT_TIME_PASSWORD &&
['test2', 'invalidData2', 'fetchError2'].includes(
process.env.WATT_TIME_PASSWORD
)
) {
return Promise.resolve({
status: 200,
Expand All @@ -18,62 +24,29 @@ export function getMockResponse(url: string) {
status: 401,
data: {},
});

case 'https://apifail.watttime.org/v2/login': {
case 'https://api2.watttime.org/v2/data':
if (
process.env.WATT_TIME_USERNAME === 'test1' &&
process.env.WATT_TIME_PASSWORD === 'test2'
process.env.WATT_TIME_USERNAME === 'invalidData1' &&
process.env.WATT_TIME_PASSWORD === 'invalidData2'
) {
return Promise.resolve({
status: 200,
data: {
token: 'test_token',
},
status: 400,
data: {},
});
} else if (
process.env.WATT_TIME_USERNAME === 'fetchError1' &&
process.env.WATT_TIME_PASSWORD === 'fetchError2'
) {
return Promise.resolve({
status: 400,
data: {none: {}},
});
}
return Promise.resolve({
status: 401,
data: {},
});
}
case 'https://apifail2.watttime.org/v2/login':
return Promise.resolve({
status: 200,
data: {
token: 'test_token',
},
});
case 'https://apifail3.watttime.org/v2/login':
return Promise.resolve({
status: 200,
data: {
token: 'test_token',
},
});
case 'https://api2.watttime.org/v2/data':

return Promise.resolve({
data: DATA,
status: 200,
});
case 'https://apifail.watttime.org/v2/data':
return Promise.resolve({
status: 400,
data: {},
});
case 'https://apifail2.watttime.org/v2/data':
return Promise.resolve({
status: 200,
data: {
none: {},
},
});
case 'https://apifail3.watttime.org/v2/data':
return Promise.reject({
status: 401,
data: {
none: {},
},
});
}
return Promise.resolve({});
}
47 changes: 26 additions & 21 deletions src/__tests__/unit/lib/watt-time/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,9 @@ describe('lib/watt-time: ', () => {
});

it('throws an error when initialize with wrong username / password.', async () => {
process.env.WATT_TIME_USERNAME = 'test1';
process.env.WATT_TIME_PASSWORD = 'test2';
const output = WattTimeGridEmissions({
baseUrl: 'https://apifail.watttime.org/v2',
});
process.env.WATT_TIME_USERNAME = 'wrong1';
process.env.WATT_TIME_PASSWORD = 'wrong2';
const output = WattTimeGridEmissions();

expect.assertions(1);

Expand All @@ -113,13 +111,13 @@ describe('lib/watt-time: ', () => {
},
]);
} catch (error) {
expect(error).toBeInstanceOf(APIRequestError);
expect(error).toBeInstanceOf(AuthorizationError);
}
});

it('throws an error when wrong `geolocation` is provided.', async () => {
const errorMessage =
'"geolocation" parameter is not a comma-separated string consisting of `latitude` and `longitude`. Error code: invalid_string.';
'"geolocation" parameter is not a comma-separated string consisting of `latitude` and `longitude`. Error code: invalid_string.,at least one of `geolocation`, `cloud/region-wt-id`, or `cloud/region-geolocation` parameters should be provided.';
process.env.WATT_TIME_USERNAME = 'test1';
process.env.WATT_TIME_PASSWORD = 'test2';

Expand All @@ -137,7 +135,11 @@ describe('lib/watt-time: ', () => {
]);
} catch (error) {
expect(error).toBeInstanceOf(InputValidationError);
expect(error).toEqual(new InputValidationError(errorMessage));
expect(error).toEqual(
new InputValidationError(
'"geolocation" parameter is not a comma-separated string consisting of `latitude` and `longitude`. Error code: invalid_string.'
)
);
}

try {
Expand All @@ -150,7 +152,11 @@ describe('lib/watt-time: ', () => {
]);
} catch (error) {
expect(error).toBeInstanceOf(InputValidationError);
expect(error).toEqual(new InputValidationError(errorMessage));
expect(error).toEqual(
new InputValidationError(
'"geolocation" parameter is not a comma-separated string consisting of `latitude` and `longitude`. Error code: invalid_string.'
)
);
}

try {
Expand All @@ -168,13 +174,12 @@ describe('lib/watt-time: ', () => {
});

it('throws an error when no data is returned by API.', async () => {
const errorMessage = 'WattTimeAPI: Invalid response from WattTime API.';
process.env.WATT_TIME_USERNAME = 'test1';
process.env.WATT_TIME_PASSWORD = 'test2';
const errorMessage =
'WattTimeAPI: Error fetching data from WattTime API: 400.';
process.env.WATT_TIME_USERNAME = 'invalidData1';
process.env.WATT_TIME_PASSWORD = 'invalidData2';

const output = WattTimeGridEmissions({
baseUrl: 'https://apifail2.watttime.org/v2',
});
const output = WattTimeGridEmissions();

expect.assertions(2);
try {
Expand All @@ -198,13 +203,12 @@ describe('lib/watt-time: ', () => {

it('throws an error when an unauthorized error occurs during data fetch.', async () => {
const errorMessage =
'WattTimeAPI: Error fetching data from WattTime API. {"status":401,"data":{"none":{}}}.';
process.env.WATT_TIME_USERNAME = 'test1';
process.env.WATT_TIME_PASSWORD = 'test2';
'WattTimeAPI: Error fetching data from WattTime API: 400.';
process.env.WATT_TIME_USERNAME = 'fetchError1';
process.env.WATT_TIME_PASSWORD = 'fetchError2';

const output = WattTimeGridEmissions({
baseUrl: 'https://apifail3.watttime.org/v2',
});
const output = WattTimeGridEmissions();
expect.assertions(2);

try {
await output.execute([
Expand Down Expand Up @@ -232,6 +236,7 @@ describe('lib/watt-time: ', () => {
process.env.WATT_TIME_PASSWORD = 'test2';

const output = WattTimeGridEmissions();
expect.assertions(2);

try {
await output.execute([
Expand Down
13 changes: 11 additions & 2 deletions src/lib/watt-time/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ 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"
- `cloud/region-geolocation`: The same as `geolocation`, with calculations performed by the `cloud-metadata` plugin
- `cloud/region-wt-id`: Region abbreviation associated with location (e.g. 'CAISO_NORTH')
- `signal-type`: The signal type of selected region (optional) (e.g 'co2_moer')

Either `geolocation`,`cloud/region-wt-id` or `cloud/region-geolocation` should be provided.

## Implementation

Expand Down Expand Up @@ -51,8 +56,12 @@ WATT_TIME_TOKEN: <your-token>
**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)
- `geolocation`: Location of the software system (latitude in decimal degrees, longitude in decimal degrees). "latitude,longitude"
- `cloud/region-geolocation`: The same as `geolocation`, with calculations performed by the `cloud-metadata` plugin
- `cloud/region-wt-id`: Region abbreviation associated with location (e.g. 'CAISO_NORTH')

Either `geolocation`,`cloud/region-wt-id` or `cloud/region-geolocation` should be provided.

### Typescript Usage

Expand Down
75 changes: 55 additions & 20 deletions src/lib/watt-time/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {ConfigParams, KeyValuePair, PluginParams} from '../../types/common';
import {PluginInterface} from '../../interfaces';
import {validate} from '../../util/validations';

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

const {InputValidationError} = ERRORS;
Expand All @@ -24,9 +24,9 @@ export const WattTimeGridEmissions = (
* Initialize authentication with global config.
*/
const initializeAuthentication = async () => {
const safeConfig = validateConfig();
validateConfig();

await wattTimeAPI.authenticate(safeConfig);
await wattTimeAPI.authenticate();
};

/**
Expand Down Expand Up @@ -62,16 +62,39 @@ export const WattTimeGridEmissions = (
* Validates input parameters.
*/
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+$'), {
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:
'not a comma-separated string consisting of `latitude` and `longitude`',
})
.optional(),
'cloud/region-wt-id': z.string().optional(),
'cloud/region-geolocation': z.string().optional(),
'signal-type': z.string().optional(),
})
.refine(
data => {
const {
geolocation,
'cloud/region-wt-id': regionWtId,
'cloud/region-geolocation': regionGeolocation,
} = data;
return geolocation || regionWtId || regionGeolocation;
},
{
message:
'not a comma-separated string consisting of `latitude` and `longitude`',
}),
});
'at least one of `geolocation`, `cloud/region-wt-id`, or `cloud/region-geolocation` parameters should be provided.',
}
);

if (input['cloud/region-geolocation']) {
input.geolocation = input['cloud/region-geolocation'];
}

return validate<z.infer<typeof schema>>(schema, input);
};
Expand All @@ -97,9 +120,10 @@ export const WattTimeGridEmissions = (
* if the data point is exactly the same as the input end, ignore it
*/
if (
!dayjs(data.point_time).isBefore(inputStart) &&
!dayjs(data.point_time).isAfter(inputEnd) &&
dayjs(data.point_time).format() !== dayjs(inputEnd).format()
!dayjs(data.point_time).isBefore(inputStart.toISOString()) &&
!dayjs(data.point_time).isAfter(inputEnd.toISOString()) &&
dayjs(data.point_time).format() !==
dayjs(inputEnd.toISOString()).format()
) {
accumulator.push(data.value / kgMWh);
}
Expand Down Expand Up @@ -130,13 +154,25 @@ export const WattTimeGridEmissions = (
*/
const getWattTimeData = async (inputs: PluginParams[]) => {
const {startTime, fetchDuration} = calculateStartDurationTime(inputs);

if (inputs[0]['cloud/region-wt-id']) {
const params: WattTimeRegionParams = {
start: dayjs(startTime).toISOString(),
end: dayjs(startTime).add(fetchDuration, 'seconds').toISOString(),
region: inputs[0]['cloud/region-wt-id'],
signal_type: inputs[0]['signal-type'],
};

return await wattTimeAPI.fetchDataWithRegion(params);
}

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

const params: WattTimeParams = {
latitude,
longitude,
starttime: dayjs(startTime).format('YYYY-MM-DDTHH:mm:ssZ'),
endtime: dayjs(startTime).add(fetchDuration, 'seconds'),
starttime: dayjs(startTime).toISOString(),
endtime: dayjs(startTime).add(fetchDuration, 'seconds').toISOString(),
};

return await wattTimeAPI.fetchAndSortData(params);
Expand All @@ -152,7 +188,7 @@ export const WattTimeGridEmissions = (
const calculateStartDurationTime = (
inputs: PluginParams[]
): {
startTime: string;
startTime: dayjs.Dayjs;
fetchDuration: number;
} => {
const {startTime, endtime} = inputs.reduce(
Expand Down Expand Up @@ -184,7 +220,7 @@ export const WattTimeGridEmissions = (
);
}

return {startTime: startTime.format(), fetchDuration};
return {startTime: startTime, fetchDuration};
};

/**
Expand All @@ -201,7 +237,6 @@ export const WattTimeGridEmissions = (
WATT_TIME_PASSWORD: z.string().min(1, {
message: 'not provided in .env file of `IF` root directory',
}),
baseUrl: z.string().optional(),
});

return validate<z.infer<typeof schema>>(schema, {
Expand Down
15 changes: 8 additions & 7 deletions src/lib/watt-time/types.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import * as dayjs from 'dayjs';

export interface WattTimeParams {
latitude: number;
longitude: number;
starttime: string;
endtime: dayjs.Dayjs;
endtime: string;
}

export interface WattTimeRegionParams {
start: string;
end: string;
region: string;
signal_type?: string;
}

export interface LatitudeLongitude {
latitude: number;
longitude: number;
}

export type WattAuthType = {
baseUrl?: string;
};
Loading

0 comments on commit 25bf096

Please sign in to comment.