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 <luisllamas@hotmail.com>
This commit is contained in:
Martin M
2020-01-11 01:39:29 -03:00
committed by GitHub
parent 6e240c181f
commit 098011b285
55 changed files with 1227 additions and 60 deletions

7
.gitignore vendored
View File

@@ -90,4 +90,9 @@ typings/
# End of https://www.gitignore.io/api/node # End of https://www.gitignore.io/api/node
.idea/ .idea/
demo.js
arp-table.json
devices-cache.json
test/_setup/credentials.js

View File

@@ -2,5 +2,8 @@
"eslint.validate": [ "eslint.validate": [
"javascript" "javascript"
], ],
"eslint.autoFixOnSave": true "eslint.autoFixOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
} }

View File

@@ -7,13 +7,14 @@
* set on/off devices * set on/off devices
* get power consumption on devices like Sonoff POW * get power consumption on devices like Sonoff POW
* listen for devices events * listen for devices events
* using zeroconf (LAN mode), no internet connection required
## Installation ## Installation
``` sh ```sh
npm install ewelink-api npm install ewelink-api
``` ```
## Usage ## Usage
Check docs at https://ewelink-api.now.sh Check library documentation and examples at https://github.com/skydiver/ewelink-api/docs

View File

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

View File

@@ -1,5 +1,7 @@
const ChangeState = require('./ChangeState'); const ChangeState = require('./ChangeState');
const ChangeStateZeroconf = require('./ChangeStateZeroconf');
module.exports = { module.exports = {
ChangeState, ChangeState,
ChangeStateZeroconf,
}; };

91
classes/Zeroconf.js Normal file
View File

@@ -0,0 +1,91 @@
const fs = require('fs');
const arpping = require('arpping')({});
class Zeroconf {
/**
* Build the ARP table
* @param ip
* @returns {Promise<unknown>}
*/
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;

27
docs/README.md Normal file
View File

@@ -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) <sup>_*deprecated_</sup>
* [Zeroconf (LAN mode)](zeroconf.md)
* [Testing](testing.md)

View File

@@ -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) <sup>_*deprecated_</sup>
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).

View File

@@ -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);
```
<sup>* _Remember to instantiate class before use_</sup>
> Access token and api key will be invalidate after you login again using email and password.

View File

@@ -0,0 +1,13 @@
# getDevice
Return information for specified device.
### Usage
```
/* get specific device information */
const device = await connection.getDevice('<your device id>');
console.log(device);
```
<sup>* _Remember to instantiate class before use_</sup>

View File

@@ -0,0 +1,12 @@
# getDeviceChannelCount
Return total channels for specified device.
### Usage
```
const channels = await connection.getDeviceChannelCount('<your device id>');
console.log(channels);
```
<sup>* _Remember to instantiate class before use_</sup>

View File

@@ -0,0 +1,21 @@
# getDeviceCurrentHumidity
Return current humidity for specified device.
### Usage
```
const humidity = await connection.getDeviceCurrentHumidity('<your device id>');
console.log(humidity);
```
<sup>* _Remember to instantiate class before use_</sup>
### Response example
```js
{
status: 'ok',
humidity: '76'
}
```

View File

@@ -0,0 +1,21 @@
# getDeviceCurrentTemperature
Return current temperature for specified device.
### Usage
```
const temperature = await connection.getDeviceCurrentTemperature('<your device id>');
console.log(temperature);
```
<sup>* _Remember to instantiate class before use_</sup>
### Response example
```js
{
status: 'ok',
temperature: '20'
}
```

View File

@@ -0,0 +1,22 @@
# getDeviceCurrentTH
Return current temperature and humidity for specified device.
### Usage
```
const temphumd = await connection.getDeviceCurrentTH('<your device id>');
console.log(temphumd);
```
<sup>* _Remember to instantiate class before use_</sup>
### Response example
```js
{
status: 'ok',
temperature: '20',
humidity: '76'
}
```

View File

@@ -0,0 +1,27 @@
# getDevicePowerState
Query for specified device power status.
### Usage
```js
const status = await connection.getDevicePowerState('<your device id>');
console.log(status);
```
```js
// multi-channel devices like Sonoff 4CH
const status = await connection.getDevicePowerState('<your device id>', <channel>);
console.log(status);
```
<sup>* _Remember to instantiate class before use_</sup>
### Response example
```js
{
status: 'ok',
state: 'off'
}
```

View File

@@ -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('<your device id>');
console.log(usage);
```
<sup>* _Remember to instantiate class before use_</sup>
### 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 }
]
}
```

View File

@@ -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);
```
<sup>* _Remember to instantiate class before use_</sup>

