From 07b28c6493020b664bfbf236df2f1235e914dab7 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 12 Jan 2024 16:17:50 +0100 Subject: [PATCH] Z-Wave JS UI integration (#1982) --- .../assets/integrations/cover/zwave-js-ui.jpg | Bin 0 -> 23033 bytes .../zwavejs-ui-gateway-configuration.jpg | Bin 0 -> 98734 bytes .../zwavejs-ui-mqtt-configuration.jpg | Bin 0 -> 84711 bytes front/src/components/app.jsx | 9 + front/src/config/i18n/de.json | 56 + front/src/config/i18n/en.json | 56 + front/src/config/i18n/fr.json | 56 + front/src/config/integrations/devices.json | 5 + .../all/zwavejs-ui/ZwaveJSUIDeviceBox.jsx | 214 + .../all/zwavejs-ui/ZwaveJSUIPage.jsx | 73 + .../all/zwavejs-ui/device-page/DeviceTab.jsx | 133 + .../all/zwavejs-ui/device-page/EmptyState.jsx | 23 + .../all/zwavejs-ui/device-page/index.js | 16 + .../all/zwavejs-ui/device-page/style.css | 3 + .../zwavejs-ui/discover-page/DiscoverTab.jsx | 108 + .../zwavejs-ui/discover-page/EmptyState.jsx | 13 + .../all/zwavejs-ui/discover-page/index.js | 16 + .../all/zwavejs-ui/discover-page/style.css | 7 + .../all/zwavejs-ui/setup-page/index.js | 258 ++ server/services/index.js | 1 + .../zwavejs-ui/api/zwaveJSUI.controller.js | 89 + server/services/zwavejs-ui/index.js | 48 + server/services/zwavejs-ui/lib/constants.js | 40 + server/services/zwavejs-ui/lib/index.js | 39 + .../zwavejs-ui/lib/zwaveJSUI.connect.js | 63 + .../zwavejs-ui/lib/zwaveJSUI.disconnect.js | 22 + .../lib/zwaveJSUI.getConfiguration.js | 21 + .../lib/zwaveJSUI.handleNewMessage.js | 55 + .../services/zwavejs-ui/lib/zwaveJSUI.init.js | 11 + .../lib/zwaveJSUI.onNewDeviceDiscover.js | 24 + .../zwavejs-ui/lib/zwaveJSUI.publish.js | 24 + .../lib/zwaveJSUI.saveConfiguration.js | 25 + .../services/zwavejs-ui/lib/zwaveJSUI.scan.js | 14 + server/services/zwavejs-ui/package-lock.json | 487 ++ server/services/zwavejs-ui/package.json | 18 + .../zwavejs-ui/utils/convertToGladysDevice.js | 58 + .../api/zwaveJSUI.controller.test.js | 96 + server/test/services/zwavejs-ui/index.test.js | 41 + .../services/zwavejs-ui/lib/exampleData.json | 4109 +++++++++++++++++ .../zwavejs-ui/lib/zwaveJSUI.connect.test.js | 112 + .../lib/zwaveJSUI.disconnect.test.js | 64 + .../lib/zwaveJSUI.getConfiguration.test.js | 47 + .../lib/zwaveJSUI.handleNewMessage.test.js | 63 + .../zwavejs-ui/lib/zwaveJSUI.init.test.js | 49 + .../lib/zwaveJSUI.onNewDeviceDiscover.test.js | 55 + .../zwavejs-ui/lib/zwaveJSUI.publish.test.js | 63 + .../lib/zwaveJSUI.saveConfiguration.test.js | 35 + server/utils/constants.js | 11 + 48 files changed, 6830 insertions(+) create mode 100644 front/src/assets/integrations/cover/zwave-js-ui.jpg create mode 100644 front/src/assets/integrations/zwavejs-ui/zwavejs-ui-gateway-configuration.jpg create mode 100644 front/src/assets/integrations/zwavejs-ui/zwavejs-ui-mqtt-configuration.jpg create mode 100644 front/src/routes/integration/all/zwavejs-ui/ZwaveJSUIDeviceBox.jsx create mode 100644 front/src/routes/integration/all/zwavejs-ui/ZwaveJSUIPage.jsx create mode 100644 front/src/routes/integration/all/zwavejs-ui/device-page/DeviceTab.jsx create mode 100644 front/src/routes/integration/all/zwavejs-ui/device-page/EmptyState.jsx create mode 100644 front/src/routes/integration/all/zwavejs-ui/device-page/index.js create mode 100644 front/src/routes/integration/all/zwavejs-ui/device-page/style.css create mode 100644 front/src/routes/integration/all/zwavejs-ui/discover-page/DiscoverTab.jsx create mode 100644 front/src/routes/integration/all/zwavejs-ui/discover-page/EmptyState.jsx create mode 100644 front/src/routes/integration/all/zwavejs-ui/discover-page/index.js create mode 100644 front/src/routes/integration/all/zwavejs-ui/discover-page/style.css create mode 100644 front/src/routes/integration/all/zwavejs-ui/setup-page/index.js create mode 100644 server/services/zwavejs-ui/api/zwaveJSUI.controller.js create mode 100644 server/services/zwavejs-ui/index.js create mode 100644 server/services/zwavejs-ui/lib/constants.js create mode 100644 server/services/zwavejs-ui/lib/index.js create mode 100644 server/services/zwavejs-ui/lib/zwaveJSUI.connect.js create mode 100644 server/services/zwavejs-ui/lib/zwaveJSUI.disconnect.js create mode 100644 server/services/zwavejs-ui/lib/zwaveJSUI.getConfiguration.js create mode 100644 server/services/zwavejs-ui/lib/zwaveJSUI.handleNewMessage.js create mode 100644 server/services/zwavejs-ui/lib/zwaveJSUI.init.js create mode 100644 server/services/zwavejs-ui/lib/zwaveJSUI.onNewDeviceDiscover.js create mode 100644 server/services/zwavejs-ui/lib/zwaveJSUI.publish.js create mode 100644 server/services/zwavejs-ui/lib/zwaveJSUI.saveConfiguration.js create mode 100644 server/services/zwavejs-ui/lib/zwaveJSUI.scan.js create mode 100644 server/services/zwavejs-ui/package-lock.json create mode 100644 server/services/zwavejs-ui/package.json create mode 100644 server/services/zwavejs-ui/utils/convertToGladysDevice.js create mode 100644 server/test/services/zwavejs-ui/api/zwaveJSUI.controller.test.js create mode 100644 server/test/services/zwavejs-ui/index.test.js create mode 100644 server/test/services/zwavejs-ui/lib/exampleData.json create mode 100644 server/test/services/zwavejs-ui/lib/zwaveJSUI.connect.test.js create mode 100644 server/test/services/zwavejs-ui/lib/zwaveJSUI.disconnect.test.js create mode 100644 server/test/services/zwavejs-ui/lib/zwaveJSUI.getConfiguration.test.js create mode 100644 server/test/services/zwavejs-ui/lib/zwaveJSUI.handleNewMessage.test.js create mode 100644 server/test/services/zwavejs-ui/lib/zwaveJSUI.init.test.js create mode 100644 server/test/services/zwavejs-ui/lib/zwaveJSUI.onNewDeviceDiscover.test.js create mode 100644 server/test/services/zwavejs-ui/lib/zwaveJSUI.publish.test.js create mode 100644 server/test/services/zwavejs-ui/lib/zwaveJSUI.saveConfiguration.test.js 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 0000000000000000000000000000000000000000..e87982939de15c821ccb4261b15c2fb924dc8016 GIT binary patch literal 23033 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 Xz~3_Pw+#F(1AoiF|DR<5K^gpCDUm?@ literal 0 HcmV?d00001 diff --git a/front/src/assets/integrations/zwavejs-ui/zwavejs-ui-gateway-configuration.jpg b/front/src/assets/integrations/zwavejs-ui/zwavejs-ui-gateway-configuration.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a23b4a4b32aca6eaee74653ad4f3f184d3ff5073 GIT binary patch literal 98734 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 literal 0 HcmV?d00001 diff --git a/front/src/components/app.jsx b/front/src/components/app.jsx index 4d842b2160..8eb49ff786 100644 --- a/front/src/components/app.jsx +++ b/front/src/components/app.jsx @@ -143,6 +143,11 @@ import TuyaDiscoverPage from '../routes/integration/all/tuya/discover-page'; import SonosDevicePage from '../routes/integration/all/sonos/device-page'; import SonosDiscoveryPage from '../routes/integration/all/sonos/discover-page'; +// ZWaveJS-UI integration +import ZwaveJSUIDevicePage from '../routes/integration/all/zwavejs-ui/device-page'; +import ZwaveJSUIDiscoveryPage from '../routes/integration/all/zwavejs-ui/discover-page'; +import ZwaveJSUISetupPage from '../routes/integration/all/zwavejs-ui/setup-page'; + // MELCloud integration import MELCloudPage from '../routes/integration/all/melcloud/device-page'; import MELCloudEditPage from '../routes/integration/all/melcloud/edit-page'; @@ -282,6 +287,10 @@ const AppRouter = connect( + + + + diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index ab4d5ded08..4b155d1ae3 100644 --- a/front/src/config/i18n/de.json +++ b/front/src/config/i18n/de.json @@ -943,6 +943,62 @@ "conflictError": "Das aktuelle Gerät ist bereits in Gladys vorhanden." } }, + "zwavejs-ui": { + "title": "ZWave JS UI", + "description": "Steuern Sie Ihre Geräte in Z-Wave JS UI", + "deviceTab": "Geräte", + "discoverTab": "Z-Wave JS UI Entdeckung", + "setupTab": "Konfiguration", + "documentation": "Z-Wave JS UI Dokumentation", + "discoverDeviceDescr": "Automatisches Scannen der Z-Wave JS UI Geräte", + "nameLabel": "Gerätename", + "namePlaceholder": "Geben Sie den Namen Ihres Geräts ein", + "noFeaturesHandled": "Dieses Gerät wird noch nicht von Gladys unterstützt. Bitte reichen Sie es ein, damit es hinzugefügt werden kann!", + "featuresLabel": "Funktionen", + "roomLabel": "Raum", + "saveButton": "Speichern", + "alreadyCreatedButton": "Bereits erstellt", + "deleteButton": "Löschen", + "alphaWarning": "Diese Integration ist eine Alpha-Version und unterstützt derzeit nur ein Gerät: den Türöffnungssensor von Fibaro. Wir sind dankbar für jede Unterstützung bei der Integration weiterer Geräte!", + "device": { + "title": "Z-Wave JS UI Geräte in Gladys", + "editButton": "Bearbeiten", + "noDeviceFound": "Kein Z-Wave JS UI Gerät gefunden.", + "featuresLabel": "Funktionen" + }, + "discover": { + "title": "Auf Ihrer Z-Wave JS UI Instanz erkannte Geräte", + "description": "Z-Wave JS UI Geräte werden automatisch erkannt. Achtung, hier werden nur Geräte mit einem Namen und einem \"Standort\" angezeigt. Wenn Sie diese Einstellungen bearbeiten, müssen Sie das Gerät in Gladys erneut verbinden.", + "error": "Fehler beim Erkennen von Z-Wave JS UI Geräten. Ist Ihr MQTT-Broker gut erreichbar und verfügbar?", + "noDeviceFound": "Kein Z-Wave JS UI Gerät wurde erkannt.", + "errorWhileScanning": "Fehler bei der Erkennung aufgetreten.", + "scan": "Scannen" + }, + "setup": { + "title": "Konfiguration", + "description": "Sie müssen Gladys mit dem MQTT-Broker verbinden, mit dem Ihre Z-Wave JS UI Instanz verbunden ist. Diese Integration richtet sich an Benutzer, die bereits Z-Wave-Geräte haben. Wenn Sie gerade mit Ihrer Installation beginnen, empfehlen wir Ihnen, den Zigbee-Protokoll zu verwenden.", + "zwaveJsUiConfigurationTitle": "Z-Wave JS UI konfigurieren", + "zwaveJsUiConfigurationMqttDescription": "Auf der Z-Wave JS UI Seite müssen Sie das Feld \"Name\" im Abschnitt \"MQTT\" mit \"zwave-js-ui\" ausfüllen, sonst wird das Präfix einiger MQTT-Themen falsch sein.", + "zwaveJsUiConfigurationGatewayDescription": "Füllen Sie anschließend den Abschnitt \"Gateway\" mit diesen genauen Einstellungen aus:", + "mqttConfigurationTitle": "MQTT-Server konfigurieren", + "urlLabel": "Broker-URL", + "urlPlaceholder": "z. B. mqtt://[broker-mqtt-Adresse]:[Port]", + "userLabel": "Benutzername", + "userPlaceholder": "Geben Sie den Benutzernamen des MQTT-Brokers ein", + "passwordLabel": "Passwort", + "passwordPlaceholder": "Geben Sie das Passwort des MQTT-Brokers ein", + "saveLabel": "Konfiguration speichern", + "error": "Fehler beim Speichern der Konfiguration aufgetreten.", + "connecting": "Konfiguration gespeichert. Verbindung zum MQTT-Broker wird hergestellt...", + "connected": "Erfolgreich mit dem MQTT-Broker verbunden!", + "notConnected": "Fehler bei der Verbindung, bitte überprüfen Sie Ihre Konfiguration." + }, + "error": { + "defaultError": "Fehler beim Speichern des Geräts aufgetreten.", + "defaultDeletionError": "Fehler beim Löschen des Geräts aufgetreten.", + "conflictError": "Das aktuelle Gerät ist bereits in Gladys." + } + }, "melcloud": { "title": "MELCloud", "description": "Steuere deine MELCloud-Geräte (funktioniert über die Cloud.)", diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index b223216605..96c4e35a22 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -945,6 +945,62 @@ "conflictError": "The current device is already in Gladys." } }, + "zwavejs-ui": { + "title": "ZWave JS UI", + "description": "Control your devices in Z-Wave JS UI", + "deviceTab": "Devices", + "discoverTab": "Z-Wave JS UI Discovery", + "setupTab": "Configuration", + "documentation": "Z-Wave JS UI Documentation", + "discoverDeviceDescr": "Automatically scan for Z-Wave JS UI devices", + "nameLabel": "Device Name", + "namePlaceholder": "Enter your device name", + "noFeaturesHandled": "This device is not yet supported by Gladys; feel free to submit it for inclusion!", + "featuresLabel": "Features", + "roomLabel": "Room", + "saveButton": "Save", + "alreadyCreatedButton": "Already Created", + "deleteButton": "Delete", + "alphaWarning": "This integration is in alpha and currently only supports one device: The Fibaro Door/Window sensor. We welcome any assistance to integrate other devices!", + "device": { + "title": "Z-Wave JS UI Devices in Gladys", + "editButton": "Edit", + "noDeviceFound": "No Z-Wave JS UI devices found.", + "featuresLabel": "Features" + }, + "discover": { + "title": "Devices detected on your Z-Wave JS UI instance", + "description": "Z-Wave JS UI devices are automatically discovered. Note that only devices with a name and a 'location' are displayed here. If you edit these settings, you will need to re-pair the device in Gladys.", + "error": "Error discovering Z-Wave JS UI devices. Is your MQTT broker available and accessible?", + "noDeviceFound": "No Z-Wave JS UI devices were discovered.", + "errorWhileScanning": "An error occurred during discovery.", + "scan": "Scan" + }, + "setup": { + "title": "Configuration", + "description": "You must connect Gladys to the MQTT broker to which your Z-Wave JS UI instance is connected. This integration is for users who already have Z-Wave devices. If you are starting your installation, we recommend using the Zigbee protocol.", + "zwaveJsUiConfigurationTitle": "Configure Z-Wave JS UI", + "zwaveJsUiConfigurationMqttDescription": "On the Z-Wave JS UI side, you need to fill in the \"Name\" field in \"MQTT\" with \"zwave-js-ui,\" otherwise, the prefix for some MQTT topics will be incorrect.", + "zwaveJsUiConfigurationGatewayDescription": "Next, please fill in the \"Gateway\" section with these exact parameters:", + "mqttConfigurationTitle": "Configure MQTT Server", + "urlLabel": "Broker URL", + "urlPlaceholder": "Ex: mqtt://[mqtt-broker-address]:[port]", + "userLabel": "Username", + "userPlaceholder": "Enter the MQTT broker username", + "passwordLabel": "Password", + "passwordPlaceholder": "Enter the MQTT broker password", + "saveLabel": "Save Configuration", + "error": "An error occurred while saving the configuration.", + "connecting": "Configuration saved. Connecting to the MQTT broker...", + "connected": "Successfully connected to the MQTT broker!", + "notConnected": "Error connecting. Please check your configuration." + }, + "error": { + "defaultError": "An error occurred while saving the device.", + "defaultDeletionError": "An error occurred while deleting the device.", + "conflictError": "The current device is already in Gladys." + } + }, "melcloud": { "title": "MELCloud", "description": "Control your MELCloud devices (works with the cloud)", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index b5e7e46259..d38a0cc868 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -1074,6 +1074,62 @@ "conflictError": "L'appareil actuel est déjà dans Gladys." } }, + "zwavejs-ui": { + "title": "ZWave JS UI", + "description": "Contrôler vos appareils dans Z-Wave JS UI", + "deviceTab": "Appareils", + "discoverTab": "Découverte Z-Wave JS UI", + "setupTab": "Configuration", + "documentation": "Documentation Z-Wave JS UI", + "discoverDeviceDescr": "Scanner automatiquement les appareils Z-Wave JS UI", + "nameLabel": "Nom de l'appareil", + "namePlaceholder": "Entrez le nom de votre appareil", + "noFeaturesHandled": "Cet appareil n'est pas encore géré par Gladys, n'hésitez pas à nous le soumettre pour qu'il soit ajouté !", + "featuresLabel": "Fonctionnalités", + "roomLabel": "Pièce", + "saveButton": "Sauvegarder", + "alreadyCreatedButton": "Déjà créé", + "deleteButton": "Supprimer", + "alphaWarning": "Cette intégration est une alpha et ne gère pour l'instant qu'un seul appareil: Le capteur d'ouverture de porte Fibaro. Nous sommes preneur de toute aide pour intégrer d'autres appareils !", + "device": { + "title": "Appareils Z-Wave JS UI dans Gladys", + "editButton": "Editer", + "noDeviceFound": "Aucun appareil Z-Wave JS UI trouvé.", + "featuresLabel": "Fonctionnalités" + }, + "discover": { + "title": "Appareils détectés sur votre instance Z-Wave JS UI", + "description": "Les appareils Z-Wave JS UI sont automatiquement découverts. Attention, seuls les appareils avec un nom et une \"location\" sont affichés ici. Si vous éditez ces paramètres, vous devrez réappairer l'appareil dans Gladys.", + "error": "Erreur de découverte des appareils Z-Wave JS UI. Est-ce que votre broker MQTT est bien disponible et accessible ?", + "noDeviceFound": "Aucun appareil Z-Wave JS UI n'a été découvert.", + "errorWhileScanning": "Une erreur est survenue lors de la découverte.", + "scan": "Scanner" + }, + "setup": { + "title": "Configuration", + "description": "Vous devez connecter Gladys au broker MQTT auquel est connecté votre instance Z-Wave JS UI. Cette intégration s'adresse à un public qui a déjà des appareils Z-Wave. Si vous commencez votre installation, nous vous recommandons de passer par le protocole Zigbee.", + "zwaveJsUiConfigurationTitle": "Configurer Z-Wave JS UI", + "zwaveJsUiConfigurationMqttDescription": "Côté Z-Wave JS UI, vous devez remplir dans \"MQTT\" le champ \"Name\" par \"zwave-js-ui\" sinon le préfix de certains topic MQTT ne sera pas bon.", + "zwaveJsUiConfigurationGatewayDescription": "Ensuite, veuillez remplir la section \"Gateway\" avec ces paramètres exactement :", + "mqttConfigurationTitle": "Configurer le serveur MQTT", + "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", + "saveLabel": "Sauvegarder la configuration", + "error": "Une erreur s'est produite lors de l'enregistrement de la configuration.", + "connecting": "Configuration enregistrée. Connexion au broker MQTT en cours...", + "connected": "Connecté au broker MQTT avec succès !", + "notConnected": "Erreur lors de la connexion, veuillez vérifier votre configuration." + }, + "error": { + "defaultError": "Une erreur s'est produite lors de l'enregistrement de l'appareil.", + "defaultDeletionError": "Une erreur s'est produite lors de la suppression de l'appareil.", + "conflictError": "L'appareil actuel est déjà dans Gladys." + } + }, "melcloud": { "title": "MELCloud", "description": "Contrôler vos appareils MELCloud", diff --git a/front/src/config/integrations/devices.json b/front/src/config/integrations/devices.json index ab4e5b6fd1..d4bed59b09 100644 --- a/front/src/config/integrations/devices.json +++ b/front/src/config/integrations/devices.json @@ -78,5 +78,10 @@ "key": "sonos", "link": "sonos", "img": "/assets/integrations/cover/sonos.jpg" + }, + { + "key": "zwavejs-ui", + "link": "zwavejs-ui", + "img": "/assets/integrations/cover/zwave-js-ui.jpg" } ] diff --git a/front/src/routes/integration/all/zwavejs-ui/ZwaveJSUIDeviceBox.jsx b/front/src/routes/integration/all/zwavejs-ui/ZwaveJSUIDeviceBox.jsx new file mode 100644 index 0000000000..2490be7d29 --- /dev/null +++ b/front/src/routes/integration/all/zwavejs-ui/ZwaveJSUIDeviceBox.jsx @@ -0,0 +1,214 @@ +import { Component } from 'preact'; +import { Text, Localizer, MarkupText } from 'preact-i18n'; +import cx from 'classnames'; +import get from 'get-value'; + +import DeviceFeatures from '../../../../components/device/view/DeviceFeatures'; + +import { connect } from 'unistore/preact'; + +class ZwaveJSUIDeviceBox extends Component { + componentWillMount() { + this.setState({ + device: this.props.device + }); + } + + componentWillReceiveProps(nextProps) { + this.setState({ + device: nextProps.device + }); + } + + updateName = e => { + this.setState({ + device: { + ...this.state.device, + name: e.target.value + } + }); + }; + + updateRoom = e => { + this.setState({ + device: { + ...this.state.device, + room_id: e.target.value + } + }); + }; + + saveDevice = async () => { + this.setState({ + loading: true, + errorMessage: null + }); + try { + let deviceDidNotExist = this.state.device.id === undefined; + const savedDevice = await this.props.httpClient.post(`/api/v1/device`, this.state.device); + if (deviceDidNotExist) { + savedDevice.alreadyExist = true; + } + this.setState({ + device: savedDevice + }); + } catch (e) { + let errorMessage = 'integration.zwavejs-ui.error.defaultError'; + if (e.response.status === 409) { + errorMessage = 'integration.zwavejs-ui.error.conflictError'; + } + this.setState({ + errorMessage + }); + } + this.setState({ + loading: false + }); + }; + + deleteDevice = async () => { + this.setState({ + loading: true, + errorMessage: null, + tooMuchStatesError: false, + statesNumber: undefined + }); + try { + if (this.state.device.created_at) { + await this.props.httpClient.delete(`/api/v1/device/${this.state.device.selector}`); + } + this.props.getZwaveJSUIDevices(); + } catch (e) { + const status = get(e, 'response.status'); + const dataMessage = get(e, 'response.data.message'); + if (status === 400 && dataMessage && dataMessage.includes('Too much states')) { + const statesNumber = new Intl.NumberFormat().format(dataMessage.split(' ')[0]); + this.setState({ tooMuchStatesError: true, statesNumber }); + } else { + this.setState({ + errorMessage: 'integration.zwavejs-ui.error.defaultDeletionError' + }); + } + } + this.setState({ + loading: false + }); + }; + + render( + { deviceIndex, editable, deleteButton, housesWithRooms }, + { device, loading, errorMessage, tooMuchStatesError, statesNumber } + ) { + const validModel = device.features && device.features.length > 0; + + return ( +
+
+
{device.name}
+
+
+
+
+ {errorMessage && ( +
+ +
+ )} + {tooMuchStatesError && ( +
+ +
+ )} +
+ + + } + disabled={!editable || !validModel} + /> + +
+ + {housesWithRooms && ( +
+ + +
+ )} + + {device.features.length > 0 && ( +
+ + +
+ )} + + {device.features.length === 0 && ( +
+ +
+ )} + +
+ {device.alreadyExist && ( + + )} + + {!device.alreadyExist && ( + + )} + + {deleteButton && ( + + )} +
+
+
+
+
+
+ ); + } +} + +export default connect('httpClient', {})(ZwaveJSUIDeviceBox); diff --git a/front/src/routes/integration/all/zwavejs-ui/ZwaveJSUIPage.jsx b/front/src/routes/integration/all/zwavejs-ui/ZwaveJSUIPage.jsx new file mode 100644 index 0000000000..b03d4d81bd --- /dev/null +++ b/front/src/routes/integration/all/zwavejs-ui/ZwaveJSUIPage.jsx @@ -0,0 +1,73 @@ +import { Text } from 'preact-i18n'; +import { Link } from 'preact-router/match'; +import DeviceConfigurationLink from '../../../../components/documentation/DeviceConfigurationLink'; + +const ZwaveJSUIPage = ({ children, user }) => ( +
+
+
+
+
+
+

+ +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
{children}
+
+
+
+
+
+); + +export default ZwaveJSUIPage; diff --git a/front/src/routes/integration/all/zwavejs-ui/device-page/DeviceTab.jsx b/front/src/routes/integration/all/zwavejs-ui/device-page/DeviceTab.jsx new file mode 100644 index 0000000000..2a15cf4371 --- /dev/null +++ b/front/src/routes/integration/all/zwavejs-ui/device-page/DeviceTab.jsx @@ -0,0 +1,133 @@ +import { Text, Localizer } from 'preact-i18n'; +import cx from 'classnames'; + +import EmptyState from './EmptyState'; +import { RequestStatus } from '../../../../../utils/consts'; +import style from './style.css'; +import CardFilter from '../../../../../components/layout/CardFilter'; +import ZwaveJSUIDeviceBox from '../ZwaveJSUIDeviceBox'; +import debounce from 'debounce'; +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; + +class DeviceTab extends Component { + search = async e => { + await this.setState({ + search: e.target.value + }); + this.getZwaveJSUIDevices(); + }; + changeOrderDir = async e => { + await this.setState({ + orderDir: e.target.value + }); + this.getZwaveJSUIDevices(); + }; + getZwaveJSUIDevices = async () => { + this.setState({ + getZwaveJSUIStatus: RequestStatus.Getting + }); + try { + const options = { + order_dir: this.state.orderDir || 'asc' + }; + if (this.state.search && this.state.search.length) { + options.search = this.state.search; + } + + const zwaveJSUIDevices = await this.props.httpClient.get('/api/v1/service/zwavejs-ui/device', options); + this.setState({ + zwaveJSUIDevices, + getZwaveJSUIStatus: RequestStatus.Success + }); + } catch (e) { + this.setState({ + getZwaveJSUIStatus: e.message + }); + } + }; + constructor(props) { + super(props); + this.debouncedSearch = debounce(this.search, 200).bind(this); + } + + componentWillMount() { + this.getZwaveJSUIDevices(); + this.getHouses(); + } + + async getHouses() { + this.setState({ + housesGetStatus: RequestStatus.Getting + }); + try { + const params = { + expand: 'rooms' + }; + const housesWithRooms = await this.props.httpClient.get(`/api/v1/house`, params); + this.setState({ + housesWithRooms, + housesGetStatus: RequestStatus.Success + }); + } catch (e) { + this.setState({ + housesGetStatus: RequestStatus.Error + }); + } + } + + render({}, { orderDir, search, getZwaveJSUIStatus, zwaveJSUIDevices, housesWithRooms }) { + return ( +
+
+

+ +

+
+ + } + /> + +
+
+
+
+
+
+
+ +
+
+ {zwaveJSUIDevices && + zwaveJSUIDevices.length > 0 && + zwaveJSUIDevices.map((device, index) => ( + + ))} + {!zwaveJSUIDevices || (zwaveJSUIDevices.length === 0 && )} +
+
+
+
+
+ ); + } +} + +export default connect('httpClient', {})(DeviceTab); diff --git a/front/src/routes/integration/all/zwavejs-ui/device-page/EmptyState.jsx b/front/src/routes/integration/all/zwavejs-ui/device-page/EmptyState.jsx new file mode 100644 index 0000000000..81bad0ee49 --- /dev/null +++ b/front/src/routes/integration/all/zwavejs-ui/device-page/EmptyState.jsx @@ -0,0 +1,23 @@ +import { Text } from 'preact-i18n'; +import { Link } from 'preact-router/match'; +import cx from 'classnames'; +import style from './style.css'; + +const EmptyState = () => ( +
+
+ + +
+ + + + +
+
+
+); + +export default EmptyState; diff --git a/front/src/routes/integration/all/zwavejs-ui/device-page/index.js b/front/src/routes/integration/all/zwavejs-ui/device-page/index.js new file mode 100644 index 0000000000..cc9602d836 --- /dev/null +++ b/front/src/routes/integration/all/zwavejs-ui/device-page/index.js @@ -0,0 +1,16 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import DeviceTab from './DeviceTab'; +import ZwaveJSUIPage from '../ZwaveJSUIPage'; + +class DevicePage extends Component { + render(props, {}) { + return ( + + + + ); + } +} + +export default connect('user', {})(DevicePage); diff --git a/front/src/routes/integration/all/zwavejs-ui/device-page/style.css b/front/src/routes/integration/all/zwavejs-ui/device-page/style.css new file mode 100644 index 0000000000..dc16983e90 --- /dev/null +++ b/front/src/routes/integration/all/zwavejs-ui/device-page/style.css @@ -0,0 +1,3 @@ +.emptyStateDivBox { + margin-top: 35px; +} diff --git a/front/src/routes/integration/all/zwavejs-ui/discover-page/DiscoverTab.jsx b/front/src/routes/integration/all/zwavejs-ui/discover-page/DiscoverTab.jsx new file mode 100644 index 0000000000..740f702a23 --- /dev/null +++ b/front/src/routes/integration/all/zwavejs-ui/discover-page/DiscoverTab.jsx @@ -0,0 +1,108 @@ +import { Text } from 'preact-i18n'; +import cx from 'classnames'; + +import EmptyState from './EmptyState'; +import style from './style.css'; +import ZwaveJSUIDeviceBox from '../ZwaveJSUIDeviceBox'; +import { connect } from 'unistore/preact'; +import { Component } from 'preact'; +import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants'; + +class DiscoverTab extends Component { + scan = async () => { + try { + await this.props.httpClient.post('/api/v1/service/zwavejs-ui/discover'); + } catch (e) { + console.error(e); + } + }; + + getDiscoveredDevices = async () => { + this.setState({ + loading: true + }); + try { + const discoveredDevices = await this.props.httpClient.get('/api/v1/service/zwavejs-ui/node'); + const existingZwaveJSUIDevices = await this.props.httpClient.get('/api/v1/service/zwavejs-ui/device', {}); + discoveredDevices.forEach(discoveredDevice => { + const existingDevice = existingZwaveJSUIDevices.find(d => d.external_id === discoveredDevice.external_id); + if (existingDevice) { + discoveredDevice.alreadyExist = true; + } + }); + this.setState({ + discoveredDevices, + loading: false, + errorLoading: false + }); + } catch (e) { + this.setState({ + loading: false, + errorLoading: true + }); + } + }; + + async componentWillMount() { + this.getDiscoveredDevices(); + this.props.session.dispatcher.addListener( + WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.SCAN_COMPLETED, + this.getDiscoveredDevices + ); + } + + componentWillUnmount() { + this.props.session.dispatcher.removeListener( + WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.SCAN_COMPLETED, + this.getDiscoveredDevices + ); + } + + render(props, { loading, errorLoading, discoveredDevices }) { + return ( +
+
+

