diff --git a/classes/PowerState/ChangeState.js b/classes/PowerState/ChangeState.js new file mode 100644 index 0000000..73f85cf --- /dev/null +++ b/classes/PowerState/ChangeState.js @@ -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; diff --git a/classes/PowerState/index.js b/classes/PowerState/index.js new file mode 100644 index 0000000..bed15bf --- /dev/null +++ b/classes/PowerState/index.js @@ -0,0 +1,5 @@ +const ChangeState = require('./ChangeState'); + +module.exports = { + ChangeState, +}; diff --git a/classes/PowerUsage/CurrentMonth.js b/classes/PowerUsage/CurrentMonth.js new file mode 100644 index 0000000..ee32af6 --- /dev/null +++ b/classes/PowerUsage/CurrentMonth.js @@ -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; diff --git a/classes/PowerUsage/DeviceRaw.js b/classes/PowerUsage/DeviceRaw.js new file mode 100644 index 0000000..f67e104 --- /dev/null +++ b/classes/PowerUsage/DeviceRaw.js @@ -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; diff --git a/classes/PowerUsage/index.js b/classes/PowerUsage/index.js new file mode 100644 index 0000000..2faaacf --- /dev/null +++ b/classes/PowerUsage/index.js @@ -0,0 +1,7 @@ +const DeviceRaw = require('./DeviceRaw'); +const CurrentMonth = require('./CurrentMonth'); + +module.exports = { + DeviceRaw, + CurrentMonth, +}; diff --git a/classes/WebSocket.js b/classes/WebSocket.js new file mode 100644 index 0000000..cedcf73 --- /dev/null +++ b/classes/WebSocket.js @@ -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; diff --git a/lib/powerUsage/currentMonthPowerUsage.js b/lib/powerUsage/currentMonthPowerUsage.js deleted file mode 100644 index a7ba7b2..0000000 --- a/lib/powerUsage/currentMonthPowerUsage.js +++ /dev/null @@ -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; diff --git a/lib/powerUsage/deviceRawPowerUsage.js b/lib/powerUsage/deviceRawPowerUsage.js deleted file mode 100644 index 2496773..0000000 --- a/lib/powerUsage/deviceRawPowerUsage.js +++ /dev/null @@ -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; diff --git a/lib/powerUsage/index.js b/lib/powerUsage/index.js deleted file mode 100644 index 537b9d4..0000000 --- a/lib/powerUsage/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const deviceRawPowerUsage = require('./deviceRawPowerUsage'); -const currentMonthPowerUsage = require('./currentMonthPowerUsage'); - -module.exports = { - deviceRawPowerUsage, - currentMonthPowerUsage, -}; diff --git a/lib/websocket.js b/lib/websocket.js deleted file mode 100644 index 60811b0..0000000 --- a/lib/websocket.js +++ /dev/null @@ -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; diff --git a/main.js b/main.js index 2a3be45..b7ffcdf 100644 --- a/main.js +++ b/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 }), }; } } diff --git a/package.json b/package.json index 462c496..408c3d0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/main-env-node.spec.js b/test/main-env-node.spec.js index 1df68fc..8369263 100644 --- a/test/main-env-node.spec.js +++ b/test/main-env-node.spec.js @@ -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'; diff --git a/test/main-env-serverless.spec.js b/test/main-env-serverless.spec.js index 68e4827..9af7be7 100644 --- a/test/main-env-serverless.spec.js +++ b/test/main-env-serverless.spec.js @@ -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; diff --git a/test/main-valid-credentials.spec.js b/test/main-valid-credentials.spec.js index a6eab58..fc91b4b 100644 --- a/test/main-valid-credentials.spec.js +++ b/test/main-valid-credentials.spec.js @@ -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'); diff --git a/test/power-usage.spec.js b/test/power-usage.spec.js index 5f0fd83..6268513 100644 --- a/test/power-usage.spec.js +++ b/test/power-usage.spec.js @@ -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);