@pipechela/ewelink-api 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +22 -0
- package/index.d.ts +223 -0
- package/main.js +114 -0
- package/package.json +67 -0
- package/src/classes/ChangeStateZeroconf.js +34 -0
- package/src/classes/DevicePowerUsageRaw.js +58 -0
- package/src/classes/WebSocket.js +57 -0
- package/src/classes/Zeroconf.js +91 -0
- package/src/data/constants.js +7 -0
- package/src/data/devices-channel-length.json +20 -0
- package/src/data/devices-type-uuid.json +55 -0
- package/src/data/errors.js +23 -0
- package/src/helpers/device-control.js +58 -0
- package/src/helpers/ewelink.js +68 -0
- package/src/helpers/utilities.js +29 -0
- package/src/mixins/checkDeviceUpdate.js +48 -0
- package/src/mixins/checkDevicesUpdates.js +54 -0
- package/src/mixins/deviceControl.js +272 -0
- package/src/mixins/getCredentials.js +54 -0
- package/src/mixins/getDevice.js +37 -0
- package/src/mixins/getDeviceChannelCount.js +26 -0
- package/src/mixins/getDeviceCurrentTH.js +55 -0
- package/src/mixins/getDeviceIP.js +15 -0
- package/src/mixins/getDevicePowerState.js +47 -0
- package/src/mixins/getDevicePowerUsage.js +27 -0
- package/src/mixins/getDevicePowerUsageRaw.js +30 -0
- package/src/mixins/getDevices.js +37 -0
- package/src/mixins/getFirmwareVersion.js +23 -0
- package/src/mixins/getRegion.js +23 -0
- package/src/mixins/index.js +43 -0
- package/src/mixins/makeRequest.js +54 -0
- package/src/mixins/openWebSocket.js +44 -0
- package/src/mixins/saveDevicesCache.js +29 -0
- package/src/mixins/setDevicePowerState.js +90 -0
- package/src/mixins/toggleDevice.js +13 -0
- package/src/parsers/parseFirmwareUpdates.js +16 -0
- package/src/parsers/parsePowerUsage.js +38 -0
- package/src/payloads/credentialsPayload.js +13 -0
- package/src/payloads/deviceStatus.js +12 -0
- package/src/payloads/wssLoginPayload.js +19 -0
- package/src/payloads/wssUpdatePayload.js +17 -0
- package/src/payloads/zeroConfUpdatePayload.js +17 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SOCKET": 1,
|
|
3
|
+
"SWITCH_CHANGE": 1,
|
|
4
|
+
"GSM_UNLIMIT_SOCKET": 1,
|
|
5
|
+
"SWITCH": 1,
|
|
6
|
+
"THERMOSTAT": 1,
|
|
7
|
+
"SOCKET_POWER": 1,
|
|
8
|
+
"GSM_SOCKET": 1,
|
|
9
|
+
"POWER_DETECTION_SOCKET": 1,
|
|
10
|
+
"SOCKET_2": 2,
|
|
11
|
+
"GSM_SOCKET_2": 2,
|
|
12
|
+
"SWITCH_2": 2,
|
|
13
|
+
"SOCKET_3": 3,
|
|
14
|
+
"GSM_SOCKET_3": 3,
|
|
15
|
+
"SWITCH_3": 3,
|
|
16
|
+
"SOCKET_4": 4,
|
|
17
|
+
"GSM_SOCKET_4": 4,
|
|
18
|
+
"SWITCH_4": 4,
|
|
19
|
+
"CUN_YOU_DOOR": 4
|
|
20
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"1": "SOCKET",
|
|
3
|
+
"2": "SOCKET_2",
|
|
4
|
+
"3": "SOCKET_3",
|
|
5
|
+
"4": "SOCKET_4",
|
|
6
|
+
"6": "SWITCH",
|
|
7
|
+
"5": "SOCKET_POWER",
|
|
8
|
+
"7": "SWITCH_2",
|
|
9
|
+
"8": "SWITCH_3",
|
|
10
|
+
"9": "SWITCH_4",
|
|
11
|
+
"10": "OSPF",
|
|
12
|
+
"11": "CURTAIN",
|
|
13
|
+
"12": "EW-RE",
|
|
14
|
+
"13": "FIREPLACE",
|
|
15
|
+
"14": "SWITCH_CHANGE",
|
|
16
|
+
"15": "THERMOSTAT",
|
|
17
|
+
"16": "COLD_WARM_LED",
|
|
18
|
+
"17": "THREE_GEAR_FAN",
|
|
19
|
+
"18": "SENSORS_CENTER",
|
|
20
|
+
"19": "HUMIDIFIER",
|
|
21
|
+
"22": "RGB_BALL_LIGHT",
|
|
22
|
+
"23": "NEST_THERMOSTAT",
|
|
23
|
+
"24": "GSM_SOCKET",
|
|
24
|
+
"25": "AROMATHERAPY",
|
|
25
|
+
"26": "BJ_THERMOSTAT",
|
|
26
|
+
"27": "GSM_UNLIMIT_SOCKET",
|
|
27
|
+
"28": "RF_BRIDGE",
|
|
28
|
+
"29": "GSM_SOCKET_2",
|
|
29
|
+
"30": "GSM_SOCKET_3",
|
|
30
|
+
"31": "GSM_SOCKET_4",
|
|
31
|
+
"32": "POWER_DETECTION_SOCKET",
|
|
32
|
+
"33": "LIGHT_BELT",
|
|
33
|
+
"34": "FAN_LIGHT",
|
|
34
|
+
"35": "EZVIZ_CAMERA",
|
|
35
|
+
"36": "SINGLE_CHANNEL_DIMMER_SWITCH",
|
|
36
|
+
"38": "HOME_KIT_BRIDGE",
|
|
37
|
+
"40": "FUJIN_OPS",
|
|
38
|
+
"41": "CUN_YOU_DOOR",
|
|
39
|
+
"42": "SMART_BEDSIDE_AND_NEW_RGB_BALL_LIGHT",
|
|
40
|
+
"43": "",
|
|
41
|
+
"44": "",
|
|
42
|
+
"45": "DOWN_CEILING_LIGHT",
|
|
43
|
+
"46": "AIR_CLEANER",
|
|
44
|
+
"49": "MACHINE_BED",
|
|
45
|
+
"51": "COLD_WARM_DESK_LIGHT",
|
|
46
|
+
"52": "DOUBLE_COLOR_DEMO_LIGHT",
|
|
47
|
+
"53": "ELECTRIC_FAN_WITH_LAMP",
|
|
48
|
+
"55": "SWEEPING_ROBOT",
|
|
49
|
+
"56": "RGB_BALL_LIGHT_4",
|
|
50
|
+
"57": "MONOCHROMATIC_BALL_LIGHT",
|
|
51
|
+
"59": "MEARICAMERA",
|
|
52
|
+
"1001": "BLADELESS_FAN",
|
|
53
|
+
"1002": "NEW_HUMIDIFIER",
|
|
54
|
+
"1003": "WARM_AIR_BLOWER"
|
|
55
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const errors = {
|
|
2
|
+
400: 'Parameter error',
|
|
3
|
+
401: 'Wrong account or password',
|
|
4
|
+
402: 'Email inactivated',
|
|
5
|
+
403: 'Forbidden',
|
|
6
|
+
404: 'Device does not exist',
|
|
7
|
+
406: 'Authentication failed',
|
|
8
|
+
503: 'Service Temporarily Unavailable or Device is offline'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const customErrors = {
|
|
12
|
+
ch404: 'Device channel does not exist',
|
|
13
|
+
unknown: 'An unknown error occurred',
|
|
14
|
+
noDevices: 'No devices found',
|
|
15
|
+
noPower: 'No power usage data found',
|
|
16
|
+
noSensor: "Can't read sensor data from device",
|
|
17
|
+
noFirmware: "Can't get model or firmware version",
|
|
18
|
+
invalidAuth: 'Library needs to be initialized using email and password',
|
|
19
|
+
invalidCredentials: 'Invalid credentials provided',
|
|
20
|
+
invalidPowerState: 'Invalid power state. Expecting: "on", "off" or "toggle"',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
module.exports = Object.assign(errors, customErrors);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const STATE_ON = 'on';
|
|
2
|
+
const STATE_OFF = 'off';
|
|
3
|
+
const STATE_TOGGLE = 'toggle';
|
|
4
|
+
|
|
5
|
+
const VALID_POWER_STATES = [STATE_ON, STATE_OFF, STATE_TOGGLE];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Return new device state based on current conditions
|
|
9
|
+
*/
|
|
10
|
+
const getNewPowerState = (currentState, newState) => {
|
|
11
|
+
if (newState !== STATE_TOGGLE) {
|
|
12
|
+
return newState;
|
|
13
|
+
}
|
|
14
|
+
return currentState === STATE_ON ? STATE_OFF : STATE_ON;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get current device parameters and
|
|
19
|
+
*/
|
|
20
|
+
const getPowerStateParams = (params, newState, channel) => {
|
|
21
|
+
if (params.switches) {
|
|
22
|
+
const switches = [...params.switches];
|
|
23
|
+
const channelToSwitch = channel - 1;
|
|
24
|
+
switches[channelToSwitch].switch = newState;
|
|
25
|
+
return { switches };
|
|
26
|
+
}
|
|
27
|
+
return { switch: newState };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Return status of all channels on a multi-channel device
|
|
32
|
+
*/
|
|
33
|
+
const getAllChannelsState = params => {
|
|
34
|
+
const { switches } = params;
|
|
35
|
+
return switches.map(ch => ({
|
|
36
|
+
channel: ch.outlet + 1,
|
|
37
|
+
state: ch.switch,
|
|
38
|
+
}));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Return status of specific channel on multi-channel device
|
|
43
|
+
*/
|
|
44
|
+
const getSpecificChannelState = (params, channel) => {
|
|
45
|
+
const { switches } = params;
|
|
46
|
+
return switches[channel - 1].switch;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
STATE_ON,
|
|
51
|
+
STATE_OFF,
|
|
52
|
+
STATE_TOGGLE,
|
|
53
|
+
VALID_POWER_STATES,
|
|
54
|
+
getNewPowerState,
|
|
55
|
+
getPowerStateParams,
|
|
56
|
+
getAllChannelsState,
|
|
57
|
+
getSpecificChannelState,
|
|
58
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const CryptoJS = require('crypto-js');
|
|
3
|
+
const random = require('random');
|
|
4
|
+
|
|
5
|
+
const DEVICE_TYPE_UUID = require('../data/devices-type-uuid.json');
|
|
6
|
+
const DEVICE_CHANNEL_LENGTH = require('../data/devices-channel-length.json');
|
|
7
|
+
|
|
8
|
+
const makeAuthorizationSign = (APP_SECRET, body) =>
|
|
9
|
+
crypto
|
|
10
|
+
.createHmac('sha256', APP_SECRET)
|
|
11
|
+
.update(JSON.stringify(body))
|
|
12
|
+
.digest('base64');
|
|
13
|
+
|
|
14
|
+
const getDeviceTypeByUiid = uiid => DEVICE_TYPE_UUID[uiid] || '';
|
|
15
|
+
|
|
16
|
+
const getDeviceChannelCountByType = deviceType =>
|
|
17
|
+
DEVICE_CHANNEL_LENGTH[deviceType] || 0;
|
|
18
|
+
|
|
19
|
+
const getDeviceChannelCount = deviceUUID => {
|
|
20
|
+
const deviceType = getDeviceTypeByUiid(deviceUUID);
|
|
21
|
+
return getDeviceChannelCountByType(deviceType);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const create16Uiid = () => {
|
|
25
|
+
let result = '';
|
|
26
|
+
for (let i = 0; i < 16; i += 1) {
|
|
27
|
+
result += random.int(0, 9);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const encryptionBase64 = t =>
|
|
33
|
+
CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(t));
|
|
34
|
+
|
|
35
|
+
const decryptionBase64 = t =>
|
|
36
|
+
CryptoJS.enc.Base64.parse(t).toString(CryptoJS.enc.Utf8);
|
|
37
|
+
|
|
38
|
+
const encryptationData = (data, key) => {
|
|
39
|
+
const encryptedMessage = {};
|
|
40
|
+
const uid = create16Uiid();
|
|
41
|
+
const iv = encryptionBase64(uid);
|
|
42
|
+
const code = CryptoJS.AES.encrypt(data, CryptoJS.MD5(key), {
|
|
43
|
+
iv: CryptoJS.enc.Utf8.parse(uid),
|
|
44
|
+
mode: CryptoJS.mode.CBC,
|
|
45
|
+
padding: CryptoJS.pad.Pkcs7,
|
|
46
|
+
});
|
|
47
|
+
encryptedMessage.uid = uid;
|
|
48
|
+
encryptedMessage.iv = iv;
|
|
49
|
+
encryptedMessage.data = code.ciphertext.toString(CryptoJS.enc.Base64);
|
|
50
|
+
return encryptedMessage;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const decryptionData = (data, key, iv) => {
|
|
54
|
+
const iv64 = decryptionBase64(iv);
|
|
55
|
+
const code = CryptoJS.AES.decrypt(data, CryptoJS.MD5(key), {
|
|
56
|
+
iv: CryptoJS.enc.Utf8.parse(iv64),
|
|
57
|
+
mode: CryptoJS.mode.CBC,
|
|
58
|
+
padding: CryptoJS.pad.Pkcs7,
|
|
59
|
+
});
|
|
60
|
+
return code.toString(CryptoJS.enc.Utf8);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
makeAuthorizationSign,
|
|
65
|
+
getDeviceChannelCount,
|
|
66
|
+
encryptationData,
|
|
67
|
+
decryptionData,
|
|
68
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const nonce = Math.random()
|
|
2
|
+
.toString(36)
|
|
3
|
+
.slice(5);
|
|
4
|
+
|
|
5
|
+
const timestamp = Math.floor(new Date() / 1000);
|
|
6
|
+
|
|
7
|
+
const _get = (obj, path, defaultValue = null) =>
|
|
8
|
+
String.prototype.split
|
|
9
|
+
.call(path, /[,[\].]+?/)
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.reduce(
|
|
12
|
+
(a, c) => (Object.hasOwnProperty.call(a, c) ? a[c] : defaultValue),
|
|
13
|
+
obj
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const _empty = obj => Object.entries(obj).length === 0;
|
|
17
|
+
|
|
18
|
+
const toQueryString = object =>
|
|
19
|
+
`?${Object.keys(object)
|
|
20
|
+
.map(key => `${key}=${object[key].toString()}`)
|
|
21
|
+
.join('&')}`;
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
nonce,
|
|
25
|
+
timestamp,
|
|
26
|
+
_get,
|
|
27
|
+
_empty,
|
|
28
|
+
toQueryString,
|
|
29
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { _get } = require('../helpers/utilities');
|
|
2
|
+
const parseFirmwareUpdates = require('../parsers/parseFirmwareUpdates');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
/**
|
|
6
|
+
* Check device firmware update
|
|
7
|
+
*
|
|
8
|
+
* @param deviceId
|
|
9
|
+
*
|
|
10
|
+
* @returns {Promise<{msg: string, version: *}|{msg: string, error: number}|{msg: string, error: *}|Device|{msg: string}>}
|
|
11
|
+
*/
|
|
12
|
+
async checkDeviceUpdate(deviceId) {
|
|
13
|
+
const device = await this.getDevice(deviceId);
|
|
14
|
+
|
|
15
|
+
const error = _get(device, 'error', false);
|
|
16
|
+
|
|
17
|
+
if (error) {
|
|
18
|
+
return device;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const deviceInfoList = parseFirmwareUpdates([device]);
|
|
22
|
+
|
|
23
|
+
const deviceInfoListError = _get(deviceInfoList, 'error', false);
|
|
24
|
+
|
|
25
|
+
if (deviceInfoListError) {
|
|
26
|
+
return deviceInfoList;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const update = await this.makeRequest({
|
|
30
|
+
method: 'post',
|
|
31
|
+
url: this.getOtaUrl(),
|
|
32
|
+
uri: '/app',
|
|
33
|
+
body: { deviceInfoList },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const isUpdate = _get(update, 'upgradeInfoList.0.version', false);
|
|
37
|
+
|
|
38
|
+
if (!isUpdate) {
|
|
39
|
+
return { status: 'ok', msg: 'No update available' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
status: 'ok',
|
|
44
|
+
msg: 'Update available',
|
|
45
|
+
version: isUpdate,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const { _get } = require('../helpers/utilities');
|
|
2
|
+
const parseFirmwareUpdates = require('../parsers/parseFirmwareUpdates');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
async checkDevicesUpdates() {
|
|
6
|
+
const devices = await this.getDevices();
|
|
7
|
+
|
|
8
|
+
const error = _get(devices, 'error', false);
|
|
9
|
+
|
|
10
|
+
if (error) {
|
|
11
|
+
return devices;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const deviceInfoList = parseFirmwareUpdates(devices);
|
|
15
|
+
|
|
16
|
+
const deviceInfoListError = _get(deviceInfoList, 'error', false);
|
|
17
|
+
|
|
18
|
+
if (deviceInfoListError) {
|
|
19
|
+
return deviceInfoList;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const updates = await this.makeRequest({
|
|
23
|
+
method: 'post',
|
|
24
|
+
url: this.getOtaUrl(),
|
|
25
|
+
uri: '/app',
|
|
26
|
+
body: { deviceInfoList },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const upgradeInfoList = _get(updates, 'upgradeInfoList', false);
|
|
30
|
+
|
|
31
|
+
if (!upgradeInfoList) {
|
|
32
|
+
return { error: "Can't find firmware update information" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return upgradeInfoList.map(device => {
|
|
36
|
+
const upd = _get(device, 'version', false);
|
|
37
|
+
|
|
38
|
+
if (!upd) {
|
|
39
|
+
return {
|
|
40
|
+
status: 'ok',
|
|
41
|
+
deviceId: device.deviceid,
|
|
42
|
+
msg: 'No update available',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
status: 'ok',
|
|
48
|
+
deviceId: device.deviceid,
|
|
49
|
+
msg: 'Update available',
|
|
50
|
+
version: upd,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
const W3CWebSocket = require('websocket').w3cwebsocket;
|
|
2
|
+
const WebSocketAsPromised = require('websocket-as-promised');
|
|
3
|
+
const delay = require('delay');
|
|
4
|
+
|
|
5
|
+
const { nonce, timestamp } = require('../helpers/utilities');
|
|
6
|
+
const errors = require('../data/errors');
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
VALID_POWER_STATES,
|
|
10
|
+
getNewPowerState,
|
|
11
|
+
getPowerStateParams,
|
|
12
|
+
getAllChannelsState,
|
|
13
|
+
getSpecificChannelState,
|
|
14
|
+
} = require('../helpers/device-control');
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
async initDeviceControl(params = {}) {
|
|
18
|
+
// check if socket is already initialized
|
|
19
|
+
if (this.wsp) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { APP_ID, at, apiKey } = this;
|
|
24
|
+
|
|
25
|
+
// set delay between socket messages
|
|
26
|
+
const { delayTime = 1000 } = params;
|
|
27
|
+
this.wsDelayTime = delayTime;
|
|
28
|
+
|
|
29
|
+
// request credentials if needed
|
|
30
|
+
if (at === null || apiKey === null) {
|
|
31
|
+
await this.getCredentials();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// request distribution service
|
|
35
|
+
const dispatch = await this.makeRequest({
|
|
36
|
+
method: 'post',
|
|
37
|
+
url: `https://${this.region}-api.coolkit.cc:8080`,
|
|
38
|
+
uri: '/dispatch/app',
|
|
39
|
+
body: {
|
|
40
|
+
accept: 'ws',
|
|
41
|
+
appid: APP_ID,
|
|
42
|
+
nonce,
|
|
43
|
+
ts: timestamp,
|
|
44
|
+
version: 8,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// WebSocket parameters
|
|
49
|
+
const WSS_URL = `wss://${dispatch.domain}:${dispatch.port}/api/ws`;
|
|
50
|
+
const WSS_CONFIG = { createWebSocket: wss => new W3CWebSocket(wss) };
|
|
51
|
+
|
|
52
|
+
// open WebSocket connection
|
|
53
|
+
this.wsp = new WebSocketAsPromised(WSS_URL, WSS_CONFIG);
|
|
54
|
+
|
|
55
|
+
// catch autentication errors
|
|
56
|
+
let socketError;
|
|
57
|
+
this.wsp.onMessage.addListener(async message => {
|
|
58
|
+
const data = JSON.parse(message);
|
|
59
|
+
if (data.error) {
|
|
60
|
+
socketError = data;
|
|
61
|
+
await this.webSocketClose();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// open socket connection
|
|
66
|
+
await this.wsp.open();
|
|
67
|
+
|
|
68
|
+
// WebSocket handshake
|
|
69
|
+
await this.webSocketHandshake();
|
|
70
|
+
|
|
71
|
+
// if auth error exists, throw an error
|
|
72
|
+
if (socketError) {
|
|
73
|
+
throw new Error(errors[socketError.error]);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* WebSocket authentication process
|
|
79
|
+
*/
|
|
80
|
+
async webSocketHandshake() {
|
|
81
|
+
const apikey = this.deviceApiKey || this.apiKey;
|
|
82
|
+
|
|
83
|
+
const payload = JSON.stringify({
|
|
84
|
+
action: 'userOnline',
|
|
85
|
+
version: 8,
|
|
86
|
+
ts: timestamp,
|
|
87
|
+
at: this.at,
|
|
88
|
+
userAgent: 'app',
|
|
89
|
+
apikey,
|
|
90
|
+
appid: this.APP_ID,
|
|
91
|
+
nonce,
|
|
92
|
+
sequence: Math.floor(timestamp * 1000),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await this.wsp.send(payload);
|
|
96
|
+
await delay(this.wsDelayTime);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Close WebSocket connection and class cleanup
|
|
101
|
+
*/
|
|
102
|
+
async webSocketClose() {
|
|
103
|
+
await this.wsp.close();
|
|
104
|
+
delete this.wsDelayTime;
|
|
105
|
+
delete this.wsp;
|
|
106
|
+
delete this.deviceApiKey;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Update device status (timers, share status, on/off etc)
|
|
111
|
+
*/
|
|
112
|
+
async updateDeviceStatus(deviceId, params) {
|
|
113
|
+
await this.initDeviceControl();
|
|
114
|
+
|
|
115
|
+
const apikey = this.deviceApiKey || this.apiKey;
|
|
116
|
+
|
|
117
|
+
const payload = JSON.stringify({
|
|
118
|
+
action: 'update',
|
|
119
|
+
deviceid: deviceId,
|
|
120
|
+
apikey,
|
|
121
|
+
userAgent: 'app',
|
|
122
|
+
sequence: Math.floor(timestamp * 1000),
|
|
123
|
+
ts: timestamp,
|
|
124
|
+
params,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return this.wsp.send(payload);
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check device status (timers, share status, on/off etc)
|
|
132
|
+
*/
|
|
133
|
+
async getWSDeviceStatus(deviceId, params) {
|
|
134
|
+
await this.initDeviceControl();
|
|
135
|
+
|
|
136
|
+
let response = null;
|
|
137
|
+
|
|
138
|
+
this.wsp.onMessage.addListener(message => {
|
|
139
|
+
const data = JSON.parse(message);
|
|
140
|
+
if (data.deviceid === deviceId) {
|
|
141
|
+
response = data;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const apikey = this.deviceApiKey || this.apiKey;
|
|
146
|
+
|
|
147
|
+
const payload = JSON.stringify({
|
|
148
|
+
action: 'query',
|
|
149
|
+
deviceid: deviceId,
|
|
150
|
+
apikey,
|
|
151
|
+
userAgent: 'app',
|
|
152
|
+
sequence: Math.floor(timestamp * 1000),
|
|
153
|
+
ts: timestamp,
|
|
154
|
+
params,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.wsp.send(payload);
|
|
158
|
+
await delay(this.wsDelayTime);
|
|
159
|
+
|
|
160
|
+
// throw error on invalid device
|
|
161
|
+
if (response.error) {
|
|
162
|
+
throw new Error(errors[response.error]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return response;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get device power state
|
|
170
|
+
*/
|
|
171
|
+
async getWSDevicePowerState(deviceId, options = {}) {
|
|
172
|
+
// get extra parameters
|
|
173
|
+
const { channel = 1, allChannels = false, shared = false } = options;
|
|
174
|
+
|
|
175
|
+
// if device is shared by other account, fetch device api key
|
|
176
|
+
if (shared) {
|
|
177
|
+
const device = await this.getDevice(deviceId);
|
|
178
|
+
this.deviceApiKey = device.apikey;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// get device current state
|
|
182
|
+
const status = await this.getWSDeviceStatus(deviceId, [
|
|
183
|
+
'switch',
|
|
184
|
+
'switches',
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
// close websocket connection
|
|
188
|
+
await this.webSocketClose();
|
|
189
|
+
|
|
190
|
+
// check for multi-channel device
|
|
191
|
+
const multiChannelDevice = !!status.params.switches;
|
|
192
|
+
|
|
193
|
+
// returns all channels
|
|
194
|
+
if (multiChannelDevice && allChannels) {
|
|
195
|
+
return {
|
|
196
|
+
status: 'ok',
|
|
197
|
+
state: getAllChannelsState(status.params),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// multi-channel device & requested channel
|
|
202
|
+
if (multiChannelDevice) {
|
|
203
|
+
return {
|
|
204
|
+
status: 'ok',
|
|
205
|
+
state: getSpecificChannelState(status.params, channel),
|
|
206
|
+
channel,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// single channel device
|
|
211
|
+
return {
|
|
212
|
+
status: 'ok',
|
|
213
|
+
state: status.params.switch,
|
|
214
|
+
channel,
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Set device power state
|
|
220
|
+
*/
|
|
221
|
+
async setWSDevicePowerState(deviceId, state, options = {}) {
|
|
222
|
+
// check for valid power state
|
|
223
|
+
if (!VALID_POWER_STATES.includes(state)) {
|
|
224
|
+
throw new Error(errors.invalidPowerState);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// get extra parameters
|
|
228
|
+
const { channel = 1, shared = false } = options;
|
|
229
|
+
|
|
230
|
+
// if device is shared by other account, fetch device api key
|
|
231
|
+
if (shared) {
|
|
232
|
+
const device = await this.getDevice(deviceId);
|
|
233
|
+
this.deviceApiKey = device.apikey;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// get device current state
|
|
237
|
+
const status = await this.getWSDeviceStatus(deviceId, [
|
|
238
|
+
'switch',
|
|
239
|
+
'switches',
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
// check for multi-channel device
|
|
243
|
+
const multiChannelDevice = !!status.params.switches;
|
|
244
|
+
|
|
245
|
+
// get current device state
|
|
246
|
+
const currentState = multiChannelDevice
|
|
247
|
+
? status.params.switches[channel - 1].switch
|
|
248
|
+
: status.params.switch;
|
|
249
|
+
|
|
250
|
+
// resolve new power state
|
|
251
|
+
const stateToSwitch = getNewPowerState(currentState, state);
|
|
252
|
+
|
|
253
|
+
// build request payload
|
|
254
|
+
const params = getPowerStateParams(status.params, stateToSwitch, channel);
|
|
255
|
+
|
|
256
|
+
// change device status
|
|
257
|
+
try {
|
|
258
|
+
await this.updateDeviceStatus(deviceId, params);
|
|
259
|
+
await delay(this.wsDelayTime);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
throw new Error(error);
|
|
262
|
+
} finally {
|
|
263
|
+
await this.webSocketClose();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
status: 'ok',
|
|
268
|
+
state: stateToSwitch,
|
|
269
|
+
channel: multiChannelDevice ? channel : 1,
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fetch = require('node-fetch');
|
|
2
|
+
|
|
3
|
+
const { _get } = require('../helpers/utilities');
|
|
4
|
+
const credentialsPayload = require('../payloads/credentialsPayload');
|
|
5
|
+
const { makeAuthorizationSign } = require('../helpers/ewelink');
|
|
6
|
+
const errors = require('../data/errors');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
/**
|
|
10
|
+
* Returns user credentials information
|
|
11
|
+
*
|
|
12
|
+
* @returns {Promise<{msg: string, error: *}>}
|
|
13
|
+
*/
|
|
14
|
+
async getCredentials() {
|
|
15
|
+
const { APP_ID, APP_SECRET } = this;
|
|
16
|
+
|
|
17
|
+
const body = credentialsPayload({
|
|
18
|
+
appid: APP_ID,
|
|
19
|
+
email: this.email,
|
|
20
|
+
phoneNumber: this.phoneNumber,
|
|
21
|
+
password: this.password,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const request = await fetch(`${this.getApiUrl()}/user/login`, {
|
|
25
|
+
method: 'post',
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Sign ${makeAuthorizationSign(APP_SECRET, body)}`,
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify(body),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let response = await request.json();
|
|
33
|
+
|
|
34
|
+
const error = _get(response, 'error', false);
|
|
35
|
+
const region = _get(response, 'region', false);
|
|
36
|
+
|
|
37
|
+
if (error && [400, 401, 404].indexOf(parseInt(error)) !== -1) {
|
|
38
|
+
return { error: 406, msg: errors['406'] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (error && parseInt(error) === 301 && region) {
|
|
42
|
+
if (this.region !== region) {
|
|
43
|
+
this.region = region;
|
|
44
|
+
response = await this.getCredentials();
|
|
45
|
+
return response;
|
|
46
|
+
}
|
|
47
|
+
return { error, msg: 'Region does not exist' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.apiKey = _get(response, 'user.apikey', '');
|
|
51
|
+
this.at = _get(response, 'at', '');
|
|
52
|
+
return response;
|
|
53
|
+
},
|
|
54
|
+
};
|