diff --git a/front/src/routes/integration/all/ewelink/EweLinkDeviceBox.jsx b/front/src/routes/integration/all/ewelink/EweLinkDeviceBox.jsx index 96faf9f9f5..03c7dcf9b4 100644 --- a/front/src/routes/integration/all/ewelink/EweLinkDeviceBox.jsx +++ b/front/src/routes/integration/all/ewelink/EweLinkDeviceBox.jsx @@ -69,7 +69,7 @@ class EweLinkDeviceBox extends Component { ) { 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; + const firmware = (device.params.find(param => param.name === DEVICE_FIRMWARE) || { value: '?.?.?' }).value; return (
diff --git a/server/services/ewelink/lib/config/ewelink.createClients.js b/server/services/ewelink/lib/config/ewelink.createClients.js index bff1d45759..68287775cd 100644 --- a/server/services/ewelink/lib/config/ewelink.createClients.js +++ b/server/services/ewelink/lib/config/ewelink.createClients.js @@ -11,6 +11,12 @@ function createClients() { appSecret: applicationSecret, region: applicationRegion, }); + + this.ewelinkWebSocketClientFactory = new this.eweLinkApi.Ws({ + appId: applicationId, + appSecret: applicationSecret, + region: applicationRegion, + }); } module.exports = { diff --git a/server/services/ewelink/lib/config/ewelink.loadConfiguration.js b/server/services/ewelink/lib/config/ewelink.loadConfiguration.js index a834f2a10d..6a47c9aaa5 100644 --- a/server/services/ewelink/lib/config/ewelink.loadConfiguration.js +++ b/server/services/ewelink/lib/config/ewelink.loadConfiguration.js @@ -42,9 +42,12 @@ async function loadConfiguration() { } const tokenObject = JSON.parse(tokens); + this.ewelinkWebAPIClient.at = tokenObject.accessToken; this.ewelinkWebAPIClient.rt = tokenObject.refreshToken; + await this.createWebSocketClient(); + logger.info('eWeLink: stored configuration well loaded...'); } catch (e) { this.updateStatus({ configured: true, connected: false }); diff --git a/server/services/ewelink/lib/config/ewelink.saveConfiguration.js b/server/services/ewelink/lib/config/ewelink.saveConfiguration.js index 6bd363abd0..78384162a5 100644 --- a/server/services/ewelink/lib/config/ewelink.saveConfiguration.js +++ b/server/services/ewelink/lib/config/ewelink.saveConfiguration.js @@ -28,6 +28,7 @@ async function saveConfiguration({ applicationId = '', applicationSecret = '', a this.configuration = { applicationId, applicationSecret, applicationRegion }; + this.closeWebSocketClient(); this.createClients(); this.updateStatus({ configured: true, connected: false }); diff --git a/server/services/ewelink/lib/device/poll.js b/server/services/ewelink/lib/device/poll.js deleted file mode 100644 index 6813c4a5e3..0000000000 --- a/server/services/ewelink/lib/device/poll.js +++ /dev/null @@ -1,88 +0,0 @@ -const { EVENTS, DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); -const { NotFoundError } = require('../../../../utils/coreErrors'); -const { getDeviceParam, setDeviceParam } = require('../../../../utils/device'); -const logger = require('../../../../utils/logger'); -const { readOnlineValue } = require('../features'); -const { pollBinary } = require('../features/binary'); -const { pollHumidity } = require('../features/humidity'); -const { pollTemperature } = require('../features/temperature'); -const { DEVICE_FIRMWARE, DEVICE_ONLINE } = require('../utils/constants'); -const { parseExternalId } = require('../utils/externalId'); - -/** - * - * @description Poll values of an eWeLink device. - * @param {object} device - The device to poll. - * @returns {Promise} Promise of nothing. - * @example - * poll(device); - */ -async function poll(device) { - const { deviceId } = parseExternalId(device.external_id); - const onlineParam = getDeviceParam(device, DEVICE_ONLINE); - - try { - const { thingList } = await this.handleRequest(async () => - this.ewelinkWebAPIClient.device.getThings({ thingList: [{ id: deviceId }] }), - ); - const [{ itemData: eWeLinkDevice }] = thingList; - logger.debug('eWeLink: load device: %j', eWeLinkDevice); - - const currentOnline = readOnlineValue(eWeLinkDevice.online); - // if the value is different from the value we have, save new param - if (onlineParam !== currentOnline) { - logger.debug(`eWeLink: Polling device "${deviceId}", online new value = ${currentOnline}`); - setDeviceParam(device, DEVICE_ONLINE, currentOnline); - } - - (device.features || []).forEach((feature) => { - let state = null; - switch (feature.category) { - case DEVICE_FEATURE_CATEGORIES.SWITCH: // Binary - if (feature.type === DEVICE_FEATURE_TYPES.SWITCH.BINARY) { - state = pollBinary(eWeLinkDevice, feature); - } - break; - case DEVICE_FEATURE_CATEGORIES.HUMIDITY_SENSOR: // Humidity - state = pollHumidity(eWeLinkDevice, feature); - break; - case DEVICE_FEATURE_CATEGORIES.TEMPERATURE_SENSOR: // Temperature - state = pollTemperature(eWeLinkDevice, feature); - break; - default: - break; - } - - if (state !== null) { - logger.debug(`eWeLink: Polling device "${deviceId}", emit feature "${feature.external_id}" update`); - this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: feature.external_id, - state, - }); - } - }); - - const firmwareParam = getDeviceParam(device, DEVICE_FIRMWARE); - if (firmwareParam) { - const currentVersion = (eWeLinkDevice.params && eWeLinkDevice.params.fwVersion) || false; - // if the value is different from the value we have, save new param - if (currentVersion && firmwareParam !== currentVersion) { - logger.debug(`eWeLink: Polling device "${deviceId}", firmware new value = ${currentVersion}`); - setDeviceParam(device, DEVICE_FIRMWARE, currentVersion); - } - } - } catch (e) { - // In case of device not found (also means offline device) - // we override the "ONLINE" parameter if needed - if (e instanceof NotFoundError && onlineParam) { - logger.debug(`eWeLink: Polling device "${deviceId}" can't be found, it seems to be offline`); - setDeviceParam(device, DEVICE_ONLINE, '0'); - } - - throw e; - } -} - -module.exports = { - poll, -}; diff --git a/server/services/ewelink/lib/device/setValue.js b/server/services/ewelink/lib/device/setValue.js index fa0c2958c3..0e93f971fe 100644 --- a/server/services/ewelink/lib/device/setValue.js +++ b/server/services/ewelink/lib/device/setValue.js @@ -1,6 +1,5 @@ const { DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); const logger = require('../../../../utils/logger'); -const { writeBinaryValue } = require('../features/binary'); const { parseExternalId } = require('../utils/externalId'); /** @@ -22,7 +21,7 @@ async function setValue(device, deviceFeature, value) { 0, ); - const binaryValue = writeBinaryValue(value); + const binaryValue = value ? 'on' : 'off'; if (nbBinaryFeatures > 1) { params.switches = [{ switch: binaryValue, outlet: channel }]; } else { diff --git a/server/services/ewelink/lib/ewelink.stop.js b/server/services/ewelink/lib/ewelink.stop.js index 344733cf8d..60d41f2605 100644 --- a/server/services/ewelink/lib/ewelink.stop.js +++ b/server/services/ewelink/lib/ewelink.stop.js @@ -4,6 +4,7 @@ * await this.stop(); */ async function stop() { + this.closeWebSocketClient(); this.ewelinkWebAPIClient = null; this.updateStatus({ connected: false }); } diff --git a/server/services/ewelink/lib/features/binary.js b/server/services/ewelink/lib/features/binary.js index 005e7831c8..98bbed6f10 100644 --- a/server/services/ewelink/lib/features/binary.js +++ b/server/services/ewelink/lib/features/binary.js @@ -1,6 +1,4 @@ const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES, STATE } = require('../../../../utils/constants'); -const logger = require('../../../../utils/logger'); -const { parseExternalId } = require('../utils/externalId'); module.exports = { // Gladys feature @@ -15,22 +13,27 @@ module.exports = { max: 1, }; }, - pollBinary: (eWeLinkDevice, feature) => { - const { deviceId, channel } = parseExternalId(feature.external_id); - let state = (eWeLinkDevice.params && eWeLinkDevice.params.switch) || false; - const switches = (eWeLinkDevice.params && eWeLinkDevice.params.switches) || false; - if (state || switches) { - if (switches) { - state = switches[channel - 1].switch; - } + readStates: (externalId, params) => { + const states = []; + + // Single switch + if (params.switch) { + const state = params.switch === 'on' ? STATE.ON : STATE.OFF; + states.push({ featureExternalId: `${externalId}:binary:1`, state }); } - const currentBinaryState = state === 'on' ? STATE.ON : STATE.OFF; - // if the value is different from the value we have, save new state - if (state && feature.last_value !== currentBinaryState) { - logger.debug(`eWeLink: Polling device "${deviceId}", binary new value = ${currentBinaryState}`); - return currentBinaryState; + + // Multiple switches + if (params.switches) { + params.switches.forEach(({ switch: value, outlet }) => { + const state = value === 'on' ? STATE.ON : STATE.OFF; + states.push({ + featureExternalId: `${externalId}:binary:${outlet}`, + state, + }); + }); } - return null; + + return states; }, // Gladys vs eWeLink transformers writeBinaryValue: (value) => { diff --git a/server/services/ewelink/lib/features/humidity.js b/server/services/ewelink/lib/features/humidity.js index 062e6201ed..4f3869419c 100644 --- a/server/services/ewelink/lib/features/humidity.js +++ b/server/services/ewelink/lib/features/humidity.js @@ -3,12 +3,10 @@ const { DEVICE_FEATURE_TYPES, DEVICE_FEATURE_UNITS, } = require('../../../../utils/constants'); -const logger = require('../../../../utils/logger'); -const { parseExternalId } = require('../utils/externalId'); module.exports = { // Gladys feature - generateFeature: (name, channel = 0) => { + generateFeature: (name) => { return { name: `${name} Humidity`, category: DEVICE_FEATURE_CATEGORIES.HUMIDITY_SENSOR, @@ -20,14 +18,17 @@ module.exports = { unit: DEVICE_FEATURE_UNITS.PERCENT, }; }, - pollHumidity: (eWeLinkDevice, feature) => { - const { deviceId } = parseExternalId(feature.external_id); - const currentHumidity = (eWeLinkDevice.params && eWeLinkDevice.params.currentHumidity) || false; - // if the value is different from the value we have, save new state - if (currentHumidity && feature.last_value !== currentHumidity) { - logger.debug(`eWeLink: Polling device "${deviceId}", humidity new value = ${currentHumidity}`); - return currentHumidity; + readStates: (externalId, params) => { + const states = []; + + // Current humidity + if (params.currentHumidity) { + states.push({ + featureExternalId: `${externalId}:humidity`, + state: params.currentHumidity, + }); } - return null; + + return states; }, }; diff --git a/server/services/ewelink/lib/features/index.js b/server/services/ewelink/lib/features/index.js index 60d5676945..5ab067bad2 100644 --- a/server/services/ewelink/lib/features/index.js +++ b/server/services/ewelink/lib/features/index.js @@ -1,7 +1,6 @@ -const { DEVICE_POLL_FREQUENCIES } = require('../../../../utils/constants'); const logger = require('../../../../utils/logger'); const { titleize } = require('../../../../utils/titleize'); -const { DEVICE_IP_ADDRESS, DEVICE_FIRMWARE, DEVICE_ONLINE } = require('../utils/constants'); +const { readParams } = require('../params'); const { getExternalId } = require('../utils/externalId'); // Features @@ -9,6 +8,8 @@ const binaryFeature = require('./binary'); const humidityFeature = require('./humidity'); const temperatureFeature = require('./temperature'); +const AVAILABLE_FEATURES = [binaryFeature, humidityFeature, temperatureFeature]; + const AVAILABLE_FEATURE_MODELS = { binary: { uiid: [1, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15], @@ -37,17 +38,6 @@ const getDeviceName = (device) => { return name.trim(); }; -/** - * @description Convert online state. - * @param {boolean} online - Online device state. - * @returns {string} Return the prefix, the device ID and the channel count. - * @example - * readOnlineValue(true); - */ -function readOnlineValue(online) { - return online ? '1' : '0'; -} - /** * @description Create an eWeLink device for Gladys. * @param {string} serviceId - The UUID of the service. @@ -68,22 +58,8 @@ function getDevice(serviceId, device) { selector: externalId, features: [], service_id: serviceId, - should_poll: true, - poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_30_SECONDS, - params: [ - { - name: DEVICE_IP_ADDRESS, - value: device.ip || '?.?.?.?', - }, - { - name: DEVICE_FIRMWARE, - value: params.fwVersion || '?.?.?', - }, - { - name: DEVICE_ONLINE, - value: readOnlineValue(device.online), - }, - ], + should_poll: false, + params: readParams(device), }; const deviceUiid = (device.extra || {}).uiid; @@ -111,7 +87,26 @@ function getDevice(serviceId, device) { return createdDevice; } +/** + * @description Read and decode Gladys feature state from eWeLink object. + * @param {string} externalId - Device external ID. + * @param {object} params - EWeLink received params. + * @returns {Array} Arry of featureExternalId / state objects. + * @example + * const states = readStates('ewelink:10001a', { switch: 'on' }); + */ +function readStates(externalId, params) { + const states = []; + AVAILABLE_FEATURES.forEach((feature) => { + const updatedStates = feature.readState(externalId, params); + updatedStates.forEach((state) => { + states.push(state); + }); + }); + return states; +} + module.exports = { - readOnlineValue, getDevice, + readStates, }; diff --git a/server/services/ewelink/lib/features/temperature.js b/server/services/ewelink/lib/features/temperature.js index 2d8e6703ea..83c20ed64b 100644 --- a/server/services/ewelink/lib/features/temperature.js +++ b/server/services/ewelink/lib/features/temperature.js @@ -3,12 +3,10 @@ const { DEVICE_FEATURE_TYPES, DEVICE_FEATURE_UNITS, } = require('../../../../utils/constants'); -const logger = require('../../../../utils/logger'); -const { parseExternalId } = require('../utils/externalId'); module.exports = { // Gladys feature - generateFeature: (name, channel = 0) => { + generateFeature: (name) => { return { name: `${name} Temperature`, category: DEVICE_FEATURE_CATEGORIES.TEMPERATURE_SENSOR, @@ -20,14 +18,17 @@ module.exports = { unit: DEVICE_FEATURE_UNITS.CELSIUS, }; }, - pollTemperature: (eWeLinkDevice, feature) => { - const { deviceId } = parseExternalId(feature.external_id); - const currentTemperature = (eWeLinkDevice.params && eWeLinkDevice.params.currentTemperature) || false; - // if the value is different from the value we have, save new state - if (currentTemperature && feature.last_value !== currentTemperature) { - logger.debug(`eWeLink: Polling device "${deviceId}", temperature new value = ${currentTemperature}`); - return currentTemperature; + readStates: (externalId, params) => { + const states = []; + + // Current temperature + if (params.currentTemperature) { + states.push({ + featureExternalId: `${externalId}:temperature`, + state: params.currentTemperature, + }); } - return null; + + return states; }, }; diff --git a/server/services/ewelink/lib/handlers/ewelink.handleResponse.js b/server/services/ewelink/lib/handlers/ewelink.handleResponse.js index 3ab0aa77b5..75a4b3ab01 100644 --- a/server/services/ewelink/lib/handlers/ewelink.handleResponse.js +++ b/server/services/ewelink/lib/handlers/ewelink.handleResponse.js @@ -20,6 +20,7 @@ async function handleResponse(response) { 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); case 400: diff --git a/server/services/ewelink/lib/index.js b/server/services/ewelink/lib/index.js index 063fd55505..19c0957c30 100644 --- a/server/services/ewelink/lib/index.js +++ b/server/services/ewelink/lib/index.js @@ -1,5 +1,4 @@ const { discover } = require('./device/discover'); -const { poll } = require('./device/poll'); const { setValue } = require('./device/setValue'); const { updateStatus } = require('./config/ewelink.updateStatus'); @@ -17,6 +16,13 @@ const { saveTokens } = require('./user/ewelink.saveTokens'); const { handleRequest } = require('./handlers/ewelink.handleRequest'); const { handleResponse } = require('./handlers/ewelink.handleResponse'); +const { createWebSocketClient } = require('./websocket/ewelink.createWebSocketClient'); +const { closeWebSocketClient } = require('./websocket/ewelink.closeWebSocketClient'); +const { onWebSocketMessage } = require('./websocket/ewelink.onWebSocketMessage'); +const { onWebSocketError } = require('./websocket/ewelink.onWebSocketError'); +const { onWebSocketClose } = require('./websocket/ewelink.onWebSocketClose'); +const { onWebSocketOpen } = require('./websocket/ewelink.onWebSocketOpen'); + const { init } = require('./ewelink.init'); const { stop } = require('./ewelink.stop'); const { upgrade } = require('./versions/ewelink.upgrade'); @@ -35,6 +41,9 @@ const EweLinkHandler = function EweLinkHandler(gladys, eweLinkApi, serviceId) { this.serviceId = serviceId; this.ewelinkWebAPIClient = null; + this.ewelinkWebSocketClientFactory = null; + this.ewelinkWebSocketClient = null; + this.loginState = null; this.configuration = {}; this.status = { @@ -58,10 +67,16 @@ EweLinkHandler.prototype.handleRequest = handleRequest; EweLinkHandler.prototype.handleResponse = handleResponse; EweLinkHandler.prototype.discover = discover; -EweLinkHandler.prototype.poll = poll; EweLinkHandler.prototype.setValue = setValue; EweLinkHandler.prototype.getStatus = getStatus; +EweLinkHandler.prototype.createWebSocketClient = createWebSocketClient; +EweLinkHandler.prototype.closeWebSocketClient = closeWebSocketClient; +EweLinkHandler.prototype.onWebSocketOpen = onWebSocketOpen; +EweLinkHandler.prototype.onWebSocketClose = onWebSocketClose; +EweLinkHandler.prototype.onWebSocketError = onWebSocketError; +EweLinkHandler.prototype.onWebSocketMessage = onWebSocketMessage; + EweLinkHandler.prototype.init = init; EweLinkHandler.prototype.stop = stop; EweLinkHandler.prototype.upgrade = upgrade; diff --git a/server/services/ewelink/lib/params/firmware.param.js b/server/services/ewelink/lib/params/firmware.param.js new file mode 100644 index 0000000000..de586be0f9 --- /dev/null +++ b/server/services/ewelink/lib/params/firmware.param.js @@ -0,0 +1,4 @@ +module.exports = { + EWELINK_KEY_PATH: 'params.fwVersion', + GLADYS_PARAM_KEY: 'FIRMWARE', +}; diff --git a/server/services/ewelink/lib/params/index.js b/server/services/ewelink/lib/params/index.js new file mode 100644 index 0000000000..e16581b789 --- /dev/null +++ b/server/services/ewelink/lib/params/index.js @@ -0,0 +1,31 @@ +const get = require('get-value'); + +const onlineParam = require('./online.param'); +const firmwareParam = require('./firmware.param'); + +const PARAMS = [firmwareParam, onlineParam]; + +/** + * @description Read device params from eWeLink device params. + * @param {object} device - Key/value map with updated params. + * @returns {Array} Array with all '{ name, value }' device params object. + * @example + * readParams({ online: true, params: { fwVersion: '2.5.0' } }) + */ +function readParams(device) { + const deviceParams = []; + + PARAMS.forEach(({ EWELINK_KEY_PATH, GLADYS_PARAM_KEY: name, convertValue = (value) => value }) => { + const rawValue = get(device, EWELINK_KEY_PATH); + if (rawValue !== undefined) { + const value = convertValue(rawValue); + deviceParams.push({ name, value }); + } + }); + + return deviceParams; +} + +module.exports = { + readParams, +}; diff --git a/server/services/ewelink/lib/params/online.param.js b/server/services/ewelink/lib/params/online.param.js new file mode 100644 index 0000000000..884a4408f0 --- /dev/null +++ b/server/services/ewelink/lib/params/online.param.js @@ -0,0 +1,16 @@ +/** + * @description Convert online state. + * @param {boolean} rawValue - Param raw value. + * @returns {string} Converted value to Gladys param value. + * @example + * const gladysParamValue = convertValue(true); + */ +function convertValue(rawValue) { + return rawValue ? '1' : '0'; +} + +module.exports = { + EWELINK_KEY_PATH: 'online', + GLADYS_PARAM_KEY: 'ONLINE', + convertValue, +}; diff --git a/server/services/ewelink/lib/user/ewelink.deleteTokens.js b/server/services/ewelink/lib/user/ewelink.deleteTokens.js index 67de302d2f..cb511258fe 100644 --- a/server/services/ewelink/lib/user/ewelink.deleteTokens.js +++ b/server/services/ewelink/lib/user/ewelink.deleteTokens.js @@ -21,9 +21,12 @@ async function deleteTokens() { // Clear tokens await this.gladys.variable.destroy(CONFIGURATION_KEYS.USER_TOKENS, this.serviceId); + this.ewelinkWebAPIClient.at = null; this.ewelinkWebAPIClient.rt = null; + this.closeWebSocketClient(); + this.updateStatus({ connected: false }); logger.info('eWeLink: user well disconnected'); } diff --git a/server/services/ewelink/lib/user/ewelink.exchangeToken.js b/server/services/ewelink/lib/user/ewelink.exchangeToken.js index 37c24532e2..d9e0ce54f5 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.createWebSocketClient(); + this.updateStatus({ connected: true }); logger.info('eWeLink: user well connected...'); } diff --git a/server/services/ewelink/lib/utils/constants.js b/server/services/ewelink/lib/utils/constants.js index f2d3726890..85c5431602 100644 --- a/server/services/ewelink/lib/utils/constants.js +++ b/server/services/ewelink/lib/utils/constants.js @@ -9,18 +9,11 @@ const CONFIGURATION_KEYS = { const DEVICE_SERVICE_ID = 'ewelink'; const DEVICE_EXTERNAL_ID_BASE = 'ewelink'; -const DEVICE_IP_ADDRESS = 'IP_ADDRESS'; -const DEVICE_FIRMWARE = 'FIRMWARE'; -const DEVICE_ONLINE = 'ONLINE'; - const NB_MAX_RETRY_EXPIRED = 1; module.exports = { CONFIGURATION_KEYS, DEVICE_SERVICE_ID, DEVICE_EXTERNAL_ID_BASE, - DEVICE_IP_ADDRESS, - DEVICE_FIRMWARE, - DEVICE_ONLINE, NB_MAX_RETRY_EXPIRED, }; diff --git a/server/services/ewelink/lib/versions/ewelink.upgrade.js b/server/services/ewelink/lib/versions/ewelink.upgrade.js index ef624c223a..037034c964 100644 --- a/server/services/ewelink/lib/versions/ewelink.upgrade.js +++ b/server/services/ewelink/lib/versions/ewelink.upgrade.js @@ -2,9 +2,10 @@ const Promise = require('bluebird'); const logger = require('../../../../utils/logger'); const v2 = require('./ewelink.v2'); +const v3 = require('./ewelink.v3'); const { CONFIGURATION_KEYS } = require('../utils/constants'); -const VERSIONS = [v2]; +const VERSIONS = [v2, v3]; /** * @description Upgrades eWeLink integration to last version. diff --git a/server/services/ewelink/lib/versions/ewelink.v3.js b/server/services/ewelink/lib/versions/ewelink.v3.js new file mode 100644 index 0000000000..76347f3d06 --- /dev/null +++ b/server/services/ewelink/lib/versions/ewelink.v3.js @@ -0,0 +1,42 @@ +const Promise = require('bluebird'); +const logger = require('../../../../utils/logger'); + +/** + * @description Upgrades integration to v3. + * @param {object} ewelink - Current eWeLink service handler. + * @example + * await v3.apply(); + */ +async function apply(ewelink) { + logger.info('eWeLink: load existing devices...'); + + // Load eWeLink devices + const { serviceId, gladys } = ewelink; + const devices = gladys.device.get({ service_id: serviceId }); + + await Promise.each(devices, async (device) => { + const { params, external_id: externalId } = device; + // Update params + logger.info('eWeLink: unset IP_ADDRESS and FIRMWARE="?.?.?" parameters from "%s" device...', externalId); + // - remove 'IP_ADDRESS' param + // - remove 'FIRMWARE' with '?.?.?' value + const clearedParams = params.filter((param) => { + return !(param.name === 'IP_ADDRESS' || (param.name === 'FIRMWARE' && param.value === '?.?.?')); + }); + device.params = clearedParams; + + // Remove poll information + logger.info('eWeLink: unset polling from "%s" device...', externalId); + device.should_poll = false; + delete device.poll_frequency; + + // Save device + logger.info('eWeLink: saving cleared "%s" device...', externalId); + await gladys.device.create(device); + }); +} + +module.exports = { + apply, + VERSION_NUMBER: 3, +}; diff --git a/server/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.js b/server/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.js new file mode 100644 index 0000000000..d1303823b5 --- /dev/null +++ b/server/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.js @@ -0,0 +1,15 @@ +/** + * @description Close WebSocket client. + * @example + * this.closeWebSocketClient(); + */ +function closeWebSocketClient() { + if (this.ewelinkWebSocketClient !== null) { + this.ewelinkWebSocketClient.close(); + this.ewelinkWebSocketClient = null; + } +} + +module.exports = { + closeWebSocketClient, +}; diff --git a/server/services/ewelink/lib/websocket/ewelink.createWebSocketClient.js b/server/services/ewelink/lib/websocket/ewelink.createWebSocketClient.js new file mode 100644 index 0000000000..9ca5444e1c --- /dev/null +++ b/server/services/ewelink/lib/websocket/ewelink.createWebSocketClient.js @@ -0,0 +1,23 @@ +/** + * @description Create WebSocket client. + * @example + * await this.createWebSocketClient(); + */ +async function createWebSocketClient() { + const { applicationId: appId, applicationRegion: region } = this.configuration; + this.ewelinkWebSocketClient = await this.ewelinkWebSocketClientFactory.Connect.create( + { + appId, + region, + at: this.ewelinkWebAPIClient.at, + }, + this.onWebSocketOpen.bind(this), + this.onWebSocketClose.bind(this), + this.onWebSocketError.bind(this), + this.onWebSocketMessage.bind(this), + ); +} + +module.exports = { + createWebSocketClient, +}; diff --git a/server/services/ewelink/lib/websocket/ewelink.onWebSocketClose.js b/server/services/ewelink/lib/websocket/ewelink.onWebSocketClose.js new file mode 100644 index 0000000000..a3fd3bf0fe --- /dev/null +++ b/server/services/ewelink/lib/websocket/ewelink.onWebSocketClose.js @@ -0,0 +1,15 @@ +const logger = require('../../../../utils/logger'); + +/** + * @description Action to execute when WebSocket is closed. + * @example + * this.onWebSocketClose(); + */ +function onWebSocketClose() { + this.closeWebSocketClient(); + logger.info('eWeLink: WebSocket is closed'); +} + +module.exports = { + onWebSocketClose, +}; diff --git a/server/services/ewelink/lib/websocket/ewelink.onWebSocketError.js b/server/services/ewelink/lib/websocket/ewelink.onWebSocketError.js new file mode 100644 index 0000000000..b8f5e6d965 --- /dev/null +++ b/server/services/ewelink/lib/websocket/ewelink.onWebSocketError.js @@ -0,0 +1,15 @@ +const logger = require('../../../../utils/logger'); + +/** + * @description Action to execute when WebSocket is on error state. + * @param {object} error - WebSocket error. + * @example + * this.onWebSocketError(); + */ +function onWebSocketError(error) { + logger.error('eWeLink: WebSocket is on error: %s', error.message); +} + +module.exports = { + onWebSocketError, +}; diff --git a/server/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.js b/server/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.js new file mode 100644 index 0000000000..e5294c97df --- /dev/null +++ b/server/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.js @@ -0,0 +1,58 @@ +const Promise = require('bluebird'); + +const logger = require('../../../../utils/logger'); +const { EVENTS } = require('../../../../utils/constants'); +const { setDeviceParam } = require('../../../../utils/device'); + +const { getExternalId } = require('../utils/externalId'); +const { readStates } = require('../features'); +const { readParams } = require('../params'); + +/** + * @description Action to execute when WebSocket receives a message. + * @param {object} ws - Current WebSocket client. + * @param {object} message - WebSocket message. + * @example + * this.onWebSocketMessage(); + */ +async function onWebSocketMessage(ws, message) { + logger.debug('eWeLink: WebSocket received a message: %j', message); + + await this.handleResponse(message); + + const { deviceid, params } = message; + const externalId = getExternalId({ deviceid }); + + // Load device to update params + const device = this.gladys.stateManager.get('deviceByExternalId', externalId); + if (!device) { + logger.info(`eWeLink: device "${deviceid} not found in Gladys`); + } else { + // Update the device feature values + const states = readStates(externalId, params); + states.forEach(({ featureExternalId, state }) => { + // Before sending event, check if feature exists + const feature = this.gladys.stateManager.get('deviceFeatureByExternalId', featureExternalId); + if (feature) { + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: featureExternalId, + state, + }); + } else { + logger.debug(`eWeLink: feature ${featureExternalId} not found in Gladys`); + } + }); + + // Update the device params + const updatedParams = readParams(params); + // Update device params + await Promise.each(updatedParams, async ({ key, value }) => { + setDeviceParam(device, key, value); + await this.gladys.device.setParam(device, key, value); + }); + } +} + +module.exports = { + onWebSocketMessage, +}; diff --git a/server/services/ewelink/lib/websocket/ewelink.onWebSocketOpen.js b/server/services/ewelink/lib/websocket/ewelink.onWebSocketOpen.js new file mode 100644 index 0000000000..c551fbce5b --- /dev/null +++ b/server/services/ewelink/lib/websocket/ewelink.onWebSocketOpen.js @@ -0,0 +1,14 @@ +const logger = require('../../../../utils/logger'); + +/** + * @description Action to execute when WebSocket is open. + * @example + * this.onWebSocketOpen(); + */ +function onWebSocketOpen() { + logger.info('eWeLink: WebSocket is ready'); +} + +module.exports = { + onWebSocketOpen, +}; diff --git a/server/services/ewelink/package-lock.json b/server/services/ewelink/package-lock.json index 586eb3bf10..3c90491585 100644 --- a/server/services/ewelink/package-lock.json +++ b/server/services/ewelink/package-lock.json @@ -19,7 +19,8 @@ ], "dependencies": { "bluebird": "^3.7.2", - "ewelink-api-next": "^1.0.3" + "ewelink-api-next": "^1.0.3", + "get-value": "^3.0.1" } }, "node_modules/@leichtgewicht/ip-codec": { @@ -196,6 +197,17 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/get-value": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz", + "integrity": "sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=6.0" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -209,6 +221,14 @@ "node": ">=0.8.19" } }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -556,6 +576,14 @@ "universalify": "^0.1.0" } }, + "get-value": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz", + "integrity": "sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==", + "requires": { + "isobject": "^3.0.1" + } + }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -566,6 +594,11 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", diff --git a/server/services/ewelink/package.json b/server/services/ewelink/package.json index f6460103e3..0eb3f02141 100644 --- a/server/services/ewelink/package.json +++ b/server/services/ewelink/package.json @@ -14,6 +14,7 @@ ], "dependencies": { "bluebird": "^3.7.2", - "ewelink-api-next": "^1.0.3" + "ewelink-api-next": "^1.0.3", + "get-value": "^3.0.1" } } 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 9d1ff197b8..95bc480b3e 100644 --- a/server/test/services/ewelink/lib/config/ewelink.loadConfiguration.test.js +++ b/server/test/services/ewelink/lib/config/ewelink.loadConfiguration.test.js @@ -1,7 +1,7 @@ const { expect } = require('chai'); const sinon = require('sinon'); -const { assert, fake, stub } = sinon; +const { assert, fake, stub, match } = sinon; const EwelinkHandler = require('../../../../../services/ewelink/lib'); const { SERVICE_ID } = require('../constants'); @@ -11,9 +11,15 @@ const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/consta describe('eWeLinkHandler loadConfiguration', () => { let eWeLinkHandler; let eWeLinkApiMock; + let eWeLinkWsMock; let gladys; beforeEach(() => { + eWeLinkWsMock = stub(); + eWeLinkWsMock.prototype.Connect = { + create: stub(), + }; + gladys = { event: { emit: fake.returns(null), @@ -25,6 +31,7 @@ describe('eWeLinkHandler loadConfiguration', () => { eWeLinkApiMock = { WebAPI: stub(), + Ws: eWeLinkWsMock, }; eWeLinkHandler = new EwelinkHandler(gladys, eWeLinkApiMock, SERVICE_ID); }); @@ -52,6 +59,7 @@ describe('eWeLinkHandler loadConfiguration', () => { assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); assert.notCalled(eWeLinkApiMock.WebAPI); + assert.notCalled(eWeLinkApiMock.Ws); } }); @@ -80,6 +88,7 @@ describe('eWeLinkHandler loadConfiguration', () => { assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); assert.notCalled(eWeLinkApiMock.WebAPI); + assert.notCalled(eWeLinkApiMock.Ws); } }); @@ -110,6 +119,7 @@ describe('eWeLinkHandler loadConfiguration', () => { assert.calledWithExactly(gladys.variable.getValue, 'APPLICATION_REGION', SERVICE_ID); assert.notCalled(eWeLinkApiMock.WebAPI); + assert.notCalled(eWeLinkApiMock.Ws); } }); @@ -151,6 +161,13 @@ describe('eWeLinkHandler loadConfiguration', () => { 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); } }); @@ -188,6 +205,21 @@ describe('eWeLinkHandler loadConfiguration', () => { 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'); 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 891439945f..6111dc12f7 100644 --- a/server/test/services/ewelink/lib/config/ewelink.saveConfiguration.test.js +++ b/server/test/services/ewelink/lib/config/ewelink.saveConfiguration.test.js @@ -26,6 +26,7 @@ describe('eWeLinkHandler saveConfiguration', () => { eWeLinkApiMock = { WebAPI: stub(), + Ws: stub(), }; eWeLinkHandler = new EwelinkHandler(gladys, eWeLinkApiMock, SERVICE_ID); }); @@ -48,6 +49,8 @@ describe('eWeLinkHandler saveConfiguration', () => { assert.notCalled(gladys.variable.destroy); expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClientFactory).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); }); it('should throw a BadParameter error as SECRET and REGION variables are mossing', async () => { @@ -64,6 +67,8 @@ describe('eWeLinkHandler saveConfiguration', () => { assert.notCalled(gladys.variable.destroy); expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClientFactory).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); }); it('should throw a BadParameter error as REGION variables is missing', async () => { @@ -80,6 +85,8 @@ describe('eWeLinkHandler saveConfiguration', () => { assert.notCalled(gladys.variable.destroy); expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClientFactory).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); }); it('should throw a error on database store failure', async () => { @@ -106,6 +113,8 @@ describe('eWeLinkHandler saveConfiguration', () => { assert.notCalled(gladys.variable.destroy); expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClientFactory).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); }); it('should save configuration and send events', async () => { @@ -132,5 +141,7 @@ 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); }); }); diff --git a/server/test/services/ewelink/lib/device/poll.test.js b/server/test/services/ewelink/lib/device/poll.test.js deleted file mode 100644 index bca0279d0b..0000000000 --- a/server/test/services/ewelink/lib/device/poll.test.js +++ /dev/null @@ -1,105 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); - -const { EVENTS } = require('../../../../../utils/constants'); -const EwelinkHandler = require('../../../../../services/ewelink/lib'); - -const { SERVICE_ID, EWELINK_DENIED_ACCESS_TOKEN } = require('../constants'); -const Gladys2ChDevice = require('../payloads/Gladys-2ch.json'); -const GladysBasicDevice = require('../payloads/Gladys-Basic.json'); -const GladysOfflineDevice = require('../payloads/Gladys-offline.json'); -const GladysPowDevice = require('../payloads/Gladys-pow.json'); -const GladysThDevice = require('../payloads/Gladys-th.json'); -const EweLinkApiMock = require('../ewelink-api.mock.test'); - -const { assert, fake } = sinon; - -describe('eWeLinkHandler poll', () => { - let eWeLinkHandler; - let gladys; - - beforeEach(() => { - gladys = { - event: { - emit: fake.resolves(null), - }, - }; - - eWeLinkHandler = new EwelinkHandler(gladys, EweLinkApiMock, SERVICE_ID); - eWeLinkHandler.ewelinkWebAPIClient = new EweLinkApiMock.WebAPI(); - eWeLinkHandler.status = { configured: true, connected: true }; - }); - - afterEach(() => { - sinon.reset(); - }); - - it('should poll device and emit 2 states for a "2CH" model', async () => { - await eWeLinkHandler.poll(Gladys2ChDevice); - assert.callCount(gladys.event.emit, 2); - assert.calledWithExactly(gladys.event.emit.getCall(0), EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: 'ewelink:10004531ae:binary:1', - state: 1, - }); - assert.calledWithExactly(gladys.event.emit.getCall(1), EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: 'ewelink:10004531ae:binary:2', - state: 0, - }); - }); - it('should poll device and emit 1 state for a "POW" device', async () => { - await eWeLinkHandler.poll(GladysPowDevice); - assert.calledOnceWithExactly(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: 'ewelink:10004533ae:binary:1', - state: 1, - }); - }); - it('should poll device and emit 3 states for a "TH" model', async () => { - await eWeLinkHandler.poll(GladysThDevice); - assert.callCount(gladys.event.emit, 3); - assert.calledWithExactly(gladys.event.emit.getCall(0), EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: 'ewelink:10004534ae:binary:1', - state: 1, - }); - assert.calledWithExactly(gladys.event.emit.getCall(1), EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: 'ewelink:10004534ae:humidity', - state: 42, - }); - assert.calledWithExactly(gladys.event.emit.getCall(2), EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: 'ewelink:10004534ae:temperature', - state: 20, - }); - }); - it('should poll device and update 2 params for a "Basic" model', async () => { - // this check that some values are set on the device, and will be changed - expect(GladysBasicDevice.params).to.deep.equal([ - { name: 'IP_ADDRESS', value: '192.168.0.6' }, - { name: 'FIRMWARE', value: '3.1.2' }, - { name: 'ONLINE', value: '0' }, - ]); - await eWeLinkHandler.poll(GladysBasicDevice); - assert.notCalled(gladys.event.emit); - expect(GladysBasicDevice.params).to.deep.equal([ - { name: 'IP_ADDRESS', value: '192.168.0.6' }, - { name: 'FIRMWARE', value: '3.3.0' }, - { name: 'ONLINE', value: '1' }, - ]); - }); - it('should throw an error when device is offline', async () => { - try { - await eWeLinkHandler.poll(GladysOfflineDevice); - assert.fail(); - } catch (error) { - 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.poll(Gladys2ChDevice); - assert.fail(); - } catch (error) { - 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 64ffc7823a..6e1d84d401 100644 --- a/server/test/services/ewelink/lib/ewelink-api.mock.test.js +++ b/server/test/services/ewelink/lib/ewelink-api.mock.test.js @@ -87,6 +87,27 @@ class WebAPI { } } -const items = { WebAPI, Device }; +class Connect { + constructor(root) { + this.root = root; + + this.create = () => {}; + } +} + +class Ws { + constructor(options = { appId: EWELINK_APP_ID, appSecret: EWELINK_APP_SECRET, region: EWELINK_APP_REGION }) { + this.appId = options.appId; + this.appSecret = options.appSecret; + this.region = options.region; + + // default with right access token + this.at = EWELINK_VALID_ACCESS_TOKEN; + + this.Connect = new Connect(this); + } +} + +const items = { WebAPI, Device, Ws, Connect }; module.exports = { default: items, ...items }; diff --git a/server/test/services/ewelink/lib/features/features.test.js b/server/test/services/ewelink/lib/features/features.test.js index 0741b03ab6..0e7251d4eb 100644 --- a/server/test/services/ewelink/lib/features/features.test.js +++ b/server/test/services/ewelink/lib/features/features.test.js @@ -24,17 +24,6 @@ describe('eWeLink features parseExternalId', () => { }); }); -describe('eWeLink features readOnlineValue', () => { - it('should return 1 if device is online', () => { - const value = features.readOnlineValue(true); - expect(value).to.equal('1'); - }); - it('should return 0 if device is offline', () => { - const value = features.readOnlineValue(false); - expect(value).to.equal('0'); - }); -}); - describe('eWeLink features getDevice', () => { it('should return device without features if offline', () => { const device = features.getDevice(SERVICE_ID, eweLinkOfflineDevice); diff --git a/server/test/services/ewelink/lib/params/online.param.test.js b/server/test/services/ewelink/lib/params/online.param.test.js new file mode 100644 index 0000000000..b0f7e20312 --- /dev/null +++ b/server/test/services/ewelink/lib/params/online.param.test.js @@ -0,0 +1,15 @@ +const { expect } = require('chai'); + +const onlineParam = require('../../../../../services/ewelink/lib/params/online.param'); + +describe('eWeLink online param', () => { + it('should return 1 if device is online', () => { + const value = onlineParam.convertValue(true); + expect(value).to.equal('1'); + }); + + it('should return 0 if device is offline', () => { + const value = onlineParam.convertValue(false); + expect(value).to.equal('0'); + }); +}); diff --git a/server/test/services/ewelink/lib/payloads/Gladys-2ch.json b/server/test/services/ewelink/lib/payloads/Gladys-2ch.json index a5b0efc6ad..3267189b09 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-2ch.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-2ch.json @@ -4,8 +4,7 @@ "model": "2CH", "external_id": "ewelink:10004531ae", "selector": "ewelink:10004531ae", - "should_poll": true, - "poll_frequency": 30000, + "should_poll": false, "features": [ { "name": "Test 1 Ch1 On/Off", @@ -31,10 +30,6 @@ } ], "params": [ - { - "name": "IP_ADDRESS", - "value": "192.168.0.1" - }, { "name": "FIRMWARE", "value": "3.3.0" diff --git a/server/test/services/ewelink/lib/payloads/Gladys-Basic.json b/server/test/services/ewelink/lib/payloads/Gladys-Basic.json index 9eeda592b3..896a6f51cc 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-Basic.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-Basic.json @@ -4,8 +4,7 @@ "model": "Basic", "external_id": "ewelink:10004536ae", "selector": "ewelink:10004536ae", - "should_poll": true, - "poll_frequency": 30000, + "should_poll": false, "features": [ { "name": "Test 6 On/Off", @@ -31,10 +30,6 @@ } ], "params": [ - { - "name": "IP_ADDRESS", - "value": "192.168.0.6" - }, { "name": "FIRMWARE", "value": "3.1.2" diff --git a/server/test/services/ewelink/lib/payloads/Gladys-offline.json b/server/test/services/ewelink/lib/payloads/Gladys-offline.json index 0f5bc8fb24..1ecd98bb95 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-offline.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-offline.json @@ -4,18 +4,9 @@ "model": "MINI", "external_id": "ewelink:10004532ae", "selector": "ewelink:10004532ae", - "should_poll": true, - "poll_frequency": 30000, + "should_poll": false, "features": [], "params": [ - { - "name": "IP_ADDRESS", - "value": "?.?.?.?" - }, - { - "name": "FIRMWARE", - "value": "?.?.?" - }, { "name": "ONLINE", "value": "0" diff --git a/server/test/services/ewelink/lib/payloads/Gladys-pow.json b/server/test/services/ewelink/lib/payloads/Gladys-pow.json index d77f7b9424..7664064c48 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-pow.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-pow.json @@ -4,8 +4,7 @@ "model": "Pow", "external_id": "ewelink:10004533ae", "selector": "ewelink:10004533ae", - "should_poll": true, - "poll_frequency": 30000, + "should_poll": false, "features": [ { "name": "Test 3 On/Off", @@ -20,10 +19,6 @@ } ], "params": [ - { - "name": "IP_ADDRESS", - "value": "192.168.0.3" - }, { "name": "FIRMWARE", "value": "3.3.0" diff --git a/server/test/services/ewelink/lib/payloads/Gladys-th.json b/server/test/services/ewelink/lib/payloads/Gladys-th.json index 293f771298..03f8e821e0 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-th.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-th.json @@ -4,8 +4,7 @@ "model": "TH", "external_id": "ewelink:10004534ae", "selector": "ewelink:10004534ae", - "should_poll": true, - "poll_frequency": 30000, + "should_poll": false, "features": [ { "name": "Test 4 On/Off", @@ -44,10 +43,6 @@ } ], "params": [ - { - "name": "IP_ADDRESS", - "value": "192.168.0.4" - }, { "name": "FIRMWARE", "value": "3.1.2" diff --git a/server/test/services/ewelink/lib/payloads/Gladys-unhandled.json b/server/test/services/ewelink/lib/payloads/Gladys-unhandled.json index 9400bb3e94..a06f24770e 100644 --- a/server/test/services/ewelink/lib/payloads/Gladys-unhandled.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-unhandled.json @@ -4,18 +4,9 @@ "model": "UNKNOWN", "external_id": "ewelink:10004535ae", "selector": "ewelink:10004535ae", - "should_poll": true, - "poll_frequency": 30000, + "should_poll": false, "features": [], "params": [ - { - "name": "IP_ADDRESS", - "value": "?.?.?.?" - }, - { - "name": "FIRMWARE", - "value": "?.?.?" - }, { "name": "ONLINE", "value": "1" 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 f4f736a314..214ed0f785 100644 --- a/server/test/services/ewelink/lib/user/ewelink.exchangeToken.test.js +++ b/server/test/services/ewelink/lib/user/ewelink.exchangeToken.test.js @@ -1,10 +1,10 @@ const { expect } = require('chai'); const sinon = require('sinon'); -const { fake, assert } = sinon; +const { fake, assert, match } = sinon; const EwelinkHandler = require('../../../../../services/ewelink/lib'); -const { SERVICE_ID } = require('../constants'); +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,11 +30,20 @@ describe('eWeLinkHandler exchangeToken', () => { getToken: fake.resolves({ data: tokens }), }, }; + eWeLinkHandler.ewelinkWebSocketClientFactory = { + Connect: { + create: fake.returns({}), + }, + }; eWeLinkHandler.status = { configured: true, connected: false, }; eWeLinkHandler.loginState = 'LOGIN_STATE'; + eWeLinkHandler.configuration = { + applicationId: EWELINK_APP_ID, + applicationRegion: EWELINK_APP_REGION, + }; }); afterEach(() => { @@ -56,6 +65,7 @@ describe('eWeLinkHandler exchangeToken', () => { } assert.notCalled(eWeLinkHandler.ewelinkWebAPIClient.oauth.getToken); + assert.notCalled(eWeLinkHandler.ewelinkWebSocketClientFactory.Connect.create); assert.notCalled(gladys.variable.setValue); assert.notCalled(gladys.event.emit); }); @@ -73,6 +83,15 @@ describe('eWeLinkHandler exchangeToken', () => { redirectUrl, region, }); + assert.calledOnce(eWeLinkHandler.ewelinkWebSocketClientFactory.Connect.create); + assert.calledWithMatch( + eWeLinkHandler.ewelinkWebSocketClientFactory.Connect.create, + match({ appId: EWELINK_APP_ID, region: EWELINK_APP_REGION, at: tokens.accessToken }), + match.func, + match.func, + match.func, + match.func, + ); assert.calledOnceWithExactly(gladys.variable.setValue, 'USER_TOKENS', JSON.stringify(tokens), SERVICE_ID); assert.calledOnceWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, diff --git a/server/test/services/ewelink/lib/version/ewelink.upgrade.test.js b/server/test/services/ewelink/lib/version/ewelink.upgrade.test.js index a3c3bbebd1..0ff4a7e49a 100644 --- a/server/test/services/ewelink/lib/version/ewelink.upgrade.test.js +++ b/server/test/services/ewelink/lib/version/ewelink.upgrade.test.js @@ -5,6 +5,73 @@ const { stub, assert } = sinon; const EwelinkHandler = require('../../../../../services/ewelink/lib'); const { SERVICE_ID } = require('../constants'); +const gladysDevices = [ + { + externalId: 'ewelink:1', + should_poll: true, + poll_frequency: 30000, + features: [{ type: 'binary', category: 'switch' }], + params: [ + { + name: 'ONLINE', + value: '1', + }, + { + name: 'FIRMWARE', + value: '?.?.?', + }, + { + name: 'IP_ADDRESS', + value: 'xx.xx.xx.xx.xx', + }, + ], + }, + { + externalId: 'ewelink:2', + should_poll: false, + features: [{ type: 'binary', category: 'light' }], + params: [ + { + name: 'ONLINE', + value: '1', + }, + { + name: 'FIRMWARE', + value: '3.2.1', + }, + ], + }, +]; + +const expectedGladysDevices = [ + { + externalId: 'ewelink:1', + should_poll: false, + features: [{ type: 'binary', category: 'switch' }], + params: [ + { + name: 'ONLINE', + value: '1', + }, + ], + }, + { + externalId: 'ewelink:2', + should_poll: false, + features: [{ type: 'binary', category: 'light' }], + params: [ + { + name: 'ONLINE', + value: '1', + }, + { + name: 'FIRMWARE', + value: '3.2.1', + }, + ], + }, +]; + describe('eWeLinkHandler init', () => { let eWeLinkHandler; let gladys; @@ -16,6 +83,10 @@ describe('eWeLinkHandler init', () => { setValue: stub(), destroy: stub(), }, + device: { + get: stub().resolves(gladysDevices), + create: stub().resolves({}), + }, }; eWeLinkHandler = new EwelinkHandler(gladys, null, SERVICE_ID); @@ -41,12 +112,19 @@ describe('eWeLinkHandler init', () => { assert.calledOnceWithExactly(gladys.variable.getValue, 'SERVICE_VERSION', SERVICE_ID); assert.callCount(gladys.variable.destroy, 3); - assert.callCount(gladys.variable.setValue, 1); + assert.callCount(gladys.variable.setValue, 2); // v2 assert.calledWithExactly(gladys.variable.destroy, 'EWELINK_EMAIL', SERVICE_ID); assert.calledWithExactly(gladys.variable.destroy, 'EWELINK_PASSWORD', SERVICE_ID); assert.calledWithExactly(gladys.variable.destroy, 'EWELINK_REGION', SERVICE_ID); assert.calledWithExactly(gladys.variable.setValue, 'SERVICE_VERSION', '2', SERVICE_ID); + + // v3 + assert.calledOnceWithExactly(gladys.device.get, { service_id: SERVICE_ID }); + assert.callCount(gladys.device.create, 2); + assert.calledWithExactly(gladys.device.create, expectedGladysDevices[0]); + assert.calledWithExactly(gladys.device.create, expectedGladysDevices[1]); + assert.calledWithExactly(gladys.variable.setValue, 'SERVICE_VERSION', '3', SERVICE_ID); }); }); diff --git a/server/utils/setDeviceFeature.js b/server/utils/setDeviceFeature.js index 12e5368165..63895010b4 100644 --- a/server/utils/setDeviceFeature.js +++ b/server/utils/setDeviceFeature.js @@ -4,7 +4,7 @@ * @param {object} feature - The feature to add. * @returns {object} The device. * @example - * setDeviceParam({ features: [] }, { selector: 'feature' }) + * setDeviceFeature({ features: [] }, { selector: 'feature' }) */ function setDeviceFeature(device, feature) { let { features } = device;