Added multi-channel devices and get/set power state (#2)

* added multi-channel device

* added get and set power state

* added some fixes

* add “this” to call class methods
* create params object before build payload
* typo fixed on “switches” param

* added new test cases

* improved error message

* added more test cases

* added authentication error

* added fix channel number too high

* added region change if wrong

* linting + small fixes

* linting + renamed method

* linting
This commit is contained in:
NickDub
2019-07-17 07:23:26 +02:00
committed by Martin M
parent f7e0c60548
commit ec66219ed7
4 changed files with 295 additions and 24 deletions

View File

@@ -63,9 +63,99 @@ const makeAuthorizationSign = body =>
.update(JSON.stringify(body))
.digest('base64');
const getDeviceTypeByUiid = uiid => {
const MAPPING = {
1: 'SOCKET',
2: 'SOCKET_2',
3: 'SOCKET_3',
4: 'SOCKET_4',
5: 'SOCKET_POWER',
6: 'SWITCH',
7: 'SWITCH_2',
8: 'SWITCH_3',
9: 'SWITCH_4',
10: 'OSPF',
11: 'CURTAIN',
12: 'EW-RE',
13: 'FIREPLACE',
14: 'SWITCH_CHANGE',
15: 'THERMOSTAT',
16: 'COLD_WARM_LED',
17: 'THREE_GEAR_FAN',
18: 'SENSORS_CENTER',
19: 'HUMIDIFIER',
22: 'RGB_BALL_LIGHT',
23: 'NEST_THERMOSTAT',
24: 'GSM_SOCKET',
25: 'AROMATHERAPY',
26: 'BJ_THERMOSTAT',
27: 'GSM_UNLIMIT_SOCKET',
28: 'RF_BRIDGE',
29: 'GSM_SOCKET_2',
30: 'GSM_SOCKET_3',
31: 'GSM_SOCKET_4',
32: 'POWER_DETECTION_SOCKET',
33: 'LIGHT_BELT',
34: 'FAN_LIGHT',
35: 'EZVIZ_CAMERA',
36: 'SINGLE_CHANNEL_DIMMER_SWITCH',
38: 'HOME_KIT_BRIDGE',
40: 'FUJIN_OPS',
41: 'CUN_YOU_DOOR',
42: 'SMART_BEDSIDE_AND_NEW_RGB_BALL_LIGHT',
43: '',
44: '',
45: 'DOWN_CEILING_LIGHT',
46: 'AIR_CLEANER',
49: 'MACHINE_BED',
51: 'COLD_WARM_DESK_LIGHT',
52: 'DOUBLE_COLOR_DEMO_LIGHT',
53: 'ELECTRIC_FAN_WITH_LAMP',
55: 'SWEEPING_ROBOT',
56: 'RGB_BALL_LIGHT_4',
57: 'MONOCHROMATIC_BALL_LIGHT',
59: 'MEARICAMERA',
1001: 'BLADELESS_FAN',
1002: 'NEW_HUMIDIFIER',
1003: 'WARM_AIR_BLOWER',
};
return MAPPING[uiid] || '';
};
const getDeviceChannelCountByType = deviceType => {
const DEVICE_CHANNEL_LENGTH = {
SOCKET: 1,
SWITCH_CHANGE: 1,
GSM_UNLIMIT_SOCKET: 1,
SWITCH: 1,
THERMOSTAT: 1,
SOCKET_POWER: 1,
GSM_SOCKET: 1,
POWER_DETECTION_SOCKET: 1,
SOCKET_2: 2,
GSM_SOCKET_2: 2,
SWITCH_2: 2,
SOCKET_3: 3,
GSM_SOCKET_3: 3,
SWITCH_3: 3,
SOCKET_4: 4,
GSM_SOCKET_4: 4,
SWITCH_4: 4,
CUN_YOU_DOOR: 4,
};
return DEVICE_CHANNEL_LENGTH[deviceType] || 0;
};
const getDeviceTotalChannels = deviceUUID => {
const deviceType = getDeviceTypeByUiid(deviceUUID);
const channels = getDeviceChannelCountByType(deviceType);
return channels;
};
module.exports = {
makeAuthorizationSign,
loginPayload,
wssLoginPayload,
wssUpdatePayload,
getDeviceTotalChannels,
};

114
main.js
View File

@@ -10,6 +10,7 @@ const {
loginPayload,
wssLoginPayload,
wssUpdatePayload,
getDeviceTotalChannels,
} = require('./lib/ewelink-helper');
class eWeLink {
@@ -18,14 +19,21 @@ class eWeLink {
return { error: 'No credentials provided' };
}
this.apiUrl = `https://${region}-api.coolkit.cc:8080/api`;
this.apiWebSocket = `wss://${region}-pconnect3.coolkit.cc:8080/api/ws`;
this.region = region;
this.email = email;
this.password = password;
this.at = at;
this.apiKey = apiKey;
}
getApiUrl() {
return `https://${this.region}-api.coolkit.cc:8080/api`;
}
getApiWebSocket() {
return `wss://${this.region}-pconnect3.coolkit.cc:8080/api/ws`;
}
async makeRequest({ method = 'GET', uri, body = {}, qs = {} }) {
const { at } = this;
@@ -35,13 +43,18 @@ class eWeLink {
const response = await rp({
method,
uri: `${this.apiUrl}${uri}`,
uri: `${this.getApiUrl()}${uri}`,
headers: { Authorization: `Bearer ${this.at}` },
body,
qs,
json: true,
});
const error = _get(response, 'error', false);
if (error && [401, 402].indexOf(parseInt(error)) !== -1) {
return { error, msg: 'Authentication error' };
}
return response;
}
@@ -50,13 +63,31 @@ class eWeLink {
email: this.email,
password: this.password,
});
const response = await rp({
let response = await rp({
method: 'POST',
uri: `${this.apiUrl}/user/login`,
uri: `${this.getApiUrl()}/user/login`,
headers: { Authorization: `Sign ${makeAuthorizationSign(body)}` },
body,
json: true,
});
const error = _get(response, 'error', false);
const region = _get(response, 'region', false);
if (error && [400, 401, 404].indexOf(parseInt(error)) !== -1) {
return { error, msg: 'Authentication error' };
}
if (error && parseInt(error) === 301 && region) {
if (this.region !== region) {
this.region = region;
response = await this.login();
return response;
}
return { error, msg: 'Region does not exist' };
}
this.apiKey = _get(response, 'user.apikey', '');
this.at = _get(response, 'at', '');
return response;
@@ -78,24 +109,58 @@ class eWeLink {
return response;
}
async toggleDevice(deviceId) {
async getDevicePowerState(deviceId, channel = 1) {
const device = await this.getDevice(deviceId);
const status = _get(device, 'params.switch', false);
const error = _get(device, 'error', false);
const switchesAmount = getDeviceTotalChannels(device.uiid);
let state = _get(device, 'params.switch', false);
const switches = _get(device, 'params.switches', false);
if (!status) {
return { error: 'Device does not exist' };
if (error || switchesAmount < channel || (!state && !switches)) {
if (error && parseInt(error) === 401) {
return device;
}
return { error, msg: 'Device does not exist' };
}
const state = status === 'on' ? 'off' : 'on';
if (switches) {
state = switches[channel - 1].switch;
}
return { status: 'ok', state };
}
async setDevicePowerState(deviceId, state, channel = 1) {
const device = await this.getDevice(deviceId);
const error = _get(device, 'error', false);
const switchesAmount = getDeviceTotalChannels(device.uiid);
const status = _get(device, 'params.switch', false);
const switches = _get(device, 'params.switches', false);
if (error || switchesAmount < channel || (!status && !switches)) {
if (error && parseInt(error) === 401) {
return device;
}
return { error, msg: 'Device does not exist' };
}
const params = {};
if (switches) {
params.switches = switches;
params.switches[channel - 1].switch = state;
} else {
params.switch = state;
}
const payloadLogin = wssLoginPayload({ at: this.at, apiKey: this.apiKey });
const payloadUpdate = wssUpdatePayload({
apiKey: this.apiKey,
deviceId,
params: { switch: state },
params,
});
const wsp = new WebSocketAsPromised(this.apiWebSocket, {
const wsp = new WebSocketAsPromised(this.getApiWebSocket(), {
createWebSocket: url => new W3CWebSocket(url),
});
@@ -107,6 +172,31 @@ class eWeLink {
return { status: 'ok', state };
}
async toggleDevice(deviceId, channel = 1) {
const powerState = await this.getDevicePowerState(deviceId, channel);
const state = _get(powerState, 'state', false);
if (!state) {
return { error: powerState.error, msg: 'Device does not exist' };
}
const newState = state === 'on' ? 'off' : 'on';
const newResponse = await this.setDevicePowerState(
deviceId,
newState,
channel
);
return newResponse;
}
async getDeviceChannelCount(deviceId) {
const device = await this.getDevice(deviceId);
const switchesAmount = getDeviceTotalChannels(device.uiid);
return switchesAmount;
}
}
module.exports = eWeLink;

View File

@@ -14,9 +14,9 @@ const allDevicesExpectations = {
};
const specificDeviceExpectations = {
apikey: expect.any(String),
deviceid: expect.any(String),
name: expect.any(String),
deviceid: expect.any(String),
apikey: expect.any(String),
online: expect.any(Boolean),
uiid: expect.any(Number),
};

View File

@@ -31,15 +31,40 @@ describe('env: node script', () => {
expect(device.deviceid).toBe(deviceId);
expect(device).toMatchObject(specificDeviceExpectations);
});
test('get device power state', async () => {
const device = await conn.getDevice(deviceId);
const currentState = device.params.switch;
const powerState = await conn.getDevicePowerState(deviceId);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.state).toBe(currentState);
});
test('set device power state', async () => {
jest.setTimeout(30000);
const device = await conn.getDevice(deviceId);
const currentState = device.params.switch;
const newState = currentState === 'on' ? 'off' : 'on';
const powerState = await conn.setDevicePowerState(deviceId, newState);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.state).toBe(newState);
const deviceVerify = await conn.getDevice(deviceId);
const currentStateVerify = deviceVerify.params.switch;
expect(newState).toBe(currentStateVerify);
});
});
describe('env: serverless', () => {
let accessToken;
let apiKey;
test('login into ewelink', async () => {
const conn = new ewelink({ email, password });
const login = await conn.login();
accessToken = login.at;
apiKey = login.user.apikey;
expect(typeof login).toBe('object');
expect(login).toMatchObject(loginExpectations);
});
@@ -58,6 +83,61 @@ describe('env: serverless', () => {
expect(device.deviceid).toBe(deviceId);
expect(device).toMatchObject(specificDeviceExpectations);
});
test('get device power state', async () => {
const conn = new ewelink({ at: accessToken });
const device = await conn.getDevice(deviceId);
const currentState = device.params.switch;
const powerState = await conn.getDevicePowerState(deviceId);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.state).toBe(currentState);
});
test('set device power state', async () => {
jest.setTimeout(30000);
const conn = new ewelink({ at: accessToken, apiKey });
const device = await conn.getDevice(deviceId);
const currentState = device.params.switch;
const newState = currentState === 'on' ? 'off' : 'on';
const powerState = await conn.setDevicePowerState(deviceId, newState);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.state).toBe(newState);
const deviceVerify = await conn.getDevice(deviceId);
const currentStateVerify = deviceVerify.params.switch;
expect(newState).toBe(currentStateVerify);
});
});
describe('valid credentials, invalid device', () => {
test('get device power state should fail', async () => {
const conn = new ewelink({ email, password });
const powerState = await conn.getDevicePowerState('invalid deviceid');
expect(typeof powerState).toBe('object');
expect(powerState.msg).toBe('Device does not exist');
expect(powerState.error).toBe(500);
});
test('set device power state should fail', async () => {
jest.setTimeout(30000);
const conn = new ewelink({ email, password });
const powerState = await conn.setDevicePowerState('invalid deviceid', 'on');
expect(typeof powerState).toBe('object');
expect(powerState.msg).toBe('Device does not exist');
expect(powerState.error).toBe(500);
});
});
describe('valid credentials, wrong region', () => {
test('login into ewelink should fail', async () => {
const conn = new ewelink({ region: 'eu', email, password });
expect(conn.region).toBe('eu');
const login = await conn.login();
expect(typeof login).toBe('object');
expect(login).toMatchObject(loginExpectations);
expect(conn.region).toBe('us');
});
});
describe('invalid credentials', () => {
@@ -71,9 +151,7 @@ describe('invalid credentials', () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const login = await conn.login();
expect(typeof login).toBe('object');
expect(login).toMatchObject({
error: expect.any(Number),
});
expect(login.msg).toBe('Authentication error');
expect(login.error).toBe(400);
});
@@ -81,19 +159,32 @@ describe('invalid credentials', () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const devices = await conn.getDevices();
expect(typeof devices).toBe('object');
expect(devices).toMatchObject({
msg: expect.any(String),
});
expect(devices.msg).toBe('Authentication error');
expect(devices.error).toBe(401);
});
test('get error response on specific device', async () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const device = await conn.getDevice('invalid device id');
const device = await conn.getDevice(deviceId);
expect(typeof device).toBe('object');
expect(device).toMatchObject({
msg: expect.any(String),
});
expect(device.msg).toBe('Authentication error');
expect(device.error).toBe(401);
});
test('get device power state should fail', async () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const powerState = await conn.getDevicePowerState(deviceId);
expect(typeof powerState).toBe('object');
expect(powerState.msg).toBe('Authentication error');
expect(powerState.error).toBe(401);
});
test('set device power state should fail', async () => {
jest.setTimeout(30000);
const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const powerState = await conn.setDevicePowerState(deviceId, 'on');
expect(typeof powerState).toBe('object');
expect(powerState.msg).toBe('Authentication error');
expect(powerState.error).toBe(401);
});
});