diff --git a/lib/ewelink-helper.js b/lib/ewelink-helper.js index 23ac07d..d061724 100644 --- a/lib/ewelink-helper.js +++ b/lib/ewelink-helper.js @@ -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, }; diff --git a/main.js b/main.js index 7bb5b19..bce30d7 100644 --- a/main.js +++ b/main.js @@ -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; diff --git a/test/_expectations.js b/test/_expectations.js index 91f778f..eb67916 100644 --- a/test/_expectations.js +++ b/test/_expectations.js @@ -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), }; diff --git a/test/main.spec.js b/test/main.spec.js index b99149b..2b65102 100644 --- a/test/main.spec.js +++ b/test/main.spec.js @@ -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); + }); });