Compare commits

...

7 Commits
2.0.0 ... 3.1.0

Author SHA1 Message Date
Martín M
381c344725 version bump 2020-10-12 19:14:50 -03:00
Martín M
92b60f21a1 npm audit 2020-10-12 19:03:57 -03:00
Martin M
b87d092a71 Release v3.1.0 (#93)
* 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
2020-10-12 19:01:57 -03:00
Martin M
c11b3a8ab7 Release v3.0.0 (#85)
* updated dependencies

* code linting

* added new app id & app secret

* cleanup requests payloads

* remove unused function

* update test cases

* enabled firmware tests

* refactor getDevice to use right api endpoint

* error messages improvements

* error messages improvements

* error messages improvements

* error messages improvements

* error messages improvements

* payload cleanup

* refactor setDevicePowerState to use right api endpoint

* update test exepectation

* removed deprecated class

* updated tests to reflect new error codes

* error messages improvements

* refactoring project structure: devices methods

refactoring project sturcture

* refactoring project structure: firmware methods

* refactoring project structure: temperature/humidity

* refactoring project structure: credentials methods

* refactoring project structure: power usage methods

* refactoring project structure: power state methods

* refactoring project structure: websocket methods

* removed deprecated login method from docs

* refactoring project structure: power usage methods

* refactoring project structure: zeroconf classes

* refactoring project structure: websocket classes

* refactoring project structure: zeroconf classes

* refactor and cleanup

* refactoring project structure: firmware methods

* moved parsers to own directory

* update tests with methods renames

* export missing temperature/humidity methods

* removed unused package

* refactor and cleanup

* fix test expectation

* refactoring project structure: moved data files

* refactoring project structure: moved data files

* refactoring project structure: moved helpers files

* refactoring project structure: moved helpers files

* refactoring project structure: moved payload files

* refactor and cleanup

* refactor getDevicePowerState

* setDevicePowerState returns channel

* convert error 400 to 404 for clarity

* updated test cases

* remove console.log

* cache path for zeroconf cache files

* installed nock

* using nock to simulate server requests during testing

* moved credentials file to config folder

* update request url when using nock

* refactor nock helper file

* move cooldown delay to setupTests file

* updating testing instructions

* restored delete code block

* fix wrong error code

* accept phone number to login to ewelink

* added test cases for initialize main class

* improvements on class initialization parameters

* allow login using phone number

* rename test file

* updated test case

* fixed regression bug

* Release v3.0.0 - use node-fetch (#87)

* replaced deprecated request library with node-fetch

* refactor: moved makeRequest to own mixin file

* refactor to use node-fetch

* fixes

* update config

* created helper method

* constant rename

* ignore files from final package

* version bump
2020-05-23 03:07:52 -03:00
Martín M
04ba4a1bb3 using new app id & app secret 2020-05-19 09:50:05 -03:00
Martín M
851d6e7a79 fixed docs url 2020-05-15 18:37:45 -03:00
Steve
3439f60465 fix broken link to getRegion documentation (#73) 2020-02-25 09:04:52 -03:00
86 changed files with 6388 additions and 3362 deletions

9
.npmignore Normal file
View File

@@ -0,0 +1,9 @@
.idea/
.vscode/
docs/
test/
.editorconfig
.eslintrc
arp-table.json
demo.js
devices-cache.json

View File

@@ -17,4 +17,4 @@
## Usage ## Usage
Check library documentation and examples at https://github.com/skydiver/ewelink-api/docs Check library documentation and examples at https://github.com/skydiver/ewelink-api/tree/master/docs

View File

@@ -1,30 +0,0 @@
const WebSocket = require('../WebSocket');
const payloads = require('../../lib/payloads');
const { _get } = require('../../lib/helpers');
class ChangeState extends WebSocket {
static async set({ apiUrl, at, apiKey, deviceId, params, state }) {
const payloadLogin = payloads.wssLoginPayload({ at, apiKey });
const payloadUpdate = payloads.wssUpdatePayload({
apiKey,
deviceId,
params,
});
const response = await this.WebSocketRequest(apiUrl, [
payloadLogin,
payloadUpdate,
]);
const error = _get(response[1], 'error', false);
if (error === 403) {
return { error, msg: response[1].reason };
}
return { status: 'ok', state };
}
}
module.exports = ChangeState;

View File

@@ -1,7 +0,0 @@
const ChangeState = require('./ChangeState');
const ChangeStateZeroconf = require('./ChangeStateZeroconf');
module.exports = {
ChangeState,
ChangeStateZeroconf,
};

View File

@@ -1,40 +0,0 @@
class CurrentMonth {
/**
* Return daily power usage
*
* @param hundredDaysKwhData
*
* @returns {{daily: *, monthly: *}}
*/
static parse({ hundredDaysKwhData }) {
const today = new Date();
const days = today.getDate();
let monthlyUsage = 0;
const dailyUsage = [];
for (let day = 0; day < days; day += 1) {
const s = hundredDaysKwhData.substr(6 * day, 2);
const c = hundredDaysKwhData.substr(6 * day + 2, 2);
const f = hundredDaysKwhData.substr(6 * day + 4, 2);
const h = parseInt(s, 16);
const y = parseInt(c, 16);
const I = parseInt(f, 16);
const E = parseFloat(`${h}.${y}${I}`);
dailyUsage.push({
day: days - day,
usage: E,
});
monthlyUsage += E;
}
return {
monthly: monthlyUsage,
daily: dailyUsage,
};
}
}
module.exports = CurrentMonth;

View File

@@ -1,7 +0,0 @@
const DeviceRaw = require('./DeviceRaw');
const CurrentMonth = require('./CurrentMonth');
module.exports = {
DeviceRaw,
CurrentMonth,
};

View File

@@ -13,6 +13,8 @@
* [getDevices](available-methods/getdevices.md) * [getDevices](available-methods/getdevices.md)
* [getDevicePowerState](available-methods/getdevicepowerstate.md) * [getDevicePowerState](available-methods/getdevicepowerstate.md)
* [setDevicePowerState](available-methods/setdevicepowerstate.md) * [setDevicePowerState](available-methods/setdevicepowerstate.md)
* [getWSDevicePowerState](available-methods/getwsdevicepowerstate.md)
* [setWSDevicePowerState](available-methods/setwsdevicepowerstate.md)
* [toggleDevice](available-methods/toggledevice.md) * [toggleDevice](available-methods/toggledevice.md)
* [getDevicePowerUsage](available-methods/getdevicepowerusage.md) * [getDevicePowerUsage](available-methods/getdevicepowerusage.md)
* [getDeviceCurrentTH](available-methods/getdevicecurrentth.md) * [getDeviceCurrentTH](available-methods/getdevicecurrentth.md)
@@ -22,6 +24,5 @@
* [getRegion](available-methods/getregion.md) * [getRegion](available-methods/getregion.md)
* [getFirmwareVersion](available-methods/getfirmwareversion.md) * [getFirmwareVersion](available-methods/getfirmwareversion.md)
* [saveDevicesCache](available-methods/savedevicescache.md) * [saveDevicesCache](available-methods/savedevicescache.md)
* [login](available-methods/login.md) <sup>_*deprecated_</sup>
* [Zeroconf (LAN mode)](zeroconf.md) * [Zeroconf (LAN mode)](zeroconf.md)
* [Testing](testing.md) * [Testing](testing.md)

View File

@@ -8,6 +8,8 @@ Here is the list of available methods.
* [getDevices](getdevices.md) * [getDevices](getdevices.md)
* [getDevicePowerState](getdevicepowerstate.md) * [getDevicePowerState](getdevicepowerstate.md)
* [setDevicePowerState](setdevicepowerstate.md) * [setDevicePowerState](setdevicepowerstate.md)
* [getWSDevicePowerState](getwsdevicepowerstate.md)
* [setWSDevicePowerState](setwsdevicepowerstate.md)
* [toggleDevice](toggledevice.md) * [toggleDevice](toggledevice.md)
* [getDevicePowerUsage](getdevicepowerusage.md) * [getDevicePowerUsage](getdevicepowerusage.md)
* [getDeviceCurrentTH](getdevicecurrentth.md) * [getDeviceCurrentTH](getdevicecurrentth.md)
@@ -17,7 +19,6 @@ Here is the list of available methods.
* [getRegion](getregion.md) * [getRegion](getregion.md)
* [getFirmwareVersion](getfirmwareversion.md) * [getFirmwareVersion](getfirmwareversion.md)
* [saveDevicesCache](savedevicescache.md) * [saveDevicesCache](savedevicescache.md)
* [login](login.md) <sup>_*deprecated_</sup>
Remember to instantiate class before usage. Remember to instantiate class before usage.

View File

@@ -22,6 +22,7 @@ Query for specified device power status.
```js ```js
{ {
status: 'ok', status: 'ok',
state: 'off' state: 'off',
channel: 1
} }
``` ```

View File

@@ -0,0 +1,60 @@
# getWSDevicePowerState
Query for specified device power status using WebSockets.
### Usage
```js
// get device power status
const status = await connection.getWSDevicePowerState('<your device id>');
console.log(status);
```
```js
// get device power status using a secondary account
const status = await connection.getWSDevicePowerState('<your device id>', {
shared: true,
});
console.log(status);
```
```js
// get channel 3 power status on multi-channel device
const status = await connection.getWSDevicePowerState('<your device id>', {
channel: 3,
});
console.log(status);
```
```js
// get all channels power status on multi-channel device
const status = await connection.getWSDevicePowerState('<your device id>', {
allChannels: true,
});
console.log(status);
```
<sup>* _Remember to instantiate class before use_</sup>
### Response example
```js
{
status: 'ok',
state: 'off',
channel: 1
}
```
```js
{
status: 'ok',
state: [
{ channel: 1, state: 'off' },
{ channel: 2, state: 'off' },
{ channel: 3, state: 'off' },
{ channel: 4, state: 'off' }
]
}
```

View File

@@ -1,20 +0,0 @@
# Login
> DEPRECATED: use [getCredentials](available-methods/getcredentials.md) method instead
Login into eWeLink API and get auth credentials.
This method is useful on serverless context, where you need to obtain auth credentials to make individual requests.
### Usage
```
const auth = await connection.login();
console.log('access token: ', auth.at);
console.log('api key: ', auth.user.apikey);
console.log('region: ', auth.region);
```
<sup>* _Remember to instantiate class before use_</sup>

View File

@@ -19,7 +19,7 @@ const connection = new ewelink({
}); });
// login into eWeLink // login into eWeLink
await connection.login(); await connection.getCredentials();
// call openWebSocket method with a callback as argument // call openWebSocket method with a callback as argument
const socket = await connection.openWebSocket(async data => { const socket = await connection.openWebSocket(async data => {

View File

@@ -24,6 +24,7 @@ Possible states: `on`, `off`, `toggle`.
```js ```js
{ {
status: 'ok', status: 'ok',
state: 'on' state: 'on',
channel: 1
} }
``` ```

View File

@@ -0,0 +1,50 @@
# setWSDevicePowerState
Change specified device power state using WebSockets.
Possible states: `on`, `off`, `toggle`.
### Usage
```js
const status = await connection.setWSDevicePowerState('<your device id>', 'on');
console.log(status);
```
```js
// multi-channel devices like Sonoff 4CH
// example will toggle power state on channel 3
const status = await connection.setWSDevicePowerState('<your device id>', 'toggle', {
channel: 3,
});
console.log(status);
```
```js
// to control a shared device using a second account, add "shared" setting
const status = await connection.setWSDevicePowerState('<your device id>', 'off', {
shared: true
});
console.log(status);
```
```js
// turn on channel 2 on a shared multi-channel device
const status = await connection.setWSDevicePowerState('<your device id>', 'on', {
channel: 2,
shared: true
});
console.log(status);
```
<sup>* _Remember to instantiate class before use_</sup>
### Response example
```js
{
status: 'ok',
state: 'on',
channel: 1
}
```

View File

@@ -1,8 +1,12 @@
# Class Instantiation # Class Instantiation
> Default region of this library is `us`. If your are in a different one, **you must** specify region parameter or error 400/401 will be returned. * Default region of this library is `us`. If your are in a different one, **you must** specify region parameter or error 400/401 will be returned.
**_Using email and password_** * If you don't know your region, use [getRegion](available-methods/getregion) method
* To get your access token and api key, use [getCredentials](available-methods/getcredentials) method
## Using email and password
``` ```
const connection = new ewelink({ const connection = new ewelink({
email: '<your ewelink email>', email: '<your ewelink email>',
@@ -11,7 +15,16 @@
}); });
``` ```
**_Using access token and api key_** ## Using phone number and password
```
const connection = new ewelink({
phoneNumber: '<your phone number>',
password: '<your ewelink password>',
region: '<your ewelink region>',
});
```
## Using access token and api key
``` ```
const connection = new ewelink({ const connection = new ewelink({
at: '<valid access token>', at: '<valid access token>',
@@ -20,5 +33,17 @@
}); });
``` ```
> * If you don't know your region, use [getRegion](available-methods/getregion) method ## Custom APP_ID and APP_SECRET
> * To get your access token and api key, use [getCredentials](available-methods/getcredentials) method This library uses an APP ID and APP Secret provided by Sonoff team.
If you want to specify another pair of settings, just pass in the class constructor:
```
const connection = new ewelink({
email: '<your ewelink email>',
password: '<your ewelink password>',
APP_ID: 'CUSTOM APP ID',
APP_SECRET: 'CUSTOM APP SECRET',
});
```
## Using devices and arp table cache files
Check [ZeroConf](zeroconf.md) docs for detailed information.

View File

@@ -27,4 +27,4 @@ const ewelink = require('ewelink-api');
})(); })();
``` ```
> If you don't know your region, use [getRegion](/docs/available-methods/getregion) method > If you don't know your region, use [getRegion](/docs/available-methods/getregion.md) method

View File

@@ -17,11 +17,11 @@ So, instead of using email and password on every api call, you can login the fir
region: '<your ewelink region>', region: '<your ewelink region>',
}); });
const login = await connection.login(); const credentials = await connection.getCredentials();
const accessToken = login.at; const accessToken = credentials.at;
const apiKey = login.user.apikey const apiKey = credentials.user.apikey
const region = login.region; const region = credentials.region;
})(); })();
``` ```

