mirror of
https://github.com/skydiver/ewelink-api.git
synced 2026-01-02 11:07:28 +01:00
Release v3.1.0 (#93)
* set APP_ID and APP_SECRET from main class * add APP_ID and APP_SECRET as class constructor parameters * updated test case * updated test case * added new test case * docs updated * Release v3.1.0 - "setWSDevicePowerState" (#96) * new mixing to control devices using websocket * switch status on single channel devices * working on deviceControl mixin * better error handling * working on fix for shared devices * refactor/cleanup * added helper function * added docs for new method * return device new status * added test cases * properly close websocket connection and clean used properties * added test cases * error detection enhancements * added test cases * error detection enhancements * added new test file to jest setup * method renamed * fix for closing websocket connection * new getWSDevicePowerState method * added test cases * re-arrange tests * added new test cases * extract helpers methods * added test case * close WebSocket connection on auth error * updated docs * updated dependencies * fix for "forbidden" error * updated dependencies
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -16,6 +16,7 @@ const customErrors = {
|
||||
noFirmware: "Can't get model or firmware version",
|
||||
invalidAuth: 'Library needs to be initialized using email and password',
|
||||
invalidCredentials: 'Invalid credentials provided',
|
||||
invalidPowerState: 'Invalid power state. Expecting: "on", "off" or "toggle"',
|
||||
};
|
||||
|
||||
module.exports = Object.assign(errors, customErrors);
|
||||
|
||||
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))
|
||||
|
||||
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,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -12,7 +12,10 @@ module.exports = {
|
||||
* @returns {Promise<{msg: string, error: *}>}
|
||||
*/
|
||||
async getCredentials() {
|
||||
const { APP_ID, APP_SECRET } = this;
|
||||
|
||||
const body = credentialsPayload({
|
||||
appid: APP_ID,
|
||||
email: this.email,
|
||||
phoneNumber: this.phoneNumber,
|
||||
password: this.password,
|
||||
@@ -20,7 +23,9 @@ module.exports = {
|
||||
|
||||
const request = await fetch(`${this.getApiUrl()}/user/login`, {
|
||||
method: 'post',
|
||||
headers: { Authorization: `Sign ${makeAuthorizationSign(body)}` },
|
||||
headers: {
|
||||
Authorization: `Sign ${makeAuthorizationSign(APP_SECRET, body)}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const { APP_ID } = require('../data/constants');
|
||||
const { nonce, timestamp, _get } = require('../helpers/utilities');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
@@ -14,6 +13,8 @@ module.exports = {
|
||||
return this.devicesCache.find(dev => dev.deviceid === deviceId) || null;
|
||||
}
|
||||
|
||||
const { APP_ID } = this;
|
||||
|
||||
const device = await this.makeRequest({
|
||||
uri: `/user/device/${deviceId}`,
|
||||
qs: {
|
||||
|
||||
@@ -15,7 +15,11 @@ module.exports = {
|
||||
async getDevicePowerState(deviceId, channel = 1) {
|
||||
const status = await this.makeRequest({
|
||||
uri: '/user/device/status',
|
||||
qs: deviceStatusPayload({ deviceId, params: 'switch|switches' }),
|
||||
qs: deviceStatusPayload({
|
||||
appid: this.APP_ID,
|
||||
deviceId,
|
||||
params: 'switch|switches',
|
||||
}),
|
||||
});
|
||||
|
||||
const error = _get(status, 'error', false);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const { APP_ID } = require('../data/constants');
|
||||
const { _get, timestamp } = require('../helpers/utilities');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
@@ -9,6 +8,8 @@ module.exports = {
|
||||
* @returns {Promise<{msg: string, error: number}|*>}
|
||||
*/
|
||||
async getDevices() {
|
||||
const { APP_ID } = this;
|
||||
|
||||
const response = await this.makeRequest({
|
||||
uri: '/user/device',
|
||||
qs: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { checkDevicesUpdates } = require('./checkDevicesUpdates');
|
||||
const { checkDeviceUpdate } = require('./checkDeviceUpdate');
|
||||
const deviceControl = require('./deviceControl');
|
||||
const { getCredentials } = require('./getCredentials');
|
||||
const { getDevice } = require('./getDevice');
|
||||
const { getDeviceChannelCount } = require('./getDeviceChannelCount');
|
||||
@@ -11,7 +12,7 @@ const { getDevicePowerUsageRaw } = require('./getDevicePowerUsageRaw');
|
||||
const { getDevices } = require('./getDevices');
|
||||
const { getFirmwareVersion } = require('./getFirmwareVersion');
|
||||
const { getRegion } = require('./getRegion');
|
||||
const { makeRequest } = require('./makeRequest')
|
||||
const { makeRequest } = require('./makeRequest');
|
||||
const { openWebSocket } = require('./openWebSocket');
|
||||
const { saveDevicesCache } = require('./saveDevicesCache');
|
||||
const { setDevicePowerState } = require('./setDevicePowerState');
|
||||
@@ -20,6 +21,7 @@ const { toggleDevice } = require('./toggleDevice');
|
||||
const mixins = {
|
||||
checkDevicesUpdates,
|
||||
checkDeviceUpdate,
|
||||
...deviceControl,
|
||||
getCredentials,
|
||||
getDevice,
|
||||
getDeviceChannelCount,
|
||||
|
||||
@@ -16,6 +16,7 @@ module.exports = {
|
||||
const payloadLogin = wssLoginPayload({
|
||||
at: this.at,
|
||||
apiKey: this.apiKey,
|
||||
appid: this.APP_ID,
|
||||
});
|
||||
|
||||
const wsp = new WebSocketAsPromised(this.getApiWebSocket(), {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const { APP_ID } = require('../data/constants');
|
||||
const { _get, timestamp, nonce } = require('../helpers/utilities');
|
||||
const errors = require('../data/errors');
|
||||
|
||||
@@ -62,6 +61,8 @@ module.exports = {
|
||||
});
|
||||
}
|
||||
|
||||
const { APP_ID } = this;
|
||||
|
||||
const response = await this.makeRequest({
|
||||
method: 'post',
|
||||
uri: '/user/device/status',
|
||||
|
||||
@@ -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,15 +1,14 @@
|
||||
const { APP_ID } = require('../data/constants');
|
||||
const { timestamp, nonce } = require('../helpers/utilities');
|
||||
|
||||
const wssLoginPayload = ({ at, apiKey }) => {
|
||||
const wssLoginPayload = ({ at, apiKey, appid }) => {
|
||||
const payload = {
|
||||
action: 'userOnline',
|
||||
at,
|
||||
apikey: apiKey,
|
||||
appid: APP_ID,
|
||||
appid,
|
||||
nonce,
|
||||
ts: timestamp,
|
||||
userAgent: 'ewelink-api',
|
||||
userAgent: 'app',
|
||||
sequence: Math.floor(timestamp * 1000),
|
||||
version: 8,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user