From 098011b285d1e665c6b0b27a51f226d0ca7d058a Mon Sep 17 00:00:00 2001 From: Martin M Date: Sat, 11 Jan 2020 01:39:29 -0300 Subject: [PATCH] Release v2.0.0 (#44) * Added arpTableSolver (#18) * Added arpTableSolver * fix package import * linting class * changed arp library * refactor arp class * using arpping fork * refactor arpTableSolver class * Added Zero Conf functionality (LAN mode) (#46) * added crypto-js * zeroconf helper functions * zeroconf update payload * new method to save devices cache file * class renamed * refactor Zeroconf class * return cached device if exists * moved method to get local ip address * fix mac addresses without leading zeroes * refactor Zeroconf class * using new zeroconf functionality * zeroconf working with single and multichannel devices * save device mixin enhancement * working on zeroconf test cases * catch errors on filesystem methods * zeroconf: added extra test cases * better error handling * zeroconf: 100% code coverage * removed deprecated login method * updates on credentials file * version bump * Docs for v2.0 (#52) * added v1 docs * added zeroconf docs * updated readme * docs updated * removed zeroconf article warning * updated vscode config Co-authored-by: Luis Llamas --- .gitignore | 7 +- .vscode/settings.json | 5 +- README.md | 5 +- classes/PowerState/ChangeStateZeroconf.js | 40 ++++++ classes/PowerState/index.js | 2 + classes/Zeroconf.js | 91 +++++++++++++ docs/README.md | 27 ++++ docs/available-methods/README.md | 24 ++++ docs/available-methods/getcredentials.md | 20 +++ docs/available-methods/getdevice.md | 13 ++ .../getdevicechannelcount.md | 12 ++ .../getdevicecurrenthumidity.md | 21 +++ .../getdevicecurrenttemperature.md | 21 +++ docs/available-methods/getdevicecurrentth.md | 22 ++++ docs/available-methods/getdevicepowerstate.md | 27 ++++ docs/available-methods/getdevicepowerusage.md | 50 ++++++++ docs/available-methods/getdevices.md | 13 ++ docs/available-methods/getfirmwareversion.md | 12 ++ docs/available-methods/getregion.md | 23 ++++ docs/available-methods/login.md | 20 +++ docs/available-methods/openwebsocket.md | 70 ++++++++++ docs/available-methods/savedevicescache.md | 19 +++ docs/available-methods/setdevicepowerstate.md | 29 +++++ docs/available-methods/toggledevice.md | 27 ++++ docs/class-instantiation.md | 24 ++++ docs/demos/README.md | 8 ++ docs/demos/node.md | 30 +++++ docs/demos/serverless.md | 73 +++++++++++ docs/introduction.md | 25 ++++ docs/quickstart.md | 22 ++++ docs/testing.md | 8 ++ docs/zeroconf.md | 66 ++++++++++ lib/ewelink-helper.js | 42 ++++++ lib/payloads/index.js | 2 + lib/payloads/zeroConfUpdatePayload.js | 17 +++ main.js | 30 ++++- mixins/devices/getDeviceMixin.js | 4 + mixins/devices/getLocalIpMixin.js | 17 +++ mixins/devices/saveDevicesCacheMixin.js | 32 +++++ mixins/powerState/setDevicePowerStateMixin.js | 15 ++- mixins/user/getCredentialsMixin.js | 10 -- package-lock.json | 87 ++++++++++--- package.json | 4 +- test/_setup/credentials.example.js | 20 +++ test/_setup/credentials.json | 13 -- test/_setup/expectations.js | 2 +- test/env-node.spec.js | 2 +- test/env-serverless.spec.js | 2 +- test/firmware.spec.js | 2 +- test/invalid-credentials.spec.js | 2 +- test/power-usage.spec.js | 2 +- test/temperature-humidity.spec.js | 2 +- test/user.spec.js | 2 +- test/valid-credentials.spec.js | 2 +- test/zeroconf.spec.js | 120 ++++++++++++++++++ 55 files changed, 1227 insertions(+), 60 deletions(-) create mode 100644 classes/PowerState/ChangeStateZeroconf.js create mode 100644 classes/Zeroconf.js create mode 100644 docs/README.md create mode 100644 docs/available-methods/README.md create mode 100644 docs/available-methods/getcredentials.md create mode 100644 docs/available-methods/getdevice.md create mode 100644 docs/available-methods/getdevicechannelcount.md create mode 100644 docs/available-methods/getdevicecurrenthumidity.md create mode 100644 docs/available-methods/getdevicecurrenttemperature.md create mode 100644 docs/available-methods/getdevicecurrentth.md create mode 100644 docs/available-methods/getdevicepowerstate.md create mode 100644 docs/available-methods/getdevicepowerusage.md create mode 100644 docs/available-methods/getdevices.md create mode 100644 docs/available-methods/getfirmwareversion.md create mode 100644 docs/available-methods/getregion.md create mode 100644 docs/available-methods/login.md create mode 100644 docs/available-methods/openwebsocket.md create mode 100644 docs/available-methods/savedevicescache.md create mode 100644 docs/available-methods/setdevicepowerstate.md create mode 100644 docs/available-methods/toggledevice.md create mode 100644 docs/class-instantiation.md create mode 100644 docs/demos/README.md create mode 100644 docs/demos/node.md create mode 100644 docs/demos/serverless.md create mode 100644 docs/introduction.md create mode 100644 docs/quickstart.md create mode 100644 docs/testing.md create mode 100644 docs/zeroconf.md create mode 100644 lib/payloads/zeroConfUpdatePayload.js create mode 100644 mixins/devices/getLocalIpMixin.js create mode 100644 mixins/devices/saveDevicesCacheMixin.js create mode 100644 test/_setup/credentials.example.js delete mode 100644 test/_setup/credentials.json create mode 100644 test/zeroconf.spec.js diff --git a/.gitignore b/.gitignore index ed9b454..3a08e06 100644 --- a/.gitignore +++ b/.gitignore @@ -90,4 +90,9 @@ typings/ # End of https://www.gitignore.io/api/node -.idea/ \ No newline at end of file +.idea/ + +demo.js +arp-table.json +devices-cache.json +test/_setup/credentials.js \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 21123e3..f4e6c82 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,8 @@ "eslint.validate": [ "javascript" ], - "eslint.autoFixOnSave": true + "eslint.autoFixOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } } \ No newline at end of file diff --git a/README.md b/README.md index 8dfc950..95a2109 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,14 @@ * set on/off devices * get power consumption on devices like Sonoff POW * listen for devices events +* using zeroconf (LAN mode), no internet connection required ## Installation -``` sh +```sh npm install ewelink-api ``` ## Usage -Check docs at https://ewelink-api.now.sh \ No newline at end of file +Check library documentation and examples at https://github.com/skydiver/ewelink-api/docs \ No newline at end of file diff --git a/classes/PowerState/ChangeStateZeroconf.js b/classes/PowerState/ChangeStateZeroconf.js new file mode 100644 index 0000000..5d14cda --- /dev/null +++ b/classes/PowerState/ChangeStateZeroconf.js @@ -0,0 +1,40 @@ +const rp = require('request-promise'); + +const WebSocket = require('../WebSocket'); +const payloads = require('../../lib/payloads'); +const { _get } = require('../../lib/helpers'); + +class ChangeStateZeroconf extends WebSocket { + static async set({ url, device, params, switches, state }) { + const selfApikey = device.apikey; + const deviceId = device.deviceid; + const deviceKey = device.devicekey; + + const endpoint = switches ? 'switches' : 'switch'; + const localUrl = `${url}/${endpoint}`; + + const body = payloads.zeroConfUpdatePayload( + selfApikey, + deviceId, + deviceKey, + params + ); + + const response = await rp({ + method: 'POST', + uri: localUrl, + body, + json: true, + }); + + const error = _get(response, 'error', false); + + if (error === 403) { + return { error, msg: response.reason }; + } + + return { status: 'ok', state }; + } +} + +module.exports = ChangeStateZeroconf; diff --git a/classes/PowerState/index.js b/classes/PowerState/index.js index bed15bf..489b79a 100644 --- a/classes/PowerState/index.js +++ b/classes/PowerState/index.js @@ -1,5 +1,7 @@ const ChangeState = require('./ChangeState'); +const ChangeStateZeroconf = require('./ChangeStateZeroconf'); module.exports = { ChangeState, + ChangeStateZeroconf, }; diff --git a/classes/Zeroconf.js b/classes/Zeroconf.js new file mode 100644 index 0000000..6167fda --- /dev/null +++ b/classes/Zeroconf.js @@ -0,0 +1,91 @@ +const fs = require('fs'); +const arpping = require('arpping')({}); + +class Zeroconf { + /** + * Build the ARP table + * @param ip + * @returns {Promise} + */ + static getArpTable(ip = null) { + return new Promise((resolve, reject) => { + arpping.discover(ip, (err, hosts) => { + if (err) { + return reject(err); + } + const arpTable = Zeroconf.fixMacAddresses(hosts); + return resolve(arpTable); + }); + }); + } + + /** + * Sometime arp command returns mac addresses without leading zeroes. + * @param hosts + */ + static fixMacAddresses(hosts) { + return hosts.map(host => { + const octets = host.mac.split(':'); + + const fixedMac = octets.map(octet => { + if (octet.length === 1) { + return `0${octet}`; + } + return octet; + }); + + return { + ip: host.ip, + mac: fixedMac.join(':'), + }; + }); + } + + /** + * Save ARP table to local file + * @param config + * @returns {Promise<{error: string}|{file: {request: string; resolved: string} | any | string | string, status: string}>} + */ + static async saveArpTable(config = {}) { + const ip = config.ip || null; + const fileName = config.file || './arp-table.json'; + try { + const arpTable = await Zeroconf.getArpTable(ip); + const jsonContent = JSON.stringify(arpTable, null, 2); + fs.writeFileSync(fileName, jsonContent, 'utf8'); + return { status: 'ok', file: fileName }; + } catch (e) { + return { error: e.toString() }; + } + } + + /** + * Read ARP table file + * @param fileName + * @returns {Promise<{error: string}|any>} + */ + static async loadArpTable(fileName = './arp-table.json') { + try { + const jsonContent = await fs.readFileSync(fileName); + return JSON.parse(jsonContent); + } catch (e) { + return { error: e.toString() }; + } + } + + /** + * Read devices cache file + * @param fileName + * @returns {Promise<{error: string}>} + */ + static async loadCachedDevices(fileName = './devices-cache.json') { + try { + const jsonContent = await fs.readFileSync(fileName); + return JSON.parse(jsonContent); + } catch (e) { + return { error: e.toString() }; + } + } +} + +module.exports = Zeroconf; diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..cb416e0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,27 @@ +# Documentation + +* [Introduction](introduction.md) +* [Quickstart](quickstart.md) +* [Class Instantiation](class-instantiation.md) +* [Demos](demos/README.md) + * [node script](demos/node.md) + * [serverless](demos/serverless.md) +* [Available Methods](available-methods/README.md) + * [getCredentials](available-methods/getcredentials.md) + * [openWebSocket](available-methods/openwebsocket.md) + * [getDevice](available-methods/getdevice.md) + * [getDevices](available-methods/getdevices.md) + * [getDevicePowerState](available-methods/getdevicepowerstate.md) + * [setDevicePowerState](available-methods/setdevicepowerstate.md) + * [toggleDevice](available-methods/toggledevice.md) + * [getDevicePowerUsage](available-methods/getdevicepowerusage.md) + * [getDeviceCurrentTH](available-methods/getdevicecurrentth.md) + * [getDeviceCurrentTemperature](available-methods/getdevicecurrenttemperature.md) + * [getDeviceCurrentHumidity](available-methods/getdevicecurrenthumidity.md) + * [getDeviceChannelCount](available-methods/getdevicechannelcount.md) + * [getRegion](available-methods/getregion.md) + * [getFirmwareVersion](available-methods/getfirmwareversion.md) + * [saveDevicesCache](available-methods/savedevicescache.md) + * [login](available-methods/login.md) _*deprecated_ +* [Zeroconf (LAN mode)](zeroconf.md) +* [Testing](testing.md) \ No newline at end of file diff --git a/docs/available-methods/README.md b/docs/available-methods/README.md new file mode 100644 index 0000000..df9e995 --- /dev/null +++ b/docs/available-methods/README.md @@ -0,0 +1,24 @@ +# Available Methods + +Here is the list of available methods. + +* [getCredentials](getcredentials.md) +* [openWebSocket](openwebsocket.md) +* [getDevice](getdevice.md) +* [getDevices](getdevices.md) +* [getDevicePowerState](getdevicepowerstate.md) +* [setDevicePowerState](setdevicepowerstate.md) +* [toggleDevice](toggledevice.md) +* [getDevicePowerUsage](getdevicepowerusage.md) +* [getDeviceCurrentTH](getdevicecurrentth.md) +* [getDeviceCurrentTemperature](getdevicecurrenttemperature.md) +* [getDeviceCurrentHumidity](getdevicecurrenthumidity.md) +* [getDeviceChannelCount](getdevicechannelcount.md) +* [getRegion](getregion.md) +* [getFirmwareVersion](getfirmwareversion.md) +* [saveDevicesCache](savedevicescache.md) +* [login](login.md) _*deprecated_ + +Remember to instantiate class before usage. + +Also, take a look at the provided demos for [node script](../demos/node.md) and [serverless](../demos/serverless.md). \ No newline at end of file diff --git a/docs/available-methods/getcredentials.md b/docs/available-methods/getcredentials.md new file mode 100644 index 0000000..6482b9c --- /dev/null +++ b/docs/available-methods/getcredentials.md @@ -0,0 +1,20 @@ +# getCredentials + +Get your access token, api key and region. + +This method is useful on serverless context, where you need to obtain auth credentials to make individual requests. + + +### Usage +``` + const auth = await connection.getCredentials(); + + console.log('access token: ', auth.at); + console.log('api key: ', auth.user.apikey); + console.log('region: ', auth.region); + +``` + +* _Remember to instantiate class before use_ + +> Access token and api key will be invalidate after you login again using email and password. \ No newline at end of file diff --git a/docs/available-methods/getdevice.md b/docs/available-methods/getdevice.md new file mode 100644 index 0000000..a4eff85 --- /dev/null +++ b/docs/available-methods/getdevice.md @@ -0,0 +1,13 @@ +# getDevice + +Return information for specified device. + + +### Usage +``` + /* get specific device information */ + const device = await connection.getDevice(''); + console.log(device); +``` + +* _Remember to instantiate class before use_ \ No newline at end of file diff --git a/docs/available-methods/getdevicechannelcount.md b/docs/available-methods/getdevicechannelcount.md new file mode 100644 index 0000000..c28b9ab --- /dev/null +++ b/docs/available-methods/getdevicechannelcount.md @@ -0,0 +1,12 @@ +# getDeviceChannelCount + +Return total channels for specified device. + + +### Usage +``` + const channels = await connection.getDeviceChannelCount(''); + console.log(channels); +``` + +* _Remember to instantiate class before use_ \ No newline at end of file diff --git a/docs/available-methods/getdevicecurrenthumidity.md b/docs/available-methods/getdevicecurrenthumidity.md new file mode 100644 index 0000000..3e82df1 --- /dev/null +++ b/docs/available-methods/getdevicecurrenthumidity.md @@ -0,0 +1,21 @@ +# getDeviceCurrentHumidity + +Return current humidity for specified device. + + +### Usage +``` + const humidity = await connection.getDeviceCurrentHumidity(''); + console.log(humidity); +``` + +* _Remember to instantiate class before use_ + + +### Response example +```js + { + status: 'ok', + humidity: '76' + } +``` \ No newline at end of file diff --git a/docs/available-methods/getdevicecurrenttemperature.md b/docs/available-methods/getdevicecurrenttemperature.md new file mode 100644 index 0000000..8b55c9a --- /dev/null +++ b/docs/available-methods/getdevicecurrenttemperature.md @@ -0,0 +1,21 @@ +# getDeviceCurrentTemperature + +Return current temperature for specified device. + + +### Usage +``` + const temperature = await connection.getDeviceCurrentTemperature(''); + console.log(temperature); +``` + +* _Remember to instantiate class before use_ + + +### Response example +```js + { + status: 'ok', + temperature: '20' + } +``` \ No newline at end of file diff --git a/docs/available-methods/getdevicecurrentth.md b/docs/available-methods/getdevicecurrentth.md new file mode 100644 index 0000000..1eeb3c6 --- /dev/null +++ b/docs/available-methods/getdevicecurrentth.md @@ -0,0 +1,22 @@ +# getDeviceCurrentTH + +Return current temperature and humidity for specified device. + + +### Usage +``` + const temphumd = await connection.getDeviceCurrentTH(''); + console.log(temphumd); +``` + +* _Remember to instantiate class before use_ + + +### Response example +```js + { + status: 'ok', + temperature: '20', + humidity: '76' + } +``` \ No newline at end of file diff --git a/docs/available-methods/getdevicepowerstate.md b/docs/available-methods/getdevicepowerstate.md new file mode 100644 index 0000000..39f1074 --- /dev/null +++ b/docs/available-methods/getdevicepowerstate.md @@ -0,0 +1,27 @@ +# getDevicePowerState + +Query for specified device power status. + + +### Usage +```js + const status = await connection.getDevicePowerState(''); + console.log(status); +``` + +```js + // multi-channel devices like Sonoff 4CH + const status = await connection.getDevicePowerState('', ); + console.log(status); +``` + +* _Remember to instantiate class before use_ + + +### Response example +```js + { + status: 'ok', + state: 'off' + } +``` \ No newline at end of file diff --git a/docs/available-methods/getdevicepowerusage.md b/docs/available-methods/getdevicepowerusage.md new file mode 100644 index 0000000..cad575b --- /dev/null +++ b/docs/available-methods/getdevicepowerusage.md @@ -0,0 +1,50 @@ +# getDevicePowerUsage + +Returns current month power usage on device who supports electricity records, like Sonoff POW. + + +### Usage +``` + const usage = await connection.getDevicePowerUsage(''); + console.log(usage); +``` + +* _Remember to instantiate class before use_ + + +### Response example +```js +{ + status: 'ok', + monthly: 109.78, + daily: + [ + { day: 26, usage: 4.19 }, + { day: 25, usage: 2.11 }, + { day: 24, usage: 3.74 }, + { day: 23, usage: 8.23 }, + { day: 22, usage: 3.16 }, + { day: 21, usage: 3.95 }, + { day: 20, usage: 3.38 }, + { day: 19, usage: 4.56 }, + { day: 18, usage: 1.51 }, + { day: 17, usage: 2.4 }, + { day: 16, usage: 1.5 }, + { day: 15, usage: 7.28 }, + { day: 14, usage: 7.44 }, + { day: 13, usage: 3.21 }, + { day: 12, usage: 5.5 }, + { day: 11, usage: 4.43 }, + { day: 10, usage: 3.15 }, + { day: 9, usage: 1.33 }, + { day: 8, usage: 2.9 }, + { day: 7, usage: 6.03 }, + { day: 6, usage: 7.48 }, + { day: 5, usage: 5.94 }, + { day: 4, usage: 3.64 }, + { day: 3, usage: 2.39 }, + { day: 2, usage: 3.10 }, + { day: 1, usage: 7.23 } + ] +} +``` \ No newline at end of file diff --git a/docs/available-methods/getdevices.md b/docs/available-methods/getdevices.md new file mode 100644 index 0000000..b847782 --- /dev/null +++ b/docs/available-methods/getdevices.md @@ -0,0 +1,13 @@ +# getDevices + +Returns a list of devices associated to logged account. + + +### Usage +``` + /* get all devices */ + const devices = await connection.getDevices(); + console.log(devices); +``` + +* _Remember to instantiate class before use_ \ No newline at end of file diff --git a/docs/available-methods/getfirmwareversion.md b/docs/available-methods/getfirmwareversion.md new file mode 100644 index 0000000..a8e7c47 --- /dev/null +++ b/docs/available-methods/getfirmwareversion.md @@ -0,0 +1,12 @@ +# getFirmwareVersion + +Return firmware version for specified device. + + +### Usage +``` + const firmware = await connection.getFirmwareVersion(''); + console.log(firmware); +``` + +* _Remember to instantiate class before use_ \ No newline at end of file diff --git a/docs/available-methods/getregion.md b/docs/available-methods/getregion.md new file mode 100644 index 0000000..3b7c175 --- /dev/null +++ b/docs/available-methods/getregion.md @@ -0,0 +1,23 @@ +# getRegion + +Return logged user region. + +> This method only works if class is initialized using email and password. + + +### Usage +``` + const region = await connection.getRegion(); + console.log(region); +``` + +* _Remember to instantiate class before use_ + + +### Response example +```js + { + email: 'user@email.com', + region: 'us' + } +``` \ No newline at end of file diff --git a/docs/available-methods/login.md b/docs/available-methods/login.md new file mode 100644 index 0000000..9214db1 --- /dev/null +++ b/docs/available-methods/login.md @@ -0,0 +1,20 @@ +# Login + +> DEPRECATED: use [getCredentials](available-methods/getcredentials.md) method instead + +Login into eWeLink API and get auth credentials. + +This method is useful on serverless context, where you need to obtain auth credentials to make individual requests. + + +### Usage +``` + const auth = await connection.login(); + + console.log('access token: ', auth.at); + console.log('api key: ', auth.user.apikey); + console.log('region: ', auth.region); + +``` + +* _Remember to instantiate class before use_ \ No newline at end of file diff --git a/docs/available-methods/openwebsocket.md b/docs/available-methods/openwebsocket.md new file mode 100644 index 0000000..184ce7e --- /dev/null +++ b/docs/available-methods/openwebsocket.md @@ -0,0 +1,70 @@ +# openWebSocket + +Opens a socket connection to eWeLink and listen for realtime events. + + +### Usage + +The `openWebSocket` method requires a callback function as an argument. + +Once an event is received, the callback function will be executed with the server message as argument. + + +```js +// instantiate class +const connection = new ewelink({ + email: '', + password: '', + region: '', +}); + +// login into eWeLink +await connection.login(); + +// call openWebSocket method with a callback as argument +const socket = await connection.openWebSocket(async data => { + // data is the message from eWeLink + console.log(data) +}); +``` + + +### Response example + +If everything went well, the first message will have the following format: +```js +{ + error: 0, + apikey: '12345678-9012-3456-7890-123456789012', + config: { + hb: 1, + hbInterval: 12345 + }, + sequence: '1234567890123' +} +``` + +When a device change his status, a similar message will be returned: +```js +{ + action: 'update', + deviceid: '1234567890', + apikey: '12345678-9012-3456-7890-123456789012', + userAgent: 'device', + sequence: '1234567890123' + ts: 0, + params: { + switch: 'on' + }, + from: 'device', + seq: '11' +} +``` + + + +### Notes + +* Because of the nature of a socket connection, the script will keep running until the connection gets closed. +* `openWebSocket` will return the socket instance +* if you need to manually kill the connection, just run `socket.close()` (where socket is the variable used). \ No newline at end of file diff --git a/docs/available-methods/savedevicescache.md b/docs/available-methods/savedevicescache.md new file mode 100644 index 0000000..cfc402c --- /dev/null +++ b/docs/available-methods/savedevicescache.md @@ -0,0 +1,19 @@ +# saveDevicesCache + +Save devices cache file (required when using zeroconf) + + +### Usage +``` + const ewelink = require('ewelink-api'); + + const connection = new ewelink({ + email: '', + password: '', + region: '', + }); + + await connection.saveDevicesCache(); +``` + +A file named devices-cache.json will be created. \ No newline at end of file diff --git a/docs/available-methods/setdevicepowerstate.md b/docs/available-methods/setdevicepowerstate.md new file mode 100644 index 0000000..821f0df --- /dev/null +++ b/docs/available-methods/setdevicepowerstate.md @@ -0,0 +1,29 @@ +# setDevicePowerState + +Change specified device power state. + + +### Usage +```js + const status = await connection.setDevicePowerState('', 'on'); + console.log(status); +``` + +```js + // multi-channel devices like Sonoff 4CH + const status = await connection.setDevicePowerState('', 'toggle', ); + console.log(status); +``` + +Possible states: `on`, `off`, `toggle`. + +* _Remember to instantiate class before use_ + + +### Response example +```js + { + status: 'ok', + state: 'on' + } +``` \ No newline at end of file diff --git a/docs/available-methods/toggledevice.md b/docs/available-methods/toggledevice.md new file mode 100644 index 0000000..86ea1c1 --- /dev/null +++ b/docs/available-methods/toggledevice.md @@ -0,0 +1,27 @@ +# toggleDevice + +Switch specified device current power state. + + +### Usage +```js + const status = await connection.toggleDevice(''); + console.log(status); +``` + +```js + // multi-channel devices like Sonoff 4CH + const status = await connection.toggleDevice('', ); + console.log(status); +``` + +* _Remember to instantiate class before use_ + + +### Response example +```js + { + status: 'ok', + state: 'off' + } +``` \ No newline at end of file diff --git a/docs/class-instantiation.md b/docs/class-instantiation.md new file mode 100644 index 0000000..6e800c5 --- /dev/null +++ b/docs/class-instantiation.md @@ -0,0 +1,24 @@ +# Class Instantiation + +> Default region of this library is `us`. If your are in a different one, **you must** specify region parameter or error 400/401 will be returned. + +**_Using email and password_** +``` + const connection = new ewelink({ + email: '', + password: '', + region: '', + }); +``` + +**_Using access token and api key_** +``` + const connection = new ewelink({ + at: '', + apiKey: '', + region: '', + }); +``` + +> * If you don't know your region, use [getRegion](available-methods/getregion) method +> * To get your access token and api key, use [getCredentials](available-methods/getcredentials) method \ No newline at end of file diff --git a/docs/demos/README.md b/docs/demos/README.md new file mode 100644 index 0000000..eed9ce2 --- /dev/null +++ b/docs/demos/README.md @@ -0,0 +1,8 @@ +# Demos + +### node script +* [Read](node.md) how to use this library on a nodejs script. + + +### serverless +* [Read](serverless.md) how to integrate on your serverles environment, like a lambda function. \ No newline at end of file diff --git a/docs/demos/node.md b/docs/demos/node.md new file mode 100644 index 0000000..a638c25 --- /dev/null +++ b/docs/demos/node.md @@ -0,0 +1,30 @@ +# node script + +> Default region of this library is `us`. If your are in a different one, **you must** specify region parameter or error 400/401 will be returned. + +```js +const ewelink = require('ewelink-api'); + +(async () => { + + const connection = new ewelink({ + email: '', + password: '', + region: '', + }); + + /* get all devices */ + const devices = await connection.getDevices(); + console.log(devices); + + /* get specific devide info */ + const device = await connection.getDevice(''); + console.log(device); + + /* toggle device */ + await connection.toggleDevice(''); + +})(); +``` + +> If you don't know your region, use [getRegion](/docs/available-methods/getregion) method \ No newline at end of file diff --git a/docs/demos/serverless.md b/docs/demos/serverless.md new file mode 100644 index 0000000..2c1b4c3 --- /dev/null +++ b/docs/demos/serverless.md @@ -0,0 +1,73 @@ +# serverless + +On a serverless scenario you need to instantiate the class on every request. + +So, instead of using email and password on every api call, you can login the first time then use auth credentials for future requests. + +> Default region of this library is `us`. If your are in a different one, **you must** specify region parameter or error 400/401 will be returned. + + +```js +/* first request: get access token and api key */ +(async () => { + + const connection = new ewelink({ + email: '', + password: '', + region: '', + }); + + const login = await connection.login(); + + const accessToken = login.at; + const apiKey = login.user.apikey + const region = login.region; + +})(); +``` + +```js +/* second request: use access token to request devices */ +(async () => { + + const newConnection = new ewelink({ + at: accessToken, + region: region + }); + + const devices = await newConnection.getDevices(); + console.log(devices); + +})(); +``` + +```js +/* third request: use access token to request specific device info */ +(async () => { + + const thirdConnection = new ewelink({ + at: accessToken, + region: region + }); + + const device = await thirdConnection.getDevice(''); + console.log(device); + +})(); +``` + +```js +/* fourth request: use access token and api key to toggle specific device info */ +(async () => { + + const anotherNewConnection = new ewelink({ + at: accessToken, + region: region + }); + + await anotherNewConnection.toggleDevice(''); + +})(); +``` + +> If you don't know your region, use [getRegion](/docs/available-methods/getregion) method \ No newline at end of file diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 0000000..3acfac6 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,25 @@ +# Getting Started + +**eWeLink API for JavaScript** is a module who let you interact directly with eWeLink API using your regular credentials. + + +## Installation + +Install package on your node project: +``` +npm install ewelink-api +``` + + +## Key features + +* can run on browsers, node scripts or serverless environment +* set on/off devices +* get power consumption on devices like Sonoff POW +* listen for devices events + + +## Help, comments, contributions? + +* Repo: https://github.com/skydiver/ewelink-api +* Issue tracker: https://github.com/skydiver/ewelink-api/issues \ No newline at end of file diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..e1f70bd --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,22 @@ +# Quickstart + +Here is a basic node script to start working with the module: + +> Default region of this library is `us`. If your are in a different one, **you must** specify region parameter or error 400/401 will be returned. + +```js +const ewelink = require('ewelink-api'); + +/* instantiate class */ +const connection = new ewelink({ + email: '', + password: '', + region: '', +}); + +/* get all devices */ +const devices = await connection.getDevices(); +console.log(devices); +``` + +> If you don't know your region, use [getRegion](available-methods/getregion.md) method \ No newline at end of file diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..3a0d87a --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,8 @@ +# Testing + +Open `test/_setup/credentials.json` and update parameters. + +In a terminal, `npm run test` or `npm run coverage`. + + +> tests needs to be performed serially, so if run jest manually, add `--runInBand` parameter. \ No newline at end of file diff --git a/docs/zeroconf.md b/docs/zeroconf.md new file mode 100644 index 0000000..6aae2ff --- /dev/null +++ b/docs/zeroconf.md @@ -0,0 +1,66 @@ +# Zeroconf (LAN mode) + +> Zeroconf only works if you're connected to the same network of the device you wanna control. + + +## Notes +* at this time, only turn on/off action is available. +* after initial setup, internet connection is not required to turn on/off your devices. + + +## Introduction +Before start, you will need to create 2 files with information about your devices (the library includes methods to generate both files). +1. a cache file with information about your devices. +2. an "arp table" cache file with info from your network connected devices. +3. toggle specific device power state + + +## 1. Generate devices cache file + +```js +const ewelink = require('ewelink-api'); + +const connection = new ewelink({ + email: '', + password: '', + region: '', +}); + +await connection.saveDevicesCache(); +``` + +A file named `devices-cache.json` will be created. + + +## 2. Generate arp table cache file + +```js +const Zeroconf = require('ewelink-api/classes/Zeroconf'); + +await Zeroconf.saveArpTable({ + ip: '' +}); +``` + +A file named `arp-table.json` will be created. + + +## 3. toggle device power state + +```js +const ewelink = require('ewelink-api'); +const Zeroconf = require('ewelink-api/classes/Zeroconf'); + +/* load cache files */ +const devicesCache = await Zeroconf.loadCachedDevices(); +const arpTable = await Zeroconf.loadArpTable(); + +/* create the connection using cache files */ +const connection = new ewelink({ devicesCache, arpTable }); + +/* turn device on */ +await connection.setDevicePowerState('', 'on'); + +/* turn device off */ +await connection.setDevicePowerState('', 'off'); +``` \ No newline at end of file diff --git a/lib/ewelink-helper.js b/lib/ewelink-helper.js index 396f827..4a12d2b 100644 --- a/lib/ewelink-helper.js +++ b/lib/ewelink-helper.js @@ -1,4 +1,5 @@ const crypto = require('crypto'); +const CryptoJS = require('crypto-js'); const random = require('random'); const DEVICE_TYPE_UUID = require('./data/devices-type-uuid'); @@ -26,8 +27,49 @@ const getDeviceChannelCount = deviceUUID => { return getDeviceChannelCountByType(deviceType); }; +const create16Uiid = () => { + let result = ''; + for (let i = 0; i < 16; i += 1) { + result += random.int(0, 9); + } + return result; +}; + +const encryptionBase64 = t => + CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(t)); + +const decryptionBase64 = t => + CryptoJS.enc.Base64.parse(t).toString(CryptoJS.enc.Utf8); + +const encryptationData = (data, key) => { + const encryptedMessage = {}; + const uid = create16Uiid(); + const iv = encryptionBase64(uid); + const code = CryptoJS.AES.encrypt(data, CryptoJS.MD5(key), { + iv: CryptoJS.enc.Utf8.parse(uid), + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }); + encryptedMessage.uid = uid; + encryptedMessage.iv = iv; + encryptedMessage.data = code.ciphertext.toString(CryptoJS.enc.Base64); + return encryptedMessage; +}; + +const decryptionData = (data, key, iv) => { + const iv64 = decryptionBase64(iv); + const code = CryptoJS.AES.decrypt(data, CryptoJS.MD5(key), { + iv: CryptoJS.enc.Utf8.parse(iv64), + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }); + return code.toString(CryptoJS.enc.Utf8); +}; + module.exports = { makeAuthorizationSign, makeFakeIMEI, getDeviceChannelCount, + encryptationData, + decryptionData, }; diff --git a/lib/payloads/index.js b/lib/payloads/index.js index 98e52eb..5b68155 100644 --- a/lib/payloads/index.js +++ b/lib/payloads/index.js @@ -2,10 +2,12 @@ const firmwareUpdate = require('./firmwareUpdate'); const credentialsPayload = require('./credentialsPayload'); const wssLoginPayload = require('./wssLoginPayload'); const wssUpdatePayload = require('./wssUpdatePayload'); +const zeroConfUpdatePayload = require('./zeroConfUpdatePayload'); module.exports = { firmwareUpdate, credentialsPayload, wssLoginPayload, wssUpdatePayload, + zeroConfUpdatePayload, }; diff --git a/lib/payloads/zeroConfUpdatePayload.js b/lib/payloads/zeroConfUpdatePayload.js new file mode 100644 index 0000000..b849138 --- /dev/null +++ b/lib/payloads/zeroConfUpdatePayload.js @@ -0,0 +1,17 @@ +const { encryptationData } = require('../ewelink-helper'); + +const zeroConfUpdatePayload = (selfApikey, deviceId, deviceKey, params) => { + const encryptedData = encryptationData(JSON.stringify(params), deviceKey); + const timeStamp = new Date() / 1000; + const sequence = Math.floor(timeStamp * 1000); + return { + sequence: sequence.toString(), + deviceid: deviceId, + selfApikey, + iv: encryptedData.iv, + encrypt: true, + data: encryptedData.data, + }; +}; + +module.exports = zeroConfUpdatePayload; diff --git a/main.js b/main.js index 9e98fa1..411f797 100644 --- a/main.js +++ b/main.js @@ -3,8 +3,16 @@ const rp = require('request-promise'); const { _get } = require('./lib/helpers'); class eWeLink { - constructor({ region = 'us', email, password, at, apiKey }) { - if (!at && (!email && !password)) { + constructor({ + region = 'us', + email, + password, + at, + apiKey, + devicesCache, + arpTable, + }) { + if (!devicesCache && !arpTable && !at && (!email && !password)) { return { error: 'No credentials provided' }; } @@ -13,6 +21,8 @@ class eWeLink { this.password = password; this.at = at; this.apiKey = apiKey; + this.devicesCache = devicesCache; + this.arpTable = arpTable; } /** @@ -41,6 +51,16 @@ class eWeLink { return `wss://${this.region}-pconnect3.coolkit.cc:8080/api/ws`; } + /** + * Generate Zeroconf URL + * @param device + * @returns {string} + */ + getZeroconfUrl(device) { + const ip = this.getLocalIp(device); + return `http://${ip}:8081/zeroconf`; + } + /** * Generate http requests helpers * @@ -103,6 +123,8 @@ const getTHMixin = require('./mixins/temphumd/getTHMixin'); const getDevicesMixin = require('./mixins/devices/getDevicesMixin'); const getDeviceMixin = require('./mixins/devices/getDeviceMixin'); const getDeviceChannelCountMixin = require('./mixins/devices/getDeviceChannelCountMixin'); +const getLocalIpMixin = require('./mixins/devices/getLocalIpMixin'); +const saveDevicesCacheMixin = require('./mixins/devices/saveDevicesCacheMixin'); /* LOAD MIXINS: firmware */ const getFirmwareVersionMixin = require('./mixins/firmware/getFirmwareVersionMixin'); @@ -133,7 +155,9 @@ Object.assign( eWeLink.prototype, getDevicesMixin, getDeviceMixin, - getDeviceChannelCountMixin + getDeviceChannelCountMixin, + getLocalIpMixin, + saveDevicesCacheMixin ); Object.assign( diff --git a/mixins/devices/getDeviceMixin.js b/mixins/devices/getDeviceMixin.js index 982bd8d..4d97a83 100644 --- a/mixins/devices/getDeviceMixin.js +++ b/mixins/devices/getDeviceMixin.js @@ -9,6 +9,10 @@ const getDeviceMixin = { * @returns {Promise<{msg: string, error: *}>} */ async getDevice(deviceId) { + if (this.devicesCache) { + return this.devicesCache.find(dev => dev.deviceid === deviceId) || null; + } + const devices = await this.getDevices(); const error = _get(devices, 'error', false); diff --git a/mixins/devices/getLocalIpMixin.js b/mixins/devices/getLocalIpMixin.js new file mode 100644 index 0000000..b58c0d6 --- /dev/null +++ b/mixins/devices/getLocalIpMixin.js @@ -0,0 +1,17 @@ +const getLocalIpMixin = { + /** + * Get local IP address from a given MAC + * + * @param device + * @returns {Promise} + */ + getLocalIp(device) { + const mac = device.extra.extra.staMac; + const arpItem = this.arpTable.find( + item => item.mac.toLowerCase() === mac.toLowerCase() + ); + return arpItem.ip; + }, +}; + +module.exports = getLocalIpMixin; diff --git a/mixins/devices/saveDevicesCacheMixin.js b/mixins/devices/saveDevicesCacheMixin.js new file mode 100644 index 0000000..0954f29 --- /dev/null +++ b/mixins/devices/saveDevicesCacheMixin.js @@ -0,0 +1,32 @@ +const fs = require('fs'); + +const { _get } = require('../../lib/helpers'); + +const saveDevicesCacheMixin = { + /** + * Save devices cache file (useful for using zeroconf) + * @returns {Promise} + */ + async saveDevicesCache(fileName = './devices-cache.json') { + const devices = await this.getDevices(); + + const error = _get(devices, 'error', false); + + if (error || !devices) { + console.log(devices); + return devices; + } + + const jsonContent = JSON.stringify(devices, null, 2); + + try { + fs.writeFileSync(fileName, jsonContent, 'utf8'); + return { status: 'ok', file: fileName }; + } catch (e) { + console.log('An error occured while writing JSON Object to File.'); + return { error: e.toString() }; + } + }, +}; + +module.exports = saveDevicesCacheMixin; diff --git a/mixins/powerState/setDevicePowerStateMixin.js b/mixins/powerState/setDevicePowerStateMixin.js index 29c82e2..5bf7a9c 100644 --- a/mixins/powerState/setDevicePowerStateMixin.js +++ b/mixins/powerState/setDevicePowerStateMixin.js @@ -1,7 +1,10 @@ const { _get } = require('../../lib/helpers'); const { getDeviceChannelCount } = require('../../lib/ewelink-helper'); -const { ChangeState } = require('../../classes/PowerState'); +const { + ChangeState, + ChangeStateZeroconf, +} = require('../../classes/PowerState'); const setDevicePowerState = { /** @@ -53,6 +56,16 @@ const setDevicePowerState = { params.switch = stateToSwitch; } + if (this.devicesCache) { + return ChangeStateZeroconf.set({ + url: this.getZeroconfUrl(device), + device, + params, + switches, + state: stateToSwitch, + }); + } + const actionParams = { apiUrl: this.getApiWebSocket(), at: this.at, diff --git a/mixins/user/getCredentialsMixin.js b/mixins/user/getCredentialsMixin.js index 0b243e4..4814301 100644 --- a/mixins/user/getCredentialsMixin.js +++ b/mixins/user/getCredentialsMixin.js @@ -44,16 +44,6 @@ const getCredentialsMixin = { this.at = _get(response, 'at', ''); return response; }, - - /** - * DEPRECATED: keept for backward compatibility - * Will be removed on version 2.0 - * - * @returns {Promise<{msg: string, error: *}>} - */ - async login() { - return this.getCredentials(); - }, }; module.exports = getCredentialsMixin; diff --git a/package-lock.json b/package-lock.json index 9eba8cb..cb334d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ewelink-api", - "version": "1.10.0", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -610,6 +610,14 @@ "commander": "^2.11.0" } }, + "arpping": { + "version": "github:skydiver/arpping#7fa7085fc55e8a0c5b3893d25505e79ba93fdb31", + "from": "github:skydiver/arpping", + "requires": { + "child_process": "^1.0.2", + "os": "^0.1.1" + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -1053,6 +1061,11 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "child_process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", + "integrity": "sha1-sffn/HPSXn/R1FWtyU4UODAYK1o=" + }, "chnl": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/chnl/-/chnl-0.5.0.tgz", @@ -1233,6 +1246,11 @@ "which": "^1.2.9" } }, + "crypto-js": { + "version": "3.1.9-1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", + "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" + }, "cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", @@ -2171,7 +2189,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2192,12 +2211,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2212,17 +2233,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2339,7 +2363,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2351,6 +2376,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2365,6 +2391,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2372,12 +2399,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2396,6 +2425,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2476,7 +2506,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2488,6 +2519,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2573,7 +2605,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2609,6 +2642,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2628,6 +2662,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2671,12 +2706,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -2768,9 +2805,9 @@ "dev": true }, "handlebars": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.2.0.tgz", - "integrity": "sha512-Kb4xn5Qh1cxAKvQnzNWZ512DhABzyFNmsaJf3OAkWNa4NkaqWcNI8Tao8Tasi0/F4JD9oyG0YxuFyvyR57d+Gw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -4417,6 +4454,11 @@ "wordwrap": "~1.0.0" } }, + "os": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/os/-/os-0.1.1.tgz", + "integrity": "sha1-IIhF6J4ZOtTZcUdLk5R3NqVtE/M=" + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -5732,16 +5774,23 @@ } }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.0.tgz", + "integrity": "sha512-PC/ee458NEMITe1OufAjal65i6lB58R1HWMRcxwvdz1UopW0DYqlRL3xdu3IcTvTXsB02CRHykidkTRL+A3hQA==", "dev": true, "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" }, "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 2c02d11..89aa83b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ewelink-api", - "version": "1.10.0", + "version": "2.0.0", "description": "eWeLink API for Node.js", "author": "Martín M.", "license": "MIT", @@ -37,6 +37,8 @@ ] }, "dependencies": { + "arpping": "github:skydiver/arpping", + "crypto-js": "^3.1.9-1", "delay": "^4.3.0", "nonce": "^1.0.4", "random": "^2.1.1", diff --git a/test/_setup/credentials.example.js b/test/_setup/credentials.example.js new file mode 100644 index 0000000..50097ba --- /dev/null +++ b/test/_setup/credentials.example.js @@ -0,0 +1,20 @@ +module.exports = { + // eWeLink credentials + email: '', + password: '', + region: '', + + // zeroconf + localIp: '', + localIpInvalid: '', + + // devices + singleChannelDeviceId: '', + deviceIdWithPower: '', + deviceIdWithoutPower: '', + deviceIdWithTempAndHum: '', + deviceIdWithoutTempAndHum: '', + fourChannelsDevice: '', + outdatedFirmwareDevice: '', + updatedFirmwareDevice: '', +}; diff --git a/test/_setup/credentials.json b/test/_setup/credentials.json deleted file mode 100644 index 8a66ef5..0000000 --- a/test/_setup/credentials.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "email": "", - "password": "", - "region": "", - "singleChannelDeviceId": "", - "deviceIdWithPower": "", - "deviceIdWithoutPower": "", - "deviceIdWithTempAndHum": "", - "deviceIdWithoutTempAndHum": "", - "fourChannelsDevice": "", - "outdatedFirmwareDevice": "", - "updatedFirmwareDevice": "" -} \ No newline at end of file diff --git a/test/_setup/expectations.js b/test/_setup/expectations.js index 35c71a3..a21d90a 100644 --- a/test/_setup/expectations.js +++ b/test/_setup/expectations.js @@ -56,7 +56,7 @@ const regionExpectations = { }; module.exports = { - credentialsExpectations: credentialsExpectations, + credentialsExpectations, allDevicesExpectations, specificDeviceExpectations, rawPowerUsageExpectations, diff --git a/test/env-node.spec.js b/test/env-node.spec.js index 2291fc9..4b3abfe 100644 --- a/test/env-node.spec.js +++ b/test/env-node.spec.js @@ -7,7 +7,7 @@ const { password, singleChannelDeviceId, fourChannelsDevice, -} = require('./_setup/credentials.json'); +} = require('./_setup/credentials.js'); const { credentialsExpectations, diff --git a/test/env-serverless.spec.js b/test/env-serverless.spec.js index d586198..902c65e 100644 --- a/test/env-serverless.spec.js +++ b/test/env-serverless.spec.js @@ -7,7 +7,7 @@ const { password, singleChannelDeviceId, fourChannelsDevice, -} = require('./_setup/credentials.json'); +} = require('./_setup/credentials.js'); const { credentialsExpectations, diff --git a/test/firmware.spec.js b/test/firmware.spec.js index 3bfc70e..71e1119 100644 --- a/test/firmware.spec.js +++ b/test/firmware.spec.js @@ -6,7 +6,7 @@ const { singleChannelDeviceId, outdatedFirmwareDevice, updatedFirmwareDevice, -} = require('./_setup/credentials.json'); +} = require('./_setup/credentials.js'); const { firmwareExpectations } = require('./_setup/expectations'); diff --git a/test/invalid-credentials.spec.js b/test/invalid-credentials.spec.js index 2fac8c0..0e9bc5f 100644 --- a/test/invalid-credentials.spec.js +++ b/test/invalid-credentials.spec.js @@ -6,7 +6,7 @@ const { singleChannelDeviceId, deviceIdWithPower, fourChannelsDevice, -} = require('./_setup/credentials.json'); +} = require('./_setup/credentials.js'); describe('invalid credentials', () => { beforeEach(async () => { diff --git a/test/power-usage.spec.js b/test/power-usage.spec.js index ca18105..9486b6c 100644 --- a/test/power-usage.spec.js +++ b/test/power-usage.spec.js @@ -6,7 +6,7 @@ const { email, password, deviceIdWithPower, -} = require('./_setup/credentials.json'); +} = require('./_setup/credentials.js'); const { rawPowerUsageExpectations, diff --git a/test/temperature-humidity.spec.js b/test/temperature-humidity.spec.js index db66f4d..707b0c7 100644 --- a/test/temperature-humidity.spec.js +++ b/test/temperature-humidity.spec.js @@ -7,7 +7,7 @@ const { password, deviceIdWithoutTempAndHum, deviceIdWithTempAndHum: thDevice, -} = require('./_setup/credentials.json'); +} = require('./_setup/credentials.js'); describe('current temperature and humidity: node script', () => { let conn; diff --git a/test/user.spec.js b/test/user.spec.js index 053427a..7a065a1 100644 --- a/test/user.spec.js +++ b/test/user.spec.js @@ -1,6 +1,6 @@ const ewelink = require('../main'); -const { email, password, region } = require('./_setup/credentials.json'); +const { email, password, region } = require('./_setup/credentials.js'); const { regionExpectations } = require('./_setup/expectations'); diff --git a/test/valid-credentials.spec.js b/test/valid-credentials.spec.js index 9da7d24..e2de022 100644 --- a/test/valid-credentials.spec.js +++ b/test/valid-credentials.spec.js @@ -7,7 +7,7 @@ const { password, deviceIdWithoutPower, fourChannelsDevice, -} = require('./_setup/credentials.json'); +} = require('./_setup/credentials.js'); const { credentialsExpectations } = require('./_setup/expectations'); diff --git a/test/zeroconf.spec.js b/test/zeroconf.spec.js new file mode 100644 index 0000000..e4f9813 --- /dev/null +++ b/test/zeroconf.spec.js @@ -0,0 +1,120 @@ +const ewelink = require('../main'); +const Zeroconf = require('../classes/Zeroconf'); + +const { + email, + password, + region, + localIp, + localIpInvalid, +} = require('./_setup/credentials.js'); + +const { allDevicesExpectations } = require('./_setup/expectations'); + +describe('zeroconf: save devices to cache file', () => { + test('can save cached devices file', async () => { + jest.setTimeout(30000); + const file = './test/_setup/devices-cache.json'; + const conn = new ewelink({ region, email, password }); + const result = await conn.saveDevicesCache(file); + expect(typeof result).toBe('object'); + expect(result.status).toBe('ok'); + expect(result.file).toBe(file); + }); + + test('error saving cached devices file', async () => { + jest.setTimeout(30000); + const file = '/tmp/non-existent-folder/devices-cache.json'; + const conn = new ewelink({ region, email, password }); + const result = await conn.saveDevicesCache(file); + expect(typeof result).toBe('object'); + expect(result.error).toContain('ENOENT: no such file or directory'); + }); + + test('invalid credentials trying to create cached devices file', async () => { + const file = '/tmp/non-existent-folder/devices-cache.json'; + const conn = new ewelink({ email: 'invalid', password: 'credentials' }); + const result = await conn.saveDevicesCache(file); + expect(typeof result).toBe('object'); + expect(result.msg).toBe('Authentication error'); + expect(result.error).toBe(401); + }); +}); + +describe('zeroconf: save arp table to file', () => { + test('can save arp table file', async () => { + jest.setTimeout(30000); + const file = './test/_setup/arp-table.json'; + const arpTable = await Zeroconf.saveArpTable({ + ip: localIp, + file, + }); + expect(typeof arpTable).toBe('object'); + expect(arpTable.status).toBe('ok'); + expect(arpTable.file).toBe(file); + }); + + test('error saving arp table file', async () => { + jest.setTimeout(30000); + const file = '/tmp/non-existent-folder/arp-table.json'; + const arpTable = await Zeroconf.saveArpTable({ + ip: localIp, + file, + }); + expect(typeof arpTable).toBe('object'); + expect(arpTable.error).toContain('ENOENT: no such file or directory'); + }); + + test('error saving arp table file with invalid local network', async () => { + jest.setTimeout(30000); + const file = './test/_setup/arp-table.json'; + const arpTable = await Zeroconf.saveArpTable({ + ip: localIpInvalid, + file, + }); + expect(typeof arpTable).toBe('object'); + expect(arpTable.error).toBe('Error: range must not be empty'); + }); +}); + +describe('zeroconf: load devices to cache file', () => { + test('can load cached devices file', async () => { + jest.setTimeout(30000); + const conn = new ewelink({ region, email, password }); + const devices = await conn.getDevices(); + const devicesCache = await Zeroconf.loadCachedDevices( + './test/_setup/devices-cache.json' + ); + expect(typeof devicesCache).toBe('object'); + expect(devicesCache.length).toBe(devices.length); + expect(devices[0]).toMatchObject(allDevicesExpectations); + }); + + test('error trying to load invalidcached devices file', async () => { + jest.setTimeout(30000); + const devicesCache = await Zeroconf.loadCachedDevices('file-not-found'); + expect(typeof devicesCache).toBe('object'); + expect(devicesCache.error).toContain('ENOENT: no such file or directory'); + }); +}); + +describe('zeroconf: load arp table file', () => { + test('can load arp table file', async () => { + const arpTable = await Zeroconf.loadArpTable( + './test/_setup/arp-table.json' + ); + expect(typeof arpTable).toBe('object'); + expect(arpTable[0]).toMatchObject({ + ip: expect.any(String), + mac: expect.any(String), + }); + }); + + test('error trying to load invalidcached devices file', async () => { + const arpTable = await Zeroconf.loadArpTable( + '/tmp/non-existent-folder/arp-table.json' + ); + expect(typeof arpTable).toBe('object'); + expect(arpTable.error).toContain('ENOENT: no such file or directory'); + }); +});