Release v3.1.0 (#93)

* set APP_ID and APP_SECRET from main class

* add APP_ID and APP_SECRET as class constructor parameters

* updated test case

* updated test case

* added new test case

* docs updated

* Release v3.1.0 - "setWSDevicePowerState" (#96)

* new mixing to control devices using websocket

* switch status on single channel devices

* working on deviceControl mixin

* better error handling

* working on fix for shared devices

* refactor/cleanup

* added helper function

* added docs for new method

* return device new status

* added test cases

* properly close websocket connection and clean used properties

* added test cases

* error detection enhancements

* added test cases

* error detection enhancements

* added new test file to jest setup

* method renamed

* fix for closing websocket connection

* new getWSDevicePowerState method

* added test cases

* re-arrange tests

* added new test cases

* extract helpers methods

* added test case

* close WebSocket connection on auth error

* updated docs

* updated dependencies

* fix for "forbidden" error

* updated dependencies
This commit is contained in:
Martin M
2020-10-12 19:01:57 -03:00
committed by GitHub
parent c11b3a8ab7
commit b87d092a71
28 changed files with 2329 additions and 2061 deletions

View File

@@ -13,6 +13,8 @@
* [getDevices](available-methods/getdevices.md) * [getDevices](available-methods/getdevices.md)
* [getDevicePowerState](available-methods/getdevicepowerstate.md) * [getDevicePowerState](available-methods/getdevicepowerstate.md)
* [setDevicePowerState](available-methods/setdevicepowerstate.md) * [setDevicePowerState](available-methods/setdevicepowerstate.md)
* [getWSDevicePowerState](available-methods/getwsdevicepowerstate.md)
* [setWSDevicePowerState](available-methods/setwsdevicepowerstate.md)
* [toggleDevice](available-methods/toggledevice.md) * [toggleDevice](available-methods/toggledevice.md)
* [getDevicePowerUsage](available-methods/getdevicepowerusage.md) * [getDevicePowerUsage](available-methods/getdevicepowerusage.md)
* [getDeviceCurrentTH](available-methods/getdevicecurrentth.md) * [getDeviceCurrentTH](available-methods/getdevicecurrentth.md)

View File

@@ -8,6 +8,8 @@ Here is the list of available methods.
* [getDevices](getdevices.md) * [getDevices](getdevices.md)
* [getDevicePowerState](getdevicepowerstate.md) * [getDevicePowerState](getdevicepowerstate.md)
* [setDevicePowerState](setdevicepowerstate.md) * [setDevicePowerState](setdevicepowerstate.md)
* [getWSDevicePowerState](getwsdevicepowerstate.md)
* [setWSDevicePowerState](setwsdevicepowerstate.md)
* [toggleDevice](toggledevice.md) * [toggleDevice](toggledevice.md)
* [getDevicePowerUsage](getdevicepowerusage.md) * [getDevicePowerUsage](getdevicepowerusage.md)
* [getDeviceCurrentTH](getdevicecurrentth.md) * [getDeviceCurrentTH](getdevicecurrentth.md)

View File

@@ -0,0 +1,60 @@
# getWSDevicePowerState
Query for specified device power status using WebSockets.
### Usage
```js
// get device power status
const status = await connection.getWSDevicePowerState('<your device id>');
console.log(status);
```
```js
// get device power status using a secondary account
const status = await connection.getWSDevicePowerState('<your device id>', {
shared: true,
});
console.log(status);
```
```js
// get channel 3 power status on multi-channel device
const status = await connection.getWSDevicePowerState('<your device id>', {
channel: 3,
});
console.log(status);
```
```js
// get all channels power status on multi-channel device
const status = await connection.getWSDevicePowerState('<your device id>', {
allChannels: true,
});
console.log(status);
```
<sup>* _Remember to instantiate class before use_</sup>
### Response example
```js
{
status: 'ok',
state: 'off',
channel: 1
}
```
```js
{
status: 'ok',
state: [
{ channel: 1, state: 'off' },
{ channel: 2, state: 'off' },
{ channel: 3, state: 'off' },
{ channel: 4, state: 'off' }
]
}
```

View File

@@ -0,0 +1,50 @@
# setWSDevicePowerState
Change specified device power state using WebSockets.
Possible states: `on`, `off`, `toggle`.
### Usage
```js
const status = await connection.setWSDevicePowerState('<your device id>', 'on');
console.log(status);
```
```js
// multi-channel devices like Sonoff 4CH
// example will toggle power state on channel 3
const status = await connection.setWSDevicePowerState('<your device id>', 'toggle', {
channel: 3,
});
console.log(status);
```
```js
// to control a shared device using a second account, add "shared" setting
const status = await connection.setWSDevicePowerState('<your device id>', 'off', {
shared: true
});
console.log(status);
```
```js
// turn on channel 2 on a shared multi-channel device
const status = await connection.setWSDevicePowerState('<your device id>', 'on', {
channel: 2,
shared: true
});
console.log(status);
```
<sup>* _Remember to instantiate class before use_</sup>
### Response example
```js
{
status: 'ok',
state: 'on',
channel: 1
}
```

View File

@@ -1,8 +1,12 @@
# Class Instantiation # 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. * 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_** * 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
## Using email and password
``` ```
const connection = new ewelink({ const connection = new ewelink({
email: '<your ewelink email>', email: '<your ewelink email>',
@@ -11,7 +15,7 @@
}); });
``` ```
**_Using phone number and password_** ## Using phone number and password
``` ```
const connection = new ewelink({ const connection = new ewelink({
phoneNumber: '<your phone number>', phoneNumber: '<your phone number>',
@@ -20,7 +24,7 @@
}); });
``` ```
**_Using access token and api key_** ## Using access token and api key
``` ```
const connection = new ewelink({ const connection = new ewelink({
at: '<valid access token>', at: '<valid access token>',
@@ -29,8 +33,17 @@
}); });
``` ```
**_Using devices and arp table cache files_** ## Custom APP_ID and APP_SECRET
Check [ZeroConf](zeroconf.md) docs for detailed information. This library uses an APP ID and APP Secret provided by Sonoff team.
If you want to specify another pair of settings, just pass in the class constructor:
```
const connection = new ewelink({
email: '<your ewelink email>',
password: '<your ewelink password>',
APP_ID: 'CUSTOM APP ID',
APP_SECRET: 'CUSTOM APP SECRET',
});
```
> * If you don't know your region, use [getRegion](available-methods/getregion) method ## Using devices and arp table cache files
> * To get your access token and api key, use [getCredentials](available-methods/getcredentials) method Check [ZeroConf](zeroconf.md) docs for detailed information.

32
main.js
View File

@@ -1,17 +1,26 @@
const {
APP_ID: DEFAULT_APP_ID,
APP_SECRET: DEFAULT_APP_SECRET,
} = require('./src/data/constants');
const mixins = require('./src/mixins'); const mixins = require('./src/mixins');
const errors = require('./src/data/errors'); const errors = require('./src/data/errors');
class eWeLink { class eWeLink {
constructor({ constructor(parameters = {}) {
region = 'us', const {
email = null, region = 'us',
phoneNumber = null, email = null,
password = null, phoneNumber = null,
at = null, password = null,
apiKey = null, at = null,
devicesCache = null, apiKey = null,
arpTable = null, devicesCache = null,
}) { arpTable = null,
APP_ID = DEFAULT_APP_ID,
APP_SECRET = DEFAULT_APP_SECRET,
} = parameters;
const check = this.checkLoginParameters({ const check = this.checkLoginParameters({
region, region,
email, email,
@@ -35,6 +44,9 @@ class eWeLink {
this.apiKey = apiKey; this.apiKey = apiKey;
this.devicesCache = devicesCache; this.devicesCache = devicesCache;
this.arpTable = arpTable; this.arpTable = arpTable;
this.APP_ID = APP_ID;
this.APP_SECRET = APP_SECRET;
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this

3550
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,25 +42,25 @@
"dependencies": { "dependencies": {
"arpping": "github:skydiver/arpping", "arpping": "github:skydiver/arpping",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"delay": "^4.3.0", "delay": "^4.4.0",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.1",
"random": "^2.2.0", "random": "^2.2.0",
"websocket": "^1.0.31", "websocket": "^1.0.32",
"websocket-as-promised": "^1.0.1" "websocket-as-promised": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^7.0.0", "eslint": "^7.11.0",
"eslint-config-airbnb": "^18.1.0", "eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.12.0",
"eslint-config-wesbos": "0.0.19", "eslint-config-wesbos": "0.0.19",
"eslint-plugin-html": "^6.0.2", "eslint-plugin-html": "^6.1.0",
"eslint-plugin-import": "^2.20.2", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.0", "eslint-plugin-react": "^7.21.4",
"eslint-plugin-react-hooks": "^4.0.2", "eslint-plugin-react-hooks": "^4.1.2",
"jest": "^26.0.1", "jest": "^26.5.3",
"nock": "^12.0.3", "nock": "^12.0.3",
"prettier": "^1.19.1" "prettier": "^1.19.1"
} }

View File

@@ -15,7 +15,7 @@ class DevicePowerUsageRaw extends WebSocket {
* @returns {Promise<{error: string}|{data: {hundredDaysKwhData: *}, status: string}|{msg: any, error: *}|{msg: string, error: number}>} * @returns {Promise<{error: string}|{data: {hundredDaysKwhData: *}, status: string}|{msg: any, error: *}|{msg: string, error: number}>}
*/ */
static async get({ apiUrl, at, apiKey, deviceId }) { static async get({ apiUrl, at, apiKey, deviceId }) {
const payloadLogin = wssLoginPayload({ at, apiKey }); const payloadLogin = wssLoginPayload({ at, apiKey, appid: this.APP_ID });
const payloadUpdate = wssUpdatePayload({ const payloadUpdate = wssUpdatePayload({
apiKey, apiKey,

View File

@@ -16,6 +16,7 @@ const customErrors = {
noFirmware: "Can't get model or firmware version", noFirmware: "Can't get model or firmware version",
invalidAuth: 'Library needs to be initialized using email and password', invalidAuth: 'Library needs to be initialized using email and password',
invalidCredentials: 'Invalid credentials provided', invalidCredentials: 'Invalid credentials provided',
invalidPowerState: 'Invalid power state. Expecting: "on", "off" or "toggle"',
}; };
module.exports = Object.assign(errors, customErrors); module.exports = Object.assign(errors, customErrors);

View File

@@ -0,0 +1,58 @@
const STATE_ON = 'on';
const STATE_OFF = 'off';
const STATE_TOGGLE = 'toggle';
const VALID_POWER_STATES = [STATE_ON, STATE_OFF, STATE_TOGGLE];
/**
* Return new device state based on current conditions
*/
const getNewPowerState = (currentState, newState) => {
if (newState !== STATE_TOGGLE) {
return newState;
}
return currentState === STATE_ON ? STATE_OFF : STATE_ON;
};
/**
* Get current device parameters and
*/
const getPowerStateParams = (params, newState, channel) => {
if (params.switches) {
const switches = [...params.switches];
const channelToSwitch = channel - 1;
switches[channelToSwitch].switch = newState;
return { switches };
}
return { switch: newState };
};
/**
* Return status of all channels on a multi-channel device
*/
const getAllChannelsState = params => {
const { switches } = params;
return switches.map(ch => ({
channel: ch.outlet + 1,
state: ch.switch,
}));
};
/**
* Return status of specific channel on multi-channel device
*/
const getSpecificChannelState = (params, channel) => {
const { switches } = params;
return switches[channel - 1].switch;
};
module.exports = {
STATE_ON,
STATE_OFF,
STATE_TOGGLE,
VALID_POWER_STATES,
getNewPowerState,
getPowerStateParams,
getAllChannelsState,
getSpecificChannelState,
};

View File

@@ -2,12 +2,10 @@ const crypto = require('crypto');
const CryptoJS = require('crypto-js'); const CryptoJS = require('crypto-js');
const random = require('random'); const random = require('random');
const { APP_SECRET } = require('../data/constants');
const DEVICE_TYPE_UUID = require('../data/devices-type-uuid.json'); const DEVICE_TYPE_UUID = require('../data/devices-type-uuid.json');
const DEVICE_CHANNEL_LENGTH = require('../data/devices-channel-length.json'); const DEVICE_CHANNEL_LENGTH = require('../data/devices-channel-length.json');
const makeAuthorizationSign = body => const makeAuthorizationSign = (APP_SECRET, body) =>
crypto crypto
.createHmac('sha256', APP_SECRET) .createHmac('sha256', APP_SECRET)
.update(JSON.stringify(body)) .update(JSON.stringify(body))

272
src/mixins/deviceControl.js Normal file
View File

@@ -0,0 +1,272 @@
const W3CWebSocket = require('websocket').w3cwebsocket;
const WebSocketAsPromised = require('websocket-as-promised');
const delay = require('delay');
const { nonce, timestamp } = require('../helpers/utilities');
const errors = require('../data/errors');
const {
VALID_POWER_STATES,
getNewPowerState,
getPowerStateParams,
getAllChannelsState,
getSpecificChannelState,
} = require('../helpers/device-control');
module.exports = {
async initDeviceControl(params = {}) {
// check if socket is already initialized
if (this.wsp) {
return;
}
const { APP_ID, at, apiKey } = this;
// set delay between socket messages
const { delayTime = 1000 } = params;
this.wsDelayTime = delayTime;
// request credentials if needed
if (at === null || apiKey === null) {
await this.getCredentials();
}
// request distribution service
const dispatch = await this.makeRequest({
method: 'post',
url: `https://${this.region}-api.coolkit.cc:8080`,
uri: '/dispatch/app',
body: {
accept: 'ws',
appid: APP_ID,
nonce,
ts: timestamp,
version: 8,
},
});
// WebSocket parameters
const WSS_URL = `wss://${dispatch.domain}:${dispatch.port}/api/ws`;
const WSS_CONFIG = { createWebSocket: wss => new W3CWebSocket(wss) };
// open WebSocket connection
this.wsp = new WebSocketAsPromised(WSS_URL, WSS_CONFIG);
// catch autentication errors
let socketError;
this.wsp.onMessage.addListener(async message => {
const data = JSON.parse(message);
if (data.error) {
socketError = data;
await this.webSocketClose();
}
});
// open socket connection
await this.wsp.open();
// WebSocket handshake
await this.webSocketHandshake();
// if auth error exists, throw an error
if (socketError) {
throw new Error(errors[socketError.error]);
}
},
/**
* WebSocket authentication process
*/
async webSocketHandshake() {
const apikey = this.deviceApiKey || this.apiKey;
const payload = JSON.stringify({
action: 'userOnline',
version: 8,
ts: timestamp,
at: this.at,
userAgent: 'app',
apikey,
appid: this.APP_ID,
nonce,
sequence: Math.floor(timestamp * 1000),
});
await this.wsp.send(payload);
await delay(this.wsDelayTime);
},
/**
* Close WebSocket connection and class cleanup
*/
async webSocketClose() {
await this.wsp.close();
delete this.wsDelayTime;
delete this.wsp;
delete this.deviceApiKey;
},
/**
* Update device status (timers, share status, on/off etc)
*/
async updateDeviceStatus(deviceId, params) {
await this.initDeviceControl();
const apikey = this.deviceApiKey || this.apiKey;
const payload = JSON.stringify({
action: 'update',
deviceid: deviceId,
apikey,
userAgent: 'app',
sequence: Math.floor(timestamp * 1000),
ts: timestamp,
params,
});
return this.wsp.send(payload);
},
/**
* Check device status (timers, share status, on/off etc)
*/
async getWSDeviceStatus(deviceId, params) {
await this.initDeviceControl();
let response = null;
this.wsp.onMessage.addListener(message => {
const data = JSON.parse(message);
if (data.deviceid === deviceId) {
response = data;
}
});
const apikey = this.deviceApiKey || this.apiKey;
const payload = JSON.stringify({
action: 'query',
deviceid: deviceId,
apikey,
userAgent: 'app',
sequence: Math.floor(timestamp * 1000),
ts: timestamp,
params,
});
this.wsp.send(payload);
await delay(this.wsDelayTime);
// throw error on invalid device
if (response.error) {
throw new Error(errors[response.error]);
}
return response;
},
/**
* Get device power state
*/
async getWSDevicePowerState(deviceId, options = {}) {
// get extra parameters
const { channel = 1, allChannels = false, shared = false } = options;
// if device is shared by other account, fetch device api key
if (shared) {
const device = await this.getDevice(deviceId);
this.deviceApiKey = device.apikey;
}
// get device current state
const status = await this.getWSDeviceStatus(deviceId, [
'switch',
'switches',
]);
// close websocket connection
await this.webSocketClose();
// check for multi-channel device
const multiChannelDevice = !!status.params.switches;
// returns all channels
if (multiChannelDevice && allChannels) {
return {
status: 'ok',
state: getAllChannelsState(status.params),
};
}
// multi-channel device & requested channel
if (multiChannelDevice) {
return {
status: 'ok',
state: getSpecificChannelState(status.params, channel),
channel,
};
}
// single channel device
return {
status: 'ok',
state: status.params.switch,
channel,
};
},
/**
* Set device power state
*/
async setWSDevicePowerState(deviceId, state, options = {}) {
// check for valid power state
if (!VALID_POWER_STATES.includes(state)) {
throw new Error(errors.invalidPowerState);
}
// get extra parameters
const { channel = 1, shared = false } = options;
// if device is shared by other account, fetch device api key
if (shared) {
const device = await this.getDevice(deviceId);
this.deviceApiKey = device.apikey;
}
// get device current state
const status = await this.getWSDeviceStatus(deviceId, [
'switch',
'switches',
]);
// check for multi-channel device
const multiChannelDevice = !!status.params.switches;
// get current device state
const currentState = multiChannelDevice
? status.params.switches[channel - 1].switch
: status.params.switch;
// resolve new power state
const stateToSwitch = getNewPowerState(currentState, state);
// build request payload
const params = getPowerStateParams(status.params, stateToSwitch, channel);
// change device status
try {
await this.updateDeviceStatus(deviceId, params);
await delay(this.wsDelayTime);
} catch (error) {
throw new Error(error);
} finally {
await this.webSocketClose();
}
return {
status: 'ok',
state: stateToSwitch,
channel: multiChannelDevice ? channel : 1,
};
},
};

View File

@@ -12,7 +12,10 @@ module.exports = {
* @returns {Promise<{msg: string, error: *}>} * @returns {Promise<{msg: string, error: *}>}
*/ */
async getCredentials() { async getCredentials() {
const { APP_ID, APP_SECRET } = this;
const body = credentialsPayload({ const body = credentialsPayload({
appid: APP_ID,
email: this.email, email: this.email,
phoneNumber: this.phoneNumber, phoneNumber: this.phoneNumber,
password: this.password, password: this.password,
@@ -20,7 +23,9 @@ module.exports = {
const request = await fetch(`${this.getApiUrl()}/user/login`, { const request = await fetch(`${this.getApiUrl()}/user/login`, {
method: 'post', method: 'post',
headers: { Authorization: `Sign ${makeAuthorizationSign(body)}` }, headers: {
Authorization: `Sign ${makeAuthorizationSign(APP_SECRET, body)}`,
},
body: JSON.stringify(body), body: JSON.stringify(body),
}); });

View File

@@ -1,4 +1,3 @@
const { APP_ID } = require('../data/constants');
const { nonce, timestamp, _get } = require('../helpers/utilities'); const { nonce, timestamp, _get } = require('../helpers/utilities');
const errors = require('../data/errors'); const errors = require('../data/errors');
@@ -14,6 +13,8 @@ module.exports = {
return this.devicesCache.find(dev => dev.deviceid === deviceId) || null; return this.devicesCache.find(dev => dev.deviceid === deviceId) || null;
} }
const { APP_ID } = this;
const device = await this.makeRequest({ const device = await this.makeRequest({
uri: `/user/device/${deviceId}`, uri: `/user/device/${deviceId}`,
qs: { qs: {

View File

@@ -15,7 +15,11 @@ module.exports = {
async getDevicePowerState(deviceId, channel = 1) { async getDevicePowerState(deviceId, channel = 1) {
const status = await this.makeRequest({ const status = await this.makeRequest({
uri: '/user/device/status', uri: '/user/device/status',
qs: deviceStatusPayload({ deviceId, params: 'switch|switches' }), qs: deviceStatusPayload({
appid: this.APP_ID,
deviceId,
params: 'switch|switches',
}),
}); });
const error = _get(status, 'error', false); const error = _get(status, 'error', false);

View File

@@ -1,4 +1,3 @@
const { APP_ID } = require('../data/constants');
const { _get, timestamp } = require('../helpers/utilities'); const { _get, timestamp } = require('../helpers/utilities');
const errors = require('../data/errors'); const errors = require('../data/errors');
@@ -9,6 +8,8 @@ module.exports = {
* @returns {Promise<{msg: string, error: number}|*>} * @returns {Promise<{msg: string, error: number}|*>}
*/ */
async getDevices() { async getDevices() {
const { APP_ID } = this;
const response = await this.makeRequest({ const response = await this.makeRequest({
uri: '/user/device', uri: '/user/device',
qs: { qs: {

View File

@@ -1,5 +1,6 @@
const { checkDevicesUpdates } = require('./checkDevicesUpdates'); const { checkDevicesUpdates } = require('./checkDevicesUpdates');
const { checkDeviceUpdate } = require('./checkDeviceUpdate'); const { checkDeviceUpdate } = require('./checkDeviceUpdate');
const deviceControl = require('./deviceControl');
const { getCredentials } = require('./getCredentials'); const { getCredentials } = require('./getCredentials');
const { getDevice } = require('./getDevice'); const { getDevice } = require('./getDevice');
const { getDeviceChannelCount } = require('./getDeviceChannelCount'); const { getDeviceChannelCount } = require('./getDeviceChannelCount');
@@ -11,7 +12,7 @@ const { getDevicePowerUsageRaw } = require('./getDevicePowerUsageRaw');
const { getDevices } = require('./getDevices'); const { getDevices } = require('./getDevices');
const { getFirmwareVersion } = require('./getFirmwareVersion'); const { getFirmwareVersion } = require('./getFirmwareVersion');
const { getRegion } = require('./getRegion'); const { getRegion } = require('./getRegion');
const { makeRequest } = require('./makeRequest') const { makeRequest } = require('./makeRequest');
const { openWebSocket } = require('./openWebSocket'); const { openWebSocket } = require('./openWebSocket');
const { saveDevicesCache } = require('./saveDevicesCache'); const { saveDevicesCache } = require('./saveDevicesCache');
const { setDevicePowerState } = require('./setDevicePowerState'); const { setDevicePowerState } = require('./setDevicePowerState');
@@ -20,6 +21,7 @@ const { toggleDevice } = require('./toggleDevice');
const mixins = { const mixins = {
checkDevicesUpdates, checkDevicesUpdates,
checkDeviceUpdate, checkDeviceUpdate,
...deviceControl,
getCredentials, getCredentials,
getDevice, getDevice,
getDeviceChannelCount, getDeviceChannelCount,

View File

@@ -16,6 +16,7 @@ module.exports = {
const payloadLogin = wssLoginPayload({ const payloadLogin = wssLoginPayload({
at: this.at, at: this.at,
apiKey: this.apiKey, apiKey: this.apiKey,
appid: this.APP_ID,
}); });
const wsp = new WebSocketAsPromised(this.getApiWebSocket(), { const wsp = new WebSocketAsPromised(this.getApiWebSocket(), {

View File

@@ -1,4 +1,3 @@
const { APP_ID } = require('../data/constants');
const { _get, timestamp, nonce } = require('../helpers/utilities'); const { _get, timestamp, nonce } = require('../helpers/utilities');
const errors = require('../data/errors'); const errors = require('../data/errors');
@@ -62,6 +61,8 @@ module.exports = {
}); });
} }
const { APP_ID } = this;
const response = await this.makeRequest({ const response = await this.makeRequest({
method: 'post', method: 'post',
uri: '/user/device/status', uri: '/user/device/status',

View File

@@ -1,8 +1,7 @@
const { APP_ID } = require('../data/constants');
const { timestamp, nonce } = require('../helpers/utilities'); const { timestamp, nonce } = require('../helpers/utilities');
const credentialsPayload = ({ email, phoneNumber, password }) => ({ const credentialsPayload = ({ appid, email, phoneNumber, password }) => ({
appid: APP_ID, appid,
email, email,
phoneNumber, phoneNumber,
password, password,

View File

@@ -1,9 +1,8 @@
const { APP_ID } = require('../data/constants');
const { timestamp, nonce } = require('../helpers/utilities'); const { timestamp, nonce } = require('../helpers/utilities');
const deviceStatus = ({ deviceId, params }) => ({ const deviceStatus = ({ appid, deviceId, params }) => ({
deviceid: deviceId, deviceid: deviceId,
appid: APP_ID, appid,
nonce, nonce,
ts: timestamp, ts: timestamp,
version: 8, version: 8,

View File

@@ -1,15 +1,14 @@
const { APP_ID } = require('../data/constants');
const { timestamp, nonce } = require('../helpers/utilities'); const { timestamp, nonce } = require('../helpers/utilities');
const wssLoginPayload = ({ at, apiKey }) => { const wssLoginPayload = ({ at, apiKey, appid }) => {
const payload = { const payload = {
action: 'userOnline', action: 'userOnline',
at, at,
apikey: apiKey, apikey: apiKey,
appid: APP_ID, appid,
nonce, nonce,
ts: timestamp, ts: timestamp,
userAgent: 'ewelink-api', userAgent: 'app',
sequence: Math.floor(timestamp * 1000), sequence: Math.floor(timestamp * 1000),
version: 8, version: 8,
}; };

View File

@@ -8,7 +8,7 @@ const wssUpdatePayload = ({ apiKey, deviceId, params }) => {
selfApikey: apiKey, selfApikey: apiKey,
params, params,
ts: timestamp, ts: timestamp,
userAgent: 'ewelink-api', userAgent: 'app',
sequence: Math.floor(timestamp * 1000), sequence: Math.floor(timestamp * 1000),
}; };
return JSON.stringify(payload); return JSON.stringify(payload);

View File

@@ -15,6 +15,7 @@ const nockAction = false;
const testsToDelay = [ const testsToDelay = [
'env-node.spec.js', 'env-node.spec.js',
'env-serverless.spec.js', 'env-serverless.spec.js',
'device-control.spec.js',
'invalid-credentials.spec.js', 'invalid-credentials.spec.js',
'power-usage.spec.js', 'power-usage.spec.js',
'temperature-humidity.spec.js', 'temperature-humidity.spec.js',

View File

@@ -1,3 +1,4 @@
const { APP_ID, APP_SECRET } = require('../src/data/constants');
const ewelink = require('../main'); const ewelink = require('../main');
const errors = require('../src/data/errors'); const errors = require('../src/data/errors');
@@ -14,6 +15,8 @@ describe('main class instantiation: valid parameters combinations', () => {
arpTable: null, arpTable: null,
at: null, at: null,
devicesCache: null, devicesCache: null,
APP_ID,
APP_SECRET,
}); });
}); });
@@ -33,6 +36,8 @@ describe('main class instantiation: valid parameters combinations', () => {
arpTable: null, arpTable: null,
at: null, at: null,
devicesCache: null, devicesCache: null,
APP_ID,
APP_SECRET,
}); });
}); });
@@ -48,6 +53,8 @@ describe('main class instantiation: valid parameters combinations', () => {
at: null, at: null,
arpTable: null, arpTable: null,
devicesCache: null, devicesCache: null,
APP_ID,
APP_SECRET,
}); });
}); });
@@ -63,6 +70,8 @@ describe('main class instantiation: valid parameters combinations', () => {
at: credentials.at, at: credentials.at,
arpTable: null, arpTable: null,
devicesCache: null, devicesCache: null,
APP_ID,
APP_SECRET,
}); });
}); });
@@ -78,6 +87,8 @@ describe('main class instantiation: valid parameters combinations', () => {
at: null, at: null,
arpTable: credentials.arpTable, arpTable: credentials.arpTable,
devicesCache: credentials.devicesCache, devicesCache: credentials.devicesCache,
APP_ID,
APP_SECRET,
}); });
}); });
@@ -96,6 +107,30 @@ describe('main class instantiation: valid parameters combinations', () => {
at: credentials.at, at: credentials.at,
arpTable: null, arpTable: null,
devicesCache: null, devicesCache: null,
APP_ID,
APP_SECRET,
});
});
test('initialize class using custom APP_ID and APP_SECRET', async () => {
const credentials = {
email: 'user@email.com',
at: 'xxxyyyzzz',
APP_ID: 'CUSTOM_APP_ID',
APP_SECRET: 'CUSTOM_APP_SECRET',
};
const connection = new ewelink(credentials);
expect(connection).toEqual({
region: 'us',
email: credentials.email,
phoneNumber: null,
password: null,
apiKey: null,
at: credentials.at,
arpTable: null,
devicesCache: null,
APP_ID: 'CUSTOM_APP_ID',
APP_SECRET: 'CUSTOM_APP_SECRET',
}); });
}); });
}); });

215
test/device-control.spec.js Normal file
View File

@@ -0,0 +1,215 @@
const ewelink = require('../main');
const errors = require('../src/data/errors');
const { getAllChannelsState } = require('../src/helpers/device-control');
const {
email,
password,
sharedAccount,
singleChannelDeviceId,
fourChannelsDevice,
} = require('./_setup/config/credentials.js');
describe('device control using WebSockets: get power state', () => {
let conn;
beforeAll(() => {
conn = new ewelink({ email, password });
});
test('get power state on single channel device', async () => {
jest.setTimeout(30000);
const device = await conn.getDevice(singleChannelDeviceId);
const { switch: originalState } = device.params;
const powerState = await conn.getWSDevicePowerState(singleChannelDeviceId, {
shared: sharedAccount,
});
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.state).toBe(originalState);
expect(powerState.channel).toBe(1);
});
test('get power state for specific channel on multi-channel device', async () => {
jest.setTimeout(30000);
const channel = 3;
const device = await conn.getDevice(fourChannelsDevice);
const { switches } = device.params;
const originalState = switches[channel - 1].switch;
const powerState = await conn.getWSDevicePowerState(fourChannelsDevice, {
channel,
shared: sharedAccount,
});
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.state).toBe(originalState);
expect(powerState.channel).toBe(channel);
});
test('get power state for all channels on multi-channel device', async () => {
jest.setTimeout(30000);
const channel = 3;
const device = await conn.getDevice(fourChannelsDevice);
const originalState = getAllChannelsState(device.params);
const powerState = await conn.getWSDevicePowerState(fourChannelsDevice, {
allChannels: true,
shared: sharedAccount,
});
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.state).toStrictEqual(originalState);
});
});
describe('device control using WebSockets: set power state', () => {
let conn;
beforeAll(() => {
conn = new ewelink({ email, password });
});
test('toggle power state on single channel device', async () => {
jest.setTimeout(30000);
const device = await conn.getDevice(singleChannelDeviceId);
const { switch: originalState } = device.params;
const newState = originalState === 'on' ? 'off' : 'on';
const powerState = await conn.setWSDevicePowerState(
singleChannelDeviceId,
'toggle',
{
shared: sharedAccount,
}
);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.channel).toBe(1);
expect(powerState.state).toBe(newState);
const deviceVerify = await conn.getDevice(singleChannelDeviceId);
const { switch: currentStateVerify } = deviceVerify.params;
expect(newState).toBe(currentStateVerify);
});
test('toggle power state on multi-channel device', async () => {
jest.setTimeout(30000);
const channel = 3;
const device = await conn.getDevice(fourChannelsDevice);
const { switches } = device.params;
const originalState = switches[channel - 1].switch;
const newState = originalState === 'on' ? 'off' : 'on';
const powerState = await conn.setWSDevicePowerState(
fourChannelsDevice,
'toggle',
{
channel,
shared: sharedAccount,
}
);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.channel).toBe(channel);
expect(powerState.state).toBe(newState);
const deviceVerify = await conn.getDevice(fourChannelsDevice);
const { switches: switchesVerify } = deviceVerify.params;
const currentStateVerify = switchesVerify[channel - 1].switch;
expect(newState).toBe(currentStateVerify);
});
test('turn off single channel device', async () => {
jest.setTimeout(30000);
const powerState = await conn.setWSDevicePowerState(
singleChannelDeviceId,
'off',
{
shared: sharedAccount,
}
);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.channel).toBe(1);
expect(powerState.state).toBe('off');
});
test('turn off multi-channel device', async () => {
jest.setTimeout(30000);
const channel = 3;
const powerState = await conn.setWSDevicePowerState(
fourChannelsDevice,
'off',
{
channel,
shared: sharedAccount,
}
);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.channel).toBe(channel);
expect(powerState.state).toBe('off');
});
});
describe('device control using WebSockets: errors and exceptions', () => {
let conn;
beforeAll(() => {
conn = new ewelink({ email, password });
});
test('get power state using invalid credentials should throw an exception', async () => {
try {
const connection = new ewelink({
email: 'invalid',
password: 'credentials',
});
await connection.getWSDevicePowerState(singleChannelDeviceId);
} catch (error) {
expect(typeof error).toBe('object');
expect(error.toString()).toBe(`Error: ${errors[406]}`);
}
});
test('get power state using invalid device should throw an exception', async () => {
jest.setTimeout(30000);
try {
await conn.getWSDevicePowerState('INVALID DEVICE', {
shared: sharedAccount,
});
} catch (error) {
expect(typeof error).toBe('object');
expect(error.toString()).toBe(`Error: ${errors[403]}`);
}
});
test('set power state using invalid credentials should throw an exception', async () => {
try {
const connection = new ewelink({
email: 'invalid',
password: 'credentials',
});
await connection.setWSDevicePowerState(singleChannelDeviceId, 'toggle');
} catch (error) {
expect(typeof error).toBe('object');
expect(error.toString()).toBe(`Error: ${errors[406]}`);
}
});
test('turn off invalid device', async () => {
jest.setTimeout(30000);
try {
await conn.setWSDevicePowerState('INVALID DEVICE', 'off', {
shared: sharedAccount,
});
} catch (error) {
expect(typeof error).toBe('object');
expect(error.toString()).toBe(`Error: ${errors[403]}`);
}
});
test('using invalid power state should throw an exception', async () => {
try {
await conn.setWSDevicePowerState(singleChannelDeviceId, 'INVALID STATE');
} catch (error) {
expect(typeof error).toBe('object');
expect(error.toString()).toBe(`Error: ${errors.invalidPowerState}`);
}
});
});

View File

@@ -1,8 +1,11 @@
const { APP_SECRET } = require('../src/data/constants');
const ewelinkHelpers = require('../src/helpers/ewelink'); const ewelinkHelpers = require('../src/helpers/ewelink');
describe('check eWeLink helpers', () => { describe('check eWeLink helpers', () => {
test('make authorization sign should return right string', async () => { test('make authorization sign should return right string', async () => {
const auth = ewelinkHelpers.makeAuthorizationSign({ data: 'string' }); const auth = ewelinkHelpers.makeAuthorizationSign(APP_SECRET, {
data: 'string',
});
expect(auth.length).toBe(44); expect(auth.length).toBe(44);
expect(auth).toBe('7Aaa/8EuRScACNrZTATW2WKIY7lcRnjgWHTiBl8G0TQ='); expect(auth).toBe('7Aaa/8EuRScACNrZTATW2WKIY7lcRnjgWHTiBl8G0TQ=');
}); });