diff --git a/package-lock.json b/package-lock.json index 9e2291d..0154c93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,12 @@ "@azure/identity": "^3.4.1", "@cloud-carbon-footprint/aws": "^0.15.0", "@tgwf/co2": "^0.14.1", + "@types/luxon": "^3.4.2", "@types/tgwf__co2": "^0.0.0", "axios": "^1.6.0", - "dayjs": "^1.11.10", "dotenv": "16.3.1", "js-yaml": "^4.1.0", + "luxon": "^3.4.4", "typescript": "^5.1.6", "typescript-cubic-spline": "^1.0.1", "zod": "^3.22.4" @@ -3070,6 +3071,11 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==" + }, "node_modules/@types/minimist": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.4.tgz", @@ -4471,11 +4477,6 @@ "node": ">=8" } }, - "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -9035,6 +9036,14 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", diff --git a/package.json b/package.json index 8c46d7b..b26c3c8 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,12 @@ "@azure/identity": "^3.4.1", "@cloud-carbon-footprint/aws": "^0.15.0", "@tgwf/co2": "^0.14.1", + "@types/luxon": "^3.4.2", "@types/tgwf__co2": "^0.0.0", "axios": "^1.6.0", - "dayjs": "^1.11.10", "dotenv": "16.3.1", "js-yaml": "^4.1.0", + "luxon": "^3.4.4", "typescript": "^5.1.6", "typescript-cubic-spline": "^1.0.1", "zod": "^3.22.4" @@ -77,4 +78,4 @@ "prepublish": "npm run build", "test": "jest --verbose" } -} \ No newline at end of file +} diff --git a/src/__mocks__/watt-time/data.json b/src/__mocks__/watt-time/data.json deleted file mode 100644 index 1522e68..0000000 --- a/src/__mocks__/watt-time/data.json +++ /dev/null @@ -1,119 +0,0 @@ -[ - { - "point_time": "2021-01-01T01:00:00.000Z", - "value": 942, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:55:00.000Z", - "value": 931, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:50:00.000Z", - "value": 930, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:45:00.000Z", - "value": 930, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:40:00.000Z", - "value": 927, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:35:00.000Z", - "value": 934, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:30:00.000Z", - "value": 933, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:25:00.000Z", - "value": 934, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:20:00.000Z", - "value": 935, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:15:00.000Z", - "value": 986, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:10:00.000Z", - "value": 989, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:05:00.000Z", - "value": 993, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - }, - { - "point_time": "2021-01-01T00:00:00.000Z", - "value": 997, - "frequency": 300, - "market": "RTM", - "ba": "CAISO_NORTH", - "datatype": "MOER", - "version": "3.2" - } -] diff --git a/src/__mocks__/watt-time/index.ts b/src/__mocks__/watt-time/index.ts index 1209b9e..b10ed7a 100644 --- a/src/__mocks__/watt-time/index.ts +++ b/src/__mocks__/watt-time/index.ts @@ -1,7 +1,12 @@ -import * as DATA from './data.json'; import * as REGION_DATA from './region-data.json'; export function getMockResponse(url: string) { + const DATA = { + region: 'CAISO_NORTH', + region_full_name: 'California ISO Northern', + signal_type: 'co2_moer', + }; + switch (url) { case 'https://api.watttime.org/login': if ( @@ -18,6 +23,19 @@ export function getMockResponse(url: string) { status: 401, data: {}, }); + } else if ( + process.env.WATT_TIME_USERNAME === 'WRONG_USERNAME' && + process.env.WATT_TIME_PASSWORD === 'WRONG_PASSWORD' + ) { + return Promise.reject({ + status: 403, + message: 'Unothorized error', + }); + } else if ( + process.env.WATT_TIME_USERNAME === 'WRONG_USERNAME1' && + process.env.WATT_TIME_PASSWORD === 'WRONG_PASSWORD1' + ) { + return Promise.reject('Unothorized error'); } return Promise.resolve({ @@ -27,7 +45,7 @@ export function getMockResponse(url: string) { }, }); - case 'https://api2.watttime.org/v2/data': + case 'https://api.watttime.org/v3/region-from-loc': if ( process.env.WATT_TIME_USERNAME === 'invalidData1' && process.env.WATT_TIME_PASSWORD === 'invalidData2' @@ -69,8 +87,10 @@ export function getMockResponse(url: string) { case 'https://api.watttime.org/v3/forecast/historical': if ( - process.env.WATT_TIME_USERNAME === 'invalidRegionWT' && - process.env.WATT_TIME_PASSWORD === 'invalidRegionWT' + (process.env.WATT_TIME_USERNAME === 'invalidRegionWT' && + process.env.WATT_TIME_PASSWORD === 'invalidRegionWT') || + (process.env.WATT_TIME_USERNAME === 'invalidData' && + process.env.WATT_TIME_PASSWORD === 'invalidData') ) { return Promise.reject({ status: 400, @@ -92,7 +112,56 @@ export function getMockResponse(url: string) { status: 200, data: {}, }); + } else if ( + process.env.WATT_TIME_USERNAME === 'SORT_DATA' && + process.env.WATT_TIME_PASSWORD === 'SORT_DATA' + ) { + return Promise.resolve({ + status: 200, + data: { + data: [ + { + generated_at: '2024-03-05T00: 00: 00+00: 00', + forecast: [ + { + point_time: '2024-03-05T00:05:00+00:00', + value: 779.8, + }, + { + point_time: '2024-03-05T00:00:00+00:00', + value: 779.8, + }, + ], + }, + ], + }, + }); + } else if ( + process.env.WATT_TIME_USERNAME === 'INVALID_DATA' && + process.env.WATT_TIME_PASSWORD === 'INVALID_DATA' + ) { + return Promise.resolve({ + status: 200, + data: { + data: [ + { + generated_at: '2024-03-05T00: 00: 00+00: 00', + forecast: [ + { + point_time: '2024-03-05T00:00:00+00:00', + value: 'nn', + }, + { + point_time: '2024-03-05T00:05:00+00:00', + value: 779.8, + }, + ], + }, + ], + }, + }); } + return Promise.resolve({ data: REGION_DATA, status: 200, diff --git a/src/__tests__/unit/lib/watt-time/index.test.ts b/src/__tests__/unit/lib/watt-time/index.test.ts index 9bff33a..ab22b2f 100644 --- a/src/__tests__/unit/lib/watt-time/index.test.ts +++ b/src/__tests__/unit/lib/watt-time/index.test.ts @@ -15,13 +15,15 @@ mockAxios.get.mockImplementation(getMockResponse); describe('lib/watt-time: ', () => { describe('WattTimeGridEmissions: ', () => { + const originalEnv = process.env; + beforeEach(() => { jest.clearAllMocks(); }); describe('init WattTimeGridEmissions: ', () => { it('initalizes object with properties.', async () => { - const output = WattTimeGridEmissions({}); + const output = WattTimeGridEmissions(); expect(output).toHaveProperty('metadata'); expect(output).toHaveProperty('execute'); @@ -37,7 +39,7 @@ describe('lib/watt-time: ', () => { const result = await output.execute([ { geolocation: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 1200, }, ]); @@ -45,9 +47,9 @@ describe('lib/watt-time: ', () => { expect(result).toStrictEqual([ { geolocation: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 1200, - 'grid/carbon-intensity': 2185.332173907599, + 'grid/carbon-intensity': 1718.9993738210367, }, ]); }); @@ -59,7 +61,7 @@ describe('lib/watt-time: ', () => { const output = WattTimeGridEmissions(); const result = await output.execute([ { - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 1200, geolocation: '37.7749,-122.4194', 'cloud/region-geolocation': '48.8567,2.3522', @@ -70,11 +72,11 @@ describe('lib/watt-time: ', () => { expect(result).toStrictEqual([ { - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 1200, geolocation: '48.8567,2.3522', 'cloud/region-geolocation': '48.8567,2.3522', - 'grid/carbon-intensity': 2185.332173907599, + 'grid/carbon-intensity': 1718.9993738210367, }, ]); }); @@ -110,19 +112,18 @@ describe('lib/watt-time: ', () => { ]); }); - it('returns a result when `grid/carbon-intensity` set to 0 when there is no data from API.', async () => { - process.env.WATT_TIME_USERNAME = 'region-wt'; - process.env.WATT_TIME_PASSWORD = 'region-wt'; + it('returns a result when the data value is not a number from API.', async () => { + process.env.WATT_TIME_USERNAME = 'INVALID_DATA'; + process.env.WATT_TIME_PASSWORD = 'INVALID_DATA'; + + expect.assertions(2); const output = WattTimeGridEmissions(); const result = await output.execute([ { - timestamp: '2021-01-01T00:00:00Z', - duration: 5, - geolocation: '37.7749,-122.4194', - 'cloud/region-geolocation': '48.8567,2.3522', + timestamp: '2024-03-05 00:00:00', + duration: 60, 'cloud/region-wt-id': 'FR', - 'signal-type': 'co2_moer', }, ]); @@ -130,13 +131,10 @@ describe('lib/watt-time: ', () => { expect(result).toStrictEqual([ { - timestamp: '2021-01-01T00:00:00Z', - duration: 5, - geolocation: '48.8567,2.3522', - 'cloud/region-geolocation': '48.8567,2.3522', + timestamp: '2024-03-05 00:00:00', + duration: 60, 'cloud/region-wt-id': 'FR', 'grid/carbon-intensity': 0, - 'signal-type': 'co2_moer', }, ]); }); @@ -150,7 +148,7 @@ describe('lib/watt-time: ', () => { const output = WattTimeGridEmissions(); const result = await output.execute([ { - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 5, geolocation: '37.7749,-122.4194', 'cloud/region-geolocation': '48.8567,2.3522', @@ -162,16 +160,224 @@ describe('lib/watt-time: ', () => { expect(result).toStrictEqual([ { - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 5, geolocation: '48.8567,2.3522', 'cloud/region-geolocation': '48.8567,2.3522', 'cloud/region-wt-id': 'FR', - 'grid/carbon-intensity': 0, + 'grid/carbon-intensity': 1719.1647205176753, + }, + ]); + }); + + it('returns a result when `cloud/region-wt-id` differs in inputs but duration is greater then 5 mins.', async () => { + process.env.WATT_TIME_USERNAME = 'REGION_WT_ID'; + process.env.WATT_TIME_PASSWORD = 'REGION_WT_ID'; + + expect.assertions(2); + + const output = WattTimeGridEmissions(); + const result = await output.execute([ + { + timestamp: '2024-03-05T00:00:00+00:00', + duration: 15 * 60, + 'cloud/region-wt-id': 'FR', + }, + { + timestamp: '2024-03-05T00:05:00+00:00', + duration: 5, + 'cloud/region-wt-id': 'CAISO_NORTH', + }, + ]); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '2024-03-05T00:00:00+00:00', + duration: 15 * 60, + 'cloud/region-wt-id': 'FR', + 'grid/carbon-intensity': 1719.091233096947, + }, + { + timestamp: '2024-03-05T00:05:00+00:00', + duration: 5, + 'cloud/region-wt-id': 'CAISO_NORTH', + 'grid/carbon-intensity': 1719.1647205176753, + }, + ]); + }); + + it('returns a result when `cloud/region-wt-id` is the same in inputs but duration is lower then 5 mins.', async () => { + process.env.WATT_TIME_USERNAME = 'REGION_WT_ID'; + process.env.WATT_TIME_PASSWORD = 'REGION_WT_ID'; + + expect.assertions(2); + + const output = WattTimeGridEmissions(); + const result = await output.execute([ + { + timestamp: '2024-03-05T00:00:00+00:00', + duration: 3 * 60, + 'cloud/region-wt-id': 'FR', + }, + { + timestamp: '2024-03-05T00:00:03+00:00', + duration: 5, + 'cloud/region-wt-id': 'FR', + }, + ]); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '2024-03-05T00:00:00+00:00', + duration: 3 * 60, + 'cloud/region-wt-id': 'FR', + 'grid/carbon-intensity': 1719.1647205176753, + }, + { + timestamp: '2024-03-05T00:00:03+00:00', + duration: 5, + 'cloud/region-wt-id': 'FR', + 'grid/carbon-intensity': 1719.1647205176753, + }, + ]); + }); + + it('returns a result when the API data is not sorted.', async () => { + process.env.WATT_TIME_USERNAME = 'SORT_DATA'; + process.env.WATT_TIME_PASSWORD = 'SORT_DATA'; + + expect.assertions(2); + + const output = WattTimeGridEmissions(); + const result = await output.execute([ + { + timestamp: '2024-03-05T00:00:00+00:00', + duration: 3 * 60, + 'cloud/region-wt-id': 'FR', + }, + ]); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '2024-03-05T00:00:00+00:00', + duration: 3 * 60, + 'cloud/region-wt-id': 'FR', + 'grid/carbon-intensity': 1719.1647205176753, + }, + ]); + }); + + it('returns a result when `timestamp` date format is not ISO date.', async () => { + process.env.WATT_TIME_USERNAME = 'REGION_WT_ID'; + process.env.WATT_TIME_PASSWORD = 'REGION_WT_ID'; + + expect.assertions(2); + + const output = WattTimeGridEmissions(); + const result = await output.execute([ + { + timestamp: '2024-03-05 00:00:00', + duration: 60, + 'cloud/region-wt-id': 'FR', + }, + ]); + + expect.assertions(1); + + expect(result).toStrictEqual([ + { + timestamp: '2024-03-05 00:00:00', + duration: 60, + 'cloud/region-wt-id': 'FR', + 'grid/carbon-intensity': 1719.1647205176753, }, ]); }); + it('throws an error when the `timestamp` has wrong data format.', async () => { + expect.assertions(2); + + const output = WattTimeGridEmissions(); + expect.assertions(2); + + try { + await output.execute([ + { + timestamp: '2024-03-050:00:00', + duration: 60, + 'cloud/region-wt-id': 'FR', + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(InputValidationError); + expect(error).toEqual( + new InputValidationError( + 'WattTimeGridEmissions: Timestamp is not valid date format.' + ) + ); + } + }); + + it('throws an error when the credentials are wrong.', async () => { + process.env.WATT_TIME_USERNAME = 'WRONG_USERNAME'; + process.env.WATT_TIME_PASSWORD = 'WRONG_PASSWORD'; + + expect.assertions(2); + + const output = WattTimeGridEmissions(); + expect.assertions(2); + + try { + await output.execute([ + { + timestamp: '2024-03-05 00:00:00', + duration: 60, + 'cloud/region-wt-id': 'FR', + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(APIRequestError); + expect(error).toEqual( + new APIRequestError( + 'WattTimeAPI: Authorization error from WattTime API. "Unothorized error".' + ) + ); + } + }); + + it('throws an error when the credentials are wrong and the message is not provided in the error object.', async () => { + process.env.WATT_TIME_USERNAME = 'WRONG_USERNAME1'; + process.env.WATT_TIME_PASSWORD = 'WRONG_PASSWORD1'; + + expect.assertions(2); + + const output = WattTimeGridEmissions(); + expect.assertions(2); + + try { + await output.execute([ + { + timestamp: '2024-03-05 00:00:00', + duration: 60, + 'cloud/region-wt-id': 'FR', + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(APIRequestError); + expect(error).toEqual( + new APIRequestError( + 'WattTimeAPI: Authorization error from WattTime API. "Unothorized error".' + ) + ); + } + }); + it('throws an error when API gives an error.', async () => { const errorMessage = 'WattTimeAPI: Error fetching data from WattTime API. {"status":400,"error":{"message":"error"}}.'; @@ -185,7 +391,7 @@ describe('lib/watt-time: ', () => { await output.execute([ { geolocation: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 360, }, ]); @@ -273,7 +479,8 @@ describe('lib/watt-time: ', () => { }); it('throws an error when the response of the API is not valid data.', async () => { - const errorMessage = 'WattTimeAPI: Invalid response from WattTime API.'; + const errorMessage = + 'WattTimeAPI: Error fetching data from WattTime API. {"status":400,"error":{"message":"error"}}.'; process.env.WATT_TIME_USERNAME = 'invalidData'; process.env.WATT_TIME_PASSWORD = 'invalidData'; @@ -284,7 +491,7 @@ describe('lib/watt-time: ', () => { await output.execute([ { geolocation: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 360, }, ]); @@ -294,11 +501,10 @@ describe('lib/watt-time: ', () => { } }); - it('throws an error when the response of the API is not valid data.', async () => { - const errorMessage = 'WattTimeAPI: Invalid response from WattTime API.'; - process.env.WATT_TIME_USERNAME = 'invalidData'; - process.env.WATT_TIME_PASSWORD = 'invalidData'; - + it('throws an error when `token` or `username` and/or `password` are not provided.', async () => { + const errorMessage = + 'WattTimeAPI: Invalid credentials provided. Either `token` or `username` and `password` should be provided.'; + process.env = {}; expect.assertions(2); try { @@ -306,13 +512,13 @@ describe('lib/watt-time: ', () => { await output.execute([ { geolocation: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 360, }, ]); } catch (error) { - expect(error).toBeInstanceOf(APIRequestError); - expect(error).toEqual(new APIRequestError(errorMessage)); + expect(error).toBeInstanceOf(AuthorizationError); + expect(error).toEqual(new AuthorizationError(errorMessage)); } }); @@ -328,7 +534,7 @@ describe('lib/watt-time: ', () => { const output = WattTimeGridEmissions(); await output.execute([ { - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 5, geolocation: '37.7749,-122.4194', 'cloud/region-geolocation': '48.8567,2.3522', @@ -352,7 +558,7 @@ describe('lib/watt-time: ', () => { const output = WattTimeGridEmissions(); await output.execute([ { - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 5, geolocation: '37.7749,-122.4194', 'cloud/region-geolocation': '48.8567,2.3522', @@ -378,7 +584,7 @@ describe('lib/watt-time: ', () => { await output.execute([ { geolocation: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 360, }, ]); @@ -398,7 +604,7 @@ describe('lib/watt-time: ', () => { await output.execute([ { geolocation: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 360, }, ]); @@ -419,7 +625,7 @@ describe('lib/watt-time: ', () => { await output.execute([ { geolocation: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 360, }, ]); @@ -442,7 +648,7 @@ describe('lib/watt-time: ', () => { await output.execute([ { geolocation: '0,-122.4194', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 3600, }, ]); @@ -459,7 +665,7 @@ describe('lib/watt-time: ', () => { await output.execute([ { geolocation: '0', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 3600, }, ]); @@ -476,7 +682,7 @@ describe('lib/watt-time: ', () => { await output.execute([ { geolocation: '', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 3600, }, ]); @@ -499,7 +705,7 @@ describe('lib/watt-time: ', () => { await output.execute([ { geolocation: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 3600, }, { @@ -527,7 +733,7 @@ describe('lib/watt-time: ', () => { await output.execute([ { geolocation: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', + timestamp: '2024-03-05T00:00:00+00:00', duration: 3600, }, { @@ -541,34 +747,7 @@ describe('lib/watt-time: ', () => { expect(error).toEqual(new APIRequestError(errorMessage)); } }); - - it('throws an error when span is more than 32 days.', async () => { - const errorMessage = - 'WattTimeGridEmissions: WattTime API supports up to 32 days. Duration of 31537200 seconds is too long.'; - process.env.WATT_TIME_USERNAME = 'test1'; - process.env.WATT_TIME_PASSWORD = 'test2'; - - const output = WattTimeGridEmissions(); - expect.assertions(2); - - try { - await output.execute([ - { - geolocation: '37.7749,-122.4194', - timestamp: '2021-01-01T00:00:00Z', - duration: 1200, - }, - { - geolocation: '37.7749,-122.4194', - timestamp: '2022-01-01T00:00:00Z', - duration: 1200, - }, - ]); - } catch (error) { - expect(error).toBeInstanceOf(InputValidationError); - expect(error).toEqual(new InputValidationError(errorMessage)); - } - }); }); + process.env = originalEnv; }); }); diff --git a/src/lib/watt-time/README.md b/src/lib/watt-time/README.md index dd383f8..ad431de 100644 --- a/src/lib/watt-time/README.md +++ b/src/lib/watt-time/README.md @@ -1,38 +1,25 @@ -# WattTime Grid Emissions plugin +# WattTime Grid Emissions Plugin -> [!NOTE] > `Watt-time` is a community plugin, not part of the IF standard library. This means the IF core team are not closely monitoring these plugins to keep them up to date. You should do your own research before implementing them! +> [!NOTE] > +> `Watt-time` is a community plugin and not a part of the IF standard library. As a result, the IF core team does not closely monitor these plugins for updates. It is recommended to conduct your own research before implementing them. ## Introduction WattTime technology—based on real-time grid data, cutting-edge algorithms, and machine learning—provides first-of-its-kind insight into your local electricity grid’s marginal emissions rate. [Read More...](https://www.watttime.org/api-documentation/#introduction) -## Scope +## Overview -WattTime plugin provides a way to calculate emissions for a given time in a specific geolocation. - -The plugin is based on the WattTime API. The plugin uses the following inputs: - -- `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 - -Limitations: - -- Set of inputs are to be within 32 days of each other. -- Emissions are aggregated for every 5 minutes regardless of the granularity of the inputs. +The `WattTimeGridEmissions` plugin is designed to compute the average carbon emissions of a power grid over a specified duration. It leverages data from the WattTime API to furnish carbon intensity information for precise locations and timeframes. This plugin proves beneficial for applications requiring carbon footprint monitoring or optimization, such as energy management systems or environmental impact assessments. The plugin supports only v3 version of the WattTime API. The API returns data in `lbs/MWh`, which the plugin converts to `Kg/MWh` (g/KWh) by dividing by `0.453592`. ### Authentication WattTime API requires activation of subscription before usage. Please refer to the [WattTime website](https://watttime.org/docs-dev/data-plans/) for more information. -Create a `.env` file in the IF project root directory. This is where you can store your WattTime authentication details. Your `.env` file should look as follows: +## Prerequisites + +Before utilizing this plugin, ensure the following prerequisites are fulfilled: + +1. **Environment Variables**: The plugin requires environment variables `WATT_TIME_USERNAME` and `WATT_TIME_PASSWORD` to be set. These credentials are utilized for authentication with the WattTime API. **Required Parameters:** @@ -47,81 +34,135 @@ WATT_TIME_PASSWORD: WATT_TIME_TOKEN: ``` -### Plugin global config +2. **Dependencies**: Confirm the installation of all required dependencies, including `luxon` and `zod`. These dependencies are imperative for date-time manipulation and input validation, respectively. -- `base-url`: The URL for the WattTime API endpoint. +## Usage -### Inputs +To employ the `WattTimeGridEmissions` plugin, adhere to these steps: -**Required Parameters:** +1. **Initialize Plugin**: Import the `WattTimeGridEmissions` function and initialize it with optional global configuration parameters. + +2. **Execute Plugin**: Invoke the `execute` method of the initialized plugin instance with an array of input parameters. Each input parameter should include a `timestamp`, `duration`, and either `geolocation`, `cloud/region-wt-id`, or `cloud/region-geolocation` information. + +3. **Result**: The plugin will return an array of plugin parameters enriched with the calculated average carbon intensity (`grid/carbon-intensity`) for each input. + +## Input Parameters + +The plugin expects the following input parameters: + +- `timestamp`: A string representing the start time of the query period. +- `duration`: A number indicating the duration of the query period in seconds. +- `geolocation`: A string representing the latitude and longitude of the location in the format `latitude,longitude`. Alternatively, this information can be provided through `cloud/region-geolocation` or `cloud/region-wt-id` parameters. +- `cloud/region-geolocation`: Similar to `geolocation`, with calculations performed by the `cloud-metadata` plugin. +- `cloud/region-wt-id`: A string representing the region abbreviation associated with the location (e.g., 'CAISO_NORTH'). +- `signal-type`: A string representing the signal type of the selected region (optional) (e.g., 'co2_moer'). + +## Output + +The plugin enriches each input parameter with the average carbon intensity (`grid/carbon-intensity`) calculated over the specified duration. + +## Error Handling + +The plugin conducts input validation using the `zod` library and may throw errors if the provided parameters are invalid or if there are authentication or data retrieval issues with the WattTime API. + +## Plugin Algorithm + +1. **Initialization**: Authenticate with the WattTime API using the provided credentials. If the `token` is not provided in the environment variables, it uses `username` and `password`, otherwise, it throws an error. To authenticate users, the plugin utilizes the `https://api.watttime.org/login` URL. + +2. **Execution**: + + - Iterate through each input. + + - If `cloud/region-wt-id is provided`, the plugin sets it to `region`, renames `signal-type` to `signal_type`, and sends a request to `https://api.watttime.org/v3/forecast/historical` endpoint with calculated `start` and `end` time as well. If the `signal_type` is not provided, the plugin requests `https://api.watttime.org/v3/my-access` to obtain access to the account and takes the first signal type from there. + - If `geolocation` is provided, the plugin converts it to a `latitude` and `longitude` pair, renames `signal-type` to `signal_type`, and sends a request to `https://api.watttime.org/v3/region-from-loc` to retrieve the `region`. Then the `https://api.watttime.org/v3/forecast/historical` endpoint is called with `region`, `signal_type`, calculated `start` and `end` time from `timestamp` and `duration`. + + - Validate input parameters. If `cloud/region-geolocation` is provided, the `geolocation` is overridden. If `cloud/region-wt-id` is provided, it takes precedence over the `geolocation`. -- `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') + - Retrieve WattTime data for the specified duration. The WattTime API adds aggregated emissions for every 5 minutes. To address this limitation, the plugin sets the previous emission's value if the specified `duration` is less than 5 minutes. -Either `geolocation`,`cloud/region-wt-id` or `cloud/region-geolocation` should be provided. + - Calculate average emissions based on retrieved data. The WattTime API returns full data for the entire duration; the plugin checks if the data's period time is within the specified input range and collects data in `kgMWh`. -### Typescript Usage +3. **Output**: Return results with the average grid emissions for each input. -```typescript -// environment variable configuration -// export WATT_TIME_USERNAME=test1 -// export WATT_TIME_PASSWORD=test2 -// use environment variables to configure the plugin -const output = WattTimeGridEmissions(); -const result = await output.execute([ +### TypeScript Usage + +```ts +// Initialize the plugin +const plugin = WattTimeGridEmissions(); + +// Execute the plugin with input parameters +const inputs = [ { - timestamp: '2021-01-01T00:00:00Z', - geolocation: '43.22,-80.22', - duration: 3600, + timestamp: '2024-03-26T12:00:000Z', + duration: 3600, // 1 hour + geolocation: '36.7783,-119.417931', // San Francisco, CA }, -]); + // Add more input parameters as needed +]; +const result = await plugin.execute(inputs); + +console.log(result); ``` ### Manifest Usage -#### Input for manifest +#### Input ```yaml -inputs: - - timestamp: 2021-01-01T00:00:00Z - geolocation: '43.22,-80.22' - duration: 3600 +name: watt-time +description: simple demo invoking watt-time +tags: +initialize: + plugins: + watt-time: + method: WattTimeGridEmissions + path: '@grnsft/if-unofficial-plugins' + outputs: + - yaml +tree: + children: + child: + pipeline: + - watt-time + inputs: + - timestamp: '2024-03-05T00:00:00.000Z' + duration: 3600 + geolocation: 36.7783,-119.417931 ``` -## Example manifest +#### Output ```yaml name: watt-time description: simple demo invoking watt-time -tags: +tags: null initialize: plugins: watt-time: - method: WattTimeGridEmissions path: '@grnsft/if-unofficial-plugins' + method: WattTimeGridEmissions + outputs: + - yaml tree: children: child: pipeline: - watt-time inputs: - - timestamp: 2023-07-06T00:00 + - timestamp: '2024-03-05T00:00:00.000Z' duration: 3600 - geolocation: 37.7749,-122.4194 + geolocation: 36.7783,-119.417931 + outputs: + - timestamp: '2024-03-05T00:00:00.000Z' + duration: 3600 + geolocation: 36.7783,-119.417931 + grid/carbon-intensity: 287.7032521512652 ``` -You can run this by passing it to `ie`. Run impact using the following command run from the project root: +You can execute this by passing it to `ie`. Run the impact using the following command from the project root: ```sh npm i -g @grnsft/if npm i -g @grnsft/if-unofficial-plugins ie --manifest ./examples/manifests/test/watt-time.yml --output ./examples/outputs/watt-time.yml ``` - -## Position and effects in the manifest: - -- 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. diff --git a/src/lib/watt-time/index.ts b/src/lib/watt-time/index.ts index f7ed2f5..ff6e663 100644 --- a/src/lib/watt-time/index.ts +++ b/src/lib/watt-time/index.ts @@ -1,26 +1,20 @@ -import * as dayjs from 'dayjs'; -import * as utc from 'dayjs/plugin/utc'; -import * as timezone from 'dayjs/plugin/timezone'; +import {Settings, DateTime, Duration} from 'luxon'; import {z} from 'zod'; import {ERRORS} from '../../util/errors'; import {buildErrorMessage} from '../../util/helpers'; -import {ConfigParams, KeyValuePair, PluginParams} from '../../types/common'; +import {KeyValuePair, PluginParams} from '../../types/common'; import {PluginInterface} from '../../interfaces'; import {validate} from '../../util/validations'; import {WattTimeParams, WattTimeRegionParams} from './types'; import {WattTimeAPI} from './watt-time-api'; -dayjs.extend(utc); -dayjs.extend(timezone); - const {InputValidationError} = ERRORS; +Settings.defaultZone = 'utc'; -export const WattTimeGridEmissions = ( - globalConfig?: ConfigParams -): PluginInterface => { +export const WattTimeGridEmissions = (): PluginInterface => { const metadata = {kind: 'execute'}; const wattTimeAPI = WattTimeAPI(); const errorBuilder = buildErrorMessage(WattTimeGridEmissions.name); @@ -29,8 +23,6 @@ export const WattTimeGridEmissions = ( * Initialize authentication with global config. */ const initializeAuthentication = async () => { - validateConfig(); - await wattTimeAPI.authenticate(); }; @@ -39,13 +31,39 @@ export const WattTimeGridEmissions = ( */ const execute = async (inputs: PluginParams[]) => { await initializeAuthentication(); + const result = []; + let lastValidTimestamp = inputs[0] && inputs[0].timestamp; + const executedInputData = { + averageEmission: 0, + locale: '', + }; - const wattTimeData = await getWattTimeData(inputs); - - return inputs.map(input => { + for await (const input of inputs) { const safeInput = Object.assign({}, input, validateInput(input)); - const inputStart = dayjs(safeInput.timestamp); - const inputEnd = inputStart.add(safeInput.duration, 'seconds'); + const validTimestamp = validateAndFormatTimestamp(safeInput.timestamp); + const timestamp = validateAndFormatTimestamp(lastValidTimestamp); + const locale = safeInput['cloud/region-wt-id'] || safeInput.geolocation; + + if ( + executedInputData.locale === locale && + safeInput.timestamp !== lastValidTimestamp && + validTimestamp.diff(timestamp, 'seconds').seconds < 300 + ) { + result.push({ + ...input, + 'grid/carbon-intensity': executedInputData.averageEmission, + }); + + continue; + } + + lastValidTimestamp = safeInput.timestamp; + + const wattTimeData = await getWattTimeData(safeInput); + const inputStart = validateAndFormatTimestamp(lastValidTimestamp); + const inputEnd = getEndTime(inputStart, safeInput.duration); + executedInputData.locale = (safeInput['cloud/region-wt-id'] || + safeInput.geolocation)!; const data = getWattTimeDataForDuration( wattTimeData, @@ -54,13 +72,15 @@ export const WattTimeGridEmissions = ( ); const totalEmission = data.reduce((a: number, b: number) => a + b, 0); - const result = totalEmission / data.length; + executedInputData.averageEmission = totalEmission / data.length; - return { + result.push({ ...input, - 'grid/carbon-intensity': result || 0, - }; - }); + 'grid/carbon-intensity': executedInputData.averageEmission || 0, + }); + } + + return result; }; /** @@ -69,7 +89,14 @@ export const WattTimeGridEmissions = ( const validateInput = (input: PluginParams) => { const schema = z .object({ - duration: z.number(), + duration: z.number().refine(value => { + if (value < 300) { + console.warn( + 'WARN (Watt-TIME): your timestamps are spaced less than 300s apart. The minimum resolution of the Watt-time API is 300s. To account for this, we make API requests every 300s and interpolate the values in between. To use real Watt-time data only, change your time resolution to >= 300 seconds.' + ); + } + return value; + }), timestamp: z.string(), geolocation: z .string() @@ -113,23 +140,17 @@ export const WattTimeGridEmissions = ( */ const getWattTimeDataForDuration = ( wattTimeData: KeyValuePair[], - inputStart: dayjs.Dayjs, - inputEnd: dayjs.Dayjs + inputStart: DateTime, + inputEnd: DateTime ) => { const kgMWh = 0.45359237; - const formatedInputStart = dayjs.tz(inputStart, 'UTC').format(); - const formatedInputEnd = dayjs.tz(inputEnd, 'UTC').format(); return wattTimeData.reduce((accumulator, data) => { - /* WattTime API returns full data for the entire duration. - * if the data point is before the input start, ignore it. - * if the data point is after the input end, ignore it. - * if the data point is exactly the same as the input end, ignore it - */ + const pointTimeInSeconds = DateTime.fromISO(data.point_time).toSeconds(); + if ( - !dayjs(data.point_time).isBefore(formatedInputStart) && - !dayjs(data.point_time).isAfter(formatedInputEnd) && - dayjs(data.point_time).format() !== dayjs(formatedInputEnd).format() + pointTimeInSeconds >= inputStart.toSeconds() && + pointTimeInSeconds < inputEnd.toSeconds() ) { accumulator.push(data.value / kgMWh); } @@ -154,109 +175,64 @@ export const WattTimeGridEmissions = ( }; /** - * Retrieves data from the WattTime API based on the provided inputs. + * Retrieves data from the WattTime API based on the provided input. * 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: PluginParams[]) => { - const {startTime, fetchDuration} = calculateStartDurationTime(inputs); + const getWattTimeData = async (input: PluginParams) => { + const {timestamp: startTime, duration} = input; + const formatedStartTime = validateAndFormatTimestamp(startTime); + const formatedEndTime = getEndTime(formatedStartTime, duration); - const formatedStartTime = dayjs.tz(startTime, 'UTC').format(); - const formatEndTime = dayjs - .tz(startTime, 'UTC') - .add(fetchDuration, 'seconds') - .format(); - - if (inputs[0]['cloud/region-wt-id']) { + if (input['cloud/region-wt-id']) { const params: WattTimeRegionParams = { - start: formatedStartTime, - end: formatEndTime, - region: inputs[0]['cloud/region-wt-id'], - signal_type: inputs[0]['signal-type'], + start: formatedStartTime.toString(), + end: formatedEndTime.toString(), + region: input['cloud/region-wt-id'], + signal_type: input['signal-type'], }; return await wattTimeAPI.fetchDataWithRegion(params); } - const {latitude, longitude} = parseLocation(inputs[0].geolocation); + const {latitude, longitude} = parseLocation(input.geolocation); const params: WattTimeParams = { latitude, longitude, - starttime: formatedStartTime, - endtime: formatEndTime, + starttime: formatedStartTime.toString(), + endtime: formatedEndTime.toString(), + signal_type: input['signal-type'], }; return await wattTimeAPI.fetchAndSortData(params); }; /** - * Calculates the start time and fetch duration based on the provided inputs. - * Iterates through the inputs to find the earliest start time and latest end time. - * Calculates the fetch duration based on the time range. - * Throws an InputValidationError if the fetch duration exceeds the maximum supported by the WattTime API. - * + * Validates timestamp format. */ - const calculateStartDurationTime = ( - inputs: PluginParams[] - ): { - startTime: dayjs.Dayjs; - fetchDuration: number; - } => { - const {startTime, endtime} = inputs.reduce( - (acc, input) => { - const safeInput = validateInput(input); - const {duration, timestamp} = safeInput; - const dayjsTimestamp = dayjs.tz(timestamp, 'UTC'); - const startTime = dayjsTimestamp.isBefore(acc.startTime) - ? dayjsTimestamp - : acc.startTime; - const durationInSeconds = dayjsTimestamp.add(duration, 'seconds'); - const endTime = durationInSeconds.isAfter(acc.endtime) - ? durationInSeconds - : acc.endtime; - - return {startTime: startTime, endtime: endTime}; - }, - {startTime: inputs[0].timestamp, endtime: inputs[0].timestamp} - ); - - const fetchDuration = endtime.diff(startTime, 'seconds'); - - // WattTime API only supports up to 32 days - if (fetchDuration > 32 * 24 * 60 * 60) { + const validateAndFormatTimestamp = (timestamp: string) => { + const isoDateTime = DateTime.fromISO(timestamp); + const sqlDateTime = DateTime.fromSQL(timestamp); + + if (isoDateTime.isValid) { + return isoDateTime; + } else if (sqlDateTime.isValid) { + return sqlDateTime; + } else { throw new InputValidationError( errorBuilder({ - message: `WattTime API supports up to 32 days. Duration of ${fetchDuration} seconds is too long`, + message: 'Timestamp is not valid date format', }) ); } - - return {startTime: startTime, fetchDuration}; }; /** - * Validates static parameters. + * Calculates end time with given start time and duration. */ - const validateConfig = () => { - const WATT_TIME_USERNAME = process.env.WATT_TIME_USERNAME; - const WATT_TIME_PASSWORD = process.env.WATT_TIME_PASSWORD; - - const schema = z.object({ - WATT_TIME_USERNAME: z.string({ - required_error: 'not provided in .env file of `IF` root directory', - }), - WATT_TIME_PASSWORD: z.string().min(1, { - message: 'not provided in .env file of `IF` root directory', - }), - }); - - return validate>(schema, { - ...(globalConfig || {}), - WATT_TIME_USERNAME, - WATT_TIME_PASSWORD, - }); - }; + const getEndTime = (startTime: DateTime, duration: number) => + startTime.plus(Duration.fromObject({seconds: duration})); return { metadata, diff --git a/src/lib/watt-time/types.ts b/src/lib/watt-time/types.ts index bf524ac..705643d 100644 --- a/src/lib/watt-time/types.ts +++ b/src/lib/watt-time/types.ts @@ -3,6 +3,7 @@ export interface WattTimeParams { longitude: number; starttime: string; endtime: string; + signal_type?: string; } export interface WattTimeRegionParams { @@ -16,3 +17,9 @@ export interface LatitudeLongitude { latitude: number; longitude: number; } + +export interface RegionFromLocationResponse { + signal_type: string; + region: string; + region_full_name: string; +} diff --git a/src/lib/watt-time/watt-time-api.ts b/src/lib/watt-time/watt-time-api.ts index 4ce1695..339e197 100644 --- a/src/lib/watt-time/watt-time-api.ts +++ b/src/lib/watt-time/watt-time-api.ts @@ -1,19 +1,24 @@ import * as dotenv from 'dotenv'; -import * as dayjs from 'dayjs'; import axios from 'axios'; +import {Settings, DateTime} from 'luxon'; import {ERRORS} from '../../util/errors'; import {buildErrorMessage} from '../../util/helpers'; -import {WattTimeParams, WattTimeRegionParams} from './types'; +import { + WattTimeParams, + WattTimeRegionParams, + RegionFromLocationResponse, +} from './types'; const {AuthorizationError, APIRequestError} = ERRORS; -export const WattTimeAPI = () => { - const baseUrl = 'https://api.watttime.org/v3'; - let token = ''; +Settings.defaultZone = 'utc'; +export const WattTimeAPI = () => { + const BASE_URL = 'https://api.watttime.org/v3'; const errorBuilder = buildErrorMessage(WattTimeAPI.name); + let token = ''; /** * Authenticates the user with the WattTime API using the provided authentication parameters. @@ -22,16 +27,27 @@ export const WattTimeAPI = () => { */ const authenticate = async (): Promise => { dotenv.config(); + validateCredentials(); token = process.env.WATT_TIME_TOKEN ?? ''; if (token === '') { - const tokenResponse = await axios.get('https://api.watttime.org/login', { - auth: { - username: process.env.WATT_TIME_USERNAME!, - password: process.env.WATT_TIME_PASSWORD!, - }, - }); + const tokenResponse = await axios + .get('https://api.watttime.org/login', { + auth: { + username: process.env.WATT_TIME_USERNAME!, + password: process.env.WATT_TIME_PASSWORD!, + }, + }) + .catch(error => { + throw new APIRequestError( + errorBuilder({ + message: `Authorization error from WattTime API. ${JSON.stringify( + error?.message || error + )}`, + }) + ); + }); if ( tokenResponse === undefined || @@ -51,14 +67,34 @@ export const WattTimeAPI = () => { }; /** - * Support v2 version of WattTime API. - * - * Fetches and sorts data from the WattTime API based on the provided parameters. + * Validates if the credentials are provided. + */ + const validateCredentials = () => { + if ( + !process.env.WATT_TIME_TOKEN && + !process.env.WATT_TIME_USERNAME && + !process.env.WATT_TIME_PASSWORD + ) { + throw new AuthorizationError( + errorBuilder({ + message: + 'Invalid credentials provided. Either `token` or `username` and `password` should be provided', + }) + ); + } + }; + + /** + * Fetches region for provided geolocation and then get forcast for provided time period. + * Sorts data from the WattTime API. * Throws an APIRequestError if an error occurs during the request or if the response is invalid. */ const fetchAndSortData = async (params: WattTimeParams) => { - const result = await axios - .get('https://api2.watttime.org/v2/data', { + const signalType = params.signal_type || (await getSignalType(token)); + Object.assign(params, {signal_type: signalType}); + + const response = await axios + .get(`${BASE_URL}/region-from-loc`, { params, headers: { Authorization: `Bearer ${token}`, @@ -68,31 +104,31 @@ export const WattTimeAPI = () => { throw new APIRequestError( errorBuilder({ message: `Error fetching data from WattTime API. ${JSON.stringify( - error?.response?.data?.message || error + error?.response?.data?.error || error )}`, }) ); }); - if (result.status !== 200) { + if (response.status !== 200) { throw new APIRequestError( errorBuilder({ message: `Error fetching data from WattTime API: ${JSON.stringify( - result.status + response.status )}`, }) ); } - if (!('data' in result) || !Array.isArray(result.data)) { - throw new APIRequestError( - errorBuilder({ - message: 'Invalid response from WattTime API', - }) - ); - } + const result = response.data; + const regionParams: WattTimeRegionParams = { + signal_type: result.signal_type || undefined, + region: result.region, + start: params.starttime, + end: params.endtime, + }; - return sortData(result.data); + return await fetchDataWithRegion(regionParams); }; /** @@ -100,7 +136,7 @@ export const WattTimeAPI = () => { */ const getSignalType = async (token: string) => { const result = await axios - .get(`${baseUrl}/my-access`, { + .get(`${BASE_URL}/my-access`, { params: {}, headers: { Authorization: `Bearer ${token}`, @@ -141,7 +177,7 @@ export const WattTimeAPI = () => { Object.assign(params, {signal_type: signalType}); const result = await axios - .get(`${baseUrl}/forecast/historical`, { + .get(`${BASE_URL}/forecast/historical`, { params, headers: { Authorization: `Bearer ${token}`, @@ -175,7 +211,7 @@ export const WattTimeAPI = () => { ); } - return simplifyAndSortData(result.data.data).flat(); + return simplifyAndSortData(result.data.data); }; const simplifyAndSortData = (data: any) => { @@ -183,7 +219,7 @@ export const WattTimeAPI = () => { (item: {forecast: []; generated_at: string}) => item.forecast ); - return sortData(forecasts); + return sortData(forecasts.flat()); }; /** @@ -191,7 +227,10 @@ export const WattTimeAPI = () => { */ const sortData = (data: T[]) => { return data.sort((a: T, b: T) => { - return dayjs(a.point_time).unix() > dayjs(b.point_time).unix() ? 1 : -1; + return DateTime.fromISO(a.point_time).toSeconds() > + DateTime.fromISO(b.point_time).toSeconds() + ? 1 + : -1; }); };