Files
ewelink-api/src/mixins/deviceControl.js
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

273 lines
6.4 KiB
JavaScript

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,
};
},
};