View File

@@ -1,8 +1,26 @@
# Testing # Testing
Open `test/_setup/credentials.json` and update parameters. ## Run test suite
Copy `test/_setup/config/credentials.example.js` to `test/_setup/config/credentials.js` and update parameters with yours.
In a terminal, `npm run test` or `npm run coverage`. In a terminal, `npm run test` or `npm run coverage`.
Tests needs to be performed serially to prevent flooding eWeLink servers, so if run jest manually, add `--runInBand` parameter.
> tests needs to be performed serially, so if run jest manually, add `--runInBand` parameter. > All devices needs to be connected before running test suite.
## Using nock
Running tests can take some time because there is many requests to eWeLink servers.
To speedup this process, you need to enable nock "record & play" feature by opening `test/_setup/setupTests.js` and change `nockAction` to `record` or `play`.
The first time you need to record all your requests then you can keep testing by "playing" recorded data.
Recorded data will be stored on `test/_setup/tapes` and you can delete folder content anytime.
Set `nockAction` to `false` to disable all nock functionality.
## ZeroConf cache
While testing ZeroConf functionalty, two temporary files will be created: `test/_setup/cache/arp-table.json` and `test/_setup/cache devices-cache.json`. These files can be safely deleted once tests finished.

View File

@@ -35,7 +35,7 @@ A file named `devices-cache.json` will be created.
## 2. Generate arp table cache file ## 2. Generate arp table cache file
```js ```js
const Zeroconf = require('ewelink-api/classes/Zeroconf'); const Zeroconf = require('ewelink-api/src/classes/Zeroconf');
await Zeroconf.saveArpTable({ await Zeroconf.saveArpTable({
ip: '<your network addres, ex: 192.168.5.1>' ip: '<your network addres, ex: 192.168.5.1>'
@@ -49,7 +49,7 @@ A file named `arp-table.json` will be created.
```js ```js
const ewelink = require('ewelink-api'); const ewelink = require('ewelink-api');
const Zeroconf = require('ewelink-api/classes/Zeroconf'); const Zeroconf = require('ewelink-api/src/classes/Zeroconf');
/* load cache files */ /* load cache files */
const devicesCache = await Zeroconf.loadCachedDevices(); const devicesCache = await Zeroconf.loadCachedDevices();

View File

@@ -1,10 +0,0 @@
const _get = (obj, path, defaultValue = null) =>
String.prototype.split
.call(path, /[,[\].]+?/)
.filter(Boolean)
.reduce(
(a, c) => (Object.hasOwnProperty.call(a, c) ? a[c] : defaultValue),
obj
);
module.exports = { _get };

View File

@@ -1,19 +0,0 @@
const nonce = require('nonce')();
const { makeFakeIMEI } = require('../ewelink-helper');
const credentialsPayload = ({ email, password }) => ({
email,
password,
version: 6,
ts: `${Math.round(new Date().getTime() / 1000)}`,
nonce: `${nonce()}`,
appid: 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq',
imei: makeFakeIMEI(),
os: 'iOS',
model: 'iPhone10,6',
romVersion: '11.1.2',
appVersion: '3.5.3',
});
module.exports = credentialsPayload;

View File

@@ -1,13 +0,0 @@
const firmwareUpdate = require('./firmwareUpdate');
const credentialsPayload = require('./credentialsPayload');
const wssLoginPayload = require('./wssLoginPayload');
const wssUpdatePayload = require('./wssUpdatePayload');
const zeroConfUpdatePayload = require('./zeroConfUpdatePayload');
module.exports = {
firmwareUpdate,
credentialsPayload,
wssLoginPayload,
wssUpdatePayload,
zeroConfUpdatePayload,
};

View File

@@ -1,24 +0,0 @@
const nonce = require('nonce')();
const wssLoginPayload = ({ at, apiKey }) => {
const timeStamp = new Date() / 1000;
const ts = Math.floor(timeStamp);
const sequence = Math.floor(timeStamp * 1000);
const payload = {
action: 'userOnline',
userAgent: 'app',
version: 6,
nonce: `${nonce()}`,
apkVesrion: '1.8',
os: 'ios',
at,
apikey: apiKey,
ts: `${ts}`,
model: 'iPhone10,6',
romVersion: '11.1.2',
sequence,
};
return JSON.stringify(payload);
};
module.exports = wssLoginPayload;

179
main.js
View File

@@ -1,28 +1,72 @@
const rp = require('request-promise'); const {
APP_ID: DEFAULT_APP_ID,
APP_SECRET: DEFAULT_APP_SECRET,
} = require('./src/data/constants');
const { _get } = require('./lib/helpers'); const mixins = require('./src/mixins');
const errors = require('./src/data/errors');
class eWeLink { class eWeLink {
constructor({ constructor(parameters = {}) {
region = 'us', const {
email, region = 'us',
password, email = null,
at, phoneNumber = null,
apiKey, password = null,
devicesCache, at = null,
arpTable, apiKey = null,
}) { devicesCache = null,
if (!devicesCache && !arpTable && !at && (!email && !password)) { arpTable = null,
return { error: 'No credentials provided' }; APP_ID = DEFAULT_APP_ID,
APP_SECRET = DEFAULT_APP_SECRET,
} = parameters;
const check = this.checkLoginParameters({
region,
email,
phoneNumber,
password,
at,
apiKey,
devicesCache,
arpTable,
});
if (check === false) {
throw new Error(errors.invalidCredentials);
} }
this.region = region; this.region = region;
this.phoneNumber = phoneNumber;
this.email = email; this.email = email;
this.password = password; this.password = password;
this.at = at; this.at = at;
this.apiKey = apiKey; this.apiKey = apiKey;
this.devicesCache = devicesCache; this.devicesCache = devicesCache;
this.arpTable = arpTable; this.arpTable = arpTable;
this.APP_ID = APP_ID;
this.APP_SECRET = APP_SECRET;
}
// eslint-disable-next-line class-methods-use-this
checkLoginParameters(params) {
const { email, phoneNumber, password, devicesCache, arpTable, at } = params;
if (email !== null && phoneNumber !== null) {
return false;
}
if (
(email !== null && password !== null) ||
(phoneNumber !== null && password !== null) ||
(devicesCache !== null && arpTable !== null) ||
at !== null
) {
return true;
}
return false;
} }
/** /**
@@ -57,116 +101,11 @@ class eWeLink {
* @returns {string} * @returns {string}
*/ */
getZeroconfUrl(device) { getZeroconfUrl(device) {
const ip = this.getLocalIp(device); const ip = this.getDeviceIP(device);
return `http://${ip}:8081/zeroconf`; return `http://${ip}:8081/zeroconf`;
} }
/**
* Generate http requests helpers
*
* @param method
* @param url
* @param uri
* @param body
* @param qs
*
* @returns {Promise<{msg: string, error: *}>}
*/
async makeRequest({ method = 'GET', url, uri, body = {}, qs = {} }) {
const { at } = this;
if (!at) {
await this.getCredentials();
}
let apiUrl = this.getApiUrl();
if (url) {
apiUrl = url;
}
const response = await rp({
method,
uri: `${apiUrl}${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;
}
} }
/* LOAD MIXINS: user */ Object.assign(eWeLink.prototype, mixins);
const getCredentialsMixin = require('./mixins/user/getCredentialsMixin');
const getRegionMixin = require('./mixins/user/getRegionMixin');
/* LOAD MIXINS: power state */
const getDevicePowerStateMixin = require('./mixins/powerState/getDevicePowerStateMixin');
const setDevicePowerState = require('./mixins/powerState/setDevicePowerStateMixin');
const toggleDeviceMixin = require('./mixins/powerState/toggleDeviceMixin');
/* LOAD MIXINS: power usage */
const getDevicePowerUsageMixin = require('./mixins/powerUsage/getDevicePowerUsageMixin');
const getDeviceRawPowerUsageMixin = require('./mixins/powerUsage/getDeviceRawPowerUsageMixin');
/* LOAD MIXINS: temperature & humidity */
const getTHMixin = require('./mixins/temphumd/getTHMixin');
/* LOAD MIXINS: devices */
const getDevicesMixin = require('./mixins/devices/getDevicesMixin');
const getDeviceMixin = require('./mixins/devices/getDeviceMixin');
const getDeviceChannelCountMixin = require('./mixins/devices/getDeviceChannelCountMixin');
const getLocalIpMixin = require('./mixins/devices/getLocalIpMixin');
const saveDevicesCacheMixin = require('./mixins/devices/saveDevicesCacheMixin');
/* LOAD MIXINS: firmware */
const getFirmwareVersionMixin = require('./mixins/firmware/getFirmwareVersionMixin');
const checkDeviceUpdateMixin = require('./mixins/firmware/checkDeviceUpdateMixin');
const checkDevicesUpdatesMixin = require('./mixins/firmware/checkDevicesUpdatesMixin');
/* LOAD MIXINS: websocket */
const openWebSocketMixin = require('./mixins/websocket/openWebSocketMixin');
Object.assign(eWeLink.prototype, getCredentialsMixin, getRegionMixin);
Object.assign(
eWeLink.prototype,
getDevicePowerStateMixin,
setDevicePowerState,
toggleDeviceMixin
);
Object.assign(
eWeLink.prototype,
getDevicePowerUsageMixin,
getDeviceRawPowerUsageMixin
);
Object.assign(eWeLink.prototype, getTHMixin);
Object.assign(
eWeLink.prototype,
getDevicesMixin,
getDeviceMixin,
getDeviceChannelCountMixin,
getLocalIpMixin,
saveDevicesCacheMixin
);
Object.assign(
eWeLink.prototype,
getFirmwareVersionMixin,
checkDeviceUpdateMixin,
checkDevicesUpdatesMixin
);
Object.assign(eWeLink.prototype, openWebSocketMixin);
module.exports = eWeLink; module.exports = eWeLink;

View File

@@ -1,38 +0,0 @@
const { _get } = require('../../lib/helpers');
const getDeviceMixin = {
/**
* Get information about all associated devices to account
*
* @param deviceId
*
* @returns {Promise<{msg: string, error: *}>}
*/
async getDevice(deviceId) {
if (this.devicesCache) {
return this.devicesCache.find(dev => dev.deviceid === deviceId) || null;
}
const devices = await this.getDevices();
const error = _get(devices, 'error', false);
if (error === 406) {
return { error: 401, msg: 'Authentication error' };
}
if (error || !devices) {
return devices;
}
const device = devices.find(dev => dev.deviceid === deviceId);
if (!device) {
return { error: 500, msg: 'Device does not exist' };
}
return device;
},
};
module.exports = getDeviceMixin;

View File

@@ -1,45 +0,0 @@
const { makeFakeIMEI } = require('../../lib/ewelink-helper');
const { _get } = require('../../lib/helpers');
const getDevicesMixin = {
/**
* Get all devices information
*
* @returns {Promise<{msg: string, error: number}|*>}
*/
async getDevices() {
const timeStamp = new Date() / 1000;
const ts = Math.floor(timeStamp);
const response = await this.makeRequest({
uri: '/user/device',
qs: {
lang: 'en',
getTags: 1,
version: 6,
ts,
appid: 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq',
imei: makeFakeIMEI(),
os: 'android',
model: '',
romVersion: '',
appVersion: '3.12.0',
},
});
const error = _get(response, 'error', false);
const devicelist = _get(response, 'devicelist', false);
if (error === 406) {
return { error: 401, msg: 'Authentication error' };
}
if (!devicelist) {
return { error: 500, msg: 'No devices found' };
}
return devicelist;
},
};
module.exports = getDevicesMixin;

View File

@@ -1,43 +0,0 @@
const { _get } = require('../../lib/helpers');
const { getDeviceChannelCount } = require('../../lib/ewelink-helper');
const getDevicePowerStateMixin = {
/**
* Get current power state for a specific device
*
* @param deviceId
* @param channel
*
* @returns {Promise<{state: *, status: string}|{msg: string, error: *}>}
*/
async getDevicePowerState(deviceId, channel = 1) {
const device = await this.getDevice(deviceId);
const error = _get(device, 'error', false);
const uiid = _get(device, 'extra.extra.uiid', false);
let state = _get(device, 'params.switch', false);
const switches = _get(device, 'params.switches', false);
const switchesAmount = getDeviceChannelCount(uiid);
if (switchesAmount > 0 && switchesAmount < channel) {
return { error, msg: 'Device channel does not exist' };
}
if (error || (!state && !switches)) {
if (error && parseInt(error) === 401) {
return device;
}
return { error, msg: 'Device does not exist' };
}
if (switches) {
state = switches[channel - 1].switch;
}
return { status: 'ok', state };
},
};
module.exports = getDevicePowerStateMixin;

7267
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "ewelink-api", "name": "ewelink-api",
"version": "2.0.0", "version": "3.1.0",
"description": "eWeLink API for Node.js", "description": "eWeLink API for Node.js",
"author": "Martín M.", "author": "Martín M.",
"license": "MIT", "license": "MIT",
@@ -27,6 +27,9 @@
"lint:fix": "eslint . --fix" "lint:fix": "eslint . --fix"
}, },
"jest": { "jest": {
"setupFilesAfterEnv": [
"<rootDir>/test/_setup/setupTests.js"
],
"testPathIgnorePatterns": [ "testPathIgnorePatterns": [
"/node_modules/", "/node_modules/",
".cache" ".cache"
@@ -38,28 +41,27 @@
}, },
"dependencies": { "dependencies": {
"arpping": "github:skydiver/arpping", "arpping": "github:skydiver/arpping",
"crypto-js": "^3.1.9-1", "crypto-js": "^4.0.0",
"delay": "^4.3.0", "delay": "^4.4.0",
"nonce": "^1.0.4", "node-fetch": "^2.6.1",
"random": "^2.1.1", "random": "^2.2.0",
"request": "^2.88.0", "websocket": "^1.0.32",
"request-promise": "^4.2.4", "websocket-as-promised": "^1.0.1"
"websocket": "^1.0.30",
"websocket-as-promised": "^0.10.1"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.0.3", "babel-eslint": "^10.1.0",
"eslint": "^6.4.0", "eslint": "^7.11.0",
"eslint-config-airbnb": "^18.0.1", "eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.3.0", "eslint-config-prettier": "^6.12.0",
"eslint-config-wesbos": "0.0.19", "eslint-config-wesbos": "0.0.19",
"eslint-plugin-html": "^6.0.0", "eslint-plugin-html": "^6.1.0",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.0", "eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.14.3", "eslint-plugin-react": "^7.21.4",
"eslint-plugin-react-hooks": "^2.0.1", "eslint-plugin-react-hooks": "^4.1.2",
"jest": "^24.9.0", "jest": "^26.5.3",
"prettier": "^1.18.2" "nock": "^12.0.3",
"prettier": "^1.19.1"
} }
} }

View File

@@ -1,8 +1,8 @@
const rp = require('request-promise'); const fetch = require('node-fetch');
const WebSocket = require('../WebSocket'); const WebSocket = require('./WebSocket');
const payloads = require('../../lib/payloads'); const zeroConfUpdatePayload = require('../payloads/zeroConfUpdatePayload');
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
class ChangeStateZeroconf extends WebSocket { class ChangeStateZeroconf extends WebSocket {
static async set({ url, device, params, switches, state }) { static async set({ url, device, params, switches, state }) {
@@ -11,22 +11,16 @@ class ChangeStateZeroconf extends WebSocket {
const deviceKey = device.devicekey; const deviceKey = device.devicekey;
const endpoint = switches ? 'switches' : 'switch'; const endpoint = switches ? 'switches' : 'switch';
const localUrl = `${url}/${endpoint}`;
const body = payloads.zeroConfUpdatePayload( const body = zeroConfUpdatePayload(selfApikey, deviceId, deviceKey, params);
selfApikey,
deviceId,
deviceKey,
params
);
const response = await rp({ const request = await fetch(`${url}/${endpoint}`, {
method: 'POST', method: 'post',
uri: localUrl, body: JSON.stringify(body),
body,
json: true,
}); });
const response = await request.json();
const error = _get(response, 'error', false); const error = _get(response, 'error', false);
if (error === 403) { if (error === 403) {

View File

@@ -1,8 +1,10 @@
const WebSocket = require('../WebSocket'); const WebSocket = require('./WebSocket');
const payloads = require('../../lib/payloads'); const wssLoginPayload = require('../payloads/wssLoginPayload');
const { _get } = require('../../lib/helpers'); const wssUpdatePayload = require('../payloads/wssUpdatePayload');
const { _get } = require('../helpers/utilities');
const errors = require('../data/errors');
class DeviceRaw extends WebSocket { class DevicePowerUsageRaw extends WebSocket {
/** /**
* Get specific device power usage (raw data) * Get specific device power usage (raw data)
* *
@@ -10,13 +12,12 @@ class DeviceRaw extends WebSocket {
* @param at * @param at
* @param apiKey * @param apiKey
* @param deviceId * @param deviceId
* * @returns {Promise<{error: string}|{data: {hundredDaysKwhData: *}, status: string}|{msg: any, error: *}|{msg: string, error: number}>}
* @returns {Promise<{error: string}|{data: {hundredDaysKwhData: *}, status: string}>}
*/ */
static async get({ apiUrl, at, apiKey, deviceId }) { static async get({ apiUrl, at, apiKey, deviceId }) {
const payloadLogin = payloads.wssLoginPayload({ at, apiKey }); const payloadLogin = wssLoginPayload({ at, apiKey, appid: this.APP_ID });
const payloadUpdate = payloads.wssUpdatePayload({ const payloadUpdate = wssUpdatePayload({
apiKey, apiKey,
deviceId, deviceId,
params: { hundredDaysKwh: 'get' }, params: { hundredDaysKwh: 'get' },
@@ -27,6 +28,10 @@ class DeviceRaw extends WebSocket {
payloadUpdate, payloadUpdate,
]); ]);
if (response.length === 1) {
return { error: errors.noPower };
}
const error = _get(response[1], 'error', false); const error = _get(response[1], 'error', false);
if (error === 403) { if (error === 403) {
@@ -40,7 +45,7 @@ class DeviceRaw extends WebSocket {
); );
if (!hundredDaysKwhData) { if (!hundredDaysKwhData) {
return { error: 'No power usage data found.' }; return { error: errors.noPower };
} }
return { return {
@@ -50,4 +55,4 @@ class DeviceRaw extends WebSocket {
} }
} }
module.exports = DeviceRaw; module.exports = DevicePowerUsageRaw;

View File

@@ -2,6 +2,8 @@ const W3CWebSocket = require('websocket').w3cwebsocket;
const WebSocketAsPromised = require('websocket-as-promised'); const WebSocketAsPromised = require('websocket-as-promised');
const delay = require('delay'); const delay = require('delay');
const errors = require('../data/errors');
class WebSocket { class WebSocket {
/** /**
* Open WebSocket connection and send provided payloads * Open WebSocket connection and send provided payloads
@@ -46,9 +48,9 @@ class WebSocket {
static customThrowError(e) { static customThrowError(e) {
const loginError = e.message.indexOf('WebSocket is not opened'); const loginError = e.message.indexOf('WebSocket is not opened');
if (loginError > -1) { if (loginError > -1) {
return { error: 401, msg: 'Authentication error' }; return { error: 406, msg: errors['406'] };
} }
return { error: 'An unknown error occurred' }; return { error: errors.unknown };
} }
} }

7
src/data/constants.js Normal file
View File

@@ -0,0 +1,7 @@
const APP_ID = 'YzfeftUVcZ6twZw1OoVKPRFYTrGEg01Q';
const APP_SECRET = '4G91qSoboqYO4Y0XJ0LPPKIsq8reHdfa';
module.exports = {
APP_ID,
APP_SECRET,
};

22
src/data/errors.js Normal file
View File

@@ -0,0 +1,22 @@
const errors = {
400: 'Parameter error',
401: 'Wrong account or password',
402: 'Email inactivated',
403: 'Forbidden',
404: 'Device does not exist',
406: 'Authentication failed',
};
const customErrors = {
ch404: 'Device channel does not exist',
unknown: 'An unknown error occurred',
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",
invalidAuth: 'Library needs to be initialized using email and password',
invalidCredentials: 'Invalid credentials provided',
invalidPowerState: 'Invalid power state. Expecting: "on", "off" or "toggle"',
};
module.exports = Object.assign(errors, customErrors);

View File

@@ -0,0 +1,58 @@
const STATE_ON = 'on';
const STATE_OFF = 'off';
const STATE_TOGGLE = 'toggle';
const VALID_POWER_STATES = [STATE_ON, STATE_OFF, STATE_TOGGLE];
/**
* Return new device state based on current conditions
*/
const getNewPowerState = (currentState, newState) => {
if (newState !== STATE_TOGGLE) {
return newState;
}
return currentState === STATE_ON ? STATE_OFF : STATE_ON;
};
/**
* Get current device parameters and
*/
const getPowerStateParams = (params, newState, channel) => {
if (params.switches) {
const switches = [...params.switches];
const channelToSwitch = channel - 1;
switches[channelToSwitch].switch = newState;
return { switches };
}
return { switch: newState };
};
/**
* Return status of all channels on a multi-channel device
*/
const getAllChannelsState = params => {
const { switches } = params;
return switches.map(ch => ({
channel: ch.outlet + 1,
state: ch.switch,
}));
};
/**
* Return status of specific channel on multi-channel device
*/
const getSpecificChannelState = (params, channel) => {
const { switches } = params;
return switches[channel - 1].switch;
};
module.exports = {
STATE_ON,
STATE_OFF,
STATE_TOGGLE,
VALID_POWER_STATES,
getNewPowerState,
getPowerStateParams,
getAllChannelsState,
getSpecificChannelState,
};

View File

@@ -2,18 +2,12 @@ const crypto = require('crypto');
const CryptoJS = require('crypto-js'); const CryptoJS = require('crypto-js');
const random = require('random'); const random = require('random');
const DEVICE_TYPE_UUID = require('./data/devices-type-uuid'); const DEVICE_TYPE_UUID = require('../data/devices-type-uuid.json');
const DEVICE_CHANNEL_LENGTH = require('./data/devices-channel-length'); const DEVICE_CHANNEL_LENGTH = require('../data/devices-channel-length.json');
const makeFakeIMEI = () => { const makeAuthorizationSign = (APP_SECRET, body) =>
const num1 = random.int(1000, 9999);
const num2 = random.int(1000, 9999);
return `DF7425A0-${num1}-${num2}-9F5E-3BC9179E48FB`;
};
const makeAuthorizationSign = body =>
crypto crypto
.createHmac('sha256', '6Nz4n0xA8s8qdxQf2GqurZj2Fs55FUvM') .createHmac('sha256', APP_SECRET)
.update(JSON.stringify(body)) .update(JSON.stringify(body))
.digest('base64'); .digest('base64');
@@ -68,7 +62,6 @@ const decryptionData = (data, key, iv) => {
module.exports = { module.exports = {
makeAuthorizationSign, makeAuthorizationSign,
makeFakeIMEI,
getDeviceChannelCount, getDeviceChannelCount,
encryptationData, encryptationData,
decryptionData, decryptionData,

29
src/helpers/utilities.js Normal file
View File

@@ -0,0 +1,29 @@
const nonce = Math.random()
.toString(36)
.slice(5);
const timestamp = Math.floor(new Date() / 1000);
const _get = (obj, path, defaultValue = null) =>
String.prototype.split
.call(path, /[,[\].]+?/)
.filter(Boolean)
.reduce(
(a, c) => (Object.hasOwnProperty.call(a, c) ? a[c] : defaultValue),
obj
);
const _empty = obj => Object.entries(obj).length === 0;
const toQueryString = object =>
`?${Object.keys(object)
.map(key => `${key}=${object[key].toString()}`)
.join('&')}`;
module.exports = {
nonce,
timestamp,
_get,
_empty,
toQueryString,
};

View File

@@ -1,7 +1,7 @@
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
const payloads = require('../../lib/payloads'); const parseFirmwareUpdates = require('../parsers/parseFirmwareUpdates');
const checkDeviceUpdateMixin = { module.exports = {
/** /**
* Check device firmware update * Check device firmware update
* *
@@ -18,7 +18,7 @@ const checkDeviceUpdateMixin = {
return device; return device;
} }
const deviceInfoList = payloads.firmwareUpdate([device]); const deviceInfoList = parseFirmwareUpdates([device]);
const deviceInfoListError = _get(deviceInfoList, 'error', false); const deviceInfoListError = _get(deviceInfoList, 'error', false);
@@ -27,7 +27,7 @@ const checkDeviceUpdateMixin = {
} }
const update = await this.makeRequest({ const update = await this.makeRequest({
method: 'POST', method: 'post',
url: this.getOtaUrl(), url: this.getOtaUrl(),
uri: '/app', uri: '/app',
body: { deviceInfoList }, body: { deviceInfoList },
@@ -46,5 +46,3 @@ const checkDeviceUpdateMixin = {
}; };
}, },
}; };
module.exports = checkDeviceUpdateMixin;

View File

@@ -1,7 +1,7 @@
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
const payloads = require('../../lib/payloads'); const parseFirmwareUpdates = require('../parsers/parseFirmwareUpdates');
const checkDevicesUpdatesMixin = { module.exports = {
async checkDevicesUpdates() { async checkDevicesUpdates() {
const devices = await this.getDevices(); const devices = await this.getDevices();
@@ -11,7 +11,7 @@ const checkDevicesUpdatesMixin = {
return devices; return devices;
} }
const deviceInfoList = payloads.firmwareUpdate(devices); const deviceInfoList = parseFirmwareUpdates(devices);
const deviceInfoListError = _get(deviceInfoList, 'error', false); const deviceInfoListError = _get(deviceInfoList, 'error', false);
@@ -20,7 +20,7 @@ const checkDevicesUpdatesMixin = {
} }
const updates = await this.makeRequest({ const updates = await this.makeRequest({
method: 'POST', method: 'post',
url: this.getOtaUrl(), url: this.getOtaUrl(),
uri: '/app', uri: '/app',
body: { deviceInfoList }, body: { deviceInfoList },
@@ -52,5 +52,3 @@ const checkDevicesUpdatesMixin = {
}); });
}, },
}; };
module.exports = checkDevicesUpdatesMixin;

272
src/mixins/deviceControl.js Normal file
View File

@@ -0,0 +1,272 @@
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,
};
},
};

View File

@@ -1,34 +1,41 @@
const rp = require('request-promise'); const fetch = require('node-fetch');
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
const { credentialsPayload } = require('../../lib/payloads'); const credentialsPayload = require('../payloads/credentialsPayload');
const { makeAuthorizationSign } = require('../../lib/ewelink-helper'); const { makeAuthorizationSign } = require('../helpers/ewelink');
const errors = require('../data/errors');
const getCredentialsMixin = { module.exports = {
/** /**
* Returns user credentials information * Returns user credentials information
* *
* @returns {Promise<{msg: string, error: *}>} * @returns {Promise<{msg: string, error: *}>}
*/ */
async getCredentials() { async getCredentials() {
const { APP_ID, APP_SECRET } = this;
const body = credentialsPayload({ const body = credentialsPayload({
appid: APP_ID,
email: this.email, email: this.email,
phoneNumber: this.phoneNumber,
password: this.password, password: this.password,
}); });
let response = await rp({ const request = await fetch(`${this.getApiUrl()}/user/login`, {
method: 'POST', method: 'post',
uri: `${this.getApiUrl()}/user/login`, headers: {
headers: { Authorization: `Sign ${makeAuthorizationSign(body)}` }, Authorization: `Sign ${makeAuthorizationSign(APP_SECRET, body)}`,
body, },
json: true, body: JSON.stringify(body),
}); });
let response = await request.json();
const error = _get(response, 'error', false); const error = _get(response, 'error', false);
const region = _get(response, 'region', false); const region = _get(response, 'region', false);
if (error && [400, 401, 404].indexOf(parseInt(error)) !== -1) { if (error && [400, 401, 404].indexOf(parseInt(error)) !== -1) {
return { error, msg: 'Authentication error' }; return { error: 406, msg: errors['406'] };
} }
if (error && parseInt(error) === 301 && region) { if (error && parseInt(error) === 301 && region) {
@@ -45,5 +52,3 @@ const getCredentialsMixin = {
return response; return response;
}, },
}; };
module.exports = getCredentialsMixin;

37
src/mixins/getDevice.js Normal file
View File

@@ -0,0 +1,37 @@
const { nonce, timestamp, _get } = require('../helpers/utilities');
const errors = require('../data/errors');
module.exports = {
/**
* Get information for a specific device
*
* @param deviceId
* @returns {Promise<*|null|{msg: string, error: *}>}
*/
async getDevice(deviceId) {
if (this.devicesCache) {
return this.devicesCache.find(dev => dev.deviceid === deviceId) || null;
}
const { APP_ID } = this;
const device = await this.makeRequest({
uri: `/user/device/${deviceId}`,
qs: {
deviceid: deviceId,
appid: APP_ID,
nonce,
ts: timestamp,
version: 8,
},
});
const error = _get(device, 'error', false);
if (error) {
return { error, msg: errors[error] };
}
return device;
},
};

View File

@@ -1,8 +1,9 @@
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
const errors = require('../data/errors');
const { getDeviceChannelCount } = require('../../lib/ewelink-helper'); const { getDeviceChannelCount } = require('../helpers/ewelink');
const getDeviceChannelCountMixin = { module.exports = {
/** /**
* Get device channel count * Get device channel count
* *
@@ -17,14 +18,9 @@ const getDeviceChannelCountMixin = {
const switchesAmount = getDeviceChannelCount(uiid); const switchesAmount = getDeviceChannelCount(uiid);
if (error) { if (error) {
if (error === 401) { return { error, msg: errors[error] };
return device;
}
return { error, msg: 'Device does not exist' };
} }
return { status: 'ok', switchesAmount }; return { status: 'ok', switchesAmount };
}, },
}; };
module.exports = getDeviceChannelCountMixin;

View File

@@ -1,6 +1,7 @@
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
const errors = require('../data/errors');
const getTHMixin = { module.exports = {
/** /**
* Get device current temperature & humidity * Get device current temperature & humidity
* @param deviceId * @param deviceId
@@ -18,7 +19,7 @@ const getTHMixin = {
} }
if (!temperature || !humidity) { if (!temperature || !humidity) {
return { error: 500, msg: "Can't read sensor data from device" }; return { error: 404, msg: errors.noSensor };
} }
const data = { status: 'ok', temperature, humidity }; const data = { status: 'ok', temperature, humidity };
@@ -52,5 +53,3 @@ const getTHMixin = {
return this.getDeviceCurrentTH(deviceId, 'humd'); return this.getDeviceCurrentTH(deviceId, 'humd');
}, },
}; };
module.exports = getTHMixin;

View File

@@ -1,11 +1,11 @@
const getLocalIpMixin = { module.exports = {
/** /**
* Get local IP address from a given MAC * Get local IP address from a given MAC
* *
* @param device * @param device
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
getLocalIp(device) { getDeviceIP(device) {
const mac = device.extra.extra.staMac; const mac = device.extra.extra.staMac;
const arpItem = this.arpTable.find( const arpItem = this.arpTable.find(
item => item.mac.toLowerCase() === mac.toLowerCase() item => item.mac.toLowerCase() === mac.toLowerCase()
@@ -13,5 +13,3 @@ const getLocalIpMixin = {
return arpItem.ip; return arpItem.ip;
}, },
}; };
module.exports = getLocalIpMixin;

View File

@@ -0,0 +1,47 @@
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
*
* @param deviceId
* @param channel
*
* @returns {Promise<{state: *, status: string}|{msg: string, error: *}>}
*/
async getDevicePowerState(deviceId, channel = 1) {
const status = await this.makeRequest({
uri: '/user/device/status',
qs: deviceStatusPayload({
appid: this.APP_ID,
deviceId,
params: 'switch|switches',
}),
});
const error = _get(status, 'error', false);
if (error) {
const err = error === 400 ? 404 : error;
return { error: err, msg: errors[err] };
}
let state = _get(status, 'params.switch', false);
const switches = _get(status, 'params.switches', false);
const switchesAmount = switches ? switches.length : 1;
if (switchesAmount > 0 && switchesAmount < channel) {
return { error: 404, msg: errors.ch404 };
}
if (switches) {
state = switches[channel - 1].switch;
}
return { status: 'ok', state, channel };
},
};

View File

@@ -1,8 +1,7 @@
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
const parsePowerUsage = require('../parsers/parsePowerUsage');
const { CurrentMonth } = require('../../classes/PowerUsage'); module.exports = {
const getDevicePowerUsageMixin = {
/** /**
* Get device power usage for current month * Get device power usage for current month
* *
@@ -11,7 +10,7 @@ const getDevicePowerUsageMixin = {
* @returns {Promise<{error: string}|{daily: *, monthly: *}>} * @returns {Promise<{error: string}|{daily: *, monthly: *}>}
*/ */
async getDevicePowerUsage(deviceId) { async getDevicePowerUsage(deviceId) {
const response = await this.getDeviceRawPowerUsage(deviceId); const response = await this.getDevicePowerUsageRaw(deviceId);
const error = _get(response, 'error', false); const error = _get(response, 'error', false);
const hundredDaysKwhData = _get(response, 'data.hundredDaysKwhData', false); const hundredDaysKwhData = _get(response, 'data.hundredDaysKwhData', false);
@@ -22,9 +21,7 @@ const getDevicePowerUsageMixin = {
return { return {
status: 'ok', status: 'ok',
...CurrentMonth.parse({ hundredDaysKwhData }), ...parsePowerUsage({ hundredDaysKwhData }),
}; };
}, },
}; };
module.exports = getDevicePowerUsageMixin;

View File

@@ -1,8 +1,8 @@
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
const { DeviceRaw } = require('../../classes/PowerUsage'); const DevicePowerUsageRaw = require('../classes/DevicePowerUsageRaw');
const getDeviceRawPowerUsageMixin = { module.exports = {
/** /**
* Get device raw power usage * Get device raw power usage
* *
@@ -10,7 +10,7 @@ const getDeviceRawPowerUsageMixin = {
* *
* @returns {Promise<{error: string}|{response: {hundredDaysKwhData: *}, status: string}>} * @returns {Promise<{error: string}|{response: {hundredDaysKwhData: *}, status: string}>}
*/ */
async getDeviceRawPowerUsage(deviceId) { async getDevicePowerUsageRaw(deviceId) {
const device = await this.getDevice(deviceId); const device = await this.getDevice(deviceId);
const deviceApiKey = _get(device, 'apikey', false); const deviceApiKey = _get(device, 'apikey', false);
@@ -25,8 +25,6 @@ const getDeviceRawPowerUsageMixin = {
actionParams.apiKey = deviceApiKey; actionParams.apiKey = deviceApiKey;
} }
return DeviceRaw.get(actionParams); return DevicePowerUsageRaw.get(actionParams);
}, },
}; };
module.exports = getDeviceRawPowerUsageMixin;

37
src/mixins/getDevices.js Normal file
View File

@@ -0,0 +1,37 @@
const { _get, timestamp } = require('../helpers/utilities');
const errors = require('../data/errors');
module.exports = {
/**
* Get all devices information
*
* @returns {Promise<{msg: string, error: number}|*>}
*/
async getDevices() {
const { APP_ID } = this;
const response = await this.makeRequest({
uri: '/user/device',
qs: {
lang: 'en',
appid: APP_ID,
ts: timestamp,
version: 8,
getTags: 1,
},
});
const error = _get(response, 'error', false);
const devicelist = _get(response, 'devicelist', false);
if (error) {
return { error, msg: errors[error] };
}
if (!devicelist) {
return { error: 404, msg: errors.noDevices };
}
return devicelist;
},
};

View File

@@ -1,6 +1,7 @@
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
const errors = require('../data/errors');
const getFirmwareVersionMixin = { module.exports = {
/** /**
* Get device firmware version * Get device firmware version
* *
@@ -14,14 +15,9 @@ const getFirmwareVersionMixin = {
const fwVersion = _get(device, 'params.fwVersion', false); const fwVersion = _get(device, 'params.fwVersion', false);
if (error || !fwVersion) { if (error || !fwVersion) {
if (error === 401) { return { error, msg: errors[error] };
return device;
}
return { error, msg: 'Device does not exist' };
} }
return { status: 'ok', fwVersion }; return { status: 'ok', fwVersion };
}, },
}; };
module.exports = getFirmwareVersionMixin;

View File

@@ -1,12 +1,10 @@
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
const errors = require('../data/errors');
const getRegionMixin = { module.exports = {
async getRegion() { async getRegion() {
if (!this.email || !this.password) { if (!this.email || !this.password) {
return { return { error: 406, msg: errors.invalidAuth };
error: 406,
msg: 'Library needs to be initialized using email and password',
};
} }
const credentials = await this.getCredentials(); const credentials = await this.getCredentials();
@@ -23,5 +21,3 @@ const getRegionMixin = {
}; };
}, },
}; };
module.exports = getRegionMixin;

43
src/mixins/index.js Normal file
View File

@@ -0,0 +1,43 @@
const { checkDevicesUpdates } = require('./checkDevicesUpdates');
const { checkDeviceUpdate } = require('./checkDeviceUpdate');
const deviceControl = require('./deviceControl');
const { getCredentials } = require('./getCredentials');
const { getDevice } = require('./getDevice');
const { getDeviceChannelCount } = require('./getDeviceChannelCount');
const getDeviceCurrentTH = require('./getDeviceCurrentTH');
const { getDeviceIP } = require('./getDeviceIP');
const { getDevicePowerState } = require('./getDevicePowerState');
const { getDevicePowerUsage } = require('./getDevicePowerUsage');
const { getDevicePowerUsageRaw } = require('./getDevicePowerUsageRaw');
const { getDevices } = require('./getDevices');
const { getFirmwareVersion } = require('./getFirmwareVersion');
const { getRegion } = require('./getRegion');
const { makeRequest } = require('./makeRequest');
const { openWebSocket } = require('./openWebSocket');
const { saveDevicesCache } = require('./saveDevicesCache');
const { setDevicePowerState } = require('./setDevicePowerState');
const { toggleDevice } = require('./toggleDevice');
const mixins = {
checkDevicesUpdates,
checkDeviceUpdate,
...deviceControl,
getCredentials,
getDevice,
getDeviceChannelCount,
...getDeviceCurrentTH,
getDeviceIP,
getDevicePowerState,
getDevicePowerUsage,
getDevicePowerUsageRaw,
getDevices,
getFirmwareVersion,
getRegion,
makeRequest,
openWebSocket,
saveDevicesCache,
setDevicePowerState,
toggleDevice,
};
module.exports = mixins;

55
src/mixins/makeRequest.js Normal file
View File

@@ -0,0 +1,55 @@
const fetch = require('node-fetch');
const { _get, _empty, toQueryString } = require('../helpers/utilities');
const errors = require('../data/errors');
module.exports = {
/**
* Helper to make api requests
*
* @param method
* @param url
* @param uri
* @param body
* @param qs
* @returns {Promise<{msg: *, error: *}|*>}
*/
async makeRequest({ method = 'get', url, uri, body = {}, qs = {} }) {
const { at } = this;
if (!at) {
await this.getCredentials();
}
let apiUrl = this.getApiUrl();
if (url) {
apiUrl = url;
}
const payload = {
method,
headers: {
Authorization: `Bearer ${this.at}`,
'Content-Type': 'application/json',
},
};
if (!_empty(body)) {
payload.body = JSON.stringify(body);
}
const queryString = !_empty(qs) ? toQueryString(qs) : '';
const requestUrl = `${apiUrl}${uri}${queryString}`;
const request = await fetch(requestUrl, payload);
const response = await request.json();
const error = _get(response, 'error', false);
if (error) {
return { error, msg: errors[error] };
}
return response;
},
};

View File

@@ -1,9 +1,9 @@
const W3CWebSocket = require('websocket').w3cwebsocket; const W3CWebSocket = require('websocket').w3cwebsocket;
const WebSocketAsPromised = require('websocket-as-promised'); const WebSocketAsPromised = require('websocket-as-promised');
const payloads = require('../../lib/payloads'); const wssLoginPayload = require('../payloads/wssLoginPayload');
const openWebSocketMixin = { module.exports = {
/** /**
* Open a socket connection to eWeLink * Open a socket connection to eWeLink
* and execute callback function with server message as argument * and execute callback function with server message as argument
@@ -13,9 +13,10 @@ const openWebSocketMixin = {
* @returns {Promise<WebSocketAsPromised>} * @returns {Promise<WebSocketAsPromised>}
*/ */
async openWebSocket(callback, ...{ heartbeat = 120000 }) { async openWebSocket(callback, ...{ heartbeat = 120000 }) {
const payloadLogin = payloads.wssLoginPayload({ const payloadLogin = wssLoginPayload({
at: this.at, at: this.at,
apiKey: this.apiKey, apiKey: this.apiKey,
appid: this.APP_ID,
}); });
const wsp = new WebSocketAsPromised(this.getApiWebSocket(), { const wsp = new WebSocketAsPromised(this.getApiWebSocket(), {
@@ -41,5 +42,3 @@ const openWebSocketMixin = {
return wsp; return wsp;
}, },
}; };
module.exports = openWebSocketMixin;

View File

@@ -1,8 +1,8 @@
const fs = require('fs'); const fs = require('fs');
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
const saveDevicesCacheMixin = { module.exports = {
/** /**
* Save devices cache file (useful for using zeroconf) * Save devices cache file (useful for using zeroconf)
* @returns {Promise<string|{msg: string, error: number}|*|Device[]|{msg: string, error: number}>} * @returns {Promise<string|{msg: string, error: number}|*|Device[]|{msg: string, error: number}>}
@@ -13,7 +13,6 @@ const saveDevicesCacheMixin = {
const error = _get(devices, 'error', false); const error = _get(devices, 'error', false);
if (error || !devices) { if (error || !devices) {
console.log(devices);
return devices; return devices;
} }
@@ -28,5 +27,3 @@ const saveDevicesCacheMixin = {
} }
}, },
}; };
module.exports = saveDevicesCacheMixin;

View File

@@ -1,12 +1,11 @@
const { _get } = require('../../lib/helpers'); const { _get, timestamp, nonce } = require('../helpers/utilities');
const errors = require('../data/errors');
const { getDeviceChannelCount } = require('../../lib/ewelink-helper'); const { getDeviceChannelCount } = require('../helpers/ewelink');
const {
ChangeState,
ChangeStateZeroconf,
} = require('../../classes/PowerState');
const setDevicePowerState = { const ChangeStateZeroconf = require('../classes/ChangeStateZeroconf');
module.exports = {
/** /**
* Change power state for a specific device * Change power state for a specific device
* *
@@ -18,7 +17,6 @@ const setDevicePowerState = {
*/ */
async setDevicePowerState(deviceId, state, channel = 1) { async setDevicePowerState(deviceId, state, channel = 1) {
const device = await this.getDevice(deviceId); const device = await this.getDevice(deviceId);
const deviceApiKey = _get(device, 'apikey', false);
const error = _get(device, 'error', false); const error = _get(device, 'error', false);
const uiid = _get(device, 'extra.extra.uiid', false); const uiid = _get(device, 'extra.extra.uiid', false);
@@ -28,14 +26,11 @@ const setDevicePowerState = {
const switchesAmount = getDeviceChannelCount(uiid); const switchesAmount = getDeviceChannelCount(uiid);
if (switchesAmount > 0 && switchesAmount < channel) { if (switchesAmount > 0 && switchesAmount < channel) {
return { error, msg: 'Device channel does not exist' }; return { error: 404, msg: errors.ch404 };
} }
if (error || (!status && !switches)) { if (error || (!status && !switches)) {
if (error && parseInt(error) === 401) { return { error, msg: errors[error] };
return device;
}
return { error, msg: 'Device does not exist' };
} }
let stateToSwitch = state; let stateToSwitch = state;
@@ -66,21 +61,27 @@ const setDevicePowerState = {
}); });
} }
const actionParams = { const { APP_ID } = this;
apiUrl: this.getApiWebSocket(),
at: this.at,
apiKey: this.apiKey,
deviceId,
params,
state: stateToSwitch,
};
if (this.apiKey !== deviceApiKey) { const response = await this.makeRequest({
actionParams.apiKey = deviceApiKey; method: 'post',
uri: '/user/device/status',
body: {
deviceid: deviceId,
params,
appid: APP_ID,
nonce,
ts: timestamp,
version: 8,
},
});
const responseError = _get(response, 'error', false);
if (responseError) {
return { error: responseError, msg: errors[responseError] };
} }
return ChangeState.set(actionParams); return { status: 'ok', state, channel };
}, },
}; };
module.exports = setDevicePowerState;

View File

@@ -1,4 +1,4 @@
const toggleDeviceMixin = { module.exports = {
/** /**
* Toggle power state for a specific device * Toggle power state for a specific device
* *
@@ -11,5 +11,3 @@ const toggleDeviceMixin = {
return this.setDevicePowerState(deviceId, 'toggle', channel); return this.setDevicePowerState(deviceId, 'toggle', channel);
}, },
}; };
module.exports = toggleDeviceMixin;

View File

@@ -1,15 +1,16 @@
const { _get } = require('../../lib/helpers'); const { _get } = require('../helpers/utilities');
const errors = require('../data/errors');
const firmwareUpdate = devicesList => const parseFirmwareUpdates = devicesList =>
devicesList.map(device => { devicesList.map(device => {
const model = _get(device, 'extra.extra.model', false); const model = _get(device, 'extra.extra.model', false);
const fwVersion = _get(device, 'params.fwVersion', false); const fwVersion = _get(device, 'params.fwVersion', false);
if (!model || !fwVersion) { if (!model || !fwVersion) {
return { error: 500, msg: "Can't get model or firmware version" }; return { error: 500, msg: errors.noFirmware };
} }
return { model, version: fwVersion, deviceid: device.deviceid }; return { model, version: fwVersion, deviceid: device.deviceid };
}); });
module.exports = firmwareUpdate; module.exports = parseFirmwareUpdates;

View File

@@ -0,0 +1,38 @@
/**
* Return daily power usage
*
* @param hundredDaysKwhData
*
* @returns {{daily: *, monthly: *}}
*/
const parsePowerUsage = ({ hundredDaysKwhData }) => {
const today = new Date();
const days = today.getDate();
let monthlyUsage = 0;
const dailyUsage = [];
for (let day = 0; day < days; day += 1) {
const s = hundredDaysKwhData.substr(6 * day, 2);
const c = hundredDaysKwhData.substr(6 * day + 2, 2);
const f = hundredDaysKwhData.substr(6 * day + 4, 2);
const h = parseInt(s, 16);
const y = parseInt(c, 16);
const I = parseInt(f, 16);
const E = parseFloat(`${h}.${y}${I}`);
dailyUsage.push({
day: days - day,
usage: E,
});
monthlyUsage += E;
}
return {
monthly: monthlyUsage,
daily: dailyUsage,
};
};
module.exports = parsePowerUsage;

View File

@@ -0,0 +1,13 @@
const { timestamp, nonce } = require('../helpers/utilities');
const credentialsPayload = ({ appid, email, phoneNumber, password }) => ({
appid,
email,
phoneNumber,
password,
ts: timestamp,
version: 8,
nonce,
});
module.exports = credentialsPayload;

View File

@@ -0,0 +1,12 @@
const { timestamp, nonce } = require('../helpers/utilities');
const deviceStatus = ({ appid, deviceId, params }) => ({
deviceid: deviceId,
appid,
nonce,
ts: timestamp,
version: 8,
params,
});
module.exports = deviceStatus;

View File

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

View File

@@ -1,14 +1,15 @@
const { timestamp } = require('../helpers/utilities');
const wssUpdatePayload = ({ apiKey, deviceId, params }) => { const wssUpdatePayload = ({ apiKey, deviceId, params }) => {
const timeStamp = new Date() / 1000;
const sequence = Math.floor(timeStamp * 1000);
const payload = { const payload = {
action: 'update', action: 'update',
deviceid: `${deviceId}`,
apikey: apiKey, apikey: apiKey,
deviceid: deviceId,
selfApikey: apiKey, selfApikey: apiKey,
params, params,
sequence, ts: timestamp,
userAgent: 'app', userAgent: 'app',
sequence: Math.floor(timestamp * 1000),
}; };
return JSON.stringify(payload); return JSON.stringify(payload);
}; };

View File

@@ -1,11 +1,11 @@
const { encryptationData } = require('../ewelink-helper'); const { encryptationData } = require('../helpers/ewelink');
const { timestamp } = require('../helpers/utilities');
const zeroConfUpdatePayload = (selfApikey, deviceId, deviceKey, params) => { const zeroConfUpdatePayload = (selfApikey, deviceId, deviceKey, params) => {
const encryptedData = encryptationData(JSON.stringify(params), deviceKey); const encryptedData = encryptationData(JSON.stringify(params), deviceKey);
const timeStamp = new Date() / 1000;
const sequence = Math.floor(timeStamp * 1000);
return { return {
sequence: sequence.toString(), sequence: Math.floor(timestamp * 1000).toString(),
deviceid: deviceId, deviceid: deviceId,
selfApikey, selfApikey,
iv: encryptedData.iv, iv: encryptedData.iv,

3
test/_setup/cache/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# this folder contains cache files used on ZeroConf test cases
*
!.gitignore

1
test/_setup/config/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
credentials.js

View File

@@ -12,7 +12,6 @@ const allDevicesExpectations = {
showBrand: expect.any(Boolean), showBrand: expect.any(Boolean),
extra: { extra: {
extra: { extra: {
uiid: expect.any(Number),
model: expect.any(String), model: expect.any(String),
}, },
}, },
@@ -25,7 +24,6 @@ const specificDeviceExpectations = {
online: expect.any(Boolean), online: expect.any(Boolean),
extra: { extra: {
extra: { extra: {
uiid: expect.any(Number),
model: expect.any(String), model: expect.any(String),
}, },
}, },

View File

@@ -0,0 +1,61 @@
const nock = require('nock');
const { writeFile } = require('fs');
exports.startNockRecording = () => {
nock.recorder.rec({
dont_print: true,
enable_reqheaders_recording: false,
output_objects: true,
});
};
exports.storeNockRecordings = pathToTape => {
const nockCallObjects = nock.recorder.play();
writeFile(pathToTape, JSON.stringify(nockCallObjects, null, 2), () => {});
};
exports.playbackNockTapes = pathToTape => {
const nocks = nock.load(pathToTape);
nocks.forEach(function(n) {
n.filteringPath(path => filteringPath(n, path));
n.filteringRequestBody((b, rb) => filteringRequestBody(b, rb));
});
nock.recorder.play();
};
/**
* Replace dynamic values on request body using recorded data
*/
const filteringRequestBody = (body, recordedBody) => {
if (typeof body !== 'string' || typeof recordedBody !== 'object') {
return body;
}
const jsonBody = JSON.parse(body);
jsonBody.ts = recordedBody.ts;
jsonBody.nonce = recordedBody.nonce;
return JSON.stringify(jsonBody);
};
/**
* Replace dynamic values on request URLs using recorded data
*/
const filteringPath = (n, path) => {
const regexTimestampInPath = /(?<=ts=)[^&]*/g;
const timestampInPath = path.match(regexTimestampInPath);
if (!timestampInPath) {
return path;
}
const regexTimestampInRecordedPath = /ts=[^&]*/g;
const recordedPath = n.interceptors[0].uri;
const timestampInRecordedPath = recordedPath.match(regexTimestampInPath);
const updatedPath = recordedPath.replace(
regexTimestampInRecordedPath,
`ts=${timestampInRecordedPath[0]}`
);
return updatedPath;
};

60
test/_setup/setupTests.js Normal file
View File

@@ -0,0 +1,60 @@
const path = require('path');
const delay = require('delay');
const {
startNockRecording,
storeNockRecordings,
playbackNockTapes,
} = require('./nock-helpers');
// Change to 'record' or 'play' to use nock on tests
// Set to false to run live against eWeLink servers
const nockAction = false;
// These files needs a cooldown delay between tests
const testsToDelay = [
'env-node.spec.js',
'env-serverless.spec.js',
'device-control.spec.js',
'invalid-credentials.spec.js',
'power-usage.spec.js',
'temperature-humidity.spec.js',
'valid-credentials.spec.js',
];
const getTapFilename = global => {
const { testPath } = global.jasmine;
const tapeFile = path.basename(testPath, '.js');
return `./test/_setup/tapes/${tapeFile}.json`;
};
global.beforeAll(() => {
if (nockAction === 'record') {
startNockRecording();
}
if (nockAction === 'play') {
const tapeFile = getTapFilename(global);
playbackNockTapes(tapeFile);
}
});
global.afterAll(() => {
if (nockAction === 'record') {
const tapeFile = getTapFilename(global);
storeNockRecordings(tapeFile);
}
});
global.beforeEach(async () => {
if (nockAction === 'play') {
return;
}
const { testPath } = global.jasmine;
const testFile = path.basename(testPath);
if (testsToDelay.includes(testFile)) {
await delay(1000);
}
});

3
test/_setup/tapes/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# this folder will store "tape" files for nock
*
!.gitignore

View File

@@ -0,0 +1,191 @@
const { APP_ID, APP_SECRET } = require('../src/data/constants');
const ewelink = require('../main');
const errors = require('../src/data/errors');
describe('main class instantiation: valid parameters combinations', () => {
test('email and password should initialize class', async () => {
const credentials = { email: 'user@email.com', password: 'pass' };
const connection = new ewelink(credentials);
expect(connection).toEqual({
region: 'us',
email: credentials.email,
phoneNumber: null,
password: credentials.password,
apiKey: null,
arpTable: null,
at: null,
devicesCache: null,
APP_ID,
APP_SECRET,
});
});
test('email and password with region should initialize class', async () => {
const credentials = {
region: 'cn',
email: 'user@email.com',
password: 'pass',
};
const connection = new ewelink(credentials);
expect(connection).toEqual({
region: 'cn',
email: credentials.email,
phoneNumber: null,
password: credentials.password,
apiKey: null,
arpTable: null,
at: null,
devicesCache: null,
APP_ID,
APP_SECRET,
});
});
test('phone number and password should initialize class', async () => {
const credentials = { phoneNumber: '555123789', password: 'pass' };
const connection = new ewelink(credentials);
expect(connection).toEqual({
region: 'us',
email: null,
phoneNumber: credentials.phoneNumber,
password: credentials.password,
apiKey: null,
at: null,
arpTable: null,
devicesCache: null,
APP_ID,
APP_SECRET,
});
});
test('access token should initialize class', async () => {
const credentials = { at: 'xxxyyyzzz' };
const connection = new ewelink(credentials);
expect(connection).toEqual({
region: 'us',
email: null,
phoneNumber: null,
password: null,
apiKey: null,
at: credentials.at,
arpTable: null,
devicesCache: null,
APP_ID,
APP_SECRET,
});
});
test('devices and arp table cache files should initialize class', async () => {
const credentials = { devicesCache: 'devices', arpTable: 'arptable' };
const connection = new ewelink(credentials);
expect(connection).toEqual({
region: 'us',
email: null,
phoneNumber: null,
password: null,
apiKey: null,
at: null,
arpTable: credentials.arpTable,
devicesCache: credentials.devicesCache,
APP_ID,
APP_SECRET,
});
});
test('email and access token should initialize class', async () => {
const credentials = {
email: 'user@email.com',
at: 'xxxyyyzzz',
};
const connection = new ewelink(credentials);
expect(connection).toEqual({
region: 'us',
email: credentials.email,
phoneNumber: null,
password: null,
apiKey: null,
at: credentials.at,
arpTable: null,
devicesCache: null,
APP_ID,
APP_SECRET,
});
});
test('initialize class using custom APP_ID and APP_SECRET', async () => {
const credentials = {
email: 'user@email.com',
at: 'xxxyyyzzz',
APP_ID: 'CUSTOM_APP_ID',
APP_SECRET: 'CUSTOM_APP_SECRET',
};
const connection = new ewelink(credentials);
expect(connection).toEqual({
region: 'us',
email: credentials.email,
phoneNumber: null,
password: null,
apiKey: null,
at: credentials.at,
arpTable: null,
devicesCache: null,
APP_ID: 'CUSTOM_APP_ID',
APP_SECRET: 'CUSTOM_APP_SECRET',
});
});
});
describe('main class instantiation: invalid parameters combinations', () => {
test('user and no password should fail', async () => {
const credentials = { email: 'user@email.com' };
expect(() => {
const connection = new ewelink(credentials);
}).toThrow(errors.invalidCredentials);
});
test('only password should fail', async () => {
const credentials = { password: 'pass' };
expect(() => {
const connection = new ewelink(credentials);
}).toThrow(errors.invalidCredentials);
});
test('phone number and no password should fail', async () => {
const credentials = { phoneNumber: '555123789' };
expect(() => {
const connection = new ewelink(credentials);
}).toThrow(errors.invalidCredentials);
});
test('email and phone number should fail', async () => {
const credentials = { email: 'user@email.com', phoneNumber: '555123789' };
expect(() => {
const connection = new ewelink(credentials);
}).toThrow(errors.invalidCredentials);
});
test('email and phone number with password should fail', async () => {
const credentials = {
email: 'user@email.com',
phoneNumber: '555123789',
password: 'pass',
};
expect(() => {
const connection = new ewelink(credentials);
}).toThrow(errors.invalidCredentials);
});
test('devices cache without arp table should fail', async () => {
const credentials = { devicesCache: 'devices' };
expect(() => {
const connection = new ewelink(credentials);
}).toThrow(errors.invalidCredentials);
});
test('arp table without devices cache should fail', async () => {
const credentials = { arpTable: 'arptable' };
expect(() => {
const connection = new ewelink(credentials);
}).toThrow(errors.invalidCredentials);
});
});

215
test/device-control.spec.js Normal file
View File

@@ -0,0 +1,215 @@
const ewelink = require('../main');
const errors = require('../src/data/errors');
const { getAllChannelsState } = require('../src/helpers/device-control');
const {
email,
password,
sharedAccount,
singleChannelDeviceId,
fourChannelsDevice,
} = require('./_setup/config/credentials.js');
describe('device control using WebSockets: get power state', () => {
let conn;
beforeAll(() => {
conn = new ewelink({ email, password });
});
test('get power state on single channel device', async () => {
jest.setTimeout(30000);
const device = await conn.getDevice(singleChannelDeviceId);
const { switch: originalState } = device.params;
const powerState = await conn.getWSDevicePowerState(singleChannelDeviceId, {
shared: sharedAccount,
});
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.state).toBe(originalState);
expect(powerState.channel).toBe(1);
});
test('get power state for specific channel on multi-channel device', async () => {
jest.setTimeout(30000);
const channel = 3;
const device = await conn.getDevice(fourChannelsDevice);
const { switches } = device.params;
const originalState = switches[channel - 1].switch;
const powerState = await conn.getWSDevicePowerState(fourChannelsDevice, {
channel,
shared: sharedAccount,
});
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.state).toBe(originalState);
expect(powerState.channel).toBe(channel);
});
test('get power state for all channels on multi-channel device', async () => {
jest.setTimeout(30000);
const channel = 3;
const device = await conn.getDevice(fourChannelsDevice);
const originalState = getAllChannelsState(device.params);
const powerState = await conn.getWSDevicePowerState(fourChannelsDevice, {
allChannels: true,
shared: sharedAccount,
});
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.state).toStrictEqual(originalState);
});
});
describe('device control using WebSockets: set power state', () => {
let conn;
beforeAll(() => {
conn = new ewelink({ email, password });
});
test('toggle power state on single channel device', async () => {
jest.setTimeout(30000);
const device = await conn.getDevice(singleChannelDeviceId);
const { switch: originalState } = device.params;
const newState = originalState === 'on' ? 'off' : 'on';
const powerState = await conn.setWSDevicePowerState(
singleChannelDeviceId,
'toggle',
{
shared: sharedAccount,
}
);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.channel).toBe(1);
expect(powerState.state).toBe(newState);
const deviceVerify = await conn.getDevice(singleChannelDeviceId);
const { switch: currentStateVerify } = deviceVerify.params;
expect(newState).toBe(currentStateVerify);
});
test('toggle power state on multi-channel device', async () => {
jest.setTimeout(30000);
const channel = 3;
const device = await conn.getDevice(fourChannelsDevice);
const { switches } = device.params;
const originalState = switches[channel - 1].switch;
const newState = originalState === 'on' ? 'off' : 'on';
const powerState = await conn.setWSDevicePowerState(
fourChannelsDevice,
'toggle',
{
channel,
shared: sharedAccount,
}
);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.channel).toBe(channel);
expect(powerState.state).toBe(newState);
const deviceVerify = await conn.getDevice(fourChannelsDevice);
const { switches: switchesVerify } = deviceVerify.params;
const currentStateVerify = switchesVerify[channel - 1].switch;
expect(newState).toBe(currentStateVerify);
});
test('turn off single channel device', async () => {
jest.setTimeout(30000);
const powerState = await conn.setWSDevicePowerState(
singleChannelDeviceId,
'off',
{
shared: sharedAccount,
}
);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.channel).toBe(1);
expect(powerState.state).toBe('off');
});
test('turn off multi-channel device', async () => {
jest.setTimeout(30000);
const channel = 3;
const powerState = await conn.setWSDevicePowerState(
fourChannelsDevice,
'off',
{
channel,
shared: sharedAccount,
}
);
expect(typeof powerState).toBe('object');
expect(powerState.status).toBe('ok');
expect(powerState.channel).toBe(channel);
expect(powerState.state).toBe('off');
});
});
describe('device control using WebSockets: errors and exceptions', () => {
let conn;
beforeAll(() => {
conn = new ewelink({ email, password });
});
test('get power state using invalid credentials should throw an exception', async () => {
try {
const connection = new ewelink({
email: 'invalid',
password: 'credentials',
});
await connection.getWSDevicePowerState(singleChannelDeviceId);
} catch (error) {
expect(typeof error).toBe('object');
expect(error.toString()).toBe(`Error: ${errors[406]}`);
}
});
test('get power state using invalid device should throw an exception', async () => {
jest.setTimeout(30000);
try {
await conn.getWSDevicePowerState('INVALID DEVICE', {
shared: sharedAccount,
});
} catch (error) {
expect(typeof error).toBe('object');
expect(error.toString()).toBe(`Error: ${errors[403]}`);
}
});
test('set power state using invalid credentials should throw an exception', async () => {
try {
const connection = new ewelink({
email: 'invalid',
password: 'credentials',
});
await connection.setWSDevicePowerState(singleChannelDeviceId, 'toggle');
} catch (error) {
expect(typeof error).toBe('object');
expect(error.toString()).toBe(`Error: ${errors[406]}`);
}
});
test('turn off invalid device', async () => {
jest.setTimeout(30000);
try {
await conn.setWSDevicePowerState('INVALID DEVICE', 'off', {
shared: sharedAccount,
});
} catch (error) {
expect(typeof error).toBe('object');
expect(error.toString()).toBe(`Error: ${errors[403]}`);
}
});
test('using invalid power state should throw an exception', async () => {
try {
await conn.setWSDevicePowerState(singleChannelDeviceId, 'INVALID STATE');
} catch (error) {
expect(typeof error).toBe('object');
expect(error.toString()).toBe(`Error: ${errors.invalidPowerState}`);
}
});
});

View File

@@ -1,5 +1,3 @@
const delay = require('delay');
const ewelink = require('../main'); const ewelink = require('../main');
const { const {
@@ -7,7 +5,7 @@ const {
password, password,
singleChannelDeviceId, singleChannelDeviceId,
fourChannelsDevice, fourChannelsDevice,
} = require('./_setup/credentials.js'); } = require('./_setup/config/credentials.js');
const { const {
credentialsExpectations, credentialsExpectations,
@@ -22,10 +20,6 @@ describe('env: node script', () => {
conn = new ewelink({ email, password }); conn = new ewelink({ email, password });
}); });
beforeEach(async () => {
await delay(1000);
});
test('get ewelink credentials', async () => { test('get ewelink credentials', async () => {
const credentials = await conn.getCredentials(); const credentials = await conn.getCredentials();
expect(typeof credentials).toBe('object'); expect(typeof credentials).toBe('object');

View File

@@ -1,5 +1,3 @@
const delay = require('delay');
const ewelink = require('../main'); const ewelink = require('../main');
const { const {
@@ -7,7 +5,7 @@ const {
password, password,
singleChannelDeviceId, singleChannelDeviceId,
fourChannelsDevice, fourChannelsDevice,
} = require('./_setup/credentials.js'); } = require('./_setup/config/credentials.js');
const { const {
credentialsExpectations, credentialsExpectations,
@@ -19,10 +17,6 @@ describe('env: serverless', () => {
let accessToken; let accessToken;
let apiKey; let apiKey;
beforeEach(async () => {
await delay(1000);
});
test('get ewelink credentials', async () => { test('get ewelink credentials', async () => {
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const credentials = await conn.getCredentials(); const credentials = await conn.getCredentials();

View File

@@ -1,4 +1,5 @@
const ewelink = require('../main'); const ewelink = require('../main');
const errors = require('../src/data/errors');
const { const {
email, email,
@@ -6,11 +7,11 @@ const {
singleChannelDeviceId, singleChannelDeviceId,
outdatedFirmwareDevice, outdatedFirmwareDevice,
updatedFirmwareDevice, updatedFirmwareDevice,
} = require('./_setup/credentials.js'); } = require('./_setup/config/credentials.js');
const { firmwareExpectations } = require('./_setup/expectations'); const { firmwareExpectations } = require('./_setup/expectations');
describe.skip('firmware: get version methods', () => { describe('firmware: get version methods', () => {
let connection; let connection;
beforeAll(() => { beforeAll(() => {
@@ -42,27 +43,27 @@ describe.skip('firmware: get version methods', () => {
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const firmwareVersion = await conn.getFirmwareVersion('invalid deviceid'); const firmwareVersion = await conn.getFirmwareVersion('invalid deviceid');
expect(typeof firmwareVersion).toBe('object'); expect(typeof firmwareVersion).toBe('object');
expect(firmwareVersion.msg).toBe('Device does not exist'); expect(firmwareVersion.msg).toBe(errors['404']);
expect(firmwareVersion.error).toBe(500); expect(firmwareVersion.error).toBe(404);
}); });
test('get device firmware version using invalid credentials should fail', async () => { test('get device firmware version using invalid credentials should fail', async () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' }); const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const firmware = await conn.getFirmwareVersion(singleChannelDeviceId); const firmware = await conn.getFirmwareVersion(singleChannelDeviceId);
expect(typeof firmware).toBe('object'); expect(typeof firmware).toBe('object');
expect(firmware.msg).toBe('Authentication error'); expect(firmware.msg).toBe(errors['406']);
expect(firmware.error).toBe(401); expect(firmware.error).toBe(406);
}); });
}); });
describe.skip('firmware: check updates methods', () => { describe('firmware: check updates methods', () => {
let connection; let connection;
beforeAll(() => { beforeAll(() => {
connection = new ewelink({ email, password }); connection = new ewelink({ email, password });
}); });
test.skip('outdated device firmware should return available version', async () => { test('outdated device firmware should return available version', async () => {
const status = await connection.checkDeviceUpdate(outdatedFirmwareDevice); const status = await connection.checkDeviceUpdate(outdatedFirmwareDevice);
expect(typeof status).toBe('object'); expect(typeof status).toBe('object');
expect(typeof status).toBe('object'); expect(typeof status).toBe('object');
@@ -82,7 +83,8 @@ describe.skip('firmware: check updates methods', () => {
test('invalid device update check should return error', async () => { test('invalid device update check should return error', async () => {
const status = await connection.checkDeviceUpdate('invalid deviceid'); const status = await connection.checkDeviceUpdate('invalid deviceid');
expect(typeof status).toBe('object'); expect(typeof status).toBe('object');
expect(status.error).toBe(500); expect(status.msg).toBe(errors['404']);
expect(status.error).toBe(404);
}); });
test('get devices update check should be valid response', async () => { test('get devices update check should be valid response', async () => {
@@ -95,7 +97,7 @@ describe.skip('firmware: check updates methods', () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' }); const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const status = await conn.checkDevicesUpdates(); const status = await conn.checkDevicesUpdates();
expect(typeof status).toBe('object'); expect(typeof status).toBe('object');
expect(status.msg).toBe('Authentication error'); expect(status.msg).toBe(errors['406']);
expect(status.error).toBe(401); expect(status.error).toBe(406);
}); });
}); });

View File

@@ -1,19 +1,13 @@
const ewelinkHelpers = require('../lib/ewelink-helper'); const { APP_SECRET } = require('../src/data/constants');
const ewelinkHelpers = require('../src/helpers/ewelink');
describe('check eWeLink helpers', () => { describe('check eWeLink helpers', () => {
test('should return fake imei', async () => {
const imei = ewelinkHelpers.makeFakeIMEI();
expect(imei.length).toBe(36);
expect(imei.substr(0, 9)).toBe('DF7425A0-');
expect(imei.substr(imei.length - 18, imei.length)).toBe(
'-9F5E-3BC9179E48FB'
);
});
test('make authorization sign should return right string', async () => { test('make authorization sign should return right string', async () => {
const auth = ewelinkHelpers.makeAuthorizationSign({ data: 'string' }); const auth = ewelinkHelpers.makeAuthorizationSign(APP_SECRET, {
data: 'string',
});
expect(auth.length).toBe(44); expect(auth.length).toBe(44);
expect(auth).toBe('WtmdvaPxqhi3pd8ck1R/bvzfRzHgxDTwgnuOib33xx4='); expect(auth).toBe('7Aaa/8EuRScACNrZTATW2WKIY7lcRnjgWHTiBl8G0TQ=');
}); });
test('getDeviceChannelCount method should return right value', async () => { test('getDeviceChannelCount method should return right value', async () => {

View File

@@ -1,54 +1,51 @@
const delay = require('delay');
const ewelink = require('../main'); const ewelink = require('../main');
const errors = require('../src/data/errors');
const { const {
singleChannelDeviceId, singleChannelDeviceId,
deviceIdWithPower, deviceIdWithPower,
fourChannelsDevice, fourChannelsDevice,
} = require('./_setup/credentials.js'); } = require('./_setup/config/credentials.js');
describe('invalid credentials', () => { describe('invalid credentials', () => {
beforeEach(async () => {
await delay(1000);
});
test('no credentials given', async () => { test('no credentials given', async () => {
const conn = new ewelink({}); expect(() => {
expect(typeof conn).toBe('object'); const conn = new ewelink({});
expect(conn.error).toBe('No credentials provided'); }).toThrow(errors.invalidCredentials);
}); });
test('get error response on ewelink credentials', async () => { test('get error response on ewelink credentials', async () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' }); const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const credentials = await conn.getCredentials(); const credentials = await conn.getCredentials();
expect(typeof credentials).toBe('object'); expect(typeof credentials).toBe('object');
expect(credentials.msg).toBe('Authentication error'); expect(credentials.msg).toBe(errors[406]);
expect(credentials.error).toBe(400); expect(credentials.error).toBe(406);
}); });
test('get error response on all devices', async () => { test('get error response on all devices', async () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' }); const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const devices = await conn.getDevices(); const devices = await conn.getDevices();
expect(typeof devices).toBe('object'); expect(typeof devices).toBe('object');
expect(devices.msg).toBe('Authentication error'); expect(devices.msg).toBe(errors['406']);
expect(devices.error).toBe(401); expect(devices.error).toBe(406);
}); });
test('get error response on specific device', async () => { test('get error response on specific device', async () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' }); const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const device = await conn.getDevice(singleChannelDeviceId); const device = await conn.getDevice(singleChannelDeviceId);
const { msg, error } = device;
expect(typeof device).toBe('object'); expect(typeof device).toBe('object');
expect(device.msg).toBe('Authentication error'); expect(msg).toBe(errors[406]);
expect(device.error).toBe(401); expect(error).toBe(406);
}); });
test('get device power state should fail', async () => { test('get device power state should fail', async () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' }); const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const powerState = await conn.getDevicePowerState(singleChannelDeviceId); const powerState = await conn.getDevicePowerState(singleChannelDeviceId);
const { msg, error } = powerState;
expect(typeof powerState).toBe('object'); expect(typeof powerState).toBe('object');
expect(powerState.msg).toBe('Authentication error'); expect(msg).toBe(errors[401]);
expect(powerState.error).toBe(401); expect(error).toBe(401);
}); });
test('set device power state should fail', async () => { test('set device power state should fail', async () => {
@@ -58,17 +55,17 @@ describe('invalid credentials', () => {
singleChannelDeviceId, singleChannelDeviceId,
'on' 'on'
); );
const { msg, error } = powerState;
expect(typeof powerState).toBe('object'); expect(typeof powerState).toBe('object');
expect(powerState.msg).toBe('Authentication error'); expect(msg).toBe(errors[406]);
expect(powerState.error).toBe(401); expect(error).toBe(406);
}); });
test('current month power usage should fail', async () => { test('current month power usage should fail', async () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' }); const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const powerUsage = await conn.getDevicePowerUsage(deviceIdWithPower); const powerUsage = await conn.getDevicePowerUsage(deviceIdWithPower);
expect(typeof powerUsage).toBe('object'); expect(typeof powerUsage).toBe('object');
expect(powerUsage.msg).toBe('Forbidden'); expect(powerUsage.error).toBe(errors.noPower);
expect(powerUsage.error).toBe(403);
}); });
test('get channel count 1 should fail', async () => { test('get channel count 1 should fail', async () => {
@@ -76,16 +73,18 @@ describe('invalid credentials', () => {
const switchesAmount = await conn.getDeviceChannelCount( const switchesAmount = await conn.getDeviceChannelCount(
singleChannelDeviceId singleChannelDeviceId
); );
const { msg, error } = switchesAmount;
expect(typeof switchesAmount).toBe('object'); expect(typeof switchesAmount).toBe('object');
expect(switchesAmount.msg).toBe('Authentication error'); expect(msg).toBe(errors[406]);
expect(switchesAmount.error).toBe(401); expect(error).toBe(406);
}); });
test('get channel count 4 should fail', async () => { test('get channel count 4 should fail', async () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' }); const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const switchesAmount = await conn.getDeviceChannelCount(fourChannelsDevice); const switchesAmount = await conn.getDeviceChannelCount(fourChannelsDevice);
const { msg, error } = switchesAmount;
expect(typeof switchesAmount).toBe('object'); expect(typeof switchesAmount).toBe('object');
expect(switchesAmount.msg).toBe('Authentication error'); expect(msg).toBe(errors[406]);
expect(switchesAmount.error).toBe(401); expect(error).toBe(406);
}); });
}); });

View File

@@ -1,12 +1,10 @@
const delay = require('delay');
const ewelink = require('../main'); const ewelink = require('../main');
const { const {
email, email,
password, password,
deviceIdWithPower, deviceIdWithPower,
} = require('./_setup/credentials.js'); } = require('./_setup/config/credentials.js');
const { const {
rawPowerUsageExpectations, rawPowerUsageExpectations,
@@ -21,13 +19,9 @@ describe('power usage: node script', () => {
await conn.getCredentials(); await conn.getCredentials();
}); });
beforeEach(async () => {
await delay(1000);
});
test('should return raw power usage', async () => { test('should return raw power usage', async () => {
jest.setTimeout(30000); jest.setTimeout(30000);
const powerUsage = await conn.getDeviceRawPowerUsage(deviceIdWithPower); const powerUsage = await conn.getDevicePowerUsageRaw(deviceIdWithPower);
expect(typeof powerUsage).toBe('object'); expect(typeof powerUsage).toBe('object');
expect(powerUsage).toMatchObject(rawPowerUsageExpectations); expect(powerUsage).toMatchObject(rawPowerUsageExpectations);
expect(powerUsage.data.hundredDaysKwhData.length).toBe(600); expect(powerUsage.data.hundredDaysKwhData.length).toBe(600);
@@ -57,7 +51,7 @@ describe('power usage: serverless', () => {
test('should return raw power usage', async () => { test('should return raw power usage', async () => {
jest.setTimeout(30000); jest.setTimeout(30000);
const conn = new ewelink({ at: accessToken, apiKey }); const conn = new ewelink({ at: accessToken, apiKey });
const powerUsage = await conn.getDeviceRawPowerUsage(deviceIdWithPower); const powerUsage = await conn.getDevicePowerUsageRaw(deviceIdWithPower);
expect(typeof powerUsage).toBe('object'); expect(typeof powerUsage).toBe('object');
expect(powerUsage).toMatchObject(rawPowerUsageExpectations); expect(powerUsage).toMatchObject(rawPowerUsageExpectations);
expect(powerUsage.data.hundredDaysKwhData.length).toBe(600); expect(powerUsage.data.hundredDaysKwhData.length).toBe(600);

View File

@@ -1,13 +1,12 @@
const delay = require('delay');
const ewelink = require('../main'); const ewelink = require('../main');
const errors = require('../src/data/errors');
const { const {
email, email,
password, password,
deviceIdWithoutTempAndHum, deviceIdWithoutTempAndHum,
deviceIdWithTempAndHum: thDevice, deviceIdWithTempAndHum: thDevice,
} = require('./_setup/credentials.js'); } = require('./_setup/config/credentials.js');
describe('current temperature and humidity: node script', () => { describe('current temperature and humidity: node script', () => {
let conn; let conn;
@@ -19,7 +18,6 @@ describe('current temperature and humidity: node script', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await delay(1000);
device = await conn.getDevice(thDevice); device = await conn.getDevice(thDevice);
}); });
@@ -63,7 +61,6 @@ describe('current temperature and humidity: serverless', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await delay(1000);
connSL = new ewelink({ at: accessToken, apiKey }); connSL = new ewelink({ at: accessToken, apiKey });
device = await connSL.getDevice(thDevice); device = await connSL.getDevice(thDevice);
}); });
@@ -99,16 +96,16 @@ describe('current temperature and humidity: invalid device', () => {
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const temperature = await conn.getDeviceCurrentTemperature('invalid'); const temperature = await conn.getDeviceCurrentTemperature('invalid');
expect(typeof temperature).toBe('object'); expect(typeof temperature).toBe('object');
expect(temperature.msg).toBe('Device does not exist'); expect(temperature.msg).toBe(errors['404']);
expect(temperature.error).toBe(500); expect(temperature.error).toBe(404);
}); });
test('get device current humidity should fail', async () => { test('get device current humidity should fail', async () => {
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const humidity = await conn.getDeviceCurrentHumidity('invalid'); const humidity = await conn.getDeviceCurrentHumidity('invalid');
expect(typeof humidity).toBe('object'); expect(typeof humidity).toBe('object');
expect(humidity.msg).toBe('Device does not exist'); expect(humidity.msg).toBe(errors['404']);
expect(humidity.error).toBe(500); expect(humidity.error).toBe(404);
}); });
}); });
@@ -119,8 +116,8 @@ describe('current temperature and humidity: device without sensor', () => {
deviceIdWithoutTempAndHum deviceIdWithoutTempAndHum
); );
expect(typeof temperature).toBe('object'); expect(typeof temperature).toBe('object');
expect(temperature.msg).toBe("Can't read sensor data from device"); expect(temperature.msg).toBe(errors.noSensor);
expect(temperature.error).toBe(500); expect(temperature.error).toBe(404);
}); });
test('get device current humidity should fail', async () => { test('get device current humidity should fail', async () => {
@@ -129,8 +126,8 @@ describe('current temperature and humidity: device without sensor', () => {
deviceIdWithoutTempAndHum deviceIdWithoutTempAndHum
); );
expect(typeof humidity).toBe('object'); expect(typeof humidity).toBe('object');
expect(humidity.msg).toBe("Can't read sensor data from device"); expect(humidity.msg).toBe(errors.noSensor);
expect(humidity.error).toBe(500); expect(humidity.error).toBe(404);
}); });
}); });
@@ -139,15 +136,15 @@ describe('current temperature and humidity: invalid credentials', () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' }); const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const result = await conn.getDeviceCurrentTemperature(thDevice); const result = await conn.getDeviceCurrentTemperature(thDevice);
expect(typeof result).toBe('object'); expect(typeof result).toBe('object');
expect(result.msg).toBe('Authentication error'); expect(result.msg).toBe(errors['406']);
expect(result.error).toBe(401); expect(result.error).toBe(406);
}); });
test('get device current humidity should fail', async () => { test('get device current humidity should fail', async () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' }); const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const result = await conn.getDeviceCurrentHumidity(thDevice); const result = await conn.getDeviceCurrentHumidity(thDevice);
expect(typeof result).toBe('object'); expect(typeof result).toBe('object');
expect(result.msg).toBe('Authentication error'); expect(result.msg).toBe(errors['406']);
expect(result.error).toBe(401); expect(result.error).toBe(406);
}); });
}); });

View File

@@ -1,6 +1,7 @@
const ewelink = require('../main'); const ewelink = require('../main');
const errors = require('../src/data/errors');
const { email, password, region } = require('./_setup/credentials.js'); const { email, password, region } = require('./_setup/config/credentials.js');
const { regionExpectations } = require('./_setup/expectations'); const { regionExpectations } = require('./_setup/expectations');
@@ -21,17 +22,15 @@ describe('check user information', () => {
}); });
const response = await connection.getRegion(); const response = await connection.getRegion();
expect(typeof response).toBe('object'); expect(typeof response).toBe('object');
expect(response.msg).toBe('Authentication error'); expect(response.msg).toBe(errors['406']);
expect(response.error).toBe(400); expect(response.error).toBe(406);
}); });
test('invalid initialization should warn user', async () => { test('invalid initialization should warn user', async () => {
const connection = new ewelink({ at: 'access token' }); const connection = new ewelink({ at: 'access token' });
const response = await connection.getRegion(); const response = await connection.getRegion();
expect(typeof response).toBe('object'); expect(typeof response).toBe('object');
expect(response.msg).toBe( expect(response.msg).toBe(errors.invalidAuth);
'Library needs to be initialized using email and password'
);
expect(response.error).toBe(406); expect(response.error).toBe(406);
}); });
}); });

View File

@@ -1,35 +1,30 @@
const delay = require('delay');
const ewelink = require('../main'); const ewelink = require('../main');
const errors = require('../src/data/errors');
const { const {
email, email,
password, password,
deviceIdWithoutPower, deviceIdWithoutPower,
fourChannelsDevice, fourChannelsDevice,
} = require('./_setup/credentials.js'); } = require('./_setup/config/credentials.js');
const { credentialsExpectations } = require('./_setup/expectations'); const { credentialsExpectations } = require('./_setup/expectations');
describe('valid credentials, invalid device', () => { describe('valid credentials, invalid device', () => {
beforeEach(async () => {
await delay(1000);
});
test('get power state on invalid device should fail', async () => { test('get power state on invalid device should fail', async () => {
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const powerState = await conn.getDevicePowerState('invalid deviceid'); const powerState = await conn.getDevicePowerState('invalid deviceid');
expect(typeof powerState).toBe('object'); expect(typeof powerState).toBe('object');
expect(powerState.msg).toBe('Device does not exist'); expect(powerState.msg).toBe(errors[404]);
expect(powerState.error).toBe(500); expect(powerState.error).toBe(404);
}); });
test('get power state on wrong device channel should fail', async () => { test('get power state on wrong device channel should fail', async () => {
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const powerState = await conn.getDevicePowerState(fourChannelsDevice, 8); const powerState = await conn.getDevicePowerState(fourChannelsDevice, 8);
expect(typeof powerState).toBe('object'); expect(typeof powerState).toBe('object');
expect(powerState.msg).toBe('Device channel does not exist'); expect(powerState.msg).toBe(errors.ch404);
expect(powerState.error).toBe(false); expect(powerState.error).toBe(404);
}); });
test('set power state on invalid device should fail', async () => { test('set power state on invalid device should fail', async () => {
@@ -37,8 +32,8 @@ describe('valid credentials, invalid device', () => {
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const powerState = await conn.setDevicePowerState('invalid deviceid', 'on'); const powerState = await conn.setDevicePowerState('invalid deviceid', 'on');
expect(typeof powerState).toBe('object'); expect(typeof powerState).toBe('object');
expect(powerState.msg).toBe('Device does not exist'); expect(powerState.msg).toBe(errors[404]);
expect(powerState.error).toBe(500); expect(powerState.error).toBe(404);
}); });
test('set power state on wrong device channel should fail', async () => { test('set power state on wrong device channel should fail', async () => {
@@ -50,8 +45,8 @@ describe('valid credentials, invalid device', () => {
8 8
); );
expect(typeof powerState).toBe('object'); expect(typeof powerState).toBe('object');
expect(powerState.msg).toBe('Device channel does not exist'); expect(powerState.msg).toBe(errors.ch404);
expect(powerState.error).toBe(false); expect(powerState.error).toBe(404);
}); });
test('toggle power state on invalid device should fail', async () => { test('toggle power state on invalid device should fail', async () => {
@@ -59,17 +54,16 @@ describe('valid credentials, invalid device', () => {
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const powerState = await conn.toggleDevice('invalid deviceid'); const powerState = await conn.toggleDevice('invalid deviceid');
expect(typeof powerState).toBe('object'); expect(typeof powerState).toBe('object');
expect(powerState.msg).toBe('Device does not exist'); expect(powerState.msg).toBe(errors[404]);
expect(powerState.error).toBe(500); expect(powerState.error).toBe(404);
}); });
test('raw power usage on invalid device should fail', async () => { test('raw power usage on invalid device should fail', async () => {
jest.setTimeout(30000); jest.setTimeout(30000);
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const powerUsage = await conn.getDeviceRawPowerUsage('invalid deviceid'); const powerUsage = await conn.getDevicePowerUsageRaw('invalid deviceid');
expect(typeof powerUsage).toBe('object'); expect(typeof powerUsage).toBe('object');
expect(powerUsage.msg).toBe('Forbidden'); expect(powerUsage.error).toBe(errors.noPower);
expect(powerUsage.error).toBe(403);
}); });
test('current month power usage on invalid device should fail', async () => { test('current month power usage on invalid device should fail', async () => {
@@ -77,24 +71,23 @@ describe('valid credentials, invalid device', () => {
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const powerUsage = await conn.getDevicePowerUsage('invalid deviceid'); const powerUsage = await conn.getDevicePowerUsage('invalid deviceid');
expect(typeof powerUsage).toBe('object'); expect(typeof powerUsage).toBe('object');
expect(powerUsage.msg).toBe('Forbidden'); expect(powerUsage.error).toBe(errors.noPower);
expect(powerUsage.error).toBe(403);
}); });
test('raw power on device without electricity monitor should fail', async () => { test('raw power on device without electricity monitor should fail', async () => {
jest.setTimeout(30000); jest.setTimeout(30000);
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const powerUsage = await conn.getDeviceRawPowerUsage(deviceIdWithoutPower); const powerUsage = await conn.getDevicePowerUsageRaw(deviceIdWithoutPower);
expect(typeof powerUsage).toBe('object'); expect(typeof powerUsage).toBe('object');
expect(powerUsage.error).toBe('No power usage data found.'); expect(powerUsage.error).toBe(errors.noPower);
}); });
test('get channel count should fail', async () => { test('get channel count should fail', async () => {
const conn = new ewelink({ email, password }); const conn = new ewelink({ email, password });
const switchesAmount = await conn.getDeviceChannelCount('invalid deviceid'); const switchesAmount = await conn.getDeviceChannelCount('invalid deviceid');
expect(typeof switchesAmount).toBe('object'); expect(typeof switchesAmount).toBe('object');
expect(switchesAmount.msg).toBe('Device does not exist'); expect(switchesAmount.msg).toBe(errors[404]);
expect(switchesAmount.error).toBe(500); expect(switchesAmount.error).toBe(404);
}); });
}); });

View File

@@ -1,5 +1,6 @@
const ewelink = require('../main'); const ewelink = require('../main');
const Zeroconf = require('../classes/Zeroconf'); const Zeroconf = require('../src/classes/Zeroconf');
const errors = require('../src/data/errors');
const { const {
email, email,
@@ -7,14 +8,14 @@ const {
region, region,
localIp, localIp,
localIpInvalid, localIpInvalid,
} = require('./_setup/credentials.js'); } = require('./_setup/config/credentials.js');
const { allDevicesExpectations } = require('./_setup/expectations'); const { allDevicesExpectations } = require('./_setup/expectations');
describe('zeroconf: save devices to cache file', () => { describe('zeroconf: save devices to cache file', () => {
test('can save cached devices file', async () => { test('can save cached devices file', async () => {
jest.setTimeout(30000); jest.setTimeout(30000);
const file = './test/_setup/devices-cache.json'; const file = './test/_setup/cache/devices-cache.json';
const conn = new ewelink({ region, email, password }); const conn = new ewelink({ region, email, password });
const result = await conn.saveDevicesCache(file); const result = await conn.saveDevicesCache(file);
expect(typeof result).toBe('object'); expect(typeof result).toBe('object');
@@ -36,15 +37,15 @@ describe('zeroconf: save devices to cache file', () => {
const conn = new ewelink({ email: 'invalid', password: 'credentials' }); const conn = new ewelink({ email: 'invalid', password: 'credentials' });
const result = await conn.saveDevicesCache(file); const result = await conn.saveDevicesCache(file);
expect(typeof result).toBe('object'); expect(typeof result).toBe('object');
expect(result.msg).toBe('Authentication error'); expect(result.msg).toBe(errors['406']);
expect(result.error).toBe(401); expect(result.error).toBe(406);
}); });
}); });
describe('zeroconf: save arp table to file', () => { describe('zeroconf: save arp table to file', () => {
test('can save arp table file', async () => { test('can save arp table file', async () => {
jest.setTimeout(30000); jest.setTimeout(30000);
const file = './test/_setup/arp-table.json'; const file = './test/_setup/cache/arp-table.json';
const arpTable = await Zeroconf.saveArpTable({ const arpTable = await Zeroconf.saveArpTable({
ip: localIp, ip: localIp,
file, file,
@@ -67,7 +68,7 @@ describe('zeroconf: save arp table to file', () => {
test('error saving arp table file with invalid local network', async () => { test('error saving arp table file with invalid local network', async () => {
jest.setTimeout(30000); jest.setTimeout(30000);
const file = './test/_setup/arp-table.json'; const file = './test/_setup/cache/arp-table.json';
const arpTable = await Zeroconf.saveArpTable({ const arpTable = await Zeroconf.saveArpTable({
ip: localIpInvalid, ip: localIpInvalid,
file, file,
@@ -83,7 +84,7 @@ describe('zeroconf: load devices to cache file', () => {
const conn = new ewelink({ region, email, password }); const conn = new ewelink({ region, email, password });
const devices = await conn.getDevices(); const devices = await conn.getDevices();
const devicesCache = await Zeroconf.loadCachedDevices( const devicesCache = await Zeroconf.loadCachedDevices(
'./test/_setup/devices-cache.json' './test/_setup/cache/devices-cache.json'
); );
expect(typeof devicesCache).toBe('object'); expect(typeof devicesCache).toBe('object');
expect(devicesCache.length).toBe(devices.length); expect(devicesCache.length).toBe(devices.length);
@@ -101,7 +102,7 @@ describe('zeroconf: load devices to cache file', () => {
describe('zeroconf: load arp table file', () => { describe('zeroconf: load arp table file', () => {
test('can load arp table file', async () => { test('can load arp table file', async () => {
const arpTable = await Zeroconf.loadArpTable( const arpTable = await Zeroconf.loadArpTable(
'./test/_setup/arp-table.json' './test/_setup/cache/arp-table.json'
); );
expect(typeof arpTable).toBe('object'); expect(typeof arpTable).toBe('object');
expect(arpTable[0]).toMatchObject({ expect(arpTable[0]).toMatchObject({