diff --git a/package.json b/package.json index 495f7d3..a4145ec 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "scripts": { "build": "rm -rf build && tsc --project tsconfig.build.json", "fix": "gts fix", + "coverage": "jest --verbose --coverage", "fix:package": "fixpack", "lint": "gts lint", "prepublish": "yarn build", diff --git a/src/__tests__/unit/lib/boavizta/index.test.ts b/src/__tests__/unit/lib/boavizta/index.test.ts index 4fcc426..f762f46 100644 --- a/src/__tests__/unit/lib/boavizta/index.test.ts +++ b/src/__tests__/unit/lib/boavizta/index.test.ts @@ -4,14 +4,10 @@ import { BoaviztaCpuOutputModel, } from '../../../../lib/boavizta/index'; -import {ERRORS} from '../../../../util/errors'; - import * as PROVIDERS from '../../../../__mocks__/boavizta/providers.json'; import * as COUNTRIES from '../../../../__mocks__/boavizta/countries.json'; import * as INSTANCE_TYPES from '../../../../__mocks__/boavizta/instance_types.json'; -const {InputValidationError} = ERRORS; - async function axiosGet>( url: string ): Promise { @@ -38,7 +34,7 @@ mockAxios.post.mockImplementation( case 'https://api.boavizta.org/v1/component/cpu?verbose=false&duration=1': return Promise.resolve({ data: { - outputs: { + impacts: { gwp: { embedded: { value: 0.0008, @@ -78,7 +74,7 @@ mockAxios.post.mockImplementation( case 'https://api.boavizta.org/v1/component/cpu?verbose=true&duration=2': return Promise.resolve({ data: { - outputs: { + impacts: { gwp: { embedded: { value: 0.0016, @@ -114,7 +110,7 @@ mockAxios.post.mockImplementation( }, }, verbose: { - outputs: { + impacts: { gwp: { embedded: { value: 0.0016, @@ -208,7 +204,7 @@ mockAxios.post.mockImplementation( value: 9.85548e-8, status: 'COMPLETED', unit: 'kg Sbeq/kWh', - source: 'ADEME Base outputS ®', + source: 'ADEME Base Impacts ®', min: 9.85548e-8, max: 9.85548e-8, }, @@ -226,7 +222,7 @@ mockAxios.post.mockImplementation( case 'https://api.boavizta.org/v1/cloud/instance?verbose=false&duration=0.004166666666666667': return Promise.resolve({ data: { - outputs: { + impacts: { gwp: { embedded: { value: 0.0016, @@ -262,7 +258,7 @@ mockAxios.post.mockImplementation( }, }, verbose: { - outputs: { + impacts: { gwp: { embedded: { value: 0.0016, @@ -356,7 +352,7 @@ mockAxios.post.mockImplementation( value: 9.85548e-8, status: 'COMPLETED', unit: 'kg Sbeq/kWh', - source: 'ADEME Base outputS ®', + source: 'ADEME Base Impacts®', min: 9.85548e-8, max: 9.85548e-8, }, @@ -381,52 +377,40 @@ describe('cpu:configure test', () => { const outputModel = new BoaviztaCpuOutputModel(); await expect( outputModel.configure({allocation: 'wrong'}) - ).rejects.toThrowError(); + ).rejects.toThrow(); }); - // test('initialize without params throws error for parameter and call execute without params throws error for input', async () => { - // const outputModel = new BoaviztaCpuOutputModel(); - // const outputModelConfigFail = new BoaviztaCpuOutputModel(); - // await expect(outputModel.authenticate({})).resolves.toBe(undefined); - // await expect( - // outputModelConfigFail.execute([ - // { - // timestamp: '2021-01-01T00:00:00Z', - // duration: 3600, - // 'cpu-util': 50, - // }, - // ]) - // ).rejects.toThrowError(); + test('initialize without params throws error for parameter and call execute without params throws error for input', async () => { + const outputModel = new BoaviztaCpuOutputModel(); + const outputModelConfigFail = new BoaviztaCpuOutputModel(); + await expect(outputModel.authenticate({})).resolves.toBe(undefined); // authenticate does not have any params / output + await expect( + outputModelConfigFail.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'cpu-util': 50, + }, + ]) + ).rejects.toThrow(); - // await expect(outputModel.configure({})).rejects.toThrow( - // Error('Improper configure: Missing processor parameter') - // ); - // await expect( - // outputModel.configure({ - // 'physical-processor': 'Intel Xeon Gold 6138f', - // }) - // ).rejects.toThrow( - // Error('Improper configure: Missing core-units parameter') - // ); - // await expect( - // outputModel.configure({ - // 'physical-processor': 'Intel Xeon Gold 6138f', - // 'core-units': 24, - // 'expected-lifespan': 4 * 365 * 24 * 60 * 60, - // }) - // ).resolves.toBeInstanceOf(BoaviztaCpuOutputModel); + await expect(outputModel.configure({})).rejects.toThrow(); + await expect( + outputModel.configure({ + 'physical-processor': 'Intel Xeon Gold 6138f', + }) + ).rejects.toThrow(); + await expect( + outputModel.configure({ + 'physical-processor': 'Intel Xeon Gold 6138f', + 'core-units': 24, + 'expected-lifespan': 4 * 365 * 24 * 60 * 60, + }) + ).resolves.toBeInstanceOf(BoaviztaCpuOutputModel); - // // not providing inputs will throw a missing inputs error - // await expect(outputModel.execute(undefined)).rejects.toStrictEqual( - // Error( - // 'Parameter Not Given: invalid inputs parameter. Expecting an array of inputs' - // ) - // ); - // // improper inputs will throw an invalid inputs error - // await expect( - // outputModel.execute([{invalid: 'input'}]) - // ).rejects.toStrictEqual(Error('Invalid Input: Invalid inputs parameter')); - // }); + // not providing inputs will throw a missing inputs error + await expect(outputModel.execute([])).rejects.toThrow(); + }); }); describe('cpu:initialize with params', () => { @@ -458,6 +442,16 @@ describe('cpu:initialize with params', () => { }); test('initialize with params and call multiple usages in IMPL format:verbose', async () => { const outputModel = new BoaviztaCpuOutputModel(); + // test configuration with verbose false + await expect( + outputModel.configure({ + 'physical-processor': 'Intel Xeon Gold 6138f', + 'core-units': 24, + location: 'USA', + verbose: false, + }) + ).resolves.toBeInstanceOf(BoaviztaCpuOutputModel); + // test configuration with verbose true await expect( outputModel.configure({ 'physical-processor': 'Intel Xeon Gold 6138f', @@ -467,7 +461,7 @@ describe('cpu:initialize with params', () => { }) ).resolves.toBeInstanceOf(BoaviztaCpuOutputModel); - // configure without static params will cause improper configure error + // verbose still results in same output await expect( outputModel.execute([ { @@ -488,15 +482,6 @@ describe('cpu:initialize with params', () => { describe('cloud:initialize with params', () => { test('initialize with params and call usage in RAW Format', async () => { const outputModel = new BoaviztaCloudOutputModel(); - await expect( - outputModel.validateLocation({location: 'SomethingFail'}) - ).rejects.toThrowError(); - await expect( - outputModel.validateInstanceType({'instance-type': 'SomethingFail'}) - ).rejects.toThrowError(); - await expect( - outputModel.validateProvider({provider: 'SomethingFail'}) - ).rejects.toThrowError(); await expect( outputModel.configure({ 'instance-type': 't2.micro', @@ -527,22 +512,55 @@ describe('cloud:initialize with params', () => { // configure without static params will cause improper configure error }); - - test("correct 'instance-type': initialize with params and call usage in IMPL Format", async () => { + test('invalid input for location throws error', async () => { + const outputModel = new BoaviztaCloudOutputModel(); + await expect( + outputModel.validateLocation({location: 'SomethingFail'}) + ).rejects.toThrow(); + }); + test('invalid input for instance type throws error', async () => { + const outputModel = new BoaviztaCloudOutputModel(); + await expect( + outputModel.validateInstanceType({'instance-type': 'SomethingFail'}) + ).rejects.toThrow(); + }); + test('invalid input for provider throws error', async () => { + const outputModel = new BoaviztaCloudOutputModel(); + await expect( + outputModel.validateProvider({provider: 'SomethingFail'}) + ).rejects.toThrow(); + }); + test('missing provider throws error', async () => { const outputModel = new BoaviztaCloudOutputModel(); - await expect( outputModel.configure({ 'instance-type': 't2.micro', location: 'USA', }) - ).rejects.toThrowError(); + ).rejects.toThrow(); + }); + test('missing `instance-type` throws error', async () => { + const outputModel = new BoaviztaCloudOutputModel(); await expect( outputModel.configure({ provider: 'aws', location: 'USA', }) - ).rejects.toThrowError(); + ).rejects.toThrow(); + }); + + test('wrong `instance-type` throws error', async () => { + const outputModel = new BoaviztaCloudOutputModel(); + await expect( + outputModel.configure({ + 'instance-type': 't5.micro', + location: 'USA', + provider: 'aws', + }) + ).rejects.toThrow(); + }); + test("correct 'instance-type': initialize with params and call usage in IMPL Format", async () => { + const outputModel = new BoaviztaCloudOutputModel(); await expect( outputModel.configure({ 'instance-type': 't2.micro', @@ -550,8 +568,6 @@ describe('cloud:initialize with params', () => { provider: 'aws', }) ).resolves.toBeInstanceOf(BoaviztaCloudOutputModel); - - // mockAxios.get.mockResolvedValue({data: {}}); await expect( outputModel.execute([ { @@ -566,44 +582,14 @@ describe('cloud:initialize with params', () => { energy: 1.6408333333333334, }, ]); - }); - - test('wrong "instance-type": initialize with params and call usage in IMPL Format throws error', async () => { - const outputModel = new BoaviztaCloudOutputModel(); - - await expect( - outputModel.configure({ - 'instance-type': 't5.micro', - location: 'USA', - provider: 'aws', - }) - ).rejects.toThrowError(); - - // configure without static params will cause improper configure error await expect( outputModel.execute([ { timestamp: '2021-01-01T00:00:00Z', duration: 15, - 'cpu-util': 34, - }, - { - timestamp: '2021-01-01T00:00:15Z', - duration: 15, - 'cpu-util': 12, - }, - { - timestamp: '2021-01-01T00:00:30Z', - duration: 15, - 'cpu-util': 1, - }, - { - timestamp: '2021-01-01T00:00:45Z', - duration: 15, - 'cpu-util': 78, }, ]) - ).rejects.toThrowError(); + ).rejects.toThrow(); }); test('without "instance-type": initialize with params and call usage in IMPL Format throws error', async () => { @@ -614,18 +600,14 @@ describe('cloud:initialize with params', () => { location: 'USA', provider: 'aws', }) - ).rejects.toStrictEqual( - new InputValidationError( - "BoaviztaOutputModel: Missing 'instance-type' parameter from configuration." - ) - ); + ).rejects.toThrow(); await expect( outputModel.configure({ location: 'USAF', provider: 'aws', 'instance-type': 't2.micro', }) - ).rejects.toThrowError(); + ).rejects.toThrow(); // configure without static params will cause improper configure error await expect( @@ -651,10 +633,6 @@ describe('cloud:initialize with params', () => { 'cpu-util': 78, }, ]) - ).rejects.toStrictEqual( - new InputValidationError( - 'BoaviztaOutputModel: Missing configuration parameters.' - ) - ); + ).rejects.toThrow(); }); }); diff --git a/src/__tests__/unit/lib/watt-time/index.test.ts b/src/__tests__/unit/lib/watt-time/index.test.ts index bd905ec..fb0a9eb 100644 --- a/src/__tests__/unit/lib/watt-time/index.test.ts +++ b/src/__tests__/unit/lib/watt-time/index.test.ts @@ -10,9 +10,50 @@ const mockAxios = axios as jest.Mocked; // Mock out all top level functions, such as get, put, delete and post: // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -mockAxios.get.mockImplementation(url => { +mockAxios.get.mockImplementation((url, data) => { switch (url) { case 'https://api2.watttime.org/v2/login': + if ( + data?.auth?.username === 'test1' && + data?.auth?.password === 'test2' + ) { + return Promise.resolve({ + status: 200, + data: { + token: 'test_token', + }, + }); + } else { + return Promise.resolve({ + status: 401, + data: {}, + }); + } + case 'https://apifail.watttime.org/v2/login': + if ( + data?.auth?.username === 'test1' && + data?.auth?.password === 'test2' + ) { + return Promise.resolve({ + status: 200, + data: { + token: 'test_token', + }, + }); + } else { + 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: { @@ -24,10 +65,210 @@ mockAxios.get.mockImplementation(url => { 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: {}, + }, + }); } }); describe('watt-time:configure test', () => { - test('initialize and test', async () => { + test('initialize without configurations throws error', async () => { + await expect( + new WattTimeGridEmissions().configure(undefined) + ).rejects.toThrow(); + }); + test('initialize with wrong credentials throw error', async () => { + await expect( + new WattTimeGridEmissions().configure({ + username: 'test1', + password: 'test1', + }) + ).rejects.toThrow(); + }); + test('initialize without either username / password throws error', async () => { + await expect( + new WattTimeGridEmissions().configure({ + password: 'test1', + }) + ).rejects.toThrow(); + await expect( + new WattTimeGridEmissions().configure({ + username: 'test1', + }) + ).rejects.toThrow(); + }); + test('initialize with wrong username / password throws error', async () => { + const modelFail = await new WattTimeGridEmissions().configure({ + baseUrl: 'https://apifail.watttime.org/v2', + username: 'test1', + password: 'test2', + }); + await expect( + modelFail.execute([ + { + location: '37.7749,-122.4194', + timestamp: '2021-01-01T00:00:00Z', + duration: 360, + }, + ]) + ).rejects.toThrow(); + }); + test('initialize with undefined environment variables throw error', async () => { + await expect( + new WattTimeGridEmissions().configure({ + username: 'ENV_WATT_USERNAME', + password: 'ENV_WATT_PASSWORD', + }) + ).rejects.toThrow(); + await expect( + new WattTimeGridEmissions().configure({ + token: 'ENV_WATT_TOKEN', + }) + ).rejects.toThrow(); + }); + + test('throws error if watttime api returns wrong data', async () => { + const model = await new WattTimeGridEmissions().configure({ + username: 'test1', + password: 'test2', + }); + + // data is not enough to proceed with computation + await expect( + model.execute([ + { + location: '37.7749,-122.4194', + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + { + location: '37.7749,-122.4194', + timestamp: '2021-01-02T01:00:00Z', + duration: 3600, + }, + ]) + ).rejects.toThrow(); + }); + test('throws error if wrong location is provided', async () => { + const model = await new WattTimeGridEmissions().configure({ + username: 'test1', + password: 'test2', + }); + await expect( + model.execute([ + { + location: '0,-122.4194', + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]) + ).rejects.toThrow(); + await expect( + model.execute([ + { + location: '0,', + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]) + ).rejects.toThrow(); + await expect( + model.execute([ + { + location: '', + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]) + ).rejects.toThrow(); + await expect( + model.execute([ + { + location: 'gsf,ief', + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]) + ).rejects.toThrow(); + }); + test('throws error if no data is returned by API', async () => { + const modelFail2 = await new WattTimeGridEmissions().configure({ + username: 'test1', + password: 'test2', + baseUrl: 'https://apifail2.watttime.org/v2', + }); + await expect( + modelFail2.execute([ + { + location: '37.7749,-122.4194', + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + { + location: '37.7749,-122.4194', + timestamp: '2021-01-02T01:00:00Z', + duration: 3600, + }, + ]) + ).rejects.toThrow(); + }); + test('throws error if unauthorized error occurs during data fetch', async () => { + const modelFail3 = await new WattTimeGridEmissions().configure({ + username: 'test1', + password: 'test2', + baseUrl: 'https://apifail3.watttime.org/v2', + }); + await expect( + modelFail3.execute([ + { + location: '37.7749,-122.4194', + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + { + location: '37.7749,-122.4194', + timestamp: '2021-01-02T01:00:00Z', + duration: 3600, + }, + ]) + ).rejects.toThrow(); + await expect( + modelFail3.execute([ + { + location: '37.7749,-122.4194', + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + { + location: '37.7749,-122.4194', + timestamp: '2021-01-15T01:00:00Z', + duration: 3600, + }, + { + location: '37.7749,-122.4194', + timestamp: '2021-01-02T01:00:00Z', + duration: 3600, + }, + ]) + ).rejects.toThrow(); + }); + + test('proper initialization and test', async () => { const model = await new WattTimeGridEmissions().configure({ username: 'test1', password: 'test2', @@ -97,20 +338,5 @@ describe('watt-time:configure test', () => { 'grid-carbon-intensity': 2193.5995087395318, }, ]); - - await expect( - model.execute([ - { - location: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', - duration: 3600, - }, - { - location: '37.7749,-122.4194', - timestamp: '2021-01-02T01:00:00Z', - duration: 3600, - }, - ]) - ).rejects.toThrowError(); }); }); diff --git a/src/lib/boavizta/index.ts b/src/lib/boavizta/index.ts index 27b8995..e224f9b 100644 --- a/src/lib/boavizta/index.ts +++ b/src/lib/boavizta/index.ts @@ -21,10 +21,8 @@ abstract class BoaviztaOutputModel implements ModelPluginInterface { this.authCredentials = authParams; } - async configure( - staticParams: object | undefined = undefined - ): Promise { - this.sharedParams = await this.captureStaticParams(staticParams ?? {}); + async configure(staticParams: object): Promise { + this.sharedParams = await this.captureStaticParams(staticParams); return this; } @@ -67,7 +65,7 @@ abstract class BoaviztaOutputModel implements ModelPluginInterface { * Calculates the output of the given usage. */ async execute(inputs: ModelParams[]): Promise { - if (Array.isArray(inputs)) { + if (Array.isArray(inputs) && inputs.length > 0) { const results: KeyValuePair[] = []; for (const input of inputs) { @@ -105,20 +103,13 @@ abstract class BoaviztaOutputModel implements ModelPluginInterface { protected formatResponse(data: KeyValuePair): KeyValuePair { let m = 0; let e = 0; - if ('outputs' in data) { + if ('impacts' in data) { // embodied-carbon output is in kgCO2eq, convert to gCO2eq - m = data['outputs']['gwp']['embedded']['value'] * 1000; + m = data['impacts']['gwp']['embedded']['value'] * 1000; // use output is in J , convert to kWh. // 1,000,000 J / 3600 = 277.7777777777778 Wh. // 1 MJ / 3.6 = 0.278 kWh - e = data['outputs']['pe']['use']['value'] / 3.6; - } else if ('gwp' in data && 'pe' in data) { - // embodied-carbon output is in kgCO2eq, convert to gCO2eq - m = data['gwp']['embodied-carbon'] * 1000; - // use output is in J , convert to kWh. - // 1,000,000 J / 3600 = 277.7777777777778 Wh. - // 1 MJ / 3.6 = 0.278 kWh - e = data['pe']['use'] / 3.6; + e = data['impacts']['pe']['use']['value'] / 3.6; } return {'embodied-carbon': m, energy: e}; @@ -130,11 +121,7 @@ abstract class BoaviztaOutputModel implements ModelPluginInterface { protected async calculateUsageForinput( input: ModelParams ): Promise { - if ( - 'timestamp' in input && - 'duration' in input && - this.metricType in input - ) { + if (this.metricType in input) { const usageInput = this.transformToBoaviztaUsage( input['duration'], input[this.metricType] @@ -194,8 +181,12 @@ export class BoaviztaCpuOutputModel } protected async captureStaticParams(staticParams: object): Promise { - if ('verbose' in staticParams) { - this.verbose = (staticParams.verbose as boolean) ?? false; + // if verbose is defined in staticParams, remove it from staticParams and set verbose to the value defined in staticParams + if ( + 'verbose' in staticParams && + (staticParams.verbose === true || staticParams.verbose === false) + ) { + this.verbose = staticParams.verbose; staticParams.verbose = undefined; } @@ -237,7 +228,7 @@ export class BoaviztaCloudOutputModel async validateLocation(staticParamsCast: object): Promise { if ('location' in staticParamsCast) { - const location = (staticParamsCast.location as string) ?? 'USA'; + const location = staticParamsCast.location as string; const countries = await this.supportedLocations(); if (!countries.includes(location)) { @@ -361,13 +352,16 @@ export class BoaviztaCloudOutputModel `https://api.boavizta.org/v1/cloud/instance?verbose=${this.verbose}&duration=${dataCast['usage']['hours_use_time']}`, dataCast ); - return this.formatResponse(response.data); } - protected async captureStaticParams(staticParams: object) { - if ('verbose' in staticParams) { - this.verbose = (staticParams.verbose as boolean) ?? false; + protected async captureStaticParams(staticParams: object): Promise { + if ( + 'verbose' in staticParams && + staticParams.verbose !== undefined && + (staticParams.verbose === true || staticParams.verbose === false) + ) { + this.verbose = staticParams.verbose; staticParams.verbose = undefined; } diff --git a/src/lib/watt-time/index.ts b/src/lib/watt-time/index.ts index 2ecbf1f..9c80cdf 100644 --- a/src/lib/watt-time/index.ts +++ b/src/lib/watt-time/index.ts @@ -9,6 +9,18 @@ import {ModelPluginInterface} from '../../interfaces'; const {AuthorizationError, InputValidationError, APIRequestError} = ERRORS; +interface WattTimeParams { + latitude: number; + longitude: number; + starttime: string; + endtime: dayjs.Dayjs; +} + +interface LatitudeLongitude { + latitude: number; + longitude: number; +} + export class WattTimeGridEmissions implements ModelPluginInterface { token = ''; staticParams: object | undefined; @@ -74,18 +86,6 @@ export class WattTimeGridEmissions implements ModelPluginInterface { } async execute(inputs: ModelParams[]): Promise { - if (inputs === undefined) { - throw new InputValidationError( - this.errorBuilder({message: 'Input data is missing'}) - ); - } - - if (!Array.isArray(inputs)) { - throw new InputValidationError( - this.errorBuilder({message: 'Input data is not an array'}) - ); - } - // validate inputs for location data + timestamp + duration this.validateinputs(inputs); // determine the earliest start and total duration of all input blocks @@ -160,9 +160,9 @@ export class WattTimeGridEmissions implements ModelPluginInterface { return {datapoints, data}; } - private validateinputs(inputs: object[]) { - inputs.forEach((input: KeyValuePair, index) => { - if (!('location' in input)) { + private validateinputs(inputs: ModelParams[]) { + inputs.forEach((input: ModelParams, index) => { + if ('location' in input) { const {latitude, longitude} = this.getLatitudeLongitudeFrominput(input); if (isNaN(latitude) || isNaN(longitude)) { @@ -173,28 +173,11 @@ export class WattTimeGridEmissions implements ModelPluginInterface { ); } } - - if (!('timestamp' in input)) { - throw new InputValidationError( - this.errorBuilder({ - message: `'timestamp' from rom input[${index}] is missing`, - }) - ); - } - - if (!('duration' in input)) { - throw new InputValidationError( - this.errorBuilder({ - message: `'duration' from rom input[${index}] is missing`, - }) - ); - } }); } - private getLatitudeLongitudeFrominput(input: KeyValuePair) { + private getLatitudeLongitudeFrominput(input: ModelParams): LatitudeLongitude { const location = input['location'].split(','); //split location into latitude and longitude - if (location.length !== 2) { throw new InputValidationError( this.errorBuilder({ @@ -226,18 +209,20 @@ export class WattTimeGridEmissions implements ModelPluginInterface { return {latitude, longitude}; } - private determineinputStartEnd(inputs: object[]) { + private determineinputStartEnd(inputs: ModelParams[]): { + startTime: dayjs.Dayjs; + fetchDuration: number; + } { let starttime = dayjs('9999-12-31'); // largest possible start time let endtime = dayjs('1970-01-01'); // smallest possible end time - inputs.forEach((input: KeyValuePair) => { + inputs.forEach((input: ModelParams) => { const duration = input.duration; // if the input timestamp is before the current starttime, set it as the new starttime starttime = dayjs(input.timestamp).isBefore(starttime) ? dayjs(input.timestamp) : starttime; - // if the input timestamp + duration is after the current endtime, set it as the new endtime endtime = dayjs(input.timestamp).add(duration, 'seconds').isAfter(endtime) ? dayjs(input.timestamp).add(duration, 'seconds') @@ -246,6 +231,7 @@ export class WattTimeGridEmissions implements ModelPluginInterface { const fetchDuration = endtime.diff(starttime, 'seconds'); + // WattTime API only supports up to 32 days if (fetchDuration > 32 * 24 * 60 * 60 /** 32 days */) { throw new InputValidationError( this.errorBuilder({ @@ -257,20 +243,12 @@ export class WattTimeGridEmissions implements ModelPluginInterface { return {startTime: starttime, fetchDuration}; } - async fetchData(input: KeyValuePair): Promise { + async fetchData(input: ModelParams): Promise { const duration = input.duration; - // WattTime API only supports up to 32 days - if (duration > 32 * 24 * 60 * 60) { - throw new InputValidationError( - this.errorBuilder({ - message: `WattTime API supports up to 32 days. Duration of ${duration} seconds is too long`, - }) - ); - } const {latitude, longitude} = this.getLatitudeLongitudeFrominput(input); - const params = { + const params: WattTimeParams = { latitude: latitude, longitude: longitude, starttime: dayjs(input.timestamp).format('YYYY-MM-DDTHH:mm:ssZ'), @@ -328,6 +306,10 @@ export class WattTimeGridEmissions implements ModelPluginInterface { await this.authenticate(staticParams); + if ('baseUrl' in staticParams) { + this.baseUrl = staticParams['baseUrl'] as string; + } + return this; } }