mirror of
https://github.com/skydiver/ewelink-api.git
synced 2026-01-05 12:25:32 +01:00
Release v1.3.0 (#8)
* helper method to login into ewelink if no auth credentials found * return websocket reponse as JSON * created function to get raw consumption data * created function to parse raw consumption data and return daily usage * renamed property * created function to get current month power usage * created function to get raw power usage * added new test cases * catch websocket connection errors * power usage enhancements * added new test case * removed unused code * updated credentials file * version bump * updated dependencies * tests reorganized
This commit is contained in:
38
lib/powerUsage/currentMonthPowerUsage.js
Normal file
38
lib/powerUsage/currentMonthPowerUsage.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Return daily power usage
|
||||
*
|
||||
* @param hundredDaysKwhData
|
||||
*
|
||||
* @returns {{daily: *, monthly: *}}
|
||||
*/
|
||||
const currentMonthPowerUsage = ({ hundredDaysKwhData }) => {
|
||||
const today = new Date();
|
||||
const days = today.getDate();
|
||||
|
||||
let monthlyUsage = 0;
|
||||
const dailyUsage = [];
|
||||
|
||||
for (let day = 0; day < days; day += 1) {
|
||||
const s = hundredDaysKwhData.substr(6 * day, 2);
|
||||
const c = hundredDaysKwhData.substr(6 * day + 2, 2);
|
||||
const f = hundredDaysKwhData.substr(6 * day + 4, 2);
|
||||
const h = parseInt(s, 16);
|
||||
const y = parseInt(c, 16);
|
||||
const I = parseInt(f, 16);
|
||||
const E = parseFloat(`${h}.${y}${I}`);
|
||||
|
||||
dailyUsage.push({
|
||||
day: days - day,
|
||||
usage: E,
|
||||
});
|
||||
|
||||
monthlyUsage += E;
|
||||
}
|
||||
|
||||
return {
|
||||
monthly: monthlyUsage,
|
||||
daily: dailyUsage,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = currentMonthPowerUsage;
|
||||
50
lib/powerUsage/deviceRawPowerUsage.js
Normal file
50
lib/powerUsage/deviceRawPowerUsage.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const WebSocketRequest = require('../websocket');
|
||||
const payloads = require('../payloads');
|
||||
const { _get } = require('../helpers');
|
||||
|
||||
/**
|
||||
* Get specific device power usage (raw data)
|
||||
*
|
||||
* @param apiUrl
|
||||
* @param at
|
||||
* @param apiKey
|
||||
* @param deviceId
|
||||
*
|
||||
* @returns {Promise<{error: string}|{response: {hundredDaysKwhData: *}, status: string}>}
|
||||
*/
|
||||
const deviceRawPowerUsage = async ({ apiUrl, at, apiKey, deviceId }) => {
|
||||
const payloadLogin = payloads.wssLoginPayload({ at, apiKey });
|
||||
|
||||
const payloadUpdate = payloads.wssUpdatePayload({
|
||||
apiKey,
|
||||
deviceId,
|
||||
params: { hundredDaysKwh: 'get' },
|
||||
});
|
||||
|
||||
const response = await WebSocketRequest(apiUrl, [
|
||||
payloadLogin,
|
||||
payloadUpdate,
|
||||
]);
|
||||
|
||||
const error = _get(response, 'error', false);
|
||||
if (error) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const hundredDaysKwhData = _get(
|
||||
response[1],
|
||||
'config.hundredDaysKwhData',
|
||||
false
|
||||
);
|
||||
|
||||
if (!hundredDaysKwhData) {
|
||||
return { error: 'No power usage data found.' };
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
data: { hundredDaysKwhData },
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = deviceRawPowerUsage;
|
||||
7
lib/powerUsage/index.js
Normal file
7
lib/powerUsage/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const deviceRawPowerUsage = require('./deviceRawPowerUsage');
|
||||
const currentMonthPowerUsage = require('./currentMonthPowerUsage');
|
||||
|
||||
module.exports = {
|
||||
deviceRawPowerUsage,
|
||||
currentMonthPowerUsage,
|
||||
};
|
||||
@@ -2,6 +2,21 @@ const W3CWebSocket = require('websocket').w3cwebsocket;
|
||||
const WebSocketAsPromised = require('websocket-as-promised');
|
||||
const delay = require('delay');
|
||||
|
||||
/**
|
||||
* Parse WebSocket errors and return user friendly messages
|
||||
*
|
||||
* @param e
|
||||
*
|
||||
* @returns {{error: string}|{msg: string, error: number}}
|
||||
*/
|
||||
const customThrowError = e => {
|
||||
const loginError = e.message.indexOf('WebSocket is not opened');
|
||||
if (loginError > -1) {
|
||||
return { error: 401, msg: 'Authentication error' };
|
||||
}
|
||||
return { error: 'An unknown error occurred' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Open WebSocket connection and send provided payloads
|
||||
*
|
||||
@@ -17,17 +32,21 @@ const WebSocketRequest = async (url, payloads, ...{ delayTime = 1000 }) => {
|
||||
});
|
||||
|
||||
const responses = [];
|
||||
wsp.onMessage.addListener(message => responses.push(message));
|
||||
wsp.onMessage.addListener(message => responses.push(JSON.parse(message)));
|
||||
|
||||
await wsp.open();
|
||||
try {
|
||||
await wsp.open();
|
||||
|
||||
for (const payload of payloads) {
|
||||
await wsp.send(payload);
|
||||
await delay(delayTime);
|
||||
for (const payload of payloads) {
|
||||
await wsp.send(payload);
|
||||
await delay(delayTime);
|
||||
}
|
||||
|
||||
await wsp.close();
|
||||
} catch (e) {
|
||||
return customThrowError(e);
|
||||
}
|
||||
|
||||
await wsp.close();
|
||||
|
||||
return responses;
|
||||
};
|
||||
|
||||
|
||||
64
main.js
64
main.js
@@ -10,6 +10,8 @@ const {
|
||||
|
||||
const payloads = require('./lib/payloads');
|
||||
|
||||
const powerUsage = require('./lib/powerUsage');
|
||||
|
||||
class eWeLink {
|
||||
constructor({ region = 'us', email, password, at, apiKey }) {
|
||||
if (!at && (!email && !password)) {
|
||||
@@ -41,6 +43,19 @@ class eWeLink {
|
||||
return `wss://${this.region}-pconnect3.coolkit.cc:8080/api/ws`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return required config for websocket requests
|
||||
*
|
||||
* @returns {{at: *, apiUrl: string, apiKey: *}}
|
||||
*/
|
||||
getWebSocketConfig() {
|
||||
return {
|
||||
apiUrl: this.getApiWebSocket(),
|
||||
at: this.at,
|
||||
apiKey: this.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate http requests helpers
|
||||
*
|
||||
@@ -115,6 +130,17 @@ class eWeLink {
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if authentication credentials doesn't exists then perform a login
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async logIfNeeded() {
|
||||
if (!this.at) {
|
||||
await this.login();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific device information
|
||||
*
|
||||
@@ -238,6 +264,44 @@ class eWeLink {
|
||||
async toggleDevice(deviceId, channel = 1) {
|
||||
return this.setDevicePowerState(deviceId, 'toggle', channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device raw power usage
|
||||
*
|
||||
* @param deviceId
|
||||
*
|
||||
* @returns {Promise<{error: string}|{response: {hundredDaysKwhData: *}, status: string}>}
|
||||
*/
|
||||
async getDeviceRawPowerUsage(deviceId) {
|
||||
await this.logIfNeeded();
|
||||
return powerUsage.deviceRawPowerUsage({
|
||||
...this.getWebSocketConfig(),
|
||||
deviceId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device power usage for current month
|
||||
*
|
||||
* @param deviceId
|
||||
*
|
||||
* @returns {Promise<{error: string}|{daily: *, monthly: *}>}
|
||||
*/
|
||||
async getDevicePowerUsage(deviceId) {
|
||||
const response = await this.getDeviceRawPowerUsage(deviceId);
|
||||
|
||||
const error = _get(response, 'error', false);
|
||||
const hundredDaysKwhData = _get(response, 'data.hundredDaysKwhData', false);
|
||||
|
||||
if (error) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
...powerUsage.currentMonthPowerUsage({ hundredDaysKwhData }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = eWeLink;
|
||||
|
||||
2080
package-lock.json
generated
2080
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ewelink-api",
|
||||
"version": "1.2.2",
|
||||
"version": "1.3.0",
|
||||
"description": "eWeLink API for Node.js",
|
||||
"author": "Martín M.",
|
||||
"license": "MIT",
|
||||
@@ -28,20 +28,20 @@
|
||||
"random": "^2.1.1",
|
||||
"request": "^2.88.0",
|
||||
"request-promise": "^4.2.4",
|
||||
"websocket": "^1.0.28",
|
||||
"websocket": "^1.0.29",
|
||||
"websocket-as-promised": "^0.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.0.2",
|
||||
"eslint": "^6.0.1",
|
||||
"eslint-config-airbnb": "^17.1.0",
|
||||
"eslint": "^6.1.0",
|
||||
"eslint-config-airbnb": "^17.1.1",
|
||||
"eslint-config-prettier": "^6.0.0",
|
||||
"eslint-config-wesbos": "0.0.19",
|
||||
"eslint-plugin-html": "^6.0.0",
|
||||
"eslint-plugin-import": "^2.18.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.1",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.0",
|
||||
"eslint-plugin-react": "^7.14.2",
|
||||
"eslint-plugin-react": "^7.14.3",
|
||||
"eslint-plugin-react-hooks": "^1.6.1",
|
||||
"jest": "^24.8.0",
|
||||
"prettier": "^1.18.2"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"email": "<your ewelink email>",
|
||||
"password": "<your ewelink password>",
|
||||
"deviceId": "<your device id>"
|
||||
"deviceId": "<your device id>",
|
||||
"deviceIdWithPower": "<your device id>",
|
||||
"deviceIdWithoutPower": "<your device id>"
|
||||
}
|
||||
@@ -21,8 +21,23 @@ const specificDeviceExpectations = {
|
||||
uiid: expect.any(Number),
|
||||
};
|
||||
|
||||
const rawPowerUsageExpectations = {
|
||||
status: 'ok',
|
||||
data: {
|
||||
hundredDaysKwhData: expect.any(String),
|
||||
},
|
||||
};
|
||||
|
||||
const currentMonthPowerUsageExpectations = {
|
||||
status: 'ok',
|
||||
monthly: expect.any(Number),
|
||||
daily: expect.any(Array),
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
loginExpectations,
|
||||
allDevicesExpectations,
|
||||
specificDeviceExpectations,
|
||||
rawPowerUsageExpectations,
|
||||
currentMonthPowerUsageExpectations,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const ewelink = require('../main');
|
||||
const { deviceId } = require('./_setup/credentials.json');
|
||||
const { deviceId, deviceIdWithPower } = require('./_setup/credentials.json');
|
||||
|
||||
describe('invalid credentials', () => {
|
||||
test('no credentials given', async () => {
|
||||
@@ -48,4 +48,12 @@ describe('invalid credentials', () => {
|
||||
expect(powerState.msg).toBe('Authentication error');
|
||||
expect(powerState.error).toBe(401);
|
||||
});
|
||||
|
||||
test('current month power usage should fail', async () => {
|
||||
const conn = new ewelink({ email: 'invalid', password: 'credentials' });
|
||||
const powerUsage = await conn.getDevicePowerUsage(deviceIdWithPower);
|
||||
expect(typeof powerUsage).toBe('object');
|
||||
expect(powerUsage.msg).toBe('Authentication error');
|
||||
expect(powerUsage.error).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
const delay = require('delay');
|
||||
|
||||
const ewelink = require('../main');
|
||||
const { email, password } = require('./_setup/credentials.json');
|
||||
|
||||
const {
|
||||
email,
|
||||
password,
|
||||
deviceIdWithoutPower,
|
||||
} = require('./_setup/credentials.json');
|
||||
const { loginExpectations } = require('./_setup/expectations');
|
||||
|
||||
describe('valid credentials, invalid device', () => {
|
||||
@@ -28,6 +35,31 @@ describe('valid credentials, invalid device', () => {
|
||||
expect(powerState.msg).toBe('Device does not exist');
|
||||
expect(powerState.error).toBe(500);
|
||||
});
|
||||
|
||||
test('raw power usage should fail', async () => {
|
||||
jest.setTimeout(30000);
|
||||
const conn = new ewelink({ email, password });
|
||||
const powerUsage = await conn.getDeviceRawPowerUsage('invalid deviceid');
|
||||
expect(typeof powerUsage).toBe('object');
|
||||
expect(powerUsage.error).toBe('No power usage data found.');
|
||||
});
|
||||
|
||||
test('current month power usage should fail', async () => {
|
||||
jest.setTimeout(30000);
|
||||
const conn = new ewelink({ email, password });
|
||||
const powerUsage = await conn.getDevicePowerUsage('invalid deviceid');
|
||||
expect(typeof powerUsage).toBe('object');
|
||||
expect(powerUsage.error).toBe('No power usage data found.');
|
||||
});
|
||||
|
||||
test('raw power on device without electricity monitor should fail', async () => {
|
||||
jest.setTimeout(30000);
|
||||
await delay(1000);
|
||||
const conn = new ewelink({ email, password });
|
||||
const powerUsage = await conn.getDeviceRawPowerUsage(deviceIdWithoutPower);
|
||||
expect(typeof powerUsage).toBe('object');
|
||||
expect(powerUsage.error).toBe('No power usage data found.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid credentials, wrong region', () => {
|
||||
|
||||
72
test/power-usage.spec.js
Normal file
72
test/power-usage.spec.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const delay = require('delay');
|
||||
|
||||
const ewelink = require('../main');
|
||||
|
||||
const {
|
||||
email,
|
||||
password,
|
||||
deviceIdWithPower,
|
||||
} = require('./_setup/credentials.json');
|
||||
const {
|
||||
rawPowerUsageExpectations,
|
||||
currentMonthPowerUsageExpectations,
|
||||
} = require('./_setup/expectations');
|
||||
|
||||
describe('power usage: node script', () => {
|
||||
let conn;
|
||||
|
||||
beforeAll(async () => {
|
||||
conn = new ewelink({ email, password });
|
||||
await conn.login();
|
||||
});
|
||||
|
||||
test('should return raw power usage', async () => {
|
||||
jest.setTimeout(30000);
|
||||
const powerUsage = await conn.getDeviceRawPowerUsage(deviceIdWithPower);
|
||||
expect(typeof powerUsage).toBe('object');
|
||||
expect(powerUsage).toMatchObject(rawPowerUsageExpectations);
|
||||
expect(powerUsage.data.hundredDaysKwhData.length).toBe(600);
|
||||
});
|
||||
|
||||
test('should return current month power usage', async () => {
|
||||
jest.setTimeout(30000);
|
||||
await delay(1000);
|
||||
const days = new Date().getDate();
|
||||
const powerUsage = await conn.getDevicePowerUsage(deviceIdWithPower);
|
||||
expect(typeof powerUsage).toBe('object');
|
||||
expect(powerUsage).toMatchObject(currentMonthPowerUsageExpectations);
|
||||
expect(powerUsage.daily.length).toBe(days);
|
||||
});
|
||||
});
|
||||
|
||||
describe('power usage: serverless', () => {
|
||||
let accessToken;
|
||||
let apiKey;
|
||||
|
||||
beforeAll(async () => {
|
||||
const conn = new ewelink({ email, password });
|
||||
const login = await conn.login();
|
||||
accessToken = login.at;
|
||||
apiKey = login.user.apikey;
|
||||
});
|
||||
|
||||
test('should return raw power usage', async () => {
|
||||
jest.setTimeout(30000);
|
||||
const conn = new ewelink({ at: accessToken, apiKey });
|
||||
const powerUsage = await conn.getDeviceRawPowerUsage(deviceIdWithPower);
|
||||
expect(typeof powerUsage).toBe('object');
|
||||
expect(powerUsage).toMatchObject(rawPowerUsageExpectations);
|
||||
expect(powerUsage.data.hundredDaysKwhData.length).toBe(600);
|
||||
});
|
||||
|
||||
test('should return current month power usage', async () => {
|
||||
jest.setTimeout(30000);
|
||||
await delay(1000);
|
||||
const days = new Date().getDate();
|
||||
const conn = new ewelink({ at: accessToken, apiKey });
|
||||
const powerUsage = await conn.getDevicePowerUsage(deviceIdWithPower);
|
||||
expect(typeof powerUsage).toBe('object');
|
||||
expect(powerUsage).toMatchObject(currentMonthPowerUsageExpectations);
|
||||
expect(powerUsage.daily.length).toBe(days);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user