View File

@@ -0,0 +1,12 @@
# getFirmwareVersion
Return firmware version for specified device.
### Usage
```
const firmware = await connection.getFirmwareVersion('<your device id>');
console.log(firmware);
```
<sup>* _Remember to instantiate class before use_</sup>

View File

@@ -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);
```
<sup>* _Remember to instantiate class before use_</sup>
### Response example
```js
{
email: 'user@email.com',
region: 'us'
}
```

View File

@@ -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);
```
<sup>* _Remember to instantiate class before use_</sup>

View File

@@ -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: '<your ewelink email>',
password: '<your ewelink password>',
region: '<your ewelink 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).

View File

@@ -0,0 +1,19 @@
# saveDevicesCache
Save devices cache file (required when using zeroconf)
### Usage
```
const ewelink = require('ewelink-api');
const connection = new ewelink({
email: '<your ewelink email>',
password: '<your ewelink password>',
region: '<your ewelink region>',
});
await connection.saveDevicesCache();
```
A file named devices-cache.json will be created.

View File

@@ -0,0 +1,29 @@
# setDevicePowerState
Change specified device power state.
### Usage
```js
const status = await connection.setDevicePowerState('<your device id>', 'on');
console.log(status);
```
```js
// multi-channel devices like Sonoff 4CH
const status = await connection.setDevicePowerState('<your device id>', 'toggle', <channel>);
console.log(status);
```
Possible states: `on`, `off`, `toggle`.
<sup>* _Remember to instantiate class before use_</sup>
### Response example
```js
{
status: 'ok',
state: 'on'
}
```

View File

@@ -0,0 +1,27 @@
# toggleDevice
Switch specified device current power state.
### Usage
```js
const status = await connection.toggleDevice('<your device id>');
console.log(status);
```
```js
// multi-channel devices like Sonoff 4CH
const status = await connection.toggleDevice('<your device id>', <channel>);
console.log(status);
```
<sup>* _Remember to instantiate class before use_</sup>
### Response example
```js
{
status: 'ok',
state: 'off'
}
```

View File

@@ -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: '<your ewelink email>',
password: '<your ewelink password>',
region: '<your ewelink region>',
});
```
**_Using access token and api key_**
```
const connection = new ewelink({
at: '<valid access token>',
apiKey: '<valid api key>',
region: '<your ewelink 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

8
docs/demos/README.md Normal file
View File

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

30
docs/demos/node.md Normal file
View File

@@ -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: '<your ewelink email>',
password: '<your ewelink password>',
region: '<your ewelink region>',
});
/* get all devices */
const devices = await connection.getDevices();
console.log(devices);
/* get specific devide info */
const device = await connection.getDevice('<your device id>');
console.log(device);
/* toggle device */
await connection.toggleDevice('<your device id>');
})();
```
> If you don't know your region, use [getRegion](/docs/available-methods/getregion) method

73
docs/demos/serverless.md Normal file
View File

@@ -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: '<your ewelink email>',
password: '<your ewelink password>',
region: '<your ewelink 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('<your device id>');
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('<your device id>');
})();
```
> If you don't know your region, use [getRegion](/docs/available-methods/getregion) method

25
docs/introduction.md Normal file
View File

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

22
docs/quickstart.md Normal file
View File

@@ -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: '<your ewelink email>',
password: '<your ewelink password>',
region: '<your ewelink 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

8
docs/testing.md Normal file
View File

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

66
docs/zeroconf.md Normal file
View File

@@ -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: '<your ewelink email>',
password: '<your ewelink password>',
region: '<your ewelink 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: '<your network addres, ex: 192.168.5.1>'
});
```
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('<your device id>', 'on');
/* turn device off */
await connection.setDevicePowerState('<your device id>', 'off');
```

View File

