Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
bef2a91207 Bump node-fetch from 2.6.1 to 2.6.7
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.1 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.1...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-25 12:59:14 +00:00
23 changed files with 759 additions and 709 deletions

View File

@@ -75,8 +75,7 @@ class eWeLink {
* @returns {string}
*/
getApiUrl() {
const domain = this.region === 'cn' ? 'cn' : 'cc';
return `https://${this.region}-apia.coolkit.${domain}`;
return `https://${this.region}-api.coolkit.cc:8080/api`;
}
/**
@@ -96,11 +95,6 @@ class eWeLink {
return `wss://${this.region}-pconnect3.coolkit.cc:8080/api/ws`;
}
getDispatchServiceUrl() {
const domain = this.region === 'cn' ? 'cn' : 'cc';
return `https://${this.region}-dispa.coolkit.${domain}`;
}
/**
* Generate Zeroconf URL
* @param device

1068
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,28 +41,27 @@
},
"dependencies": {
"arpping": "github:skydiver/arpping",
"compare-versions": "^3.6.0",
"crypto-js": "^4.0.0",
"delay": "^4.4.0",
"node-fetch": "^2.6.1",
"random": "^2.2.0",
"websocket": "^1.0.32",
"websocket-as-promised": "^1.1.0"
"websocket-as-promised": "^1.0.1"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint": "^7.12.0",
"eslint": "^7.11.0",
"eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.14.0",
"eslint-config-wesbos": "1.0.1",
"eslint-config-prettier": "^6.12.0",
"eslint-config-wesbos": "0.0.19",
"eslint-plugin-html": "^6.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.2.0",
"jest": "^26.6.1",
"nock": "^13.0.4",
"prettier": "^2.1.2"
"eslint-plugin-react": "^7.21.4",
"eslint-plugin-react-hooks": "^4.1.2",
"jest": "^26.5.3",
"nock": "^12.0.3",
"prettier": "^1.19.1"
}
}

View File

@@ -5,19 +5,16 @@ const errors = {
403: 'Forbidden',
404: 'Device does not exist',
406: 'Authentication failed',
503: 'Service Temporarily Unavailable or Device is offline',
503: 'Service Temporarily Unavailable or Device is offline'
};
const customErrors = {
ch404: 'Device channel does not exist',
unknown: 'An unknown error occurred',
noARP: 'No ARP information found. You need to generate the ARP file.',
noDevice: 'No device found',
noDevices: 'No devices found',
noPower: 'No power usage data found',
noSensor: "Can't read sensor data from device",
noFirmware: "Can't get model or firmware version",
noFirmwares: "Can't find firmware update information",
invalidAuth: 'Library needs to be initialized using email and password',
invalidCredentials: 'Invalid credentials provided',
invalidPowerState: 'Invalid power state. Expecting: "on", "off" or "toggle"',

View File

@@ -1,4 +1,5 @@
const errors = require('../data/errors');
const { _get } = require('../helpers/utilities');
const parseFirmwareUpdates = require('../parsers/parseFirmwareUpdates');
module.exports = {
/**
@@ -9,14 +10,39 @@ module.exports = {
* @returns {Promise<{msg: string, version: *}|{msg: string, error: number}|{msg: string, error: *}|Device|{msg: string}>}
*/
async checkDeviceUpdate(deviceId) {
const updates = await this.checkDevicesUpdates();
const device = await this.getDevice(deviceId);
const update = updates.find((device) => device.deviceId === deviceId);
const error = _get(device, 'error', false);
if (!update) {
throw new Error(`${errors.noFirmware}`);
if (error) {
return device;
}
return update;
const deviceInfoList = parseFirmwareUpdates([device]);
const deviceInfoListError = _get(deviceInfoList, 'error', false);
if (deviceInfoListError) {
return deviceInfoList;
}
const update = await this.makeRequest({
method: 'post',
url: this.getOtaUrl(),
uri: '/app',
body: { deviceInfoList },
});
const isUpdate = _get(update, 'upgradeInfoList.0.version', false);
if (!isUpdate) {
return { status: 'ok', msg: 'No update available' };
}
return {
status: 'ok',
msg: 'Update available',
version: isUpdate,
};
},
};

View File

@@ -1,42 +1,53 @@
const compareVersions = require('compare-versions');
const { _get } = require('../helpers/utilities');
const parseFirmwareUpdates = require('../parsers/parseFirmwareUpdates');
const errors = require('../data/errors');
module.exports = {
async checkDevicesUpdates() {
const devices = await this.getDevices();
const error = _get(devices, 'error', false);
if (error) {
return devices;
}
const deviceInfoList = parseFirmwareUpdates(devices);
const deviceInfoListError = _get(deviceInfoList, 'error', false);
if (deviceInfoListError) {
return deviceInfoList;
}
const updates = await this.makeRequest({
method: 'post',
uri: '/v2/device/ota/query',
url: this.getOtaUrl(),
uri: '/app',
body: { deviceInfoList },
});
const { otaInfoList } = updates;
const upgradeInfoList = _get(updates, 'upgradeInfoList', false);
if (!otaInfoList) {
throw new Error(`${errors.noFirmwares}`);
if (!upgradeInfoList) {
return { error: "Can't find firmware update information" };
}
/** Get current versions */
const currentVersions = {};
deviceInfoList.forEach((device) => {
currentVersions[device.deviceid] = device.version;
});
return upgradeInfoList.map(device => {
const upd = _get(device, 'version', false);
return otaInfoList.map((device) => {
const current = currentVersions[device.deviceid];
const { version } = device;
const outdated = compareVersions(version, current);
if (!upd) {
return {
status: 'ok',
deviceId: device.deviceid,
msg: 'No update available',
};
}
return {
update: !!outdated,
status: 'ok',
deviceId: device.deviceid,
msg: outdated ? 'Update available' : 'No update available',
current,
version,
msg: 'Update available',
version: upd,
};
});
},

View File

@@ -1,46 +1,54 @@
const fetch = require('node-fetch');
const { _get } = require('../helpers/utilities');
const credentialsPayload = require('../payloads/credentialsPayload');
const { makeAuthorizationSign } = require('../helpers/ewelink');
const errors = require('../data/errors');
module.exports = {
/**
* Returns user credentials information
*
* @returns {Promise<{msg: string, error: *}>}
*/
async getCredentials() {
const { APP_ID, APP_SECRET } = this;
const body = {
countryCode: '+1',
const body = credentialsPayload({
appid: APP_ID,
email: this.email,
phoneNumber: this.phoneNumber,
password: this.password,
};
});
if (this.phoneNumber) {
body.phoneNumber = this.phoneNumber;
}
const request = await fetch(`${this.getApiUrl()}/v2/user/login`, {
const request = await fetch(`${this.getApiUrl()}/user/login`, {
method: 'post',
headers: {
Authorization: `Sign ${makeAuthorizationSign(APP_SECRET, body)}`,
'Content-Type': 'application/json',
'X-CK-Appid': APP_ID,
},
body: JSON.stringify(body),
});
const response = await request.json();
let response = await request.json();
const error = _get(response, 'error', false);
const region = _get(response, 'region', false);
if (error) {
throw new Error(`[${error}] ${response.msg}`);
if (error && [400, 401, 404].indexOf(parseInt(error)) !== -1) {
return { error: 406, msg: errors['406'] };
}
this.apiKey = _get(response, 'data.user.apikey', '');
this.at = _get(response, 'data.at', '');
if (error && parseInt(error) === 301 && region) {
if (this.region !== region) {
this.region = region;
response = await this.getCredentials();
return response;
}
return { error, msg: 'Region does not exist' };
}
return response.data;
this.apiKey = _get(response, 'user.apikey', '');
this.at = _get(response, 'at', '');
return response;
},
};

View File

@@ -1,4 +1,4 @@
const { _get } = require('../helpers/utilities');
const { nonce, timestamp, _get } = require('../helpers/utilities');
const errors = require('../data/errors');
module.exports = {
@@ -10,27 +10,28 @@ module.exports = {
*/
async getDevice(deviceId) {
if (this.devicesCache) {
return this.devicesCache.find((dev) => dev.deviceid === deviceId) || null;
return this.devicesCache.find(dev => dev.deviceid === deviceId) || null;
}
const { APP_ID } = this;
const device = await this.makeRequest({
method: 'post',
uri: `/v2/device/thing/`,
body: {
thingList: [{ id: deviceId, itemType: 1 }],
uri: `/user/device/${deviceId}`,
qs: {
deviceid: deviceId,
appid: APP_ID,
nonce,
ts: timestamp,
version: 8,
},
});
const error = _get(device, 'error', false);
if (error) {
throw new Error(`[${error}] ${errors[error]}`);
return { error, msg: errors[error] };
}
if (device.thingList.length === 0) {
throw new Error(`${errors.noDevice}`);
}
return device.thingList.shift().itemData;
return device;
},
};

View File

@@ -1,4 +1,7 @@
const { _get } = require('../helpers/utilities');
const errors = require('../data/errors');
const { getDeviceChannelCount } = require('../helpers/ewelink');
module.exports = {
/**
@@ -10,18 +13,12 @@ module.exports = {
*/
async getDeviceChannelCount(deviceId) {
const device = await this.getDevice(deviceId);
const error = _get(device, 'error', false);
const uiid = _get(device, 'extra.extra.uiid', false);
const switchesAmount = getDeviceChannelCount(uiid);
const paramSwitch = _get(device, 'params.switch', false);
const paramSwitches = _get(device, 'params.switches', false);
let switchesAmount;
if (paramSwitches) {
switchesAmount = paramSwitches.length;
}
if (!paramSwitches && paramSwitch) {
switchesAmount = 1;
if (error) {
return { error, msg: errors[error] };
}
return { status: 'ok', switchesAmount };

View File

@@ -10,12 +10,16 @@ module.exports = {
*/
async getDeviceCurrentTH(deviceId, type = '') {
const device = await this.getDevice(deviceId);
const error = _get(device, 'error', false);
const temperature = _get(device, 'params.currentTemperature', false);
const humidity = _get(device, 'params.currentHumidity', false);
if (error) {
return device;
}
if (!temperature || !humidity) {
throw new Error(`${errors.noSensor}`);
return { error: 404, msg: errors.noSensor };
}
const data = { status: 'ok', temperature, humidity };

View File

@@ -1,5 +1,3 @@
const errors = require('../data/errors');
module.exports = {
/**
* Get local IP address from a given MAC
@@ -8,16 +6,10 @@ module.exports = {
* @returns {Promise<string>}
*/
getDeviceIP(device) {
if (!this.arpTable) {
throw new Error(errors.noARP);
}
const mac = device.extra.staMac;
const mac = device.extra.extra.staMac;
const arpItem = this.arpTable.find(
(item) => item.mac.toLowerCase() === mac.toLowerCase()
item => item.mac.toLowerCase() === mac.toLowerCase()
);
return arpItem.ip;
},
};

View File

@@ -1,6 +1,8 @@
const { _get } = require('../helpers/utilities');
const errors = require('../data/errors');
const deviceStatusPayload = require('../payloads/deviceStatus');
module.exports = {
/**
* Get current power state for a specific device
@@ -12,18 +14,19 @@ module.exports = {
*/
async getDevicePowerState(deviceId, channel = 1) {
const status = await this.makeRequest({
uri: '/v2/device/thing/status',
qs: {
type: 1,
id: deviceId,
uri: '/user/device/status',
qs: deviceStatusPayload({
appid: this.APP_ID,
deviceId,
params: 'switch|switches',
},
}),
});
const error = _get(status, 'error', false);
if (error) {
throw new Error(`[${error}] ${errors[error]}`);
const err = error === 400 ? 404 : error;
return { error: err, msg: errors[err] };
}
let state = _get(status, 'params.switch', false);
@@ -32,7 +35,7 @@ module.exports = {
const switchesAmount = switches ? switches.length : 1;
if (switchesAmount > 0 && switchesAmount < channel) {
throw new Error(`${errors.ch404}`);
return { error: 404, msg: errors.ch404 };
}
if (switches) {

View File

@@ -1,4 +1,4 @@
const { _get } = require('../helpers/utilities');
const { _get, timestamp } = require('../helpers/utilities');
const errors = require('../data/errors');
module.exports = {
@@ -8,21 +8,30 @@ module.exports = {
* @returns {Promise<{msg: string, error: number}|*>}
*/
async getDevices() {
const { APP_ID } = this;
const response = await this.makeRequest({
uri: `/v2/device/thing/`,
uri: '/user/device',
qs: {
lang: 'en',
appid: APP_ID,
ts: timestamp,
version: 8,
getTags: 1,
},
});
const error = _get(response, 'error', false);
const thingList = _get(response, 'thingList', false);
const devicelist = _get(response, 'devicelist', false);
if (error) {
throw new Error(`[${error}] ${errors[error]}`);
return { error, msg: errors[error] };
}
if (!thingList) {
throw new Error(`${errors.noDevices}`);
if (!devicelist) {
return { error: 404, msg: errors.noDevices };
}
return thingList.map((thing) => thing.itemData);
return devicelist;
},
};

View File

@@ -11,11 +11,11 @@ module.exports = {
*/
async getFirmwareVersion(deviceId) {
const device = await this.getDevice(deviceId);
const error = _get(device, 'error', false);
const fwVersion = _get(device, 'params.fwVersion', false);
if (!fwVersion) {
throw new Error(`${errors.noFirmware}`);
if (error || !fwVersion) {
return { error, msg: errors[error] };
}
return { status: 'ok', fwVersion };

View File

@@ -3,12 +3,16 @@ const errors = require('../data/errors');
module.exports = {
async getRegion() {
if (!this.email || !this.password) {
return { error: 406, msg: errors.invalidAuth };
}
const credentials = await this.getCredentials();
const error = _get(credentials, 'error', false);
if (error) {
throw new Error(`[${error}] ${errors[error]}`);
return credentials;
}
return {

View File

@@ -13,10 +13,10 @@ const { getDevices } = require('./getDevices');
const { getFirmwareVersion } = require('./getFirmwareVersion');
const { getRegion } = require('./getRegion');
const { makeRequest } = require('./makeRequest');
const openWebSocket = require('./openWebSocket');
const { openWebSocket } = require('./openWebSocket');
const { saveDevicesCache } = require('./saveDevicesCache');
const { setDevicePowerState } = require('./setDevicePowerState');
const { toggleDevicePowerState } = require('./toggleDevicePowerState');
const { toggleDevice } = require('./toggleDevice');
const mixins = {
checkDevicesUpdates,
@@ -34,10 +34,10 @@ const mixins = {
getFirmwareVersion,
getRegion,
makeRequest,
...openWebSocket,
openWebSocket,
saveDevicesCache,
setDevicePowerState,
toggleDevicePowerState,
toggleDevice,
};
module.exports = mixins;

View File

@@ -14,7 +14,7 @@ module.exports = {
* @returns {Promise<{msg: *, error: *}|*>}
*/
async makeRequest({ method = 'get', url, uri, body = {}, qs = {} }) {
const { at, APP_ID } = this;
const { at } = this;
if (!at) {
await this.getCredentials();
@@ -31,7 +31,6 @@ module.exports = {
headers: {
Authorization: `Bearer ${this.at}`,
'Content-Type': 'application/json',
'X-CK-Appid': APP_ID,
},
};
@@ -44,23 +43,12 @@ module.exports = {
const request = await fetch(requestUrl, payload);
/** Catch request status code other than 200 */
if (!request.ok) {
throw new Error(`[${request.status}] ${errors[request.status]}`);
return { error: request.status, msg: errors[request.status] };
}
/** Parse API response */
const response = await request.json();
/** Catch errors with status code 200 */
const error = _get(response, 'error', false);
/** Throw error if needed */
if (error) {
throw new Error(`[${error}] ${response.msg}`);
}
/** Return response data */
return response.data;
return response
},
};

View File

@@ -1,9 +1,7 @@
const fetch = require('node-fetch');
const W3CWebSocket = require('websocket').w3cwebsocket;
const WebSocketAsPromised = require('websocket-as-promised');
const wssLoginPayload = require('../payloads/wssLoginPayload');
const errors = require('../data/errors');
module.exports = {
/**
@@ -15,20 +13,17 @@ module.exports = {
* @returns {Promise<WebSocketAsPromised>}
*/
async openWebSocket(callback, ...{ heartbeat = 120000 }) {
const dispatch = await this.getWebSocketServer();
const WSS_URL = `wss://${dispatch.domain}:${dispatch.port}/api/ws`;
const payloadLogin = wssLoginPayload({
at: this.at,
apiKey: this.apiKey,
appid: this.APP_ID,
});
const wsp = new WebSocketAsPromised(WSS_URL, {
createWebSocket: (wss) => new W3CWebSocket(wss),
const wsp = new WebSocketAsPromised(this.getApiWebSocket(), {
createWebSocket: wss => new W3CWebSocket(wss),
});
wsp.onMessage.addListener((message) => {
wsp.onMessage.addListener(message => {
try {
const data = JSON.parse(message);
callback(data);
@@ -46,15 +41,4 @@ module.exports = {
return wsp;
},
async getWebSocketServer() {
const requestUrl = this.getDispatchServiceUrl();
const request = await fetch(`${requestUrl}/dispatch/app`);
if (!request.ok) {
throw new Error(`[${request.status}] ${errors[request.status]}`);
}
return request.json();
},
};

View File

@@ -1,5 +1,7 @@
const fs = require('fs');
const { _get } = require('../helpers/utilities');
module.exports = {
/**
* Save devices cache file (useful for using zeroconf)
@@ -8,13 +10,20 @@ module.exports = {
async saveDevicesCache(fileName = './devices-cache.json') {
const devices = await this.getDevices();
const error = _get(devices, 'error', false);
if (error || !devices) {
return devices;
}
const jsonContent = JSON.stringify(devices, null, 2);
try {
fs.writeFileSync(fileName, jsonContent, 'utf8');
return { status: 'ok', file: fileName };
} catch (e) {
throw new Error('An error occured while writing JSON Object to File.');
console.log('An error occured while writing JSON Object to File.');
return { error: e.toString() };
}
},
};

View File

@@ -1,6 +1,10 @@
const { _get } = require('../helpers/utilities');
const { _get, timestamp, nonce } = require('../helpers/utilities');
const errors = require('../data/errors');
const { getDeviceChannelCount } = require('../helpers/ewelink');
const ChangeStateZeroconf = require('../classes/ChangeStateZeroconf');
module.exports = {
/**
* Change power state for a specific device
@@ -13,14 +17,20 @@ module.exports = {
*/
async setDevicePowerState(deviceId, state, channel = 1) {
const device = await this.getDevice(deviceId);
const error = _get(device, 'error', false);
const uiid = _get(device, 'extra.extra.uiid', false);
let status = _get(device, 'params.switch', false);
const switches = _get(device, 'params.switches', false);
const switchesAmount = switches.length;
const switchesAmount = getDeviceChannelCount(uiid);
if (switchesAmount > 0 && switchesAmount < channel) {
throw new Error(`${errors.ch404}`);
return { error: 404, msg: errors.ch404 };
}
if (error || (!status && !switches)) {
return { error, msg: errors[error] };
}
let stateToSwitch = state;
@@ -41,31 +51,35 @@ module.exports = {
params.switch = stateToSwitch;
}
// DISABLED DURING v4.0.0 DEVELOPMENT
// if (this.devicesCache) {
// return ChangeStateZeroconf.set({
// url: this.getZeroconfUrl(device),
// device,
// params,
// switches,
// state: stateToSwitch,
// });
// }
if (this.devicesCache) {
return ChangeStateZeroconf.set({
url: this.getZeroconfUrl(device),
device,
params,
switches,
state: stateToSwitch,
});
}
const { APP_ID } = this;
const response = await this.makeRequest({
method: 'post',
uri: '/v2/device/thing/status',
uri: '/user/device/status',
body: {
type: 1,
id: deviceId,
deviceid: deviceId,
params,
appid: APP_ID,
nonce,
ts: timestamp,
version: 8,
},
});
const responseError = _get(response, 'error', false);
if (responseError) {
throw new Error(`[${error}] ${errors[error]}`);
return { error: responseError, msg: errors[responseError] };
}
return { status: 'ok', state, channel };

View File

@@ -7,7 +7,7 @@ module.exports = {
*
* @returns {Promise<{state: *, status: string}|{msg: string, error: *}>}
*/
async toggleDevicePowerState(deviceId, channel = 1) {
async toggleDevice(deviceId, channel = 1) {
return this.setDevicePowerState(deviceId, 'toggle', channel);
},
};

View File

@@ -1,13 +1,13 @@
const { _get } = require('../helpers/utilities');
const errors = require('../data/errors');
const parseFirmwareUpdates = (devicesList) =>
devicesList.map((device) => {
const model = _get(device, 'extra.model', false);
const parseFirmwareUpdates = devicesList =>
devicesList.map(device => {
const model = _get(device, 'extra.extra.model', false);
const fwVersion = _get(device, 'params.fwVersion', false);
if (!model || !fwVersion) {
throw new Error(`${errors.noFirmware}`);
return { error: 500, msg: errors.noFirmware };
}
return { model, version: fwVersion, deviceid: device.deviceid };

View File

@@ -3,14 +3,14 @@ const { timestamp, nonce } = require('../helpers/utilities');
const wssLoginPayload = ({ at, apiKey, appid }) => {
const payload = {
action: 'userOnline',
version: 8,
ts: timestamp,
at,
userAgent: 'app',
apikey: apiKey,
appid,
nonce,
ts: timestamp,
userAgent: 'app',
sequence: Math.floor(timestamp * 1000),
version: 8,
};
return JSON.stringify(payload);