@etainabl/nodejs-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +29 -0
- package/api.js +119 -0
- package/api.js.map +1 -0
- package/consumption.js +47 -0
- package/consumption.js.map +1 -0
- package/index.js +5 -0
- package/index.js.map +1 -0
- package/lib/readingsInterpolate.d.ts +1 -0
- package/lib/readingsInterpolate.js +73 -0
- package/lib/readingsInterpolate.js.map +1 -0
- package/logger.js +10 -0
- package/logger.js.map +1 -0
- package/package.json +15 -0
- package/src/api.d.ts +46 -0
- package/src/api.ts +212 -0
- package/src/consumption.d.ts +18 -0
- package/src/consumption.ts +73 -0
- package/src/db.ts +34 -0
- package/src/index.d.ts +4 -0
- package/src/index.ts +9 -0
- package/src/lib/readingsInterpolate.ts +100 -0
- package/src/logger.d.ts +3 -0
- package/src/logger.ts +13 -0
- package/tsconfig.json +36 -0
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
extends: ['airbnb-base'],
|
|
3
|
+
parser: '@typescript-eslint/parser',
|
|
4
|
+
rules: {
|
|
5
|
+
'no-console': 'off',
|
|
6
|
+
'comma-dangle': [2, 'never'],
|
|
7
|
+
'arrow-parens': [2, 'as-needed'],
|
|
8
|
+
'linebreak-style': 0,
|
|
9
|
+
'padded-blocks': 0,
|
|
10
|
+
'no-underscore-dangle': [0],
|
|
11
|
+
'no-param-reassign': [2, { props: false }],
|
|
12
|
+
'new-cap': [2, { capIsNewExceptions: ['Router', 'ObjectId'] }],
|
|
13
|
+
'no-tabs': 2,
|
|
14
|
+
'max-len': [1, 150],
|
|
15
|
+
'no-plusplus': 0,
|
|
16
|
+
'generator-star-spacing': 0,
|
|
17
|
+
'class-methods-use-this': 0,
|
|
18
|
+
'import/extensions': 0,
|
|
19
|
+
'import/no-unresolved': 0,
|
|
20
|
+
'prefer-destructuring': 0,
|
|
21
|
+
'no-use-before-define': 0,
|
|
22
|
+
'import/prefer-default-export': 'off'
|
|
23
|
+
},
|
|
24
|
+
env: {
|
|
25
|
+
node: true,
|
|
26
|
+
browser: true
|
|
27
|
+
},
|
|
28
|
+
parserOptions: { ecmaVersion: 2020 }
|
|
29
|
+
};
|
package/api.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import logger from './logger.js';
|
|
3
|
+
const log = logger('etainablApi');
|
|
4
|
+
const factory = {
|
|
5
|
+
get: (etainablApi, endpoint, postEndpoint) => async (id, options = {}) => {
|
|
6
|
+
log.info(`API Request: GET ${process.env.ETAINABL_API_URL}/${endpoint}/${id}${postEndpoint ? `/${postEndpoint}` : ''}`);
|
|
7
|
+
const response = await etainablApi.get(`${endpoint}/${id}${postEndpoint ? `/${postEndpoint}` : ''}`, options);
|
|
8
|
+
if (response.status !== 200) {
|
|
9
|
+
throw new Error(`${response.status} ${response.statusText} response from API (GET ${endpoint}/:id${postEndpoint ? `/${postEndpoint}` : ''})`);
|
|
10
|
+
}
|
|
11
|
+
if (!response.data) {
|
|
12
|
+
throw new Error(`No data from API (GET ${endpoint}/:id${postEndpoint ? `/${postEndpoint}` : ''})`);
|
|
13
|
+
}
|
|
14
|
+
return response.data;
|
|
15
|
+
},
|
|
16
|
+
list: (etainablApi, endpoint) => async (options = {}) => {
|
|
17
|
+
log.info(`API Request: GET ${process.env.ETAINABL_API_URL}/${endpoint}`);
|
|
18
|
+
const response = await etainablApi.get(`${endpoint}`, options);
|
|
19
|
+
if (response.status !== 200) {
|
|
20
|
+
throw new Error(`${response.status} ${response.statusText} response from API (GET ${endpoint})`);
|
|
21
|
+
}
|
|
22
|
+
if (!response.data || !response.data.data) {
|
|
23
|
+
throw new Error(`No data from API (GET ${endpoint})`);
|
|
24
|
+
}
|
|
25
|
+
return response.data;
|
|
26
|
+
},
|
|
27
|
+
update: (etainablApi, endpoint) => async (id, data, options = {}) => {
|
|
28
|
+
log.info(`API Request: PATCH ${process.env.ETAINABL_API_URL}/${endpoint}/${id}`);
|
|
29
|
+
const response = await etainablApi.patch(`${endpoint}/${id}`, data, options);
|
|
30
|
+
if (response.status !== 200) {
|
|
31
|
+
throw new Error(`${response.status} ${response.statusText} response from API (PATCH ${endpoint})`);
|
|
32
|
+
}
|
|
33
|
+
if (!response.data) {
|
|
34
|
+
throw new Error(`No data from API (PATCH ${endpoint})`);
|
|
35
|
+
}
|
|
36
|
+
return response.data;
|
|
37
|
+
},
|
|
38
|
+
create: (etainablApi, endpoint) => async (data, options = {}) => {
|
|
39
|
+
log.info(`API Request: POST ${process.env.ETAINABL_API_URL}/${endpoint}`);
|
|
40
|
+
const response = await etainablApi.post(`${endpoint}`, data, options);
|
|
41
|
+
if (response.status !== 200) {
|
|
42
|
+
throw new Error(`${response.status} ${response.statusText} response from API (POST ${endpoint})`);
|
|
43
|
+
}
|
|
44
|
+
if (!response.data) {
|
|
45
|
+
throw new Error(`No data from API (POST ${endpoint})`);
|
|
46
|
+
}
|
|
47
|
+
return response.data;
|
|
48
|
+
},
|
|
49
|
+
remove: (etainablApi, endpoint) => async (id, options = {}) => {
|
|
50
|
+
log.info(`API Request: DELETE ${process.env.ETAINABL_API_URL}/${endpoint}/${id}`);
|
|
51
|
+
const response = await etainablApi.delete(`${endpoint}/${id}`, options);
|
|
52
|
+
if (response.status !== 200) {
|
|
53
|
+
throw new Error(`${response.status} ${response.statusText} response from API (DELETE ${endpoint})`);
|
|
54
|
+
}
|
|
55
|
+
if (!response.data) {
|
|
56
|
+
throw new Error(`No data from API (DELETE ${endpoint})`);
|
|
57
|
+
}
|
|
58
|
+
return response.data;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
export default (auth, instanceOptions = {}) => {
|
|
62
|
+
const headers = {};
|
|
63
|
+
if (auth.key) {
|
|
64
|
+
headers['x-key'] = auth.key;
|
|
65
|
+
}
|
|
66
|
+
else if (auth.token) {
|
|
67
|
+
headers['Authorization'] = auth.token;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
headers['x-key'] = process.env.ETAINABL_API_KEY;
|
|
71
|
+
}
|
|
72
|
+
const etainablApi = axios.create({
|
|
73
|
+
baseURL: process.env.ETAINABL_API_URL,
|
|
74
|
+
timeout: 30000,
|
|
75
|
+
headers,
|
|
76
|
+
...instanceOptions
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
instance: etainablApi,
|
|
80
|
+
// accounts
|
|
81
|
+
getAccount: factory.get(etainablApi, 'accounts'),
|
|
82
|
+
listAccounts: factory.list(etainablApi, 'accounts'),
|
|
83
|
+
updateAccount: factory.update(etainablApi, 'accounts'),
|
|
84
|
+
createAccount: factory.create(etainablApi, 'accounts'),
|
|
85
|
+
removeAccount: factory.remove(etainablApi, 'accounts'),
|
|
86
|
+
// assets
|
|
87
|
+
getAsset: factory.get(etainablApi, 'assets'),
|
|
88
|
+
listAssets: factory.list(etainablApi, 'assets'),
|
|
89
|
+
updateAsset: factory.update(etainablApi, 'assets'),
|
|
90
|
+
createAsset: factory.create(etainablApi, 'assets'),
|
|
91
|
+
removeAsset: factory.remove(etainablApi, 'assets'),
|
|
92
|
+
// assetGroups
|
|
93
|
+
getAssetGroup: factory.get(etainablApi, 'asset-groups'),
|
|
94
|
+
listAssetGroups: factory.list(etainablApi, 'asset-groups'),
|
|
95
|
+
updateAssetGroup: factory.update(etainablApi, 'asset-groups'),
|
|
96
|
+
createAssetGroup: factory.create(etainablApi, 'asset-groups'),
|
|
97
|
+
removeAssetGroup: factory.remove(etainablApi, 'asset-groups'),
|
|
98
|
+
getAssetGroupAssets: factory.get(etainablApi, 'asset-groups', 'assets'),
|
|
99
|
+
// automation
|
|
100
|
+
getAutomation: factory.get(etainablApi, 'automation'),
|
|
101
|
+
listAutomations: factory.list(etainablApi, 'automation'),
|
|
102
|
+
updateAutomation: factory.update(etainablApi, 'automation'),
|
|
103
|
+
createAutomation: factory.create(etainablApi, 'automation'),
|
|
104
|
+
removeAutomation: factory.remove(etainablApi, 'automation'),
|
|
105
|
+
// consumption
|
|
106
|
+
getConsumption: factory.get(etainablApi, 'consumptions'),
|
|
107
|
+
listConsumptions: factory.list(etainablApi, 'consumptions'),
|
|
108
|
+
updateConsumption: factory.update(etainablApi, 'consumptions'),
|
|
109
|
+
createConsumption: factory.create(etainablApi, 'consumptions'),
|
|
110
|
+
removeConsumption: factory.remove(etainablApi, 'consumptions'),
|
|
111
|
+
// readings
|
|
112
|
+
getReading: factory.get(etainablApi, 'readings'),
|
|
113
|
+
listReadings: factory.list(etainablApi, 'readings'),
|
|
114
|
+
updateReading: factory.update(etainablApi, 'readings'),
|
|
115
|
+
createReading: factory.create(etainablApi, 'readings'),
|
|
116
|
+
removeReading: factory.remove(etainablApi, 'readings'),
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=api.js.map
|
package/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["src/api.ts"],"names":[],"mappings":"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"}
|
package/consumption.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
import { interpolate } from './lib/readingsInterpolate.js';
|
|
3
|
+
const apportionedReadings = (readings, granularity = 'days', interpolationMethod = 'linear', startDate, endDate) => {
|
|
4
|
+
// Readings sorted from newest to oldest
|
|
5
|
+
const sortedReadings = [...readings].sort((a, b) => moment(a.submittedAt).diff(b.submittedAt));
|
|
6
|
+
// Array of every hour/day etc between the first and last reading
|
|
7
|
+
const intervals = moment(sortedReadings[sortedReadings.length - 1].submittedAt).diff(moment(sortedReadings[0].submittedAt), granularity);
|
|
8
|
+
const dates = [...Array(intervals).keys()]
|
|
9
|
+
.slice(1)
|
|
10
|
+
.map((interval) => moment(sortedReadings[0].submittedAt).startOf(granularity).add(interval, granularity));
|
|
11
|
+
let interpolatedReadings = dates.map((date) => {
|
|
12
|
+
const reading = interpolate(sortedReadings, date, interpolationMethod);
|
|
13
|
+
return {
|
|
14
|
+
date,
|
|
15
|
+
reading
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
interpolatedReadings = interpolatedReadings.map((reading, index) => {
|
|
19
|
+
const prevReading = interpolatedReadings[index - 1];
|
|
20
|
+
if (prevReading) {
|
|
21
|
+
return {
|
|
22
|
+
...reading,
|
|
23
|
+
consumption: reading.reading - prevReading.reading
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return reading;
|
|
27
|
+
});
|
|
28
|
+
if (startDate) {
|
|
29
|
+
interpolatedReadings = interpolatedReadings.filter((reading) => moment(reading.date).isSameOrAfter(startDate, granularity));
|
|
30
|
+
}
|
|
31
|
+
if (endDate) {
|
|
32
|
+
interpolatedReadings = interpolatedReadings.filter((reading) => moment(reading.date).isSameOrBefore(endDate, granularity));
|
|
33
|
+
}
|
|
34
|
+
return interpolatedReadings;
|
|
35
|
+
};
|
|
36
|
+
const consumptionFromReadings = (readings, startDate, endDate, granularity = 'hours', interpolationMethod = 'linear') => {
|
|
37
|
+
// Split the readings into hourly readings and calculate the consumption for each hour
|
|
38
|
+
const apportioned = apportionedReadings(readings, granularity, interpolationMethod, startDate, endDate);
|
|
39
|
+
// Calculate the total consumption between the start and end dates
|
|
40
|
+
const totalConsumption = apportioned.reduce((acc, reading) => acc + reading.consumption, 0);
|
|
41
|
+
return { totalConsumption, apportioned };
|
|
42
|
+
};
|
|
43
|
+
export default {
|
|
44
|
+
apportionedReadings,
|
|
45
|
+
consumptionFromReadings
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=consumption.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consumption.js","sourceRoot":"","sources":["src/consumption.ts"],"names":[],"mappings":"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"}
|
package/index.js
ADDED
package/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,WAAW,MAAM,kBAAkB,CAAC;AAC3C,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,OAAO,EACL,GAAG,EACH,MAAM,EACN,WAAW,EACZ,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function interpolate(meterReadings: any[], targetDate: any, method?: string): any;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
export function interpolate(meterReadings, targetDate, method = 'linear') {
|
|
3
|
+
if (method === 'cubic') {
|
|
4
|
+
return cubicInterpolation(meterReadings, targetDate);
|
|
5
|
+
}
|
|
6
|
+
if (method === 'linear') {
|
|
7
|
+
return linearInterpolation(meterReadings, targetDate);
|
|
8
|
+
}
|
|
9
|
+
throw new Error(`Interpolation method ${method} not supported`);
|
|
10
|
+
}
|
|
11
|
+
function linearInterpolation(meterReadings, targetDate) {
|
|
12
|
+
// Ensure the meter readings are sorted by date
|
|
13
|
+
meterReadings.sort((a, b) => moment(a.submittedAt).unix() - moment(b.submittedAt).unix());
|
|
14
|
+
const targetTimestamp = moment(targetDate).startOf('minute').unix();
|
|
15
|
+
// Find the two nearest meter readings for the target date
|
|
16
|
+
let lowerIndex = -1;
|
|
17
|
+
let upperIndex = -1;
|
|
18
|
+
for (let i = 0; i < meterReadings.length; i++) {
|
|
19
|
+
const reading = meterReadings[i];
|
|
20
|
+
if (moment(reading.submittedAt).startOf('minute').unix() <= targetTimestamp) {
|
|
21
|
+
lowerIndex = i;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
upperIndex = i;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// If no readings are available for interpolation, return null
|
|
29
|
+
if (lowerIndex === -1 || upperIndex === -1) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const lowerReading = meterReadings[lowerIndex];
|
|
33
|
+
const upperReading = meterReadings[upperIndex];
|
|
34
|
+
// Linear interpolation formula
|
|
35
|
+
const consumption = lowerReading.value
|
|
36
|
+
+ ((targetTimestamp - moment(lowerReading.submittedAt).startOf('minute').unix())
|
|
37
|
+
/ (moment(upperReading.submittedAt).startOf('minute').unix() - moment(lowerReading.submittedAt).startOf('minute').unix()))
|
|
38
|
+
* (upperReading.value - lowerReading.value);
|
|
39
|
+
return consumption;
|
|
40
|
+
}
|
|
41
|
+
;
|
|
42
|
+
function cubicInterpolateCalc(x0, y0, x1, y1, x2, y2, x3, y3, x) {
|
|
43
|
+
const t = (x - x1) / (x2 - x1);
|
|
44
|
+
const t2 = t * t;
|
|
45
|
+
const t3 = t2 * t;
|
|
46
|
+
const a0 = y3 - y2 - y0 + y1;
|
|
47
|
+
const a1 = y0 - y1 - a0;
|
|
48
|
+
const a2 = y2 - y0;
|
|
49
|
+
const a3 = y1;
|
|
50
|
+
return a0 * t3 + a1 * t2 + a2 * t + a3;
|
|
51
|
+
}
|
|
52
|
+
function cubicInterpolation(meterReadings, targetDate) {
|
|
53
|
+
// Ensure the meter readings are sorted by date
|
|
54
|
+
meterReadings.sort((a, b) => moment(a.submittedAt).unix() - moment(b.submittedAt).unix());
|
|
55
|
+
const targetTimestamp = new Date(targetDate).getTime();
|
|
56
|
+
// Find the four nearest meter readings for the target date
|
|
57
|
+
let startIndex = -1;
|
|
58
|
+
for (let i = 0; i < meterReadings.length; i++) {
|
|
59
|
+
const reading = meterReadings[i];
|
|
60
|
+
if (moment(reading.submittedAt).startOf('minute').unix() > targetTimestamp) {
|
|
61
|
+
startIndex = i - 2;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// If no readings are available for interpolation, return null
|
|
66
|
+
if (startIndex < 0 || startIndex + 3 >= meterReadings.length) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const readings = meterReadings.slice(startIndex, startIndex + 4);
|
|
70
|
+
const interpolatedConsumption = cubicInterpolateCalc(moment(readings[0].submittedAt).startOf('minute').unix(), readings[0].value, moment(readings[1].submittedAt).startOf('minute').unix(), readings[1].value, moment(readings[2].submittedAt).startOf('minute').unix(), readings[2].value, moment(readings[3].submittedAt).startOf('minute').unix(), readings[3].value, targetTimestamp);
|
|
71
|
+
return interpolatedConsumption;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=readingsInterpolate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readingsInterpolate.js","sourceRoot":"","sources":["../src/lib/readingsInterpolate.ts"],"names":[],"mappings":"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"}
|
package/logger.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
export default (namespace) => winston.createLogger({
|
|
3
|
+
level: 'debug',
|
|
4
|
+
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
|
5
|
+
defaultMeta: { service: process.env.AWS_LAMBDA_FUNCTION_NAME, script: namespace },
|
|
6
|
+
transports: [
|
|
7
|
+
new winston.transports.Console()
|
|
8
|
+
]
|
|
9
|
+
});
|
|
10
|
+
//# sourceMappingURL=logger.js.map
|
package/logger.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,eAAe,CAAC,SAAiB,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC;IACzD,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAC5B,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,EAC1B,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CACtB;IACD,WAAW,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,MAAM,EAAE,SAAS,EAAE;IACjF,UAAU,EAAE;QACV,IAAI,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE;KACjC;CACF,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@etainabl/nodejs-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"author": "Jonathan Lambert <jonathan@etainabl.com>",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"axios": "^1.4.0",
|
|
9
|
+
"moment": "^2.29.4",
|
|
10
|
+
"winston": "^3.10.0"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/node": "^20.4.5"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/api.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AxiosRequestConfig, AxiosInstance, CreateAxiosDefaults } from 'axios';
|
|
2
|
+
interface ETNPagedResponse {
|
|
3
|
+
data: any[];
|
|
4
|
+
total: number;
|
|
5
|
+
limit: number;
|
|
6
|
+
skip: number;
|
|
7
|
+
}
|
|
8
|
+
interface AuthOptions {
|
|
9
|
+
key?: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const _default: (auth: AuthOptions, instanceOptions?: CreateAxiosDefaults) => {
|
|
13
|
+
instance: AxiosInstance;
|
|
14
|
+
getAccount: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
15
|
+
listAccounts: (options?: AxiosRequestConfig<any>) => Promise<ETNPagedResponse>;
|
|
16
|
+
updateAccount: (id: string, data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
17
|
+
createAccount: (data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
18
|
+
removeAccount: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
19
|
+
getAsset: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
20
|
+
listAssets: (options?: AxiosRequestConfig<any>) => Promise<ETNPagedResponse>;
|
|
21
|
+
updateAsset: (id: string, data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
22
|
+
createAsset: (data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
23
|
+
removeAsset: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
24
|
+
getAssetGroup: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
25
|
+
listAssetGroups: (options?: AxiosRequestConfig<any>) => Promise<ETNPagedResponse>;
|
|
26
|
+
updateAssetGroup: (id: string, data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
27
|
+
createAssetGroup: (data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
28
|
+
removeAssetGroup: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
29
|
+
getAssetGroupAssets: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
30
|
+
getAutomation: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
31
|
+
listAutomations: (options?: AxiosRequestConfig<any>) => Promise<ETNPagedResponse>;
|
|
32
|
+
updateAutomation: (id: string, data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
33
|
+
createAutomation: (data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
34
|
+
removeAutomation: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
35
|
+
getConsumption: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
36
|
+
listConsumptions: (options?: AxiosRequestConfig<any>) => Promise<ETNPagedResponse>;
|
|
37
|
+
updateConsumption: (id: string, data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
38
|
+
createConsumption: (data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
39
|
+
removeConsumption: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
40
|
+
getReading: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
41
|
+
listReadings: (options?: AxiosRequestConfig<any>) => Promise<ETNPagedResponse>;
|
|
42
|
+
updateReading: (id: string, data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
43
|
+
createReading: (data: any, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
44
|
+
removeReading: (id: string, options?: AxiosRequestConfig<any>) => Promise<any>;
|
|
45
|
+
};
|
|
46
|
+
export default _default;
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import type { AxiosRequestConfig, AxiosInstance, CreateAxiosDefaults } from 'axios';
|
|
3
|
+
|
|
4
|
+
import logger from './logger.js';
|
|
5
|
+
|
|
6
|
+
const log = logger('etainablApi');
|
|
7
|
+
|
|
8
|
+
interface ETNPagedResponse {
|
|
9
|
+
data: any[];
|
|
10
|
+
total: number;
|
|
11
|
+
limit: number;
|
|
12
|
+
skip: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const factory = {
|
|
16
|
+
get: (etainablApi: AxiosInstance, endpoint: any, postEndpoint?: any) => async (id: string, options: AxiosRequestConfig = {}) => {
|
|
17
|
+
log.info(`API Request: GET ${process.env.ETAINABL_API_URL}/${endpoint}/${id}${postEndpoint ? `/${postEndpoint}` : ''}`);
|
|
18
|
+
|
|
19
|
+
let response;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
response = await etainablApi.get(`${endpoint}/${id}${postEndpoint ? `/${postEndpoint}` : ''}`, options);
|
|
23
|
+
} catch (e: any) {
|
|
24
|
+
if (e.response?.data) throw new Error(`API error response: ${JSON.stringify(e.response.data)}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!response) {
|
|
28
|
+
throw new Error(`No response from API (GET ${endpoint}/:id${postEndpoint ? `/${postEndpoint}` : ''})`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (response.status !== 200) {
|
|
32
|
+
throw new Error(`${response.status} ${response.statusText} response from API (GET ${endpoint}/:id${postEndpoint ? `/${postEndpoint}` : ''})`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!response.data) {
|
|
36
|
+
throw new Error(`No data from API (GET ${endpoint}/:id${postEndpoint ? `/${postEndpoint}` : ''})`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return response.data;
|
|
40
|
+
},
|
|
41
|
+
list: (etainablApi: AxiosInstance, endpoint: any) => async (options: AxiosRequestConfig = {}): Promise<ETNPagedResponse> => {
|
|
42
|
+
log.info(`API Request: GET ${process.env.ETAINABL_API_URL}/${endpoint}`);
|
|
43
|
+
|
|
44
|
+
let response;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
response = await etainablApi.get(`${endpoint}`, options);
|
|
48
|
+
} catch (e: any) {
|
|
49
|
+
if (e.response?.data) throw new Error(`API error response: ${JSON.stringify(e.response.data)}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!response) {
|
|
53
|
+
throw new Error(`No response from API (GET ${endpoint}/:id${postEndpoint ? `/${postEndpoint}` : ''})`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (response.status !== 200) {
|
|
57
|
+
throw new Error(`${response.status} ${response.statusText} response from API (GET ${endpoint})`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!response.data || !response.data.data) {
|
|
61
|
+
throw new Error(`No data from API (GET ${endpoint})`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return response.data;
|
|
65
|
+
},
|
|
66
|
+
update: (etainablApi: AxiosInstance, endpoint: any) => async (id: string, data: any, options: AxiosRequestConfig = {}) => {
|
|
67
|
+
log.info(`API Request: PATCH ${process.env.ETAINABL_API_URL}/${endpoint}/${id}`);
|
|
68
|
+
|
|
69
|
+
let response;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
response = await etainablApi.patch(`${endpoint}/${id}`, data, options);
|
|
73
|
+
} catch (e: any) {
|
|
74
|
+
if (e.response?.data) throw new Error(`API error response: ${JSON.stringify(e.response.data)}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!response) {
|
|
78
|
+
throw new Error(`No response from API (GET ${endpoint}/:id${postEndpoint ? `/${postEndpoint}` : ''})`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (response.status !== 200) {
|
|
82
|
+
throw new Error(`${response.status} ${response.statusText} response from API (PATCH ${endpoint})`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!response.data) {
|
|
86
|
+
throw new Error(`No data from API (PATCH ${endpoint})`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return response.data;
|
|
90
|
+
},
|
|
91
|
+
create: (etainablApi: AxiosInstance, endpoint: any) => async (data: any, options: AxiosRequestConfig = {}) => {
|
|
92
|
+
log.info(`API Request: POST ${process.env.ETAINABL_API_URL}/${endpoint}`);
|
|
93
|
+
|
|
94
|
+
let response;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
response = await etainablApi.post(`${endpoint}`, data, options);
|
|
98
|
+
} catch (e: any) {
|
|
99
|
+
if (e.response?.data) throw new Error(`API error response: ${JSON.stringify(e.response.data)}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!response) {
|
|
103
|
+
throw new Error(`No response from API (GET ${endpoint}/:id${postEndpoint ? `/${postEndpoint}` : ''})`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (response.status !== 200) {
|
|
107
|
+
throw new Error(`${response.status} ${response.statusText} response from API (POST ${endpoint})`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!response.data) {
|
|
111
|
+
throw new Error(`No data from API (POST ${endpoint})`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return response.data;
|
|
115
|
+
},
|
|
116
|
+
remove: (etainablApi: AxiosInstance, endpoint: any) => async (id: string, options: AxiosRequestConfig = {}) => {
|
|
117
|
+
log.info(`API Request: DELETE ${process.env.ETAINABL_API_URL}/${endpoint}/${id}`);
|
|
118
|
+
|
|
119
|
+
let response;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
response = await etainablApi.delete(`${endpoint}/${id}`, options);
|
|
123
|
+
} catch (e: any) {
|
|
124
|
+
if (e.response?.data) throw new Error(`API error response: ${JSON.stringify(e.response.data)}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!response) {
|
|
128
|
+
throw new Error(`No response from API (GET ${endpoint}/:id${postEndpoint ? `/${postEndpoint}` : ''})`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (response.status !== 200) {
|
|
132
|
+
throw new Error(`${response.status} ${response.statusText} response from API (DELETE ${endpoint})`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!response.data) {
|
|
136
|
+
throw new Error(`No data from API (DELETE ${endpoint})`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return response.data;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
interface AuthOptions {
|
|
144
|
+
key?: string;
|
|
145
|
+
token?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default (auth: AuthOptions, instanceOptions: CreateAxiosDefaults = {}) => {
|
|
149
|
+
const headers: any = {};
|
|
150
|
+
|
|
151
|
+
if (auth.key) {
|
|
152
|
+
headers['x-key'] = auth.key;
|
|
153
|
+
} else if (auth.token) {
|
|
154
|
+
headers['Authorization'] = auth.token;
|
|
155
|
+
} else {
|
|
156
|
+
headers['x-key'] = process.env.ETAINABL_API_KEY;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const etainablApi = axios.create({
|
|
160
|
+
baseURL: process.env.ETAINABL_API_URL,
|
|
161
|
+
timeout: 30000,
|
|
162
|
+
headers,
|
|
163
|
+
...instanceOptions
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
instance: etainablApi,
|
|
168
|
+
|
|
169
|
+
// accounts
|
|
170
|
+
getAccount: factory.get(etainablApi, 'accounts'),
|
|
171
|
+
listAccounts: factory.list(etainablApi, 'accounts'),
|
|
172
|
+
updateAccount: factory.update(etainablApi, 'accounts'),
|
|
173
|
+
createAccount: factory.create(etainablApi, 'accounts'),
|
|
174
|
+
removeAccount: factory.remove(etainablApi, 'accounts'),
|
|
175
|
+
|
|
176
|
+
// assets
|
|
177
|
+
getAsset: factory.get(etainablApi, 'assets'),
|
|
178
|
+
listAssets: factory.list(etainablApi, 'assets'),
|
|
179
|
+
updateAsset: factory.update(etainablApi, 'assets'),
|
|
180
|
+
createAsset: factory.create(etainablApi, 'assets'),
|
|
181
|
+
removeAsset: factory.remove(etainablApi, 'assets'),
|
|
182
|
+
|
|
183
|
+
// assetGroups
|
|
184
|
+
getAssetGroup: factory.get(etainablApi, 'asset-groups'),
|
|
185
|
+
listAssetGroups: factory.list(etainablApi, 'asset-groups'),
|
|
186
|
+
updateAssetGroup: factory.update(etainablApi, 'asset-groups'),
|
|
187
|
+
createAssetGroup: factory.create(etainablApi, 'asset-groups'),
|
|
188
|
+
removeAssetGroup: factory.remove(etainablApi, 'asset-groups'),
|
|
189
|
+
getAssetGroupAssets: factory.get(etainablApi, 'asset-groups', 'assets'),
|
|
190
|
+
|
|
191
|
+
// automation
|
|
192
|
+
getAutomation: factory.get(etainablApi, 'automation'),
|
|
193
|
+
listAutomations: factory.list(etainablApi, 'automation'),
|
|
194
|
+
updateAutomation: factory.update(etainablApi, 'automation'),
|
|
195
|
+
createAutomation: factory.create(etainablApi, 'automation'),
|
|
196
|
+
removeAutomation: factory.remove(etainablApi, 'automation'),
|
|
197
|
+
|
|
198
|
+
// consumption
|
|
199
|
+
getConsumption: factory.get(etainablApi, 'consumptions'),
|
|
200
|
+
listConsumptions: factory.list(etainablApi, 'consumptions'),
|
|
201
|
+
updateConsumption: factory.update(etainablApi, 'consumptions'),
|
|
202
|
+
createConsumption: factory.create(etainablApi, 'consumptions'),
|
|
203
|
+
removeConsumption: factory.remove(etainablApi, 'consumptions'),
|
|
204
|
+
|
|
205
|
+
// readings
|
|
206
|
+
getReading: factory.get(etainablApi, 'readings'),
|
|
207
|
+
listReadings: factory.list(etainablApi, 'readings'),
|
|
208
|
+
updateReading: factory.update(etainablApi, 'readings'),
|
|
209
|
+
createReading: factory.create(etainablApi, 'readings'),
|
|
210
|
+
removeReading: factory.remove(etainablApi, 'readings'),
|
|
211
|
+
}
|
|
212
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
export interface Reading {
|
|
3
|
+
submittedAt: string;
|
|
4
|
+
value: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ApportionedReading {
|
|
7
|
+
date: moment.Moment;
|
|
8
|
+
reading: number;
|
|
9
|
+
consumption?: number;
|
|
10
|
+
}
|
|
11
|
+
declare const _default: {
|
|
12
|
+
apportionedReadings: (readings: Reading[], granularity?: moment.unitOfTime.Diff, interpolationMethod?: string, startDate?: string | undefined, endDate?: string | undefined) => ApportionedReading[];
|
|
13
|
+
consumptionFromReadings: (readings: Reading[], startDate: string, endDate: string, granularity?: moment.unitOfTime.Diff, interpolationMethod?: string) => {
|
|
14
|
+
totalConsumption: any;
|
|
15
|
+
apportioned: ApportionedReading[];
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export default _default;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
import { interpolate } from './lib/readingsInterpolate.js';
|
|
3
|
+
|
|
4
|
+
export interface Reading {
|
|
5
|
+
submittedAt: string,
|
|
6
|
+
value: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ApportionedReading {
|
|
10
|
+
date: moment.Moment,
|
|
11
|
+
reading: number,
|
|
12
|
+
consumption?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const apportionedReadings = (readings: Reading[], granularity: moment.unitOfTime.Diff = 'days', interpolationMethod: string = 'linear', startDate?: string, endDate?: string): ApportionedReading[] => {
|
|
16
|
+
// Readings sorted from newest to oldest
|
|
17
|
+
const sortedReadings = [...readings].sort((a, b) => moment(a.submittedAt).diff(b.submittedAt));
|
|
18
|
+
|
|
19
|
+
// Array of every hour/day etc between the first and last reading
|
|
20
|
+
const intervals = moment(sortedReadings[sortedReadings.length - 1].submittedAt).diff(moment(sortedReadings[0].submittedAt), granularity);
|
|
21
|
+
|
|
22
|
+
const dates = [...Array(intervals).keys()]
|
|
23
|
+
.slice(1)
|
|
24
|
+
.map((interval: number) => moment(sortedReadings[0].submittedAt).startOf(granularity).add(interval, granularity));
|
|
25
|
+
|
|
26
|
+
let interpolatedReadings = dates.map((date: moment.Moment) => {
|
|
27
|
+
const reading = interpolate(sortedReadings, date, interpolationMethod);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
date,
|
|
31
|
+
reading
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
interpolatedReadings = interpolatedReadings.map((reading: ApportionedReading, index: number) => {
|
|
36
|
+
const prevReading = interpolatedReadings[index - 1];
|
|
37
|
+
|
|
38
|
+
if (prevReading) {
|
|
39
|
+
return {
|
|
40
|
+
...reading,
|
|
41
|
+
consumption: reading.reading - prevReading.reading
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return reading;
|
|
46
|
+
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (startDate) {
|
|
50
|
+
interpolatedReadings = interpolatedReadings.filter((reading: ApportionedReading) => moment(reading.date).isSameOrAfter(startDate, granularity));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (endDate) {
|
|
54
|
+
interpolatedReadings = interpolatedReadings.filter((reading: ApportionedReading) => moment(reading.date).isSameOrBefore(endDate, granularity));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return interpolatedReadings;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const consumptionFromReadings = (readings: Reading[], startDate: string, endDate: string, granularity: moment.unitOfTime.Diff = 'hours', interpolationMethod: string = 'linear') => {
|
|
61
|
+
// Split the readings into hourly readings and calculate the consumption for each hour
|
|
62
|
+
const apportioned = apportionedReadings(readings, granularity, interpolationMethod, startDate, endDate);
|
|
63
|
+
|
|
64
|
+
// Calculate the total consumption between the start and end dates
|
|
65
|
+
const totalConsumption = apportioned.reduce((acc: any, reading) => acc + reading.consumption, 0);
|
|
66
|
+
|
|
67
|
+
return { totalConsumption, apportioned };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default {
|
|
71
|
+
apportionedReadings,
|
|
72
|
+
consumptionFromReadings
|
|
73
|
+
};
|
package/src/db.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { MongoClient, Db } from 'mongodb';
|
|
2
|
+
import { logger } from './logger';
|
|
3
|
+
|
|
4
|
+
const log = logger('dbHelpers');
|
|
5
|
+
|
|
6
|
+
let cachedDb: Db;
|
|
7
|
+
|
|
8
|
+
export async function connectToDatabase() {
|
|
9
|
+
log.debug('Connecting to MongoDB server...');
|
|
10
|
+
|
|
11
|
+
if (cachedDb) {
|
|
12
|
+
log.debug('Using cached MongoDB connection.');
|
|
13
|
+
return Promise.resolve(cachedDb);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const uri = `mongodb+srv://${process.env.ETAINABL_DB_URL}`;
|
|
17
|
+
|
|
18
|
+
const client = new MongoClient(uri, {
|
|
19
|
+
auth: {
|
|
20
|
+
username: process.env.AWS_ACCESS_KEY_ID,
|
|
21
|
+
password: process.env.AWS_SECRET_ACCESS_KEY
|
|
22
|
+
},
|
|
23
|
+
authSource: '$external',
|
|
24
|
+
authMechanism: 'MONGODB-AWS'
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await client.connect();
|
|
28
|
+
|
|
29
|
+
log.debug('Connected successfully to MongoDB server!');
|
|
30
|
+
|
|
31
|
+
cachedDb = client.db('etainabl');
|
|
32
|
+
|
|
33
|
+
return cachedDb;
|
|
34
|
+
}
|
package/src/index.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
|
|
3
|
+
export function interpolate(meterReadings: any[], targetDate: any, method: string = 'linear') {
|
|
4
|
+
if (method === 'cubic') {
|
|
5
|
+
return cubicInterpolation(meterReadings, targetDate);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (method === 'linear') {
|
|
9
|
+
return linearInterpolation(meterReadings, targetDate);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
throw new Error(`Interpolation method ${method} not supported`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function linearInterpolation(meterReadings: any[], targetDate: any) {
|
|
16
|
+
// Ensure the meter readings are sorted by date
|
|
17
|
+
meterReadings.sort((a, b) => moment(a.submittedAt).unix() - moment(b.submittedAt).unix());
|
|
18
|
+
|
|
19
|
+
const targetTimestamp = moment(targetDate).startOf('minute').unix();
|
|
20
|
+
|
|
21
|
+
// Find the two nearest meter readings for the target date
|
|
22
|
+
let lowerIndex = -1;
|
|
23
|
+
let upperIndex = -1;
|
|
24
|
+
for (let i = 0; i < meterReadings.length; i++) {
|
|
25
|
+
const reading = meterReadings[i];
|
|
26
|
+
if (moment(reading.submittedAt).startOf('minute').unix() <= targetTimestamp) {
|
|
27
|
+
lowerIndex = i;
|
|
28
|
+
} else {
|
|
29
|
+
upperIndex = i;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If no readings are available for interpolation, return null
|
|
35
|
+
if (lowerIndex === -1 || upperIndex === -1) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const lowerReading = meterReadings[lowerIndex];
|
|
40
|
+
const upperReading = meterReadings[upperIndex];
|
|
41
|
+
|
|
42
|
+
// Linear interpolation formula
|
|
43
|
+
const consumption = lowerReading.value
|
|
44
|
+
+ ((targetTimestamp - moment(lowerReading.submittedAt).startOf('minute').unix())
|
|
45
|
+
/ (moment(upperReading.submittedAt).startOf('minute').unix() - moment(lowerReading.submittedAt).startOf('minute').unix()))
|
|
46
|
+
* (upperReading.value - lowerReading.value);
|
|
47
|
+
|
|
48
|
+
return consumption;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function cubicInterpolateCalc(x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x: number) {
|
|
52
|
+
const t = (x - x1) / (x2 - x1);
|
|
53
|
+
const t2 = t * t;
|
|
54
|
+
const t3 = t2 * t;
|
|
55
|
+
|
|
56
|
+
const a0 = y3 - y2 - y0 + y1;
|
|
57
|
+
const a1 = y0 - y1 - a0;
|
|
58
|
+
const a2 = y2 - y0;
|
|
59
|
+
const a3 = y1;
|
|
60
|
+
|
|
61
|
+
return a0 * t3 + a1 * t2 + a2 * t + a3;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cubicInterpolation(meterReadings: any[], targetDate: any) {
|
|
65
|
+
// Ensure the meter readings are sorted by date
|
|
66
|
+
meterReadings.sort((a, b) => moment(a.submittedAt).unix() - moment(b.submittedAt).unix());
|
|
67
|
+
|
|
68
|
+
const targetTimestamp = new Date(targetDate).getTime();
|
|
69
|
+
|
|
70
|
+
// Find the four nearest meter readings for the target date
|
|
71
|
+
let startIndex = -1;
|
|
72
|
+
for (let i = 0; i < meterReadings.length; i++) {
|
|
73
|
+
const reading = meterReadings[i];
|
|
74
|
+
if (moment(reading.submittedAt).startOf('minute').unix() > targetTimestamp) {
|
|
75
|
+
startIndex = i - 2;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If no readings are available for interpolation, return null
|
|
81
|
+
if (startIndex < 0 || startIndex + 3 >= meterReadings.length) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const readings = meterReadings.slice(startIndex, startIndex + 4);
|
|
86
|
+
|
|
87
|
+
const interpolatedConsumption = cubicInterpolateCalc(
|
|
88
|
+
moment(readings[0].submittedAt).startOf('minute').unix(),
|
|
89
|
+
readings[0].value,
|
|
90
|
+
moment(readings[1].submittedAt).startOf('minute').unix(),
|
|
91
|
+
readings[1].value,
|
|
92
|
+
moment(readings[2].submittedAt).startOf('minute').unix(),
|
|
93
|
+
readings[2].value,
|
|
94
|
+
moment(readings[3].submittedAt).startOf('minute').unix(),
|
|
95
|
+
readings[3].value,
|
|
96
|
+
targetTimestamp
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return interpolatedConsumption;
|
|
100
|
+
}
|
package/src/logger.d.ts
ADDED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
|
|
3
|
+
export default (namespace: string) => winston.createLogger({
|
|
4
|
+
level: 'debug',
|
|
5
|
+
format: winston.format.combine(
|
|
6
|
+
winston.format.timestamp(),
|
|
7
|
+
winston.format.json()
|
|
8
|
+
),
|
|
9
|
+
defaultMeta: { service: process.env.AWS_LAMBDA_FUNCTION_NAME, script: namespace },
|
|
10
|
+
transports: [
|
|
11
|
+
new winston.transports.Console()
|
|
12
|
+
]
|
|
13
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"display": "soclean-connector",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "es2022",
|
|
5
|
+
"strict": true,
|
|
6
|
+
"outDir": "./",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"preserveConstEnums": true,
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
"module":"ESNext",
|
|
11
|
+
"moduleResolution":"NodeNext",
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"pretty": true,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"allowJs": true,
|
|
19
|
+
"declaration": true
|
|
20
|
+
},
|
|
21
|
+
"watchOptions": {
|
|
22
|
+
// Use native file system events for files and directories
|
|
23
|
+
"watchFile": "useFsEvents",
|
|
24
|
+
"watchDirectory": "useFsEvents",
|
|
25
|
+
// Poll files for updates more frequently
|
|
26
|
+
// when they're updated a lot.
|
|
27
|
+
"fallbackPolling": "dynamicPriority",
|
|
28
|
+
// Don't coalesce watch notification
|
|
29
|
+
"synchronousWatchDirectory": true
|
|
30
|
+
// Finally, two additional settings for reducing the amount of possible
|
|
31
|
+
// files to track work from these directories
|
|
32
|
+
},
|
|
33
|
+
"exclude": [],
|
|
34
|
+
"include": ["./src/*.ts"],
|
|
35
|
+
"compileOnSave": true
|
|
36
|
+
}
|