mirror of
https://github.com/skydiver/ewelink-api.git
synced 2025-12-26 23:21:37 +01:00
Release v1.4.0 (#11)
* refactor websocket / power usage * cooldown between api requests during testing * refactor power state methods * remove unused code * cooldown between api requests during testing * version bump
This commit is contained in:
30
classes/PowerState/ChangeState.js
Normal file
30
classes/PowerState/ChangeState.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const WebSocket = require('../WebSocket');
|
||||
const payloads = require('../../lib/payloads');
|
||||
const { _get } = require('../../lib/helpers');
|
||||
|
||||
class ChangeState extends WebSocket {
|
||||
static async set({ apiUrl, at, apiKey, deviceId, params, state }) {
|
||||
const payloadLogin = payloads.wssLoginPayload({ at, apiKey });
|
||||
|
||||
const payloadUpdate = payloads.wssUpdatePayload({
|
||||
apiKey,
|
||||
deviceId,
|
||||
params,
|
||||
});
|
||||
|
||||
const response = await this.WebSocketRequest(apiUrl, [
|
||||
payloadLogin,
|
||||
payloadUpdate,
|
||||
]);
|
||||
|
||||
const error = _get(response[1], 'error', false);
|
||||
|
||||
if (error === 403) {
|
||||
return { error, msg: response[1].reason };
|
||||
}
|
||||
|
||||
return { status: 'ok', state };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChangeState;
|
||||
5
classes/PowerState/index.js
Normal file
5
classes/PowerState/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const ChangeState = require('./ChangeState');
|
||||
|
||||
module.exports = {
|
||||
ChangeState,
|
||||
};
|
||||
40
classes/PowerUsage/CurrentMonth.js
Normal file
40
classes/PowerUsage/CurrentMonth.js
Normal file
@@ -0,0 +1,40 @@
|
||||
class CurrentMonth {
|
||||
/**
|
||||
* Return daily power usage
|
||||
*
|
||||
* @param hundredDaysKwhData
|
||||
*
|
||||
* @returns {{daily: *, monthly: *}}
|
||||
*/
|
||||
static parse({ 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 = CurrentMonth;
|
||||
52
classes/PowerUsage/DeviceRaw.js
Normal file
52
classes/PowerUsage/DeviceRaw.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const WebSocket = require('../WebSocket');
|
||||
const payloads = require('../../lib/payloads');
|
||||
const { _get } = require('../../lib/helpers');
|
||||
|
||||
class DeviceRaw extends WebSocket {
|
||||
/**
|
||||
* Get specific device power usage (raw data)
|
||||
*
|
||||
* @param apiUrl
|
||||
* @param at
|
||||
* @param apiKey
|
||||
* @param deviceId
|
||||
*
|
||||
* @returns {Promise<{error: string}|{data: {hundredDaysKwhData: *}, status: string}>}
|
||||
*/
|
||||
static async get({ apiUrl, at, apiKey, deviceId }) {
|
||||
const payloadLogin = payloads.wssLoginPayload({ at, apiKey });
|
||||
|
||||
const payloadUpdate = payloads.wssUpdatePayload({
|
||||
apiKey,
|
||||
deviceId,
|
||||
params: { hundredDaysKwh: 'get' },
|
||||
});
|
||||
|
||||
const response = await this.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 = DeviceRaw;
|
||||
7
classes/PowerUsage/index.js
Normal file
7
classes/PowerUsage/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const DeviceRaw = require('./DeviceRaw');
|
||||
const CurrentMonth = require('./CurrentMonth');
|
||||
|
||||
module.exports = {
|
||||
DeviceRaw,
|
||||
CurrentMonth,
|
||||
};
|
||||
55
classes/WebSocket.js
Normal file
55
classes/WebSocket.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const W3CWebSocket = require('websocket').w3cwebsocket;
|
||||
const WebSocketAsPromised = require('websocket-as-promised');
|
||||
const delay = require('delay');
|
||||
|
||||
class WebSocket {
|
||||
/**
|
||||
* Open WebSocket connection and send provided payloads
|
||||
*
|
||||
* @param url
|
||||
* @param payloads
|
||||
* @param delayTime
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
static async WebSocketRequest(url, payloads, ...{ delayTime = 1000 }) {
|
||||
const wsp = new WebSocketAsPromised(url, {
|
||||
createWebSocket: wss => new W3CWebSocket(wss),
|
||||
});
|
||||
|
||||
const responses = [];
|
||||
wsp.onMessage.addListener(message => responses.push(JSON.parse(message)));
|
||||
|
||||
try {
|
||||
await wsp.open();
|
||||
|
||||
for (const payload of payloads) {
|
||||
await wsp.send(payload);
|
||||
await delay(delayTime);
|
||||
}
|
||||
|
||||
await wsp.close();
|
||||
} catch (e) {
|
||||
return this.customThrowError(e);
|
||||
}
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse WebSocket errors and return user friendly messages
|
||||
*
|
||||
* @param e
|
||||
*
|
||||
* @returns {{error: string}|{msg: string, error: number}}
|
||||
*/
|
||||
static 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' };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebSocket;
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -1,50 +0,0 @@
|
||||
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;
|
||||
@@ -1,7 +0,0 @@
|
||||
const deviceRawPowerUsage = require('./deviceRawPowerUsage');
|
||||
const currentMonthPowerUsage = require('./currentMonthPowerUsage');
|
||||
|
||||
module.exports = {
|
||||
deviceRawPowerUsage,
|
||||
currentMonthPowerUsage,
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
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
|
||||
*
|
||||
* @param url
|
||||
* @param payloads
|
||||
* @param delayTime
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
const WebSocketRequest = async (url, payloads, ...{ delayTime = 1000 }) => {
|
||||
const wsp = new WebSocketAsPromised(url, {
|
||||
createWebSocket: wss => new W3CWebSocket(wss),
|
||||
});
|
||||
|
||||
const responses = [];
|
||||
wsp.onMessage.addListener(message => responses.push(JSON.parse(message)));
|
||||
|
||||
try {
|
||||
await wsp.open();
|
||||
|
||||
for (const payload of payloads) {
|
||||
await wsp.send(payload);
|
||||
await delay(delayTime);
|
||||
}
|
||||
|
||||
await wsp.close();
|
||||
} catch (e) {
|
||||
return customThrowError(e);
|
||||
}
|
||||
|
||||
return responses;
|
||||
};
|
||||
|
||||
module.exports = WebSocketRequest;
|
||||
41
main.js
41
main.js
@@ -1,6 +1,5 @@
|
||||
const rp = require('request-promise');
|
||||
|
||||
const WebSocketRequest = require('./lib/websocket');
|
||||
const { _get } = require('./lib/helpers');
|
||||
|
||||
const {
|
||||
@@ -10,7 +9,8 @@ const {
|
||||
|
||||
const payloads = require('./lib/payloads');
|
||||
|
||||
const powerUsage = require('./lib/powerUsage');
|
||||
const { ChangeState } = require('./classes/PowerState');
|
||||
const { DeviceRaw, CurrentMonth } = require('./classes/PowerUsage');
|
||||
|
||||
class eWeLink {
|
||||
constructor({ region = 'us', email, password, at, apiKey }) {
|
||||
@@ -43,19 +43,6 @@ 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
|
||||
*
|
||||
@@ -234,23 +221,14 @@ class eWeLink {
|
||||
params.switch = stateToSwitch;
|
||||
}
|
||||
|
||||
const payloadLogin = payloads.wssLoginPayload({
|
||||
return ChangeState.set({
|
||||
apiUrl: this.getApiWebSocket(),
|
||||
at: this.at,
|
||||
apiKey: this.apiKey,
|
||||
});
|
||||
|
||||
const payloadUpdate = payloads.wssUpdatePayload({
|
||||
apiKey: this.apiKey,
|
||||
deviceId,
|
||||
params,
|
||||
state: stateToSwitch,
|
||||
});
|
||||
|
||||
await WebSocketRequest(this.getApiWebSocket(), [
|
||||
payloadLogin,
|
||||
payloadUpdate,
|
||||
]);
|
||||
|
||||
return { status: 'ok', state };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,8 +252,11 @@ class eWeLink {
|
||||
*/
|
||||
async getDeviceRawPowerUsage(deviceId) {
|
||||
await this.logIfNeeded();
|
||||
return powerUsage.deviceRawPowerUsage({
|
||||
...this.getWebSocketConfig(),
|
||||
|
||||
return DeviceRaw.get({
|
||||
apiUrl: this.getApiWebSocket(),
|
||||
at: this.at,
|
||||
apiKey: this.apiKey,
|
||||
deviceId,
|
||||
});
|
||||
}
|
||||
@@ -299,7 +280,7 @@ class eWeLink {
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
...powerUsage.currentMonthPowerUsage({ hundredDaysKwhData }),
|
||||
...CurrentMonth.parse({ hundredDaysKwhData }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ewelink-api",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"description": "eWeLink API for Node.js",
|
||||
"author": "Martín M.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const delay = require('delay');
|
||||
|
||||
const ewelink = require('../main');
|
||||
const { email, password, deviceId } = require('./_setup/credentials.json');
|
||||
const {
|
||||
@@ -43,6 +45,7 @@ describe('env: node script', () => {
|
||||
|
||||
test('set device power state', async () => {
|
||||
jest.setTimeout(30000);
|
||||
await delay(3000);
|
||||
const device = await conn.getDevice(deviceId);
|
||||
const currentState = device.params.switch;
|
||||
const newState = currentState === 'on' ? 'off' : 'on';
|
||||
@@ -57,6 +60,7 @@ describe('env: node script', () => {
|
||||
|
||||
test('toggle device power state', async () => {
|
||||
jest.setTimeout(30000);
|
||||
await delay(3000);
|
||||
const device = await conn.getDevice(deviceId);
|
||||
const currentState = device.params.switch;
|
||||
const newState = currentState === 'on' ? 'off' : 'on';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const delay = require('delay');
|
||||
|
||||
const ewelink = require('../main');
|
||||
const { email, password, deviceId } = require('./_setup/credentials.json');
|
||||
const {
|
||||
@@ -46,6 +48,7 @@ describe('env: serverless', () => {
|
||||
|
||||
test('set device power state', async () => {
|
||||
jest.setTimeout(30000);
|
||||
await delay(3000);
|
||||
const conn = new ewelink({ at: accessToken, apiKey });
|
||||
const device = await conn.getDevice(deviceId);
|
||||
const currentState = device.params.switch;
|
||||
@@ -61,6 +64,7 @@ describe('env: serverless', () => {
|
||||
|
||||
test('toggle device power state', async () => {
|
||||
jest.setTimeout(30000);
|
||||
await delay(3000);
|
||||
const conn = new ewelink({ at: accessToken, apiKey });
|
||||
const device = await conn.getDevice(deviceId);
|
||||
const currentState = device.params.switch;
|
||||
|
||||
@@ -10,6 +10,10 @@ const {
|
||||
const { loginExpectations } = require('./_setup/expectations');
|
||||
|
||||
describe('valid credentials, invalid device', () => {
|
||||
beforeEach(async () => {
|
||||
await delay(1000);
|
||||
});
|
||||
|
||||
test('get device power state should fail', async () => {
|
||||
const conn = new ewelink({ email, password });
|
||||
const powerState = await conn.getDevicePowerState('invalid deviceid');
|
||||
@@ -54,7 +58,6 @@ describe('valid credentials, invalid device', () => {
|
||||
|
||||
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');
|
||||
|
||||
@@ -20,6 +20,10 @@ describe('power usage: node script', () => {
|
||||
await conn.login();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await delay(1000);
|
||||
});
|
||||
|
||||
test('should return raw power usage', async () => {
|
||||
jest.setTimeout(30000);
|
||||
const powerUsage = await conn.getDeviceRawPowerUsage(deviceIdWithPower);
|
||||
@@ -30,7 +34,6 @@ describe('power usage: node script', () => {
|
||||
|
||||
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');
|
||||
@@ -61,7 +64,6 @@ describe('power usage: serverless', () => {
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user