diff --git a/front/src/routes/integration/all/ewelink/EweLinkDeviceBox.jsx b/front/src/routes/integration/all/ewelink/EweLinkDeviceBox.jsx index 03c7dcf9b4..d68cb24271 100644 --- a/front/src/routes/integration/all/ewelink/EweLinkDeviceBox.jsx +++ b/front/src/routes/integration/all/ewelink/EweLinkDeviceBox.jsx @@ -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 { @@ -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 (
@@ -82,7 +82,7 @@ class EweLinkDeviceBox extends Component {
- {device.params.find(param => param.name === DEVICE_FIRMWARE) && ( + {device.params.find(param => param.name === DEVICE_PARAMS.FIRMWARE) && (
{`Firmware: ${firmware}`}
)}
diff --git a/server/services/ewelink/lib/config/ewelink.createClients.js b/server/services/ewelink/lib/config/ewelink.createClients.js index 68287775cd..b033361ffa 100644 --- a/server/services/ewelink/lib/config/ewelink.createClients.js +++ b/server/services/ewelink/lib/config/ewelink.createClients.js @@ -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, diff --git a/server/services/ewelink/lib/config/ewelink.loadConfiguration.js b/server/services/ewelink/lib/config/ewelink.loadConfiguration.js index 6a47c9aaa5..bfb68a0a7a 100644 --- a/server/services/ewelink/lib/config/ewelink.loadConfiguration.js +++ b/server/services/ewelink/lib/config/ewelink.loadConfiguration.js @@ -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...'); diff --git a/server/services/ewelink/lib/device/discover.js b/server/services/ewelink/lib/device/discover.js index 6463ba1185..813d56c655 100644 --- a/server/services/ewelink/lib/device/discover.js +++ b/server/services/ewelink/lib/device/discover.js @@ -1,3 +1,4 @@ +const { mergeDevices } = require('../../../../utils/device'); const logger = require('../../../../utils/logger'); const features = require('../features'); const { getExternalId } = require('../utils/externalId'); @@ -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; } diff --git a/server/services/ewelink/lib/device/setValue.js b/server/services/ewelink/lib/device/setValue.js index 0e93f971fe..89fb4630d9 100644 --- a/server/services/ewelink/lib/device/setValue.js +++ b/server/services/ewelink/lib/device/setValue.js @@ -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!`); } } diff --git a/server/services/ewelink/lib/ewelink.stop.js b/server/services/ewelink/lib/ewelink.stop.js index 60d41f2605..fb293e9131 100644 --- a/server/services/ewelink/lib/ewelink.stop.js +++ b/server/services/ewelink/lib/ewelink.stop.js @@ -4,8 +4,8 @@ * await this.stop(); */ async function stop() { - this.closeWebSocketClient(); this.ewelinkWebAPIClient = null; + this.closeWebSocketClient(); this.updateStatus({ connected: false }); } diff --git a/server/services/ewelink/lib/features/binary.js b/server/services/ewelink/lib/features/binary.js index f420fc7e60..6c8c16ccf3 100644 --- a/server/services/ewelink/lib/features/binary.js +++ b/server/services/ewelink/lib/features/binary.js @@ -27,7 +27,7 @@ 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, }); }); @@ -35,4 +35,20 @@ module.exports = { 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 }; + }, }; diff --git a/server/services/ewelink/lib/features/index.js b/server/services/ewelink/lib/features/index.js index 377aec6da3..588f402373 100644 --- a/server/services/ewelink/lib/features/index.js +++ b/server/services/ewelink/lib/features/index.js @@ -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: { diff --git a/server/services/ewelink/lib/handlers/ewelink.handleRequest.js b/server/services/ewelink/lib/handlers/ewelink.handleRequest.js index a64a1a73c9..2e7577a1f9 100644 --- a/server/services/ewelink/lib/handlers/ewelink.handleRequest.js +++ b/server/services/ewelink/lib/handlers/ewelink.handleRequest.js @@ -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'); } @@ -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); diff --git a/server/services/ewelink/lib/handlers/ewelink.handleResponse.js b/server/services/ewelink/lib/handlers/ewelink.handleResponse.js index 75a4b3ab01..3f8d147117 100644 --- a/server/services/ewelink/lib/handlers/ewelink.handleResponse.js +++ b/server/services/ewelink/lib/handlers/ewelink.handleResponse.js @@ -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); } } diff --git a/server/services/ewelink/lib/index.js b/server/services/ewelink/lib/index.js index 19c0957c30..2caf77566b 100644 --- a/server/services/ewelink/lib/index.js +++ b/server/services/ewelink/lib/index.js @@ -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'); @@ -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 = {}; @@ -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; @@ -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; diff --git a/server/services/ewelink/lib/params/apikey.param.js b/server/services/ewelink/lib/params/apikey.param.js new file mode 100644 index 0000000000..442a39bad1 --- /dev/null +++ b/server/services/ewelink/lib/params/apikey.param.js @@ -0,0 +1,6 @@ +const { DEVICE_PARAMS } = require('../utils/constants'); + +module.exports = { + EWELINK_KEY_PATH: 'apikey', + GLADYS_PARAM_KEY: DEVICE_PARAMS.API_KEY, +}; diff --git a/server/services/ewelink/lib/params/firmware.param.js b/server/services/ewelink/lib/params/firmware.param.js index de586be0f9..f15a989097 100644 --- a/server/services/ewelink/lib/params/firmware.param.js +++ b/server/services/ewelink/lib/params/firmware.param.js @@ -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, }; diff --git a/server/services/ewelink/lib/params/index.js b/server/services/ewelink/lib/params/index.js index e16581b789..e5197f662f 100644 --- a/server/services/ewelink/lib/params/index.js +++ b/server/services/ewelink/lib/params/index.js @@ -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. diff --git a/server/services/ewelink/lib/params/online.param.js b/server/services/ewelink/lib/params/online.param.js index 884a4408f0..284ae015d7 100644 --- a/server/services/ewelink/lib/params/online.param.js +++ b/server/services/ewelink/lib/params/online.param.js @@ -1,3 +1,5 @@ +const { DEVICE_PARAMS } = require('../utils/constants'); + /** * @description Convert online state. * @param {boolean} rawValue - Param raw value. @@ -11,6 +13,6 @@ function convertValue(rawValue) { module.exports = { EWELINK_KEY_PATH: 'online', - GLADYS_PARAM_KEY: 'ONLINE', + GLADYS_PARAM_KEY: DEVICE_PARAMS.ONLINE, convertValue, }; diff --git a/server/services/ewelink/lib/user/ewelink.exchangeToken.js b/server/services/ewelink/lib/user/ewelink.exchangeToken.js index d9e0ce54f5..2413368721 100644 --- a/server/services/ewelink/lib/user/ewelink.exchangeToken.js +++ b/server/services/ewelink/lib/user/ewelink.exchangeToken.js @@ -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 }); diff --git a/server/services/ewelink/lib/user/ewelink.retrieveUserApiKey.js b/server/services/ewelink/lib/user/ewelink.retrieveUserApiKey.js new file mode 100644 index 0000000000..915dbb5a9a --- /dev/null +++ b/server/services/ewelink/lib/user/ewelink.retrieveUserApiKey.js @@ -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, +}; diff --git a/server/services/ewelink/lib/utils/constants.js b/server/services/ewelink/lib/utils/constants.js index 85c5431602..3b454525ad 100644 --- a/server/services/ewelink/lib/utils/constants.js +++ b/server/services/ewelink/lib/utils/constants.js @@ -3,9 +3,16 @@ const CONFIGURATION_KEYS = { APPLICATION_SECRET: 'APPLICATION_SECRET', APPLICATION_REGION: 'APPLICATION_REGION', USER_TOKENS: 'USER_TOKENS', + USER_API_KEY: 'USER_API_KEY', SERVICE_VERSION: 'SERVICE_VERSION', }; +const DEVICE_PARAMS = { + ONLINE: 'ONLINE', + FIRMWARE: 'FIRMWARE', + API_KEY: 'API_KEY', +}; + const DEVICE_SERVICE_ID = 'ewelink'; const DEVICE_EXTERNAL_ID_BASE = 'ewelink'; @@ -13,6 +20,7 @@ const NB_MAX_RETRY_EXPIRED = 1; module.exports = { CONFIGURATION_KEYS, + DEVICE_PARAMS, DEVICE_SERVICE_ID, DEVICE_EXTERNAL_ID_BASE, NB_MAX_RETRY_EXPIRED, diff --git a/server/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.js b/server/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.js index d1303823b5..9929433077 100644 --- a/server/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.js +++ b/server/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.js @@ -4,9 +4,8 @@ * this.closeWebSocketClient(); */ function closeWebSocketClient() { - if (this.ewelinkWebSocketClient !== null) { - this.ewelinkWebSocketClient.close(); - this.ewelinkWebSocketClient = null; + if (this.ewelinkWebSocketClient && this.ewelinkWebSocketClient.Connect && this.ewelinkWebSocketClient.Connect.ws) { + this.ewelinkWebSocketClient.Connect.ws.close(); } } diff --git a/server/services/ewelink/lib/websocket/ewelink.createWebSocketClient.js b/server/services/ewelink/lib/websocket/ewelink.createWebSocketClient.js index 9ca5444e1c..351359dae3 100644 --- a/server/services/ewelink/lib/websocket/ewelink.createWebSocketClient.js +++ b/server/services/ewelink/lib/websocket/ewelink.createWebSocketClient.js @@ -5,7 +5,8 @@ */ async function createWebSocketClient() { const { applicationId: appId, applicationRegion: region } = this.configuration; - this.ewelinkWebSocketClient = await this.ewelinkWebSocketClientFactory.Connect.create( + this.ewelinkWebSocketClient.userApiKey = this.userApiKey; + await this.ewelinkWebSocketClient.Connect.create( { appId, region, diff --git a/server/services/ewelink/lib/websocket/ewelink.onWebSocketClose.js b/server/services/ewelink/lib/websocket/ewelink.onWebSocketClose.js index a3fd3bf0fe..ccf2609b18 100644 --- a/server/services/ewelink/lib/websocket/ewelink.onWebSocketClose.js +++ b/server/services/ewelink/lib/websocket/ewelink.onWebSocketClose.js @@ -3,11 +3,15 @@ const logger = require('../../../../utils/logger'); /** * @description Action to execute when WebSocket is closed. * @example - * this.onWebSocketClose(); + * await this.onWebSocketClose(); */ -function onWebSocketClose() { - this.closeWebSocketClient(); - logger.info('eWeLink: WebSocket is closed'); +async function onWebSocketClose() { + logger.warn('eWeLink: WebSocket is closed'); + + // Try to reopen it if Gladys is not stopping + if (this.ewelinkWebSocketClient !== null) { + await this.createWebSocketClient(); + } } module.exports = { diff --git a/server/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.js b/server/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.js index 623fbb5fab..cf95b91e84 100644 --- a/server/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.js +++ b/server/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.js @@ -11,16 +11,30 @@ const { readParams } = require('../params'); /** * @description Action to execute when WebSocket receives a message. * @param {object} ws - Current WebSocket client. - * @param {object} message - WebSocket message. + * @param {object} message - WebSocket event message. * @example - * this.onWebSocketMessage(); + * await this.onWebSocketMessage(); */ async function onWebSocketMessage(ws, message) { - logger.debug('eWeLink: WebSocket received a message: %j', message); + const { data: rawData = '' } = message; + logger.debug('eWeLink: WebSocket received a message with data: %s', rawData); + let data = {}; + try { + data = JSON.parse(rawData); + } catch (e) { + logger.debug('eWeLink: WebSocket message is not a JSON object'); + } + + await this.handleResponse(data); + + const { deviceid, params = {} } = data; - await this.handleResponse(message); + // Message is not concerning a device + if (!deviceid) { + logger.debug('eWeLink: WebSocket message is not about a device, skipping it...'); + return; + } - const { deviceid = '', params = {} } = message; const externalId = getExternalId({ deviceid }); // Load device to update params @@ -33,13 +47,16 @@ async function onWebSocketMessage(ws, message) { states.forEach(({ featureExternalId, state }) => { // Before sending event, check if feature exists const feature = this.gladys.stateManager.get('deviceFeatureByExternalId', featureExternalId); - if (feature) { + if (!feature) { + logger.debug(`eWeLink: feature "${featureExternalId}" not found in Gladys`); + } else if (feature.last_value === state) { + // And check if value has really changed + logger.debug(`eWeLink: feature "${featureExternalId}" state already up-to-date`); + } else { this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { device_feature_external_id: featureExternalId, state, }); - } else { - logger.debug(`eWeLink: feature ${featureExternalId} not found in Gladys`); } }); diff --git a/server/test/services/ewelink/lib/config/ewelink.loadConfiguration.test.js b/server/test/services/ewelink/lib/config/ewelink.loadConfiguration.test.js index 95bc480b3e..cc95f5815a 100644 --- a/server/test/services/ewelink/lib/config/ewelink.loadConfiguration.test.js +++ b/server/test/services/ewelink/lib/config/ewelink.loadConfiguration.test.js @@ -1,9 +1,14 @@ const { expect } = require('chai'); const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); const { assert, fake, stub, match } = sinon; -const EwelinkHandler = require('../../../../../services/ewelink/lib'); +const retrieveUserApiKey = fake.resolves(null); + +const EwelinkHandler = proxyquire('../../../../../services/ewelink/lib', { + './user/ewelink.retrieveUserApiKey': { retrieveUserApiKey }, +}); const { SERVICE_ID } = require('../constants'); const { ServiceNotConfiguredError } = require('../../../../../utils/coreErrors'); const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants'); @@ -46,21 +51,22 @@ describe('eWeLinkHandler loadConfiguration', () => { } catch (e) { expect(e).instanceOf(ServiceNotConfiguredError); expect(e.message).is.eq('eWeLink configuration is not setup'); + } - assert.callCount(gladys.event.emit, 2); - assert.alwaysCalledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, - payload: { configured: false, connected: false }, - }); + assert.callCount(gladys.event.emit, 2); + assert.alwaysCalledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: { configured: false, connected: false }, + }); - assert.callCount(gladys.variable.getValue, 3); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_ID', SERVICE_ID); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_SECRET', SERVICE_ID); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); + assert.callCount(gladys.variable.getValue, 3); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_ID', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_SECRET', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); - assert.notCalled(eWeLinkApiMock.WebAPI); - assert.notCalled(eWeLinkApiMock.Ws); - } + assert.notCalled(eWeLinkApiMock.WebAPI); + assert.notCalled(eWeLinkApiMock.Ws); + assert.notCalled(retrieveUserApiKey); }); it('should throw a ServiceNotConfiguredError as only APPLICATION_ID variable is stored in database', async () => { @@ -75,21 +81,22 @@ describe('eWeLinkHandler loadConfiguration', () => { } catch (e) { expect(e).instanceOf(ServiceNotConfiguredError); expect(e.message).is.eq('eWeLink configuration is not setup'); + } - assert.callCount(gladys.event.emit, 2); - assert.alwaysCalledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, - payload: { configured: false, connected: false }, - }); + assert.callCount(gladys.event.emit, 2); + assert.alwaysCalledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: { configured: false, connected: false }, + }); - assert.callCount(gladys.variable.getValue, 3); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_ID', SERVICE_ID); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_SECRET', SERVICE_ID); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); + assert.callCount(gladys.variable.getValue, 3); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_ID', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_SECRET', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); - assert.notCalled(eWeLinkApiMock.WebAPI); - assert.notCalled(eWeLinkApiMock.Ws); - } + assert.notCalled(eWeLinkApiMock.WebAPI); + assert.notCalled(eWeLinkApiMock.Ws); + assert.notCalled(retrieveUserApiKey); }); it('should throw a ServiceNotConfiguredError as only APPLICATION_ID and APPLICATION_SECRET variable are stored in database', async () => { @@ -106,21 +113,22 @@ describe('eWeLinkHandler loadConfiguration', () => { } catch (e) { expect(e).instanceOf(ServiceNotConfiguredError); expect(e.message).is.eq('eWeLink configuration is not setup'); + } - assert.callCount(gladys.event.emit, 2); - assert.alwaysCalledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, - payload: { configured: false, connected: false }, - }); + assert.callCount(gladys.event.emit, 2); + assert.alwaysCalledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: { configured: false, connected: false }, + }); - assert.callCount(gladys.variable.getValue, 3); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_ID', SERVICE_ID); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_SECRET', SERVICE_ID); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); + assert.callCount(gladys.variable.getValue, 3); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_ID', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_SECRET', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); - assert.notCalled(eWeLinkApiMock.WebAPI); - assert.notCalled(eWeLinkApiMock.Ws); - } + assert.notCalled(eWeLinkApiMock.WebAPI); + assert.notCalled(eWeLinkApiMock.Ws); + assert.notCalled(retrieveUserApiKey); }); it('should throw a ServiceNotConfiguredError as USER_TOKENS variable is missing in database', async () => { @@ -139,36 +147,37 @@ describe('eWeLinkHandler loadConfiguration', () => { } catch (e) { expect(e).instanceOf(ServiceNotConfiguredError); expect(e.message).is.eq('eWeLink user is not connected'); - - assert.callCount(gladys.event.emit, 2); - assert.calledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, - payload: { configured: false, connected: false }, - }); - assert.calledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, - payload: { configured: true, connected: false }, - }); - - assert.callCount(gladys.variable.getValue, 4); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_ID', SERVICE_ID); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_SECRET', SERVICE_ID); - assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); - assert.calledWithExactly(gladys.variable.getValue, 'USER_TOKENS', SERVICE_ID); - - assert.calledOnceWithExactly(eWeLinkApiMock.WebAPI, { - appId: 'APPLICATION_ID_VALUE', - appSecret: 'APPLICATION_SECRET_VALUE', - region: 'APPLICATION_REGION_VALUE', - }); - assert.calledOnceWithExactly(eWeLinkApiMock.Ws, { - appId: 'APPLICATION_ID_VALUE', - appSecret: 'APPLICATION_SECRET_VALUE', - region: 'APPLICATION_REGION_VALUE', - }); - - assert.notCalled(eWeLinkWsMock.prototype.Connect.create); } + + assert.callCount(gladys.event.emit, 2); + assert.calledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: { configured: false, connected: false }, + }); + assert.calledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: { configured: true, connected: false }, + }); + + assert.callCount(gladys.variable.getValue, 4); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_ID', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_SECRET', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'USER_TOKENS', SERVICE_ID); + + assert.calledOnceWithExactly(eWeLinkApiMock.WebAPI, { + appId: 'APPLICATION_ID_VALUE', + appSecret: 'APPLICATION_SECRET_VALUE', + region: 'APPLICATION_REGION_VALUE', + }); + assert.calledOnceWithExactly(eWeLinkApiMock.Ws, { + appId: 'APPLICATION_ID_VALUE', + appSecret: 'APPLICATION_SECRET_VALUE', + region: 'APPLICATION_REGION_VALUE', + }); + + assert.notCalled(eWeLinkWsMock.prototype.Connect.create); + assert.notCalled(retrieveUserApiKey); }); it('should be well configured', async () => { @@ -180,7 +189,9 @@ describe('eWeLinkHandler loadConfiguration', () => { .resolves('APPLICATION_SECRET_VALUE') .onThirdCall() .resolves('APPLICATION_REGION_VALUE') - .resolves('{ "accessToken": "ACCESS_TOKEN", "refreshToken": "REFRESH_TOKEN" }'); + .onCall(3) + .resolves('{ "accessToken": "ACCESS_TOKEN", "refreshToken": "REFRESH_TOKEN" }') + .resolves('API_KEY'); await eWeLinkHandler.loadConfiguration(); @@ -194,11 +205,74 @@ describe('eWeLinkHandler loadConfiguration', () => { payload: { configured: true, connected: true }, }); - assert.callCount(gladys.variable.getValue, 4); + assert.callCount(gladys.variable.getValue, 5); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_ID', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_SECRET', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'USER_TOKENS', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'USER_API_KEY', SERVICE_ID); + + assert.notCalled(retrieveUserApiKey); + + assert.calledOnceWithExactly(eWeLinkApiMock.WebAPI, { + appId: 'APPLICATION_ID_VALUE', + appSecret: 'APPLICATION_SECRET_VALUE', + region: 'APPLICATION_REGION_VALUE', + }); + assert.calledOnceWithExactly(eWeLinkApiMock.Ws, { + appId: 'APPLICATION_ID_VALUE', + appSecret: 'APPLICATION_SECRET_VALUE', + region: 'APPLICATION_REGION_VALUE', + }); + + assert.calledOnce(eWeLinkWsMock.prototype.Connect.create); + assert.calledWithMatch( + eWeLinkWsMock.prototype.Connect.create, + match({ appId: 'APPLICATION_ID_VALUE', region: 'APPLICATION_REGION_VALUE', at: 'ACCESS_TOKEN' }), + match.func, + match.func, + match.func, + match.func, + ); + + expect(eWeLinkHandler.ewelinkWebAPIClient.at).eq('ACCESS_TOKEN'); + expect(eWeLinkHandler.ewelinkWebAPIClient.rt).eq('REFRESH_TOKEN'); + expect(eWeLinkHandler.ewelinkWebSocketClient.userApiKey).eq('API_KEY'); + }); + + it('should retreive API key', async () => { + gladys.variable.getValue = sinon + .stub() + .onFirstCall() + .resolves('APPLICATION_ID_VALUE') + .onSecondCall() + .resolves('APPLICATION_SECRET_VALUE') + .onThirdCall() + .resolves('APPLICATION_REGION_VALUE') + .onCall(3) + .resolves('{ "accessToken": "ACCESS_TOKEN", "refreshToken": "REFRESH_TOKEN" }') + .resolves(null); + + await eWeLinkHandler.loadConfiguration(); + + assert.callCount(gladys.event.emit, 2); + assert.calledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: { configured: false, connected: false }, + }); + assert.calledWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: { configured: true, connected: true }, + }); + + assert.callCount(gladys.variable.getValue, 5); assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_ID', SERVICE_ID); assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_SECRET', SERVICE_ID); assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); assert.calledWithExactly(gladys.variable.getValue, 'USER_TOKENS', SERVICE_ID); + assert.calledWithExactly(gladys.variable.getValue, 'USER_API_KEY', SERVICE_ID); + + assert.calledOnceWithExactly(retrieveUserApiKey); assert.calledOnceWithExactly(eWeLinkApiMock.WebAPI, { appId: 'APPLICATION_ID_VALUE', diff --git a/server/test/services/ewelink/lib/config/ewelink.saveConfiguration.test.js b/server/test/services/ewelink/lib/config/ewelink.saveConfiguration.test.js index 6111dc12f7..8784c430e8 100644 --- a/server/test/services/ewelink/lib/config/ewelink.saveConfiguration.test.js +++ b/server/test/services/ewelink/lib/config/ewelink.saveConfiguration.test.js @@ -49,7 +49,7 @@ describe('eWeLinkHandler saveConfiguration', () => { assert.notCalled(gladys.variable.destroy); expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); - expect(eWeLinkHandler.ewelinkWebSocketClientFactory).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); }); @@ -67,7 +67,7 @@ describe('eWeLinkHandler saveConfiguration', () => { assert.notCalled(gladys.variable.destroy); expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); - expect(eWeLinkHandler.ewelinkWebSocketClientFactory).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); }); @@ -85,7 +85,7 @@ describe('eWeLinkHandler saveConfiguration', () => { assert.notCalled(gladys.variable.destroy); expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); - expect(eWeLinkHandler.ewelinkWebSocketClientFactory).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); }); @@ -113,7 +113,7 @@ describe('eWeLinkHandler saveConfiguration', () => { assert.notCalled(gladys.variable.destroy); expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); - expect(eWeLinkHandler.ewelinkWebSocketClientFactory).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); }); @@ -141,7 +141,6 @@ describe('eWeLinkHandler saveConfiguration', () => { assert.calledOnceWithExactly(gladys.variable.destroy, 'USER_TOKENS', SERVICE_ID); expect(eWeLinkHandler.ewelinkWebAPIClient).not.eq(null); - expect(eWeLinkHandler.ewelinkWebSocketClientFactory).not.eq(null); - expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).not.eq(null); }); }); diff --git a/server/test/services/ewelink/lib/device/discover.test.js b/server/test/services/ewelink/lib/device/discover.test.js index 357af47941..e12f6299fe 100644 --- a/server/test/services/ewelink/lib/device/discover.test.js +++ b/server/test/services/ewelink/lib/device/discover.test.js @@ -47,17 +47,28 @@ describe('EweLinkHandler discover', () => { sinon.reset(); }); - it('should found 3 devices, 2 of wich are already in Gladys and 3 are a new unknown device', async () => { + it('should found 5 devices, 2 of wich are already in Gladys and 3 are a new unknown device', async () => { const newDevices = await eWeLinkHandler.discover(); - expect(newDevices.length).to.equal(3); - expect(newDevices).to.have.deep.members([GladysOfflineDevice, GladysThDevice, GladysUnhandledDevice]); + expect(newDevices.length).to.equal(5); + expect(newDevices).to.have.deep.members([ + { ...Gladys2ChDevice, room_id: undefined, updatable: false }, + GladysOfflineDevice, + { ...GladysPowDevice, room_id: undefined, updatable: false }, + GladysThDevice, + GladysUnhandledDevice, + ]); + + expect(eWeLinkHandler.discoveredDevices).to.deep.eq(newDevices); }); + it('should found 0 devices', async () => { // Force eWeLink API to give empty response sinon.stub(eWeLinkHandler.ewelinkWebAPIClient.device, 'getAllThingsAllPages').resolves({ error: 0, data: {} }); const newDevices = await eWeLinkHandler.discover(); expect(newDevices).to.have.deep.members([]); + expect(eWeLinkHandler.discoveredDevices).to.deep.eq([]); }); + it('should return not configured error', async () => { eWeLinkHandler.ewelinkWebAPIClient.at = EWELINK_INVALID_ACCESS_TOKEN; try { @@ -66,8 +77,10 @@ describe('EweLinkHandler discover', () => { } catch (error) { expect(error).instanceOf(ServiceNotConfiguredError); expect(error.message).to.equal('eWeLink: Error, service is not configured'); + expect(eWeLinkHandler.discoveredDevices).to.deep.eq([]); } }); + it('should throw an error and emit a message when AccessToken is no more valid', async () => { eWeLinkHandler.ewelinkWebAPIClient.at = EWELINK_DENIED_ACCESS_TOKEN; try { @@ -76,6 +89,7 @@ describe('EweLinkHandler discover', () => { } catch (error) { expect(error).instanceOf(Error); expect(error.message).to.equal('eWeLink: Authentication error'); + expect(eWeLinkHandler.discoveredDevices).to.deep.eq([]); } }); }); diff --git a/server/test/services/ewelink/lib/device/setValue.test.js b/server/test/services/ewelink/lib/device/setValue.test.js index 84269eef3a..f0f7e24775 100644 --- a/server/test/services/ewelink/lib/device/setValue.test.js +++ b/server/test/services/ewelink/lib/device/setValue.test.js @@ -1,12 +1,9 @@ -const { expect } = require('chai'); const sinon = require('sinon'); -const { NotFoundError } = require('../../../../../utils/coreErrors'); const EwelinkHandler = require('../../../../../services/ewelink/lib'); -const { SERVICE_ID, EWELINK_DENIED_ACCESS_TOKEN } = require('../constants'); +const { SERVICE_ID } = require('../constants'); const Gladys2ChDevice = require('../payloads/Gladys-2ch.json'); -const GladysOfflineDevice = require('../payloads/Gladys-offline.json'); const GladysPowDevice = require('../payloads/Gladys-pow.json'); const EweLinkApiMock = require('../ewelink-api.mock.test'); @@ -14,12 +11,12 @@ const { assert } = sinon; describe('eWeLinkHandler setValue', () => { let eWeLinkHandler; - const functionToTest = sinon.spy(EweLinkApiMock.Device.prototype, 'setThingStatus'); + const functionToTest = sinon.spy(EweLinkApiMock.Connect.prototype, 'updateState'); beforeEach(() => { const gladys = {}; eWeLinkHandler = new EwelinkHandler(gladys, EweLinkApiMock, SERVICE_ID); - eWeLinkHandler.ewelinkWebAPIClient = new EweLinkApiMock.WebAPI(); + eWeLinkHandler.ewelinkWebSocketClient = new EweLinkApiMock.Ws(); eWeLinkHandler.status = { configured: true, connected: true }; }); @@ -27,22 +24,24 @@ describe('eWeLinkHandler setValue', () => { sinon.reset(); }); - it('should set the binary value of the channel 1 of the "2CH" device to 1', async () => { + it('should set the binary value of the single channel of the "pow" device to 1', async () => { await eWeLinkHandler.setValue( GladysPowDevice, { external_id: 'ewelink:10004533ae:power:1', category: 'switch', type: 'binary' }, 1, ); - assert.calledOnceWithExactly(functionToTest, 1, '10004533ae', { switch: 'on' }); + assert.calledOnceWithExactly(functionToTest, '10004533ae', { switch: 'on' }); }); + it('should set the binary value of the channel 2 of the "2CH" device to 0', async () => { await eWeLinkHandler.setValue( Gladys2ChDevice, { external_id: 'ewelink:10004531ae:power:2', category: 'switch', type: 'binary' }, 0, ); - assert.calledOnceWithExactly(functionToTest, 1, '10004531ae', { switches: [{ outlet: 2, switch: 'off' }] }); + assert.calledOnceWithExactly(functionToTest, '10004531ae', { switches: [{ outlet: 1, switch: 'off' }] }); }); + it('should do nothing because of the feature type is not handled yet', async () => { await eWeLinkHandler.setValue( GladysPowDevice, @@ -51,33 +50,4 @@ describe('eWeLinkHandler setValue', () => { ); assert.notCalled(functionToTest); }); - it('should throw an error when device is offline', async () => { - try { - await eWeLinkHandler.setValue( - GladysOfflineDevice, - { external_id: 'ewelink:10004532ae:power:1', category: 'switch', type: 'binary' }, - 1, - ); - assert.fail(); - } catch (error) { - assert.calledOnceWithExactly(functionToTest, 1, '10004532ae', { switch: 'on' }); - expect(error).instanceOf(NotFoundError); - expect(error.message).to.equal('eWeLink: Error, device is not currently online'); - } - }); - it('should throw an error when AccessToken is no more valid', async () => { - eWeLinkHandler.ewelinkWebAPIClient.at = EWELINK_DENIED_ACCESS_TOKEN; - try { - await eWeLinkHandler.setValue( - Gladys2ChDevice, - { external_id: 'ewelink:10004531ae:power:2', category: 'switch', type: 'binary' }, - 1, - ); - assert.fail(); - } catch (error) { - assert.calledOnceWithExactly(functionToTest, 1, '10004531ae', { switches: [{ outlet: 2, switch: 'on' }] }); - expect(error).instanceOf(Error); - expect(error.message).to.equal('eWeLink: Authentication error'); - } - }); }); diff --git a/server/test/services/ewelink/lib/ewelink-api.mock.test.js b/server/test/services/ewelink/lib/ewelink-api.mock.test.js index 6e1d84d401..ba8b8227bb 100644 --- a/server/test/services/ewelink/lib/ewelink-api.mock.test.js +++ b/server/test/services/ewelink/lib/ewelink-api.mock.test.js @@ -12,6 +12,7 @@ const { EWELINK_VALID_ACCESS_TOKEN, EWELINK_INVALID_ACCESS_TOKEN, } = require('./constants'); +const logger = require('../../../../utils/logger'); const fakeDevices = [EweLink2ChDevice, EweLinkOfflineDevice, EweLinkPowDevice, EweLinkThDevice, EweLinkUnhandledDevice]; @@ -67,11 +68,6 @@ class Device { return response; } - - async setThingStatus(type, id, params) { - const deviceResponse = await this.getThings({ thingList: [{ id }] }); - return { ...deviceResponse, data: {} }; - } } class WebAPI { @@ -93,6 +89,18 @@ class Connect { this.create = () => {}; } + + updateState(deviceId, params, action, userAgent, userApiKey) { + logger.debug( + 'update state with user token %s and deviceId=%s - param=%j - action=%s - userAgent=%s - apiKey=%s', + this.root.at, + deviceId, + params, + action, + userAgent, + userApiKey, + ); + } } class Ws { diff --git a/server/test/services/ewelink/lib/features/binary.test.js b/server/test/services/ewelink/lib/features/binary.test.js index 630577df5e..264669fb23 100644 --- a/server/test/services/ewelink/lib/features/binary.test.js +++ b/server/test/services/ewelink/lib/features/binary.test.js @@ -16,8 +16,8 @@ describe('eWeLink binary feature -> readState', () => { it('should return switches states', () => { const states = readStates('ewelink-id', { switches: [ - { switch: 'on', outlet: 1 }, - { switch: 'off', outlet: 2 }, + { switch: 'on', outlet: 0 }, + { switch: 'off', outlet: 1 }, ], }); expect(states).deep.eq([ diff --git a/server/test/services/ewelink/lib/handlers/ewelink.handleRequest.test.js b/server/test/services/ewelink/lib/handlers/ewelink.handleRequest.test.js index c4f167f751..4dbd195885 100644 --- a/server/test/services/ewelink/lib/handlers/ewelink.handleRequest.test.js +++ b/server/test/services/ewelink/lib/handlers/ewelink.handleRequest.test.js @@ -5,7 +5,7 @@ const { stub, assert } = sinon; const EwelinkHandler = require('../../../../../services/ewelink/lib'); const { SERVICE_ID } = require('../constants'); -const { ServiceNotConfiguredError, BadParameters } = require('../../../../../utils/coreErrors'); +const { ServiceNotConfiguredError, BadParameters, NotFoundError } = require('../../../../../utils/coreErrors'); const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants'); const tokens = { accessToken: 'ACCESS_TOKEN', refreshToken: 'REFRESH_TOKEN' }; @@ -168,4 +168,40 @@ describe('eWeLinkHandler handleRequest', () => { assert.notCalled(gladys.variable.destroy); assert.notCalled(gladys.event.emit); }); + + it('should throws NotFoundError on 405 error', async () => { + const request = stub().resolves({ data: 'ERROR', error: 405, msg: 'API ERROR' }); + + try { + await eWeLinkHandler.handleRequest(request); + assert.fail(); + } catch (e) { + expect(e).instanceOf(NotFoundError); + expect(e.message).to.equal('API ERROR'); + } + + assert.calledOnceWithExactly(request); + assert.notCalled(eWeLinkHandler.ewelinkWebAPIClient.user.refreshToken); + assert.notCalled(gladys.variable.setValue); + assert.notCalled(gladys.variable.destroy); + assert.notCalled(gladys.event.emit); + }); + + it('should throws NotFoundError on 4002 error', async () => { + const request = stub().resolves({ data: 'ERROR', error: 4002, msg: 'API ERROR' }); + + try { + await eWeLinkHandler.handleRequest(request); + assert.fail(); + } catch (e) { + expect(e).instanceOf(NotFoundError); + expect(e.message).to.equal('API ERROR'); + } + + assert.calledOnceWithExactly(request); + assert.notCalled(eWeLinkHandler.ewelinkWebAPIClient.user.refreshToken); + assert.notCalled(gladys.variable.setValue); + assert.notCalled(gladys.variable.destroy); + assert.notCalled(gladys.event.emit); + }); }); diff --git a/server/test/services/ewelink/lib/payloads/Gladys-2ch.json b/server/test/services/ewelink/lib/payloads/Gladys-2ch.json index 3267189b09..78fd936ec4 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-2ch.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-2ch.json @@ -37,6 +37,10 @@ { "name": "ONLINE", "value": "1" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/lib/payloads/Gladys-Basic.json b/server/test/services/ewelink/lib/payloads/Gladys-Basic.json index 896a6f51cc..d63e643a0b 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-Basic.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-Basic.json @@ -37,6 +37,10 @@ { "name": "ONLINE", "value": "0" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/lib/payloads/Gladys-offline.json b/server/test/services/ewelink/lib/payloads/Gladys-offline.json index 1ecd98bb95..120baf9322 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-offline.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-offline.json @@ -10,6 +10,10 @@ { "name": "ONLINE", "value": "0" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/lib/payloads/Gladys-pow.json b/server/test/services/ewelink/lib/payloads/Gladys-pow.json index 7664064c48..464ef899be 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-pow.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-pow.json @@ -26,6 +26,10 @@ { "name": "ONLINE", "value": "1" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/lib/payloads/Gladys-th.json b/server/test/services/ewelink/lib/payloads/Gladys-th.json index 03f8e821e0..3c31b20c3b 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-th.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-th.json @@ -50,6 +50,10 @@ { "name": "ONLINE", "value": "1" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/lib/payloads/Gladys-unhandled.json b/server/test/services/ewelink/lib/payloads/Gladys-unhandled.json index a06f24770e..1f4462cda6 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-unhandled.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-unhandled.json @@ -10,6 +10,10 @@ { "name": "ONLINE", "value": "1" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/lib/user/ewelink.deleteTokens.test.js b/server/test/services/ewelink/lib/user/ewelink.deleteTokens.test.js index 1cf0c5ce10..8baf7223ff 100644 --- a/server/test/services/ewelink/lib/user/ewelink.deleteTokens.test.js +++ b/server/test/services/ewelink/lib/user/ewelink.deleteTokens.test.js @@ -29,6 +29,7 @@ describe('eWeLinkHandler deleteTokens', () => { delete: fake.resolves({}), }, }; + eWeLinkHandler.ewelinkWebSocketClient = {}; eWeLinkHandler.status = { configured: true, connected: true }; }); diff --git a/server/test/services/ewelink/lib/user/ewelink.exchangeToken.test.js b/server/test/services/ewelink/lib/user/ewelink.exchangeToken.test.js index 214ed0f785..49c1b64964 100644 --- a/server/test/services/ewelink/lib/user/ewelink.exchangeToken.test.js +++ b/server/test/services/ewelink/lib/user/ewelink.exchangeToken.test.js @@ -1,9 +1,14 @@ const { expect } = require('chai'); const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); const { fake, assert, match } = sinon; -const EwelinkHandler = require('../../../../../services/ewelink/lib'); +const retrieveUserApiKey = fake.resolves(null); + +const EwelinkHandler = proxyquire('../../../../../services/ewelink/lib', { + './user/ewelink.retrieveUserApiKey': { retrieveUserApiKey }, +}); const { SERVICE_ID, EWELINK_APP_ID, EWELINK_APP_REGION } = require('../constants'); const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants'); const { BadParameters } = require('../../../../../utils/coreErrors'); @@ -30,7 +35,7 @@ describe('eWeLinkHandler exchangeToken', () => { getToken: fake.resolves({ data: tokens }), }, }; - eWeLinkHandler.ewelinkWebSocketClientFactory = { + eWeLinkHandler.ewelinkWebSocketClient = { Connect: { create: fake.returns({}), }, @@ -65,12 +70,13 @@ describe('eWeLinkHandler exchangeToken', () => { } assert.notCalled(eWeLinkHandler.ewelinkWebAPIClient.oauth.getToken); - assert.notCalled(eWeLinkHandler.ewelinkWebSocketClientFactory.Connect.create); + assert.notCalled(retrieveUserApiKey); + assert.notCalled(eWeLinkHandler.ewelinkWebSocketClient.Connect.create); assert.notCalled(gladys.variable.setValue); assert.notCalled(gladys.event.emit); }); - it('should retreive, store user token and emit event', async () => { + it('should retreive, store user token and API key and emit event', async () => { const redirectUrl = 'http://localhost:1440'; const code = 'auth_code'; const region = 'app_region'; @@ -83,9 +89,10 @@ describe('eWeLinkHandler exchangeToken', () => { redirectUrl, region, }); - assert.calledOnce(eWeLinkHandler.ewelinkWebSocketClientFactory.Connect.create); + assert.calledOnceWithExactly(retrieveUserApiKey); + assert.calledOnce(eWeLinkHandler.ewelinkWebSocketClient.Connect.create); assert.calledWithMatch( - eWeLinkHandler.ewelinkWebSocketClientFactory.Connect.create, + eWeLinkHandler.ewelinkWebSocketClient.Connect.create, match({ appId: EWELINK_APP_ID, region: EWELINK_APP_REGION, at: tokens.accessToken }), match.func, match.func, diff --git a/server/test/services/ewelink/lib/user/ewelink.retrieveUserApiKey.test.js b/server/test/services/ewelink/lib/user/ewelink.retrieveUserApiKey.test.js new file mode 100644 index 0000000000..26deea0601 --- /dev/null +++ b/server/test/services/ewelink/lib/user/ewelink.retrieveUserApiKey.test.js @@ -0,0 +1,69 @@ +const sinon = require('sinon'); + +const { fake, assert } = sinon; + +const { expect } = require('chai'); +const EwelinkHandler = require('../../../../../services/ewelink/lib'); +const { SERVICE_ID } = require('../constants'); +const { ServiceNotConfiguredError } = require('../../../../../utils/coreErrors'); + +describe('eWeLinkHandler retrieveUserApiKey', () => { + let eWeLinkHandler; + let gladys; + + beforeEach(() => { + gladys = { + variable: { + setValue: fake.resolves(null), + }, + }; + + eWeLinkHandler = new EwelinkHandler(gladys, null, SERVICE_ID); + eWeLinkHandler.ewelinkWebAPIClient = { + home: { + getFamily: fake.resolves({ + data: { currentFamilyId: 'current-family', familyList: [{ id: 'current-family', apikey: 'USER-API-KEY' }] }, + }), + }, + }; + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should throw an error as API key is not found', async () => { + eWeLinkHandler.ewelinkWebAPIClient = { + home: { + getFamily: fake.resolves({ + data: { + currentFamilyId: 'current-family', + familyList: [{ id: 'not-current-family', apikey: 'USER-API-KEY' }], + }, + }), + }, + }; + + try { + await eWeLinkHandler.retrieveUserApiKey(); + assert.fail(); + } catch (e) { + expect(e).instanceOf(ServiceNotConfiguredError); + expect(e.message).to.eq('eWeLink: no user API key retrieved'); + } + + assert.calledOnceWithExactly(eWeLinkHandler.ewelinkWebAPIClient.home.getFamily); + assert.notCalled(gladys.variable.setValue); + }); + + it('should store user API key in database', async () => { + // Check value is not already set + expect(eWeLinkHandler.userApiKey).to.eq(null); + + await eWeLinkHandler.retrieveUserApiKey(); + + assert.calledOnceWithExactly(eWeLinkHandler.ewelinkWebAPIClient.home.getFamily); + assert.calledOnceWithExactly(gladys.variable.setValue, 'USER_API_KEY', 'USER-API-KEY', SERVICE_ID); + expect(eWeLinkHandler.userApiKey).to.eq('USER-API-KEY'); + }); +}); diff --git a/server/test/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.test.js b/server/test/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.test.js index df878a5585..30e6264f6f 100644 --- a/server/test/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.test.js +++ b/server/test/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.test.js @@ -11,6 +11,9 @@ describe('eWeLinkHandler closeWebSocketClient', () => { beforeEach(() => { eWeLinkHandler = new EwelinkHandler({}, null, SERVICE_ID); + eWeLinkHandler.ewelinkWebSocketClient = { + Connect: {}, + }; }); afterEach(() => { @@ -19,20 +22,18 @@ describe('eWeLinkHandler closeWebSocketClient', () => { it('should do nothing', async () => { // Check client is not set first - expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient.ws).eq(undefined); eWeLinkHandler.closeWebSocketClient(); - expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient.ws).eq(undefined); }); it('should close websocket client', async () => { - const wsClient = { + eWeLinkHandler.ewelinkWebSocketClient.Connect.ws = { close: fake.resolves(null), }; - eWeLinkHandler.ewelinkWebSocketClient = wsClient; eWeLinkHandler.closeWebSocketClient(); - assert.calledOnceWithExactly(wsClient.close); - expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + assert.calledOnceWithExactly(eWeLinkHandler.ewelinkWebSocketClient.Connect.ws.close); }); }); diff --git a/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketClose.test.js b/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketClose.test.js index 6af3c50f60..c86ae29eb7 100644 --- a/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketClose.test.js +++ b/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketClose.test.js @@ -4,16 +4,16 @@ const proxyquire = require('proxyquire').noCallThru(); const { fake, assert } = sinon; const logger = { - info: fake.returns(null), + warn: fake.returns(null), }; -const closeWebSocketClient = fake.returns(null); +const createWebSocketClient = fake.returns(null); const onWebSocketClose = proxyquire('../../../../../services/ewelink/lib/websocket/ewelink.onWebSocketClose', { '../../../../utils/logger': logger, }); const EwelinkHandler = proxyquire('../../../../../services/ewelink/lib', { './websocket/ewelink.onWebSocketClose': onWebSocketClose, - './websocket/ewelink.closeWebSocketClient': { closeWebSocketClient }, + './websocket/ewelink.createWebSocketClient': { createWebSocketClient }, }); const { SERVICE_ID } = require('../constants'); @@ -28,9 +28,17 @@ describe('eWeLinkHandler onWebSocketClose', () => { sinon.reset(); }); - it('should log info and close websocket', () => { - eWeLinkHandler.onWebSocketClose(); - assert.calledOnceWithExactly(logger.info, 'eWeLink: WebSocket is closed'); - assert.calledOnceWithExactly(closeWebSocketClient); + it('should only log warn', async () => { + await eWeLinkHandler.onWebSocketClose(); + assert.calledOnceWithExactly(logger.warn, 'eWeLink: WebSocket is closed'); + assert.notCalled(createWebSocketClient); + }); + + it('should log warn and close websocket', async () => { + eWeLinkHandler.ewelinkWebSocketClient = {}; + + await eWeLinkHandler.onWebSocketClose(); + assert.calledOnceWithExactly(logger.warn, 'eWeLink: WebSocket is closed'); + assert.calledOnceWithExactly(createWebSocketClient); }); }); diff --git a/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.test.js b/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.test.js index 1dd8fd6b50..bc096155a9 100644 --- a/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.test.js +++ b/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.test.js @@ -35,8 +35,11 @@ describe('eWeLinkHandler onWebSocketMessage', () => { it('should do nothing, device is not found', async () => { gladys.stateManager.get.returns(null); - const message = { deviceid: 'unknown-device' }; - await eWeLinkHandler.onWebSocketMessage(null, message); + const eventMessage = { + data: JSON.stringify({ deviceid: 'unknown-device' }), + }; + + await eWeLinkHandler.onWebSocketMessage(null, eventMessage); assert.calledOnceWithExactly(gladys.stateManager.get, 'deviceByExternalId', 'ewelink:unknown-device'); assert.notCalled(gladys.event.emit); @@ -48,8 +51,11 @@ describe('eWeLinkHandler onWebSocketMessage', () => { it('should do nothing, feature not exists', async () => { gladys.stateManager.get.onSecondCall().returns(null); - const message = { deviceid: 'known-device', params: { switch: 'on' } }; - await eWeLinkHandler.onWebSocketMessage(null, message); + const eventMessage = { + data: JSON.stringify({ deviceid: 'known-device', params: { switch: 'on' } }), + }; + + await eWeLinkHandler.onWebSocketMessage(null, eventMessage); assert.callCount(gladys.stateManager.get, 2); assert.calledWithExactly(gladys.stateManager.get, 'deviceByExternalId', 'ewelink:known-device'); @@ -62,8 +68,14 @@ describe('eWeLinkHandler onWebSocketMessage', () => { }); it('should emit state event', async () => { - const message = { deviceid: 'known-device', params: { switch: 'on', currentTemperature: 17, currentHumidity: 23 } }; - await eWeLinkHandler.onWebSocketMessage(null, message); + const eventMessage = { + data: JSON.stringify({ + deviceid: 'known-device', + params: { switch: 'on', currentTemperature: 17, currentHumidity: 23 }, + }), + }; + + await eWeLinkHandler.onWebSocketMessage(null, eventMessage); assert.callCount(gladys.stateManager.get, 4); assert.calledWithExactly(gladys.stateManager.get, 'deviceByExternalId', 'ewelink:known-device'); @@ -91,8 +103,11 @@ describe('eWeLinkHandler onWebSocketMessage', () => { }); it('should update device params', async () => { - const message = { deviceid: 'known-device', params: { online: true } }; - await eWeLinkHandler.onWebSocketMessage(null, message); + const eventMessage = { + data: JSON.stringify({ deviceid: 'known-device', params: { online: true } }), + }; + + await eWeLinkHandler.onWebSocketMessage(null, eventMessage); assert.calledOnceWithExactly(gladys.stateManager.get, 'deviceByExternalId', 'ewelink:known-device'); @@ -101,4 +116,28 @@ describe('eWeLinkHandler onWebSocketMessage', () => { assert.calledOnceWithExactly(gladys.device.setParam, device, 'ONLINE', '1'); expect(device).deep.eq({ params: [{ name: 'ONLINE', value: '1' }] }); }); + + it('should ignore non JSON data', async () => { + const eventMessage = { + data: 'pong', + }; + + await eWeLinkHandler.onWebSocketMessage(null, eventMessage); + + assert.notCalled(gladys.stateManager.get); + assert.notCalled(gladys.event.emit); + assert.notCalled(gladys.device.setParam); + }); + + it('should ignore not device oriented data', async () => { + const eventMessage = { + data: JSON.stringify({}), + }; + + await eWeLinkHandler.onWebSocketMessage(null, eventMessage); + + assert.notCalled(gladys.stateManager.get); + assert.notCalled(gladys.event.emit); + assert.notCalled(gladys.device.setParam); + }); });