diff --git a/front/src/assets/integrations/cover/zwave-js-ui.jpg b/front/src/assets/integrations/cover/zwave-js-ui.jpg
new file mode 100644
index 0000000000..0ee9094967
Binary files /dev/null and b/front/src/assets/integrations/cover/zwave-js-ui.jpg differ
diff --git a/front/src/assets/integrations/logos/logo_zwave-js-ui.png b/front/src/assets/integrations/logos/logo_zwave-js-ui.png
new file mode 100644
index 0000000000..ae33ff4cc3
Binary files /dev/null and b/front/src/assets/integrations/logos/logo_zwave-js-ui.png differ
diff --git a/front/src/components/app.jsx b/front/src/components/app.jsx
index a63c43cccd..4b42cc94a7 100644
--- a/front/src/components/app.jsx
+++ b/front/src/components/app.jsx
@@ -130,6 +130,13 @@ 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';
+// ZwaveJSUI
+import ZwaveJSUIDevicePage from '../routes/integration/all/zwave-js-ui/device-page';
+import ZwaveJSUIDeviceOperationPage from '../routes/integration/all/zwave-js-ui/node-operation-page';
+import ZwaveJSUIDiscoverPage from '../routes/integration/all/zwave-js-ui/discover-page';
+import ZwaveJSUISettingsPage from '../routes/integration/all/zwave-js-ui/settings-page';
+import ZwaveJSUIEditPage from '../routes/integration/all/zwave-js-ui/edit-page';
+
// OpenAI integration
import OpenAIPage from '../routes/integration/all/openai/index';
@@ -291,6 +298,13 @@ const AppRouter = connect(
+
+
+
+
+
+
+
diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json
index f38e921e51..61c2e1eebe 100644
--- a/front/src/config/i18n/en.json
+++ b/front/src/config/i18n/en.json
@@ -811,6 +811,126 @@
"noValueReceived": "No value received."
}
},
+ "zwavejsui": {
+ "title": "Z-Wave",
+ "description": "Control your Z-Wave devices.",
+ "deviceTab": "Devices",
+ "networkTab": "Network",
+ "settingsTab": "Settings",
+ "discoverTab": "Discover",
+ "documentation": "Z-Wave JS UI documentation",
+ "status": {
+ "error": "Failed to connect to Zwave network",
+ "connected": "Zwave network connected",
+ "notConfigured": "ZwaveJSUI USB dongle is not configured, please go to ",
+ "notEnabled": "ZwaveJSUI is not activated, please go to ",
+ "mqttNotInstalled": "MQTT broker failed to install",
+ "mqttNotRunning": "MQTT broker failed to start",
+ "zwave-js-uiNotInstalled": "ZwaveJSUI failed to install",
+ "zwave-js-uiNotRunning": "ZwaveJSUI failed to start",
+ "zwave-js-uiNotConnected": "ZwaveJSUI failed to connect to MQTT",
+ "settingsPageLink": "USB dongle configuration page",
+ "setupPageLink": "ZwaveJSUI configuration page"
+ },
+ "device": {
+ "title": "Z-Wave Devices",
+ "search": "Search devices",
+ "noDevices": "No Z-Wave devices added yet.",
+ "scanButton": "Scan",
+ "nameLabel": "Name",
+ "roomLabel": "Room",
+ "featuresLabel": "Features",
+ "saveButton": "Save",
+ "deleteButton": "Delete",
+ "editButton": "Edit",
+ "mostRecentValueAt": "Last value received {{mostRecentValueAt}}.",
+ "noValueReceived": "No value received.",
+ "conflictError": "Current device is already in Gladys.",
+ "deviceUpdatedSuccess": "The device was successfully added",
+ "updateDeviceError": "There was an error while creating this device in Gladys."
+ },
+ "discover": {
+ "title": "Z-Wave Devices",
+ "noDeviceDiscovered": "No Z-Wave devices found. Have you correctly configured Zwavejs UI in Settings?",
+ "hideExistingDevices": "Hide already added devices",
+ "addNodeButton": "Add",
+ "addNodeSecureButton": "Add Secure",
+ "removeNode": "Remove",
+ "scanButton": "Scan",
+ "manufacturer": "Manufacturer",
+ "name": "Name",
+ "scanInProgressText": "Scan in Progress...",
+ "createDeviceInGladys": "Connect in Gladys",
+ "features": "Features",
+ "params": "Params",
+ "nodeId": "Node",
+ "zwaveNotConfigured": "Z-wave is not configured. Please configure ZWave JS UI in settings.",
+ "createDeviceError": "There was an error while creating this device in Gladys.",
+ "conflictError": "A device with this name already exist, please rename the device or delete the existing device.",
+ "deviceCreatedSuccess": "The device was added with success.",
+ "unknowNode": "Unknow node",
+ "sleepingNodeMsg": "Node sleeping or dead. Wake it up then refresh this page.",
+ "createGithubIssue": "Report a bug with this device"
+ },
+ "settings": {
+ "title": "Z-Wave Settings",
+ "description": "This service uses two independent docker containers (MQTT broker and ZwaveJS UI). The administration of ZwaveJS UI is available at following address, user = admin, password = zwave.",
+ "zwave-js-ui": "Zwavejs UI interface",
+ "zwaveJSUIVersionError": "Zwavejs UI version not supported: expected version {{expectedVersion}} but current version is {{currentVersion}}",
+ "securityKeysDescription": "If you are migrating from an existing ZWave network, please enter the 4 security keys of the network. If you are using ZwaveJS UI, you can found these keys in the administration interface: Section Settings > Z-Wave. Itherwise, feel free to keep these fields empty, security keys will be generated for you.",
+ "urlLabel": "Broker URL",
+ "urlPlaceholder": "Ex: mqtt://[mqtt-broker-address]:[port]",
+ "userLabel": "Username",
+ "userPlaceholder": "Enter MQTT broker username",
+ "passwordLabel": "Password",
+ "passwordPlaceholder": "Enter MQTT broker password",
+ "mqttTopicPrefixLabel": "Topic Prefix",
+ "mqttTopicPrefixPlaceholder": "Enter MQTT broker topic prefix",
+ "mqttTopicWithLocationLabel": "Node location in topic",
+ "mqttTopicWithLocationDescription": "Add nodes location to values topic",
+ "s2UnauthenticatedKeyLabel": "S2 Unauthenticated",
+ "s2UnauthenticatedKeyPlaceholder": "Enter S2 Unauthenticated key",
+ "s2AuthenticatedKeyLabel": "S2 Authenticated",
+ "s2AuthenticatedKeyPlaceholder": "Enter S2 Authenticated key",
+ "s2AccessControlKeyLabel": "S2 AccessControlKey",
+ "s2AccessControlKeyPlaceholder": "Enter S2 AccessControl key",
+ "s0LegacyKeyLabel": "S0 LegacyKey",
+ "s0LegacyKeyPlaceholder": "Enter S0 Legacy key",
+ "connectButton": "Connect/Reconnect",
+ "disconnectButton": "Disconnect",
+ "connected": "Zwavejs UI was started with success.",
+ "notConnected": "Zwavejs UI is not connected",
+ "zwaveNotConfigured": "Z-wave is not configured. Please configure the Zwavejs UI in settings.",
+ "zwaveUsbNotConfigured": "Z-wave USB stick is not configured. Please configure the Zwavejs UI in settings.",
+ "connecting": "Trying to connect to Z-Wave USB stick...",
+ "zwaveUsbDriverPathLabel": "Select the USB port where your Z-Wave stick is connected",
+ "refreshButton": "Refresh USB list",
+ "error": "An error occured while saving configuration.",
+ "nonDockerEnv": "Gladys is not running on Docker, you cannot install a MQTT broker from here.",
+ "invalidDockerNetwork": "Gladys is under custom installation, to install broker from here, Gladys container should be configured with \"host\" network mode.",
+ "externalZwaveJSUI": "External Zwavejs UI",
+ "containersStatus": "ZwaveJSUI Containers",
+ "serviceStatus": "Zwavejs UI Service Status",
+ "link": "Link",
+ "mqttZwavejsLink": "MQTT - ZwaveJS",
+ "gladysMqttLink": "Gladys - MQTT",
+ "zwave2Mqtt": "Zwavejs UI",
+ "gladys": "Gladys",
+ "mqtt": "MQTT",
+ "status": "Status"
+ },
+ "nodeOperation": {
+ "addNodeInstructions": "You can now include your device following instructions in your device manual.",
+ "removeNodeInstructions": "You can now exclude your device following instructions in your device manual.",
+ "addNodeTitle": "Inclusion Mode",
+ "removeNodeTitle": "Exclusion Mode",
+ "seconds": "seconds remaining",
+ "cancelButton": "Cancel",
+ "noNodeAdded": "No new device discovered, please click on scan button.",
+ "nodeAddedTitle": "A new node was found",
+ "nodeAddedDescription": "Wait a few seconds while we get all of the information from this node..."
+ }
+ },
"openWeather": {
"title": "OpenWeather API",
"description": "Display the weather in your town on your dashboard.",
diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json
index c729195c25..d081742fa2 100644
--- a/front/src/config/i18n/fr.json
+++ b/front/src/config/i18n/fr.json
@@ -936,6 +936,126 @@
"noValueReceived": "Aucune valeur reçue."
}
},
+ "zwavejsui": {
+ "title": "Z-Wave",
+ "description": "Contrôlez vos appareils Z-Wave.",
+ "deviceTab": "Appareils",
+ "networkTab": "Réseau",
+ "settingsTab": "Paramètres",
+ "discoverTab": "Découverte Z-Wave",
+ "documentation": "Documentation Z-Wave JS UI",
+ "status": {
+ "error": "Impossible de se connecter au réseau Zwave",
+ "connected": "Réseau Zwave connecté",
+ "notConfigured": "Aucun dongle USB ZwaveJS UI configuré, veuillez vous rendre sur ",
+ "notEnabled": "Le service ZwaveJS UI n'est pas activé, veuillez vous rendre sur ",
+ "mqttNotInstalled": "Le broker MQTT n'a pas pu être installé.",
+ "mqttNotRunning": "Le broker MQTT n'a pas démarré.",
+ "zwave-js-uiNotInstalled": "ZwaveJS UI n'a pas pu être installé.",
+ "zwave-js-uiNotRunning": "ZwaveJS UI n'a pas démarré.",
+ "zwave-js-uiNotConnected": "ZwaveJS UI n'a pas réussi à se connecter au broker MQTT",
+ "settingsPageLink": "la page de paramétrage du dongle USB",
+ "setupPageLink": "la page de configuration de ZwaveJSUI"
+ },
+ "device": {
+ "title": "Appareils Z-Wave",
+ "search": "Chercher un appareil",
+ "noDevices": "Aucun appareil Z-Wave n'a encore été ajouté.",
+ "scanButton": "Rechercher",
+ "nameLabel": "Nom",
+ "roomLabel": "Pièce",
+ "featuresLabel": "Fonctionnalités",
+ "saveButton": "Sauvegarder",
+ "deleteButton": "Supprimer",
+ "editButton": "Editer",
+ "mostRecentValueAt": "Dernière valeur reçue {{mostRecentValueAt}}.",
+ "noValueReceived": "Aucune valeur reçue.",
+ "conflictError": "L'appareil actuel est déjà dans Gladys.",
+ "deviceUpdatedSuccess": "L'appareil a été ajouté avec succès.",
+ "updateDeviceError": "Une erreur s'est produite lors de la création de cet appareil dans Gladys."
+ },
+ "discover": {
+ "title": "Appareils Z-Wave",
+ "noDeviceDiscovered": "Aucun appareil Z-Wave trouvé. Avez-vous correctement configuré Zwavejs UI dans les paramètres ?",
+ "hideExistingDevices": "Cacher les appareils déjà ajoutés",
+ "addNodeButton": "Ajouter",
+ "addNodeSecureButton": "Ajout sécurisé",
+ "removeNode": "Supprimer",
+ "scanButton": "Recherche",
+ "manufacturer": "Fabricant",
+ "name": "Nom",
+ "scanInProgressText": "Recherche en cours...",
+ "createDeviceInGladys": "Connecter dans Gladys",
+ "features": "Fonctionnalités",
+ "params": "Paramètre",
+ "nodeId": "Noeud",
+ "zwaveNotConfigured": "Ce service Z-wave n'est pas configuré. Veuillez configurer le service Zwavejs UI dans les paramètres.",
+ "createDeviceError": "Une erreur s'est produite lors de la création de cet appareil dans Gladys.",
+ "conflictError": "Un appareil avec ce nom existe déjà, merci de renommer cet appareil ou de supprimer l'existant.",
+ "deviceCreatedSuccess": "L'appareil a été ajouté avec succès.",
+ "unknowNode": "Noeud inconnu",
+ "sleepingNodeMsg": "Noeud endormi ou mort. Réveillez le noeud puis rafraîchissez cette page.",
+ "createGithubIssue": "Signaler un bug avec cet appareil"
+ },
+ "settings": {
+ "title": "Paramètres Z-Wave",
+ "description": "Ce service utilise deux containers Docker (MQTT broker and ZwaveJS UI). L'interface ZwaveJS UI est disponible à l'URL ci-dessous avec user = admin, mot de passe = zwave.",
+ "zwave-js-ui": "Zwavejs UI interface",
+ "zwaveJSUIVersionError": "Zwavejs UI version non supportée: version supportée {{expectedVersion}} mais version actuelle est {{currentVersion}} ",
+ "securityKeysDescription": "Si vous migrer un réseau ZWave existant, spécifier ci-dessous les 4 clés de sécurité du réseau. Si vous utilisez ZWave JS UI, vous trouverez ces clés dans l'interface d'administration ZWave JS UI: Section Settings > Z-Wave. Dans le cas contraire, laisser les champs vides, ces clés seront générées pour vous.",
+ "urlLabel": "URL du broker",
+ "urlPlaceholder": "Ex: mqtt://[adresse-broker-mqtt]:[port]",
+ "userLabel": "Nom d'utilisateur",
+ "userPlaceholder": "Entrez le nom d'utilisateur du broker MQTT",
+ "passwordLabel": "Mot de passe",
+ "passwordPlaceholder": "Entrez le mot de passe du broker MQTT",
+ "mqttTopicPrefixLabel": "Topic Préfix",
+ "mqttTopicPrefixPlaceholder": "Entrez le préfix du topic MQTT à écouter",
+ "mqttTopicWithLocationLabel": "Topic avec localisation",
+ "mqttTopicWithLocationDescription": "La topic contient l'endroit où se trouve le noeud",
+ "s2UnauthenticatedKeyLabel": "S2 Unauthenticated",
+ "s2UnauthenticatedKeyPlaceholder": "Entrez la clé S2 Unauthenticated",
+ "s2AuthenticatedKeyLabel": "S2 Authenticated",
+ "s2AuthenticatedKeyPlaceholder": "Entrez la clé S2 Authenticated",
+ "s2AccessControlKeyLabel": "S2 AccessControl",
+ "s2AccessControlKeyPlaceholder": "Entrez la clé S2 AccessControl",
+ "s0LegacyKeyLabel": "S0 LegacyKey",
+ "s0LegacyKeyPlaceholder": "Entrez la clé S0 Legacy",
+ "connectButton": "Connecter/Reconnecter",
+ "disconnectButton": "Déconnecter",
+ "connected": "Zwavejs UI démarré avec succès.",
+ "notConnected": "Zwavejs UI n'est pas connecté",
+ "zwaveNotConfigured": "Ce service Z-wave n'est pas configuré. Veuillez configurer Zwavejs UI dans les paramètres.",
+ "zwaveUsbNotConfigured": "La clé USB Z-Wave n'est pas configuré. Veuillez configurer Zwavejs UI dans les paramètres.",
+ "connecting": "Tentative de connexion à la clé USB Z-Wave...",
+ "zwaveUsbDriverPathLabel": "Sélectionnez le port USB auquel votre clé Z-Wave est connecté",
+ "refreshButton": "Rafraîchir la liste des appareils USB",
+ "error": "Une erreur s'est produite au démarrage du service Zwavejs UI.",
+ "nonDockerEnv": "Gladys ne s'exécute actuellement pas dans Docker, il est impossible d'activer ZwaveJSUI depuis Gladys.",
+ "invalidDockerNetwork": "Gladys est basée sur une installation personalisée, pour installer ZwaveJSUI depuis cette page, le conteneur Docker de Gladys doit être démarré avec l'option \"network=host\".",
+ "externalZwaveJSUI": "ZwaveJS UI externe",
+ "containersStatus": "Conteneurs liés à Zwavejs UI",
+ "serviceStatus": "Etat du service Zwavejs UI",
+ "link": "Lien",
+ "mqttZwavejsLink": "MQTT - ZwaveJS",
+ "gladysMqttLink": "Gladys - MQTT",
+ "zwave2Mqtt": "Zwavejs UI",
+ "gladys": "Gladys",
+ "mqtt": "MQTT",
+ "status": "Status"
+ },
+ "nodeOperation": {
+ "addNodeInstructions": "Vous pouvez maintenant inclure votre appareil en suivant les instructions du manuel de celui-ci.",
+ "removeNodeInstructions": "Vous pouvez désormais exclure votre appareil en suivant les instructions du manuel de celui-ci.",
+ "addNodeTitle": "Mode d'inclusion",
+ "removeNodeTitle": "Mode d'exclusion",
+ "seconds": "secondes restantes",
+ "cancelButton": "Annuler",
+ "noNodeAdded": "Aucun nouvel appareil Z-Wave n'a encore été ajouté, essayez .",
+ "nodeAddedTitle": "Un nouveau nœud a été trouvé",
+ "nodeAddedDescription": "Attendez quelques secondes pendant que nous obtenons toutes les informations de ce nœud..."
+ }
+ },
"openWeather": {
"title": "API OpenWeather",
"description": "Affichez les données de météo de votre ville dans Gladys.",
diff --git a/front/src/config/integrations/devices.json b/front/src/config/integrations/devices.json
index 95915d48de..b5ed7f900f 100644
--- a/front/src/config/integrations/devices.json
+++ b/front/src/config/integrations/devices.json
@@ -3,6 +3,11 @@
"key": "zwave",
"img": "/assets/integrations/cover/zwave.jpg"
},
+ {
+ "key": "zwavejsui",
+ "link": "zwave-js-ui",
+ "img": "/assets/integrations/cover/zwave-js-ui.jpg"
+ },
{
"key": "rtspCamera",
"link": "rtsp-camera",
diff --git a/front/src/routes/integration/all/zwave-js-ui/ZwaveJSUIPage.js b/front/src/routes/integration/all/zwave-js-ui/ZwaveJSUIPage.js
new file mode 100644
index 0000000000..2579633cb5
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/ZwaveJSUIPage.js
@@ -0,0 +1,72 @@
+import { Text } from 'preact-i18n';
+import { Link } from 'preact-router/match';
+import DeviceConfigurationLink from '../../../../components/documentation/DeviceConfigurationLink';
+
+const DashboardSettings = ({ children, user }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{children}
+
+
+
+
+
+);
+
+export default DashboardSettings;
diff --git a/front/src/routes/integration/all/zwave-js-ui/device-page/Device.jsx b/front/src/routes/integration/all/zwave-js-ui/device-page/Device.jsx
new file mode 100644
index 0000000000..feefb32faa
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/device-page/Device.jsx
@@ -0,0 +1,191 @@
+import { Text, Localizer } from 'preact-i18n';
+import { Component } from 'preact';
+import cx from 'classnames';
+import get from 'get-value';
+import { Link } from 'preact-router/match';
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+
+dayjs.extend(relativeTime);
+
+import { DEVICE_FEATURE_CATEGORIES } from '../../../../../../../server/utils/constants';
+import { RequestStatus } from '../../../../../utils/consts';
+import BatteryLevelFeature from '../../../../../components/device/view/BatteryLevelFeature';
+import DeviceFeatures from '../../../../../components/device/view/DeviceFeatures';
+
+class Device extends Component {
+ refreshDeviceProperty = () => {
+ if (!this.props.device.features) {
+ return null;
+ }
+ const batteryLevelDeviceFeature = this.props.device.features.find(
+ deviceFeature => deviceFeature.category === DEVICE_FEATURE_CATEGORIES.BATTERY
+ );
+ const batteryLevel = get(batteryLevelDeviceFeature, 'last_value');
+ let mostRecentValueAt = null;
+ this.props.device.features.forEach(feature => {
+ if (feature.last_value_changed && new Date(feature.last_value_changed) > mostRecentValueAt) {
+ mostRecentValueAt = new Date(feature.last_value_changed);
+ }
+ });
+ this.setState({
+ batteryLevel,
+ mostRecentValueAt
+ });
+ };
+
+ saveDevice = async () => {
+ this.setState({ loading: true, error: undefined });
+ try {
+ await this.props.saveDevice(this.props.device);
+ this.setState({ deviceUpdated: true });
+ } catch (e) {
+ const status = get(e, 'response.status');
+ if (status === 409) {
+ this.setState({ error: RequestStatus.ConflictError });
+ } else {
+ this.setState({ error: RequestStatus.Error });
+ }
+ }
+ this.setState({ loading: false });
+ };
+
+ deleteDevice = async () => {
+ this.setState({ loading: true, error: undefined });
+ try {
+ await this.props.deleteDevice(this.props.device, this.props.deviceIndex);
+ } catch (e) {
+ this.setState({ error: RequestStatus.Error });
+ }
+ this.setState({ loading: false });
+ };
+
+ updateName = e => {
+ this.props.updateDeviceProperty(this.props.deviceIndex, 'name', e.target.value);
+ };
+
+ updateRoom = e => {
+ this.props.updateDeviceProperty(this.props.deviceIndex, 'room_id', e.target.value);
+ };
+
+ componentWillMount() {
+ this.refreshDeviceProperty();
+ }
+
+ componentWillUpdate() {
+ this.refreshDeviceProperty();
+ }
+
+ render(props, { batteryLevel, mostRecentValueAt, loading, error, deviceUpdated }) {
+ return (
+
+
+
+
+
+
+ {error === RequestStatus.Error && (
+
+
+
+ )}
+ {error === RequestStatus.ConflictError && (
+
+
+
+ )}
+ {deviceUpdated && (
+
+
+
+ )}
+
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+ {props.houses &&
+ props.houses.map(house => (
+
+ {house.rooms &&
+ house.rooms.map(room => (
+
+ {room.name}
+
+ ))}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default Device;
diff --git a/front/src/routes/integration/all/zwave-js-ui/device-page/DeviceTab.jsx b/front/src/routes/integration/all/zwave-js-ui/device-page/DeviceTab.jsx
new file mode 100644
index 0000000000..3f8a404490
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/device-page/DeviceTab.jsx
@@ -0,0 +1,58 @@
+import { Text, Localizer } from 'preact-i18n';
+import cx from 'classnames';
+
+import EmptyState from './EmptyState';
+import { RequestStatus } from '../../../../../utils/consts';
+import Device from './Device';
+import style from './style.css';
+import CardFilter from '../../../../../components/layout/CardFilter';
+
+const DeviceTab = ({ children, ...props }) => (
+
+
+
+
+
+
+ {props.zwaveDevices && props.zwaveDevices.length === 0 &&
}
+ {props.zwaveGetDevicesStatus === RequestStatus.Getting &&
}
+
+ {props.zwaveDevices &&
+ props.zwaveDevices.map((zwaveDevice, index) => (
+
+ ))}
+
+
+
+
+
+);
+
+export default DeviceTab;
diff --git a/front/src/routes/integration/all/zwave-js-ui/device-page/EmptyState.jsx b/front/src/routes/integration/all/zwave-js-ui/device-page/EmptyState.jsx
new file mode 100644
index 0000000000..71fa46950b
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/device-page/EmptyState.jsx
@@ -0,0 +1,13 @@
+import { Text } from 'preact-i18n';
+import cx from 'classnames';
+import style from './style.css';
+
+const EmptyState = () => (
+
+);
+
+export default EmptyState;
diff --git a/front/src/routes/integration/all/zwave-js-ui/device-page/actions.js b/front/src/routes/integration/all/zwave-js-ui/device-page/actions.js
new file mode 100644
index 0000000000..799656b041
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/device-page/actions.js
@@ -0,0 +1,73 @@
+import { RequestStatus } from '../../../../../utils/consts';
+import { DEFAULT } from '../../../../../../../server/services/zwave-js-ui/lib/constants';
+import update from 'immutability-helper';
+import createActionsHouse from '../../../../../actions/house';
+import debounce from 'debounce';
+
+function createActions(store) {
+ const houseActions = createActionsHouse(store);
+ const actions = {
+ async getZWaveDevices(state) {
+ store.setState({
+ zwaveGetDevicesStatus: RequestStatus.Getting
+ });
+ try {
+ const options = {
+ orderDir: state.orderDir || DEFAULT.NODES_ORDER_DIR
+ };
+ if (state.searchKeyword && state.searchKeyword.length) {
+ options.search = state.searchKeyword;
+ }
+ const zwaveDevices = await state.httpClient.get('/api/v1/service/zwave-js-ui/device', options);
+ store.setState({
+ zwaveDevices,
+ zwaveGetDevicesStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ zwaveGetDevicesStatus: RequestStatus.Error
+ });
+ }
+ },
+ async saveDevice(state, device) {
+ await state.httpClient.post('/api/v1/device', device);
+ },
+ updateDeviceProperty(state, index, property, value) {
+ const newState = update(state, {
+ zwaveDevices: {
+ [index]: {
+ [property]: {
+ $set: value
+ }
+ }
+ }
+ });
+ store.setState(newState);
+ },
+ async deleteDevice(state, device, index) {
+ await state.httpClient.delete(`/api/v1/device/${device.selector}`);
+ const newState = update(state, {
+ zwaveDevices: {
+ $splice: [[index, 1]]
+ }
+ });
+ store.setState(newState);
+ },
+ async search(state, e) {
+ store.setState({
+ searchKeyword: e.target.value
+ });
+ await actions.getZWaveDevices(store.getState());
+ },
+ async changeOrderDir(state, e) {
+ store.setState({
+ orderDir: e.target.value
+ });
+ await actions.getZWaveDevices(store.getState());
+ }
+ };
+ actions.debouncedSearch = debounce(actions.search, 200);
+ return Object.assign({}, houseActions, actions);
+}
+
+export default createActions;
diff --git a/front/src/routes/integration/all/zwave-js-ui/device-page/index.js b/front/src/routes/integration/all/zwave-js-ui/device-page/index.js
new file mode 100644
index 0000000000..2801ea568d
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/device-page/index.js
@@ -0,0 +1,25 @@
+import { Component } from 'preact';
+import { connect } from 'unistore/preact';
+import actions from './actions';
+import ZwaveJSUIPage from '../ZwaveJSUIPage';
+import DeviceTab from './DeviceTab';
+
+class ZwaveJSUIDevicePage extends Component {
+ componentWillMount() {
+ this.props.getZWaveDevices();
+ this.props.getHouses();
+ }
+
+ render(props, {}) {
+ return (
+
+
+
+ );
+ }
+}
+
+export default connect(
+ 'session,user,zwaveDevices,houses,zwaveGetDevicesStatus,orderDir,searchKeyword',
+ actions
+)(ZwaveJSUIDevicePage);
diff --git a/front/src/routes/integration/all/zwave-js-ui/device-page/style.css b/front/src/routes/integration/all/zwave-js-ui/device-page/style.css
new file mode 100644
index 0000000000..1b4343b7c4
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/device-page/style.css
@@ -0,0 +1,3 @@
+.emptyDiv {
+ min-height: 200px;
+}
diff --git a/front/src/routes/integration/all/zwave-js-ui/discover-page/EmptyState.jsx b/front/src/routes/integration/all/zwave-js-ui/discover-page/EmptyState.jsx
new file mode 100644
index 0000000000..0b043867b4
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/discover-page/EmptyState.jsx
@@ -0,0 +1,13 @@
+import { Text } from 'preact-i18n';
+import cx from 'classnames';
+import style from './style.css';
+
+const EmptyState = () => (
+
+);
+
+export default EmptyState;
diff --git a/front/src/routes/integration/all/zwave-js-ui/discover-page/Node.jsx b/front/src/routes/integration/all/zwave-js-ui/discover-page/Node.jsx
new file mode 100644
index 0000000000..cbbe319d66
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/discover-page/Node.jsx
@@ -0,0 +1,156 @@
+import get from 'get-value';
+import { Text } from 'preact-i18n';
+import { Component } from 'preact';
+import { Link } from 'preact-router/match';
+import cx from 'classnames';
+
+import { RequestStatus } from '../../../../../utils/consts';
+import DeviceFeatures from '../../../../../components/device/view/DeviceFeatures';
+import { PARAMS } from '../../../../../../../server/services/zwave-js-ui/lib/constants';
+
+const GITHUB_BASE_URL = 'https://github.com/GladysAssistant/Gladys/issues/new';
+
+const createGithubUrl = node => {
+ const { params } = node;
+ const deviceToSend = {
+ product: params.find(param => param.name === PARAMS.NODE_PRODUCT).value,
+ classes: params.find(param => param.name === PARAMS.NODE_CLASSES).value
+ };
+ const title = encodeURIComponent(`Z-Wave: Handle device "${params.product}"`);
+ const body = encodeURIComponent(`\`\`\`\n${JSON.stringify(deviceToSend, null, 2)}\n\`\`\``);
+ return `${GITHUB_BASE_URL}?title=${title}&body=${body}`;
+};
+
+const displayRawNode = node => () => {
+ // eslint-disable-next-line no-console
+ console.log(node);
+};
+
+class ZwaveNode extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ zwaveSaveNodeStatus: null
+ };
+ }
+
+ createDevice = async () => {
+ this.setState({
+ zwaveSaveNodeStatus: RequestStatus.Getting
+ });
+ try {
+ await this.props.createDevice(this.props.node);
+ this.setState({
+ zwaveSaveNodeStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ const status = get(e, 'response.status');
+ if (status === 409) {
+ this.setState({
+ zwaveSaveNodeStatus: RequestStatus.ConflictError
+ });
+ } else {
+ this.setState({
+ zwaveSaveNodeStatus: RequestStatus.Error
+ });
+ }
+ }
+ };
+
+ editNodeName = e => {
+ this.props.editNodeName(this.props.nodeIndex, e.target.value);
+ };
+
+ render({ ...props }, { zwaveSaveNodeStatus }) {
+ const loading = zwaveSaveNodeStatus === RequestStatus.Getting;
+ return (
+
+
+
+
+
+
+ {zwaveSaveNodeStatus === RequestStatus.Error && (
+
+
+
+ )}
+ {zwaveSaveNodeStatus === RequestStatus.ConflictError && (
+
+
+
+ )}
+ {zwaveSaveNodeStatus === RequestStatus.Success && (
+
+
+
+ )}
+ {props.node.ready ? (
+
+
+
+
+
+
+
+ {props.node.features.length > 0 && (
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+ }
+}
+
+export default ZwaveNode;
diff --git a/front/src/routes/integration/all/zwave-js-ui/discover-page/NodeTab.jsx b/front/src/routes/integration/all/zwave-js-ui/discover-page/NodeTab.jsx
new file mode 100644
index 0000000000..85448542c4
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/discover-page/NodeTab.jsx
@@ -0,0 +1,127 @@
+import { Text, Localizer } from 'preact-i18n';
+import { Component } from 'preact';
+import cx from 'classnames';
+import get from 'get-value';
+
+import EmptyState from './EmptyState';
+import Node from './Node';
+import style from './style.css';
+import { RequestStatus } from '../../../../../utils/consts';
+import CardFilter from '../../../../../components/layout/CardFilter';
+
+class NodeTab extends Component {
+ render({ zwaveGetNodesStatus, zwaveStatus, filterExisting = true, ...props }) {
+ const zwaveNotConfigured = zwaveGetNodesStatus === RequestStatus.ServiceNotConfigured;
+ const scanInProgress = get(zwaveStatus, 'scanInProgress');
+ const gettingNodesInProgress = zwaveGetNodesStatus === RequestStatus.Getting;
+ const zwaveActionsDisabled = scanInProgress || gettingNodesInProgress;
+ const zwaveActionsEnabled = !zwaveActionsDisabled;
+ return (
+
+
+ {zwaveActionsDisabled && (
+
+ )}
+
+
+
+
+ {zwaveNotConfigured && (
+
+
+
+ )}
+
+
+ {(!props.zwaveNodes || props.zwaveNodes.length === 0) && }
+ {props.zwaveNodes &&
+ props.zwaveNodes.length > 0 &&
+ props.zwaveNodes.map((zwaveNode, index) => (
+
+ ))}
+
+
+
+
+
+ );
+ }
+}
+
+export default NodeTab;
diff --git a/front/src/routes/integration/all/zwave-js-ui/discover-page/actions.js b/front/src/routes/integration/all/zwave-js-ui/discover-page/actions.js
new file mode 100644
index 0000000000..2851ff2ae8
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/discover-page/actions.js
@@ -0,0 +1,161 @@
+import get from 'get-value';
+import update from 'immutability-helper';
+
+import { RequestStatus } from '../../../../../utils/consts';
+import { ERROR_MESSAGES } from '../../../../../../../server/utils/constants';
+import { DEFAULT } from '../../../../../../../server/services/zwave-js-ui/lib/constants';
+import { slugify } from '../../../../../../../server/utils/slugify';
+import createActionsIntegration from '../../../../../actions/integration';
+import debounce from 'debounce';
+
+const createActions = store => {
+ const integrationActions = createActionsIntegration(store);
+ const actions = {
+ async getNodes(state) {
+ store.setState({
+ zwaveGetNodesStatus: RequestStatus.Getting
+ });
+ try {
+ const {
+ orderDir = DEFAULT.NODES_ORDER_DIR,
+ filterExisting = DEFAULT.NODES_FILTER_EXISTING,
+ searchKeyword = null
+ } = state;
+ const zwaveNodes = await state.httpClient.get('/api/v1/service/zwave-js-ui/node', {
+ orderDir,
+ filterExisting,
+ search: searchKeyword
+ });
+
+ store.setState({
+ zwaveNodes,
+ zwaveGetNodesStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ const responseMessage = get(e, 'response.data.message');
+ if (responseMessage === ERROR_MESSAGES.SERVICE_NOT_CONFIGURED) {
+ store.setState({
+ zwaveGetNodesStatus: RequestStatus.ServiceNotConfigured
+ });
+ } else {
+ store.setState({
+ zwaveGetNodesStatus: RequestStatus.Error
+ });
+ }
+ }
+ },
+ async scanNetwork(state) {
+ store.setState({
+ zwaveScanNetworkStatus: RequestStatus.Getting
+ });
+ try {
+ await state.httpClient.post('/api/v1/service/zwave-js-ui/scan');
+ store.setState({
+ zwaveScanNetworkStatus: RequestStatus.Success
+ });
+ actions.getStatus(store.getState());
+ } catch (e) {
+ store.setState({
+ zwaveScanNetworkStatus: RequestStatus.Error
+ });
+ }
+ },
+ async addNode(state, e, secure = false) {
+ if (e) {
+ e.preventDefault();
+ }
+ store.setState({
+ zwaveAddNodeStatus: RequestStatus.Getting
+ });
+ try {
+ await state.httpClient.post('/api/v1/service/zwave-js-ui/node/add', {
+ secure
+ });
+ store.setState({
+ zwaveAddNodeStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ zwaveAddNodeStatus: RequestStatus.Error
+ });
+ }
+ },
+ async addNodeSecure(state, e) {
+ actions.addNode(state, e, true);
+ },
+ async stopAddNode(state) {
+ store.setState({
+ zwaveStopAddNodeStatus: RequestStatus.Getting
+ });
+ try {
+ await state.httpClient.post('/api/v1/service/zwave-js-ui/cancel');
+ store.setState({
+ zwaveStopAddNodeStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ zwaveStopAddNodeStatus: RequestStatus.Error
+ });
+ }
+ },
+ async getStatus(state) {
+ store.setState({
+ zwaveGetStatusStatus: RequestStatus.Getting
+ });
+ try {
+ const zwaveStatus = await state.httpClient.get('/api/v1/service/zwave-js-ui/status');
+ store.setState({
+ zwaveStatus,
+ zwaveGetStatusStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ zwaveGetStatusStatus: RequestStatus.Error
+ });
+ }
+ },
+ async createDevice(state, newDevice) {
+ await state.httpClient.post('/api/v1/device', newDevice);
+ await actions.getNodes(store.getState());
+ },
+ editNodeName(state, index, name) {
+ const newState = update(state, {
+ zwaveNodes: {
+ [index]: {
+ name: {
+ $set: name
+ },
+ selector: {
+ $set: slugify(name)
+ }
+ }
+ }
+ });
+ store.setState(newState);
+ },
+ async toggleFilterOnExisting(state = {}) {
+ const { filterExisting = true } = state;
+ store.setState({
+ filterExisting: !filterExisting
+ });
+
+ await actions.getNodes(store.getState());
+ },
+ async search(state, e) {
+ store.setState({
+ searchKeyword: e.target.value
+ });
+ await actions.getNodes(store.getState());
+ },
+ async changeOrderDir(state, e) {
+ store.setState({
+ orderDir: e.target.value
+ });
+ await actions.getNodes(store.getState());
+ }
+ };
+ actions.debouncedSearch = debounce(actions.search, 200);
+ return Object.assign({}, integrationActions, actions);
+};
+
+export default createActions;
diff --git a/front/src/routes/integration/all/zwave-js-ui/discover-page/index.js b/front/src/routes/integration/all/zwave-js-ui/discover-page/index.js
new file mode 100644
index 0000000000..e2510f1836
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/discover-page/index.js
@@ -0,0 +1,51 @@
+import { Component } from 'preact';
+import { connect } from 'unistore/preact';
+import actions from './actions';
+import ZwaveJSUIPage from '../ZwaveJSUIPage';
+import NodeTab from './NodeTab';
+import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants';
+
+class ZwaveJSUINodePage extends Component {
+ scanCompleteListener = () => {
+ this.props.getStatus();
+ this.props.getNodes();
+ };
+
+ componentWillMount() {
+ this.props.session.dispatcher.addListener(
+ WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ this.scanCompleteListener
+ );
+ this.props.session.dispatcher.addListener(
+ WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.SCAN_COMPLETE,
+ this.scanCompleteListener
+ );
+ this.props.getIntegrationByName('zwave-js-ui');
+ this.props.getNodes();
+ this.props.getStatus();
+ }
+
+ componentWillUnmount() {
+ this.props.session.dispatcher.removeListener(
+ WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ this.scanCompleteListener
+ );
+ this.props.session.dispatcher.removeListener(
+ WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.SCAN_COMPLETE,
+ this.scanCompleteListener
+ );
+ }
+
+ render(props, {}) {
+ return (
+
+
+
+ );
+ }
+}
+
+export default connect(
+ 'user,session,zwaveNodes,zwaveStatus,orderDir,searchKeyword,filterExisting,zwaveGetNodesStatus,zwaveSaveNodeStatus',
+ actions
+)(ZwaveJSUINodePage);
diff --git a/front/src/routes/integration/all/zwave-js-ui/discover-page/style.css b/front/src/routes/integration/all/zwave-js-ui/discover-page/style.css
new file mode 100644
index 0000000000..8512486412
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/discover-page/style.css
@@ -0,0 +1,7 @@
+.emptyDiv {
+ min-height: 400px;
+}
+
+.page-options {
+ margin-bottom: 10px;
+}
diff --git a/front/src/routes/integration/all/zwave-js-ui/edit-page/index.js b/front/src/routes/integration/all/zwave-js-ui/edit-page/index.js
new file mode 100644
index 0000000000..76370fa05c
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/edit-page/index.js
@@ -0,0 +1,24 @@
+import { Component } from 'preact';
+import { connect } from 'unistore/preact';
+// import actions from '../actions';
+import ZwaveJSUIPage from '../ZwaveJSUIPage';
+import UpdateDevice from '../../../../../components/device';
+
+const ZWAVE_PAGE_PATH = '/dashboard/integration/device/zwave-js-ui';
+
+class EditZwaveJSUIDevice extends Component {
+ render(props, {}) {
+ return (
+
+
+
+ );
+ }
+}
+
+export default connect('user,session,httpClient,currentIntegration,houses', {})(EditZwaveJSUIDevice);
diff --git a/front/src/routes/integration/all/zwave-js-ui/node-operation-page/AddRemoveNode.jsx b/front/src/routes/integration/all/zwave-js-ui/node-operation-page/AddRemoveNode.jsx
new file mode 100644
index 0000000000..82b423490a
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/node-operation-page/AddRemoveNode.jsx
@@ -0,0 +1,48 @@
+import { Text } from 'preact-i18n';
+
+const AddNode = ({ children, ...props }) => (
+
+
+
+ {!props.nodeAdded && (
+
+
+ {props.remainingTimeInSeconds}
+
+
+ {props.action === 'remove' ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+ {props.nodeAdded && (
+
+ )}
+
+
+);
+
+export default AddNode;
diff --git a/front/src/routes/integration/all/zwave-js-ui/node-operation-page/actions.js b/front/src/routes/integration/all/zwave-js-ui/node-operation-page/actions.js
new file mode 100644
index 0000000000..fda67c4780
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/node-operation-page/actions.js
@@ -0,0 +1,78 @@
+import { RequestStatus } from '../../../../../utils/consts';
+
+const actions = store => {
+ const actions = {
+ async addNode(state, e, secure = false) {
+ if (e) {
+ e.preventDefault();
+ }
+ store.setState({
+ zwaveAddNodeStatus: RequestStatus.Getting
+ });
+ try {
+ await state.httpClient.post('/api/v1/service/zwave-js-ui/node/add', {
+ secure
+ });
+ store.setState({
+ zwaveAddNodeStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ zwaveAddNodeStatus: RequestStatus.Error
+ });
+ }
+ },
+ async addNodeSecure(state, e) {
+ actions.addNode(state, e, true);
+ },
+ async cancelZwaveCommand(state) {
+ store.setState({
+ zwaveCancelZwaveCommandStatus: RequestStatus.Getting
+ });
+ try {
+ await state.httpClient.post('/api/v1/service/zwave-js-ui/cancel');
+ store.setState({
+ zwaveCancelZwaveCommandStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ zwaveCancelZwaveCommandStatus: RequestStatus.Error
+ });
+ }
+ },
+ async removeNode(state) {
+ store.setState({
+ zwaveRemoveNodeStatus: RequestStatus.Getting
+ });
+ try {
+ await state.httpClient.post('/api/v1/service/zwave-js-ui/node/remove');
+ store.setState({
+ zwaveRemoveNodeStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ zwaveRemoveNodeStatus: RequestStatus.Error
+ });
+ }
+ },
+ async scanNetwork(state) {
+ store.setState({
+ zwaveScanNetworkStatus: RequestStatus.Getting
+ });
+ try {
+ await state.httpClient.post('/api/v1/service/zwave-js-ui/scan');
+ store.setState({
+ zwaveScanNetworkStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ zwaveScanNetworkStatus: RequestStatus.Error
+ });
+ }
+ }
+ };
+
+ return actions;
+};
+
+export default actions;
diff --git a/front/src/routes/integration/all/zwave-js-ui/node-operation-page/index.js b/front/src/routes/integration/all/zwave-js-ui/node-operation-page/index.js
new file mode 100644
index 0000000000..a3cdfceb27
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/node-operation-page/index.js
@@ -0,0 +1,113 @@
+import { Component } from 'preact';
+import { connect } from 'unistore/preact';
+import { route } from 'preact-router';
+import actions from './actions';
+import ZwaveJSUIPage from '../ZwaveJSUIPage';
+import NodeOperationPage from './AddRemoveNode';
+import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants';
+
+class ZwaveJSUIDeviceOperationPage extends Component {
+ scanCompletedListener = () => {
+ this.setState({
+ scanComplete: true
+ });
+ };
+ nodeAddedListener = () => {
+ this.setState({
+ nodeAdded: true
+ });
+ };
+ nodeReadyListener = () => {
+ if (this.props.action === 'add' || this.props.action === 'add-secure') {
+ route('/dashboard/integration/device/zwave-js-ui/discover');
+ }
+ };
+ nodeRemovedListener = () => {
+ if (this.props.action === 'remove') {
+ route('/dashboard/integration/device/zwave-js-ui/discover');
+ }
+ };
+ decrementTimer = () => {
+ this.setState(prevState => {
+ return { remainingTimeInSeconds: prevState.remainingTimeInSeconds - 1 };
+ });
+ if (this.state.remainingTimeInSeconds > 1) {
+ setTimeout(this.decrementTimer, 1000);
+ } else {
+ route('/dashboard/integration/device/zwave-js-ui/discover');
+ }
+ };
+ scanNetwork = () => {
+ this.props.scanNetwork();
+ };
+ addNode = () => {
+ this.props.addNode();
+ setTimeout(this.decrementTimer, 1000);
+ };
+ addNodeSecure = () => {
+ this.props.addNodeSecure();
+ setTimeout(this.decrementTimer, 1000);
+ };
+ removeNode = () => {
+ this.props.removeNode();
+ setTimeout(this.decrementTimer, 1000);
+ };
+ cancel = () => {
+ this.props.cancelZwaveCommand();
+ route('/dashboard/integration/device/zwave-js-ui/discover');
+ };
+ constructor(props) {
+ super(props);
+ this.state = {
+ remainingTimeInSeconds: 60
+ };
+ }
+ componentWillMount() {
+ switch (this.props.action) {
+ case 'scan':
+ this.scanNetwork();
+ break;
+ case 'add':
+ this.addNode();
+ break;
+ case 'add-secure':
+ this.addNodeSecure();
+ break;
+ case 'remove':
+ this.removeNode();
+ break;
+ }
+ this.props.session.dispatcher.addListener(
+ WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.SCAN_COMPLETE,
+ this.scanCompletedListener
+ );
+ this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.NODE_ADDED, this.nodeAddedListener);
+ this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.NODE_READY, this.nodeReadyListener);
+ this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.NODE_REMOVED, this.nodeRemovedListener);
+ }
+
+ componentWillUnmount() {
+ this.props.session.dispatcher.removeListener(
+ WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.SCAN_COMPLETE,
+ this.scanCompletedListener
+ );
+ this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.NODE_ADDED, this.nodeAddedListener);
+ this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.NODE_READY, this.nodeReadyListener);
+ this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.NODE_REMOVED, this.nodeRemovedListener);
+ }
+
+ render(props, { remainingTimeInSeconds, nodeAdded }) {
+ return (
+
+
+
+ );
+ }
+}
+
+export default connect('session,user,zwaveDevices,houses,zwaveGetDevicesStatus', actions)(ZwaveJSUIDeviceOperationPage);
diff --git a/front/src/routes/integration/all/zwave-js-ui/settings-page/CheckStatus.js b/front/src/routes/integration/all/zwave-js-ui/settings-page/CheckStatus.js
new file mode 100644
index 0000000000..a114cd7ef5
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/settings-page/CheckStatus.js
@@ -0,0 +1,46 @@
+import { Component } from 'preact';
+import { Link } from 'preact-router/match';
+import { connect } from 'unistore/preact';
+import actions from './actions';
+import { Text } from 'preact-i18n';
+
+class CheckStatus extends Component {
+ componentWillMount() {
+ this.props.getStatus();
+ }
+
+ render(props, {}) {
+ let messageKey;
+ let linkUrl = '';
+ let linkText = '';
+ if (!props.usbConfigured) {
+ messageKey = 'integration.zwavejsui.status.notConfigured';
+ linkUrl = '/dashboard/integration/device/zwave-js-ui/settings';
+ linkText = 'integration.zwavejsui.status.settingsPageLink';
+ } else if (!props.mqttExist) {
+ messageKey = 'integration.zwavejsui.status.mqttNotInstalled';
+ } else if (!props.mqttRunning) {
+ messageKey = 'integration.zwavejsui.status.mqttNotRunning';
+ } else if (!props.mqttConnected) {
+ messageKey = 'integration.zwavejsui.status.gladysNotConnected';
+ } else if (!props.zwaveJSUIExist) {
+ messageKey = 'integration.zwavejsui.status.zwave-js-uiNotInstalled';
+ } else if (!props.zwaveJSUIRunning) {
+ messageKey = 'integration.zwavejsui.status.zwave-js-uiNotRunning';
+ }
+
+ return (
+
+
+
+
+
+
+ );
+ }
+}
+
+export default connect(
+ 'user,session,usbConfigured,mqttExist,mqttRunning,mqttConnected,zwaveJSUIExist,zwaveJSUIRunning',
+ actions
+)(CheckStatus);
diff --git a/front/src/routes/integration/all/zwave-js-ui/settings-page/SettingsTab.jsx b/front/src/routes/integration/all/zwave-js-ui/settings-page/SettingsTab.jsx
new file mode 100644
index 0000000000..88324a2390
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/settings-page/SettingsTab.jsx
@@ -0,0 +1,569 @@
+import { Component } from 'preact';
+import { Text, Localizer } from 'preact-i18n';
+import { Link } from 'preact-router/match';
+import classNames from 'classnames/bind';
+import style from './style.css';
+
+let cx = classNames.bind(style);
+
+class SettingsTab extends Component {
+ toggleExternalZwavejsUI = () => {
+ this.props.externalZwaveJSUI = !this.props.externalZwaveJSUI;
+ this.props.updateConfiguration({ externalZwaveJSUI: this.props.externalZwaveJSUI });
+ };
+
+ toggleMqttTopicWithLocation = () => {
+ this.props.mqttTopicWithLocation = !this.props.mqttTopicWithLocation;
+ this.props.updateConfiguration({ mqttTopicWithLocation: this.props.mqttTopicWithLocation });
+ };
+
+ updateS2UnauthenticatedKey = e => {
+ this.props.updateConfiguration({ s2UnauthenticatedKey: e.target.value });
+ };
+
+ updateS2AuthenticatedKey = e => {
+ this.props.updateConfiguration({ s2AuthenticatedKey: e.target.value });
+ };
+
+ updateS2AccessControlKey = e => {
+ this.props.updateConfiguration({ s2AccessControlKey: e.target.value });
+ };
+
+ updateS0LegacyKey = e => {
+ this.props.updateConfiguration({ s0LegacyKey: e.target.value });
+ };
+
+ updateUrl = e => {
+ this.props.updateConfiguration({ mqttUrl: e.target.value });
+ };
+
+ updateUsername = e => {
+ this.props.updateConfiguration({ mqttUsername: e.target.value });
+ };
+
+ updatePassword = e => {
+ this.props.updateConfiguration({ mqttPassword: e.target.value, passwordChanges: true });
+ };
+
+ showPassword = () => {
+ this.setState({ showPassword: true });
+ setTimeout(() => this.setState({ showPassword: false }), 5000);
+ };
+
+ updateUsbDriverPath = e => {
+ this.props.updateConfiguration({ driverPath: e.target.value });
+ };
+
+ updateMqttTopicPrefix = e => {
+ this.props.updateConfiguration({ mqttTopicPrefix: e.target.value });
+ };
+
+ render(props, { showPassword }) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {!props.externalZwaveJSUI && (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ {!props.usbConfigured && (
+
+
+
+ )}
+
+ {props.usbConfigured && !props.zwaveJSUIConnected && (
+
+
+
+ )}
+
+ {!props.mqttConnected ||
+ (!props.zwaveJSUIConnected && (
+
+
+
+ ))}
+
+ {props.mqttConnected && props.zwaveJSUIConnected && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {props.externalZwaveJSUI && (
+ <>
+
+
+
+
+
+ }
+ value={props.mqttUrl}
+ class="form-control"
+ onInput={this.updateUrl}
+ />
+
+
+
+
+
+
+
+ }
+ value={props.mqttUsername}
+ class="form-control"
+ onInput={this.updateUsername}
+ autoComplete="no"
+ />
+
+
+
+
+
+
+
+
+ }
+ value={props.mqttTopicPrefix}
+ class="form-control"
+ onInput={this.updateMqttTopicPrefix}
+ autoComplete="no"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {!props.externalZwaveJSUI && (
+ <>
+
+
+
+
+
+
+
+
+ {props.usbPorts &&
+ props.usbPorts.map(
+ usbPort =>
+ usbPort.comPath && (
+
+ {usbPort.comPath}
+ {usbPort.comName ? ` - ${usbPort.comName}` : ''}
+ {usbPort.comVID ? ` - ${usbPort.comVID}` : ''}
+
+ )
+ )}
+
+
+
+
+
+
+
+
+
+
+ }
+ value={props.s2UnauthenticatedKey}
+ class="form-control"
+ onInput={this.updateS2UnauthenticatedKey}
+ autoComplete="no"
+ />
+
+
+
+
+
+
+
+ }
+ value={props.s2AuthenticatedKey}
+ class="form-control"
+ onInput={this.updateS2AuthenticatedKey}
+ autoComplete="no"
+ />
+
+
+
+
+
+
+
+ }
+ value={props.s2AccessControlKey}
+ class="form-control"
+ onInput={this.updateS2AccessControlKey}
+ autoComplete="no"
+ />
+
+
+
+
+
+
+
+ }
+ value={props.s0LegacyKey}
+ class="form-control"
+ onInput={this.updateS0LegacyKey}
+ autoComplete="no"
+ />
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {props.mqttRunning && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {props.zwaveJSUIRunning && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {props.mqttRunning && (
+
+ )}
+
+
+
+
+
+
+
+ {props.zwaveJSUIRunning && (
+
+ )}
+
+
+
+
+
+
+ >
+ );
+ }
+}
+
+export default SettingsTab;
diff --git a/front/src/routes/integration/all/zwave-js-ui/settings-page/actions.js b/front/src/routes/integration/all/zwave-js-ui/settings-page/actions.js
new file mode 100644
index 0000000000..7a2d9ce0a3
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/settings-page/actions.js
@@ -0,0 +1,141 @@
+import createActionsIntegration from '../../../../../actions/integration';
+import { RequestStatus } from '../../../../../utils/consts';
+
+const createActions = store => {
+ const integrationActions = createActionsIntegration(store);
+ const actions = {
+ async getUsbPorts(state) {
+ store.setState({
+ zwaveGetUsbPortStatus: RequestStatus.Getting
+ });
+ try {
+ const usbPorts = await state.httpClient.get('/api/v1/service/usb/port');
+ store.setState({
+ usbPorts,
+ zwaveGetUsbPortStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ zwaveGetUsbPortStatus: RequestStatus.Error
+ });
+ }
+ },
+ async getConfiguration(state) {
+ store.setState({
+ zwaveGetConfigurationStatus: RequestStatus.Getting
+ });
+ try {
+ const configuration = await state.httpClient.get('/api/v1/service/zwave-js-ui/configuration');
+ store.setState({
+ zwaveGetConfigurationStatus: RequestStatus.Success,
+ ...configuration
+ });
+ } catch (e) {
+ store.setState({
+ zwaveGetConfigurationStatus: RequestStatus.Error
+ });
+ }
+ },
+ async getStatus(state) {
+ store.setState({
+ zwaveGetStatusStatus: RequestStatus.Getting
+ });
+ try {
+ const zwaveStatus = await state.httpClient.get('/api/v1/service/zwave-js-ui/status');
+ store.setState({
+ zwaveGetStatusStatus: RequestStatus.Success,
+ ...zwaveStatus
+ });
+ } catch (e) {
+ store.setState({
+ zwaveGetStatusStatus: RequestStatus.Error,
+ zwaveConnectionInProgress: false
+ });
+ }
+ },
+ updateConfiguration(state, configuration) {
+ store.setState(configuration);
+ },
+ async connect(state) {
+ await actions.disconnect(state);
+ await actions.saveConfiguration(state);
+ store.setState({
+ zwaveConnectStatus: RequestStatus.Getting
+ });
+ try {
+ await state.httpClient.post('/api/v1/service/zwave-js-ui/connect');
+ await actions.getStatus(store.getState());
+ store.setState({
+ zwaveConnectStatus: RequestStatus.Success,
+ zwaveConnectionInProgress: true
+ });
+ } catch (e) {
+ store.setState({
+ zwaveConnectStatus: RequestStatus.Error
+ });
+ }
+ },
+ async disconnect(state) {
+ store.setState({
+ zwaveDisconnectStatus: RequestStatus.Getting
+ });
+ try {
+ await state.httpClient.post('/api/v1/service/zwave-js-ui/disconnect');
+ await actions.getStatus(store.getState());
+ store.setState({
+ zwaveDisconnectStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ zwaveDisconnectStatus: RequestStatus.Error
+ });
+ }
+ },
+ async saveConfiguration(state) {
+ event.preventDefault();
+ store.setState({
+ saveConfigurationStatus: RequestStatus.Getting
+ });
+
+ const {
+ externalZwaveJSUI,
+ driverPath,
+ mqttUrl,
+ mqttUsername,
+ mqttPassword,
+ mqttTopicPrefix,
+ mqttTopicWithLocation,
+ s2UnauthenticatedKey,
+ s2AuthenticatedKey,
+ s2AccessControlKey,
+ s0LegacyKey
+ } = state;
+ try {
+ await state.httpClient.post(`/api/v1/service/zwave-js-ui/configuration`, {
+ externalZwaveJSUI,
+ driverPath,
+ mqttUrl,
+ mqttUsername,
+ mqttPassword,
+ mqttTopicPrefix,
+ mqttTopicWithLocation,
+ s2UnauthenticatedKey,
+ s2AuthenticatedKey,
+ s2AccessControlKey,
+ s0LegacyKey
+ });
+
+ store.setState({
+ saveConfigurationStatus: RequestStatus.Success
+ });
+ } catch (e) {
+ store.setState({
+ saveConfigurationStatus: RequestStatus.Error
+ });
+ }
+ }
+ };
+ return Object.assign({}, actions, integrationActions);
+};
+
+export default createActions;
diff --git a/front/src/routes/integration/all/zwave-js-ui/settings-page/index.js b/front/src/routes/integration/all/zwave-js-ui/settings-page/index.js
new file mode 100644
index 0000000000..8a0943ff52
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/settings-page/index.js
@@ -0,0 +1,40 @@
+import { Component } from 'preact';
+import { connect } from 'unistore/preact';
+import actions from './actions';
+import ZwaveJSUIPage from '../ZwaveJSUIPage';
+import SettingsTab from './SettingsTab';
+import { RequestStatus } from '../../../../../utils/consts';
+import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants';
+
+class ZwaveJSUISettingsPage extends Component {
+ componentWillMount() {
+ this.props.getStatus();
+ this.props.getUsbPorts();
+ this.props.getConfiguration();
+ this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE, this.props.getStatus);
+ }
+
+ componentWillUnmount() {
+ this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE, this.props.getStatus);
+ }
+
+ render(props, {}) {
+ const loading =
+ props.zwaveGetStatusStatus === RequestStatus.Getting ||
+ props.zwaveGetConfigurationStatus === RequestStatus.Getting ||
+ props.zwaveGetUsbPortStatus === RequestStatus.Getting ||
+ props.zwaveDisconnectStatus === RequestStatus.Getting ||
+ props.zwaveConnectStatus === RequestStatus.Getting;
+
+ return (
+
+
+
+ );
+ }
+}
+
+export default connect(
+ 'user,session,ready,externalZwaveJSUI,driverPath,mqttUrl,mqttUsername,mqttPassword,mqttTopicPrefix,mqttTopicWithLocation,s2UnauthenticatedKey,s2AuthenticatedKey,s2AccessControlKey,s0LegacyKey,usbPorts,usbConfigured,mqttExist,mqttRunning,mqttConnected,zwaveJSUIExist,zwaveJSUIRunning,zwaveJSUIConnected,zwaveJSUIVersion,zwaveJSUIExpectedVersion,dockerBased,zwaveGetConfigurationStatus,zwaveGetUsbPortStatus,zwaveGetStatusStatus,zwaveDisconnectStatus,zwaveConnectStatus',
+ actions
+)(ZwaveJSUISettingsPage);
diff --git a/front/src/routes/integration/all/zwave-js-ui/settings-page/style.css b/front/src/routes/integration/all/zwave-js-ui/settings-page/style.css
new file mode 100644
index 0000000000..117e83421d
--- /dev/null
+++ b/front/src/routes/integration/all/zwave-js-ui/settings-page/style.css
@@ -0,0 +1,23 @@
+.tdCenter {
+ vertical-align: middle;
+ display: flex;
+ align-items: center;
+}
+
+.greenIcon {
+ color: #5eba00;;
+ font-size: 24px;
+}
+
+.redIcon {
+ color: #cd201f;
+ font-size: 24px;
+}
+
+.line {
+ color: #555;
+ background-color: #555;
+ border-color: #555;
+ height: 1px;
+ width: 40px;
+}
\ No newline at end of file
diff --git a/front/src/utils/consts.js b/front/src/utils/consts.js
index d344680196..a6018095cf 100644
--- a/front/src/utils/consts.js
+++ b/front/src/utils/consts.js
@@ -210,10 +210,6 @@ export const DeviceFeatureCategoriesIcon = {
[DEVICE_FEATURE_TYPES.SENSOR.BINARY]: 'wind',
[DEVICE_FEATURE_TYPES.SENSOR.DECIMAL]: 'wind'
},
- [DEVICE_FEATURE_CATEGORIES.LIGHT_SENSOR]: {
- [DEVICE_FEATURE_TYPES.SENSOR.BINARY]: 'sun',
- [DEVICE_FEATURE_TYPES.SENSOR.DECIMAL]: 'sun'
- },
[DEVICE_FEATURE_CATEGORIES.PRESSURE_SENSOR]: {
[DEVICE_FEATURE_TYPES.SENSOR.DECIMAL]: 'cloud'
},
diff --git a/package-lock.json b/package-lock.json
index 7389a5ea20..693b187d29 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"devDependencies": {
"all-contributors-cli": "^6.15.0",
"apidoc": "^1.0.3",
+ "eslint-plugin-promise": "^6.1.1",
"npm-run-all": "^4.1.5",
"shx": "^0.3.2"
},
@@ -20,6 +21,16 @@
"npm": "9.x"
}
},
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/@babel/runtime": {
"version": "7.9.6",
"dev": true,
@@ -409,6 +420,152 @@
"node": ">=12"
}
},
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz",
+ "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
+ "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/eslintrc/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==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz",
+ "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.11",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
+ "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/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==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@@ -467,6 +624,44 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/@types/color-name": {
"version": "1.1.1",
"dev": true,
@@ -717,9 +912,9 @@
"dev": true
},
"node_modules/acorn": {
- "version": "8.8.2",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
- "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -737,6 +932,16 @@
"acorn": "^8"
}
},
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peer": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -876,9 +1081,10 @@
}
},
"node_modules/ansi-regex": {
- "version": "5.0.0",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -1135,6 +1341,16 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/camelcase": {
"version": "5.3.1",
"dev": true,
@@ -1384,6 +1600,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/define-properties": {
"version": "1.1.3",
"dev": true,
@@ -1414,6 +1637,19 @@
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
"dev": true
},
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"dev": true,
@@ -1568,54 +1804,458 @@
"@esbuild/win32-x64": "0.16.17"
}
},
- "node_modules/esbuild-loader": {
- "version": "2.21.0",
- "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-2.21.0.tgz",
- "integrity": "sha512-k7ijTkCT43YBSZ6+fBCW1Gin7s46RrJ0VQaM8qA7lq7W+OLsGgtLyFV8470FzYi/4TeDexniTBTPTwZUnXXR5g==",
+ "node_modules/esbuild-loader": {
+ "version": "2.21.0",
+ "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-2.21.0.tgz",
+ "integrity": "sha512-k7ijTkCT43YBSZ6+fBCW1Gin7s46RrJ0VQaM8qA7lq7W+OLsGgtLyFV8470FzYi/4TeDexniTBTPTwZUnXXR5g==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.16.17",
+ "joycon": "^3.0.1",
+ "json5": "^2.2.0",
+ "loader-utils": "^2.0.0",
+ "tapable": "^2.2.0",
+ "webpack-sources": "^1.4.3"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/esbuild-loader?sponsor=1"
+ },
+ "peerDependencies": {
+ "webpack": "^4.40.0 || ^5.0.0"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz",
+ "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.2",
+ "@eslint/js": "8.49.0",
+ "@humanwhocodes/config-array": "^0.11.11",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-plugin-promise": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
+ "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/eslint/node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/eslint/node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/eslint/node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/eslint/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eslint/node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/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==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/eslint/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eslint/node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eslint/node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eslint/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
+ "peer": true,
"dependencies": {
- "esbuild": "^0.16.17",
- "joycon": "^3.0.1",
- "json5": "^2.2.0",
- "loader-utils": "^2.0.0",
- "tapable": "^2.2.0",
- "webpack-sources": "^1.4.3"
- },
- "funding": {
- "url": "https://github.com/privatenumber/esbuild-loader?sponsor=1"
+ "has-flag": "^4.0.0"
},
- "peerDependencies": {
- "webpack": "^4.40.0 || ^5.0.0"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/escalade": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
- "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "node_modules/eslint/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
+ "peer": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
"engines": {
- "node": ">=6"
+ "node": ">= 8"
}
},
- "node_modules/escape-string-regexp": {
- "version": "1.0.5",
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
- "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
"engines": {
- "node": ">=0.8.0"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
"dev": true,
+ "peer": true,
"dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^4.1.1"
+ "estraverse": "^5.1.0"
},
"engines": {
- "node": ">=8.0.0"
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esquery/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=4.0"
}
},
"node_modules/esrecurse": {
@@ -1648,6 +2288,16 @@
"node": ">=4.0"
}
},
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -1700,15 +2350,23 @@
"license": "MIT"
},
"node_modules/fast-deep-equal": {
- "version": "3.1.1",
- "dev": true,
- "license": "MIT"
+ "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==",
+ "dev": true
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/fastest-levenshtein": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
@@ -1718,6 +2376,16 @@
"node": ">= 4.9.1"
}
},
+ "node_modules/fastq": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
@@ -1738,6 +2406,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -1762,6 +2443,28 @@
"node": ">=8"
}
},
+ "node_modules/flat-cache": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz",
+ "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "flatted": "^3.2.7",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
@@ -1881,12 +2584,48 @@
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true
},
+ "node_modules/globals": {
+ "version": "13.21.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz",
+ "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globals/node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"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==",
"dev": true
},
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/handlebars": {
"version": "4.7.7",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
@@ -1985,12 +2724,49 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ignore": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
"dev": true
},
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-fresh/node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/import-local": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
@@ -2010,6 +2786,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
"node_modules/inflight": {
"version": "1.0.6",
"dev": true,
@@ -2188,6 +2974,16 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -2311,11 +3107,31 @@
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
"dev": true
},
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/jsbn": {
"version": "0.1.1",
"dev": true,
"license": "MIT"
},
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/json-fixer": {
"version": "1.4.1",
"dev": true,
@@ -2411,6 +3227,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"dev": true,
@@ -2459,6 +3282,16 @@
"verror": "1.10.0"
}
},
+ "node_modules/keyv": {
+ "version": "4.5.3",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz",
+ "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2483,6 +3316,20 @@
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
"dev": true
},
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/linkify-it": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
@@ -2545,6 +3392,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/logform": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz",
@@ -2661,6 +3515,13 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
@@ -2809,6 +3670,24 @@
"node": ">=6"
}
},
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/os-tmpdir": {
"version": "1.0.2",
"dev": true,
@@ -2850,6 +3729,19 @@
"node": ">=6"
}
},
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/parse-json": {
"version": "4.0.0",
"dev": true,
@@ -2967,6 +3859,16 @@
"node": ">=8"
}
},
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/prismjs": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
@@ -3003,6 +3905,27 @@
"node": ">=0.6"
}
},
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "peer": true
+ },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -3150,6 +4073,33 @@
"node": ">=8"
}
},
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/run-async": {
"version": "2.4.1",
"dev": true,
@@ -3158,6 +4108,30 @@
"node": ">=0.12.0"
}
},
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "peer": true,
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
"node_modules/rxjs": {
"version": "6.5.5",
"dev": true,
@@ -3480,11 +4454,12 @@
}
},
"node_modules/strip-ansi": {
- "version": "6.0.0",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "ansi-regex": "^5.0.0"
+ "ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
@@ -3498,6 +4473,19 @@
"node": ">=4"
}
},
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/style-loader": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz",
@@ -3598,6 +4586,13 @@
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
"dev": true
},
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/through": {
"version": "2.3.8",
"dev": true,
@@ -3677,6 +4672,19 @@
"dev": true,
"license": "Unlicense"
},
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/type-fest": {
"version": "0.11.0",
"dev": true,
@@ -4180,9 +5188,29 @@
"engines": {
"node": ">=6"
}
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
}
},
"dependencies": {
+ "@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "peer": true
+ },
"@babel/runtime": {
"version": "7.9.6",
"dev": true,
@@ -4367,6 +5395,112 @@
"dev": true,
"optional": true
},
+ "@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "eslint-visitor-keys": "^3.3.0"
+ }
+ },
+ "@eslint-community/regexpp": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz",
+ "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==",
+ "dev": true,
+ "peer": true
+ },
+ "@eslint/eslintrc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
+ "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "peer": true,
+ "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==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "@eslint/js": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz",
+ "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==",
+ "dev": true,
+ "peer": true
+ },
+ "@humanwhocodes/config-array": {
+ "version": "0.11.11",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
+ "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.5"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "peer": true,
+ "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==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "peer": true
+ },
+ "@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true,
+ "peer": true
+ },
"@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@@ -4416,6 +5550,35 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
+ "@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "peer": true
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ }
+ },
"@types/color-name": {
"version": "1.1.1",
"dev": true
@@ -4652,9 +5815,9 @@
"dev": true
},
"acorn": {
- "version": "8.8.2",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
- "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
"dev": true
},
"acorn-import-assertions": {
@@ -4664,6 +5827,14 @@
"dev": true,
"requires": {}
},
+ "acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {}
+ },
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -4751,7 +5922,9 @@
}
},
"ansi-regex": {
- "version": "5.0.0",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true
},
"ansi-styles": {
@@ -4930,6 +6103,13 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "peer": true
+ },
"camelcase": {
"version": "5.3.1",
"dev": true
@@ -5109,6 +6289,13 @@
"version": "1.2.0",
"dev": true
},
+ "deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "peer": true
+ },
"define-properties": {
"version": "1.1.3",
"dev": true,
@@ -5130,6 +6317,16 @@
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
"dev": true
},
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
"ecc-jsbn": {
"version": "0.1.2",
"dev": true,
@@ -5189,99 +6386,386 @@
"is-arrayish": "^0.2.1"
}
},
- "es-abstract": {
- "version": "1.13.0",
+ "es-abstract": {
+ "version": "1.13.0",
+ "dev": true,
+ "requires": {
+ "es-to-primitive": "^1.2.0",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "is-callable": "^1.1.4",
+ "is-regex": "^1.0.4",
+ "object-keys": "^1.0.12"
+ }
+ },
+ "es-module-lexer": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz",
+ "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==",
+ "dev": true
+ },
+ "es-to-primitive": {
+ "version": "1.2.0",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "es6-object-assign": {
+ "version": "1.1.0",
+ "dev": true
+ },
+ "esbuild": {
+ "version": "0.16.17",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz",
+ "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==",
+ "dev": true,
+ "requires": {
+ "@esbuild/android-arm": "0.16.17",
+ "@esbuild/android-arm64": "0.16.17",
+ "@esbuild/android-x64": "0.16.17",
+ "@esbuild/darwin-arm64": "0.16.17",
+ "@esbuild/darwin-x64": "0.16.17",
+ "@esbuild/freebsd-arm64": "0.16.17",
+ "@esbuild/freebsd-x64": "0.16.17",
+ "@esbuild/linux-arm": "0.16.17",
+ "@esbuild/linux-arm64": "0.16.17",
+ "@esbuild/linux-ia32": "0.16.17",
+ "@esbuild/linux-loong64": "0.16.17",
+ "@esbuild/linux-mips64el": "0.16.17",
+ "@esbuild/linux-ppc64": "0.16.17",
+ "@esbuild/linux-riscv64": "0.16.17",
+ "@esbuild/linux-s390x": "0.16.17",
+ "@esbuild/linux-x64": "0.16.17",
+ "@esbuild/netbsd-x64": "0.16.17",
+ "@esbuild/openbsd-x64": "0.16.17",
+ "@esbuild/sunos-x64": "0.16.17",
+ "@esbuild/win32-arm64": "0.16.17",
+ "@esbuild/win32-ia32": "0.16.17",
+ "@esbuild/win32-x64": "0.16.17"
+ }
+ },
+ "esbuild-loader": {
+ "version": "2.21.0",
+ "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-2.21.0.tgz",
+ "integrity": "sha512-k7ijTkCT43YBSZ6+fBCW1Gin7s46RrJ0VQaM8qA7lq7W+OLsGgtLyFV8470FzYi/4TeDexniTBTPTwZUnXXR5g==",
+ "dev": true,
+ "requires": {
+ "esbuild": "^0.16.17",
+ "joycon": "^3.0.1",
+ "json5": "^2.2.0",
+ "loader-utils": "^2.0.0",
+ "tapable": "^2.2.0",
+ "webpack-sources": "^1.4.3"
+ }
+ },
+ "escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "dev": true
+ },
+ "eslint": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz",
+ "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.2",
+ "@eslint/js": "8.49.0",
+ "@humanwhocodes/config-array": "^0.11.11",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "peer": true
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "peer": true
+ },
+ "eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ }
+ },
+ "estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "peer": true
+ },
+ "find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "is-glob": "^4.0.3"
+ }
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "peer": true
+ },
+ "locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "p-locate": "^5.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true,
+ "peer": true
+ },
+ "p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "yocto-queue": "^0.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "p-limit": "^3.0.2"
+ }
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "peer": true
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "peer": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ }
+ }
+ },
+ "eslint-plugin-promise": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
+ "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==",
"dev": true,
- "requires": {
- "es-to-primitive": "^1.2.0",
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
- "is-callable": "^1.1.4",
- "is-regex": "^1.0.4",
- "object-keys": "^1.0.12"
- }
- },
- "es-module-lexer": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz",
- "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==",
- "dev": true
+ "requires": {}
},
- "es-to-primitive": {
- "version": "1.2.0",
+ "eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"requires": {
- "is-callable": "^1.1.4",
- "is-date-object": "^1.0.1",
- "is-symbol": "^1.0.2"
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
}
},
- "es6-object-assign": {
- "version": "1.1.0",
- "dev": true
- },
- "esbuild": {
- "version": "0.16.17",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz",
- "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==",
+ "eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
- "requires": {
- "@esbuild/android-arm": "0.16.17",
- "@esbuild/android-arm64": "0.16.17",
- "@esbuild/android-x64": "0.16.17",
- "@esbuild/darwin-arm64": "0.16.17",
- "@esbuild/darwin-x64": "0.16.17",
- "@esbuild/freebsd-arm64": "0.16.17",
- "@esbuild/freebsd-x64": "0.16.17",
- "@esbuild/linux-arm": "0.16.17",
- "@esbuild/linux-arm64": "0.16.17",
- "@esbuild/linux-ia32": "0.16.17",
- "@esbuild/linux-loong64": "0.16.17",
- "@esbuild/linux-mips64el": "0.16.17",
- "@esbuild/linux-ppc64": "0.16.17",
- "@esbuild/linux-riscv64": "0.16.17",
- "@esbuild/linux-s390x": "0.16.17",
- "@esbuild/linux-x64": "0.16.17",
- "@esbuild/netbsd-x64": "0.16.17",
- "@esbuild/openbsd-x64": "0.16.17",
- "@esbuild/sunos-x64": "0.16.17",
- "@esbuild/win32-arm64": "0.16.17",
- "@esbuild/win32-ia32": "0.16.17",
- "@esbuild/win32-x64": "0.16.17"
- }
+ "peer": true
},
- "esbuild-loader": {
- "version": "2.21.0",
- "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-2.21.0.tgz",
- "integrity": "sha512-k7ijTkCT43YBSZ6+fBCW1Gin7s46RrJ0VQaM8qA7lq7W+OLsGgtLyFV8470FzYi/4TeDexniTBTPTwZUnXXR5g==",
+ "espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
+ "peer": true,
"requires": {
- "esbuild": "^0.16.17",
- "joycon": "^3.0.1",
- "json5": "^2.2.0",
- "loader-utils": "^2.0.0",
- "tapable": "^2.2.0",
- "webpack-sources": "^1.4.3"
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
}
},
- "escalade": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
- "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
- "dev": true
- },
- "escape-string-regexp": {
- "version": "1.0.5",
- "dev": true
- },
- "eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
"dev": true,
+ "peer": true,
"requires": {
- "esrecurse": "^4.3.0",
- "estraverse": "^4.1.1"
+ "estraverse": "^5.1.0"
+ },
+ "dependencies": {
+ "estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "peer": true
+ }
}
},
"esrecurse": {
@@ -5307,6 +6791,13 @@
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true
},
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "peer": true
+ },
"events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -5338,19 +6829,38 @@
"dev": true
},
"fast-deep-equal": {
- "version": "3.1.1",
+ "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==",
"dev": true
},
"fast-json-stable-stringify": {
"version": "2.1.0",
"dev": true
},
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "peer": true
+ },
"fastest-levenshtein": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
"integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
"dev": true
},
+ "fastq": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
"fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
@@ -5364,6 +6874,16 @@
"escape-string-regexp": "^1.0.5"
}
},
+ "file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "flat-cache": "^3.0.4"
+ }
+ },
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -5381,6 +6901,25 @@
"path-exists": "^4.0.0"
}
},
+ "flat-cache": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz",
+ "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "flatted": "^3.2.7",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ }
+ },
+ "flatted": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+ "dev": true,
+ "peer": true
+ },
"fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
@@ -5466,12 +7005,38 @@
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true
},
+ "globals": {
+ "version": "13.21.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz",
+ "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ },
+ "dependencies": {
+ "type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
"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==",
"dev": true
},
+ "graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "peer": true
+ },
"handlebars": {
"version": "4.7.7",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
@@ -5532,12 +7097,39 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
+ "ignore": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "dev": true,
+ "peer": true
+ },
"ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
"dev": true
},
+ "import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "dependencies": {
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
"import-local": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
@@ -5548,6 +7140,13 @@
"resolve-cwd": "^3.0.0"
}
},
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "peer": true
+ },
"inflight": {
"version": "1.0.6",
"dev": true,
@@ -5669,6 +7268,13 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
+ "is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "peer": true
+ },
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -5756,10 +7362,27 @@
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
"dev": true
},
+ "js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "argparse": "^2.0.1"
+ }
+ },
"jsbn": {
"version": "0.1.1",
"dev": true
},
+ "json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "peer": true
+ },
"json-fixer": {
"version": "1.4.1",
"dev": true,
@@ -5827,6 +7450,13 @@
"version": "0.4.1",
"dev": true
},
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "peer": true
+ },
"json-stringify-safe": {
"version": "5.0.1",
"dev": true
@@ -5861,6 +7491,16 @@
"verror": "1.10.0"
}
},
+ "keyv": {
+ "version": "4.5.3",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz",
+ "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "json-buffer": "3.0.1"
+ }
+ },
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -5882,6 +7522,17 @@
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
"dev": true
},
+ "levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
"linkify-it": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
@@ -5929,6 +7580,13 @@
"version": "4.17.21",
"dev": true
},
+ "lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "peer": true
+ },
"logform": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz",
@@ -6019,6 +7677,13 @@
"version": "0.0.8",
"dev": true
},
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "peer": true
+ },
"neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
@@ -6124,6 +7789,21 @@
"mimic-fn": "^2.1.0"
}
},
+ "optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ }
+ },
"os-tmpdir": {
"version": "1.0.2",
"dev": true
@@ -6146,6 +7826,16 @@
"version": "2.2.0",
"dev": true
},
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
"parse-json": {
"version": "4.0.0",
"dev": true,
@@ -6214,6 +7904,13 @@
"find-up": "^4.0.0"
}
},
+ "prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "peer": true
+ },
"prismjs": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
@@ -6238,6 +7935,13 @@
"version": "6.5.2",
"dev": true
},
+ "queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "peer": true
+ },
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -6351,10 +8055,37 @@
"signal-exit": "^3.0.2"
}
},
+ "reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "peer": true
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
"run-async": {
"version": "2.4.1",
"dev": true
},
+ "run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
"rxjs": {
"version": "6.5.5",
"dev": true,
@@ -6587,16 +8318,25 @@
}
},
"strip-ansi": {
- "version": "6.0.0",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"requires": {
- "ansi-regex": "^5.0.0"
+ "ansi-regex": "^5.0.1"
}
},
"strip-bom": {
"version": "3.0.0",
"dev": true
},
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "peer": true
+ },
"style-loader": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz",
@@ -6656,6 +8396,13 @@
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
"dev": true
},
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "peer": true
+ },
"through": {
"version": "2.3.8",
"dev": true
@@ -6714,6 +8461,16 @@
"version": "0.14.5",
"dev": true
},
+ "type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "prelude-ls": "^1.2.1"
+ }
+ },
"type-fest": {
"version": "0.11.0",
"dev": true
@@ -7062,6 +8819,13 @@
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
+ },
+ "yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "peer": true
}
}
}
diff --git a/package.json b/package.json
index 65d0e177ce..86389f3b52 100644
--- a/package.json
+++ b/package.json
@@ -46,8 +46,8 @@
"devDependencies": {
"apidoc": "^1.0.3",
"all-contributors-cli": "^6.15.0",
+ "eslint-plugin-promise": "^6.1.1",
"npm-run-all": "^4.1.5",
"shx": "^0.3.2"
- },
- "dependencies": {}
+ }
}
diff --git a/server/lib/system/index.js b/server/lib/system/index.js
index d08f206804..b277f3bfc2 100644
--- a/server/lib/system/index.js
+++ b/server/lib/system/index.js
@@ -9,6 +9,7 @@ const { isDocker } = require('./system.isDocker');
const { getGladysBasePath } = require('./system.getGladysBasePath');
const { getContainers } = require('./system.getContainers');
const { getContainerMounts } = require('./system.getContainerMounts');
+const { getContainerDevices } = require('./system.getContainerDevices');
const { inspectContainer } = require('./system.inspectContainer');
const { getGladysContainerId } = require('./system.getGladysContainerId');
const { getInfos } = require('./system.getInfos');
@@ -48,6 +49,7 @@ System.prototype.installUpgrade = installUpgrade;
System.prototype.isDocker = isDocker;
System.prototype.getContainers = getContainers;
System.prototype.getContainerMounts = getContainerMounts;
+System.prototype.getContainerDevices = getContainerDevices;
System.prototype.inspectContainer = inspectContainer;
System.prototype.getGladysBasePath = getGladysBasePath;
System.prototype.getGladysContainerId = getGladysContainerId;
diff --git a/server/lib/system/system.getContainerDevices.js b/server/lib/system/system.getContainerDevices.js
new file mode 100644
index 0000000000..d5c6134060
--- /dev/null
+++ b/server/lib/system/system.getContainerDevices.js
@@ -0,0 +1,28 @@
+const { PlatformNotCompatible } = require('../../utils/coreErrors');
+
+/**
+ * @description Return list of devices for this container.
+ * @param {string} containerId - Id of the container.
+ * @returns {Promise} Resolve with list of devices.
+ * @example
+ * const binds = await getContainerDevices('e24ae1745d91');
+ */
+async function getContainerDevices(containerId) {
+ if (!this.dockerode) {
+ throw new PlatformNotCompatible('SYSTEM_NOT_RUNNING_DOCKER');
+ }
+
+ const container = await this.dockerode.getContainer(containerId);
+ if (!container) {
+ return [];
+ }
+ const inspect = await container.inspect();
+ if (!inspect) {
+ return [];
+ }
+ return inspect.HostConfig.Devices || [];
+}
+
+module.exports = {
+ getContainerDevices,
+};
diff --git a/server/services/index.js b/server/services/index.js
index 83c929aee9..53c49fe30b 100644
--- a/server/services/index.js
+++ b/server/services/index.js
@@ -10,6 +10,7 @@ module.exports['rtsp-camera'] = require('./rtsp-camera');
module.exports.telegram = require('./telegram');
module.exports.usb = require('./usb');
module.exports.xiaomi = require('./xiaomi');
+module.exports['zwave-js-ui'] = require('./zwave-js-ui');
module.exports.tasmota = require('./tasmota');
module.exports.bluetooth = require('./bluetooth');
module.exports.ewelink = require('./ewelink');
diff --git a/server/services/zwave-js-ui/api/zwavejsui.controller.js b/server/services/zwave-js-ui/api/zwavejsui.controller.js
new file mode 100644
index 0000000000..3e252eb753
--- /dev/null
+++ b/server/services/zwave-js-ui/api/zwavejsui.controller.js
@@ -0,0 +1,148 @@
+const asyncMiddleware = require('../../../api/middlewares/asyncMiddleware');
+
+module.exports = function ZwaveController(gladys, zwaveJSUIManager, serviceId) {
+ /**
+ * @api {get} /api/v1/service/zwave-js-ui/node Get Zwave nodes
+ * @apiName getNodes
+ * @apiGroup ZwaveJSUI
+ */
+ async function getNodes(req, res) {
+ const nodes = await zwaveJSUIManager.getNodes(req.query);
+ res.json(nodes);
+ }
+
+ /**
+ * @api {get} /api/v1/service/zwave-js-ui/status Get Zwave Status
+ * @apiName getStatus
+ * @apiGroup ZwaveJSUI
+ */
+ async function getStatus(req, res) {
+ const status = await zwaveJSUIManager.getStatus();
+ res.json(status);
+ }
+
+ /**
+ * @api {get} /api/v1/service/zwave-js-ui/configuration Get Zwave configuration
+ * @apiName getConfiguration
+ * @apiGroup ZwaveJSUI
+ */
+ async function getConfiguration(req, res) {
+ const configuration = await zwaveJSUIManager.getConfiguration();
+ res.json(configuration);
+ }
+
+ /**
+ * @api {post} /api/v1/service/zwave-js-ui/configuration Update configuration
+ * @apiName updateConfiguration
+ * @apiGroup ZwaveJSUI
+ */
+ async function updateConfiguration(req, res) {
+ const result = await zwaveJSUIManager.updateConfiguration(req.body);
+ res.json({
+ success: result,
+ });
+ }
+
+ /**
+ * @api {post} /api/v1/service/zwave-js-ui/connect Connect
+ * @apiName connect
+ * @apiGroup ZwaveJSUI
+ */
+ async function connect(req, res) {
+ await zwaveJSUIManager.connect();
+ res.json({
+ success: true,
+ });
+ }
+
+ /**
+ * @api {post} /api/v1/service/zwave-js-ui/disconnect Disconnect
+ * @apiName disconnect
+ * @apiGroup ZwaveJSUI
+ */
+ async function disconnect(req, res) {
+ await zwaveJSUIManager.disconnect();
+ res.json({
+ success: true,
+ });
+ }
+
+ /**
+ * @api {post} /api/v1/service/zwave-js-ui/scan Scan Zwave network
+ * @apiName scanNetwork
+ * @apiGroup ZwaveJSUI
+ */
+ async function scanNetwork(req, res) {
+ await zwaveJSUIManager.scanNetwork();
+ res.json({
+ success: true,
+ });
+ }
+
+ /**
+ * @api {post} /api/v1/service/zwave-js-ui/node/add Add Node
+ * @apiName addNode
+ * @apiGroup ZwaveJSUI
+ */
+ function addNode(req, res) {
+ zwaveJSUIManager.addNode(req.body.secure);
+ res.json({
+ success: true,
+ });
+ }
+
+ /**
+ * @api {post} /api/v1/service/zwave-js-ui/node/remove Remove Node
+ * @apiName removeNode
+ * @apiGroup ZwaveJSUI
+ */
+ function removeNode(req, res) {
+ zwaveJSUIManager.removeNode();
+ res.json({
+ success: true,
+ });
+ }
+
+ return {
+ 'get /api/v1/service/zwave-js-ui/node': {
+ authenticated: true,
+ controller: asyncMiddleware(getNodes),
+ },
+ 'post /api/v1/service/zwave-js-ui/scan': {
+ authenticated: true,
+ controller: asyncMiddleware(scanNetwork),
+ },
+ 'get /api/v1/service/zwave-js-ui/status': {
+ authenticated: false,
+ controller: asyncMiddleware(getStatus),
+ },
+ 'get /api/v1/service/zwave-js-ui/configuration': {
+ authenticated: true,
+ controller: asyncMiddleware(getConfiguration),
+ },
+ 'post /api/v1/service/zwave-js-ui/configuration': {
+ authenticated: true,
+ controller: asyncMiddleware(updateConfiguration),
+ },
+ 'post /api/v1/service/zwave-js-ui/connect': {
+ authenticated: true,
+ admin: true,
+ controller: asyncMiddleware(connect),
+ },
+ 'post /api/v1/service/zwave-js-ui/disconnect': {
+ authenticated: true,
+ admin: true,
+ controller: asyncMiddleware(disconnect),
+ },
+ 'post /api/v1/service/zwave-js-ui/node/add': {
+ authenticated: true,
+ admin: true,
+ controller: asyncMiddleware(addNode),
+ },
+ 'post /api/v1/service/zwave-js-ui/node/remove': {
+ authenticated: true,
+ admin: true,
+ controller: asyncMiddleware(removeNode),
+ },
+ };
+};
diff --git a/server/services/zwave-js-ui/docker/gladys-zwavejsui-mqtt-container.json b/server/services/zwave-js-ui/docker/gladys-zwavejsui-mqtt-container.json
new file mode 100644
index 0000000000..aaf006af4a
--- /dev/null
+++ b/server/services/zwave-js-ui/docker/gladys-zwavejsui-mqtt-container.json
@@ -0,0 +1,31 @@
+{
+ "name": "gladys-zwave-js-ui-mqtt",
+ "Image": "eclipse-mosquitto:2.0.15",
+ "ExposedPorts": {
+ "1885/tcp": {}
+ },
+ "HostConfig": {
+ "Binds": [],
+ "PortBindings": {
+ "1885/tcp": [
+ {
+ "HostPort": "1885"
+ }
+ ]
+ },
+ "RestartPolicy": {
+ "Name": "always"
+ },
+ "NetworkMode": "host",
+ "Dns": [],
+ "DnsOptions": [],
+ "DnsSearch": [],
+ "BlkioWeightDevice": [],
+ "Devices": []
+ },
+ "NetworkDisabled": false,
+ "AttachStdin": false,
+ "AttachStdout": false,
+ "AttachStderr": false,
+ "Tty": false
+}
diff --git a/server/services/zwave-js-ui/docker/gladys-zwavejsui-mqtt-env.sh b/server/services/zwave-js-ui/docker/gladys-zwavejsui-mqtt-env.sh
new file mode 100644
index 0000000000..c63789b724
--- /dev/null
+++ b/server/services/zwave-js-ui/docker/gladys-zwavejsui-mqtt-env.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+# Base path
+base_path_container=$1
+
+# Configuration path
+mosquitto_dir=${base_path_container}/mosquitto/config
+# Configuration file
+mosquitto_config_file=${mosquitto_dir}/mosquitto.conf
+# Password file
+mosquitto_passwd_file=${mosquitto_dir}/mosquitto.passwd
+internal_mosquitto_passwd_file=/mosquitto/config/mosquitto.passwd
+
+# Create configuration path (if not exists)
+mkdir -p $mosquitto_dir
+
+# Check if config file not already exists
+if [ ! -f "$mosquitto_config_file" ]; then
+ echo "zwave-js-ui : Writing MQTT configuration..."
+
+ # Create config file
+ touch $mosquitto_config_file
+
+ # Write defaults
+ echo "listener 1885" >> $mosquitto_config_file
+ echo "allow_anonymous false" >> $mosquitto_config_file
+ echo "# connection_messages false" >> $mosquitto_config_file
+ echo "password_file ${internal_mosquitto_passwd_file}" >> $mosquitto_config_file
+
+ echo "zwave-js-ui : MQTT configuration written"
+else
+ echo "zwave-js-ui : MQTT configuration file already exists."
+fi
+
+# Create passwd file if not exists
+touch ${mosquitto_passwd_file}
diff --git a/server/services/zwave-js-ui/docker/gladys-zwavejsui-zwavejsui-container.json b/server/services/zwave-js-ui/docker/gladys-zwavejsui-zwavejsui-container.json
new file mode 100644
index 0000000000..59d2c41214
--- /dev/null
+++ b/server/services/zwave-js-ui/docker/gladys-zwavejsui-zwavejsui-container.json
@@ -0,0 +1,35 @@
+{
+ "name": "gladys-zwave-js-ui-zwave-js-ui",
+ "Image": "zwavejs/zwave-js-ui:latest",
+ "ExposedPorts": {},
+ "HostConfig": {
+ "Binds": ["/run/udev:/run/udev:ro"],
+ "PortBindings": {
+ "8091/tcp": [
+ {
+ "HostPort": "8091"
+ }
+ ]
+ },
+ "RestartPolicy": {
+ "Name": "always"
+ },
+ "NetworkMode": "host",
+ "Dns": [],
+ "DnsOptions": [],
+ "DnsSearch": [],
+ "BlkioWeightDevice": [],
+ "Devices": [
+ {
+ "PathOnHost": "/dev/ttyUSB0",
+ "PathInContainer": "/dev/ttyUSB0",
+ "CgroupPermissions": "rwm"
+ }
+ ]
+ },
+ "NetworkDisabled": false,
+ "AttachStdin": false,
+ "AttachStdout": false,
+ "AttachStderr": false,
+ "Tty": false
+}
diff --git a/server/services/zwave-js-ui/docker/gladys-zwavejsui-zwavejsui-env.sh b/server/services/zwave-js-ui/docker/gladys-zwavejsui-zwavejsui-env.sh
new file mode 100644
index 0000000000..469ce64d72
--- /dev/null
+++ b/server/services/zwave-js-ui/docker/gladys-zwavejsui-zwavejsui-env.sh
@@ -0,0 +1,85 @@
+#!/bin/sh
+
+# Base path
+base_path_container=$1
+
+# Configuration path
+zwave_js_ui_dir=${base_path_container}/zwave-js-ui
+# Configuration file
+zwave_js_ui_config_file=${zwave_js_ui_dir}/settings.json
+
+# Create configuration path (if not exists)
+mkdir -p $zwave_js_ui_dir
+
+echo "ZwaveJSUI : Writing ZwaveJSUI configuration..."
+
+rm -f $zwave_js_ui_config_file
+
+# Create config file
+touch $zwave_js_ui_config_file
+chmod o-r $zwave_js_ui_config_file
+
+# Write defaults
+cat <>$zwave_js_ui_config_file
+{
+ "mqtt": {
+ "name": "Gladys",
+ "host": "mqtt://localhost",
+ "port": 1885,
+ "qos": 1,
+ "prefix": "zwave-js-ui",
+ "reconnectPeriod": 10000,
+ "retain": true,
+ "clean": true,
+ "auth": true,
+ "username": "$2",
+ "password": "$3"
+ },
+ "gateway": {
+ "type": 0,
+ "plugins": [],
+ "authEnabled": true,
+ "payloadType": 2,
+ "nodeNames": true,
+ "hassDiscovery": false,
+ "discoveryPrefix": "homeassistant",
+ "logEnabled": true,
+ "logLevel": "info",
+ "logToFile": true,
+ "values": [],
+ "sendEvents": true,
+ "ignoreStatus": false,
+ "ignoreLoc": true,
+ "includeNodeInfo": true,
+ "publishNodeDetails": false
+ },
+ "zwave": {
+ "port": "$4",
+ "commandsTimeout": 30000,
+ "logLevel": "info",
+ "logEnabled": true,
+ "deviceConfigPriorityDir": "/usr/src/app/store/config",
+ "logToFile": true,
+ "serverEnabled": false,
+ "serverServiceDiscoveryDisabled": false,
+ "enableSoftReset": true,
+ "enableStatistics": true,
+ "serverPort": 3000,
+ "logging": true,
+ "autoUpdateConfig": true,
+ "saveConfig": true,
+ "assumeAwake": true,
+ "pollInterval": 2000,
+ "nodeFilter": [],
+ "disclaimerVersion": 1,
+ "securityKeys": {
+ "S2_Unauthenticated": "$5",
+ "S2_Authenticated": "$6",
+ "S2_AccessControl": "$7",
+ "S0_Legacy": "$8"
+ }
+ }
+}
+EOF
+
+echo "zwave-js-ui : configuration written"
diff --git a/server/services/zwave-js-ui/index.js b/server/services/zwave-js-ui/index.js
new file mode 100644
index 0000000000..70ffba013a
--- /dev/null
+++ b/server/services/zwave-js-ui/index.js
@@ -0,0 +1,50 @@
+const logger = require('../../utils/logger');
+const ZwaveJSUIManager = require('./lib');
+const ZwaveJSUIController = require('./api/zwavejsui.controller');
+
+module.exports = function ZwaveJSUIService(gladys, serviceId) {
+ const mqtt = require('mqtt');
+
+ const zwaveJSUIManager = new ZwaveJSUIManager(gladys, mqtt, serviceId);
+
+ /**
+ * @public
+ * @description This function starts the service.
+ * @example
+ * gladys.services.zwave-js-ui.start();
+ */
+ async function start() {
+ logger.log('Starting ZwaveJSUI service');
+ await zwaveJSUIManager.connect();
+ }
+
+ /**
+ * @public
+ * @description This function stops the service.
+ * @example
+ * gladys.services.zwave-js-ui.stop();
+ */
+ async function stop() {
+ logger.info('Stopping ZwaveJSUI service');
+ await zwaveJSUIManager.disconnect();
+ }
+
+ /**
+ * @public
+ * @description Get info if the service is used.
+ * @returns {Promise} Returns true if the service is used.
+ * @example
+ * gladys.services.zwave-js-ui.isUsed();
+ */
+ async function isUsed() {
+ return zwaveJSUIManager.mqttConnected && zwaveJSUIManager.nodes && zwaveJSUIManager.nodes.length > 0;
+ }
+
+ return Object.freeze({
+ start,
+ stop,
+ isUsed,
+ device: zwaveJSUIManager,
+ controllers: ZwaveJSUIController(gladys, zwaveJSUIManager, serviceId),
+ });
+};
diff --git a/server/services/zwave-js-ui/lib/commands/addNode.js b/server/services/zwave-js-ui/lib/commands/addNode.js
new file mode 100644
index 0000000000..875ec7d0be
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/addNode.js
@@ -0,0 +1,23 @@
+const logger = require('../../../../utils/logger');
+const { DEFAULT } = require('../constants');
+
+/**
+ * @description Add node.
+ * @param {boolean} secure - Secure node.
+ * @example
+ * zwave.addNode(true);
+ */
+function addNode(secure = true) {
+ logger.debug(`Zwave : Entering inclusion mode`);
+
+ this.mqttClient.publish(`${this.mqttTopicPrefix}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/startInclusion/set`);
+
+ setTimeout(() => {
+ this.mqttClient.publish(`${this.mqttTopicPrefix}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/stopInclusion/set`);
+ this.scanNetwork();
+ }, DEFAULT.ADD_NODE_TIMEOUT);
+}
+
+module.exports = {
+ addNode,
+};
diff --git a/server/services/zwave-js-ui/lib/commands/connect.js b/server/services/zwave-js-ui/lib/commands/connect.js
new file mode 100644
index 0000000000..1a29420dc7
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/connect.js
@@ -0,0 +1,202 @@
+const crypto = require('crypto');
+const logger = require('../../../../utils/logger');
+
+const { DEFAULT, CONFIGURATION } = require('../constants');
+const { WEBSOCKET_MESSAGE_TYPES, EVENTS } = require('../../../../utils/constants');
+const { generate } = require('../../../../utils/password');
+
+/**
+ * @description Initialize service with dependencies and connect to devices.
+ * @example
+ * connect();
+ */
+async function connect() {
+ const externalZwaveJSUIStr = await this.gladys.variable.getValue(CONFIGURATION.EXTERNAL_ZWAVEJSUI, this.serviceId);
+ let externalZwaveJSUI;
+ if (externalZwaveJSUIStr) {
+ externalZwaveJSUI = externalZwaveJSUIStr === '1';
+ } else {
+ externalZwaveJSUI = DEFAULT.EXTERNAL_ZWAVEJSUI;
+
+ await this.gladys.variable.setValue(
+ CONFIGURATION.EXTERNAL_ZWAVEJSUI,
+ externalZwaveJSUI ? '1' : '0',
+ this.serviceId,
+ );
+ }
+
+ let mqttPassword = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJSUI_MQTT_PASSWORD, this.serviceId);
+
+ // Test if dongle is present
+ this.usbConfigured = false;
+ if (externalZwaveJSUI) {
+ logger.info(`ZwaveJS UI USB dongle assumed to be attached`);
+ this.usbConfigured = true;
+ this.driverPath = 'N.A.';
+ this.mqttExist = true;
+ this.zwaveJSUIExist = true;
+ this.mqttRunning = true;
+ this.zwaveJSUIRunning = true;
+ } else {
+ // MQTT configuration
+ if (!mqttPassword) {
+ // First start, use default value for MQTT
+ const mqttUrl = DEFAULT.ZWAVEJSUI_MQTT_URL_VALUE;
+ await this.gladys.variable.setValue(CONFIGURATION.ZWAVEJSUI_MQTT_URL, mqttUrl, this.serviceId);
+ const mqttUsername = DEFAULT.ZWAVEJSUI_MQTT_USERNAME_VALUE;
+ await this.gladys.variable.setValue(CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME, mqttUsername, this.serviceId);
+ mqttPassword = generate(20, { number: true, lowercase: true, uppercase: true });
+ await this.gladys.variable.setValue(CONFIGURATION.ZWAVEJSUI_MQTT_PASSWORD, mqttPassword, this.serviceId);
+ // Keep copy in case switch between external/gladys ZwaveJS UI
+ await this.gladys.variable.setValue(CONFIGURATION.DEFAULT_ZWAVEJSUI_MQTT_PASSWORD, mqttPassword, this.serviceId);
+ }
+
+ const driverPath = await this.gladys.variable.getValue(CONFIGURATION.DRIVER_PATH, this.serviceId);
+ if (driverPath) {
+ const usb = this.gladys.service.getService('usb');
+ const usbList = await usb.list();
+ usbList.forEach((usbPort) => {
+ if (driverPath === usbPort.path) {
+ this.usbConfigured = true;
+ logger.info(`ZwaveJS UI USB dongle attached to ${driverPath}`);
+ }
+ });
+ if (!this.usbConfigured) {
+ logger.info(`ZwaveJS UI USB dongle detached to ${driverPath}`);
+ }
+ } else {
+ logger.info(`ZwaveJSUI USB dongle not attached`);
+ }
+
+ if (this.usbConfigured) {
+ // Security keys configuration
+ let s2UnauthenticatedKey = await this.gladys.variable.getValue(CONFIGURATION.S2_UNAUTHENTICATED, this.serviceId);
+ if (!s2UnauthenticatedKey) {
+ s2UnauthenticatedKey = crypto.randomBytes(16).toString('hex');
+ await this.gladys.variable.setValue(CONFIGURATION.S2_UNAUTHENTICATED, s2UnauthenticatedKey, this.serviceId);
+ }
+ let s2AuthenticatedKey = await this.gladys.variable.getValue(CONFIGURATION.S2_AUTHENTICATED, this.serviceId);
+ if (!s2AuthenticatedKey) {
+ s2AuthenticatedKey = crypto.randomBytes(16).toString('hex');
+ await this.gladys.variable.setValue(CONFIGURATION.S2_AUTHENTICATED, s2AuthenticatedKey, this.serviceId);
+ }
+ let s2AccessControlKey = await this.gladys.variable.getValue(CONFIGURATION.S2_ACCESS_CONTROL, this.serviceId);
+ if (!s2AccessControlKey) {
+ s2AccessControlKey = crypto.randomBytes(16).toString('hex');
+ await this.gladys.variable.setValue(CONFIGURATION.S2_ACCESS_CONTROL, s2AccessControlKey, this.serviceId);
+ }
+ let s0LegacyKey = await this.gladys.variable.getValue(CONFIGURATION.S0_LEGACY, this.serviceId);
+ if (!s0LegacyKey) {
+ s0LegacyKey = crypto.randomBytes(16).toString('hex');
+ await this.gladys.variable.setValue(CONFIGURATION.S0_LEGACY, s0LegacyKey, this.serviceId);
+ }
+
+ this.dockerBased = await this.gladys.system.isDocker();
+ if (this.dockerBased) {
+ await this.installMqttContainer();
+ if (this.usbConfigured) {
+ await this.installZwaveJSUIContainer();
+ }
+ } else {
+ this.mqttExist = true;
+ this.mqttRunning = true;
+ this.zwaveJSUIExist = true;
+ this.zwaveJSUIRunning = true;
+ }
+ }
+ }
+
+ this.mqttTopicPrefix = DEFAULT.ZWAVEJSUI_MQTT_TOPIC_PREFIX;
+ this.mqttTopicWithLocation = false;
+ if (externalZwaveJSUI) {
+ const storedMqttTopicPrefix = await this.gladys.variable.getValue(
+ CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_PREFIX,
+ this.serviceId,
+ );
+ if (storedMqttTopicPrefix) {
+ this.mqttTopicPrefix = storedMqttTopicPrefix;
+ } else {
+ await this.gladys.variable.setValue(
+ CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_PREFIX,
+ this.mqttTopicPrefix,
+ this.serviceId,
+ );
+ }
+ const mqttTopicWithLocationStr = await this.gladys.variable.getValue(
+ CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_WITH_LOCATION,
+ this.serviceId,
+ );
+ this.mqttTopicWithLocation = mqttTopicWithLocationStr === '1';
+ } else {
+ await this.gladys.variable.setValue(
+ CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_PREFIX,
+ this.mqttTopicPrefix,
+ this.serviceId,
+ );
+ }
+
+ const mqttUrl = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJSUI_MQTT_URL, this.serviceId);
+ const mqttUsername = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME, this.serviceId);
+ if (this.mqttRunning) {
+ const options = {
+ // reconnectPeriod: 5000,
+ // clientId: DEFAULT.MQTT_CLIENT_ID,
+ };
+ if (mqttUsername && mqttPassword) {
+ options.username = mqttUsername;
+ options.password = mqttPassword;
+ }
+ this.mqttClient = this.mqtt.connect(mqttUrl, options);
+
+ this.mqttClient.on('connect', () => {
+ logger.info('Connected to MQTT container');
+ this.mqttClient.subscribe(`${this.mqttTopicPrefix}/#`);
+ logger.info(`Listening to MQTT topics ${this.mqttTopicPrefix}/#`);
+ this.mqttConnected = true;
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ });
+
+ this.mqttClient.on('error', (err) => {
+ logger.warn(`Error while connecting to MQTT - ${err}`);
+ if (err.code === 5 || err.code === 'ECONNREFUSED') {
+ // Connection refused: Not authorized
+ this.disconnect();
+ }
+ this.mqttConnected = false;
+ this.scanInProgress = false;
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.MQTT_ERROR,
+ payload: err,
+ });
+ });
+
+ this.mqttClient.on('offline', () => {
+ logger.warn(`Disconnected from MQTT server`);
+ this.mqttConnected = false;
+ this.scanInProgress = false;
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.MQTT_ERROR,
+ payload: 'DISCONNECTED',
+ });
+ });
+
+ this.mqttClient.on('message', (topic, message) => {
+ try {
+ this.mqttConnected = true;
+ this.handleMqttMessage(topic, message.toString());
+ } catch (e) {
+ logger.error(`Unable to process message on topic ${topic}: ${e}`);
+ }
+ });
+
+ this.scanNetwork();
+ } else {
+ logger.warn("Can't connect Gladys cause MQTT not running !");
+ }
+}
+
+module.exports = {
+ connect,
+};
diff --git a/server/services/zwave-js-ui/lib/commands/disconnect.js b/server/services/zwave-js-ui/lib/commands/disconnect.js
new file mode 100644
index 0000000000..1d6098d729
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/disconnect.js
@@ -0,0 +1,31 @@
+const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants');
+const logger = require('../../../../utils/logger');
+
+/**
+ * @description Disconnect zwave MQTT.
+ * @example
+ * zwave.disconnect();
+ */
+async function disconnect() {
+ logger.debug(`ZwaveJSUI : Disconnecting...`);
+ if (this.mqttClient) {
+ this.mqttClient.end();
+ this.mqttClient.removeAllListeners();
+ this.mqttClient = null;
+ }
+
+ if (this.mqttConnected) {
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ } else {
+ logger.debug('ZwaveJSUI: Not connected, by-pass disconnecting');
+ }
+
+ this.mqttConnected = false;
+ this.scanInProgress = false;
+}
+
+module.exports = {
+ disconnect,
+};
diff --git a/server/services/zwave-js-ui/lib/commands/getConfiguration.js b/server/services/zwave-js-ui/lib/commands/getConfiguration.js
new file mode 100644
index 0000000000..bb7181af2c
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/getConfiguration.js
@@ -0,0 +1,52 @@
+const logger = require('../../../../utils/logger');
+const { CONFIGURATION, DEFAULT } = require('../constants');
+
+/**
+ * @description Getting Z-Wave information.
+ * @returns {Promise} Return Object of information.
+ * @example
+ * zwave.getConfiguration();
+ */
+async function getConfiguration() {
+ logger.debug(`Zwave : Getting informations...`);
+
+ const externalZwaveJSUIStr = await this.gladys.variable.getValue(CONFIGURATION.EXTERNAL_ZWAVEJSUI, this.serviceId);
+ const externalZwaveJSUI = externalZwaveJSUIStr !== undefined && externalZwaveJSUIStr === '1';
+ const mqttUrl = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJSUI_MQTT_URL, this.serviceId);
+ const mqttUsername = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME, this.serviceId);
+ const mqttPassword = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJSUI_MQTT_PASSWORD, this.serviceId);
+ const mqttTopicPrefix = await this.gladys.variable.getValue(
+ CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_PREFIX,
+ this.serviceId,
+ );
+ const mqttTopicWithLocationStr = await this.gladys.variable.getValue(
+ CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_WITH_LOCATION,
+ this.serviceId,
+ );
+ const mqttTopicWithLocation = mqttTopicWithLocationStr !== undefined && mqttTopicWithLocationStr === '1';
+ const driverPath = await this.gladys.variable.getValue(CONFIGURATION.DRIVER_PATH, this.serviceId);
+ const s2UnauthenticatedKey = await this.gladys.variable.getValue(CONFIGURATION.S2_UNAUTHENTICATED, this.serviceId);
+ const s2AuthenticatedKey = await this.gladys.variable.getValue(CONFIGURATION.S2_AUTHENTICATED, this.serviceId);
+ const s2AccessControlKey = await this.gladys.variable.getValue(CONFIGURATION.S2_ACCESS_CONTROL, this.serviceId);
+ const s0LegacyKey = await this.gladys.variable.getValue(CONFIGURATION.S0_LEGACY, this.serviceId);
+
+ return {
+ externalZwaveJSUI,
+ zwaveJSUIVersion: this.zwaveJSUIVersion,
+ zwaveJSUIExpectedVersion: DEFAULT.ZWAVEJSUI_VERSION_EXPECTED,
+ driverPath,
+ s2UnauthenticatedKey,
+ s2AuthenticatedKey,
+ s2AccessControlKey,
+ s0LegacyKey,
+ mqttUrl,
+ mqttUsername,
+ mqttPassword,
+ mqttTopicPrefix,
+ mqttTopicWithLocation,
+ };
+}
+
+module.exports = {
+ getConfiguration,
+};
diff --git a/server/services/zwave-js-ui/lib/commands/getNodes.js b/server/services/zwave-js-ui/lib/commands/getNodes.js
new file mode 100644
index 0000000000..65946d0e8c
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/getNodes.js
@@ -0,0 +1,172 @@
+const { slugify } = require('../../../../utils/slugify');
+const { getCategory } = require('../utils/getCategory');
+const { getUnit } = require('../utils/getUnit');
+const {
+ getDeviceFeatureExternalId,
+ getDeviceExternalId,
+ getDeviceName,
+ getDeviceFeatureName,
+} = require('../utils/externalId');
+const logger = require('../../../../utils/logger');
+const { unbindValue } = require('../utils/bindValue');
+const { splitNode } = require('../utils/splitNode');
+const { transformClasses } = require('../utils/transformClasses');
+const { PARAMS, DEFAULT } = require('../constants');
+const { mergeDevices } = require('../../../../utils/device');
+
+/**
+ * @description Check if keyword matches value.
+ * @param {string} value - Value to check.
+ * @param {string} keyword - Keyword to match.
+ * @returns {boolean} True if keyword matches value.
+ * @example
+ * const res = zwaveManager.match('test', 'te');
+ */
+function match(value, keyword) {
+ return value ? value.toLowerCase().includes(keyword.toLowerCase()) : true;
+}
+
+/**
+ * @description Return array of Nodes.
+ * @param {object} filters - Filtering and ordering.
+ * @param {string} filters.orderDir - Filtering and ordering.
+ * @param {string} filters.search - Filtering and ordering.
+ * @param {string} filters.filterExisting - Filtering and ordering.
+ * @returns {Array} Return list of nodes.
+ * @example
+ * const nodes = zwaveManager.getNodes();
+ */
+function getNodes(
+ { orderDir, search, filterExisting } = {
+ orderDir: DEFAULT.NODES_ORDER_DIR,
+ search: null,
+ filterExisting: DEFAULT.NODES_FILTER_EXISTING,
+ },
+) {
+ const nodeIds = Object.keys(this.nodes);
+
+ // transform object in array
+ const nodes = nodeIds
+ .map((nodeId) => this.nodes[nodeId])
+ // .filter((node) => node.ready)
+ .flatMap((node) => splitNode(node));
+ return nodes
+ .filter((node) =>
+ search
+ ? match(node.name, search) ||
+ match(node.productLabel, search) ||
+ match(node.productDescription, search) ||
+ match(node.nodeId.toString(), search)
+ : true,
+ )
+ .map((node) => {
+ const newDevice = {
+ name: getDeviceName(node),
+ selector: slugify(`zwave-js-ui-node-${node.nodeId}-${getDeviceName(node)}`),
+ model: `${node.product} ${node.firmwareVersion}`,
+ service_id: this.serviceId,
+ external_id: getDeviceExternalId(node),
+ ready: node.ready,
+ features: [],
+ params: [
+ {
+ name: PARAMS.NODE_ID,
+ value: node.nodeId,
+ },
+ {
+ name: PARAMS.NODE_PRODUCT,
+ value: node.product,
+ },
+ {
+ name: PARAMS.NODE_ROOM,
+ value: node.loc,
+ },
+ {
+ name: PARAMS.NODE_CLASSES,
+ value: Object.keys(node.classes).join('-'),
+ },
+ ],
+ };
+
+ Object.entries(transformClasses(node)).forEach(([commandClassKey, commandClassValue]) => {
+ Object.entries(commandClassValue).forEach(([endpointKey, endpointValue]) => {
+ Object.entries(endpointValue).forEach(([propertyKey, propertyValue]) => {
+ const { property, genre, label, unit, commandClass, endpoint, writeable } = propertyValue;
+ let { min, max } = propertyValue;
+ const { value } = propertyValue;
+ if (genre === 'user') {
+ const { category, type, min: categoryMin, max: categoryMax, hasFeedback, prefLabel } = getCategory(node, {
+ commandClass,
+ endpoint,
+ property,
+ });
+ if (category !== 'unknown') {
+ if (categoryMin !== undefined) {
+ min = categoryMin;
+ }
+ if (categoryMax !== undefined) {
+ max = categoryMax;
+ }
+ const valueUnbind = unbindValue(
+ {
+ commandClass,
+ endpoint,
+ property,
+ fullProperty: property,
+ },
+ value,
+ );
+ if (min === undefined || max === undefined) {
+ logger.warn(
+ `Missing min/max for property ${property} of node ${node.nodeId}, product ${node.product}`,
+ );
+ }
+ newDevice.features.push({
+ name: getDeviceFeatureName({ label, prefLabel, endpoint }),
+ selector: slugify(`zwave-js-ui-node-${node.nodeId}-${property}-${commandClass}-${endpoint}-${label}`),
+ category,
+ type,
+ external_id: getDeviceFeatureExternalId({ nodeId: node.nodeId, commandClass, endpoint, property }),
+ read_only: !writeable,
+ unit: getUnit(unit),
+ has_feedback: hasFeedback,
+ min,
+ max,
+ last_value: valueUnbind,
+ });
+ } else {
+ logger.info(
+ `Unkown category/type for property ${JSON.stringify(propertyValue)} of node ${node.nodeId}, product ${
+ node.product
+ }`,
+ );
+ }
+ } else {
+ newDevice.params.push({
+ name: slugify(`${endpointKey}-${label}-${propertyValue.value_id}`),
+ value: propertyValue.value || '',
+ });
+ }
+ });
+ });
+ });
+
+ return newDevice;
+ })
+ .map((device) => {
+ const existingDevice = this.gladys.stateManager.get('deviceByExternalId', device.external_id);
+ // Merge with existing device.
+ return mergeDevices(device, existingDevice);
+ })
+ .filter((newDevice) => newDevice.features && newDevice.features.length > 0)
+ .filter((newDevice) => filterExisting === 'false' || newDevice.id === undefined)
+ .sort((a, b) => {
+ const aNodeId = a.params.find((param) => param.name === PARAMS.NODE_ID).value;
+ const bNodeId = b.params.find((param) => param.name === PARAMS.NODE_ID).value;
+ return orderDir === DEFAULT.NODES_ORDER_DIR ? aNodeId - bNodeId : bNodeId - aNodeId;
+ });
+}
+
+module.exports = {
+ getNodes,
+};
diff --git a/server/services/zwave-js-ui/lib/commands/getStatus.js b/server/services/zwave-js-ui/lib/commands/getStatus.js
new file mode 100644
index 0000000000..04b19be926
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/getStatus.js
@@ -0,0 +1,32 @@
+const logger = require('../../../../utils/logger');
+
+/**
+ * @description Getting Z-Wave status.
+ * @returns {object} Return Object of status.
+ * @example
+ * zwave.getStatus();
+ */
+function getStatus() {
+ logger.debug(`ZwaveJSUI : Getting status...`);
+
+ return {
+ ready: this.ready,
+
+ scanInProgress: this.scanInProgress,
+
+ mqttExist: this.mqttExist,
+ mqttRunning: this.mqttRunning,
+ mqttConnected: this.mqttConnected,
+
+ zwaveJSUIExist: this.zwaveJSUIExist,
+ zwaveJSUIRunning: this.zwaveJSUIRunning,
+ zwaveJSUIConnected: this.zwaveJSUIConnected,
+ usbConfigured: this.usbConfigured,
+
+ dockerBased: this.dockerBased,
+ };
+}
+
+module.exports = {
+ getStatus,
+};
diff --git a/server/services/zwave-js-ui/lib/commands/installMqttContainer.js b/server/services/zwave-js-ui/lib/commands/installMqttContainer.js
new file mode 100644
index 0000000000..62f11033d2
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/installMqttContainer.js
@@ -0,0 +1,132 @@
+const { promisify } = require('util');
+const cloneDeep = require('lodash.clonedeep');
+const { exec } = require('../../../../utils/childProcess');
+const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants');
+const containerDescriptor = require('../../docker/gladys-zwavejsui-mqtt-container.json');
+const logger = require('../../../../utils/logger');
+const { CONFIGURATION } = require('../constants');
+
+const sleep = promisify(setTimeout);
+
+/**
+ * @description Install and starts MQTT container.
+ * @example
+ * installMqttContainer();
+ */
+async function installMqttContainer() {
+ this.mqttRunning = false;
+ this.mqttExist = false;
+
+ const mqttUsername = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME, this.serviceId);
+ const mqttPassword = await this.gladys.variable.getValue(
+ CONFIGURATION.DEFAULT_ZWAVEJSUI_MQTT_PASSWORD,
+ this.serviceId,
+ );
+
+ let dockerContainers = await this.gladys.system.getContainers({
+ all: true,
+ filters: { name: [containerDescriptor.name] },
+ });
+ let [container] = dockerContainers;
+
+ if (dockerContainers.length === 0) {
+ let containerMqtt;
+ try {
+ logger.info('ZwaveJSUI MQTT is being installed as Docker container...');
+ logger.info(`Pulling ${containerDescriptor.Image} image...`);
+ await this.gladys.system.pull(containerDescriptor.Image);
+
+ const containerDescriptorToMutate = cloneDeep(containerDescriptor);
+
+ // Prepare broker env
+ logger.info(`Preparing ZwaveJSUI MQTT environment...`);
+ const { basePathOnContainer, basePathOnHost } = await this.gladys.system.getGladysBasePath();
+ const brokerEnv = await exec(
+ `sh ./services/zwave-js-ui/docker/gladys-zwavejsui-mqtt-env.sh ${basePathOnContainer}/zwave-js-ui`,
+ );
+ logger.info(`ZwaveJSUI MQTT configuration updated: ${brokerEnv}`);
+ containerDescriptorToMutate.HostConfig.Binds.push(
+ `${basePathOnHost}/zwave-js-ui/mosquitto/config:/mosquitto/config`,
+ );
+
+ logger.info(`Creating ZwaveJSUI MQTT container...`);
+ containerMqtt = await this.gladys.system.createContainer(containerDescriptorToMutate);
+ logger.info(`ZwaveJSUI MQTT successfully installed and configured as Docker container: ${containerMqtt}`);
+ this.mqttExist = true;
+ } catch (e) {
+ logger.error('ZwaveJSUI MQTT failed to install as Docker container:', e);
+ this.mqttExist = false;
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ throw e;
+ }
+
+ try {
+ // Container restart to initialize configuration
+ logger.info('ZwaveJSUI MQTT is starting...');
+ await this.gladys.system.restartContainer(containerMqtt.id);
+
+ // wait 5 seconds for the container to restart
+ await sleep(5 * 1000);
+ logger.info('ZwaveJSUI MQTT container successfully started');
+
+ // Copy password in broker container
+ logger.info(`Creating user/pass...`);
+ await this.gladys.system.exec(containerMqtt.id, {
+ Cmd: ['mosquitto_passwd', '-b', '/mosquitto/config/mosquitto.passwd', mqttUsername, mqttPassword],
+ });
+
+ // Container restart to inintialize users configuration
+ logger.info('ZwaveJSUI MQTT is restarting...');
+ await this.gladys.system.restartContainer(containerMqtt.id);
+ // wait 5 seconds for the container to restart
+ await sleep(5 * 1000);
+ logger.info('ZwaveJSUI MQTT container successfully started and configured');
+
+ this.mqttRunning = true;
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ } catch (e) {
+ logger.error('ZwaveJSUI MQTT container failed to start:', e);
+ this.mqttRunning = false;
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ throw e;
+ }
+ } else {
+ this.mqttExist = true;
+ try {
+ dockerContainers = await this.gladys.system.getContainers({
+ all: true,
+ filters: { name: [containerDescriptor.name] },
+ });
+ [container] = dockerContainers;
+ if (container.state !== 'running' && container.state !== 'restarting') {
+ logger.info('ZwaveJSUI MQTT is starting...');
+ await this.gladys.system.restartContainer(container.id);
+ // wait 5 seconds for the container to restart
+ await sleep(5 * 1000);
+ }
+
+ logger.info('ZwaveJSUI MQTT container successfully started');
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ this.mqttRunning = true;
+ } catch (e) {
+ logger.error('ZwaveJSUI MQTT container failed to start:', e);
+ this.mqttRunning = false;
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ throw e;
+ }
+ }
+}
+
+module.exports = {
+ installMqttContainer,
+};
diff --git a/server/services/zwave-js-ui/lib/commands/installZwaveJSUIContainer.js b/server/services/zwave-js-ui/lib/commands/installZwaveJSUIContainer.js
new file mode 100644
index 0000000000..247e027796
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/installZwaveJSUIContainer.js
@@ -0,0 +1,126 @@
+const cloneDeep = require('lodash.clonedeep');
+const { promisify } = require('util');
+const { exec } = require('../../../../utils/childProcess');
+const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants');
+const containerDescriptor = require('../../docker/gladys-zwavejsui-zwavejsui-container.json');
+const logger = require('../../../../utils/logger');
+const { CONFIGURATION } = require('../constants');
+
+const sleep = promisify(setTimeout);
+
+/**
+ * @description Install and starts ZwaveJSUI container.
+ * @example
+ * installZwaveJSUIContainer();
+ */
+async function installZwaveJSUIContainer() {
+ this.zwaveJSUIExist = false;
+ this.zwaveJSUIRunning = false;
+
+ const mqttUsername = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME, this.serviceId);
+ const mqttPassword = await this.gladys.variable.getValue(
+ CONFIGURATION.DEFAULT_ZWAVEJSUI_MQTT_PASSWORD,
+ this.serviceId,
+ );
+ const driverPath = await this.gladys.variable.getValue(CONFIGURATION.DRIVER_PATH, this.serviceId);
+ const s2UnauthenticatedKey = await this.gladys.variable.getValue(CONFIGURATION.S2_UNAUTHENTICATED, this.serviceId);
+ const s2AuthenticatedKey = await this.gladys.variable.getValue(CONFIGURATION.S2_AUTHENTICATED, this.serviceId);
+ const s2AccessControlKey = await this.gladys.variable.getValue(CONFIGURATION.S2_ACCESS_CONTROL, this.serviceId);
+ const s0LegacyKey = await this.gladys.variable.getValue(CONFIGURATION.S0_LEGACY, this.serviceId);
+
+ const { basePathOnContainer, basePathOnHost } = await this.gladys.system.getGladysBasePath();
+ let dockerContainers = await this.gladys.system.getContainers({
+ all: true,
+ filters: { name: [containerDescriptor.name] },
+ });
+ let [container] = dockerContainers;
+
+ if (dockerContainers.length === 0 || (container && container.state === 'created')) {
+ if (container && container.state === 'created') {
+ logger.info('ZwaveJSUI is already installed as Docker container...');
+ logger.info(`Removing ${container.id} container...`);
+ await this.gladys.system.removeContainer(container.id);
+ }
+
+ try {
+ logger.info('ZwaveJSUI is being installed as Docker container...');
+ logger.info(`Pulling ${containerDescriptor.Image} image...`);
+ await this.gladys.system.pull(containerDescriptor.Image);
+
+ const containerDescriptorToMutate = cloneDeep(containerDescriptor);
+
+ logger.info(`Preparing ZwaveJSUI environment...`);
+ logger.info(`Creating configuration file ${basePathOnHost}/zwave-js-ui/settings.json...`);
+ const brokerEnv = await exec(
+ `sh ./services/zwave-js-ui/docker/gladys-zwavejsui-zwavejsui-env.sh ${basePathOnContainer} ${mqttUsername} "${mqttPassword}" ${driverPath} "${s2UnauthenticatedKey}" "${s2AuthenticatedKey}" "${s2AccessControlKey}" "${s0LegacyKey}"`,
+ );
+ logger.info(`Configuration file ${basePathOnHost}/zwave-js-ui/settings.json created: ${brokerEnv}`);
+ containerDescriptorToMutate.HostConfig.Binds.push(`${basePathOnHost}/zwave-js-ui:/usr/src/app/store`);
+
+ containerDescriptorToMutate.HostConfig.Devices[0].PathOnHost = driverPath;
+
+ logger.info(`Creation of container...`);
+ const containerLog = await this.gladys.system.createContainer(containerDescriptorToMutate);
+ logger.info(`ZwaveJSUI successfully installed and configured as Docker container: ${containerLog}`);
+ this.zwaveJSUIExist = true;
+ } catch (e) {
+ this.zwaveJSUIExist = false;
+ logger.error('ZwaveJSUI failed to install as Docker container:', e);
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ throw e;
+ }
+ }
+
+ try {
+ dockerContainers = await this.gladys.system.getContainers({
+ all: true,
+ filters: { name: [containerDescriptor.name] },
+ });
+ [container] = dockerContainers;
+ if (container.state !== 'running') {
+ logger.info('ZwaveJSUI container is starting...');
+ await this.gladys.system.restartContainer(container.id);
+
+ // wait 5 seconds for the container to restart
+ await sleep(5 * 1000);
+ }
+
+ this.zwaveJSUIExist = true;
+
+ // Check if config is up-to-date
+ const devices = await this.gladys.system.getContainerDevices(container.id);
+ if (devices.length === 0 || devices[0].PathOnHost !== driverPath) {
+ // Update ZwaveJSUI env
+ logger.info(`Updating ZwaveJSUI environment...`);
+ const brokerEnv = await exec(
+ `sh ./services/zwave-js-ui/docker/gladys-zwavejsui-zwavejsui-env.sh ${basePathOnContainer} ${mqttUsername} "${mqttPassword}" ${driverPath} "${s2UnauthenticatedKey}" "${s2AuthenticatedKey}" "${s2AccessControlKey}" "${s0LegacyKey}"`,
+ );
+ logger.info(`ZwaveJSUI configuration updated: ${brokerEnv}`);
+
+ logger.info('ZwaveJSUI container is restarting...');
+ await this.gladys.system.restartContainer(container.id);
+
+ // wait 5 seconds for the container to restart
+ await sleep(5 * 1000);
+ }
+
+ logger.info('ZwaveJSUI container successfully started');
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ this.zwaveJSUIRunning = true;
+ } catch (e) {
+ logger.error('ZwaveJSUI container failed to start:', e);
+ this.zwaveJSUIRunning = false;
+ this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ throw e;
+ }
+}
+
+module.exports = {
+ installZwaveJSUIContainer,
+};
diff --git a/server/services/zwave-js-ui/lib/commands/removeNode.js b/server/services/zwave-js-ui/lib/commands/removeNode.js
new file mode 100644
index 0000000000..9f5271492f
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/removeNode.js
@@ -0,0 +1,22 @@
+const logger = require('../../../../utils/logger');
+const { DEFAULT } = require('../constants');
+
+/**
+ * @description Add node.
+ * @example
+ * zwave.removeNode();
+ */
+function removeNode() {
+ logger.debug(`Zwave : Entering exclusion mode`);
+
+ this.mqttClient.publish(`${this.mqttTopicPrefix}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/startExclusion/set`);
+
+ setTimeout(() => {
+ this.mqttClient.publish(`${this.mqttTopicPrefix}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/stopExclusion/set`);
+ this.scanNetwork();
+ }, DEFAULT.REMOVE_NODE_TIMEOUT);
+}
+
+module.exports = {
+ removeNode,
+};
diff --git a/server/services/zwave-js-ui/lib/commands/scanNetwork.js b/server/services/zwave-js-ui/lib/commands/scanNetwork.js
new file mode 100644
index 0000000000..6aedda5257
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/scanNetwork.js
@@ -0,0 +1,20 @@
+const logger = require('../../../../utils/logger');
+const { DEFAULT } = require('../constants');
+
+/**
+ * @description Scan ZWave Network.
+ * @example
+ * zwave.scanNetwork();
+ */
+function scanNetwork() {
+ logger.info(`Zwave : Scanning network`);
+
+ this.mqttClient.publish(`${this.mqttTopicPrefix}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/getNodes/set`, 'true');
+ this.scanInProgress = true;
+
+ setTimeout(this.scanComplete.bind(this), DEFAULT.SCAN_NETWORK_TIMEOUT);
+}
+
+module.exports = {
+ scanNetwork,
+};
diff --git a/server/services/zwave-js-ui/lib/commands/setValue.js b/server/services/zwave-js-ui/lib/commands/setValue.js
new file mode 100644
index 0000000000..93ce371494
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/setValue.js
@@ -0,0 +1,28 @@
+const logger = require('../../../../utils/logger');
+const { bindValue } = require('../utils/bindValue');
+const { getNodeInfoByExternalId } = require('../utils/externalId');
+
+/**
+ * @description Set value.
+ * @param {object} device - The device to control.
+ * @param {object} deviceFeature - The device feature to control.
+ * @param {number} value - The value to set.
+ * @example
+ * zwave.setValue();
+ */
+function setValue(device, deviceFeature, value) {
+ const { nodeId, commandClass, endpoint, property, propertyKey } = getNodeInfoByExternalId(deviceFeature.external_id);
+ logger.debug(`Zwave : Setting value for feature ${deviceFeature.name} of device ${nodeId}: ${value}`);
+ const zwaveValue = bindValue({ nodeId, commandClass, endpoint, property, propertyKey }, value);
+
+ this.mqttClient.publish(
+ `${this.mqttTopicPrefix}/${
+ this.mqttTopicWithLocation ? `${this.nodes[nodeId].loc}/` : ''
+ }nodeID_${nodeId}/${commandClass}/${endpoint}/${property}${propertyKey !== undefined ? `/${propertyKey}` : ''}/set`,
+ zwaveValue.toString(),
+ );
+}
+
+module.exports = {
+ setValue,
+};
diff --git a/server/services/zwave-js-ui/lib/commands/updateConfiguration.js b/server/services/zwave-js-ui/lib/commands/updateConfiguration.js
new file mode 100644
index 0000000000..28a8a3d155
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/commands/updateConfiguration.js
@@ -0,0 +1,111 @@
+const logger = require('../../../../utils/logger');
+const { CONFIGURATION, DEFAULT } = require('../constants');
+
+/**
+ * @description Update Z-Wave configuration.
+ * @param {object} configuration - The configuration data.
+ * @example
+ * zwave.updateConfiguration({ driverPath: '' });
+ */
+async function updateConfiguration(configuration) {
+ logger.debug(`Zwave : Updating configuration...`);
+
+ const {
+ externalZwaveJSUI,
+ driverPath,
+ mqttUrl,
+ mqttUsername,
+ mqttPassword,
+ mqttTopicPrefix,
+ mqttTopicWithLocation,
+ s2UnauthenticatedKey,
+ s2AuthenticatedKey,
+ s2AccessControlKey,
+ s0LegacyKey,
+ } = configuration;
+
+ if (externalZwaveJSUI !== undefined) {
+ await this.gladys.variable.setValue(
+ CONFIGURATION.EXTERNAL_ZWAVEJSUI,
+ externalZwaveJSUI ? '1' : '0',
+ this.serviceId,
+ );
+ }
+
+ if (!externalZwaveJSUI) {
+ await this.gladys.variable.setValue(
+ CONFIGURATION.ZWAVEJSUI_MQTT_URL,
+ DEFAULT.ZWAVEJSUI_MQTT_URL_VALUE,
+ this.serviceId,
+ );
+ await this.gladys.variable.setValue(
+ CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME,
+ DEFAULT.ZWAVEJSUI_MQTT_USERNAME_VALUE,
+ this.serviceId,
+ );
+
+ const defaultMqttPassword = await this.gladys.variable.getValue(
+ CONFIGURATION.DEFAULT_ZWAVEJSUI_MQTT_PASSWORD,
+ this.serviceId,
+ );
+ await this.gladys.variable.setValue(CONFIGURATION.ZWAVEJSUI_MQTT_PASSWORD, defaultMqttPassword, this.serviceId);
+
+ if (driverPath) {
+ await this.gladys.variable.setValue(CONFIGURATION.DRIVER_PATH, driverPath, this.serviceId);
+ }
+
+ if (s2UnauthenticatedKey) {
+ await this.gladys.variable.setValue(CONFIGURATION.S2_UNAUTHENTICATED, s2UnauthenticatedKey, this.serviceId);
+ }
+
+ if (s2AuthenticatedKey) {
+ await this.gladys.variable.setValue(CONFIGURATION.S2_AUTHENTICATED, s2AuthenticatedKey, this.serviceId);
+ }
+
+ if (s2AccessControlKey) {
+ await this.gladys.variable.setValue(CONFIGURATION.S2_ACCESS_CONTROL, s2AccessControlKey, this.serviceId);
+ }
+
+ if (s0LegacyKey) {
+ await this.gladys.variable.setValue(CONFIGURATION.S0_LEGACY, s0LegacyKey, this.serviceId);
+ }
+ }
+
+ if (externalZwaveJSUI) {
+ if (mqttUrl) {
+ await this.gladys.variable.setValue(CONFIGURATION.ZWAVEJSUI_MQTT_URL, mqttUrl, this.serviceId);
+ }
+
+ if (mqttUsername) {
+ await this.gladys.variable.setValue(CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME, mqttUsername, this.serviceId);
+ }
+
+ if (mqttUsername === '') {
+ await this.gladys.variable.destroy(CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME, this.serviceId);
+ }
+
+ if (mqttPassword) {
+ await this.gladys.variable.setValue(CONFIGURATION.ZWAVEJSUI_MQTT_PASSWORD, mqttPassword, this.serviceId);
+ }
+
+ if (mqttPassword === '') {
+ await this.gladys.variable.destroy(CONFIGURATION.ZWAVEJSUI_MQTT_PASSWORD, this.serviceId);
+ }
+
+ if (mqttTopicPrefix) {
+ await this.gladys.variable.setValue(CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_PREFIX, mqttTopicPrefix, this.serviceId);
+ }
+
+ if (mqttTopicWithLocation !== undefined) {
+ await this.gladys.variable.setValue(
+ CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_WITH_LOCATION,
+ mqttTopicWithLocation ? '1' : '0',
+ this.serviceId,
+ );
+ }
+ }
+}
+
+module.exports = {
+ updateConfiguration,
+};
diff --git a/server/services/zwave-js-ui/lib/constants.js b/server/services/zwave-js-ui/lib/constants.js
new file mode 100644
index 0000000000..0fdf5067e7
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/constants.js
@@ -0,0 +1,431 @@
+const {
+ DEVICE_FEATURE_CATEGORIES,
+ DEVICE_FEATURE_TYPES,
+ BUTTON_STATUS,
+ DEVICE_FEATURE_MINMAX_BY_TYPE,
+ STATE,
+ DEVICE_FEATURE_UNITS,
+} = require('../../../utils/constants');
+
+const COMMAND_CLASSES = {
+ COMMAND_CLASS_ANTITHEFT: 93,
+ COMMAND_CLASS_APPLICATION_CAPABILITY: 87,
+ COMMAND_CLASS_APPLICATION_STATUS: 34,
+ COMMAND_CLASS_ASSOCIATION: 133,
+ COMMAND_CLASS_ASSOCIATION_COMMAND_CONFIGURATION: 155,
+ COMMAND_CLASS_ASSOCIATION_GRP_INFO: 89,
+ COMMAND_CLASS_BARRIER_OPERATOR: 102,
+ COMMAND_CLASS_BASIC: 32,
+ COMMAND_CLASS_BASIC_TARIFF_INFO: 54,
+ COMMAND_CLASS_BASIC_WINDOW_COVERING: 80,
+ COMMAND_CLASS_BATTERY: 128,
+ COMMAND_CLASS_CENTRAL_SCENE: 91,
+ COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE: 70,
+ COMMAND_CLASS_CLOCK: 129,
+ COMMAND_CLASS_CONFIGURATION: 112,
+ COMMAND_CLASS_CONTROLLER_REPLICATION: 33,
+ COMMAND_CLASS_CRC_16_ENCAP: 86,
+ COMMAND_CLASS_DCP_CONFIG: 58,
+ COMMAND_CLASS_DCP_MONITOR: 59,
+ COMMAND_CLASS_DEVICE_RESET_LOCALLY: 90,
+ COMMAND_CLASS_DOOR_LOCK: 98,
+ COMMAND_CLASS_DOOR_LOCK_LOGGING: 76,
+ COMMAND_CLASS_ENERGY_PRODUCTION: 144,
+ COMMAND_CLASS_ENTRY_CONTROL: 111,
+ COMMAND_CLASS_FIRMWARE_UPDATE_MD: 122,
+ COMMAND_CLASS_GEOGRAPHIC_LOCATION: 140,
+ COMMAND_CLASS_GROUPING_NAME: 123,
+ COMMAND_CLASS_HAIL: 130,
+ COMMAND_CLASS_HRV_CONTROL: 57,
+ COMMAND_CLASS_HRV_STATUS: 55,
+ COMMAND_CLASS_HUMIDITY_CONTROL_MODE: 109,
+ COMMAND_CLASS_HUMIDITY_CONTROL_OPERATING_STATE: 110,
+ COMMAND_CLASS_HUMIDITY_CONTROL_SETPOINT: 100,
+ COMMAND_CLASS_INDICATOR: 135,
+ COMMAND_CLASS_IP_ASSOCIATION: 92,
+ COMMAND_CLASS_IP_CONFIGURATION: 14,
+ COMMAND_CLASS_IRRIGATION: 107,
+ COMMAND_CLASS_LANGUAGE: 137,
+ COMMAND_CLASS_LOCK: 118,
+ COMMAND_CLASS_MAILBOX: 105,
+ COMMAND_CLASS_MANUFACTURER_PROPRIETARY: 145,
+ COMMAND_CLASS_MANUFACTURER_SPECIFIC: 114,
+ COMMAND_CLASS_MARK: 239,
+ COMMAND_CLASS_METER: 50,
+ COMMAND_CLASS_METER_PULSE: 53,
+ COMMAND_CLASS_METER_TBL_CONFIG: 60,
+ COMMAND_CLASS_METER_TBL_MONITOR: 61,
+ COMMAND_CLASS_METER_TBL_PUSH: 62,
+ COMMAND_CLASS_MTP_WINDOW_COVERING: 81,
+ COMMAND_CLASS_MULTI_CHANNEL: 96,
+ COMMAND_CLASS_MULTI_CHANNEL_ASSOCIATION: 142,
+ COMMAND_CLASS_MULTI_COMMAND: 143,
+ COMMAND_CLASS_NETWORK_MANAGEMENT_BASIC: 77,
+ COMMAND_CLASS_NETWORK_MANAGEMENT_INCLUSION: 52,
+ COMMAND_CLASS_NETWORK_MANAGEMENT_PRIMARY: 84,
+ COMMAND_CLASS_NETWORK_MANAGEMENT_PROXY: 82,
+ COMMAND_CLASS_NO_OPERATION: 0,
+ COMMAND_CLASS_NODE_NAMING: 119,
+ COMMAND_CLASS_NON_INTEROPERABLE: 240,
+ COMMAND_CLASS_NOTIFICATION: 113,
+ COMMAND_CLASS_POWERLEVEL: 115,
+ COMMAND_CLASS_PREPAYMENT: 63,
+ COMMAND_CLASS_PREPAYMENT_ENCAPSULATION: 65,
+ COMMAND_CLASS_PROPRIETARY: 136,
+ COMMAND_CLASS_PROTECTION: 117,
+ COMMAND_CLASS_RATE_TBL_CONFIG: 72,
+ COMMAND_CLASS_RATE_TBL_MONITOR: 73,
+ COMMAND_CLASS_REMOTE_ASSOCIATION_ACTIVATE: 124,
+ COMMAND_CLASS_REMOTE_ASSOCIATION: 125,
+ COMMAND_CLASS_SCENE_ACTIVATION: 43,
+ COMMAND_CLASS_SCENE_ACTUATOR_CONF: 44,
+ COMMAND_CLASS_SCENE_CONTROLLER_CONF: 45,
+ COMMAND_CLASS_SCHEDULE: 83,
+ COMMAND_CLASS_SCHEDULE_ENTRY_LOCK: 78,
+ COMMAND_CLASS_SCREEN_ATTRIBUTES: 147,
+ COMMAND_CLASS_SCREEN_MD: 146,
+ COMMAND_CLASS_SECURITY: 152,
+ COMMAND_CLASS_SECURITY_SCHEME0_MARK: 61696,
+ COMMAND_CLASS_SENSOR_ALARM: 156,
+ COMMAND_CLASS_SENSOR_BINARY: 48,
+ COMMAND_CLASS_SENSOR_CONFIGURATION: 158,
+ COMMAND_CLASS_SENSOR_MULTILEVEL: 49,
+ COMMAND_CLASS_SILENCE_ALARM: 157,
+ COMMAND_CLASS_SIMPLE_AV_CONTROL: 148,
+ COMMAND_CLASS_SUPERVISION: 108,
+ COMMAND_CLASS_SWITCH_ALL: 39,
+ COMMAND_CLASS_SWITCH_BINARY: 37,
+ COMMAND_CLASS_SWITCH_COLOR: 51,
+ COMMAND_CLASS_SWITCH_MULTILEVEL: 38,
+ COMMAND_CLASS_SWITCH_TOGGLE_BINARY: 40,
+ COMMAND_CLASS_SWITCH_TOGGLE_MULTILEVEL: 41,
+ COMMAND_CLASS_TARIFF_TBL_CONFIG: 74,
+ COMMAND_CLASS_TARIFF_TBL_MONITOR: 75,
+ COMMAND_CLASS_THERMOSTAT_FAN_MODE: 68,
+ COMMAND_CLASS_THERMOSTAT_FAN_STATE: 69,
+ COMMAND_CLASS_THERMOSTAT_MODE: 64,
+ COMMAND_CLASS_THERMOSTAT_OPERATING_STATE: 66,
+ COMMAND_CLASS_THERMOSTAT_SETBACK: 71,
+ COMMAND_CLASS_THERMOSTAT_SETPOINT: 67,
+ COMMAND_CLASS_TIME: 138,
+ COMMAND_CLASS_TIME_PARAMETERS: 139,
+ COMMAND_CLASS_TRANSPORT_SERVICE: 85,
+ COMMAND_CLASS_USER_CODE: 99,
+ COMMAND_CLASS_VERSION: 134,
+ COMMAND_CLASS_WAKE_UP: 132,
+ COMMAND_CLASS_ZIP: 35,
+ COMMAND_CLASS_ZIP_NAMING: 104,
+ COMMAND_CLASS_ZIP_ND: 88,
+ COMMAND_CLASS_ZIP_6LOWPAN: 79,
+ COMMAND_CLASS_ZIP_GATEWAY: 95,
+ COMMAND_CLASS_ZIP_PORTAL: 97,
+ COMMAND_CLASS_ZWAVEPLUS_INFO: 94,
+ COMMAND_CLASS_WINDOW_COVERING: 106,
+};
+
+const PROPERTIES = {
+ CURRENT_VALUE: 'currentValue',
+ TARGET_VALUE: 'targetValue',
+ ELECTRIC_VOLTAGE: 'value-66561',
+ ELECTRIC_CURRENT: 'value-66817',
+ ELECTRIC_W: 'value-66048',
+ ELECTRIC_CONSUMED_W: 'value-66049',
+ ELECTRIC_CONSUMED_KWH: 'value-65537',
+ AIR_TEMPERATURE: 'Air_temperature',
+ HUMIDITY: 'Humidity',
+ ILLUMINANCE: 'Illuminance',
+ ULTRAVIOLET: 'Ultraviolet',
+ MOTION: 'Motion',
+ ANY: 'Any',
+ SMOKE_ALARM: 'Smoke_Alarm-Sensor_status',
+ SLOW_REFRESH: 'slowRefresh',
+ SCENE_001: 'scene-001',
+ SCENE_002: 'scene-002',
+ SCENE_003: 'scene-003',
+ SCENE_004: 'scene-004',
+ SCENE_005: 'scene-005',
+ SCENE_006: 'scene-006',
+ BATTERY_LEVEL: 'level',
+ CURRENT_COLOR: 'currentColor',
+ TARGET_COLOR: 'targetColor',
+ HEX_COLOR: 'hexColor',
+ POWER: 'Power',
+ MOTION_ALARM: 'Home_Security-Motion_sensor_status',
+};
+
+const ENDPOINTS = {
+ TARGET_COLOR: 0,
+};
+
+const PREF_LABELS = {
+ MOTION_SENSOR: {
+ en: 'Motion sensor',
+ fr: 'Détecteur de présence',
+ },
+ LIGHT_TEMPERATURE: {
+ en: 'Temperature',
+ fr: 'Température',
+ },
+};
+
+const CATEGORIES = [
+ // switch binary
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.SWITCH,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_SWITCH_BINARY],
+ PROPERTIES: [PROPERTIES.TARGET_VALUE],
+ TYPE: DEVICE_FEATURE_TYPES.SWITCH.BINARY,
+ MIN: 0,
+ MAX: 1,
+ },
+ // scene switch
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.BUTTON,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_CENTRAL_SCENE],
+ PROPERTIES: [
+ PROPERTIES.SCENE_001,
+ PROPERTIES.SCENE_002,
+ PROPERTIES.SCENE_003,
+ PROPERTIES.SCENE_004,
+ PROPERTIES.SCENE_005,
+ PROPERTIES.SCENE_006,
+ ],
+ TYPE: DEVICE_FEATURE_TYPES.BUTTON.CLICK,
+ MIN: 1,
+ MAX: BUTTON_STATUS.LONG_CLICK,
+ },
+ // dimmer binary
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.SWITCH,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_SWITCH_MULTILEVEL],
+ PROPERTIES: [PROPERTIES.TARGET_VALUE],
+ TYPE: DEVICE_FEATURE_TYPES.SWITCH.DIMMER,
+ },
+ // color light
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.LIGHT,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_SWITCH_COLOR],
+ PROPERTIES: [PROPERTIES.HEX_COLOR],
+ TYPE: DEVICE_FEATURE_TYPES.LIGHT.COLOR,
+ MIN: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.LIGHT.COLOR].MIN,
+ MAX: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.LIGHT.COLOR].MAX,
+ },
+ // color temperature light
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.LIGHT,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_SWITCH_COLOR],
+ PROPERTIES: [`${PROPERTIES.TARGET_COLOR}-${ENDPOINTS.TARGET_COLOR}`],
+ INDEXES: [ENDPOINTS.TARGET_COLOR], // = Warm white (assume Cold white exist)
+ TYPE: DEVICE_FEATURE_TYPES.LIGHT.TEMPERATURE,
+ LABEL: PREF_LABELS.LIGHT_TEMPERATURE.fr,
+ MIN: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.LIGHT.TEMPERATURE].MIN,
+ MAX: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.LIGHT.TEMPERATURE].MAX,
+ UNIT: DEVICE_FEATURE_UNITS.PERCENT,
+ },
+ // switch energy meter
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.SWITCH,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_METER],
+ PROPERTIES: [PROPERTIES.ELECTRIC_W, PROPERTIES.ELECTRIC_CONSUMED_KWH],
+ TYPE: DEVICE_FEATURE_TYPES.SWITCH.ENERGY,
+ MIN: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.SWITCH.ENERGY].MIN,
+ MAX: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.SWITCH.ENERGY].MAX,
+ },
+ // switch power meter
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.SWITCH,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_METER],
+ PROPERTIES: [PROPERTIES.ELECTRIC_CONSUMED_W],
+ TYPE: DEVICE_FEATURE_TYPES.SWITCH.POWER,
+ MIN: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.SWITCH.POWER].MIN,
+ MAX: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.SWITCH.POWER].MAX,
+ },
+ // dimmer power meter
+ /* {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.SWITCH,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_SENSOR_MULTILEVEL],
+ PROPERTIES: [PROPERTIES.POWER],
+ TYPE: DEVICE_FEATURE_TYPES.SWITCH.POWER,
+ MIN: 0,
+ MAX: Number.MAX_VALUE,
+ }, */
+ // switch voltage
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.SWITCH,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_METER],
+ TYPE: DEVICE_FEATURE_TYPES.SWITCH.VOLTAGE,
+ PROPERTIES: [PROPERTIES.ELECTRIC_VOLTAGE],
+ MIN: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.SWITCH.VOLTAGE].MIN,
+ MAX: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.SWITCH.VOLTAGE].MAX,
+ },
+ // switch current
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.SWITCH,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_METER],
+ TYPE: DEVICE_FEATURE_TYPES.SWITCH.CURRENT,
+ PROPERTIES: [PROPERTIES.ELECTRIC_CURRENT],
+ MIN: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.SWITCH.CURRENT].MIN,
+ MAX: DEVICE_FEATURE_MINMAX_BY_TYPE[DEVICE_FEATURE_TYPES.SWITCH.CURRENT].MAX,
+ },
+ // temperature sensor
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.TEMPERATURE_SENSOR,
+ TYPE: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_SENSOR_MULTILEVEL],
+ PROPERTIES: [PROPERTIES.AIR_TEMPERATURE],
+ MIN: -30,
+ MAX: 50,
+ },
+ // humidity sensor
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.HUMIDITY_SENSOR,
+ TYPE: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_SENSOR_MULTILEVEL],
+ PROPERTIES: [PROPERTIES.HUMIDITY],
+ MIN: 0,
+ MAX: 100,
+ },
+ // ultraviolet sensor
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.UV_SENSOR,
+ TYPE: DEVICE_FEATURE_TYPES.SENSOR.INTEGER,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_SENSOR_MULTILEVEL],
+ PROPERTIES: [PROPERTIES.ULTRAVIOLET],
+ MIN: 0,
+ MAX: 100,
+ },
+ // battery
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.BATTERY,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_BATTERY],
+ TYPE: DEVICE_FEATURE_TYPES.BATTERY.INTEGER,
+ PROPERTIES: [PROPERTIES.BATTERY_LEVEL],
+ MIN: 0,
+ MAX: 100,
+ },
+ // light sensor
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.LIGHT_SENSOR,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_SENSOR_MULTILEVEL],
+ PROPERTIES: [PROPERTIES.ILLUMINANCE],
+ TYPE: DEVICE_FEATURE_TYPES.SENSOR.INTEGER,
+ MIN: 0,
+ MAX: 100,
+ },
+ // motion sensor
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.MOTION_SENSOR,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_SENSOR_BINARY],
+ PROPERTIES: [PROPERTIES.MOTION, PROPERTIES.ANY],
+ LABEL: PREF_LABELS.MOTION_SENSOR.fr,
+ TYPE: DEVICE_FEATURE_TYPES.SENSOR.BINARY,
+ MIN: 0,
+ MAX: 1,
+ },
+ // motion sensor - notification
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.MOTION_SENSOR,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION],
+ PROPERTIES: [PROPERTIES.MOTION_ALARM],
+ LABEL: PREF_LABELS.MOTION_SENSOR.fr,
+ TYPE: DEVICE_FEATURE_TYPES.SENSOR.BINARY,
+ },
+ // smoke sensor
+ {
+ CATEGORY: DEVICE_FEATURE_CATEGORIES.SMOKE_SENSOR,
+ COMMAND_CLASSES: [COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION],
+ PROPERTIES: [PROPERTIES.SMOKE_ALARM],
+ TYPE: DEVICE_FEATURE_TYPES.SENSOR.BINARY,
+ },
+];
+
+const GENRE = {
+ 112: 'config', // COMMAND_CLASS_CONFIGURATION
+ 114: 'system', // COMMAND_CLASS_MANUFACTURER_SPECIFIC
+ 115: 'system', // COMMAND_CLASS_POWERLEVEL
+ 119: 'system', // Location
+ 132: 'system', // COMMAND_CLASS_WAKE_UP
+ 134: 'system', // COMMAND_CLASS_VERSION
+ 94: 'system', // COMMAND_CLASS_ZWAVEPLUS_INFO
+ 44: 'config', // COMMAND_CLASS_SCENE_ACTUATOR_CONF
+ 32: 'notsupported', // COMMAND_CLASS_BASIC
+ 135: 'notsupported', // COMMAND_CLASS_INDICATOR
+};
+
+const SCENE_VALUES = {
+ 0: BUTTON_STATUS.CLICK,
+ 3: BUTTON_STATUS.DOUBLE_CLICK,
+ 2: BUTTON_STATUS.LONG_CLICK_PRESS,
+ 1: BUTTON_STATUS.LONG_CLICK_RELEASE,
+};
+
+const SMOKE_ALARM_VALUES = {
+ 0: STATE.OFF,
+ 2: STATE.ON,
+};
+
+const NODE_STATES = {
+ ALIVE: 'Alive',
+ DEAD: 'Dead',
+ ASLEEP: 'Asleep',
+ WAKE_UP: 'wakeUp',
+};
+
+const CONFIGURATION = {
+ EXTERNAL_ZWAVEJSUI: 'EXTERNAL_ZWAVEJSUI',
+ ZWAVEJSUI_MQTT_URL: 'ZWAVEJSUI_MQTT_URL',
+ ZWAVEJSUI_MQTT_USERNAME: 'ZWAVEJSUI_MQTT_USERNAME',
+ ZWAVEJSUI_MQTT_PASSWORD: 'ZWAVEJSUI_MQTT_PASSWORD',
+ DEFAULT_ZWAVEJSUI_MQTT_PASSWORD: 'DEFAULT_ZWAVEJSUI_MQTT_PASSWORD',
+ ZWAVEJSUI_MQTT_TOPIC_PREFIX: 'ZWAVEJSUI_MQTT_TOPIC_PREFIX',
+ ZWAVEJSUI_MQTT_TOPIC_WITH_LOCATION: 'ZWAVEJSUI_MQTT_TOPIC_WITH_LOCATION',
+ DRIVER_PATH: 'DRIVER_PATH',
+ S2_UNAUTHENTICATED: 'S2_UNAUTHENTICATED',
+ S2_AUTHENTICATED: 'S2_AUTHENTICATED',
+ S2_ACCESS_CONTROL: 'S2_ACCESS_CONTROL',
+ S0_LEGACY: 'S0_LEGACY',
+};
+
+const DEFAULT = {
+ EXTERNAL_ZWAVEJSUI: false,
+ ZWAVEJSUI_MQTT_TOPIC_PREFIX: 'zwave-js-ui',
+ ZWAVEJSUI_MQTT_URL_VALUE: 'mqtt://localhost:1885',
+ ZWAVEJSUI_MQTT_USERNAME_VALUE: 'gladys',
+ MQTT_CLIENT_ID: 'gladys-main-instance',
+ ZWAVEJSUI_CLIENT_ID: process.env.NODE_ENV === 'production' ? 'ZWAVE_GATEWAY-Gladys' : 'ZWAVE_GATEWAY-Gladys-dev',
+ ZWAVEJSUI_VERSION_EXPECTED: '8.18.1',
+ ADD_NODE_TIMEOUT: 60 * 1000,
+ REMOVE_NODE_TIMEOUT: 60 * 1000,
+ SCAN_NETWORK_TIMEOUT: 60 * 1000,
+ SCAN_NETWORK_RETRY_TIMEOUT: 10 * 1000,
+ NODES_ORDER_DIR: 'asc',
+ NODES_FILTER_EXISTING: 'true',
+};
+
+const PRODUCT = {
+ FIBARO_DIMMER2: '271-4096-258',
+};
+
+const PARAMS = {
+ NODE_ID: 'node-id',
+ NODE_PRODUCT: 'node-product',
+ NODE_ROOM: 'node-room',
+ NODE_CLASSES: 'node-classes',
+};
+
+module.exports = {
+ COMMAND_CLASSES,
+ PROPERTIES,
+ ENDPOINTS,
+ CATEGORIES,
+ GENRE,
+ UNKNOWN_CATEGORY: DEVICE_FEATURE_CATEGORIES.UNKNOWN,
+ UNKNOWN_TYPE: DEVICE_FEATURE_TYPES.SENSOR.UNKNOWN,
+ SCENE_VALUES,
+ SMOKE_ALARM_VALUES,
+ NODE_STATES,
+ CONFIGURATION,
+ DEFAULT,
+ PRODUCT,
+ PARAMS,
+};
diff --git a/server/services/zwave-js-ui/lib/events/handleMqttMessage.js b/server/services/zwave-js-ui/lib/events/handleMqttMessage.js
new file mode 100644
index 0000000000..faaefea78b
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/events/handleMqttMessage.js
@@ -0,0 +1,173 @@
+const { WEBSOCKET_MESSAGE_TYPES, EVENTS } = require('../../../../utils/constants');
+const logger = require('../../../../utils/logger');
+const { DEFAULT, COMMAND_CLASSES, GENRE } = require('../constants');
+
+/**
+ * @description Escape a property name receive in MQTT.
+ * @param {string} property - Property name.
+ * @returns {object} Escaped property name.
+ * @example
+ * escapeProperty('unknwon (0x1c)');
+ */
+function escapeProperty(property) {
+ return property
+ .replace(/ /g, '_')
+ .replace(/\(/g, '')
+ .replace(/\)/g, '');
+}
+
+/**
+ * @description Handle a new message receive in MQTT.
+ * @param {string} topic - MQTT topic.
+ * @param {object} message - The message sent.
+ * @returns {object} Null.
+ * @example
+ * handleMqttMessage('zwave-js-ui/POWER', 'ON');
+ */
+function handleMqttMessage(topic, message) {
+ switch (topic) {
+ case `${this.mqttTopicPrefix}/driver/status`: {
+ const newStatus = message === 'true';
+ logger.debug(`Driver status ${newStatus}, was ${this.zwaveJSUIConnected}`);
+ if (newStatus !== this.zwaveJSUIConnected) {
+ this.zwaveJSUIConnected = newStatus;
+ this.eventManager.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ }
+ break;
+ }
+ case `${this.mqttTopicPrefix}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/version`: {
+ this.zwaveJSUIVersion = JSON.parse(message).value;
+ this.eventManager.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ break;
+ }
+ case `${this.mqttTopicPrefix}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/getNodes`: {
+ const { success, result } = message instanceof Object ? message : JSON.parse(message);
+ if (success) {
+ logger.info(`Receive nodes [${result.length}]...`);
+ this.nodes = {};
+ result.forEach((data) => {
+ const node = {
+ nodeId: data.id,
+ classes: {},
+ values: {},
+ ...data,
+ };
+
+ this.nodes[data.id] = node;
+
+ this.nodeReady(node);
+ Object.keys(node.values)
+ .filter((valueId) => !valueId.startsWith(COMMAND_CLASSES.COMMAND_CLASS_BASIC.toString()))
+ .forEach((valueId) => {
+ const value = node.values[valueId];
+ if (value.property) {
+ value.property = escapeProperty(value.property.toString());
+ } else {
+ value.property = escapeProperty(value.propertyName);
+ }
+ delete value.propertyName;
+ value.propertyKey = value.propertyKey ? escapeProperty(`${value.propertyKey}`) : undefined;
+
+ this.valueAdded(
+ {
+ id: data.id,
+ },
+ value,
+ );
+ });
+
+ // Clean node
+ delete node.id;
+ delete node.values;
+ delete node.groups;
+ delete node.deviceConfig;
+ });
+
+ this.scanComplete();
+ } else {
+ logger.warn(`Error getting nodes, retry in ${DEFAULT.SCAN_NETWORK_RETRY_TIMEOUT}ms`);
+ setTimeout(this.scanNetwork.bind(this), DEFAULT.SCAN_NETWORK_RETRY_TIMEOUT);
+ }
+ break;
+ }
+ default: {
+ // //////
+ const splittedTopic = topic.split('/');
+ if (splittedTopic[1] === '_CLIENTS') {
+ // Nothing to do
+ } else if (splittedTopic[1] === '_EVENTS') {
+ // Nothing to do
+ } else if (splittedTopic[2] === 'status') {
+ break;
+ } else if (splittedTopic[2] === 'nodeinfo') {
+ break;
+ } else if (splittedTopic.length >= 5) {
+ splittedTopic.shift();
+ if (this.mqttTopicWithLocation) {
+ splittedTopic.shift();
+ }
+ const [nodeId, commandClass, endpoint, propertyName, propertyKey] = splittedTopic;
+ if (propertyKey === 'set') {
+ // logger.debug(`ZwaveJSUI set. Bypass message.`);
+ break;
+ }
+ if (GENRE[commandClass * 1] !== undefined) {
+ // logger.debug(`ZwaveJSUI command class not supported. Bypass message.`);
+ break;
+ }
+ const id = nodeId.split('_')[1] * 1;
+
+ logger.debug(`Topic ${topic}: messsage "${message}"`);
+
+ let newValue = message;
+ if (message === '') {
+ newValue = '';
+ } else if (message === 'false') {
+ newValue = false;
+ } else if (message === 'true') {
+ newValue = true;
+ } else if (message.charAt && message.charAt(0) === '{') {
+ // new Value is a object...
+ break;
+ } else if (message.charAt && message.charAt(0) === '"') {
+ // new Value is a string...
+ } else {
+ // assume message is an integer
+ try {
+ newValue = Number(message);
+ } catch (e) {
+ break;
+ }
+ if (Number.isNaN(newValue)) {
+ break;
+ }
+ }
+
+ this.valueUpdated(
+ {
+ id,
+ },
+ {
+ commandClass: commandClass * 1,
+ endpoint: endpoint * 1 || 0,
+ property: propertyName,
+ propertyKey: propertyKey ? `${propertyKey}` : undefined,
+ newValue,
+ },
+ );
+ } else {
+ logger.debug(`ZwaveJSUI topic ${topic} not handled.`);
+ }
+ }
+ }
+
+ return null;
+}
+
+module.exports = {
+ handleMqttMessage,
+};
diff --git a/server/services/zwave-js-ui/lib/events/nodeReady.js b/server/services/zwave-js-ui/lib/events/nodeReady.js
new file mode 100644
index 0000000000..734d441f17
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/events/nodeReady.js
@@ -0,0 +1,26 @@
+const logger = require('../../../../utils/logger');
+
+/**
+ * @description When a node is ready.
+ * @param {object} zwaveNode - Informations about the node.
+ * @example
+ * zwave.on('node ready', this.nodeReady);
+ */
+function nodeReady(zwaveNode) {
+ const nodeId = zwaveNode.id;
+ logger.debug(`Zwave : Node Ready, nodeId = ${nodeId}`);
+
+ const node = this.nodes[nodeId];
+ node.nodeId = nodeId;
+ node.product = zwaveNode.deviceId;
+ node.firmwareVersion = zwaveNode.firmwareVersion;
+ node.name = `${zwaveNode.name || zwaveNode.productLabel || `${zwaveNode.product}`}`;
+ node.loc = zwaveNode.loc;
+ node.status = zwaveNode.status;
+ node.ready = zwaveNode.ready;
+ node.classes = {};
+}
+
+module.exports = {
+ nodeReady,
+};
diff --git a/server/services/zwave-js-ui/lib/events/scanComplete.js b/server/services/zwave-js-ui/lib/events/scanComplete.js
new file mode 100644
index 0000000000..ca66eb9f12
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/events/scanComplete.js
@@ -0,0 +1,21 @@
+const logger = require('../../../../utils/logger');
+const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants');
+
+/**
+ * @description When the scan is complete.
+ * @example
+ * this.scanComplete();
+ */
+function scanComplete() {
+ if (this.scanInProgress) {
+ logger.info(`Zwave : Scan Complete!`);
+ this.scanInProgress = false;
+ this.eventManager.emit(EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.SCAN_COMPLETE,
+ });
+ }
+}
+
+module.exports = {
+ scanComplete,
+};
diff --git a/server/services/zwave-js-ui/lib/events/valueAdded.js b/server/services/zwave-js-ui/lib/events/valueAdded.js
new file mode 100644
index 0000000000..cf2e612bbe
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/events/valueAdded.js
@@ -0,0 +1,91 @@
+const { EVENTS } = require('../../../../utils/constants');
+const logger = require('../../../../utils/logger');
+const { GENRE, PROPERTIES, ENDPOINTS } = require('../constants');
+const { unbindValue } = require('../utils/bindValue');
+const { getDeviceFeatureExternalId } = require('../utils/externalId');
+
+/**
+ * ValueAddedArgs.
+ * @description When a value is added.
+ * @param {object} zwaveNode - ZWave Node.
+ * @param {object} args - ZWaveNodeValueAddedArgs.
+ * @example
+ * valueAdded({id: 0}, { commandClass: 0, endpoint: 0, property: '', propertyKey: '' });
+ */
+function valueAdded(zwaveNode, args) {
+ const { commandClass, endpoint, property, propertyKey, value, label } = args;
+ const nodeId = zwaveNode.id;
+ const node = this.nodes[nodeId];
+ if (!node) {
+ logger.info(`Node ${nodeId} not available. By-pass message`);
+ return;
+ }
+
+ // Current value is the final state of target value, so drop it
+ if (property === PROPERTIES.CURRENT_VALUE) {
+ return;
+ }
+ if (property === PROPERTIES.CURRENT_COLOR) {
+ return;
+ }
+
+ if (!node.classes[commandClass]) {
+ node.classes[commandClass] = {};
+ }
+ if (!node.classes[commandClass][endpoint]) {
+ node.classes[commandClass][endpoint] = {};
+ }
+ let fullProperty = property + (propertyKey ? `-${propertyKey}` : '');
+ if (fullProperty === PROPERTIES.TARGET_COLOR) {
+ fullProperty = `${fullProperty}-${ENDPOINTS.TARGET_COLOR}`;
+ }
+ logger.debug(
+ `Value Added: nodeId = ${nodeId}, comClass = ${commandClass}[${endpoint}], property = ${fullProperty}, value = ${JSON.stringify(
+ value,
+ )}`,
+ );
+
+ if ((GENRE[commandClass] || 'user') !== 'user') {
+ // TODO Do not add non-user metadata, latter converted as device parameters
+ return;
+ }
+
+ node.classes[commandClass][endpoint][fullProperty] = Object.assign(args, {
+ genre: GENRE[commandClass] || 'user',
+ // For technical use: number as key > string
+ nodeId,
+ commandClass,
+ endpoint,
+ property: fullProperty,
+ label,
+ });
+
+ // if (node.ready) {
+ const deviceFeatureExternalId = getDeviceFeatureExternalId({
+ nodeId,
+ commandClass,
+ endpoint: endpoint || 0,
+ property: fullProperty,
+ });
+ const deviceFeature = this.gladys.stateManager.get('deviceFeatureByExternalId', deviceFeatureExternalId);
+
+ if (value !== undefined && value !== null) {
+ const newValueUnbind = unbindValue(args, value);
+ node.classes[commandClass][endpoint][fullProperty].value = newValueUnbind;
+
+ if (deviceFeature) {
+ this.eventManager.emit(EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: deviceFeatureExternalId,
+ state: newValueUnbind,
+ });
+ }
+ } else if (deviceFeature) {
+ this.eventManager.emit(EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: deviceFeatureExternalId,
+ });
+ }
+}
+
+module.exports = {
+ valueAdded,
+};
diff --git a/server/services/zwave-js-ui/lib/events/valueUpdated.js b/server/services/zwave-js-ui/lib/events/valueUpdated.js
new file mode 100644
index 0000000000..8a39a5a07b
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/events/valueUpdated.js
@@ -0,0 +1,64 @@
+const logger = require('../../../../utils/logger');
+const { EVENTS } = require('../../../../utils/constants');
+const { getDeviceFeatureExternalId } = require('../utils/externalId');
+const { unbindValue } = require('../utils/bindValue');
+const { PROPERTIES, ENDPOINTS } = require('../constants');
+
+/**
+ * @description When a value changed.
+ * @param {object} zwaveNode - ZWave Node.
+ * @param {object} args - ValueUpdatedArgs.
+ * @example
+ * zwave.on('value updated', this.valueUpdated);
+ */
+function valueUpdated(zwaveNode, args) {
+ const { commandClass, endpoint, property, propertyKey, /* prevValue, */ newValue } = args;
+ const nodeId = zwaveNode.id;
+ const node = this.nodes[nodeId];
+ if (!node) {
+ logger.info(`Node ${nodeId} not available. By-pass message`);
+ return;
+ }
+
+ // Current value is the final state of target value, so drop it
+ if (property === PROPERTIES.CURRENT_VALUE) {
+ return;
+ }
+ if (property === PROPERTIES.CURRENT_COLOR) {
+ return;
+ }
+
+ let fullProperty = property + (propertyKey ? `-${propertyKey}` : '');
+ if (fullProperty === PROPERTIES.TARGET_COLOR) {
+ fullProperty = `${fullProperty}-${ENDPOINTS.TARGET_COLOR}`;
+ }
+ args.fullProperty = fullProperty;
+ const newValueUnbind = unbindValue(args, newValue);
+
+ // if (node.ready) {
+ node.classes[commandClass][endpoint][fullProperty].value = newValueUnbind;
+ logger.debug(
+ `Value Updated: nodeId = ${nodeId}, comClass = ${commandClass}, endpoint = ${endpoint}, property = ${fullProperty}: ${
+ node.classes[commandClass][endpoint][fullProperty].value
+ } > ${JSON.stringify(newValueUnbind)}`,
+ );
+
+ const deviceFeatureExternalId = getDeviceFeatureExternalId({
+ nodeId,
+ commandClass,
+ endpoint: endpoint || 0,
+ property: fullProperty,
+ });
+ const deviceFeature = this.gladys.stateManager.get('deviceFeatureByExternalId', deviceFeatureExternalId);
+ if (deviceFeature && newValueUnbind !== undefined && newValueUnbind !== null) {
+ this.eventManager.emit(EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: deviceFeatureExternalId,
+ state: newValueUnbind,
+ });
+ }
+ // }
+}
+
+module.exports = {
+ valueUpdated,
+};
diff --git a/server/services/zwave-js-ui/lib/index.js b/server/services/zwave-js-ui/lib/index.js
new file mode 100644
index 0000000000..3188fb6a2c
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/index.js
@@ -0,0 +1,62 @@
+const { addNode } = require('./commands/addNode');
+const { connect } = require('./commands/connect');
+const { disconnect } = require('./commands/disconnect');
+const { getStatus } = require('./commands/getStatus');
+const { getNodes } = require('./commands/getNodes');
+const { removeNode } = require('./commands/removeNode');
+const { setValue } = require('./commands/setValue');
+const { valueAdded } = require('./events/valueAdded');
+const { valueUpdated } = require('./events/valueUpdated');
+const { nodeReady } = require('./events/nodeReady');
+const { scanComplete } = require('./events/scanComplete');
+const { installMqttContainer } = require('./commands/installMqttContainer');
+const { installZwaveJSUIContainer } = require('./commands/installZwaveJSUIContainer');
+const { getConfiguration } = require('./commands/getConfiguration');
+const { handleMqttMessage } = require('./events/handleMqttMessage');
+const { updateConfiguration } = require('./commands/updateConfiguration');
+const { scanNetwork } = require('./commands/scanNetwork');
+
+const ZwaveJSUIManager = function ZwaveJSUIManager(gladys, mqtt, serviceId) {
+ this.gladys = gladys;
+ this.eventManager = gladys.event;
+ this.serviceId = serviceId;
+ this.nodes = {};
+
+ this.mqttExist = false;
+ this.mqttRunning = false;
+ this.mqttConnected = false;
+ this.mqtt = mqtt;
+ this.mqttClient = null;
+
+ this.zwaveJSUIExist = false;
+ this.zwaveJSUIRunning = false;
+ this.zwaveJSUIConnected = false;
+
+ this.usbConfigured = false;
+
+ this.dockerBased = true;
+ this.scanInProgress = false;
+};
+
+// EVENTS
+ZwaveJSUIManager.prototype.valueAdded = valueAdded;
+ZwaveJSUIManager.prototype.valueUpdated = valueUpdated;
+ZwaveJSUIManager.prototype.nodeReady = nodeReady;
+ZwaveJSUIManager.prototype.scanComplete = scanComplete;
+ZwaveJSUIManager.prototype.handleMqttMessage = handleMqttMessage;
+
+// COMMANDS
+ZwaveJSUIManager.prototype.connect = connect;
+ZwaveJSUIManager.prototype.disconnect = disconnect;
+ZwaveJSUIManager.prototype.getStatus = getStatus;
+ZwaveJSUIManager.prototype.getConfiguration = getConfiguration;
+ZwaveJSUIManager.prototype.getNodes = getNodes;
+ZwaveJSUIManager.prototype.addNode = addNode;
+ZwaveJSUIManager.prototype.removeNode = removeNode;
+ZwaveJSUIManager.prototype.scanNetwork = scanNetwork;
+ZwaveJSUIManager.prototype.setValue = setValue;
+ZwaveJSUIManager.prototype.updateConfiguration = updateConfiguration;
+ZwaveJSUIManager.prototype.installMqttContainer = installMqttContainer;
+ZwaveJSUIManager.prototype.installZwaveJSUIContainer = installZwaveJSUIContainer;
+
+module.exports = ZwaveJSUIManager;
diff --git a/server/services/zwave-js-ui/lib/utils/bindValue.js b/server/services/zwave-js-ui/lib/utils/bindValue.js
new file mode 100644
index 0000000000..c169165a3d
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/utils/bindValue.js
@@ -0,0 +1,94 @@
+const { STATE } = require('../../../../utils/constants');
+const { COMMAND_CLASSES, SCENE_VALUES, SMOKE_ALARM_VALUES, PROPERTIES, ENDPOINTS } = require('../constants');
+
+/**
+ * @description Bind value.
+ * @param {object} valueId - Value ID.
+ * @param {object} value - Value object to send.
+ * @returns {object} Return the value adapted.
+ * @example
+ * const value = bindValue({}, '8');
+ */
+function bindValue(valueId, value) {
+ if (valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_SWITCH_BINARY) {
+ return value === 1;
+ }
+ if (valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_SWITCH_MULTILEVEL) {
+ return Number.parseInt(value, 10);
+ }
+ if (valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION) {
+ return value === '8';
+ }
+ if (
+ valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_SWITCH_COLOR &&
+ valueId.property === PROPERTIES.HEX_COLOR
+ ) {
+ return `"${value.toString(16).padStart(6, '0')}"`;
+ }
+ if (
+ valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_SWITCH_COLOR &&
+ valueId.property === PROPERTIES.TARGET_COLOR &&
+ valueId.endpoint === ENDPOINTS.TARGET_COLOR
+ ) {
+ return Math.round((value / 100) * 255);
+ }
+ return value;
+}
+
+/**
+ * @description Unbind value.
+ * @param {object} valueId - Value ID.
+ * @param {object} value - Value object received.
+ * @returns {object} Return the value adapted.
+ * @example
+ * const value = unbindValue({}, true);
+ */
+function unbindValue(valueId, value) {
+ if (
+ valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_SWITCH_BINARY ||
+ valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_SENSOR_BINARY
+ ) {
+ return value ? STATE.ON : STATE.OFF;
+ }
+ if (valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION) {
+ if (valueId.fullProperty === PROPERTIES.MOTION_ALARM) {
+ return value === 8 ? 1 : 0;
+ }
+ if (valueId.fullProperty === PROPERTIES.SMOKE_ALARM) {
+ return SMOKE_ALARM_VALUES[value];
+ }
+ }
+ if (valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_CENTRAL_SCENE) {
+ return value === '' ? 0 : SCENE_VALUES[value % 10];
+ }
+ if (valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_SCENE_ACTIVATION) {
+ return SCENE_VALUES[value % 10];
+ }
+ if (
+ valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_SWITCH_COLOR &&
+ valueId.property === PROPERTIES.HEX_COLOR
+ ) {
+ if (value.substring) {
+ if (value.charAt(0) === '"') {
+ // Case value updated message
+ return parseInt(value.substring(1, value.length - 1), 16);
+ }
+ // Case getNodes message
+ return parseInt(value, 16);
+ }
+ return value;
+ }
+ if (
+ valueId.commandClass === COMMAND_CLASSES.COMMAND_CLASS_SWITCH_COLOR &&
+ valueId.property === PROPERTIES.TARGET_COLOR &&
+ valueId.endpoint === ENDPOINTS.TARGET_COLOR
+ ) {
+ return Math.round((value / 255) * 100);
+ }
+ return value;
+}
+
+module.exports = {
+ bindValue,
+ unbindValue,
+};
diff --git a/server/services/zwave-js-ui/lib/utils/externalId.js b/server/services/zwave-js-ui/lib/utils/externalId.js
new file mode 100644
index 0000000000..653bad3d1a
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/utils/externalId.js
@@ -0,0 +1,80 @@
+const { COMMAND_CLASSES } = require('../constants');
+
+/**
+ * @description Return name of device.
+ * @param {object} node - The zwave node.
+ * @returns {string} Return name.
+ * @example
+ * getDeviceName(node);
+ */
+function getDeviceName(node) {
+ return `${node.name} - ${node.nodeId}${node.endpoint > 0 ? ` [${node.endpoint}]` : ''}`;
+}
+
+/**
+ * @description Return external id of device.
+ * @param {object} node - The zwave node.
+ * @returns {string} Return external id.
+ * @example
+ * getDeviceExternalId(node);
+ */
+function getDeviceExternalId(node) {
+ return `zwave-js-ui:node_id:${node.nodeId}${node.endpoint > 0 ? `_${node.endpoint}` : ''}`;
+}
+
+/**
+ * @description Return name of device feature.
+ * @param {object} property - The zwave property.
+ * @returns {string} Return name.
+ * @example
+ * getDeviceFeatureName(feature);
+ */
+function getDeviceFeatureName(property) {
+ return `${property.prefLabel ? property.prefLabel : property.label}${
+ property.endpoint > 0 ? ` [${property.endpoint}]` : ''
+ }`;
+}
+
+/**
+ * @description Return external id of deviceFeature.
+ * @param {object} property - The zwave property.
+ * @returns {string} Return external id.
+ * @example
+ * getDeviceFeatureExternalId(property);
+ */
+function getDeviceFeatureExternalId(property) {
+ if (property.commandClass === COMMAND_CLASSES.COMMAND_CLASS_CENTRAL_SCENE) {
+ property.endpoint = Number(property.property.split('-')[1]);
+ }
+ return `zwave-js-ui:node_id:${property.nodeId}:comclass:${property.commandClass}:endpoint:${property.endpoint}:property:${property.property}`;
+}
+
+/**
+ * @description Return node info of devicefeature.
+ * @param {object} externalId - The externalId.
+ * @returns {object} Return all informations.
+ * @example
+ * getNodeInfoByExternalId(externalId);
+ */
+function getNodeInfoByExternalId(externalId) {
+ const array = externalId.split(':');
+ const nodeId = parseInt(array[2], 10);
+ const commandClass = parseInt(array[4], 10);
+ const endpoint = parseInt(array[6], 10);
+ const property = array[8].split('-');
+ return {
+ nodeId,
+ commandClass,
+ endpoint,
+ property: property[0],
+ propertyKey: property.length > 1 ? property[1] : undefined,
+ };
+}
+
+module.exports = {
+ getDeviceName,
+ getDeviceExternalId,
+ getDeviceFeatureName,
+ getDeviceFeatureExternalId,
+ getNodeInfoByExternalId,
+};
diff --git a/server/services/zwave-js-ui/lib/utils/getCategory.js b/server/services/zwave-js-ui/lib/utils/getCategory.js
new file mode 100644
index 0000000000..cb51fecc97
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/utils/getCategory.js
@@ -0,0 +1,58 @@
+const { DEVICE_POLL_FREQUENCIES } = require('../../../../utils/constants');
+const { CATEGORIES, UNKNOWN_CATEGORY, UNKNOWN_TYPE } = require('../constants');
+
+/**
+ * @description Get a ZWave value and return a category in Gladys.
+ * @param {object} node - The node object.
+ * @param {object} value - Value object.
+ * @returns {object} Return the category in Gladys.
+ * @example
+ * const { category, type } = getCategory({
+ * product: '',
+ * type: ''
+ * }, {
+ * commandClass: 49,
+ * endpoint: 1,
+ * fullProperty: 'currentValue',
+ * });
+ */
+function getCategory(node, value) {
+ let found = false;
+ let categoryFound = null;
+ let i = 0;
+
+ while (!found && i < CATEGORIES.length) {
+ const category = CATEGORIES[i];
+ const validComClass = category.COMMAND_CLASSES ? category.COMMAND_CLASSES.includes(value.commandClass) : true;
+ const validEndpoint = category.INDEXES ? category.INDEXES.includes(value.endpoint) : true;
+ const validProperty = category.PROPERTIES ? category.PROPERTIES.includes(value.property) : true;
+ const validProduct = category.PRODUCTS ? category.PRODUCTS.includes(node.product) : true;
+ const invalidProduct = category.EXCLUDED_PRODUCTS ? category.EXCLUDED_PRODUCTS.includes(node.product) : false;
+ found = validComClass && validEndpoint && validProperty && validProduct && !invalidProduct;
+ if (found) {
+ categoryFound = {
+ category: category.CATEGORY,
+ type: category.TYPE,
+ prefLabel: category.LABEL,
+ min: category.MIN,
+ max: category.MAX,
+ unit: category.UNIT,
+ hasFeedback: true, // TODO
+ should_poll: false,
+ poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_MINUTES,
+ };
+ }
+ i += 1;
+ }
+
+ return found
+ ? categoryFound
+ : {
+ category: UNKNOWN_CATEGORY,
+ type: UNKNOWN_TYPE,
+ };
+}
+
+module.exports = {
+ getCategory,
+};
diff --git a/server/services/zwave-js-ui/lib/utils/getUnit.js b/server/services/zwave-js-ui/lib/utils/getUnit.js
new file mode 100644
index 0000000000..2363a4a205
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/utils/getUnit.js
@@ -0,0 +1,39 @@
+const { DEVICE_FEATURE_UNITS } = require('../../../../utils/constants');
+
+/**
+ * @description Convert Z-Wave unit in Gladys unit.
+ * @param {string} zwaveUnit - Unit in Z-Wave.
+ * @returns {string} Return the unit in Gladys.
+ * @example
+ * const unit = getUnit('C');
+ */
+function getUnit(zwaveUnit) {
+ switch (zwaveUnit) {
+ case '°C':
+ return DEVICE_FEATURE_UNITS.CELSIUS;
+ case '°F':
+ return DEVICE_FEATURE_UNITS.FAHRENHEIT;
+ case '%':
+ return DEVICE_FEATURE_UNITS.PERCENT;
+ case 'lux':
+ return DEVICE_FEATURE_UNITS.LUX;
+ case 'Lux':
+ return DEVICE_FEATURE_UNITS.LUX;
+ case 'A':
+ return DEVICE_FEATURE_UNITS.AMPERE;
+ case 'V':
+ return DEVICE_FEATURE_UNITS.VOLT;
+ case 'kWh':
+ return DEVICE_FEATURE_UNITS.KILOWATT_HOUR;
+ case 'W':
+ return DEVICE_FEATURE_UNITS.WATT;
+ case 'Watt':
+ return DEVICE_FEATURE_UNITS.WATT;
+ default:
+ return null;
+ }
+}
+
+module.exports = {
+ getUnit,
+};
diff --git a/server/services/zwave-js-ui/lib/utils/splitNode.js b/server/services/zwave-js-ui/lib/utils/splitNode.js
new file mode 100644
index 0000000000..cd0c65498c
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/utils/splitNode.js
@@ -0,0 +1,50 @@
+const cloneDeep = require('lodash.clonedeep');
+const logger = require('../../../../utils/logger');
+
+/**
+ * @description Split Node into each endpoints.
+ * @param {object} node - Z-Wave node .
+ * @returns {Array} Splitted nodes.
+ * @example
+ * const nodes = zwaveManager.splitNode({});
+ */
+function splitNode(node) {
+ if (node.endpoints.length < 2) {
+ node.endpoint = 0;
+ return node;
+ }
+
+ // Temporary remove endpoints for clone
+ const { endpoints } = node;
+ node.endpoints = undefined;
+
+ const commonNode = cloneDeep(node);
+ commonNode.endpoint = 0;
+
+ const nodes = [commonNode];
+ endpoints.forEach((endpoint) => {
+ const eNode = cloneDeep(node);
+ eNode.endpoint = endpoint.index;
+ eNode.classes = {};
+ Object.keys(node.classes).forEach((comclass) => {
+ const valuesClass = node.classes[comclass];
+ if (valuesClass[endpoint.index]) {
+ eNode.classes[comclass] = {};
+ eNode.classes[comclass][endpoint.index] = valuesClass[endpoint.index];
+ }
+ if (commonNode.classes[comclass]) {
+ delete commonNode.classes[comclass][endpoint.index];
+ }
+ });
+ nodes.push(eNode);
+ });
+ logger.debug(`splitNode: got ${nodes.length} devices`);
+
+ node.endpoints = endpoints;
+
+ return nodes;
+}
+
+module.exports = {
+ splitNode,
+};
diff --git a/server/services/zwave-js-ui/lib/utils/transformClasses.js b/server/services/zwave-js-ui/lib/utils/transformClasses.js
new file mode 100644
index 0000000000..49028296ef
--- /dev/null
+++ b/server/services/zwave-js-ui/lib/utils/transformClasses.js
@@ -0,0 +1,60 @@
+const cloneDeep = require('lodash.clonedeep');
+const { COMMAND_CLASSES, PROPERTIES, PRODUCT } = require('../constants');
+
+/**
+ * @description Return filtered classes (e.g. Manage command classs version).
+ * @param {object} node - Z-Wave node.
+ * @returns {object} Return filtered classes.
+ * @example
+ * const filteredClasses = zwaveManager.transformClasses({});
+ */
+function transformClasses(node) {
+ const filteredClasses = cloneDeep(node.classes);
+ if (node.product === PRODUCT.FIBARO_DIMMER2) {
+ node.endpointsCount = 1;
+ if (filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SWITCH_MULTILEVEL]) {
+ delete filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SWITCH_MULTILEVEL][0];
+ delete filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SWITCH_MULTILEVEL][2];
+ }
+ }
+ if (filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SENSOR_BINARY]) {
+ if (
+ filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SENSOR_BINARY][0][PROPERTIES.ANY] &&
+ filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SENSOR_BINARY][0][PROPERTIES.MOTION]
+ ) {
+ delete filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SENSOR_BINARY][0][PROPERTIES.ANY];
+ }
+ if (
+ filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION] &&
+ filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION][0]
+ ) {
+ delete filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION][0][PROPERTIES.MOTION_ALARM];
+ }
+ }
+ /* if (filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SWITCH_MULTILEVEL]) {
+ filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SWITCH_BINARY] = {};
+ Object.keys(filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SWITCH_MULTILEVEL]).forEach(endpoint => {
+ filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SWITCH_BINARY][endpoint] = {};
+ filteredClasses[COMMAND_CLASSES.COMMAND_CLASS_SWITCH_BINARY][endpoint][PROPERTIES.TARGET_VALUE] = {
+ id: `${node.nodeId}-${COMMAND_CLASSES.COMMAND_CLASS_SWITCH_BINARY}-${endpoint}-targetValue`,
+ nodeId: node.nodeId,
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_SWITCH_BINARY,
+ endpoint,
+ property: 'targetValue',
+ type: 'number',
+ readable: true,
+ writeable: true,
+ label: 'Target value',
+ min: 0,
+ max: 1,
+ value: 0,
+ genre: 'user',
+ };
+ });
+ } */
+ return filteredClasses;
+}
+
+module.exports = {
+ transformClasses,
+};
diff --git a/server/services/zwave-js-ui/package-lock.json b/server/services/zwave-js-ui/package-lock.json
new file mode 100644
index 0000000000..f736f3ace1
--- /dev/null
+++ b/server/services/zwave-js-ui/package-lock.json
@@ -0,0 +1,808 @@
+{
+ "name": "gladys-zwave-js-ui",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "gladys-zwave-js-ui",
+ "cpu": [
+ "x64",
+ "arm",
+ "arm64"
+ ],
+ "os": [
+ "darwin",
+ "linux",
+ "win32"
+ ],
+ "dependencies": {
+ "dayjs": "^1.10.7",
+ "lodash.clonedeep": "^4.5.0",
+ "mqtt": "^4.2.6"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
+ },
+ "node_modules/commist": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz",
+ "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==",
+ "dependencies": {
+ "leven": "^2.1.0",
+ "minimist": "^1.1.0"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ },
+ "node_modules/concat-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+ "engines": [
+ "node >= 6.0"
+ ],
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.0.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.10.7",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
+ "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig=="
+ },
+ "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"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/duplexify": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz",
+ "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==",
+ "dependencies": {
+ "end-of-stream": "^1.4.1",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1",
+ "stream-shift": "^1.0.0"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+ },
+ "node_modules/glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/help-me": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/help-me/-/help-me-3.0.0.tgz",
+ "integrity": "sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==",
+ "dependencies": {
+ "glob": "^7.1.6",
+ "readable-stream": "^3.6.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/js-sdsl": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-2.1.4.tgz",
+ "integrity": "sha512-/Ew+CJWHNddr7sjwgxaVeIORIH4AMVC9dy0hPf540ZGMVgS9d3ajwuVdyhDt6/QUvT8ATjR3yuYBKsS79F+H4A=="
+ },
+ "node_modules/leven": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
+ "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+ },
+ "node_modules/mqtt": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz",
+ "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==",
+ "dependencies": {
+ "commist": "^1.0.0",
+ "concat-stream": "^2.0.0",
+ "debug": "^4.1.1",
+ "duplexify": "^4.1.1",
+ "help-me": "^3.0.0",
+ "inherits": "^2.0.3",
+ "lru-cache": "^6.0.0",
+ "minimist": "^1.2.5",
+ "mqtt-packet": "^6.8.0",
+ "number-allocator": "^1.0.9",
+ "pump": "^3.0.0",
+ "readable-stream": "^3.6.0",
+ "reinterval": "^1.1.0",
+ "rfdc": "^1.3.0",
+ "split2": "^3.1.0",
+ "ws": "^7.5.5",
+ "xtend": "^4.0.2"
+ },
+ "bin": {
+ "mqtt": "bin/mqtt.js",
+ "mqtt_pub": "bin/pub.js",
+ "mqtt_sub": "bin/sub.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/mqtt-packet": {
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz",
+ "integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==",
+ "dependencies": {
+ "bl": "^4.0.2",
+ "debug": "^4.1.1",
+ "process-nextick-args": "^2.0.1"
+ }
+ },
+ "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/number-allocator": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.10.tgz",
+ "integrity": "sha512-K4AvNGKo9lP6HqsZyfSr9KDaqnwFzW203inhQEOwFrmFaYevpdX4VNwdOLk197aHujzbT//z6pCBrCOUYSM5iw==",
+ "dependencies": {
+ "debug": "^4.3.1",
+ "js-sdsl": "^2.1.2"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+ },
+ "node_modules/pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/reinterval": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz",
+ "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc="
+ },
+ "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/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/split2": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
+ "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==",
+ "dependencies": {
+ "readable-stream": "^3.0.0"
+ }
+ },
+ "node_modules/stream-shift": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
+ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "node_modules/ws": {
+ "version": "7.5.7",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
+ "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ }
+ },
+ "dependencies": {
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
+ },
+ "bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "requires": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
+ },
+ "commist": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz",
+ "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==",
+ "requires": {
+ "leven": "^2.1.0",
+ "minimist": "^1.1.0"
+ }
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ },
+ "concat-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.0.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "dayjs": {
+ "version": "1.10.7",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
+ "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig=="
+ },
+ "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"
+ }
+ },
+ "duplexify": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz",
+ "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==",
+ "requires": {
+ "end-of-stream": "^1.4.1",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1",
+ "stream-shift": "^1.0.0"
+ }
+ },
+ "end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+ },
+ "glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "help-me": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/help-me/-/help-me-3.0.0.tgz",
+ "integrity": "sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==",
+ "requires": {
+ "glob": "^7.1.6",
+ "readable-stream": "^3.6.0"
+ }
+ },
+ "ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "js-sdsl": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-2.1.4.tgz",
+ "integrity": "sha512-/Ew+CJWHNddr7sjwgxaVeIORIH4AMVC9dy0hPf540ZGMVgS9d3ajwuVdyhDt6/QUvT8ATjR3yuYBKsS79F+H4A=="
+ },
+ "leven": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
+ "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA="
+ },
+ "lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+ },
+ "mqtt": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz",
+ "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==",
+ "requires": {
+ "commist": "^1.0.0",
+ "concat-stream": "^2.0.0",
+ "debug": "^4.1.1",
+ "duplexify": "^4.1.1",
+ "help-me": "^3.0.0",
+ "inherits": "^2.0.3",
+ "lru-cache": "^6.0.0",
+ "minimist": "^1.2.5",
+ "mqtt-packet": "^6.8.0",
+ "number-allocator": "^1.0.9",
+ "pump": "^3.0.0",
+ "readable-stream": "^3.6.0",
+ "reinterval": "^1.1.0",
+ "rfdc": "^1.3.0",
+ "split2": "^3.1.0",
+ "ws": "^7.5.5",
+ "xtend": "^4.0.2"
+ }
+ },
+ "mqtt-packet": {
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz",
+ "integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==",
+ "requires": {
+ "bl": "^4.0.2",
+ "debug": "^4.1.1",
+ "process-nextick-args": "^2.0.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "number-allocator": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.10.tgz",
+ "integrity": "sha512-K4AvNGKo9lP6HqsZyfSr9KDaqnwFzW203inhQEOwFrmFaYevpdX4VNwdOLk197aHujzbT//z6pCBrCOUYSM5iw==",
+ "requires": {
+ "debug": "^4.3.1",
+ "js-sdsl": "^2.1.2"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+ },
+ "pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "reinterval": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz",
+ "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc="
+ },
+ "rfdc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
+ "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA=="
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+ },
+ "split2": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
+ "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==",
+ "requires": {
+ "readable-stream": "^3.0.0"
+ }
+ },
+ "stream-shift": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
+ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "ws": {
+ "version": "7.5.7",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz",
+ "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
+ "requires": {}
+ },
+ "xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ }
+ }
+}
diff --git a/server/services/zwave-js-ui/package.json b/server/services/zwave-js-ui/package.json
new file mode 100644
index 0000000000..95bc34fa0a
--- /dev/null
+++ b/server/services/zwave-js-ui/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "gladys-zwave-js-ui",
+ "main": "index.js",
+ "os": [
+ "darwin",
+ "linux",
+ "win32"
+ ],
+ "cpu": [
+ "x64",
+ "arm",
+ "arm64"
+ ],
+ "scripts": {},
+ "dependencies": {
+ "dayjs": "^1.10.7",
+ "lodash.clonedeep": "^4.5.0",
+ "mqtt": "^4.2.6"
+ }
+}
diff --git a/server/test/lib/system/system.getContainerDevices.test.js b/server/test/lib/system/system.getContainerDevices.test.js
new file mode 100644
index 0000000000..e66010b879
--- /dev/null
+++ b/server/test/lib/system/system.getContainerDevices.test.js
@@ -0,0 +1,96 @@
+const { expect } = require('chai');
+const sinon = require('sinon');
+
+const { fake, assert } = sinon;
+
+const proxyquire = require('proxyquire').noCallThru();
+
+const { PlatformNotCompatible } = require('../../../utils/coreErrors');
+const DockerodeMock = require('./DockerodeMock.test');
+
+const System = proxyquire('../../../lib/system', {
+ dockerode: DockerodeMock,
+});
+const Job = require('../../../lib/job');
+
+const sequelize = {
+ close: fake.resolves(null),
+};
+
+const event = {
+ on: fake.resolves(null),
+ emit: fake.resolves(null),
+};
+
+const job = new Job(event);
+
+const config = {
+ tempFolder: '/tmp/gladys',
+};
+
+describe('system.getContainerDevices', () => {
+ let system;
+
+ beforeEach(async () => {
+ system = new System(sequelize, event, config, job);
+ await system.init();
+ // Reset all fakes invoked within init call
+ sinon.reset();
+ });
+
+ afterEach(() => {
+ sinon.reset();
+ });
+
+ it('should failed as not on docker env', async () => {
+ system.dockerode = undefined;
+
+ try {
+ await system.getContainerDevices('fake_id');
+ assert.fail('should have fail');
+ } catch (e) {
+ expect(e).be.instanceOf(PlatformNotCompatible);
+ }
+ });
+
+ it('should return container devices', async () => {
+ const devices = await system.getContainerDevices(
+ 'a8293feec54547a797aa2e52cc14b93f89a007d6c5608c587e30491feec8ee61',
+ );
+ assert.match(devices, [
+ {
+ PathOnHost: '/dev/ttyUSB0',
+ PathInContainer: '/dev/ttyACM0',
+ CgroupPermissions: 'rwm',
+ },
+ ]);
+ });
+
+ it('should return empty list because no container found', async () => {
+ system.dockerode.getContainer = fake.resolves(null);
+ const devices = await system.getContainerDevices('fake_id');
+ assert.match(devices, []);
+ });
+
+ it('should return empty list because inspect return null', async () => {
+ system.dockerode.getContainer = fake.returns({
+ inspect: fake.resolves(null),
+ });
+ const devices = await system.getContainerDevices('fake_id');
+ assert.match(devices, []);
+ });
+
+ it('should return empty list because no devices found', async () => {
+ system.dockerode.getContainer = fake.returns({
+ inspect: fake.resolves({
+ HostConfig: {
+ Devices: [],
+ },
+ }),
+ });
+ const devices = await system.getContainerDevices(
+ 'a8293feec54547a797aa2e52cc14b93f89a007d6c5608c587e30491feec8ee61',
+ );
+ assert.match(devices, []);
+ });
+});
diff --git a/server/test/services/zwave-js-ui/README.md b/server/test/services/zwave-js-ui/README.md
new file mode 100644
index 0000000000..7d3b56edaa
--- /dev/null
+++ b/server/test/services/zwave-js-ui/README.md
@@ -0,0 +1,77 @@
+"commandClass":32,"property":"event"}"
+"commandClass":32,"property":"currentValue"}"
+"commandClass":32,"property":"targetValue"}" Same as currentValue
+"commandClass":32,"property":"duration"}"
+
+"commandClass":37,"property":"currentValue"}"
+"commandClass":37,"property":"targetValue"}" Same as currentValue
+"commandClass":37,"property":"duration"}"
+
+"commandClass":38,"property":"Up"}" Not supported
+"commandClass":38,"property":"Down"}" Not supported
+"commandClass":38,"property":"On"}"
+"commandClass":38,"property":"Off"}"
+"commandClass":38,"property":"targetValue"}" Same as currentValue
+"commandClass":38,"property":"duration"}"
+"commandClass":38,"property":"currentValue"}"
+
+"commandClass":48,"property":"Any"}" OK
+"commandClass":48,"property":"Motion"}" OK
+"commandClass":48,"property":"unknown (0x1c)"}" ???
+
+"commandClass":49,"property":"Power"}" OK
+"commandClass":49,"property":"Illuminance"}" OK
+"commandClass":49,"property":"Ultraviolet"}" OK
+"commandClass":49,"property":"Air temperature"}" OK
+"commandClass":49,"property":"Humidity"}" OK
+
+"commandClass":50,"property":"reset"}"
+"commandClass":50,"property":"value","propertyKey":66048}" OK
+"commandClass":50,"property":"value","propertyKey":66049}" OK
+"commandClass":50,"property":"value","propertyKey":66051}" What for?
+"commandClass":50,"property":"value","propertyKey":65536}" What for?
+"commandClass":50,"property":"value","propertyKey":65537}" What for?
+"commandClass":50,"property":"value","propertyKey":65539}" What for?
+"commandClass":50,"property":"value","propertyKey":66561}"
+"commandClass":50,"property":"value","propertyKey":66817}"
+
+"commandClass":51,"property":"currentColor"}"
+"commandClass":51,"property":"currentColor","propertyKey":0}"
+"commandClass":51,"property":"currentColor","propertyKey":1}"
+"commandClass":51,"property":"currentColor","propertyKey":2}"
+"commandClass":51,"property":"currentColor","propertyKey":3}"
+"commandClass":51,"property":"currentColor","propertyKey":4}"
+"commandClass":51,"property":"targetColor"}"
+"commandClass":51,"property":"targetColor","propertyKey":0}"
+"commandClass":51,"property":"targetColor","propertyKey":1}"
+"commandClass":51,"property":"targetColor","propertyKey":2}"
+"commandClass":51,"property":"targetColor","propertyKey":3}"
+"commandClass":51,"property":"targetColor","propertyKey":4}"
+"commandClass":51,"property":"hexColor"}"
+
+"commandClass":91,"property":"scene","propertyKey":"001"}"
+"commandClass":91,"property":"scene","propertyKey":"002"}"
+"commandClass":91,"property":"scene","propertyKey":"003"}"
+"commandClass":91,"property":"scene","propertyKey":"004"}"
+"commandClass":91,"property":"scene","propertyKey":"005"}"
+"commandClass":91,"property":"scene","propertyKey":"006"}"
+
+"commandClass":113,"property":"Home Security","propertyKey":"Cover status"}" Not supported
+"commandClass":113,"property":"Home Security","propertyKey":"Motion sensor status"}" Not supported
+"commandClass":113,"property":"Heat Alarm","propertyKey":"Heat sensor status"}" Not supported
+"commandClass":113,"property":"Power Management","propertyKey":"Over-current status"}" Not supported
+"commandClass":113,"property":"Power Management","propertyKey":"Over-load status"}" Not supported
+"commandClass":113,"property":"Power Management","propertyKey":"Load error status"}" Not supported
+"commandClass":113,"property":"System","propertyKey":"Hardware status"}" Not supported
+"commandClass":113,"property":"Home Security","propertyKey":"Sensor status"}" Not supported
+"commandClass":113,"property":"alarmType"}" Not supported
+"commandClass":113,"property":"alarmLevel"}" Not supported
+"commandClass":113,"property":"Smoke Alarm","propertyKey":"Sensor status"}" Ok
+"commandClass":113,"property":"Smoke Alarm","propertyKey":"Alarm status"}" Not supported
+"commandClass":113,"property":"Power Management","propertyKey":"Battery maintenance status"}" Not supported
+"commandClass":113,"property":"Power Management","propertyKey":"Power status"}" Not supported
+"commandClass":113,"property":"Power Management","propertyKey":"Battery load status"}" Not supported
+"commandClass":113,"property":"Power Management","propertyKey":"Battery level status"}" Not supported
+
+"commandClass":128,"property":"level"}"
+"commandClass":128,"property":"isLow"}"
\ No newline at end of file
diff --git a/server/test/services/zwave-js-ui/api/zwavejs2mqtt.controller.test.js b/server/test/services/zwave-js-ui/api/zwavejs2mqtt.controller.test.js
new file mode 100644
index 0000000000..a440a28800
--- /dev/null
+++ b/server/test/services/zwave-js-ui/api/zwavejs2mqtt.controller.test.js
@@ -0,0 +1,151 @@
+const sinon = require('sinon');
+
+const { assert, fake } = sinon;
+const ZwaveJSUIController = require('../../../../services/zwave-js-ui/api/zwavejsui.controller');
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+const event = {
+ emit: fake.resolves(null),
+};
+
+const gladys = {
+ event,
+};
+const zwaveJSUIManager = {};
+
+let zwaveJSUIController;
+
+describe('GET /api/v1/service/zwave-js-ui', () => {
+ beforeEach(() => {
+ zwaveJSUIController = ZwaveJSUIController(gladys, zwaveJSUIManager, ZWAVEJSUI_SERVICE_ID);
+ sinon.reset();
+ });
+
+ it('should get status', async () => {
+ const req = {};
+ const res = {
+ json: fake.returns(null),
+ };
+ const status = {
+ mqttConnected: false,
+ scanInProgress: false,
+ };
+ zwaveJSUIManager.getStatus = fake.returns(status);
+ await zwaveJSUIController['get /api/v1/service/zwave-js-ui/status'].controller(req, res);
+ assert.calledOnce(zwaveJSUIManager.getStatus);
+ assert.calledOnceWithExactly(res.json, status);
+ });
+
+ it('should get configuration', async () => {
+ const req = {};
+ const res = {
+ json: fake.returns(null),
+ };
+ const configuration = {};
+ zwaveJSUIManager.getConfiguration = fake.returns(configuration);
+ await zwaveJSUIController['get /api/v1/service/zwave-js-ui/configuration'].controller(req, res);
+ assert.calledOnce(zwaveJSUIManager.getConfiguration);
+ assert.calledOnceWithExactly(res.json, configuration);
+ });
+
+ it('should update configuration', async () => {
+ const req = {
+ body: {
+ externalZwaveJSUI: 'externalZwaveJSUI',
+ driverPath: 'driverPath',
+ },
+ };
+ const result = true;
+ const res = {
+ json: fake.returns(null),
+ };
+ zwaveJSUIManager.updateConfiguration = fake.returns(result);
+ zwaveJSUIManager.connect = fake.returns(null);
+ await zwaveJSUIController['post /api/v1/service/zwave-js-ui/configuration'].controller(req, res);
+ assert.calledOnceWithExactly(zwaveJSUIManager.updateConfiguration, req.body);
+ assert.calledOnceWithExactly(res.json, {
+ success: result,
+ });
+ });
+
+ it('should get nodes', async () => {
+ const req = {};
+ const res = {
+ json: fake.returns(null),
+ };
+ const nodes = [];
+ zwaveJSUIManager.getNodes = fake.returns(nodes);
+ await zwaveJSUIController['get /api/v1/service/zwave-js-ui/node'].controller(req, res);
+ assert.calledOnce(zwaveJSUIManager.getNodes);
+ assert.calledOnceWithExactly(res.json, nodes);
+ });
+
+ it('should connect', async () => {
+ const req = {};
+ const res = {
+ json: fake.returns(null),
+ };
+ zwaveJSUIManager.connect = fake.returns(null);
+ await zwaveJSUIController['post /api/v1/service/zwave-js-ui/connect'].controller(req, res);
+ assert.calledOnce(zwaveJSUIManager.connect);
+ assert.calledOnceWithExactly(res.json, {
+ success: true,
+ });
+ });
+
+ it('should disconnect', async () => {
+ const req = {};
+ const res = {
+ json: fake.returns(null),
+ };
+ zwaveJSUIManager.disconnect = fake.returns(null);
+ await zwaveJSUIController['post /api/v1/service/zwave-js-ui/disconnect'].controller(req, res);
+ assert.calledOnce(zwaveJSUIManager.disconnect);
+ assert.calledOnceWithExactly(res.json, {
+ success: true,
+ });
+ });
+
+ it('should add node', async () => {
+ const req = {
+ body: {
+ secure: false,
+ },
+ };
+ const res = {
+ json: fake.returns(null),
+ };
+ zwaveJSUIManager.addNode = fake.returns(null);
+ await zwaveJSUIController['post /api/v1/service/zwave-js-ui/node/add'].controller(req, res);
+ assert.calledOnce(zwaveJSUIManager.addNode);
+ assert.calledOnceWithExactly(res.json, {
+ success: true,
+ });
+ });
+
+ it('should remove node', async () => {
+ const req = {};
+ const res = {
+ json: fake.returns(null),
+ };
+ zwaveJSUIManager.removeNode = fake.returns(null);
+ await zwaveJSUIController['post /api/v1/service/zwave-js-ui/node/remove'].controller(req, res);
+ assert.calledOnce(zwaveJSUIManager.removeNode);
+ assert.calledOnceWithExactly(res.json, {
+ success: true,
+ });
+ });
+
+ it('should scanh network', async () => {
+ const req = {};
+ const res = {
+ json: fake.returns(null),
+ };
+ zwaveJSUIManager.scanNetwork = fake.returns(null);
+ await zwaveJSUIController['post /api/v1/service/zwave-js-ui/scan'].controller(req, res);
+ assert.calledOnce(zwaveJSUIManager.scanNetwork);
+ assert.calledOnceWithExactly(res.json, {
+ success: true,
+ });
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/commands/connect.test.js b/server/test/services/zwave-js-ui/lib/commands/connect.test.js
new file mode 100644
index 0000000000..52bc250a08
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/commands/connect.test.js
@@ -0,0 +1,385 @@
+const sinon = require('sinon');
+
+const { expect } = require('chai');
+
+const { assert, fake } = sinon;
+const EventEmitter = require('events');
+const proxyquire = require('proxyquire').noCallThru();
+
+const { CONFIGURATION, DEFAULT } = require('../../../../../services/zwave-js-ui/lib/constants');
+const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants');
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+const DRIVER_PATH = 'DRIVER_PATH';
+
+const { connect } = proxyquire('../../../../../services/zwave-js-ui/lib/commands/connect', {
+ '../../../../utils/password': { generate: fake.returns('********') },
+});
+const disconnectMock = fake.returns(true);
+const installMqttContainerMock = fake.resolves(true);
+const installZwaveJSUIContainerMock = fake.resolves(true);
+const handleMqttMessageMock = fake.returns(true);
+const ZwaveJSUIManager = proxyquire('../../../../../services/zwave-js-ui/lib', {
+ './commands/installMqttContainer': { installMqttContainer: installMqttContainerMock },
+ './commands/installZwaveJSUIContainer': { installZwaveJSUIContainer: installZwaveJSUIContainerMock },
+ './events/handleMqttMessage': { handleMqttMessage: handleMqttMessageMock },
+ './commands/connect': { connect },
+ './commands/disconnect': { disconnect: disconnectMock },
+});
+
+const event = {
+ emit: fake.resolves(null),
+};
+
+const eventMqtt = new EventEmitter();
+
+const mqttClient = Object.assign(eventMqtt, {
+ subscribe: fake.resolves(null),
+ publish: fake.returns(true),
+ end: fake.resolves(true),
+ removeAllListeners: fake.resolves(true),
+});
+
+const mqtt = {
+ connect: fake.returns(mqttClient),
+};
+
+describe('zwaveJSUIManager connect', () => {
+ let gladys;
+ let zwaveJSUIManager;
+
+ beforeEach(() => {
+ gladys = {
+ event,
+ service: {
+ getService: () => {
+ return {
+ list: () =>
+ Promise.resolve([
+ {
+ path: DRIVER_PATH,
+ },
+ ]),
+ };
+ },
+ },
+ variable: {
+ getValue: fake.resolves(true),
+ setValue: fake.resolves(true),
+ },
+ system: {
+ isDocker: fake.resolves(true),
+ },
+ };
+ zwaveJSUIManager = new ZwaveJSUIManager(gladys, mqtt, ZWAVEJSUI_SERVICE_ID);
+ });
+
+ afterEach(() => {
+ sinon.reset();
+ zwaveJSUIManager.mqttExist = false;
+ zwaveJSUIManager.mqttRunning = false;
+ zwaveJSUIManager.mqttConnected = false;
+ zwaveJSUIManager.zwaveJSUIExist = false;
+ zwaveJSUIManager.zwaveJSUIRunning = false;
+ zwaveJSUIManager.scanInProgress = false;
+ zwaveJSUIManager.usbConfigured = false;
+ });
+
+ it('should connect to zwave-js-ui gladys instance as default', async () => {
+ gladys.variable.getValue = sinon.stub();
+ gladys.variable.getValue
+ .onCall(0) // EXTERNAL_ZWAVEJSUI
+ .resolves(null)
+ .onCall(1) // MQTT_PASSWORD
+ .resolves(null)
+ .onCall(2) // MQTT_URL
+ .resolves('MQTT_URL')
+ .onCall(3) // MQTT_USERNAME
+ .resolves('MQTT_USERNAME')
+ .onCall(4) // DRIVER_PATH
+ .resolves(null);
+
+ await zwaveJSUIManager.connect();
+
+ expect(zwaveJSUIManager.mqttConnected).to.equal(false);
+ expect(zwaveJSUIManager.mqttExist).to.equal(false);
+ expect(zwaveJSUIManager.mqttRunning).to.equal(false);
+ expect(zwaveJSUIManager.zwaveJSUIExist).to.equal(false);
+ expect(zwaveJSUIManager.zwaveJSUIRunning).to.equal(false);
+
+ // expect(password.generate()).;
+
+ assert.calledWithExactly(
+ gladys.variable.setValue,
+ CONFIGURATION.EXTERNAL_ZWAVEJSUI,
+ DEFAULT.EXTERNAL_ZWAVEJSUI ? '1' : '0',
+ ZWAVEJSUI_SERVICE_ID,
+ );
+ assert.calledWithExactly(
+ gladys.variable.setValue,
+ CONFIGURATION.ZWAVEJSUI_MQTT_URL,
+ DEFAULT.ZWAVEJSUI_MQTT_URL_VALUE,
+ ZWAVEJSUI_SERVICE_ID,
+ );
+ assert.calledWithExactly(
+ gladys.variable.setValue,
+ CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME,
+ DEFAULT.ZWAVEJSUI_MQTT_USERNAME_VALUE,
+ ZWAVEJSUI_SERVICE_ID,
+ );
+ assert.calledWithExactly(
+ gladys.variable.setValue,
+ CONFIGURATION.ZWAVEJSUI_MQTT_PASSWORD,
+ '********',
+ ZWAVEJSUI_SERVICE_ID,
+ );
+ });
+
+ it('should connect to zwave-js-ui external instance', async () => {
+ gladys.variable.getValue = sinon.stub();
+ gladys.variable.getValue
+ .onFirstCall() // EXTERNAL_ZWAVEJSUI
+ .resolves('1')
+ .onSecondCall() // DRIVER_PATH
+ .resolves(DRIVER_PATH);
+
+ await zwaveJSUIManager.connect();
+ zwaveJSUIManager.mqttClient.emit('connect');
+
+ assert.calledOnceWithExactly(zwaveJSUIManager.eventManager.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.calledOnce(mqtt.connect);
+ assert.calledWith(mqttClient.subscribe, 'zwave-js-ui/#');
+ expect(zwaveJSUIManager.mqttConnected).to.equal(true);
+ expect(zwaveJSUIManager.mqttExist).to.equal(true);
+ expect(zwaveJSUIManager.mqttRunning).to.equal(true);
+ expect(zwaveJSUIManager.zwaveJSUIExist).to.equal(true);
+ expect(zwaveJSUIManager.zwaveJSUIRunning).to.equal(true);
+ });
+
+ it('should connect to zwave-js-ui gladys instance no driver', async () => {
+ gladys.variable.getValue = sinon.stub();
+ gladys.variable.getValue
+ .onCall(0) // EXTERNAL_ZWAVEJSUI
+ .resolves('0')
+ .onCall(1) // MQTT_PASSWORD
+ .resolves('MQTT_PASSWORD')
+ .onCall(2) // DRIVER_PATH
+ .resolves(null);
+
+ await zwaveJSUIManager.connect();
+
+ expect(zwaveJSUIManager.mqttConnected).to.equal(false);
+ expect(zwaveJSUIManager.mqttExist).to.equal(false);
+ expect(zwaveJSUIManager.mqttRunning).to.equal(false);
+ expect(zwaveJSUIManager.zwaveJSUIExist).to.equal(false);
+ expect(zwaveJSUIManager.zwaveJSUIRunning).to.equal(false);
+ });
+
+ it('should connect to zwave-js-ui gladys instance driver set', async () => {
+ zwaveJSUIManager.mqttExist = true;
+ zwaveJSUIManager.mqttRunning = true;
+ zwaveJSUIManager.zwaveJSUIExist = true;
+ zwaveJSUIManager.zwaveJSUIRunning = true;
+ gladys.variable.getValue = sinon.stub();
+ gladys.variable.getValue
+ .onFirstCall() // EXTERNAL_ZWAVEJSUI
+ .resolves('0')
+ .onSecondCall() // MQTT_PASSWORD
+ .resolves('MQTT_PASSWORD')
+ .onThirdCall() // DRIVER_PATH
+ .resolves('DRIVER_PATH')
+ .onCall(7) // MQTT_URL
+ .resolves('MQTT_URL')
+ .onCall(8) // MQTT_USERNAME
+ .resolves('MQTT_USERNAME');
+
+ await zwaveJSUIManager.connect();
+ zwaveJSUIManager.mqttClient.emit('connect');
+ assert.calledOnce(zwaveJSUIManager.installMqttContainer);
+ assert.calledOnce(zwaveJSUIManager.installZwaveJSUIContainer);
+
+ assert.calledTwice(zwaveJSUIManager.eventManager.emit);
+ assert.calledOnce(mqtt.connect);
+ assert.calledWith(mqttClient.subscribe, 'zwave-js-ui/#');
+ expect(zwaveJSUIManager.mqttConnected).to.equal(true);
+ expect(zwaveJSUIManager.mqttExist).to.equal(true);
+ expect(zwaveJSUIManager.mqttRunning).to.equal(true);
+ expect(zwaveJSUIManager.zwaveJSUIExist).to.equal(true);
+ expect(zwaveJSUIManager.zwaveJSUIRunning).to.equal(true);
+ });
+
+ it('should connect to zwave-js-ui gladys instance not docker', async () => {
+ zwaveJSUIManager.mqttExist = true;
+ zwaveJSUIManager.mqttRunning = true;
+ zwaveJSUIManager.zwaveJSUIExist = true;
+ zwaveJSUIManager.zwaveJSUIRunning = true;
+ gladys.system.isDocker = fake.resolves(false);
+ gladys.variable.getValue = sinon.stub();
+ gladys.variable.getValue
+ .onFirstCall() // EXTERNAL_ZWAVEJSUI
+ .resolves('0')
+ .onSecondCall() // MQTT_PASSWORD
+ .resolves('MQTT_PASSWORD')
+ .onThirdCall() // DRIVER_PATH
+ .resolves('DRIVER_PATH')
+ .onCall(7) // MQTT_URL
+ .resolves('MQTT_URL')
+ .onCall(8) // MQTT_USERNAME
+ .resolves('MQTT_USERNAME');
+
+ await zwaveJSUIManager.connect();
+ zwaveJSUIManager.mqttClient.emit('connect');
+ assert.notCalled(zwaveJSUIManager.installMqttContainer);
+ assert.notCalled(zwaveJSUIManager.installZwaveJSUIContainer);
+
+ assert.calledThrice(zwaveJSUIManager.eventManager.emit);
+ assert.calledOnce(mqtt.connect);
+ assert.calledWith(mqttClient.subscribe, 'zwave-js-ui/#');
+ expect(zwaveJSUIManager.mqttConnected).to.equal(true);
+ expect(zwaveJSUIManager.mqttExist).to.equal(true);
+ expect(zwaveJSUIManager.mqttRunning).to.equal(true);
+ expect(zwaveJSUIManager.zwaveJSUIExist).to.equal(true);
+ expect(zwaveJSUIManager.zwaveJSUIRunning).to.equal(true);
+ });
+
+ it('should connect to zwave-js-ui gladys topic prefix', async () => {
+ gladys.variable.getValue = sinon.stub();
+ gladys.variable.getValue
+ .onCall(0) // EXTERNAL_ZWAVEJSUI
+ .resolves('1')
+ .onCall(1) // MQTT_PASSWORD
+ .resolves('MQTT_PASSWORD')
+ .onCall(2) // ZWAVEJSUI_MQTT_TOPIC_PREFIX
+ .resolves('ZWAVEJSUI_MQTT_TOPIC_PREFIX')
+ .onCall(3) // ZWAVEJSUI_MQTT_TOPIC_WITH_LOCATION
+ .resolves('1');
+
+ await zwaveJSUIManager.connect();
+
+ expect(zwaveJSUIManager.mqttConnected).to.equal(false);
+ expect(zwaveJSUIManager.mqttExist).to.equal(true);
+ expect(zwaveJSUIManager.mqttRunning).to.equal(true);
+ expect(zwaveJSUIManager.zwaveJSUIExist).to.equal(true);
+ expect(zwaveJSUIManager.zwaveJSUIRunning).to.equal(true);
+ expect(zwaveJSUIManager.mqttTopicPrefix).to.equal('ZWAVEJSUI_MQTT_TOPIC_PREFIX');
+ expect(zwaveJSUIManager.mqttTopicWithLocation).to.equal(true);
+ });
+});
+
+describe('zwaveJSUIManager mqtt event', () => {
+ let gladys;
+ let zwaveJSUIManager;
+
+ before(() => {
+ gladys = {
+ event,
+ service: {
+ getService: () => {
+ return {
+ list: () =>
+ Promise.resolve([
+ {
+ path: DRIVER_PATH,
+ },
+ ]),
+ };
+ },
+ },
+ variable: {
+ getValue: fake.resolves(true),
+ setValue: fake.resolves(true),
+ },
+ system: {
+ isDocker: fake.resolves(true),
+ },
+ };
+ zwaveJSUIManager = new ZwaveJSUIManager(gladys, mqtt, ZWAVEJSUI_SERVICE_ID);
+ zwaveJSUIManager.installMqttContainer = fake.returns(true);
+ zwaveJSUIManager.installZwaveJSUIContainer = fake.returns(true);
+ });
+
+ beforeEach(() => {
+ sinon.reset();
+ zwaveJSUIManager.mqttExist = true;
+ zwaveJSUIManager.mqttRunning = true;
+ zwaveJSUIManager.mqttConnected = true;
+ zwaveJSUIManager.zwaveJSUIExist = true;
+ zwaveJSUIManager.zwaveJSUIRunning = true;
+ zwaveJSUIManager.scanInProgress = false;
+ zwaveJSUIManager.usbConfigured = true;
+ });
+
+ it('should handle MQTT error event', async () => {
+ gladys.variable.getValue = sinon.stub();
+ gladys.variable.getValue
+ .onFirstCall() // EXTERNAL_ZWAVEJSUI
+ .resolves('1')
+ .onSecondCall() // DRIVER_PATH
+ .resolves(DRIVER_PATH);
+
+ await zwaveJSUIManager.connect();
+ zwaveJSUIManager.mqttClient.emit('error', 'An error occured');
+
+ assert.calledWithExactly(zwaveJSUIManager.eventManager.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.MQTT_ERROR,
+ payload: 'An error occured',
+ });
+ expect(zwaveJSUIManager.mqttConnected).to.equal(false);
+ expect(zwaveJSUIManager.scanInProgress).to.equal(false);
+ });
+
+ it('should handle MQTT authentication error event', async () => {
+ gladys.variable.getValue = sinon.stub();
+ gladys.variable.getValue
+ .onFirstCall() // EXTERNAL_ZWAVEJSUI
+ .resolves('1')
+ .onSecondCall() // DRIVER_PATH
+ .resolves(DRIVER_PATH);
+
+ await zwaveJSUIManager.connect();
+ zwaveJSUIManager.mqttClient.emit('error', { code: 5 });
+
+ assert.calledWithExactly(zwaveJSUIManager.eventManager.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.MQTT_ERROR,
+ payload: { code: 5 },
+ });
+ assert.called(zwaveJSUIManager.disconnect);
+ expect(zwaveJSUIManager.mqttConnected).to.equal(false);
+ expect(zwaveJSUIManager.scanInProgress).to.equal(false);
+ });
+
+ it('should handle MQTT offline event', async () => {
+ gladys.variable.getValue = sinon.stub();
+ gladys.variable.getValue
+ .onFirstCall() // EXTERNAL_ZWAVEJSUI
+ .resolves('1')
+ .onSecondCall() // DRIVER_PATH
+ .resolves(DRIVER_PATH);
+
+ await zwaveJSUIManager.connect();
+ zwaveJSUIManager.mqttClient.emit('offline');
+
+ assert.calledWithExactly(zwaveJSUIManager.eventManager.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.MQTT_ERROR,
+ payload: 'DISCONNECTED',
+ });
+ expect(zwaveJSUIManager.mqttConnected).to.equal(false);
+ expect(zwaveJSUIManager.scanInProgress).to.equal(false);
+ });
+
+ it('should handle MQTT message event', async () => {
+ gladys.variable.getValue = sinon.stub();
+ gladys.variable.getValue
+ .onFirstCall() // EXTERNAL_ZWAVEJSUI
+ .resolves('1')
+ .onSecondCall() // DRIVER_PATH
+ .resolves(DRIVER_PATH);
+
+ await zwaveJSUIManager.connect();
+ zwaveJSUIManager.mqttClient.emit('message', 'topic', Buffer.from('{}'));
+
+ assert.calledWithExactly(zwaveJSUIManager.handleMqttMessage, 'topic', '{}');
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/commands/disconnect.test.js b/server/test/services/zwave-js-ui/lib/commands/disconnect.test.js
new file mode 100644
index 0000000000..aba79c8578
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/commands/disconnect.test.js
@@ -0,0 +1,93 @@
+const sinon = require('sinon');
+
+const { expect } = require('chai');
+
+const { assert, fake } = sinon;
+const EventEmitter = require('events');
+
+const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants');
+const ZwaveJSUIManager = require('../../../../../services/zwave-js-ui/lib');
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+const DRIVER_PATH = 'DRIVER_PATH';
+
+const event = {
+ emit: fake.resolves(null),
+};
+
+const eventMqtt = new EventEmitter();
+
+const mqttClient = Object.assign(eventMqtt, {
+ subscribe: fake.resolves(null),
+ publish: fake.returns(true),
+ end: fake.resolves(true),
+ removeAllListeners: fake.resolves(true),
+});
+
+const mqtt = {
+ connect: fake.returns(mqttClient),
+};
+
+describe('zwaveJSUIManager commands', () => {
+ let gladys;
+ let zwaveJSUIManager;
+
+ before(() => {
+ gladys = {
+ event,
+ service: {
+ getService: () => {
+ return {
+ list: () =>
+ Promise.resolve([
+ {
+ path: DRIVER_PATH,
+ },
+ ]),
+ };
+ },
+ },
+ variable: {
+ getValue: fake.resolves(true),
+ setValue: fake.resolves(true),
+ },
+ system: {
+ isDocker: fake.resolves(true),
+ },
+ };
+ zwaveJSUIManager = new ZwaveJSUIManager(gladys, mqtt, ZWAVEJSUI_SERVICE_ID);
+ zwaveJSUIManager.installMqttContainer = fake.returns(true);
+ zwaveJSUIManager.installZwaveJSUIContainer = fake.returns(true);
+ });
+
+ beforeEach(() => {
+ sinon.reset();
+ });
+
+ it('should disconnect from zwave-js-ui external instance', async () => {
+ zwaveJSUIManager.mqttConnected = true;
+ zwaveJSUIManager.mqttClient = mqttClient;
+
+ await zwaveJSUIManager.disconnect();
+
+ assert.calledOnceWithExactly(zwaveJSUIManager.eventManager.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.calledOnce(mqttClient.end);
+ assert.calledOnce(mqttClient.removeAllListeners);
+ expect(zwaveJSUIManager.mqttConnected).to.equal(false);
+ expect(zwaveJSUIManager.scanInProgress).to.equal(false);
+ });
+
+ it('should disconnect again from zwave-js-ui external instance', async () => {
+ zwaveJSUIManager.mqttConnected = false;
+
+ await zwaveJSUIManager.disconnect();
+
+ assert.notCalled(zwaveJSUIManager.eventManager.emit);
+ assert.notCalled(mqttClient.end);
+ assert.notCalled(mqttClient.removeAllListeners);
+ expect(zwaveJSUIManager.mqttConnected).to.equal(false);
+ expect(zwaveJSUIManager.scanInProgress).to.equal(false);
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/commands/getConfiguration.test.js b/server/test/services/zwave-js-ui/lib/commands/getConfiguration.test.js
new file mode 100644
index 0000000000..2e43d18ed2
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/commands/getConfiguration.test.js
@@ -0,0 +1,96 @@
+const sinon = require('sinon');
+
+const { assert } = sinon;
+const ZwaveJSUIManager = require('../../../../../services/zwave-js-ui/lib');
+
+const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee';
+const gladys = {
+ variable: {},
+};
+
+describe('zwave-js-ui getConfiguration', () => {
+ // PREPARE
+ const zwaveJSUIManager = new ZwaveJSUIManager(gladys, null, serviceId);
+
+ beforeEach(() => {
+ sinon.reset();
+ });
+
+ it('it should getConfiguration not external', async () => {
+ // PREPARE
+ const getValueStub = sinon.stub();
+ getValueStub
+ .onCall(0)
+ .returns('0')
+ .onCall(1)
+ .returns('mqttUrl')
+ .onCall(2)
+ .returns('mqttUsername')
+ .onCall(3)
+ .returns('mqttPassword')
+ .onCall(4)
+ .returns('mqttTopicPrefix')
+ .onCall(5)
+ .returns('mqttTopicWithLocation')
+ .onCall(6)
+ .returns('driverPath')
+ .onCall(7)
+ .returns('s2UnauthenticatedKey')
+ .onCall(8)
+ .returns('s2AuthenticatedKey')
+ .onCall(9)
+ .returns('s2AccessControlKey')
+ .onCall(10)
+ .returns('s0LegacyKey');
+ zwaveJSUIManager.gladys.variable.getValue = getValueStub;
+ // EXECUTE
+ const configuration = await zwaveJSUIManager.getConfiguration();
+ // ASSERT
+ assert.match(configuration, {
+ externalZwaveJSUI: false,
+ driverPath: 'driverPath',
+ s2UnauthenticatedKey: 's2UnauthenticatedKey',
+ s2AuthenticatedKey: 's2AuthenticatedKey',
+ s2AccessControlKey: 's2AccessControlKey',
+ s0LegacyKey: 's0LegacyKey',
+ });
+ });
+
+ it('it should getConfiguration external', async () => {
+ // PREPARE
+ const getValueStub = sinon.stub();
+ getValueStub
+ .onCall(0)
+ .returns('1')
+ .onCall(1)
+ .returns('mqttUrl')
+ .onCall(2)
+ .returns('mqttUsername')
+ .onCall(3)
+ .returns('mqttPassword')
+ .onCall(4)
+ .returns('mqttTopicPrefix')
+ .onCall(5)
+ .returns('mqttTopicWithLocation')
+ .onCall(6)
+ .returns('driverPath')
+ .onCall(7)
+ .returns('s2UnauthenticatedKey')
+ .onCall(8)
+ .returns('s2AuthenticatedKey')
+ .onCall(9)
+ .returns('s2AuthenticatedKey')
+ .onCall(10)
+ .returns('s2AuthenticatedKey');
+ zwaveJSUIManager.gladys.variable.getValue = getValueStub;
+ // EXECUTE
+ const configuration = await zwaveJSUIManager.getConfiguration();
+ // ASSERT
+ assert.match(configuration, {
+ externalZwaveJSUI: true,
+ mqttUrl: 'mqttUrl',
+ mqttUsername: 'mqttUsername',
+ mqttPassword: 'mqttPassword',
+ });
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/commands/getNodes.test.js b/server/test/services/zwave-js-ui/lib/commands/getNodes.test.js
new file mode 100644
index 0000000000..dc9304ba47
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/commands/getNodes.test.js
@@ -0,0 +1,478 @@
+const sinon = require('sinon');
+
+const { expect } = require('chai');
+
+const { fake } = sinon;
+
+const ZwaveJSUIManager = require('../../../../../services/zwave-js-ui/lib');
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+
+describe('zwaveJSUIManager getNodes', () => {
+ let gladys;
+ let zwaveJSUIManager;
+
+ before(() => {
+ gladys = {
+ stateManager: {
+ get: fake.returns(null),
+ },
+ };
+ zwaveJSUIManager = new ZwaveJSUIManager(gladys, null, ZWAVEJSUI_SERVICE_ID);
+ });
+
+ afterEach(() => {
+ sinon.reset();
+ });
+
+ it('should receive node feature Motion 113', () => {
+ zwaveJSUIManager.nodes = {
+ 1: {
+ nodeId: 1,
+ endpoints: [],
+ manufacturerId: 'manufacturerId',
+ product: 'product',
+ productType: 'productType',
+ productId: 'productId',
+ type: 'type',
+ firmwareVersion: 'firmwareVersion',
+ name: 'name',
+ loc: 'location',
+ status: 'status',
+ ready: true,
+ nodeType: 'nodeType',
+ classes: {
+ 113: {
+ 0: {
+ 'Home_Security-Motion_sensor_status': {
+ genre: 'user',
+ label: 'label',
+ readOnly: true,
+ commandClass: 113,
+ endpoint: 0,
+ property: 'Home_Security-Motion_sensor_status',
+ },
+ },
+ },
+ },
+ },
+ };
+ const devices = zwaveJSUIManager.getNodes();
+ expect(devices).to.deep.equal([
+ {
+ service_id: ZWAVEJSUI_SERVICE_ID,
+ external_id: 'zwave-js-ui:node_id:1',
+ selector: 'zwave-js-ui-node-1-name-1',
+ model: 'product firmwareVersion',
+ name: 'name - 1',
+ ready: true,
+ features: [
+ {
+ name: 'Détecteur de présence',
+ selector: 'zwave-js-ui-node-1-home-security-motion-sensor-status-113-0-label',
+ category: 'motion-sensor',
+ external_id: 'zwave-js-ui:node_id:1:comclass:113:endpoint:0:property:Home_Security-Motion_sensor_status',
+ type: 'binary',
+ min: undefined,
+ max: undefined,
+ unit: null,
+ read_only: true,
+ has_feedback: true,
+ last_value: 0,
+ },
+ ],
+ params: [
+ { name: 'node-id', value: 1 },
+ { name: 'node-product', value: 'product' },
+ { name: 'node-room', value: 'location' },
+ { name: 'node-classes', value: '113' },
+ ],
+ },
+ ]);
+ });
+
+ it('should receive node feature Temperature', () => {
+ zwaveJSUIManager.nodes = {
+ 1: {
+ nodeId: 1,
+ endpoints: [],
+ manufacturerId: 'manufacturerId',
+ product: 'product',
+ productType: 'productType',
+ productId: 'productId',
+ type: 'type',
+ firmwareVersion: 'firmwareVersion',
+ name: 'name',
+ loc: 'location',
+ status: 'status',
+ ready: true,
+ nodeType: 'nodeType',
+ classes: {
+ 49: {
+ 0: {
+ Air_temperature: {
+ genre: 'user',
+ label: 'label',
+ min: -20,
+ max: 40,
+ unit: '°C',
+ readOnly: true,
+ commandClass: 49,
+ endpoint: 0,
+ property: 'Air_temperature',
+ },
+ },
+ },
+ },
+ },
+ };
+ const devices = zwaveJSUIManager.getNodes();
+ expect(devices).to.deep.equal([
+ {
+ service_id: ZWAVEJSUI_SERVICE_ID,
+ external_id: 'zwave-js-ui:node_id:1',
+ selector: 'zwave-js-ui-node-1-name-1',
+ model: 'product firmwareVersion',
+ name: 'name - 1',
+ ready: true,
+ features: [
+ {
+ name: 'label',
+ selector: 'zwave-js-ui-node-1-air-temperature-49-0-label',
+ category: 'temperature-sensor',
+ type: 'decimal',
+ external_id: 'zwave-js-ui:node_id:1:comclass:49:endpoint:0:property:Air_temperature',
+ read_only: true,
+ unit: 'celsius',
+ has_feedback: true,
+ last_value: undefined,
+ min: -30,
+ max: 50,
+ },
+ ],
+ params: [
+ { name: 'node-id', value: 1 },
+ { name: 'node-product', value: 'product' },
+ { name: 'node-room', value: 'location' },
+ { name: 'node-classes', value: '49' },
+ ],
+ },
+ ]);
+ });
+
+ it('should receive node with param', () => {
+ zwaveJSUIManager.nodes = {
+ '1': {
+ nodeId: 1,
+ endpoints: [],
+ manufacturerId: 'manufacturerId',
+ product: 'product',
+ productType: 'productType',
+ productId: 'productId',
+ type: 'type',
+ firmwareVersion: 'firmwareVersion',
+ name: 'name',
+ loc: 'location',
+ status: 'status',
+ ready: true,
+ nodeType: 'nodeType',
+ classes: {
+ 112: {
+ 0: {
+ 'Parameter 1': {
+ genre: 'config',
+ label: 'label',
+ value_id: 'value_id',
+ value: 'value',
+ },
+ },
+ },
+ },
+ },
+ };
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should receive 3 nodes feature Switch', () => {
+ zwaveJSUIManager.nodes = {
+ 1: {
+ nodeId: 1,
+ endpoints: [
+ {
+ index: 1,
+ },
+ {
+ index: 2,
+ },
+ ],
+ manufacturerId: 'manufacturerId',
+ product: 'product',
+ productType: 'productType',
+ productId: 'productId',
+ type: 'type',
+ firmwareVersion: 'firmwareVersion',
+ name: 'name',
+ loc: 'location',
+ status: 'status',
+ ready: true,
+ nodeType: 'nodeType',
+ classes: {
+ 37: {
+ 0: {
+ targetValue: {
+ genre: 'user',
+ label: 'label',
+ min: 0,
+ max: 1,
+ readOnly: false,
+ commandClass: 37,
+ endpoint: 0,
+ property: 'targetValue',
+ },
+ },
+ 1: {
+ targetValue: {
+ genre: 'user',
+ label: 'label',
+ min: 0,
+ max: 1,
+ readOnly: false,
+ commandClass: 37,
+ endpoint: 1,
+ property: 'targetValue',
+ },
+ },
+ 2: {
+ targetValue: {
+ genre: 'user',
+ label: 'label',
+ min: 0,
+ max: 1,
+ readOnly: false,
+ commandClass: 37,
+ endpoint: 2,
+ property: 'targetValue',
+ },
+ },
+ },
+ },
+ },
+ };
+ const devices = zwaveJSUIManager.getNodes();
+ expect(devices).to.deep.equal([
+ {
+ service_id: ZWAVEJSUI_SERVICE_ID,
+ external_id: 'zwave-js-ui:node_id:1',
+ model: 'product firmwareVersion',
+ name: 'name - 1',
+ selector: 'zwave-js-ui-node-1-name-1',
+ ready: true,
+ features: [
+ {
+ name: 'label',
+ selector: 'zwave-js-ui-node-1-targetvalue-37-0-label',
+ category: 'switch',
+ type: 'binary',
+ external_id: 'zwave-js-ui:node_id:1:comclass:37:endpoint:0:property:targetValue',
+ read_only: true,
+ has_feedback: true,
+ last_value: 0,
+ min: 0,
+ max: 1,
+ unit: null,
+ },
+ ],
+ params: [
+ { name: 'node-id', value: 1 },
+ { name: 'node-product', value: 'product' },
+ { name: 'node-room', value: 'location' },
+ { name: 'node-classes', value: '37' },
+ ],
+ },
+ {
+ service_id: ZWAVEJSUI_SERVICE_ID,
+ external_id: 'zwave-js-ui:node_id:1_1',
+ model: 'product firmwareVersion',
+ name: 'name - 1 [1]',
+ selector: 'zwave-js-ui-node-1-name-1-1',
+ ready: true,
+ features: [
+ {
+ name: 'label [1]',
+ selector: 'zwave-js-ui-node-1-targetvalue-37-1-label',
+ category: 'switch',
+ type: 'binary',
+ external_id: 'zwave-js-ui:node_id:1:comclass:37:endpoint:1:property:targetValue',
+ read_only: true,
+ has_feedback: true,
+ last_value: 0,
+ min: 0,
+ max: 1,
+ unit: null,
+ },
+ ],
+ params: [
+ { name: 'node-id', value: 1 },
+ { name: 'node-product', value: 'product' },
+ { name: 'node-room', value: 'location' },
+ { name: 'node-classes', value: '37' },
+ ],
+ },
+ {
+ service_id: ZWAVEJSUI_SERVICE_ID,
+ external_id: 'zwave-js-ui:node_id:1_2',
+ name: 'name - 1 [2]',
+ model: 'product firmwareVersion',
+ selector: 'zwave-js-ui-node-1-name-1-2',
+ ready: true,
+ features: [
+ {
+ name: 'label [2]',
+ selector: 'zwave-js-ui-node-1-targetvalue-37-2-label',
+ category: 'switch',
+ type: 'binary',
+ external_id: 'zwave-js-ui:node_id:1:comclass:37:endpoint:2:property:targetValue',
+ read_only: true,
+ has_feedback: true,
+ last_value: 0,
+ min: 0,
+ max: 1,
+ unit: null,
+ },
+ ],
+ params: [
+ { name: 'node-id', value: 1 },
+ { name: 'node-product', value: 'product' },
+ { name: 'node-room', value: 'location' },
+ { name: 'node-classes', value: '37' },
+ ],
+ },
+ ]);
+ });
+
+ it('should receive node without feature/params', () => {
+ zwaveJSUIManager.nodes = {
+ '1': {
+ nodeId: 1,
+ endpoints: [],
+ manufacturerId: 'manufacturerId',
+ product: 'product',
+ productType: 'productType',
+ productId: 'productId',
+ type: 'type',
+ firmwareVersion: 'firmwareVersion',
+ name: 'name',
+ loc: 'location',
+ status: 'status',
+ ready: true,
+ nodeType: 'nodeType',
+ classes: {},
+ },
+ };
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should return no-feature node', () => {
+ zwaveJSUIManager.mqttConnected = true;
+ zwaveJSUIManager.nodes = {
+ '1': {
+ nodeId: 1,
+ endpoints: [], // No split
+ manufacturerId: 'manufacturerId',
+ product: 'product',
+ productType: 'productType',
+ productId: 'productId',
+ type: 'type',
+ firmwareVersion: 'firmwareVersion',
+ name: 'name',
+ loc: 'location',
+ status: 'status',
+ ready: true,
+ nodeType: 'nodeType',
+ classes: {},
+ },
+ };
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should return FIBARO_DIMMER2 node', () => {
+ zwaveJSUIManager.mqttConnected = true;
+ zwaveJSUIManager.nodes = {
+ '1': {
+ nodeId: 1,
+ endpoints: [
+ {
+ index: 1,
+ },
+ {
+ index: 2,
+ },
+ ],
+ manufacturerId: 'manufacturerId',
+ product: '271-4096-258',
+ productType: 'productType',
+ productId: 'productId',
+ type: 'type',
+ firmwareVersion: 'firmwareVersion',
+ name: 'name',
+ loc: 'location',
+ status: 'status',
+ ready: true,
+ nodeType: 'nodeType',
+ classes: {
+ 38: {
+ 0: {},
+ 1: {
+ targetValue: {
+ genre: 'user',
+ label: 'label',
+ min: 0,
+ max: 100,
+ readOnly: false,
+ commandClass: 38,
+ endpoint: 1,
+ property: 'targetValue',
+ },
+ },
+ 2: {},
+ },
+ },
+ },
+ };
+ const devices = zwaveJSUIManager.getNodes();
+ expect(devices).to.deep.equal([
+ {
+ service_id: ZWAVEJSUI_SERVICE_ID,
+ external_id: 'zwave-js-ui:node_id:1_1',
+ model: '271-4096-258 firmwareVersion',
+ name: 'name - 1 [1]',
+ selector: 'zwave-js-ui-node-1-name-1-1',
+ ready: true,
+ features: [
+ {
+ name: 'label [1]',
+ selector: 'zwave-js-ui-node-1-targetvalue-38-1-label',
+ category: 'switch',
+ type: 'dimmer',
+ external_id: 'zwave-js-ui:node_id:1:comclass:38:endpoint:1:property:targetValue',
+ read_only: true,
+ has_feedback: true,
+ last_value: undefined,
+ min: 0,
+ max: 100,
+ unit: null,
+ },
+ ],
+ params: [
+ { name: 'node-id', value: 1 },
+ { name: 'node-product', value: '271-4096-258' },
+ { name: 'node-room', value: 'location' },
+ { name: 'node-classes', value: '38' },
+ ],
+ },
+ ]);
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/commands/installMqttContainer.test.js b/server/test/services/zwave-js-ui/lib/commands/installMqttContainer.test.js
new file mode 100644
index 0000000000..f378205155
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/commands/installMqttContainer.test.js
@@ -0,0 +1,180 @@
+const sinon = require('sinon');
+
+const { assert, fake } = sinon;
+
+const proxyquire = require('proxyquire').noCallThru();
+const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants');
+
+const { installMqttContainer } = proxyquire('../../../../../services/zwave-js-ui/lib/commands/installMqttContainer', {
+ '../../../../utils/childProcess': { exec: fake.resolves(true) },
+});
+const ZwaveJSUIManager = proxyquire('../../../../../services/zwave-js-ui/lib', {
+ './commands/installMqttContainer': { installMqttContainer },
+});
+
+const event = {
+ emit: fake.resolves(null),
+};
+
+const container = {
+ id: 'docker-test',
+ state: 'running',
+};
+
+const containerStopped = {
+ id: 'docker-test',
+ state: 'stopped',
+};
+
+const gladys = {
+ event,
+ variable: {
+ setValue: fake.resolves(true),
+ getValue: fake.resolves(true),
+ },
+ system: {
+ getContainers: fake.resolves([containerStopped]),
+ stopContainer: fake.resolves(true),
+ pull: fake.resolves(true),
+ restartContainer: fake.resolves(true),
+ createContainer: fake.resolves(true),
+ exec: fake.resolves(true),
+ getGladysBasePath: fake.resolves({
+ basePathOnHost: '/var/lib/gladysassistant',
+ basePathOnContainer: '/var/lib/gladysassistant',
+ }),
+ },
+};
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+
+describe('zwave-js-ui installMqttContainer', () => {
+ // PREPARE
+ const zwaveJSUIManager = new ZwaveJSUIManager(gladys, null, ZWAVEJSUI_SERVICE_ID);
+
+ beforeEach(() => {
+ sinon.reset();
+ zwaveJSUIManager.zwavejsuiRunning = false;
+ zwaveJSUIManager.zwavejsuiExist = false;
+ });
+
+ it('it should restart MQTT container', async function Test() {
+ // PREPARE
+ this.timeout(6000);
+ // EXECUTE
+ await zwaveJSUIManager.installMqttContainer();
+ // ASSERT
+ assert.calledWith(gladys.system.restartContainer, container.id);
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.match(zwaveJSUIManager.mqttRunning, true);
+ assert.match(zwaveJSUIManager.mqttExist, true);
+ });
+
+ it('it should do nothing', async () => {
+ // PREPARE
+ gladys.system.getContainers = fake.resolves([container]);
+ // EXECUTE
+ await zwaveJSUIManager.installMqttContainer();
+ // ASSERT
+ assert.notCalled(gladys.system.restartContainer);
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.match(zwaveJSUIManager.mqttRunning, true);
+ assert.match(zwaveJSUIManager.mqttExist, true);
+ });
+
+ it('it should fail to start MQTT container', async () => {
+ // PREPARE
+ gladys.system.getContainers = fake.resolves([containerStopped]);
+ gladys.system.restartContainer = fake.throws(new Error('docker fail'));
+ // EXECUTE
+ try {
+ await zwaveJSUIManager.installMqttContainer();
+ assert.fail();
+ } catch (e) {
+ assert.match(e.message, 'docker fail');
+ }
+ // ASSERT
+ assert.calledWith(gladys.system.restartContainer, container.id);
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.match(zwaveJSUIManager.mqttRunning, false);
+ assert.match(zwaveJSUIManager.mqttExist, true);
+ gladys.system.restartContainer = fake.resolves(true);
+ });
+
+ it('it should fail to install MQTT container', async () => {
+ // PREPARE
+ gladys.system.getContainers = fake.resolves([]);
+ gladys.system.pull = fake.throws(new Error('docker fail pull'));
+ // EXECUTE
+ try {
+ await zwaveJSUIManager.installMqttContainer();
+ assert.fail();
+ } catch (e) {
+ assert.match(e.message, 'docker fail pull');
+ }
+ // ASSERT
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.match(zwaveJSUIManager.mqttRunning, false);
+ assert.match(zwaveJSUIManager.mqttExist, false);
+ });
+
+ it('it should install MQTT container', async function Test() {
+ // PREPARE
+ this.timeout(11000);
+ const getContainersStub = sinon.stub();
+ getContainersStub
+ .onFirstCall()
+ .resolves([])
+ .onSecondCall()
+ .resolves([container]);
+ gladys.system.getContainers = getContainersStub;
+ gladys.system.pull = fake.resolves(true);
+
+ // EXECUTE
+ await zwaveJSUIManager.installMqttContainer();
+ // ASSERT
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.calledOnce(gladys.system.createContainer);
+ assert.calledTwice(gladys.system.restartContainer);
+ assert.match(zwaveJSUIManager.mqttRunning, true);
+ assert.match(zwaveJSUIManager.mqttExist, true);
+ });
+ it('it should fail to configure MQTT container', async function Test() {
+ // PREPARE
+ this.timeout(11000);
+ const getContainersStub = sinon.stub();
+ getContainersStub
+ .onFirstCall()
+ .resolves([])
+ .onSecondCall()
+ .resolves([container]);
+ gladys.system.getContainers = getContainersStub;
+ gladys.system.restartContainer = fake.throws(new Error('docker fail restart'));
+
+ // EXECUTE
+ try {
+ await zwaveJSUIManager.installMqttContainer();
+ assert.fail();
+ } catch (e) {
+ assert.match(e.message, 'docker fail restart');
+ }
+ // ASSERT
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.calledOnce(gladys.system.createContainer);
+ assert.calledOnce(gladys.system.restartContainer);
+ assert.match(zwaveJSUIManager.mqttRunning, false);
+ assert.match(zwaveJSUIManager.mqttExist, true);
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/commands/installZwaveJSUIContainer.test.js b/server/test/services/zwave-js-ui/lib/commands/installZwaveJSUIContainer.test.js
new file mode 100644
index 0000000000..22bb4a5545
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/commands/installZwaveJSUIContainer.test.js
@@ -0,0 +1,197 @@
+const sinon = require('sinon');
+
+const { assert, fake } = sinon;
+
+const proxyquire = require('proxyquire').noCallThru();
+const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants');
+
+const { installZwaveJSUIContainer } = proxyquire(
+ '../../../../../services/zwave-js-ui/lib/commands/installZwaveJSUIContainer',
+ {
+ '../../../../utils/childProcess': { exec: fake.resolves(true) },
+ },
+);
+const ZwaveJSUIManager = proxyquire('../../../../../services/zwave-js-ui/lib', {
+ './commands/installZwaveJSUIContainer': { installZwaveJSUIContainer },
+});
+
+const event = {
+ emit: fake.resolves(null),
+};
+
+const container = {
+ id: 'docker-test',
+ state: 'running',
+};
+
+const containerStopped = {
+ id: 'docker-test',
+ state: 'stopped',
+};
+
+const gladys = {
+ event,
+ variable: {
+ setValue: fake.resolves(true),
+ getValue: fake.resolves(true),
+ },
+ system: {
+ getContainers: fake.resolves([containerStopped]),
+ stopContainer: fake.resolves(true),
+ pull: fake.resolves(true),
+ restartContainer: fake.resolves(true),
+ createContainer: fake.resolves(true),
+ getContainerDevices: fake.resolves([]),
+ exec: fake.resolves(true),
+ getGladysBasePath: fake.resolves({
+ basePathOnHost: '/var/lib/gladysassistant',
+ basePathOnContainer: '/var/lib/gladysassistant',
+ }),
+ },
+};
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+
+describe('zwave-js-ui installZwaveJSUIContainer', () => {
+ // PREPARE
+ const zwaveJSUIManager = new ZwaveJSUIManager(gladys, null, ZWAVEJSUI_SERVICE_ID);
+
+ beforeEach(() => {
+ sinon.reset();
+ zwaveJSUIManager.zwaveJSUIRunning = false;
+ zwaveJSUIManager.zwaveJSUIExist = false;
+ });
+
+ it('it should restart ZwaveJSUI container', async function Test() {
+ // PREPARE
+ this.timeout(11000);
+
+ // EXECUTE
+ await zwaveJSUIManager.installZwaveJSUIContainer();
+
+ // ASSERT
+ assert.calledWith(gladys.system.restartContainer, container.id);
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.match(zwaveJSUIManager.zwaveJSUIRunning, true);
+ assert.match(zwaveJSUIManager.zwaveJSUIExist, true);
+ });
+
+ it('it should update container and restart', async function Test() {
+ // PREPARE
+ this.timeout(6000);
+ gladys.system.getContainers = fake.resolves([container]);
+
+ // EXECUTE
+ await zwaveJSUIManager.installZwaveJSUIContainer();
+
+ // ASSERT
+ assert.calledWith(gladys.system.restartContainer, container.id);
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.match(zwaveJSUIManager.zwaveJSUIRunning, true);
+ assert.match(zwaveJSUIManager.zwaveJSUIExist, true);
+ });
+
+ it('it should fail to start ZwaveJSUI container', async function Test() {
+ // PREPARE
+ this.timeout(6000);
+ gladys.system.getContainers = fake.resolves([containerStopped]);
+ gladys.system.restartContainer = fake.throws(new Error('docker fail'));
+
+ // EXECUTE
+ try {
+ await zwaveJSUIManager.installZwaveJSUIContainer();
+ assert.fail();
+ } catch (e) {
+ assert.match(e.message, 'docker fail');
+ }
+
+ // ASSERT
+ assert.calledWith(gladys.system.restartContainer, container.id);
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.match(zwaveJSUIManager.zwaveJSUIRunning, false);
+ assert.match(zwaveJSUIManager.zwaveJSUIExist, false);
+ gladys.system.restartContainer = fake.resolves(true);
+ });
+
+ it('it should fail to install ZwaveJSUI container', async () => {
+ // PREPARE
+ gladys.system.getContainers = fake.resolves([]);
+ gladys.system.pull = fake.throws(new Error('docker fail pull'));
+
+ // EXECUTE
+ try {
+ await zwaveJSUIManager.installZwaveJSUIContainer();
+ assert.fail();
+ } catch (e) {
+ assert.match(e.message, 'docker fail pull');
+ }
+
+ // ASSERT
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.match(zwaveJSUIManager.zwaveJSUIRunning, false);
+ assert.match(zwaveJSUIManager.zwaveJSUIExist, false);
+ });
+
+ it('it should install ZwaveJSUI container', async function Test() {
+ // PREPARE
+ this.timeout(11000);
+ const getContainersStub = sinon.stub();
+ getContainersStub
+ .onFirstCall()
+ .resolves([])
+ .onSecondCall()
+ .resolves([container]);
+ gladys.system.getContainers = getContainersStub;
+ gladys.system.pull = fake.resolves(true);
+
+ // EXECUTE
+ await zwaveJSUIManager.installZwaveJSUIContainer();
+
+ // ASSERT
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.calledOnce(gladys.system.createContainer);
+ assert.calledOnce(gladys.system.restartContainer);
+ assert.match(zwaveJSUIManager.zwaveJSUIRunning, true);
+ assert.match(zwaveJSUIManager.zwaveJSUIExist, true);
+ });
+
+ it('it should fail to configure ZwaveJSUI container', async function Test() {
+ // PREPARE
+ this.timeout(11000);
+ const getContainersStub = sinon.stub();
+ getContainersStub
+ .onFirstCall()
+ .resolves([])
+ .onSecondCall()
+ .resolves([container]);
+ gladys.system.getContainers = getContainersStub;
+ gladys.system.restartContainer = fake.throws(new Error('docker fail restart'));
+
+ // EXECUTE
+ try {
+ await zwaveJSUIManager.installZwaveJSUIContainer();
+ assert.fail();
+ } catch (e) {
+ assert.match(e.message, 'docker fail restart');
+ }
+
+ // ASSERT
+ assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ assert.calledOnce(gladys.system.createContainer);
+ assert.calledOnce(gladys.system.restartContainer);
+ assert.match(zwaveJSUIManager.zwaveJSUIRunning, false);
+ assert.match(zwaveJSUIManager.zwaveJSUIExist, true);
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/commands/updateConfiguration.test.js b/server/test/services/zwave-js-ui/lib/commands/updateConfiguration.test.js
new file mode 100644
index 0000000000..00dc4f330e
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/commands/updateConfiguration.test.js
@@ -0,0 +1,154 @@
+const sinon = require('sinon');
+const ZwaveJSUIManager = require('../../../../../services/zwave-js-ui/lib');
+const { CONFIGURATION, DEFAULT } = require('../../../../../services/zwave-js-ui/lib/constants');
+
+const { fake } = sinon;
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+
+describe('zwaveJSUIManager commands', () => {
+ let gladys;
+ let zwaveJSUIManager;
+
+ before(() => {
+ gladys = {
+ variable: {
+ getValue: fake.resolves(true),
+ setValue: fake.resolves(true),
+ },
+ };
+ zwaveJSUIManager = new ZwaveJSUIManager(gladys, null, ZWAVEJSUI_SERVICE_ID);
+ });
+
+ beforeEach(() => {
+ sinon.reset();
+ zwaveJSUIManager.mqttExist = false;
+ zwaveJSUIManager.mqttRunning = false;
+ zwaveJSUIManager.mqttConnected = false;
+ zwaveJSUIManager.zwaveJSUIExist = false;
+ zwaveJSUIManager.zwaveJSUIRunning = false;
+ zwaveJSUIManager.zwaveJSUIConnected = false;
+ zwaveJSUIManager.scanInProgress = false;
+ zwaveJSUIManager.usbConfigured = false;
+ });
+
+ it('should updateConfiguration', () => {
+ const configuration = {
+ externalZwaveJSUI: false,
+ driverPath: 'driverPath',
+ mqttUrl: 'mqttUrl',
+ mqttUsername: 'mqttUsername',
+ mqttPassword: 'mqttPassword',
+ mqttTopicPrefix: 'mqttTopicPrefix',
+ mqttTopicWithLocation: true,
+ s2UnauthenticatedKey: 's2UnauthenticatedKey',
+ s2AuthenticatedKey: 's2AuthenticatedKey',
+ s2AccessControlKey: 's2AccessControlKey',
+ s0LegacyKey: 's0LegacyKey',
+ };
+
+ const setValueStub = sinon.stub();
+ setValueStub.returns(true);
+ zwaveJSUIManager.gladys.variable.setValue = setValueStub;
+
+ zwaveJSUIManager.updateConfiguration(configuration);
+
+ setValueStub.calledOnceWith(CONFIGURATION.EXTERNAL_ZWAVEJSUI, '1', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.DRIVER_PATH, 'driverPath', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(
+ CONFIGURATION.ZWAVEJSUI_MQTT_URL,
+ DEFAULT.ZWAVEJSUI_MQTT_URL_VALUE,
+ ZWAVEJSUI_SERVICE_ID,
+ );
+
+ setValueStub.calledOnceWith(
+ CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME,
+ DEFAULT.ZWAVEJSUI_MQTT_USERNAME_VALUE,
+ ZWAVEJSUI_SERVICE_ID,
+ );
+
+ setValueStub.calledOnceWith(CONFIGURATION.ZWAVEJSUI_MQTT_PASSWORD, 'mqttPassword', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.S2_UNAUTHENTICATED, 's2UnauthenticatedKey', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.S2_AUTHENTICATED, 's2AuthenticatedKey', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.S2_ACCESS_CONTROL, 's2AccessControlKey', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.S0_LEGACY, 's0LegacyKey', ZWAVEJSUI_SERVICE_ID);
+ });
+
+ it('should updateConfiguration external', () => {
+ const configuration = {
+ externalZwaveJSUI: true,
+ driverPath: 'driverPath',
+ mqttUrl: 'mqttUrl',
+ mqttUsername: 'mqttUsername',
+ mqttPassword: 'mqttPassword',
+ mqttTopicPrefix: 'mqttTopicPrefix',
+ mqttTopicWithLocation: true,
+ s2UnauthenticatedKey: 's2UnauthenticatedKey',
+ s2AuthenticatedKey: 's2AuthenticatedKey',
+ s2AccessControlKey: 's2AccessControlKey',
+ s0LegacyKey: 's0LegacyKey',
+ };
+
+ const setValueStub = sinon.stub();
+ setValueStub.returns(true);
+ zwaveJSUIManager.gladys.variable.setValue = setValueStub;
+
+ zwaveJSUIManager.updateConfiguration(configuration);
+
+ setValueStub.calledOnceWith(CONFIGURATION.EXTERNAL_ZWAVEJSUI, '1', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.ZWAVEJSUI_MQTT_URL, 'mqttUrl', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME, 'mqttUsername', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.ZWAVEJSUI_MQTT_PASSWORD, 'mqttPassword', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_PREFIX, 'mqttTopicPrefix', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_WITH_LOCATION, '1', ZWAVEJSUI_SERVICE_ID);
+ });
+
+ it('should updateConfiguration external - reset', () => {
+ const configuration = {
+ externalZwaveJSUI: true,
+ driverPath: 'driverPath',
+ mqttUrl: 'mqttUrl',
+ mqttUsername: '',
+ mqttPassword: '',
+ mqttTopicPrefix: 'mqttTopicPrefix',
+ mqttTopicWithLocation: true,
+ s2UnauthenticatedKey: 's2UnauthenticatedKey',
+ s2AuthenticatedKey: 's2AuthenticatedKey',
+ s2AccessControlKey: 's2AccessControlKey',
+ s0LegacyKey: 's0LegacyKey',
+ };
+
+ const setValueStub = sinon.stub();
+ setValueStub.returns(true);
+ zwaveJSUIManager.gladys.variable.setValue = setValueStub;
+
+ const destroyStub = sinon.stub();
+ setValueStub.returns(true);
+ zwaveJSUIManager.gladys.variable.destroy = destroyStub;
+
+ zwaveJSUIManager.updateConfiguration(configuration);
+
+ setValueStub.calledOnceWith(CONFIGURATION.EXTERNAL_ZWAVEJSUI, '1', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.ZWAVEJSUI_MQTT_URL, 'mqttUrl', ZWAVEJSUI_SERVICE_ID);
+
+ destroyStub.calledOnceWith(CONFIGURATION.ZWAVEJSUI_MQTT_USERNAME, ZWAVEJSUI_SERVICE_ID);
+
+ destroyStub.calledOnceWith(CONFIGURATION.ZWAVEJSUI_MQTT_PASSWORD, ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_PREFIX, 'mqttTopicPrefix', ZWAVEJSUI_SERVICE_ID);
+
+ setValueStub.calledOnceWith(CONFIGURATION.ZWAVEJSUI_MQTT_TOPIC_WITH_LOCATION, '1', ZWAVEJSUI_SERVICE_ID);
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/events/handleMqttMessage.test.js b/server/test/services/zwave-js-ui/lib/events/handleMqttMessage.test.js
new file mode 100644
index 0000000000..5cbaabf4ec
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/events/handleMqttMessage.test.js
@@ -0,0 +1,301 @@
+const { expect } = require('chai');
+const sinon = require('sinon');
+const ZwaveJSUIManager = require('../../../../../services/zwave-js-ui/lib');
+const { DEFAULT } = require('../../../../../services/zwave-js-ui/lib/constants');
+const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants');
+
+const { assert, fake } = sinon;
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+const event = {
+ emit: fake.resolves(null),
+};
+const mqtt = fake.resolves(null);
+
+describe('zwave gladys node event', () => {
+ let gladys;
+ let zwaveJSUIManager;
+ let node;
+ let clock;
+
+ before(() => {
+ gladys = {
+ event,
+ };
+ zwaveJSUIManager = new ZwaveJSUIManager(gladys, mqtt, ZWAVEJSUI_SERVICE_ID);
+ zwaveJSUIManager.mqttConnected = true;
+ });
+
+ beforeEach(() => {
+ node = {
+ id: 1,
+ ready: true,
+ classes: {},
+ };
+ zwaveJSUIManager.nodes = {
+ '1': node,
+ };
+ zwaveJSUIManager.scanInProgress = false;
+ zwaveJSUIManager.valueUpdated = fake.returns(null);
+ zwaveJSUIManager.nodeReady = fake.returns(null);
+ zwaveJSUIManager.valueAdded = fake.returns(null);
+ zwaveJSUIManager.scanComplete = fake.resolves(null);
+ zwaveJSUIManager.scanNetwork = fake.resolves(null);
+ // sinon.reset();
+ clock = sinon.useFakeTimers();
+ });
+
+ afterEach(() => {
+ clock.restore();
+ sinon.reset();
+ });
+
+ it('should default _CLIENTS', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/_CLIENTS`, null);
+ assert.notCalled(zwaveJSUIManager.valueUpdated);
+ });
+
+ it('should default status', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/???/status`, null);
+ assert.notCalled(zwaveJSUIManager.valueUpdated);
+ });
+
+ it('should default nodeinfo', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/???/nodeinfo`, null);
+ assert.notCalled(zwaveJSUIManager.valueUpdated);
+ });
+
+ it('should get zwaveJSUI Version', () => {
+ const message = {
+ value: 'version',
+ };
+ zwaveJSUIManager.handleMqttMessage(
+ `${this.mqttTopicPrefix}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/version`,
+ JSON.stringify(message),
+ );
+ assert.notCalled(zwaveJSUIManager.valueUpdated);
+ expect(zwaveJSUIManager.zwaveJSUIVersion).to.equal('version');
+ });
+
+ it('should default scanInProgress', () => {
+ zwaveJSUIManager.scanInProgress = true;
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/???`, null);
+ assert.notCalled(zwaveJSUIManager.valueUpdated);
+ });
+
+ it('should not managed set', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/nodeId/commandClass/endpoint/propertyName/set`, null);
+ assert.notCalled(zwaveJSUIManager.valueUpdated);
+ });
+
+ it('should not managed not supported commandClass', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/nodeId/112/endpoint/propertyName`, null);
+ assert.notCalled(zwaveJSUIManager.valueUpdated);
+ });
+
+ it('should update node empty message', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/nodeID_1/0/0/propertyName/propertyKey`, '');
+ assert.calledOnceWithExactly(
+ zwaveJSUIManager.valueUpdated,
+ {
+ id: 1,
+ },
+ {
+ commandClass: 0,
+ endpoint: 0,
+ property: 'propertyName',
+ propertyKey: 'propertyKey',
+ newValue: '',
+ },
+ );
+ });
+
+ it('should default node true message', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/nodeID_1/0/0/propertyName/propertyKey`, 'true');
+ assert.calledOnce(zwaveJSUIManager.valueUpdated);
+ /* assert.calledOnceWithExactly(zwaveJSUIManager.valueUpdated, {
+ id: 1,
+ },
+ {
+ commandClass: 0,
+ endpoint: 0,
+ property: 'propertyName',
+ propertyKey: 'propertyKey',
+ newValue: true,
+ }); */
+ });
+
+ it('should default node false message', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/nodeID_1/0/0/propertyName/propertyKey`, 'false');
+ assert.calledOnce(zwaveJSUIManager.valueUpdated);
+ /* assert.calledOnceWithExactly(zwaveJSUIManager.valueUpdated, {
+ id: 1,
+ },
+ {
+ commandClass: 0,
+ endpoint: 0,
+ property: 'propertyName',
+ propertyKey: 'propertyKey',
+ newValue: false,
+ }); */
+ });
+
+ it('should default node number message', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/nodeID_1/0/0/propertyName/propertyKey`, '1');
+ assert.calledOnce(zwaveJSUIManager.valueUpdated);
+ /* assert.calledOnceWithExactly(zwaveJSUIManager.valueUpdated, {
+ id: 1,
+ },
+ {
+ commandClass: 0,
+ endpoint: 0,
+ property: 'propertyName',
+ propertyKey: 'propertyKey',
+ newValue: 1,
+ }); */
+ });
+
+ it('should not managed message - object message', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/nodeID_1/0/0/propertyName/propertyKey`, '{}');
+ assert.notCalled(zwaveJSUIManager.valueUpdated);
+ });
+
+ it('should not managed message - not a number message', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/nodeID_1/0/0/propertyName/propertyKey`, '???');
+ assert.notCalled(zwaveJSUIManager.valueUpdated);
+ });
+
+ it('should managed string message', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/nodeID_1/38/0/propertyName/propertyKey`, '"???"');
+ assert.calledWithExactly(
+ zwaveJSUIManager.valueUpdated,
+ {
+ id: 1,
+ },
+ {
+ commandClass: 38,
+ endpoint: 0,
+ property: 'propertyName',
+ propertyKey: 'propertyKey',
+ newValue: '"???"',
+ },
+ );
+ });
+
+ it('should shift node location', () => {
+ zwaveJSUIManager.mqttTopicWithLocation = true;
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/location/nodeId_1/48/0/propertyName`, 0);
+ zwaveJSUIManager.mqttTopicWithLocation = false;
+ assert.calledOnceWithExactly(
+ zwaveJSUIManager.valueUpdated,
+ {
+ id: 1,
+ },
+ {
+ commandClass: 48,
+ endpoint: 0,
+ property: 'propertyName',
+ propertyKey: undefined,
+ newValue: 0,
+ },
+ );
+ });
+
+ it('should not managed message', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/0123456789`, 0);
+ assert.notCalled(zwaveJSUIManager.valueUpdated);
+ });
+
+ it('should getNodes in scan mode success', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/getNodes`, {
+ success: true,
+ result: [
+ {
+ id: 1,
+ endpoints: [
+ {
+ index: 0,
+ },
+ ],
+ label: 'productLabel',
+ values: {
+ 38: {
+ commandClass: 38,
+ endpoint: 0,
+ property: 'property_property',
+ },
+ 48: {
+ commandClass: 48,
+ endpoint: 0,
+ propertyName: 'propertyName_propertyName',
+ },
+ },
+ },
+ ],
+ });
+ assert.calledOnceWithExactly(zwaveJSUIManager.nodeReady, {
+ nodeId: 1,
+ classes: {},
+ endpoints: [
+ {
+ index: 0,
+ },
+ ],
+ label: 'productLabel',
+ });
+ assert.calledWithExactly(
+ zwaveJSUIManager.valueAdded,
+ {
+ id: 1,
+ },
+ {
+ commandClass: 38,
+ endpoint: 0,
+ property: 'property_property',
+ propertyKey: undefined,
+ },
+ );
+ assert.calledWithExactly(
+ zwaveJSUIManager.valueAdded,
+ {
+ id: 1,
+ },
+ {
+ commandClass: 48,
+ endpoint: 0,
+ property: 'propertyName_propertyName',
+ propertyKey: undefined,
+ },
+ );
+ assert.calledOnce(zwaveJSUIManager.scanComplete);
+ expect(Object.keys(zwaveJSUIManager.nodes).length).to.equal(1);
+ expect(zwaveJSUIManager.nodes['1']).to.not.be.null; // eslint-disable-line
+ });
+
+ it('should getNodes in scan mode error', () => {
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/getNodes`, {
+ success: false,
+ });
+
+ clock.tick(DEFAULT.SCAN_NETWORK_RETRY_TIMEOUT);
+
+ assert.notCalled(zwaveJSUIManager.scanComplete);
+ assert.calledOnce(zwaveJSUIManager.scanNetwork);
+ });
+
+ it('should send driver new status event', () => {
+ const message = true;
+ zwaveJSUIManager.zwaveJSUIConnected = false;
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/driver/status`, JSON.stringify(message));
+ assert.calledOnceWithExactly(event.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.STATUS_CHANGE,
+ });
+ });
+
+ it('should not send driver same status event', () => {
+ const message = true;
+ zwaveJSUIManager.zwaveJSUIConnected = true;
+ zwaveJSUIManager.handleMqttMessage(`${this.mqttTopicPrefix}/driver/status`, JSON.stringify(message));
+ assert.notCalled(event.emit);
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/events/valueAdded.test.js b/server/test/services/zwave-js-ui/lib/events/valueAdded.test.js
new file mode 100644
index 0000000000..b2a8f2c6da
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/events/valueAdded.test.js
@@ -0,0 +1,766 @@
+const sinon = require('sinon');
+
+const { expect } = require('chai');
+
+const { stub, fake, assert } = sinon;
+const EventEmitter = require('events');
+
+const ZwaveJSUIManager = require('../../../../../services/zwave-js-ui/lib');
+const { CONFIGURATION } = require('../../../../../services/zwave-js-ui/lib/constants');
+const { EVENTS } = require('../../../../../utils/constants');
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+const DRIVER_PATH = 'DRIVER_PATH';
+
+const event = {
+ emit: fake.resolves(null),
+};
+
+const eventMqtt = new EventEmitter();
+
+const mqttClient = Object.assign(eventMqtt, {
+ subscribe: fake.resolves(null),
+ publish: fake.returns(true),
+ end: fake.resolves(true),
+ removeAllListeners: fake.resolves(true),
+});
+
+const mqtt = {
+ connect: fake.returns(mqttClient),
+};
+
+describe('zwaveJSUIManager valueAdded', () => {
+ let gladys;
+ let zwaveJSUIManager;
+ let zwaveNode;
+
+ before(() => {
+ gladys = {
+ event,
+ user: {
+ get: stub().resolves([{ id: ZWAVEJSUI_SERVICE_ID }]),
+ },
+ service: {
+ getService: stub().resolves({
+ list: Promise.resolve([DRIVER_PATH]),
+ }),
+ },
+ variable: {
+ getValue: (name) => Promise.resolve(CONFIGURATION.EXTERNAL_ZWAVEJSUI ? true : null),
+ setValue: (name) => Promise.resolve(null),
+ },
+ stateManager: {
+ get: (name, value) => fake.returns(value),
+ },
+ };
+ zwaveJSUIManager = new ZwaveJSUIManager(gladys, mqtt, ZWAVEJSUI_SERVICE_ID);
+
+ zwaveNode = {
+ id: 1,
+ };
+ });
+
+ beforeEach(() => {
+ sinon.reset();
+
+ zwaveJSUIManager.mqttConnected = true;
+ zwaveJSUIManager.nodes = {
+ '1': {
+ nodeId: 1,
+ name: 'name',
+ ready: true,
+ classes: {},
+ endpoints: [],
+ type: 'type',
+ product: 'product',
+ keysClasses: [],
+ },
+ };
+ });
+
+ it('should handle unknown node', () => {
+ zwaveJSUIManager.valueAdded(
+ {
+ id: 999,
+ },
+ {
+ commandClass: 20,
+ endpoint: 0,
+ property: 'property',
+ },
+ );
+ expect(zwaveJSUIManager.nodes[1].classes).to.be.empty; // eslint-disable-line
+ assert.notCalled(zwaveJSUIManager.eventManager.emit);
+ });
+
+ it('should handle value added 37-0-currentValue', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 37,
+ endpoint: 0,
+ property: 'currentValue',
+ type: 'boolean',
+ label: 'Current value',
+ min: 0,
+ max: 99,
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes).to.deep.equal({});
+ });
+
+ it('should handle value added 51-0-currentColor', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 51,
+ endpoint: 0,
+ property: 'currentColor',
+ type: 'number',
+ label: 'Current color',
+ min: 0,
+ max: 255,
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes).to.deep.equal({});
+ });
+
+ it('should handle value added unsupported command class', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 112,
+ endpoint: 0,
+ property: 'Test',
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[112][0]).to.deep.equal({});
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should handle value added with value', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 38,
+ endpoint: 0,
+ property: 'Test',
+ value: 'newValue',
+ });
+ assert.calledOnceWithExactly(zwaveJSUIManager.eventManager.emit, EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: 'zwave-js-ui:node_id:1:comclass:38:endpoint:0:property:Test',
+ state: 'newValue',
+ });
+ });
+
+ it('should handle value added 48-0-Any', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 48,
+ endpoint: 0,
+ property: 'Any',
+ type: 'number',
+ label: 'Any',
+ unit: 'W',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[48][0].Any).to.deep.equal({
+ commandClass: 48,
+ endpoint: 0,
+ genre: 'user',
+ label: 'Any',
+ type: 'number',
+ unit: 'W',
+ nodeId: 1,
+ property: 'Any',
+ writeable: false,
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'motion-sensor',
+ external_id: 'zwave-js-ui:node_id:1:comclass:48:endpoint:0:property:Any',
+ type: 'binary',
+ has_feedback: true,
+ last_value: 0,
+ name: 'Détecteur de présence',
+ read_only: true,
+ unit: 'watt',
+ min: 0,
+ max: 1,
+ selector: 'zwave-js-ui-node-1-any-48-0-any',
+ },
+ ]);
+ });
+
+ it('should handle value added 48-0-Motion', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 48,
+ endpoint: 0,
+ property: 'Motion',
+ type: 'binary',
+ label: 'Motion',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[48][0].Motion).to.deep.equal({
+ commandClass: 48,
+ endpoint: 0,
+ genre: 'user',
+ label: 'Motion',
+ type: 'binary',
+ nodeId: 1,
+ property: 'Motion',
+ writeable: false,
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'motion-sensor',
+ external_id: 'zwave-js-ui:node_id:1:comclass:48:endpoint:0:property:Motion',
+ type: 'binary',
+ has_feedback: true,
+ last_value: 0,
+ name: 'Détecteur de présence',
+ read_only: true,
+ unit: null,
+ min: 0,
+ max: 1,
+ selector: 'zwave-js-ui-node-1-motion-48-0-motion',
+ },
+ ]);
+ });
+
+ it('should handle value added 48-0-Any and 48-0-Motion', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 48,
+ endpoint: 0,
+ property: 'Any',
+ type: 'binary',
+ label: 'Any',
+ writeable: false,
+ });
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 48,
+ endpoint: 0,
+ property: 'Motion',
+ type: 'binary',
+ label: 'Motion',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[48][0].Any).to.deep.equal({
+ commandClass: 48,
+ endpoint: 0,
+ genre: 'user',
+ label: 'Any',
+ type: 'binary',
+ nodeId: 1,
+ property: 'Any',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[48][0].Motion).to.deep.equal({
+ commandClass: 48,
+ endpoint: 0,
+ genre: 'user',
+ label: 'Motion',
+ type: 'binary',
+ nodeId: 1,
+ property: 'Motion',
+ writeable: false,
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'motion-sensor',
+ external_id: 'zwave-js-ui:node_id:1:comclass:48:endpoint:0:property:Motion',
+ type: 'binary',
+ has_feedback: true,
+ last_value: 0,
+ name: 'Détecteur de présence',
+ read_only: true,
+ unit: null,
+ min: 0,
+ max: 1,
+ selector: 'zwave-js-ui-node-1-motion-48-0-motion',
+ },
+ ]);
+ });
+
+ /**
+ * Power should be handled by Meter command class.
+ */
+ it('should handle value added 49-0-Power', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 49,
+ endpoint: 0,
+ property: 'Power',
+ type: 'number',
+ label: 'Power',
+ unit: 'W',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[49][0].Power).to.deep.equal({
+ commandClass: 49,
+ endpoint: 0,
+ genre: 'user',
+ label: 'Power',
+ type: 'number',
+ unit: 'W',
+ nodeId: 1,
+ property: 'Power',
+ writeable: false,
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should handle value added 49-0-Illuminance', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 49,
+ endpoint: 0,
+ property: 'Illuminance',
+ type: 'number',
+ label: 'Illuminance',
+ unit: 'Lux',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[49][0].Illuminance).to.deep.equal({
+ commandClass: 49,
+ endpoint: 0,
+ genre: 'user',
+ label: 'Illuminance',
+ type: 'number',
+ unit: 'Lux',
+ nodeId: 1,
+ property: 'Illuminance',
+ writeable: false,
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'light-sensor',
+ external_id: 'zwave-js-ui:node_id:1:comclass:49:endpoint:0:property:Illuminance',
+ has_feedback: true,
+ last_value: undefined,
+ name: 'Illuminance',
+ read_only: true,
+ selector: 'zwave-js-ui-node-1-illuminance-49-0-illuminance',
+ type: 'integer',
+ unit: 'lux',
+ max: 100,
+ min: 0,
+ },
+ ]);
+ });
+
+ it('should handle value added 49-0-Humidity', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 49,
+ endpoint: 0,
+ property: 'Humidity',
+ type: 'number',
+ label: 'Humidity',
+ unit: '%',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[49][0].Humidity).to.deep.equal({
+ commandClass: 49,
+ endpoint: 0,
+ genre: 'user',
+ type: 'number',
+ label: 'Humidity',
+ unit: '%',
+ nodeId: 1,
+ property: 'Humidity',
+ writeable: false,
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'humidity-sensor',
+ external_id: 'zwave-js-ui:node_id:1:comclass:49:endpoint:0:property:Humidity',
+ has_feedback: true,
+ last_value: undefined,
+ name: 'Humidity',
+ read_only: true,
+ selector: 'zwave-js-ui-node-1-humidity-49-0-humidity',
+ type: 'decimal',
+ unit: 'percent',
+ min: 0,
+ max: 100,
+ },
+ ]);
+ });
+
+ it('should handle value added 49-0-Ultraviolet', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 49,
+ endpoint: 0,
+ property: 'Ultraviolet',
+ type: 'number',
+ label: 'Ultraviolet',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[49][0].Ultraviolet).to.deep.equal({
+ commandClass: 49,
+ endpoint: 0,
+ genre: 'user',
+ label: 'Ultraviolet',
+ type: 'number',
+ nodeId: 1,
+ property: 'Ultraviolet',
+ writeable: false,
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'uv-sensor',
+ external_id: 'zwave-js-ui:node_id:1:comclass:49:endpoint:0:property:Ultraviolet',
+ has_feedback: true,
+ last_value: undefined,
+ name: 'Ultraviolet',
+ read_only: true,
+ selector: 'zwave-js-ui-node-1-ultraviolet-49-0-ultraviolet',
+ type: 'integer',
+ unit: null,
+ min: 0,
+ max: 100,
+ },
+ ]);
+ });
+
+ it('should handle value added 49-0-Air_temperature', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 49,
+ endpoint: 0,
+ property: 'Air_temperature',
+ type: 'number',
+ label: 'Air temperature',
+ unit: '°C',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[49][0].Air_temperature).to.deep.equal({
+ commandClass: 49,
+ endpoint: 0,
+ genre: 'user',
+ type: 'number',
+ label: 'Air temperature',
+ unit: '°C',
+ nodeId: 1,
+ property: 'Air_temperature',
+ writeable: false,
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'temperature-sensor',
+ external_id: 'zwave-js-ui:node_id:1:comclass:49:endpoint:0:property:Air_temperature',
+ type: 'decimal',
+ has_feedback: true,
+ last_value: undefined,
+ name: 'Air temperature',
+ read_only: true,
+ selector: 'zwave-js-ui-node-1-air-temperature-49-0-air-temperature',
+ unit: 'celsius',
+ min: -30,
+ max: 50,
+ },
+ ]);
+ });
+
+ it('should handle value added 113-0-Smoke_Alarm-Sensor_status', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 113,
+ endpoint: 0,
+ property: 'Smoke_Alarm-Sensor_status',
+ type: 'number',
+ label: 'Motion sensor status',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[113][0]['Smoke_Alarm-Sensor_status']).to.deep.equal({
+ commandClass: 113,
+ endpoint: 0,
+ genre: 'user',
+ label: 'Motion sensor status',
+ type: 'number',
+ nodeId: 1,
+ property: 'Smoke_Alarm-Sensor_status',
+ writeable: false,
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'smoke-sensor',
+ external_id: 'zwave-js-ui:node_id:1:comclass:113:endpoint:0:property:Smoke_Alarm-Sensor_status',
+ has_feedback: true,
+ last_value: undefined,
+ name: 'Motion sensor status',
+ read_only: true,
+ selector: 'zwave-js-ui-node-1-smoke-alarm-sensor-status-113-0-motion-sensor-status',
+ type: 'binary',
+ unit: null,
+ max: undefined,
+ min: undefined,
+ },
+ ]);
+ });
+
+ it('should not handle value added 132-0-wakeUpInterval', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 132,
+ endpoint: 0,
+ property: 'wakeUpInterval',
+ type: 'number',
+ label: 'Wake Up interval',
+ min: 300,
+ max: 16777200,
+ writeable: true,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[132][0]).to.deep.equal({});
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should not handle value added 132-0-controllerNodeId', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 132,
+ endpoint: 0,
+ property: 'controllerNodeId',
+ type: 'any',
+ label: 'Node ID of the controller',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[132][0]).to.deep.equal({});
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should not handle value added 132-0-level', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 132,
+ endpoint: 0,
+ property: 'level',
+ type: 'number',
+ label: 'Battery level',
+ min: 0,
+ max: 100,
+ unit: '%',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[132][0]).to.deep.equal({});
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should not handle value added 132-0-isLow', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 132,
+ endpoint: 0,
+ property: 'isLow',
+ type: 'boolean',
+ label: 'Low battery level',
+ writeable: false,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[132][0]).to.deep.equal({});
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should not handle value added 50-0-value-66048', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 50,
+ endpoint: 0,
+ property: 'value-66048',
+ type: 'number',
+ label: 'Electric [W]',
+ writeable: false,
+ unit: 'kWh',
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'switch',
+ external_id: 'zwave-js-ui:node_id:1:comclass:50:endpoint:0:property:value-66048',
+ has_feedback: true,
+ last_value: undefined,
+ name: 'Electric [W]',
+ read_only: true,
+ selector: 'zwave-js-ui-node-1-value-66048-50-0-electric-w',
+ type: 'energy',
+ unit: 'kilowatt-hour',
+ max: 100000,
+ min: 0,
+ },
+ ]);
+ });
+
+ it('should not handle value added 50-0-value-66049', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 50,
+ endpoint: 0,
+ property: 'value-66049',
+ type: 'number',
+ label: 'Electric Consumption [kWh]',
+ writeable: false,
+ unit: 'kWh',
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'switch',
+ external_id: 'zwave-js-ui:node_id:1:comclass:50:endpoint:0:property:value-66049',
+ has_feedback: true,
+ last_value: undefined,
+ name: 'Electric Consumption [kWh]',
+ read_only: true,
+ selector: 'zwave-js-ui-node-1-value-66049-50-0-electric-consumption-kwh',
+ type: 'power',
+ unit: 'kilowatt-hour',
+ max: 10000,
+ min: 0,
+ },
+ ]);
+ });
+
+ it('should not handle value added 50-0-value-66051', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 50,
+ endpoint: 0,
+ property: 'value-66051',
+ type: 'number',
+ label: 'Electric [W]',
+ writeable: false,
+ unit: 'W',
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should not handle value added 50-0-value-65536', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 50,
+ endpoint: 0,
+ property: 'value-65536',
+ type: 'number',
+ label: 'Electric [kWh]',
+ writeable: false,
+ unit: 'kWh',
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should not handle value added 50-0-value-65537', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 50,
+ endpoint: 0,
+ property: 'value-65537',
+ type: 'number',
+ label: 'Electric Consumption [W]',
+ writeable: false,
+ unit: 'W',
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'switch',
+ external_id: 'zwave-js-ui:node_id:1:comclass:50:endpoint:0:property:value-65537',
+ has_feedback: true,
+ last_value: undefined,
+ name: 'Electric Consumption [W]',
+ read_only: true,
+ selector: 'zwave-js-ui-node-1-value-65537-50-0-electric-consumption-w',
+ type: 'energy',
+ unit: 'watt',
+ max: 100000,
+ min: 0,
+ },
+ ]);
+ });
+
+ it('should not handle value added 50-0-value-65539', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 50,
+ endpoint: 0,
+ property: 'value-65539',
+ type: 'number',
+ label: 'Electric [kWh]',
+ writeable: false,
+ unit: 'kWh',
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(0);
+ });
+
+ it('should not handle value added 50-0-value-66561', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 50,
+ endpoint: 0,
+ property: 'value-66561',
+ type: 'number',
+ label: 'Electric Consumption [V]',
+ writeable: false,
+ unit: 'V',
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'switch',
+ external_id: 'zwave-js-ui:node_id:1:comclass:50:endpoint:0:property:value-66561',
+ has_feedback: true,
+ last_value: undefined,
+ name: 'Electric Consumption [V]',
+ read_only: true,
+ selector: 'zwave-js-ui-node-1-value-66561-50-0-electric-consumption-v',
+ type: 'voltage',
+ unit: 'volt',
+ max: 400,
+ min: 0,
+ },
+ ]);
+ });
+
+ it('should not handle value added 50-0-value-66817', () => {
+ zwaveJSUIManager.valueAdded(zwaveNode, {
+ commandClass: 50,
+ endpoint: 0,
+ property: 'value-66817',
+ type: 'number',
+ label: 'Electric Consumption [A]',
+ writeable: false,
+ unit: 'A',
+ });
+ const nodes = zwaveJSUIManager.getNodes();
+ expect(nodes).to.have.lengthOf(1);
+ expect(nodes[0].params).to.have.lengthOf(4);
+ expect(nodes[0].features).to.deep.equal([
+ {
+ category: 'switch',
+ external_id: 'zwave-js-ui:node_id:1:comclass:50:endpoint:0:property:value-66817',
+ has_feedback: true,
+ last_value: undefined,
+ name: 'Electric Consumption [A]',
+ read_only: true,
+ selector: 'zwave-js-ui-node-1-value-66817-50-0-electric-consumption-a',
+ type: 'current',
+ unit: 'ampere',
+ max: 40,
+ min: 0,
+ },
+ ]);
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/events/valueUpdated.test.js b/server/test/services/zwave-js-ui/lib/events/valueUpdated.test.js
new file mode 100644
index 0000000000..40aaaddcf5
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/events/valueUpdated.test.js
@@ -0,0 +1,168 @@
+const sinon = require('sinon');
+
+const { expect } = require('chai');
+
+const { stub, fake, assert } = sinon;
+const EventEmitter = require('events');
+
+const ZwaveJSUIManager = require('../../../../../services/zwave-js-ui/lib');
+const { CONFIGURATION } = require('../../../../../services/zwave-js-ui/lib/constants');
+const { EVENTS } = require('../../../../../utils/constants');
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+const DRIVER_PATH = 'DRIVER_PATH';
+
+const event = {
+ emit: fake.resolves(null),
+};
+
+const eventMqtt = new EventEmitter();
+
+const mqttClient = Object.assign(eventMqtt, {
+ subscribe: fake.resolves(null),
+ publish: fake.returns(true),
+ end: fake.resolves(true),
+ removeAllListeners: fake.resolves(true),
+});
+
+const mqtt = {
+ connect: fake.returns(mqttClient),
+};
+
+describe('zwaveJSUIManager valueUpdated', () => {
+ let gladys;
+ let zwaveJSUIManager;
+ let zwaveNode;
+
+ before(() => {
+ gladys = {
+ event,
+ user: {
+ get: stub().resolves([{ id: ZWAVEJSUI_SERVICE_ID }]),
+ },
+ service: {
+ getService: stub().resolves({
+ list: Promise.resolve([DRIVER_PATH]),
+ }),
+ },
+ variable: {
+ getValue: (name) => Promise.resolve(CONFIGURATION.EXTERNAL_ZWAVEJSUI ? true : null),
+ setValue: (name) => Promise.resolve(null),
+ },
+ stateManager: {
+ get: (name, value) => fake.returns(value),
+ },
+ };
+ zwaveJSUIManager = new ZwaveJSUIManager(gladys, mqtt, ZWAVEJSUI_SERVICE_ID);
+
+ zwaveNode = {
+ id: 1,
+ };
+ });
+
+ beforeEach(() => {
+ sinon.reset();
+
+ zwaveJSUIManager.mqttConnected = true;
+ zwaveJSUIManager.nodes = {
+ '1': {
+ nodeId: 1,
+ name: 'name',
+ ready: true,
+ endpoints: [],
+ type: 'type',
+ product: 'product',
+ classes: {
+ '20': {
+ 0: {
+ property: {},
+ targetValue: {},
+ },
+ },
+ '43': {
+ 0: {
+ property: {},
+ },
+ },
+ '51': {
+ 0: {
+ 'targetColor-0': {},
+ },
+ },
+ },
+ },
+ };
+ });
+
+ it('should handle unknown node', () => {
+ zwaveJSUIManager.valueUpdated(
+ {
+ id: 999,
+ },
+ {
+ commandClass: 20,
+ endpoint: 0,
+ property: 'property',
+ newValue: 'newValue',
+ },
+ );
+ expect(zwaveJSUIManager.nodes[1].classes[20][0].property).to.be.empty; // eslint-disable-line
+ assert.notCalled(zwaveJSUIManager.eventManager.emit);
+ });
+
+ it('should not handle property currentValue', () => {
+ zwaveJSUIManager.valueUpdated(zwaveNode, {
+ commandClass: 20,
+ endpoint: 0,
+ property: 'currentValue',
+ newValue: 'newValue',
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[20][0].currentValue).to.be.undefined; // eslint-disable-line
+ expect(zwaveJSUIManager.nodes[1].classes[20][0].targetValue).to.deep.equal({});
+ assert.notCalled(zwaveJSUIManager.eventManager.emit);
+ });
+
+ it('should not handle property currentColor', () => {
+ zwaveJSUIManager.valueUpdated(zwaveNode, {
+ commandClass: 51,
+ endpoint: 0,
+ property: 'currentColor',
+ newValue: 180,
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[51][0].currentColor).to.be.undefined; // eslint-disable-line
+ expect(zwaveJSUIManager.nodes[1].classes[51][0]['targetColor-0']).to.deep.equal({});
+ assert.notCalled(zwaveJSUIManager.eventManager.emit);
+ });
+
+ it('should handle value valueUpdated 20-0-property', () => {
+ zwaveJSUIManager.valueUpdated(zwaveNode, {
+ commandClass: 20,
+ endpoint: 0,
+ property: 'property',
+ newValue: 'newValue',
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[20][0].property).to.deep.equal({
+ value: 'newValue',
+ });
+ assert.calledOnceWithExactly(zwaveJSUIManager.eventManager.emit, EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: 'zwave-js-ui:node_id:1:comclass:20:endpoint:0:property:property',
+ state: 'newValue',
+ });
+ });
+
+ it('should handle value valueUpdated 43-0-property', () => {
+ zwaveJSUIManager.valueUpdated(zwaveNode, {
+ commandClass: 43,
+ endpoint: 0,
+ property: 'property',
+ newValue: '10',
+ });
+ expect(zwaveJSUIManager.nodes[1].classes[43][0].property).to.deep.equal({
+ value: 1,
+ });
+ assert.calledOnceWithExactly(zwaveJSUIManager.eventManager.emit, EVENTS.DEVICE.NEW_STATE, {
+ device_feature_external_id: 'zwave-js-ui:node_id:1:comclass:43:endpoint:0:property:property',
+ state: 1,
+ });
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/nodesExpectedResult.json b/server/test/services/zwave-js-ui/lib/nodesExpectedResult.json
new file mode 100644
index 0000000000..8439048953
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/nodesExpectedResult.json
@@ -0,0 +1,256 @@
+[
+ {
+ "name": "ZME_UZB1 USB Stick",
+ "service_id": "de051f90-f34a-4fd5-be2e-e502339ec9bc",
+ "external_id": "zwave-js-ui:node_id:1",
+ "ready": true,
+ "features": [],
+ "params": [
+ { "name": "node-id", "value": "" },
+ { "name": "node-product", "value": "" },
+ { "name": "node-loc", "value": "" },
+ { "name": "node-keysClasses", "value": "" },
+ { "name": "basic-1-32-1-0", "value": "" },
+ { "name": "basic-target-1-32-1-1", "value": "" },
+ { "name": "basic-duration-1-32-1-2", "value": "" },
+ { "name": "loaded-config-revision-1-114-1-0", "value": "" },
+ { "name": "config-file-revision-1-114-1-1", "value": "" },
+ { "name": "latest-available-config-file-revision-1-114-1-2", "value": "" }
+ ]
+ },
+ {
+ "name": "FGMS001-ZW5 Motion Sensor",
+ "service_id": "de051f90-f34a-4fd5-be2e-e502339ec9bc",
+ "external_id": "zwave-js-ui:node_id:15",
+ "ready": true,
+ "features": [
+ {
+ "name": "Sensor",
+ "selector": "zwave-js-ui-1-0-sensor-fgms001-zw5-motion-sensor-node-15",
+ "category": "motion-sensor",
+ "type": "binary",
+ "external_id": "zwave-js-ui:node_id:15:comclass:48:index:0:instance:1",
+ "read_only": true,
+ "unit": null,
+ "has_feedback": true,
+ "min": 0,
+ "max": 0
+ },
+ {
+ "name": "Air Temperature",
+ "selector": "zwave-js-ui-1-1-air-temperature-fgms001-zw5-motion-sensor-node-15",
+ "category": "temperature-sensor",
+ "type": "decimal",
+ "external_id": "zwave-js-ui:node_id:15:comclass:49:index:1:instance:1",
+ "read_only": true,
+ "unit": "celsius",
+ "has_feedback": true,
+ "min": 0,
+ "max": 0
+ },
+ {
+ "name": "Illuminance",
+ "selector": "zwave-js-ui-1-3-illuminance-fgms001-zw5-motion-sensor-node-15",
+ "category": "light-sensor",
+ "type": "integer",
+ "external_id": "zwave-js-ui:node_id:15:comclass:49:index:3:instance:1",
+ "read_only": true,
+ "unit": "lux",
+ "has_feedback": true,
+ "min": 0,
+ "max": 0
+ },
+ {
+ "name": "Seismic Intensity",
+ "selector": "zwave-js-ui-1-25-seismic-intensity-fgms001-zw5-motion-sensor-node-15",
+ "category": "sismic-sensor",
+ "type": "decimal",
+ "external_id": "zwave-js-ui:node_id:15:comclass:49:index:25:instance:1",
+ "read_only": true,
+ "unit": null,
+ "has_feedback": true,
+ "min": 0,
+ "max": 0
+ },
+ {
+ "name": "Battery Level",
+ "selector": "zwave-js-ui-1-0-battery-level-fgms001-zw5-motion-sensor-node-15",
+ "category": "battery",
+ "type": "integer",
+ "external_id": "zwave-js-ui:node_id:15:comclass:128:index:0:instance:1",
+ "read_only": true,
+ "unit": "percent",
+ "has_feedback": true,
+ "min": 0,
+ "max": 255
+ }
+ ],
+ "params": [
+ { "name": "node-id", "value": "" },
+ { "name": "node-product", "value": "" },
+ { "name": "node-loc", "value": "" },
+ { "name": "node-keysClasses", "value": "" },
+ { "name": "basic-15-32-1-0", "value": "" },
+ { "name": "basic-target-15-32-1-1", "value": "" },
+ { "name": "basic-duration-15-32-1-2", "value": "" },
+ { "name": "air-temperature-units-15-49-1-256", "value": "Celsius" },
+ { "name": "illuminance-units-15-49-1-258", "value": "Lux" },
+ { "name": "seismic-intensity-units-15-49-1-280", "value": "Mercalli" },
+ { "name": "x-axis-acceleration-units-15-49-1-307", "value": "Meter per Second Squared" },
+ { "name": "y-axis-acceleration-units-15-49-1-308", "value": "Meter per Second Squared" },
+ { "name": "z-axis-acceleration-units-15-49-1-309", "value": "Meter per Second Squared" },
+ { "name": "zwave-js-ui-version-15-94-1-0", "value": 1 },
+ { "name": "installericon-15-94-1-1", "value": 3079 },
+ { "name": "usericon-15-94-1-2", "value": 3079 },
+ { "name": "motion-detection-sensitivity-15-112-1-1", "value": 15 },
+ { "name": "motion-detection-blind-time-15-112-1-2", "value": 3 },
+ { "name": "motion-detection-pulse-counter-15-112-1-3", "value": "2 pulses" },
+ { "name": "motion-detection-window-time-15-112-1-4", "value": "12 seconds" },
+ { "name": "motion-detection-alarm-cancellation-delay-15-112-1-6", "value": 30 },
+ { "name": "motion-detection-operating-mode-15-112-1-8", "value": "PIR sensor always active" },
+ { "name": "motion-detection-night-day-15-112-1-9", "value": 200 },
+ { "name": "basic-command-class-configuration-15-112-1-12", "value": "BASIC On and OFF" },
+ { "name": "basic-on-command-frame-value-15-112-1-14", "value": 255 },
+ { "name": "basic-off-command-frame-value-15-112-1-16", "value": "" },
+ { "name": "associations-in-z-wave-network-security-mode-15-112-1-18", "value": 15 },
+ { "name": "tamper-sensitivity-15-112-1-20", "value": 20 },
+ { "name": "tamper-alarm-cancellation-delay-15-112-1-22", "value": 30 },
+ { "name": "tamper-operating-modes-15-112-1-24", "value": "Tamper only" },
+ { "name": "tamper-alarm-cancellation-15-112-1-25", "value": "Send tamper cancellation report" },
+ { "name": "tamper-alarm-broadcast-mode-15-112-1-28", "value": "Tamper alarm sent to 3rd association group" },
+ {
+ "name": "tamper-backward-compatible-broadcast-mode-15-112-1-29",
+ "value": "Backward compatible tamper alarm sent to 5th association group"
+ },
+ { "name": "luminance-report-threshold-15-112-1-40", "value": 200 },
+ { "name": "luminance-reports-interval-15-112-1-42", "value": "" },
+ { "name": "temperature-report-threshold-15-112-1-60", "value": 10 },
+ { "name": "temperature-measuring-interval-15-112-1-62", "value": 900 },
+ { "name": "temperature-report-interval-15-112-1-64", "value": "" },
+ { "name": "temperature-offset-15-112-1-66", "value": "" },
+ {
+ "name": "visual-led-indicator-signalling-mode-15-112-1-80",
+ "value": "Long blink, then short blink, LED colour depends on the temperature. Set by parameters 86 and 87."
+ },
+ { "name": "visual-led-indicator-brightness-15-112-1-81", "value": 50 },
+ { "name": "visual-led-indicator-luminance-for-low-indicator-brightness-15-112-1-82", "value": 100 },
+ { "name": "visual-led-indicator-luminance-for-high-indicator-brightness-15-112-1-83", "value": 1000 },
+ { "name": "visual-led-indicator-temperature-for-blue-colour-15-112-1-86", "value": 18 },
+ { "name": "visual-led-indicator-temperature-for-red-colour-15-112-1-87", "value": 28 },
+ { "name": "visual-led-indicator-tamper-alarm-15-112-1-89", "value": "Tamper alarm is indicated" },
+ { "name": "loaded-config-revision-15-114-1-0", "value": 11 },
+ { "name": "config-file-revision-15-114-1-1", "value": 11 },
+ { "name": "latest-available-config-file-revision-15-114-1-2", "value": 11 },
+ { "name": "serial-number-15-114-1-4", "value": "000000000004ecdb" },
+ { "name": "powerlevel-15-115-1-0", "value": "Normal" },
+ { "name": "timeout-15-115-1-1", "value": "" },
+ { "name": "set-powerlevel-15-115-1-2", "value": "" },
+ { "name": "test-node-15-115-1-3", "value": "" },
+ { "name": "test-powerlevel-15-115-1-4", "value": "Normal" },
+ { "name": "frame-count-15-115-1-5", "value": "" },
+ { "name": "test-15-115-1-6", "value": "" },
+ { "name": "report-15-115-1-7", "value": "" },
+ { "name": "test-status-15-115-1-8", "value": "Failed" },
+ { "name": "acked-frames-15-115-1-9", "value": "" },
+ { "name": "wake-up-interval-15-132-1-0", "value": 7200 },
+ { "name": "minimum-wake-up-interval-15-132-1-1", "value": 1 },
+ { "name": "maximum-wake-up-interval-15-132-1-2", "value": 65535 },
+ { "name": "default-wake-up-interval-15-132-1-3", "value": 7200 },
+ { "name": "wake-up-interval-step-15-132-1-4", "value": 1 },
+ { "name": "library-version-15-134-1-0", "value": "3" },
+ { "name": "protocol-version-15-134-1-1", "value": "4.05" },
+ { "name": "application-version-15-134-1-2", "value": "3.02" }
+ ]
+ },
+ {
+ "name": "",
+ "service_id": "de051f90-f34a-4fd5-be2e-e502339ec9bc",
+ "external_id": "zwave-js-ui:node_id:2",
+ "ready": false,
+ "features": [],
+ "params": [
+ { "name": "node-id", "value": "" },
+ { "name": "node-product", "value": "" },
+ { "name": "node-loc", "value": "" },
+ { "name": "node-keysClasses", "value": "" }
+ ]
+ },
+ {
+ "name": "",
+ "service_id": "de051f90-f34a-4fd5-be2e-e502339ec9bc",
+ "external_id": "zwave-js-ui:node_id:3",
+ "ready": false,
+ "features": [],
+ "params": [
+ { "name": "node-id", "value": "" },
+ { "name": "node-product", "value": "" },
+ { "name": "node-loc", "value": "" },
+ { "name": "node-keysClasses", "value": "" }
+ ]
+ },
+ {
+ "name": "",
+ "service_id": "de051f90-f34a-4fd5-be2e-e502339ec9bc",
+ "external_id": "zwave-js-ui:node_id:4",
+ "ready": false,
+ "features": [],
+ "params": [
+ { "name": "node-id", "value": "" },
+ { "name": "node-product", "value": "" },
+ { "name": "node-loc", "value": "" },
+ { "name": "node-keysClasses", "value": "" }
+ ]
+ },
+ {
+ "name": "",
+ "service_id": "de051f90-f34a-4fd5-be2e-e502339ec9bc",
+ "external_id": "zwave-js-ui:node_id:5",
+ "ready": false,
+ "features": [],
+ "params": [
+ { "name": "node-id", "value": "" },
+ { "name": "node-product", "value": "" },
+ { "name": "node-loc", "value": "" },
+ { "name": "node-keysClasses", "value": "" }
+ ]
+ },
+ {
+ "name": "",
+ "service_id": "de051f90-f34a-4fd5-be2e-e502339ec9bc",
+ "external_id": "zwave-js-ui:node_id:6",
+ "ready": false,
+ "features": [],
+ "params": [
+ { "name": "node-id", "value": "" },
+ { "name": "node-product", "value": "" },
+ { "name": "node-loc", "value": "" },
+ { "name": "node-keysClasses", "value": "" }
+ ]
+ },
+ {
+ "name": "",
+ "service_id": "de051f90-f34a-4fd5-be2e-e502339ec9bc",
+ "external_id": "zwave-js-ui:node_id:7",
+ "ready": false,
+ "features": [],
+ "params": [
+ { "name": "node-id", "value": "" },
+ { "name": "node-product", "value": "" },
+ { "name": "node-loc", "value": "" },
+ { "name": "node-keysClasses", "value": "" }
+ ]
+ },
+ {
+ "name": "",
+ "service_id": "de051f90-f34a-4fd5-be2e-e502339ec9bc",
+ "external_id": "zwave-js-ui:node_id:8",
+ "ready": false,
+ "features": [],
+ "params": [
+ { "name": "node-id", "value": "" },
+ { "name": "node-product", "value": "" },
+ { "name": "node-loc", "value": "" },
+ { "name": "node-keysClasses", "value": "" }
+ ]
+ }
+]
diff --git a/server/test/services/zwave-js-ui/lib/utils/bindValue.test.js b/server/test/services/zwave-js-ui/lib/utils/bindValue.test.js
new file mode 100644
index 0000000000..9dea2c8ff2
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/utils/bindValue.test.js
@@ -0,0 +1,189 @@
+const { expect } = require('chai');
+const { COMMAND_CLASSES, PROPERTIES } = require('../../../../../services/zwave-js-ui/lib/constants');
+const { bindValue, unbindValue } = require('../../../../../services/zwave-js-ui/lib/utils/bindValue');
+const { BUTTON_STATUS, STATE } = require('../../../../../utils/constants');
+
+describe('zwave.bindValue', () => {
+ it('should bindValue commandClass COMMAND_CLASS_SWITCH_BINARY', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_SWITCH_BINARY,
+ };
+ const value = 1;
+ const bindedValue = bindValue(valueId, value);
+ expect(bindedValue).to.equal(true);
+ });
+
+ it('should bindValue commandClass COMMAND_CLASS_SWITCH_MULTILEVEL', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_SWITCH_MULTILEVEL,
+ };
+ const value = '15';
+ const bindedValue = bindValue(valueId, value);
+ expect(bindedValue).to.equal(15);
+ });
+
+ it('should bindValue commandClass COMMAND_CLASS_NOTIFICATION ON', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION,
+ };
+ const value = '8';
+ const bindedValue = bindValue(valueId, value);
+ expect(bindedValue).to.equal(true);
+ });
+
+ it('should bindValue commandClass COMMAND_CLASS_NOTIFICATION OFF', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION,
+ };
+ const value = '0';
+ const bindedValue = bindValue(valueId, value);
+ expect(bindedValue).to.equal(false);
+ });
+
+ it('should bindValue commandClass COMMAND_CLASS_SWITCH_COLOR - HEX_COLOR', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_SWITCH_COLOR,
+ property: PROPERTIES.HEX_COLOR,
+ };
+ const value = 0xff00ff;
+ const bindedValue = bindValue(valueId, value);
+ expect(bindedValue).to.equal('"ff00ff"');
+ });
+
+ it('should bindValue commandClass COMMAND_CLASS_SWITCH_COLOR - TARGET_COLOR', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_SWITCH_COLOR,
+ property: PROPERTIES.TARGET_COLOR,
+ endpoint: 0,
+ };
+ const value = 100;
+ const bindedValue = bindValue(valueId, value);
+ expect(bindedValue).to.equal(255);
+ });
+
+ it('should bindValue commandClass other', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_BASIC,
+ };
+ const value = 'test';
+ const bindedValue = bindValue(valueId, value);
+ expect(bindedValue).to.equal(value);
+ });
+
+ it('should bindValue commandClass other - Number', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_BASIC,
+ };
+ const value = 1;
+ const bindedValue = bindValue(valueId, value);
+ expect(bindedValue).to.equal(value);
+ });
+});
+
+describe('zwave.unbindValue', () => {
+ it('should unbindValue commandClass COMMAND_CLASS_SWITCH_BINARY', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_SWITCH_BINARY,
+ };
+ const value = true;
+ const unbindedValue = unbindValue(valueId, value);
+ expect(unbindedValue).to.equal(1);
+ });
+
+ it('should unbindValue commandClass COMMAND_CLASS_SENSOR_BINARY', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_SENSOR_BINARY,
+ };
+ const value = false;
+ const unbindedValue = unbindValue(valueId, value);
+ expect(unbindedValue).to.equal(0);
+ });
+
+ it('should unbindValue commandClass COMMAND_CLASS_NOTIFICATION - Motion ON', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION,
+ fullProperty: PROPERTIES.MOTION_ALARM,
+ };
+ const value = 8;
+ const unbindedValue = unbindValue(valueId, value);
+ expect(unbindedValue).to.equal(1);
+ });
+
+ it('should unbindValue commandClass COMMAND_CLASS_NOTIFICATION - Motion OFF', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION,
+ fullProperty: PROPERTIES.MOTION_ALARM,
+ };
+ const value = false;
+ const unbindedValue = unbindValue(valueId, value);
+ expect(unbindedValue).to.equal(0);
+ });
+
+ it('should unbindValue commandClass COMMAND_CLASS_NOTIFICATION - Smoke Alarm-Sensor status OFF', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION,
+ fullProperty: PROPERTIES.SMOKE_ALARM,
+ };
+ const value = 0;
+ const unbindedValue = unbindValue(valueId, value);
+ expect(unbindedValue).to.equal(STATE.OFF);
+ });
+
+ it('should unbindValue commandClass COMMAND_CLASS_NOTIFICATION - Smoke Alarm-Sensor status ON', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_NOTIFICATION,
+ fullProperty: PROPERTIES.SMOKE_ALARM,
+ };
+ const value = 2;
+ const unbindedValue = unbindValue(valueId, value);
+ expect(unbindedValue).to.equal(STATE.ON);
+ });
+
+ it('should unbindValue commandClass COMMAND_CLASS_CENTRAL_SCENE - empty', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_CENTRAL_SCENE,
+ };
+ const value = '';
+ const unbindedValue = unbindValue(valueId, value);
+ expect(unbindedValue).to.equal(0);
+ });
+
+ it('should unbindValue commandClass COMMAND_CLASS_CENTRAL_SCENE - 34', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_CENTRAL_SCENE,
+ };
+ const value = 23;
+ const unbindedValue = unbindValue(valueId, value);
+ expect(unbindedValue).to.equal(BUTTON_STATUS.DOUBLE_CLICK);
+ });
+
+ it('should unbindValue commandClass COMMAND_CLASS_SCENE_ACTIVATION', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_SCENE_ACTIVATION,
+ };
+ const value = 20;
+ const unbindedValue = unbindValue(valueId, value);
+ expect(unbindedValue).to.equal(BUTTON_STATUS.CLICK);
+ });
+
+ it('should unbindValue commandClass COMMAND_CLASS_SWITCH_COLOR - HEX_COLOR', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_SWITCH_COLOR,
+ property: PROPERTIES.HEX_COLOR,
+ };
+ const value = '"ff00ff"';
+ const bindedValue = unbindValue(valueId, value);
+ expect(bindedValue).to.equal(0xff00ff);
+ });
+
+ it('should unbindValue commandClass COMMAND_CLASS_SWITCH_COLOR - TARGET_COLOR', () => {
+ const valueId = {
+ commandClass: COMMAND_CLASSES.COMMAND_CLASS_SWITCH_COLOR,
+ property: PROPERTIES.TARGET_COLOR,
+ endpoint: 0,
+ };
+ const value = 255;
+ const bindedValue = unbindValue(valueId, value);
+ expect(bindedValue).to.equal(100);
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/utils/getUnit.test.js b/server/test/services/zwave-js-ui/lib/utils/getUnit.test.js
new file mode 100644
index 0000000000..2734f68678
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/utils/getUnit.test.js
@@ -0,0 +1,34 @@
+const { expect } = require('chai');
+const { getUnit } = require('../../../../../services/zwave-js-ui/lib/utils/getUnit');
+const { DEVICE_FEATURE_UNITS } = require('../../../../../utils/constants');
+
+describe('zwave.getUnit', () => {
+ it('should return temperature unit', () => {
+ const celsius = getUnit('°C');
+ const fahrenheit = getUnit('°F');
+ expect(celsius).to.equal(DEVICE_FEATURE_UNITS.CELSIUS);
+ expect(fahrenheit).to.equal(DEVICE_FEATURE_UNITS.FAHRENHEIT);
+ });
+ it('should return percent unit', () => {
+ const percent = getUnit('%');
+ expect(percent).to.equal(DEVICE_FEATURE_UNITS.PERCENT);
+ });
+ it('should return luminosity unit', () => {
+ const lux1 = getUnit('Lux');
+ const lux2 = getUnit('lux');
+ expect(lux1).to.equal(DEVICE_FEATURE_UNITS.LUX);
+ expect(lux2).to.equal(DEVICE_FEATURE_UNITS.LUX);
+ });
+ it('should return electricity unit', () => {
+ const ampere = getUnit('A');
+ const volt = getUnit('V');
+ const kilowatthour = getUnit('kWh');
+ const watt1 = getUnit('W');
+ const watt2 = getUnit('Watt');
+ expect(ampere).to.equal(DEVICE_FEATURE_UNITS.AMPERE);
+ expect(volt).to.equal(DEVICE_FEATURE_UNITS.VOLT);
+ expect(kilowatthour).to.equal(DEVICE_FEATURE_UNITS.KILOWATT_HOUR);
+ expect(watt1).to.equal(DEVICE_FEATURE_UNITS.WATT);
+ expect(watt2).to.equal(DEVICE_FEATURE_UNITS.WATT);
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/utils/splitNode.test.js b/server/test/services/zwave-js-ui/lib/utils/splitNode.test.js
new file mode 100644
index 0000000000..435dfe0d0d
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/utils/splitNode.test.js
@@ -0,0 +1,26 @@
+const { expect } = require('chai');
+const { splitNode } = require('../../../../../services/zwave-js-ui/lib/utils/splitNode');
+
+describe('zwave.splitNode', () => {
+ it('should get one node', () => {
+ const nodes = splitNode({
+ endpoints: [],
+ });
+ expect(nodes).to.not.be.an('array');
+ });
+
+ it('should get 3 nodes for 2 endpoint', () => {
+ const nodes = splitNode({
+ endpoints: [
+ {
+ index: 0,
+ },
+ {
+ index: 1,
+ },
+ ],
+ classes: {},
+ });
+ expect(nodes).to.be.an('array');
+ });
+});
diff --git a/server/test/services/zwave-js-ui/lib/zwaveManager.test.js b/server/test/services/zwave-js-ui/lib/zwaveManager.test.js
new file mode 100644
index 0000000000..ab8e052c3e
--- /dev/null
+++ b/server/test/services/zwave-js-ui/lib/zwaveManager.test.js
@@ -0,0 +1,275 @@
+const sinon = require('sinon');
+
+const { expect } = require('chai');
+
+const { assert, fake, useFakeTimers } = sinon;
+const EventEmitter = require('events');
+
+const { CONFIGURATION, DEFAULT } = require('../../../../services/zwave-js-ui/lib/constants');
+const ZwaveJSUIManager = require('../../../../services/zwave-js-ui/lib');
+const { WEBSOCKET_MESSAGE_TYPES, EVENTS } = require('../../../../utils/constants');
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+const DRIVER_PATH = 'DRIVER_PATH';
+
+const event = {
+ emit: fake.resolves(null),
+};
+
+const eventMqtt = new EventEmitter();
+
+const mqttClient = Object.assign(eventMqtt, {
+ subscribe: fake.resolves(null),
+ publish: fake.returns(true),
+ end: fake.resolves(true),
+ removeAllListeners: fake.resolves(true),
+});
+
+const mqtt = {
+ connect: fake.returns(mqttClient),
+};
+
+describe('zwaveJSUIManager commands', () => {
+ let gladys;
+ let zwaveJSUIManager;
+
+ before(() => {
+ gladys = {
+ event,
+ service: {
+ getService: () => {
+ return {
+ list: () =>
+ Promise.resolve([
+ {
+ path: DRIVER_PATH,
+ },
+ ]),
+ };
+ },
+ },
+ variable: {
+ getValue: fake.resolves(true),
+ setValue: fake.resolves(true),
+ },
+ system: {
+ isDocker: fake.resolves(true),
+ getContainers: fake.resolves([]),
+ },
+ };
+ zwaveJSUIManager = new ZwaveJSUIManager(gladys, mqtt, ZWAVEJSUI_SERVICE_ID);
+ zwaveJSUIManager.installMqttContainer = Promise.resolve();
+ zwaveJSUIManager.installZwaveJSUIContainer = Promise.resolve();
+ });
+
+ beforeEach(() => {
+ sinon.reset();
+ zwaveJSUIManager.mqttExist = false;
+ zwaveJSUIManager.mqttRunning = false;
+ zwaveJSUIManager.mqttConnected = false;
+ zwaveJSUIManager.zwaveJSUIExist = false;
+ zwaveJSUIManager.zwaveJSUIRunning = false;
+ zwaveJSUIManager.zwaveJSUIConnected = false;
+ zwaveJSUIManager.scanInProgress = false;
+ zwaveJSUIManager.usbConfigured = false;
+ });
+
+ it('should addNode', () => {
+ const ADD_NODE_TIMEOUT = 60 * 1000;
+ const clock = useFakeTimers();
+ zwaveJSUIManager.mqttConnected = true;
+ zwaveJSUIManager.mqttClient = mqttClient;
+
+ zwaveJSUIManager.addNode();
+ assert.calledWithExactly(
+ zwaveJSUIManager.mqttClient.publish,
+ `${DEFAULT.ROOT}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/startInclusion/set`,
+ );
+
+ clock.tick(ADD_NODE_TIMEOUT);
+ expect(zwaveJSUIManager.scanInProgress).to.equal(true);
+ assert.calledWithExactly(
+ zwaveJSUIManager.mqttClient.publish,
+ `${DEFAULT.ROOT}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/stopInclusion/set`,
+ );
+ assert.calledWithExactly(
+ zwaveJSUIManager.mqttClient.publish,
+ `${DEFAULT.ROOT}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/getNodes/set`,
+ 'true',
+ );
+ clock.restore();
+ });
+
+ it('should removeNode', () => {
+ const REMOVE_NODE_TIMEOUT = 60 * 1000;
+ const clock = useFakeTimers();
+ zwaveJSUIManager.mqttConnected = true;
+ zwaveJSUIManager.mqttClient = mqttClient;
+
+ zwaveJSUIManager.removeNode();
+ assert.calledWithExactly(
+ zwaveJSUIManager.mqttClient.publish,
+ `${DEFAULT.ROOT}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/startExclusion/set`,
+ );
+
+ clock.tick(REMOVE_NODE_TIMEOUT);
+ expect(zwaveJSUIManager.scanInProgress).to.equal(true);
+ assert.calledWithExactly(
+ zwaveJSUIManager.mqttClient.publish,
+ `${DEFAULT.ROOT}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/startExclusion/set`,
+ );
+ assert.calledWithExactly(
+ zwaveJSUIManager.mqttClient.publish,
+ `${DEFAULT.ROOT}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/getNodes/set`,
+ 'true',
+ );
+ clock.restore();
+ });
+
+ it('should scanNetwork', () => {
+ zwaveJSUIManager.mqttConnected = true;
+ zwaveJSUIManager.mqttClient = mqttClient;
+
+ zwaveJSUIManager.scanNetwork();
+ assert.calledWithExactly(
+ zwaveJSUIManager.mqttClient.publish,
+ `${DEFAULT.ROOT}/_CLIENTS/${DEFAULT.ZWAVEJSUI_CLIENT_ID}/api/getNodes/set`,
+ 'true',
+ );
+ });
+
+ it('should setValue', () => {
+ zwaveJSUIManager.mqttConnected = true;
+ zwaveJSUIManager.mqttClient = mqttClient;
+
+ const commandClass = 2;
+ const endpoint = 3;
+ const property = 'property';
+ const device = {
+ nodeId: 1,
+ };
+ const deviceFeature = {
+ external_id: `zwave-js-ui:node_id:${device.nodeId}:comclass:${commandClass}:endpoint:${endpoint}:property:${property}`,
+ };
+ zwaveJSUIManager.setValue(device, deviceFeature, 0);
+ assert.calledWithExactly(
+ zwaveJSUIManager.mqttClient.publish,
+ `${DEFAULT.ROOT}/nodeID_${device.nodeId}/${commandClass}/${endpoint}/${property}/set`,
+ '0',
+ );
+ });
+
+ it('should setValue with property key', () => {
+ zwaveJSUIManager.mqttConnected = true;
+ zwaveJSUIManager.mqttClient = mqttClient;
+
+ const commandClass = 2;
+ const endpoint = 3;
+ const property = 'property';
+ const propertyKey = 'propertyKey';
+ const device = {
+ nodeId: 1,
+ };
+ const deviceFeature = {
+ external_id: `zwave-js-ui:node_id:${device.nodeId}:comclass:${commandClass}:endpoint:${endpoint}:property:${property}-${propertyKey}`,
+ };
+ zwaveJSUIManager.setValue(device, deviceFeature, 0);
+ assert.calledWithExactly(
+ zwaveJSUIManager.mqttClient.publish,
+ `${DEFAULT.ROOT}/nodeID_${device.nodeId}/${commandClass}/${endpoint}/${property}/${propertyKey}/set`,
+ '0',
+ );
+ });
+
+ it('should return Z-Wave status', () => {
+ const status = zwaveJSUIManager.getStatus();
+ expect(status).to.deep.equal({
+ dockerBased: true,
+ mqttConnected: false,
+ mqttExist: false,
+ mqttRunning: false,
+ ready: undefined,
+ scanInProgress: false,
+ usbConfigured: false,
+ zwaveJSUIExist: false,
+ zwaveJSUIRunning: false,
+ zwaveJSUIConnected: false,
+ });
+ });
+});
+
+describe('zwaveJSUIManager events', () => {
+ let gladys;
+ let zwaveJSUIManager;
+
+ before(() => {
+ gladys = {
+ event,
+ service: {
+ getService: fake.resolves({
+ list: fake.resolves([DRIVER_PATH]),
+ }),
+ },
+ variable: {
+ getValue: (name) => Promise.resolve(CONFIGURATION.EXTERNAL_ZWAVEJSUI ? true : null),
+ setValue: (name) => Promise.resolve(null),
+ },
+ };
+ zwaveJSUIManager = new ZwaveJSUIManager(gladys, mqtt, ZWAVEJSUI_SERVICE_ID);
+ zwaveJSUIManager.mqttConnected = true;
+ });
+
+ beforeEach(() => {
+ sinon.reset();
+ });
+
+ it('should receive scanComplete', () => {
+ zwaveJSUIManager.scanInProgress = true;
+ zwaveJSUIManager.scanComplete();
+ assert.calledOnceWithExactly(zwaveJSUIManager.eventManager.emit, EVENTS.WEBSOCKET.SEND_ALL, {
+ type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJSUI.SCAN_COMPLETE,
+ });
+ expect(zwaveJSUIManager.scanInProgress).to.eq(false);
+ });
+
+ it('should receive node ready info', () => {
+ const zwaveNode = {
+ id: 1,
+ manufacturerId: 'manufacturerId',
+ deviceId: 'deviceId',
+ product: 'product',
+ productType: 'productType',
+ productId: 'productId',
+ type: 'type',
+ firmwareVersion: 'firmwareVersion',
+ name: 'name',
+ loc: 'location',
+ status: 'status',
+ ready: true,
+ nodeType: 'nodeType',
+ getDefinedValueIDs: fake.returns([]),
+ };
+ zwaveJSUIManager.nodes = {
+ '1': {
+ nodeId: 1,
+ classes: {},
+ ready: false,
+ endpoints: [2],
+ },
+ };
+ zwaveJSUIManager.nodeReady(zwaveNode);
+ expect(zwaveJSUIManager.nodes).to.deep.equal({
+ '1': {
+ nodeId: 1,
+ classes: {},
+ endpoints: [2],
+ product: 'deviceId',
+ firmwareVersion: 'firmwareVersion',
+ name: 'name',
+ loc: 'location',
+ status: 'status',
+ ready: true,
+ },
+ });
+ });
+});
diff --git a/server/test/services/zwave-js-ui/zwave.test.js b/server/test/services/zwave-js-ui/zwave.test.js
new file mode 100644
index 0000000000..4efcac6207
--- /dev/null
+++ b/server/test/services/zwave-js-ui/zwave.test.js
@@ -0,0 +1,83 @@
+const sinon = require('sinon');
+
+const { expect } = require('chai');
+
+const { fake } = sinon;
+
+const ZwaveJSUIService = require('../../../services/zwave-js-ui/index');
+
+const ZWAVEJSUI_SERVICE_ID = 'ZWAVEJSUI_SERVICE_ID';
+const DRIVER_PATH = 'DRIVER_PATH';
+
+const event = {
+ emit: fake.resolves(null),
+};
+
+const gladys = {
+ event,
+ service: {
+ getService: () => {
+ return {
+ list: () => Promise.resolve([DRIVER_PATH]),
+ };
+ },
+ },
+ variable: {
+ getValue: fake.resolves(true),
+ setValue: fake.resolves(true),
+ },
+ system: {
+ isDocker: fake.resolves(true),
+ },
+ installMqttContainer: fake.returns(true),
+ installZwaveJSUIContainer: fake.returns(true),
+};
+
+describe('zwaveJSUIService', () => {
+ const zwaveJSUIService = ZwaveJSUIService(gladys, ZWAVEJSUI_SERVICE_ID);
+
+ beforeEach(() => {
+ sinon.reset();
+ });
+
+ it('should have controllers', () => {
+ expect(zwaveJSUIService)
+ .to.have.property('controllers')
+ .and.be.instanceOf(Object);
+ });
+
+ it('should start service', async () => {
+ gladys.variable.getValue = sinon.stub();
+ gladys.variable.getValue
+ .onFirstCall() // EXTERNAL_ZWAVEJSUI
+ .resolves('1')
+ .onSecondCall() // DRIVER_PATH
+ .resolves(DRIVER_PATH);
+ await zwaveJSUIService.start();
+ zwaveJSUIService.device.mqttClient.emit('connect');
+ expect(zwaveJSUIService.device.mqttConnected).to.equal(true);
+ expect(zwaveJSUIService.device.scanInProgress).to.equal(true);
+ });
+
+ it('should stop service', async () => {
+ await zwaveJSUIService.stop();
+ expect(zwaveJSUIService.device.mqttConnected).to.equal(false);
+ });
+
+ it('should stop isUsed', async () => {
+ zwaveJSUIService.device.mqttConnected = true;
+ zwaveJSUIService.device.nodes = [{}];
+ await zwaveJSUIService.isUsed();
+ });
+
+ it('should stop isNotUsed not connected', async () => {
+ zwaveJSUIService.device.mqttConnected = false;
+ await zwaveJSUIService.isUsed();
+ });
+
+ it('should stop isNotUsed no node', async () => {
+ zwaveJSUIService.device.mqttConnected = true;
+ zwaveJSUIService.device.nodes = [];
+ await zwaveJSUIService.isUsed();
+ });
+});
diff --git a/server/utils/constants.js b/server/utils/constants.js
index 09a07b77c0..d86e5a0bcd 100644
--- a/server/utils/constants.js
+++ b/server/utils/constants.js
@@ -803,6 +803,37 @@ const DEVICE_FEATURE_UNITS_BY_CATEGORY = {
],
};
+const DEVICE_FEATURE_MINMAX_BY_TYPE = {
+ [DEVICE_FEATURE_TYPES.SENSOR.BINARY]: {
+ MIN: 0,
+ MAX: 1,
+ },
+ [DEVICE_FEATURE_TYPES.SWITCH.POWER]: {
+ MIN: 0,
+ MAX: 10000, // 10 kW
+ },
+ [DEVICE_FEATURE_TYPES.SWITCH.ENERGY]: {
+ MIN: 0,
+ MAX: 100000, // 10 kW during 10000 hour (more than one year)
+ },
+ [DEVICE_FEATURE_TYPES.SWITCH.CURRENT]: {
+ MIN: 0,
+ MAX: 40,
+ },
+ [DEVICE_FEATURE_TYPES.SWITCH.VOLTAGE]: {
+ MIN: 0,
+ MAX: 400,
+ },
+ [DEVICE_FEATURE_TYPES.LIGHT.COLOR]: {
+ MIN: 0,
+ MAX: 0xffffff,
+ },
+ [DEVICE_FEATURE_TYPES.LIGHT.TEMPERATURE]: {
+ MIN: 0,
+ MAX: 100,
+ },
+};
+
const ACTIONS_STATUS = {
PENDING: 'pending',
SUCCESS: 'success',
@@ -871,6 +902,16 @@ const WEBSOCKET_MESSAGE_TYPES = {
DOWNLOAD_FINISHED: 'upgrade.download-finished',
DOWNLOAD_FAILED: 'upgrade.download-failed',
},
+ ZWAVEJSUI: {
+ DISCOVER: 'zwave-js-ui.discover',
+ STATUS_CHANGE: 'zwave-js-ui.status-change',
+ SCAN_COMPLETE: 'zwave-js-ui.scan-complete',
+ MQTT_ERROR: 'zwave-js-ui.mqtt-error',
+ PERMIT_JOIN: 'zwave-js-ui.permit-join',
+ NODE_READY: 'zwave-js-ui.node-ready',
+ NODE_ADDED: 'zwave-js-ui.node-added',
+ NODE_REMOVED: 'zwave-js-ui.node-removed',
+ },
LAN: {
SCANNING: 'lan.scanning',
},
@@ -1070,6 +1111,8 @@ module.exports.DEVICE_FEATURE_UNITS_LIST = DEVICE_FEATURE_UNITS_LIST;
module.exports.DEVICE_FEATURE_UNITS_BY_CATEGORY = DEVICE_FEATURE_UNITS_BY_CATEGORY;
+module.exports.DEVICE_FEATURE_MINMAX_BY_TYPE = DEVICE_FEATURE_MINMAX_BY_TYPE;
+
module.exports.SERVICE_STATUS = SERVICE_STATUS;
module.exports.SERVICE_STATUS_LIST = createList(SERVICE_STATUS);