mirror of
https://github.com/skydiver/ewelink-api.git
synced 2025-12-21 21:33:11 +01:00
Compare commits
31 Commits
3.0.0
...
release/4.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3655b095d | ||
|
|
82f152d624 | ||
|
|
08e84b5fea | ||
|
|
979664dc34 | ||
|
|
f81fd771a6 | ||
|
|
f2a39c34b4 | ||
|
|
646dfed03c | ||
|
|
4038c8c7bd | ||
|
|
9e7bef86ff | ||
|
|
894aa6f5ca | ||
|
|
ad2d72f4ce | ||
|
|
e5242589f8 | ||
|
|
bf6ab11590 | ||
|
|
ac80e06b0f | ||
|
|
2a64768822 | ||
|
|
930cf2878c | ||
|
|
996c678204 | ||
|
|
45b1c4363a | ||
|
|
47fd13f105 | ||
|
|
94b3bc084a | ||
|
|
9c00d8280a | ||
|
|
16772a5c54 | ||
|
|
03307994ea | ||
|
|
d274a8d560 | ||
|
|
2f4716f379 | ||
|
|
012d0ee092 | ||
|
|
1109e70019 | ||
|
|
5a07f6b615 | ||
|
|
381c344725 | ||
|
|
92b60f21a1 | ||
|
|
b87d092a71 |
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
60
docs/available-methods/getwsdevicepowerstate.md
Normal file
60
docs/available-methods/getwsdevicepowerstate.md
Normal 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' }
|
||||
]
|
||||
}
|
||||
```
|
||||
50
docs/available-methods/setwsdevicepowerstate.md
Normal file
50
docs/available-methods/setwsdevicepowerstate.md
Normal 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
|
||||
}
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
40
main.js
40
main.js
@@ -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 errors = require('./src/data/errors');
|
||||
|
||||
class eWeLink {
|
||||
constructor({
|
||||
region = 'us',
|
||||
email = null,
|
||||
phoneNumber = null,
|
||||
password = null,
|
||||
at = null,
|
||||
apiKey = null,
|
||||
devicesCache = null,
|
||||
arpTable = null,
|
||||
}) {
|
||||
constructor(parameters = {}) {
|
||||
const {
|
||||
region = 'us',
|
||||
email = null,
|
||||
phoneNumber = null,
|
||||
password = null,
|
||||
at = null,
|
||||
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
|
||||
@@ -63,7 +75,8 @@ class eWeLink {
|
||||
* @returns {string}
|
||||
*/
|
||||
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`;
|
||||
}
|
||||
|
||||
getDispatchServiceUrl() {
|
||||
const domain = this.region === 'cn' ? 'cn' : 'cc';
|
||||
return `https://${this.region}-dispa.coolkit.${domain}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Zeroconf URL
|
||||
* @param device
|
||||
|
||||
3752
package-lock.json
generated
3752
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ewelink-api",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.1",
|
||||
"description": "eWeLink API for Node.js",
|
||||
"author": "Martín M.",
|
||||
"license": "MIT",
|
||||
@@ -41,27 +41,28 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"arpping": "github:skydiver/arpping",
|
||||
"compare-versions": "^3.6.0",
|
||||
"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-as-promised": "^1.0.1"
|
||||
"websocket": "^1.0.32",
|
||||
"websocket-as-promised": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.0.0",
|
||||
"eslint-config-airbnb": "^18.1.0",
|
||||
"eslint-config-prettier": "^6.11.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",
|
||||
"nock": "^12.0.3",
|
||||
"prettier": "^1.19.1"
|
||||
"eslint": "^7.12.0",
|
||||
"eslint-config-airbnb": "^18.2.0",
|
||||
"eslint-config-prettier": "^6.14.0",
|
||||
"eslint-config-wesbos": "1.0.1",
|
||||
"eslint-plugin-html": "^6.1.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"jest": "^26.6.1",
|
||||
"nock": "^13.0.4",
|
||||
"prettier": "^2.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,17 +5,22 @@ const errors = {
|
||||
403: 'Forbidden',
|
||||
404: 'Device does not exist',
|
||||
406: 'Authentication failed',
|
||||
503: 'Service Temporarily Unavailable or Device is offline',
|
||||
};
|
||||
|
||||
const customErrors = {
|
||||
ch404: 'Device channel does not exist',
|
||||
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',
|
||||
noPower: 'No power usage data found',
|
||||
noSensor: "Can't read sensor data from device",
|
||||
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',
|
||||
invalidCredentials: 'Invalid credentials provided',
|
||||
invalidPowerState: 'Invalid power state. Expecting: "on", "off" or "toggle"',
|
||||
};
|
||||
|
||||
module.exports = Object.assign(errors, customErrors);
|
||||
|
||||
58
src/helpers/device-control.js
Normal file
58
src/helpers/device-control.js
Normal 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,
|
||||
};
|
||||
@@ -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))
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const { _get } = require('../helpers/utilities');
|
||||
const parseFirmwareUpdates = require('../parsers/parseFirmwareUpdates');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@@ -10,39 +9,14 @@ module.exports = {
|
||||
* @returns {Promise<{msg: string, version: *}|{msg: string, error: number}|{msg: string, error: *}|Device|{msg: string}>}
|
||||
*/
|
||||
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) {
|
||||
return device;
|
||||
if (!update) {
|
||||
throw new Error(`${errors.noFirmware}`);
|
||||
}
|
||||
|
||||
const deviceInfoList = parseFirmwareUpdates([device]);
|
||||
|
||||
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,
|
||||
};
|
||||
return update;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,53 +1,42 @@
|
||||
const { _get } = require('../helpers/utilities');
|
||||
const compareVersions = require('compare-versions');
|
||||
const parseFirmwareUpdates = require('../parsers/parseFirmwareUpdates');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
module.exports = {
|
||||
async checkDevicesUpdates() {
|
||||
const devices = await this.getDevices();
|
||||
|
||||
const error = _get(devices, 'error', false);
|
||||
|
||||
if (error) {
|
||||
return devices;
|
||||
}
|
||||
|
||||
const deviceInfoList = parseFirmwareUpdates(devices);
|
||||
|
||||
const deviceInfoListError = _get(deviceInfoList, 'error', false);
|
||||
|
||||
if (deviceInfoListError) {
|
||||
return deviceInfoList;
|
||||
}
|
||||
|
||||
const updates = await this.makeRequest({
|
||||
method: 'post',
|
||||
url: this.getOtaUrl(),
|
||||
uri: '/app',
|
||||
uri: '/v2/device/ota/query',
|
||||
body: { deviceInfoList },
|
||||
});
|
||||
|
||||
const upgradeInfoList = _get(updates, 'upgradeInfoList', false);
|
||||
const { otaInfoList } = updates;
|
||||
|
||||
if (!upgradeInfoList) {
|
||||
return { error: "Can't find firmware update information" };
|
||||
if (!otaInfoList) {
|
||||
throw new Error(`${errors.noFirmwares}`);
|
||||
}
|
||||
|
||||
return upgradeInfoList.map(device => {
|
||||
const upd = _get(device, 'version', false);
|
||||
/** Get current versions */
|
||||
const currentVersions = {};
|
||||
deviceInfoList.forEach((device) => {
|
||||
currentVersions[device.deviceid] = device.version;
|
||||
});
|
||||
|
||||
if (!upd) {
|
||||
return {
|
||||
status: 'ok',
|
||||
deviceId: device.deviceid,
|
||||
msg: 'No update available',
|
||||
};
|
||||
}
|
||||
return otaInfoList.map((device) => {
|
||||
const current = currentVersions[device.deviceid];
|
||||
const { version } = device;
|
||||
const outdated = compareVersions(version, current);
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
update: !!outdated,
|
||||
deviceId: device.deviceid,
|
||||
msg: 'Update available',
|
||||
version: upd,
|
||||
msg: outdated ? 'Update available' : 'No update available',
|
||||
current,
|
||||
version,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
272
src/mixins/deviceControl.js
Normal file
272
src/mixins/deviceControl.js
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,49 +1,46 @@
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const { _get } = require('../helpers/utilities');
|
||||
const credentialsPayload = require('../payloads/credentialsPayload');
|
||||
const { makeAuthorizationSign } = require('../helpers/ewelink');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Returns user credentials information
|
||||
*
|
||||
* @returns {Promise<{msg: string, error: *}>}
|
||||
*/
|
||||
async getCredentials() {
|
||||
const body = credentialsPayload({
|
||||
email: this.email,
|
||||
phoneNumber: this.phoneNumber,
|
||||
password: this.password,
|
||||
});
|
||||
const { APP_ID, APP_SECRET } = this;
|
||||
|
||||
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',
|
||||
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),
|
||||
});
|
||||
|
||||
let response = await request.json();
|
||||
const response = await request.json();
|
||||
|
||||
const error = _get(response, 'error', false);
|
||||
const region = _get(response, 'region', false);
|
||||
|
||||
if (error && [400, 401, 404].indexOf(parseInt(error)) !== -1) {
|
||||
return { error: 406, msg: errors['406'] };
|
||||
if (error) {
|
||||
throw new Error(`[${error}] ${response.msg}`);
|
||||
}
|
||||
|
||||
if (error && parseInt(error) === 301 && region) {
|
||||
if (this.region !== region) {
|
||||
this.region = region;
|
||||
response = await this.getCredentials();
|
||||
return response;
|
||||
}
|
||||
return { error, msg: 'Region does not exist' };
|
||||
}
|
||||
this.apiKey = _get(response, 'data.user.apikey', '');
|
||||
this.at = _get(response, 'data.at', '');
|
||||
|
||||
this.apiKey = _get(response, 'user.apikey', '');
|
||||
this.at = _get(response, 'at', '');
|
||||
return response;
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const { APP_ID } = require('../data/constants');
|
||||
const { nonce, timestamp, _get } = require('../helpers/utilities');
|
||||
const { _get } = require('../helpers/utilities');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
module.exports = {
|
||||
@@ -11,26 +10,27 @@ module.exports = {
|
||||
*/
|
||||
async getDevice(deviceId) {
|
||||
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({
|
||||
uri: `/user/device/${deviceId}`,
|
||||
qs: {
|
||||
deviceid: deviceId,
|
||||
appid: APP_ID,
|
||||
nonce,
|
||||
ts: timestamp,
|
||||
version: 8,
|
||||
method: 'post',
|
||||
uri: `/v2/device/thing/`,
|
||||
body: {
|
||||
thingList: [{ id: deviceId, itemType: 1 }],
|
||||
},
|
||||
});
|
||||
|
||||
const error = _get(device, 'error', false);
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
const { _get } = require('../helpers/utilities');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
const { getDeviceChannelCount } = require('../helpers/ewelink');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@@ -13,12 +10,18 @@ module.exports = {
|
||||
*/
|
||||
async getDeviceChannelCount(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) {
|
||||
return { error, msg: errors[error] };
|
||||
const paramSwitch = _get(device, 'params.switch', false);
|
||||
const paramSwitches = _get(device, 'params.switches', false);
|
||||
|
||||
let switchesAmount;
|
||||
|
||||
if (paramSwitches) {
|
||||
switchesAmount = paramSwitches.length;
|
||||
}
|
||||
|
||||
if (!paramSwitches && paramSwitch) {
|
||||
switchesAmount = 1;
|
||||
}
|
||||
|
||||
return { status: 'ok', switchesAmount };
|
||||
|
||||
@@ -10,16 +10,12 @@ module.exports = {
|
||||
*/
|
||||
async getDeviceCurrentTH(deviceId, type = '') {
|
||||
const device = await this.getDevice(deviceId);
|
||||
const error = _get(device, 'error', false);
|
||||
|
||||
const temperature = _get(device, 'params.currentTemperature', false);
|
||||
const humidity = _get(device, 'params.currentHumidity', false);
|
||||
|
||||
if (error) {
|
||||
return device;
|
||||
}
|
||||
|
||||
if (!temperature || !humidity) {
|
||||
return { error: 404, msg: errors.noSensor };
|
||||
throw new Error(`${errors.noSensor}`);
|
||||
}
|
||||
|
||||
const data = { status: 'ok', temperature, humidity };
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const errors = require('../data/errors');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Get local IP address from a given MAC
|
||||
@@ -6,10 +8,16 @@ module.exports = {
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
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(
|
||||
item => item.mac.toLowerCase() === mac.toLowerCase()
|
||||
(item) => item.mac.toLowerCase() === mac.toLowerCase()
|
||||
);
|
||||
|
||||
return arpItem.ip;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
const { _get } = require('../helpers/utilities');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
const deviceStatusPayload = require('../payloads/deviceStatus');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Get current power state for a specific device
|
||||
@@ -14,15 +12,18 @@ module.exports = {
|
||||
*/
|
||||
async getDevicePowerState(deviceId, channel = 1) {
|
||||
const status = await this.makeRequest({
|
||||
uri: '/user/device/status',
|
||||
qs: deviceStatusPayload({ deviceId, params: 'switch|switches' }),
|
||||
uri: '/v2/device/thing/status',
|
||||
qs: {
|
||||
type: 1,
|
||||
id: deviceId,
|
||||
params: 'switch|switches',
|
||||
},
|
||||
});
|
||||
|
||||
const error = _get(status, 'error', false);
|
||||
|
||||
if (error) {
|
||||
const err = error === 400 ? 404 : error;
|
||||
return { error: err, msg: errors[err] };
|
||||
throw new Error(`[${error}] ${errors[error]}`);
|
||||
}
|
||||
|
||||
let state = _get(status, 'params.switch', false);
|
||||
@@ -31,7 +32,7 @@ module.exports = {
|
||||
const switchesAmount = switches ? switches.length : 1;
|
||||
|
||||
if (switchesAmount > 0 && switchesAmount < channel) {
|
||||
return { error: 404, msg: errors.ch404 };
|
||||
throw new Error(`${errors.ch404}`);
|
||||
}
|
||||
|
||||
if (switches) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const { APP_ID } = require('../data/constants');
|
||||
const { _get, timestamp } = require('../helpers/utilities');
|
||||
const { _get } = require('../helpers/utilities');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
module.exports = {
|
||||
@@ -10,27 +9,20 @@ module.exports = {
|
||||
*/
|
||||
async getDevices() {
|
||||
const response = await this.makeRequest({
|
||||
uri: '/user/device',
|
||||
qs: {
|
||||
lang: 'en',
|
||||
appid: APP_ID,
|
||||
ts: timestamp,
|
||||
version: 8,
|
||||
getTags: 1,
|
||||
},
|
||||
uri: `/v2/device/thing/`,
|
||||
});
|
||||
|
||||
const error = _get(response, 'error', false);
|
||||
const devicelist = _get(response, 'devicelist', false);
|
||||
const thingList = _get(response, 'thingList', false);
|
||||
|
||||
if (error) {
|
||||
return { error, msg: errors[error] };
|
||||
throw new Error(`[${error}] ${errors[error]}`);
|
||||
}
|
||||
|
||||
if (!devicelist) {
|
||||
return { error: 404, msg: errors.noDevices };
|
||||
if (!thingList) {
|
||||
throw new Error(`${errors.noDevices}`);
|
||||
}
|
||||
|
||||
return devicelist;
|
||||
return thingList.map((thing) => thing.itemData);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,11 +11,11 @@ module.exports = {
|
||||
*/
|
||||
async getFirmwareVersion(deviceId) {
|
||||
const device = await this.getDevice(deviceId);
|
||||
const error = _get(device, 'error', false);
|
||||
|
||||
const fwVersion = _get(device, 'params.fwVersion', false);
|
||||
|
||||
if (error || !fwVersion) {
|
||||
return { error, msg: errors[error] };
|
||||
if (!fwVersion) {
|
||||
throw new Error(`${errors.noFirmware}`);
|
||||
}
|
||||
|
||||
return { status: 'ok', fwVersion };
|
||||
|
||||
@@ -3,16 +3,12 @@ const errors = require('../data/errors');
|
||||
|
||||
module.exports = {
|
||||
async getRegion() {
|
||||
if (!this.email || !this.password) {
|
||||
return { error: 406, msg: errors.invalidAuth };
|
||||
}
|
||||
|
||||
const credentials = await this.getCredentials();
|
||||
|
||||
const error = _get(credentials, 'error', false);
|
||||
|
||||
if (error) {
|
||||
return credentials;
|
||||
throw new Error(`[${error}] ${errors[error]}`);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -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,15 +12,16 @@ const { getDevicePowerUsageRaw } = require('./getDevicePowerUsageRaw');
|
||||
const { getDevices } = require('./getDevices');
|
||||
const { getFirmwareVersion } = require('./getFirmwareVersion');
|
||||
const { getRegion } = require('./getRegion');
|
||||
const { makeRequest } = require('./makeRequest')
|
||||
const { openWebSocket } = require('./openWebSocket');
|
||||
const { makeRequest } = require('./makeRequest');
|
||||
const openWebSocket = require('./openWebSocket');
|
||||
const { saveDevicesCache } = require('./saveDevicesCache');
|
||||
const { setDevicePowerState } = require('./setDevicePowerState');
|
||||
const { toggleDevice } = require('./toggleDevice');
|
||||
const { toggleDevicePowerState } = require('./toggleDevicePowerState');
|
||||
|
||||
const mixins = {
|
||||
checkDevicesUpdates,
|
||||
checkDeviceUpdate,
|
||||
...deviceControl,
|
||||
getCredentials,
|
||||
getDevice,
|
||||
getDeviceChannelCount,
|
||||
@@ -32,10 +34,10 @@ const mixins = {
|
||||
getFirmwareVersion,
|
||||
getRegion,
|
||||
makeRequest,
|
||||
openWebSocket,
|
||||
...openWebSocket,
|
||||
saveDevicesCache,
|
||||
setDevicePowerState,
|
||||
toggleDevice,
|
||||
toggleDevicePowerState,
|
||||
};
|
||||
|
||||
module.exports = mixins;
|
||||
|
||||
@@ -14,7 +14,7 @@ module.exports = {
|
||||
* @returns {Promise<{msg: *, error: *}|*>}
|
||||
*/
|
||||
async makeRequest({ method = 'get', url, uri, body = {}, qs = {} }) {
|
||||
const { at } = this;
|
||||
const { at, APP_ID } = this;
|
||||
|
||||
if (!at) {
|
||||
await this.getCredentials();
|
||||
@@ -31,6 +31,7 @@ module.exports = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.at}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CK-Appid': APP_ID,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -42,14 +43,24 @@ module.exports = {
|
||||
const requestUrl = `${apiUrl}${uri}${queryString}`;
|
||||
|
||||
const request = await fetch(requestUrl, payload);
|
||||
const response = await request.json();
|
||||
|
||||
const error = _get(response, 'error', false);
|
||||
|
||||
if (error) {
|
||||
return { error, msg: errors[error] };
|
||||
/** Catch request status code other than 200 */
|
||||
if (!request.ok) {
|
||||
throw new Error(`[${request.status}] ${errors[request.status]}`);
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const fetch = require('node-fetch');
|
||||
const W3CWebSocket = require('websocket').w3cwebsocket;
|
||||
const WebSocketAsPromised = require('websocket-as-promised');
|
||||
|
||||
const wssLoginPayload = require('../payloads/wssLoginPayload');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@@ -13,16 +15,20 @@ module.exports = {
|
||||
* @returns {Promise<WebSocketAsPromised>}
|
||||
*/
|
||||
async openWebSocket(callback, ...{ heartbeat = 120000 }) {
|
||||
const dispatch = await this.getWebSocketServer();
|
||||
const WSS_URL = `wss://${dispatch.domain}:${dispatch.port}/api/ws`;
|
||||
|
||||
const payloadLogin = wssLoginPayload({
|
||||
at: this.at,
|
||||
apiKey: this.apiKey,
|
||||
appid: this.APP_ID,
|
||||
});
|
||||
|
||||
const wsp = new WebSocketAsPromised(this.getApiWebSocket(), {
|
||||
createWebSocket: wss => new W3CWebSocket(wss),
|
||||
const wsp = new WebSocketAsPromised(WSS_URL, {
|
||||
createWebSocket: (wss) => new W3CWebSocket(wss),
|
||||
});
|
||||
|
||||
wsp.onMessage.addListener(message => {
|
||||
wsp.onMessage.addListener((message) => {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
callback(data);
|
||||
@@ -40,4 +46,15 @@ module.exports = {
|
||||
|
||||
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();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const { _get } = require('../helpers/utilities');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Save devices cache file (useful for using zeroconf)
|
||||
@@ -10,20 +8,13 @@ module.exports = {
|
||||
async saveDevicesCache(fileName = './devices-cache.json') {
|
||||
const devices = await this.getDevices();
|
||||
|
||||
const error = _get(devices, 'error', false);
|
||||
|
||||
if (error || !devices) {
|
||||
return devices;
|
||||
}
|
||||
|
||||
const jsonContent = JSON.stringify(devices, null, 2);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(fileName, jsonContent, 'utf8');
|
||||
return { status: 'ok', file: fileName };
|
||||
} catch (e) {
|
||||
console.log('An error occured while writing JSON Object to File.');
|
||||
return { error: e.toString() };
|
||||
throw new Error('An error occured while writing JSON Object to File.');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
const { APP_ID } = require('../data/constants');
|
||||
const { _get, timestamp, nonce } = require('../helpers/utilities');
|
||||
const { _get } = require('../helpers/utilities');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
const { getDeviceChannelCount } = require('../helpers/ewelink');
|
||||
|
||||
const ChangeStateZeroconf = require('../classes/ChangeStateZeroconf');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Change power state for a specific device
|
||||
@@ -18,20 +13,14 @@ module.exports = {
|
||||
*/
|
||||
async setDevicePowerState(deviceId, state, channel = 1) {
|
||||
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);
|
||||
const switches = _get(device, 'params.switches', false);
|
||||
|
||||
const switchesAmount = getDeviceChannelCount(uiid);
|
||||
const switchesAmount = switches.length;
|
||||
|
||||
if (switchesAmount > 0 && switchesAmount < channel) {
|
||||
return { error: 404, msg: errors.ch404 };
|
||||
}
|
||||
|
||||
if (error || (!status && !switches)) {
|
||||
return { error, msg: errors[error] };
|
||||
throw new Error(`${errors.ch404}`);
|
||||
}
|
||||
|
||||
let stateToSwitch = state;
|
||||
@@ -52,33 +41,31 @@ module.exports = {
|
||||
params.switch = stateToSwitch;
|
||||
}
|
||||
|
||||
if (this.devicesCache) {
|
||||
return ChangeStateZeroconf.set({
|
||||
url: this.getZeroconfUrl(device),
|
||||
device,
|
||||
params,
|
||||
switches,
|
||||
state: stateToSwitch,
|
||||
});
|
||||
}
|
||||
// DISABLED DURING v4.0.0 DEVELOPMENT
|
||||
// if (this.devicesCache) {
|
||||
// return ChangeStateZeroconf.set({
|
||||
// url: this.getZeroconfUrl(device),
|
||||
// device,
|
||||
// params,
|
||||
// switches,
|
||||
// state: stateToSwitch,
|
||||
// });
|
||||
// }
|
||||
|
||||
const response = await this.makeRequest({
|
||||
method: 'post',
|
||||
uri: '/user/device/status',
|
||||
uri: '/v2/device/thing/status',
|
||||
body: {
|
||||
deviceid: deviceId,
|
||||
type: 1,
|
||||
id: deviceId,
|
||||
params,
|
||||
appid: APP_ID,
|
||||
nonce,
|
||||
ts: timestamp,
|
||||
version: 8,
|
||||
},
|
||||
});
|
||||
|
||||
const responseError = _get(response, 'error', false);
|
||||
|
||||
if (responseError) {
|
||||
return { error: responseError, msg: errors[responseError] };
|
||||
throw new Error(`[${error}] ${errors[error]}`);
|
||||
}
|
||||
|
||||
return { status: 'ok', state, channel };
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = {
|
||||
*
|
||||
* @returns {Promise<{state: *, status: string}|{msg: string, error: *}>}
|
||||
*/
|
||||
async toggleDevice(deviceId, channel = 1) {
|
||||
async toggleDevicePowerState(deviceId, channel = 1) {
|
||||
return this.setDevicePowerState(deviceId, 'toggle', channel);
|
||||
},
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
const { _get } = require('../helpers/utilities');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
const parseFirmwareUpdates = devicesList =>
|
||||
devicesList.map(device => {
|
||||
const model = _get(device, 'extra.extra.model', false);
|
||||
const parseFirmwareUpdates = (devicesList) =>
|
||||
devicesList.map((device) => {
|
||||
const model = _get(device, 'extra.model', false);
|
||||
const fwVersion = _get(device, 'params.fwVersion', false);
|
||||
|
||||
if (!model || !fwVersion) {
|
||||
return { error: 500, msg: errors.noFirmware };
|
||||
throw new Error(`${errors.noFirmware}`);
|
||||
}
|
||||
|
||||
return { model, version: fwVersion, deviceid: device.deviceid };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
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,
|
||||
nonce,
|
||||
ts: timestamp,
|
||||
userAgent: 'ewelink-api',
|
||||
sequence: Math.floor(timestamp * 1000),
|
||||
version: 8,
|
||||
ts: timestamp,
|
||||
at,
|
||||
userAgent: 'app',
|
||||
apikey: apiKey,
|
||||
appid,
|
||||
nonce,
|
||||
sequence: Math.floor(timestamp * 1000),
|
||||
};
|
||||
|
||||
return JSON.stringify(payload);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
215
test/device-control.spec.js
Normal 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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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=');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user