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 (
+
+ );
+ }
+}
+
+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);
-
+ 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 }) => (
+
+);
+
+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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
}
+ />
+
+
+
+
+ {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;