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 ( +
+
+
+ {props.device.name} + {batteryLevel && ( +
+ +
+ )} +
+
+
+
+ {error === RequestStatus.Error && ( +
+ +
+ )} + {error === RequestStatus.ConflictError && ( +
+ +
+ )} + {deviceUpdated && ( +
+ +
+ )} +
+
+ + + } + /> + +
+
+ + +
+
+ + +

+ {mostRecentValueAt ? ( + + ) : ( + + )} +

+
+
+ + + + + +
+
+
+
+
+
+ ); + } +} + +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 ( +
+
+
+ {props.node.ready ? ( +

{props.node.name}

+ ) : ( +

+ +

+ )} +
+ + {' '} + {props.node.params.find(param => param.name === PARAMS.NODE_ID).value} + +
+
+
+
+
+ {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.action === 'remove' ? ( + + ) : ( + + )} +

+
+ +
+
+
+ {!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.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.mqttPassword} + class="form-control" + onInput={this.updatePassword} + autoComplete="new-password" + /> + + + + +
+
+
+ + + } + value={props.mqttTopicPrefix} + class="form-control" + onInput={this.updateMqttTopicPrefix} + autoComplete="no" + /> + +
+
+ + +
+ + )} + + {!props.externalZwaveJSUI && ( + <> +
+ + +
+

+ +

+
+ + + } + 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.mqttExist && 'MQTT'} + {props.zwaveJSUIExist && 'zwaveJSUI'}
+ {`Gladys`} + +
+ +
+
+ {props.mqttExist && ( + {`MQTT`} + )} + +
+ +
+
+ {props.zwaveJSUIExist && ( + {`zwaveJSUI`} + )} +
+
+ +
+
+ + {props.mqttRunning && ( + + + + )} + + + {props.zwaveJSUIRunning && ( + + + + )} +
+
+
+
+

+ +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ + + {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);