@@ -1,4 +1,5 @@
const crypto = require('crypto'); const crypto = require('crypto');
const CryptoJS = require('crypto-js');
const random = require('random'); const random = require('random');
const DEVICE_TYPE_UUID = require('./data/devices-type-uuid'); const DEVICE_TYPE_UUID = require('./data/devices-type-uuid');
@@ -26,8 +27,49 @@ const getDeviceChannelCount = deviceUUID => {
return getDeviceChannelCountByType(deviceType); 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 = { module.exports = {
makeAuthorizationSign, makeAuthorizationSign,
makeFakeIMEI, makeFakeIMEI,
getDeviceChannelCount, getDeviceChannelCount,
encryptationData,
decryptionData,
}; };

View File

@@ -2,10 +2,12 @@ const firmwareUpdate = require('./firmwareUpdate');
const credentialsPayload = require('./credentialsPayload'); const credentialsPayload = require('./credentialsPayload');
const wssLoginPayload = require('./wssLoginPayload'); const wssLoginPayload = require('./wssLoginPayload');
const wssUpdatePayload = require('./wssUpdatePayload'); const wssUpdatePayload = require('./wssUpdatePayload');
const zeroConfUpdatePayload = require('./zeroConfUpdatePayload');
module.exports = { module.exports = {
firmwareUpdate, firmwareUpdate,
credentialsPayload, credentialsPayload,
wssLoginPayload, wssLoginPayload,
wssUpdatePayload, wssUpdatePayload,
zeroConfUpdatePayload,
}; };

View File

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

30
main.js
View File

@@ -3,8 +3,16 @@ const rp = require('request-promise');
const { _get } = require('./lib/helpers'); const { _get } = require('./lib/helpers');
class eWeLink { class eWeLink {
constructor({ region = 'us', email, password, at, apiKey }) { constructor({
if (!at && (!email && !password)) { region = 'us',
email,
password,
at,
apiKey,
devicesCache,
arpTable,
}) {
if (!devicesCache && !arpTable && !at && (!email && !password)) {
return { error: 'No credentials provided' }; return { error: 'No credentials provided' };
} }
@@ -13,6 +21,8 @@ class eWeLink {
this.password = password; this.password = password;
this.at = at; this.at = at;
this.apiKey = apiKey; 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`; 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 * Generate http requests helpers
* *
@@ -103,6 +123,8 @@ const getTHMixin = require('./mixins/temphumd/getTHMixin');
const getDevicesMixin = require('./mixins/devices/getDevicesMixin'); const getDevicesMixin = require('./mixins/devices/getDevicesMixin');
const getDeviceMixin = require('./mixins/devices/getDeviceMixin'); const getDeviceMixin = require('./mixins/devices/getDeviceMixin');
const getDeviceChannelCountMixin = require('./mixins/devices/getDeviceChannelCountMixin'); const getDeviceChannelCountMixin = require('./mixins/devices/getDeviceChannelCountMixin');
const getLocalIpMixin = require('./mixins/devices/getLocalIpMixin');
const saveDevicesCacheMixin = require('./mixins/devices/saveDevicesCacheMixin');
/* LOAD MIXINS: firmware */ /* LOAD MIXINS: firmware */
const getFirmwareVersionMixin = require('./mixins/firmware/getFirmwareVersionMixin'); const getFirmwareVersionMixin = require('./mixins/firmware/getFirmwareVersionMixin');
@@ -133,7 +155,9 @@ Object.assign(
eWeLink.prototype, eWeLink.prototype,
getDevicesMixin, getDevicesMixin,
getDeviceMixin, getDeviceMixin,
getDeviceChannelCountMixin getDeviceChannelCountMixin,
getLocalIpMixin,
saveDevicesCacheMixin
); );
Object.assign( Object.assign(

View File

@@ -9,6 +9,10 @@ const getDeviceMixin = {
* @returns {Promise<{msg: string, error: *}>} * @returns {Promise<{msg: string, error: *}>}
*/ */
async getDevice(deviceId) { async getDevice(deviceId) {
if (this.devicesCache) {
return this.devicesCache.find(dev => dev.deviceid === deviceId) || null;
}
const devices = await this.getDevices(); const devices = await this.getDevices();
const error = _get(devices, 'error', false); const error = _get(devices, 'error', false);

View File

@@ -0,0 +1,17 @@
const getLocalIpMixin = {
/**
* Get local IP address from a given MAC
*
* @param device
* @returns {Promise<string>}
*/
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;

View File

@@ -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<string|{msg: string, error: number}|*|Device[]|{msg: string, error: number}>}
*/
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;

View File

@@ -1,7 +1,10 @@
const { _get } = require('../../lib/helpers'); const { _get } = require('../../lib/helpers');
const { getDeviceChannelCount } = require('../../lib/ewelink-helper'); const { getDeviceChannelCount } = require('../../lib/ewelink-helper');
const { ChangeState } = require('../../classes/PowerState'); const {
ChangeState,
ChangeStateZeroconf,
} = require('../../classes/PowerState');
const setDevicePowerState = { const setDevicePowerState = {
/** /**
@@ -53,6 +56,16 @@ const setDevicePowerState = {
params.switch = stateToSwitch; params.switch = stateToSwitch;
} }
if (this.devicesCache) {
return ChangeStateZeroconf.set({
url: this.getZeroconfUrl(device),
device,
params,
switches,
state: stateToSwitch,
});
}
const actionParams = { const actionParams = {
apiUrl: this.getApiWebSocket(), apiUrl: this.getApiWebSocket(),
at: this.at, at: this.at,

View File

@@ -44,16 +44,6 @@ const getCredentialsMixin = {
this.at = _get(response, 'at', ''); this.at = _get(response, 'at', '');
return response; 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; module.exports = getCredentialsMixin;

87
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "ewelink-api", "name": "ewelink-api",
"version": "1.10.0", "version": "2.0.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -610,6 +610,14 @@
"commander": "^2.11.0" "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": { "arr-diff": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -1053,6 +1061,11 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true "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": { "chnl": {
"version": "0.5.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/chnl/-/chnl-0.5.0.tgz", "resolved": "https://registry.npmjs.org/chnl/-/chnl-0.5.0.tgz",
@@ -1233,6 +1246,11 @@
"which": "^1.2.9" "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": { "cssom": {
"version": "0.3.8", "version": "0.3.8",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
@@ -2171,7 +2189,8 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@@ -2192,12 +2211,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -2212,17 +2233,20 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@@ -2339,7 +2363,8 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@@ -2351,6 +2376,7 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@@ -2365,6 +2391,7 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@@ -2372,12 +2399,14 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@@ -2396,6 +2425,7 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@@ -2476,7 +2506,8 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@@ -2488,6 +2519,7 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@@ -2573,7 +2605,8 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@@ -2609,6 +2642,7 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@@ -2628,6 +2662,7 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@@ -2671,12 +2706,14 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
} }
} }
}, },
@@ -2768,9 +2805,9 @@
"dev": true "dev": true
}, },
"handlebars": { "handlebars": {
"version": "4.2.0", "version": "4.5.3",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.2.0.tgz", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz",
"integrity": "sha512-Kb4xn5Qh1cxAKvQnzNWZ512DhABzyFNmsaJf3OAkWNa4NkaqWcNI8Tao8Tasi0/F4JD9oyG0YxuFyvyR57d+Gw==", "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==",
"dev": true, "dev": true,
"requires": { "requires": {
"neo-async": "^2.6.0", "neo-async": "^2.6.0",
@@ -4417,6 +4454,11 @@
"wordwrap": "~1.0.0" "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": { "os-tmpdir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@@ -5732,16 +5774,23 @@
} }
}, },
"uglify-js": { "uglify-js": {
"version": "3.6.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.0.tgz",
"integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", "integrity": "sha512-PC/ee458NEMITe1OufAjal65i6lB58R1HWMRcxwvdz1UopW0DYqlRL3xdu3IcTvTXsB02CRHykidkTRL+A3hQA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
"commander": "~2.20.0", "commander": "~2.20.3",
"source-map": "~0.6.1" "source-map": "~0.6.1"
}, },
"dependencies": { "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": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "ewelink-api", "name": "ewelink-api",
"version": "1.10.0", "version": "2.0.0",
"description": "eWeLink API for Node.js", "description": "eWeLink API for Node.js",
"author": "Martín M.", "author": "Martín M.",
"license": "MIT", "license": "MIT",
@@ -37,6 +37,8 @@
] ]
}, },
"dependencies": { "dependencies": {
"arpping": "github:skydiver/arpping",
"crypto-js": "^3.1.9-1",
"delay": "^4.3.0", "delay": "^4.3.0",
"nonce": "^1.0.4", "nonce": "^1.0.4",
"random": "^2.1.1", "random": "^2.1.1",

View File

@@ -0,0 +1,20 @@
module.exports = {
// eWeLink credentials
email: '<your ewelink email>',
password: '<your ewelink password>',
region: '<your ewelink region>',
// zeroconf
localIp: '<your network ip addreess>',
localIpInvalid: '<an invalid network address>',
// devices
singleChannelDeviceId: '<your device id>',
deviceIdWithPower: '<your device id>',
deviceIdWithoutPower: '<your device id>',
deviceIdWithTempAndHum: '<your device id>',
deviceIdWithoutTempAndHum: '<your device id>',
fourChannelsDevice: '<your device id>',
outdatedFirmwareDevice: '<your device id>',
updatedFirmwareDevice: '<your device id>',
};

View File

@@ -1,13 +0,0 @@
{
"email": "<your ewelink email>",
"password": "<your ewelink password>",
"region": "<your ewelink region>",
"singleChannelDeviceId": "<your device id>",
"deviceIdWithPower": "<your device id>",
"deviceIdWithoutPower": "<your device id>",
"deviceIdWithTempAndHum": "<your device id>",
"deviceIdWithoutTempAndHum": "<your device id>",
"fourChannelsDevice": "<your device id>",
"outdatedFirmwareDevice": "<your device id>",
"updatedFirmwareDevice": "<your device id>"
}

View File

@@ -56,7 +56,7 @@ const regionExpectations = {
}; };
module.exports = { module.exports = {
credentialsExpectations: credentialsExpectations, credentialsExpectations,
allDevicesExpectations, allDevicesExpectations,
specificDeviceExpectations, specificDeviceExpectations,
rawPowerUsageExpectations, rawPowerUsageExpectations,

View File

@@ -7,7 +7,7 @@ const {
password, password,
singleChannelDeviceId, singleChannelDeviceId,
fourChannelsDevice, fourChannelsDevice,
} = require('./_setup/credentials.json'); } = require('./_setup/credentials.js');
const { const {
credentialsExpectations, credentialsExpectations,

View File

@@ -7,7 +7,7 @@ const {
password, password,
singleChannelDeviceId, singleChannelDeviceId,
fourChannelsDevice, fourChannelsDevice,
} = require('./_setup/credentials.json'); } = require('./_setup/credentials.js');
const { const {
credentialsExpectations, credentialsExpectations,

View File

@@ -6,7 +6,7 @@ const {
singleChannelDeviceId, singleChannelDeviceId,
outdatedFirmwareDevice, outdatedFirmwareDevice,
updatedFirmwareDevice, updatedFirmwareDevice,
} = require('./_setup/credentials.json'); } = require('./_setup/credentials.js');
const { firmwareExpectations } = require('./_setup/expectations'); const { firmwareExpectations } = require('./_setup/expectations');

View File

@@ -6,7 +6,7 @@ const {
singleChannelDeviceId, singleChannelDeviceId,
deviceIdWithPower, deviceIdWithPower,
fourChannelsDevice, fourChannelsDevice,
} = require('./_setup/credentials.json'); } = require('./_setup/credentials.js');
describe('invalid credentials', () => { describe('invalid credentials', () => {
beforeEach(async () => { beforeEach(async () => {

View File

@@ -6,7 +6,7 @@ const {
email, email,
password, password,
deviceIdWithPower, deviceIdWithPower,
} = require('./_setup/credentials.json'); } = require('./_setup/credentials.js');
const { const {
rawPowerUsageExpectations, rawPowerUsageExpectations,

View File

@@ -7,7 +7,7 @@ const {
password, password,
deviceIdWithoutTempAndHum, deviceIdWithoutTempAndHum,
deviceIdWithTempAndHum: thDevice, deviceIdWithTempAndHum: thDevice,
} = require('./_setup/credentials.json'); } = require('./_setup/credentials.js');
describe('current temperature and humidity: node script', () => { describe('current temperature and humidity: node script', () => {
let conn; let conn;

View File

@@ -1,6 +1,6 @@
const ewelink = require('../main'); 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'); const { regionExpectations } = require('./_setup/expectations');

View File

@@ -7,7 +7,7 @@ const {
password, password,
deviceIdWithoutPower, deviceIdWithoutPower,
fourChannelsDevice, fourChannelsDevice,
} = require('./_setup/credentials.json'); } = require('./_setup/credentials.js');
const { credentialsExpectations } = require('./_setup/expectations'); const { credentialsExpectations } = require('./_setup/expectations');

120
test/zeroconf.spec.js Normal file
View File

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