Compare commits

...

31 Commits

Author SHA1 Message Date
Martín M
b3655b095d getRegion: use v2 API 2020-10-26 21:17:01 -03:00
Martín M
82f152d624 openWebSocket: use v2 API 2020-10-26 21:10:51 -03:00
Martín M
08e84b5fea updated dependencies 2020-10-26 20:48:41 -03:00
Martín M
979664dc34 getDeviceIP: refactor 2020-10-22 02:59:00 -03:00
Martín M
f81fd771a6 saveDevicesCache: use v2 API 2020-10-22 02:53:40 -03:00
Martín M
f2a39c34b4 checkDeviceUpdate: refactor 2020-10-22 02:50:54 -03:00
Martín M
646dfed03c checkDevicesUpdates: use v2 API 2020-10-22 02:44:07 -03:00
Martín M
4038c8c7bd checkDeviceUpdate: use v2 API 2020-10-22 02:18:53 -03:00
Martín M
9e7bef86ff getFirmwareVersion: use v2 API 2020-10-21 21:26:34 -03:00
Martín M
894aa6f5ca getDeviceChannelCount: use v2 API 2020-10-21 21:24:17 -03:00
Martín M
ad2d72f4ce setDevicePowerState: improved error handling 2020-10-21 21:19:01 -03:00
Martín M
e5242589f8 getDeviceCurrentTH: improved error handling 2020-10-21 21:18:21 -03:00
Martín M
bf6ab11590 toggleDevicePowerState: renamed to toggleDevicePowerState 2020-10-21 21:11:39 -03:00
Martín M
ac80e06b0f setDevicePowerState: use v2 API 2020-10-21 21:00:23 -03:00
Martín M
2a64768822 getDevicePowerState: use v2 API 2020-10-21 20:23:01 -03:00
Martín M
930cf2878c getDevices: improved error message 2020-10-21 20:12:50 -03:00
Martín M
996c678204 getDevices: use v2 API 2020-10-21 20:12:13 -03:00
Martín M
45b1c4363a DEV: update eslint + prettier + jest 2020-10-21 20:03:11 -03:00
Martín M
47fd13f105 getDevice: improved error message 2020-10-21 19:59:08 -03:00
Martín M
94b3bc084a makeRequest: improved error message 2020-10-21 19:58:51 -03:00
Martín M
9c00d8280a getDevice: use v2 API 2020-10-21 19:53:18 -03:00
Martín M
16772a5c54 makeRequest: improved error message 2020-10-21 19:53:18 -03:00
Martín M
03307994ea makeRequest: use v2 API 2020-10-21 19:53:18 -03:00
Martín M
d274a8d560 getCredentials: improved error message 2020-10-21 19:53:18 -03:00
Martín M
2f4716f379 getCredentials: fix getting api key and access token 2020-10-21 19:53:17 -03:00
Martín M
012d0ee092 getCredentials: use v2 API 2020-10-21 19:53:17 -03:00
Martín M
1109e70019 use v2 API URL 2020-10-21 19:53:17 -03:00
Martin M
5a07f6b615 Release v3.1.1 (#112)
* fix JSON parse error when device is offline (#111)

* add 503 error and fix makeRequest mixin when device is Offline or Service is unavailable

* add 503 error and fix makeRequest mixin when device is Offline or Service is unavailable

* version bump

Co-authored-by: Luigui Delyer <git@s1x.com.br>
2020-10-18 19:17:16 -03:00
Martín M
381c344725 version bump 2020-10-12 19:14:50 -03:00
Martín M
92b60f21a1 npm audit 2020-10-12 19:03:57 -03:00
Martin M
b87d092a71 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
2020-10-12 19:01:57 -03:00
39 changed files with 2608 additions and 2366 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.

40
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
@@ -63,7 +75,8 @@ class eWeLink {
* @returns {string} * @returns {string}
*/ */
getApiUrl() { getApiUrl() {
return `https://${this.region}-api.coolkit.cc:8080/api`; const domain = this.region === 'cn' ? 'cn' : 'cc';
return `https://${this.region}-apia.coolkit.${domain}`;
} }
/** /**
@@ -83,6 +96,11 @@ class eWeLink {
return `wss://${this.region}-pconnect3.coolkit.cc:8080/api/ws`; return `wss://${this.region}-pconnect3.coolkit.cc:8080/api/ws`;
} }
getDispatchServiceUrl() {
const domain = this.region === 'cn' ? 'cn' : 'cc';
return `https://${this.region}-dispa.coolkit.${domain}`;
}
/** /**
* Generate Zeroconf URL * Generate Zeroconf URL
* @param device * @param device

3752
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "ewelink-api", "name": "ewelink-api",
"version": "3.0.0", "version": "3.1.1",
"description": "eWeLink API for Node.js", "description": "eWeLink API for Node.js",
"author": "Martín M.", "author": "Martín M.",
"license": "MIT", "license": "MIT",
@@ -41,27 +41,28 @@
}, },
"dependencies": { "dependencies": {
"arpping": "github:skydiver/arpping", "arpping": "github:skydiver/arpping",
"compare-versions": "^3.6.0",
"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.1.0"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^7.0.0", "eslint": "^7.12.0",
"eslint-config-airbnb": "^18.1.0", "eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.14.0",
"eslint-config-wesbos": "0.0.19", "eslint-config-wesbos": "1.0.1",
"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.4.1",
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.0", "eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.0.2", "eslint-plugin-react-hooks": "^4.2.0",
"jest": "^26.0.1", "jest": "^26.6.1",
"nock": "^12.0.3", "nock": "^13.0.4",
"prettier": "^1.19.1" "prettier": "^2.1.2"
} }
} }

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

@@ -5,17 +5,22 @@ const errors = {
403: 'Forbidden', 403: 'Forbidden',
404: 'Device does not exist', 404: 'Device does not exist',
406: 'Authentication failed', 406: 'Authentication failed',
503: 'Service Temporarily Unavailable or Device is offline',
}; };
const customErrors = { const customErrors = {
ch404: 'Device channel does not exist', ch404: 'Device channel does not exist',
unknown: 'An unknown error occurred', unknown: 'An unknown error occurred',
noARP: 'No ARP information found. You need to generate the ARP file.',
noDevice: 'No device found',
noDevices: 'No devices found', noDevices: 'No devices found',
noPower: 'No power usage data found', noPower: 'No power usage data found',
noSensor: "Can't read sensor data from device", noSensor: "Can't read sensor data from device",
noFirmware: "Can't get model or firmware version", noFirmware: "Can't get model or firmware version",
noFirmwares: "Can't find firmware update information",
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))

View File

@@ -1,5 +1,4 @@
const { _get } = require('../helpers/utilities'); const errors = require('../data/errors');
const parseFirmwareUpdates = require('../parsers/parseFirmwareUpdates');
module.exports = { module.exports = {
/** /**
@@ -10,39 +9,14 @@ module.exports = {
* @returns {Promise<{msg: string, version: *}|{msg: string, error: number}|{msg: string, error: *}|Device|{msg: string}>} * @returns {Promise<{msg: string, version: *}|{msg: string, error: number}|{msg: string, error: *}|Device|{msg: string}>}
*/ */
async checkDeviceUpdate(deviceId) { async checkDeviceUpdate(deviceId) {
const device = await this.getDevice(deviceId); const updates = await this.checkDevicesUpdates();
const error = _get(device, 'error', false); const update = updates.find((device) => device.deviceId === deviceId);
if (error) { if (!update) {
return device; throw new Error(`${errors.noFirmware}`);
} }
const deviceInfoList = parseFirmwareUpdates([device]); return update;
const deviceInfoListError = _get(deviceInfoList, 'error', false);
if (deviceInfoListError) {
return deviceInfoList;
}
const update = await this.makeRequest({
method: 'post',
url: this.getOtaUrl(),
uri: '/app',
body: { deviceInfoList },
});
const isUpdate = _get(update, 'upgradeInfoList.0.version', false);
if (!isUpdate) {
return { status: 'ok', msg: 'No update available' };
}
return {
status: 'ok',
msg: 'Update available',
version: isUpdate,
};
}, },
}; };

View File

@@ -1,53 +1,42 @@
const { _get } = require('../helpers/utilities'); const compareVersions = require('compare-versions');
const parseFirmwareUpdates = require('../parsers/parseFirmwareUpdates'); const parseFirmwareUpdates = require('../parsers/parseFirmwareUpdates');
const errors = require('../data/errors');
module.exports = { module.exports = {
async checkDevicesUpdates() { async checkDevicesUpdates() {
const devices = await this.getDevices(); const devices = await this.getDevices();
const error = _get(devices, 'error', false);
if (error) {
return devices;
}
const deviceInfoList = parseFirmwareUpdates(devices); const deviceInfoList = parseFirmwareUpdates(devices);
const deviceInfoListError = _get(deviceInfoList, 'error', false);
if (deviceInfoListError) {
return deviceInfoList;
}
const updates = await this.makeRequest({ const updates = await this.makeRequest({
method: 'post', method: 'post',
url: this.getOtaUrl(), uri: '/v2/device/ota/query',
uri: '/app',
body: { deviceInfoList }, body: { deviceInfoList },
}); });
const upgradeInfoList = _get(updates, 'upgradeInfoList', false); const { otaInfoList } = updates;
if (!upgradeInfoList) { if (!otaInfoList) {
return { error: "Can't find firmware update information" }; throw new Error(`${errors.noFirmwares}`);
} }
return upgradeInfoList.map(device => { /** Get current versions */
const upd = _get(device, 'version', false); const currentVersions = {};
deviceInfoList.forEach((device) => {
currentVersions[device.deviceid] = device.version;
});
if (!upd) { return otaInfoList.map((device) => {
return { const current = currentVersions[device.deviceid];
status: 'ok', const { version } = device;
deviceId: device.deviceid, const outdated = compareVersions(version, current);
msg: 'No update available',
};
}
return { return {
status: 'ok', update: !!outdated,
deviceId: device.deviceid, deviceId: device.deviceid,
msg: 'Update available', msg: outdated ? 'Update available' : 'No update available',
version: upd, current,
version,
}; };
}); });
}, },

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

@@ -1,49 +1,46 @@
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const { _get } = require('../helpers/utilities'); const { _get } = require('../helpers/utilities');
const credentialsPayload = require('../payloads/credentialsPayload');
const { makeAuthorizationSign } = require('../helpers/ewelink'); const { makeAuthorizationSign } = require('../helpers/ewelink');
const errors = require('../data/errors');
module.exports = { module.exports = {
/** /**
* Returns user credentials information * Returns user credentials information
*
* @returns {Promise<{msg: string, error: *}>}
*/ */
async getCredentials() { async getCredentials() {
const body = credentialsPayload({ const { APP_ID, APP_SECRET } = this;
email: this.email,
phoneNumber: this.phoneNumber,
password: this.password,
});
const request = await fetch(`${this.getApiUrl()}/user/login`, { const body = {
countryCode: '+1',
email: this.email,
password: this.password,
};
if (this.phoneNumber) {
body.phoneNumber = this.phoneNumber;
}
const request = await fetch(`${this.getApiUrl()}/v2/user/login`, {
method: 'post', method: 'post',
headers: { Authorization: `Sign ${makeAuthorizationSign(body)}` }, headers: {
Authorization: `Sign ${makeAuthorizationSign(APP_SECRET, body)}`,
'Content-Type': 'application/json',
'X-CK-Appid': APP_ID,
},
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
let response = await request.json(); const response = await request.json();
const error = _get(response, 'error', false); const error = _get(response, 'error', false);
const region = _get(response, 'region', false);
if (error && [400, 401, 404].indexOf(parseInt(error)) !== -1) { if (error) {
return { error: 406, msg: errors['406'] }; throw new Error(`[${error}] ${response.msg}`);
} }
if (error && parseInt(error) === 301 && region) { this.apiKey = _get(response, 'data.user.apikey', '');
if (this.region !== region) { this.at = _get(response, 'data.at', '');
this.region = region;
response = await this.getCredentials();
return response;
}
return { error, msg: 'Region does not exist' };
}
this.apiKey = _get(response, 'user.apikey', ''); return response.data;
this.at = _get(response, 'at', '');
return response;
}, },
}; };

View File

@@ -1,5 +1,4 @@
const { APP_ID } = require('../data/constants'); const { _get } = require('../helpers/utilities');
const { nonce, timestamp, _get } = require('../helpers/utilities');
const errors = require('../data/errors'); const errors = require('../data/errors');
module.exports = { module.exports = {
@@ -11,26 +10,27 @@ module.exports = {
*/ */
async getDevice(deviceId) { async getDevice(deviceId) {
if (this.devicesCache) { if (this.devicesCache) {
return this.devicesCache.find(dev => dev.deviceid === deviceId) || null; return this.devicesCache.find((dev) => dev.deviceid === deviceId) || null;
} }
const device = await this.makeRequest({ const device = await this.makeRequest({
uri: `/user/device/${deviceId}`, method: 'post',
qs: { uri: `/v2/device/thing/`,
deviceid: deviceId, body: {
appid: APP_ID, thingList: [{ id: deviceId, itemType: 1 }],
nonce,
ts: timestamp,
version: 8,
}, },
}); });
const error = _get(device, 'error', false); const error = _get(device, 'error', false);
if (error) { if (error) {
return { error, msg: errors[error] }; throw new Error(`[${error}] ${errors[error]}`);
} }
return device; if (device.thingList.length === 0) {
throw new Error(`${errors.noDevice}`);
}
return device.thingList.shift().itemData;
}, },
}; };

View File

@@ -1,7 +1,4 @@
const { _get } = require('../helpers/utilities'); const { _get } = require('../helpers/utilities');
const errors = require('../data/errors');
const { getDeviceChannelCount } = require('../helpers/ewelink');
module.exports = { module.exports = {
/** /**
@@ -13,12 +10,18 @@ module.exports = {
*/ */
async getDeviceChannelCount(deviceId) { async getDeviceChannelCount(deviceId) {
const device = await this.getDevice(deviceId); const device = await this.getDevice(deviceId);
const error = _get(device, 'error', false);
const uiid = _get(device, 'extra.extra.uiid', false);
const switchesAmount = getDeviceChannelCount(uiid);
if (error) { const paramSwitch = _get(device, 'params.switch', false);
return { error, msg: errors[error] }; const paramSwitches = _get(device, 'params.switches', false);
let switchesAmount;
if (paramSwitches) {
switchesAmount = paramSwitches.length;
}
if (!paramSwitches && paramSwitch) {
switchesAmount = 1;
} }
return { status: 'ok', switchesAmount }; return { status: 'ok', switchesAmount };

View File

@@ -10,16 +10,12 @@ module.exports = {
*/ */
async getDeviceCurrentTH(deviceId, type = '') { async getDeviceCurrentTH(deviceId, type = '') {
const device = await this.getDevice(deviceId); const device = await this.getDevice(deviceId);
const error = _get(device, 'error', false);
const temperature = _get(device, 'params.currentTemperature', false); const temperature = _get(device, 'params.currentTemperature', false);
const humidity = _get(device, 'params.currentHumidity', false); const humidity = _get(device, 'params.currentHumidity', false);
if (error) {
return device;
}
if (!temperature || !humidity) { if (!temperature || !humidity) {
return { error: 404, msg: errors.noSensor }; throw new Error(`${errors.noSensor}`);
} }
const data = { status: 'ok', temperature, humidity }; const data = { status: 'ok', temperature, humidity };

View File

@@ -1,3 +1,5 @@
const errors = require('../data/errors');
module.exports = { module.exports = {
/** /**
* Get local IP address from a given MAC * Get local IP address from a given MAC
@@ -6,10 +8,16 @@ module.exports = {
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
getDeviceIP(device) { getDeviceIP(device) {
const mac = device.extra.extra.staMac; if (!this.arpTable) {
throw new Error(errors.noARP);
}
const mac = device.extra.staMac;
const arpItem = this.arpTable.find( const arpItem = this.arpTable.find(
item => item.mac.toLowerCase() === mac.toLowerCase() (item) => item.mac.toLowerCase() === mac.toLowerCase()
); );
return arpItem.ip; return arpItem.ip;
}, },
}; };

View File

@@ -1,8 +1,6 @@
const { _get } = require('../helpers/utilities'); const { _get } = require('../helpers/utilities');
const errors = require('../data/errors'); const errors = require('../data/errors');
const deviceStatusPayload = require('../payloads/deviceStatus');
module.exports = { module.exports = {
/** /**
* Get current power state for a specific device * Get current power state for a specific device
@@ -14,15 +12,18 @@ 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: '/v2/device/thing/status',
qs: deviceStatusPayload({ deviceId, params: 'switch|switches' }), qs: {
type: 1,
id: deviceId,
params: 'switch|switches',
},
}); });
const error = _get(status, 'error', false); const error = _get(status, 'error', false);
if (error) { if (error) {
const err = error === 400 ? 404 : error; throw new Error(`[${error}] ${errors[error]}`);
return { error: err, msg: errors[err] };
} }
let state = _get(status, 'params.switch', false); let state = _get(status, 'params.switch', false);
@@ -31,7 +32,7 @@ module.exports = {
const switchesAmount = switches ? switches.length : 1; const switchesAmount = switches ? switches.length : 1;
if (switchesAmount > 0 && switchesAmount < channel) { if (switchesAmount > 0 && switchesAmount < channel) {
return { error: 404, msg: errors.ch404 }; throw new Error(`${errors.ch404}`);
} }
if (switches) { if (switches) {

View File

@@ -1,5 +1,4 @@
const { APP_ID } = require('../data/constants'); const { _get } = require('../helpers/utilities');
const { _get, timestamp } = require('../helpers/utilities');
const errors = require('../data/errors'); const errors = require('../data/errors');
module.exports = { module.exports = {
@@ -10,27 +9,20 @@ module.exports = {
*/ */
async getDevices() { async getDevices() {
const response = await this.makeRequest({ const response = await this.makeRequest({
uri: '/user/device', uri: `/v2/device/thing/`,
qs: {
lang: 'en',
appid: APP_ID,
ts: timestamp,
version: 8,
getTags: 1,
},
}); });
const error = _get(response, 'error', false); const error = _get(response, 'error', false);
const devicelist = _get(response, 'devicelist', false); const thingList = _get(response, 'thingList', false);
if (error) { if (error) {
return { error, msg: errors[error] }; throw new Error(`[${error}] ${errors[error]}`);
} }
if (!devicelist) { if (!thingList) {
return { error: 404, msg: errors.noDevices }; throw new Error(`${errors.noDevices}`);
} }
return devicelist; return thingList.map((thing) => thing.itemData);
}, },
}; };

View File

@@ -11,11 +11,11 @@ module.exports = {
*/ */
async getFirmwareVersion(deviceId) { async getFirmwareVersion(deviceId) {
const device = await this.getDevice(deviceId); const device = await this.getDevice(deviceId);
const error = _get(device, 'error', false);
const fwVersion = _get(device, 'params.fwVersion', false); const fwVersion = _get(device, 'params.fwVersion', false);
if (error || !fwVersion) { if (!fwVersion) {
return { error, msg: errors[error] }; throw new Error(`${errors.noFirmware}`);
} }
return { status: 'ok', fwVersion }; return { status: 'ok', fwVersion };

View File

@@ -3,16 +3,12 @@ const errors = require('../data/errors');
module.exports = { module.exports = {
async getRegion() { async getRegion() {
if (!this.email || !this.password) {
return { error: 406, msg: errors.invalidAuth };
}
const credentials = await this.getCredentials(); const credentials = await this.getCredentials();
const error = _get(credentials, 'error', false); const error = _get(credentials, 'error', false);
if (error) { if (error) {
return credentials; throw new Error(`[${error}] ${errors[error]}`);
} }
return { return {

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,15 +12,16 @@ 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');
const { toggleDevice } = require('./toggleDevice'); const { toggleDevicePowerState } = require('./toggleDevicePowerState');
const mixins = { const mixins = {
checkDevicesUpdates, checkDevicesUpdates,
checkDeviceUpdate, checkDeviceUpdate,
...deviceControl,
getCredentials, getCredentials,
getDevice, getDevice,
getDeviceChannelCount, getDeviceChannelCount,
@@ -32,10 +34,10 @@ const mixins = {
getFirmwareVersion, getFirmwareVersion,
getRegion, getRegion,
makeRequest, makeRequest,
openWebSocket, ...openWebSocket,
saveDevicesCache, saveDevicesCache,
setDevicePowerState, setDevicePowerState,
toggleDevice, toggleDevicePowerState,
}; };
module.exports = mixins; module.exports = mixins;

View File

@@ -14,7 +14,7 @@ module.exports = {
* @returns {Promise<{msg: *, error: *}|*>} * @returns {Promise<{msg: *, error: *}|*>}
*/ */
async makeRequest({ method = 'get', url, uri, body = {}, qs = {} }) { async makeRequest({ method = 'get', url, uri, body = {}, qs = {} }) {
const { at } = this; const { at, APP_ID } = this;
if (!at) { if (!at) {
await this.getCredentials(); await this.getCredentials();
@@ -31,6 +31,7 @@ module.exports = {
headers: { headers: {
Authorization: `Bearer ${this.at}`, Authorization: `Bearer ${this.at}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CK-Appid': APP_ID,
}, },
}; };
@@ -42,14 +43,24 @@ module.exports = {
const requestUrl = `${apiUrl}${uri}${queryString}`; const requestUrl = `${apiUrl}${uri}${queryString}`;
const request = await fetch(requestUrl, payload); const request = await fetch(requestUrl, payload);
const response = await request.json();
const error = _get(response, 'error', false); /** Catch request status code other than 200 */
if (!request.ok) {
if (error) { throw new Error(`[${request.status}] ${errors[request.status]}`);
return { error, msg: errors[error] };
} }
return response; /** Parse API response */
const response = await request.json();
/** Catch errors with status code 200 */
const error = _get(response, 'error', false);
/** Throw error if needed */
if (error) {
throw new Error(`[${error}] ${response.msg}`);
}
/** Return response data */
return response.data;
}, },
}; };

View File

@@ -1,7 +1,9 @@
const fetch = require('node-fetch');
const W3CWebSocket = require('websocket').w3cwebsocket; const W3CWebSocket = require('websocket').w3cwebsocket;
const WebSocketAsPromised = require('websocket-as-promised'); const WebSocketAsPromised = require('websocket-as-promised');
const wssLoginPayload = require('../payloads/wssLoginPayload'); const wssLoginPayload = require('../payloads/wssLoginPayload');
const errors = require('../data/errors');
module.exports = { module.exports = {
/** /**
@@ -13,16 +15,20 @@ module.exports = {
* @returns {Promise<WebSocketAsPromised>} * @returns {Promise<WebSocketAsPromised>}
*/ */
async openWebSocket(callback, ...{ heartbeat = 120000 }) { async openWebSocket(callback, ...{ heartbeat = 120000 }) {
const dispatch = await this.getWebSocketServer();
const WSS_URL = `wss://${dispatch.domain}:${dispatch.port}/api/ws`;
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(WSS_URL, {
createWebSocket: wss => new W3CWebSocket(wss), createWebSocket: (wss) => new W3CWebSocket(wss),
}); });
wsp.onMessage.addListener(message => { wsp.onMessage.addListener((message) => {
try { try {
const data = JSON.parse(message); const data = JSON.parse(message);
callback(data); callback(data);
@@ -40,4 +46,15 @@ module.exports = {
return wsp; return wsp;
}, },
async getWebSocketServer() {
const requestUrl = this.getDispatchServiceUrl();
const request = await fetch(`${requestUrl}/dispatch/app`);
if (!request.ok) {
throw new Error(`[${request.status}] ${errors[request.status]}`);
}
return request.json();
},
}; };

View File

@@ -1,7 +1,5 @@
const fs = require('fs'); const fs = require('fs');
const { _get } = require('../helpers/utilities');
module.exports = { module.exports = {
/** /**
* Save devices cache file (useful for using zeroconf) * Save devices cache file (useful for using zeroconf)
@@ -10,20 +8,13 @@ module.exports = {
async saveDevicesCache(fileName = './devices-cache.json') { async saveDevicesCache(fileName = './devices-cache.json') {
const devices = await this.getDevices(); const devices = await this.getDevices();
const error = _get(devices, 'error', false);
if (error || !devices) {
return devices;
}
const jsonContent = JSON.stringify(devices, null, 2); const jsonContent = JSON.stringify(devices, null, 2);
try { try {
fs.writeFileSync(fileName, jsonContent, 'utf8'); fs.writeFileSync(fileName, jsonContent, 'utf8');
return { status: 'ok', file: fileName }; return { status: 'ok', file: fileName };
} catch (e) { } catch (e) {
console.log('An error occured while writing JSON Object to File.'); throw new Error('An error occured while writing JSON Object to File.');
return { error: e.toString() };
} }
}, },
}; };

View File

@@ -1,11 +1,6 @@
const { APP_ID } = require('../data/constants'); const { _get } = require('../helpers/utilities');
const { _get, timestamp, nonce } = require('../helpers/utilities');
const errors = require('../data/errors'); const errors = require('../data/errors');
const { getDeviceChannelCount } = require('../helpers/ewelink');
const ChangeStateZeroconf = require('../classes/ChangeStateZeroconf');
module.exports = { module.exports = {
/** /**
* Change power state for a specific device * Change power state for a specific device
@@ -18,20 +13,14 @@ module.exports = {
*/ */
async setDevicePowerState(deviceId, state, channel = 1) { async setDevicePowerState(deviceId, state, channel = 1) {
const device = await this.getDevice(deviceId); const device = await this.getDevice(deviceId);
const error = _get(device, 'error', false);
const uiid = _get(device, 'extra.extra.uiid', false);
let status = _get(device, 'params.switch', false); let status = _get(device, 'params.switch', false);
const switches = _get(device, 'params.switches', false); const switches = _get(device, 'params.switches', false);
const switchesAmount = getDeviceChannelCount(uiid); const switchesAmount = switches.length;
if (switchesAmount > 0 && switchesAmount < channel) { if (switchesAmount > 0 && switchesAmount < channel) {
return { error: 404, msg: errors.ch404 }; throw new Error(`${errors.ch404}`);
}
if (error || (!status && !switches)) {
return { error, msg: errors[error] };
} }
let stateToSwitch = state; let stateToSwitch = state;
@@ -52,33 +41,31 @@ module.exports = {
params.switch = stateToSwitch; params.switch = stateToSwitch;
} }
if (this.devicesCache) { // DISABLED DURING v4.0.0 DEVELOPMENT
return ChangeStateZeroconf.set({ // if (this.devicesCache) {
url: this.getZeroconfUrl(device), // return ChangeStateZeroconf.set({
device, // url: this.getZeroconfUrl(device),
params, // device,
switches, // params,
state: stateToSwitch, // switches,
}); // state: stateToSwitch,
} // });
// }
const response = await this.makeRequest({ const response = await this.makeRequest({
method: 'post', method: 'post',
uri: '/user/device/status', uri: '/v2/device/thing/status',
body: { body: {
deviceid: deviceId, type: 1,
id: deviceId,
params, params,
appid: APP_ID,
nonce,
ts: timestamp,
version: 8,
}, },
}); });
const responseError = _get(response, 'error', false); const responseError = _get(response, 'error', false);
if (responseError) { if (responseError) {
return { error: responseError, msg: errors[responseError] }; throw new Error(`[${error}] ${errors[error]}`);
} }
return { status: 'ok', state, channel }; return { status: 'ok', state, channel };

View File

@@ -7,7 +7,7 @@ module.exports = {
* *
* @returns {Promise<{state: *, status: string}|{msg: string, error: *}>} * @returns {Promise<{state: *, status: string}|{msg: string, error: *}>}
*/ */
async toggleDevice(deviceId, channel = 1) { async toggleDevicePowerState(deviceId, channel = 1) {
return this.setDevicePowerState(deviceId, 'toggle', channel); return this.setDevicePowerState(deviceId, 'toggle', channel);
}, },
}; };

View File

@@ -1,13 +1,13 @@
const { _get } = require('../helpers/utilities'); const { _get } = require('../helpers/utilities');
const errors = require('../data/errors'); const errors = require('../data/errors');
const parseFirmwareUpdates = devicesList => const parseFirmwareUpdates = (devicesList) =>
devicesList.map(device => { devicesList.map((device) => {
const model = _get(device, 'extra.extra.model', false); const model = _get(device, 'extra.model', false);
const fwVersion = _get(device, 'params.fwVersion', false); const fwVersion = _get(device, 'params.fwVersion', false);
if (!model || !fwVersion) { if (!model || !fwVersion) {
return { error: 500, msg: errors.noFirmware }; throw new Error(`${errors.noFirmware}`);
} }
return { model, version: fwVersion, deviceid: device.deviceid }; return { model, version: fwVersion, deviceid: device.deviceid };

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,17 +1,16 @@
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,
apikey: apiKey,
appid: APP_ID,
nonce,
ts: timestamp,
userAgent: 'ewelink-api',
sequence: Math.floor(timestamp * 1000),
version: 8, version: 8,
ts: timestamp,
at,
userAgent: 'app',
apikey: apiKey,
appid,
nonce,
sequence: Math.floor(timestamp * 1000),
}; };
return JSON.stringify(payload); return JSON.stringify(payload);

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