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:
Martin M
2019-07-27 14:52:12 -03:00
committed by GitHub
parent d1cf9da212
commit 90f7cde28c
12 changed files with 2015 additions and 406 deletions

View 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;

View 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
View File

@@ -0,0 +1,7 @@
const deviceRawPowerUsage = require('./deviceRawPowerUsage');
const currentMonthPowerUsage = require('./currentMonthPowerUsage');
module.exports = {
deviceRawPowerUsage,
currentMonthPowerUsage,
};

View File

@@ -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
View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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>"
}

View File

@@ -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,
};

View File

@@ -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);
});
});

View File

@@ -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
View 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);
});
});