+ +

+
+ +
+
+
+
+ +
+
+ +
+ {errorLoading && ( +
+ +
+ )} +
+
+
+
+ {discoveredDevices && + discoveredDevices.map((device, index) => ( + + ))} + {!discoveredDevices || (discoveredDevices.length === 0 && )} +
+
+
+
+
+ ); + } +} + +export default connect('httpClient,session', {})(DiscoverTab); diff --git a/front/src/routes/integration/all/zwavejs-ui/discover-page/EmptyState.jsx b/front/src/routes/integration/all/zwavejs-ui/discover-page/EmptyState.jsx new file mode 100644 index 0000000000..eb63a13bf6 --- /dev/null +++ b/front/src/routes/integration/all/zwavejs-ui/discover-page/EmptyState.jsx @@ -0,0 +1,13 @@ +import { MarkupText } 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/zwavejs-ui/discover-page/index.js b/front/src/routes/integration/all/zwavejs-ui/discover-page/index.js new file mode 100644 index 0000000000..109e10410e --- /dev/null +++ b/front/src/routes/integration/all/zwavejs-ui/discover-page/index.js @@ -0,0 +1,16 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import DiscoverTab from './DiscoverTab'; +import ZwaveJSUIPage from '../ZwaveJSUIPage'; + +class ZwaveJSUIDiscoverPage extends Component { + render(props) { + return ( + + + + ); + } +} + +export default connect('user', {})(ZwaveJSUIDiscoverPage); diff --git a/front/src/routes/integration/all/zwavejs-ui/discover-page/style.css b/front/src/routes/integration/all/zwavejs-ui/discover-page/style.css new file mode 100644 index 0000000000..2bce902933 --- /dev/null +++ b/front/src/routes/integration/all/zwavejs-ui/discover-page/style.css @@ -0,0 +1,7 @@ +.emptyStateDivBox { + margin-top: 89px; +} + +.zwaveJSUIListBody { + min-height: 200px; +} diff --git a/front/src/routes/integration/all/zwavejs-ui/setup-page/index.js b/front/src/routes/integration/all/zwavejs-ui/setup-page/index.js new file mode 100644 index 0000000000..8521e3eca8 --- /dev/null +++ b/front/src/routes/integration/all/zwavejs-ui/setup-page/index.js @@ -0,0 +1,258 @@ +import { Text, Localizer } from 'preact-i18n'; +import cx from 'classnames'; +import { connect } from 'unistore/preact'; +import { Component } from 'preact'; +import get from 'get-value'; + +import ZwaveJSUIPage from '../ZwaveJSUIPage'; +import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants'; + +class DiscoverTab extends Component { + showPasswordTimer = null; + + state = { + loading: true, + mqttUrl: '', + mqttUsername: '', + mqttPassword: '' + }; + + updateUrl = e => { + this.setState({ mqttUrl: e.target.value }); + }; + + updateUsername = e => { + this.setState({ mqttUsername: e.target.value }); + }; + + updatePassword = e => { + this.setState({ mqttPassword: e.target.value }); + }; + + togglePassword = () => { + const { showPassword } = this.state; + + if (this.showPasswordTimer) { + clearTimeout(this.showPasswordTimer); + this.showPasswordTimer = null; + } + + this.setState({ showPassword: !showPassword }); + + if (!showPassword) { + this.showPasswordTimer = setTimeout(() => this.setState({ showPassword: false }), 5000); + } + }; + + getStatus = async () => { + try { + const { configured, connected } = await this.props.httpClient.get(`/api/v1/service/zwavejs-ui/status`); + await this.setState({ configured, connected }); + } catch (e) { + console.error(e); + } + }; + + getConfiguration = async () => { + try { + const config = await this.props.httpClient.get('/api/v1/service/zwavejs-ui/configuration'); + const mqttUrl = config.mqtt_url || ''; + const mqttUsername = config.mqtt_username || ''; + const mqttPassword = config.mqtt_password || ''; + await this.setState({ mqttUrl, mqttUsername, mqttPassword }); + } catch (e) { + console.error(e); + } + }; + + init = async () => { + await this.setState({ loading: true }); + await this.getStatus(); + await this.getConfiguration(); + console.log('Finish init'); + await this.setState({ loading: false }); + }; + + saveConfiguration = async e => { + e.preventDefault(); + const { mqttUrl, mqttUsername, mqttPassword } = this.state; + try { + await this.setState({ unknownError: null, unknownErrorStatus: null }); + await this.props.httpClient.post('/api/v1/service/zwavejs-ui/configuration', { + mqtt_url: mqttUrl, + mqtt_username: mqttUsername, + mqtt_password: mqttPassword + }); + await this.props.httpClient.post('/api/v1/service/zwavejs-ui/connect'); + } catch (e) { + console.error(e); + const status = get(e, 'response.status'); + const error = get(e, 'response.data.error'); + this.setState({ unknownError: JSON.stringify(error), unknownErrorStatus: status }); + } + }; + + componentDidMount() { + this.init(); + this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.CONNECTED, this.getStatus); + this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.ERROR, this.getStatus); + } + + componentWillUnmount() { + if (this.showPasswordTimer) { + clearTimeout(this.showPasswordTimer); + this.showPasswordTimer = null; + } + this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.CONNECTED, this.getStatus); + this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.ERROR, this.getStatus); + } + + render( + props, + { + loading, + configured, + connected, + unknownError, + unknownErrorStatus, + mqttUrl, + mqttUsername, + mqttPassword, + showPassword + } + ) { + return ( + +
+
+

