diff --git a/front/src/assets/integrations/cover/zwave-js-ui.jpg b/front/src/assets/integrations/cover/zwave-js-ui.jpg new file mode 100644 index 0000000000..e87982939d Binary files /dev/null and b/front/src/assets/integrations/cover/zwave-js-ui.jpg differ 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 0000000000..a23b4a4b32 Binary files /dev/null and b/front/src/assets/integrations/zwavejs-ui/zwavejs-ui-gateway-configuration.jpg differ diff --git a/front/src/assets/integrations/zwavejs-ui/zwavejs-ui-mqtt-configuration.jpg b/front/src/assets/integrations/zwavejs-ui/zwavejs-ui-mqtt-configuration.jpg new file mode 100644 index 0000000000..a592386adb Binary files /dev/null and b/front/src/assets/integrations/zwavejs-ui/zwavejs-ui-mqtt-configuration.jpg differ 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;