From 15b6027fdcb8d806f2d2ec6ce77bb58354e36ab1 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 19 Feb 2021 19:00:57 +0100 Subject: [PATCH] Add Bluetooth + scene user presence (#1077) * BLE presence scanner * Update translations + add presence sensor on device state change in scene * Add new checkuserpresence action in scene * Add user check presence action server side with tests * Add en translations * Fix checkUserpresence front build Co-authored-by: atrovato <1839717+atrovato@users.noreply.github.com> --- front/src/components/app.jsx | 2 + front/src/config/demo.json | 8 +- front/src/config/i18n/en.json | 34 +++- front/src/config/i18n/fr.json | 34 +++- .../all/bluetooth/BluetoothPage.js | 15 +- .../BluetoothPresenceScanner.jsx | 82 ++++++++ .../settings-page/BluetoothSettingsTab.jsx | 116 +++++++++++ .../all/bluetooth/settings-page/index.js | 19 ++ .../setup-page/BluetoothPeripheral.jsx | 15 +- .../BluetoothPeripheralFeatures.jsx | 84 +++----- .../setup-page/BluetoothPeripheralTab.jsx | 7 +- .../setup-peripheral/ConfigurePeripheral.jsx | 13 +- .../ConfigurePeripheralForm.jsx | 98 +++++++-- .../ConfigurePeripheralSuccess.jsx | 4 +- .../setup-peripheral/PeripheralNotFound.jsx | 2 +- .../setup-page/setup-peripheral/index.js | 19 +- .../routes/scene/edit-scene/ActionCard.jsx | 12 +- .../edit-scene/actions/CheckUserPresence.jsx | 189 ++++++++++++++++++ .../actions/ChooseActionTypeCard.jsx | 1 + .../triggers/DeviceFeatureState.jsx | 153 ++++++++------ server/lib/scene/scene.actions.js | 22 ++ server/models/scene.js | 1 + .../bluetooth/api/bluetooth.controller.js | 42 ++++ .../lib/commands/bluetooth.connectDevices.js | 22 -- .../bluetooth/lib/commands/bluetooth.poll.js | 46 ----- .../lib/commands/bluetooth.postCreate.js | 47 ----- .../lib/commands/bluetooth.postDelete.js | 37 ---- .../bluetooth/lib/commands/bluetooth.scan.js | 4 +- .../lib/commands/bluetooth.scanPresence.js | 46 +++++ .../lib/commands/bluetooth.setValue.js | 23 --- .../bluetooth/lib/commands/bluetooth.start.js | 25 ++- .../lib/config/bluetooth.getConfiguration.js | 16 ++ .../config/bluetooth.initPresenceScanner.js | 26 +++ .../lib/config/bluetooth.saveConfiguration.js | 49 +++++ .../lib/events/bluetooth.discover.js | 5 +- server/services/bluetooth/lib/index.js | 29 ++- .../lib/utils/bluetooth.constants.js | 13 +- .../lib/scene/scene.executeActions.test.js | 54 +++++ .../api/bluetooth.controller.test.js | 48 +++++ server/test/services/bluetooth/index.test.js | 3 + .../commands/bluetooth.connectDevices.test.js | 93 --------- .../lib/commands/bluetooth.poll.test.js | 115 ----------- .../lib/commands/bluetooth.postCreate.test.js | 113 ----------- .../lib/commands/bluetooth.postDelete.test.js | 98 --------- .../commands/bluetooth.scanPresence.test.js | 104 ++++++++++ .../lib/commands/bluetooth.setValue.test.js | 81 -------- .../lib/commands/bluetooth.start.test.js | 33 ++- .../bluetooth.subscribeDevice.test.js | 135 +++++++++++++ .../bluetooth.unsubscribeDevice.test.js | 121 +++++++++++ .../config/bluetooth.getConfiguration.test.js | 20 ++ .../bluetooth.initPresenceScanner.test.js | 78 ++++++++ .../bluetooth.saveConfiguration.test.js | 170 ++++++++++++++++ server/utils/constants.js | 1 + 53 files changed, 1734 insertions(+), 893 deletions(-) create mode 100644 front/src/routes/integration/all/bluetooth/settings-page/BluetoothPresenceScanner.jsx create mode 100644 front/src/routes/integration/all/bluetooth/settings-page/BluetoothSettingsTab.jsx create mode 100644 front/src/routes/integration/all/bluetooth/settings-page/index.js create mode 100644 front/src/routes/scene/edit-scene/actions/CheckUserPresence.jsx delete mode 100644 server/services/bluetooth/lib/commands/bluetooth.connectDevices.js delete mode 100644 server/services/bluetooth/lib/commands/bluetooth.poll.js delete mode 100644 server/services/bluetooth/lib/commands/bluetooth.postCreate.js delete mode 100644 server/services/bluetooth/lib/commands/bluetooth.postDelete.js create mode 100644 server/services/bluetooth/lib/commands/bluetooth.scanPresence.js delete mode 100644 server/services/bluetooth/lib/commands/bluetooth.setValue.js create mode 100644 server/services/bluetooth/lib/config/bluetooth.getConfiguration.js create mode 100644 server/services/bluetooth/lib/config/bluetooth.initPresenceScanner.js create mode 100644 server/services/bluetooth/lib/config/bluetooth.saveConfiguration.js delete mode 100644 server/test/services/bluetooth/lib/commands/bluetooth.connectDevices.test.js delete mode 100644 server/test/services/bluetooth/lib/commands/bluetooth.poll.test.js delete mode 100644 server/test/services/bluetooth/lib/commands/bluetooth.postCreate.test.js delete mode 100644 server/test/services/bluetooth/lib/commands/bluetooth.postDelete.test.js create mode 100644 server/test/services/bluetooth/lib/commands/bluetooth.scanPresence.test.js delete mode 100644 server/test/services/bluetooth/lib/commands/bluetooth.setValue.test.js create mode 100644 server/test/services/bluetooth/lib/commands/bluetooth.subscribeDevice.test.js create mode 100644 server/test/services/bluetooth/lib/commands/bluetooth.unsubscribeDevice.test.js create mode 100644 server/test/services/bluetooth/lib/config/bluetooth.getConfiguration.test.js create mode 100644 server/test/services/bluetooth/lib/config/bluetooth.initPresenceScanner.test.js create mode 100644 server/test/services/bluetooth/lib/config/bluetooth.saveConfiguration.test.js diff --git a/front/src/components/app.jsx b/front/src/components/app.jsx index a20f73c6b3..f2df84de98 100644 --- a/front/src/components/app.jsx +++ b/front/src/components/app.jsx @@ -89,6 +89,7 @@ import BluetoothDevicePage from '../routes/integration/all/bluetooth/device-page import BluetoothEditDevicePage from '../routes/integration/all/bluetooth/edit-page'; import BluetoothSetupPage from '../routes/integration/all/bluetooth/setup-page'; import BluetoothSetupPeripheralPage from '../routes/integration/all/bluetooth/setup-page/setup-peripheral'; +import BluetoothSettingsPage from '../routes/integration/all/bluetooth/settings-page'; // EweLink import EweLinkPage from '../routes/integration/all/ewelink/device-page'; @@ -205,6 +206,7 @@ const AppRouter = connect( + diff --git a/front/src/config/demo.json b/front/src/config/demo.json index 5a92b7a060..3cadf9dddc 100644 --- a/front/src/config/demo.json +++ b/front/src/config/demo.json @@ -1668,6 +1668,12 @@ "name": "bluetooth", "enabled": true }, + "get /api/v1/service/bluetooth/config": { + "presenceScanner": { + "status": "enabled", + "frequency": 60000 + } + }, "get /api/v1/service/bluetooth/device": [ { "id": "fbedb47f-4d25-4381-8923-2633b23192a0", @@ -1723,7 +1729,7 @@ } }, "get /api/v1/service/bluetooth/status": { - "running": true + "ready": true }, "get /api/v1/service/bluetooth/peripheral": [ { diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index d3c4616847..5f51dafe39 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -644,7 +644,8 @@ "title": "Bluetooth", "description": "Control your Bluetooth devices.", "deviceTab": "Devices", - "setupTab": "Setup", + "discoverTab": "Discover", + "setupTab": "Presence scanner", "bluetoothNotReadyError": "Bluetooth device is not reachable, please check it is enabled.", "device": { "title": "Bluetooth Devices", @@ -657,6 +658,7 @@ "externalIdLabel": "External ID", "manufacturerLabel": "Manufacturer", "modelLabel": "Model", + "presenceSensorLabel": "Use this device as presence sensor", "featuresLabel": "Features", "noFeatureDiscovered": "No features discovered.", "featureNamePlaceholder": "Enter feature name", @@ -664,10 +666,9 @@ "deleteButton": "Delete", "editButton": "Edit" }, - "setup": { - "title": "Bluetooth Setup", + "discover": { + "title": "Bluetooth Discover", "scanButton": "Scan", - "reloadButton": "Reload", "noDeviceFound": "No bluetooth device discovered.", "createDeviceInGladys": "Connect in Gladys", "updateDeviceInGladys": "Update in Gladys", @@ -684,6 +685,18 @@ "cancelLabel": "Cancel", "successLabel": "Done" } + }, + "setup": { + "title": "Presence scanner", + "noConfigLabel": "Configuration not loaded, please retry.", + "errorLabel": "An error occurred while loading configuration.", + "presenceScannerDescription": "The presence scanner feature is doing a bluetooth scan at a defined interval. When a device is detected, the device will be flagged as \"seen\" in Gladys. In scene, you can create a trigger on this state change to set you home when a device is detected at home for example.", + "presenceScannerStatusLabel": "Enable or disable presence scanner", + "presenceScannerFrequencyLabel": "Scanner interval", + "presenceScannerFrequencyUnit": "minutes", + "presenceScannerFrequencyError": "Only integers are accepted.", + "presenceScannerButton": "Scan device presence now", + "saveLabel": "Save configuration" } }, "eWeLink": { @@ -810,6 +823,15 @@ "userSeenDescription": "This action set the user as \"at home\".", "userLeftHomeDescription": "This action set the user as \"left home\"." }, + "checkUserPresence": { + "description": "When this action is executed, Gladys checks if one of the selected device was seen at home in the last X minutes. If one of the device was seen, the action does nothing more. If no device was seen, the user is marked as \"left home\".", + "userLabel": "User", + "houseLabel": "House", + "deviceLabel": "Devices", + "minutesLabel": "Devices seen less than", + "minutesPlaceholder": "Duration (in minutes)", + "minutes": "minutes" + }, "httpRequest": { "description": "This action let you make HTTP requests.", "methodLabel": "Method", @@ -855,7 +877,8 @@ }, "user": { "set-seen-at-home": "User seen at home", - "set-out-of-home": "User left home" + "set-out-of-home": "User left home", + "check-presence": "Check user presence" }, "http": { "request": "Make a HTTP request" @@ -887,6 +910,7 @@ "valuePlaceholder": "Value", "on": "On", "off": "Off", + "deviceSeen": "If the device is detected", "onlyExecuteAtThreshold": "Execute only when threshold is passed (and not at every value sent by the device)" }, "scheduledTrigger": { diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index e780af8c76..710b5c9347 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -308,7 +308,8 @@ "title": "Bluetooth", "description": "Contrôler vos appareils Bluetooth.", "deviceTab": "Appareils", - "setupTab": "Configuration", + "discoverTab": "Découverte", + "setupTab": "Scanner de présence", "bluetoothNotReadyError": "Le module Bluetooth n'est pas disponible, merci de vérifier qu'il est bien activé.", "device": { "title": "Appareils Bluetooth", @@ -321,6 +322,7 @@ "externalIdLabel": "ID externe", "manufacturerLabel": "Fabricant", "modelLabel": "Modèle", + "presenceSensorLabel": "Utiliser cet appareil comme un détecteur de présence", "featuresLabel": "Fonctionnalités", "noFeatureDiscovered": "Aucune fonctionnalité détectée.", "featureNamePlaceholder": "Nom de la fonctionnalité", @@ -328,10 +330,9 @@ "deleteButton": "Supprimer", "editButton": "Editer" }, - "setup": { - "title": "Configuration Bluetooth", + "discover": { + "title": "Découverte Bluetooth", "scanButton": "Rechercher", - "reloadButton": "Recharger", "noDeviceFound": "Aucun périphérique Bluetooth détecté.", "createDeviceInGladys": "Connecter dans Gladys", "updateDeviceInGladys": "Mettre à jour dans Gladys", @@ -348,6 +349,18 @@ "cancelLabel": "Annuler", "successLabel": "Effectué" } + }, + "setup": { + "title": "Scanner de présence", + "noConfigLabel": "La configuration n'est pas chargée, merci de réessayer.", + "errorLabel": "Une erreur est survenue lors du chargement de la configuration.", + "presenceScannerDescription": "Le scanner de présence effectue un scan Bluetooth à interval régulier. Dès qu’un appareil est détecté, l'appareil est marqué comme \"présent\" dans Gladys. Dans les scènes, vous avez la possibilité de créer un déclencheur qui écoute sur cet état. Ainsi, vous pouvez facilement faire une scène qui vous met comme présent à la maison dès qu'un appareil bluetooth est détecté.", + "presenceScannerStatusLabel": "Activer ou désactiver l'analyse de présence d'appareils", + "presenceScannerFrequencyLabel": "Fréquence de scan bluetooth", + "presenceScannerFrequencyUnit": "minutes", + "presenceScannerFrequencyError": "Seul un nombre entier est autorisé.", + "presenceScannerButton": "Scanner la présence maintenant", + "saveLabel": "Enregistrer" } }, "telegram": { @@ -810,6 +823,15 @@ "userSeenDescription": "Cette action marque l'utilisateur comme présent à la maison.", "userLeftHomeDescription": "Cette action indique que l'utilisateur a quitté la maison." }, + "checkUserPresence": { + "description": "Lorsque cette action est exécutée, Gladys va regarder si les appareils sélectionnées ont été détectés dans les dernières minutes ( selon le nombre de minutes sélectionnée ci-dessous ). Si un appareil a été détecté, Gladys ne fera rien. Si aucun n'appareil n'a été détecté, Gladys marquera l'utilisateur comme hors de la maison.", + "userLabel": "Utilisateur", + "houseLabel": "Maison", + "deviceLabel": "Appareils", + "minutesLabel": "Appareil vu il y a moins de", + "minutesPlaceholder": "Durée (en minutes)", + "minutes": "minutes" + }, "httpRequest": { "description": "Cette action permet d'envoyer une requête HTTP.", "methodLabel": "Méthode", @@ -855,7 +877,8 @@ }, "user": { "set-seen-at-home": "Utilisateur vu à la maison", - "set-out-of-home": "Utilisateur parti de la maison" + "set-out-of-home": "Utilisateur parti de la maison", + "check-presence": "Vérifier la présence" }, "http": { "request": "Faire une requête HTTP" @@ -887,6 +910,7 @@ "valuePlaceholder": "Valeur", "on": "On", "off": "Off", + "deviceSeen": "Si l'appareil est détecté", "onlyExecuteAtThreshold": "Exécuter seulement lorsque le seuil est passé ( et non pas à chaque valeur envoyée )" }, "scheduledTrigger": { diff --git a/front/src/routes/integration/all/bluetooth/BluetoothPage.js b/front/src/routes/integration/all/bluetooth/BluetoothPage.js index 28dff14fca..7e94190517 100644 --- a/front/src/routes/integration/all/bluetooth/BluetoothPage.js +++ b/front/src/routes/integration/all/bluetooth/BluetoothPage.js @@ -19,7 +19,7 @@ const BluetoothPage = ({ children, ...props }) => ( class="list-group-item list-group-item-action d-flex align-items-center" > - + @@ -30,7 +30,18 @@ const BluetoothPage = ({ children, ...props }) => ( class="list-group-item list-group-item-action d-flex align-items-center" > - + + + + + + + + diff --git a/front/src/routes/integration/all/bluetooth/settings-page/BluetoothPresenceScanner.jsx b/front/src/routes/integration/all/bluetooth/settings-page/BluetoothPresenceScanner.jsx new file mode 100644 index 0000000000..884cdafea7 --- /dev/null +++ b/front/src/routes/integration/all/bluetooth/settings-page/BluetoothPresenceScanner.jsx @@ -0,0 +1,82 @@ +import { Component } from 'preact'; +import { Text } from 'preact-i18n'; +import cx from 'classnames'; + +import { PRESENCE_STATUS } from '../../../../../../../server/services/bluetooth/lib/utils/bluetooth.constants'; + +class BluetoothPresenceScanner extends Component { + toggleStatus = () => { + const newStatus = + this.props.config.status === PRESENCE_STATUS.ENABLED ? PRESENCE_STATUS.DISABLED : PRESENCE_STATUS.ENABLED; + this.props.updateConfig('presenceScanner', 'status', newStatus); + }; + + updateFrequency = e => { + const { value } = e.target; + const rawFrequency = parseFloat(value, 10); + + if (Number.isInteger(rawFrequency) && rawFrequency >= 1) { + this.props.updateConfig('presenceScanner', 'frequency', rawFrequency * 60000); + this.setState({ frequencyError: undefined }); + } else { + this.setState({ frequencyError: value }); + } + }; + + render({ config = {}, disabled }, { frequencyError }) { + const enabled = config.status === PRESENCE_STATUS.ENABLED; + const frequency = config.frequency / 60000; + + return ( +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+
+ {frequencyError && ( +
+ +
+ )} +
+
+ ); + } +} + +export default BluetoothPresenceScanner; diff --git a/front/src/routes/integration/all/bluetooth/settings-page/BluetoothSettingsTab.jsx b/front/src/routes/integration/all/bluetooth/settings-page/BluetoothSettingsTab.jsx new file mode 100644 index 0000000000..31c8452b71 --- /dev/null +++ b/front/src/routes/integration/all/bluetooth/settings-page/BluetoothSettingsTab.jsx @@ -0,0 +1,116 @@ +import { Component } from 'preact'; +import { Text } from 'preact-i18n'; +import cx from 'classnames'; +import update from 'immutability-helper'; + +import CheckBluetoothPanel from '../commons/CheckBluetoothPanel'; +import BluetoothPresenceScanner from './BluetoothPresenceScanner'; + +class BluetoothSettingsTab extends Component { + loadConfiguration = async () => { + this.setState({ loading: true, error: null }); + try { + const config = await this.props.httpClient.get('/api/v1/service/bluetooth/config'); + this.setState({ loading: false, config }); + } catch (e) { + console.error(e); + this.setState({ loading: false, error: e }); + } + }; + + updateConfig = async (configGroup, configItem, value) => { + const updatedConfig = update(this.state.config, { + [configGroup]: { + [configItem]: { + $set: value + } + } + }); + + this.setState({ config: updatedConfig, updated: true }); + }; + + saveConfig = async () => { + this.setState({ saving: true, error: null }); + try { + const config = await this.props.httpClient.post('/api/v1/service/bluetooth/config', this.state.config); + this.setState({ saving: false, config, updated: false }); + } catch (e) { + console.error(e); + this.setState({ saving: false, error: e }); + } + }; + + scanPresence = async () => { + try { + await this.props.httpClient.post('/api/v1/service/bluetooth/presence'); + } catch (e) { + console.error(e); + } + }; + + async componentWillMount() { + await this.loadConfiguration(); + } + + render({ bluetoothStatus = {} }, { config, loading, error, saving, updated }) { + return ( +
+
+

+ +

+
+
+ + +
+
+
+ {error && ( +
+ +
+ )} + + {!error && !config && ( +
+ +
+ )} + + {config && ( + + )} + +
+ + + +
+
+
+
+
+ ); + } +} + +export default BluetoothSettingsTab; diff --git a/front/src/routes/integration/all/bluetooth/settings-page/index.js b/front/src/routes/integration/all/bluetooth/settings-page/index.js new file mode 100644 index 0000000000..96943167ae --- /dev/null +++ b/front/src/routes/integration/all/bluetooth/settings-page/index.js @@ -0,0 +1,19 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; + +import BluetoothPage from '../BluetoothPage'; +import BluetoothSettingsTab from './BluetoothSettingsTab'; +import actions from '../commons/actions'; + +@connect('user,httpClient,bluetoothStatus', actions) +class BluetoothSettingsPage extends Component { + render(props) { + return ( + + + + ); + } +} + +export default BluetoothSettingsPage; diff --git a/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheral.jsx b/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheral.jsx index 88caaa727f..0f3841ffa2 100644 --- a/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheral.jsx +++ b/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheral.jsx @@ -12,7 +12,7 @@ class BluetoothPeripheral extends Component { this.props.scan(this.props.peripheral.selector); }; - render({ peripheral, bluetoothStatus, currentIntegration }) { + render({ peripheral, currentIntegration }) { const params = peripheral.params || []; const manufacturerParam = params.find(p => p.name === PARAMS.MANUFACTURER); const manufacturerValue = (manufacturerParam || { value: null }).value; @@ -44,23 +44,18 @@ class BluetoothPeripheral extends Component {
- +
{bluetoothDevice && ( {!peripheralService && ( )} {peripheralService && ( )} @@ -68,7 +63,7 @@ class BluetoothPeripheral extends Component { {!bluetoothDevice && ( diff --git a/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheralFeatures.jsx b/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheralFeatures.jsx index a0db3f240c..68f41be324 100644 --- a/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheralFeatures.jsx +++ b/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheralFeatures.jsx @@ -1,66 +1,38 @@ import { Text } from 'preact-i18n'; -import { Component } from 'preact'; -import cx from 'classnames'; import get from 'get-value'; import { DeviceFeatureCategoriesIcon } from '../../../../../utils/consts'; -import { PARAMS } from '../../../../../../../server/services/bluetooth/lib/utils/bluetooth.constants'; +const BluetoothPeripheralFeatures = ({ children, device }) => { + const { features = [] } = device; -import style from '../style.css'; - -class BluetoothPeripheralFeatures extends Component { - scan = () => { - this.props.scan(this.props.peripheral.selector); - }; - - render({ children, peripheral, bluetoothStatus, bluetoothDevice }) { - const params = peripheral.params || []; - const loadedParam = params.find(p => p.name === PARAMS.LOADED); - const loadedValue = (loadedParam || { value: false }).value; - - return ( -
- -
- -
-
-
- {loadedParam && (!peripheral.features || peripheral.features.length === 0) && ( -
- -
- )} - {loadedParam && - peripheral.features && - (children || ( -
- {peripheral.features.map(feature => ( - - -
- -
-
- ))} -
- ))} + return ( +
+ +
+ {features.length === 0 && ( +
+
-
-
+ )} + {features.length > 0 && + (children || ( +
+ {features.map(feature => ( + + +
+ +
+
+ ))} +
+ ))}
- ); - } -} +
+ ); +}; export default BluetoothPeripheralFeatures; diff --git a/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheralTab.jsx b/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheralTab.jsx index a1bc2801a9..d653325def 100644 --- a/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheralTab.jsx +++ b/front/src/routes/integration/all/bluetooth/setup-page/BluetoothPeripheralTab.jsx @@ -21,7 +21,7 @@ const BluetoothPeripheralTab = ({

- +

@@ -48,7 +48,7 @@ const BluetoothPeripheralTab = ({
{bluetoothStatus.ready && bluetoothPeripherals.length === 0 && ( - + )} {bluetoothStatus.ready && bluetoothPeripherals.map((peripheral, index) => ( @@ -57,7 +57,6 @@ const BluetoothPeripheralTab = ({ peripheralIndex={index} createDevice={createDevice} scan={scan} - bluetoothStatus={bluetoothStatus} currentIntegration={currentIntegration} /> ))} diff --git a/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheral.jsx b/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheral.jsx index 1f79f14745..f546a43e3f 100644 --- a/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheral.jsx +++ b/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheral.jsx @@ -12,26 +12,21 @@ class ConfigurePeripheral extends Component { this.props.resetSaveStatus(); } - render({ peripheral, bluetoothSaveStatus, bluetoothStatus, reloadDevice }) { + render({ device, bluetoothSaveStatus }) { return (
-

{peripheral.name || peripheral.address}

+

{device.name}

{bluetoothSaveStatus === RequestStatus.Error && (
- +
)} {bluetoothSaveStatus === RequestStatus.Success && } {bluetoothSaveStatus !== RequestStatus.Success && ( - + )}
); diff --git a/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheralForm.jsx b/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheralForm.jsx index 0f9bb92805..4bfbe73ef2 100644 --- a/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheralForm.jsx +++ b/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheralForm.jsx @@ -6,8 +6,10 @@ import cx from 'classnames'; import get from 'get-value'; import update from 'immutability-helper'; -import actions from '../actions'; import { RequestStatus } from '../../../../../../utils/consts'; +import { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } from '../../../../../../../../server/utils/constants'; + +import actions from '../actions'; import UpdateDeviceFeature from '../../../../../../components/device/UpdateDeviceFeature'; import BluetoothPeripheralFeatures from '../BluetoothPeripheralFeatures'; @@ -62,14 +64,66 @@ class ConfigurePeripheralForm extends Component { e.preventDefault(); const { device } = this.state; - this.props.createDevice({ ...device, service_id: this.props.currentIntegration.id }); + const deviceCopy = { ...device, service_id: this.props.currentIntegration.id }; + this.props.createDevice(deviceCopy); + }; + + switchPresenceSensor = () => { + const { presenceSensorIndex, device } = this.state; + + let updatedDevice; + let updatedPresenceSensorIndex; + + if (presenceSensorIndex >= 0) { + updatedDevice = update(device, { + features: { + $splice: [[presenceSensorIndex, 1]] + } + }); + + updatedPresenceSensorIndex = -1; + } else { + updatedDevice = update(device, { + features: { + $push: [ + { + name: get( + this.context.intl.dictionary, + `deviceFeatureCategory.${DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR}.${DEVICE_FEATURE_TYPES.SENSOR.PUSH}` + ), + external_id: `${device.external_id}:${DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR}`, + category: DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.PUSH, + min: 0, + max: 1, + read_only: true, + has_feedback: false, + keep_history: true + } + ] + } + }); + + updatedPresenceSensorIndex = updatedDevice.features.length - 1; + } + + this.setState({ + device: updatedDevice, + presenceSensorIndex: updatedPresenceSensorIndex + }); }; constructor(props) { super(props); + const { device } = props; + const presenceSensorIndex = device.features.findIndex( + f => f.category === DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR + ); + this.state = { - device: props.peripheral + device, + presenceSensorIndex }; this.createDevice = this.createDevice.bind(this); @@ -82,17 +136,17 @@ class ConfigurePeripheralForm extends Component { this.props.getIntegrationByName('bluetooth'); } - render({ houses, bluetoothStatus, reloadDevice, bluetoothSaveStatus, currentIntegration = {} }, { device }) { - const disableForm = bluetoothSaveStatus === RequestStatus.Getting || !bluetoothDevice; - const deviceFeatures = device.features || []; + render({ houses, bluetoothSaveStatus, currentIntegration = {} }, { device, presenceSensorIndex }) { const deviceService = get(device, 'service_id'); const bluetoothDevice = !deviceService || deviceService === currentIntegration.id; + const disableForm = bluetoothSaveStatus === RequestStatus.Getting || !bluetoothDevice; + const { features = [] } = device; return (
{!bluetoothDevice && (
- +
)} @@ -148,14 +202,24 @@ class ConfigurePeripheralForm extends Component {
- +
+ +
+ +
- {deviceFeatures.map((feature, index) => ( + {features.map((feature, index) => ( - +
diff --git a/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheralSuccess.jsx b/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheralSuccess.jsx index 8307cf36fd..7397e8056e 100644 --- a/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheralSuccess.jsx +++ b/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/ConfigurePeripheralSuccess.jsx @@ -5,12 +5,12 @@ const ConfigurePeripheralSuccess = () => { return (
- +
diff --git a/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/PeripheralNotFound.jsx b/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/PeripheralNotFound.jsx index 2566a5dc2b..ceccac7477 100644 --- a/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/PeripheralNotFound.jsx +++ b/front/src/routes/integration/all/bluetooth/setup-page/setup-peripheral/PeripheralNotFound.jsx @@ -4,7 +4,7 @@ import { Link } from 'preact-router/match'; const PeripheralNotFound = ({ uuid }) => (
- +
); + getPresenceSensor = () => ( +
+ +
+ ); render(props, { selectedDeviceFeature }) { return ( @@ -89,74 +103,87 @@ class TurnOnLight extends Component { />
- {selectedDeviceFeature && selectedDeviceFeature.type === 'binary' && this.getBinaryOperator()} - {selectedDeviceFeature && selectedDeviceFeature.type === 'binary' && this.getBinaryButtons()} - {selectedDeviceFeature && selectedDeviceFeature.type !== 'binary' && ( -
-
- + {selectedDeviceFeature && + selectedDeviceFeature.type === DEVICE_FEATURE_TYPES.SWITCH.BINARY && + this.getBinaryOperator()} + {selectedDeviceFeature && + selectedDeviceFeature.type === DEVICE_FEATURE_TYPES.SWITCH.BINARY && + this.getBinaryButtons()} + {selectedDeviceFeature && + selectedDeviceFeature.category === DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR && + this.getPresenceSensor()} + {selectedDeviceFeature && + selectedDeviceFeature.type !== DEVICE_FEATURE_TYPES.SWITCH.BINARY && + selectedDeviceFeature.category !== DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR && ( +
+
+ +
-
- )} - {selectedDeviceFeature && selectedDeviceFeature.type !== 'binary' && ( -
-
-
- - } - value={props.trigger.value} - onChange={this.handleValueChange} - /> - - {selectedDeviceFeature.unit && ( - - - + )} + {selectedDeviceFeature && + selectedDeviceFeature.type !== DEVICE_FEATURE_TYPES.SWITCH.BINARY && + selectedDeviceFeature.category !== DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR && ( +
+
+
+ + } + value={props.trigger.value} + onChange={this.handleValueChange} + /> + + {selectedDeviceFeature.unit && ( + + + + - - )} + )} +
+ )} + {selectedDeviceFeature && selectedDeviceFeature.category !== DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR && ( +
+
)} -
- -
); diff --git a/server/lib/scene/scene.actions.js b/server/lib/scene/scene.actions.js index bbb03cc289..285c05082c 100644 --- a/server/lib/scene/scene.actions.js +++ b/server/lib/scene/scene.actions.js @@ -152,6 +152,28 @@ const actionsFunc = { ); set(scope, `${columnIndex}.${rowIndex}`, response); }, + [ACTIONS.USER.CHECK_PRESENCE]: async (self, action, scope, columnIndex, rowIndex) => { + let deviceSeenRecently = false; + // we want to see if a device was seen before now - XX minutes + const thresholdDate = new Date(Date.now() - action.minutes * 60 * 1000); + // foreach selected device + action.device_features.forEach((deviceFeatureSelector) => { + // we get the time when the device was last seen + const deviceFeature = self.stateManager.get('deviceFeature', deviceFeatureSelector); + // if it's recent, we save true + if (deviceFeature.last_value_changed > thresholdDate) { + deviceSeenRecently = true; + } + }); + // if no device was seen, the user has left home + if (deviceSeenRecently === false) { + logger.info( + `CheckUserPresence action: No devices of the user "${action.user}" were seen in the last ${action.minutes} minutes.`, + ); + logger.info(`CheckUserPresence action: Set "${action.user}" to left home of house "${action.house}"`); + await self.house.userLeft(action.house, action.user); + } + }, }; module.exports = { diff --git a/server/models/scene.js b/server/models/scene.js index 1438ec5995..5117276ed2 100644 --- a/server/models/scene.js +++ b/server/models/scene.js @@ -17,6 +17,7 @@ const actionSchema = Joi.array().items( house: Joi.string(), text: Joi.string(), value: Joi.number(), + minutes: Joi.number(), unit: Joi.string(), url: Joi.string().uri(), body: Joi.string(), diff --git a/server/services/bluetooth/api/bluetooth.controller.js b/server/services/bluetooth/api/bluetooth.controller.js index 58f5be0dd2..5f560649dd 100644 --- a/server/services/bluetooth/api/bluetooth.controller.js +++ b/server/services/bluetooth/api/bluetooth.controller.js @@ -54,6 +54,36 @@ module.exports = function BluetoothController(bluetoothManager) { res.json(bluetoothManager.getStatus()); } + /** + * @api {post} /api/v1/service/bluetooth/presence Start presence scanner + * @apiName scanPresence + * @apiGroup Bluetooth + */ + async function scanPresence(req, res) { + bluetoothManager.scanPresence(); + res.status(200); + } + + /** + * @api {get} /api/v1/service/bluetooth/config Get Bluetooth configuration + * @apiName getConfiguration + * @apiGroup Bluetooth + */ + async function getConfiguration(req, res) { + const config = bluetoothManager.getConfiguration(); + res.json(config); + } + + /** + * @api {post} /api/v1/service/bluetooth/config Save Bluetooth configuration + * @apiName saveConfiguration + * @apiGroup Bluetooth + */ + async function saveConfiguration(req, res) { + const config = await bluetoothManager.saveConfiguration(req.body); + res.json(config); + } + return { 'get /api/v1/service/bluetooth/status': { authenticated: true, @@ -67,6 +97,18 @@ module.exports = function BluetoothController(bluetoothManager) { authenticated: true, controller: getDiscoveredDevice, }, + 'post /api/v1/service/bluetooth/presence': { + authenticated: true, + controller: scanPresence, + }, + 'get /api/v1/service/bluetooth/config': { + authenticated: true, + controller: getConfiguration, + }, + 'post /api/v1/service/bluetooth/config': { + authenticated: true, + controller: saveConfiguration, + }, 'post /api/v1/service/bluetooth/scan': { authenticated: true, controller: scan, diff --git a/server/services/bluetooth/lib/commands/bluetooth.connectDevices.js b/server/services/bluetooth/lib/commands/bluetooth.connectDevices.js deleted file mode 100644 index 4cb0d98ca1..0000000000 --- a/server/services/bluetooth/lib/commands/bluetooth.connectDevices.js +++ /dev/null @@ -1,22 +0,0 @@ -const Promise = require('bluebird'); - -const logger = require('../../../../utils/logger'); - -/** - * @description Look for Gladys Bluetooth devices and subscribe to notifications. - * @returns {Promise} All subscription promises. - * @example - * await bluetooth.connectDevices(); - */ -async function connectDevices() { - logger.debug(`Bluetooth: subscribing to existing devices...`); - const devices = await this.gladys.device.get({ - service: 'bluetooth', - }); - - return Promise.map(devices, (device) => this.postCreate(device), { concurrency: 1 }); -} - -module.exports = { - connectDevices, -}; diff --git a/server/services/bluetooth/lib/commands/bluetooth.poll.js b/server/services/bluetooth/lib/commands/bluetooth.poll.js deleted file mode 100644 index eacf992fd1..0000000000 --- a/server/services/bluetooth/lib/commands/bluetooth.poll.js +++ /dev/null @@ -1,46 +0,0 @@ -const Promise = require('bluebird'); - -const logger = require('../../../../utils/logger'); -const { EVENTS } = require('../../../../utils/constants'); - -const { decodeValue } = require('../device/bluetooth.information'); - -/** - * @description Poll value of a Bluetooth device - * @param {Object} device - The device to control. - * @returns {Promise} Promise of all read values. - * @example - * await bluetooth.poll({ external_id: 'bluetooth:uuid'}); - */ -async function poll(device) { - const [, peripheralUuid] = device.external_id.split(':'); - - const readFeature = (feature, peripheral) => { - const featureExternalId = feature.external_id; - const [, , serviceUuid, characteristicUuid] = featureExternalId.split(':'); - - return this.readDevice(peripheral, serviceUuid, characteristicUuid) - .then((value) => { - const state = decodeValue(serviceUuid, characteristicUuid, feature, value); - this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: featureExternalId, - state, - }); - return state; - }) - .catch((e) => { - logger.warn(e.message); - return Promise.resolve(); - }); - }; - - const readFeatures = (peripheral) => { - return Promise.map(device.features, (feature) => readFeature(feature, peripheral), { concurrency: 1 }); - }; - - return this.applyOnPeripheral(peripheralUuid, readFeatures); -} - -module.exports = { - poll, -}; diff --git a/server/services/bluetooth/lib/commands/bluetooth.postCreate.js b/server/services/bluetooth/lib/commands/bluetooth.postCreate.js deleted file mode 100644 index a410bcd119..0000000000 --- a/server/services/bluetooth/lib/commands/bluetooth.postCreate.js +++ /dev/null @@ -1,47 +0,0 @@ -const Promise = require('bluebird'); - -const logger = require('../../../../utils/logger'); -const { EVENTS } = require('../../../../utils/constants'); -const { decodeValue } = require('../device/bluetooth.information'); - -/** - * @description Subscribe to peripheral notification on device creation. - * @param {Object} device - Newly created Gladys device. - * @returns {Promise} All subscription promises. - * @example - * await bluetooth.postCreate(device); - */ -async function postCreate(device) { - const [, peripheralUuid] = device.external_id.split(':'); - - const subscribe = (peripheral) => { - return Promise.map( - device.features, - (feature) => { - const [, , serviceUuid, characteristicUuid] = feature.external_id.split(':'); - - const onNotify = (value) => { - this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: feature.external_id, - state: decodeValue(serviceUuid, characteristicUuid, feature, value), - }); - }; - - return this.subscribeDevice(peripheral, serviceUuid, characteristicUuid, onNotify); - }, - { concurrency: 1 }, - ).catch((e) => { - logger.error(e.message); - return Promise.resolve(); - }); - }; - - return this.applyOnPeripheral(peripheralUuid, subscribe, true).catch((e) => { - logger.error(e.message); - return Promise.resolve(); - }); -} - -module.exports = { - postCreate, -}; diff --git a/server/services/bluetooth/lib/commands/bluetooth.postDelete.js b/server/services/bluetooth/lib/commands/bluetooth.postDelete.js deleted file mode 100644 index 015ac15917..0000000000 --- a/server/services/bluetooth/lib/commands/bluetooth.postDelete.js +++ /dev/null @@ -1,37 +0,0 @@ -const Promise = require('bluebird'); - -const logger = require('../../../../utils/logger'); - -/** - * @description Unsubscribe to peripheral notification on device delete. - * @param {Object} device - Newly created Gladys device. - * @returns {Promise} All subscription promises. - * @example - * await bluetooth.postDelete(device); - */ -async function postDelete(device) { - const [, peripheralUuid] = device.external_id.split(':'); - - const unsubscribe = (peripheral) => { - return Promise.map( - device.features, - (feature) => { - const [, , serviceUuid, characteristicUuid] = feature.external_id.split(':'); - return this.unsubscribeDevice(peripheral, serviceUuid, characteristicUuid); - }, - { concurrency: 1 }, - ).catch((e) => { - logger.error(e.message); - return Promise.resolve(); - }); - }; - - return this.applyOnPeripheral(peripheralUuid, unsubscribe, true).catch((e) => { - logger.error(e.message); - return Promise.resolve(); - }); -} - -module.exports = { - postDelete, -}; diff --git a/server/services/bluetooth/lib/commands/bluetooth.scan.js b/server/services/bluetooth/lib/commands/bluetooth.scan.js index 2a17fdbd46..012c4f00d0 100644 --- a/server/services/bluetooth/lib/commands/bluetooth.scan.js +++ b/server/services/bluetooth/lib/commands/bluetooth.scan.js @@ -9,7 +9,7 @@ const { TIMERS } = require('../utils/bluetooth.constants'); * @description Scan Bluetooth peripherals. * @param {boolean} state - Set _true_ to start scanning, default _false_. * @param {string} peripheralUuid - Peripheral UUID to look for. - * @returns {Promise} Found peripherals. + * @returns {Promise} Found peripherals by uuid, or single requested peripheral. * @example * bluetooth.scan(true); */ @@ -46,7 +46,7 @@ async function scan(state, peripheralUuid = undefined) { reject(new NotFoundError(`Bluetooth: peripheral ${peripheralUuid} not found`)); } } else { - resolve(Object.values(peripherals)); + resolve(peripherals); } }; diff --git a/server/services/bluetooth/lib/commands/bluetooth.scanPresence.js b/server/services/bluetooth/lib/commands/bluetooth.scanPresence.js new file mode 100644 index 0000000000..e599f64269 --- /dev/null +++ b/server/services/bluetooth/lib/commands/bluetooth.scanPresence.js @@ -0,0 +1,46 @@ +const logger = require('../../../../utils/logger'); +const { DEVICE_FEATURE_CATEGORIES, EVENTS } = require('../../../../utils/constants'); + +/** + * @description Scan periodically for device presence. + * @example + * await this.scanPresence(); + */ +async function scanPresence() { + const devices = await this.gladys.device.get({ + service: 'bluetooth', + device_feature_category: DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR, + }); + + const features = []; + // Extract only presence features + devices.forEach((device) => { + device.features + .filter((feature) => feature.category === DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR) + .forEach((feature) => features.push(feature)); + }); + + const nbPresenceFeatures = features.length; + logger.debug(`Bluetooth: ${nbPresenceFeatures} presence sensor features`); + if (nbPresenceFeatures > 0) { + const peripherals = await this.scan(true); + + features.forEach((feature) => { + const { external_id: externalId } = feature; + const [, peripheralUuid] = externalId.split(':'); + + if (peripherals[peripheralUuid]) { + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: externalId, + state: 1, + }); + } + }); + } + + return null; +} + +module.exports = { + scanPresence, +}; diff --git a/server/services/bluetooth/lib/commands/bluetooth.setValue.js b/server/services/bluetooth/lib/commands/bluetooth.setValue.js deleted file mode 100644 index 82209b4014..0000000000 --- a/server/services/bluetooth/lib/commands/bluetooth.setValue.js +++ /dev/null @@ -1,23 +0,0 @@ -const { encodeValue } = require('../device/bluetooth.information'); - -/** - * @description Control a remote Bluetooth device - * @param {Object} device - The device to control. - * @param {Object} deviceFeature - The binary deviceFeature to control. - * @param {string|number} value - The new value. - * @returns {Promise} Resolve when the Bluetooth message is published. - * @example - * setValue({ external_id: 'bluetooth:0102030405'}, { external_id: 'mqtt:0102030405:1800:2a6e'}, 1); - */ -async function setValue(device, deviceFeature, value) { - const [, peripheralUuid, serviceUuid, characteristicUuid] = deviceFeature.external_id.split(':'); - - const encodedValue = encodeValue(serviceUuid, characteristicUuid, value); - return this.applyOnPeripheral(peripheralUuid, (peripheral) => - this.writeDevice(peripheral, serviceUuid, characteristicUuid, encodedValue), - ); -} - -module.exports = { - setValue, -}; diff --git a/server/services/bluetooth/lib/commands/bluetooth.start.js b/server/services/bluetooth/lib/commands/bluetooth.start.js index 6806d96107..70bd291e90 100644 --- a/server/services/bluetooth/lib/commands/bluetooth.start.js +++ b/server/services/bluetooth/lib/commands/bluetooth.start.js @@ -2,15 +2,15 @@ const Promise = require('bluebird'); const logger = require('../../../../utils/logger'); -const { TIMERS } = require('../device/bluetooth.information'); +const { VARIABLES, TIMERS } = require('../utils/bluetooth.constants'); /** * @description Starts to Bluetooth device. - * @returns {any} Null. + * @returns {Promise} Null. * @example - * bluetooth.start(); + * await bluetooth.start(); */ -function start() { +async function start() { logger.debug(`Bluetooth: Listening Bluetooth events`); this.bluetooth = require('@abandonware/noble'); @@ -26,7 +26,22 @@ function start() { this.bluetooth.on('discover', this.discover.bind(this)); this.bluetooth.on('connect', (peripheral) => Promise.delay(TIMERS.CONNECTION).then(() => peripheral.disconnect())); - this.connectDevices(); + // Load configuration + const scanPresenceStatus = await this.gladys.variable.getValue(VARIABLES.PRESENCE_STATUS, this.serviceId); + if (scanPresenceStatus !== null) { + this.presenceScanner.status = scanPresenceStatus; + } else { + await this.gladys.variable.setValue(VARIABLES.PRESENCE_STATUS, this.presenceScanner.status, this.serviceId); + } + + const scanFrequency = await this.gladys.variable.getValue(VARIABLES.PRESENCE_FREQUENCY, this.serviceId); + if (scanFrequency !== null) { + this.presenceScanner.frequency = scanFrequency; + } else { + await this.gladys.variable.setValue(VARIABLES.PRESENCE_FREQUENCY, this.presenceScanner.frequency, this.serviceId); + } + + this.initPresenceScanner(); return null; } diff --git a/server/services/bluetooth/lib/config/bluetooth.getConfiguration.js b/server/services/bluetooth/lib/config/bluetooth.getConfiguration.js new file mode 100644 index 0000000000..e2d8b5a0d2 --- /dev/null +++ b/server/services/bluetooth/lib/config/bluetooth.getConfiguration.js @@ -0,0 +1,16 @@ +/** + * @description Get Bluetooth configuration. + * @returns {Object} - Configuration. + * @example + * this.getConfiguration(); + */ +function getConfiguration() { + const { frequency, status } = this.presenceScanner; + return { + presenceScanner: { frequency, status }, + }; +} + +module.exports = { + getConfiguration, +}; diff --git a/server/services/bluetooth/lib/config/bluetooth.initPresenceScanner.js b/server/services/bluetooth/lib/config/bluetooth.initPresenceScanner.js new file mode 100644 index 0000000000..ffe88265d8 --- /dev/null +++ b/server/services/bluetooth/lib/config/bluetooth.initPresenceScanner.js @@ -0,0 +1,26 @@ +const logger = require('../../../../utils/logger'); +const { PRESENCE_STATUS } = require('../utils/bluetooth.constants'); + +/** + * @description Init scanner presence. + * @example + * this.initPresenceScanner(); + */ +function initPresenceScanner() { + // Manages presence scanner + const { status, frequency, timer } = this.presenceScanner; + if (timer) { + logger.info(`Bluetooth configuration: stopping presence scanner`); + clearInterval(timer); + this.presenceScanner.timer = undefined; + } + if (status === PRESENCE_STATUS.ENABLED) { + logger.info(`Bluetooth configuration: starting presence scanner`); + this.scanPresence(); + this.presenceScanner.timer = setInterval(this.scanPresence.bind(this), frequency); + } +} + +module.exports = { + initPresenceScanner, +}; diff --git a/server/services/bluetooth/lib/config/bluetooth.saveConfiguration.js b/server/services/bluetooth/lib/config/bluetooth.saveConfiguration.js new file mode 100644 index 0000000000..a370041e91 --- /dev/null +++ b/server/services/bluetooth/lib/config/bluetooth.saveConfiguration.js @@ -0,0 +1,49 @@ +const logger = require('../../../../utils/logger'); +const { BadParameters } = require('../../../../utils/coreErrors'); +const { VARIABLES, PRESENCE_STATUS, TIMERS } = require('../utils/bluetooth.constants'); + +/** + * @description Save Bluetooth configuration. + * @param {Object} configuration - Configuration to store. + * @returns {Promise} - Always null. + * @example + * await this.saveConfiguration({ ... }); + */ +async function saveConfiguration(configuration = {}) { + const { presenceScanner = {} } = configuration; + + // Check and store presence scanner frequency + if (presenceScanner.frequency !== undefined) { + const minFrequency = TIMERS.SCAN * 2; + if (presenceScanner.frequency <= minFrequency) { + throw new BadParameters(`Bluetooth presence scan frequency should greater than ${minFrequency}`); + } + + logger.debug(`Bluetooth configuration: presence scanner frequency set to ${presenceScanner.frequency}`); + await this.gladys.variable.setValue(VARIABLES.PRESENCE_FREQUENCY, presenceScanner.frequency, this.serviceId); + } + + let newStatus = this.presenceScanner.status; + // Check and store presence scanner status + if (presenceScanner.status !== undefined) { + newStatus = PRESENCE_STATUS.DISABLED; + if (presenceScanner.status === PRESENCE_STATUS.ENABLED) { + newStatus = PRESENCE_STATUS.ENABLED; + } + + logger.debug(`Bluetooth configuration: presence scanner status set to ${newStatus}`); + await this.gladys.variable.setValue(VARIABLES.PRESENCE_STATUS, newStatus, this.serviceId); + } + + // Store in memory + this.presenceScanner = { ...this.presenceScanner, ...presenceScanner, status: newStatus }; + + // Manages presence scanner + this.initPresenceScanner(); + + return this.getConfiguration(); +} + +module.exports = { + saveConfiguration, +}; diff --git a/server/services/bluetooth/lib/events/bluetooth.discover.js b/server/services/bluetooth/lib/events/bluetooth.discover.js index 58eadc1d6d..ffbe1af094 100644 --- a/server/services/bluetooth/lib/events/bluetooth.discover.js +++ b/server/services/bluetooth/lib/events/bluetooth.discover.js @@ -14,13 +14,12 @@ function discover(noblePeripheral) { // Store device if not already there if (!this.peripheralLookup && !this.discoveredDevices[noblePeripheral.uuid]) { - let device = transformToDevice(noblePeripheral); - device = this.completeDevice(device); + const device = transformToDevice(noblePeripheral); this.discoveredDevices[noblePeripheral.uuid] = device; this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { type: WEBSOCKET_MESSAGE_TYPES.BLUETOOTH.DISCOVER, - payload: device, + payload: this.completeDevice(device), }); } } diff --git a/server/services/bluetooth/lib/index.js b/server/services/bluetooth/lib/index.js index e5b44f43ac..97eb47aa98 100644 --- a/server/services/bluetooth/lib/index.js +++ b/server/services/bluetooth/lib/index.js @@ -1,3 +1,5 @@ +const { PRESENCE_STATUS, TIMERS } = require('./utils/bluetooth.constants'); + // EVENTS const { stateChange } = require('./events/bluetooth.stateChange'); const { scanStart } = require('./events/bluetooth.scanStart'); @@ -9,22 +11,23 @@ const { broadcastStatus } = require('./events/bluetooth.broadcastStatus'); const { start } = require('./commands/bluetooth.start'); const { stop } = require('./commands/bluetooth.stop'); const { scan } = require('./commands/bluetooth.scan'); +const { scanPresence } = require('./commands/bluetooth.scanPresence'); const { completeDevice } = require('./commands/bluetooth.completeDevice'); const { getDiscoveredDevice } = require('./commands/bluetooth.getDiscoveredDevice'); const { getDiscoveredDevices } = require('./commands/bluetooth.getDiscoveredDevices'); const { getStatus } = require('./commands/bluetooth.getStatus'); const { readDevice } = require('./commands/bluetooth.readDevice'); -const { poll } = require('./commands/bluetooth.poll'); -const { setValue } = require('./commands/bluetooth.setValue'); const { writeDevice } = require('./commands/bluetooth.writeDevice'); const { scanDevice } = require('./commands/bluetooth.scanDevice'); -const { connectDevices } = require('./commands/bluetooth.connectDevices'); const { subscribeDevice } = require('./commands/bluetooth.subscribeDevice'); const { unsubscribeDevice } = require('./commands/bluetooth.unsubscribeDevice'); const { applyOnPeripheral } = require('./commands/bluetooth.applyOnPeripheral'); const { getCharacteristic } = require('./commands/bluetooth.getCharacteristic'); -const { postCreate } = require('./commands/bluetooth.postCreate'); -const { postDelete } = require('./commands/bluetooth.postDelete'); + +// CONFIG +const { saveConfiguration } = require('./config/bluetooth.saveConfiguration'); +const { getConfiguration } = require('./config/bluetooth.getConfiguration'); +const { initPresenceScanner } = require('./config/bluetooth.initPresenceScanner'); const BluetoothManager = function BluetoothManager(gladys, serviceId) { this.bluetooth = undefined; @@ -39,6 +42,11 @@ const BluetoothManager = function BluetoothManager(gladys, serviceId) { this.peripheralLookup = false; this.discoveredDevices = {}; + + this.presenceScanner = { + status: PRESENCE_STATUS.ENABLED, + frequency: TIMERS.PRESENCE, + }; }; // EVENTS @@ -52,6 +60,7 @@ BluetoothManager.prototype.broadcastStatus = broadcastStatus; BluetoothManager.prototype.start = start; BluetoothManager.prototype.stop = stop; BluetoothManager.prototype.scan = scan; +BluetoothManager.prototype.scanPresence = scanPresence; BluetoothManager.prototype.completeDevice = completeDevice; BluetoothManager.prototype.getDiscoveredDevice = getDiscoveredDevice; BluetoothManager.prototype.getDiscoveredDevices = getDiscoveredDevices; @@ -59,16 +68,14 @@ BluetoothManager.prototype.getStatus = getStatus; BluetoothManager.prototype.readDevice = readDevice; BluetoothManager.prototype.writeDevice = writeDevice; BluetoothManager.prototype.scanDevice = scanDevice; -BluetoothManager.prototype.connectDevices = connectDevices; BluetoothManager.prototype.subscribeDevice = subscribeDevice; BluetoothManager.prototype.unsubscribeDevice = unsubscribeDevice; BluetoothManager.prototype.applyOnPeripheral = applyOnPeripheral; BluetoothManager.prototype.getCharacteristic = getCharacteristic; -// Gladys commands -BluetoothManager.prototype.setValue = setValue; -BluetoothManager.prototype.poll = poll; -BluetoothManager.prototype.postCreate = postCreate; -BluetoothManager.prototype.postDelete = postDelete; +// CONFIG +BluetoothManager.prototype.saveConfiguration = saveConfiguration; +BluetoothManager.prototype.getConfiguration = getConfiguration; +BluetoothManager.prototype.initPresenceScanner = initPresenceScanner; module.exports = BluetoothManager; diff --git a/server/services/bluetooth/lib/utils/bluetooth.constants.js b/server/services/bluetooth/lib/utils/bluetooth.constants.js index 77f1ec9b09..7db104978c 100644 --- a/server/services/bluetooth/lib/utils/bluetooth.constants.js +++ b/server/services/bluetooth/lib/utils/bluetooth.constants.js @@ -1,9 +1,20 @@ +const VARIABLES = { + PRESENCE_STATUS: 'BLUETOOTH_PRESENCE_STATUS', + PRESENCE_FREQUENCY: 'BLUETOOTH_PRESENCE_FREQUENCY', +}; + +const PRESENCE_STATUS = { + ENABLED: 'enabled', + DISABLED: 'disabled', +}; + const TIMERS = { SCAN: 5000, CONNECT: 5000, DISCOVER: 5000, READ: 2000, WRITE: 2000, + PRESENCE: 60000, }; const PARAMS = { @@ -12,4 +23,4 @@ const PARAMS = { MANUFACTURER: 'manufacturer', }; -module.exports = { TIMERS, PARAMS }; +module.exports = { VARIABLES, PRESENCE_STATUS, TIMERS, PARAMS }; diff --git a/server/test/lib/scene/scene.executeActions.test.js b/server/test/lib/scene/scene.executeActions.test.js index a4e975d667..481f38bc49 100644 --- a/server/test/lib/scene/scene.executeActions.test.js +++ b/server/test/lib/scene/scene.executeActions.test.js @@ -325,6 +325,60 @@ describe('scene.executeActions', () => { ); assert.calledWith(house.userLeft, 'my-house', 'john'); }); + it('should execute action user.checkPresence and not call userLeft because user was seen', async () => { + const stateManager = new StateManager(event); + stateManager.setState('deviceFeature', 'my-device', { + last_value_changed: Date.now(), + }); + const house = { + userSeen: fake.resolves(null), + userLeft: fake.resolves(null), + }; + const scope = {}; + await executeActions( + { stateManager, event, house }, + [ + [ + { + type: ACTIONS.USER.CHECK_PRESENCE, + user: 'john', + house: 'my-house', + minutes: 10, + device_features: ['my-device'], + }, + ], + ], + scope, + ); + assert.notCalled(house.userLeft); + }); + it('should execute action user.checkPresence and call userLeft because user was not seen', async () => { + const stateManager = new StateManager(event); + stateManager.setState('deviceFeature', 'my-device', { + last_value_changed: Date.now() - 15 * 60 * 1000, + }); + const house = { + userSeen: fake.resolves(null), + userLeft: fake.resolves(null), + }; + const scope = {}; + await executeActions( + { stateManager, event, house }, + [ + [ + { + type: ACTIONS.USER.CHECK_PRESENCE, + user: 'john', + house: 'my-house', + minutes: 10, + device_features: ['my-device'], + }, + ], + ], + scope, + ); + assert.calledWith(house.userLeft, 'my-house', 'john'); + }); it('should execute action http.request', async () => { const stateManager = new StateManager(event); const http = { diff --git a/server/test/services/bluetooth/api/bluetooth.controller.test.js b/server/test/services/bluetooth/api/bluetooth.controller.test.js index d0680ed419..ebe1c3fc67 100644 --- a/server/test/services/bluetooth/api/bluetooth.controller.test.js +++ b/server/test/services/bluetooth/api/bluetooth.controller.test.js @@ -13,12 +13,17 @@ const peripherals = [ }, ]; +const config = { config: true }; + const bluetoothManager = function bluetoothManager() {}; bluetoothManager.getStatus = fake.returns(status); bluetoothManager.getDiscoveredDevices = fake.returns(peripherals); bluetoothManager.scan = fake.returns(null); bluetoothManager.scanDevice = fake.resolves(null); +bluetoothManager.scanPresence = fake.resolves(null); +bluetoothManager.getConfiguration = fake.returns(config); +bluetoothManager.saveConfiguration = fake.resolves(config); const res = { json: fake.returns(null), @@ -114,3 +119,46 @@ describe('POST /api/v1/service/bluetooth/scan/bluetooth-:uuid', () => { assert.calledWith(res.json, status); }); }); + +describe('POST /api/v1/service/bluetooth/presence', () => { + beforeEach(() => { + sinon.reset(); + }); + + it('should start presence scanner', async () => { + const bluetoothController = BluetoothController(bluetoothManager); + const req = {}; + await bluetoothController['post /api/v1/service/bluetooth/presence'].controller(req, res); + assert.calledOnce(bluetoothManager.scanPresence); + assert.calledWith(res.status, 200); + }); +}); + +describe('GET /api/v1/service/bluetooth/config', () => { + beforeEach(() => { + sinon.reset(); + }); + + it('should get config', async () => { + const bluetoothController = BluetoothController(bluetoothManager); + const req = {}; + await bluetoothController['get /api/v1/service/bluetooth/config'].controller(req, res); + assert.calledOnce(bluetoothManager.getConfiguration); + assert.calledWith(res.json, config); + }); +}); + +describe('POST /api/v1/service/bluetooth/config', () => { + beforeEach(() => { + sinon.reset(); + }); + + it('should save config', async () => { + const bluetoothController = BluetoothController(bluetoothManager); + const body = {}; + const req = { body }; + await bluetoothController['post /api/v1/service/bluetooth/config'].controller(req, res); + assert.calledWith(bluetoothManager.saveConfiguration, body); + assert.calledWith(res.json, config); + }); +}); diff --git a/server/test/services/bluetooth/index.test.js b/server/test/services/bluetooth/index.test.js index bd07ca6041..bffeea9008 100644 --- a/server/test/services/bluetooth/index.test.js +++ b/server/test/services/bluetooth/index.test.js @@ -26,6 +26,9 @@ const gladys = { device: { get: () => [], }, + variable: { + getValue: () => 'value', + }, }; describe('BluetoothService', () => { diff --git a/server/test/services/bluetooth/lib/commands/bluetooth.connectDevices.test.js b/server/test/services/bluetooth/lib/commands/bluetooth.connectDevices.test.js deleted file mode 100644 index 27c878f866..0000000000 --- a/server/test/services/bluetooth/lib/commands/bluetooth.connectDevices.test.js +++ /dev/null @@ -1,93 +0,0 @@ -const sinon = require('sinon'); - -const { fake, assert } = sinon; - -const BluetoothManager = require('../../../../../services/bluetooth/lib'); -const BluetoothMock = require('../../BluetoothMock.test'); - -const device = { - external_id: 'bluetooth:uuid', - features: [ - { - external_id: 'bluetooth:uuid:1809:2a6e', - }, - ], -}; -const gladys = { - device: { - get: fake.resolves([device]), - }, - event: { - emit: fake.returns(null), - }, -}; -const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; - -describe('bluetooth.connectDevices command', () => { - let bluetooth; - let bluetoothManager; - - let peripheral; - let service; - let characteristic; - - beforeEach(() => { - characteristic = { - uuid: '2a6e', - properties: ['notify'], - subscribe: fake.yields(null), - on: fake.returns(null), - }; - - service = { - uuid: '1809', - discoverCharacteristics: fake.yields(null, [characteristic]), - }; - - peripheral = { - uuid: 'uuid', - connectable: true, - connect: fake.yields(null), - discoverServices: fake.yields(null, [service]), - }; - - bluetooth = new BluetoothMock(); - - bluetoothManager = new BluetoothManager(gladys, serviceId); - bluetoothManager.bluetooth = bluetooth; - bluetooth.startScanning = () => { - bluetooth.emit('discover', peripheral); - bluetooth.emit('scanStop'); - }; - }); - - afterEach(() => { - if (bluetoothManager.scanPromise && bluetoothManager.scanPromise.isPending()) { - bluetoothManager.scanPromise.cancel(); - } - - sinon.reset(); - }); - - it('subscribe to peripheral', async () => { - await bluetoothManager.connectDevices(); - - assert.calledOnce(peripheral.connect); - assert.calledOnce(peripheral.discoverServices); - assert.calledOnce(service.discoverCharacteristics); - assert.calledOnce(characteristic.subscribe); - assert.calledOnce(characteristic.on); - }); - - it('subscribe to peripheral with error', async () => { - peripheral.discoverServices = fake.yields('error'); - - await bluetoothManager.connectDevices(); - - assert.calledOnce(peripheral.connect); - assert.calledOnce(peripheral.discoverServices); - assert.notCalled(service.discoverCharacteristics); - assert.notCalled(characteristic.subscribe); - assert.notCalled(characteristic.on); - }); -}); diff --git a/server/test/services/bluetooth/lib/commands/bluetooth.poll.test.js b/server/test/services/bluetooth/lib/commands/bluetooth.poll.test.js deleted file mode 100644 index 3d504e1bc6..0000000000 --- a/server/test/services/bluetooth/lib/commands/bluetooth.poll.test.js +++ /dev/null @@ -1,115 +0,0 @@ -const sinon = require('sinon'); - -const { fake, assert } = sinon; - -const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../../utils/constants'); - -const BluetoothManager = require('../../../../../services/bluetooth/lib'); -const BluetoothMock = require('../../BluetoothMock.test'); - -const gladys = { - event: { - emit: fake.returns(null), - }, -}; -const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; - -describe('bluetooth.poll command', () => { - let bluetooth; - let bluetoothManager; - - let peripheral; - let service; - let characteristic; - - beforeEach(() => { - characteristic = { - uuid: '2a6e', - properties: ['read'], - read: fake.yields(null, 'd'), - }; - - service = { - uuid: '1809', - discoverCharacteristics: fake.yields(null, [characteristic]), - }; - - peripheral = { - uuid: 'uuid', - connectable: true, - connect: fake.yields(null), - disconnect: fake.resolves(null), - discoverServices: fake.yields(null, [service]), - }; - - bluetooth = new BluetoothMock(); - - bluetoothManager = new BluetoothManager(gladys, serviceId); - bluetoothManager.bluetooth = bluetooth; - bluetooth.startScanning = () => { - bluetooth.emit('discover', peripheral); - bluetooth.emit('scanStop'); - }; - }); - - afterEach(() => { - if (bluetoothManager.scanPromise && bluetoothManager.scanPromise.isPending()) { - bluetoothManager.scanPromise.cancel(); - } - - sinon.reset(); - }); - - it('feature read value', async () => { - const device = { - external_id: 'bluetooth:uuid', - features: [ - { - external_id: 'bluetooth:uuid:1809:2a6e', - }, - ], - }; - - await bluetoothManager.poll(device); - - assert.calledOnce(peripheral.connect); - assert.calledOnce(peripheral.discoverServices); - assert.calledOnce(peripheral.disconnect); - assert.calledOnce(service.discoverCharacteristics); - assert.calledOnce(characteristic.read); - assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.BLUETOOTH.STATE, - payload: { peripheralLookup: false, ready: false, scanning: false }, - }); - assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { - device_feature_external_id: 'bluetooth:uuid:1809:2a6e', - state: 13, - }); - }); - - it('error on getCharacteristic', async () => { - service.discoverCharacteristics = fake.yields(new Error('error')); - - const device = { - external_id: 'bluetooth:uuid', - features: [ - { - external_id: 'bluetooth:uuid:1809:2a6e', - }, - ], - }; - - await bluetoothManager.poll(device); - - assert.calledOnce(peripheral.connect); - assert.calledOnce(peripheral.discoverServices); - assert.calledOnce(peripheral.disconnect); - assert.calledOnce(service.discoverCharacteristics); - assert.notCalled(characteristic.read); - assert.calledOnce(gladys.event.emit); - assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.BLUETOOTH.STATE, - payload: { peripheralLookup: false, ready: false, scanning: false }, - }); - }); -}); diff --git a/server/test/services/bluetooth/lib/commands/bluetooth.postCreate.test.js b/server/test/services/bluetooth/lib/commands/bluetooth.postCreate.test.js deleted file mode 100644 index a260a6d0c8..0000000000 --- a/server/test/services/bluetooth/lib/commands/bluetooth.postCreate.test.js +++ /dev/null @@ -1,113 +0,0 @@ -const sinon = require('sinon'); -const EventEmitter = require('events'); - -const { fake, assert } = sinon; - -const BluetoothManager = require('../../../../../services/bluetooth/lib'); -const BluetoothMock = require('../../BluetoothMock.test'); - -const device = { - external_id: 'bluetooth:uuid', - features: [ - { - external_id: 'bluetooth:uuid:1809:2a6e', - }, - ], -}; -const gladys = { - event: { - emit: fake.returns(null), - }, -}; -const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; - -describe('bluetooth.postCreate command', () => { - let bluetooth; - let bluetoothManager; - - let peripheral; - let service; - let characteristic; - - beforeEach(() => { - characteristic = new EventEmitter(); - characteristic.uuid = '2a6e'; - characteristic.properties = ['notify']; - characteristic.subscribe = fake.yields(null); - - service = { - uuid: '1809', - discoverCharacteristics: fake.yields(null, [characteristic]), - }; - - peripheral = { - uuid: 'uuid', - connectable: true, - connect: fake.yields(null), - discoverServices: fake.yields(null, [service]), - }; - - bluetooth = new BluetoothMock(); - - bluetoothManager = new BluetoothManager(gladys, serviceId); - bluetoothManager.bluetooth = bluetooth; - bluetooth.startScanning = () => { - bluetooth.emit('discover', peripheral); - bluetooth.emit('scanStop'); - }; - }); - - afterEach(() => { - if (bluetoothManager.scanPromise && bluetoothManager.scanPromise.isPending()) { - bluetoothManager.scanPromise.cancel(); - } - - sinon.reset(); - }); - - it('subscribe to peripheral', async () => { - await bluetoothManager.postCreate(device); - - assert.calledOnce(peripheral.connect); - assert.calledOnce(peripheral.discoverServices); - assert.calledOnce(service.discoverCharacteristics); - assert.calledOnce(characteristic.subscribe); - assert.calledOnce(gladys.event.emit); - - characteristic.emit('notify', 'value'); - - assert.calledTwice(gladys.event.emit); - }); - - it('subscribe to peripheral with error', async () => { - peripheral.discoverServices = fake.yields('error'); - - await bluetoothManager.postCreate(device); - - assert.calledOnce(peripheral.connect); - assert.calledOnce(peripheral.discoverServices); - assert.notCalled(service.discoverCharacteristics); - assert.notCalled(characteristic.subscribe); - assert.calledOnce(gladys.event.emit); - - characteristic.emit('notify', 'value'); - - assert.calledOnce(gladys.event.emit); - }); - - it('subscribe to peripheral with error (on connect)', async () => { - peripheral.connect = fake.yields(new Error('error')); - - await bluetoothManager.postCreate(device); - - assert.calledOnce(peripheral.connect); - assert.notCalled(peripheral.discoverServices); - assert.notCalled(service.discoverCharacteristics); - assert.notCalled(characteristic.subscribe); - assert.calledOnce(gladys.event.emit); - - characteristic.emit('notify', 'value'); - - assert.calledOnce(gladys.event.emit); - }); -}); diff --git a/server/test/services/bluetooth/lib/commands/bluetooth.postDelete.test.js b/server/test/services/bluetooth/lib/commands/bluetooth.postDelete.test.js deleted file mode 100644 index 080dd45d89..0000000000 --- a/server/test/services/bluetooth/lib/commands/bluetooth.postDelete.test.js +++ /dev/null @@ -1,98 +0,0 @@ -const sinon = require('sinon'); - -const { fake, assert } = sinon; - -const BluetoothManager = require('../../../../../services/bluetooth/lib'); -const BluetoothMock = require('../../BluetoothMock.test'); - -const device = { - external_id: 'bluetooth:uuid', - features: [ - { - external_id: 'bluetooth:uuid:1809:2a6e', - }, - ], -}; -const gladys = { - event: { - emit: fake.returns(null), - }, -}; -const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; - -describe('bluetooth.postDelete command', () => { - let bluetooth; - let bluetoothManager; - - let peripheral; - let service; - let characteristic; - - beforeEach(() => { - characteristic = { - uuid: '2a6e', - properties: ['notify'], - unsubscribe: fake.yields(null), - }; - - service = { - uuid: '1809', - discoverCharacteristics: fake.yields(null, [characteristic]), - }; - - peripheral = { - uuid: 'uuid', - connectable: true, - connect: fake.yields(null), - discoverServices: fake.yields(null, [service]), - }; - - bluetooth = new BluetoothMock(); - - bluetoothManager = new BluetoothManager(gladys, serviceId); - bluetoothManager.bluetooth = bluetooth; - bluetooth.startScanning = () => { - bluetooth.emit('discover', peripheral); - bluetooth.emit('scanStop'); - }; - }); - - afterEach(() => { - if (bluetoothManager.scanPromise && bluetoothManager.scanPromise.isPending()) { - bluetoothManager.scanPromise.cancel(); - } - - sinon.reset(); - }); - - it('unsubscribe to peripheral', async () => { - await bluetoothManager.postDelete(device); - - assert.calledOnce(peripheral.connect); - assert.calledOnce(peripheral.discoverServices); - assert.calledOnce(service.discoverCharacteristics); - assert.calledOnce(characteristic.unsubscribe); - }); - - it('unsubscribe to peripheral with error', async () => { - peripheral.discoverServices = fake.yields('error'); - - await bluetoothManager.postDelete(device); - - assert.calledOnce(peripheral.connect); - assert.calledOnce(peripheral.discoverServices); - assert.notCalled(service.discoverCharacteristics); - assert.notCalled(characteristic.unsubscribe); - }); - - it('unsubscribe to peripheral with error (on connect)', async () => { - peripheral.connect = fake.yields(new Error('error')); - - await bluetoothManager.postDelete(device); - - assert.calledOnce(peripheral.connect); - assert.notCalled(peripheral.discoverServices); - assert.notCalled(service.discoverCharacteristics); - assert.notCalled(characteristic.unsubscribe); - }); -}); diff --git a/server/test/services/bluetooth/lib/commands/bluetooth.scanPresence.test.js b/server/test/services/bluetooth/lib/commands/bluetooth.scanPresence.test.js new file mode 100644 index 0000000000..453f154398 --- /dev/null +++ b/server/test/services/bluetooth/lib/commands/bluetooth.scanPresence.test.js @@ -0,0 +1,104 @@ +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); + +const { assert, fake } = sinon; + +const peripherals = { + 'presence-uuid': {}, +}; +const bluetoothScanCommandMock = fake.resolves(peripherals); +const BluetoothManager = proxyquire('../../../../../services/bluetooth/lib', { + './commands/bluetooth.scan.js': { scan: bluetoothScanCommandMock }, +}); +const { DEVICE_FEATURE_CATEGORIES } = require('../../../../../utils/constants'); + +const BluetoothMock = require('../../BluetoothMock.test'); + +const gladys = { + event: { + emit: fake.returns(null), + }, + device: { + get: fake.resolves([]), + }, +}; +const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; + +describe('bluetooth.scanPresence', () => { + let bluetoothManager; + let bluetooth; + + beforeEach(() => { + bluetooth = new BluetoothMock(); + bluetoothManager = new BluetoothManager(gladys, serviceId); + bluetoothManager.bluetooth = bluetooth; + }); + + afterEach(() => { + sinon.reset(); + }); + + it('bluetooth.scanPresence no stored devices', async () => { + await bluetoothManager.scanPresence(); + + assert.calledOnce(gladys.device.get); + assert.notCalled(bluetoothScanCommandMock); + assert.notCalled(gladys.event.emit); + }); + + it('bluetooth.scanPresence no presence devices', async () => { + gladys.device.get = fake.resolves([ + { + features: [ + { + category: DEVICE_FEATURE_CATEGORIES.BATTERY, + }, + ], + }, + ]); + + await bluetoothManager.scanPresence(); + + assert.calledOnce(gladys.device.get); + assert.notCalled(bluetoothScanCommandMock); + assert.notCalled(gladys.event.emit); + }); + + it('bluetooth.scanPresence presence device not discovered', async () => { + gladys.device.get = fake.resolves([ + { + features: [ + { + external_id: 'bluetooth:unknown-uuid', + category: DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR, + }, + ], + }, + ]); + + await bluetoothManager.scanPresence(); + + assert.calledOnce(gladys.device.get); + assert.calledOnce(bluetoothScanCommandMock); + assert.notCalled(gladys.event.emit); + }); + + it('bluetooth.scanPresence matching presence device', async () => { + gladys.device.get = fake.resolves([ + { + features: [ + { + external_id: 'bluetooth:presence-uuid', + category: DEVICE_FEATURE_CATEGORIES.PRESENCE_SENSOR, + }, + ], + }, + ]); + + await bluetoothManager.scanPresence(); + + assert.calledOnce(gladys.device.get); + assert.calledOnce(bluetoothScanCommandMock); + assert.calledOnce(gladys.event.emit); + }); +}); diff --git a/server/test/services/bluetooth/lib/commands/bluetooth.setValue.test.js b/server/test/services/bluetooth/lib/commands/bluetooth.setValue.test.js deleted file mode 100644 index aabc3c0f08..0000000000 --- a/server/test/services/bluetooth/lib/commands/bluetooth.setValue.test.js +++ /dev/null @@ -1,81 +0,0 @@ -const sinon = require('sinon'); - -const { assert, fake } = sinon; - -const BluetoothManager = require('../../../../../services/bluetooth/lib'); -const BluetoothMock = require('../../BluetoothMock.test'); - -const gladys = { - event: { - emit: fake.returns(null), - }, -}; -const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; - -describe('bluetooth.setValue command', () => { - let characteristic; - let service; - let peripheral; - - let bluetooth; - let bluetoothManager; - - beforeEach(() => { - characteristic = { - uuid: '2a00', - properties: ['write'], - write: fake.yields(null), - }; - - service = { - uuid: '1800', - discoverCharacteristics: fake.yields(null, [characteristic]), - }; - - peripheral = { - uuid: 'uuid', - address: 'A1', - addressType: 'public', - rssi: 'R1', - advertisement: { - localName: 'P1', - }, - lastSeen: 'D1', - connectable: true, - connect: fake.yields(null), - disconnect: fake.resolves(null), - discoverServices: fake.yields(null, [service]), - }; - - bluetooth = new BluetoothMock(); - bluetoothManager = new BluetoothManager(gladys, serviceId); - bluetoothManager.bluetooth = bluetooth; - - bluetooth.startScanning = () => { - bluetooth.emit('discover', peripheral); - bluetooth.emit('scanStop'); - }; - }); - - afterEach(() => { - sinon.reset(); - }); - - it('should write on peripheral', async () => { - const device = {}; - const feature = { - external_id: 'bluetooth:uuid:1800:2a00', - }; - const value = 1; - - await bluetoothManager.setValue(device, feature, value); - - assert.calledOnce(peripheral.connect); - assert.calledOnce(peripheral.disconnect); - assert.calledOnce(peripheral.discoverServices); - assert.calledOnce(service.discoverCharacteristics); - assert.calledOnce(characteristic.write); - - assert.calledOnce(gladys.event.emit); - }); -}); diff --git a/server/test/services/bluetooth/lib/commands/bluetooth.start.test.js b/server/test/services/bluetooth/lib/commands/bluetooth.start.test.js index 9d2ffb3336..718ffc4ff1 100644 --- a/server/test/services/bluetooth/lib/commands/bluetooth.start.test.js +++ b/server/test/services/bluetooth/lib/commands/bluetooth.start.test.js @@ -2,6 +2,8 @@ const { expect } = require('chai'); const sinon = require('sinon'); const proxyquire = require('proxyquire').noCallThru(); +const { assert, fake } = sinon; + const BluetoothMock = require('../../BluetoothMock.test'); const bluetooth = new BluetoothMock(); @@ -14,8 +16,12 @@ const BluetoothManager = proxyquire('../../../../../services/bluetooth/lib', { }); const gladys = { + variable: { + getValue: fake.resolves(null), + setValue: fake.resolves(null), + }, device: { - get: () => [], + get: fake.resolves([]), }, }; const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; @@ -32,8 +38,26 @@ describe('bluetooth.start command', () => { sinon.reset(); }); - it('check listeners well added', () => { - bluetoothManager.start(); + it('check listeners well added, init default values', async () => { + await bluetoothManager.start(); + + expect(bluetooth.listenerCount('stateChange')).eq(1); + expect(bluetooth.listenerCount('scanStart')).eq(1); + expect(bluetooth.listenerCount('scanStop')).eq(1); + expect(bluetooth.listenerCount('discover')).eq(1); + expect(bluetooth.listenerCount('connect')).eq(1); + + // All listeners + expect(bluetooth.eventNames().length).eq(5); + + assert.callCount(gladys.variable.getValue, 2); + assert.callCount(gladys.variable.setValue, 2); + }); + + it('check listeners well added, load default values', async () => { + gladys.variable.getValue = fake.resolves('value'); + + await bluetoothManager.start(); expect(bluetooth.listenerCount('stateChange')).eq(1); expect(bluetooth.listenerCount('scanStart')).eq(1); @@ -43,5 +67,8 @@ describe('bluetooth.start command', () => { // All listeners expect(bluetooth.eventNames().length).eq(5); + + assert.callCount(gladys.variable.getValue, 2); + assert.notCalled(gladys.variable.setValue); }); }); diff --git a/server/test/services/bluetooth/lib/commands/bluetooth.subscribeDevice.test.js b/server/test/services/bluetooth/lib/commands/bluetooth.subscribeDevice.test.js new file mode 100644 index 0000000000..a24bcf44f3 --- /dev/null +++ b/server/test/services/bluetooth/lib/commands/bluetooth.subscribeDevice.test.js @@ -0,0 +1,135 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { assert, fake } = sinon; + +const BluetoothManager = require('../../../../../services/bluetooth/lib'); +const BluetoothMock = require('../../BluetoothMock.test'); + +const { BadParameters } = require('../../../../../utils/coreErrors'); + +const gladys = {}; +const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; +const onNotify = fake.resolves(null); + +describe('bluetooth.subscribeDevice', () => { + let throwError; + + let characteristic; + let service; + let peripheral; + + let bluetooth; + let bluetoothManager; + + beforeEach(() => { + throwError = false; + + characteristic = { + uuid: '2a00', + properties: ['notify', 'read'], + subscribe: (callback) => { + if (throwError) { + callback('error'); + } else { + callback(null, 'd'); + } + }, + read: (callback) => { + if (throwError) { + callback('error'); + } else { + callback(null, 'd'); + } + }, + on: fake.returns(null), + }; + + service = { + uuid: '1800', + discoverCharacteristics: fake.yields(null, [characteristic]), + }; + + peripheral = { + uuid: 'uuid', + address: 'A1', + addressType: 'public', + rssi: 'R1', + advertisement: { + localName: 'P1', + }, + lastSeen: 'D1', + discoverServices: fake.yields(null, [service]), + }; + + bluetooth = new BluetoothMock(); + bluetoothManager = new BluetoothManager(gladys, serviceId); + bluetoothManager.bluetooth = bluetooth; + + bluetooth.startScanning = () => { + bluetooth.emit('discover', peripheral); + bluetooth.emit('scanStop'); + }; + }); + + afterEach(() => { + if (bluetoothManager.scanPromise && bluetoothManager.scanPromise.isPending()) { + bluetoothManager.scanPromise.cancel(); + } + + sinon.reset(); + }); + + it('bluetooth.subscribeDevice success', async () => { + await bluetoothManager.subscribeDevice(peripheral, service.uuid, characteristic.uuid, onNotify); + + assert.calledOnce(peripheral.discoverServices); + assert.calledOnce(service.discoverCharacteristics); + assert.calledOnce(characteristic.on); + assert.calledOnce(onNotify); + }); + + it('bluetooth.subscribeDevice error on notify', async () => { + throwError = true; + + try { + await bluetoothManager.subscribeDevice(peripheral, service.uuid, characteristic.uuid, onNotify); + assert.fail(); + } catch (e) { + expect(e).instanceOf(Error); + } + + assert.calledOnce(peripheral.discoverServices); + assert.calledOnce(service.discoverCharacteristics); + assert.notCalled(onNotify); + }); + + it('bluetooth.subscribeDevice not notifiable (no props)', async () => { + characteristic.properties = undefined; + + try { + await bluetoothManager.subscribeDevice(peripheral, service.uuid, characteristic.uuid, onNotify); + assert.fail(); + } catch (e) { + expect(e).instanceOf(BadParameters); + } + assert.calledOnce(peripheral.discoverServices); + assert.calledOnce(service.discoverCharacteristics); + assert.notCalled(onNotify); + }); + + it('bluetooth.subscribeDevice not notifiable prop', async () => { + characteristic.properties = ['write']; + + try { + await bluetoothManager.subscribeDevice(peripheral, service.uuid, characteristic.uuid, onNotify); + assert.fail(); + } catch (e) { + expect(e).instanceOf(BadParameters); + } + + assert.calledOnce(peripheral.discoverServices); + assert.calledOnce(service.discoverCharacteristics); + assert.notCalled(onNotify); + }); +}); diff --git a/server/test/services/bluetooth/lib/commands/bluetooth.unsubscribeDevice.test.js b/server/test/services/bluetooth/lib/commands/bluetooth.unsubscribeDevice.test.js new file mode 100644 index 0000000000..ceb1e41ddb --- /dev/null +++ b/server/test/services/bluetooth/lib/commands/bluetooth.unsubscribeDevice.test.js @@ -0,0 +1,121 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { assert, fake } = sinon; + +const BluetoothManager = require('../../../../../services/bluetooth/lib'); +const BluetoothMock = require('../../BluetoothMock.test'); + +const { BadParameters } = require('../../../../../utils/coreErrors'); + +const gladys = {}; +const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; + +describe('bluetooth.unsubscribeDevice', () => { + let throwError; + + let characteristic; + let service; + let peripheral; + + let bluetooth; + let bluetoothManager; + + beforeEach(() => { + throwError = false; + + characteristic = { + uuid: '2a00', + properties: ['notify', 'read'], + unsubscribe: (callback) => { + if (throwError) { + callback('error'); + } else { + callback(null, 'd'); + } + }, + }; + + service = { + uuid: '1800', + discoverCharacteristics: fake.yields(null, [characteristic]), + }; + + peripheral = { + uuid: 'uuid', + address: 'A1', + addressType: 'public', + rssi: 'R1', + advertisement: { + localName: 'P1', + }, + lastSeen: 'D1', + discoverServices: fake.yields(null, [service]), + }; + + bluetooth = new BluetoothMock(); + bluetoothManager = new BluetoothManager(gladys, serviceId); + bluetoothManager.bluetooth = bluetooth; + + bluetooth.startScanning = () => { + bluetooth.emit('discover', peripheral); + bluetooth.emit('scanStop'); + }; + }); + + afterEach(() => { + if (bluetoothManager.scanPromise && bluetoothManager.scanPromise.isPending()) { + bluetoothManager.scanPromise.cancel(); + } + + sinon.reset(); + }); + + it('bluetooth.unsubscribeDevice success', async () => { + await bluetoothManager.unsubscribeDevice(peripheral, service.uuid, characteristic.uuid); + + assert.calledOnce(peripheral.discoverServices); + assert.calledOnce(service.discoverCharacteristics); + }); + + it('bluetooth.unsubscribeDevice error on notify', async () => { + throwError = true; + + try { + await bluetoothManager.unsubscribeDevice(peripheral, service.uuid, characteristic.uuid); + assert.fail(); + } catch (e) { + expect(e).instanceOf(Error); + } + + assert.calledOnce(peripheral.discoverServices); + assert.calledOnce(service.discoverCharacteristics); + }); + + it('bluetooth.unsubscribeDevice not notifiable (no props)', async () => { + characteristic.properties = undefined; + + try { + await bluetoothManager.unsubscribeDevice(peripheral, service.uuid, characteristic.uuid); + assert.fail(); + } catch (e) { + expect(e).instanceOf(BadParameters); + } + assert.calledOnce(peripheral.discoverServices); + assert.calledOnce(service.discoverCharacteristics); + }); + + it('bluetooth.unsubscribeDevice not notifiable prop', async () => { + characteristic.properties = ['write']; + + try { + await bluetoothManager.unsubscribeDevice(peripheral, service.uuid, characteristic.uuid); + assert.fail(); + } catch (e) { + expect(e).instanceOf(BadParameters); + } + + assert.calledOnce(peripheral.discoverServices); + assert.calledOnce(service.discoverCharacteristics); + }); +}); diff --git a/server/test/services/bluetooth/lib/config/bluetooth.getConfiguration.test.js b/server/test/services/bluetooth/lib/config/bluetooth.getConfiguration.test.js new file mode 100644 index 0000000000..c3daa67b5d --- /dev/null +++ b/server/test/services/bluetooth/lib/config/bluetooth.getConfiguration.test.js @@ -0,0 +1,20 @@ +const { expect } = require('chai'); + +const BluetoothManager = require('../../../../../services/bluetooth/lib'); + +const gladys = {}; +const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; + +describe('bluetooth.getConfiguration', () => { + it('get config from service', async () => { + const bluetoothManager = new BluetoothManager(gladys, serviceId); + const configuration = bluetoothManager.getConfiguration(); + + expect(configuration).deep.eq({ + presenceScanner: { + frequency: 60000, + status: 'enabled', + }, + }); + }); +}); diff --git a/server/test/services/bluetooth/lib/config/bluetooth.initPresenceScanner.test.js b/server/test/services/bluetooth/lib/config/bluetooth.initPresenceScanner.test.js new file mode 100644 index 0000000000..ff7b19c39d --- /dev/null +++ b/server/test/services/bluetooth/lib/config/bluetooth.initPresenceScanner.test.js @@ -0,0 +1,78 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); + +const { fake, assert } = sinon; + +const scanPresenceMock = fake.resolves(null); +const BluetoothManager = proxyquire('../../../../../services/bluetooth/lib', { + './commands/bluetooth.scanPresence': { scanPresence: scanPresenceMock }, +}); + +const gladys = {}; +const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; + +describe('bluetooth.initPresenceScanner', () => { + let clock; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + sinon.reset(); + }); + + it('stop scanner', async () => { + const timer = setInterval(() => {}, 5000); + + const bluetoothManager = new BluetoothManager(gladys, serviceId); + bluetoothManager.presenceScanner = { + status: 'disabled', + timer, + }; + + bluetoothManager.initPresenceScanner(); + + expect(bluetoothManager.presenceScanner.timer).eq(undefined); + assert.notCalled(scanPresenceMock); + }); + + it('restart scanner', async () => { + const timer = setInterval(() => {}, 5000); + + const bluetoothManager = new BluetoothManager(gladys, serviceId); + bluetoothManager.presenceScanner = { + status: 'enabled', + frequency: 5000, + timer, + }; + + bluetoothManager.initPresenceScanner(); + + expect(bluetoothManager.presenceScanner.timer).not.eq(undefined); + assert.calledOnce(scanPresenceMock); + + clock.tick(6000); + + assert.callCount(scanPresenceMock, 2); + }); + + it('start scanner', async () => { + const bluetoothManager = new BluetoothManager(gladys, serviceId); + bluetoothManager.presenceScanner = { + status: 'enabled', + frequency: 5000, + }; + + bluetoothManager.initPresenceScanner(); + + expect(bluetoothManager.presenceScanner.timer).not.eq(undefined); + assert.calledOnce(scanPresenceMock); + + clock.tick(6000); + + assert.callCount(scanPresenceMock, 2); + }); +}); diff --git a/server/test/services/bluetooth/lib/config/bluetooth.saveConfiguration.test.js b/server/test/services/bluetooth/lib/config/bluetooth.saveConfiguration.test.js new file mode 100644 index 0000000000..4444395e93 --- /dev/null +++ b/server/test/services/bluetooth/lib/config/bluetooth.saveConfiguration.test.js @@ -0,0 +1,170 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); + +const { fake, assert } = sinon; + +const { BadParameters } = require('../../../../../utils/coreErrors'); + +const initPresenceScannerMock = fake.resolves(null); +const BluetoothManager = proxyquire('../../../../../services/bluetooth/lib', { + './config/bluetooth.initPresenceScanner': { initPresenceScanner: initPresenceScannerMock }, +}); + +const { + VARIABLES, + PRESENCE_STATUS, + TIMERS, +} = require('../../../../../services/bluetooth/lib/utils/bluetooth.constants'); + +const gladys = { + variable: { + setValue: fake.resolves(null), + }, +}; +const serviceId = 'de051f90-f34a-4fd5-be2e-e502339ec9bc'; + +describe('bluetooth.saveConfiguration', () => { + afterEach(() => { + sinon.reset(); + }); + + it('not config param', async () => { + const configuration = undefined; + const expectedConfig = { + presenceScanner: { + frequency: TIMERS.PRESENCE, + status: PRESENCE_STATUS.ENABLED, + }, + }; + + const bluetoothManager = new BluetoothManager(gladys, serviceId); + const savedConfig = await bluetoothManager.saveConfiguration(configuration); + + expect(savedConfig).deep.eq(expectedConfig); + assert.notCalled(gladys.variable.setValue); + assert.calledOnce(initPresenceScannerMock); + }); + + it('empty config param', async () => { + const configuration = {}; + const expectedConfig = { + presenceScanner: { + frequency: TIMERS.PRESENCE, + status: PRESENCE_STATUS.ENABLED, + }, + }; + + const bluetoothManager = new BluetoothManager(gladys, serviceId); + const savedConfig = await bluetoothManager.saveConfiguration(configuration); + + expect(savedConfig).deep.eq(expectedConfig); + assert.notCalled(gladys.variable.setValue); + assert.calledOnce(initPresenceScannerMock); + }); + + it('empty presenceScanner config param', async () => { + const configuration = { presenceScanner: {} }; + const expectedConfig = { + presenceScanner: { + frequency: TIMERS.PRESENCE, + status: PRESENCE_STATUS.ENABLED, + }, + }; + + const bluetoothManager = new BluetoothManager(gladys, serviceId); + const savedConfig = await bluetoothManager.saveConfiguration(configuration); + + expect(savedConfig).deep.eq(expectedConfig); + assert.notCalled(gladys.variable.setValue); + assert.calledOnce(initPresenceScannerMock); + }); + + it('only invalid frequency presenceScanner config param', async () => { + const configuration = { presenceScanner: { frequency: TIMERS.SCAN } }; + + const bluetoothManager = new BluetoothManager(gladys, serviceId); + try { + await bluetoothManager.saveConfiguration(configuration); + assert.fail(); + } catch (e) { + expect(e).instanceOf(BadParameters); + } + + assert.notCalled(gladys.variable.setValue); + assert.notCalled(initPresenceScannerMock); + }); + + it('only frequency presenceScanner config param', async () => { + const configuration = { presenceScanner: { frequency: 90000 } }; + const expectedConfig = { + presenceScanner: { + frequency: 90000, + status: PRESENCE_STATUS.ENABLED, + }, + }; + + const bluetoothManager = new BluetoothManager(gladys, serviceId); + const savedConfig = await bluetoothManager.saveConfiguration(configuration); + + expect(savedConfig).deep.eq(expectedConfig); + assert.calledOnce(gladys.variable.setValue); + assert.calledWith(gladys.variable.setValue, VARIABLES.PRESENCE_FREQUENCY, 90000, serviceId); + assert.calledOnce(initPresenceScannerMock); + }); + + it('only status presenceScanner config param (disabled)', async () => { + const configuration = { presenceScanner: { status: 'any_but_enabled' } }; + const expectedConfig = { + presenceScanner: { + frequency: TIMERS.PRESENCE, + status: PRESENCE_STATUS.DISABLED, + }, + }; + + const bluetoothManager = new BluetoothManager(gladys, serviceId); + const savedConfig = await bluetoothManager.saveConfiguration(configuration); + + expect(savedConfig).deep.eq(expectedConfig); + assert.calledOnce(gladys.variable.setValue); + assert.calledWith(gladys.variable.setValue, VARIABLES.PRESENCE_STATUS, PRESENCE_STATUS.DISABLED, serviceId); + assert.calledOnce(initPresenceScannerMock); + }); + + it('only status presenceScanner config param (enabled)', async () => { + const configuration = { presenceScanner: { status: PRESENCE_STATUS.ENABLED } }; + const expectedConfig = { + presenceScanner: { + frequency: TIMERS.PRESENCE, + status: PRESENCE_STATUS.ENABLED, + }, + }; + + const bluetoothManager = new BluetoothManager(gladys, serviceId); + const savedConfig = await bluetoothManager.saveConfiguration(configuration); + + expect(savedConfig).deep.eq(expectedConfig); + assert.calledOnce(gladys.variable.setValue); + assert.calledWith(gladys.variable.setValue, VARIABLES.PRESENCE_STATUS, PRESENCE_STATUS.ENABLED, serviceId); + assert.calledOnce(initPresenceScannerMock); + }); + + it('all presenceScanner config param', async () => { + const configuration = { presenceScanner: { status: 'any_but_enabled', frequency: 90000 } }; + const expectedConfig = { + presenceScanner: { + frequency: 90000, + status: PRESENCE_STATUS.DISABLED, + }, + }; + + const bluetoothManager = new BluetoothManager(gladys, serviceId); + const savedConfig = await bluetoothManager.saveConfiguration(configuration); + + expect(savedConfig).deep.eq(expectedConfig); + assert.calledTwice(gladys.variable.setValue); + assert.calledWith(gladys.variable.setValue, VARIABLES.PRESENCE_FREQUENCY, 90000, serviceId); + assert.calledWith(gladys.variable.setValue, VARIABLES.PRESENCE_STATUS, PRESENCE_STATUS.DISABLED, serviceId); + assert.calledOnce(initPresenceScannerMock); + }); +}); diff --git a/server/utils/constants.js b/server/utils/constants.js index bbb087a120..a95e37aff0 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -219,6 +219,7 @@ const ACTIONS = { USER: { SET_SEEN_AT_HOME: 'user.set-seen-at-home', SET_OUT_OF_HOME: 'user.set-out-of-home', + CHECK_PRESENCE: 'user.check-presence', }, HTTP: { REQUEST: 'http.request',