diff --git a/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceEdit.cy.js b/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceEdit.cy.js new file mode 100755 index 0000000000..9afbbffd91 --- /dev/null +++ b/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceEdit.cy.js @@ -0,0 +1,82 @@ +describe('RFLink device edit', () => { + const device = { + id: '86aa7', + switch: 'switch', + service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279', + name: `Prise`, + selector: `rflink:86aa7:11`, + external_id: `rflink:86aa7:11`, + model: 'NewKaku', + should_poll: false, + features: [ + { + name: 'switch', + selector: `rflink:86aa7:switch:11`, + external_id: `rflink:86aa7:switch:11`, + rfcode: 'CMD', + category: 'switch', + type: 'binary', + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 1 + } + ] + }; + + before(() => { + cy.login(); + + // Create a new peripheral + cy.createDevice(device, 'rflink'); + + cy.visit('/dashboard/integration/device/rflink/edit/rflink-86aa7-11'); + }); + + after(() => { + // Delete all RFLink devices + cy.deleteDevices('rflink'); + }); + + it('Check first device', () => { + cy.contains('.card-header', device.name) + .should('exist') + .parent('.card') + .within(() => { + cy.get('input') + .first() + .should('have.value', device.name); + cy.get('select').should('have.value', ''); + }); + }); + + it('Update device', () => { + const { rooms } = Cypress.env('house'); + cy.contains('.card-header', device.name) + .should('exist') + .parent('.card') + .within(() => { + cy.get('input') + .first() + .clear() + .type('Switch Living room'); + + cy.get('select').select(rooms[0].name); + cy.contains('button', 'integration.rflink.device.saveButton').click(); + }); + }); + + it('Check updated device', () => { + cy.contains('button', 'global.backButton').click(); + + const { rooms } = Cypress.env('house'); + cy.contains('.card-header', 'Switch Living room') + .should('exist') + .parent('.card') + .within(() => { + cy.get('input').should('have.value', 'Switch Living room'); + cy.get('select option:selected').should('have.text', rooms[0].name); + }); + }); +}); diff --git a/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceEdit.js b/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceEdit.js new file mode 100755 index 0000000000..9afbbffd91 --- /dev/null +++ b/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceEdit.js @@ -0,0 +1,82 @@ +describe('RFLink device edit', () => { + const device = { + id: '86aa7', + switch: 'switch', + service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279', + name: `Prise`, + selector: `rflink:86aa7:11`, + external_id: `rflink:86aa7:11`, + model: 'NewKaku', + should_poll: false, + features: [ + { + name: 'switch', + selector: `rflink:86aa7:switch:11`, + external_id: `rflink:86aa7:switch:11`, + rfcode: 'CMD', + category: 'switch', + type: 'binary', + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 1 + } + ] + }; + + before(() => { + cy.login(); + + // Create a new peripheral + cy.createDevice(device, 'rflink'); + + cy.visit('/dashboard/integration/device/rflink/edit/rflink-86aa7-11'); + }); + + after(() => { + // Delete all RFLink devices + cy.deleteDevices('rflink'); + }); + + it('Check first device', () => { + cy.contains('.card-header', device.name) + .should('exist') + .parent('.card') + .within(() => { + cy.get('input') + .first() + .should('have.value', device.name); + cy.get('select').should('have.value', ''); + }); + }); + + it('Update device', () => { + const { rooms } = Cypress.env('house'); + cy.contains('.card-header', device.name) + .should('exist') + .parent('.card') + .within(() => { + cy.get('input') + .first() + .clear() + .type('Switch Living room'); + + cy.get('select').select(rooms[0].name); + cy.contains('button', 'integration.rflink.device.saveButton').click(); + }); + }); + + it('Check updated device', () => { + cy.contains('button', 'global.backButton').click(); + + const { rooms } = Cypress.env('house'); + cy.contains('.card-header', 'Switch Living room') + .should('exist') + .parent('.card') + .within(() => { + cy.get('input').should('have.value', 'Switch Living room'); + cy.get('select option:selected').should('have.text', rooms[0].name); + }); + }); +}); diff --git a/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceList.cy.js b/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceList.cy.js new file mode 100755 index 0000000000..2a3d01dccc --- /dev/null +++ b/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceList.cy.js @@ -0,0 +1,105 @@ +describe('RFLink device list', () => { + const device = { + id: '86aa7', + switch: 'switch', + service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279', + name: `Prise`, + selector: `rflink:86aa7:11`, + external_id: `rflink:86aa7:11`, + model: 'NewKaku', + should_poll: false, + features: [ + { + name: 'switch', + selector: `rflink:86aa7:switch:11`, + external_id: `rflink:86aa7:switch:11`, + rfcode: 'CMD', + category: 'switch', + type: 'binary', + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 1 + } + ] + }; + + before(() => { + cy.login(); + + // Create a new peripheral + cy.createDevice(device, 'rflink'); + + cy.visit('/dashboard/integration/device/rflink'); + }); + + after(() => { + // Delete all Bluetooth devices + cy.deleteDevices('rflink'); + }); + + it('Check first device', () => { + cy.contains('.card-header', device.name) + .should('exist') + .parent('.card') + .within(() => { + cy.get('input').should('have.value', device.name); + cy.get('select').should('have.value', ''); + }); + }); + + it('Update device', () => { + const { rooms } = Cypress.env('house'); + cy.contains('.card-header', device.name) + .should('exist') + .parent('.card') + .within(() => { + cy.get('input') + .clear() + .type('New name'); + cy.get('select').select(rooms[0].name); + + cy.get('.card-header').should('have.text', 'New name'); + + cy.contains('button', 'integration.bluetooth.device.saveButton').click(); + }); + }); + + it('Check updated device', () => { + const { rooms } = Cypress.env('house'); + cy.contains('.card-header', 'New name') + .should('exist') + .parent('.card') + .within(() => { + cy.get('input').should('have.value', 'New name'); + cy.get('select option:selected').should('have.text', rooms[0].name); + }); + }); + + it('Edit delete', () => { + cy.contains('.card-header', 'New name') + .should('exist') + .parent('.card') + .within(() => { + cy.contains('button', 'integration.rflink.device.editButton').click(); + }); + + // Check redirected to edit page + cy.location('pathname').should('eq', '/dashboard/integration/device/rflink/edit/rflink-86aa7-11'); + + // Go back + cy.contains('global.backButton').click(); + }); + + it('Delete delete', () => { + cy.contains('.card-header', 'New name') + .should('exist') + .parent('.card') + .within(() => { + cy.contains('button', 'integration.rflink.device.deleteButton').click(); + }); + + cy.contains('.card-header', 'New name').should('not.exist'); + }); +}); diff --git a/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceList.js b/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceList.js new file mode 100755 index 0000000000..2a3d01dccc --- /dev/null +++ b/front/cypress/e2e/routes/integration/rflink/devices/RFLinkDeviceList.js @@ -0,0 +1,105 @@ +describe('RFLink device list', () => { + const device = { + id: '86aa7', + switch: 'switch', + service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279', + name: `Prise`, + selector: `rflink:86aa7:11`, + external_id: `rflink:86aa7:11`, + model: 'NewKaku', + should_poll: false, + features: [ + { + name: 'switch', + selector: `rflink:86aa7:switch:11`, + external_id: `rflink:86aa7:switch:11`, + rfcode: 'CMD', + category: 'switch', + type: 'binary', + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 1 + } + ] + }; + + before(() => { + cy.login(); + + // Create a new peripheral + cy.createDevice(device, 'rflink'); + + cy.visit('/dashboard/integration/device/rflink'); + }); + + after(() => { + // Delete all Bluetooth devices + cy.deleteDevices('rflink'); + }); + + it('Check first device', () => { + cy.contains('.card-header', device.name) + .should('exist') + .parent('.card') + .within(() => { + cy.get('input').should('have.value', device.name); + cy.get('select').should('have.value', ''); + }); + }); + + it('Update device', () => { + const { rooms } = Cypress.env('house'); + cy.contains('.card-header', device.name) + .should('exist') + .parent('.card') + .within(() => { + cy.get('input') + .clear() + .type('New name'); + cy.get('select').select(rooms[0].name); + + cy.get('.card-header').should('have.text', 'New name'); + + cy.contains('button', 'integration.bluetooth.device.saveButton').click(); + }); + }); + + it('Check updated device', () => { + const { rooms } = Cypress.env('house'); + cy.contains('.card-header', 'New name') + .should('exist') + .parent('.card') + .within(() => { + cy.get('input').should('have.value', 'New name'); + cy.get('select option:selected').should('have.text', rooms[0].name); + }); + }); + + it('Edit delete', () => { + cy.contains('.card-header', 'New name') + .should('exist') + .parent('.card') + .within(() => { + cy.contains('button', 'integration.rflink.device.editButton').click(); + }); + + // Check redirected to edit page + cy.location('pathname').should('eq', '/dashboard/integration/device/rflink/edit/rflink-86aa7-11'); + + // Go back + cy.contains('global.backButton').click(); + }); + + it('Delete delete', () => { + cy.contains('.card-header', 'New name') + .should('exist') + .parent('.card') + .within(() => { + cy.contains('button', 'integration.rflink.device.deleteButton').click(); + }); + + cy.contains('.card-header', 'New name').should('not.exist'); + }); +}); diff --git a/front/src/assets/integrations/cover/rflink.png b/front/src/assets/integrations/cover/rflink.png new file mode 100644 index 0000000000..0500ddf70c Binary files /dev/null and b/front/src/assets/integrations/cover/rflink.png differ diff --git a/front/src/components/app.jsx b/front/src/components/app.jsx index 2962f58aae..35e123707d 100644 --- a/front/src/components/app.jsx +++ b/front/src/components/app.jsx @@ -86,6 +86,11 @@ import XiaomiPage from '../routes/integration/all/xiaomi'; import EditXiaomiPage from '../routes/integration/all/xiaomi/edit-page'; import NextcloudTalkPage from '../routes/integration/all/nextcloud-talk'; +// RFLink integration +import RflinkDevicePage from '../routes/integration/all/rflink/device-page'; +import RflinkSettingsPage from '../routes/integration/all/rflink/settings-page'; +import RflinkEditPage from '../routes/integration/all/rflink/device-page/setup'; + // Deprecated integration import ZwaveNodePage from '../routes/integration/all/zwave/node-page'; @@ -288,6 +293,12 @@ const AppRouter = connect( + + + + + + diff --git a/front/src/config/demo.js b/front/src/config/demo.js index a15b5a57be..0aa119617e 100644 --- a/front/src/config/demo.js +++ b/front/src/config/demo.js @@ -1265,6 +1265,82 @@ const data = { ] } ], + 'get /api/v1/service/rflink/device': [ + { + id: '86aa70', + switch: 'switch', + service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279', + room_id: 'f99ab22a-e6a8-4756-b1fe-4d19dc8c8620', + name: `Coffea machine power switch`, + selector: `rflink:86aa70:10`, + external_id: `rflink:86aa70:10`, + model: 'Tristate', + should_poll: false, + features: [ + { + name: 'switch', + selector: `rflink:86aa70:switch:10`, + external_id: `rflink:86aa70:switch:10`, + rfcode: 'CMD', + category: 'switch', + type: 'binary', + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 1 + } + ] + } + ], + 'get /api/v1/service/rflink/variable/RFLINK_PATH': {}, + 'get /api/v1/service/rflink': {}, + 'get /api/v1/device/rflink:86aa70:10': { + name: 'Switch', + external_id: 'rflink:86aa6:switch:10', + selector: 'rflink:1234', + room_id: 'f99ab22a-e6a8-4756-b1fe-4d19dc8c8620', + model: '', + features: [ + { + name: 'switch', + selector: `rflink:86aa70:switch:10`, + external_id: `rflink:86aa70:switch:10`, + rfcode: 'CMD', + category: 'switch', + type: 'binary' + } + ] + }, + 'get /api/v1/service/rflink/newDevices': [ + { + id: 'fbedb47f-4d25-4381-8923-2633b23192a0', + service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279', + room_id: 'cecc52c7-3e67-4b75-9b13-9a8867b0443d', + name: 'PC bureau', + selector: 'rflink:1234', + external_id: 'rflink:86aa6:switch:10', + should_poll: false, + poll_frequency: null, + created_at: '2019-02-12T07:49:07.556Z', + updated_at: '2019-02-12T07:49:07.556Z', + features: [ + { + name: 'power', + selector: 'switch-test', + category: 'switch', + type: 'binary' + } + ] + } + ], + 'get /api/v1/service/rflink/status': { + currentMilightGateway: '9076', + lastCommand: '20;OK', + connected: true, + scanInProgress: false, + ready: true + }, 'get /api/v1/service/zwave/device': [ { id: 'fbedb47f-4d25-4381-8923-2633b23192a0', diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index 5744a1f368..2471acf8ec 100644 --- a/front/src/config/i18n/de.json +++ b/front/src/config/i18n/de.json @@ -461,6 +461,85 @@ "navigation": "Navigation" } }, + "rflink": { + "title": "RFLink", + "description": "Verbindung zum RFLink-Gateway herstellen.", + "settingsTab": "Einstellungen", + "deviceTab": "Geräte", + "roomLabel": "Raum", + "namePlaceholder": "Geben Sie den Namen Ihres Geräts ein", + "settings": { + "title": "RFLink-USB-Einstellungen", + "description": "Um RFLink in Gladys zu verwenden, benötigen Sie ein mit Ihrer Gladys-Instanz verbundenes RFLink-Gateway.", + "connectButton": "Verbinden", + "disconnectButton": "Trennen", + "refreshButton": "USB-Liste aktualisieren", + "notConnected": "Gladys ist nicht mit einem RFLink-Gateway verbunden.", + "connectedWithSuccess": "RFLink-Gateway erfolgreich verbunden.", + "connecting": "Versuche, eine Verbindung zum RFLink-Gateway herzustellen...", + "driverFailedError": "Beim Versuch, eine Verbindung zum RFLink-Gateway herzustellen, ist ein Fehler aufgetreten.", + "rflinkUsbDriverPathLabel": "Wählen Sie den Pfad zum RFLink-Gateway-USB aus:", + "debug": { + "title": "RFLink-Debug-Konsole", + "info": "Hier können Sie den letzten Befehl sehen, den RFLink an Gladys gesendet hat. Es hat dieses Format: '20;02;MODEL;ID;LABEL=Daten;LABEL1=Daten1;'...", + "placeholder": "Die zu sendende Nachricht", + "sendButton": "Senden" + }, + "milight": { + "title": "RFLink Milight-Einstellungen", + "gatewayBarinfo": "Gateway-Nummer:", + "zoneInfo": "Gateway-Zone:", + "about": "Sie können Ihre aktuelle Milight-Bridge-ID verwenden oder, wenn Sie kein Gateway haben, ein neues verwenden (standardmäßig F746). Die ID ist nur ein Code zur Identifizierung des Gateways. Sie können beliebig viele Milight verwenden, aber jedes Gateway hat 4 Zonen.", + "pairButton": "Koppeln", + "unpairButton": "Trennen" + } + }, + "feature": { + "nameLabel": "Name", + "readOnlyLabel": "Ist dies eine schreibgeschützte Funktion?", + "readOnlyButton": "Ja, diese Funktion sendet nur Daten an Gladys.", + "model": "Modell", + "message": "Sie können nur Aktoren erstellen. Sensoren werden automatisch erkannt und im Geräte-Tab hinzugefügt, wenn sie Nachrichten senden.", + "namePlaceholder": "Feature-Namen eingeben", + "switchIdLabel": "ID", + "switchIdPlaceholder": "RFLink-Geräte-ID", + "switchNumberLabel": "Kanal", + "switchNumberPlaceholder": "Wert von SWITCH in der Debug-Konsole (Einstellungen-Tab)", + "unitLabel": "Einheit", + "minLabel": "Minimalwert", + "minPlaceholder": "Minimalwert des Features eingeben", + "maxLabel": "Maximalwert", + "maxPlaceholder": "Maximalwert des Features eingeben", + "addButton": "Feature hinzufügen", + "deleteLabel": "Feature löschen" + }, + "device": { + "title": "Geräte in Gladys", + "devicesInfo": "Einige Geräte werden als Sensoren erkannt. In diesem Fall können Sie dies ändern, indem Sie die Schreibgeschützt-Eigenschaft des Geräts bearbeiten.", + "deviceOnNetworkTitle": "Vom Gateway erkannte Geräte", + "connectButton": "Verbinden/Erneut verbinden", + "search": "Geräte suchen", + "deviceNotHandled": "Gerät noch nicht behandelt, bitte kontaktieren Sie uns, um uns bei der Verbindung in Gladys zu helfen!", + "noDevices": "Keine Geräte gefunden. Sie können Geräte mit dem neuen Geräte-Tab hinzufügen.", + "scanButton": "Scannen", + "noDevicesFound": "Keine Geräte gefunden. Stellen Sie sicher, dass Sie ein verbundenes RFLink-Gateway im Einstellungen-Tab haben.", + "found": "Alle erkannten Geräte werden hier angezeigt.", + "nameLabel": "Name", + "featuresLabel": "Features", + "noFeatures": "Keine Features", + "newButton": "Neu", + "saveButton": "Speichern", + "deleteButton": "Löschen", + "editButton": "Bearbeiten", + "noNameLabel": "Kein Name", + "roomLabel": "Raum", + "returnButton": "Zurück", + "notFound": "Angefordertes Gerät nicht gefunden.", + "backToList": "Zurück zur Geräteliste", + "saveError": "Fehler beim Speichern oder Löschen des Geräts", + "saveConflictError": "Konflikt: Sind Sie sicher, dass alle externen IDs der Gerätefunktionen eindeutig sind?" + } + }, "telegram": { "title": "Telegram", "description": "Chatte in Telegram mit Gladys.", diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 7357e830ab..bf7f24fce2 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -461,6 +461,85 @@ "navigation": "navigation" } }, + "rflink": { + "title": "RFLink", + "description": "Connect to RFLink gateway.", + "settingsTab": "Settings", + "deviceTab": "Devices", + "roomLabel": "Room", + "namePlaceholder": "Enter the name of your device", + "settings": { + "title": "RFLink Usb Settings", + "description": "To use RFLink in Gladys, you need to have a RFLink gateway connected to your Gladys instance.", + "connectButton": "Connect", + "disconnectButton": "Disconnect", + "refreshButton": "Refresh USB list", + "notConnected": "Gladys is not connected to any Rflink Gateway.", + "connectedWithSuccess": "RFLink Gateway connected with success.", + "connecting": "Trying to connect to RFLink Gateway...", + "driverFailedError": "An error occured while trying to connect to RFLink Gateway.", + "rflinkUsbDriverPathLabel": "Select the rflink gateway usb path:", + "debug": { + "title": "RFLink debug console", + "info": "Here you can see the last command that RFLink sent to Gladys it's in this form: '20;02;MODEL;ID;LABEL=data;LABEL1=data1;'...", + "placeholder": "The message to send", + "sendButton": "Send" + }, + "milight": { + "title": "RFLink Milight settings", + "gatewayBarinfo": "Gateway number:", + "zoneInfo": "Gateway zone:", + "about": "You can use your actual milight bridge id or if you don't have a gateway, use a new one (F746 by default) the id is just a code to identify the gateway. You can use unlimited milight but each bridge has 4 zones", + "pairButton": "Pair", + "unpairButton": "Unpair" + } + }, + "feature": { + "nameLabel": "Name", + "readOnlyLabel": "Is it read only feature?", + "readOnlyButton": "Yes, this feature is sending data to Gladys only.", + "model": "Model", + "message": "You can only create actuators, sensors are automatically detected and added in the device tab when they send messages", + "namePlaceholder": "Enter feature name", + "switchIdLabel": "ID", + "switchIdPlaceholder": "RFLink device ID", + "switchNumberLabel": "Channel", + "switchNumberPlaceholder": "Value of SWITCH in debug console (settings tab)", + "unitLabel": "Unit", + "minLabel": "Minimum value", + "minPlaceholder": "Enter feature minimum value", + "maxLabel": "Maximum value", + "maxPlaceholder": "Enter feature maximum value", + "addButton": "Add feature", + "deleteLabel": "Delete feature" + }, + "device": { + "title": "Devices in Gladys", + "devicesInfo": "Some devices are detected as sensors. In this case, you can change it by editing the device's read only property", + "deviceOnNetworkTitle": "Devices detected by the gateway", + "connectButton": "Connect/Reconnect", + "search": "Search devices", + "deviceNotHandled": "Device not handled yet, please contact us to help us connect it in Gladys!", + "noDevices": "No devices, you can add devices with the new device tab", + "scanButton": "Scan", + "noDevicesFound": "No devices found. Make sure you have a connected RFLink Gateway in the settings Tab", + "found": "All detected devices will appear here", + "nameLabel": "Name", + "featuresLabel": "Features", + "noFeatures": "No features", + "newButton": "New", + "saveButton": "Save", + "deleteButton": "Delete", + "editButton": "Edit", + "noNameLabel": "No name", + "roomLabel": "Room", + "returnButton": "Return back", + "notFound": "Requested device not found.", + "backToList": "Back to device list", + "saveError": "Error saving or deleting device", + "saveConflictError": "Conflict: Are you sure all device feature external IDs are unique?" + } + }, "telegram": { "title": "Telegram", "description": "Chat with Gladys remotely through Telegram.", @@ -1764,12 +1843,12 @@ "selectTriggerLabel": "Select a trigger type", "newAction": "New action", "selectActionType": "Select an action type", - "addActionButton": "Add action", + "addActionButton": "Add new action", "noTriggersYet": "No trigger added yet. It's not mandatory to have a trigger in a scene.", "noActionsYet": "No actions added yet. Click on the + button to add an action to this scene.", "triggersDescription": "Every trigger is independent. When any of those triggers are triggered, the scene will run.", "actionsDescription": "All actions in this block will run in parallel. To make a sequence of actions, add actions to the next block.", - "addNewTriggerButton": "Add trigger", + "addNewTriggerButton": "Add new trigger", "saveSceneError": "There was an error saving your scene. Please check that all actions/triggers are filled and correct.", "actionsCard": { "delay": { diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 98625dd217..0689ae59b8 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -589,6 +589,85 @@ "saveLabel": "Enregistrer" } }, + "rflink": { + "title": "RFLink", + "description": "Connection à une passerelle RFLink.", + "settingsTab": "Paramètres", + "deviceTab": "Appareils", + "roomLabel": "Pièce", + "namePlaceholder": "Entrez le nom de votre appareil", + "settings": { + "title": "Configurations USB", + "description": "Pour utiliser le module RFLink dans Gladys, vous devez avoir une passerelle RFLink connectée à votre instance Gladys.", + "connectButton": "Connecter", + "disconnectButton": "Déconnecter", + "refreshButton": "Actualiser la liste des ports USB", + "notConnected": "Gladys n'est connectée à aucune passerelle RFLink.", + "connectedWithSuccess": "Passerelle RFLink connectée avec succès...", + "connecting": "Tentative de connexion à la passerelle RFLink...", + "driverFailedError": "Une erreur s'est produite lors de la tentative de connexion à la passerelle RFLink.", + "rflinkUsbDriverPathLabel": "Sélectionner le chemin usb de la gateway rflink :", + "debug": { + "title": "RFLink debug console", + "info": "Ici vous pouvez voir la dernière commande envoyée par la passerelle RFLink à Gladys. Sous la forme : '20;02;MODEL;ID;LABEL=data;LABEL1=data1;' ...", + "placeholder": "Message à envoyer", + "sendButton": "Envoyer" + }, + "milight": { + "title": "Paramètres Milight", + "gatewayBarinfo": "Numéro de passerelle :", + "zoneInfo": "Zone de passerelle :", + "about": " Vous pouvez utiliser votre véritable identifiant de pont milight ou si vous n'avez pas de passerelle, utilisez-en un nouveau (F746 par défaut), l'identifiant est juste un code pour identifier la passerelle. Le nombre d'appareils MiLight est illimité mais chaque pont n'a que 4 zones", + "pairButton": "Appairage", + "unpairButton": "Dissocier" + } + }, + "feature": { + "nameLabel": "Nom", + "readOnlyLabel": "Est-ce une fonctionnalité en lecture seule ?", + "readOnlyButton": "Oui, cette fonctionnalité ne fait qu'envoyer des données à Gladys.", + "model": "Modèle", + "message": "Vous ne pouvez uniquement créer que des actionneurs. Les capteurs sont automatiquement détectés et ajoutés dans l'onglet appareil lorsqu'ils envoient des messages", + "namePlaceholder": "Entrez le nom de la fonction", + "switchIdLabel": "ID", + "switchIdPlaceholder": "ID appareil RFLink", + "switchNumberLabel": "Canal", + "switchNumberPlaceholder": "Valeur de SWITCH dans la console de débogage (onglet Paramètres)", + "unitLabel": "Unité", + "minLabel": "Valeur minimum", + "minPlaceholder": "Entrez la valeur minimale de la fonction", + "maxLabel": "Valeur maximale", + "maxPlaceholder": "Entrez la valeur maximale de la fonction", + "addButton": "Ajouter une fonctionnalité", + "deleteLabel": "Supprimer la fonction" + }, + "device": { + "title": "Appareils dans Gladys", + "devicesInfo": "Certains actionneurs sont détectés comme étant des capteurs. Dans ce cas, vous pouvez le modifier en éditant la propriété d'un appareil", + "deviceOnNetworkTitle": "Appareils détectés par la passerelle", + "connectButton": "Connecter / Reconnecter", + "search": "Rechercher des appareils", + "deviceNotHandled": "Appareil non géré, veuillez nous contacter pour nous aider à le connecter dans Gladys !", + "noDevices": "Aucun appareil, vous pouvez ajouter des appareils avec l'onglet ", + "scanButton": "Scanner", + "noDevicesFound": "Aucun périphérique trouvé. Assurez-vous que vous avez une passerelle RFLink connectée dans l'onglet ", + "found": "Tous les appareils détectés apparaîtront ici", + "nameLabel": "Nom", + "featuresLabel": "Fonctionnalités", + "noFeatures": "Aucune fonctionnalité", + "newButton": "Nouveau", + "saveButton": "Sauvegarder", + "deleteButton": "Supprimer", + "editButton": "Editer", + "noNameLabel": "Sans nom", + "roomLabel": "Pièce", + "returnButton": "Retour en arrière", + "notFound": "Appareil demandé introuvable.", + "backToList": "Retour à la liste des appareils", + "saveError": "Erreur lors de l'enregistrement ou de la suppression de l'appareil", + "saveConflictError": "Conflit: êtes-vous sûr que tous les ID externes des fonctionnalités de l'appareil sont uniques?" + } + }, "telegram": { "title": "Telegram", "description": "Parlez à Gladys grâce à Telegram.", diff --git a/front/src/config/integrations/devices.json b/front/src/config/integrations/devices.json index 945a3b34f0..6927ecfe3a 100644 --- a/front/src/config/integrations/devices.json +++ b/front/src/config/integrations/devices.json @@ -29,6 +29,10 @@ "key": "tasmota", "img": "/assets/integrations/cover/tasmota.jpg" }, + { + "key": "rflink", + "img": "/assets/integrations/cover/rflink.png" + }, { "key": "tpLink", "link": "tp-link", diff --git a/front/src/routes/integration/all/rflink/RflinkPage.jsx b/front/src/routes/integration/all/rflink/RflinkPage.jsx new file mode 100644 index 0000000000..3ac7774d8f --- /dev/null +++ b/front/src/routes/integration/all/rflink/RflinkPage.jsx @@ -0,0 +1,48 @@ +import { Text } from 'preact-i18n'; +import { Link } from 'preact-router/match'; + +const RflinkPage = ({ children, ...props }) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + {children} + + + + + +); + +export default RflinkPage; diff --git a/front/src/routes/integration/all/rflink/device-page/Device.jsx b/front/src/routes/integration/all/rflink/device-page/Device.jsx new file mode 100644 index 0000000000..41dcc0a887 --- /dev/null +++ b/front/src/routes/integration/all/rflink/device-page/Device.jsx @@ -0,0 +1,157 @@ +import { Text, Localizer } from 'preact-i18n'; +import { Component } from 'preact'; +import cx from 'classnames'; +import get from 'get-value'; +import { Link } from 'preact-router/match'; +import { DEVICE_FEATURE_CATEGORIES } from '../../../../../../../server/utils/constants'; +import { RequestStatus, DeviceFeatureCategoriesIcon } from '../../../../../utils/consts'; + +class RflinkDeviceBox extends Component { + refreshDeviceProperty = () => { + if (!this.props.device.features) { + return null; + } + const batteryLevelDeviceFeature = this.props.device.features.find( + deviceFeature => deviceFeature.category === DEVICE_FEATURE_CATEGORIES.BATTERY + ); + const batteryLevel = get(batteryLevelDeviceFeature, 'last_value'); + this.setState({ + batteryLevel + }); + }; + saveDevice = async () => { + this.setState({ loading: true }); + try { + await this.props.saveDevice(this.props.device); + } catch (e) { + this.setState({ error: RequestStatus.Error }); + } + this.setState({ loading: false }); + }; + deleteDevice = async () => { + this.setState({ loading: true }); + try { + await this.props.deleteDevice(this.props.device, this.props.deviceIndex); + } catch (e) { + this.setState({ error: RequestStatus.Error }); + } + this.setState({ loading: false }); + }; + updateName = e => { + this.props.updateDeviceProperty(this.props.deviceIndex, 'name', e.target.value); + }; + updateRoom = e => { + this.props.updateDeviceProperty(this.props.deviceIndex, 'room_id', e.target.value); + }; + componentWillMount() { + this.refreshDeviceProperty(); + } + + componentWillUpdate() { + this.refreshDeviceProperty(); + } + + render(props, { batteryLevel, loading }) { + return ( + + + + {props.device.name} + {batteryLevel && ( + + + + + + + + + )} + + + + + + + + + + + } + /> + + + + + + + + + + + {props.houses && + props.houses.map(house => ( + + {house.rooms.map(room => ( + + {room.name} + + ))} + + ))} + + + + + + + + {props.device && + props.device.features && + props.device.features.map(feature => ( + + + + + + + ))} + {(!props.device.features || props.device.features.length === 0) && ( + + )} + + + + + + + + + + + + + + + + + + + + + ); + } +} + +export default RflinkDeviceBox; diff --git a/front/src/routes/integration/all/rflink/device-page/DeviceForm.jsx b/front/src/routes/integration/all/rflink/device-page/DeviceForm.jsx new file mode 100644 index 0000000000..5e0ab701e0 --- /dev/null +++ b/front/src/routes/integration/all/rflink/device-page/DeviceForm.jsx @@ -0,0 +1,118 @@ +import { Text, Localizer } from 'preact-i18n'; +import { Component } from 'preact'; +import { DeviceFeatureCategoriesIcon } from '../../../../../utils/consts'; +import { DEVICE_MODELS_LIST } from '../../../../../../../server/utils/constants'; +import get from 'get-value'; + +class RflinkDeviceForm extends Component { + updateName = e => { + this.props.updateDeviceProperty(this.props.deviceIndex, 'name', e.target.value); + }; + + updateRoom = e => { + this.props.updateDeviceProperty(this.props.deviceIndex, 'room_id', e.target.value); + }; + + updateModel = e => { + this.props.updateDeviceProperty(this.props.deviceIndex, 'model', e.target.value); + }; + checkModel = e => { + for (let i = 0; i < DEVICE_MODELS_LIST.length; i++) { + if (e === DEVICE_MODELS_LIST[`${i}`]) { + return true; + } + } + + DEVICE_MODELS_LIST.forEach(model => { + if (e === model) { + return true; + } + }); + }; + + updateExternalId = e => { + this.props.updateDeviceProperty(this.props.deviceIndex, 'external_id', e.target.value); + }; + render({ ...props }) { + return ( + + + + + + + } + /> + + + + + + + + + + + + {props.houses && + props.houses.map(house => ( + + {house.rooms.map(room => ( + + {room.name} + + ))} + + ))} + + + + {(props.device.model === undefined || this.checkModel(props.device.model) === true) && ( + + + + + + + + + {DEVICE_MODELS_LIST.map(model => ( + + {model} + + ))} + + + )} + + + + + + {props.device && + props.device.features && + props.device.features.map(feature => ( + + + + + + + ))} + {(!props.device.features || props.device.features.length === 0) && ( + + )} + + + + ); + } +} + +export default RflinkDeviceForm; diff --git a/front/src/routes/integration/all/rflink/device-page/DevicePage.jsx b/front/src/routes/integration/all/rflink/device-page/DevicePage.jsx new file mode 100644 index 0000000000..2cd7758515 --- /dev/null +++ b/front/src/routes/integration/all/rflink/device-page/DevicePage.jsx @@ -0,0 +1,81 @@ +import { Text, Localizer } from 'preact-i18n'; +import cx from 'classnames'; + +import { RequestStatus } from '../../../../../utils/consts'; +import Device from './Device'; +import { Link } from 'preact-router/match'; +import style from './style.css'; + +const NodeTab = ({ children, ...props }) => ( + + + + + + + + + + + + + + + + + + + + } + onInput={props.debouncedSearch} + /> + + + + + + + + + + + + + + + + + {!props.rflinkDevices || + (props.rflinkDevices.length === 0 && ( + + + + ))} + {props.getRflinkDevicesStatus === RequestStatus.Getting && } + + {props.rflinkDevices && + props.rflinkDevices.map((rflinkDevice, index) => ( + + ))} + + + + + +); + +export default NodeTab; diff --git a/front/src/routes/integration/all/rflink/device-page/FoundDevices.jsx b/front/src/routes/integration/all/rflink/device-page/FoundDevices.jsx new file mode 100644 index 0000000000..fc8ca1d41f --- /dev/null +++ b/front/src/routes/integration/all/rflink/device-page/FoundDevices.jsx @@ -0,0 +1,59 @@ +import { Text } from 'preact-i18n'; +import cx from 'classnames'; + +import style from './style.css'; +import { RequestStatus } from '../../../../../utils/consts'; + +const createDevice = (props, device) => () => { + props.createDevice(device); +}; + +const FoundDevices = ({ children, ...props }) => ( + + + + + + + + + + + {props.getRflinkNewDevicesStatus === RequestStatus.Getting && } + + {props.rflinkNewDevices && props.rflinkNewDevices.length === 0 && ( + + + + + + )} + {props.rflinkNewDevices && + props.rflinkNewDevices.map(device => ( + + + + {device.name} + + + + + + + + + ))} + + + + + +); + +export default FoundDevices; diff --git a/front/src/routes/integration/all/rflink/device-page/actions.js b/front/src/routes/integration/all/rflink/device-page/actions.js new file mode 100644 index 0000000000..521a6ca16d --- /dev/null +++ b/front/src/routes/integration/all/rflink/device-page/actions.js @@ -0,0 +1,140 @@ +import { RequestStatus } from '../../../../../utils/consts'; +import update from 'immutability-helper'; +import createActionsHouse from '../../../../../actions/house'; +import debounce from 'debounce'; +import createActionsIntegration from '../../../../../actions/integration'; + +function createActions(store) { + const integrationActions = createActionsIntegration(store); + store.setState({ integrationActions }); + const houseActions = createActionsHouse(store); + const actions = { + async getRflinkDevices(state, take, skip) { + store.setState({ + getRflinkDevicesStatus: RequestStatus.Getting + }); + try { + const options = { + service: 'rflink', + order_dir: state.getRflinkDeviceOrderDir || 'asc', + take, + skip + }; + if (state.rflinkDeviceSearch && state.rflinkDeviceSearch.length) { + options.search = state.rflinkDeviceSearch; + } + const rflinkDevicesReceived = await state.httpClient.get('/api/v1/service/rflink/device', options); + let rflinkDevices; + if (skip === 0) { + rflinkDevices = rflinkDevicesReceived; + } else { + rflinkDevices = update(state.rflinkDevices, { + $push: rflinkDevicesReceived + }); + } + + const rflinkDevicesMap = new Map(); + rflinkDevices.forEach(device => rflinkDevicesMap.set(device.external_id, device)); + store.setState({ + rflinkDevices, + rflinkDevicesMap, + getRflinkDevicesStatus: RequestStatus.Success + }); + } catch (e) { + store.setState({ + getRflinkDevicesStatus: RequestStatus.Error + }); + } + }, + async getRflinkNewDevices(state) { + store.setState({ + getRflinkNewDevicesStatus: RequestStatus.Getting + }); + try { + const rflinkNewDevices = await state.httpClient.get('/api/v1/service/rflink/newDevices'); + let rflinkNewDevicesFiltered = rflinkNewDevices.filter(device => { + if (!state.rflinkDevicesMap) { + return true; + } + return !state.rflinkDevicesMap.has(device.external_id); + }); + + if (state.rflinkDevices !== undefined && rflinkNewDevicesFiltered !== undefined) { + rflinkNewDevicesFiltered = rflinkNewDevicesFiltered.filter(newDevice => { + return state.rflinkDevices.indexOf(device => device.external_id === newDevice.external_id) < 0; + }); + } + + store.setState({ + rflinkNewDevices: rflinkNewDevicesFiltered, + getRflinkNewDevicesStatus: RequestStatus.Success + }); + } catch (e) { + store.setState({ + getRflinkNewDevicesStatus: RequestStatus.Error + }); + } + }, + async createDevice(state, device) { + store.setState({ + getRflinkCreateDeviceStatus: RequestStatus.Getting + }); + try { + await state.httpClient.post('/api/v1/device', device); + store.setState({ + getRflinkCreateDeviceStatus: RequestStatus.Success + }); + await state.httpClient.post('/api/v1/service/rflink/remove/', { + external_id: device.selector + }); + actions.getRflinkNewDevices(store.getState()); + actions.getRflinkDevices(store.getState(), 20, 0); + } catch (e) { + store.setState({ + getRflinkCreateDeviceStatus: RequestStatus.Error + }); + } + }, + + async saveDevice(state, device) { + await state.httpClient.post('/api/v1/device', device); + }, + updateDeviceProperty(state, index, property, value) { + const newState = update(state, { + rflinkDevices: { + [index]: { + [property]: { + $set: value + } + } + } + }); + store.setState(newState); + }, + async deleteDevice(state, device, index) { + await state.httpClient.delete(`/api/v1/device/${device.selector}`); + const newState = update(state, { + rflinkDevices: { + $splice: [[index, 1]] + } + }); + store.setState(newState); + }, + async search(state, e) { + store.setState({ + rflinkDeviceSearch: e.target.value + }); + await actions.getRflinkDevices(store.getState(), 20, 0); + }, + async changeOrderDir(state, e) { + store.setState({ + getRflinkDeviceOrderDir: e.target.value + }); + await actions.getRflinkDevices(store.getState(), 20, 0); + } + }; + actions.debouncedSearch = debounce(actions.search, 200); + return Object.assign({}, houseActions, integrationActions, actions); +} + +export default createActions; diff --git a/front/src/routes/integration/all/rflink/device-page/index.js b/front/src/routes/integration/all/rflink/device-page/index.js new file mode 100644 index 0000000000..8d717c4ad1 --- /dev/null +++ b/front/src/routes/integration/all/rflink/device-page/index.js @@ -0,0 +1,33 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import actions from './actions'; +import RflinkPage from '../RflinkPage'; +import DevicePage from './DevicePage'; +import FoundDevices from './FoundDevices'; +import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants'; + +class RflinkDevicePage extends Component { + componentWillMount() { + this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.RFLINK.NEW_DEVICE, () => { + this.props.getRflinkNewDevices(); + }); + this.props.getRflinkDevices(20, 0); + this.props.getRflinkNewDevices(); + this.props.getHouses(); + this.props.getIntegrationByName('rflink'); + } + + render(props, {}) { + return ( + + + + + ); + } +} + +export default connect( + 'session,user,rflinkDevices,houses,getRflinkDevicesStatus,currentIntegration,rflinkNewDevices,getRflinkCreateDeviceStatus,getRflinkNewDevicesStatus', + actions +)(RflinkDevicePage); diff --git a/front/src/routes/integration/all/rflink/device-page/setup/Feature.jsx b/front/src/routes/integration/all/rflink/device-page/setup/Feature.jsx new file mode 100644 index 0000000000..e5636ad007 --- /dev/null +++ b/front/src/routes/integration/all/rflink/device-page/setup/Feature.jsx @@ -0,0 +1,227 @@ +import { Text, Localizer } from 'preact-i18n'; +import { Component } from 'preact'; +import { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_UNITS } from '../../../../../../../../server/utils/constants'; +import { DeviceFeatureCategoriesIcon } from '../../../../../../utils/consts'; +import get from 'get-value'; + +const RflinkFeatureBox = ({ children, ...props }) => { + return ( + + + + + + + + + + + + + } + /> + + + + + + + + + + } + /> + + + + + {(props.feature.read_only === false || props.feature.read_only === undefined) && + props.feature.category !== DEVICE_FEATURE_CATEGORIES.LIGHT && ( // Switch + + + + + + } + /> + + + )} + + {props.feature.category === DEVICE_FEATURE_CATEGORIES.TEMPERATURE_SENSOR && ( + + + + + + + + + + + + + + + + + + + )} + + + + + + + + } + /> + + + + + + + + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +class RflinkFeatureBoxComponent extends Component { + updateName = e => { + this.props.updateFeatureProperty(e, 'name', this.props.featureIndex); + }; + updateExternalId = e => { + this.props.updateFeatureProperty(e, 'external_id', this.props.featureIndex); + }; + updateMin = e => { + this.props.updateFeatureProperty(e, 'min', this.props.featureIndex); + }; + updateMax = e => { + this.props.updateFeatureProperty(e, 'max', this.props.featureIndex); + }; + updateReadOnly = () => { + const e = { + target: { + value: !this.props.feature.read_only + } + }; + this.props.updateFeatureProperty(e, 'read_only', this.props.featureIndex); + }; + updateUnit = e => { + this.props.updateFeatureProperty(e, 'unit', this.props.featureIndex); + }; + updateSwitchId = e => { + this.props.feature.switchId = e.target.value; + let external = { + target: { + value: '' + } + }; + external.target.value = `rflink:${e.target.value}:switch:${this.props.feature.switchNumber}`; + this.updateExternalId(external); + }; + updateSwitchNumber = e => { + this.props.feature.switchNumber = e.target.value; + let external = { + target: { + value: '' + } + }; + external.target.value = `rflink:${this.props.feature.switchId}:switch:${e.target.value}`; + this.updateExternalId(external); + }; + + deleteFeature = () => { + this.props.deleteFeature(this.props.featureIndex); + }; + render() { + return ( + + ); + } +} + +export default RflinkFeatureBoxComponent; diff --git a/front/src/routes/integration/all/rflink/device-page/setup/FeatureTab.jsx b/front/src/routes/integration/all/rflink/device-page/setup/FeatureTab.jsx new file mode 100644 index 0000000000..b68452b210 --- /dev/null +++ b/front/src/routes/integration/all/rflink/device-page/setup/FeatureTab.jsx @@ -0,0 +1,104 @@ +import { Text, MarkupText } from 'preact-i18n'; +import { Link } from 'preact-router/match'; + +import Feature from './Feature'; +import Select from 'react-select'; +import { RequestStatus } from '../../../../../../utils/consts'; + +import RflinkDeviceForm from '../DeviceForm'; +import cx from 'classnames'; + +const FeatureTab = ({ children, ...props }) => ( + + + + + + + + + {(props.device && props.device.name) || } + + + + + + + + + + {props.saveStatus === RequestStatus.Error && ( + + + + )} + {props.saveStatus === RequestStatus.ConflictError && ( + + + + )} + {!props.loading && !props.device && ( + + + + + + + + + + + )} + {props.device && ( + + + + + + + + + + + + + + + + {props.device && + props.device.features.map((feature, index) => ( + + ))} + + + + + + + + + + + + + + )} + + + + +); + +export default FeatureTab; diff --git a/front/src/routes/integration/all/rflink/device-page/setup/index.js b/front/src/routes/integration/all/rflink/device-page/setup/index.js new file mode 100644 index 0000000000..97eec09740 --- /dev/null +++ b/front/src/routes/integration/all/rflink/device-page/setup/index.js @@ -0,0 +1,270 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import actions from '../actions'; +import FeatureTab from './FeatureTab'; +import RflinkPage from '../../RflinkPage'; +import uuid from 'uuid'; +import get from 'get-value'; +import update from 'immutability-helper'; +import { RequestStatus } from '../../../../../../utils/consts'; +import withIntlAsProp from '../../../../../../utils/withIntlAsProp'; +import { DEVICE_FEATURE_CATEGORIES } from '../../../../../../../../server/utils/constants'; + +class RflinkDeviceSetupPage extends Component { + selectFeature(selectedFeatureOption) { + if (selectedFeatureOption && selectedFeatureOption.value) { + this.setState({ + selectedFeature: selectedFeatureOption.value, + selectedFeatureOption + }); + } else { + this.setState({ + selectedFeature: null, + selectedFeatureOption: null + }); + } + } + + addFeature() { + const featureData = this.state.selectedFeature.split('|'); + + const device = update(this.state.device, { + features: { + $push: [ + { + id: uuid.v4(), + category: featureData[0], + external_id: 'rflink:', + type: featureData[1], + read_only: false, + has_feedback: false, + keep_history: true + } + ] + } + }); + + this.setState({ + device, + selectedFeature: undefined + }); + } + + deleteFeature(featureIndex) { + const device = update(this.state.device, { + features: { + $splice: [[featureIndex, 1]] + } + }); + + this.setState({ + device + }); + } + + updateDeviceProperty(deviceIndex, property, value) { + let device; + if (property === 'external_id' && !value.startsWith('rflink:')) { + if (value.length < 5) { + value = 'rflink:'; + } else { + value = `rflink:${value}`; + } + } + + device = update(this.state.device, { + [property]: { + $set: value + } + }); + + if (property === 'external_id') { + device = update(device, { + selector: { + $set: value + } + }); + } + + this.setState({ + device + }); + } + + updateFeatureProperty(e, property, featureIndex) { + let value = e.target.value; + let device; + if (property === 'external_id' && !value.startsWith('rflink:')) { + if (value.length < 5) { + value = 'rflink:'; + } else { + value = `rflink:${value}`; + } + } + + device = update(this.state.device, { + features: { + [featureIndex]: { + [property]: { + $set: value + } + } + } + }); + + if (property === 'external_id') { + device = update(device, { + features: { + [featureIndex]: { + selector: { + $set: value + } + } + } + }); + } + + this.setState({ + device + }); + } + + async saveDevice() { + this.setState({ + loading: true + }); + try { + const id = this.state.device.features[0].external_id.split(':'); + this.state.device.external_id = `rflink:${id[1]}:${id[3]}`; + const device = await this.props.httpClient.post('/api/v1/device', this.state.device); + this.setState({ + saveStatus: RequestStatus.Success, + loading: false, + device + }); + } catch (e) { + const status = get(e, 'response.status'); + if (status === 409) { + this.setState({ + saveStatus: RequestStatus.ConflictError, + loading: false + }); + } else { + this.setState({ + saveStatus: RequestStatus.Error, + loading: false + }); + } + } + } + + getDeviceFeaturesOptions = () => { + const deviceFeaturesOptions = []; + Object.keys(DEVICE_FEATURE_CATEGORIES).forEach(category => { + const categoryValue = DEVICE_FEATURE_CATEGORIES[category]; + if (get(this.props.intl.dictionary, `deviceFeatureCategory.${categoryValue}`)) { + const categoryFeatureTypeOptions = []; + + const types = Object.keys(get(this.props.intl.dictionary, `deviceFeatureCategory.${categoryValue}`)); + + types.forEach(type => { + const typeValue = type; + if ( + get(this.props.intl.dictionary, `deviceFeatureCategory.${categoryValue}.${typeValue}`) && + typeValue !== 'shortCategoryName' + ) { + categoryFeatureTypeOptions.push({ + value: `${categoryValue}|${typeValue}`, + label: get(this.props.intl.dictionary, `deviceFeatureCategory.${categoryValue}.${typeValue}`) + }); + } + }); + deviceFeaturesOptions.push({ + label: get(this.props.intl.dictionary, `deviceFeatureCategory.${categoryValue}.shortCategoryName`), + options: categoryFeatureTypeOptions + }); + } + }); + this.setState({ deviceFeaturesOptions }); + }; + + constructor(props) { + super(props); + + this.state = { + loading: true + }; + this.selectFeature = this.selectFeature.bind(this); + this.addFeature = this.addFeature.bind(this); + this.deleteFeature = this.deleteFeature.bind(this); + this.updateDeviceProperty = this.updateDeviceProperty.bind(this); + this.updateFeatureProperty = this.updateFeatureProperty.bind(this); + this.saveDevice = this.saveDevice.bind(this); + } + + async componentWillMount() { + this.props.getHouses(); + this.getDeviceFeaturesOptions(); + await this.props.getIntegrationByName('rflink'); + + let { deviceSelector } = this.props; + let device; + + if (!deviceSelector) { + const uniqueId = uuid.v4(); + device = { + id: uniqueId, + name: null, + should_poll: false, + external_id: 'rflink:', + service_id: this.props.currentIntegration.id, + features: [] + }; + } else { + const loadedDevice = await this.props.httpClient.get(`/api/v1/device/${deviceSelector}`); + + if ( + loadedDevice && + this.props.currentIntegration && + loadedDevice.service_id === this.props.currentIntegration.id + ) { + device = loadedDevice; + } + } + + for (let feature in device.features) { + if (feature !== undefined) { + if (device.external_id.split(':')[1] !== 'milight') { + device.features[`${feature}`].switchId = device.features[`${feature}`].external_id.split(':')[1]; + device.features[`${feature}`].switchNumber = device.features[`${feature}`].external_id.split(':')[3]; + } + } + } + this.setState({ + device, + loading: false + }); + } + + render(props, state) { + return ( + + + + ); + } +} + +export default withIntlAsProp( + connect('session,user,httpClient,houses,currentIntegration', actions)(RflinkDeviceSetupPage) +); diff --git a/front/src/routes/integration/all/rflink/device-page/style.css b/front/src/routes/integration/all/rflink/device-page/style.css new file mode 100644 index 0000000000..1b4343b7c4 --- /dev/null +++ b/front/src/routes/integration/all/rflink/device-page/style.css @@ -0,0 +1,3 @@ +.emptyDiv { + min-height: 200px; +} diff --git a/front/src/routes/integration/all/rflink/edit-page/index.js b/front/src/routes/integration/all/rflink/edit-page/index.js new file mode 100644 index 0000000000..5d02d62079 --- /dev/null +++ b/front/src/routes/integration/all/rflink/edit-page/index.js @@ -0,0 +1,19 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +// import actions from '../actions'; +import RflinkPage from '../RflinkPage'; +import UpdateDevice from '../../../../../components/device'; + +const RFLINK_PAGE_PATH = '/dashboard/integration/device/rflink'; + +class EditRflinkDevice extends Component { + render(props, {}) { + return ( + + + + ); + } +} + +export default connect('user,session,httpClient,currentIntegration,houses', {})(EditRflinkDevice); diff --git a/front/src/routes/integration/all/rflink/settings-page/SettingsTab.jsx b/front/src/routes/integration/all/rflink/settings-page/SettingsTab.jsx new file mode 100644 index 0000000000..c8420b83c1 --- /dev/null +++ b/front/src/routes/integration/all/rflink/settings-page/SettingsTab.jsx @@ -0,0 +1,147 @@ +import { Text } from 'preact-i18n'; +import get from 'get-value'; +import cx from 'classnames'; + +const ZONES = [1, 2, 3, 4]; + +const SettingsTab = ({ children, ...props }) => ( + + + + + + + + + + + + + + + + + {get(props, 'rflinkStatus.connected') && ( + + + + )} + {!get(props, 'rflinkStatus.connected') && ( + + + + )} + {props.rflinkConnectionInProgress && ( + + + + )} + {props.rflinkFailed && ( + + + + )} + + + + + + + + + + + + {props.usbPorts && + props.usbPorts.map( + usbPort => + usbPort.comPath && ( + + {usbPort.comPath} + {usbPort.comName ? ` - ${usbPort.comName}` : ''} + {usbPort.comVID ? ` - ${usbPort.comVID}` : ''} + + ) + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {ZONES.map(zone => ( + {zone} + ))} + + + + + + + + + + + + + + + + + + + + + + + + {`> ${get(props, 'rflinkStatus.lastCommand')} \n`} + + + + + + + + + + + + +); + +export default SettingsTab; diff --git a/front/src/routes/integration/all/rflink/settings-page/actions.js b/front/src/routes/integration/all/rflink/settings-page/actions.js new file mode 100644 index 0000000000..2dcb41b916 --- /dev/null +++ b/front/src/routes/integration/all/rflink/settings-page/actions.js @@ -0,0 +1,154 @@ +import { RequestStatus } from '../../../../../utils/consts'; + +const actions = store => { + const actions = { + async getUsbPorts(state) { + store.setState({ + getRflinkUsbPortStatus: RequestStatus.Getting + }); + try { + const usbPorts = await state.httpClient.get('/api/v1/service/usb/port'); + store.setState({ + usbPorts, + getRflinkUsbPortStatus: RequestStatus.Success + }); + } catch (e) { + store.setState({ + getRflinkUsbPortStatus: RequestStatus.Error + }); + } + }, + async getCurrentRflinkPath(state) { + store.setState({ + getCurrentRflinkPathStatus: RequestStatus.Getting + }); + try { + const RflinkPath = await state.httpClient.get('/api/v1/service/rflink/variable/RFLINK_PATH'); + store.setState({ + RflinkPath: RflinkPath.value, + getCurrentRflinkPathStatus: RequestStatus.Success + }); + } catch (e) { + store.setState({ + getCurrentRflinkPathStatus: RequestStatus.Error + }); + } + }, + updateRflinkPath(state, e) { + store.setState({ + RflinkPath: e.target.value + }); + }, + updateDebugCommand(state, e) { + store.setState({ commandToSend: e.target.value }); + }, + updateMilight(state, e) { + store.setState({ + currentMilightGateway: e.target.value + }); + }, + updateZone(state, e) { + store.setState({ + currentMilightZone: e.target.value + }); + }, + async saveDriverPathAndConnect(state) { + store.setState({ + connectRflinkStatus: RequestStatus.Getting, + rflinkFailed: false + }); + try { + await state.httpClient.post('/api/v1/service/rflink/variable/RFLINK_PATH', { + value: state.RflinkPath + }); + await state.httpClient.post('/api/v1/service/rflink/connect'); + store.setState({ + connectRflinkStatus: RequestStatus.Success, + rflinkConnectionInProgress: true + }); + } catch (e) { + store.setState({ + connectRflinkStatus: RequestStatus.Error + }); + } + }, + async disconnect(state) { + store.setState({ + rflinkDisconnectStatus: RequestStatus.Getting + }); + try { + await state.httpClient.post('/api/v1/service/rflink/disconnect'); + await actions.getStatus(store.getState()); + store.setState({ + rflinkDisconnectStatus: RequestStatus.Success + }); + } catch (e) { + store.setState({ + rflinkDisconnectStatus: RequestStatus.Error + }); + } + }, + async pair(state) { + try { + await state.httpClient.post('/api/v1/service/rflink/variable/CURRENT_MILIGHT_GATEWAY', { + value: state.currentMilightGateway + }); + await state.httpClient.post('/api/v1/service/rflink/pair', { + zone: state.currentMilightZone + }); + } catch (e) {} + }, + async unpair(state) { + try { + await state.httpClient.post('/api/v1/service/rflink/variable/CURRENT_MILIGHT_GATEWAY', { + value: state.currentMilightGateway + }); + await state.httpClient.post('/api/v1/service/rflink/unpair', { + zone: state.currentMilightZone + }); + } catch (e) {} + }, + async sendDebug(state) { + try { + await state.httpClient.post('/api/v1/service/rflink/debug', { + value: state.commandToSend + }); + } catch (e) {} + }, + async getStatus(state) { + store.setState({ + rflinkGetStatusStatus: RequestStatus.Getting + }); + try { + const rflinkStatus = await state.httpClient.get('/api/v1/service/rflink/status'); + let currentMilightGateway = rflinkStatus.currentMilightGateway; + if (currentMilightGateway === undefined || currentMilightGateway === null) { + currentMilightGateway = 'error'; + } + + store.setState({ + rflinkStatus, + currentMilightGateway, + rflinkConnectionInProgress: false, + rflinkGetStatusStatus: RequestStatus.Success + }); + return rflinkStatus; + } catch (e) { + store.setState({ + rflinkGetStatusStatus: RequestStatus.Error, + rflinkConnectionInProgress: false + }); + } + }, + driverFailed() { + store.setState({ + rflinkFailed: true, + rflinkConnectionInProgress: false + }); + } + }; + + return actions; +}; + +export default actions; diff --git a/front/src/routes/integration/all/rflink/settings-page/index.js b/front/src/routes/integration/all/rflink/settings-page/index.js new file mode 100644 index 0000000000..fff92eeccb --- /dev/null +++ b/front/src/routes/integration/all/rflink/settings-page/index.js @@ -0,0 +1,50 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import actions from './actions'; +import RflinkPage from '../RflinkPage'; +import SettingsTab from './SettingsTab'; +import { RequestStatus } from '../../../../../utils/consts'; +import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants'; + +class RflinkSettingsPage extends Component { + rflinkReadyListener = () => this.props.getStatus(); + rflinkFailedListener = () => this.props.rflinkFailed(); + rflinkGetStatus = () => this.props.getStatus(); + + componentWillMount() { + this.props.getUsbPorts(); + this.props.getStatus(); + this.props.getCurrentRflinkPath(); + this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.RFLINK.DRIVER_READY, this.rflinkReadyListener); + this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.RFLINK.DRIVER_FAILED, this.rflinkFailedListener); + this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.RFLINK.NEW_MESSAGE, this.rflinkGetStatus); + } + + componentWillUnmount() { + this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.RFLINK.DRIVER_READY, this.rflinkReadyListener); + this.props.session.dispatcher.removeListener( + WEBSOCKET_MESSAGE_TYPES.RFLINK.DRIVER_FAILED, + this.rflinkFailedListener + ); + } + + render(props, {}) { + const loading = + props.getRflinkUsbPortStatus === RequestStatus.Getting || + props.getCurrentRflinkPathStatus === RequestStatus.Getting || + props.rflinkGetStatusStatus === RequestStatus.Getting || + props.rflinkDisconnectStatus === RequestStatus.Getting || + props.connectRflinkStatus === RequestStatus.Getting; + + return ( + + + + ); + } +} + +export default connect( + 'user,session,usbPorts,RflinkPath,rflinkStatus,getRflinkUsbPortStatus,getCurrentRflinkPathStatus,rflinkGetStatusStatus,rflinkFailed,rflinkDisconnectStatus,connectRflinkStatus,RflinkConnectionInProgress,currentMilightGateway,currentMilightZone', + actions +)(RflinkSettingsPage); diff --git a/insomnia.json b/insomnia.json index 270d61e5e0..0676897cb5 100644 --- a/insomnia.json +++ b/insomnia.json @@ -1 +1 @@ -{"_type":"export","__export_format":4,"__export_date":"2019-05-09T04:52:33.359Z","__export_source":"insomnia.desktop.app:v6.4.1","resources":[{"_id":"req_0298023736434d0e90e6a6a2e97fd5fc","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Home\",\n\t\"latitude\": 48,\n\t\"longitude\": 12,\n\t\"radius\": 100,\n\t\"color\": \"#5042f4\"\n}"},"created":1557371111811,"description":"","headers":[{"id":"pair_5ef51180a9574e98816d513d3fe22c2a","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1557371111811,"method":"POST","modified":1557377493512,"name":"create","parameters":[],"parentId":"fld_79924a2637d94faab604cf43ea617458","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/area","_type":"request"},{"_id":"fld_79924a2637d94faab604cf43ea617458","created":1557371046130,"description":"","environment":{},"metaSortKey":-1557371046130,"modified":1557371046130,"name":"Area","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"wrk_8105dd03993841b69b41389db181688d","created":1557370941801,"description":"","modified":1557370941801,"name":"Gladys Assistant","parentId":null,"_type":"workspace"},{"_id":"req_a803992e55c845e59111fcdde8f0d778","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Home\",\n\t\"latitude\": 48,\n\t\"longitude\": 12,\n\t\"radius\": 100,\n\t\"color\": \"#5042f4\"\n}"},"created":1557377517345,"description":"","headers":[{"id":"pair_5ef51180a9574e98816d513d3fe22c2a","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1557331566843,"method":"PATCH","modified":1557377525032,"name":"update","parameters":[],"parentId":"fld_79924a2637d94faab604cf43ea617458","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/area/home","_type":"request"},{"_id":"req_0d0161528b7746d5bc47e49e96da2ad9","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Home\",\n\t\"latitude\": 48,\n\t\"longitude\": 12,\n\t\"radius\": 100,\n\t\"color\": \"#5042f4\"\n}"},"created":1557377531490,"description":"","headers":[{"id":"pair_5ef51180a9574e98816d513d3fe22c2a","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1557311794359,"method":"DELETE","modified":1557377535003,"name":"delete","parameters":[],"parentId":"fld_79924a2637d94faab604cf43ea617458","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/area/home","_type":"request"},{"_id":"req_ce9f267557a04afca85c4ca00308952a","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557377489344,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557292021875,"method":"GET","modified":1557377509968,"name":"get","parameters":[],"parentId":"fld_79924a2637d94faab604cf43ea617458","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/area","_type":"request"},{"_id":"req_477c7de7bd7343d3ae91945fe9fe5a81","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My calendar\",\n\t\"description\": \"My personal events\"\n}"},"created":1557371612581,"description":"","headers":[{"id":"pair_6c05ee0f41fc447fbd4fc1f814573dff","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1557371612581,"method":"POST","modified":1557371905446,"name":"create","parameters":[],"parentId":"fld_7b562e7c40844c5b9789a89690e49096","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/calendar","_type":"request"},{"_id":"fld_7b562e7c40844c5b9789a89690e49096","created":1557371598378,"description":"","environment":{},"metaSortKey":-1554749026738,"modified":1557371605718,"name":"Calendar","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"req_c8f67444984c478fa4803133d696eacc","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My calendar\",\n\t\"description\": \"My personal events\"\n}"},"created":1557371937153,"description":"","headers":[{"id":"pair_6c05ee0f41fc447fbd4fc1f814573dff","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1557371487388.5,"method":"PATCH","modified":1557371960411,"name":"update","parameters":[],"parentId":"fld_7b562e7c40844c5b9789a89690e49096","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/calendar/my-calendar","_type":"request"},{"_id":"req_15af0f4978714425bd8a388337fd0b77","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557372765910,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557371424792.25,"method":"DELETE","modified":1557372786672,"name":"delete","parameters":[],"parentId":"fld_7b562e7c40844c5b9789a89690e49096","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/calendar/my-calendar","_type":"request"},{"_id":"req_0d9e72d6038e4faca6052ee92ae8b156","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557371786040,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557371362196,"method":"GET","modified":1557371899466,"name":"get","parameters":[],"parentId":"fld_7b562e7c40844c5b9789a89690e49096","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/calendar","_type":"request"},{"_id":"req_8d0b71a861ef430685e6e0c2078ed60a","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557372078182,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557371237003.5,"method":"GET","modified":1557372091592,"name":"get events","parameters":[],"parentId":"fld_7b562e7c40844c5b9789a89690e49096","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/calendar/event","_type":"request"},{"_id":"req_c5eb08dd4fa140bdbfa8f2cfb5ee3dae","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Code on Gladys\",\n\t\"start\": \"2019-02-12 07:00:00\",\n\t\"end\": \"2019-02-12 17:00:00\"\n}"},"created":1557372105960,"description":"","headers":[{"id":"pair_59cb69f462e549b3a9f58df46975b0f8","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1557371174407.25,"method":"POST","modified":1557372163877,"name":"create event","parameters":[],"parentId":"fld_7b562e7c40844c5b9789a89690e49096","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/calendar/my-calendar/event","_type":"request"},{"_id":"req_fdd71d4b879944eba8d96955ed127b73","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Code on Gladys\",\n\t\"start\": \"2019-02-12 07:00:00\",\n\t\"end\": \"2019-02-12 17:00:00\"\n}"},"created":1557372525224,"description":"","headers":[{"id":"pair_59cb69f462e549b3a9f58df46975b0f8","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1557371143109.125,"method":"PATCH","modified":1557372551534,"name":"update event","parameters":[],"parentId":"fld_7b562e7c40844c5b9789a89690e49096","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/calendar/event/code-on-gladys","_type":"request"},{"_id":"req_bf40c45243f54bf0803e52c61b0a133d","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557372774937,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557371127460.0625,"method":"DELETE","modified":1557372783035,"name":"delete event","parameters":[],"parentId":"fld_7b562e7c40844c5b9789a89690e49096","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/calendar/event/code-on-gladys","_type":"request"},{"_id":"req_608067f06b5e48dcafe37adf96edadc9","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557370952846,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557204955912,"method":"GET","modified":1557373021826,"name":"get image","parameters":[],"parentId":"fld_f149a3bbc709488ea0d81ee283ff1054","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/camera/test-camera/image","_type":"request"},{"_id":"fld_f149a3bbc709488ea0d81ee283ff1054","created":1557373002046,"description":"","environment":{},"metaSortKey":-1552127007346,"modified":1557373007331,"name":"Camera","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"req_c98eb86773ff459ba895735b34a9fc2c","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"image\": \"image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==\"\n}"},"created":1557373029848,"description":"","headers":[{"id":"pair_414f07fd9d7143e28beceac1a548f47e","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1556697413348.5,"method":"POST","modified":1557373050121,"name":"set image","parameters":[],"parentId":"fld_f149a3bbc709488ea0d81ee283ff1054","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/camera/test-camera/image","_type":"request"},{"_id":"req_cae73f1a00f442829fc81e6b227122d1","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My house\"\n}"},"created":1557373387048,"description":"","headers":[{"id":"pair_daad23fc9bb3420181d476f872aeceda","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1557373387048,"method":"POST","modified":1557373432792,"name":"create","parameters":[],"parentId":"fld_421f1328af7749d3ad73368dd9ac9e5a","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/house","_type":"request"},{"_id":"fld_421f1328af7749d3ad73368dd9ac9e5a","created":1557373369941,"description":"","environment":{},"metaSortKey":-1551799254922,"modified":1557373373697,"name":"House","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"req_f2ab7e73ed584ccda213d75a4131e927","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My house\"\n}"},"created":1557374195989,"description":"","headers":[{"id":"pair_daad23fc9bb3420181d476f872aeceda","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1557372499814.5,"method":"PATCH","modified":1557374206050,"name":"update","parameters":[],"parentId":"fld_421f1328af7749d3ad73368dd9ac9e5a","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/house/my-house","_type":"request"},{"_id":"req_027598be745e45489fde370acb151176","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557374210204,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557372056197.75,"method":"GET","modified":1557374216979,"name":"get","parameters":[],"parentId":"fld_421f1328af7749d3ad73368dd9ac9e5a","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/house","_type":"request"},{"_id":"req_e99573a308d846e9bb06308b99ebc780","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557374254713,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557372056147.75,"method":"POST","modified":1557374276178,"name":"user seen","parameters":[],"parentId":"fld_421f1328af7749d3ad73368dd9ac9e5a","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/house/my-house/user/tony/seen","_type":"request"},{"_id":"req_527bfc103bc0409f90d3d015076f33c1","authentication":{"prefix":"","token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557370952819,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1553594253452,"method":"POST","modified":1557373081380,"name":"Turn On","parameters":[],"parentId":"fld_81db0a4d5e6c4deebfebb194db04d009","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/light/test/on","_type":"request"},{"_id":"fld_81db0a4d5e6c4deebfebb194db04d009","created":1557373062482,"description":"","environment":{},"metaSortKey":-1550815997650,"modified":1557373066236,"name":"Light","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"req_7ef7757f3c1749a2aee43a503039d2d6","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"latitude\": 48,\n\t\"longitude\": 12,\n\t\"altitude\": 0,\n\t\"accuracy\": 20\n}"},"created":1557370952860,"description":"","headers":[{"id":"pair_286222e752c74d2e89cf6dbbb3503311","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1549268375325.25,"method":"POST","modified":1557373340357,"name":"create","parameters":[],"parentId":"fld_4497c72efadc48ad9c0e2324c6fe5a97","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/user/tony/location","_type":"request"},{"_id":"fld_4497c72efadc48ad9c0e2324c6fe5a97","created":1557373318030,"description":"","environment":{},"metaSortKey":-1550160492802,"modified":1557373321815,"name":"Location","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"req_db4deb6054ee495a97203034041df940","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"firstname\" : \"tony\",\n\t\"lastname\": \"Stark\",\n\t\"email\": \"tony.stark@gladysassistant.com\",\n\t\"password\": \"testtesttest\",\n\t\"birthdate\": \"2011-02-04\",\n\t\"language\": \"fr\",\n\t\"role\": \"admin\"\n}"},"created":1557370952856,"description":"","headers":[{"id":"pair_286222e752c74d2e89cf6dbbb3503311","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1549268375275.25,"method":"GET","modified":1557373350036,"name":"get","parameters":[],"parentId":"fld_4497c72efadc48ad9c0e2324c6fe5a97","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/user/tony/location","_type":"request"},{"_id":"req_5cb68278a49849d89de5ed73314cae37","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"text\": \"Turn on the light in the kitchen\"\n}"},"created":1557370952811,"description":"","headers":[{"id":"pair_286222e752c74d2e89cf6dbbb3503311","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1549181763012.5,"method":"POST","modified":1557373160398,"name":"send","parameters":[],"parentId":"fld_fa9678df017946a2a24d5eaa57076c1e","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/message","_type":"request"},{"_id":"fld_fa9678df017946a2a24d5eaa57076c1e","created":1557373092397,"description":"","environment":{},"metaSortKey":-1549832740378,"modified":1557373377236,"name":"Message","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"req_435dc2755110497b9a4cd080fb4382fa","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557370952852,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557208943925.5,"method":"GET","modified":1557372925337,"name":"get","parameters":[],"parentId":"fld_8111c5da33b8467e807da33f5b02478b","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/room?expand=devices","_type":"request"},{"_id":"fld_8111c5da33b8467e807da33f5b02478b","created":1557371590759,"description":"","environment":{},"metaSortKey":-1549504987954,"modified":1557371603590,"name":"Room","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"req_269160123f1f4d2bad40dec1d48f2f24","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"my room\"\n}"},"created":1557374469807,"description":"","headers":[{"id":"pair_8d1634976aa34c008e7920729a217aa1","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1557206949918.75,"method":"POST","modified":1557374500301,"name":"create","parameters":[],"parentId":"fld_8111c5da33b8467e807da33f5b02478b","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/house/my-house/room","_type":"request"},{"_id":"req_4b2a83573a3a490e9d713ea18c8db2aa","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"my room\"\n}"},"created":1557374580829,"description":"","headers":[{"id":"pair_8d1634976aa34c008e7920729a217aa1","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1557205952915.375,"method":"PATCH","modified":1557374589339,"name":"update","parameters":[],"parentId":"fld_8111c5da33b8467e807da33f5b02478b","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/room/my-room","_type":"request"},{"_id":"req_7f81e74e652d4b3589b1997660baf554","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557374592719,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557205454413.6875,"method":"GET","modified":1557374663403,"name":"get by selector","parameters":[],"parentId":"fld_8111c5da33b8467e807da33f5b02478b","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/room/my-room?expand=temperature","_type":"request"},{"_id":"req_1843656d85d54425b76f4a7c8e405c64","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"firstname\" : \"tony\",\n\t\"lastname\": \"Stark\",\n\t\"email\": \"tony.stark@gladysassistant.com\",\n\t\"password\": \"testtesttest\",\n\t\"birthdate\": \"2011-02-04\",\n\t\"language\": \"fr\",\n\t\"role\": \"admin\"\n}"},"created":1557370952801,"description":"","headers":[{"id":"pair_286222e752c74d2e89cf6dbbb3503311","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1549504988054,"method":"POST","modified":1557371442374,"name":"create","parameters":[],"parentId":"fld_2f1305d16cb64f4f94193ded63d30613","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/user","_type":"request"},{"_id":"fld_2f1305d16cb64f4f94193ded63d30613","created":1557371323514,"description":"","environment":{},"metaSortKey":-1548193978258,"modified":1557372791221,"name":"User","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"req_cf2228a5f9e048bcb77e822723a6af48","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557370952808,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1549504988004,"method":"GET","modified":1557377430891,"name":"get myself","parameters":[],"parentId":"fld_2f1305d16cb64f4f94193ded63d30613","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/me","_type":"request"},{"_id":"req_368fce1398cd4b90aec0f4a0104f7609","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557370952822,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1549504987991.5,"method":"GET","modified":1557377441003,"name":"get my picture","parameters":[],"parentId":"fld_2f1305d16cb64f4f94193ded63d30613","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/me/picture","_type":"request"},{"_id":"req_af2cc6c0597f4d29b91758e3660ffa51","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"email\": \"tony.stark@gladysassistant.com\",\n\t\"password\": \"testtesttest\"\n}"},"created":1557370952805,"description":"","headers":[{"id":"pair_286222e752c74d2e89cf6dbbb3503311","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1549504987985.25,"method":"POST","modified":1557371450697,"name":"login","parameters":[],"parentId":"fld_2f1305d16cb64f4f94193ded63d30613","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/login","_type":"request"},{"_id":"req_218a9373f70c4285a91b78a84a39235b","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557370952826,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1549504987979,"method":"GET","modified":1557371459684,"name":"get list of users","parameters":[],"parentId":"fld_2f1305d16cb64f4f94193ded63d30613","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/user","_type":"request"},{"_id":"req_2c00c9df1cb6432ea35d70087229169d","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"value\": \"my-password\"\n}"},"created":1557370952850,"description":"","headers":[{"id":"pair_be58b234961b41868b2d1d62775aab2c","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1547169751933,"method":"POST","modified":1557372902322,"name":"create","parameters":[],"parentId":"fld_596820825aa5472cbc65bc7af7a03929","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/service/mqtt/variable/MQTT_PASSWORD","_type":"request"},{"_id":"fld_596820825aa5472cbc65bc7af7a03929","created":1557372876492,"description":"","environment":{},"metaSortKey":-1547210720986,"modified":1557372878486,"name":"Variable","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"req_eac1fd654a664013bc0e6f3b4fbca475","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557375282743,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557373291366,"method":"POST","modified":1557375652775,"name":"start","parameters":[],"parentId":"fld_badf90fd71cb4f5590230c1dc3ff5486","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/service/mqtt/start","_type":"request"},{"_id":"fld_badf90fd71cb4f5590230c1dc3ff5486","created":1557372952197,"description":"","environment":{},"metaSortKey":-1547128782880,"modified":1557372955921,"name":"Services","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"req_6957db57187a490f8cc78d6737f65095","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557375655910,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557373291353.5,"method":"POST","modified":1557375663476,"name":"stop","parameters":[],"parentId":"fld_badf90fd71cb4f5590230c1dc3ff5486","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/service/mqtt/stop","_type":"request"},{"_id":"req_41f0209ce1d742e1a05c37563b8146dd","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557370952814,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1552889735892,"method":"GET","modified":1557373306456,"name":"get bridges","parameters":[],"parentId":"fld_589e853af00643029ff0d45e123b554c","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/service/philips-hue/bridge","_type":"request"},{"_id":"fld_589e853af00643029ff0d45e123b554c","created":1557373291316,"description":"","environment":{},"metaSortKey":-1557373291316,"modified":1557373291316,"name":"Philips Hue","parentId":"fld_badf90fd71cb4f5590230c1dc3ff5486","_type":"request_group"},{"_id":"req_8db74ea880a74470b6b98a4ce906f207","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557370952833,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1547087813927,"method":"GET","modified":1557372991733,"name":"list ports","parameters":[],"parentId":"fld_6bf90917285a4ad8bb65f0dd17f26cc5","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/service/usb/port","_type":"request"},{"_id":"fld_6bf90917285a4ad8bb65f0dd17f26cc5","created":1557372943183,"description":"","environment":{},"metaSortKey":-1547087813877,"modified":1557372965586,"name":"USB","parentId":"fld_badf90fd71cb4f5590230c1dc3ff5486","_type":"request_group"},{"_id":"req_bd8eea7482374299849e3ed1827b2de1","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557370952843,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1554027100848.5,"method":"GET","modified":1557372843969,"name":"get neighbors","parameters":[],"parentId":"fld_758adf17222c43e090c72d00c2a23aa0","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/service/zwave/neighbor","_type":"request"},{"_id":"fld_758adf17222c43e090c72d00c2a23aa0","created":1557372801630,"description":"","environment":{},"metaSortKey":-1547087813827,"modified":1557372963529,"name":"Z-Wave","parentId":"fld_badf90fd71cb4f5590230c1dc3ff5486","_type":"request_group"},{"_id":"req_27473d1f666940b78342700f3f667cc1","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557370952828,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1554027100823.5,"method":"GET","modified":1557372857520,"name":"get nodes","parameters":[],"parentId":"fld_758adf17222c43e090c72d00c2a23aa0","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/service/zwave/node","_type":"request"},{"_id":"req_a1bc4e07ee2843c38873201505787e7b","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557370952837,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1554027100798.5,"method":"GET","modified":1557372867449,"name":"get informations","parameters":[],"parentId":"fld_758adf17222c43e090c72d00c2a23aa0","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/service/zwave/info","_type":"request"},{"_id":"req_fefb794603514b57b476d32fc355fb30","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557375773569,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557375773569,"method":"GET","modified":1557375801016,"name":"get weather in house","parameters":[],"parentId":"fld_4e69545b79bb4c8e9884fc6a15977df2","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/house/test-house/weather","_type":"request"},{"_id":"fld_4e69545b79bb4c8e9884fc6a15977df2","created":1557375762859,"description":"","environment":{},"metaSortKey":-1547128782830,"modified":1557375765155,"name":"Weather","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"request_group"},{"_id":"req_438af9bfd77548ef93ad9a1e1fe675c5","authentication":{"token":"{{ bearer_token }}","type":"bearer"},"body":{},"created":1557376064235,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1557374580308.5,"method":"GET","modified":1557376075249,"name":"get weather user","parameters":[],"parentId":"fld_4e69545b79bb4c8e9884fc6a15977df2","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingMaxTimelineDataSize":1000,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/api/v1/user/tony/weather","_type":"request"},{"_id":"env_2a65cf3c43f04c422daba3c66b2f03d5e29e13cf","color":null,"created":1557370941828,"data":{},"isPrivate":false,"metaSortKey":1557370941828,"modified":1557371074712,"name":"Base Environment","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"environment"},{"_id":"jar_2a65cf3c43f04c422daba3c66b2f03d5e29e13cf","cookies":[],"created":1557370941832,"modified":1557370941832,"name":"Default Jar","parentId":"wrk_8105dd03993841b69b41389db181688d","_type":"cookie_jar"},{"_id":"env_0572a709ef8a4c2ab69074fe93f03a82","color":"#00c8c2","created":1557371062816,"data":{"base_url":"http://localhost:1443","bearer_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZTRlM2YwM2UtNjBiOS00ODVlLWJjMGEtYzU4MmI2OTA4OWJkIiwic2NvcGUiOlsiZGFzaGJvYXJkOndyaXRlIiwiZGFzaGJvYXJkOnJlYWQiXSwic2Vzc2lvbl9pZCI6Ijc1NzU2NjIzLTUwYWQtNDFhYy1iOWE0LWIzZWZhY2M4NTA1YyIsImlhdCI6MTU1NzM3MTM2NywiZXhwIjoxNTU3NDU3NzY3LCJhdWQiOiJ1c2VyIiwiaXNzIjoiZ2xhZHlzIn0.XDvO4TREbjk5k9t1bhTDqb1NYjKHk2MksfCP08FCGBU"},"isPrivate":false,"metaSortKey":1557371062816,"modified":1557371390105,"name":"Local","parentId":"env_2a65cf3c43f04c422daba3c66b2f03d5e29e13cf","_type":"environment"}]} \ No newline at end of file +{"_type":"export","__export_format":4,"__export_date":"2022-02-24T09:27:26.039Z","__export_source":"insomnia.desktop.app:v2021.7.2","resources":[{"_id":"req_9ac36a7b6502492687bc5e8fb5c1c69c","parentId":"fld_71775fa5489241a4a95e91e9317c1ea2","modified":1645694212243,"created":1645694212243,"url":"{{ base_url }}/api/v1/area","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Home\",\n\t\"latitude\": 48,\n\t\"longitude\": 12,\n\t\"radius\": 100,\n\t\"color\": \"#5042f4\"\n}"},"parameters":[],"headers":[{"id":"pair_5ef51180a9574e98816d513d3fe22c2a","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557371111811,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_71775fa5489241a4a95e91e9317c1ea2","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212239,"created":1645694212239,"name":"Area","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1557371046130,"_type":"request_group"},{"_id":"wrk_7abf468172864f308078eb4e967f403f","parentId":null,"modified":1645694212213,"created":1645694212213,"name":"Gladys Assistant","description":"","scope":"collection","_type":"workspace"},{"_id":"req_dee9d7c5d3544959ba77084555b68d4a","parentId":"fld_71775fa5489241a4a95e91e9317c1ea2","modified":1645694212251,"created":1645694212251,"url":"{{ base_url }}/api/v1/area/home","name":"update","description":"","method":"PATCH","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Home\",\n\t\"latitude\": 48,\n\t\"longitude\": 12,\n\t\"radius\": 100,\n\t\"color\": \"#5042f4\"\n}"},"parameters":[],"headers":[{"id":"pair_5ef51180a9574e98816d513d3fe22c2a","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557331566843,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_f98393a8e22943388937da77f99dfac8","parentId":"fld_71775fa5489241a4a95e91e9317c1ea2","modified":1645694212255,"created":1645694212255,"url":"{{ base_url }}/api/v1/area/home","name":"delete","description":"","method":"DELETE","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Home\",\n\t\"latitude\": 48,\n\t\"longitude\": 12,\n\t\"radius\": 100,\n\t\"color\": \"#5042f4\"\n}"},"parameters":[],"headers":[{"id":"pair_5ef51180a9574e98816d513d3fe22c2a","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557311794359,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_6b761f432d5f47419e7939b9b63bcd84","parentId":"fld_71775fa5489241a4a95e91e9317c1ea2","modified":1645694212247,"created":1645694212247,"url":"{{ base_url }}/api/v1/area","name":"get","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557292021875,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_8806b4d0541748ecb10a44b5a42b157c","parentId":"fld_9616401afabe4b1e92e9e257ca4c1701","modified":1645694212314,"created":1645694212314,"url":"{{ base_url }}/api/v1/calendar","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My calendar\",\n\t\"description\": \"My personal events\"\n}"},"parameters":[],"headers":[{"id":"pair_6c05ee0f41fc447fbd4fc1f814573dff","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557371612581,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_9616401afabe4b1e92e9e257ca4c1701","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212311,"created":1645694212311,"name":"Calendar","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1554749026738,"_type":"request_group"},{"_id":"req_0bd0340fa2c04cc38f99e627d5a7bef0","parentId":"fld_9616401afabe4b1e92e9e257ca4c1701","modified":1645694212324,"created":1645694212324,"url":"{{ base_url }}/api/v1/calendar/my-calendar","name":"update","description":"","method":"PATCH","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My calendar\",\n\t\"description\": \"My personal events\"\n}"},"parameters":[],"headers":[{"id":"pair_6c05ee0f41fc447fbd4fc1f814573dff","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557371487388.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_a7e60a7b45d641f3aaff28ff806a0158","parentId":"fld_9616401afabe4b1e92e9e257ca4c1701","modified":1645694212340,"created":1645694212340,"url":"{{ base_url }}/api/v1/calendar/my-calendar","name":"delete","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557371424792.25,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_fca24e364727493289bd4f2c31bc10e9","parentId":"fld_9616401afabe4b1e92e9e257ca4c1701","modified":1645694212318,"created":1645694212318,"url":"{{ base_url }}/api/v1/calendar","name":"get","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557371362196,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_bcc6c6b510584f64a24613e9cfe2b899","parentId":"fld_9616401afabe4b1e92e9e257ca4c1701","modified":1645694212329,"created":1645694212329,"url":"{{ base_url }}/api/v1/calendar/event","name":"get events","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557371237003.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_76b0a70b2e114514bd16a58575c9a2d6","parentId":"fld_9616401afabe4b1e92e9e257ca4c1701","modified":1645694212333,"created":1645694212333,"url":"{{ base_url }}/api/v1/calendar/my-calendar/event","name":"create event","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Code on Gladys\",\n\t\"start\": \"2019-02-12 07:00:00\",\n\t\"end\": \"2019-02-12 17:00:00\"\n}"},"parameters":[],"headers":[{"id":"pair_59cb69f462e549b3a9f58df46975b0f8","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557371174407.25,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_2707433e08cc4dd5b84515ef861613ac","parentId":"fld_9616401afabe4b1e92e9e257ca4c1701","modified":1645694212336,"created":1645694212336,"url":"{{ base_url }}/api/v1/calendar/event/code-on-gladys","name":"update event","description":"","method":"PATCH","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Code on Gladys\",\n\t\"start\": \"2019-02-12 07:00:00\",\n\t\"end\": \"2019-02-12 17:00:00\"\n}"},"parameters":[],"headers":[{"id":"pair_59cb69f462e549b3a9f58df46975b0f8","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557371143109.125,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_b760056c64b44afdb74bc9c5c1b27a02","parentId":"fld_9616401afabe4b1e92e9e257ca4c1701","modified":1645694212344,"created":1645694212344,"url":"{{ base_url }}/api/v1/calendar/event/code-on-gladys","name":"delete event","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557371127460.0625,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_28bcff4260b74fc6b08d0708abc88158","parentId":"fld_c6e96d500f3f4097a3d6847dd3dc9b7e","modified":1645694212402,"created":1645694212402,"url":"{{ base_url }}/api/v1/camera/test-camera/image","name":"get image","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557204955912,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_c6e96d500f3f4097a3d6847dd3dc9b7e","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212399,"created":1645694212399,"name":"Camera","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1552127007346,"_type":"request_group"},{"_id":"req_6f8c702d2a7942c5adbf8426c71e36af","parentId":"fld_c6e96d500f3f4097a3d6847dd3dc9b7e","modified":1645694212406,"created":1645694212406,"url":"{{ base_url }}/api/v1/camera/test-camera/image","name":"set image","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"image\": \"image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==\"\n}"},"parameters":[],"headers":[{"id":"pair_414f07fd9d7143e28beceac1a548f47e","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1556697413348.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_1f45c3a0970047f28debdc60cacf21e9","parentId":"fld_62b7ae0b2d264a329c9d0868c6543229","modified":1645694212442,"created":1645694212442,"url":"{{ base_url }}/api/v1/house","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My house\"\n}"},"parameters":[],"headers":[{"id":"pair_daad23fc9bb3420181d476f872aeceda","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557373387048,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_62b7ae0b2d264a329c9d0868c6543229","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212440,"created":1645694212440,"name":"House","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1551799254922,"_type":"request_group"},{"_id":"req_ba86c23bcdce414da75b26b805b7e35a","parentId":"fld_62b7ae0b2d264a329c9d0868c6543229","modified":1645694212446,"created":1645694212446,"url":"{{ base_url }}/api/v1/house/my-house","name":"update","description":"","method":"PATCH","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My house\"\n}"},"parameters":[],"headers":[{"id":"pair_daad23fc9bb3420181d476f872aeceda","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557372499814.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_eb6030001fe14ad782e60574093ff890","parentId":"fld_62b7ae0b2d264a329c9d0868c6543229","modified":1645694840048,"created":1645694212450,"url":"{{ base_url }}/api/v1/house","name":"get","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{% response 'body', 'req_af2cc6c0597f4d29b91758e3660ffa51', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always' %}","type":"bearer"},"metaSortKey":-1557372056197.75,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_172d9729f15e4911a6f2f7817da0599a","parentId":"fld_62b7ae0b2d264a329c9d0868c6543229","modified":1645694212453,"created":1645694212453,"url":"{{ base_url }}/api/v1/house/my-house/user/tony/seen","name":"user seen","description":"","method":"POST","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557372056147.75,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_30de7c048eac4765ae5beb4621e3e390","parentId":"fld_15b9c72eb0d84580a682dd75d9c3d80e","modified":1645694212413,"created":1645694212413,"url":"{{ base_url }}/api/v1/light/test/on","name":"Turn On","description":"","method":"POST","body":{},"parameters":[],"headers":[],"authentication":{"prefix":"","token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1553594253452,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_15b9c72eb0d84580a682dd75d9c3d80e","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212411,"created":1645694212411,"name":"Light","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1550815997650,"_type":"request_group"},{"_id":"req_dc462461790f4590a906c98f547a10c3","parentId":"fld_47531c5f2dba45a0b345d5757fbb5efd","modified":1645694212434,"created":1645694212434,"url":"{{ base_url }}/api/v1/user/tony/location","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"latitude\": 48,\n\t\"longitude\": 12,\n\t\"altitude\": 0,\n\t\"accuracy\": 20\n}"},"parameters":[],"headers":[{"id":"pair_286222e752c74d2e89cf6dbbb3503311","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1549268375325.25,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_47531c5f2dba45a0b345d5757fbb5efd","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212428,"created":1645694212428,"name":"Location","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1550160492802,"_type":"request_group"},{"_id":"req_ec269a10c1b5469cadb79629909783bd","parentId":"fld_47531c5f2dba45a0b345d5757fbb5efd","modified":1645694212431,"created":1645694212431,"url":"{{ base_url }}/api/v1/user/tony/location","name":"get","description":"","method":"GET","body":{"mimeType":"application/json","text":"{\n\t\"firstname\" : \"tony\",\n\t\"lastname\": \"Stark\",\n\t\"email\": \"tony.stark@gladysassistant.com\",\n\t\"password\": \"testtesttest\",\n\t\"birthdate\": \"2011-02-04\",\n\t\"language\": \"fr\",\n\t\"role\": \"admin\"\n}"},"parameters":[],"headers":[{"id":"pair_286222e752c74d2e89cf6dbbb3503311","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1549268375275.25,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_be2acf5a557d4a0d8f449db9ae29d0cf","parentId":"fld_fcd931bc5d064295b29669ae26bacd4b","modified":1645694568041,"created":1645694212422,"url":"{{ base_url }}/api/v1/message","name":"send","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"text\": \"Turn on the light in the kitchen\"\n}"},"parameters":[],"headers":[{"id":"pair_286222e752c74d2e89cf6dbbb3503311","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1549181763012.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_fcd931bc5d064295b29669ae26bacd4b","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212420,"created":1645694212420,"name":"Message","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1549832740378,"_type":"request_group"},{"_id":"req_6c047c1a1e894094ad3b1dc3ef0aa542","parentId":"fld_9063a3b729404388b3aff08bd933f631","modified":1645694532406,"created":1645694212293,"url":"{{ base_url }}/api/v1/room?expand=devices","name":"get","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{% response 'body', 'req_af2cc6c0597f4d29b91758e3660ffa51', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always' %}","type":"bearer"},"metaSortKey":-1557208943925.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_9063a3b729404388b3aff08bd933f631","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212289,"created":1645694212289,"name":"Room","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1549504987954,"_type":"request_group"},{"_id":"req_8f2aecb3bdff43f1885214823c6f32b6","parentId":"fld_9063a3b729404388b3aff08bd933f631","modified":1645694212297,"created":1645694212297,"url":"{{ base_url }}/api/v1/house/my-house/room","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"my room\"\n}"},"parameters":[],"headers":[{"id":"pair_8d1634976aa34c008e7920729a217aa1","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557206949918.75,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_cfe169e05d344b97a6e65cbbaa0db8ff","parentId":"fld_9063a3b729404388b3aff08bd933f631","modified":1645694212300,"created":1645694212300,"url":"{{ base_url }}/api/v1/room/my-room","name":"update","description":"","method":"PATCH","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"my room\"\n}"},"parameters":[],"headers":[{"id":"pair_8d1634976aa34c008e7920729a217aa1","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557205952915.375,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_81964a67631e483e8b0a7244c7e09e5e","parentId":"fld_9063a3b729404388b3aff08bd933f631","modified":1645694541367,"created":1645694212305,"url":"{{ base_url }}/api/v1/room/my-room?expand=temperature","name":"get by selector","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{% response 'body', 'req_af2cc6c0597f4d29b91758e3660ffa51', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always' %}","type":"bearer"},"metaSortKey":-1557205454413.6875,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_1d7f93fbaf78468db1b6362ff94dea44","parentId":"fld_56a66e5c0fa54bf2af007aeb53b9b511","modified":1645694212264,"created":1645694212264,"url":"{{ base_url }}/api/v1/user","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"firstname\" : \"tony\",\n\t\"lastname\": \"Stark\",\n\t\"email\": \"tony.stark@gladysassistant.com\",\n\t\"password\": \"testtesttest\",\n\t\"birthdate\": \"2011-02-04\",\n\t\"language\": \"fr\",\n\t\"role\": \"admin\"\n}"},"parameters":[],"headers":[{"id":"pair_286222e752c74d2e89cf6dbbb3503311","name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1549504988054,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_56a66e5c0fa54bf2af007aeb53b9b511","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212261,"created":1645694212261,"name":"User","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1548193978258,"_type":"request_group"},{"_id":"req_3b0f71ee6ab340edae27597b69399938","parentId":"fld_56a66e5c0fa54bf2af007aeb53b9b511","modified":1645694212272,"created":1645694212272,"url":"{{ base_url }}/api/v1/me","name":"get myself","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1549504988004,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_a8ce6fd88f904be497144c82da600223","parentId":"fld_56a66e5c0fa54bf2af007aeb53b9b511","modified":1645694212277,"created":1645694212277,"url":"{{ base_url }}/api/v1/me/picture","name":"get my picture","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1549504987991.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_f500b7f66b2c436dbeedd69df68f3efe","parentId":"fld_56a66e5c0fa54bf2af007aeb53b9b511","modified":1645694239861,"created":1645694212268,"url":"{{ base_url }}/api/v1/login","name":"login","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"email\": \"tony.stark@gladysassistant.com\",\n\t\"password\": \"testtesttest\"\n}"},"parameters":[],"headers":[{"id":"pair_286222e752c74d2e89cf6dbbb3503311","name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1549504987985.25,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_94d5f3baf464437aacbe64607b2cd196","parentId":"fld_56a66e5c0fa54bf2af007aeb53b9b511","modified":1645694212281,"created":1645694212281,"url":"{{ base_url }}/api/v1/user","name":"get list of users","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1549504987979,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_812838d9291b449997247c54505bde24","parentId":"fld_c5166acdda0c4809a4f6fa686322103b","modified":1645694212351,"created":1645694212351,"url":"{{ base_url }}/api/v1/service/mqtt/variable/MQTT_PASSWORD","name":"create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"value\": \"my-password\"\n}"},"parameters":[],"headers":[{"id":"pair_be58b234961b41868b2d1d62775aab2c","name":"Content-Type","value":"application/json"}],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1547169751933,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_c5166acdda0c4809a4f6fa686322103b","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212349,"created":1645694212349,"name":"Variable","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1547210720986,"_type":"request_group"},{"_id":"req_0ff19585187544b0a11084563daf309c","parentId":"fld_937e18704ed74741833ae283e3dc8a52","modified":1645694513700,"created":1645694212499,"url":"{{ base_url }}/api/v1/service/rflink/connect","name":"connect","description":"","method":"POST","body":{},"parameters":[],"headers":[],"authentication":{"token":"{% response 'body', 'req_af2cc6c0597f4d29b91758e3660ffa51', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always' %}","type":"bearer"},"metaSortKey":-1557371834464.375,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_937e18704ed74741833ae283e3dc8a52","parentId":"fld_94c5448ca6154d869a77483f1a96ab90","modified":1645694425143,"created":1645694425143,"name":"RFLink","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1645694425143,"_type":"request_group"},{"_id":"fld_94c5448ca6154d869a77483f1a96ab90","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212357,"created":1645694212357,"name":"Services","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1547128782880,"_type":"request_group"},{"_id":"req_c30f335dcffe42949618b60c890ebee0","parentId":"fld_937e18704ed74741833ae283e3dc8a52","modified":1645694447168,"created":1645694212503,"url":"{{ base_url }}/api/v1/service/rflink/disconnect","name":"disconnect","description":"","method":"POST","body":{},"parameters":[],"headers":[],"authentication":{"token":"{% response 'body', 'req_af2cc6c0597f4d29b91758e3660ffa51', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always' %}","type":"bearer"},"metaSortKey":-1557371834458.125,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_7ba7cdd5b2e34e1a992c654260841316","parentId":"fld_937e18704ed74741833ae283e3dc8a52","modified":1645694443268,"created":1645694212507,"url":"{{ base_url }}/api/v1/service/rflink/pair","name":"pair","description":"","method":"POST","body":{},"parameters":[],"headers":[],"authentication":{"token":"{% response 'body', 'req_af2cc6c0597f4d29b91758e3660ffa51', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always' %}","type":"bearer"},"metaSortKey":-1557371834451.875,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_ef288019e18549e898ad873bc4bfdfc8","parentId":"fld_937e18704ed74741833ae283e3dc8a52","modified":1645694445232,"created":1645694212510,"url":"{{ base_url }}/api/v1/service/rflink/unpair","name":"unpair","description":"","method":"POST","body":{},"parameters":[],"headers":[],"authentication":{"token":"{% response 'body', 'req_af2cc6c0597f4d29b91758e3660ffa51', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always' %}","type":"bearer"},"metaSortKey":-1557371834445.625,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_834c40d9497b42ccb925c094ae44c33a","parentId":"fld_937e18704ed74741833ae283e3dc8a52","modified":1645694441195,"created":1645694212518,"url":"{{ base_url }}/api/v1/service/rflink/remove","name":"remove","description":"","method":"POST","body":{},"parameters":[],"headers":[],"authentication":{"token":"{% response 'body', 'req_af2cc6c0597f4d29b91758e3660ffa51', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always' %}","type":"bearer"},"metaSortKey":-1557371834439.375,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_55e036a57f1b47cca85f12257b065245","parentId":"fld_937e18704ed74741833ae283e3dc8a52","modified":1645694451720,"created":1645694212494,"url":"{{ base_url }}/api/v1/service/rflink/debug","name":"debug","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"value\": \"10;MiLightv1;9926;02;34BC;OFF;\\n\"\n}"},"parameters":[],"headers":[{"id":"pair_7aa0cc1cd45d442fa27fc31ce976a725","name":"Content-Type","value":"application/json"},{"description":"","id":"pair_0a157b4c23ab41d8971eaa3f05d94706","name":"value","value":"10;Newkaku;00f79162;1;ON;"}],"authentication":{"token":"{% response 'body', 'req_af2cc6c0597f4d29b91758e3660ffa51', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always' %}","type":"bearer"},"metaSortKey":-1557371834426.875,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_c4214367dbea4af49e9a847d4514e427","parentId":"fld_937e18704ed74741833ae283e3dc8a52","modified":1645694436993,"created":1645694212514,"url":"{{ base_url }}/api/v1/service/rflink/newDevices","name":"get new devices","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{% response 'body', 'req_af2cc6c0597f4d29b91758e3660ffa51', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always' %}","type":"bearer"},"metaSortKey":-1557371834414.375,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_df7a992edc6040f29f612cca3e5f6a22","parentId":"fld_937e18704ed74741833ae283e3dc8a52","modified":1645694431469,"created":1645694212490,"url":"{{ base_url }}/api/v1/service/rflink/status","name":"get status","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{% response 'body', 'req_af2cc6c0597f4d29b91758e3660ffa51', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always' %}","type":"bearer"},"metaSortKey":-1557371834364.375,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_26822005bd754fdfb1848bc5871f790e","parentId":"fld_94c5448ca6154d869a77483f1a96ab90","modified":1645694212391,"created":1645694212391,"url":"{{ base_url }}/api/v1/service/mqtt/start","name":"start","description":"","method":"POST","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557373291366,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_dc150fe104d44faca5efc99e4f858ab0","parentId":"fld_94c5448ca6154d869a77483f1a96ab90","modified":1645694212395,"created":1645694212395,"url":"{{ base_url }}/api/v1/service/mqtt/stop","name":"stop","description":"","method":"POST","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557373291353.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_162ea74a472d4136a00f3561a33d821c","parentId":"fld_cd0ab7ea379f4f7a8570f9295ba25f52","modified":1645694212385,"created":1645694212385,"url":"{{ base_url }}/api/v1/service/philips-hue/bridge","name":"get bridges","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1552889735892,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_cd0ab7ea379f4f7a8570f9295ba25f52","parentId":"fld_94c5448ca6154d869a77483f1a96ab90","modified":1645694212382,"created":1645694212382,"name":"Philips Hue","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1557373291316,"_type":"request_group"},{"_id":"req_4eb24eff654145dd9b16d3b317fc90a5","parentId":"fld_c8fd24c830714870ad0acad21011278b","modified":1645694212375,"created":1645694212375,"url":"{{ base_url }}/api/v1/service/usb/port","name":"list ports","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1547087813927,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_c8fd24c830714870ad0acad21011278b","parentId":"fld_94c5448ca6154d869a77483f1a96ab90","modified":1645694212373,"created":1645694212373,"name":"USB","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1547087813877,"_type":"request_group"},{"_id":"req_bcc792a015834aefa26545d916c94d5a","parentId":"fld_65f9e59f6f8a4a5e90e17b5b65632e0a","modified":1645694212368,"created":1645694212368,"url":"{{ base_url }}/api/v1/service/zwave/neighbor","name":"get neighbors","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1554027100848.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_65f9e59f6f8a4a5e90e17b5b65632e0a","parentId":"fld_94c5448ca6154d869a77483f1a96ab90","modified":1645694212359,"created":1645694212359,"name":"Z-Wave","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1547087813827,"_type":"request_group"},{"_id":"req_9fcab57821be4d7181dc11783aa3062e","parentId":"fld_65f9e59f6f8a4a5e90e17b5b65632e0a","modified":1645694212361,"created":1645694212361,"url":"{{ base_url }}/api/v1/service/zwave/node","name":"get nodes","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1554027100823.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_f9fc695b9900446da633e1fa9b341a82","parentId":"fld_65f9e59f6f8a4a5e90e17b5b65632e0a","modified":1645694212364,"created":1645694212364,"url":"{{ base_url }}/api/v1/service/zwave/info","name":"get informations","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1554027100798.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_1e42d71c6e6e4f2eb87d4996addd5bdc","parentId":"fld_1f85f2eb6f9b4b4098d18293a2f654b6","modified":1645694212461,"created":1645694212461,"url":"{{ base_url }}/api/v1/house/test-house/weather","name":"get weather in house","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557375773569,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_1f85f2eb6f9b4b4098d18293a2f654b6","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212459,"created":1645694212459,"name":"Weather","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1547128782830,"_type":"request_group"},{"_id":"req_e89d8fc8cefb4e568d2ae1b453e4b122","parentId":"fld_1f85f2eb6f9b4b4098d18293a2f654b6","modified":1645694212464,"created":1645694212464,"url":"{{ base_url }}/api/v1/user/tony/weather","name":"get weather user","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"token":"{{ bearer_token }}","type":"bearer"},"metaSortKey":-1557374580308.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_4925ac50896748adb8803ef809fbe5aa","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212215,"created":1645694212215,"name":"Base Environment","data":{},"dataPropertyOrder":{"&":["base_url","bearer_token"]},"color":null,"isPrivate":false,"metaSortKey":1557370941828,"_type":"environment"},{"_id":"jar_aede92959ff9439593877a7c7c163d3c","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212230,"created":1645694212230,"name":"Default Jar","cookies":[],"_type":"cookie_jar"},{"_id":"spc_c425314d1990414eb41891b9825f6995","parentId":"wrk_7abf468172864f308078eb4e967f403f","modified":1645694212542,"created":1645694212234,"fileName":"Gladys Assistant","contents":"","contentType":"yaml","_type":"api_spec"},{"_id":"env_f34eab2b2903473e94fca4e1757384ba","parentId":"env_4925ac50896748adb8803ef809fbe5aa","modified":1645694212218,"created":1645694212218,"name":"Local","data":{"base_url":"http://localhost:1443","bearer_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZTRlM2YwM2UtNjBiOS00ODVlLWJjMGEtYzU4MmI2OTA4OWJkIiwic2NvcGUiOlsiZGFzaGJvYXJkOndyaXRlIiwiZGFzaGJvYXJkOnJlYWQiXSwic2Vzc2lvbl9pZCI6Ijc1NzU2NjIzLTUwYWQtNDFhYy1iOWE0LWIzZWZhY2M4NTA1YyIsImlhdCI6MTU1NzM3MTM2NywiZXhwIjoxNTU3NDU3NzY3LCJhdWQiOiJ1c2VyIiwiaXNzIjoiZ2xhZHlzIn0.XDvO4TREbjk5k9t1bhTDqb1NYjKHk2MksfCP08FCGBU"},"dataPropertyOrder":null,"color":"#00c8c2","isPrivate":false,"metaSortKey":1557371062816,"_type":"environment"},{"_id":"env_aa242f12c2b04479b57a3a3134d52952","parentId":"env_4925ac50896748adb8803ef809fbe5aa","modified":1645694212223,"created":1645694212223,"name":"GladysDev","data":{"base_url":"http://192.168.0.36:1443","user":{"email":"nicolas.geissel@gmail.com","password":"deepspace"}},"dataPropertyOrder":{"&":["base_url","user"],"&~|user":["email","password"]},"color":null,"isPrivate":false,"metaSortKey":1588632646983,"_type":"environment"}]} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 diff --git a/server/lib/device/device.getBySelector.js b/server/lib/device/device.getBySelector.js index 0e86daa34e..aa0a9bb32f 100644 --- a/server/lib/device/device.getBySelector.js +++ b/server/lib/device/device.getBySelector.js @@ -5,7 +5,7 @@ const { NotFoundError } = require('../../utils/coreErrors'); * @param {string} selector - Device selector. * @returns {Promise} Resolve with device. * @example - * device.getBySelector('test-devivce'); + * device.getBySelector('test-device'); */ function getBySelector(selector) { const device = this.stateManager.get('device', selector); diff --git a/server/services/index.js b/server/services/index.js index dec39add7c..19642069af 100644 --- a/server/services/index.js +++ b/server/services/index.js @@ -12,6 +12,7 @@ module.exports.telegram = require('./telegram'); module.exports.usb = require('./usb'); module.exports.xiaomi = require('./xiaomi'); module.exports.tasmota = require('./tasmota'); +module.exports.rflink = require('./rflink'); module.exports.bluetooth = require('./bluetooth'); module.exports.ewelink = require('./ewelink'); module.exports['tp-link'] = require('./tp-link'); diff --git a/server/services/rflink/README.md b/server/services/rflink/README.md new file mode 100755 index 0000000000..f3331bc070 --- /dev/null +++ b/server/services/rflink/README.md @@ -0,0 +1,88 @@ +How To +====== + +Let's google it. + +Changelog +========= +MAJOR 05-2023 Upgrading to "serialport": "11.x.x" & "@serialport/parser-readline": "11.x.x" (some code changes are required) + +Todos +===== + +1. Real life test (On going since 2021) + +2. Videos to show this working (in progress) + +3. Documentation + +4. Front tests (Cypress) : Ok for integration route + + +Known issues +============ + +At gladys server startup +------------------------ + +Should have no impact : + +2021-06-28T15:09:46+0200 index.js:15 (process.on) unhandledRejection catched: Promise { + TypeError: Cannot read property 'event' of undefined + at SerialPort.returnOpenErr (../server/services/rflink/lib/commands/rflink.connect.js:36:21) + at SerialPort._error (../server/services/rflink/node_modules/@serialport/stream/lib/index.js:198:14) + at binding.open.then.err (../server/services/rflink/node_modules/@serialport/stream/lib/index.js:242:12) } +2021-06-28T15:09:46+0200 index.js:16 (process.on) TypeError: Cannot read property 'event' of undefined + at SerialPort.returnOpenErr (../server/services/rflink/lib/commands/rflink.connect.js:36:21) + at SerialPort._error (../server/services/rflink/node_modules/@serialport/stream/lib/index.js:198:14) + at binding.open.then.err (../server/services/rflink/node_modules/@serialport/stream/lib/index.js:242:12) + + +MiLight deviceFeature +--------------------- + +"Mode" and "Disco" are not handled yet. +MiLightv1;ID=9999;SWITCH=01;RGBW=dcb8;CMD=DISCO+; +20;62;MiLightv1;ID=9999;SWITCH=01;RGBW=dcb8;CMD=MODE1 + +Milight device control +---------------------- + +Color is not managed properly : + +- solution 1 : find a better way to convert the color picker value to 8bits color ) + +- solution 2 : replace the glady color picker by a 8bits palette color picker (https://codepen.io/kevinli/pen/GRpXOvo) ) + +Memory leak +----------- + +On device setting page, error in front console : +debug.js?c91e:365 Can't call "this.setState" on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method. + + in RflinkDeviceBox + in NodeTab + in RflinkDevicePage + ... + RflinkDeviceBox._this.deleteDevice @ Device.jsx?46ce:31 + + +Improvments +=========== + +* Show RFLink gateway status + 10;status; => to display if RF and MiLight are ON on the RFLink gateway + +Devices compliance +================== + +433Mhz +------ + +* Chacon DiO +* Chinese movement detector + +2,4Ghz +------ + +* MiLight gu10, E27, ... diff --git a/server/services/rflink/api/rflink.controller.js b/server/services/rflink/api/rflink.controller.js new file mode 100644 index 0000000000..e7ae73b281 --- /dev/null +++ b/server/services/rflink/api/rflink.controller.js @@ -0,0 +1,171 @@ +/* eslint-disable prefer-destructuring */ +const asyncMiddleware = require('../../../api/middlewares/asyncMiddleware'); +const logger = require('../../../utils/logger'); + +module.exports = function RFlinkController(gladys, rflinkManager, serviceId) { + /** + * @api {get} /api/v1/service/rflink/newDevices get rflink devices + * @apiName newDevices + * @apiGroup RFlink + */ + async function getNewDevices(req, res) { + const newDevices = rflinkManager.getNewDevices(); + res.json(newDevices); + } + + /** + * @api {post} /api/v1/service/rflink/connect connect to the gateway + * @apiName connect + * @apiGroup RFlink + */ + async function connect(req, res) { + const rflinkPath = await gladys.variable.getValue('RFLINK_PATH', serviceId); + try { + rflinkManager.connect(rflinkPath); + } catch (e) { + logger.error('RFLink gateway cannot connect : no usb path configured'); + res.json({ success: false }); + } + res.json({ success: true }); + } + + /** + * @api {post} /api/v1/service/rflink/disconnect disconnect the gateway + * @apiName disconnect + * @apiGroup RFlink + */ + async function disconnect(req, res) { + rflinkManager.disconnect(); + res.json({ + success: true, + }); + } + + /** + * @api {get} /api/v1/service/rflink/status get the gateway's status + * @apiName getStatus + * @apiGroup RFlink + */ + async function getStatus(req, res) { + res.json({ + currentMilightGateway: rflinkManager.currentMilightGateway, + lastCommand: rflinkManager.lastCommand, + connected: rflinkManager.connected, + scanInProgress: rflinkManager.scanInProgress, + ready: rflinkManager.ready, + }); + } + + /** + * @api {post} /api/v1/service/rflink/pair send a milight pairing command + * @apiName pair + * @apiGroup RFlink + */ + async function pair(req, res) { + let milightZone = req.body.zone; + if (milightZone === undefined || milightZone === null) { + milightZone = 1; + } + let currentMilightGateway = await gladys.variable.getValue('CURRENT_MILIGHT_GATEWAY', serviceId); + if (currentMilightGateway === null) { + currentMilightGateway = rflinkManager.currentMilightGateway; + } + rflinkManager.currentMilightGateway = currentMilightGateway; + rflinkManager.pair(currentMilightGateway, milightZone); + + res.json({ + success: true, + currentMilightGateway, + milightZone, + }); + } + + /** + * @api {post} /api/v1/service/rflink/pair send a milight unpairing command + * @apiName unpair + * @apiGroup RFlink + */ + async function unpair(req, res) { + let milightZone = req.body.zone; + if (milightZone === undefined || milightZone === null) { + milightZone = 1; + } + let currentMilightGateway = await gladys.variable.getValue('CURRENT_MILIGHT_GATEWAY', serviceId); + if (currentMilightGateway === null) { + currentMilightGateway = rflinkManager.currentMilightGateway; + } + rflinkManager.currentMilightGateway = currentMilightGateway; + + rflinkManager.unpair(currentMilightGateway, milightZone); + res.json({ + success: true, + currentMilightGateway, + milightZone, + }); + } + + /** + * @api {post} /api/v1/service/rflink/debug send a rflink command + * @apiName debug + * @apiGroup RFlink + */ + async function sendDebug(req, res) { + const command = `${req.body.value}\n`; + logger.debug(`Command send to port : ${command}`); + rflinkManager.sendUsb.write(command); + res.json({ + success: true, + }); + } + + /** + * @api {post} /api/v1/service/rflink/remove remove a device from the device list + * @apiName remove + * @apiGroup RFlink + */ + async function remove(req, res) { + // Deleting the device from the new device list + const index = rflinkManager.newDevices.findIndex((element) => element.external_id === req.body.external_id); + if (index !== -1) { + rflinkManager.newDevices.splice(index, 1); + } + res.json({ + success: true, + }); + } + + return { + 'post /api/v1/service/rflink/pair': { + authenticated: true, + controller: asyncMiddleware(pair), + }, + 'post /api/v1/service/rflink/unpair': { + authenticated: true, + controller: asyncMiddleware(unpair), + }, + 'get /api/v1/service/rflink/newDevices': { + authenticated: true, + controller: asyncMiddleware(getNewDevices), + }, + 'post /api/v1/service/rflink/connect': { + authenticated: true, + controller: asyncMiddleware(connect), + }, + 'post /api/v1/service/rflink/disconnect': { + authenticated: true, + controller: asyncMiddleware(disconnect), + }, + 'post /api/v1/service/rflink/debug': { + authenticated: true, + controller: asyncMiddleware(sendDebug), + }, + 'get /api/v1/service/rflink/status': { + authenticated: true, + controller: asyncMiddleware(getStatus), + }, + 'post /api/v1/service/rflink/remove': { + authenticated: true, + controller: asyncMiddleware(remove), + }, + }; +}; diff --git a/server/services/rflink/api/rflink.parse.ObjToRF.js b/server/services/rflink/api/rflink.parse.ObjToRF.js new file mode 100644 index 0000000000..1c777c552b --- /dev/null +++ b/server/services/rflink/api/rflink.parse.ObjToRF.js @@ -0,0 +1,36 @@ +const logger = require('../../../utils/logger'); + +// eslint-disable-next-line jsdoc/check-alignment +/** + * @description Convert a rflink device object to a string that can be sent to rflink. + * @param {object} device - Secure node. + * @param {string} deviceFeature - The devicce feature. + * @param {any} state - The state of the device. + * @returns {string} Rfcode - A code understable for rflink gateway. + * @example + * rflink.ObjToRF(device); + */ +function ObjToRF(device, deviceFeature, state) { + const id = device.external_id.split(':')[1]; + const channel = device.external_id.split(':')[2]; + + let Rfcode = `10;${device.model};${id};`; + logger.debug(`Send to RFLINK : ${Rfcode}`); + if (channel !== undefined) { + Rfcode += `${channel};`; + } else { + logger.log('channel undefined'); + } + + if (state !== undefined) { + Rfcode += `${state};`; + } else { + logger.log('no state'); + } + + Rfcode += '\n'; + + return Rfcode; +} + +module.exports = ObjToRF; diff --git a/server/services/rflink/api/rflink.parse.RFtoObject.js b/server/services/rflink/api/rflink.parse.RFtoObject.js new file mode 100644 index 0000000000..642c609b1a --- /dev/null +++ b/server/services/rflink/api/rflink.parse.RFtoObject.js @@ -0,0 +1,28 @@ +/* eslint-disable prefer-destructuring */ +// eslint-disable-next-line jsdoc/check-alignment +/** + * @description Convert a rflink message to an array. + * @param {string} data - Secure node. + * @returns {object} Return an array with an index for each value. + * @example + * rflink.RftoObj(data); + */ +function RfToObj(data) { + const newDevice = {}; + const msg = String(data).split(';'); + + if (parseInt(msg[0], 10) === 20 && parseInt(msg[1], 10) !== 0 && msg[2] !== 'OK' && msg[3] !== undefined) { + newDevice.protocol = msg[2]; + newDevice.features = []; + for (let i = 3; i < msg.length; i += 1) { + if (msg[i].includes('=') === true) { + const temp = msg[i].split('='); + newDevice[`${temp[0].toLowerCase()}`] = temp[1]; + } + } + } + + return newDevice; +} + +module.exports = RfToObj; diff --git a/server/services/rflink/index.js b/server/services/rflink/index.js new file mode 100644 index 0000000000..4cb0828d0a --- /dev/null +++ b/server/services/rflink/index.js @@ -0,0 +1,54 @@ +const logger = require('../../utils/logger'); +const RfLinkManager = require('./lib'); +const RflinkController = require('./api/rflink.controller'); +const { ServiceNotConfiguredError } = require('../../utils/coreErrors'); + +const RFLINK_PATH = 'RFLINK_PATH'; + +module.exports = function RfLink(gladys, serviceId) { + const rfLinkManager = new RfLinkManager(gladys, serviceId); + let RflinkPath; + + /** + * @public + * @description Start rflink service. + * @example + * gladys.services.rflink.start(); + */ + async function start() { + RflinkPath = await gladys.variable.getValue(RFLINK_PATH, serviceId); + if (!RflinkPath) { + throw new ServiceNotConfiguredError('RFLINK_PATH_NOT_FOUND'); + } else { + logger.info('Starting Rflink service'); + } + try { + rfLinkManager.connect(RflinkPath); + } catch (err) { + Promise.reject(Error(err)); + } + const currentMilightGateway = await gladys.variable.getValue('CURRENT_MILIGHT_GATEWAY', serviceId); + rfLinkManager.currentMilightGateway = currentMilightGateway; + if (rfLinkManager.currentMilightGateway === null) { + rfLinkManager.currentMilightGateway = 'F746'; + } + } + + /** + * @public + * @description Stop rfllink service. + * @example + * gladys.services.rflink.stop(); + */ + async function stop() { + logger.info('Stopping Rflink service'); + await rfLinkManager.disconnect(); + } + + return Object.freeze({ + start, + stop, + device: rfLinkManager, + controllers: RflinkController(gladys, rfLinkManager, serviceId), + }); +}; diff --git a/server/services/rflink/lib/commands/rflink.addNewDevice.js b/server/services/rflink/lib/commands/rflink.addNewDevice.js new file mode 100644 index 0000000000..73c470d4ef --- /dev/null +++ b/server/services/rflink/lib/commands/rflink.addNewDevice.js @@ -0,0 +1,20 @@ +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); +const logger = require('../../../../utils/logger'); + +/** + * @description Add a new device that can be added to Gladys. + * @param {object} device - Device to add in the list of new discovered devices. + * @example + * Rflink.addNewDevice(device); + */ +function addNewDevice(device) { + if (!this.devices.includes(device)) { + logger.debug(`New device discovered by RFLink Gateway ${device.external_id}`); + this.newDevices.push(device); + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.RFLINK.NEW_DEVICE, + }); + } +} + +module.exports = { addNewDevice }; diff --git a/server/services/rflink/lib/commands/rflink.connect.js b/server/services/rflink/lib/commands/rflink.connect.js new file mode 100644 index 0000000000..a0a02c5509 --- /dev/null +++ b/server/services/rflink/lib/commands/rflink.connect.js @@ -0,0 +1,72 @@ +const os = require('os'); +const { SerialPort, ReadlineParser } = require('serialport'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); +const { ServiceNotConfiguredError } = require('../../../../utils/coreErrors'); +const logger = require('../../../../utils/logger'); + +/** + * @description Connect to Rflink. + * @param {string} path - Path to the Rflink gateway. + * @example + * rflink.connect(path); + */ +function connect(path) { + this.connected = false; + this.ready = false; + this.scanInProgress = false; + + if (!path) { + throw new ServiceNotConfiguredError('RFLINK_PATH_NOT_FOUND'); + } + // special case for macOS + if (os.platform() === 'darwin') { + this.path = path.replace('/dev/tty.', '/dev/cu.'); + } else { + this.path = path; + } + + try { + const port = new SerialPort({ + path: this.path, + baudRate: 57600, + dataBits: 8, + parity: 'none', + autoOpen: false, + }); + + this.sendUsb = port; + + this.sendUsb.open(function returnOpenErr(err) { + if (err) { + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.RFLINK.DRIVER_FAILED, + }); + logger.error(`Error opening port: : ${err.message}`); + } + logger.info(`Success on opening port`); + }); + + const readline = new ReadlineParser({ + baudRate: 57600, + }); + this.sendUsb.pipe(readline); + this.usb = readline; + + logger.debug(`Rflink : Connecting to USB = ${path}`); + this.connected = true; + this.ready = true; + this.scanInProgress = true; + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.RFLINK.DRIVER_READY, + }); + this.listen(); + } catch (error) { + this.connected = false; + this.ready = false; + this.scanInProgress = false; + } +} + +module.exports = { + connect, +}; diff --git a/server/services/rflink/lib/commands/rflink.disconnect.js b/server/services/rflink/lib/commands/rflink.disconnect.js new file mode 100644 index 0000000000..c6dba530fa --- /dev/null +++ b/server/services/rflink/lib/commands/rflink.disconnect.js @@ -0,0 +1,22 @@ +const logger = require('../../../../utils/logger'); + +/** + * @description Disconnect Rflink Gateway. + * @example + * rflink.disconnect(); + */ +function disconnect() { + if (this.path && this.connected) { + logger.debug(`Rflink : Disconnecting...`); + this.sendUsb.close(); + } else { + logger.debug('Rflink: Not connected'); + } + this.connected = false; + this.ready = false; + this.scanInProgress = false; +} + +module.exports = { + disconnect, +}; diff --git a/server/services/rflink/lib/commands/rflink.getNewDevice.js b/server/services/rflink/lib/commands/rflink.getNewDevice.js new file mode 100644 index 0000000000..3a222a9963 --- /dev/null +++ b/server/services/rflink/lib/commands/rflink.getNewDevice.js @@ -0,0 +1,14 @@ +/** + * @description Return a list of the rflink devices discovered by the gateway. + * @example + * rflink.getNewDevices(); + * @returns {object} Devices. + */ +function getNewDevices() { + const devices = this.newDevices; + return devices; +} + +module.exports = { + getNewDevices, +}; diff --git a/server/services/rflink/lib/commands/rflink.listen.js b/server/services/rflink/lib/commands/rflink.listen.js new file mode 100644 index 0000000000..f8fe6de991 --- /dev/null +++ b/server/services/rflink/lib/commands/rflink.listen.js @@ -0,0 +1,16 @@ +const logger = require('../../../../utils/logger'); +/** + * @description Listen to usb port. + * @example + * rflink.listen(); + */ +function listen() { + this.usb.on('data', (data) => { + this.message(data); + logger.debug(`Rflink : message reçu : ${data}`); + }); +} + +module.exports = { + listen, +}; diff --git a/server/services/rflink/lib/commands/rflink.milight.pair.js b/server/services/rflink/lib/commands/rflink.milight.pair.js new file mode 100644 index 0000000000..d5cc0c9893 --- /dev/null +++ b/server/services/rflink/lib/commands/rflink.milight.pair.js @@ -0,0 +1,99 @@ +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); + +/** + * @description Pair a milight device. + * @param {string} currentMilightGateway - Milight gateway. + * @param {string} milightZone - Milight zone. + * @example + * rflink.pair() + */ +function pair(currentMilightGateway, milightZone) { + let newLight; + const number = milightZone; + if (currentMilightGateway !== undefined) { + // if (this.currentMilightGateway.number < 10) { + // number = `0${this.currentMilightGateway.number}`; + // } else { + // number = `${this.currentMilightGateway.number}`; + // } + const msg = `10;MiLightv1;${this.currentMilightGateway};0${number};34BC;PAIR;`; + this.sendUsb.write(msg, (error) => {}); + + newLight = { + service_id: this.serviceId, + name: ` Milight ${currentMilightGateway} number${number} `, + selector: `rflink:milight:${currentMilightGateway}:${number}`, + external_id: `rflink:milight:${currentMilightGateway}:${number}`, + model: `Milight`, + should_poll: false, + features: [ + { + name: 'power', + selector: `rflink:milight:${currentMilightGateway}:${number}:power`, + external_id: `rflink:milight:${currentMilightGateway}:${number}:power`, + rfcode: { + value: 'CMD', + cmd: 'ON', + }, + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.BINARY, + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 1, + }, + { + name: 'color', + selector: `rflink:milight:${currentMilightGateway}:${number}:color`, + external_id: `rflink:milight:${currentMilightGateway}:${number}:color`, + rfcode: { + value: 'RGBW', + cmd: 'COLOR', + }, + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.COLOR, + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 255, + }, + { + name: 'brightness', + selector: `rflink:milight:${currentMilightGateway}:${number}:brightness`, + external_id: `rflink:milight:${currentMilightGateway}:${number}:brightness`, + rfcode: { + value: 'RGBW', + cmd: 'BRIGHT', + }, + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.BRIGHTNESS, + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 100, + }, + { + name: 'milight-mode', + selector: `rflink:milight:${currentMilightGateway}:${number}:milight-mode`, + external_id: `rflink:milight:${currentMilightGateway}:${number}:milight-mode`, + rfcode: 'CMD', + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.EFFECT_MODE, + read_only: false, + keep_history: true, + has_feedback: false, + min: 1, + max: 8, + }, + ], + }; + this.addNewDevice(newLight); + } +} + +module.exports = { + pair, +}; diff --git a/server/services/rflink/lib/commands/rflink.milight.unpair.js b/server/services/rflink/lib/commands/rflink.milight.unpair.js new file mode 100644 index 0000000000..49fad4d20d --- /dev/null +++ b/server/services/rflink/lib/commands/rflink.milight.unpair.js @@ -0,0 +1,21 @@ +const logger = require('../../../../utils/logger'); +/** + * @description Unpair a milight device. + * @param {string} currentMilightGateway - Milight gateway. + * @param {string} milightZone - Milight zone. + * @example + * rflink.unpair() + */ +function unpair(currentMilightGateway, milightZone) { + const number = milightZone; + if (this.currentMilightGateway !== undefined) { + const msg = `10;MiLightv1;${this.currentMilightGateway};0${number};34BC;UNPAIR;`; + logger.debug(msg); + this.sendUsb.write(msg, (error) => {}); + } + // @TODO : show a message in setup tab to tell user that gateway is undefined +} + +module.exports = { + unpair, +}; diff --git a/server/services/rflink/lib/commands/rflink.setValue.js b/server/services/rflink/lib/commands/rflink.setValue.js new file mode 100644 index 0000000000..34caa4926f --- /dev/null +++ b/server/services/rflink/lib/commands/rflink.setValue.js @@ -0,0 +1,99 @@ +const ObjToRF = require('../../api/rflink.parse.ObjToRF'); +const { rgbToMilightHue, intToRgb, rgbToHsb } = require('../../../../utils/colors'); +const { DEVICE_FEATURE_CATEGORIES } = require('../../../../utils/constants'); +const logger = require('../../../../utils/logger'); + +/** + * @description Send a message to change a device's value. + * @param {object} device - The device to control. + * @param {object} deviceFeature - The name of feature to control. + * @param {string} state - The new state. + * @returns {string} - The message send to RFLink gateway. + * @example + * rflink.setValue(device, deviceFeature, state); + */ +function setValue(device, deviceFeature, state) { + let msg; + let value; + + value = state; + + if (deviceFeature.type === 'binary') { + switch (state) { + case 0: + case false: + if (deviceFeature.category === DEVICE_FEATURE_CATEGORIES.SWITCH) { + value = 'OFF'; + } else if (deviceFeature.category === DEVICE_FEATURE_CATEGORIES.BUTTON) { + value = 'DOWN'; + } + break; + case 1: + case true: + if (deviceFeature.category === DEVICE_FEATURE_CATEGORIES.SWITCH) { + value = 'ON'; + } else if (deviceFeature.category === DEVICE_FEATURE_CATEGORIES.BUTTON) { + value = 'UP'; + } + break; + default: + value = state; + break; + } + } + + msg = ObjToRF(device, deviceFeature, value); + // unless we have milight + if (deviceFeature.external_id !== undefined) { + if (deviceFeature.external_id.split(':')[4] === 'milight') { + // sample milight device external_id: `rflink:${msg.id}:${msg.switch}`; + // sample milight feature external_id: `rflink:${msg.id}:milight-mode:${msg.switch}:milight`, + const id = device.external_id.split(':')[1]; + const channel = `0${device.external_id.split(':')[2]}`; + const feature = deviceFeature.external_id.split(':')[2].toLowerCase(); + + if (feature === 'color') { + const rgb = intToRgb(value); + const hsv = rgbToHsb(rgb); + const color = rgbToMilightHue(rgb); + const hex = Number(color).toString(16); + const brightness = Number(hsv[2]).toString(16) || 22; + msg = `10;MiLightv1;${id};${channel};${hex}${brightness};COLOR;\n`; + } else if (feature === 'brightness') { + const featureIndex = device.features.findIndex((f) => f.type === 'color'); + let lastColorValue = '34'; + if (device.features[featureIndex].last_value) { + const rgb = intToRgb(device.features[featureIndex].last_value); + lastColorValue = Number(rgbToMilightHue(rgb)).toString(16); + } + // The brightness is controlled in 32 steps. + const hex = Number(Math.round((value * 232) / 100)).toString(16); + // get the last color value and set brightness (a value from 0 to 100 converted in 8 bits hex) + msg = `10;MiLightv1;${id};${channel};${lastColorValue}${hex};BRIGHT;\n`; + } else if (feature === 'power') { + switch (state) { + case 0: + case false: + value = 'OFF'; + break; + case 1: + case true: + value = 'ON'; + break; + default: + value = 'ON'; + } + msg = `10;MiLightv1;${id};${channel};34BC;${value};\n`; + } else if (feature === 'milight-mode') { + msg = `10;MiLightv1;${id};${channel};34BC;MODE${value};\n`; + } + } + } + logger.debug(`Message send to USB : "${msg}"`); + this.sendUsb.write(msg); + return msg; +} + +module.exports = { + setValue, +}; diff --git a/server/services/rflink/lib/events/rflink.addDevice.js b/server/services/rflink/lib/events/rflink.addDevice.js new file mode 100644 index 0000000000..33e160f582 --- /dev/null +++ b/server/services/rflink/lib/events/rflink.addDevice.js @@ -0,0 +1,16 @@ +/* eslint-disable no-prototype-builtins */ +/* eslint-disable no-restricted-syntax */ + +/** + * @description Add device. + * @param {object} deviceList - Device list to add. + * @example + * Rflink.addDevice(device); + */ +function addDevice(deviceList) { + if (deviceList !== undefined) { + this.devices = deviceList; + } +} + +module.exports = { addDevice }; diff --git a/server/services/rflink/lib/events/rflink.connected.js b/server/services/rflink/lib/events/rflink.connected.js new file mode 100644 index 0000000000..c5f19272e4 --- /dev/null +++ b/server/services/rflink/lib/events/rflink.connected.js @@ -0,0 +1,21 @@ +const logger = require('../../../../utils/logger'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); + +/** + * @description When the gateway is connected. + * @example + * rflink.on('connected', this.connected); + */ +function connected() { + logger.debug(`Rflink : Gateway is connected`); + this.connected = true; + this.ready = true; + this.eventManager.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.RFLINK.CONNECTED, + payload: {}, + }); +} + +module.exports = { + connected, +}; diff --git a/server/services/rflink/lib/events/rflink.error.js b/server/services/rflink/lib/events/rflink.error.js new file mode 100644 index 0000000000..6982b9b172 --- /dev/null +++ b/server/services/rflink/lib/events/rflink.error.js @@ -0,0 +1,20 @@ +const logger = require('../../../../utils/logger'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); + +/** + * @description When an error occur. + * @example + * rflink.on('failed', this.driverFailed); + */ +function failed() { + logger.debug(`RFlink: Failed to start`); + this.connected = false; + this.eventManager.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.RFLINK.ERROR, + payload: {}, + }); +} + +module.exports = { + failed, +}; diff --git a/server/services/rflink/lib/events/rflink.message.js b/server/services/rflink/lib/events/rflink.message.js new file mode 100644 index 0000000000..9fa60dfe9f --- /dev/null +++ b/server/services/rflink/lib/events/rflink.message.js @@ -0,0 +1,381 @@ +const logger = require('../../../../utils/logger'); +const RFtoObj = require('../../api/rflink.parse.RFtoObject'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); +const { + DEVICE_FEATURE_CATEGORIES, + DEVICE_FEATURE_TYPES, + DEVICE_FEATURE_UNITS, +} = require('../../../../utils/constants'); + +/** + * @description Event triggered when a message is received by the rflink gateway. + * @param {string} msgRF - The message from the RFLink Gateway. + * @example + * rflink.message(msg); + */ +function message(msgRF) { + this.lastCommand = msgRF; + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.RFLINK.NEW_MESSAGE, + }); + logger.debug(`sending new message ${msgRF}`); + const msg = RFtoObj(msgRF); + logger.debug(`message RFtoObj => ${JSON.stringify(msg)}`); + let newDevice; + let doesntExistYet = true; + + if (typeof msg.id === 'string') { + if (msg.id.includes('=') === false) { + this.newDevices.forEach((d) => { + if (`rflink:${msg.id}:${msg.switch}` === d.external_id) { + doesntExistYet = false; + } + }); + + if (doesntExistYet === true) { + const model = `${msg.protocol.charAt(0).toUpperCase()}${msg.protocol.toLowerCase().slice(1)}`; + + newDevice = { + service_id: this.serviceId, + name: `${msg.protocol} `, + selector: `rflink:${msg.id}:${msg.switch}`, + external_id: `rflink:${msg.id}:${msg.switch}`, + model, + should_poll: false, + features: [], + }; + + if (msg.temp !== undefined) { + newDevice.name += 'temperature sensor'; + newDevice.name += msg.switch !== undefined ? ` ${msg.switch}` : ' '; + newDevice.features.push({ + name: 'temperature', + selector: `rflink:${msg.id}:temperature:${msg.switch}`, + external_id: `rflink:${msg.id}:temperature:${msg.switch}`, + rfcode: 'TEMP', + category: DEVICE_FEATURE_CATEGORIES.TEMPERATURE_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + unit: DEVICE_FEATURE_UNITS.CELSIUS, + read_only: true, + keep_history: true, + has_feedback: false, + min: -50, + max: 100, + }); + } + if (msg.hum !== undefined) { + newDevice.features.push({ + name: 'humidity', + selector: `rflink:${msg.id}:humidity:${msg.switch}`, + external_id: `rflink:${msg.id}:humidity:${msg.switch}`, + rfcode: 'HUM', + category: DEVICE_FEATURE_CATEGORIES.HUMIDITY_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + unit: DEVICE_FEATURE_UNITS.PERCENT, + read_only: true, + keep_history: true, + has_feedback: false, + min: 0, + max: 100, + }); + } + if (msg.baro !== undefined) { + newDevice.name += 'pressure sensor'; + newDevice.name += msg.switch !== undefined ? ` ${msg.switch}` : ' '; + newDevice.features.push({ + name: 'pressure', + selector: `rflink:${msg.id}:pressure:${msg.switch}`, + external_id: `rflink:${msg.id}:pressure:${msg.switch}`, + rfcode: 'BARO', + category: DEVICE_FEATURE_CATEGORIES.PRESSURE_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + unit: DEVICE_FEATURE_UNITS.PASCAL, + read_only: true, + keep_history: true, + has_feedback: false, + min: 0, + max: 100000000, + }); + } + if (msg.uv !== undefined) { + newDevice.name += 'uv sensor'; + newDevice.name += msg.switch !== undefined ? ` ${msg.switch}` : ' '; + newDevice.features.push({ + name: 'uv intensity', + selector: `rflink:${msg.id}:uv:${msg.switch}`, + external_id: `rflink:${msg.id}:uv:${msg.switch}`, + rfcode: 'UV', + category: DEVICE_FEATURE_CATEGORIES.LIGHT_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + read_only: true, + keep_history: true, + has_feedback: false, + min: -50, + max: 100, + }); + } + if (msg.lux !== undefined) { + newDevice.name += 'light sensor'; + newDevice.name += msg.switch !== undefined ? ` ${msg.switch}` : ' '; + newDevice.features.push({ + name: 'light intensity', + selector: `rflink:${msg.id}:light-intensity:${msg.switch}`, + external_id: `rflink:${msg.id}:light-intensity:${msg.switch}`, + rfcode: 'LUX', + category: DEVICE_FEATURE_CATEGORIES.LIGHT_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + unit: DEVICE_FEATURE_UNITS.LUX, + read_only: true, + keep_history: true, + has_feedback: false, + min: -50, + max: 100, + }); + } + if (msg.bat !== undefined) { + newDevice.features.push({ + name: 'battery', + selector: `rflink:${msg.id}:battery:${msg.switch}`, + external_id: `rflink:${msg.id}:battery:${msg.switch}`, + rfcode: 'BAT', + category: DEVICE_FEATURE_CATEGORIES.BATTERY, + type: DEVICE_FEATURE_TYPES.SENSOR.UNKNOWN, + read_only: true, + keep_history: true, + has_feedback: false, + min: 0, + max: 100, + }); + } + if (msg.rain !== undefined || msg.rainrate !== undefined) { + newDevice.name += 'rain sensor'; + newDevice.name += msg.switch !== undefined ? ` ${msg.switch}` : ' '; + newDevice.features.push({ + name: 'rain', + selector: `rflink:${msg.id}:rain:${msg.switch}`, + external_id: `rflink:${msg.id}:rain:${msg.switch}`, + rfcode: 'RAIN', + category: DEVICE_FEATURE_CATEGORIES.RAIN_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.INTEGER, + read_only: true, + keep_history: true, + has_feedback: false, + min: 0, + max: 1000, + }); + } + if (msg.winsp !== undefined || msg.awinsp !== undefined || msg.wings !== undefined) { + newDevice.name += 'wind speed sensor'; + newDevice.name += msg.switch !== undefined ? ` ${msg.switch}` : ' '; + newDevice.features.push({ + name: 'wind speed', + selector: `rflink:${msg.id}:wind-speed:${msg.switch}`, + external_id: `rflink:${msg.id}:wind-speed:${msg.switch}`, + rfcode: 'WINSP', + category: DEVICE_FEATURE_CATEGORIES.WIND_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.INTEGER, + read_only: true, + keep_history: true, + has_feedback: false, + min: 0, + max: 500, + }); + } + if (msg.windir !== undefined) { + newDevice.name += 'wind direction sensor'; + newDevice.name += msg.switch !== undefined ? ` ${msg.switch}` : ' '; + newDevice.features.push({ + name: 'wind direction', + selector: `rflink:${msg.id}:wind-dir:${msg.switch}`, + external_id: `rflink:${msg.id}:wind-dir:${msg.switch}`, + rfcode: 'WINDIR', + category: DEVICE_FEATURE_CATEGORIES.WIND_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.INTEGER, + read_only: true, + keep_history: true, + has_feedback: false, + min: 0, + max: 100, + }); + } + if (msg.co2 !== undefined) { + newDevice.name += 'co2 sensor'; + newDevice.name += msg.switch !== undefined ? ` ${msg.switch}` : ' '; + newDevice.features.push({ + name: 'co2', + selector: `rflink:${msg.id}:co2:${msg.switch}`, + external_id: `rflink:${msg.id}:co2:${msg.switch}`, + rfcode: 'CO2', + category: DEVICE_FEATURE_CATEGORIES.SMOKE_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.INTEGER, + read_only: true, + keep_history: true, + has_feedback: false, + min: 0, + max: 1000, + }); + } + if ( + msg.switch !== undefined && + msg.rgwb === undefined && + msg.protocol !== 'MiLightv1' && + (msg.cmd === 'ON' || + msg.cmd === 'OFF' || + msg.cmd === 'ALLON' || + msg.cmd === 'ALLOFF' || + msg.cmd === 'UP' || + msg.cmd === 'DOWN') + ) { + newDevice.name += 'switch'; + newDevice.name += ` ${msg.id}`; + newDevice.features.push({ + name: 'switch', + selector: `rflink:${msg.id}:switch:${msg.switch}`, + external_id: `rflink:${msg.id}:switch:${msg.switch}`, + rfcode: 'CMD', + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + type: DEVICE_FEATURE_TYPES.SENSOR.BINARY, + read_only: true, + keep_history: true, + has_feedback: false, + min: 0, + max: 1, + }); + } + + if ( + msg.protocol === 'MiLightv1' && + msg.cmd !== undefined && + (msg.rgbw !== undefined || msg.cmd.includes('MODE') === true || msg.cmd.includes('DISCO') === true) + ) { + newDevice.selector = `rflink:${msg.id}:${msg.switch}`; + newDevice.external_id = `rflink:${msg.id}:${msg.switch}`; + newDevice.name += `switch ${msg.id}-${msg.switch}`; + newDevice.features.push( + { + name: 'Power', + selector: `rflink:${msg.id}:power:${msg.switch}:milight`, + external_id: `rflink:${msg.id}:power:${msg.switch}:milight`, + rfcode: { + value: 'CMD', + cmd: 'ON', + }, + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.BINARY, + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 1, + }, + { + name: 'color', + selector: `rflink:${msg.id}:color:${msg.switch}:milight`, + external_id: `rflink:${msg.id}:color:${msg.switch}:milight`, + rfcode: { + value: 'RGBW', + cmd: 'COLOR', + }, + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.COLOR, + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 255, + }, + ); + newDevice.features.push({ + name: 'brightness', + selector: `rflink:${msg.id}:brightness:${msg.switch}:milight`, + external_id: `rflink:${msg.id}:brightness:${msg.switch}:milight`, + rfcode: { + value: 'RGBW', + cmd: 'BRIGHT', + }, + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.BRIGHTNESS, + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 100, + }); + newDevice.features.push({ + name: 'milight-mode', + selector: `rflink:${msg.id}:milight-mode:${msg.switch}:milight`, + external_id: `rflink:${msg.id}:milight-mode:${msg.switch}:milight`, + rfcode: 'CMD', + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.EFFECT_MODE, + read_only: false, + keep_history: true, + has_feedback: false, + min: 1, + max: 8, + }); + } + this.addNewDevice(newDevice); + } else if (doesntExistYet === false) { + if (msg.temp !== undefined) { + this.newValue(msg, 'temperature', msg.temp); + } + if (msg.hum !== undefined) { + this.newValue(msg, 'humidity', msg.hum); + } + if (msg.baro !== undefined) { + this.newValue(msg, 'pressure', msg.baro); + } + if (msg.uv !== undefined) { + this.newValue(msg, 'uv', msg.uv); + } + if (msg.lux !== undefined) { + this.newValue(msg, 'light-intensity', msg.lux); + } + if (msg.bat !== undefined) { + this.newValue(msg, 'battery', msg.bat); + } + if (msg.rain !== undefined) { + this.newValue(msg, 'rain', msg.rain); + } + if (msg.winsp !== undefined) { + this.newValue(msg, 'wind-speed', msg.winsp); + } + if (msg.awinsp !== undefined) { + this.newValue(msg, 'wind-speed', msg.awinsp); + } + if (msg.wings !== undefined) { + this.newValue(msg, 'wind-speed', msg.wings); + } + if (msg.windir !== undefined) { + this.newValue(msg, 'wind-dir', msg.windir); + } + if (msg.co2 !== undefined) { + this.newValue(msg, 'co2', msg.co2); + } + if ( + msg.switch !== undefined && + (msg.cmd === 'ON' || msg.cmd === 'OFF' || msg.cmd === 'ALLON' || msg.cmd === 'ALLOFF') + ) { + this.newValue(msg, 'switch', msg.cmd); + } + if (msg.rgbw !== undefined) { + this.newValue(msg, 'color', msg.rgbw); + this.newValue(msg, 'brightness', msg.rgbw); + } + if (msg.cmd !== undefined && msg.cmd.includes('MODE') === true) { + this.newValue(msg, 'milight-mode', msg.cmd); + } + if (msg.cmd !== undefined && msg.cmd.includes('DISCO') === true) { + this.newValue(msg, 'milight-mode', msg.cmd); + } + } + } else { + logger.log(`${msg.id} n'est pas une id valide`); + } + } +} + +module.exports = { + message, +}; diff --git a/server/services/rflink/lib/events/rflink.newValue.js b/server/services/rflink/lib/events/rflink.newValue.js new file mode 100644 index 0000000000..337d2e3c1c --- /dev/null +++ b/server/services/rflink/lib/events/rflink.newValue.js @@ -0,0 +1,67 @@ +const logger = require('../../../../utils/logger'); +const { EVENTS } = require('../../../../utils/constants'); +// eslint-disable-next-line jsdoc/require-param +/** + * @description When a new value is received. + * @param {object} device - Device to update. + * @param {string} deviceFeature - Feature to update. + * @param {string} state - State. + * @example + * newValue(Object, 'temperature', 30) + */ +function newValue(device, deviceFeature, state) { + logger.debug( + `RFlink : value ${deviceFeature} of device rflink:${device.id}:${deviceFeature}:${device.switch} changed to ${state}`, + ); + let value = state; + switch (state) { + case 'ON': + case 'ALLON': + case 'UP': + value = 1; + break; + case 'OFF': + case 'ALLOFF': + case 'DOWN': + value = 0; + break; + default: + value = state; + break; + } + switch (deviceFeature) { + case 'temperature': + value = parseInt(value, 16); + value /= 10; + break; + case 'battery': + value = 'NA'; + break; + case 'uv': + case 'light-intensity': + value = parseInt(value, 16); + break; + case 'pressure': + value = parseInt(value, 16); + break; + + default: + break; + } + if (deviceFeature !== undefined) { + if (device.id !== undefined) { + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: `rflink:${device.id}:${deviceFeature}:${device.switch}`, + state: value, + }); + } else { + logger.debug(`device id to change: ${device.id}`); + } + } else { + logger.debug(`device feature to change: ${deviceFeature}`); + } +} + +module.exports = { + newValue, +}; diff --git a/server/services/rflink/lib/index.js b/server/services/rflink/lib/index.js new file mode 100644 index 0000000000..87903b1277 --- /dev/null +++ b/server/services/rflink/lib/index.js @@ -0,0 +1,47 @@ +// Events + +const { newValue } = require('./events/rflink.newValue'); +const { addNewDevice } = require('./commands/rflink.addNewDevice'); +const { addDevice } = require('./events/rflink.addDevice'); +const { message } = require('./events/rflink.message'); + +// COMMANDS +const { setValue } = require('./commands/rflink.setValue'); +const { connect } = require('./commands/rflink.connect'); +const { disconnect } = require('./commands/rflink.disconnect'); +const { listen } = require('./commands/rflink.listen'); +const { getNewDevices } = require('./commands/rflink.getNewDevice'); +const { pair } = require('./commands/rflink.milight.pair'); +const { unpair } = require('./commands/rflink.milight.unpair'); + +const RFlinkManager = function RFlinkManager(gladys, serviceId) { + this.gladys = gladys; + this.serviceId = serviceId; + this.connected = false; + this.ready = false; + this.scanInProgress = false; + this.newDevices = []; + this.devices = []; + this.currentMilightGateway = 'F746'; + this.milightBridges = {}; + this.sendUsb = null; +}; + +// Events + +RFlinkManager.prototype.message = message; +RFlinkManager.prototype.newValue = newValue; +RFlinkManager.prototype.addNewDevice = addNewDevice; +RFlinkManager.prototype.addDevice = addDevice; + +// Commands + +RFlinkManager.prototype.setValue = setValue; +RFlinkManager.prototype.connect = connect; +RFlinkManager.prototype.disconnect = disconnect; +RFlinkManager.prototype.listen = listen; +RFlinkManager.prototype.getNewDevices = getNewDevices; +RFlinkManager.prototype.pair = pair; +RFlinkManager.prototype.unpair = unpair; + +module.exports = RFlinkManager; diff --git a/server/services/rflink/package-lock.json b/server/services/rflink/package-lock.json new file mode 100644 index 0000000000..4bf33a617c --- /dev/null +++ b/server/services/rflink/package-lock.json @@ -0,0 +1,425 @@ +{ + "name": "gladys-rflink", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "gladys-rflink", + "cpu": [ + "x64", + "arm", + "arm64" + ], + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "@serialport/parser-readline": "11.0.0", + "serialport": "11.0.0" + } + }, + "node_modules/@serialport/binding-mock": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", + "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", + "dependencies": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@serialport/bindings-cpp": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-11.0.1.tgz", + "integrity": "sha512-3I1mniVg3osYuIUXxU0jB5AHPsxWmErmc3JC3WfUSlfXsjWMHkHfFzbW9Scuv/z/6DLCJIDyltabRa2FoW2qsQ==", + "hasInstallScript": true, + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "10.5.0", + "debug": "4.3.4", + "node-addon-api": "6.1.0", + "node-gyp-build": "4.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-10.5.0.tgz", + "integrity": "sha512-/uR/yT3jmrcwnl2FJU/2ySvwgo5+XpksDUR4NF/nwTS5i3CcuKS+FKi/tLzy1k8F+rCx5JzpiK+koqPqOUWArA==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-10.5.0.tgz", + "integrity": "sha512-0aXJknodcl94W9zSjvU+sLdXiyEG2rqjQmvBWZCr8wJZjWEtv3RgrnYiWq4i2OTOyC8C/oPK8ZjpBjQptRsoJQ==", + "dependencies": { + "@serialport/parser-delimiter": "10.5.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-interface": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", + "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==", + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/@serialport/parser-byte-length": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-11.0.0.tgz", + "integrity": "sha512-rExsdFKdzOIHOBqTwzxUF1A9nrluVIZKZOtvMq5i0Hc3euooGdmkx0VXYNRlI2rd6kJLTL2P+uIR+ZtCTRyT+w==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-cctalk": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-11.0.0.tgz", + "integrity": "sha512-eN1MvEIFwI4GedWJhte6eWF+NZtrjchZbMf0CE6NV9TRzJI1KLnFf90ZOj/mhGuANojX4sqWfJKQXwN6E8VSHQ==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-delimiter": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-11.0.0.tgz", + "integrity": "sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-inter-byte-timeout": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-11.0.0.tgz", + "integrity": "sha512-RLgqZC50IET6FtEIt6Oi0vdRsesSBWLNwB7ldzR9OzyXKgK0XHRzqKqbB0u5Q+tC5OScdWeiQ2AO6jooKUZtsw==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-packet-length": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-11.0.0.tgz", + "integrity": "sha512-6ZkOiaCooabpV/EM7ttSRbisbDWpGEf7Yxyr13t28LicYR43THRdjdMZcRnWxEM/jpwfskkLLXAR6wziVpKrlw==", + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@serialport/parser-readline": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-11.0.0.tgz", + "integrity": "sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==", + "dependencies": { + "@serialport/parser-delimiter": "11.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-ready": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-11.0.0.tgz", + "integrity": "sha512-lSsCPIctoc5kADCKnZDYBz1j69TsFqtnaWUicBzUAIAoUXpYKeYld8YX5NrvjViuVfIJeiqLZeGjxOWe5fqQqQ==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-regex": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-11.0.0.tgz", + "integrity": "sha512-aKuc/+/KE9swahTbYpSuOsQa7LggPx7jhfobJLPVVbAic80OpfCIY+MKr6Ax4R6UtQwF90O5Yk6OEmbbvtEmiA==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-slip-encoder": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-11.0.0.tgz", + "integrity": "sha512-3ZI/swd2it20vmu2tzqDbkyE4dqy+kExEDY6T33YQ210HDKPVhqj7FAVGo5P++MZ3dup1of11t4P9UPBNkuJnQ==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-spacepacket": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-11.0.0.tgz", + "integrity": "sha512-+hqRckrTEqz+/uAUZY0Tq6YIRyCl4oQOH1MeVzKiFiGNjZP7hDJCDoY7LTr9CeJhxvcT0ItTbtjGBqGumV8fxg==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-11.0.0.tgz", + "integrity": "sha512-Zty7B8C1H2XRnay2mVmW1ygEHXRHXQDcaC5wAVvOZMbQSc7ye03rMlPvviDS+pGxU2t2A2bMo34CUrRduSBong==", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "debug": "4.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "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/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/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "node_modules/node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/serialport": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-11.0.0.tgz", + "integrity": "sha512-bxs3XejQcOHWpzPAaXMhxVRlbem6fjNUrux3ToqrGvFR6BcjOYhqE5CsHOuutv37kmhmnuHrn+/hN+1BpTmaFg==", + "dependencies": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "11.0.1", + "@serialport/parser-byte-length": "11.0.0", + "@serialport/parser-cctalk": "11.0.0", + "@serialport/parser-delimiter": "11.0.0", + "@serialport/parser-inter-byte-timeout": "11.0.0", + "@serialport/parser-packet-length": "11.0.0", + "@serialport/parser-readline": "11.0.0", + "@serialport/parser-ready": "11.0.0", + "@serialport/parser-regex": "11.0.0", + "@serialport/parser-slip-encoder": "11.0.0", + "@serialport/parser-spacepacket": "11.0.0", + "@serialport/stream": "11.0.0", + "debug": "4.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + } + }, + "dependencies": { + "@serialport/binding-mock": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", + "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", + "requires": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + } + }, + "@serialport/bindings-cpp": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-11.0.1.tgz", + "integrity": "sha512-3I1mniVg3osYuIUXxU0jB5AHPsxWmErmc3JC3WfUSlfXsjWMHkHfFzbW9Scuv/z/6DLCJIDyltabRa2FoW2qsQ==", + "requires": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "10.5.0", + "debug": "4.3.4", + "node-addon-api": "6.1.0", + "node-gyp-build": "4.6.0" + }, + "dependencies": { + "@serialport/parser-delimiter": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-10.5.0.tgz", + "integrity": "sha512-/uR/yT3jmrcwnl2FJU/2ySvwgo5+XpksDUR4NF/nwTS5i3CcuKS+FKi/tLzy1k8F+rCx5JzpiK+koqPqOUWArA==" + }, + "@serialport/parser-readline": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-10.5.0.tgz", + "integrity": "sha512-0aXJknodcl94W9zSjvU+sLdXiyEG2rqjQmvBWZCr8wJZjWEtv3RgrnYiWq4i2OTOyC8C/oPK8ZjpBjQptRsoJQ==", + "requires": { + "@serialport/parser-delimiter": "10.5.0" + } + } + } + }, + "@serialport/bindings-interface": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", + "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==" + }, + "@serialport/parser-byte-length": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-11.0.0.tgz", + "integrity": "sha512-rExsdFKdzOIHOBqTwzxUF1A9nrluVIZKZOtvMq5i0Hc3euooGdmkx0VXYNRlI2rd6kJLTL2P+uIR+ZtCTRyT+w==" + }, + "@serialport/parser-cctalk": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-11.0.0.tgz", + "integrity": "sha512-eN1MvEIFwI4GedWJhte6eWF+NZtrjchZbMf0CE6NV9TRzJI1KLnFf90ZOj/mhGuANojX4sqWfJKQXwN6E8VSHQ==" + }, + "@serialport/parser-delimiter": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-11.0.0.tgz", + "integrity": "sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==" + }, + "@serialport/parser-inter-byte-timeout": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-11.0.0.tgz", + "integrity": "sha512-RLgqZC50IET6FtEIt6Oi0vdRsesSBWLNwB7ldzR9OzyXKgK0XHRzqKqbB0u5Q+tC5OScdWeiQ2AO6jooKUZtsw==" + }, + "@serialport/parser-packet-length": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-11.0.0.tgz", + "integrity": "sha512-6ZkOiaCooabpV/EM7ttSRbisbDWpGEf7Yxyr13t28LicYR43THRdjdMZcRnWxEM/jpwfskkLLXAR6wziVpKrlw==" + }, + "@serialport/parser-readline": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-11.0.0.tgz", + "integrity": "sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==", + "requires": { + "@serialport/parser-delimiter": "11.0.0" + } + }, + "@serialport/parser-ready": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-11.0.0.tgz", + "integrity": "sha512-lSsCPIctoc5kADCKnZDYBz1j69TsFqtnaWUicBzUAIAoUXpYKeYld8YX5NrvjViuVfIJeiqLZeGjxOWe5fqQqQ==" + }, + "@serialport/parser-regex": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-11.0.0.tgz", + "integrity": "sha512-aKuc/+/KE9swahTbYpSuOsQa7LggPx7jhfobJLPVVbAic80OpfCIY+MKr6Ax4R6UtQwF90O5Yk6OEmbbvtEmiA==" + }, + "@serialport/parser-slip-encoder": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-11.0.0.tgz", + "integrity": "sha512-3ZI/swd2it20vmu2tzqDbkyE4dqy+kExEDY6T33YQ210HDKPVhqj7FAVGo5P++MZ3dup1of11t4P9UPBNkuJnQ==" + }, + "@serialport/parser-spacepacket": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-11.0.0.tgz", + "integrity": "sha512-+hqRckrTEqz+/uAUZY0Tq6YIRyCl4oQOH1MeVzKiFiGNjZP7hDJCDoY7LTr9CeJhxvcT0ItTbtjGBqGumV8fxg==" + }, + "@serialport/stream": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-11.0.0.tgz", + "integrity": "sha512-Zty7B8C1H2XRnay2mVmW1ygEHXRHXQDcaC5wAVvOZMbQSc7ye03rMlPvviDS+pGxU2t2A2bMo34CUrRduSBong==", + "requires": { + "@serialport/bindings-interface": "1.2.2", + "debug": "4.3.4" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==" + }, + "serialport": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-11.0.0.tgz", + "integrity": "sha512-bxs3XejQcOHWpzPAaXMhxVRlbem6fjNUrux3ToqrGvFR6BcjOYhqE5CsHOuutv37kmhmnuHrn+/hN+1BpTmaFg==", + "requires": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "11.0.1", + "@serialport/parser-byte-length": "11.0.0", + "@serialport/parser-cctalk": "11.0.0", + "@serialport/parser-delimiter": "11.0.0", + "@serialport/parser-inter-byte-timeout": "11.0.0", + "@serialport/parser-packet-length": "11.0.0", + "@serialport/parser-readline": "11.0.0", + "@serialport/parser-ready": "11.0.0", + "@serialport/parser-regex": "11.0.0", + "@serialport/parser-slip-encoder": "11.0.0", + "@serialport/parser-spacepacket": "11.0.0", + "@serialport/stream": "11.0.0", + "debug": "4.3.4" + } + } + } +} diff --git a/server/services/rflink/package.json b/server/services/rflink/package.json new file mode 100644 index 0000000000..f55f8644fb --- /dev/null +++ b/server/services/rflink/package.json @@ -0,0 +1,24 @@ +{ + "author": { + "name": "mathis tondenier" + }, + "contributors": [ + "Nicolas Geissel (https://github.com/ngeissel/)" + ], + "name": "gladys-rflink", + "main": "index.js", + "os": [ + "darwin", + "linux", + "win32" + ], + "cpu": [ + "x64", + "arm", + "arm64" + ], + "dependencies": { + "serialport": "11.0.0", + "@serialport/parser-readline": "11.0.0" + } +} diff --git a/server/test/services/rflink/SerialPortMock.test.js b/server/test/services/rflink/SerialPortMock.test.js new file mode 100755 index 0000000000..9a1a9a94aa --- /dev/null +++ b/server/test/services/rflink/SerialPortMock.test.js @@ -0,0 +1,32 @@ +const { fake } = require('sinon'); + +const EventEmitter = require('events'); + +const ReadlineParserMock = function ReadlineParser() {}; + +ReadlineParserMock.prototype = Object.create(new EventEmitter()); + +const SerialPort = function SerialPort(path) { + this.path = path; +}; + +SerialPort.prototype = Object.create(new EventEmitter()); +SerialPort.prototype.write = fake.returns(true); +SerialPort.prototype.open = fake.returns(true); + +const path = '/dev/tty.HC-05-DevB'; + +const ports = new SerialPort(path, { + baudRate: 57600, + dataBits: 8, + parity: 'none', + autoOpen: false, +}); + +SerialPort.list = () => { + return new Promise((resolve, reject) => { + resolve(ports); + }); +}; + +module.exports = { SerialPort, ReadlineParserMock }; diff --git a/server/test/services/rflink/controllers/rflink.controller.test.js b/server/test/services/rflink/controllers/rflink.controller.test.js new file mode 100755 index 0000000000..a9eeb94a70 --- /dev/null +++ b/server/test/services/rflink/controllers/rflink.controller.test.js @@ -0,0 +1,293 @@ +const sinon = require('sinon'); + +const { assert, fake, stub } = sinon; +const { expect } = require('chai'); +const RFLinkController = require('../../../../services/rflink/api/rflink.controller'); + +const serviceId = '6d1bd783-ab5c-4d90-8551-6bc5fcd02212'; +const gladys = { + service: { + getLocalServiceByName: stub().resolves({ + id: serviceId, + }), + }, + event: { + emit: fake.returns(null), + }, + stateManager: { + get: fake.returns(null), + }, + variable: { + getValue: stub() + .withArgs('RFLINK_PATH', serviceId) + .resolves('//tty'), + }, +}; + +const rflinkHandler = { + gladys: fake.returns(gladys), + serviceId: fake.returns(serviceId), + connected: false, + ready: false, + scanInProgress: false, + newDevices: [], + devices: fake.returns([]), + currentMilightGateway: fake.returns('F746'), + milightBridges: fake.returns({}), + pair: stub().resolves(), + unpair: stub().resolves(), + getNewDevices: fake.resolves(true), + connect: stub() + .withArgs('rflinkPath') + .resolves('connected'), + disconnect: stub().resolves(), + sendUsb: { + write: stub() + .withArgs('command') + .resolves(), + }, +}; + +describe('POST /api/v1/service/rflink/connect', () => { + let controller; + + beforeEach(() => { + controller = RFLinkController(gladys, rflinkHandler, serviceId); + sinon.reset(); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should connect successfully', async () => { + const req = {}; + const res = { + json: fake.returns(null), + }; + gladys.variable.getValue = stub() + .withArgs('RFLINK_PATH') + .resolves('//tty'); + await controller['post /api/v1/service/rflink/connect'].controller(req, res); + assert.calledOnce(gladys.variable.getValue); + assert.calledOnce(rflinkHandler.connect); + assert.calledWith(res.json, { success: true }); + }); + + it('should raise an error on connection when no path is given', async () => { + rflinkHandler.connect = fake.throws(new Error('path null')); + gladys.variable.getValue = stub() + .withArgs('RFLINK_PATH', serviceId) + .resolves(''); + const req = {}; + const res = { + json: fake.returns(null), + }; + await controller['post /api/v1/service/rflink/connect'].controller(req, res); + assert.calledOnce(rflinkHandler.connect); + // assert.calledWith(res.json, { success: false }); + expect(rflinkHandler.connected).to.be.deep.equal(false); + }); +}); + +describe('POST /api/v1/service/rflink/disconnect', () => { + let controller; + + beforeEach(() => { + controller = RFLinkController(gladys, rflinkHandler, serviceId); + }); + + it('should disconnect successfully', async () => { + const req = {}; + const res = { + json: fake.returns(null), + }; + await controller['post /api/v1/service/rflink/disconnect'].controller(req, res); + assert.calledOnce(rflinkHandler.disconnect); + assert.calledOnce(res.json); + }); +}); + +describe('POST /api/v1/service/rflink/pair', () => { + let controller; + + beforeEach(() => { + gladys.variable.getValue = stub() + .withArgs('CURRENT_MILIGHT_GATEWAY') + .resolves('64b'); + controller = RFLinkController(gladys, rflinkHandler, serviceId); + }); + + it('should send a milight pairing command', async () => { + const req = { + body: { + zone: 42, + }, + }; + const res = { + json: fake.returns(null), + }; + await controller['post /api/v1/service/rflink/pair'].controller(req, res); + assert.calledOnce(gladys.variable.getValue); + assert.calledOnce(res.json); + }); + + it('should send a milight pairing command even without a milight zone defined nor a gateway', async () => { + gladys.variable.getValue = stub() + .withArgs('CURRENT_MILIGHT_GATEWAY') + .resolves(null); + controller = RFLinkController(gladys, rflinkHandler, serviceId); + const req = { + body: { + zone: undefined, + }, + }; + const res = { + json: fake.returns(null), + }; + await controller['post /api/v1/service/rflink/pair'].controller(req, res); + assert.calledWith(rflinkHandler.pair, rflinkHandler.currentMilightGateway, 1); + assert.calledOnce(res.json); + }); +}); + +describe('POST /api/v1/service/rflink/unpair', () => { + let controller; + + beforeEach(() => { + sinon.reset(); + controller = RFLinkController(gladys, rflinkHandler, serviceId); + }); + + it('should send a milight unpairing command even without a milight zone defined nor a gateway', async () => { + gladys.variable.getValue = stub() + .withArgs('CURRENT_MILIGHT_GATEWAY') + .resolves(null); + const req = { + body: { + zone: undefined, + }, + }; + const res = { + json: fake.returns(null), + }; + await controller['post /api/v1/service/rflink/unpair'].controller(req, res); + assert.calledWith(rflinkHandler.unpair, rflinkHandler.currentMilightGateway, 1); + assert.calledOnce(rflinkHandler.unpair); + assert.calledOnce(res.json); + }); + + it('should send a milight unpairing command', async () => { + const req = { + body: { + zone: 42, + }, + }; + const res = { + json: fake.returns(null), + }; + await controller['post /api/v1/service/rflink/unpair'].controller(req, res); + assert.calledOnce(rflinkHandler.unpair); + assert.calledOnce(res.json); + }); +}); + +describe('GET /api/v1/service/rflink/newDevices', () => { + let controller; + + beforeEach(() => { + controller = RFLinkController(gladys, rflinkHandler, serviceId); + }); + + it('should get new devices', async () => { + const req = {}; + const res = { + json: fake.returns(null), + }; + await controller['get /api/v1/service/rflink/newDevices'].controller(req, res); + assert.calledOnce(rflinkHandler.getNewDevices); + assert.calledOnce(res.json); + }); +}); + +describe('GET /api/v1/service/rflink/status', () => { + let controller; + + beforeEach(() => { + controller = RFLinkController(gladys, rflinkHandler, serviceId); + }); + + it('should get rflink status', async () => { + const req = {}; + const res = { + json: fake.returns(null), + }; + await controller['get /api/v1/service/rflink/status'].controller(req, res); + assert.calledOnce(res.json); + }); +}); + +describe('POST /api/v1/service/rflink/debug', () => { + let controller; + + beforeEach(() => { + controller = RFLinkController(gladys, rflinkHandler, serviceId); + }); + + it('should send a debug test message', async () => { + const req = { + body: { + value: '10;MiLightv1;9926;02;34BC;ON;', + }, + }; + const command = '10;MiLightv1;9926;02;34BC;ON;\n'; + const res = { + json: fake.returns(null), + }; + await controller['post /api/v1/service/rflink/debug'].controller(req, res); + assert.calledWith(rflinkHandler.sendUsb.write, command); + assert.calledOnce(res.json); + }); +}); + +describe('POST /api/v1/service/rflink/remove', () => { + let controller; + + beforeEach(() => { + controller = RFLinkController(gladys, rflinkHandler, serviceId); + }); + + it('should remove a device from the new device list', async () => { + const externalId = `rflink:86aa7:11`; + const device = { + external_id: externalId, + }; + rflinkHandler.newDevices = [device]; + const req = { + body: { + external_id: externalId, + }, + }; + const res = { json: fake.returns(true) }; + await controller['post /api/v1/service/rflink/remove'].controller(req, res); + expect(rflinkHandler.newDevices).to.be.deep.equal([]); + assert.calledOnce(res.json); + }); + + it('should not remove a device from the new device list (because not found)', async () => { + const externalId = `rflink:86aa7:11`; + const device = { + external_id: externalId, + }; + rflinkHandler.newDevices = [device]; + const req = { + body: { + external_id: `rflink:86aa7:12`, + }, + }; + const res = { json: fake.returns(true) }; + await controller['post /api/v1/service/rflink/remove'].controller(req, res); + expect(rflinkHandler.newDevices).to.be.deep.equal([device]); + assert.calledOnce(res.json); + }); +}); diff --git a/server/test/services/rflink/controllers/rflink.parse.ObjtoRF.test.js b/server/test/services/rflink/controllers/rflink.parse.ObjtoRF.test.js new file mode 100755 index 0000000000..ff6fbb111d --- /dev/null +++ b/server/test/services/rflink/controllers/rflink.parse.ObjtoRF.test.js @@ -0,0 +1,34 @@ +const { expect } = require('chai'); +const ObjToRF = require('../../../../services/rflink/api/rflink.parse.ObjToRF'); + +describe('rflink.parse.ObjToRF', () => { + it('should return a string rfcode', async () => { + const device = { + id: '86aa7', + switch: 'switch', + external_id: `rflink:86aa7:11`, + model: 'Tristate', + }; + const deviceFeature = 'SWITCH'; + const state = 'ON'; + const rfcode = ObjToRF(device, deviceFeature, state); + const expected = '10;Tristate;86aa7;11;ON;\n'; + expect(rfcode).to.be.an('string'); + expect(rfcode).to.deep.equal(expected); + }); + + it('should return a string rfcode without channel nor state', async () => { + const device = { + id: '86aa7', + switch: 'switch', + external_id: `rflink:86aa7`, + model: 'Tristate', + }; + const deviceFeature = 'SWITCH'; + const state = undefined; + const rfcode = ObjToRF(device, deviceFeature, state); + const expected = '10;Tristate;86aa7;\n'; + expect(rfcode).to.be.an('string'); + expect(rfcode).to.deep.equal(expected); + }); +}); diff --git a/server/test/services/rflink/controllers/rflink.parse.RFtoObject.test.js b/server/test/services/rflink/controllers/rflink.parse.RFtoObject.test.js new file mode 100755 index 0000000000..34d7e53917 --- /dev/null +++ b/server/test/services/rflink/controllers/rflink.parse.RFtoObject.test.js @@ -0,0 +1,19 @@ +const { expect } = require('chai'); +const RfToObj = require('../../../../services/rflink/api/rflink.parse.RFtoObject'); + +describe('rflink.parse.RFtoObject', () => { + it('should return an array with an index for each value', async () => { + const data = '20;16;NewKaku;ID=00f79162;SWITCH=1;CMD=ON;'; + const newDevice = RfToObj(data); + const expected = { protocol: 'NewKaku', features: [], id: '00f79162', switch: '1', cmd: 'ON' }; + expect(newDevice).to.be.an('object'); + expect(newDevice).to.deep.equal(expected); + }); + + it('should return nothing', async () => { + const data = '20;16;OK;;;;'; + const newDevice = RfToObj(data); + const expected = {}; + expect(newDevice).to.deep.equal(expected); + }); +}); diff --git a/server/test/services/rflink/lib/addDevice.test.js b/server/test/services/rflink/lib/addDevice.test.js new file mode 100755 index 0000000000..6454203a25 --- /dev/null +++ b/server/test/services/rflink/lib/addDevice.test.js @@ -0,0 +1,30 @@ +const sinon = require('sinon'); +const chai = require('chai'); +const proxyquire = require('proxyquire').noCallThru(); +const SerialPortMock = require('../SerialPortMock.test'); + +const RFLinkHandler = proxyquire('../../../../services/rflink/lib', { + serialport: SerialPortMock, +}); + +const { expect } = chai; + +describe('RFLinkHandler.addDevice', () => { + beforeEach(() => { + sinon.reset(); + }); + + it('should add new devices', async () => { + const gladys = {}; + const rflinkHandler = new RFLinkHandler(gladys, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'); + const deviceList = ['1', '2']; + rflinkHandler.addDevice(deviceList); + expect(rflinkHandler.devices) + .to.be.an('array') + .that.deep.equal(deviceList); + rflinkHandler.addDevice(undefined); + expect(rflinkHandler.devices) + .to.be.an('array') + .that.deep.equal(deviceList); + }); +}); diff --git a/server/test/services/rflink/lib/addNewDevice.test.js b/server/test/services/rflink/lib/addNewDevice.test.js new file mode 100755 index 0000000000..6888f5982d --- /dev/null +++ b/server/test/services/rflink/lib/addNewDevice.test.js @@ -0,0 +1,55 @@ +const sinon = require('sinon'); +const chai = require('chai'); +const proxyquire = require('proxyquire').noCallThru(); +const SerialPortMock = require('../SerialPortMock.test'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); +const DEVICES = require('./devicesToTest.test'); + +const { assert, fake } = sinon; +const { expect } = chai; + +const RFLinkHandler = proxyquire('../../../../services/rflink/lib', { + serialport: SerialPortMock, +}); + +describe('RFLinkHandler.addDevice', () => { + let rflinkHandler; + let gladys; + const event = { emit: fake.returns(null) }; + beforeEach(() => { + sinon.reset(); + gladys = { event }; + rflinkHandler = new RFLinkHandler(gladys, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'); + rflinkHandler.addDevice(DEVICES); + }); + + it('should add new devices', async () => { + const device = { + external_id: `rflink:86aa7:666`, + }; + expect(rflinkHandler.newDevices).to.have.lengthOf(0); + expect(rflinkHandler.devices) + .to.be.an('array') + .that.deep.equal(DEVICES); + rflinkHandler.addNewDevice(device); + assert.calledOnce(gladys.event.emit); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.RFLINK.NEW_DEVICE, + }); + expect(rflinkHandler.newDevices).to.have.lengthOf(1); + expect(rflinkHandler.newDevices) + .to.be.an('array') + .that.includes(device); + }); + + it('should not add 2 same devices in the list', async () => { + const device = DEVICES[0]; + expect(rflinkHandler.newDevices).to.have.lengthOf(0); + expect(rflinkHandler.devices) + .to.be.an('array') + .that.deep.equal(DEVICES); + rflinkHandler.addNewDevice(device); + assert.notCalled(gladys.event.emit); + expect(rflinkHandler.newDevices).to.have.lengthOf(0); + }); +}); diff --git a/server/test/services/rflink/lib/connect.test.js b/server/test/services/rflink/lib/connect.test.js new file mode 100755 index 0000000000..62fdda8be7 --- /dev/null +++ b/server/test/services/rflink/lib/connect.test.js @@ -0,0 +1,62 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); +const os = require('os'); +const proxyquire = require('proxyquire').noCallThru(); +const SerialPortMock = require('../SerialPortMock.test'); + +const RFLinkHandler = proxyquire('../../../../services/rflink/lib', { + serialport: SerialPortMock, +}); + +const { assert, fake, stub } = sinon; +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); + +let rflinkHandler; +let gladys; + +describe('RFLinkHandler.connect', () => { + gladys = { + event: { + emit: fake.returns(null), + }, + }; + rflinkHandler = new RFLinkHandler(gladys, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'); + beforeEach(() => { + sinon.reset(); + }); + + it('should connect and receive success', async () => { + const path = '/tty1'; + await rflinkHandler.connect(path); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.RFLINK.DRIVER_READY, + }); + expect(rflinkHandler.path).to.be.equal('/tty1'); + expect(rflinkHandler.connected).to.be.equal(true); + expect(rflinkHandler.ready).to.be.equal(true); + expect(rflinkHandler.scanInProgress).to.be.equal(true); + }); + + it('should adapt the path depending on the os platform', async () => { + const osStub = stub(os, 'platform').returns('darwin'); + const path = '/dev/tty.'; + await rflinkHandler.connect(path); + assert.calledOnce(osStub); + expect(rflinkHandler.path).to.be.equal('/dev/cu.'); + }); + + it('should fail connection with a non defined path', async () => { + rflinkHandler.listen = stub(); + const path = ''; + try { + await rflinkHandler.connect(path); + assert.fail(); + } catch (e) { + assert.match(e.message, 'RFLINK_PATH_NOT_FOUND'); + } + assert.notCalled(rflinkHandler.listen); + expect(rflinkHandler.connected).to.be.equal(false); + expect(rflinkHandler.ready).to.be.equal(false); + expect(rflinkHandler.scanInProgress).to.be.equal(false); + }); +}); diff --git a/server/test/services/rflink/lib/devicesToTest.test.js b/server/test/services/rflink/lib/devicesToTest.test.js new file mode 100644 index 0000000000..843047f884 --- /dev/null +++ b/server/test/services/rflink/lib/devicesToTest.test.js @@ -0,0 +1,129 @@ +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); + +const DEVICES = [ + { + id: '86aa7', + switch: 'switch', + service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279', + name: `Prise `, + selector: `rflink:86aa7:11`, + external_id: `rflink:86aa7:11`, + model: 'Tristate', + should_poll: false, + features: [ + { + name: 'switch', + selector: `rflink:86aa7:switch:11`, + external_id: `rflink:86aa7:switch:11`, + rfcode: 'CMD', + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + type: DEVICE_FEATURE_TYPES.SENSOR.BINARY, + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 1, + }, + ], + }, + { + id: '86aa70', + switch: 'switch', + service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279', + name: `Prise `, + selector: `rflink:86aa70:10`, + external_id: `rflink:86aa70:10`, + model: 'Tristate', + should_poll: false, + features: [ + { + name: 'switch', + selector: `rflink:86aa70:switch:10`, + external_id: `rflink:86aa70:switch:10`, + rfcode: 'CMD', + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + type: DEVICE_FEATURE_TYPES.SENSOR.BINARY, + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 1, + }, + ], + }, + { + id: '86aa70', + switch: 'switch', + service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279', + name: `MiLightv1 switch 86aa70-00`, + selector: `rflink:86aa70:00`, + external_id: `rflink:86aa70:00`, + model: 'Milightv1', + should_poll: false, + features: [ + { + name: 'Power', + selector: `rflink:86aa70:power:00:milight`, + external_id: `rflink:86aa70:power:00:milight`, + rfcode: { + value: 'CMD', + cmd: 'ON', + }, + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.BINARY, + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 1, + }, + { + name: 'color', + selector: `rflink:86aa70:color:00:milight`, + external_id: `rflink:86aa70:color:00:milight`, + rfcode: { + value: 'RGBW', + cmd: 'COLOR', + }, + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.COLOR, + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 255, + }, + { + name: 'brightness', + selector: `rflink:86aa70:brightness:00:milight`, + external_id: `rflink:86aa70:brightness:00:milight`, + rfcode: { + value: 'RGBW', + cmd: 'BRIGHT', + }, + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.BRIGHTNESS, + read_only: false, + keep_history: true, + has_feedback: false, + min: 0, + max: 100, + }, + { + name: 'milight-mode', + selector: `rflink:86aa70:milight-mode:00:milight`, + external_id: `rflink:86aa70:milight-mode:00:milight`, + rfcode: 'CMD', + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.EFFECT_MODE, + read_only: false, + keep_history: true, + has_feedback: false, + min: 1, + max: 8, + }, + ], + }, +]; + +module.exports = DEVICES; diff --git a/server/test/services/rflink/lib/disconnect.test.js b/server/test/services/rflink/lib/disconnect.test.js new file mode 100755 index 0000000000..84a5952e41 --- /dev/null +++ b/server/test/services/rflink/lib/disconnect.test.js @@ -0,0 +1,48 @@ +const sinon = require('sinon'); +const chai = require('chai'); +const proxyquire = require('proxyquire').noCallThru(); +const SerialPortMock = require('../SerialPortMock.test'); + +const RFLinkHandler = proxyquire('../../../../services/rflink/lib', { + serialport: SerialPortMock, +}); + +const { assert, fake, stub } = sinon; +const { expect } = chai; + +describe('RFLinkHandler.disconnect', () => { + let rflinkHandler; + let gladys; + + beforeEach(() => { + gladys = { + event: { + emit: fake.returns(null), + }, + }; + rflinkHandler = new RFLinkHandler(gladys, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'); + sinon.reset(); + }); + + it('should disconnect by closing opened port and set right state of handler', async () => { + rflinkHandler.path = '/tty'; + rflinkHandler.connected = true; + rflinkHandler.sendUsb = { close: stub().resolves(true) }; + await rflinkHandler.disconnect(); + assert.calledOnce(rflinkHandler.sendUsb.close); + expect(rflinkHandler.connected).to.equal(false); + expect(rflinkHandler.ready).to.equal(false); + expect(rflinkHandler.scanInProgress).to.equal(false); + }); + + it('should set right state of handler and do nothing else since port is not opened', async () => { + rflinkHandler.path = ''; + rflinkHandler.connected = false; + rflinkHandler.sendUsb = { close: stub().resolves(true) }; + await rflinkHandler.disconnect(); + assert.notCalled(rflinkHandler.sendUsb.close); + expect(rflinkHandler.connected).to.equal(false); + expect(rflinkHandler.ready).to.equal(false); + expect(rflinkHandler.scanInProgress).to.equal(false); + }); +}); diff --git a/server/test/services/rflink/lib/getNewDevice.test.js b/server/test/services/rflink/lib/getNewDevice.test.js new file mode 100755 index 0000000000..cc67cf5ce8 --- /dev/null +++ b/server/test/services/rflink/lib/getNewDevice.test.js @@ -0,0 +1,31 @@ +const sinon = require('sinon'); +const chai = require('chai'); +const proxyquire = require('proxyquire').noCallThru(); +const SerialPortMock = require('../SerialPortMock.test'); + +const RFLinkHandler = proxyquire('../../../../services/rflink/lib', { + serialport: SerialPortMock, +}); + +const { fake } = sinon; +const { expect } = chai; + +describe('RFLinkHandler.getNewDevice', () => { + beforeEach(() => { + sinon.reset(); + }); + + it('should get new devices', async () => { + const gladys = { + event: { + emit: fake.returns(null), + }, + }; + const rflinkHandler = new RFLinkHandler(gladys, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'); + rflinkHandler.newDevices = ['1']; + const devices = rflinkHandler.getNewDevices(); + expect(devices) + .to.be.an('array') + .that.deep.equal(['1']); + }); +}); diff --git a/server/test/services/rflink/lib/listen.test.js b/server/test/services/rflink/lib/listen.test.js new file mode 100755 index 0000000000..f9128da163 --- /dev/null +++ b/server/test/services/rflink/lib/listen.test.js @@ -0,0 +1,32 @@ +const sinon = require('sinon'); + +const { ReadlineParserMock } = require('../SerialPortMock.test'); +const RFLinkHandler = require('../../../../services/rflink/lib'); + +const { assert, stub } = sinon; + +describe('RFLinkHandler.listen', () => { + let rflinkHandler; + let gladys; + + beforeEach(() => { + gladys = {}; + rflinkHandler = new RFLinkHandler(gladys, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'); + sinon.reset(); + }); + + it('should listen', async () => { + rflinkHandler.usb = { on: stub().resolves('data') }; + rflinkHandler.message = stub().returns(true); + rflinkHandler.listen(); + assert.calledOnce(rflinkHandler.usb.on); + }); + + it('should store the message listened', async () => { + rflinkHandler.usb = new ReadlineParserMock(); + rflinkHandler.message = stub().returns(true); + rflinkHandler.listen(); + rflinkHandler.usb.emit('data'); + assert.calledOnce(rflinkHandler.message); + }); +}); diff --git a/server/test/services/rflink/lib/message.test.js b/server/test/services/rflink/lib/message.test.js new file mode 100755 index 0000000000..a3cd5dcac2 --- /dev/null +++ b/server/test/services/rflink/lib/message.test.js @@ -0,0 +1,184 @@ +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); +const SerialPortMock = require('../SerialPortMock.test'); + +const RFLinkHandler = proxyquire('../../../../services/rflink/lib', { + serialport: SerialPortMock, +}); + +const { assert, fake, stub } = sinon; +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); + +describe('RFLinkHandler.message', () => { + let gladys; + let rflinkHandler; + const serviceId = 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'; + + beforeEach(() => { + gladys = { + event: { + emit: fake.returns(null), + }, + }; + rflinkHandler = new RFLinkHandler(gladys, serviceId); + rflinkHandler.addNewDevice = stub(); + rflinkHandler.newValue = fake.returns(true); + }); + + it('should get a message from the RFLink with success', async () => { + const msgRF = '20;16;NewKaku;ID=00f79162;SWITCH=1;CMD=ON;'; + + await rflinkHandler.message(msgRF); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.RFLINK.NEW_MESSAGE, + }); + }); + + it('should not get a message from the RFLink', async () => { + const msgRF = '20;03;OK;'; + await rflinkHandler.message(msgRF); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.RFLINK.NEW_MESSAGE, + }); + assert.notCalled(rflinkHandler.newValue); + assert.notCalled(rflinkHandler.addNewDevice); + }); + + it('should add a new sensor', async () => { + // this "super" device is a sample of all the paramater that could be handled + const msgRF = + '20;e5;Oregon BTHR;ID=5a6d;TEMP=00be;HUM=40;BARO=03d7;UV=40;LUX=40;BAT=OK;RAIN=0010;WINDIR=0002;WINSP=0060;WINGS=0088;AWINSP=0060;CO2=50;RGBW=50;'; + await rflinkHandler.message(msgRF); + assert.calledOnce(rflinkHandler.addNewDevice); + }); + + it('should add a new switch', async () => { + // this "super" device is a sample of all the paramater that could be handled + const msgRF = '20;e5;Oregon BTHR;ID=5a6d;RGBW=50;SWITCH=11;CMD=ON'; + await rflinkHandler.message(msgRF); + assert.calledOnce(rflinkHandler.addNewDevice); + }); + + it('should add a new milight device', async () => { + // this "super" device is a sample of all the paramater that could be handled + const msgRF = '20;29;MiLightv1;ID=5a6d;SWITCH=01;RGBW=07c8;CMD=MODE;'; + await rflinkHandler.message(msgRF); + assert.calledOnce(rflinkHandler.addNewDevice); + }); + + it('should update value of an existing sensor', async () => { + const msgRF = + '20;e5;Oregon BTHR;ID=5a6d;TEMP=00be;HUM=40;BARO=03d7;UV=40;LUX=40;BAT=OK;RAIN=0010;WINDIR=0002;WINSP=0060;WINGS=0088;AWINSP=0060;CO2=50;RGBW=50;'; + const device = { external_id: 'rflink:5a6d:undefined' }; + rflinkHandler.newDevices = [device]; + await rflinkHandler.message(msgRF); + const msg = { + id: '5a6d', + protocol: 'Oregon BTHR', + features: [], + temp: '00be', + baro: '03d7', + hum: '40', + uv: '40', + lux: '40', + bat: 'OK', + rain: '0010', + winsp: '0060', + windir: '0002', + wings: '0088', + awinsp: '0060', + co2: '50', + rgbw: '50', + }; + assert.notCalled(rflinkHandler.addNewDevice); + assert.callCount(rflinkHandler.newValue, 14); + assert.calledWith(rflinkHandler.newValue, msg, 'temperature', msg.temp); + assert.calledWith(rflinkHandler.newValue, msg, 'humidity', msg.hum); + assert.calledWith(rflinkHandler.newValue, msg, 'pressure', msg.baro); + assert.calledWith(rflinkHandler.newValue, msg, 'light-intensity', msg.lux); + assert.calledWith(rflinkHandler.newValue, msg, 'uv', msg.uv); + assert.calledWith(rflinkHandler.newValue, msg, 'battery', msg.bat); + assert.calledWith(rflinkHandler.newValue, msg, 'rain', msg.rain); + assert.calledWith(rflinkHandler.newValue, msg, 'wind-speed', msg.winsp); + assert.calledWith(rflinkHandler.newValue, msg, 'wind-dir', msg.windir); + assert.calledWith(rflinkHandler.newValue, msg, 'wind-speed', msg.wings); + assert.calledWith(rflinkHandler.newValue, msg, 'wind-speed', msg.awinsp); + assert.calledWith(rflinkHandler.newValue, msg, 'co2', msg.co2); + assert.calledWith(rflinkHandler.newValue, msg, 'color', msg.rgbw); + assert.calledWith(rflinkHandler.newValue, msg, 'brightness', msg.rgbw); + }); + + it('should add a new sensor (temperature, humidity, pressure)', async () => { + const msgRF = '20;e5;Oregon BTHR;ID=5a6d;TEMP=00be;HUM=40;BARO=03d7;BAT=OK;'; + await rflinkHandler.message(msgRF); + assert.calledOnce(rflinkHandler.addNewDevice); + }); + + it('should add a new wind sensor (direction, speed, chill, gust)', async () => { + const msgRF = '20;47;Cresta;ID=8001;WINDIR=0002;WINSP=0060;WINGS=0088;WINCHL=b0;'; + rflinkHandler.newDevices = []; + await rflinkHandler.message(msgRF); + assert.calledOnce(rflinkHandler.addNewDevice); + }); + + it('should add a new CO2 sensor', async () => { + const msgRF = '20;ba;Oregon UVN128/138;ID=ea7c;CO2=0030;BAT=OK;'; + await rflinkHandler.message(msgRF); + assert.calledOnce(rflinkHandler.addNewDevice); + }); + + it('should add a new uv sensor', async () => { + const msgRF = '20;ba;Oregon UVN128/138;ID=ea7c;UV=0030;BAT=OK;'; + await rflinkHandler.message(msgRF); + assert.calledOnce(rflinkHandler.addNewDevice); + }); + + it('should add a new rain sensor', async () => { + const msgRF = '20;08;UPM/Esic;ID=1003;RAIN=0010;BAT=OK;'; + await rflinkHandler.message(msgRF); + assert.calledOnce(rflinkHandler.addNewDevice); + }); + + it('should handle a message with a cmd SWITCH value of an existing device', async () => { + const msgRF = '20;47;Cresta;ID=8001;SWITCH=11;CMD=ON;'; + const device = { external_id: 'rflink:8001:11' }; + // store device + rflinkHandler.newDevices = [device]; + await rflinkHandler.message(msgRF); + + const msg = { protocol: 'Cresta', features: [], id: '8001', switch: '11', cmd: 'ON' }; + assert.notCalled(rflinkHandler.addNewDevice); + assert.calledWith(rflinkHandler.newValue, msg, 'switch', msg.cmd); + }); + + it('should handle a message with a cmd MODE value of an existing milight device', async () => { + const msgRF = '20;47;MiLightv1;ID=8001;CMD=MODE;'; + const device = { external_id: 'rflink:8001:undefined' }; + // store device + rflinkHandler.newDevices = [device]; + await rflinkHandler.message(msgRF); + + const msg = { protocol: 'MiLightv1', features: [], id: '8001', cmd: 'MODE' }; + assert.notCalled(rflinkHandler.addNewDevice); + assert.calledWith(rflinkHandler.newValue, msg, 'milight-mode', msg.cmd); + }); + + it('should handle a message with a cmd DISCO value of an existing milight device', async () => { + const msgRF = '20;47;MiLightv1;ID=8001;CMD=DISCO;'; + const device = { external_id: 'rflink:8001:undefined' }; + // store device + rflinkHandler.newDevices = [device]; + await rflinkHandler.message(msgRF); + + const msg = { protocol: 'MiLightv1', features: [], id: '8001', cmd: 'DISCO' }; + assert.notCalled(rflinkHandler.addNewDevice); + assert.calledWith(rflinkHandler.newValue, msg, 'milight-mode', msg.cmd); + }); + + it('should log an undefined id', async () => { + const msgRF = '20;08;UPM/Esic;10;RAINY=0010;BAT=OK;'; + await rflinkHandler.message(msgRF); + assert.notCalled(rflinkHandler.newValue); + assert.notCalled(rflinkHandler.addNewDevice); + }); +}); diff --git a/server/test/services/rflink/lib/newValue.test.js b/server/test/services/rflink/lib/newValue.test.js new file mode 100755 index 0000000000..be1f78334b --- /dev/null +++ b/server/test/services/rflink/lib/newValue.test.js @@ -0,0 +1,137 @@ +const sinon = require('sinon'); + +const proxyquire = require('proxyquire').noCallThru(); +const SerialPortMock = require('../SerialPortMock.test'); +const DEVICES = require('./devicesToTest.test'); + +const RFLinkHandler = proxyquire('../../../../services/rflink/lib', { + serialport: SerialPortMock, +}); + +const { assert, fake } = sinon; +const { EVENTS } = require('../../../../utils/constants'); + +describe('RFLinkHandler.newValue', () => { + let gladys; + let rflinkHandler; + + beforeEach(() => { + sinon.reset(); + gladys = { + event: { + emit: fake.returns(null), + }, + }; + rflinkHandler = new RFLinkHandler(gladys, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'); + }); + + it('should update a device with a new value state ON', async () => { + const device = DEVICES[0]; + const deviceFeature = 'temperature'; + const state = 'ON'; + + await rflinkHandler.newValue(device, deviceFeature, state); + + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: `rflink:${device.id}:${deviceFeature}:${device.switch}`, + state: 0.1, + }); + }); + + it('should update a device with a new value with a default state', async () => { + const device = DEVICES[0]; + const deviceFeature = 'default'; + const state = 'default'; + + await rflinkHandler.newValue(device, deviceFeature, state); + + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: `rflink:${device.id}:${deviceFeature}:${device.switch}`, + state: 'default', + }); + }); + + it('should update a device with a new value state OFF', async () => { + const device = DEVICES[0]; + const deviceFeature = 'temperature'; + const state = 'OFF'; + + await rflinkHandler.newValue(device, deviceFeature, state); + + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: `rflink:${device.id}:${deviceFeature}:${device.switch}`, + state: 0.0, + }); + }); + + it('should update a device with a new value of battery', async () => { + const device = DEVICES[0]; + const deviceFeature = 'battery'; + const state = 'ON'; + + await rflinkHandler.newValue(device, deviceFeature, state); + + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: `rflink:${device.id}:${deviceFeature}:${device.switch}`, + state: 'NA', + }); + }); + + it('should update a device with a new light-intensity', async () => { + const device = DEVICES[0]; + const deviceFeature = 'light-intensity'; + const state = 'ON'; + + await rflinkHandler.newValue(device, deviceFeature, state); + + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: `rflink:${device.id}:${deviceFeature}:${device.switch}`, + state: 1, + }); + }); + + it('should update a device with a new uv value', async () => { + const device = DEVICES[0]; + const deviceFeature = 'uv'; + const state = 'ON'; + + await rflinkHandler.newValue(device, deviceFeature, state); + + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: `rflink:${device.id}:${deviceFeature}:${device.switch}`, + state: 1, + }); + }); + + it('should update a device with a new pressure', async () => { + const device = DEVICES[0]; + const deviceFeature = 'pressure'; + const state = 'ON'; + + await rflinkHandler.newValue(device, deviceFeature, state); + + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: `rflink:${device.id}:${deviceFeature}:${device.switch}`, + state: 1, + }); + }); + + it('should not update a device on a undefined feature', async () => { + const device = DEVICES[0]; + const deviceFeature = undefined; + const state = 'ON'; + + await rflinkHandler.newValue(device, deviceFeature, state); + assert.notCalled(gladys.event.emit); + }); + + it('should not update a device on a undefined device id', async () => { + const device = DEVICES[0]; + device.id = undefined; + const deviceFeature = 'pressure'; + const state = 'ON'; + + await rflinkHandler.newValue(device, deviceFeature, state); + assert.notCalled(gladys.event.emit); + }); +}); diff --git a/server/test/services/rflink/lib/pair.milight.test.js b/server/test/services/rflink/lib/pair.milight.test.js new file mode 100755 index 0000000000..b2cd2e9b27 --- /dev/null +++ b/server/test/services/rflink/lib/pair.milight.test.js @@ -0,0 +1,52 @@ +const sinon = require('sinon'); +const chai = require('chai'); +const proxyquire = require('proxyquire').noCallThru(); +const SerialPortMock = require('../SerialPortMock.test'); + +const RFLinkHandler = proxyquire('../../../../services/rflink/lib', { + serialport: SerialPortMock, +}); + +const { fake } = sinon; +const { expect } = chai; + +describe('RFLinkHandler.pair', () => { + beforeEach(() => { + sinon.reset(); + }); + + it('should pair a milight device', async () => { + const gladys = { + event: { + emit: fake.returns(null), + }, + }; + + const currentMilightGateway = '1'; + const milightZone = '2'; + + const rflinkHandler = new RFLinkHandler(gladys, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'); + expect(rflinkHandler.newDevices).to.have.lengthOf(0); + await rflinkHandler.connect('/dev/tty1'); + await rflinkHandler.pair(currentMilightGateway, milightZone); + expect(rflinkHandler.newDevices).to.have.lengthOf(1); + }); + + it('should pair nothing', async () => { + const gladys = { + event: { + emit: fake.returns(null), + }, + }; + + const currentMilightGateway = undefined; + const milightZone = '2'; + + const rflinkHandler = new RFLinkHandler(gladys, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'); + rflinkHandler.sendUsb = SerialPortMock; + expect(rflinkHandler.newDevices).to.have.lengthOf(0); + await rflinkHandler.connect('/dev/tty1'); + await rflinkHandler.pair(currentMilightGateway, milightZone); + expect(rflinkHandler.newDevices).to.have.lengthOf(0); + }); +}); diff --git a/server/test/services/rflink/lib/setValue.test.js b/server/test/services/rflink/lib/setValue.test.js new file mode 100755 index 0000000000..3b52f40249 --- /dev/null +++ b/server/test/services/rflink/lib/setValue.test.js @@ -0,0 +1,214 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); +const proxyquire = require('proxyquire').noCallThru(); +const SerialPortMock = require('../SerialPortMock.test'); +const DEVICES = require('./devicesToTest.test'); +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); + +const RFLinkHandler = proxyquire('../../../../services/rflink/lib', { + serialport: SerialPortMock, +}); + +const { assert, fake, stub } = sinon; + +describe('RFLinkHandler.setValue', () => { + let gladys; + let rflinkHandler; + + beforeEach(() => { + sinon.reset(); + gladys = { + event: { + emit: fake.returns(null), + }, + }; + rflinkHandler = new RFLinkHandler(gladys, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'); + rflinkHandler.sendUsb = { + write: stub() + .withArgs('msg') + .resolves(), + }; + }); + + it('should send a message to change value of a SWITCH where feature is binary and state is 0', async () => { + const device = DEVICES[0]; + const deviceFeature = { + type: DEVICE_FEATURE_TYPES.SENSOR.BINARY, + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + }; + const state = 0; + const expectedMsg = '10;Tristate;86aa7;11;OFF;\n'; + await rflinkHandler.setValue(device, deviceFeature, state); + + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to change value of a SWITCH where feature is binary and state is 1', async () => { + const device = DEVICES[0]; + const deviceFeature = { + type: DEVICE_FEATURE_TYPES.SENSOR.BINARY, + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + }; + const state = 1; + const expectedMsg = '10;Tristate;86aa7;11;ON;\n'; + await rflinkHandler.setValue(device, deviceFeature, state); + + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to change value of a SWITCH where feature is binary and state is undefined', async () => { + const device = DEVICES[0]; + const deviceFeature = { + type: DEVICE_FEATURE_TYPES.SENSOR.BINARY, + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + }; + const state = 'undefined'; + const expectedMsg = '10;Tristate;86aa7;11;undefined;\n'; + await rflinkHandler.setValue(device, deviceFeature, state); + + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to change value of a BUTTON where feature is binary and state is 0', async () => { + const device = DEVICES[0]; + const deviceFeature = { + type: DEVICE_FEATURE_TYPES.SENSOR.BINARY, + category: DEVICE_FEATURE_CATEGORIES.BUTTON, + }; + const state = 0; + const expectedMsg = '10;Tristate;86aa7;11;DOWN;\n'; // cmd;model;deviceId;external_id last item;cmd + await rflinkHandler.setValue(device, deviceFeature, state); + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to change value of a BUTTON where feature is binary and state is false', async () => { + const device = DEVICES[0]; + const deviceFeature = { + type: DEVICE_FEATURE_TYPES.SENSOR.BINARY, + category: DEVICE_FEATURE_CATEGORIES.BUTTON, + }; + const state = false; + const expectedMsg = '10;Tristate;86aa7;11;DOWN;\n'; // cmd;model;deviceId;external_id last item;cmd + await rflinkHandler.setValue(device, deviceFeature, state); + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to change value of a BUTTON where feature is binary and state is 1', async () => { + const device = DEVICES[0]; + const deviceFeature = { + type: DEVICE_FEATURE_TYPES.SENSOR.BINARY, + category: DEVICE_FEATURE_CATEGORIES.BUTTON, + }; + const state = 1; + const expectedMsg = '10;Tristate;86aa7;11;UP;\n'; // cmd;model;deviceId;external_id last item;cmd + await rflinkHandler.setValue(device, deviceFeature, state); + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to change value of a BUTTON where feature is binary and state is true', async () => { + const device = DEVICES[0]; + const deviceFeature = { + type: DEVICE_FEATURE_TYPES.SENSOR.BINARY, + category: DEVICE_FEATURE_CATEGORIES.BUTTON, + }; + const state = true; + const expectedMsg = '10;Tristate;86aa7;11;UP;\n'; // cmd;model;deviceId;external_id last item;cmd + await rflinkHandler.setValue(device, deviceFeature, state); + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to power on (state 1) a Milight device', async () => { + // 10;MiLightv1;9926;00;fed0;ON; + const device = DEVICES[2]; + const deviceFeature = { + external_id: `rflink:86aa70:power:12:milight`, + }; + const state = 1; + const expectedMsg = '10;MiLightv1;86aa70;000;34BC;ON;\n'; // cmd;model;deviceId;external_id last item;cmd + await rflinkHandler.setValue(device, deviceFeature, state); + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to power off (state 0) a Milight device', async () => { + const device = DEVICES[2]; + const deviceFeature = { + external_id: `rflink:86aa70:power:12:milight`, + }; + const state = 0; + const expectedMsg = '10;MiLightv1;86aa70;000;34BC;OFF;\n'; // cmd;model;deviceId;external_id last item;cmd + await rflinkHandler.setValue(device, deviceFeature, state); + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message with non standard state to a Milight device', async () => { + // 10;MiLightv1;9926;00;fed0;ON; + const device = DEVICES[2]; + const deviceFeature = { + external_id: `rflink:86aa70:power:12:milight`, + }; + const state = 'undefined'; + const expectedMsg = '10;MiLightv1;86aa70;000;34BC;ON;\n'; // cmd;model;deviceId;external_id last item;cmd + await rflinkHandler.setValue(device, deviceFeature, state); + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to change the color of a Milight device', async () => { + const device = DEVICES[2]; + const deviceFeature = { + external_id: `rflink:86aa70:color:12:milight`, + }; + const state = 10; + const expectedMsg = '10;MiLightv1;86aa70;000;64;COLOR;\n'; // cmd;model;deviceId;external_id last item;cmd + await rflinkHandler.setValue(device, deviceFeature, state); + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to change the brightness of a Milight device', async () => { + const device = DEVICES[2]; + const deviceFeature = { + external_id: `rflink:86aa70:brightness:12:milight`, + }; + const state = 10; + const expectedMsg = '10;MiLightv1;86aa70;000;3417;BRIGHT;\n'; // cmd;model;deviceId;external_id last item;cmd + await rflinkHandler.setValue(device, deviceFeature, state); + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to change the brightness of a Milight device and keep the previous color set', async () => { + const device = DEVICES[2]; + const featureIndex = device.features.findIndex((f) => f.type === 'color'); + device.features[featureIndex].last_value = '15666'; + const deviceFeature = { + external_id: `rflink:86aa70:brightness:12:milight`, + }; + const state = 10; + const expectedMsg = '10;MiLightv1;86aa70;000;3917;BRIGHT;\n'; // cmd;model;deviceId;external_id last item;cmd + await rflinkHandler.setValue(device, deviceFeature, state); + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); + + it('should send a message to access milight-mode of a Milight device', async () => { + const device = DEVICES[2]; + const deviceFeature = { + external_id: `rflink:86aa70:milight-mode:12:milight`, + }; + const state = 4; + const expectedMsg = '10;MiLightv1;86aa70;000;34BC;MODE4;\n'; // cmd;model;deviceId;external_id last item;cmd + await rflinkHandler.setValue(device, deviceFeature, state); + assert.calledOnce(rflinkHandler.sendUsb.write); + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); +}); diff --git a/server/test/services/rflink/lib/unpair.milight.test.js b/server/test/services/rflink/lib/unpair.milight.test.js new file mode 100755 index 0000000000..1b22457b23 --- /dev/null +++ b/server/test/services/rflink/lib/unpair.milight.test.js @@ -0,0 +1,37 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); +const proxyquire = require('proxyquire').noCallThru(); +const SerialPortMock = require('../SerialPortMock.test'); + +const RFLinkHandler = proxyquire('../../../../services/rflink/lib', { + serialport: SerialPortMock, +}); + +const { assert, fake, stub } = sinon; + +describe('RFLinkHandler.unpair', () => { + let gladys; + beforeEach(() => { + sinon.reset(); + gladys = { + event: { + emit: fake.returns(null), + }, + }; + }); + + it('should unpair a milight device', async () => { + const currentMilightGateway = 'F746'; + const milightZone = '2'; + const rflinkHandler = new RFLinkHandler(gladys, 'faea9c35-759a-44d5-bcc9-2af1de37b8b4'); + rflinkHandler.sendUsb = { + write: stub() + .withArgs('msg') + .resolves(true), + }; + await rflinkHandler.unpair(currentMilightGateway, milightZone); + assert.called(rflinkHandler.sendUsb.write); + const expectedMsg = `10;MiLightv1;${currentMilightGateway};0${milightZone};34BC;UNPAIR;`; + expect(rflinkHandler.sendUsb.write.args[0][0]).to.equal(expectedMsg); + }); +}); diff --git a/server/test/services/rflink/rflink.test.js b/server/test/services/rflink/rflink.test.js new file mode 100644 index 0000000000..0ad65fab82 --- /dev/null +++ b/server/test/services/rflink/rflink.test.js @@ -0,0 +1,92 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); +const EventEmitter = require('events'); +const proxyquire = require('proxyquire').noCallThru(); +const SerialPortMock = require('./SerialPortMock.test'); +const { ServiceNotConfiguredError } = require('../../../utils/coreErrors'); + +const { assert, fake, stub } = sinon; + +const rflinkConnectFailMock = fake.throws('Error'); +const RFLinkHandler = proxyquire('../../../services/rflink/lib', { + './commands/rflink.connect.js': { connect: rflinkConnectFailMock }, +}); + +const RflinkService = proxyquire('../../../services/rflink/index', { + serialport: SerialPortMock, + './lib': RFLinkHandler, +}); + +describe('RFLinkService', () => { + let gladys; + let rflinkService; + const serviceId = '6d1bd783-ab5c-4d90-8551-6bc5fcd02212'; + beforeEach(() => { + sinon.reset(); + gladys = { + event: new EventEmitter(), + variable: { + getValue: stub(), + }, + }; + gladys.variable.getValue.withArgs('RFLINK_PATH', serviceId).returns('/usb/tty'); + gladys.variable.getValue.withArgs('CURRENT_MILIGHT_GATEWAY', serviceId).returns(null); + rflinkService = RflinkService(gladys, serviceId); + }); + + it('should have controllers', () => { + expect(rflinkService) + .to.have.property('controllers') + .and.be.instanceOf(Object); + }); + + it('should start service', async () => { + expect(rflinkService) + .to.have.property('start') + .and.be.instanceOf(Function); + expect(rflinkService) + .to.have.property('device') + .and.be.instanceOf(Object); + await rflinkService.start(); + assert.calledTwice(gladys.variable.getValue); + }); + + it('should throw an error, service not configured and not start service because RFLINK_PATH is not found', async () => { + gladys.variable.getValue = fake.returns(undefined); + expect(rflinkService.start()).to.be.rejectedWith(/RFLINK_PATH_NOT_FOUND/); + try { + await rflinkService.start(); + assert.fail(); + } catch (e) { + expect(e).to.be.instanceOf(ServiceNotConfiguredError); + } + }); + + it('should throw an error and not start service because rfLinkManager is not defined', async () => { + try { + await rflinkService.start(); + assert.fail(); + } catch (e) { + expect(e).to.be.instanceOf(Error); + } + }); + + it('should start service and set default value to miligh gateway', async () => { + await rflinkService.start(); + expect(rflinkService.device.currentMilightGateway).to.be.equal('F746'); + }); + + it('should start service and get the value of miligh gateway', async () => { + gladys.variable.getValue.withArgs('CURRENT_MILIGHT_GATEWAY', serviceId).returns('BB8'); + await rflinkService.start(); + expect(rflinkService.device.currentMilightGateway).to.be.equal('BB8'); + }); + + it('should stop service', async () => { + expect(rflinkService) + .to.have.property('stop') + .and.be.instanceOf(Function); + await rflinkService.stop(); + expect(rflinkService.device.connected).to.be.equal(false); + }); +}); diff --git a/server/test/services/rflink/rflinkMock.test.js b/server/test/services/rflink/rflinkMock.test.js new file mode 100644 index 0000000000..0b0519c753 --- /dev/null +++ b/server/test/services/rflink/rflinkMock.test.js @@ -0,0 +1,20 @@ +const { fake } = require('sinon'); +const EventEmitter = require('events'); + +const Rflink = function Rflink(options) {}; + +Rflink.prototype = Object.create(new EventEmitter()); + +Rflink.prototype.message = fake.returns(null); +Rflink.prototype.newValue = fake.returns(null); +Rflink.prototype.addDevice = fake.returns(null); +Rflink.prototype.setValue = fake.returns(null); +Rflink.prototype.connect = fake.returns(null); +Rflink.prototype.disconnect = fake.returns(null); +Rflink.prototype.listen = fake.returns(null); +Rflink.prototype.getDevices = fake.returns(null); +Rflink.prototype.pair = fake.returns(null); +Rflink.prototype.unpair = fake.returns(null); +Rflink.prototype.write = fake.returns(null); + +module.exports = Rflink; diff --git a/server/test/utils/colors.test.js b/server/test/utils/colors.test.js index fc60f6630f..15f897a39e 100644 --- a/server/test/utils/colors.test.js +++ b/server/test/utils/colors.test.js @@ -7,30 +7,45 @@ const { xyToInt, hsbToRgb, rgbToHsb, + rgbToMilightHue, kelvinToRGB, } = require('../../utils/colors'); describe('colors', () => { const matchingTable = { - cyan: { int: 65535, hex: '00FFFF', rgb: [0, 255, 255], hsb: [180, 100, 100] }, - violet: { int: 16711935, hex: 'FF00FF', rgb: [255, 0, 255], hsb: [300, 100, 100] }, - yellow: { int: 16776960, hex: 'FFFF00', rgb: [255, 255, 0], hsb: [60, 100, 100] }, - red: { int: 16711680, hex: 'FF0000', rgb: [255, 0, 0], xy: { x: 0.701, y: 0.299 }, hsb: [0, 100, 100] }, - lime: { int: 65280, hex: '00FF00', rgb: [0, 255, 0], xy: { x: 0, y: 1 }, hsb: [120, 100, 100] }, - blue: { int: 255, hex: '0000FF', rgb: [0, 0, 255], hsb: [240, 100, 100] }, - teal: { int: 32896, hex: '008080', rgb: [0, 128, 128], hsb: [180, 100, 50] }, - purple: { int: 8388736, hex: '800080', rgb: [128, 0, 128], hsb: [300, 100, 50] }, - olive: { int: 8421376, hex: '808000', rgb: [128, 128, 0], hsb: [60, 100, 50] }, - maroon: { int: 8388608, hex: '800000', rgb: [128, 0, 0], hsb: [0, 100, 50] }, - green: { int: 32768, hex: '008000', rgb: [0, 128, 0], hsb: [120, 100, 50] }, - navy: { int: 128, hex: '000080', rgb: [0, 0, 128], hsb: [240, 100, 50] }, - white: { int: 16777215, hex: 'FFFFFF', rgb: [255, 255, 255], xy: { x: 0.323, y: 0.329 }, hsb: [0, 0, 100] }, - gray: { int: 12632256, hex: 'C0C0C0', rgb: [192, 192, 192] }, - black: { int: 0, hex: '000000', rgb: [0, 0, 0], xy: { x: 0, y: 0 }, hsb: [0, 0, 0] }, + cyan: { int: 65535, hex: '00FFFF', rgb: [0, 255, 255], hsb: [180, 100, 100], milighthue: 49 }, + violet: { int: 16711935, hex: 'FF00FF', rgb: [255, 0, 255], hsb: [300, 100, 100], milighthue: 220 }, + yellow: { int: 16776960, hex: 'FFFF00', rgb: [255, 255, 0], hsb: [60, 100, 100], milighthue: 134 }, + red: { + int: 16711680, + hex: 'FF0000', + rgb: [255, 0, 0], + xy: { x: 0.701, y: 0.299 }, + hsb: [0, 100, 100], + milighthue: 176, + }, + lime: { int: 65280, hex: '00FF00', rgb: [0, 255, 0], xy: { x: 0, y: 1 }, hsb: [120, 100, 100], milighthue: 91 }, + blue: { int: 255, hex: '0000FF', rgb: [0, 0, 255], hsb: [240, 100, 100], milighthue: 6 }, + teal: { int: 32896, hex: '008080', rgb: [0, 128, 128], hsb: [180, 100, 50], milighthue: 49 }, + purple: { int: 8388736, hex: '800080', rgb: [128, 0, 128], hsb: [300, 100, 50], milighthue: 220 }, + olive: { int: 8421376, hex: '808000', rgb: [128, 128, 0], hsb: [60, 100, 50], milighthue: 134 }, + maroon: { int: 8388608, hex: '800000', rgb: [128, 0, 0], hsb: [0, 100, 50], milighthue: 176 }, + green: { int: 32768, hex: '008000', rgb: [0, 128, 0], hsb: [120, 100, 50], milighthue: 91 }, + navy: { int: 128, hex: '000080', rgb: [0, 0, 128], hsb: [240, 100, 50], milighthue: 6 }, + white: { + int: 16777215, + hex: 'FFFFFF', + rgb: [255, 255, 255], + xy: { x: 0.323, y: 0.329 }, + hsb: [0, 0, 100], + milighthue: 176, + }, + gray: { int: 12632256, hex: 'C0C0C0', rgb: [192, 192, 192], milighthue: 176 }, + black: { int: 0, hex: '000000', rgb: [0, 0, 0], xy: { x: 0, y: 0 }, hsb: [0, 0, 0], milighthue: 176 }, }; Object.keys(matchingTable).forEach((color) => { - const { int, hex, rgb, xy, hsb } = matchingTable[color]; + const { int, hex, rgb, xy, hsb, milighthue } = matchingTable[color]; it(`[${color}] intToHex (${int} -> ${hex})`, () => { const value = intToHex(int); @@ -74,6 +89,11 @@ describe('colors', () => { expect(value).to.equal(int); }); } + + it(`[${color}] milighthue (${rgb} -> ${milighthue})`, () => { + const value = rgbToMilightHue(rgb); + expect(value).to.equal(milighthue); + }); }); }); diff --git a/server/utils/colors.js b/server/utils/colors.js index e60656fa1e..7b6d861546 100644 --- a/server/utils/colors.js +++ b/server/utils/colors.js @@ -214,6 +214,21 @@ function xyToInt(x, y) { // eslint-disable-next-line no-bitwise return (red << 16) | (green << 8) | blue; } +/** + * @description Converts RGB array color to int 0-255. + * @param {Array} rgb - [red, green, blue ] array. + * @returns {number} Int color255. + * @example + * rgbToMilightHue([255,255,255]); + * console.log(int === FF); + */ +function rgbToMilightHue(rgb) { + // On the HSV color circle (0..360) the hue value start with red at 0 degrees. We need to convert this + // to the Milight color circle which has 256 values with red at position 176 + // from https://github.com/mwittig/node-milight-promise/blob/master/src/helper.js + const hsb = rgbToHsb(rgb); + return (256 + 176 - Math.floor((Number(hsb[0]) / 360.0) * 255.0)) % 256; +} /** * @description Convert mired to kelvin. @@ -243,6 +258,7 @@ module.exports = { xyToInt, hsbToRgb, rgbToHsb, + rgbToMilightHue, miredToKelvin, kelvinToMired, kelvinToRGB, diff --git a/server/utils/constants.js b/server/utils/constants.js index aaba8e41e1..bf5315ef89 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -505,6 +505,27 @@ const DEVICE_FEATURE_CATEGORIES = { INPUT: 'input', }; +const DEVICE_MODELS = { + TRISTATE: 'Tristate', + KAKU: 'Kaku', + NEWKAKU: 'Newkaku', + HOMEEASY: 'Homeeasy', + CONRAD: 'Conrad rsl2', + BLYSS: 'Blyss', + RTS: 'Rts', + AB400D: 'Ab400d', + IMPULS: 'Impuls', + EURODOMEST: 'Eurodomest', + X10: 'X10', + HOMECOMFORT: 'Homeconfort', + KOPPLA: 'Ikea koppla', + CHUANGO: 'Chuango', + SELECTPLUS: 'Selectplus', + DELTRONIC: 'Deltronic', + MERTIK: 'Mertik', + EV1527: 'Ev1527', +}; + const DEVICE_FEATURE_TYPES = { LIGHT: { BINARY: 'binary', @@ -524,6 +545,51 @@ const DEVICE_FEATURE_TYPES = { PUSH: 'push', UNKNOWN: 'unknown', }, + MOTION_SENSOR: { + BINARY: 'binary', + PUSH: 'push', + }, + LIGHT_SENSOR: { + DECIMAL: 'decimal', + INTEGER: 'integer', + }, + SMOKE_SENSOR: { + DECIMAL: 'decimal', + BINARY: 'binary', + }, + SISMIC_SENSOR: { + DECIMAL: 'decimal', + }, + PRESSURE_SENSOR: { + DECIMAL: 'decimal', + }, + OPENING_SENSOR: { + BINARY: 'binary', + }, + HUMIDITY_SENSOR: { + DECIMAL: 'decimal', + }, + TEMPERATURE_SENSOR: { + DECIMAL: 'decimal', + }, + CO2_SENSOR: { + DECIMAL: 'decimal', + }, + COUNTER_SENSOR: { + INTEGER: 'integer', + }, + LEAK_SENSOR: { + BINARY: 'binary', + }, + PRESENCE_SENSOR: { + PUSH: 'push', + }, + DISTANCE_SENSOR: { + DECIMAL: 'decimal', + }, + CAMERA: { + IMAGE: 'image', + }, SWITCH: { BINARY: 'binary', POWER: 'power', @@ -533,9 +599,6 @@ const DEVICE_FEATURE_TYPES = { BURGLAR: 'burglar', DIMMER: 'dimmer', }, - CAMERA: { - IMAGE: 'image', - }, SIREN: { BINARY: 'binary', LMH_VOLUME: 'lmh_volume', @@ -1056,6 +1119,12 @@ const WEBSOCKET_MESSAGE_TYPES = { INSTALLATION_STATUS: 'mqtt.install-status', DEBUG_NEW_MQTT_MESSAGE: 'mqtt.debug.new-mqtt-message', }, + RFLINK: { + NEW_MESSAGE: 'rflink.new-message', + DRIVER_FAILED: 'rflink.driver-failed', + DRIVER_READY: 'rflink.driver-ready', + NEW_DEVICE: 'rflink.new-device', + }, ZWAVEJS_UI: { CONNECTED: 'zwavejs-ui.connected', ERROR: 'zwavejs-ui.error', @@ -1224,6 +1293,7 @@ const DEVICE_FEATURE_UNITS_LIST = createList(DEVICE_FEATURE_UNITS); const DASHBOARD_TYPE_LIST = createList(DASHBOARD_TYPE); const DASHBOARD_VISIBILITY_LIST = createList(DASHBOARD_VISIBILITY); const DASHBOARD_BOX_TYPE_LIST = createList(DASHBOARD_BOX_TYPE); +const DEVICE_MODELS_LIST = createList(DEVICE_MODELS); const DEVICE_FEATURE_STATE_AGGREGATE_TYPES_LIST = createList(DEVICE_FEATURE_STATE_AGGREGATE_TYPES); const JOB_TYPES_LIST = createList(JOB_TYPES); const JOB_STATUS_LIST = createList(JOB_STATUS); @@ -1248,6 +1318,7 @@ module.exports.ACTIONS_STATUS = ACTIONS_STATUS; module.exports.USER_ROLE = USER_ROLE; module.exports.AVAILABLE_LANGUAGES = AVAILABLE_LANGUAGES; module.exports.SESSION_TOKEN_TYPES = SESSION_TOKEN_TYPES; +module.exports.DEVICE_MODELS_LIST = DEVICE_MODELS_LIST; module.exports.EVENT_LIST = EVENT_LIST; module.exports.LIFE_EVENT_LIST = LIFE_EVENT_LIST;
+ +
{`> ${get(props, 'rflinkStatus.lastCommand')} \n`}