mirror of
https://github.com/skydiver/ewelink-api.git
synced 2025-12-21 13:23:05 +01:00
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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -91,3 +91,8 @@ typings/
|
||||
# End of https://www.gitignore.io/api/node
|
||||
|
||||
.idea/
|
||||
|
||||
demo.js
|
||||
arp-table.json
|
||||
devices-cache.json
|
||||
test/_setup/credentials.js
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -2,5 +2,8 @@
|
||||
"eslint.validate": [
|
||||
"javascript"
|
||||
],
|
||||
"eslint.autoFixOnSave": true
|
||||
"eslint.autoFixOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Check library documentation and examples at https://github.com/skydiver/ewelink-api/docs
|
||||
40
classes/PowerState/ChangeStateZeroconf.js
Normal file
40
classes/PowerState/ChangeStateZeroconf.js
Normal 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;
|
||||
@@ -1,5 +1,7 @@
|
||||
const ChangeState = require('./ChangeState');
|
||||
const ChangeStateZeroconf = require('./ChangeStateZeroconf');
|
||||
|
||||
module.exports = {
|
||||
ChangeState,
|
||||
ChangeStateZeroconf,
|
||||
};
|
||||
|
||||
91
classes/Zeroconf.js
Normal file
91
classes/Zeroconf.js
Normal 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
27
docs/README.md
Normal 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)
|
||||
24
docs/available-methods/README.md
Normal file
24
docs/available-methods/README.md
Normal 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).
|
||||
20
docs/available-methods/getcredentials.md
Normal file
20
docs/available-methods/getcredentials.md
Normal 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.
|
||||
13
docs/available-methods/getdevice.md
Normal file
13
docs/available-methods/getdevice.md
Normal 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>
|
||||
12
docs/available-methods/getdevicechannelcount.md
Normal file
12
docs/available-methods/getdevicechannelcount.md
Normal 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>
|
||||
21
docs/available-methods/getdevicecurrenthumidity.md
Normal file
21
docs/available-methods/getdevicecurrenthumidity.md
Normal 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'
|
||||
}
|
||||
```
|
||||
21
docs/available-methods/getdevicecurrenttemperature.md
Normal file
21
docs/available-methods/getdevicecurrenttemperature.md
Normal 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'
|
||||
}
|
||||
```
|
||||
22
docs/available-methods/getdevicecurrentth.md
Normal file
22
docs/available-methods/getdevicecurrentth.md
Normal 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'
|
||||
}
|
||||
```
|
||||
27
docs/available-methods/getdevicepowerstate.md
Normal file
27
docs/available-methods/getdevicepowerstate.md
Normal 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'
|
||||
}
|
||||
```
|
||||
50
docs/available-methods/getdevicepowerusage.md
Normal file
50
docs/available-methods/getdevicepowerusage.md
Normal 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 }
|
||||
]
|
||||
}
|
||||
```
|
||||
13
docs/available-methods/getdevices.md
Normal file
13
docs/available-methods/getdevices.md
Normal 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>
|
||||
12
docs/available-methods/getfirmwareversion.md
Normal file
12
docs/available-methods/getfirmwareversion.md
Normal 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>
|
||||
23
docs/available-methods/getregion.md
Normal file
23
docs/available-methods/getregion.md
Normal 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'
|
||||
}
|
||||
```
|
||||
20
docs/available-methods/login.md
Normal file
20
docs/available-methods/login.md
Normal 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>
|
||||
70
docs/available-methods/openwebsocket.md
Normal file
70
docs/available-methods/openwebsocket.md
Normal 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).
|
||||
19
docs/available-methods/savedevicescache.md
Normal file
19
docs/available-methods/savedevicescache.md
Normal 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.
|
||||
29
docs/available-methods/setdevicepowerstate.md
Normal file
29
docs/available-methods/setdevicepowerstate.md
Normal 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'
|
||||
}
|
||||
```
|
||||
27
docs/available-methods/toggledevice.md
Normal file
27
docs/available-methods/toggledevice.md
Normal 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'
|
||||
}
|
||||
```
|
||||
24
docs/class-instantiation.md
Normal file
24
docs/class-instantiation.md
Normal 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
8
docs/demos/README.md
Normal 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
30
docs/demos/node.md
Normal 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
73
docs/demos/serverless.md
Normal 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
25
docs/introduction.md
Normal 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
22
docs/quickstart.md
Normal 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
8
docs/testing.md
Normal 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
66
docs/zeroconf.md
Normal 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');
|
||||
```
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
17
lib/payloads/zeroConfUpdatePayload.js
Normal file
17
lib/payloads/zeroConfUpdatePayload.js
Normal 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
30
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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
17
mixins/devices/getLocalIpMixin.js
Normal file
17
mixins/devices/getLocalIpMixin.js
Normal 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;
|
||||
32
mixins/devices/saveDevicesCacheMixin.js
Normal file
32
mixins/devices/saveDevicesCacheMixin.js
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
87
package-lock.json
generated
87
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
20
test/_setup/credentials.example.js
Normal file
20
test/_setup/credentials.example.js
Normal 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>',
|
||||
};
|
||||
@@ -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>"
|
||||
}
|
||||
@@ -56,7 +56,7 @@ const regionExpectations = {
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
credentialsExpectations: credentialsExpectations,
|
||||
credentialsExpectations,
|
||||
allDevicesExpectations,
|
||||
specificDeviceExpectations,
|
||||
rawPowerUsageExpectations,
|
||||
|
||||
@@ -7,7 +7,7 @@ const {
|
||||
password,
|
||||
singleChannelDeviceId,
|
||||
fourChannelsDevice,
|
||||
} = require('./_setup/credentials.json');
|
||||
} = require('./_setup/credentials.js');
|
||||
|
||||
const {
|
||||
credentialsExpectations,
|
||||
|
||||
@@ -7,7 +7,7 @@ const {
|
||||
password,
|
||||
singleChannelDeviceId,
|
||||
fourChannelsDevice,
|
||||
} = require('./_setup/credentials.json');
|
||||
} = require('./_setup/credentials.js');
|
||||
|
||||
const {
|
||||
credentialsExpectations,
|
||||
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
singleChannelDeviceId,
|
||||
outdatedFirmwareDevice,
|
||||
updatedFirmwareDevice,
|
||||
} = require('./_setup/credentials.json');
|
||||
} = require('./_setup/credentials.js');
|
||||
|
||||
const { firmwareExpectations } = require('./_setup/expectations');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
singleChannelDeviceId,
|
||||
deviceIdWithPower,
|
||||
fourChannelsDevice,
|
||||
} = require('./_setup/credentials.json');
|
||||
} = require('./_setup/credentials.js');
|
||||
|
||||
describe('invalid credentials', () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
email,
|
||||
password,
|
||||
deviceIdWithPower,
|
||||
} = require('./_setup/credentials.json');
|
||||
} = require('./_setup/credentials.js');
|
||||
|
||||
const {
|
||||
rawPowerUsageExpectations,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const {
|
||||
password,
|
||||
deviceIdWithoutPower,
|
||||
fourChannelsDevice,
|
||||
} = require('./_setup/credentials.json');
|
||||
} = require('./_setup/credentials.js');
|
||||
|
||||
const { credentialsExpectations } = require('./_setup/expectations');
|
||||
|
||||
|
||||
120
test/zeroconf.spec.js
Normal file
120
test/zeroconf.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user