Skip to content

Commit

Permalink
ewelink: set value with websocket
Browse files Browse the repository at this point in the history
  • Loading branch information
atrovato committed Dec 15, 2023
1 parent c401384 commit a76f65d
Show file tree
Hide file tree
Showing 41 changed files with 586 additions and 222 deletions.
8 changes: 4 additions & 4 deletions front/src/routes/integration/all/ewelink/EweLinkDeviceBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cx from 'classnames';
import { Link } from 'preact-router';
import get from 'get-value';

import { DEVICE_FIRMWARE, DEVICE_ONLINE } from '../../../../../../server/services/ewelink/lib/utils/constants';
import { DEVICE_PARAMS } from '../../../../../../server/services/ewelink/lib/utils/constants';
import DeviceFeatures from '../../../../components/device/view/DeviceFeatures';

class EweLinkDeviceBox extends Component {
Expand Down Expand Up @@ -68,8 +68,8 @@ class EweLinkDeviceBox extends Component {
{ loading, errorMessage, tooMuchStatesError, statesNumber }
) {
const validModel = device.features && device.features.length > 0;
const online = device.params.find(param => param.name === DEVICE_ONLINE).value === '1';
const firmware = (device.params.find(param => param.name === DEVICE_FIRMWARE) || { value: '?.?.?' }).value;
const online = device.params.find(param => param.name === DEVICE_PARAMS.ONLINE).value === '1';
const firmware = (device.params.find(param => param.name === DEVICE_PARAMS.FIRMWARE) || { value: '?.?.?' }).value;

return (
<div class="col-md-6">
Expand All @@ -82,7 +82,7 @@ class EweLinkDeviceBox extends Component {
</div>
</Localizer>
<div class="page-options d-flex">
{device.params.find(param => param.name === DEVICE_FIRMWARE) && (
{device.params.find(param => param.name === DEVICE_PARAMS.FIRMWARE) && (
<div class="tag tag-blue">{`Firmware: ${firmware}`}</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function createClients() {
region: applicationRegion,
});

this.ewelinkWebSocketClientFactory = new this.eweLinkApi.Ws({
this.ewelinkWebSocketClient = new this.eweLinkApi.Ws({
appId: applicationId,
appSecret: applicationSecret,
region: applicationRegion,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ async function loadConfiguration() {
this.ewelinkWebAPIClient.at = tokenObject.accessToken;
this.ewelinkWebAPIClient.rt = tokenObject.refreshToken;

// Load user API key
this.userApiKey = await this.gladys.variable.getValue(CONFIGURATION_KEYS.USER_API_KEY, this.serviceId);
if (!this.userApiKey) {
await this.retrieveUserApiKey();
}

await this.createWebSocketClient();

logger.info('eWeLink: stored configuration well loaded...');
Expand Down
17 changes: 7 additions & 10 deletions server/services/ewelink/lib/device/discover.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { mergeDevices } = require('../../../../utils/device');
const logger = require('../../../../utils/logger');
const features = require('../features');
const { getExternalId } = require('../utils/externalId');
Expand All @@ -14,20 +15,16 @@ async function discover() {
);
logger.info(`eWeLink: ${thingList.length} device(s) found while retrieving from the cloud !`);

const discoveredDevices = [];
const discoveredDevices = thingList.map(({ itemData }) => {
logger.debug(`eWeLink: new device "${itemData.deviceid}" (${itemData.productModel}) discovered`);

thingList.forEach(({ itemData }) => {
const deviceInGladys = this.gladys.stateManager.get('deviceByExternalId', getExternalId(itemData));
// ...if it is already in Gladys, ignore it...
if (deviceInGladys) {
logger.debug(`eWeLink: device "${itemData.deviceid}" is already in Gladys !`);
} else {
logger.debug(`eWeLink: new device "${itemData.deviceid}" (${itemData.productModel}) discovered`);
const discoveredDevice = features.getDevice(this.serviceId, itemData);
discoveredDevices.push(discoveredDevice);
}
const discoveredDevice = features.getDevice(this.serviceId, itemData);
return mergeDevices(discoveredDevice, deviceInGladys);
});

this.discoveredDevices = discoveredDevices;

return discoveredDevices;
}

Expand Down
37 changes: 15 additions & 22 deletions server/services/ewelink/lib/device/setValue.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,32 @@
const { DEVICE_FEATURE_TYPES } = require('../../../../utils/constants');
const logger = require('../../../../utils/logger');
const binary = require('../features/binary');
const { parseExternalId } = require('../utils/externalId');

const FEATURE_TYPE_MAP = {
[DEVICE_FEATURE_TYPES.SWITCH.BINARY]: binary,
};

/**
* @description Change value of an eWeLink device.
* @param {object} device - The device to control.
* @param {object} deviceFeature - The deviceFeature to control.
* @param {string|number} value - The new value.
* @example
* setValue(device, deviceFeature);
* setValue({ ...device }, { ...deviceFeature }, 1);
*/
async function setValue(device, deviceFeature, value) {
const { deviceId, channel } = parseExternalId(deviceFeature.external_id);
switch (deviceFeature.type) {
case DEVICE_FEATURE_TYPES.SWITCH.BINARY: {
const params = {};
// Count number of binary features to determine if "switch" or "switches" param need to be changed
const nbBinaryFeatures = device.features.reduce(
(acc, currentFeature) => (currentFeature.type === DEVICE_FEATURE_TYPES.SWITCH.BINARY ? acc + 1 : acc),
0,
);
const { external_id: featureExternalId, type } = deviceFeature;

const binaryValue = value ? 'on' : 'off';
if (nbBinaryFeatures > 1) {
params.switches = [{ switch: binaryValue, outlet: channel }];
} else {
params.switch = binaryValue;
}
const mapper = FEATURE_TYPE_MAP[type];
if (mapper) {
const parsedExternalId = parseExternalId(featureExternalId);
const { deviceId } = parsedExternalId;
const params = binary.writeParams(device, parsedExternalId, value);

await this.handleRequest(async () => this.ewelinkWebAPIClient.device.setThingStatus(1, deviceId, params));
break;
}
default:
logger.warn(`eWeLink: Warning, feature type "${deviceFeature.type}" not handled yet!`);
break;
this.ewelinkWebSocketClient.Connect.updateState(deviceId, params);
} else {
logger.warn(`eWeLink: Warning, feature type "${type}" not handled yet!`);
}
}

Expand Down
2 changes: 1 addition & 1 deletion server/services/ewelink/lib/ewelink.stop.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* await this.stop();
*/
async function stop() {
this.closeWebSocketClient();
this.ewelinkWebAPIClient = null;
this.closeWebSocketClient();
this.updateStatus({ connected: false });
}

Expand Down
18 changes: 17 additions & 1 deletion server/services/ewelink/lib/features/binary.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,28 @@ module.exports = {
params.switches.forEach(({ switch: value, outlet }) => {
const state = value === 'on' ? STATE.ON : STATE.OFF;
states.push({
featureExternalId: `${externalId}:binary:${outlet}`,
featureExternalId: `${externalId}:binary:${outlet + 1}`,
state,
});
});
}

return states;
},
writeParams: (device, parsedExternalId, value) => {
const convertedValue = value ? 'on' : 'off';

// Count number of binary features to determine if "switch" or "switches" param need to be changed
const nbBinaryFeatures = device.features.reduce(
(acc, currentFeature) => (currentFeature.type === DEVICE_FEATURE_TYPES.SWITCH.BINARY ? acc + 1 : acc),
0,
);

if (nbBinaryFeatures > 1) {
const { channel } = parsedExternalId;
return { switches: [{ switch: convertedValue, outlet: channel - 1 }] };
}

return { switch: convertedValue };
},
};
2 changes: 1 addition & 1 deletion server/services/ewelink/lib/features/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const AVAILABLE_FEATURES = [binaryFeature, humidityFeature, temperatureFeature];

const AVAILABLE_FEATURE_MODELS = {
binary: {
uiid: [1, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15],
uiid: [1, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15, 126],
feature: binaryFeature,
},
humidity: {
Expand Down
7 changes: 4 additions & 3 deletions server/services/ewelink/lib/handlers/ewelink.handleRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ const { NB_MAX_RETRY_EXPIRED } = require('../utils/constants');
/**
* @description Provides a single way to manage WS requests with retries and refresh token.
* @param {Function} request - Client request.
* @param {boolean} force - Forces API call even if service is not marked as ready (eg. At the init phase).
* @param {number} nbRetry - Number of retry.
* @returns {Promise} The WS call response.
* @example
* const data = await this.handleRequest(() => client.getDevices());
*/
async function handleRequest(request, nbRetry = 0) {
async function handleRequest(request, force = false, nbRetry = 0) {
// Do not call API if service is not ready
const { configured, connected } = this.status;
if (!configured || !connected) {
if (!force && (!configured || !connected)) {
throw new ServiceNotConfiguredError('eWeLink is not ready, please complete the configuration');
}

Expand All @@ -26,7 +27,7 @@ async function handleRequest(request, nbRetry = 0) {
// Store new tokens
await this.saveTokens(tokens);
// Retry request
return this.handleRequest(request, nbRetry + 1);
return this.handleRequest(request, force, nbRetry + 1);
}

return this.handleResponse(response);
Expand Down
13 changes: 7 additions & 6 deletions server/services/ewelink/lib/handlers/ewelink.handleResponse.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,27 @@ const { CONFIGURATION_KEYS } = require('../utils/constants');
* const data = this.handleResponse(res, (data) => console.log);
*/
async function handleResponse(response) {
const { error, msg, data } = response;
const { error, msg, reason, data } = response;
const message = msg || reason;
logger.debug(`eWeLink response: %j`, response);

if (error) {
// see https://coolkit-technologies.github.io/eWeLink-API/#/en/APICenterV2?id=error-codes
logger.error(`eWeLink: error with API - ${msg}`);
logger.error(`eWeLink: error with API - ${message}`);
switch (error) {
case 401:
case 402:
await this.gladys.variable.destroy(CONFIGURATION_KEYS.USER_TOKENS, this.serviceId);
this.closeWebSocketClient();
this.updateStatus({ connected: false });
throw new ServiceNotConfiguredError(msg);
throw new ServiceNotConfiguredError(message);
case 400:
throw new BadParameters(msg);
throw new BadParameters(message);
case 405:
case 4002:
throw new NotFoundError(msg);
throw new NotFoundError(message);
default:
throw new Error(msg);
throw new Error(message);
}
}

Expand Down
8 changes: 6 additions & 2 deletions server/services/ewelink/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const { buildLoginUrl } = require('./user/ewelink.buildLoginUrl');
const { exchangeToken } = require('./user/ewelink.exchangeToken');
const { deleteTokens } = require('./user/ewelink.deleteTokens');
const { saveTokens } = require('./user/ewelink.saveTokens');
const { retrieveUserApiKey } = require('./user/ewelink.retrieveUserApiKey');

const { handleRequest } = require('./handlers/ewelink.handleRequest');
const { handleResponse } = require('./handlers/ewelink.handleResponse');
Expand Down Expand Up @@ -41,8 +42,10 @@ const EweLinkHandler = function EweLinkHandler(gladys, eweLinkApi, serviceId) {
this.serviceId = serviceId;

this.ewelinkWebAPIClient = null;
this.ewelinkWebSocketClientFactory = null;
this.ewelinkWebSocketClient = null;
this.userApiKey = null;

this.discoveredDevices = [];

this.loginState = null;
this.configuration = {};
Expand All @@ -53,6 +56,7 @@ const EweLinkHandler = function EweLinkHandler(gladys, eweLinkApi, serviceId) {
};

EweLinkHandler.prototype.updateStatus = updateStatus;
EweLinkHandler.prototype.getStatus = getStatus;

EweLinkHandler.prototype.saveConfiguration = saveConfiguration;
EweLinkHandler.prototype.loadConfiguration = loadConfiguration;
Expand All @@ -62,13 +66,13 @@ EweLinkHandler.prototype.buildLoginUrl = buildLoginUrl;
EweLinkHandler.prototype.exchangeToken = exchangeToken;
EweLinkHandler.prototype.deleteTokens = deleteTokens;
EweLinkHandler.prototype.saveTokens = saveTokens;
EweLinkHandler.prototype.retrieveUserApiKey = retrieveUserApiKey;

EweLinkHandler.prototype.handleRequest = handleRequest;
EweLinkHandler.prototype.handleResponse = handleResponse;

EweLinkHandler.prototype.discover = discover;
EweLinkHandler.prototype.setValue = setValue;
EweLinkHandler.prototype.getStatus = getStatus;

EweLinkHandler.prototype.createWebSocketClient = createWebSocketClient;
EweLinkHandler.prototype.closeWebSocketClient = closeWebSocketClient;
Expand Down
6 changes: 6 additions & 0 deletions server/services/ewelink/lib/params/apikey.param.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { DEVICE_PARAMS } = require('../utils/constants');

module.exports = {
EWELINK_KEY_PATH: 'apikey',
GLADYS_PARAM_KEY: DEVICE_PARAMS.API_KEY,
};
4 changes: 3 additions & 1 deletion server/services/ewelink/lib/params/firmware.param.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const { DEVICE_PARAMS } = require('../utils/constants');

module.exports = {
EWELINK_KEY_PATH: 'params.fwVersion',
GLADYS_PARAM_KEY: 'FIRMWARE',
GLADYS_PARAM_KEY: DEVICE_PARAMS.FIRMWARE,
};
5 changes: 3 additions & 2 deletions server/services/ewelink/lib/params/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const get = require('get-value');

const onlineParam = require('./online.param');
const firmwareParam = require('./firmware.param');
const onlineParam = require('./online.param');
const apiKeyParam = require('./apikey.param');

const PARAMS = [firmwareParam, onlineParam];
const PARAMS = [firmwareParam, onlineParam, apiKeyParam];

/**
* @description Read device params from eWeLink device params.
Expand Down
4 changes: 3 additions & 1 deletion server/services/ewelink/lib/params/online.param.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { DEVICE_PARAMS } = require('../utils/constants');

/**
* @description Convert online state.
* @param {boolean} rawValue - Param raw value.
Expand All @@ -11,6 +13,6 @@ function convertValue(rawValue) {

module.exports = {
EWELINK_KEY_PATH: 'online',
GLADYS_PARAM_KEY: 'ONLINE',
GLADYS_PARAM_KEY: DEVICE_PARAMS.ONLINE,
convertValue,
};
2 changes: 2 additions & 0 deletions server/services/ewelink/lib/user/ewelink.exchangeToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ async function exchangeToken({ redirectUrl, code, region, state }) {

await this.saveTokens(data);

await this.retrieveUserApiKey();

await this.createWebSocketClient();

this.updateStatus({ connected: true });
Expand Down
32 changes: 32 additions & 0 deletions server/services/ewelink/lib/user/ewelink.retrieveUserApiKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { ServiceNotConfiguredError } = require('../../../../utils/coreErrors');
const logger = require('../../../../utils/logger');
const { CONFIGURATION_KEYS } = require('../utils/constants');

/**
* @description Retrieve the user API key from eWeLink user family. It also store in database.
* @example
* await this.retrieveUserApiKey();
*/
async function retrieveUserApiKey() {
logger.info('eWeLink: loading user API key...');

// Load API key
const { currentFamilyId, familyList } = await this.handleRequest(
() => this.ewelinkWebAPIClient.home.getFamily(),
true,
);
const { apikey: apiKey } = familyList.find((family) => family.id === currentFamilyId) || {};

// Store API key
if (apiKey) {
logger.info('eWeLink: saving user API key...');
this.userApiKey = apiKey;
await this.gladys.variable.setValue(CONFIGURATION_KEYS.USER_API_KEY, apiKey, this.serviceId);
} else {
throw new ServiceNotConfiguredError('eWeLink: no user API key retrieved');
}
}

module.exports = {
retrieveUserApiKey,
};
Loading

0 comments on commit a76f65d

Please sign in to comment.