diff --git a/front/src/assets/integrations/cover/ewelink_logo.png b/front/src/assets/integrations/cover/ewelink_logo.png new file mode 100644 index 0000000000..15c5376474 Binary files /dev/null and b/front/src/assets/integrations/cover/ewelink_logo.png differ diff --git a/front/src/components/app.jsx b/front/src/components/app.jsx index 65e4408662..8ab22753de 100644 --- a/front/src/components/app.jsx +++ b/front/src/components/app.jsx @@ -129,6 +129,7 @@ import EweLinkPage from '../routes/integration/all/ewelink/device-page'; import EweLinkEditPage from '../routes/integration/all/ewelink/edit-page'; import EweLinkDiscoverPage from '../routes/integration/all/ewelink/discover-page'; import EweLinkSetupPage from '../routes/integration/all/ewelink/setup-page'; +import EweLinkSetupLoginPage from '../routes/integration/all/ewelink/setup-page/login'; // OpenAI integration import OpenAIPage from '../routes/integration/all/openai/index'; @@ -281,6 +282,7 @@ const AppRouter = connect( + diff --git a/front/src/components/header/index.jsx b/front/src/components/header/index.jsx index 7203c8aefc..188c744441 100644 --- a/front/src/components/header/index.jsx +++ b/front/src/components/header/index.jsx @@ -22,7 +22,8 @@ const PAGES_WITHOUT_HEADER = [ '/confirm-email', '/dashboard/integration/device/google-home/authorize', '/dashboard/integration/device/alexa/authorize', - '/locked' + '/locked', + '/dashboard/integration/device/ewelink/setup/login' ]; const Header = ({ ...props }) => { diff --git a/front/src/config/demo.js b/front/src/config/demo.js index 889e8b9580..d3f223c6bc 100644 --- a/front/src/config/demo.js +++ b/front/src/config/demo.js @@ -3752,6 +3752,21 @@ const data = { ] } ], + 'get /api/v1/service/ewelink/status': { + configured: true, + connected: true + }, + 'get /api/v1/service/ewelink/config': { + application_id: 'ewelink_APPID', + application_secret: 'ewelink_APP_SECRET', + application_region: 'eu' + }, + 'post /api/v1/service/ewelink/config': { + application_id: 'ewelink_APPID', + application_secret: 'ewelink_APP_SECRET', + application_region: 'eu' + }, + 'post /api/v1/service/ewelink/token': {}, 'get /api/v1/service/tp-link': { id: 'c9fe2705-35dc-417b-b6fc-c4bbb9c69886', pod_id: null, diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index beef13dd4e..070cdc31a2 100644 --- a/front/src/config/i18n/de.json +++ b/front/src/config/i18n/de.json @@ -14,6 +14,8 @@ "degreeValue": "{{value}}°", "workInProgress": "In Arbeit …", "save": "Sichern", + "edit": "Bearbeiten", + "cancel": "Abbrechen", "celsius": "C", "fahrenheit": "F", "metersPerSec": "m/s", @@ -1582,16 +1584,27 @@ "setup": { "title": "eWeLink-Einrichtung", "eweLinkDescription": "Du kannst Gladys mit deinem eWeLink-Cloud-Account verbinden, um die zugehörigen Geräte zu steuern.", - "userLabel": "Benutzername", - "userPlaceholder": "eWeLink-Benutzernamen eingeben", - "passwordLabel": "Passwort", - "passwordPlaceholder": "eWeLink-Passwort eingeben", - "saveLabel": "Konfiguration sichern", - "error": "Beim Sichern der Konfiguration ist ein Fehler aufgetreten.", - "connecting": "Konfiguration gesichert. Die Verbindung mit deinem eWeLink-Cloud-Account wird jetzt aufgebaut …", - "connected": "Erfolgreich mit dem eWeLink-Cloud-Account verbunden!", - "connectionError": "Verbinden fehlgeschlagen. Überprüfe deine Konfiguration.", - "ewelinkIntegrationDeprecated": "September 2023: Leider scheint diese Integration aufgrund von Änderungen an der eWeLink-API nicht mehr zu funktionieren." + "applicationSetupLabel": "eWeLink-Anwendung einrichten", + "applicationIdLabel": "Anwendungskennzeichen (APPID)", + "applicationIdPlaceholder": "APPID-Wert eingeben", + "applicationSecretLabel": "Bewerbung Geheim (APP SECRET)", + "applicationSecretPlaceholder": "APP SECRET-Wert eingeben", + "applicationRegionLabel": "Region", + "userConnectedLabel": "Verbunden", + "userNotConnectedLabel": "Kein Benutzer verbunden", + "connectLabel": "Verbinden Sie", + "disconnectLabel": "Trennen Sie die Verbindung", + "loadStatusError": "Beim Abrufen des eWeLink-Integrationsstatus ist ein Fehler aufgetreten. Gladys ist möglicherweise nicht erreichbar.", + "saveConfigError": "Die Konfiguration der eWeLink-Anwendung kann nicht gespeichert werden. Gladys ist möglicherweise nicht erreichbar.", + "loginRedirectError": "Bei der Verbindung zu eWeLink ist ein Fehler aufgetreten. Bitte kontaktieren Sie Gladys Community.", + "exchangeTokenError": "Beim Verbindungsaufbau zum Austausch des eWeLink-Benutzer-Tokens ist ein Fehler aufgetreten. Bitte wenden Sie sich an die Gladys Community.", + "exchangeTokenSuccess": "Authentifizierung erfolgreich, aktuelle Registerkarte kann geschlossen werden.", + "regions": { + "cn": "Festland China", + "as": "Asien", + "us": "Amerikas", + "eu": "Europa" + } }, "error": { "defaultError": "Beim Sichern des Geräts ist ein Fehler aufgetreten.", diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 00576bbfbc..90caa9962a 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -14,6 +14,8 @@ "degreeValue": "{{value}}°", "workInProgress": "Work in progress...", "save": "Save", + "edit": "Edit", + "cancel": "Cancel", "celsius": "C", "fahrenheit": "F", "metersPerSec": "m/s", @@ -1584,16 +1586,27 @@ "setup": { "title": "eWeLink configuration", "eweLinkDescription": "You can connect Gladys to your eWeLink cloud account to command the associated devices.", - "userLabel": "Username", - "userPlaceholder": "Enter eWeLink username", - "passwordLabel": "Password", - "passwordPlaceholder": "Enter eWeLink password", - "saveLabel": "Save configuration", - "error": "An error occured while saving configuration.", - "connecting": "Configuration saved. Now connecting to your eWeLink cloud account...", - "connected": "Connected to the eWeLink cloud account with success !", - "connectionError": "Error while connecting, please check your configuration.", - "ewelinkIntegrationDeprecated": "September 2023: Unfortunately, this integration no longer seems to work due to changes in the eWeLink API." + "applicationSetupLabel": "eWeLink application setup", + "applicationIdLabel": "Application Identifier (APPID)", + "applicationIdPlaceholder": "Enter APPID value", + "applicationSecretLabel": "Application Secret (APP SECRET)", + "applicationSecretPlaceholder": "Enter APP SECRET value", + "applicationRegionLabel": "Region", + "userConnectedLabel": "Connected", + "userNotConnectedLabel": "No user connected", + "connectLabel": "Connect", + "disconnectLabel": "Disconnect", + "loadStatusError": "An error occured while fetching eWeLink integration status. Gladys may be not reachable.", + "saveConfigError": "Unable to save eWeLink application configuration. Gladys may be not reachable.", + "loginRedirectError": "An error occured while connecting to eWeLink, please contact Gladys community.", + "exchangeTokenError": "An error occured while connecting to exchanging eWeLink user token, please contact Gladys community.", + "exchangeTokenSuccess": "Authentication succeed, current tab can be closed.", + "regions": { + "cn": "Mainland China", + "as": "Asia", + "us": "Americas", + "eu": "Europe" + } }, "error": { "defaultError": "There was an error saving the device.", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 87c227e013..7da6956d5b 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -14,6 +14,8 @@ "degreeValue": "{{value}}°", "workInProgress": "Travail en cours...", "save": "Enregistrer", + "edit": "Modifier", + "cancel": "Annuler", "celsius": "C", "fahrenheit": "F", "metersPerSec": "m/s", @@ -1586,16 +1588,27 @@ "setup": { "title": "Configuration eWeLink", "eweLinkDescription": "Vous pouvez connecter Gladys à votre compte cloud eWeLink pour commander les appareils associés.", - "userLabel": "Nom d'utilisateur", - "userPlaceholder": "Entrez le nom d'utilisateur eWeLink", - "passwordLabel": "Mot de passe", - "passwordPlaceholder": "Entrez le mot de passe utilisateur eWeLink", - "saveLabel": "Enregistrer la configuration", - "error": "Une erreur s'est produite lors de la sauvegarde de la configuration.", - "connecting": "Configuration sauvegardée. Connexion à votre compte cloud eWeLink...", - "connected": "Connexion réussie au compte cloud eWeLink !", - "connectionError": "Erreur lors de la connexion, veuillez vérifier votre configuration.", - "ewelinkIntegrationDeprecated": "Septembre 2023: Cette intégration ne semble malheureusement plus fonctionner suite à des changements côté API eWeLink. Discussion sur le forum ici." + "applicationSetupLabel": "Configuration de l'application eWeLink", + "applicationIdLabel": "Identifiant de l'application (APPID)", + "applicationIdPlaceholder": "Entrez la valeur de APPID", + "applicationSecretLabel": "Secret de l'application (APP SECRET)", + "applicationSecretPlaceholder": "Entrez la valeur de APP SECRET", + "applicationRegionLabel": "Region", + "userConnectedLabel": "Connecté", + "userNotConnectedLabel": "Aucun utilisateur connecté", + "connectLabel": "Connexion", + "disconnectLabel": "Déconnexion", + "loadStatusError": "Impossible de remonter les informations sur l'état de l'intégration eWeLink. Gladys ne semble pas joignable.", + "saveConfigError": "Impossible d'enregistrer la configuration de l'application eWeLink. Gladys ne semble pas joignable.", + "loginRedirectError": "Une erreur est survenue lors de la connexion à eWeLink, merci de contacter la communauté.", + "exchangeTokenError": "Une erreur est survenue lors de la génération du jeton utilisateur eWeLink, merci de contacter la communauté.", + "exchangeTokenSuccess": "L'authentification est un succès, cette page peut être fermée.", + "regions": { + "cn": "Chine", + "as": "Asie", + "us": "Ameriques", + "eu": "Europe" + } }, "error": { "defaultError": "Une erreur s'est produite lors de l'enregistrement de l'appareil.", diff --git a/front/src/routes/integration/all/ewelink/EweLinkDeviceBox.jsx b/front/src/routes/integration/all/ewelink/EweLinkDeviceBox.jsx index 96faf9f9f5..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; + 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/front/src/routes/integration/all/ewelink/actions.js b/front/src/routes/integration/all/ewelink/actions.js index 81fbb85621..d16b1e90f6 100644 --- a/front/src/routes/integration/all/ewelink/actions.js +++ b/front/src/routes/integration/all/ewelink/actions.js @@ -6,83 +6,6 @@ import createActionsIntegration from '../../../../actions/integration'; function createActions(store) { const integrationActions = createActionsIntegration(store); const actions = { - async loadProps(state) { - let eweLinkUsername; - let eweLinkPassword; - try { - eweLinkUsername = await state.httpClient.get('/api/v1/service/ewelink/variable/EWELINK_EMAIL'); - if (eweLinkUsername.value) { - eweLinkPassword = '*********'; // this is just used so that the field is filled - } - } finally { - store.setState({ - eweLinkUsername: (eweLinkUsername || { value: '' }).value, - eweLinkPassword, - passwordChanges: false, - connected: false - }); - } - }, - updateConfigration(state, e) { - const data = {}; - data[e.target.name] = e.target.value; - if (e.target.name === 'eweLinkPassword') { - data.passwordChanges = true; - } - store.setState(data); - }, - async saveConfiguration(state) { - event.preventDefault(); - store.setState({ - connectEweLinkStatus: RequestStatus.Getting, - eweLinkConnected: false, - eweLinkConnectionError: undefined - }); - try { - await state.httpClient.post('/api/v1/service/ewelink/variable/EWELINK_EMAIL', { - value: state.eweLinkUsername - }); - if (state.passwordChanges) { - await state.httpClient.post('/api/v1/service/ewelink/variable/EWELINK_PASSWORD', { - value: state.eweLinkPassword - }); - } - await state.httpClient.post(`/api/v1/service/ewelink/connect`); - - store.setState({ - connectEweLinkStatus: RequestStatus.Success - }); - - setTimeout(() => store.setState({ connectEweLinkStatus: undefined }), 3000); - } catch (e) { - store.setState({ - connectEweLinkStatus: RequestStatus.Error, - passwordChanges: false - }); - } - }, - displayConnectedMessage() { - // display 3 seconds a message "EweLink connected" - store.setState({ - eweLinkConnected: true, - eweLinkConnectionError: undefined - }); - setTimeout( - () => - store.setState({ - eweLinkConnected: false, - connectEweLinkStatus: undefined - }), - 3000 - ); - }, - displayEweLinkError(state, error) { - store.setState({ - eweLinkConnected: false, - connectEweLinkStatus: undefined, - eweLinkConnectionError: error - }); - }, async getEweLinkDevices(state) { store.setState({ getEweLinkStatus: RequestStatus.Getting @@ -155,23 +78,6 @@ function createActions(store) { [listName]: devices }); }, - updateFeatureProperty(state, listName, deviceIndex, featureIndex, property, value) { - const devices = update(state[listName], { - [deviceIndex]: { - features: { - [featureIndex]: { - [property]: { - $set: value - } - } - } - } - }); - - store.setState({ - [listName]: devices - }); - }, async saveDevice(state, listName, index) { const device = state[listName][index]; const savedDevice = await state.httpClient.post(`/api/v1/device`, device); diff --git a/front/src/routes/integration/all/ewelink/setup-page/ApplicationSetup.jsx b/front/src/routes/integration/all/ewelink/setup-page/ApplicationSetup.jsx new file mode 100644 index 0000000000..6ab6aeb749 --- /dev/null +++ b/front/src/routes/integration/all/ewelink/setup-page/ApplicationSetup.jsx @@ -0,0 +1,192 @@ +import { Component } from 'preact'; +import { Localizer, Text } from 'preact-i18n'; +import cx from 'classnames'; + +import { REGIONS } from './constants'; + +class ApplicationSetup extends Component { + showPasswordTimer = null; + + updateApplicationId = event => { + const { value: applicationId } = event.target; + this.setState({ applicationId }); + }; + + updateApplicationSecret = event => { + const { value: applicationSecret } = event.target; + this.setState({ applicationSecret }); + }; + + updateApplicationRegion = event => { + const { value: applicationRegion } = event.target; + this.setState({ applicationRegion }); + }; + + saveConfiguration = async event => { + event.preventDefault(); + const { applicationId, applicationSecret, applicationRegion } = this.state; + const configuration = { applicationId, applicationSecret, applicationRegion }; + this.props.saveConfiguration(configuration); + }; + + resetConfiguration = event => { + event.preventDefault(); + const { ewelinkConfig = {} } = this.props; + const { applicationId = '', applicationSecret = '', applicationRegion } = ewelinkConfig; + this.setState({ applicationId, applicationSecret, applicationRegion }); + this.props.resetConfiguration(); + }; + + togglePassword = () => { + const { showPassword } = this.state; + + if (this.showPasswordTimer) { + clearTimeout(this.showPasswordTimer); + this.showPasswordTimer = null; + } + + this.setState({ showPassword: !showPassword }); + + if (!showPassword) { + this.showPasswordTimer = setTimeout(() => this.setState({ showPassword: false }), 5000); + } + }; + + constructor(props) { + super(props); + + const { ewelinkConfig = {} } = props; + const { applicationId = '', applicationSecret = '', applicationRegion } = ewelinkConfig; + this.state = { + applicationId, + applicationSecret, + applicationRegion + }; + } + + componentWillUnmount() { + if (this.showPasswordTimer) { + clearTimeout(this.showPasswordTimer); + this.showPasswordTimer = null; + } + } + + componentWillReceiveProps(nextProps) { + const { ewelinkConfig = {} } = nextProps; + const { applicationId = '', applicationSecret = '', applicationRegion } = ewelinkConfig; + const { + applicationId: currentApplicationId, + applicationSecret: currentApplicationSecret, + applicationRegion: currentApplicationRegion + } = this.state; + + if ( + applicationId !== currentApplicationId || + applicationSecret !== currentApplicationSecret || + applicationRegion !== currentApplicationRegion + ) { + this.setState({ applicationId, applicationSecret, applicationRegion }); + } + } + + render({ disabled }, { applicationId, applicationSecret, applicationRegion, showPassword }) { + const saveDisabled = applicationId === '' || applicationSecret === '' || !applicationRegion; + return ( +
+
+ + + } + value={applicationId} + onInput={this.updateApplicationId} + disabled={disabled} + autocomplete="off" + required + data-cy="ewelink-application-setup-app-id" + /> + +
+
+ +
+ + } + value={applicationSecret} + onInput={this.updateApplicationSecret} + disabled={disabled} + autocomplete="off" + required + data-cy="ewelink-application-setup-app-secret" + /> + + + + +
+
+
+ +
+ + + +
+
+
+ + +
+ + ); + } +} + +export default ApplicationSetup; diff --git a/front/src/routes/integration/all/ewelink/setup-page/SetupSummary.jsx b/front/src/routes/integration/all/ewelink/setup-page/SetupSummary.jsx new file mode 100644 index 0000000000..3efc2d4337 --- /dev/null +++ b/front/src/routes/integration/all/ewelink/setup-page/SetupSummary.jsx @@ -0,0 +1,75 @@ +import { Fragment } from 'preact'; +import { Text } from 'preact-i18n'; +import cx from 'classnames'; + +const SetupSummary = ({ ewelinkStatus = {}, enableEditionMode, disabled, connectUser, disconnectUser }) => { + const { configured, connected } = ewelinkStatus; + + return ( +
+
+
+ +
+ + +
+
+
+ +
+ {connected && ( + + + + + )} + {!connected && ( + + +
+ {configured && ( + + )} +
+
+ )} +
+
+ ); +}; + +export default SetupSummary; diff --git a/front/src/routes/integration/all/ewelink/setup-page/SetupTab.jsx b/front/src/routes/integration/all/ewelink/setup-page/SetupTab.jsx index 850603b2c9..ebb641fa73 100644 --- a/front/src/routes/integration/all/ewelink/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/ewelink/setup-page/SetupTab.jsx @@ -1,96 +1,115 @@ -import { Text, Localizer, MarkupText } from 'preact-i18n'; +import { Component } from 'preact'; +import { Text } from 'preact-i18n'; import cx from 'classnames'; import { RequestStatus } from '../../../../../utils/consts'; -const SetupTab = ({ children, ...props }) => { - return ( -
-
-

- -

-
-
-
-
-
-

- -

-

- -

- {props.connectEweLinkStatus === RequestStatus.Error && !props.eweLinkConnectionError && ( -

- -

- )} - {props.connectEweLinkStatus === RequestStatus.Success && !props.eweLinkConnected && ( -

- -

- )} - {props.eweLinkConnected && ( -

- -

- )} - {props.eweLinkConnectionError && ( -

- -

- )} +import ApplicationSetup from './ApplicationSetup'; +import SetupSummary from './SetupSummary'; + +class SetupTab extends Component { + enableEditionMode = () => { + this.setState({ editionMode: true }); + }; + + resetConfiguration = () => { + const { ewelinkStatus = {} } = this.props; + const { configured = false } = ewelinkStatus; + this.setState({ + editionMode: !configured + }); + }; + + constructor(props) { + super(props); -
-
- - - } - value={props.eweLinkUsername} - class="form-control" - onInput={props.updateConfigration} - /> - -
+ const { ewelinkStatus = {} } = props; + const { configured = false } = ewelinkStatus; + this.state = { + editionMode: !configured + }; + } -
- - - } - value={props.eweLinkPassword} - class="form-control" - onInput={props.updateConfigration} - /> - -
+ componentWillReceiveProps(nextProps) { + const { ewelinkStatus = {} } = nextProps; -
-
- -
-
-
+ if (ewelinkStatus.configured && this.state.editionMode) { + this.setState({ editionMode: false }); + } + } + + render( + { + ewelinkStatus, + loadEwelinkStatus = RequestStatus.Getting, + ewelinkConfig, + loadEwelinkConfig = RequestStatus.Getting, + saveEwelinkConfig, + saveConfiguration, + connectUser, + loadConnectUser, + disconnectUser, + loadDisconnectUser + }, + { editionMode } + ) { + const formDisabled = + saveEwelinkConfig === RequestStatus.Getting || + loadConnectUser === RequestStatus.Getting || + loadDisconnectUser === RequestStatus.Getting; + + return ( +
+
+

+ +

+
+
+

+ +

+
+
+
+ {loadEwelinkStatus === RequestStatus.Error && ( +

+ +

+ )} + {saveEwelinkConfig === RequestStatus.Error && ( +

+ +

+ )} + {editionMode && ( + + )} + {!editionMode && ( + + )} +
-
- ); -}; + ); + } +} export default SetupTab; diff --git a/front/src/routes/integration/all/ewelink/setup-page/constants.js b/front/src/routes/integration/all/ewelink/setup-page/constants.js new file mode 100644 index 0000000000..2a7afd0984 --- /dev/null +++ b/front/src/routes/integration/all/ewelink/setup-page/constants.js @@ -0,0 +1,4 @@ +const REGIONS = ['eu', 'us', 'as', 'cn']; +const DEFAULT_REGION = REGIONS[0]; + +export { REGIONS, DEFAULT_REGION }; diff --git a/front/src/routes/integration/all/ewelink/setup-page/index.js b/front/src/routes/integration/all/ewelink/setup-page/index.js index 7f418588d8..c5a0847582 100644 --- a/front/src/routes/integration/all/ewelink/setup-page/index.js +++ b/front/src/routes/integration/all/ewelink/setup-page/index.js @@ -1,39 +1,170 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; -import actions from '../actions'; + import EweLinkPage from '../EweLinkPage'; import SetupTab from './SetupTab'; +import { DEFAULT_REGION } from './constants'; + import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants'; +import { RequestStatus } from '../../../../../utils/consts'; class EweLinkSetupPage extends Component { - componentWillMount() { - this.props.getIntegrationByName('ewelink'); - this.props.loadProps(); - this.props.session.dispatcher.addListener( - WEBSOCKET_MESSAGE_TYPES.EWELINK.CONNECTED, - this.props.displayConnectedMessage - ); - this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.EWELINK.ERROR, this.props.displayEweLinkError); + loadEwelinkStatus = async () => { + this.setState({ + loadEwelinkStatus: RequestStatus.Getting + }); + try { + const { configured, connected } = await this.props.httpClient.get('/api/v1/service/ewelink/status'); + const ewelinkStatus = { configured, connected }; + this.setState({ + ewelinkStatus, + loadEwelinkStatus: RequestStatus.Success + }); + } catch (e) { + console.error('eWeLink error loading status', e); + this.setState({ + loadEwelinkStatus: RequestStatus.Error + }); + } + }; + + loadEwelinkConfig = async () => { + this.setState({ + loadEwelinkConfig: RequestStatus.Getting + }); + try { + const { + application_id: applicationId, + application_secret: applicationSecret, + application_region: applicationRegion = DEFAULT_REGION + } = await this.props.httpClient.get('/api/v1/service/ewelink/config'); + this.setState({ + ewelinkConfig: { applicationId, applicationSecret, applicationRegion }, + loadEwelinkConfig: RequestStatus.Success + }); + } catch (e) { + console.error('eWeLink error loading config', e); + this.setState({ + loadEwelinkConfig: RequestStatus.Error + }); + } + }; + + saveEwelinkConfig = async config => { + this.setState({ + saveEwelinkConfig: RequestStatus.Getting + }); + try { + const { applicationId, applicationSecret, applicationRegion } = config; + const savedConfig = await this.props.httpClient.post('/api/v1/service/ewelink/config', { + application_id: applicationId, + application_secret: applicationSecret, + application_region: applicationRegion + }); + const ewelinkConfig = { + applicationId: savedConfig.application_id, + applicationSecret: savedConfig.application_secret, + applicationRegion: savedConfig.application_secret + }; + this.setState({ + ewelinkConfig, + saveEwelinkConfig: RequestStatus.Success + }); + } catch (e) { + console.error('eWeLink error saving config', e); + this.setState({ + saveEwelinkConfig: RequestStatus.Error + }); + } + }; + + connectUser = async () => { + this.setState({ loadConnectUser: RequestStatus.Getting }); + + const { origin, pathname } = window.location; + try { + const { login_url: loginUrl } = await this.props.httpClient.get('/api/v1/service/ewelink/loginUrl', { + redirect_url: `${origin}${pathname}/login` + }); + const loginPopup = window.open(loginUrl, 'ewelinkLogin'); + this.setState({ loadConnectUser: RequestStatus.Success, loginPopup }); + } catch (e) { + console.error(e); + this.setState({ loadConnectUser: RequestStatus.Error }); + } + }; + + disconnectUser = async () => { + this.setState({ loadDisconnectUser: RequestStatus.Getting }); + + try { + await this.props.httpClient.delete('/api/v1/service/ewelink/token'); + this.setState({ loadDisconnectUser: RequestStatus.Success }); + } catch (e) { + console.error(e); + this.setState({ loadDisconnectUser: RequestStatus.Error }); + } + }; + + updateStatus = payload => { + const { configured, connected } = payload; + const ewelinkStatus = { configured, connected }; + + const { loginPopup, ewelinkStatus: currentStatus = {} } = this.state; + if (!currentStatus.connected && connected && loginPopup && !loginPopup.closed) { + loginPopup.close(); + } + + this.setState({ ewelinkStatus }); + }; + + constructor(props) { + super(props); + + this.state = { + loadEwelinkStatus: RequestStatus.Getting + }; + } + + componentDidMount() { + this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, this.updateStatus); + this.loadEwelinkStatus(); + this.loadEwelinkConfig(); } componentWillUnmount() { - this.props.session.dispatcher.removeListener( - WEBSOCKET_MESSAGE_TYPES.EWELINK.CONNECTED, - this.props.displayConnectedMessage - ); - this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.EWELINK.ERROR, this.props.displayEweLinkError); + this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, this.updateStatus); } - render(props, {}) { + render( + { user }, + { + loadEwelinkStatus, + ewelinkStatus, + loadEwelinkConfig, + ewelinkConfig, + saveEwelinkConfig, + loadDisconnectUser, + loadConnectUser + } + ) { return ( - - + + ); } } -export default connect( - 'user,session,eweLinkUsername,eweLinkPassword,connectEweLinkStatus,eweLinkConnected,eweLinkConnectionError', - actions -)(EweLinkSetupPage); +export default connect('user,session,httpClient')(EweLinkSetupPage); diff --git a/front/src/routes/integration/all/ewelink/setup-page/login/ErrorPage.jsx b/front/src/routes/integration/all/ewelink/setup-page/login/ErrorPage.jsx new file mode 100644 index 0000000000..97698bc65a --- /dev/null +++ b/front/src/routes/integration/all/ewelink/setup-page/login/ErrorPage.jsx @@ -0,0 +1,10 @@ +const ErrorPage = ({ children }) => ( +
+
+ +
+ {children} +
+); + +export default ErrorPage; diff --git a/front/src/routes/integration/all/ewelink/setup-page/login/ExchangeTokenPage.jsx b/front/src/routes/integration/all/ewelink/setup-page/login/ExchangeTokenPage.jsx new file mode 100644 index 0000000000..20ebf4aa91 --- /dev/null +++ b/front/src/routes/integration/all/ewelink/setup-page/login/ExchangeTokenPage.jsx @@ -0,0 +1,75 @@ +import { Component } from 'preact'; +import cx from 'classnames'; + +import { RequestStatus } from '../../../../../../utils/consts'; +import { Text } from 'preact-i18n'; +import ErrorPage from './ErrorPage'; + +class ExchangeTokenPage extends Component { + exchangeToken = async () => { + this.setState({ + exchangeTokenStatus: RequestStatus.Getting, + exchangeTokenError: null + }); + + try { + const { code, region, redirectUrl, state } = this.props; + + await this.props.httpClient.post('/api/v1/service/ewelink/token', { + code, + region, + redirect_url: redirectUrl, + state + }); + this.setState({ + exchangeTokenStatus: RequestStatus.Success + }); + } catch (e) { + console.error(e); + console.error(e.response); + this.setState({ + exchangeTokenStatus: RequestStatus.Error, + exchangeTokenError: e + }); + } + }; + + componentDidMount() { + this.exchangeToken(); + } + + render({}, { exchangeTokenStatus = RequestStatus.Getting, exchangeTokenError }) { + const { response = {} } = exchangeTokenError || {}; + const { data = {} } = response; + const { message } = data; + + return ( +
+
+
+ {exchangeTokenStatus === RequestStatus.Error && ( + +
+ +
+ {message ? message : exchangeTokenError} +
+
+ )} + {exchangeTokenStatus === RequestStatus.Success && ( +
+ +
+ )} +
+
+ ); + } +} + +export default ExchangeTokenPage; diff --git a/front/src/routes/integration/all/ewelink/setup-page/login/index.js b/front/src/routes/integration/all/ewelink/setup-page/login/index.js new file mode 100644 index 0000000000..98d9379c82 --- /dev/null +++ b/front/src/routes/integration/all/ewelink/setup-page/login/index.js @@ -0,0 +1,109 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import { Localizer, Text } from 'preact-i18n'; + +import { RequestStatus } from '../../../../../../utils/consts'; +import ErrorPage from './ErrorPage'; +import ExchangeTokenPage from './ExchangeTokenPage'; + +class EweLinkSetupLoginPage extends Component { + exchangeToken = async config => { + this.setState({ + saveEwelinkConfig: RequestStatus.Getting + }); + try { + const { applicationId, applicationSecret, applicationRegion } = config; + const savedConfig = await this.props.httpClient.post('/api/v1/service/ewelink/token', { + application_id: applicationId, + application_secret: applicationSecret, + application_region: applicationRegion + }); + const ewelinkConfig = { + applicationId: savedConfig.application_id, + applicationSecret: savedConfig.application_secret, + applicationRegion: savedConfig.application_secret + }; + this.setState({ + ewelinkConfig, + saveEwelinkConfig: RequestStatus.Success + }); + } catch (e) { + console.error('eWeLink error saving config', e); + this.setState({ + saveEwelinkConfig: RequestStatus.Error + }); + } + }; + + constructor(props) { + super(props); + + const { search: queryString, origin, pathname } = window.location; + const queryParams = new URLSearchParams(queryString); + + // Map query params to JSON Object + const params = {}; + for (const [key, value] of queryParams.entries()) { + params[key] = value; + } + + this.state = { + params, + redirectUrl: `${origin}${pathname}` + }; + } + + render({ httpClient }, { params, redirectUrl }) { + const { code, region, state } = params; + const success = code && region && state; + + return ( +
+
+
+
+
+
+
+
+
+
+

+ + {<Text} + /> + + +

+
+ {success && ( + + )} + {!success && ( + + + + )} +
+
+
+
+
+
+
+
+
+ ); + } +} + +export default connect('user,session,httpClient')(EweLinkSetupLoginPage); diff --git a/server/services/ewelink/api/ewelink.controller.js b/server/services/ewelink/api/ewelink.controller.js index c81fbeef50..f78f825263 100644 --- a/server/services/ewelink/api/ewelink.controller.js +++ b/server/services/ewelink/api/ewelink.controller.js @@ -2,24 +2,86 @@ const asyncMiddleware = require('../../../api/middlewares/asyncMiddleware'); module.exports = function EwelinkController(eweLinkHandler) { /** - * @api {post} /api/v1/service/ewelink/connect Connect to eWeLink cloud account. - * @apiName save + * @api {get} /api/v1/service/ewelink/config Get eWeLink application configuration. + * @apiName getConfig * @apiGroup Ewelink */ - async function connect(req, res) { - await eweLinkHandler.connect(); + function getConfig(req, res) { + const { applicationId, applicationSecret, applicationRegion } = eweLinkHandler.configuration; res.json({ - success: true, + application_id: applicationId, + application_secret: applicationSecret, + application_region: applicationRegion, }); } + /** + * @api {post} /api/v1/service/ewelink/config Save eWeLink application configuration. + * @apiName saveConfig + * @apiGroup Ewelink + */ + async function saveConfig(req, res) { + const { + application_id: applicationId, + application_secret: applicationSecret, + application_region: applicationRegion, + } = req.body; + + await eweLinkHandler.saveConfiguration({ + applicationId, + applicationSecret, + applicationRegion, + }); + + getConfig(req, res); + } + + /** + * @api {get} /api/v1/service/ewelink/loginUrl Gets the eWelink login URL. + * @apiName loginUrl + * @apiGroup Ewelink + */ + async function loginUrl(req, res) { + const { redirect_url: redirectUrl } = req.query; + const ewelinkLoginUrl = await eweLinkHandler.buildLoginUrl({ + redirectUrl, + }); + res.json({ login_url: ewelinkLoginUrl }); + } + + /** + * @api {post} /api/v1/service/ewelink/token Generates a user token. + * @apiName exchangeToken + * @apiGroup Ewelink + */ + async function exchangeToken(req, res) { + const { redirect_url: redirectUrl, code, region, state } = req.body; + await eweLinkHandler.exchangeToken({ + redirectUrl, + code, + region, + state, + }); + res.json({ success: true }); + } + + /** + * @api {delete} /api/v1/service/ewelink/token Delete stored tokens and logout user. + * @apiName deleteTokens + * @apiGroup Ewelink + */ + async function deleteTokens(req, res) { + await eweLinkHandler.deleteTokens(); + res.json({ success: true }); + } + /** * @api {get} /api/v1/service/ewelink/status Get eWeLink connection status. * @apiName status * @apiGroup Ewelink */ async function status(req, res) { - const response = eweLinkHandler.status(); + const response = eweLinkHandler.getStatus(); res.json(response); } @@ -34,10 +96,30 @@ module.exports = function EwelinkController(eweLinkHandler) { } return { - 'post /api/v1/service/ewelink/connect': { + 'get /api/v1/service/ewelink/config': { + authenticated: true, + admin: true, + controller: asyncMiddleware(getConfig), + }, + 'post /api/v1/service/ewelink/config': { + authenticated: true, + admin: true, + controller: asyncMiddleware(saveConfig), + }, + 'get /api/v1/service/ewelink/loginUrl': { + authenticated: true, + admin: true, + controller: asyncMiddleware(loginUrl), + }, + 'post /api/v1/service/ewelink/token': { + authenticated: true, + admin: true, + controller: asyncMiddleware(exchangeToken), + }, + 'delete /api/v1/service/ewelink/token': { authenticated: true, admin: true, - controller: asyncMiddleware(connect), + controller: asyncMiddleware(deleteTokens), }, 'get /api/v1/service/ewelink/status': { authenticated: true, diff --git a/server/services/ewelink/index.js b/server/services/ewelink/index.js index 299ca1a1a2..3991995b2e 100644 --- a/server/services/ewelink/index.js +++ b/server/services/ewelink/index.js @@ -1,11 +1,11 @@ const logger = require('../../utils/logger'); -const EweLinkHandler = require('./lib/device'); +const EweLinkHandler = require('./lib'); const EwelinkController = require('./api/ewelink.controller'); module.exports = function EwelinkService(gladys, serviceId) { // require the eWeLink module - const eWeLinkApi = require('ewelink-api'); - const eWeLinkHandler = new EweLinkHandler(gladys, eWeLinkApi, serviceId); + const { default: eweLinkApi } = require('ewelink-api-next'); + const eWeLinkHandler = new EweLinkHandler(gladys, eweLinkApi, serviceId); /** * @public @@ -15,7 +15,7 @@ module.exports = function EwelinkService(gladys, serviceId) { */ async function start() { logger.info('Starting eWeLink service'); - await eWeLinkHandler.connect(); + await eWeLinkHandler.init(); } /** @@ -26,6 +26,7 @@ module.exports = function EwelinkService(gladys, serviceId) { */ async function stop() { logger.info('Stopping eWeLink service'); + await eWeLinkHandler.stop(); } return Object.freeze({ diff --git a/server/services/ewelink/lib/config/ewelink.createClients.js b/server/services/ewelink/lib/config/ewelink.createClients.js new file mode 100644 index 0000000000..b033361ffa --- /dev/null +++ b/server/services/ewelink/lib/config/ewelink.createClients.js @@ -0,0 +1,24 @@ +/** + * @description Create eWeLink Web API and WebSocket clients. + * @example + * this.createClients(); + */ +function createClients() { + const { applicationId, applicationSecret, applicationRegion } = this.configuration; + + this.ewelinkWebAPIClient = new this.eweLinkApi.WebAPI({ + appId: applicationId, + appSecret: applicationSecret, + region: applicationRegion, + }); + + this.ewelinkWebSocketClient = new this.eweLinkApi.Ws({ + appId: applicationId, + appSecret: applicationSecret, + region: applicationRegion, + }); +} + +module.exports = { + createClients, +}; diff --git a/server/services/ewelink/lib/config/ewelink.getStatus.js b/server/services/ewelink/lib/config/ewelink.getStatus.js new file mode 100644 index 0000000000..58e84dfc30 --- /dev/null +++ b/server/services/ewelink/lib/config/ewelink.getStatus.js @@ -0,0 +1,13 @@ +/** + * @description Get eWeLink status. + * @returns {object} Current eWeLink network status. + * @example + * this.getStatus(); + */ +function getStatus() { + return this.status; +} + +module.exports = { + getStatus, +}; diff --git a/server/services/ewelink/lib/config/ewelink.loadConfiguration.js b/server/services/ewelink/lib/config/ewelink.loadConfiguration.js new file mode 100644 index 0000000000..bfb68a0a7a --- /dev/null +++ b/server/services/ewelink/lib/config/ewelink.loadConfiguration.js @@ -0,0 +1,68 @@ +const { ServiceNotConfiguredError } = require('../../../../utils/coreErrors'); +const logger = require('../../../../utils/logger'); +const { CONFIGURATION_KEYS } = require('../utils/constants'); + +/** + * @description Load eWeLink configuration. + * @example + * await this.loadConfiguration(); + */ +async function loadConfiguration() { + logger.info('eWeLink: loading stored configuration...'); + this.updateStatus({ configured: false, connected: false }); + + try { + const applicationId = await this.gladys.variable.getValue(CONFIGURATION_KEYS.APPLICATION_ID, this.serviceId); + const applicationSecret = await this.gladys.variable.getValue( + CONFIGURATION_KEYS.APPLICATION_SECRET, + this.serviceId, + ); + const applicationRegion = await this.gladys.variable.getValue( + CONFIGURATION_KEYS.APPLICATION_REGION, + this.serviceId, + ); + + if (!applicationId || !applicationSecret || !applicationRegion) { + throw new ServiceNotConfiguredError('eWeLink configuration is not setup'); + } + + this.configuration = { applicationId, applicationSecret, applicationRegion }; + + this.createClients(); + } catch (e) { + this.updateStatus({ configured: false, connected: false }); + throw e; + } + + try { + // Load tokens from databate + const tokens = await this.gladys.variable.getValue(CONFIGURATION_KEYS.USER_TOKENS, this.serviceId); + if (!tokens) { + throw new ServiceNotConfiguredError('eWeLink user is not connected'); + } + + const tokenObject = JSON.parse(tokens); + + 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...'); + } catch (e) { + this.updateStatus({ configured: true, connected: false }); + throw e; + } + + this.updateStatus({ configured: true, connected: true }); +} + +module.exports = { + loadConfiguration, +}; diff --git a/server/services/ewelink/lib/config/ewelink.saveConfiguration.js b/server/services/ewelink/lib/config/ewelink.saveConfiguration.js new file mode 100644 index 0000000000..78384162a5 --- /dev/null +++ b/server/services/ewelink/lib/config/ewelink.saveConfiguration.js @@ -0,0 +1,44 @@ +const { BadParameters } = require('../../../../utils/coreErrors'); +const logger = require('../../../../utils/logger'); +const { CONFIGURATION_KEYS } = require('../utils/constants'); + +/** + * @description Save eWeLink application configuration. + * @param {object} configuration - EWeLink application configuration. + * @param {string} [configuration.applicationId] - Application ID. + * @param {string} [configuration.applicationSecret] - Application secret. + * @param {string} [configuration.applicationRegion] - Application region. + * @example + * await this.saveConfiguration(configuration); + */ +async function saveConfiguration({ applicationId = '', applicationSecret = '', applicationRegion = '' }) { + logger.info('eWeLink: saving new configuration...'); + + if (applicationId === '' || applicationSecret === '' || applicationRegion === '') { + throw new BadParameters('eWeLink: all application ID/Secret/Region are required'); + } + + this.updateStatus({ configured: false, connected: false }); + + try { + await this.gladys.variable.setValue(CONFIGURATION_KEYS.APPLICATION_ID, applicationId, this.serviceId); + await this.gladys.variable.setValue(CONFIGURATION_KEYS.APPLICATION_SECRET, applicationSecret, this.serviceId); + await this.gladys.variable.setValue(CONFIGURATION_KEYS.APPLICATION_REGION, applicationRegion, this.serviceId); + await this.gladys.variable.destroy(CONFIGURATION_KEYS.USER_TOKENS, this.serviceId); + + this.configuration = { applicationId, applicationSecret, applicationRegion }; + + this.closeWebSocketClient(); + this.createClients(); + + this.updateStatus({ configured: true, connected: false }); + logger.info('eWeLink: new configuration well saved...'); + } catch (e) { + this.updateStatus({ configured: false, connected: false }); + throw e; + } +} + +module.exports = { + saveConfiguration, +}; diff --git a/server/services/ewelink/lib/config/ewelink.updateStatus.js b/server/services/ewelink/lib/config/ewelink.updateStatus.js new file mode 100644 index 0000000000..1297015eec --- /dev/null +++ b/server/services/ewelink/lib/config/ewelink.updateStatus.js @@ -0,0 +1,20 @@ +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); + +/** + * @description Update the service status and emit WebSocket event. + * @param {object} newStatus - New service status. + * @example + * this.updateStatus({ configured: true }); + */ +function updateStatus(newStatus) { + this.status = { ...this.status, ...newStatus }; + + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: this.status, + }); +} + +module.exports = { + updateStatus, +}; diff --git a/server/services/ewelink/lib/device/connect.js b/server/services/ewelink/lib/device/connect.js deleted file mode 100644 index e0bf91419d..0000000000 --- a/server/services/ewelink/lib/device/connect.js +++ /dev/null @@ -1,56 +0,0 @@ -const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); -const { ServiceNotConfiguredError } = require('../../../../utils/coreErrors'); -const { EWELINK_EMAIL_KEY, EWELINK_PASSWORD_KEY, EWELINK_REGION_KEY, EWELINK_REGIONS } = require('../utils/constants'); - -/** - * @description Connect to eWeLink cloud account and get access token and api key. - * @example - * connect(); - */ -async function connect() { - this.configured = false; - this.connected = false; - - const email = await this.gladys.variable.getValue(EWELINK_EMAIL_KEY, this.serviceId); - const password = await this.gladys.variable.getValue(EWELINK_PASSWORD_KEY, this.serviceId); - let region = await this.gladys.variable.getValue(EWELINK_REGION_KEY, this.serviceId); - - if (!email || !password) { - this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.ERROR, - payload: 'Service is not configured', - }); - throw new ServiceNotConfiguredError('eWeLink: Error, service is not configured'); - } - - if (!Object.values(EWELINK_REGIONS).includes(region)) { - const connection = new this.EweLinkApi({ email, password }); - const response = await connection.getRegion(); - // belt, suspenders ;) - if (response.error && [401, 406].indexOf(response.error) !== -1) { - response.msg = 'Service is not configured'; - } - await this.throwErrorIfNeeded(response, true, true); - - ({ region } = response); - await this.gladys.variable.setValue(EWELINK_REGION_KEY, region, this.serviceId); - } - - this.configured = true; - - const connection = new this.EweLinkApi({ email, password, region }); - const auth = await connection.getCredentials(); - await this.throwErrorIfNeeded(auth, true, true); - - this.connected = true; - this.accessToken = auth.at; - this.apiKey = auth.user.apikey; - - this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.CONNECTED, - }); -} - -module.exports = { - connect, -}; diff --git a/server/services/ewelink/lib/device/discover.js b/server/services/ewelink/lib/device/discover.js index 3ab8bf19bd..813d56c655 100644 --- a/server/services/ewelink/lib/device/discover.js +++ b/server/services/ewelink/lib/device/discover.js @@ -1,7 +1,6 @@ -const Promise = require('bluebird'); +const { mergeDevices } = require('../../../../utils/device'); const logger = require('../../../../utils/logger'); const features = require('../features'); -const { EWELINK_REGION_KEY } = require('../utils/constants'); const { getExternalId } = require('../utils/externalId'); /** @@ -11,43 +10,22 @@ const { getExternalId } = require('../utils/externalId'); * discover(); */ async function discover() { - if (!this.connected) { - await this.connect(); - } + const { thingList = [] } = await this.handleRequest(async () => + this.ewelinkWebAPIClient.device.getAllThingsAllPages(), + ); + logger.info(`eWeLink: ${thingList.length} device(s) found while retrieving from the cloud !`); - const region = await this.gladys.variable.getValue(EWELINK_REGION_KEY, this.serviceId); - const connection = new this.EweLinkApi({ at: this.accessToken, region }); - const discoveredDevices = await connection.getDevices(); - logger.debug(`eWeLink: Get devices: ${JSON.stringify(discoveredDevices)}`); - await this.throwErrorIfNeeded(discoveredDevices, true); + const discoveredDevices = thingList.map(({ itemData }) => { + logger.debug(`eWeLink: new device "${itemData.deviceid}" (${itemData.productModel}) discovered`); - const unknownDevices = []; + const deviceInGladys = this.gladys.stateManager.get('deviceByExternalId', getExternalId(itemData)); + const discoveredDevice = features.getDevice(this.serviceId, itemData); + return mergeDevices(discoveredDevice, deviceInGladys); + }); - // If devices are found... - logger.info(`eWeLink: ${discoveredDevices.length} device(s) found while retrieving from the cloud !`); - if (discoveredDevices.length) { - // ...check, for each of them, ... - await Promise.map( - discoveredDevices, - async (discoveredDevice) => { - // ...if it is already in Gladys... - const deviceInGladys = this.gladys.stateManager.get('deviceByExternalId', getExternalId(discoveredDevice)); - if (deviceInGladys) { - logger.debug(`eWeLink: Device "${discoveredDevice.deviceid}" is already in Gladys !`); - } else { - const channels = await connection.getDeviceChannelCount(discoveredDevice.deviceid); - logger.debug(`eWeLink: Get device channel count "${discoveredDevice.deviceid}": ${JSON.stringify(channels)}`); + this.discoveredDevices = discoveredDevices; - logger.debug( - `eWeLink: Device "${discoveredDevice.deviceid}" found, uiid: ${discoveredDevice.uiid}, model: "${discoveredDevice.productModel}, switches: ${channels.switchesAmount}`, - ); - unknownDevices.push(features.getDevice(this.serviceId, discoveredDevice, channels.switchesAmount)); - } - }, - { concurrency: 1 }, - ); - } - return unknownDevices; + return discoveredDevices; } module.exports = { diff --git a/server/services/ewelink/lib/device/index.js b/server/services/ewelink/lib/device/index.js deleted file mode 100644 index 56b2523f0f..0000000000 --- a/server/services/ewelink/lib/device/index.js +++ /dev/null @@ -1,76 +0,0 @@ -const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); -const { Error403, Error500 } = require('../../../../utils/httpErrors'); -const { EWELINK_EMAIL_KEY, EWELINK_PASSWORD_KEY, EWELINK_REGION_KEY } = require('../utils/constants'); -const { connect } = require('./connect'); -const { discover } = require('./discover'); -const { poll } = require('./poll'); -const { setValue } = require('./setValue'); -const { status } = require('./status'); - -/** - * @description Add ability to control an eWeLink device. - * @param {object} gladys - Gladys instance. - * @param {object} eweLinkApi - EweLink Client. - * @param {string} serviceId - UUID of the service in DB. - * @example - * const EweLinkHandler = new EweLinkHandler(gladys, client, serviceId); - */ -const EweLinkHandler = function EweLinkHandler(gladys, eweLinkApi, serviceId) { - this.gladys = gladys; - this.EweLinkApi = eweLinkApi; - this.serviceId = serviceId; - - this.configured = false; - this.connected = false; - this.accessToken = ''; - this.apiKey = ''; -}; - -/** - * @description Throw error if EweLinkApi call response has error. - * @param {object} response - EweLinkApi call response. - * @param {boolean} emit - True to emit message. - * @param {boolean} config - True to reset config. - * @example - * const EweLinkHandler = new EweLinkHandler(gladys, client, serviceId); - */ -async function throwErrorIfNeeded(response, emit = false, config = false) { - if (response.error) { - if (response.error === 406) { - this.connected = false; - this.accessToken = ''; - this.apiKey = ''; - if (emit) { - this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.ERROR, - payload: response.msg, - }); - } - if (config) { - await Promise.all([ - this.gladys.variable.setValue(EWELINK_EMAIL_KEY, '', this.serviceId), - this.gladys.variable.setValue(EWELINK_PASSWORD_KEY, '', this.serviceId), - this.gladys.variable.setValue(EWELINK_REGION_KEY, '', this.serviceId), - ]); - this.configured = false; - } - throw new Error403(`eWeLink: ${response.msg}`); - } - if (emit) { - this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.ERROR, - payload: response.msg, - }); - } - throw new Error500(`eWeLink: ${response.msg}`); - } -} - -EweLinkHandler.prototype.connect = connect; -EweLinkHandler.prototype.discover = discover; -EweLinkHandler.prototype.poll = poll; -EweLinkHandler.prototype.setValue = setValue; -EweLinkHandler.prototype.status = status; -EweLinkHandler.prototype.throwErrorIfNeeded = throwErrorIfNeeded; - -module.exports = EweLinkHandler; diff --git a/server/services/ewelink/lib/device/poll.js b/server/services/ewelink/lib/device/poll.js deleted file mode 100644 index 9aede1e221..0000000000 --- a/server/services/ewelink/lib/device/poll.js +++ /dev/null @@ -1,85 +0,0 @@ -const Promise = require('bluebird'); -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, EWELINK_REGION_KEY, 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) { - if (!this.connected) { - await this.connect(); - } - - const region = await this.gladys.variable.getValue(EWELINK_REGION_KEY, this.serviceId); - const { deviceId } = parseExternalId(device.external_id); - const connection = new this.EweLinkApi({ at: this.accessToken, region }); - const eWeLinkDevice = await connection.getDevice(deviceId); - logger.debug(`eWeLink: eWeLinkDevice: ${JSON.stringify(eWeLinkDevice)}`); - await this.throwErrorIfNeeded(eWeLinkDevice); - - const onlineParam = getDeviceParam(device, DEVICE_ONLINE); - 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); - } - - if (!eWeLinkDevice.online) { - throw new NotFoundError('eWeLink: Error, device is not currently online'); - } - - await Promise.mapSeries(device.features || [], (feature) => { - let state; - 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); - } - } -} - -module.exports = { - poll, -}; diff --git a/server/services/ewelink/lib/device/setValue.js b/server/services/ewelink/lib/device/setValue.js index d823ee2efd..89fb4630d9 100644 --- a/server/services/ewelink/lib/device/setValue.js +++ b/server/services/ewelink/lib/device/setValue.js @@ -1,43 +1,32 @@ const { DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); -const { NotFoundError } = require('../../../../utils/coreErrors'); const logger = require('../../../../utils/logger'); -const { writeBinaryValue } = require('../features/binary'); -const { EWELINK_REGION_KEY } = require('../utils/constants'); +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) { - if (!this.connected) { - await this.connect(); - } + const { external_id: featureExternalId, type } = deviceFeature; - const region = await this.gladys.variable.getValue(EWELINK_REGION_KEY, this.serviceId); - const connection = new this.EweLinkApi({ at: this.accessToken, apiKey: this.apiKey, region }); - - const { deviceId, channel } = parseExternalId(deviceFeature.external_id); - const eweLinkDevice = await connection.getDevice(deviceId); - await this.throwErrorIfNeeded(eweLinkDevice); - - if (!eweLinkDevice.online) { - throw new NotFoundError('eWeLink: Error, device is not currently online'); - } + const mapper = FEATURE_TYPE_MAP[type]; + if (mapper) { + const parsedExternalId = parseExternalId(featureExternalId); + const { deviceId } = parsedExternalId; + const params = binary.writeParams(device, parsedExternalId, value); - let response; - switch (deviceFeature.type) { - case DEVICE_FEATURE_TYPES.SWITCH.BINARY: - response = await connection.setDevicePowerState(deviceId, writeBinaryValue(value), channel); - await this.throwErrorIfNeeded(response); - 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/device/status.js b/server/services/ewelink/lib/device/status.js deleted file mode 100644 index 286085afcf..0000000000 --- a/server/services/ewelink/lib/device/status.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @description Get eWeLink status. - * @returns {object} Current eWeLink network status. - * @example - * status(); - */ -function status() { - const eweLinkStatus = { - configured: this.configured, - connected: this.connected, - }; - return eweLinkStatus; -} - -module.exports = { - status, -}; diff --git a/server/services/ewelink/lib/ewelink.init.js b/server/services/ewelink/lib/ewelink.init.js new file mode 100644 index 0000000000..34c4113594 --- /dev/null +++ b/server/services/ewelink/lib/ewelink.init.js @@ -0,0 +1,13 @@ +/** + * @description Initialize eWeLink service. + * @example + * await this.init(); + */ +async function init() { + await this.upgrade(); + await this.loadConfiguration(); +} + +module.exports = { + init, +}; diff --git a/server/services/ewelink/lib/ewelink.stop.js b/server/services/ewelink/lib/ewelink.stop.js new file mode 100644 index 0000000000..fb293e9131 --- /dev/null +++ b/server/services/ewelink/lib/ewelink.stop.js @@ -0,0 +1,14 @@ +/** + * @description Stop eWeLink service. + * @example + * await this.stop(); + */ +async function stop() { + this.ewelinkWebAPIClient = null; + this.closeWebSocketClient(); + this.updateStatus({ connected: false }); +} + +module.exports = { + stop, +}; diff --git a/server/services/ewelink/lib/features/binary.js b/server/services/ewelink/lib/features/binary.js index 005e7831c8..6c8c16ccf3 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,25 +13,42 @@ 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:0`, 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 + 1}`, + state, + }); + }); } - return null; + + return states; }, - // Gladys vs eWeLink transformers - writeBinaryValue: (value) => { - return value ? 'on' : 'off'; + 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/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 a89d654a3b..588f402373 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,9 +8,11 @@ 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], + uiid: [1, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15, 126], feature: binaryFeature, }, humidity: { @@ -37,29 +38,18 @@ 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. * @param {object} device - The eWeLink device. - * @param {number} channel - The channel of the device to control. * @returns {object} Return Gladys device. * @example * getDevice(serviceId, device, channel); */ -function getDevice(serviceId, device, channel = 0) { +function getDevice(serviceId, device) { const name = getDeviceName(device); const externalId = getExternalId(device); + const { params = {} } = device; const createdDevice = { name, @@ -68,27 +58,15 @@ function getDevice(serviceId, device, channel = 0) { 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: (device.params && device.params.fwVersion) || '?.?.?', - }, - { - name: DEVICE_ONLINE, - value: readOnlineValue(device.online), - }, - ], + should_poll: false, + params: readParams(device), }; - if (device.online) { + const deviceUiid = (device.extra || {}).uiid; + if (device.online && deviceUiid) { + const channel = params.switch ? 1 : (params.switches || []).length; Object.keys(AVAILABLE_FEATURE_MODELS).forEach((type) => { - if (AVAILABLE_FEATURE_MODELS[type].uiid.includes(device.uiid)) { + if (AVAILABLE_FEATURE_MODELS[type].uiid.includes(deviceUiid)) { let ch = 1; do { const featureExternalId = (type === 'binary' ? [externalId, type, ch] : [externalId, type]).join(':'); @@ -109,7 +87,26 @@ function getDevice(serviceId, device, channel = 0) { 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.readStates(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.handleRequest.js b/server/services/ewelink/lib/handlers/ewelink.handleRequest.js new file mode 100644 index 0000000000..2e7577a1f9 --- /dev/null +++ b/server/services/ewelink/lib/handlers/ewelink.handleRequest.js @@ -0,0 +1,36 @@ +const { ServiceNotConfiguredError } = require('../../../../utils/coreErrors'); +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, force = false, nbRetry = 0) { + // Do not call API if service is not ready + const { configured, connected } = this.status; + if (!force && (!configured || !connected)) { + throw new ServiceNotConfiguredError('eWeLink is not ready, please complete the configuration'); + } + + const response = await request(); + + // 402 - Access token expired, so refresh it and retry. + // see https://coolkit-technologies.github.io/eWeLink-API/#/en/APICenterV2?id=error-codes + if (response.error === 402 && nbRetry < NB_MAX_RETRY_EXPIRED) { + const tokenResponse = await this.ewelinkWebAPIClient.user.refreshToken(); + const tokens = await this.handleResponse(tokenResponse); + // Store new tokens + await this.saveTokens(tokens); + // Retry request + return this.handleRequest(request, force, nbRetry + 1); + } + + return this.handleResponse(response); +} + +module.exports = { handleRequest }; diff --git a/server/services/ewelink/lib/handlers/ewelink.handleResponse.js b/server/services/ewelink/lib/handlers/ewelink.handleResponse.js new file mode 100644 index 0000000000..3f8d147117 --- /dev/null +++ b/server/services/ewelink/lib/handlers/ewelink.handleResponse.js @@ -0,0 +1,40 @@ +const { BadParameters, NotFoundError, ServiceNotConfiguredError } = require('../../../../utils/coreErrors'); +const logger = require('../../../../utils/logger'); +const { CONFIGURATION_KEYS } = require('../utils/constants'); + +/** + * @description Provides a single way to manage WS responses. + * @param {object} response - WebService response. + * @returns {Promise} The WS call response. + * @example + * const data = this.handleResponse(res, (data) => console.log); + */ +async function handleResponse(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 - ${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(message); + case 400: + throw new BadParameters(message); + case 405: + case 4002: + throw new NotFoundError(message); + default: + throw new Error(message); + } + } + + return data; +} + +module.exports = { handleResponse }; diff --git a/server/services/ewelink/lib/index.js b/server/services/ewelink/lib/index.js new file mode 100644 index 0000000000..2caf77566b --- /dev/null +++ b/server/services/ewelink/lib/index.js @@ -0,0 +1,88 @@ +const { discover } = require('./device/discover'); +const { setValue } = require('./device/setValue'); + +const { updateStatus } = require('./config/ewelink.updateStatus'); + +const { getStatus } = require('./config/ewelink.getStatus'); +const { saveConfiguration } = require('./config/ewelink.saveConfiguration'); +const { loadConfiguration } = require('./config/ewelink.loadConfiguration'); +const { createClients } = require('./config/ewelink.createClients'); + +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'); + +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'); + +/** + * @description Add ability to control an eWeLink device. + * @param {object} gladys - Gladys instance. + * @param {object} eweLinkApi - Next eweLink Client. + * @param {string} serviceId - UUID of the service in DB. + * @example + * const EweLinkHandler = new EweLinkHandler(gladys, client, serviceId); + */ +const EweLinkHandler = function EweLinkHandler(gladys, eweLinkApi, serviceId) { + this.gladys = gladys; + this.eweLinkApi = eweLinkApi; + this.serviceId = serviceId; + + this.ewelinkWebAPIClient = null; + this.ewelinkWebSocketClient = null; + this.userApiKey = null; + + this.discoveredDevices = []; + + this.loginState = null; + this.configuration = {}; + this.status = { + configured: false, + connected: false, + }; +}; + +EweLinkHandler.prototype.updateStatus = updateStatus; +EweLinkHandler.prototype.getStatus = getStatus; + +EweLinkHandler.prototype.saveConfiguration = saveConfiguration; +EweLinkHandler.prototype.loadConfiguration = loadConfiguration; +EweLinkHandler.prototype.createClients = createClients; + +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.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; + +module.exports = EweLinkHandler; 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 new file mode 100644 index 0000000000..f15a989097 --- /dev/null +++ b/server/services/ewelink/lib/params/firmware.param.js @@ -0,0 +1,6 @@ +const { DEVICE_PARAMS } = require('../utils/constants'); + +module.exports = { + EWELINK_KEY_PATH: 'params.fwVersion', + GLADYS_PARAM_KEY: DEVICE_PARAMS.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..e5197f662f --- /dev/null +++ b/server/services/ewelink/lib/params/index.js @@ -0,0 +1,32 @@ +const get = require('get-value'); + +const firmwareParam = require('./firmware.param'); +const onlineParam = require('./online.param'); +const apiKeyParam = require('./apikey.param'); + +const PARAMS = [firmwareParam, onlineParam, apiKeyParam]; + +/** + * @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..284ae015d7 --- /dev/null +++ b/server/services/ewelink/lib/params/online.param.js @@ -0,0 +1,18 @@ +const { DEVICE_PARAMS } = require('../utils/constants'); + +/** + * @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: DEVICE_PARAMS.ONLINE, + convertValue, +}; diff --git a/server/services/ewelink/lib/user/ewelink.buildLoginUrl.js b/server/services/ewelink/lib/user/ewelink.buildLoginUrl.js new file mode 100644 index 0000000000..d41b31494a --- /dev/null +++ b/server/services/ewelink/lib/user/ewelink.buildLoginUrl.js @@ -0,0 +1,25 @@ +const logger = require('../../../../utils/logger'); +const { generate } = require('../../../../utils/password'); + +/** + * @description Generates eWeLink login URL. + * @param {object} params - EWeLink login configuration. + * @param {string} [params.redirectUrl] - Login redirect URL. + * @returns {string} Login URL. + * @example + * const loginURL = this.buildLoginUrl({ redirect_url: 'http://gladys' }); + */ +function buildLoginUrl({ redirectUrl }) { + logger.info('eWeLink: create new login URL'); + const state = generate(10, { number: true, lowercase: true, uppercase: true }); + this.loginState = state; + return this.ewelinkWebAPIClient.oauth.createLoginUrl({ + redirectUrl, + grantType: 'authorization_code', + state, + }); +} + +module.exports = { + buildLoginUrl, +}; diff --git a/server/services/ewelink/lib/user/ewelink.deleteTokens.js b/server/services/ewelink/lib/user/ewelink.deleteTokens.js new file mode 100644 index 0000000000..cb511258fe --- /dev/null +++ b/server/services/ewelink/lib/user/ewelink.deleteTokens.js @@ -0,0 +1,36 @@ +const logger = require('../../../../utils/logger'); +const { CONFIGURATION_KEYS } = require('../utils/constants'); + +/** + * @description Delete tokens and logout user. + * @example + * await this.deleteTokens(); + */ +async function deleteTokens() { + logger.info('eWeLink: disconnecting user...'); + // see https://coolkit-technologies.github.io/eWeLink-API/#/en/OAuth2.0?id=unbind-third-party-accounts + const logoutCall = async () => + this.ewelinkWebAPIClient.request.delete('/v2/user/oauth/token', { + headers: { + 'X-CK-Appid': this.ewelinkWebAPIClient.appId || '', + Authorization: `Bearer ${this.ewelinkWebAPIClient.at}`, + }, + }); + + await this.handleRequest(logoutCall); + + // 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'); +} + +module.exports = { + deleteTokens, +}; diff --git a/server/services/ewelink/lib/user/ewelink.exchangeToken.js b/server/services/ewelink/lib/user/ewelink.exchangeToken.js new file mode 100644 index 0000000000..2413368721 --- /dev/null +++ b/server/services/ewelink/lib/user/ewelink.exchangeToken.js @@ -0,0 +1,44 @@ +const { BadParameters } = require('../../../../utils/coreErrors'); +const logger = require('../../../../utils/logger'); + +/** + * @description Generates a token for the connected user. + * @param {object} params - EWeLink login configuration. + * @param {string} [params.redirectUrl] - Login redirect URL. + * @param {string} [params.code] - OAuth authorization code. + * @param {string} [params.region] - User region. + * @param {string} [params.state] - Login state. + * @example + * await this.exchangeToken({ redirectUrl, code, region, state }); + */ +async function exchangeToken({ redirectUrl, code, region, state }) { + logger.info('eWeLink: exchanging user authorization code...'); + + if (state !== this.loginState) { + throw new BadParameters('eWeLink login state is invalid'); + } + + const tokenResponse = await this.ewelinkWebAPIClient.oauth.getToken({ + region, + redirectUrl, + code, + }); + + const data = await this.handleResponse(tokenResponse); + + this.ewelinkWebAPIClient.at = data.accessToken; + this.ewelinkWebAPIClient.rt = data.refreshToken; + + await this.saveTokens(data); + + await this.retrieveUserApiKey(); + + await this.createWebSocketClient(); + + this.updateStatus({ connected: true }); + logger.info('eWeLink: user well connected...'); +} + +module.exports = { + exchangeToken, +}; 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/user/ewelink.saveTokens.js b/server/services/ewelink/lib/user/ewelink.saveTokens.js new file mode 100644 index 0000000000..dc68640e46 --- /dev/null +++ b/server/services/ewelink/lib/user/ewelink.saveTokens.js @@ -0,0 +1,16 @@ +const { CONFIGURATION_KEYS } = require('../utils/constants'); + +/** + * @description Store user tokens into database. + * @param {object} params - Raw eWeLink user tokens. + * @example + * await this.saveTokens({ accessToken, refreshToken }); + */ +async function saveTokens(params) { + // Store tokens into databate + await this.gladys.variable.setValue(CONFIGURATION_KEYS.USER_TOKENS, JSON.stringify(params), this.serviceId); +} + +module.exports = { + saveTokens, +}; diff --git a/server/services/ewelink/lib/utils/constants.js b/server/services/ewelink/lib/utils/constants.js index aa754d5a51..3b454525ad 100644 --- a/server/services/ewelink/lib/utils/constants.js +++ b/server/services/ewelink/lib/utils/constants.js @@ -1,26 +1,27 @@ -const EWELINK_EMAIL_KEY = 'EWELINK_EMAIL'; -const EWELINK_PASSWORD_KEY = 'EWELINK_PASSWORD'; -const EWELINK_REGION_KEY = 'EWELINK_REGION'; -const EWELINK_REGIONS = { - EU: 'eu', - US: 'us', +const CONFIGURATION_KEYS = { + APPLICATION_ID: 'APPLICATION_ID', + 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'; -const DEVICE_IP_ADDRESS = 'IP_ADDRESS'; -const DEVICE_FIRMWARE = 'FIRMWARE'; -const DEVICE_ONLINE = 'ONLINE'; +const NB_MAX_RETRY_EXPIRED = 1; module.exports = { - EWELINK_EMAIL_KEY, - EWELINK_PASSWORD_KEY, - EWELINK_REGION_KEY, - EWELINK_REGIONS, + CONFIGURATION_KEYS, + DEVICE_PARAMS, 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 new file mode 100644 index 0000000000..037034c964 --- /dev/null +++ b/server/services/ewelink/lib/versions/ewelink.upgrade.js @@ -0,0 +1,35 @@ +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, v3]; + +/** + * @description Upgrades eWeLink integration to last version. + * @example + * await this.upgrade(); + */ +async function upgrade() { + const storedVersion = await this.gladys.variable.getValue(CONFIGURATION_KEYS.SERVICE_VERSION, this.serviceId); + const currentVersion = storedVersion ? Number.parseInt(storedVersion, 10) : 0; + + logger.info('eWeLink: service is currently on version %s...', currentVersion); + + // Versions to apply + const toApply = VERSIONS.filter((version) => currentVersion < version.VERSION_NUMBER); + + await Promise.each(toApply, async (version) => { + const { VERSION_NUMBER, apply } = version; + logger.info('eWeLink: upgrading service to version %d...', VERSION_NUMBER); + await apply(this); + await this.gladys.variable.setValue(CONFIGURATION_KEYS.SERVICE_VERSION, `${VERSION_NUMBER}`, this.serviceId); + logger.info('eWeLink: service well upgraded to version %d', VERSION_NUMBER); + }); +} + +module.exports = { + upgrade, +}; diff --git a/server/services/ewelink/lib/versions/ewelink.v2.js b/server/services/ewelink/lib/versions/ewelink.v2.js new file mode 100644 index 0000000000..e79eece6bb --- /dev/null +++ b/server/services/ewelink/lib/versions/ewelink.v2.js @@ -0,0 +1,20 @@ +const logger = require('../../../../utils/logger'); + +/** + * @description Upgrades integration to v2. + * @param {object} ewelink - Current eWeLink service handler. + * @example + * await v2.apply(); + */ +async function apply(ewelink) { + logger.info('eWeLink: removing EMAIL/PASSWORD/REGION variables from database...'); + const { serviceId, gladys } = ewelink; + await gladys.variable.destroy('EWELINK_EMAIL', serviceId); + await gladys.variable.destroy('EWELINK_PASSWORD', serviceId); + await gladys.variable.destroy('EWELINK_REGION', serviceId); +} + +module.exports = { + apply, + VERSION_NUMBER: 2, +}; 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..9929433077 --- /dev/null +++ b/server/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.js @@ -0,0 +1,14 @@ +/** + * @description Close WebSocket client. + * @example + * this.closeWebSocketClient(); + */ +function closeWebSocketClient() { + if (this.ewelinkWebSocketClient && this.ewelinkWebSocketClient.Connect && this.ewelinkWebSocketClient.Connect.ws) { + this.ewelinkWebSocketClient.Connect.ws.close(); + } +} + +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..351359dae3 --- /dev/null +++ b/server/services/ewelink/lib/websocket/ewelink.createWebSocketClient.js @@ -0,0 +1,24 @@ +/** + * @description Create WebSocket client. + * @example + * await this.createWebSocketClient(); + */ +async function createWebSocketClient() { + const { applicationId: appId, applicationRegion: region } = this.configuration; + this.ewelinkWebSocketClient.userApiKey = this.userApiKey; + await this.ewelinkWebSocketClient.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..ccf2609b18 --- /dev/null +++ b/server/services/ewelink/lib/websocket/ewelink.onWebSocketClose.js @@ -0,0 +1,19 @@ +const logger = require('../../../../utils/logger'); + +/** + * @description Action to execute when WebSocket is closed. + * @example + * await this.onWebSocketClose(); + */ +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 = { + 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..cf95b91e84 --- /dev/null +++ b/server/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.js @@ -0,0 +1,75 @@ +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 event message. + * @example + * await this.onWebSocketMessage(); + */ +async function onWebSocketMessage(ws, 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; + + // Message is not concerning a device + if (!deviceid) { + logger.debug('eWeLink: WebSocket message is not about a device, skipping it...'); + return; + } + + 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) { + 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, + }); + } + }); + + // Update the device params + const updatedParams = readParams(params); + // Update device params + await Promise.each(updatedParams, async ({ name, value }) => { + setDeviceParam(device, name, value); + await this.gladys.device.setParam(device, name, 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 965b82cfc7..3c90491585 100644 --- a/server/services/ewelink/package-lock.json +++ b/server/services/ewelink/package-lock.json @@ -19,26 +19,33 @@ ], "dependencies": { "bluebird": "^3.7.2", - "ewelink-api": "^3.1.1" + "ewelink-api-next": "^1.0.3", + "get-value": "^3.0.1" } }, - "node_modules/arpping": { - "version": "0.3.1", - "resolved": "git+ssh://git@github.com/skydiver/arpping.git#ae65410343bdcbddb64b37ac9f674c65af1fe92c", - "integrity": "sha512-Sa474Qr/j4z0nDgJ5xfVwKnKpe8fOXW9CRafTP1gm7oYj+3drUDe5CUJpSYw6IC301GMJ9gDtlal9tNQZ2XYLw==", - "license": "MIT", - "dependencies": { - "child_process": "^1.0.2", - "os": "^0.1.1" - } + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, - "node_modules/babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", + "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", "dependencies": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/bluebird": { @@ -46,502 +53,343 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, + "node_modules/bonjour-service": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz", + "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==", + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, "node_modules/bufferutil": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.2.tgz", "integrity": "sha512-AtnG3W6M8B2n4xDQ5R+70EXvOpnXsFYg/AK2yTZd+HQ/oxAdz+GI+DvjmhBw3L0ole+LJ0ngqY4JMbDzkfNzhA==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.2.0" } }, - "node_modules/call-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", - "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.0" + "delayed-stream": "~1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.8" } }, - "node_modules/child_process": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", - "integrity": "sha1-sffn/HPSXn/R1FWtyU4UODAYK1o=" - }, - "node_modules/chnl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/chnl/-/chnl-1.2.0.tgz", - "integrity": "sha512-g5gJb59edwCliFbX2j7G6sBfY4sX9YLy211yctONI2GRaiX0f2zIbKWmBm+sPqFNEpM7Ljzm7IJX/xrjiEbPrw==" - }, - "node_modules/core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true - }, "node_modules/crypto-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz", - "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==" - }, - "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "engines": { + "node": ">=4.0" } }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } + "node_modules/dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dependencies": { - "object-keys": "^1.0.12" - }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "engines": { - "node": ">= 0.4" + "node": ">=0.4.0" } }, - "node_modules/delay": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/delay/-/delay-4.4.0.tgz", - "integrity": "sha512-txgOrJu3OdtOfTiEOT2e76dJVfG/1dz2NZ4F0Pyt4UGZJryssMRp5vdM5wQoLwSOBNdrJv3F9PAhp/heqd7vrA==", + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, "engines": { "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "node_modules/ewelink-api-next": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ewelink-api-next/-/ewelink-api-next-1.0.3.tgz", + "integrity": "sha512-TeBbo+CU81sk5Zeo69pwVuct3kg6OLrl0VtsMvYfGxDeFQnU5tQRrfuQ/IuWYSdQUTUbCIpHUiIaDDaExK/nCA==", "dependencies": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - }, + "axios": "1.2.1", + "bonjour-service": "1.0.14", + "crypto-js": "^4.1.1", + "dayjs": "1.11.7", + "log4js": "6.7.1", + "node-localstorage": "^2.2.1", + "ws": "8.11.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], "engines": { - "node": ">= 0.4" + "node": ">=4.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 6" } }, - "node_modules/es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dependencies": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" } }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "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": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=6.0" } }, - "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" - } + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/ewelink-api": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/ewelink-api/-/ewelink-api-3.1.1.tgz", - "integrity": "sha512-MJyVrEoAKFVbSJhlYeBqb6B5EFVIkMZG4L95CeF4YmjUO5/wr2mtfZkZfBwBwrJKUHqAI4qKJiTZeSXEQlzwgA==", - "dependencies": { - "arpping": "github:skydiver/arpping", - "crypto-js": "^4.0.0", - "delay": "^4.4.0", - "node-fetch": "^2.6.1", - "random": "^2.2.0", - "websocket": "^1.0.32", - "websocket-as-promised": "^1.0.1" + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" } }, - "node_modules/ext": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", - "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", - "dependencies": { - "type": "^2.0.0" + "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/ext/node_modules/type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", - "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==" - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/get-intrinsic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", - "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/log4js": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.7.1.tgz", + "integrity": "sha512-lzbd0Eq1HRdWM2abSD7mk6YIVY0AogGJzb/z+lqzRk+8+XJP+M6L1MS5FUSc3jjGru4dbKjEMJmqlsoYYpuivQ==", "dependencies": { - "function-bind": "^1.1.1" + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.3" }, "engines": { - "node": ">= 0.4.0" + "node": ">=8.0" } }, - "node_modules/has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "engines": { - "node": ">= 0.4" + "node_modules/log4js/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", "engines": { - "node": ">= 0.4" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/log4js/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/is-negative-zero": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", - "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "engines": { - "node": ">= 0.4" + "node": ">= 0.6" } }, - "node_modules/is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dependencies": { - "has-symbols": "^1.0.1" + "mime-db": "1.52.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.6" } }, - "node_modules/is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dependencies": { - "has-symbols": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" - }, - "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "engines": { - "node": "4.x || >=6.0.0" + "bin": { + "multicast-dns": "cli.js" } }, "node_modules/node-gyp-build": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", + "optional": true, + "peer": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, - "node_modules/object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "node_modules/node-localstorage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.2.1.tgz", + "integrity": "sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==", "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" + "write-file-atomic": "^1.1.4" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/os": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/os/-/os-0.1.1.tgz", - "integrity": "sha1-IIhF6J4ZOtTZcUdLk5R3NqVtE/M=" - }, - "node_modules/ow": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/ow/-/ow-0.4.0.tgz", - "integrity": "sha512-kJNzxUgVd6EF5LoGs+s2/etJPwjfRDLXPTCfEgV8At77sRrV+PSFA8lcoW2HF15Qd455mIR2Stee/2MzDiFBDA==", - "engines": { - "node": ">=6" + "node": ">=0.12" } }, - "node_modules/ow-lite": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/ow-lite/-/ow-lite-0.0.2.tgz", - "integrity": "sha1-359QDmdAtlkKHpqWVzDUmo61l9E=", - "engines": { - "node": ">=6" - } - }, - "node_modules/promise-controller": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/promise-controller/-/promise-controller-1.0.0.tgz", - "integrity": "sha512-goA0zA9L91tuQbUmiMinSYqlyUtEgg4fxJcjYnLYOQnrktb4o4UqciXDNXiRUPiDBPACmsr1k8jDW4r7UDq9Qw==", + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", "engines": { - "node": ">=8" + "node": "*" } }, - "node_modules/promise.prototype.finally": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.2.tgz", - "integrity": "sha512-A2HuJWl2opDH0EafgdjwEw7HysI8ff/n4lW4QEVBCUXFk9QeGecBWv0Deph0UmLe3tTNYegz8MOjsVuE6SMoJA==", + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.0", - "function-bind": "^1.1.1" + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8.0" } }, - "node_modules/random": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/random/-/random-2.2.0.tgz", - "integrity": "sha512-4HBR4Xye4jJ41QBi6RfIaO1yKQpxVUZafQtdE6NvvjzirNlwWgsk3tkGLTbQtWUarF4ofZsUVEmWqB1TDQlkwA==", + "node_modules/streamroller/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "babel-runtime": "^6.26.0", - "ow": "^0.4.0", - "ow-lite": "^0.0.2", - "seedrandom": "^3.0.5" + "ms": "2.1.2" }, "engines": { - "node": ">=8" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "node_modules/seedrandom": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", - "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", - "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/string.prototype.trimend/node_modules/es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", - "dependencies": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/streamroller/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", - "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, - "node_modules/string.prototype.trimstart/node_modules/es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", - "dependencies": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dependencies": { - "is-typedarray": "^1.0.0" + "node": ">= 4.0.0" } }, "node_modules/utf-8-validate": { @@ -549,65 +397,67 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.3.tgz", "integrity": "sha512-jtJM6fpGv8C1SoH4PtG22pGto6x+Y8uPprW0tw3//gGFhDDTiuksgradgFN6yRayDP4SyZZa6ZMGHLIa17+M8A==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.2.0" } }, - "node_modules/websocket": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.32.tgz", - "integrity": "sha512-i4yhcllSP4wrpoPMU2N0TQ/q0O94LRG/eUQjEAamRltjQ1oT1PFFKOG4i877OlJgCG8rw6LrrowJp+TYCEWF7Q==", + "node_modules/write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", "dependencies": { - "bufferutil": "^4.0.1", - "debug": "^2.2.0", - "es5-ext": "^0.10.50", - "typedarray-to-buffer": "^3.1.5", - "utf-8-validate": "^5.0.2", - "yaeti": "^0.0.6" - }, - "engines": { - "node": ">=4.0.0" + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" } }, - "node_modules/websocket-as-promised": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/websocket-as-promised/-/websocket-as-promised-1.1.0.tgz", - "integrity": "sha512-agq8bPsPFKBWinKQkoXwY7LoBYe+2fQ7Gnuxx964+BTIiyAdL130FnB60bXuVQdUCdaS17R/MyRaaO4WIqtl4Q==", - "dependencies": { - "chnl": "^1.2.0", - "promise-controller": "^1.0.0", - "promise.prototype.finally": "^3.1.2" - }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "engines": { - "node": ">=6" - } - }, - "node_modules/yaeti": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=", - "engines": { - "node": ">=0.10.32" + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } } }, "dependencies": { - "arpping": { - "version": "git+ssh://git@github.com/skydiver/arpping.git#ae65410343bdcbddb64b37ac9f674c65af1fe92c", - "integrity": "sha512-Sa474Qr/j4z0nDgJ5xfVwKnKpe8fOXW9CRafTP1gm7oYj+3drUDe5CUJpSYw6IC301GMJ9gDtlal9tNQZ2XYLw==", - "from": "arpping@github:skydiver/arpping", - "requires": { - "child_process": "^1.0.2", - "os": "^0.1.1" - } + "@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", + "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "bluebird": { @@ -615,423 +465,287 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, + "bonjour-service": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz", + "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==", + "requires": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, "bufferutil": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.2.tgz", "integrity": "sha512-AtnG3W6M8B2n4xDQ5R+70EXvOpnXsFYg/AK2yTZd+HQ/oxAdz+GI+DvjmhBw3L0ole+LJ0ngqY4JMbDzkfNzhA==", + "optional": true, + "peer": true, "requires": { "node-gyp-build": "^4.2.0" } }, - "call-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", - "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.0" + "delayed-stream": "~1.0.0" } }, - "child_process": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", - "integrity": "sha1-sffn/HPSXn/R1FWtyU4UODAYK1o=" + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" }, - "chnl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/chnl/-/chnl-1.2.0.tgz", - "integrity": "sha512-g5gJb59edwCliFbX2j7G6sBfY4sX9YLy211yctONI2GRaiX0f2zIbKWmBm+sPqFNEpM7Ljzm7IJX/xrjiEbPrw==" + "date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==" }, - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + "dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" }, - "crypto-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz", - "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==" + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "requires": { - "ms": "2.0.0" + "@leichtgewicht/ip-codec": "^2.0.1" } }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "ewelink-api-next": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ewelink-api-next/-/ewelink-api-next-1.0.3.tgz", + "integrity": "sha512-TeBbo+CU81sk5Zeo69pwVuct3kg6OLrl0VtsMvYfGxDeFQnU5tQRrfuQ/IuWYSdQUTUbCIpHUiIaDDaExK/nCA==", "requires": { - "object-keys": "^1.0.12" + "axios": "1.2.1", + "bonjour-service": "1.0.14", + "crypto-js": "^4.1.1", + "dayjs": "1.11.7", + "log4js": "6.7.1", + "node-localstorage": "^2.2.1", + "ws": "8.11.0" } }, - "delay": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/delay/-/delay-4.4.0.tgz", - "integrity": "sha512-txgOrJu3OdtOfTiEOT2e76dJVfG/1dz2NZ4F0Pyt4UGZJryssMRp5vdM5wQoLwSOBNdrJv3F9PAhp/heqd7vrA==" + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } + "flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } + "follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" }, - "es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" } }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, - "es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "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": { - "d": "^1.0.1", - "ext": "^1.1.2" + "isobject": "^3.0.1" } }, - "ewelink-api": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/ewelink-api/-/ewelink-api-3.1.1.tgz", - "integrity": "sha512-MJyVrEoAKFVbSJhlYeBqb6B5EFVIkMZG4L95CeF4YmjUO5/wr2mtfZkZfBwBwrJKUHqAI4qKJiTZeSXEQlzwgA==", + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "imurmurhash": { + "version": "0.1.4", + "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", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "requires": { - "arpping": "github:skydiver/arpping", - "crypto-js": "^4.0.0", - "delay": "^4.4.0", - "node-fetch": "^2.6.1", - "random": "^2.2.0", - "websocket": "^1.0.32", - "websocket-as-promised": "^1.0.1" + "graceful-fs": "^4.1.6" } }, - "ext": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", - "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "log4js": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.7.1.tgz", + "integrity": "sha512-lzbd0Eq1HRdWM2abSD7mk6YIVY0AogGJzb/z+lqzRk+8+XJP+M6L1MS5FUSc3jjGru4dbKjEMJmqlsoYYpuivQ==", "requires": { - "type": "^2.0.0" + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.3" }, "dependencies": { - "type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", - "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==" + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, - "get-intrinsic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", - "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "mime-db": "1.52.0" } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "requires": { - "function-bind": "^1.1.1" + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" } }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" - }, - "is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" - }, - "is-negative-zero": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", - "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=" - }, - "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" - }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" - }, "node-gyp-build": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", - "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==" - }, - "object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "os": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/os/-/os-0.1.1.tgz", - "integrity": "sha1-IIhF6J4ZOtTZcUdLk5R3NqVtE/M=" - }, - "ow": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/ow/-/ow-0.4.0.tgz", - "integrity": "sha512-kJNzxUgVd6EF5LoGs+s2/etJPwjfRDLXPTCfEgV8At77sRrV+PSFA8lcoW2HF15Qd455mIR2Stee/2MzDiFBDA==" - }, - "ow-lite": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/ow-lite/-/ow-lite-0.0.2.tgz", - "integrity": "sha1-359QDmdAtlkKHpqWVzDUmo61l9E=" - }, - "promise-controller": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/promise-controller/-/promise-controller-1.0.0.tgz", - "integrity": "sha512-goA0zA9L91tuQbUmiMinSYqlyUtEgg4fxJcjYnLYOQnrktb4o4UqciXDNXiRUPiDBPACmsr1k8jDW4r7UDq9Qw==" - }, - "promise.prototype.finally": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.2.tgz", - "integrity": "sha512-A2HuJWl2opDH0EafgdjwEw7HysI8ff/n4lW4QEVBCUXFk9QeGecBWv0Deph0UmLe3tTNYegz8MOjsVuE6SMoJA==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.0", - "function-bind": "^1.1.1" - } + "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", + "optional": true, + "peer": true }, - "random": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/random/-/random-2.2.0.tgz", - "integrity": "sha512-4HBR4Xye4jJ41QBi6RfIaO1yKQpxVUZafQtdE6NvvjzirNlwWgsk3tkGLTbQtWUarF4ofZsUVEmWqB1TDQlkwA==", + "node-localstorage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.2.1.tgz", + "integrity": "sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==", "requires": { - "babel-runtime": "^6.26.0", - "ow": "^0.4.0", - "ow-lite": "^0.0.2", - "seedrandom": "^3.0.5" + "write-file-atomic": "^1.1.4" } }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, - "seedrandom": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", - "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" }, - "string.prototype.trimend": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", - "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==" }, - "string.prototype.trimstart": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", - "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", + "streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" }, "dependencies": { - "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "ms": "2.1.2" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, - "type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "requires": { - "is-typedarray": "^1.0.0" - } + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, "utf-8-validate": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.3.tgz", "integrity": "sha512-jtJM6fpGv8C1SoH4PtG22pGto6x+Y8uPprW0tw3//gGFhDDTiuksgradgFN6yRayDP4SyZZa6ZMGHLIa17+M8A==", + "optional": true, + "peer": true, "requires": { "node-gyp-build": "^4.2.0" } }, - "websocket": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.32.tgz", - "integrity": "sha512-i4yhcllSP4wrpoPMU2N0TQ/q0O94LRG/eUQjEAamRltjQ1oT1PFFKOG4i877OlJgCG8rw6LrrowJp+TYCEWF7Q==", - "requires": { - "bufferutil": "^4.0.1", - "debug": "^2.2.0", - "es5-ext": "^0.10.50", - "typedarray-to-buffer": "^3.1.5", - "utf-8-validate": "^5.0.2", - "yaeti": "^0.0.6" - } - }, - "websocket-as-promised": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/websocket-as-promised/-/websocket-as-promised-1.1.0.tgz", - "integrity": "sha512-agq8bPsPFKBWinKQkoXwY7LoBYe+2fQ7Gnuxx964+BTIiyAdL130FnB60bXuVQdUCdaS17R/MyRaaO4WIqtl4Q==", + "write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", "requires": { - "chnl": "^1.2.0", - "promise-controller": "^1.0.0", - "promise.prototype.finally": "^3.1.2" + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" } }, - "yaeti": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "requires": {} } } } diff --git a/server/services/ewelink/package.json b/server/services/ewelink/package.json index a9b5695817..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": "^3.1.1" + "ewelink-api-next": "^1.0.3", + "get-value": "^3.0.1" } } diff --git a/server/test/services/ewelink/controllers/ewelink.controller.test.js b/server/test/services/ewelink/controllers/ewelink.controller.test.js index d756f92c31..8284e8e704 100644 --- a/server/test/services/ewelink/controllers/ewelink.controller.test.js +++ b/server/test/services/ewelink/controllers/ewelink.controller.test.js @@ -1,64 +1,212 @@ const sinon = require('sinon'); -const EweLinkController = require('../../../../services/ewelink/api/ewelink.controller'); +const eWeLinkController = require('../../../../services/ewelink/api/ewelink.controller'); +const { EWELINK_APP_ID, EWELINK_APP_SECRET, EWELINK_APP_REGION } = require('../lib/constants'); const { assert, fake } = sinon; -const status = {}; +const status = { configured: true, connected: true }; +const configuration = { + applicationId: EWELINK_APP_ID, + applicationSecret: EWELINK_APP_SECRET, + applicationRegion: EWELINK_APP_REGION, +}; const devices = []; + const ewelinkHandler = { - connect: fake.returns(true), - status: fake.resolves(status), - discover: fake.returns(devices), + configuration, + discover: fake.resolves(devices), + getStatus: fake.returns(status), + saveConfiguration: fake.resolves(null), + buildLoginUrl: fake.resolves('LOGIN_URL'), + exchangeToken: fake.resolves(null), + deleteTokens: fake.resolves(null), }; -describe('EweLinkController POST /api/v1/service/ewelink/connect', () => { +describe('eWeLinkController GET /api/v1/service/ewelink/config', () => { let controller; beforeEach(() => { - controller = EweLinkController(ewelinkHandler); + controller = eWeLinkController(ewelinkHandler); + }); + + afterEach(() => { sinon.reset(); }); - it('should connect', async () => { + it('should return config', async () => { const req = {}; const res = { json: fake.returns(null), }; - await controller['post /api/v1/service/ewelink/connect'].controller(req, res); - assert.calledOnce(ewelinkHandler.connect); - assert.calledOnce(res.json); + await controller['get /api/v1/service/ewelink/config'].controller(req, res); + assert.calledOnceWithExactly(res.json, { + application_id: EWELINK_APP_ID, + application_secret: EWELINK_APP_SECRET, + application_region: EWELINK_APP_REGION, + }); }); }); -describe('EweLinkController GET /api/v1/service/ewelink/status', () => { +describe('eWeLinkController POST /api/v1/service/ewelink/config', () => { let controller; beforeEach(() => { - controller = EweLinkController(ewelinkHandler); + controller = eWeLinkController(ewelinkHandler); + }); + + afterEach(() => { sinon.reset(); }); - it('should return status', async () => { - status.configured = true; - status.connected = true; + it('should save and return config', async () => { + const req = { + body: { + application_id: 'NEW_APP_ID', + application_secret: 'NEW_APP_SECRET', + application_region: 'NEW_APP_REGION', + }, + }; + const res = { + json: fake.returns(null), + }; + + await controller['post /api/v1/service/ewelink/config'].controller(req, res); + assert.calledOnceWithExactly(ewelinkHandler.saveConfiguration, { + applicationId: 'NEW_APP_ID', + applicationSecret: 'NEW_APP_SECRET', + applicationRegion: 'NEW_APP_REGION', + }); + assert.calledOnceWithExactly(res.json, { + application_id: EWELINK_APP_ID, + application_secret: EWELINK_APP_SECRET, + application_region: EWELINK_APP_REGION, + }); + }); +}); + +describe('eWeLinkController GET /api/v1/service/ewelink/loginUrl', () => { + let controller; + + beforeEach(() => { + controller = eWeLinkController(ewelinkHandler); + }); + afterEach(() => { + sinon.reset(); + }); + + it('should return login URL', async () => { + const req = { + query: { + redirect_url: 'REDIRECT_URL', + }, + }; + const res = { + json: fake.returns(null), + }; + + await controller['get /api/v1/service/ewelink/loginUrl'].controller(req, res); + assert.calledOnceWithExactly(ewelinkHandler.buildLoginUrl, { redirectUrl: 'REDIRECT_URL' }); + assert.calledOnceWithExactly(res.json, { + login_url: 'LOGIN_URL', + }); + }); +}); + +describe('eWeLinkController POST /api/v1/service/ewelink/token', () => { + let controller; + + beforeEach(() => { + controller = eWeLinkController(ewelinkHandler); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should exchange auth_code by user token', async () => { + const req = { + body: { + redirect_url: 'REDIRECT_URL', + code: 'AUTH_CODE', + region: EWELINK_APP_REGION, + state: 'LOGIN_STATE', + }, + }; + const res = { + json: fake.returns(null), + }; + + await controller['post /api/v1/service/ewelink/token'].controller(req, res); + assert.calledOnceWithExactly(ewelinkHandler.exchangeToken, { + redirectUrl: 'REDIRECT_URL', + code: 'AUTH_CODE', + region: EWELINK_APP_REGION, + state: 'LOGIN_STATE', + }); + assert.calledOnceWithExactly(res.json, { + success: true, + }); + }); +}); + +describe('eWeLinkController DELETE /api/v1/service/ewelink/token', () => { + let controller; + + beforeEach(() => { + controller = eWeLinkController(ewelinkHandler); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should delete stored tokens', async () => { + const req = {}; + const res = { + json: fake.returns(null), + }; + + await controller['delete /api/v1/service/ewelink/token'].controller(req, res); + assert.calledOnceWithExactly(ewelinkHandler.deleteTokens); + assert.calledOnceWithExactly(res.json, { + success: true, + }); + }); +}); + +describe('eWeLinkController GET /api/v1/service/ewelink/status', () => { + let controller; + + beforeEach(() => { + controller = eWeLinkController(ewelinkHandler); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should return status', async () => { const req = {}; const res = { json: fake.returns(null), }; await controller['get /api/v1/service/ewelink/status'].controller(req, res); - assert.calledOnce(ewelinkHandler.status); - assert.calledOnce(res.json); + assert.calledOnceWithExactly(ewelinkHandler.getStatus); + assert.calledOnceWithExactly(res.json, status); }); }); -describe('EweLinkController GET /api/v1/service/ewelink/discover', () => { +describe('eWeLinkController GET /api/v1/service/ewelink/discover', () => { let controller; beforeEach(() => { - controller = EweLinkController(ewelinkHandler); + controller = eWeLinkController(ewelinkHandler); + }); + + afterEach(() => { sinon.reset(); }); @@ -69,7 +217,7 @@ describe('EweLinkController GET /api/v1/service/ewelink/discover', () => { }; await controller['get /api/v1/service/ewelink/discover'].controller(req, res); - assert.calledOnce(ewelinkHandler.discover); - assert.calledOnce(res.json); + assert.calledOnceWithExactly(ewelinkHandler.discover); + assert.calledOnceWithExactly(res.json, devices); }); }); diff --git a/server/test/services/ewelink/index.test.js b/server/test/services/ewelink/index.test.js index cb5cb9b04b..7c222760ed 100644 --- a/server/test/services/ewelink/index.test.js +++ b/server/test/services/ewelink/index.test.js @@ -1,29 +1,44 @@ const { expect } = require('chai'); +const sinon = require('sinon'); const proxyquire = require('proxyquire').noCallThru(); -const { event, variableOk } = require('./mocks/consts.test'); -const EwelinkApi = require('./mocks/ewelink-api.mock.test'); +const EwelinkApi = require('./lib/ewelink-api.mock.test'); +const { SERVICE_ID } = require('./lib/constants'); + +const { fake, assert } = sinon; + +const EweLinkHandlerMock = sinon.stub(); +EweLinkHandlerMock.prototype.init = fake.resolves(null); +EweLinkHandlerMock.prototype.stop = fake.resolves(null); const EweLinkService = proxyquire('../../../services/ewelink/index', { - 'ewelink-api': EwelinkApi, + './lib': EweLinkHandlerMock, + 'ewelink-api-next': EwelinkApi, }); -const gladys = { - event, - variable: variableOk, -}; +const gladys = {}; describe('EweLinkService', () => { - const eweLinkService = EweLinkService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); + const eweLinkService = EweLinkService(gladys, SERVICE_ID); + + afterEach(() => { + sinon.reset(); + }); it('should have controllers', () => { expect(eweLinkService) .to.have.property('controllers') .and.be.instanceOf(Object); }); + it('should start service', async () => { await eweLinkService.start(); + assert.calledOnceWithExactly(eweLinkService.device.init); + assert.notCalled(eweLinkService.device.stop); }); + it('should stop service', async () => { await eweLinkService.stop(); + assert.notCalled(eweLinkService.device.init); + assert.calledOnceWithExactly(eweLinkService.device.stop); }); }); diff --git a/server/test/services/ewelink/lib/config/ewelink.getStatus.test.js b/server/test/services/ewelink/lib/config/ewelink.getStatus.test.js new file mode 100644 index 0000000000..8e08d96776 --- /dev/null +++ b/server/test/services/ewelink/lib/config/ewelink.getStatus.test.js @@ -0,0 +1,24 @@ +const { expect } = require('chai'); + +const EwelinkHandler = require('../../../../../services/ewelink/lib'); +const { SERVICE_ID } = require('../constants'); + +describe('eWeLinkHandler getStatus', () => { + let eWeLinkHandler; + + beforeEach(() => { + eWeLinkHandler = new EwelinkHandler({}, null, SERVICE_ID); + }); + + it('should returns default status', () => { + const status = eWeLinkHandler.getStatus(); + expect(status).deep.eq({ configured: false, connected: false }); + }); + + it('should returns overriden status', () => { + eWeLinkHandler.status = { configured: true, connected: true }; + + const status = eWeLinkHandler.getStatus(); + expect(status).deep.eq({ configured: true, connected: true }); + }); +}); diff --git a/server/test/services/ewelink/lib/config/ewelink.loadConfiguration.test.js b/server/test/services/ewelink/lib/config/ewelink.loadConfiguration.test.js new file mode 100644 index 0000000000..cc95f5815a --- /dev/null +++ b/server/test/services/ewelink/lib/config/ewelink.loadConfiguration.test.js @@ -0,0 +1,301 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); + +const { assert, fake, stub, match } = sinon; + +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'); + +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), + }, + variable: { + getValue: stub().resolves(null), + }, + }; + + eWeLinkApiMock = { + WebAPI: stub(), + Ws: eWeLinkWsMock, + }; + eWeLinkHandler = new EwelinkHandler(gladys, eWeLinkApiMock, SERVICE_ID); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should throw a ServiceNotConfiguredError as no variable is stored in database', async () => { + try { + await 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.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(retrieveUserApiKey); + }); + + it('should throw a ServiceNotConfiguredError as only APPLICATION_ID variable is stored in database', async () => { + gladys.variable.getValue = sinon + .stub() + .onFirstCall() + .resolves('APPLICATION_ID_VALUE') + .resolves(null); + + try { + await 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.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(retrieveUserApiKey); + }); + + it('should throw a ServiceNotConfiguredError as only APPLICATION_ID and APPLICATION_SECRET variable are stored in database', async () => { + gladys.variable.getValue = sinon + .stub() + .onFirstCall() + .resolves('APPLICATION_ID_VALUE') + .onSecondCall() + .resolves('APPLICATION_SECRET_VALUE') + .resolves(null); + + try { + await 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.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(retrieveUserApiKey); + }); + + it('should throw a ServiceNotConfiguredError as USER_TOKENS variable is missing in database', async () => { + gladys.variable.getValue = sinon + .stub() + .onFirstCall() + .resolves('APPLICATION_ID_VALUE') + .onSecondCall() + .resolves('APPLICATION_SECRET_VALUE') + .onThirdCall() + .resolves('APPLICATION_REGION_VALUE') + .resolves(null); + + try { + await 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.notCalled(retrieveUserApiKey); + }); + + it('should be well configured', 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('API_KEY'); + + 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.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', + 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 new file mode 100644 index 0000000000..8784c430e8 --- /dev/null +++ b/server/test/services/ewelink/lib/config/ewelink.saveConfiguration.test.js @@ -0,0 +1,146 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { assert, fake, stub } = sinon; + +const EwelinkHandler = require('../../../../../services/ewelink/lib'); +const { SERVICE_ID, EWELINK_APP_ID, EWELINK_APP_SECRET, EWELINK_APP_REGION } = require('../constants'); +const { BadParameters } = require('../../../../../utils/coreErrors'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants'); + +describe('eWeLinkHandler saveConfiguration', () => { + let eWeLinkHandler; + let eWeLinkApiMock; + let gladys; + + beforeEach(() => { + gladys = { + event: { + emit: fake.returns(null), + }, + variable: { + setValue: stub().resolves(null), + destroy: stub().resolves(null), + }, + }; + + eWeLinkApiMock = { + WebAPI: stub(), + Ws: stub(), + }; + eWeLinkHandler = new EwelinkHandler(gladys, eWeLinkApiMock, SERVICE_ID); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should throw a BadParameter error as no variable is passed', async () => { + try { + await eWeLinkHandler.saveConfiguration({}); + assert.fail(); + } catch (e) { + expect(e).instanceOf(BadParameters); + expect(e.message).is.eq('eWeLink: all application ID/Secret/Region are required'); + } + + assert.notCalled(gladys.event.emit); + assert.notCalled(gladys.variable.setValue); + assert.notCalled(gladys.variable.destroy); + + expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + }); + + it('should throw a BadParameter error as SECRET and REGION variables are mossing', async () => { + try { + await eWeLinkHandler.saveConfiguration({ applicationId: EWELINK_APP_ID }); + assert.fail(); + } catch (e) { + expect(e).instanceOf(BadParameters); + expect(e.message).is.eq('eWeLink: all application ID/Secret/Region are required'); + } + + assert.notCalled(gladys.event.emit); + assert.notCalled(gladys.variable.setValue); + assert.notCalled(gladys.variable.destroy); + + expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + }); + + it('should throw a BadParameter error as REGION variables is missing', async () => { + try { + await eWeLinkHandler.saveConfiguration({ applicationId: EWELINK_APP_ID, applicationSecret: EWELINK_APP_SECRET }); + assert.fail(); + } catch (e) { + expect(e).instanceOf(BadParameters); + expect(e.message).is.eq('eWeLink: all application ID/Secret/Region are required'); + } + + assert.notCalled(gladys.event.emit); + assert.notCalled(gladys.variable.setValue); + assert.notCalled(gladys.variable.destroy); + + expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + }); + + it('should throw a error on database store failure', async () => { + gladys.variable.setValue = fake.rejects('UNABLE TO STORE IN DB'); + try { + await eWeLinkHandler.saveConfiguration({ + applicationId: EWELINK_APP_ID, + applicationSecret: EWELINK_APP_SECRET, + applicationRegion: EWELINK_APP_REGION, + }); + assert.fail(); + } catch (e) { + expect(e).instanceOf(Error); + expect(e.message).is.eq('UNABLE TO STORE IN DB'); + } + + 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.calledOnceWithExactly(gladys.variable.setValue, 'APPLICATION_ID', EWELINK_APP_ID, SERVICE_ID); + assert.notCalled(gladys.variable.destroy); + + expect(eWeLinkHandler.ewelinkWebAPIClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).eq(null); + }); + + it('should save configuration and send events', async () => { + await eWeLinkHandler.saveConfiguration({ + applicationId: EWELINK_APP_ID, + applicationSecret: EWELINK_APP_SECRET, + applicationRegion: EWELINK_APP_REGION, + }); + + 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.setValue, 3); + assert.calledWithExactly(gladys.variable.setValue, 'APPLICATION_ID', EWELINK_APP_ID, SERVICE_ID); + assert.calledWithExactly(gladys.variable.setValue, 'APPLICATION_SECRET', EWELINK_APP_SECRET, SERVICE_ID); + assert.calledWithExactly(gladys.variable.setValue, 'APPLICATION_REGION', EWELINK_APP_REGION, SERVICE_ID); + assert.calledOnceWithExactly(gladys.variable.destroy, 'USER_TOKENS', SERVICE_ID); + + expect(eWeLinkHandler.ewelinkWebAPIClient).not.eq(null); + expect(eWeLinkHandler.ewelinkWebSocketClient).not.eq(null); + }); +}); diff --git a/server/test/services/ewelink/lib/constants.js b/server/test/services/ewelink/lib/constants.js new file mode 100644 index 0000000000..60762c2ee6 --- /dev/null +++ b/server/test/services/ewelink/lib/constants.js @@ -0,0 +1,19 @@ +const SERVICE_ID = 'a810b8db-6d04-4697-bed3-c4b72c996279'; + +const EWELINK_APP_ID = 'ewelink-app-id'; +const EWELINK_APP_SECRET = 'ewelink-app-secret'; +const EWELINK_APP_REGION = 'ewelink-app-region'; + +const EWELINK_VALID_ACCESS_TOKEN = 'ewelink-valid-access-token'; +const EWELINK_DENIED_ACCESS_TOKEN = 'ewelink-invalid-access-token'; +const EWELINK_INVALID_ACCESS_TOKEN = 'ewelink-not-configured-access-token'; + +module.exports = { + SERVICE_ID, + EWELINK_APP_ID, + EWELINK_APP_SECRET, + EWELINK_APP_REGION, + EWELINK_VALID_ACCESS_TOKEN, + EWELINK_INVALID_ACCESS_TOKEN, + EWELINK_DENIED_ACCESS_TOKEN, +}; diff --git a/server/test/services/ewelink/lib/device/connect.test.js b/server/test/services/ewelink/lib/device/connect.test.js deleted file mode 100644 index 040c2d1e9b..0000000000 --- a/server/test/services/ewelink/lib/device/connect.test.js +++ /dev/null @@ -1,101 +0,0 @@ -const { expect } = require('chai'); -const proxyquire = require('proxyquire').noCallThru(); -const sinon = require('sinon'); -const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants'); -const { - event, - serviceId, - variableNok, - variableNotConfigured, - variableOk, - variableOkFalseRegion, - variableOkNoRegion, -} = require('../../mocks/consts.test'); -const EweLinkApiMock = require('../../mocks/ewelink-api.mock.test'); - -const { assert } = sinon; - -const EwelinkService = proxyquire('../../../../../services/ewelink/index', { - 'ewelink-api': EweLinkApiMock, -}); - -describe('EweLinkHandler connect', () => { - beforeEach(() => { - sinon.reset(); - }); - - it('should connect and receive success', async () => { - const gladys = { event, variable: variableOk }; - const eweLinkService = EwelinkService(gladys, serviceId); - await eweLinkService.device.connect(); - - assert.notCalled(gladys.variable.setValue); - assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.CONNECTED, - }); - - expect(eweLinkService.device.configured).to.equal(true); - expect(eweLinkService.device.connected).to.equal(true); - expect(eweLinkService.device.accessToken).to.equal('validAccessToken'); - expect(eweLinkService.device.apiKey).to.equal('validApiKey'); - }); - it('should return not configured error', async () => { - const gladys = { event, variable: variableNotConfigured }; - const eweLinkService = EwelinkService(gladys, serviceId); - try { - await eweLinkService.device.connect(); - assert.fail(); - } catch (error) { - assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.ERROR, - payload: 'Service is not configured', - }); - expect(error.message).to.equal('eWeLink: Error, service is not configured'); - } - }); - it('should get region and connect', async () => { - const gladys = { event, variable: variableOkNoRegion }; - const eweLinkService = EwelinkService(gladys, serviceId); - await eweLinkService.device.connect(); - - assert.calledWith(gladys.variable.setValue, 'EWELINK_REGION', 'eu', serviceId); - assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.CONNECTED, - }); - - expect(eweLinkService.device.configured).to.equal(true); - expect(eweLinkService.device.connected).to.equal(true); - expect(eweLinkService.device.accessToken).to.equal('validAccessToken'); - expect(eweLinkService.device.apiKey).to.equal('validApiKey'); - }); - it('should get right region and connect', async () => { - const gladys = { event, variable: variableOkFalseRegion }; - const eweLinkService = EwelinkService(gladys, serviceId); - await eweLinkService.device.connect(); - - assert.calledWith(gladys.variable.setValue, 'EWELINK_REGION', 'eu', serviceId); - assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.CONNECTED, - }); - - expect(eweLinkService.device.configured).to.equal(true); - expect(eweLinkService.device.connected).to.equal(true); - expect(eweLinkService.device.accessToken).to.equal('validAccessToken'); - expect(eweLinkService.device.apiKey).to.equal('validApiKey'); - }); - it('should throw an error and emit a message when authentication fail', async () => { - const gladys = { event, variable: variableNok }; - const eweLinkService = EwelinkService(gladys, serviceId); - try { - await eweLinkService.device.connect(); - assert.fail(); - } catch (error) { - assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.ERROR, - payload: 'Authentication error', - }); - expect(error.status).to.equal(403); - expect(error.message).to.equal('eWeLink: Authentication error'); - } - }); -}); diff --git a/server/test/services/ewelink/lib/device/discover.test.js b/server/test/services/ewelink/lib/device/discover.test.js index e12099b65e..e12f6299fe 100644 --- a/server/test/services/ewelink/lib/device/discover.test.js +++ b/server/test/services/ewelink/lib/device/discover.test.js @@ -1,101 +1,95 @@ const { expect } = require('chai'); -const proxyquire = require('proxyquire').noCallThru(); const sinon = require('sinon'); -const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants'); -const { - event, - serviceId, - stateManagerWith0Devices, - stateManagerWith2Devices, - variableNotConfigured, - variableOk, -} = require('../../mocks/consts.test'); -const Gladys2ChDevice = require('../../mocks/Gladys-2ch.json'); -const GladysOfflineDevice = require('../../mocks/Gladys-offline.json'); -const GladysPowDevice = require('../../mocks/Gladys-pow.json'); -const GladysThDevice = require('../../mocks/Gladys-th.json'); -const GladysUnhandledDevice = require('../../mocks/Gladys-unhandled.json'); -const EweLinkApiMock = require('../../mocks/ewelink-api.mock.test'); -const EweLinkApiEmptyMock = require('../../mocks/ewelink-api-empty.mock.test'); + +const { SERVICE_ID, EWELINK_INVALID_ACCESS_TOKEN, EWELINK_DENIED_ACCESS_TOKEN } = require('../constants'); +const Gladys2ChDevice = require('../payloads/Gladys-2ch.json'); +const GladysOfflineDevice = require('../payloads/Gladys-offline.json'); +const GladysPowDevice = require('../payloads/Gladys-pow.json'); +const GladysThDevice = require('../payloads/Gladys-th.json'); +const GladysUnhandledDevice = require('../payloads/Gladys-unhandled.json'); const { assert } = sinon; -const EwelinkService = proxyquire('../../../../../services/ewelink/index', { - 'ewelink-api': EweLinkApiMock, -}); -const EwelinkServiceEmpty = proxyquire('../../../../../services/ewelink/index', { - 'ewelink-api': EweLinkApiEmptyMock, -}); +const EwelinkHandler = require('../../../../../services/ewelink/lib'); +const EweLinkApiMock = require('../ewelink-api.mock.test'); +const { ServiceNotConfiguredError } = require('../../../../../utils/coreErrors'); -const gladysWith0Devices = { - variable: variableOk, - event, - stateManager: stateManagerWith0Devices, -}; -const gladysWith2Devices = { - variable: variableOk, - event, - stateManager: stateManagerWith2Devices, +const gladys = { + stateManager: { + get: (key, externalId) => { + if (externalId === 'ewelink:10004531ae') { + return Gladys2ChDevice; + } + if (externalId === 'ewelink:10004533ae') { + return GladysPowDevice; + } + return undefined; + }, + }, + event: { + emit: () => {}, + }, + variable: { + destroy: async () => {}, + }, }; describe('EweLinkHandler discover', () => { + let eWeLinkHandler; + beforeEach(() => { + eWeLinkHandler = new EwelinkHandler(gladys, EweLinkApiMock, SERVICE_ID); + eWeLinkHandler.ewelinkWebAPIClient = new EweLinkApiMock.WebAPI(); + eWeLinkHandler.status = { configured: true, connected: true }; + }); + + afterEach(() => { sinon.reset(); }); - it('should found 5 devices, 5 of wich are new unknown devices', async () => { - const eweLinkService = EwelinkService(gladysWith0Devices, serviceId); - const newDevices = await eweLinkService.device.discover(); + 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(5); expect(newDevices).to.have.deep.members([ - Gladys2ChDevice, - GladysUnhandledDevice, - GladysThDevice, + { ...Gladys2ChDevice, room_id: undefined, updatable: false }, GladysOfflineDevice, - GladysPowDevice, + { ...GladysPowDevice, room_id: undefined, updatable: false }, + GladysThDevice, + GladysUnhandledDevice, ]); + + expect(eWeLinkHandler.discoveredDevices).to.deep.eq(newDevices); }); - it('should found 5 devices, 2 of wich are already in Gladys and 3 are a new unknown device', async () => { - const eweLinkService = EwelinkService(gladysWith2Devices, serviceId); - const newDevices = await eweLinkService.device.discover(); - expect(newDevices.length).to.equal(3); - expect(newDevices).to.have.deep.members([GladysOfflineDevice, GladysThDevice, GladysUnhandledDevice]); - }); + it('should found 0 devices', async () => { - const eweLinkService = EwelinkServiceEmpty(gladysWith0Devices, serviceId); - const newDevices = await eweLinkService.device.discover(); + // 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 () => { - const gladys = { event, variable: variableNotConfigured }; - const eweLinkService = EwelinkService(gladys, serviceId); - eweLinkService.device.connected = false; + eWeLinkHandler.ewelinkWebAPIClient.at = EWELINK_INVALID_ACCESS_TOKEN; try { - await eweLinkService.device.discover(); + await eWeLinkHandler.discover(); assert.fail(); } catch (error) { - assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.ERROR, - payload: 'Service is not configured', - }); + 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 () => { - const gladys = { event, variable: variableOk }; - const eweLinkService = EwelinkService(gladys, serviceId); - eweLinkService.device.connected = true; - eweLinkService.device.accessToken = 'NoMoreValidAccessToken'; + eWeLinkHandler.ewelinkWebAPIClient.at = EWELINK_DENIED_ACCESS_TOKEN; try { - await eweLinkService.device.discover(); + await eWeLinkHandler.discover(); assert.fail(); } catch (error) { - assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.ERROR, - payload: 'Authentication error', - }); - expect(error.status).to.equal(403); + 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/poll.test.js b/server/test/services/ewelink/lib/device/poll.test.js deleted file mode 100644 index 16612d7ec6..0000000000 --- a/server/test/services/ewelink/lib/device/poll.test.js +++ /dev/null @@ -1,116 +0,0 @@ -const { expect } = require('chai'); -const proxyquire = require('proxyquire').noCallThru(); -const sinon = require('sinon'); -const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants'); -const { deviceManagerFull, event, serviceId, stateManagerFull, variableOk } = require('../../mocks/consts.test'); -const Gladys2ChDevice = require('../../mocks/Gladys-2ch.json'); -const GladysBasicDevice = require('../../mocks/Gladys-Basic.json'); -const GladysOfflineDevice = require('../../mocks/Gladys-offline.json'); -const GladysPowDevice = require('../../mocks/Gladys-pow.json'); -const GladysThDevice = require('../../mocks/Gladys-th.json'); -const EwelinkApiMock = require('../../mocks/ewelink-api.mock.test'); - -const { assert } = sinon; - -const EwelinkService = proxyquire('../../../../../services/ewelink/index', { - 'ewelink-api': EwelinkApiMock, -}); - -const gladys = { - variable: variableOk, - event, - device: deviceManagerFull, - stateManager: stateManagerFull, -}; - -describe('EweLinkHandler poll', () => { - const eweLinkService = EwelinkService(gladys, serviceId); - - beforeEach(() => { - sinon.reset(); - eweLinkService.device.connected = false; - eweLinkService.device.accessToken = ''; - }); - - it('should poll device and emit 2 states for a "2CH" model', async () => { - await eweLinkService.device.poll(Gladys2ChDevice); - assert.callCount(gladys.event.emit, 3); - assert.calledWith(gladys.event.emit.getCall(0), EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.CONNECTED, - }); - assert.calledWith(gladys.event.emit.getCall(1), EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: 'ewelink:10004531ae:binary:1', - state: 1, - }); - assert.calledWith(gladys.event.emit.getCall(2), 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 eweLinkService.device.poll(GladysPowDevice); - assert.callCount(gladys.event.emit, 2); - assert.calledWith(gladys.event.emit.getCall(0), EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.CONNECTED, - }); - assert.calledWith(gladys.event.emit.getCall(1), 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 eweLinkService.device.poll(GladysThDevice); - assert.callCount(gladys.event.emit, 4); - assert.calledWith(gladys.event.emit.getCall(0), EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.CONNECTED, - }); - assert.calledWith(gladys.event.emit.getCall(1), EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: 'ewelink:10004534ae:binary:1', - state: 1, - }); - assert.calledWith(gladys.event.emit.getCall(2), EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: 'ewelink:10004534ae:humidity', - state: 42, - }); - assert.calledWith(gladys.event.emit.getCall(3), 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 () => { - 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 eweLinkService.device.poll(GladysBasicDevice); - assert.callCount(gladys.event.emit, 1); - assert.calledWith(gladys.event.emit.getCall(0), EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.EWELINK.CONNECTED, - }); - 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 eweLinkService.device.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 () => { - eweLinkService.device.connected = true; - eweLinkService.device.accessToken = 'NoMoreValidAccessToken'; - try { - await eweLinkService.device.poll(Gladys2ChDevice); - assert.fail(); - } catch (error) { - expect(error.status).to.equal(403); - expect(error.message).to.equal('eWeLink: Authentication error'); - } - }); -}); diff --git a/server/test/services/ewelink/lib/device/setValue.test.js b/server/test/services/ewelink/lib/device/setValue.test.js index c3e8e82e2a..f0f7e24775 100644 --- a/server/test/services/ewelink/lib/device/setValue.test.js +++ b/server/test/services/ewelink/lib/device/setValue.test.js @@ -1,85 +1,53 @@ -const { expect } = require('chai'); -const proxyquire = require('proxyquire').noCallThru(); const sinon = require('sinon'); -const { event, serviceId, variableOk } = require('../../mocks/consts.test'); -const Gladys2ChDevice = require('../../mocks/Gladys-2ch.json'); -const GladysOfflineDevice = require('../../mocks/Gladys-offline.json'); -const GladysPowDevice = require('../../mocks/Gladys-pow.json'); -const EweLinkApiMock = require('../../mocks/ewelink-api.mock.test'); -const { assert } = sinon; +const EwelinkHandler = require('../../../../../services/ewelink/lib'); -const EwelinkService = proxyquire('../../../../../services/ewelink/index', { - 'ewelink-api': EweLinkApiMock, -}); +const { SERVICE_ID } = require('../constants'); +const Gladys2ChDevice = require('../payloads/Gladys-2ch.json'); +const GladysPowDevice = require('../payloads/Gladys-pow.json'); +const EweLinkApiMock = require('../ewelink-api.mock.test'); -const gladys = { - event, - variable: variableOk, -}; +const { assert } = sinon; -describe('EweLinkHandler setValue', () => { - const eweLinkService = EwelinkService(gladys, serviceId); - const functionToTest = sinon.spy(EweLinkApiMock.prototype, 'setDevicePowerState'); +describe('eWeLinkHandler setValue', () => { + let eWeLinkHandler; + const functionToTest = sinon.spy(EweLinkApiMock.Connect.prototype, 'updateState'); beforeEach(() => { + const gladys = {}; + eWeLinkHandler = new EwelinkHandler(gladys, EweLinkApiMock, SERVICE_ID); + eWeLinkHandler.ewelinkWebSocketClient = new EweLinkApiMock.Ws(); + eWeLinkHandler.status = { configured: true, connected: true }; + }); + + afterEach(() => { sinon.reset(); - eweLinkService.device.connected = false; - eweLinkService.device.accessToken = ''; - eweLinkService.device.apiKey = ''; }); - it('should set the binary value of the channel 1 of the "2CH" device to 1', async () => { - await eweLinkService.device.setValue( + 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.calledWith(functionToTest, '10004533ae', 'on', 1); + assert.calledOnceWithExactly(functionToTest, '10004533ae', { switch: 'on' }); }); + it('should set the binary value of the channel 2 of the "2CH" device to 0', async () => { - await eweLinkService.device.setValue( + await eWeLinkHandler.setValue( Gladys2ChDevice, { external_id: 'ewelink:10004531ae:power:2', category: 'switch', type: 'binary' }, 0, ); - assert.calledWith(functionToTest, '10004531ae', 'off', 2); + assert.calledOnceWithExactly(functionToTest, '10004531ae', { switches: [{ outlet: 1, switch: 'off' }] }); }); + it('should do nothing because of the feature type is not handled yet', async () => { - await eweLinkService.device.setValue( + await eWeLinkHandler.setValue( GladysPowDevice, { external_id: 'ewelink:10004533ae:power:1', category: 'switch', type: 'not_handled' }, 1, ); assert.notCalled(functionToTest); }); - it('should throw an error when device is offline', async () => { - try { - await eweLinkService.device.setValue( - GladysOfflineDevice, - { external_id: 'ewelink:10004532ae:power:1', category: 'switch', type: 'binary' }, - 1, - ); - assert.fail(); - } catch (error) { - assert.notCalled(functionToTest); - expect(error.message).to.equal('eWeLink: Error, device is not currently online'); - } - }); - it('should throw an error when AccessToken is no more valid', async () => { - eweLinkService.device.connected = true; - eweLinkService.device.accessToken = 'NoMoreValidAccessToken'; - try { - await eweLinkService.device.setValue( - Gladys2ChDevice, - { external_id: 'ewelink:10004531ae:power:2', category: 'switch', type: 'binary' }, - 1, - ); - assert.fail(); - } catch (error) { - assert.notCalled(functionToTest); - expect(error.status).to.equal(403); - expect(error.message).to.equal('eWeLink: Authentication error'); - } - }); }); diff --git a/server/test/services/ewelink/lib/device/throwErrorIfNeeded.test.js b/server/test/services/ewelink/lib/device/throwErrorIfNeeded.test.js deleted file mode 100644 index a7699d5410..0000000000 --- a/server/test/services/ewelink/lib/device/throwErrorIfNeeded.test.js +++ /dev/null @@ -1,95 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const proxyquire = require('proxyquire').noCallThru(); -const { serviceId, event } = require('../../mocks/consts.test'); -const EweLinkApi = require('../../mocks/ewelink-api.mock.test'); - -const { assert } = sinon; - -const EwelinkService = proxyquire('../../../../../services/ewelink/index', { - 'ewelink-api': EweLinkApi, -}); - -describe('EweLinkHandler throwErrorIfNeeded', () => { - const gladys = { event }; - const eweLinkService = EwelinkService(gladys, serviceId); - - beforeEach(() => { - sinon.reset(); - eweLinkService.device.connected = true; - eweLinkService.device.accessToken = 'validAccessToken'; - eweLinkService.device.apiKey = 'validApiKey'; - }); - - it('should throws a error and not emit a message', async () => { - try { - const response = { error: 406, msg: 'Authentication error' }; - await eweLinkService.device.throwErrorIfNeeded(response); - assert.fail(); - } catch (error) { - expect(error.status).to.equal(403); - expect(error.message).to.equal('eWeLink: Authentication error'); - } - }); - it('should throws an error and emit a message', async () => { - try { - const response = { error: 406, msg: 'Authentication error' }; - await eweLinkService.device.throwErrorIfNeeded(response, true); - assert.fail(); - } catch (error) { - assert.calledOnceWithExactly(gladys.event.emit, 'websocket.send-all', { - type: 'ewelink.error', - payload: 'Authentication error', - }); - expect(error.status).to.equal(403); - expect(error.message).to.equal('eWeLink: Authentication error'); - } - }); - it('should reset authentication values when authentication fail', async () => { - eweLinkService.device.accessToken = 'NoMoreValidAccessToken'; - try { - const response = { error: 406, msg: 'Authentication error' }; - await eweLinkService.device.throwErrorIfNeeded(response); - assert.fail(); - } catch (error) { - expect(eweLinkService.device.connected).to.equal(false); - expect(eweLinkService.device.accessToken).to.equal(''); - expect(eweLinkService.device.apiKey).to.equal(''); - } - }); - it('should throws a error and not emit a message', async () => { - try { - const response = { error: 404, msg: 'Device does not exist' }; - await eweLinkService.device.throwErrorIfNeeded(response); - assert.fail(); - } catch (error) { - expect(error.status).to.equal(500); - expect(error.error).to.equal('eWeLink: Device does not exist'); - } - }); - it('should throws an error and emit a message', async () => { - try { - const response = { error: 404, msg: 'Device does not exist' }; - await eweLinkService.device.throwErrorIfNeeded(response, true); - assert.fail(); - } catch (error) { - assert.calledOnceWithExactly(gladys.event.emit, 'websocket.send-all', { - type: 'ewelink.error', - payload: 'Device does not exist', - }); - expect(error.status).to.equal(500); - expect(error.error).to.equal('eWeLink: Device does not exist'); - } - }); - it('should not reset authentication values', async () => { - try { - const response = { error: 500, msg: 'Device does not exist' }; - await eweLinkService.device.throwErrorIfNeeded(response); - assert.fail(); - } catch (error) { - expect(eweLinkService.device.connected).to.equal(true); - expect(eweLinkService.device.accessToken).to.equal('validAccessToken'); - expect(eweLinkService.device.apiKey).to.equal('validApiKey'); - } - }); -}); diff --git a/server/test/services/ewelink/lib/ewelink-api.mock.test.js b/server/test/services/ewelink/lib/ewelink-api.mock.test.js new file mode 100644 index 0000000000..ba8b8227bb --- /dev/null +++ b/server/test/services/ewelink/lib/ewelink-api.mock.test.js @@ -0,0 +1,121 @@ +const Promise = require('bluebird'); +const EweLink2ChDevice = require('./payloads/eweLink-2ch.json'); +const EweLinkBasicDevice = require('./payloads/eweLink-basic.json'); +const EweLinkOfflineDevice = require('./payloads/eweLink-offline.json'); +const EweLinkPowDevice = require('./payloads/eweLink-pow.json'); +const EweLinkThDevice = require('./payloads/eweLink-th.json'); +const EweLinkUnhandledDevice = require('./payloads/eweLink-unhandled.json'); +const { + EWELINK_APP_ID, + EWELINK_APP_SECRET, + EWELINK_APP_REGION, + EWELINK_VALID_ACCESS_TOKEN, + EWELINK_INVALID_ACCESS_TOKEN, +} = require('./constants'); +const logger = require('../../../../utils/logger'); + +const fakeDevices = [EweLink2ChDevice, EweLinkOfflineDevice, EweLinkPowDevice, EweLinkThDevice, EweLinkUnhandledDevice]; + +const buildResponse = async (data, accessToken) => { + const response = { + status: 200, + error: 0, + data, + }; + + if (accessToken === EWELINK_INVALID_ACCESS_TOKEN) { + response.error = 401; + response.msg = 'eWeLink: Error, service is not configured'; + } else if (accessToken !== EWELINK_VALID_ACCESS_TOKEN) { + response.error = 406; + response.msg = 'eWeLink: Authentication error'; + } + + return Promise.resolve(response); +}; + +class Device { + constructor(root) { + this.root = root; + } + + async getAllThingsAllPages() { + const data = { + thingList: fakeDevices.map((device) => { + return { itemData: device }; + }), + }; + + return buildResponse(data, this.root.at); + } + + async getThings({ thingList }) { + const [firstItem] = thingList; + const { id: deviceId } = firstItem; + + const device = [...fakeDevices, EweLinkBasicDevice].find((fakeDevice) => fakeDevice.deviceid === deviceId); + const response = await buildResponse({ thingList: [{ itemData: device }] }, this.root.at); + + if (!response.error) { + if (!device) { + response.error = 405; + response.msg = 'Device does not exist'; + } else if (!device.online) { + response.error = 4002; + response.msg = 'eWeLink: Error, device is not currently online'; + } + } + + return response; + } +} + +class WebAPI { + 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.device = new Device(this); + } +} + +class Connect { + constructor(root) { + this.root = root; + + 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 { + 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/ewelink.init.test.js b/server/test/services/ewelink/lib/ewelink.init.test.js new file mode 100644 index 0000000000..7ab138b737 --- /dev/null +++ b/server/test/services/ewelink/lib/ewelink.init.test.js @@ -0,0 +1,26 @@ +const sinon = require('sinon'); + +const { fake, assert } = sinon; + +const EwelinkHandler = require('../../../../services/ewelink/lib'); +const { SERVICE_ID } = require('./constants'); + +describe('eWeLinkHandler init', () => { + let eWeLinkHandler; + + beforeEach(() => { + eWeLinkHandler = new EwelinkHandler({}, null, SERVICE_ID); + eWeLinkHandler.loadConfiguration = fake.resolves(null); + eWeLinkHandler.upgrade = fake.resolves(null); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should call both methods', async () => { + await eWeLinkHandler.init(); + assert.calledOnceWithExactly(eWeLinkHandler.loadConfiguration); + assert.calledOnceWithExactly(eWeLinkHandler.upgrade); + }); +}); diff --git a/server/test/services/ewelink/lib/ewelink.stop.test.js b/server/test/services/ewelink/lib/ewelink.stop.test.js new file mode 100644 index 0000000000..ff9bdaf747 --- /dev/null +++ b/server/test/services/ewelink/lib/ewelink.stop.test.js @@ -0,0 +1,38 @@ +const sinon = require('sinon'); + +const { fake, assert } = sinon; + +const { expect } = require('chai'); +const EwelinkHandler = require('../../../../services/ewelink/lib'); +const { SERVICE_ID } = require('./constants'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); + +describe('eWeLinkHandler stop', () => { + let eWeLinkHandler; + let gladys; + + beforeEach(() => { + gladys = { + event: { + emit: fake.returns(null), + }, + }; + eWeLinkHandler = new EwelinkHandler(gladys, null, SERVICE_ID); + eWeLinkHandler.ewelinkWebAPIClient = {}; + eWeLinkHandler.status = { configured: true, connected: true }; + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should emit event', async () => { + await eWeLinkHandler.stop(); + expect(eWeLinkHandler.ewelinkWebAPIClient).to.eq(null); + + assert.calledOnceWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: { configured: true, connected: false }, + }); + }); +}); diff --git a/server/test/services/ewelink/lib/features/binary.test.js b/server/test/services/ewelink/lib/features/binary.test.js new file mode 100644 index 0000000000..264669fb23 --- /dev/null +++ b/server/test/services/ewelink/lib/features/binary.test.js @@ -0,0 +1,28 @@ +const { expect } = require('chai'); + +const { readStates } = require('../../../../../services/ewelink/lib/features/binary'); + +describe('eWeLink binary feature -> readState', () => { + it('should return empty states', () => { + const states = readStates('ewelink-id', { currentTemperature: 17 }); + expect(states).deep.eq([]); + }); + + it('should return switch states', () => { + const states = readStates('ewelink-id', { switch: 'on' }); + expect(states).deep.eq([{ featureExternalId: 'ewelink-id:binary:0', state: 1 }]); + }); + + it('should return switches states', () => { + const states = readStates('ewelink-id', { + switches: [ + { switch: 'on', outlet: 0 }, + { switch: 'off', outlet: 1 }, + ], + }); + expect(states).deep.eq([ + { featureExternalId: 'ewelink-id:binary:1', state: 1 }, + { featureExternalId: 'ewelink-id:binary:2', state: 0 }, + ]); + }); +}); diff --git a/server/test/services/ewelink/lib/features/features.test.js b/server/test/services/ewelink/lib/features/features.test.js index 6feb135935..0e7251d4eb 100644 --- a/server/test/services/ewelink/lib/features/features.test.js +++ b/server/test/services/ewelink/lib/features/features.test.js @@ -1,16 +1,18 @@ const { expect } = require('chai'); + +const { SERVICE_ID } = require('../constants'); const features = require('../../../../../services/ewelink/lib/features'); const { parseExternalId } = require('../../../../../services/ewelink/lib/utils/externalId'); -const GladysOfflineDevice = require('../../mocks/Gladys-offline.json'); -const GladysPowDevice = require('../../mocks/Gladys-pow.json'); -const GladysThDevice = require('../../mocks/Gladys-th.json'); -const Gladys2ChDevice = require('../../mocks/Gladys-2ch.json'); -const GladysUnhandledDevice = require('../../mocks/Gladys-unhandled.json'); -const eweLinkOfflineDevice = require('../../mocks/eweLink-offline.json'); -const eweLinkPowDevice = require('../../mocks/eweLink-pow.json'); -const eweLinkThDevice = require('../../mocks/eweLink-th.json'); -const eweLink2ChDevice = require('../../mocks/eweLink-2ch.json'); -const eweLinkUnhandledDevice = require('../../mocks/eweLink-unhandled.json'); +const GladysOfflineDevice = require('../payloads/Gladys-offline.json'); +const GladysPowDevice = require('../payloads/Gladys-pow.json'); +const GladysThDevice = require('../payloads/Gladys-th.json'); +const Gladys2ChDevice = require('../payloads/Gladys-2ch.json'); +const GladysUnhandledDevice = require('../payloads/Gladys-unhandled.json'); +const eweLinkOfflineDevice = require('../payloads/eweLink-offline.json'); +const eweLinkPowDevice = require('../payloads/eweLink-pow.json'); +const eweLinkThDevice = require('../payloads/eweLink-th.json'); +const eweLink2ChDevice = require('../payloads/eweLink-2ch.json'); +const eweLinkUnhandledDevice = require('../payloads/eweLink-unhandled.json'); describe('eWeLink features parseExternalId', () => { it('should return prefix, deviceId, channel and type', () => { @@ -22,36 +24,25 @@ 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('a810b8db-6d04-4697-bed3-c4b72c996279', eweLinkOfflineDevice, 0); + const device = features.getDevice(SERVICE_ID, eweLinkOfflineDevice); expect(device).to.deep.equal(GladysOfflineDevice); }); it('should return device with binary feature for a "POW" model', () => { - const device = features.getDevice('a810b8db-6d04-4697-bed3-c4b72c996279', eweLinkPowDevice, 1); + const device = features.getDevice(SERVICE_ID, eweLinkPowDevice); expect(device).to.deep.equal(GladysPowDevice); }); it('should return a device with 2 binary features for a "2CH" model', () => { - const device = features.getDevice('a810b8db-6d04-4697-bed3-c4b72c996279', eweLink2ChDevice, 2); + const device = features.getDevice(SERVICE_ID, eweLink2ChDevice); expect(device).to.deep.equal(Gladys2ChDevice); }); it('should return device with binary, humidity and temperature features for a "TH" model', () => { - const device = features.getDevice('a810b8db-6d04-4697-bed3-c4b72c996279', eweLinkThDevice, 1); + const device = features.getDevice(SERVICE_ID, eweLinkThDevice); expect(device).to.deep.equal(GladysThDevice); }); it('should return device without features for an unhandled model', () => { - const device = features.getDevice('a810b8db-6d04-4697-bed3-c4b72c996279', eweLinkUnhandledDevice, 0); + const device = features.getDevice(SERVICE_ID, eweLinkUnhandledDevice); expect(device).to.deep.equal(GladysUnhandledDevice); }); }); diff --git a/server/test/services/ewelink/lib/features/humidity.test.js b/server/test/services/ewelink/lib/features/humidity.test.js new file mode 100644 index 0000000000..eaf554080f --- /dev/null +++ b/server/test/services/ewelink/lib/features/humidity.test.js @@ -0,0 +1,15 @@ +const { expect } = require('chai'); + +const { readStates } = require('../../../../../services/ewelink/lib/features/humidity'); + +describe('eWeLink humidity feature -> readState', () => { + it('should return empty states', () => { + const states = readStates('ewelink-id', { switch: 'on' }); + expect(states).deep.eq([]); + }); + + it('should return humidity states', () => { + const states = readStates('ewelink-id', { currentHumidity: 17 }); + expect(states).deep.eq([{ featureExternalId: 'ewelink-id:humidity', state: 17 }]); + }); +}); diff --git a/server/test/services/ewelink/lib/features/temperature.test.js b/server/test/services/ewelink/lib/features/temperature.test.js new file mode 100644 index 0000000000..8d00f70e44 --- /dev/null +++ b/server/test/services/ewelink/lib/features/temperature.test.js @@ -0,0 +1,15 @@ +const { expect } = require('chai'); + +const { readStates } = require('../../../../../services/ewelink/lib/features/temperature'); + +describe('eWeLink temperature feature -> readState', () => { + it('should return empty states', () => { + const states = readStates('ewelink-id', { switch: 'on' }); + expect(states).deep.eq([]); + }); + + it('should return temperature states', () => { + const states = readStates('ewelink-id', { currentTemperature: 17 }); + expect(states).deep.eq([{ featureExternalId: 'ewelink-id:temperature', state: 17 }]); + }); +}); diff --git a/server/test/services/ewelink/lib/handlers/ewelink.handleRequest.test.js b/server/test/services/ewelink/lib/handlers/ewelink.handleRequest.test.js new file mode 100644 index 0000000000..4dbd195885 --- /dev/null +++ b/server/test/services/ewelink/lib/handlers/ewelink.handleRequest.test.js @@ -0,0 +1,207 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { stub, assert } = sinon; + +const EwelinkHandler = require('../../../../../services/ewelink/lib'); +const { SERVICE_ID } = require('../constants'); +const { ServiceNotConfiguredError, BadParameters, NotFoundError } = require('../../../../../utils/coreErrors'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants'); + +const tokens = { accessToken: 'ACCESS_TOKEN', refreshToken: 'REFRESH_TOKEN' }; + +describe('eWeLinkHandler handleRequest', () => { + let eWeLinkHandler; + let gladys; + + beforeEach(() => { + gladys = { + variable: { + setValue: stub().resolves(true), + destroy: stub().resolves(true), + }, + event: { + emit: stub().returns(null), + }, + }; + + eWeLinkHandler = new EwelinkHandler(gladys, null, SERVICE_ID); + eWeLinkHandler.status = { configured: true, connected: true }; + eWeLinkHandler.ewelinkWebAPIClient = { + user: { + refreshToken: stub().resolves({ error: 0, data: tokens }), + }, + }; + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should throws ServiceNotConfiguredError when service is not completly configured (not configured)', async () => { + eWeLinkHandler.status = { configured: false, connected: true }; + const request = stub().resolves({ data: 'SUCCESS' }); + + try { + await eWeLinkHandler.handleRequest(request); + assert.fail(); + } catch (e) { + expect(e).instanceOf(ServiceNotConfiguredError); + expect(e.message).to.equal('eWeLink is not ready, please complete the configuration'); + } + + assert.notCalled(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 ServiceNotConfiguredError when service is not completly configured (not connected)', async () => { + eWeLinkHandler.status = { configured: true, connected: false }; + const request = stub().resolves({ data: 'SUCCESS' }); + + try { + await eWeLinkHandler.handleRequest(request); + assert.fail(); + } catch (e) { + expect(e).instanceOf(ServiceNotConfiguredError); + expect(e.message).to.equal('eWeLink is not ready, please complete the configuration'); + } + + assert.notCalled(request); + assert.notCalled(eWeLinkHandler.ewelinkWebAPIClient.user.refreshToken); + assert.notCalled(gladys.variable.setValue); + assert.notCalled(gladys.variable.destroy); + assert.notCalled(gladys.event.emit); + }); + + it('should returns response data at first call', async () => { + const request = stub().resolves({ data: 'SUCCESS' }); + + const result = await eWeLinkHandler.handleRequest(request); + expect(result).eq('SUCCESS'); + + 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 retry once before returning response data', async () => { + const request = stub() + .onFirstCall() + .resolves({ data: 'RETRY', error: 402 }) + .resolves({ data: 'SUCCESS' }); + + const result = await eWeLinkHandler.handleRequest(request); + expect(result).eq('SUCCESS'); + + assert.calledTwice(request); + assert.alwaysCalledWithExactly(request); + assert.calledOnce(eWeLinkHandler.ewelinkWebAPIClient.user.refreshToken); + assert.calledOnceWithExactly(gladys.variable.setValue, 'USER_TOKENS', JSON.stringify(tokens), SERVICE_ID); + assert.notCalled(gladys.variable.destroy); + assert.notCalled(gladys.event.emit); + }); + + it('should retry only once even if all calls return 402 error', async () => { + const request = stub().resolves({ data: 'RETRY', error: 402, msg: 'ERROR FROM API' }); + + try { + await eWeLinkHandler.handleRequest(request); + assert.fail(); + } catch (e) { + expect(e).instanceOf(ServiceNotConfiguredError); + expect(e.message).to.equal('ERROR FROM API'); + } + + assert.calledTwice(request); + assert.alwaysCalledWithExactly(request); + assert.calledOnceWithExactly(eWeLinkHandler.ewelinkWebAPIClient.user.refreshToken); + assert.calledOnceWithExactly(gladys.variable.setValue, 'USER_TOKENS', JSON.stringify(tokens), SERVICE_ID); + assert.calledOnceWithExactly(gladys.variable.destroy, 'USER_TOKENS', SERVICE_ID); + assert.calledOnceWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: { configured: true, connected: false }, + }); + }); + + it('should not retry if refresh token returns 402 error', async () => { + eWeLinkHandler.ewelinkWebAPIClient.user.refreshToken = stub().resolves({ error: 402, msg: 'ERROR FROM API' }); + + const request = stub().resolves({ data: 'RETRY', error: 402 }); + + try { + await eWeLinkHandler.handleRequest(request); + assert.fail(); + } catch (e) { + expect(e).instanceOf(ServiceNotConfiguredError); + expect(e.message).to.equal('ERROR FROM API'); + } + + assert.calledOnceWithExactly(request); + assert.calledOnceWithExactly(eWeLinkHandler.ewelinkWebAPIClient.user.refreshToken); + assert.notCalled(gladys.variable.setValue); + assert.calledOnceWithExactly(gladys.variable.destroy, 'USER_TOKENS', SERVICE_ID); + assert.calledOnceWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: { configured: true, connected: false }, + }); + }); + + it('should throws BadParameters error on 400 error', async () => { + const request = stub().resolves({ data: 'ERROR', error: 400, msg: 'API ERROR' }); + + try { + await eWeLinkHandler.handleRequest(request); + assert.fail(); + } catch (e) { + expect(e).instanceOf(BadParameters); + 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 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/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/mocks/Gladys-2ch.json b/server/test/services/ewelink/lib/payloads/Gladys-2ch.json similarity index 89% rename from server/test/services/ewelink/mocks/Gladys-2ch.json rename to server/test/services/ewelink/lib/payloads/Gladys-2ch.json index a5b0efc6ad..78fd936ec4 100644 --- a/server/test/services/ewelink/mocks/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" @@ -42,6 +37,10 @@ { "name": "ONLINE", "value": "1" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/mocks/Gladys-Basic.json b/server/test/services/ewelink/lib/payloads/Gladys-Basic.json similarity index 63% rename from server/test/services/ewelink/mocks/Gladys-Basic.json rename to server/test/services/ewelink/lib/payloads/Gladys-Basic.json index 243c10550d..d63e643a0b 100644 --- a/server/test/services/ewelink/mocks/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", @@ -17,13 +16,20 @@ "has_feedback": true, "min": 0, "max": 1 + }, + { + "name": "Test 6 unknown", + "external_id": "ewelink:10004536ae:unknown", + "selector": "ewelink:10004536ae:unknown", + "category": "unknown", + "type": "unknown", + "read_only": false, + "has_feedback": true, + "min": 0, + "max": 1 } ], "params": [ - { - "name": "IP_ADDRESS", - "value": "192.168.0.6" - }, { "name": "FIRMWARE", "value": "3.1.2" @@ -31,6 +37,10 @@ { "name": "ONLINE", "value": "0" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/mocks/Gladys-offline.json b/server/test/services/ewelink/lib/payloads/Gladys-offline.json similarity index 62% rename from server/test/services/ewelink/mocks/Gladys-offline.json rename to server/test/services/ewelink/lib/payloads/Gladys-offline.json index 0f5bc8fb24..120baf9322 100644 --- a/server/test/services/ewelink/mocks/Gladys-offline.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-offline.json @@ -4,21 +4,16 @@ "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" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/mocks/Gladys-pow.json b/server/test/services/ewelink/lib/payloads/Gladys-pow.json similarity index 85% rename from server/test/services/ewelink/mocks/Gladys-pow.json rename to server/test/services/ewelink/lib/payloads/Gladys-pow.json index d77f7b9424..464ef899be 100644 --- a/server/test/services/ewelink/mocks/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" @@ -31,6 +26,10 @@ { "name": "ONLINE", "value": "1" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/mocks/Gladys-th.json b/server/test/services/ewelink/lib/payloads/Gladys-th.json similarity index 92% rename from server/test/services/ewelink/mocks/Gladys-th.json rename to server/test/services/ewelink/lib/payloads/Gladys-th.json index 293f771298..3c31b20c3b 100644 --- a/server/test/services/ewelink/mocks/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" @@ -55,6 +50,10 @@ { "name": "ONLINE", "value": "1" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/mocks/Gladys-unhandled.json b/server/test/services/ewelink/lib/payloads/Gladys-unhandled.json similarity index 63% rename from server/test/services/ewelink/mocks/Gladys-unhandled.json rename to server/test/services/ewelink/lib/payloads/Gladys-unhandled.json index 9400bb3e94..1f4462cda6 100644 --- a/server/test/services/ewelink/mocks/Gladys-unhandled.json +++ b/server/test/services/ewelink/lib/payloads/Gladys-unhandled.json @@ -4,21 +4,16 @@ "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" + }, + { + "name": "API_KEY", + "value": "validApikey" } ] } diff --git a/server/test/services/ewelink/mocks/eweLink-2ch.json b/server/test/services/ewelink/lib/payloads/eweLink-2ch.json similarity index 62% rename from server/test/services/ewelink/mocks/eweLink-2ch.json rename to server/test/services/ewelink/lib/payloads/eweLink-2ch.json index a674065359..7cd0a38a04 100644 --- a/server/test/services/ewelink/mocks/eweLink-2ch.json +++ b/server/test/services/ewelink/lib/payloads/eweLink-2ch.json @@ -4,12 +4,14 @@ "deviceid": "10004531ae", "apikey": "validApikey", "extra": { - "extra": { - "uiid": 7, - "model": "PSA-BHA-GL" - } + "uiid": 7, + "model": "PSA-BHA-GL" }, "params": { + "switches": [ + { "switch": "on", "outlet": 0 }, + { "switch": "off", "outlet": 1 } + ], "fwVersion": "3.3.0" }, "ip": "192.168.0.1", diff --git a/server/test/services/ewelink/mocks/eweLink-basic.json b/server/test/services/ewelink/lib/payloads/eweLink-basic.json similarity index 78% rename from server/test/services/ewelink/mocks/eweLink-basic.json rename to server/test/services/ewelink/lib/payloads/eweLink-basic.json index ae1ad8ff39..35467bc54c 100644 --- a/server/test/services/ewelink/mocks/eweLink-basic.json +++ b/server/test/services/ewelink/lib/payloads/eweLink-basic.json @@ -4,10 +4,8 @@ "deviceid": "10004536ae", "apikey": "validApikey", "extra": { - "extra": { - "uiid": 6, - "model": "PSA-BHA-GL" - } + "uiid": 6, + "model": "PSA-BHA-GL" }, "params": { "fwVersion": "3.3.0" diff --git a/server/test/services/ewelink/mocks/eweLink-offline.json b/server/test/services/ewelink/lib/payloads/eweLink-offline.json similarity index 74% rename from server/test/services/ewelink/mocks/eweLink-offline.json rename to server/test/services/ewelink/lib/payloads/eweLink-offline.json index 88b0319b91..b7efbb7eb4 100644 --- a/server/test/services/ewelink/mocks/eweLink-offline.json +++ b/server/test/services/ewelink/lib/payloads/eweLink-offline.json @@ -5,10 +5,8 @@ "deviceid": "10004532ae", "apikey": "validApikey", "extra": { - "extra": { - "uiid": 1, - "model": "PSF-BD1-GL" - } + "uiid": 1, + "model": "PSF-BD1-GL" }, "brandName": "SONOFF", "productModel": "MINI", diff --git a/server/test/services/ewelink/mocks/eweLink-pow.json b/server/test/services/ewelink/lib/payloads/eweLink-pow.json similarity index 78% rename from server/test/services/ewelink/mocks/eweLink-pow.json rename to server/test/services/ewelink/lib/payloads/eweLink-pow.json index f6f3a53b63..7a695aca7c 100644 --- a/server/test/services/ewelink/mocks/eweLink-pow.json +++ b/server/test/services/ewelink/lib/payloads/eweLink-pow.json @@ -4,12 +4,11 @@ "deviceid": "10004533ae", "apikey": "validApikey", "extra": { - "extra": { - "uiid": 5, - "model": "ITA-GZ1-GL" - } + "uiid": 5, + "model": "ITA-GZ1-GL" }, "params": { + "switch": "on", "fwVersion": "3.3.0" }, "ip": "192.168.0.3", diff --git a/server/test/services/ewelink/mocks/eweLink-th.json b/server/test/services/ewelink/lib/payloads/eweLink-th.json similarity index 67% rename from server/test/services/ewelink/mocks/eweLink-th.json rename to server/test/services/ewelink/lib/payloads/eweLink-th.json index dd7f16c814..a3a42bfc91 100644 --- a/server/test/services/ewelink/mocks/eweLink-th.json +++ b/server/test/services/ewelink/lib/payloads/eweLink-th.json @@ -4,12 +4,13 @@ "deviceid": "10004534ae", "apikey": "validApikey", "extra": { - "extra": { - "uiid": 15, - "model": "ITA-GZ1-GL" - } + "uiid": 15, + "model": "ITA-GZ1-GL" }, "params": { + "switch": "on", + "currentHumidity": 42, + "currentTemperature": 20, "fwVersion": "3.1.2" }, "ip": "192.168.0.4", diff --git a/server/test/services/ewelink/mocks/eweLink-unhandled.json b/server/test/services/ewelink/lib/payloads/eweLink-unhandled.json similarity index 71% rename from server/test/services/ewelink/mocks/eweLink-unhandled.json rename to server/test/services/ewelink/lib/payloads/eweLink-unhandled.json index 4409120554..da7e643fb8 100644 --- a/server/test/services/ewelink/mocks/eweLink-unhandled.json +++ b/server/test/services/ewelink/lib/payloads/eweLink-unhandled.json @@ -4,10 +4,8 @@ "deviceid": "10004535ae", "apikey": "validApikey", "extra": { - "extra": { - "uiid": 10000, - "model": "ITU-GB1-LG" - } + "uiid": 10000, + "model": "ITU-GB1-LG" }, "brandName": "SONOFF", "productModel": "UNKNOWN", diff --git a/server/test/services/ewelink/lib/user/ewelink.buildLoginUrl.test.js b/server/test/services/ewelink/lib/user/ewelink.buildLoginUrl.test.js new file mode 100644 index 0000000000..38d01fe41b --- /dev/null +++ b/server/test/services/ewelink/lib/user/ewelink.buildLoginUrl.test.js @@ -0,0 +1,43 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { fake, assert, match } = sinon; + +const EwelinkHandler = require('../../../../../services/ewelink/lib'); +const { SERVICE_ID } = require('../constants'); + +describe('eWeLinkHandler buildLoginUrl', () => { + let eWeLinkHandler; + + beforeEach(() => { + eWeLinkHandler = new EwelinkHandler({}, null, SERVICE_ID); + eWeLinkHandler.ewelinkWebAPIClient = { + oauth: { + createLoginUrl: fake.returns('LOGIN_URL'), + }, + }; + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should returns login URL and override loginState variable', () => { + eWeLinkHandler.loginState = 'any_state'; + + const redirectUrl = 'http://localhost:1440/redirectUrl'; + const loginUrl = eWeLinkHandler.buildLoginUrl({ redirectUrl }); + + assert.calledOnce(eWeLinkHandler.ewelinkWebAPIClient.oauth.createLoginUrl); + assert.calledWithMatch( + eWeLinkHandler.ewelinkWebAPIClient.oauth.createLoginUrl, + match({ + redirectUrl, + grantType: 'authorization_code', + }), + ); + + expect(eWeLinkHandler.loginState).not.eq('any_state'); + expect(loginUrl).eq('LOGIN_URL'); + }); +}); diff --git a/server/test/services/ewelink/lib/user/ewelink.deleteTokens.test.js b/server/test/services/ewelink/lib/user/ewelink.deleteTokens.test.js new file mode 100644 index 0000000000..8baf7223ff --- /dev/null +++ b/server/test/services/ewelink/lib/user/ewelink.deleteTokens.test.js @@ -0,0 +1,55 @@ +const sinon = require('sinon'); + +const { fake, assert } = sinon; + +const EwelinkHandler = require('../../../../../services/ewelink/lib'); +const { SERVICE_ID } = require('../constants'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants'); + +describe('eWeLinkHandler deleteTokens', () => { + let eWeLinkHandler; + let gladys; + + beforeEach(() => { + gladys = { + variable: { + destroy: fake.resolves(null), + }, + event: { + emit: fake.returns(null), + }, + }; + + eWeLinkHandler = new EwelinkHandler(gladys, null, SERVICE_ID); + eWeLinkHandler.ewelinkWebAPIClient = { + appId: 'APP_ID', + at: 'ACCESS_TOKEN', + rt: 'REFRESH_TOKEN', + request: { + delete: fake.resolves({}), + }, + }; + eWeLinkHandler.ewelinkWebSocketClient = {}; + eWeLinkHandler.status = { configured: true, connected: true }; + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should clear user tokens', async () => { + await eWeLinkHandler.deleteTokens(); + + assert.calledOnceWithExactly(eWeLinkHandler.ewelinkWebAPIClient.request.delete, '/v2/user/oauth/token', { + headers: { + 'X-CK-Appid': 'APP_ID', + Authorization: `Bearer ACCESS_TOKEN`, + }, + }); + assert.calledOnceWithExactly(gladys.variable.destroy, 'USER_TOKENS', SERVICE_ID); + assert.calledOnceWithExactly(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.EWELINK.STATUS, + payload: { configured: true, connected: false }, + }); + }); +}); diff --git a/server/test/services/ewelink/lib/user/ewelink.exchangeToken.test.js b/server/test/services/ewelink/lib/user/ewelink.exchangeToken.test.js new file mode 100644 index 0000000000..49c1b64964 --- /dev/null +++ b/server/test/services/ewelink/lib/user/ewelink.exchangeToken.test.js @@ -0,0 +1,108 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); + +const { fake, assert, match } = sinon; + +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'); + +const tokens = { accessToken: 'ACCESS_TOKEN', refreshToken: 'REFRESH_TOKEN' }; + +describe('eWeLinkHandler exchangeToken', () => { + let eWeLinkHandler; + let gladys; + + beforeEach(() => { + gladys = { + variable: { + setValue: fake.resolves(null), + }, + event: { + emit: fake.returns(null), + }, + }; + + eWeLinkHandler = new EwelinkHandler(gladys, null, SERVICE_ID); + eWeLinkHandler.ewelinkWebAPIClient = { + oauth: { + getToken: fake.resolves({ data: tokens }), + }, + }; + eWeLinkHandler.ewelinkWebSocketClient = { + 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(() => { + sinon.reset(); + }); + + it('should throw an error as loginState does not match', async () => { + const redirectUrl = 'http://localhost:1440'; + const code = 'auth_code'; + const region = 'app_region'; + const state = 'invalid_state'; + + try { + await eWeLinkHandler.exchangeToken({ redirectUrl, code, region, state }); + assert.fail(); + } catch (e) { + expect(e).instanceOf(BadParameters); + expect(e.message).eq('eWeLink login state is invalid'); + } + + assert.notCalled(eWeLinkHandler.ewelinkWebAPIClient.oauth.getToken); + 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 API key and emit event', async () => { + const redirectUrl = 'http://localhost:1440'; + const code = 'auth_code'; + const region = 'app_region'; + const state = 'LOGIN_STATE'; + + await eWeLinkHandler.exchangeToken({ redirectUrl, code, region, state }); + + assert.calledOnceWithExactly(eWeLinkHandler.ewelinkWebAPIClient.oauth.getToken, { + code, + redirectUrl, + region, + }); + assert.calledOnceWithExactly(retrieveUserApiKey); + assert.calledOnce(eWeLinkHandler.ewelinkWebSocketClient.Connect.create); + assert.calledWithMatch( + eWeLinkHandler.ewelinkWebSocketClient.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, + payload: { configured: true, connected: true }, + }); + }); +}); 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/version/ewelink.upgrade.test.js b/server/test/services/ewelink/lib/version/ewelink.upgrade.test.js new file mode 100644 index 0000000000..0ff4a7e49a --- /dev/null +++ b/server/test/services/ewelink/lib/version/ewelink.upgrade.test.js @@ -0,0 +1,130 @@ +const sinon = require('sinon'); + +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; + + beforeEach(() => { + gladys = { + variable: { + getValue: stub().resolves(null), + setValue: stub(), + destroy: stub(), + }, + device: { + get: stub().resolves(gladysDevices), + create: stub().resolves({}), + }, + }; + + eWeLinkHandler = new EwelinkHandler(gladys, null, SERVICE_ID); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should not apply updates', async () => { + gladys.variable.getValue.onFirstCall().resolves('10000'); + + await eWeLinkHandler.upgrade(); + + assert.calledOnceWithExactly(gladys.variable.getValue, 'SERVICE_VERSION', SERVICE_ID); + assert.notCalled(gladys.variable.setValue); + assert.notCalled(gladys.variable.destroy); + }); + + it('should apply all updates', async () => { + await eWeLinkHandler.upgrade(); + + assert.calledOnceWithExactly(gladys.variable.getValue, 'SERVICE_VERSION', SERVICE_ID); + + assert.callCount(gladys.variable.destroy, 3); + 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/test/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.test.js b/server/test/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.test.js new file mode 100644 index 0000000000..30e6264f6f --- /dev/null +++ b/server/test/services/ewelink/lib/websocket/ewelink.closeWebSocketClient.test.js @@ -0,0 +1,39 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { assert, fake } = sinon; + +const EwelinkHandler = require('../../../../../services/ewelink/lib'); +const { SERVICE_ID } = require('../constants'); + +describe('eWeLinkHandler closeWebSocketClient', () => { + let eWeLinkHandler; + + beforeEach(() => { + eWeLinkHandler = new EwelinkHandler({}, null, SERVICE_ID); + eWeLinkHandler.ewelinkWebSocketClient = { + Connect: {}, + }; + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should do nothing', async () => { + // Check client is not set first + expect(eWeLinkHandler.ewelinkWebSocketClient.ws).eq(undefined); + eWeLinkHandler.closeWebSocketClient(); + expect(eWeLinkHandler.ewelinkWebSocketClient.ws).eq(undefined); + }); + + it('should close websocket client', async () => { + eWeLinkHandler.ewelinkWebSocketClient.Connect.ws = { + close: fake.resolves(null), + }; + + eWeLinkHandler.closeWebSocketClient(); + + 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 new file mode 100644 index 0000000000..c86ae29eb7 --- /dev/null +++ b/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketClose.test.js @@ -0,0 +1,44 @@ +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); + +const { fake, assert } = sinon; + +const logger = { + warn: 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.createWebSocketClient': { createWebSocketClient }, +}); +const { SERVICE_ID } = require('../constants'); + +describe('eWeLinkHandler onWebSocketClose', () => { + let eWeLinkHandler; + + beforeEach(() => { + eWeLinkHandler = new EwelinkHandler({}, null, SERVICE_ID); + }); + + afterEach(() => { + sinon.reset(); + }); + + 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.onWebSocketError.test.js b/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketError.test.js new file mode 100644 index 0000000000..d522adca35 --- /dev/null +++ b/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketError.test.js @@ -0,0 +1,33 @@ +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); + +const { fake, assert } = sinon; + +const logger = { + error: fake.returns(null), +}; + +const onWebSocketError = proxyquire('../../../../../services/ewelink/lib/websocket/ewelink.onWebSocketError', { + '../../../../utils/logger': logger, +}); +const EwelinkHandler = proxyquire('../../../../../services/ewelink/lib', { + './websocket/ewelink.onWebSocketError': onWebSocketError, +}); +const { SERVICE_ID } = require('../constants'); + +describe('eWeLinkHandler onWebSocketError', () => { + let eWeLinkHandler; + + beforeEach(() => { + eWeLinkHandler = new EwelinkHandler({}, null, SERVICE_ID); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should log error', () => { + eWeLinkHandler.onWebSocketError({ message: 'THIS IS AN ERROR' }); + assert.calledOnceWithExactly(logger.error, 'eWeLink: WebSocket is on error: %s', 'THIS IS AN ERROR'); + }); +}); diff --git a/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.test.js b/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.test.js new file mode 100644 index 0000000000..f13d6db21a --- /dev/null +++ b/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketMessage.test.js @@ -0,0 +1,165 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { stub, assert } = sinon; + +const EwelinkHandler = require('../../../../../services/ewelink/lib'); +const { SERVICE_ID } = require('../constants'); +const { EVENTS } = require('../../../../../utils/constants'); + +const device = { params: [] }; + +describe('eWeLinkHandler onWebSocketMessage', () => { + let eWeLinkHandler; + let gladys; + + beforeEach(() => { + gladys = { + stateManager: { + get: stub() + .onFirstCall() + .returns(device) + .returns({ last_value: 45 }), + }, + event: { + emit: stub().returns(null), + }, + device: { + setParam: stub().resolves(null), + }, + }; + eWeLinkHandler = new EwelinkHandler(gladys, null, SERVICE_ID); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should do nothing, device is not found', async () => { + gladys.stateManager.get = stub().returns(null); + + 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); + assert.notCalled(gladys.device.setParam); + + expect(device).deep.eq({ params: [] }); + }); + + it('should do nothing, feature not exists', async () => { + gladys.stateManager.get.onSecondCall().returns(null); + + 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'); + assert.calledWithExactly(gladys.stateManager.get, 'deviceFeatureByExternalId', 'ewelink:known-device:binary:0'); + + assert.notCalled(gladys.event.emit); + assert.notCalled(gladys.device.setParam); + + expect(device).deep.eq({ params: [] }); + }); + + it('should not emit state event as feature is up-to-date', async () => { + const eventMessage = { + data: JSON.stringify({ + deviceid: 'known-device', + params: { currentHumidity: 45 }, + }), + }; + + await eWeLinkHandler.onWebSocketMessage(null, eventMessage); + + assert.callCount(gladys.stateManager.get, 2); + assert.calledWithExactly(gladys.stateManager.get, 'deviceByExternalId', 'ewelink:known-device'); + assert.calledWithExactly(gladys.stateManager.get, 'deviceFeatureByExternalId', 'ewelink:known-device:humidity'); + + assert.notCalled(gladys.event.emit); + assert.notCalled(gladys.device.setParam); + expect(device).deep.eq({ params: [] }); + }); + + it('should emit state event', async () => { + 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'); + assert.calledWithExactly(gladys.stateManager.get, 'deviceFeatureByExternalId', 'ewelink:known-device:binary:0'); + assert.calledWithExactly(gladys.stateManager.get, 'deviceFeatureByExternalId', 'ewelink:known-device:temperature'); + assert.calledWithExactly(gladys.stateManager.get, 'deviceFeatureByExternalId', 'ewelink:known-device:humidity'); + + assert.callCount(gladys.event.emit, 3); + assert.calledWithExactly(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'ewelink:known-device:binary:0', + state: 1, + }); + assert.calledWithExactly(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'ewelink:known-device:temperature', + state: 17, + }); + assert.calledWithExactly(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'ewelink:known-device:humidity', + state: 23, + }); + + assert.notCalled(gladys.device.setParam); + + expect(device).deep.eq({ params: [] }); + }); + + it('should update device params', async () => { + 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'); + + assert.notCalled(gladys.event.emit); + + 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); + }); +}); diff --git a/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketOpen.test.js b/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketOpen.test.js new file mode 100644 index 0000000000..60e4a25ff4 --- /dev/null +++ b/server/test/services/ewelink/lib/websocket/ewelink.onWebSocketOpen.test.js @@ -0,0 +1,33 @@ +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); + +const { fake, assert } = sinon; + +const logger = { + info: fake.returns(null), +}; + +const onWebSocketOpen = proxyquire('../../../../../services/ewelink/lib/websocket/ewelink.onWebSocketOpen', { + '../../../../utils/logger': logger, +}); +const EwelinkHandler = proxyquire('../../../../../services/ewelink/lib', { + './websocket/ewelink.onWebSocketOpen': onWebSocketOpen, +}); +const { SERVICE_ID } = require('../constants'); + +describe('eWeLinkHandler onWebSocketOpen', () => { + let eWeLinkHandler; + + beforeEach(() => { + eWeLinkHandler = new EwelinkHandler({}, null, SERVICE_ID); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should log info', () => { + eWeLinkHandler.onWebSocketOpen({ message: 'THIS IS AN ERROR' }); + assert.calledOnceWithExactly(logger.info, 'eWeLink: WebSocket is ready'); + }); +}); diff --git a/server/test/services/ewelink/mocks/consts.test.js b/server/test/services/ewelink/mocks/consts.test.js deleted file mode 100644 index 3fd2e2cc5f..0000000000 --- a/server/test/services/ewelink/mocks/consts.test.js +++ /dev/null @@ -1,136 +0,0 @@ -const Promise = require('bluebird'); -const { fake } = require('sinon'); -const GladysPowDevice = require('./Gladys-pow.json'); -const GladysOfflineDevice = require('./Gladys-offline.json'); -const Gladys2ChDevice = require('./Gladys-2ch.json'); -const GladysUnhandledDevice = require('./Gladys-unhandled.json'); -const GladysThDevice = require('./Gladys-th.json'); - -const serviceId = 'a810b8db-6d04-4697-bed3-c4b72c996279'; - -const event = { emit: fake.resolves(null) }; - -const variableNotConfigured = { - getValue: (valueId, notUsed) => { - return Promise.resolve(undefined); - }, - setValue: fake.returns(null), -}; - -const variableOk = { - getValue: (valueId, notUsed) => { - if (valueId === 'EWELINK_EMAIL') { - return Promise.resolve('email@valid.ok'); - } - if (valueId === 'EWELINK_PASSWORD') { - return Promise.resolve('S0m3Th1ngTru3'); - } - if (valueId === 'EWELINK_REGION') { - return Promise.resolve('eu'); - } - return Promise.resolve(undefined); - }, - setValue: fake.returns(null), -}; - -const variableOkNoRegion = { - getValue: (valueId, notUsed) => { - if (valueId === 'EWELINK_EMAIL') { - return Promise.resolve('email@valid.ok'); - } - if (valueId === 'EWELINK_PASSWORD') { - return Promise.resolve('S0m3Th1ngTru3'); - } - return Promise.resolve(undefined); - }, - setValue: fake.returns(null), -}; - -const variableOkFalseRegion = { - getValue: (valueId, notUsed) => { - if (valueId === 'EWELINK_EMAIL') { - return Promise.resolve('email@valid.ok'); - } - if (valueId === 'EWELINK_PASSWORD') { - return Promise.resolve('S0m3Th1ngTru3'); - } - if (valueId === 'EWELINK_REGION') { - return Promise.resolve('uk'); - } - return Promise.resolve(undefined); - }, - setValue: fake.returns(null), -}; - -const variableNok = { - getValue: (valueId, notUsed) => { - if (valueId === 'EWELINK_EMAIL') { - return Promise.resolve('email@unvalid.ko'); - } - if (valueId === 'EWELINK_PASSWORD') { - return Promise.resolve('S0m3Th1ngF4ls3'); - } - if (valueId === 'EWELINK_REGION') { - return Promise.resolve('eu'); - } - return Promise.resolve(undefined); - }, - setValue: fake.returns(null), -}; - -const deviceManagerFull = { - get: fake.resolves([Gladys2ChDevice, GladysOfflineDevice, GladysPowDevice, GladysThDevice, GladysUnhandledDevice]), -}; - -const stateManagerWith0Devices = { - get: (key, externalId) => { - return undefined; - }, -}; - -const stateManagerWith2Devices = { - get: (key, externalId) => { - if (externalId === 'ewelink:10004531ae') { - return Gladys2ChDevice; - } - if (externalId === 'ewelink:10004533ae') { - return GladysPowDevice; - } - return undefined; - }, -}; - -const stateManagerFull = { - get: (key, externalId) => { - if (externalId === 'ewelink:10004531ae') { - return Gladys2ChDevice; - } - if (externalId === 'ewelink:10004532ae') { - return GladysOfflineDevice; - } - if (externalId === 'ewelink:10004533ae') { - return GladysPowDevice; - } - if (externalId === 'ewelink:10004534ae') { - return GladysThDevice; - } - if (externalId === 'ewelink:10004535ae') { - return GladysUnhandledDevice; - } - return undefined; - }, -}; - -module.exports = { - serviceId, - event, - variableNotConfigured, - variableOk, - variableOkNoRegion, - variableOkFalseRegion, - variableNok, - deviceManagerFull, - stateManagerWith0Devices, - stateManagerWith2Devices, - stateManagerFull, -}; diff --git a/server/test/services/ewelink/mocks/ewelink-api-empty.mock.test.js b/server/test/services/ewelink/mocks/ewelink-api-empty.mock.test.js deleted file mode 100644 index e6e88a6f2a..0000000000 --- a/server/test/services/ewelink/mocks/ewelink-api-empty.mock.test.js +++ /dev/null @@ -1,100 +0,0 @@ -const Promise = require('bluebird'); - -class EwelinkApi { - constructor({ region = 'us', email, password, at, apiKey, devicesCache, arpTable }) { - this.region = region; - this.email = email; - this.password = password; - this.at = at || undefined; - this.apiKey = apiKey || undefined; - this.devicesCache = devicesCache; - this.arpTable = arpTable; - } - - getCredentials() { - if (this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') { - return Promise.resolve({ - at: 'validAccessToken', - user: { apikey: 'validApiKey' }, - region: 'eu', - }); - } - return Promise.resolve({ error: 401, msg: 'Authentication error' }); - } - - getRegion() { - if (this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') { - return Promise.resolve({ - email: this.email, - region: 'eu', - }); - } - return Promise.resolve({ error: 401, msg: 'Authentication error' }); - } - - getDevices() { - if ((this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') || this.at === 'validAccessToken') { - return Promise.resolve([]); - } - return Promise.resolve({ error: 401, msg: 'Authentication error' }); - } - - getDevice(deviceId) { - if ((this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') || this.at === 'validAccessToken') { - return Promise.resolve({ error: false, msg: 'Device does not exist' }); - } - return Promise.resolve({ error: 401, msg: 'Authentication error' }); - } - - getDeviceChannelCount(deviceId) { - if ((this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') || this.at === 'validAccessToken') { - return Promise.resolve({ error: false, msg: 'Device does not exist' }); - } - return Promise.resolve({ error: 401, msg: 'Authentication error' }); - } - - setDevicePowerState(deviceId, state, channel = 0) { - if (this.email === 'email@valid.ok' || (this.at === 'validAccessToken' && this.apiKey === 'validApiKey')) { - return Promise.resolve({ error: false, msg: 'Device does not exist' }); - } - return Promise.resolve({ error: 401, msg: 'Authentication error' }); - } - - getDevicePowerState(deviceId, channel = 1) { - if ((this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') || this.at === 'validAccessToken') { - return Promise.resolve({ error: false, msg: 'Device does not exist' }); - } - return Promise.resolve({ error: 401, msg: 'Authentication error' }); - } - - async getDevicePowerUsage(deviceId) { - if ((this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') || this.at === 'validAccessToken') { - return Promise.resolve({ error: false, msg: 'Device does not exist' }); - } - return Promise.resolve({ error: 401, msg: 'Authentication error' }); - } - - async getDeviceCurrentTH(deviceId, type = '') { - if ((this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') || this.at === 'validAccessToken') { - return Promise.resolve({ error: false, msg: 'Device does not exist' }); - } - return Promise.resolve({ error: 401, msg: 'Authentication error' }); - } - - getDeviceCurrentTemperature(deviceId) { - return this.getDeviceCurrentTH(deviceId, 'temp'); - } - - getDeviceCurrentHumidity(deviceId) { - return this.getDeviceCurrentTH(deviceId, 'humd'); - } - - getFirmwareVersion(deviceId) { - if ((this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') || this.at === 'validAccessToken') { - return Promise.resolve({ error: false, msg: 'Device does not exist' }); - } - return Promise.resolve({ error: 401, msg: 'Authentication error' }); - } -} - -module.exports = EwelinkApi; diff --git a/server/test/services/ewelink/mocks/ewelink-api.mock.test.js b/server/test/services/ewelink/mocks/ewelink-api.mock.test.js deleted file mode 100644 index 028fdcdc59..0000000000 --- a/server/test/services/ewelink/mocks/ewelink-api.mock.test.js +++ /dev/null @@ -1,158 +0,0 @@ -const Promise = require('bluebird'); -const EweLink2ChDevice = require('./eweLink-2ch.json'); -const EweLinkBasicDevice = require('./eweLink-basic.json'); -const EweLinkOfflineDevice = require('./eweLink-offline.json'); -const EweLinkPowDevice = require('./eweLink-pow.json'); -const EweLinkThDevice = require('./eweLink-th.json'); -const EweLinkUnhandledDevice = require('./eweLink-unhandled.json'); - -const fakeDevices = [EweLink2ChDevice, EweLinkOfflineDevice, EweLinkPowDevice, EweLinkThDevice, EweLinkUnhandledDevice]; - -class EwelinkApi { - constructor({ region = 'us', email, password, at, apiKey, devicesCache, arpTable }) { - this.region = region; - this.email = email; - this.password = password; - this.at = at || undefined; - this.apiKey = apiKey || undefined; - this.devicesCache = devicesCache; - this.arpTable = arpTable; - } - - getCredentials() { - if (this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') { - return Promise.resolve({ - at: 'validAccessToken', - user: { apikey: 'validApiKey' }, - region: 'eu', - }); - } - return Promise.resolve({ error: 406, msg: 'Authentication error' }); - } - - getRegion() { - if (this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') { - return Promise.resolve({ - email: this.email, - region: 'eu', - }); - } - return Promise.resolve({ error: 406, msg: 'Authentication error' }); - } - - getDevices() { - if ((this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') || this.at === 'validAccessToken') { - return Promise.resolve(fakeDevices); - } - return Promise.resolve({ error: 406, msg: 'Authentication error' }); - } - - getDevice(deviceId) { - if ((this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') || this.at === 'validAccessToken') { - const device = [...fakeDevices, EweLinkBasicDevice].find((fakeDevice) => fakeDevice.deviceid === deviceId); - if (device) { - if (deviceId === '10004531ae') { - return Promise.resolve({ ...device, params: { switches: [{ switch: 'on' }, { switch: 'off' }] } }); - } - if (deviceId === '10004533ae') { - return Promise.resolve({ ...device, params: { switch: 'on' } }); - } - if (deviceId === '10004534ae') { - return Promise.resolve({ ...device, params: { switch: 'on', currentHumidity: 42, currentTemperature: 20 } }); - } - if (deviceId === '10004536ae') { - return Promise.resolve(device); - } - return Promise.resolve(device); - } - return Promise.resolve({ error: false, msg: 'Device does not exist' }); - } - return Promise.resolve({ error: 406, msg: 'Authentication error' }); - } - - getDeviceChannelCount(deviceId) { - if ((this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') || this.at === 'validAccessToken') { - if (deviceId === '10004533ae' || deviceId === '10004534ae') { - return Promise.resolve({ status: 'ok', switchesAmount: 1 }); - } - if (deviceId === '10004531ae') { - return Promise.resolve({ status: 'ok', switchesAmount: 2 }); - } - return Promise.resolve({ error: false, msg: 'Device does not exist' }); - } - return Promise.resolve({ error: 406, msg: 'Authentication error' }); - } - - async setDevicePowerState(deviceId, state, channel = 0) { - const device = await this.getDevice(deviceId); - if (device && !device.error && device.online) { - return Promise.resolve({ status: 'ok', state }); - } - return Promise.resolve(device); - } - - async getDevicePowerState(deviceId, channel = 1) { - const device = await this.getDevice(deviceId); - if (device && !device.error && device.online) { - if (deviceId === '10004531ae' && channel === 2) { - return Promise.resolve({ status: 'ok', state: 'off' }); - } - return Promise.resolve({ status: 'ok', state: 'on' }); - } - return Promise.resolve(device); - } - - async getDevicePowerUsage(deviceId) { - const device = await this.getDevice(deviceId); - if (device && !device.error && device.online) { - return Promise.resolve({ - status: 'ok', - monthly: 22.3, - daily: [ - { day: 5, usage: 5.94 }, - { day: 4, usage: 3.64 }, - { day: 3, usage: 2.39 }, - { day: 2, usage: 3.1 }, - { day: 1, usage: 7.23 }, - ], - }); - } - return Promise.resolve(device); - } - - async getDeviceCurrentTH(deviceId, type = '') { - const device = await this.getDevice(deviceId); - if (device && !device.error && device.online) { - const data = { status: 'ok', temperature: 20, humidity: 42 }; - if (type === 'temp') { - delete data.humidity; - } - if (type === 'humd') { - delete data.temperature; - } - return Promise.resolve(data); - } - return Promise.resolve(device); - } - - getDeviceCurrentTemperature(deviceId) { - return this.getDeviceCurrentTH(deviceId, 'temp'); - } - - getDeviceCurrentHumidity(deviceId) { - return this.getDeviceCurrentTH(deviceId, 'humd'); - } - - getFirmwareVersion(deviceId) { - if ((this.email === 'email@valid.ok' && this.password === 'S0m3Th1ngTru3') || this.at === 'validAccessToken') { - const device = fakeDevices.find((fakeDevice) => fakeDevice.deviceid === deviceId); - if (device && device.params && device.params.fwVersion) { - return Promise.resolve({ status: 'ok', fwVersion: device.params.fwVersion }); - } - return Promise.resolve({ error: false, msg: 'Device does not exist' }); - } - return Promise.resolve({ error: 406, msg: 'Authentication error' }); - } -} - -module.exports = EwelinkApi; diff --git a/server/utils/constants.js b/server/utils/constants.js index 38fbe4bcef..97bb1de783 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -952,9 +952,7 @@ const WEBSOCKET_MESSAGE_TYPES = { DISCOVER: 'bluetooth.discover', }, EWELINK: { - CONNECTED: 'ewelink.connected', - NEW_DEVICE: 'ewelink.new-device', - ERROR: 'ewelink.error', + STATUS: 'ewelink.status', }, BROADLINK: { LEARN_MODE: 'broadlink.learn', 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;