mirror of
https://github.com/skydiver/ewelink-api.git
synced 2025-12-21 21:33:11 +01:00
* 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
273 lines
6.4 KiB
JavaScript
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,
|
|
};
|
|
},
|
|
};
|