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)
* [getDevicePowerState](available-methods/getdevicepowerstate.md)
* [setDevicePowerState](available-methods/setdevicepowerstate.md)
* [getWSDevicePowerState](available-methods/getwsdevicepowerstate.md)
* [setWSDevicePowerState](available-methods/setwsdevicepowerstate.md)
* [toggleDevice](available-methods/toggledevice.md)
* [getDevicePowerUsage](available-methods/getdevicepowerusage.md)
* [getDeviceCurrentTH](available-methods/getdevicecurrentth.md)

View File

@@ -8,6 +8,8 @@ Here is the list of available methods.
* [getDevices](getdevices.md)
* [getDevicePowerState](getdevicepowerstate.md)
* [setDevicePowerState](setdevicepowerstate.md)
* [getWSDevicePowerState](getwsdevicepowerstate.md)
* [setWSDevicePowerState](setwsdevicepowerstate.md)
* [toggleDevice](toggledevice.md)
* [getDevicePowerUsage](getdevicepowerusage.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
> 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({
email: '<your ewelink email>',
@@ -11,7 +15,7 @@
});
```
**_Using phone number and password_**
## Using phone number and password
```
const connection = new ewelink({
phoneNumber: '<your phone number>',
@@ -20,7 +24,7 @@
});
```
**_Using access token and api key_**
## Using access token and api key
```
const connection = new ewelink({
at: '<valid access token>',
@@ -29,8 +33,17 @@
});
```
**_Using devices and arp table cache files_**
Check [ZeroConf](zeroconf.md) docs for detailed information.
## Custom APP_ID and APP_SECRET
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
> * To get your access token and api key, use [getCredentials](available-methods/getcredentials) method
## Using devices and arp table cache files
Check [ZeroConf](zeroconf.md) docs for detailed information.

16
main.js
View File

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

3544
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,25 +42,25 @@
"dependencies": {
"arpping": "github:skydiver/arpping",
"crypto-js": "^4.0.0",
"delay": "^4.3.0",
"node-fetch": "^2.6.0",
"delay": "^4.4.0",
"node-fetch": "^2.6.1",
"random": "^2.2.0",
"websocket": "^1.0.31",
"websocket": "^1.0.32",
"websocket-as-promised": "^1.0.1"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint": "^7.0.0",
"eslint-config-airbnb": "^18.1.0",
"eslint-config-prettier": "^6.11.0",
"eslint": "^7.11.0",
"eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.12.0",
"eslint-config-wesbos": "0.0.19",
"eslint-plugin-html": "^6.0.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.0.2",
"jest": "^26.0.1",
"eslint-plugin-html": "^6.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.4",
"eslint-plugin-react-hooks": "^4.1.2",
"jest": "^26.5.3",
"nock": "^12.0.3",
"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}>}
*/
static async get({ apiUrl, at, apiKey, deviceId }) {
const payloadLogin = wssLoginPayload({ at, apiKey });
const payloadLogin = wssLoginPayload({ at, apiKey, appid: this.APP_ID });
const payloadUpdate = wssUpdatePayload({
apiKey,

View File

@@ -16,6 +16,7 @@ const customErrors = {
noFirmware: "Can't get model or firmware version",
invalidAuth: 'Library needs to be initialized using email and password',
invalidCredentials: 'Invalid credentials provided',
invalidPowerState: 'Invalid power state. Expecting: "on", "off" or "toggle"',
};
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 random = require('random');
const { APP_SECRET } = require('../data/constants');
const DEVICE_TYPE_UUID = require('../data/devices-type-uuid.json');
const DEVICE_CHANNEL_LENGTH = require('../data/devices-channel-length.json');
const makeAuthorizationSign = body =>
const makeAuthorizationSign = (APP_SECRET, body) =>
crypto
.createHmac('sha256', APP_SECRET)
.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: *}>}
*/
async getCredentials() {
const { APP_ID, APP_SECRET } = this;
const body = credentialsPayload({
appid: APP_ID,
email: this.email,
phoneNumber: this.phoneNumber,
password: this.password,
@@ -20,7 +23,9 @@ module.exports = {
const request = await fetch(`${this.getApiUrl()}/user/login`, {
method: 'post',
headers: { Authorization: `Sign ${makeAuthorizationSign(body)}` },
headers: {
Authorization: `Sign ${makeAuthorizationSign(APP_SECRET, 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 errors = require('../data/errors');
@@ -14,6 +13,8 @@ module.exports = {
return this.devicesCache.find(dev => dev.deviceid === deviceId) || null;
}
const { APP_ID } = this;
const device = await this.makeRequest({
uri: `/user/device/${deviceId}`,
qs: {

View File

@@ -15,7 +15,11 @@ module.exports = {
async getDevicePowerState(deviceId, channel = 1) {
const status = await this.makeRequest({
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);

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ module.exports = {
const payloadLogin = wssLoginPayload({
at: this.at,
apiKey: this.apiKey,
appid: this.APP_ID,
});
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 errors = require('../data/errors');
@@ -62,6 +61,8 @@ module.exports = {
});
}
const { APP_ID } = this;
const response = await this.makeRequest({
method: 'post',
uri: '/user/device/status',

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ const nockAction = false;
const testsToDelay = [
'env-node.spec.js',
'env-serverless.spec.js',
'device-control.spec.js',
'invalid-credentials.spec.js',
'power-usage.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 errors = require('../src/data/errors');
@@ -14,6 +15,8 @@ describe('main class instantiation: valid parameters combinations', () => {
arpTable: null,
at: null,
devicesCache: null,
APP_ID,
APP_SECRET,
});
});
@@ -33,6 +36,8 @@ describe('main class instantiation: valid parameters combinations', () => {
arpTable: null,
at: null,
devicesCache: null,
APP_ID,
APP_SECRET,
});
});
@@ -48,6 +53,8 @@ describe('main class instantiation: valid parameters combinations', () => {
at: null,
arpTable: null,
devicesCache: null,
APP_ID,
APP_SECRET,
});
});
@@ -63,6 +70,8 @@ describe('main class instantiation: valid parameters combinations', () => {
at: credentials.at,
arpTable: null,
devicesCache: null,
APP_ID,
APP_SECRET,
});
});
@@ -78,6 +87,8 @@ describe('main class instantiation: valid parameters combinations', () => {
at: null,
arpTable: credentials.arpTable,
devicesCache: credentials.devicesCache,
APP_ID,
APP_SECRET,
});
});
@@ -96,6 +107,30 @@ describe('main class instantiation: valid parameters combinations', () => {
at: credentials.at,
arpTable: 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');
describe('check eWeLink helpers', () => {
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).toBe('7Aaa/8EuRScACNrZTATW2WKIY7lcRnjgWHTiBl8G0TQ=');
});