+ +

+
+
+
+ +
+
+ +
+ +

+ +

+

+ +

+ +

+ +

+ + +

+ +

+ + {configured && !connected && ( +
+ +
+ )} + {connected && ( +
+ +
+ )} + {unknownError && ( +
+ {unknownErrorStatus} {unknownError} +
+ )} +
+
+
+
+
+ + + } + value={mqttUrl} + class="form-control" + onInput={this.updateUrl} + /> + +
+ +
+ + + } + value={mqttUsername} + class="form-control" + onInput={this.updateUsername} + autocomplete="off" + /> + +
+ +
+ +
+ + } + value={mqttPassword} + class="form-control" + onInput={this.updatePassword} + autocomplete="off" + /> + + + + +
+
+ +
+ +
+ +
+
+
+
+ + ); + } +} + +export default connect('httpClient,user,session', {})(DiscoverTab); diff --git a/server/services/index.js b/server/services/index.js index 7e3ba5d726..359bb3701a 100644 --- a/server/services/index.js +++ b/server/services/index.js @@ -24,3 +24,4 @@ module.exports.tuya = require('./tuya'); module.exports.melcloud = require('./melcloud'); module.exports['node-red'] = require('./node-red'); module.exports.sonos = require('./sonos'); +module.exports['zwavejs-ui'] = require('./zwavejs-ui'); diff --git a/server/services/zwavejs-ui/api/zwaveJSUI.controller.js b/server/services/zwavejs-ui/api/zwaveJSUI.controller.js new file mode 100644 index 0000000000..9ed6c3f936 --- /dev/null +++ b/server/services/zwavejs-ui/api/zwaveJSUI.controller.js @@ -0,0 +1,89 @@ +const asyncMiddleware = require('../../../api/middlewares/asyncMiddleware'); + +module.exports = function ZwaveJSUIController(zwaveJSUIHandler) { + /** + * @api {post} /api/v1/service/zwavejs-ui/discover Start a new discovery of Z-Wave devices + * @apiName discover + * @apiGroup ZwaveJSUI + */ + async function discover(req, res) { + await zwaveJSUIHandler.scan(); + res.json({ success: true }); + } + /** + * @api {post} /api/v1/service/zwavejs-ui/configuration Save configuration + * @apiName saveConfiguration + * @apiGroup ZwaveJSUI + */ + async function saveConfiguration(req, res) { + await zwaveJSUIHandler.saveConfiguration(req.body); + res.json({ success: true }); + } + + /** + * @api {get} /api/v1/service/zwavejs-ui/configuration Get configuration + * @apiName getConfiguration + * @apiGroup ZwaveJSUI + */ + async function getConfiguration(req, res) { + const config = await zwaveJSUIHandler.getConfiguration(); + res.json(config); + } + /** + * @api {post} /api/v1/service/zwavejs-ui/connect Connect to MQTT broker + * @apiName connect + * @apiGroup ZwaveJSUI + */ + async function connect(req, res) { + await zwaveJSUIHandler.connect(); + res.json({ success: true }); + } + + /** + * @api {get} /api/v1/service/zwavejs-ui/node Connect to MQTT broker + * @apiName getNodes + * @apiGroup ZwaveJSUI + */ + async function getNodes(req, res) { + res.json(zwaveJSUIHandler.devices); + } + + /** + * @api {get} /api/v1/service/zwavejs-ui/status Get MQTT Status + * @apiName getStatus + * @apiGroup ZwaveJSUI + */ + async function getStatus(req, res) { + res.json({ + connected: zwaveJSUIHandler.connected, + configured: zwaveJSUIHandler.configured, + }); + } + + return { + 'post /api/v1/service/zwavejs-ui/discover': { + authenticated: true, + controller: asyncMiddleware(discover), + }, + 'post /api/v1/service/zwavejs-ui/configuration': { + authenticated: true, + controller: asyncMiddleware(saveConfiguration), + }, + 'get /api/v1/service/zwavejs-ui/configuration': { + authenticated: true, + controller: asyncMiddleware(getConfiguration), + }, + 'post /api/v1/service/zwavejs-ui/connect': { + authenticated: true, + controller: asyncMiddleware(connect), + }, + 'get /api/v1/service/zwavejs-ui/node': { + authenticated: true, + controller: asyncMiddleware(getNodes), + }, + 'get /api/v1/service/zwavejs-ui/status': { + authenticated: true, + controller: asyncMiddleware(getStatus), + }, + }; +}; diff --git a/server/services/zwavejs-ui/index.js b/server/services/zwavejs-ui/index.js new file mode 100644 index 0000000000..ed9e6f5393 --- /dev/null +++ b/server/services/zwavejs-ui/index.js @@ -0,0 +1,48 @@ +const logger = require('../../utils/logger'); +const ZwaveJSUIHandler = require('./lib'); +const zwaveJSUIController = require('./api/zwaveJSUI.controller'); + +module.exports = function ZwaveJSUIService(gladys, serviceId) { + const mqtt = require('mqtt'); + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, mqtt, serviceId); + + /** + * @public + * @description This function starts service. + * @example + * gladys.services['zwavejs-ui'].start(); + */ + async function start() { + logger.info('Starting Z-Wave JS UI service'); + await zwaveJSUIHandler.init(); + } + + /** + * @public + * @description This function stops the service. + * @example + * gladys.services['zwavejs-ui'].stop(); + */ + async function stop() { + logger.info('Stopping Z-Wave JS UI service'); + } + + /** + * @public + * @description This function return true if the service is used. + * @returns {Promise} Resolves with a boolean. + * @example + * const isUsed = await gladys.services['zwavejs-ui'].isUsed(); + */ + async function isUsed() { + return zwaveJSUIHandler.devices.length > 0; + } + + return Object.freeze({ + start, + stop, + isUsed, + device: zwaveJSUIHandler, + controllers: zwaveJSUIController(zwaveJSUIHandler), + }); +}; diff --git a/server/services/zwavejs-ui/lib/constants.js b/server/services/zwavejs-ui/lib/constants.js new file mode 100644 index 0000000000..b2e4b21166 --- /dev/null +++ b/server/services/zwavejs-ui/lib/constants.js @@ -0,0 +1,40 @@ +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES, OPENING_SENSOR_STATE } = require('../../../utils/constants'); + +const CONFIGURATION = { + ZWAVEJS_UI_MQTT_URL_KEY: 'ZWAVEJS_UI_MQTT_URL', + ZWAVEJS_UI_MQTT_USERNAME_KEY: 'ZWAVEJS_UI_MQTT_USERNAME', + ZWAVEJS_UI_MQTT_PASSWORD_KEY: 'ZWAVEJS_UI_MQTT_PASSWORD', +}; + +const EXPOSES = { + notification: { + access_control: { + door_state_simple: { + category: DEVICE_FEATURE_CATEGORIES.OPENING_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.BINARY, + min: 0, + max: 1, + keep_history: true, + read_only: true, + has_feedback: true, + }, + }, + }, +}; + +const STATES = { + notification: { + access_control: { + door_state_simple: { + 22: OPENING_SENSOR_STATE.OPEN, + 23: OPENING_SENSOR_STATE.CLOSE, + }, + }, + }, +}; + +module.exports = { + CONFIGURATION, + EXPOSES, + STATES, +}; diff --git a/server/services/zwavejs-ui/lib/index.js b/server/services/zwavejs-ui/lib/index.js new file mode 100644 index 0000000000..9fd89be8db --- /dev/null +++ b/server/services/zwavejs-ui/lib/index.js @@ -0,0 +1,39 @@ +const { init } = require('./zwaveJSUI.init'); +const { connect } = require('./zwaveJSUI.connect'); +const { disconnect } = require('./zwaveJSUI.disconnect'); +const { getConfiguration } = require('./zwaveJSUI.getConfiguration'); +const { handleNewMessage } = require('./zwaveJSUI.handleNewMessage'); +const { onNewDeviceDiscover } = require('./zwaveJSUI.onNewDeviceDiscover'); +const { publish } = require('./zwaveJSUI.publish'); +const { scan } = require('./zwaveJSUI.scan'); +const { saveConfiguration } = require('./zwaveJSUI.saveConfiguration'); + +/** + * @description Z-Wave JS UI handler. + * @param {object} gladys - Gladys instance. + * @param {object} mqttLibrary - MQTT lib. + * @param {string} serviceId - UUID of the service in DB. + * @example + * const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, client, serviceId); + */ +const ZwaveJSUIHandler = function ZwaveJSUIHandler(gladys, mqttLibrary, serviceId) { + this.gladys = gladys; + this.mqttLibrary = mqttLibrary; + this.serviceId = serviceId; + this.mqttClient = null; + this.configured = false; + this.connected = false; + this.devices = []; +}; + +ZwaveJSUIHandler.prototype.init = init; +ZwaveJSUIHandler.prototype.connect = connect; +ZwaveJSUIHandler.prototype.disconnect = disconnect; +ZwaveJSUIHandler.prototype.getConfiguration = getConfiguration; +ZwaveJSUIHandler.prototype.handleNewMessage = handleNewMessage; +ZwaveJSUIHandler.prototype.onNewDeviceDiscover = onNewDeviceDiscover; +ZwaveJSUIHandler.prototype.publish = publish; +ZwaveJSUIHandler.prototype.scan = scan; +ZwaveJSUIHandler.prototype.saveConfiguration = saveConfiguration; + +module.exports = ZwaveJSUIHandler; diff --git a/server/services/zwavejs-ui/lib/zwaveJSUI.connect.js b/server/services/zwavejs-ui/lib/zwaveJSUI.connect.js new file mode 100644 index 0000000000..84354d53e8 --- /dev/null +++ b/server/services/zwavejs-ui/lib/zwaveJSUI.connect.js @@ -0,0 +1,63 @@ +const logger = require('../../../utils/logger'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants'); +const { ServiceNotConfiguredError } = require('../../../utils/coreErrors'); +const { CONFIGURATION } = require('./constants'); + +/** + * @description This function will connect to the MQTT broker. + * @example zwaveJSUI.connect(); + */ +async function connect() { + const mqttUrl = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJS_UI_MQTT_URL_KEY, this.serviceId); + const mqttUsername = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJS_UI_MQTT_USERNAME_KEY, this.serviceId); + const mqttPassword = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJS_UI_MQTT_PASSWORD_KEY, this.serviceId); + if (!mqttUrl) { + this.configured = false; + throw new ServiceNotConfiguredError('MQTT is not configured.'); + } + this.configured = true; + logger.info(`Trying to connect to MQTT server ${mqttUrl}...`); + this.mqttClient = this.mqttLibrary.connect(mqttUrl, { + username: mqttUsername, + password: mqttPassword, + reconnectPeriod: 5000, + clientId: `gladys-main-instance-zwavejs-ui-${Math.floor(Math.random() * 1000000)}`, + }); + + this.mqttClient.on('connect', () => { + logger.info(`Connected to MQTT server ${mqttUrl}`); + + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.CONNECTED, + }); + + this.mqttClient.subscribe('zwave/#'); + this.connected = true; + this.scan(); + }); + this.mqttClient.on('error', (err) => { + logger.warn(`Error while connecting to MQTT - ${err}`); + + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.ERROR, + payload: err, + }); + + this.disconnect(); + }); + this.mqttClient.on('offline', () => { + logger.warn(`Disconnected from MQTT server`); + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.ERROR, + payload: 'DISCONNECTED', + }); + this.connected = false; + }); + this.mqttClient.on('message', (topic, message) => { + this.handleNewMessage(topic, message.toString()); + }); +} + +module.exports = { + connect, +}; diff --git a/server/services/zwavejs-ui/lib/zwaveJSUI.disconnect.js b/server/services/zwavejs-ui/lib/zwaveJSUI.disconnect.js new file mode 100644 index 0000000000..d196d09908 --- /dev/null +++ b/server/services/zwavejs-ui/lib/zwaveJSUI.disconnect.js @@ -0,0 +1,22 @@ +const logger = require('../../../utils/logger'); + +/** + * @description This function will disconnect the MQTT broker. + * @example zwaveJSUI.disconnect(); + */ +async function disconnect() { + this.connected = false; + + if (this.mqttClient) { + logger.debug(`Disconnecting existing MQTT server...`); + this.mqttClient.end(); + this.mqttClient.removeAllListeners(); + this.mqttClient = null; + } else { + logger.debug('Not connected'); + } +} + +module.exports = { + disconnect, +}; diff --git a/server/services/zwavejs-ui/lib/zwaveJSUI.getConfiguration.js b/server/services/zwavejs-ui/lib/zwaveJSUI.getConfiguration.js new file mode 100644 index 0000000000..415185ce1b --- /dev/null +++ b/server/services/zwavejs-ui/lib/zwaveJSUI.getConfiguration.js @@ -0,0 +1,21 @@ +const { CONFIGURATION } = require('./constants'); + +/** + * @description This will return the current configuration. + * @returns {Promise} Resolve with configuration. + * @example zwaveJSUI.getConfiguration(); + */ +async function getConfiguration() { + const mqttUrl = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJS_UI_MQTT_URL_KEY, this.serviceId); + const mqttUsername = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJS_UI_MQTT_USERNAME_KEY, this.serviceId); + const mqttPassword = await this.gladys.variable.getValue(CONFIGURATION.ZWAVEJS_UI_MQTT_PASSWORD_KEY, this.serviceId); + return { + mqtt_url: mqttUrl, + mqtt_username: mqttUsername, + mqtt_password: mqttPassword, + }; +} + +module.exports = { + getConfiguration, +}; diff --git a/server/services/zwavejs-ui/lib/zwaveJSUI.handleNewMessage.js b/server/services/zwavejs-ui/lib/zwaveJSUI.handleNewMessage.js new file mode 100644 index 0000000000..56ed5c7e84 --- /dev/null +++ b/server/services/zwavejs-ui/lib/zwaveJSUI.handleNewMessage.js @@ -0,0 +1,55 @@ +const get = require('get-value'); +const { EVENTS } = require('../../../utils/constants'); +const logger = require('../../../utils/logger'); +const { STATES } = require('./constants'); +const { cleanNames, getDeviceFeatureExternalId } = require('../utils/convertToGladysDevice'); + +/** + * @description Handle a new message receive in MQTT. + * @param {string} topic - The topic where the message was posted. + * @param {string} message - The message sent. + * @example + * handleNewMessage('/zwave/#', '{}'); + */ +function handleNewMessage(topic, message) { + logger.debug(`Receives MQTT message from ${topic}`); + + try { + const splittedTopic = topic.split('/'); + const parsedMessage = JSON.parse(message); + // On list of devices received + if (topic === 'zwave/_CLIENTS/ZWAVE_GATEWAY-zwave-js-ui/api/getNodes') { + this.onNewDeviceDiscover(parsedMessage); + } else if (splittedTopic.length === 7) { + // trying to match example: zwave/living-room/my-sensor/notification/endpoint_0/Access_Control/Door_state_simple + const [, , , comClassName, endpoint, property, propertyKey] = splittedTopic; + const [, endpointNumber] = endpoint.split('_'); + const comClassNameClean = cleanNames(comClassName); + const propertyClean = cleanNames(property); + const propertyKeyClean = cleanNames(propertyKey); + const valueConverted = get( + STATES, + `${comClassNameClean}.${propertyClean}.${propertyKeyClean}.${parsedMessage.value}`, + ); + if (valueConverted !== undefined) { + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: getDeviceFeatureExternalId( + parsedMessage.nodeId, + endpointNumber, + comClassNameClean, + propertyClean, + propertyKeyClean, + ), + state: valueConverted, + }); + } + } + } catch (e) { + logger.warn(`Unable to handle new MQTT message in topic ${topic}`); + logger.warn(e); + } +} + +module.exports = { + handleNewMessage, +}; diff --git a/server/services/zwavejs-ui/lib/zwaveJSUI.init.js b/server/services/zwavejs-ui/lib/zwaveJSUI.init.js new file mode 100644 index 0000000000..033123dedc --- /dev/null +++ b/server/services/zwavejs-ui/lib/zwaveJSUI.init.js @@ -0,0 +1,11 @@ +/** + * @description This will init the Z-Wave JS UI MQTT connection. + * @example zwaveJSUI.init(); + */ +async function init() { + await this.connect(); +} + +module.exports = { + init, +}; diff --git a/server/services/zwavejs-ui/lib/zwaveJSUI.onNewDeviceDiscover.js b/server/services/zwavejs-ui/lib/zwaveJSUI.onNewDeviceDiscover.js new file mode 100644 index 0000000000..3c665e361f --- /dev/null +++ b/server/services/zwavejs-ui/lib/zwaveJSUI.onNewDeviceDiscover.js @@ -0,0 +1,24 @@ +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants'); +const { convertToGladysDevice } = require('../utils/convertToGladysDevice'); + +/** + * @description This will be called when new Z-Wave devices are discovered. + * @param {object} data - Data sent by ZWave JS UI. + * @example zwaveJSUI.onNewDeviceDiscover(); + */ +async function onNewDeviceDiscover(data) { + const devices = []; + data.result.forEach((zwaveJSDevice) => { + if (zwaveJSDevice.name && zwaveJSDevice.name.length > 0 && zwaveJSDevice.loc && zwaveJSDevice.loc.length > 0) { + devices.push(convertToGladysDevice(this.serviceId, zwaveJSDevice)); + } + }); + this.devices = devices; + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.SCAN_COMPLETED, + }); +} + +module.exports = { + onNewDeviceDiscover, +}; diff --git a/server/services/zwavejs-ui/lib/zwaveJSUI.publish.js b/server/services/zwavejs-ui/lib/zwaveJSUI.publish.js new file mode 100644 index 0000000000..03f2bb5dc2 --- /dev/null +++ b/server/services/zwavejs-ui/lib/zwaveJSUI.publish.js @@ -0,0 +1,24 @@ +const logger = require('../../../utils/logger'); +const { ServiceNotConfiguredError } = require('../../../utils/coreErrors'); + +/** + * @description Publish a MQTT message. + * @param {string} topic - MQTT Topic. + * @param {string} message - MQTT message. + * @example zwaveJSUI.publish('zwave/test', '{}'); + */ +async function publish(topic, message) { + if (!this.mqttClient) { + throw new ServiceNotConfiguredError('MQTT is not configured.'); + } + logger.debug(`Publishing MQTT message on topic ${topic}`); + this.mqttClient.publish(topic, message, undefined, (err) => { + if (err) { + logger.warn(`MQTT - Error publishing to ${topic} : ${err}`); + } + }); +} + +module.exports = { + publish, +}; diff --git a/server/services/zwavejs-ui/lib/zwaveJSUI.saveConfiguration.js b/server/services/zwavejs-ui/lib/zwaveJSUI.saveConfiguration.js new file mode 100644 index 0000000000..897ca12472 --- /dev/null +++ b/server/services/zwavejs-ui/lib/zwaveJSUI.saveConfiguration.js @@ -0,0 +1,25 @@ +const { CONFIGURATION } = require('./constants'); + +/** + * @description This will save the Z-Wave JS UI MQTT configuration. + * @param {object} configuration - The configuration object. + * @example zwaveJSUI.saveConfiguration({ + * }); + */ +async function saveConfiguration(configuration) { + await this.gladys.variable.setValue(CONFIGURATION.ZWAVEJS_UI_MQTT_URL_KEY, configuration.mqtt_url, this.serviceId); + await this.gladys.variable.setValue( + CONFIGURATION.ZWAVEJS_UI_MQTT_USERNAME_KEY, + configuration.mqtt_username, + this.serviceId, + ); + await this.gladys.variable.setValue( + CONFIGURATION.ZWAVEJS_UI_MQTT_PASSWORD_KEY, + configuration.mqtt_password, + this.serviceId, + ); +} + +module.exports = { + saveConfiguration, +}; diff --git a/server/services/zwavejs-ui/lib/zwaveJSUI.scan.js b/server/services/zwavejs-ui/lib/zwaveJSUI.scan.js new file mode 100644 index 0000000000..deb5ace460 --- /dev/null +++ b/server/services/zwavejs-ui/lib/zwaveJSUI.scan.js @@ -0,0 +1,14 @@ +const logger = require('../../../utils/logger'); + +/** + * @description This will discovery Z-Wave JS UI devices. + * @example zwaveJSUI.scan(); + */ +async function scan() { + logger.info('Asking ZWave JS UI for the list of devices'); + this.publish('zwave/_CLIENTS/ZWAVE_GATEWAY-zwave-js-ui/api/getNodes/set', 'true'); +} + +module.exports = { + scan, +}; diff --git a/server/services/zwavejs-ui/package-lock.json b/server/services/zwavejs-ui/package-lock.json new file mode 100644 index 0000000000..af1369d94d --- /dev/null +++ b/server/services/zwavejs-ui/package-lock.json @@ -0,0 +1,487 @@ +{ + "name": "gladys-zwavejs-ui", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gladys-zwavejs-ui", + "cpu": [ + "x64", + "arm", + "arm64" + ], + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "get-value": "^3.0.1", + "mqtt": "^4.0.0" + } + }, + "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": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "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/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": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/get-value": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz", + "integrity": "sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "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": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "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/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==", + "engines": { + "node": ">=0.10.0" + } + }, + "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.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mqtt": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.8.tgz", + "integrity": "sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw==", + "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.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "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": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "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.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "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": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, + "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": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "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==" + } + } +} diff --git a/server/services/zwavejs-ui/package.json b/server/services/zwavejs-ui/package.json new file mode 100644 index 0000000000..d12dad8b81 --- /dev/null +++ b/server/services/zwavejs-ui/package.json @@ -0,0 +1,18 @@ +{ + "name": "gladys-zwavejs-ui", + "main": "index.js", + "os": [ + "darwin", + "linux", + "win32" + ], + "cpu": [ + "x64", + "arm", + "arm64" + ], + "dependencies": { + "get-value": "^3.0.1", + "mqtt": "^4.0.0" + } +} diff --git a/server/services/zwavejs-ui/utils/convertToGladysDevice.js b/server/services/zwavejs-ui/utils/convertToGladysDevice.js new file mode 100644 index 0000000000..7454bac07a --- /dev/null +++ b/server/services/zwavejs-ui/utils/convertToGladysDevice.js @@ -0,0 +1,58 @@ +const get = require('get-value'); + +const { EXPOSES } = require('../lib/constants'); + +const cleanNames = (text) => { + if (!text || typeof text !== 'string') { + return ''; + } + return text + .replaceAll(' ', '_') + .replaceAll('(', '') + .replaceAll(')', '') + .toLowerCase(); +}; + +const getDeviceFeatureExternalId = (nodeId, endpoint, comClass, property, propertyKey) => + `zwavejs-ui:${nodeId}:${endpoint}:${comClass}:${property}:${propertyKey}`; + +const convertToGladysDevice = (serviceId, device) => { + const features = []; + + // Foreach value, we check if there is a matching feature in Gladys + Object.keys(device.values).forEach((valueKey) => { + const value = device.values[valueKey]; + const { commandClassName, property, propertyKey, endpoint } = value; + const commandClassNameClean = cleanNames(commandClassName); + const propertyClean = cleanNames(property); + const propertyKeyClean = cleanNames(propertyKey); + const exposeFound = get(EXPOSES, `${commandClassNameClean}.${propertyClean}.${propertyKeyClean}`); + if (exposeFound) { + features.push({ + ...exposeFound, + name: value.id, + external_id: getDeviceFeatureExternalId( + device.id, + endpoint, + commandClassNameClean, + propertyClean, + propertyKeyClean, + ), + }); + } + }); + + return { + name: device.name, + external_id: `zwavejs-ui:${device.id}`, + service_id: serviceId, + should_poll: false, + features, + }; +}; + +module.exports = { + cleanNames, + getDeviceFeatureExternalId, + convertToGladysDevice, +}; diff --git a/server/test/services/zwavejs-ui/api/zwaveJSUI.controller.test.js b/server/test/services/zwavejs-ui/api/zwaveJSUI.controller.test.js new file mode 100644 index 0000000000..c9b77e0f86 --- /dev/null +++ b/server/test/services/zwavejs-ui/api/zwaveJSUI.controller.test.js @@ -0,0 +1,96 @@ +const sinon = require('sinon'); +const ZwaveJSUIController = require('../../../../services/zwavejs-ui/api/zwaveJSUI.controller'); + +const { assert, fake } = sinon; + +const zwaveJSUIHandler = { + scan: fake.resolves(null), + connect: fake.resolves(null), + saveConfiguration: fake.resolves(null), + devices: [{ name: 'toto' }], + getConfiguration: fake.resolves({ + mqtt_url: 'mqtt://localhost', + mqtt_username: 'my_username', + mqtt_password: 'my_password', + }), + configured: true, + connected: false, +}; + +describe('ZwaveJSUIController', () => { + let controller; + + beforeEach(() => { + controller = ZwaveJSUIController(zwaveJSUIHandler); + sinon.reset(); + }); + + it('should call scan function', async () => { + const req = {}; + const res = { + json: fake.returns([]), + }; + + await controller['post /api/v1/service/zwavejs-ui/discover'].controller(req, res); + assert.calledOnce(zwaveJSUIHandler.scan); + assert.calledWith(res.json, { success: true }); + }); + it('should call connect function', async () => { + const req = {}; + const res = { + json: fake.returns([]), + }; + + await controller['post /api/v1/service/zwavejs-ui/connect'].controller(req, res); + assert.calledOnce(zwaveJSUIHandler.connect); + assert.calledWith(res.json, { success: true }); + }); + it('should call saveConfiguration function', async () => { + const req = { + body: { + mqtt_url: 'mqtt://localhost', + mqtt_username: 'my_username', + mqtt_password: 'my_password', + }, + }; + const res = { + json: fake.returns([]), + }; + + await controller['post /api/v1/service/zwavejs-ui/configuration'].controller(req, res); + assert.calledWith(zwaveJSUIHandler.saveConfiguration, req.body); + assert.calledWith(res.json, { success: true }); + }); + it('should get configuration', async () => { + const req = {}; + const res = { + json: fake.returns([]), + }; + + await controller['get /api/v1/service/zwavejs-ui/configuration'].controller(req, res); + assert.called(zwaveJSUIHandler.getConfiguration); + assert.calledWith(res.json, { + mqtt_url: 'mqtt://localhost', + mqtt_username: 'my_username', + mqtt_password: 'my_password', + }); + }); + it('should call getNodes function', async () => { + const req = {}; + const res = { + json: fake.returns([]), + }; + + await controller['get /api/v1/service/zwavejs-ui/node'].controller(req, res); + assert.calledWith(res.json, [{ name: 'toto' }]); + }); + it('should call getStatus function', async () => { + const req = {}; + const res = { + json: fake.returns([]), + }; + + await controller['get /api/v1/service/zwavejs-ui/status'].controller(req, res); + assert.calledWith(res.json, { configured: true, connected: false }); + }); +}); diff --git a/server/test/services/zwavejs-ui/index.test.js b/server/test/services/zwavejs-ui/index.test.js new file mode 100644 index 0000000000..257594db9d --- /dev/null +++ b/server/test/services/zwavejs-ui/index.test.js @@ -0,0 +1,41 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); +const proxyquire = require('proxyquire').noCallThru(); + +const { assert, fake } = sinon; + +const ZwaveJSUIHandlerMock = sinon.stub(); +ZwaveJSUIHandlerMock.prototype.init = fake.returns(null); +ZwaveJSUIHandlerMock.prototype.devices = []; + +const ZwaveJSUIService = proxyquire('../../../services/zwavejs-ui/index', { './lib': ZwaveJSUIHandlerMock }); + +const gladys = {}; +const serviceId = 'ffa13430-df93-488a-9733-5c540e9558e0'; + +describe('ZwaveJSUIService', () => { + const zwaveJSUIService = ZwaveJSUIService(gladys, serviceId); + + beforeEach(() => { + sinon.reset(); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should start service', async () => { + await zwaveJSUIService.start(); + assert.calledOnce(zwaveJSUIService.device.init); + }); + + it('should stop service', async () => { + zwaveJSUIService.stop(); + assert.notCalled(zwaveJSUIService.device.init); + }); + + it('isUsed: should return false, service not used', async () => { + const used = await zwaveJSUIService.isUsed(); + expect(used).to.equal(false); + }); +}); diff --git a/server/test/services/zwavejs-ui/lib/exampleData.json b/server/test/services/zwavejs-ui/lib/exampleData.json new file mode 100644 index 0000000000..e7a1d91c5b --- /dev/null +++ b/server/test/services/zwavejs-ui/lib/exampleData.json @@ -0,0 +1,4109 @@ +{ + "success": true, + "message": "Success zwave api call", + "result": [ + { + "id": 1, + "name": "", + "loc": "", + "values": { + "32-0-currentValue": { + "id": "1-32-0-currentValue", + "nodeId": 1, + "toUpdate": false, + "commandClass": 32, + "commandClassName": "Basic", + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "stateless": false, + "commandClassVersion": 0, + "min": 0, + "max": 99, + "list": false, + "isCurrentValue": true, + "lastUpdate": 1702649469988 + } + }, + "groups": [], + "neighbors": [], + "ready": true, + "available": true, + "hassDevices": {}, + "failed": false, + "inited": true, + "eventsQueue": [ + { + "time": "2023-12-15T14:11:09.987Z", + "event": "alive", + "args": [0] + }, + { + "time": "2023-12-15T14:11:10.007Z", + "event": "ready", + "args": [] + } + ], + "status": "Alive", + "interviewStage": "Complete", + "priorityReturnRoute": {}, + "customReturnRoute": {}, + "customSUCReturnRoutes": [], + "applicationRoute": null, + "hexId": "0x0086 0x0001-0x005a", + "dbLink": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0001:0x005a:1.2", + "manufacturerId": 134, + "productId": 90, + "productType": 1, + "deviceConfig": { + "filename": "/Users/pierregilles/code/zwave-js-ui/node_modules/@zwave-js/config/config/devices/0x0086/zw090.json", + "isEmbedded": true, + "manufacturer": "AEON Labs", + "manufacturerId": 134, + "label": "ZW090", + "description": "Z‐Stick Gen5 USB Controller", + "devices": [ + { "productType": 1, "productId": 90 }, + { "productType": 257, "productId": 90 }, + { "productType": 513, "productId": 90 } + ], + "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "preferred": false, + "associations": {}, + "paramInformation": { "_map": {} }, + "compat": { "disableCallbackFunctionTypeCheck": [81, 85] }, + "metadata": { + "reset": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable.\n\nPress and hold the Action Button on Z-Stick for 20 seconds and then release", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1345/Z%20Stick%20Gen5%20manual%201.pdf" + } + }, + "productLabel": "ZW090", + "productDescription": "Z‐Stick Gen5 USB Controller", + "manufacturer": "AEON Labs", + "firmwareVersion": "1.2", + "sdkVersion": "6.81.6", + "protocolVersion": 3, + "endpointsCount": 0, + "endpoints": [{ "index": 0, "label": "Root Endpoint" }], + "supportsSecurity": false, + "supportsBeaming": true, + "isControllerNode": true, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "keepAwake": false, + "maxDataRate": 100000, + "deviceClass": { "basic": 2, "generic": 2, "specific": 1 }, + "lastActive": 1702653315161, + "firmwareCapabilities": { "firmwareUpgradable": false }, + "deviceId": "134-90-1", + "hasDeviceConfigChanged": false, + "supportsTime": false, + "statistics": { + "messagesTX": 142, + "messagesRX": 142, + "messagesDroppedRX": 0, + "NAK": 0, + "CAN": 0, + "timeoutACK": 0, + "timeoutResponse": 0, + "timeoutCallback": 0, + "messagesDroppedTX": 0, + "backgroundRSSI": { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702653315163 + } + }, + "powerlevel": 0, + "measured0dBm": 1, + "bgRSSIPoints": [ + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -69 }, + "timestamp": 1702649475044 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -69 }, + "timestamp": 1702649475044 + }, + { + "channel0": { "current": -71, "average": -69 }, + "channel1": { "current": -71, "average": -69 }, + "timestamp": 1702649505046 + }, + { + "channel0": { "current": -71, "average": -69 }, + "channel1": { "current": -71, "average": -69 }, + "timestamp": 1702649505046 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -75, "average": -70 }, + "timestamp": 1702649535062 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -75, "average": -70 }, + "timestamp": 1702649535062 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702649565048 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702649565048 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702649595051 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702649595051 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702649625055 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702649625055 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702649655052 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702649655052 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702649685050 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702649685050 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702649715055 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702649715055 + }, + { + "channel0": { "current": -72, "average": -69 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702649745061 + }, + { + "channel0": { "current": -72, "average": -69 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702649745061 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702649775066 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702649775066 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -79, "average": -71 }, + "timestamp": 1702649805059 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -79, "average": -71 }, + "timestamp": 1702649805059 + }, + { + "channel0": { "current": -72, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702649835065 + }, + { + "channel0": { "current": -72, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702649835065 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702649865061 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702649865061 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702649895063 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702649895063 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702649925064 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702649925064 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702649955068 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702649955068 + }, + { + "channel0": { "current": -65, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702649985063 + }, + { + "channel0": { "current": -65, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702649985063 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650015064 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650015064 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650045067 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650045067 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650075068 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650075068 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650105069 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650105069 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650135074 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650135074 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650165071 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650165071 + }, + { + "channel0": { "current": -65, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650195074 + }, + { + "channel0": { "current": -65, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650195074 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650225076 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650225076 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650255083 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650255083 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650285082 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650285082 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650315085 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650315085 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650345088 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650345088 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650375082 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650375082 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650405085 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650405085 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650435086 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650435086 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702650465094 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702650465094 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650495091 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650495091 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650525086 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650525086 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650555085 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650555085 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650585088 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650585088 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -68, "average": -71 }, + "timestamp": 1702650615089 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -68, "average": -71 }, + "timestamp": 1702650615089 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650645090 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650645090 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650675089 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702650675089 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650705091 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650705091 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650735094 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650735094 + }, + { + "channel0": { "current": -71, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650765092 + }, + { + "channel0": { "current": -71, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650765092 + }, + { + "channel0": { "current": -71, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702650795095 + }, + { + "channel0": { "current": -71, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702650795095 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650825107 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650825107 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650855098 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650855098 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650885095 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702650885095 + }, + { + "channel0": { "current": -72, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650915097 + }, + { + "channel0": { "current": -72, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650915097 + }, + { + "channel0": { "current": -71, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650945093 + }, + { + "channel0": { "current": -71, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702650945093 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650975096 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702650975096 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702651005096 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702651005096 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702651035096 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702651035096 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702651065102 + }, + { + "channel0": { "current": -66, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702651065102 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702651095099 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702651095099 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702651125102 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702651125102 + }, + { + "channel0": { "current": -71, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702651155105 + }, + { + "channel0": { "current": -71, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702651155105 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702651185105 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702651185105 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702651215112 + }, + { + "channel0": { "current": -68, "average": -69 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702651215112 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702651245106 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702651245106 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702651275117 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -75, "average": -71 }, + "timestamp": 1702651275117 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702651305109 + }, + { + "channel0": { "current": -69, "average": -69 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702651305109 + }, + { + "channel0": { "current": -63, "average": -68 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702651335119 + }, + { + "channel0": { "current": -63, "average": -68 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702651335119 + }, + { + "channel0": { "current": -66, "average": -68 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702651365123 + }, + { + "channel0": { "current": -66, "average": -68 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702651365123 + }, + { + "channel0": { "current": -65, "average": -68 }, + "channel1": { "current": -66, "average": -70 }, + "timestamp": 1702651395109 + }, + { + "channel0": { "current": -65, "average": -68 }, + "channel1": { "current": -66, "average": -70 }, + "timestamp": 1702651395109 + }, + { + "channel0": { "current": -63, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651425110 + }, + { + "channel0": { "current": -63, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651425110 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651455110 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651455110 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651485114 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651485114 + }, + { + "channel0": { "current": -72, "average": -67 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702651515111 + }, + { + "channel0": { "current": -72, "average": -67 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702651515111 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -68, "average": -70 }, + "timestamp": 1702651545114 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -68, "average": -70 }, + "timestamp": 1702651545114 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702651575113 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702651575113 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702651605125 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702651605125 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702651635112 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702651635112 + }, + { + "channel0": { "current": -63, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651665107 + }, + { + "channel0": { "current": -63, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651665107 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651695110 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651695110 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -68, "average": -70 }, + "timestamp": 1702651725110 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -68, "average": -70 }, + "timestamp": 1702651725110 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702651755124 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702651755124 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651785116 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651785116 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651815115 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651815115 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702651845117 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702651845117 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651875125 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651875125 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651905126 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651905126 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651935122 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702651935122 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651965122 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651965122 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651995121 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702651995121 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652025123 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652025123 + }, + { + "channel0": { "current": -71, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652055131 + }, + { + "channel0": { "current": -71, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652055131 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652085126 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652085126 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652115127 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652115127 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652145128 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652145128 + }, + { + "channel0": { "current": -63, "average": -67 }, + "channel1": { "current": -68, "average": -70 }, + "timestamp": 1702652175140 + }, + { + "channel0": { "current": -63, "average": -67 }, + "channel1": { "current": -68, "average": -70 }, + "timestamp": 1702652175140 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652205142 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652205142 + }, + { + "channel0": { "current": -71, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652235148 + }, + { + "channel0": { "current": -71, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652235148 + }, + { + "channel0": { "current": -71, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652265141 + }, + { + "channel0": { "current": -71, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652265141 + }, + { + "channel0": { "current": -71, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652295137 + }, + { + "channel0": { "current": -71, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652295137 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652325138 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652325138 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652355139 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652355139 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652385138 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652385138 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652415140 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652415140 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -68, "average": -70 }, + "timestamp": 1702652445142 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -68, "average": -70 }, + "timestamp": 1702652445142 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702652475141 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -71, "average": -70 }, + "timestamp": 1702652475141 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -68, "average": -70 }, + "timestamp": 1702652505143 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -68, "average": -70 }, + "timestamp": 1702652505143 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652535143 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652535143 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652565144 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652565144 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652595137 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652595137 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -75, "average": -70 }, + "timestamp": 1702652625143 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -75, "average": -70 }, + "timestamp": 1702652625143 + }, + { + "channel0": { "current": -71, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652655144 + }, + { + "channel0": { "current": -71, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652655144 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652685149 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652685149 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652715144 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -70 }, + "timestamp": 1702652715144 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652745146 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652745146 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652775146 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -70 }, + "timestamp": 1702652775146 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652805154 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652805154 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652835150 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -72, "average": -70 }, + "timestamp": 1702652835150 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -80, "average": -71 }, + "timestamp": 1702652865151 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -80, "average": -71 }, + "timestamp": 1702652865151 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702652895152 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702652895152 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702652925160 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702652925160 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702652955157 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702652955157 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702652985156 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702652985156 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702653015150 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702653015150 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702653045153 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702653045153 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702653075152 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -71, "average": -71 }, + "timestamp": 1702653075152 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702653105160 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702653105160 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702653135168 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702653135168 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702653165165 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -69, "average": -71 }, + "timestamp": 1702653165165 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702653195162 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702653195162 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702653225155 + }, + { + "channel0": { "current": -66, "average": -67 }, + "channel1": { "current": -72, "average": -71 }, + "timestamp": 1702653225155 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -68, "average": -71 }, + "timestamp": 1702653255168 + }, + { + "channel0": { "current": -65, "average": -67 }, + "channel1": { "current": -68, "average": -71 }, + "timestamp": 1702653255168 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702653285161 + }, + { + "channel0": { "current": -69, "average": -67 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702653285161 + }, + { + "channel0": { "current": -68, "average": -67 }, + "channel1": { "current": -74, "average": -71 }, + "timestamp": 1702653315163 + } + ] + }, + { + "id": 2, + "name": "capteur-ouverture", + "loc": "salon", + "values": { + "49-0-Air temperature": { + "id": "2-49-0-Air temperature", + "nodeId": 2, + "toUpdate": false, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "endpoint": 0, + "property": "Air temperature", + "propertyName": "Air temperature", + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { "sensorType": 1, "scale": 0 }, + "stateless": false, + "commandClassVersion": 5, + "unit": "°C", + "list": false, + "value": 19.2, + "lastUpdate": 1702651498703 + }, + "112-0-1": { + "id": "2-112-0-1", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 1, + "propertyName": "Door/window State", + "type": "number", + "readable": true, + "writeable": true, + "description": "What state is door/window when the magnet is close to the sensor", + "label": "Door/window State", + "default": 0, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 1, + "list": true, + "allowManualEntry": false, + "states": [ + { "text": "Closed when magnet near", "value": 0 }, + { "text": "Opened when magnet near", "value": 1 } + ], + "value": 0, + "lastUpdate": 1702648857765 + }, + "112-0-3": { + "id": "2-112-0-3", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 3, + "propertyName": "Associations in Z-Wave Network Security Mode", + "type": "number", + "readable": true, + "writeable": true, + "label": "Associations in Z-Wave Network Security Mode", + "default": 3, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 3, + "list": true, + "allowManualEntry": false, + "states": [ + { + "text": "None of the groups sent as secure", + "value": 0 + }, + { + "text": "2nd group \"On/off\" sent as secure", + "value": 1 + }, + { + "text": "3rd group \"Tamper\" sent as secure", + "value": 2 + }, + { + "text": "2nd and 3rd group sent as secure", + "value": 3 + } + ], + "value": 3, + "lastUpdate": 1702648857807 + }, + "112-0-11": { + "id": "2-112-0-11", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 11, + "propertyName": "2nd Association Group Triggers", + "type": "number", + "readable": true, + "writeable": true, + "label": "2nd Association Group Triggers", + "default": 0, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 2, + "list": true, + "allowManualEntry": false, + "states": [ + { + "text": "Switch after opening and closing", + "value": 0 + }, + { + "text": "Switch after opening (Parameter 12)", + "value": 1 + }, + { + "text": "Switch after closing (Parameter 13)", + "value": 2 + } + ], + "value": 0, + "lastUpdate": 1702648857849 + }, + "112-0-12": { + "id": "2-112-0-12", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 12, + "propertyName": "Value of ON Command Sent to 2nd Association Group", + "type": "number", + "readable": true, + "writeable": true, + "label": "Value of ON Command Sent to 2nd Association Group", + "default": 255, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 255, + "list": false, + "value": 255, + "lastUpdate": 1702648857895 + }, + "112-0-13": { + "id": "2-112-0-13", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 13, + "propertyName": "Value of OFF Command Sent to 2nd Association Group", + "type": "number", + "readable": true, + "writeable": true, + "label": "Value of OFF Command Sent to 2nd Association Group", + "default": 255, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 255, + "list": false, + "value": 0, + "lastUpdate": 1702648857940 + }, + "112-0-14": { + "id": "2-112-0-14", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 14, + "propertyName": "Association for Opening - Time Delay", + "type": "number", + "readable": true, + "writeable": true, + "label": "Association for Opening - Time Delay", + "default": 0, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 32400, + "list": false, + "value": 0, + "lastUpdate": 1702648857985 + }, + "112-0-15": { + "id": "2-112-0-15", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 15, + "propertyName": "Association for Closing - Time Delay", + "type": "number", + "readable": true, + "writeable": true, + "label": "Association for Closing - Time Delay", + "default": 0, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 32400, + "list": false, + "value": 0, + "lastUpdate": 1702648858032 + }, + "112-0-30": { + "id": "2-112-0-30", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 30, + "propertyName": "Tamper - Alarm Cancellation Delay", + "type": "number", + "readable": true, + "writeable": true, + "label": "Tamper - Alarm Cancellation Delay", + "default": 5, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 32400, + "list": false, + "value": 5, + "lastUpdate": 1702648858078 + }, + "112-0-31": { + "id": "2-112-0-31", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 31, + "propertyName": "Tamper - Reporting Alarm Cancellation", + "type": "number", + "readable": true, + "writeable": true, + "label": "Tamper - Reporting Alarm Cancellation", + "default": 0, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 1, + "list": true, + "allowManualEntry": false, + "states": [ + { + "text": "Do not send tamper cancellation report", + "value": 0 + }, + { + "text": "Send tamper cancellation report", + "value": 1 + } + ], + "value": 1, + "lastUpdate": 1702648858128 + }, + "112-0-50": { + "id": "2-112-0-50", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 50, + "propertyName": "Interval of Temperature Measurements", + "type": "number", + "readable": true, + "writeable": true, + "label": "Interval of Temperature Measurements", + "default": 300, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 32400, + "list": false, + "value": 300, + "lastUpdate": 1702648858176 + }, + "112-0-51": { + "id": "2-112-0-51", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 51, + "propertyName": "Temperature Reports Threshold", + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature Reports Threshold", + "default": 10, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 300, + "list": false, + "value": 10, + "lastUpdate": 1702648858219 + }, + "112-0-52": { + "id": "2-112-0-52", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 52, + "propertyName": "Interval of Temperature Reports", + "type": "number", + "readable": true, + "writeable": true, + "label": "Interval of Temperature Reports", + "default": 0, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 32400, + "list": false, + "value": 0, + "lastUpdate": 1702648858264 + }, + "112-0-53": { + "id": "2-112-0-53", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 53, + "propertyName": "Temperature Offset", + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature Offset", + "default": 0, + "stateless": false, + "commandClassVersion": 1, + "min": -1000, + "max": 1000, + "list": false, + "value": 0, + "lastUpdate": 1702648858309 + }, + "112-0-54": { + "id": "2-112-0-54", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 54, + "propertyName": "Temperature Alarm Reports", + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature Alarm Reports", + "default": 0, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 3, + "list": true, + "allowManualEntry": false, + "states": [ + { "text": "Temperature alarms disabled", "value": 0 }, + { "text": "High temperature alarm", "value": 1 }, + { "text": "Low temperature alarm", "value": 2 }, + { + "text": "High and low temperature alarms enabled", + "value": 3 + } + ], + "value": 0, + "lastUpdate": 1702648858356 + }, + "112-0-55": { + "id": "2-112-0-55", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 55, + "propertyName": "High Temperature Alarm Threshold", + "type": "number", + "readable": true, + "writeable": true, + "label": "High Temperature Alarm Threshold", + "default": 350, + "stateless": false, + "commandClassVersion": 1, + "min": 1, + "max": 600, + "list": false, + "value": 350, + "lastUpdate": 1702648858400 + }, + "112-0-56": { + "id": "2-112-0-56", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 56, + "propertyName": "Low Temperature Alarm Threshold", + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Temperature Alarm Threshold", + "default": 100, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 599, + "list": false, + "value": 100, + "lastUpdate": 1702648858449 + }, + "112-0-2-1": { + "id": "2-112-0-2-1", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 2, + "propertyName": "Visual LED Indications - Open/close", + "propertyKey": 1, + "type": "number", + "readable": true, + "writeable": true, + "label": "Visual LED Indications - Open/close", + "default": 0, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 1, + "list": true, + "allowManualEntry": false, + "states": [ + { "text": "No indication", "value": 0 }, + { "text": "Open/Close indication", "value": 1 } + ], + "value": 0, + "lastUpdate": 1702648858493 + }, + "112-0-2-2": { + "id": "2-112-0-2-2", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 2, + "propertyName": "Visual LED Indications - Wake Up", + "propertyKey": 2, + "type": "number", + "readable": true, + "writeable": true, + "label": "Visual LED Indications - Wake Up", + "default": 1, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 1, + "list": true, + "allowManualEntry": false, + "states": [ + { "text": "No indication", "value": 0 }, + { "text": "Enable wake up notification", "value": 1 } + ], + "value": 1, + "lastUpdate": 1702648858494 + }, + "112-0-2-4": { + "id": "2-112-0-2-4", + "nodeId": 2, + "toUpdate": false, + "commandClass": 112, + "commandClassName": "Configuration", + "endpoint": 0, + "property": 2, + "propertyName": "Visual LED Indications - Tampering", + "propertyKey": 4, + "type": "number", + "readable": true, + "writeable": true, + "label": "Visual LED Indications - Tampering", + "default": 1, + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 1, + "list": true, + "allowManualEntry": false, + "states": [ + { "text": "No indication", "value": 0 }, + { + "text": "Enable indication of device tampering", + "value": 1 + } + ], + "value": 1, + "lastUpdate": 1702648858494 + }, + "113-0-Heat Alarm-Heat sensor status": { + "id": "2-113-0-Heat Alarm-Heat sensor status", + "nodeId": 2, + "toUpdate": false, + "commandClass": 113, + "commandClassName": "Notification", + "endpoint": 0, + "property": "Heat Alarm", + "propertyName": "Heat Alarm", + "propertyKey": "Heat sensor status", + "propertyKeyName": "Heat sensor status", + "type": "number", + "readable": true, + "writeable": false, + "label": "Heat sensor status", + "ccSpecific": { "notificationType": 4 }, + "stateless": false, + "commandClassVersion": 5, + "min": 0, + "max": 255, + "list": true, + "states": [ + { "text": "idle", "value": 0 }, + { "text": "Overheat detected", "value": 2 }, + { "text": "Underheat detected", "value": 6 } + ], + "value": 0, + "lastUpdate": 1702648858777 + }, + "113-0-Home Security-Cover status": { + "id": "2-113-0-Home Security-Cover status", + "nodeId": 2, + "toUpdate": false, + "commandClass": 113, + "commandClassName": "Notification", + "endpoint": 0, + "property": "Home Security", + "propertyName": "Home Security", + "propertyKey": "Cover status", + "propertyKeyName": "Cover status", + "type": "number", + "readable": true, + "writeable": false, + "label": "Cover status", + "ccSpecific": { "notificationType": 7 }, + "stateless": false, + "commandClassVersion": 5, + "min": 0, + "max": 255, + "list": true, + "states": [ + { "text": "idle", "value": 0 }, + { + "text": "Tampering, product cover removed", + "value": 3 + } + ], + "value": 0, + "lastUpdate": 1702648859067 + }, + "113-0-Power Management-Battery maintenance status": { + "id": "2-113-0-Power Management-Battery maintenance status", + "nodeId": 2, + "toUpdate": false, + "commandClass": 113, + "commandClassName": "Notification", + "endpoint": 0, + "property": "Power Management", + "propertyName": "Power Management", + "propertyKey": "Battery maintenance status", + "propertyKeyName": "Battery maintenance status", + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery maintenance status", + "ccSpecific": { "notificationType": 8 }, + "stateless": false, + "commandClassVersion": 5, + "min": 0, + "max": 255, + "list": true, + "states": [ + { "text": "idle", "value": 0 }, + { "text": "Replace battery now", "value": 11 } + ], + "value": 0, + "lastUpdate": 1702648859209 + }, + "113-0-alarmType": { + "id": "2-113-0-alarmType", + "nodeId": 2, + "toUpdate": false, + "commandClass": 113, + "commandClassName": "Notification", + "endpoint": 0, + "property": "alarmType", + "propertyName": "alarmType", + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "stateless": false, + "commandClassVersion": 5, + "min": 0, + "max": 255, + "list": false, + "value": 0, + "lastUpdate": 1702650360959 + }, + "113-0-alarmLevel": { + "id": "2-113-0-alarmLevel", + "nodeId": 2, + "toUpdate": false, + "commandClass": 113, + "commandClassName": "Notification", + "endpoint": 0, + "property": "alarmLevel", + "propertyName": "alarmLevel", + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "stateless": false, + "commandClassVersion": 5, + "min": 0, + "max": 255, + "list": false, + "value": 0, + "lastUpdate": 1702650360960 + }, + "113-0-Access Control-Door state (simple)": { + "id": "2-113-0-Access Control-Door state (simple)", + "nodeId": 2, + "toUpdate": false, + "commandClass": 113, + "commandClassName": "Notification", + "endpoint": 0, + "property": "Access Control", + "propertyName": "Access Control", + "propertyKey": "Door state (simple)", + "propertyKeyName": "Door state (simple)", + "type": "number", + "readable": true, + "writeable": false, + "label": "Door state (simple)", + "ccSpecific": { "notificationType": 6 }, + "stateless": false, + "commandClassVersion": 5, + "min": 0, + "max": 255, + "list": true, + "states": [ + { "text": "Window/door is open", "value": 22 }, + { "text": "Window/door is closed", "value": 23 } + ], + "value": 22, + "lastUpdate": 1702650360961 + }, + "113-0-Access Control-Door state": { + "id": "2-113-0-Access Control-Door state", + "nodeId": 2, + "toUpdate": false, + "commandClass": 113, + "commandClassName": "Notification", + "endpoint": 0, + "property": "Access Control", + "propertyName": "Access Control", + "propertyKey": "Door state", + "propertyKeyName": "Door state", + "type": "number", + "readable": true, + "writeable": false, + "label": "Door state", + "ccSpecific": { "notificationType": 6 }, + "stateless": false, + "commandClassVersion": 5, + "min": 0, + "max": 255, + "list": true, + "states": [ + { "text": "Window/door is open", "value": 22 }, + { "text": "Window/door is closed", "value": 23 }, + { + "text": "Window/door is open in regular position", + "value": 5632 + }, + { + "text": "Window/door is open in tilt position", + "value": 5633 + } + ], + "value": 22, + "lastUpdate": 1702650360961 + }, + "114-0-manufacturerId": { + "id": "2-114-0-manufacturerId", + "nodeId": 2, + "toUpdate": false, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "stateless": false, + "commandClassVersion": 2, + "min": 0, + "max": 65535, + "list": false, + "value": 271, + "lastUpdate": 1702648855677 + }, + "114-0-productType": { + "id": "2-114-0-productType", + "nodeId": 2, + "toUpdate": false, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "stateless": false, + "commandClassVersion": 2, + "min": 0, + "max": 65535, + "list": false, + "value": 1794, + "lastUpdate": 1702648855677 + }, + "114-0-productId": { + "id": "2-114-0-productId", + "nodeId": 2, + "toUpdate": false, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "stateless": false, + "commandClassVersion": 2, + "min": 0, + "max": 65535, + "list": false, + "value": 4096, + "lastUpdate": 1702648855678 + }, + "128-0-level": { + "id": "2-128-0-level", + "nodeId": 2, + "toUpdate": false, + "commandClass": 128, + "commandClassName": "Battery", + "endpoint": 0, + "property": "level", + "propertyName": "level", + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "stateless": false, + "commandClassVersion": 1, + "min": 0, + "max": 100, + "unit": "%", + "list": false, + "value": 100, + "lastUpdate": 1702648856714 + }, + "128-0-isLow": { + "id": "2-128-0-isLow", + "nodeId": 2, + "toUpdate": false, + "commandClass": 128, + "commandClassName": "Battery", + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level", + "stateless": false, + "commandClassVersion": 1, + "list": false, + "value": false, + "lastUpdate": 1702648856716 + }, + "132-0-wakeUpInterval": { + "id": "2-132-0-wakeUpInterval", + "nodeId": 2, + "toUpdate": false, + "commandClass": 132, + "commandClassName": "Wake Up", + "endpoint": 0, + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "type": "number", + "readable": false, + "writeable": true, + "label": "wakeUpInterval (property)", + "default": 21600, + "stateless": false, + "commandClassVersion": 2, + "min": 0, + "max": 64800, + "step": 3600, + "list": false, + "value": 21600, + "lastUpdate": 1702648856565 + }, + "132-0-controllerNodeId": { + "id": "2-132-0-controllerNodeId", + "nodeId": 2, + "toUpdate": false, + "commandClass": 132, + "commandClassName": "Wake Up", + "endpoint": 0, + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller", + "stateless": false, + "commandClassVersion": 2, + "list": false, + "value": 1, + "lastUpdate": 1702648856603 + }, + "134-0-libraryType": { + "id": "2-134-0-libraryType", + "nodeId": 2, + "toUpdate": false, + "commandClass": 134, + "commandClassName": "Version", + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "stateless": false, + "commandClassVersion": 2, + "list": true, + "states": [ + { "text": "Unknown", "value": 0 }, + { "text": "Static Controller", "value": 1 }, + { "text": "Controller", "value": 2 }, + { "text": "Enhanced Slave", "value": 3 }, + { "text": "Slave", "value": 4 }, + { "text": "Installer", "value": 5 }, + { "text": "Routing Slave", "value": 6 }, + { "text": "Bridge Controller", "value": 7 }, + { "text": "Device under Test", "value": 8 }, + { "text": "N/A", "value": 9 }, + { "text": "AV Remote", "value": 10 }, + { "text": "AV Device", "value": 11 } + ], + "value": 3, + "lastUpdate": 1702648855777 + }, + "134-0-protocolVersion": { + "id": "2-134-0-protocolVersion", + "nodeId": 2, + "toUpdate": false, + "commandClass": 134, + "commandClassName": "Version", + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateless": false, + "commandClassVersion": 2, + "list": false, + "value": "4.38", + "lastUpdate": 1702648855778 + }, + "134-0-firmwareVersions": { + "id": "2-134-0-firmwareVersions", + "nodeId": 2, + "toUpdate": false, + "commandClass": 134, + "commandClassName": "Version", + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateless": false, + "commandClassVersion": 2, + "list": false, + "value": ["3.2"], + "lastUpdate": 1702648855779 + }, + "134-0-hardwareVersion": { + "id": "2-134-0-hardwareVersion", + "nodeId": 2, + "toUpdate": false, + "commandClass": 134, + "commandClassName": "Version", + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateless": false, + "commandClassVersion": 2, + "list": false, + "value": 2, + "lastUpdate": 1702648855780 + }, + "119-0-name": { + "id": "2-119-0-name", + "nodeId": 2, + "toUpdate": false, + "commandClass": 119, + "commandClassName": "Node Naming and Location", + "property": "name", + "propertyName": "name", + "type": "string", + "readable": true, + "writeable": true, + "label": "Node name", + "stateless": false, + "commandClassVersion": 0, + "list": false, + "value": "Capteur ouverture/porte salon", + "lastUpdate": 1702653154778 + }, + "119-0-location": { + "id": "2-119-0-location", + "nodeId": 2, + "toUpdate": false, + "commandClass": 119, + "commandClassName": "Node Naming and Location", + "property": "location", + "propertyName": "location", + "type": "string", + "readable": true, + "writeable": true, + "label": "Node location", + "stateless": false, + "commandClassVersion": 0, + "list": false, + "value": "salon", + "lastUpdate": 1702653191305 + } + }, + "groups": [ + { + "text": "Lifeline", + "endpoint": 0, + "value": 1, + "maxNodes": 1, + "isLifeline": true, + "multiChannel": true + }, + { + "text": "On/Off", + "endpoint": 0, + "value": 2, + "maxNodes": 5, + "isLifeline": false, + "multiChannel": true + }, + { + "text": "Tamper", + "endpoint": 0, + "value": 3, + "maxNodes": 5, + "isLifeline": false, + "multiChannel": true + } + ], + "neighbors": [], + "ready": true, + "available": true, + "hassDevices": {}, + "failed": false, + "inited": true, + "eventsQueue": [ + { + "time": "2023-12-15T14:24:13.158Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:30.186Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:30.187Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:30.189Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:30.190Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:30.642Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:30.642Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:30.643Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:30.644Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:31.341Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:31.342Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:31.343Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:31.344Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:31.740Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:31.740Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:31.741Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:31.742Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:32.437Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:32.438Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:32.444Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:32.444Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:32.935Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:32.936Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:32.938Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:32.939Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:33.033Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:33.033Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:33.034Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:33.035Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:33.132Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:33.133Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:33.134Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:33.135Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:33.232Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:33.233Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:33.235Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:33.235Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:33.332Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:33.333Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:33.334Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:33.335Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:33.431Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:33.432Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:33.433Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:33.434Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:33.534Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:33.535Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:33.536Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:33.536Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:52.116Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:52.117Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:52.119Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:52.120Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:52.209Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:52.210Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:52.211Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:52.211Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:52.613Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:52.614Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:52.615Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:52.616Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:52.812Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:52.812Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:52.814Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:52.815Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:53.113Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:53.114Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:53.116Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:53.116Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:53.312Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:53.312Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:53.314Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:53.315Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:24:54.614Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:24:54.615Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:24:54.618Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:24:54.618Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:25:01.320Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:25:01.321Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:25:01.322Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:25:01.323Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:25:58.157Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:25:58.158Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:25:58.159Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:25:58.164Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:25:58.250Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:25:58.251Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:25:58.252Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:25:58.253Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:26:00.056Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:26:00.061Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:26:00.062Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:26:00.064Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 23, + "prevValue": 22, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:26:00.960Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmType", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmType" + } + ] + }, + { + "time": "2023-12-15T14:26:00.960Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "alarmLevel", + "endpoint": 0, + "newValue": 0, + "prevValue": 0, + "propertyName": "alarmLevel" + } + ] + }, + { + "time": "2023-12-15T14:26:00.961Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state (simple)", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state (simple)" + } + ] + }, + { + "time": "2023-12-15T14:26:00.962Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Notification", + "commandClass": 113, + "property": "Access Control", + "propertyKey": "Door state", + "endpoint": 0, + "newValue": 22, + "prevValue": 23, + "propertyName": "Access Control", + "propertyKeyName": "Door state" + } + ] + }, + { + "time": "2023-12-15T14:44:58.704Z", + "event": "value updated", + "args": [ + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "property": "Air temperature", + "endpoint": 0, + "newValue": 19.2, + "prevValue": 21.3, + "propertyName": "Air temperature" + } + ] + }, + { + "time": "2023-12-15T15:12:34.780Z", + "event": "value added", + "args": [ + { + "commandClassName": "Node Naming and Location", + "commandClass": 119, + "property": "name", + "newValue": "Capteur ouverture/porte salon", + "propertyName": "name", + "nodeId": 2 + } + ] + }, + { + "time": "2023-12-15T15:13:11.310Z", + "event": "value added", + "args": [ + { + "commandClassName": "Node Naming and Location", + "commandClass": 119, + "property": "location", + "newValue": "salon", + "propertyName": "location", + "nodeId": 2 + } + ] + } + ], + "status": "Asleep", + "interviewStage": "Complete", + "priorityReturnRoute": {}, + "customReturnRoute": {}, + "prioritySUCReturnRoute": null, + "customSUCReturnRoutes": [], + "hexId": "0x010f 0x0702-0x1000", + "dbLink": "https://devices.zwave-js.io/?jumpTo=0x010f:0x0702:0x1000:3.2", + "manufacturerId": 271, + "productId": 4096, + "productType": 1794, + "deviceConfig": { + "filename": "/Users/pierregilles/code/zwave-js-ui/node_modules/@zwave-js/config/config/devices/0x010f/fgdw002.json", + "isEmbedded": true, + "manufacturer": "Fibargroup", + "manufacturerId": 271, + "label": "FGDW002", + "description": "Fibaro Door Window Sensor 2", + "devices": [ + { "productType": 1794, "productId": 4096 }, + { "productType": 1794, "productId": 8192 }, + { "productType": 1794, "productId": 12288 }, + { "productType": 1794, "productId": 16384 }, + { "productType": 1794, "productId": 28672 } + ], + "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "preferred": false, + "paramInformation": { "_map": {} } + }, + "productLabel": "FGDW002", + "productDescription": "Fibaro Door Window Sensor 2", + "manufacturer": "Fibargroup", + "firmwareVersion": "3.2", + "protocolVersion": 3, + "zwavePlusVersion": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 6, + "nodeType": 1, + "endpointsCount": 0, + "endpoints": [{ "index": 0, "label": "Root Endpoint" }], + "isSecure": false, + "security": "None", + "supportsSecurity": false, + "supportsBeaming": true, + "isControllerNode": false, + "isListening": false, + "isFrequentListening": false, + "isRouting": true, + "keepAwake": false, + "maxDataRate": 100000, + "deviceClass": { "basic": 4, "generic": 7, "specific": 1 }, + "lastActive": 1702651498694, + "firmwareCapabilities": { + "firmwareUpgradable": true, + "firmwareTargets": [0] + }, + "deviceId": "271-4096-1794", + "hasDeviceConfigChanged": false, + "batteryLevels": { "0": 100 }, + "minBatteryLevel": 100, + "supportsTime": false, + "statistics": { + "commandsTX": 0, + "commandsRX": 35, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + "lwr": { "repeaters": [], "protocolDataRate": 3 }, + "lastSeen": "2023-12-15T14:44:58.694Z" + } + } + ], + "args": [], + "origin": true +} diff --git a/server/test/services/zwavejs-ui/lib/zwaveJSUI.connect.test.js b/server/test/services/zwavejs-ui/lib/zwaveJSUI.connect.test.js new file mode 100644 index 0000000000..2f65333a96 --- /dev/null +++ b/server/test/services/zwavejs-ui/lib/zwaveJSUI.connect.test.js @@ -0,0 +1,112 @@ +const sinon = require('sinon'); +const Promise = require('bluebird'); + +const { assert: chaiAssert } = require('chai'); + +const { assert, fake } = sinon; + +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); +const ZwaveJSUIHandler = require('../../../../services/zwavejs-ui/lib'); + +const serviceId = 'ffa13430-df93-488a-9733-5c540e9558e0'; + +const gladys = { + variable: { + getValue: fake.resolves('toto'), + }, + event: { + emit: fake.returns(null), + }, +}; + +describe('zwaveJSUIHandler.connect', () => { + beforeEach(() => { + sinon.reset(); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should not connect, mqttUrl not defined', async () => { + const gladysNotConfigured = { + variable: { + getValue: fake.resolves(null), + }, + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladysNotConfigured, {}, serviceId); + await chaiAssert.isRejected(zwaveJSUIHandler.connect()); + }); + + it('should connect to MQTT broker with success', async () => { + const mqttClient = { + end: fake.returns(null), + removeAllListeners: fake.returns(null), + subscribe: fake.returns(null), + on: (event, cb) => { + if (event === 'connect') { + cb(); + } + }, + }; + const mqttLibrary = { + connect: fake.returns(mqttClient), + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, mqttLibrary, serviceId); + await zwaveJSUIHandler.connect(); + assert.calledThrice(gladys.variable.getValue); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.CONNECTED, + }); + }); + it('should connect to MQTT broker with error', async () => { + const error = new Error('test-error'); + let mqttClient; + const waitForCallBackToBeCalled = new Promise((resolve) => { + mqttClient = { + end: fake.returns(null), + removeAllListeners: fake.returns(null), + on: (event, cb) => { + if (event === 'error') { + setTimeout(() => { + cb(error); + resolve(); + }, 0); + } + }, + }; + }); + const mqttLibrary = { + connect: fake.returns(mqttClient), + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, mqttLibrary, serviceId); + await zwaveJSUIHandler.connect(); + await waitForCallBackToBeCalled; + assert.calledThrice(gladys.variable.getValue); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.ERROR, + payload: error, + }); + }); + it('should connect to MQTT broker and get offline', async () => { + const mqttClient = { + end: fake.returns(null), + removeAllListeners: fake.returns(null), + on: (event, cb) => { + if (event === 'offline') { + cb(); + } + }, + }; + const mqttLibrary = { + connect: fake.returns(mqttClient), + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, mqttLibrary, serviceId); + await zwaveJSUIHandler.connect(); + assert.calledThrice(gladys.variable.getValue); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.ERROR, + payload: 'DISCONNECTED', + }); + }); +}); diff --git a/server/test/services/zwavejs-ui/lib/zwaveJSUI.disconnect.test.js b/server/test/services/zwavejs-ui/lib/zwaveJSUI.disconnect.test.js new file mode 100644 index 0000000000..00c55c6b75 --- /dev/null +++ b/server/test/services/zwavejs-ui/lib/zwaveJSUI.disconnect.test.js @@ -0,0 +1,64 @@ +const sinon = require('sinon'); + +const { assert, fake } = sinon; + +const ZwaveJSUIHandler = require('../../../../services/zwavejs-ui/lib'); + +const serviceId = 'ffa13430-df93-488a-9733-5c540e9558e0'; + +const gladys = { + variable: { + getValue: fake.resolves('toto'), + }, + event: { + emit: fake.returns(null), + }, +}; + +describe('zwaveJSUIHandler.disconnect', () => { + beforeEach(() => { + sinon.reset(); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should disconnect MQTT broker', async () => { + const mqttClient = { + end: fake.returns(null), + removeAllListeners: fake.returns(null), + on: (event, cb) => { + if (event === 'connect') { + cb(); + } + }, + }; + const mqttLibrary = { + connect: fake.returns(mqttClient), + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, mqttLibrary, serviceId); + zwaveJSUIHandler.mqttClient = mqttClient; + await zwaveJSUIHandler.disconnect(); + assert.called(mqttClient.end); + assert.called(mqttClient.removeAllListeners); + }); + it('should not disconnect MQTT broker (not connected)', async () => { + const mqttClient = { + end: fake.returns(null), + removeAllListeners: fake.returns(null), + on: (event, cb) => { + if (event === 'connect') { + cb(); + } + }, + }; + const mqttLibrary = { + connect: fake.returns(mqttClient), + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, mqttLibrary, serviceId); + await zwaveJSUIHandler.disconnect(); + assert.notCalled(mqttClient.end); + assert.notCalled(mqttClient.removeAllListeners); + }); +}); diff --git a/server/test/services/zwavejs-ui/lib/zwaveJSUI.getConfiguration.test.js b/server/test/services/zwavejs-ui/lib/zwaveJSUI.getConfiguration.test.js new file mode 100644 index 0000000000..a35340a890 --- /dev/null +++ b/server/test/services/zwavejs-ui/lib/zwaveJSUI.getConfiguration.test.js @@ -0,0 +1,47 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); + +const { fake } = sinon; + +const ZwaveJSUIHandler = require('../../../../services/zwavejs-ui/lib'); + +const serviceId = 'ffa13430-df93-488a-9733-5c540e9558e0'; + +describe('zwaveJSUIHandler.getConfiguration', () => { + beforeEach(() => { + sinon.reset(); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should get mqtt configuration', async () => { + const gladys = { + variable: { + getValue: fake.resolves('toto'), + }, + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId); + const config = await zwaveJSUIHandler.getConfiguration(); + expect(config).to.deep.equal({ + mqtt_url: 'toto', + mqtt_username: 'toto', + mqtt_password: 'toto', + }); + }); + it('should return null values', async () => { + const gladys = { + variable: { + getValue: fake.resolves(null), + }, + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId); + const config = await zwaveJSUIHandler.getConfiguration(); + expect(config).to.deep.equal({ + mqtt_url: null, + mqtt_username: null, + mqtt_password: null, + }); + }); +}); diff --git a/server/test/services/zwavejs-ui/lib/zwaveJSUI.handleNewMessage.test.js b/server/test/services/zwavejs-ui/lib/zwaveJSUI.handleNewMessage.test.js new file mode 100644 index 0000000000..c3281e4a20 --- /dev/null +++ b/server/test/services/zwavejs-ui/lib/zwaveJSUI.handleNewMessage.test.js @@ -0,0 +1,63 @@ +const sinon = require('sinon'); + +const { assert, fake } = sinon; + +const ZwaveJSUIHandler = require('../../../../services/zwavejs-ui/lib'); + +const serviceId = 'ffa13430-df93-488a-9733-5c540e9558e0'; + +const gladys = { + variable: { + getValue: fake.resolves('toto'), + }, + event: { + emit: fake.returns(null), + }, +}; + +describe('zwaveJSUIHandler.handleNewMessage', () => { + beforeEach(() => { + sinon.reset(); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should save nodes received', async () => { + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId); + const data = { + result: [], + }; + await zwaveJSUIHandler.handleNewMessage( + 'zwave/_CLIENTS/ZWAVE_GATEWAY-zwave-js-ui/api/getNodes', + JSON.stringify(data), + ); + }); + it('should not crash even with broken JSON', async () => { + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId); + await zwaveJSUIHandler.handleNewMessage('zwave/_CLIENTS/ZWAVE_GATEWAY-zwave-js-ui/api/getNodes', 'toto'); + }); + it('should save a new open value', async () => { + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId); + await zwaveJSUIHandler.handleNewMessage( + 'zwave/living-room/my-sensor/notification/endpoint_0/Access_Control/Door_state_simple', + '{"time":1702654592227,"value":22, "nodeId": 2}', + ); + assert.calledWith(gladys.event.emit, 'device.new-state', { + device_feature_external_id: 'zwavejs-ui:2:0:notification:access_control:door_state_simple', + state: 0, + }); + }); + it('should save a new closed value', async () => { + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId); + await zwaveJSUIHandler.handleNewMessage( + 'zwave/living-room/my-sensor/notification/endpoint_0/Access_Control/Door_state_simple', + '{"time":1702654592227,"value":23, "nodeId": 2}', + ); + assert.calledWith(gladys.event.emit, 'device.new-state', { + device_feature_external_id: 'zwavejs-ui:2:0:notification:access_control:door_state_simple', + state: 1, + }); + }); +}); diff --git a/server/test/services/zwavejs-ui/lib/zwaveJSUI.init.test.js b/server/test/services/zwavejs-ui/lib/zwaveJSUI.init.test.js new file mode 100644 index 0000000000..bb10474709 --- /dev/null +++ b/server/test/services/zwavejs-ui/lib/zwaveJSUI.init.test.js @@ -0,0 +1,49 @@ +const sinon = require('sinon'); + +const { assert, fake } = sinon; + +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); +const ZwaveJSUIHandler = require('../../../../services/zwavejs-ui/lib'); + +const serviceId = 'ffa13430-df93-488a-9733-5c540e9558e0'; + +const gladys = { + variable: { + getValue: fake.resolves('toto'), + }, + event: { + emit: fake.returns(null), + }, +}; + +describe('zwaveJSUIHandler.init', () => { + beforeEach(() => { + sinon.reset(); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should init connection', async () => { + const mqttClient = { + end: fake.returns(null), + removeAllListeners: fake.returns(null), + subscribe: fake.returns(null), + on: (event, cb) => { + if (event === 'connect') { + cb(); + } + }, + }; + const mqttLibrary = { + connect: fake.returns(mqttClient), + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, mqttLibrary, serviceId); + await zwaveJSUIHandler.init(); + assert.calledThrice(gladys.variable.getValue); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZWAVEJS_UI.CONNECTED, + }); + }); +}); diff --git a/server/test/services/zwavejs-ui/lib/zwaveJSUI.onNewDeviceDiscover.test.js b/server/test/services/zwavejs-ui/lib/zwaveJSUI.onNewDeviceDiscover.test.js new file mode 100644 index 0000000000..b05ca202ef --- /dev/null +++ b/server/test/services/zwavejs-ui/lib/zwaveJSUI.onNewDeviceDiscover.test.js @@ -0,0 +1,55 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); + +const { fake } = sinon; + +const exampleData = require('./exampleData.json'); + +const ZwaveJSUIHandler = require('../../../../services/zwavejs-ui/lib'); + +const serviceId = 'ffa13430-df93-488a-9733-5c540e9558e0'; + +const gladys = { + variable: { + getValue: fake.resolves('toto'), + }, + event: { + emit: fake.returns(null), + }, +}; + +describe('zwaveJSUIHandler.onNewDeviceDiscover.js', () => { + beforeEach(() => { + sinon.reset(); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should set list of Gladys devices', async () => { + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId); + await zwaveJSUIHandler.onNewDeviceDiscover(exampleData); + expect(zwaveJSUIHandler.devices).to.deep.equal([ + { + name: 'capteur-ouverture', + external_id: 'zwavejs-ui:2', + service_id: 'ffa13430-df93-488a-9733-5c540e9558e0', + should_poll: false, + features: [ + { + category: 'opening-sensor', + type: 'binary', + min: 0, + max: 1, + keep_history: true, + read_only: true, + has_feedback: true, + name: '2-113-0-Access Control-Door state (simple)', + external_id: 'zwavejs-ui:2:0:notification:access_control:door_state_simple', + }, + ], + }, + ]); + }); +}); diff --git a/server/test/services/zwavejs-ui/lib/zwaveJSUI.publish.test.js b/server/test/services/zwavejs-ui/lib/zwaveJSUI.publish.test.js new file mode 100644 index 0000000000..2427c3b7a2 --- /dev/null +++ b/server/test/services/zwavejs-ui/lib/zwaveJSUI.publish.test.js @@ -0,0 +1,63 @@ +const sinon = require('sinon'); + +const { assert, fake } = sinon; +const { assert: chaiAssert } = require('chai'); + +const ZwaveJSUIHandler = require('../../../../services/zwavejs-ui/lib'); + +const serviceId = 'ffa13430-df93-488a-9733-5c540e9558e0'; + +const gladys = { + variable: { + getValue: fake.resolves('toto'), + }, + event: { + emit: fake.returns(null), + }, +}; + +describe('zwaveJSUIHandler.publish', () => { + beforeEach(() => { + sinon.reset(); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should publish MQTT message', async () => { + const mqttClient = { + publish: fake.returns(null), + }; + const mqttLibrary = { + connect: fake.returns(mqttClient), + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, mqttLibrary, serviceId); + zwaveJSUIHandler.mqttClient = mqttClient; + await zwaveJSUIHandler.publish('toto', 'message'); + assert.calledWith(mqttClient.publish, 'toto', 'message'); + }); + it('should publish MQTT message with error', async () => { + const mqttClient = { + publish: (topic, message, random, cb) => { + cb('toto'); + }, + }; + const mqttLibrary = { + connect: fake.returns(mqttClient), + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, mqttLibrary, serviceId); + zwaveJSUIHandler.mqttClient = mqttClient; + await zwaveJSUIHandler.publish('toto', 'mesage'); + }); + it('should not publish MQTT message', async () => { + const mqttClient = { + publish: fake.returns(null), + }; + const mqttLibrary = { + connect: fake.returns(mqttClient), + }; + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, mqttLibrary, serviceId); + await chaiAssert.isRejected(zwaveJSUIHandler.publish('toto', 'mesage')); + }); +}); diff --git a/server/test/services/zwavejs-ui/lib/zwaveJSUI.saveConfiguration.test.js b/server/test/services/zwavejs-ui/lib/zwaveJSUI.saveConfiguration.test.js new file mode 100644 index 0000000000..a14ef5c80e --- /dev/null +++ b/server/test/services/zwavejs-ui/lib/zwaveJSUI.saveConfiguration.test.js @@ -0,0 +1,35 @@ +const sinon = require('sinon'); + +const { assert, fake } = sinon; + +const ZwaveJSUIHandler = require('../../../../services/zwavejs-ui/lib'); + +const serviceId = 'ffa13430-df93-488a-9733-5c540e9558e0'; + +const gladys = { + variable: { + setValue: fake.resolves(null), + }, +}; + +describe('zwaveJSUIHandler.saveConfiguration', () => { + beforeEach(() => { + sinon.reset(); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should save mqtt configuration', async () => { + const zwaveJSUIHandler = new ZwaveJSUIHandler(gladys, {}, serviceId); + await zwaveJSUIHandler.saveConfiguration({ + mqtt_url: 'mqtt://localhost', + mqtt_username: 'my_username', + mqtt_password: 'my_password', + }); + assert.calledWith(gladys.variable.setValue, 'ZWAVEJS_UI_MQTT_URL', 'mqtt://localhost', serviceId); + assert.calledWith(gladys.variable.setValue, 'ZWAVEJS_UI_MQTT_USERNAME', 'my_username', serviceId); + assert.calledWith(gladys.variable.setValue, 'ZWAVEJS_UI_MQTT_PASSWORD', 'my_password', serviceId); + }); +}); diff --git a/server/utils/constants.js b/server/utils/constants.js index 9679268e20..83058877b6 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -63,6 +63,11 @@ const MUSIC_PLAYBACK_STATE = { PAUSED: 0, }; +const OPENING_SENSOR_STATE = { + OPEN: 0, + CLOSE: 1, +}; + const USER_ROLE = { ADMIN: 'admin', HABITANT: 'habitant', @@ -916,6 +921,11 @@ const WEBSOCKET_MESSAGE_TYPES = { ERROR: 'mqtt.error', INSTALLATION_STATUS: 'mqtt.install-status', }, + ZWAVEJS_UI: { + CONNECTED: 'zwavejs-ui.connected', + ERROR: 'zwavejs-ui.error', + SCAN_COMPLETED: 'zwavejs-ui.scan-completed', + }, ZIGBEE2MQTT: { DISCOVER: 'zigbee2mqtt.discover', STATUS_CHANGE: 'zigbee2mqtt.status-change', @@ -1140,3 +1150,4 @@ module.exports.ALARM_MODES = ALARM_MODES; module.exports.ALARM_MODES_LIST = ALARM_MODES_LIST; module.exports.MUSIC_PLAYBACK_STATE = MUSIC_PLAYBACK_STATE; +module.exports.OPENING_SENSOR_STATE = OPENING_SENSOR_STATE;