From 5a1f873bc9337a0e05371a991dd3b33f074c5d87 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 15 Sep 2023 13:58:49 +0200 Subject: [PATCH 1/4] Dashboard: Camera image should expires (#1880) --- front/src/components/boxs/camera/Camera.jsx | 4 +- front/src/components/boxs/camera/style.css | 12 ++++ front/src/config/i18n/en.json | 2 +- front/src/config/i18n/fr.json | 2 +- server/lib/device/camera/camera.getImage.js | 12 ++++ .../lib/device/camera/camera.getImage.test.js | 55 +++++++++++++++++++ 6 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 front/src/components/boxs/camera/style.css diff --git a/front/src/components/boxs/camera/Camera.jsx b/front/src/components/boxs/camera/Camera.jsx index c44ff91773..2c160b7390 100644 --- a/front/src/components/boxs/camera/Camera.jsx +++ b/front/src/components/boxs/camera/Camera.jsx @@ -7,6 +7,7 @@ import Hls from 'hls.js'; import config from '../../../config'; import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../server/utils/constants'; import get from 'get-value'; +import style from './style.css'; const SEGMENT_DURATIONS_PER_LATENCY = { 'ultra-low': 1, @@ -270,8 +271,7 @@ class CameraBoxComponent extends Component { {image && {props.roomName}} {error && (
-

- +

diff --git a/front/src/components/boxs/camera/style.css b/front/src/components/boxs/camera/style.css new file mode 100644 index 0000000000..583570c9b5 --- /dev/null +++ b/front/src/components/boxs/camera/style.css @@ -0,0 +1,12 @@ +.noImageToShowError { + background-color: #34495e; + color: white; + height: 244px; + padding: 12px; + padding-bottom: 0px; + margin-bottom: 0px; + display: table-cell; + vertical-align: middle; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 21e388e72c..28b8647a97 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -269,7 +269,7 @@ }, "camera": { "editCameraLabel": "Select the camera you want to display here.", - "noImageToShow": "No image to show", + "noImageToShow": "No image to show. Are you sure you camera is connected and accessible to Gladys?", "editBoxNameLabel": "Enter the name you want to give to the box:", "editBoxNamePlaceholder": "Name of the box", "latencyBoxName": "Latency", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 5ad42629b3..38918180c7 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -269,7 +269,7 @@ }, "camera": { "editCameraLabel": "Sélectionnez la caméra que vous souhaitez afficher ici.", - "noImageToShow": "Aucune image à afficher", + "noImageToShow": "Aucune image à afficher. Êtes-vous sûr que votre caméra est connectée et accessible par Gladys ?", "editBoxNameLabel": "Entrez le nom que vous souhaitez donner à la box :", "editBoxNamePlaceholder": "Nom de la box", "latencyBoxName": "Latence", diff --git a/server/lib/device/camera/camera.getImage.js b/server/lib/device/camera/camera.getImage.js index 1dc7f64501..e26b42637f 100644 --- a/server/lib/device/camera/camera.getImage.js +++ b/server/lib/device/camera/camera.getImage.js @@ -1,5 +1,8 @@ const { NotFoundError } = require('../../../utils/coreErrors'); const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../utils/constants'); +const { isNumeric } = require('../../../utils/device'); + +const CAMERA_IMAGE_EXPIRATION_TIME_IN_HOURS = 1; /** * @description Get image of a camera. @@ -19,6 +22,15 @@ async function getImage(selector) { if (!deviceFeature) { throw new NotFoundError('Camera image feature not found'); } + let lastValueInTimestamp = new Date(deviceFeature.last_value_changed).getTime(); + if (!isNumeric(lastValueInTimestamp)) { + lastValueInTimestamp = 0; + } + const tooOldTimestamp = Date.now() - CAMERA_IMAGE_EXPIRATION_TIME_IN_HOURS * 60 * 60 * 1000; + const lastValueIsToOld = lastValueInTimestamp < tooOldTimestamp; + if (lastValueIsToOld) { + throw new NotFoundError('Camera image is too old'); + } return Promise.resolve(deviceFeature.last_value_string); } diff --git a/server/test/lib/device/camera/camera.getImage.test.js b/server/test/lib/device/camera/camera.getImage.test.js index 4cb97b6d46..a20a10f300 100644 --- a/server/test/lib/device/camera/camera.getImage.test.js +++ b/server/test/lib/device/camera/camera.getImage.test.js @@ -21,6 +21,7 @@ describe('Camera.getImage', () => { selector: 'test-camera', category: 'camera', type: 'image', + last_value_changed: new Date().toISOString(), last_value_string: RANDOM_IMAGE, }, ], @@ -45,6 +46,60 @@ describe('Camera.getImage', () => { const promise = deviceManager.camera.getImage('camera-not-found'); return assert.isRejected(promise, 'Camera not found'); }); + it('should return camera image is too old (old date)', async () => { + const stateManager = new StateManager(event); + const deviceManager = new Device(event, {}, stateManager, {}, {}, {}, job); + stateManager.setState('device', 'test-camera', { + features: [ + { + id: '565d05fc-1736-4b76-99ca-581232901d96', + selector: 'test-camera', + category: 'camera', + type: 'image', + last_value_changed: new Date(Date.now() - 5 * 60 * 60 * 1000).toISOString(), + last_value_string: RANDOM_IMAGE, + }, + ], + }); + const promise = deviceManager.camera.getImage('test-camera'); + return assert.isRejected(promise, 'Camera image is too old'); + }); + it('should return camera image is too old (null date)', async () => { + const stateManager = new StateManager(event); + const deviceManager = new Device(event, {}, stateManager, {}, {}, {}, job); + stateManager.setState('device', 'test-camera', { + features: [ + { + id: '565d05fc-1736-4b76-99ca-581232901d96', + selector: 'test-camera', + category: 'camera', + type: 'image', + last_value_changed: null, + last_value_string: null, + }, + ], + }); + const promise = deviceManager.camera.getImage('test-camera'); + return assert.isRejected(promise, 'Camera image is too old'); + }); + it('should return camera image is too old (wrong date)', async () => { + const stateManager = new StateManager(event); + const deviceManager = new Device(event, {}, stateManager, {}, {}, {}, job); + stateManager.setState('device', 'test-camera', { + features: [ + { + id: '565d05fc-1736-4b76-99ca-581232901d96', + selector: 'test-camera', + category: 'camera', + type: 'image', + last_value_changed: 'lalala', + last_value_string: null, + }, + ], + }); + const promise = deviceManager.camera.getImage('test-camera'); + return assert.isRejected(promise, 'Camera image is too old'); + }); it('should return camera not found', async () => { const stateManager = new StateManager(event); const deviceManager = new Device(event, {}, stateManager, {}, {}, {}, job); From b998ccb0bc9f488ea7b15b49f067bc2bb94c0ff8 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 15 Sep 2023 14:15:38 +0200 Subject: [PATCH 2/4] Integration form password should not be autocompleted by browser (#1881) --- .../src/routes/integration/all/mqtt/setup-page/SetupForm.jsx | 4 ++-- .../routes/integration/all/nextcloud-talk/NextcloudTalk.jsx | 2 ++ front/src/routes/integration/all/tasmota/TasmotaDeviceBox.jsx | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/front/src/routes/integration/all/mqtt/setup-page/SetupForm.jsx b/front/src/routes/integration/all/mqtt/setup-page/SetupForm.jsx index d8a151dec5..fdbd2a9aef 100644 --- a/front/src/routes/integration/all/mqtt/setup-page/SetupForm.jsx +++ b/front/src/routes/integration/all/mqtt/setup-page/SetupForm.jsx @@ -73,7 +73,7 @@ class SetupForm extends Component { value={props.mqttUsername} class="form-control" onInput={this.updateUsername} - autoComplete="no" + autocomplete="off" disabled={gladysNotAvailable} /> @@ -93,7 +93,7 @@ class SetupForm extends Component { value={props.mqttPassword} class="form-control" onInput={this.updatePassword} - autoComplete="new-password" + autocomplete="off" disabled={gladysNotAvailable} /> diff --git a/front/src/routes/integration/all/nextcloud-talk/NextcloudTalk.jsx b/front/src/routes/integration/all/nextcloud-talk/NextcloudTalk.jsx index c3b8e8e394..4b85c0cde1 100644 --- a/front/src/routes/integration/all/nextcloud-talk/NextcloudTalk.jsx +++ b/front/src/routes/integration/all/nextcloud-talk/NextcloudTalk.jsx @@ -48,6 +48,7 @@ const NextcloudTalkPage = ({ children, ...props }) => ( } onInput={props.updateNextcloudBotUsername} value={props.nextcloudBotUsername} @@ -62,6 +63,7 @@ const NextcloudTalkPage = ({ children, ...props }) => ( } onInput={props.updateNextcloudBotPassword} diff --git a/front/src/routes/integration/all/tasmota/TasmotaDeviceBox.jsx b/front/src/routes/integration/all/tasmota/TasmotaDeviceBox.jsx index 487f6d637e..e705a4055e 100644 --- a/front/src/routes/integration/all/tasmota/TasmotaDeviceBox.jsx +++ b/front/src/routes/integration/all/tasmota/TasmotaDeviceBox.jsx @@ -147,6 +147,7 @@ class TasmotaDeviceBox extends Component { id={`username_${deviceIndex}`} type="text" class="form-control" + autocomplete="off" onInput={this.updateUsername} placeholder={} /> @@ -159,6 +160,7 @@ class TasmotaDeviceBox extends Component { Date: Mon, 18 Sep 2023 11:50:08 +0200 Subject: [PATCH 3/4] Increase session validity for local users (#1882) --- server/api/controllers/user.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api/controllers/user.controller.js b/server/api/controllers/user.controller.js index 5e59c84ef2..c8ed71a5dc 100644 --- a/server/api/controllers/user.controller.js +++ b/server/api/controllers/user.controller.js @@ -2,7 +2,7 @@ const asyncMiddleware = require('../middlewares/asyncMiddleware'); const logger = require('../../utils/logger'); const { BadParameters } = require('../../utils/coreErrors'); -const LOGIN_SESSION_VALIDITY_IN_SECONDS = 30 * 24 * 60 * 60; +const LOGIN_SESSION_VALIDITY_IN_SECONDS = 365 * 24 * 60 * 60; module.exports = function UserController(gladys) { /** From 323d8f6d9f2b98cbebc9043d9bc47a49cd876f17 Mon Sep 17 00:00:00 2001 From: callemand Date: Wed, 20 Sep 2023 19:04:43 +0200 Subject: [PATCH 4/4] Humidity widget: User can define the humidity threshold (#1877) --- front/package-lock.json | 112 ++++++------------ front/package.json | 1 + .../room-humidity/EditRoomHumidityBox.jsx | 72 ++++++++++- .../boxs/room-humidity/RoomHumidity.jsx | 51 ++++++-- front/src/components/house/RoomSelector.jsx | 13 +- front/src/config/i18n/en.json | 3 +- front/src/config/i18n/fr.json | 3 +- front/src/style/index.css | 51 ++++++++ server/models/dashboard.js | 3 + server/utils/constants.js | 7 ++ 10 files changed, 226 insertions(+), 90 deletions(-) diff --git a/front/package-lock.json b/front/package-lock.json index 6174a5d769..2e3f424f90 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -37,6 +37,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dnd-touch-backend": "^16.0.1", "react-select": "^4.3.1", + "react-slider": "^2.0.6", "unistore": "^3.5.2", "useragent-parser-js": "^1.0.3", "uuid": "^3.4.0" @@ -12693,23 +12694,6 @@ "node": ">=4.0" } }, - "node_modules/eslint-plugin-react/node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", @@ -23910,13 +23894,13 @@ } }, "node_modules/prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", - "react-is": "^16.8.1" + "react-is": "^16.13.1" } }, "node_modules/proxy-addr": { @@ -24356,21 +24340,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/react-big-calendar/node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/react-big-calendar/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/react-clock": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/react-clock/-/react-clock-4.5.0.tgz", @@ -24491,9 +24460,9 @@ } }, "node_modules/react-is": { - "version": "16.11.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz", - "integrity": "sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==" + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", @@ -24581,6 +24550,17 @@ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, + "node_modules/react-slider": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-2.0.6.tgz", + "integrity": "sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18" + } + }, "node_modules/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -41559,23 +41539,6 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, "resolve": { "version": "2.0.0-next.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", @@ -50077,13 +50040,13 @@ } }, "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", - "react-is": "^16.8.1" + "react-is": "^16.13.1" } }, "proxy-addr": { @@ -50433,21 +50396,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" } } }, @@ -50530,9 +50478,9 @@ } }, "react-is": { - "version": "16.11.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz", - "integrity": "sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==" + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-lifecycles-compat": { "version": "3.0.4", @@ -50601,6 +50549,14 @@ } } }, + "react-slider": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-2.0.6.tgz", + "integrity": "sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA==", + "requires": { + "prop-types": "^15.8.1" + } + }, "react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", diff --git a/front/package.json b/front/package.json index 1697828014..5bce21c9cc 100644 --- a/front/package.json +++ b/front/package.json @@ -75,6 +75,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dnd-touch-backend": "^16.0.1", "react-select": "^4.3.1", + "react-slider": "^2.0.6", "unistore": "^3.5.2", "useragent-parser-js": "^1.0.3", "uuid": "^3.4.0" diff --git a/front/src/components/boxs/room-humidity/EditRoomHumidityBox.jsx b/front/src/components/boxs/room-humidity/EditRoomHumidityBox.jsx index 8cad86ece2..b22fdf1203 100644 --- a/front/src/components/boxs/room-humidity/EditRoomHumidityBox.jsx +++ b/front/src/components/boxs/room-humidity/EditRoomHumidityBox.jsx @@ -1,8 +1,11 @@ import { Component } from 'preact'; import { Text } from 'preact-i18n'; import BaseEditBox from '../baseEditBox'; +import ReactSlider from 'react-slider'; +import { DEFAULT_VALUE_HUMIDITY } from '../../../../../server/utils/constants'; import RoomSelector from '../../house/RoomSelector'; +import cx from 'classnames'; const updateBoxRoom = (updateBoxRoomFunc, x, y) => room => { updateBoxRoomFunc(x, y, room.selector); @@ -19,6 +22,35 @@ const EditRoomHumidityBox = ({ children, ...props }) => ( updateRoomSelection={updateBoxRoom(props.updateBoxRoom, props.x, props.y)} />

+
+ +
+ +
+
{state.valueNow}%
} + pearling + minDistance={10} + onAfterChange={props.updateBoxValue} + value={[props.humidityMin, props.humidityMax]} + disabled={!(props.box.humidity_use_custom_value || false)} + /> +
); @@ -28,8 +60,46 @@ class EditRoomHumidityBoxComponent extends Component { room: selector }); }; + + updateBoxUseCustomValue = e => { + this.props.updateBoxConfig(this.props.x, this.props.y, { + humidity_use_custom_value: e.target.checked + }); + }; + + updateBoxValue = values => { + this.props.updateBoxConfig(this.props.x, this.props.y, { + humidity_min: values[0], + humidity_max: values[1] + }); + }; + render(props, {}) { - return ; + let humidity_min = this.props.box.humidity_min; + let humidity_max = this.props.box.humidity_max; + + if (!this.props.box.humidity_use_custom_value) { + humidity_min = DEFAULT_VALUE_HUMIDITY.MINIMUM; + humidity_max = DEFAULT_VALUE_HUMIDITY.MAXIMUM; + } + + if (isNaN(humidity_min)) { + humidity_min = DEFAULT_VALUE_HUMIDITY.MINIMUM; + } + if (isNaN(humidity_max)) { + humidity_max = DEFAULT_VALUE_HUMIDITY.MAXIMUM; + } + + return ( + + ); } } diff --git a/front/src/components/boxs/room-humidity/RoomHumidity.jsx b/front/src/components/boxs/room-humidity/RoomHumidity.jsx index 660fb13026..3d6d174d48 100644 --- a/front/src/components/boxs/room-humidity/RoomHumidity.jsx +++ b/front/src/components/boxs/room-humidity/RoomHumidity.jsx @@ -5,25 +5,32 @@ import get from 'get-value'; import actions from '../../../actions/dashboard/boxes/humidityInRoom'; import { DASHBOARD_BOX_STATUS_KEY, DASHBOARD_BOX_DATA_KEY } from '../../../utils/consts'; -import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../server/utils/constants'; +import { DEFAULT_VALUE_HUMIDITY, WEBSOCKET_MESSAGE_TYPES } from '../../../../../server/utils/constants'; const isNotNullOrUndefined = value => value !== undefined && value !== null; const RoomHumidityBox = ({ children, ...props }) => (
- {props.humidity > 45 && props.humidity < 60 && ( - + {isNotNullOrUndefined(props.humidity) && + props.humidity >= props.humidityMin && + props.humidity <= props.humidityMax && ( + + + + )} + {isNotNullOrUndefined(props.humidity) && props.humidity < props.humidityMin && ( + )} - {props.humidity <= 45 && ( - + {isNotNullOrUndefined(props.humidity) && props.humidity > props.humidityMax && ( + )} - {props.humidity >= 60 && ( - + {!isNotNullOrUndefined(props.humidity) && ( + )} @@ -74,8 +81,36 @@ class RoomHumidityBoxComponent extends Component { const boxStatus = get(props, `${DASHBOARD_BOX_STATUS_KEY}HumidityInRoom.${props.x}_${props.y}`); const humidity = get(boxData, 'room.humidity.humidity'); const unit = get(boxData, 'room.humidity.unit'); + + const humidity_use_custom_value = get(props, 'box.humidity_use_custom_value'); + let humidity_min = get(props, 'box.humidity_min'); + let humidity_max = get(props, 'box.humidity_max'); + + if (!humidity_use_custom_value) { + humidity_min = DEFAULT_VALUE_HUMIDITY.MINIMUM; + humidity_max = DEFAULT_VALUE_HUMIDITY.MAXIMUM; + } + + if (isNaN(humidity_min)) { + humidity_min = DEFAULT_VALUE_HUMIDITY.MINIMUM; + } + if (isNaN(humidity_max)) { + humidity_max = DEFAULT_VALUE_HUMIDITY.MAXIMUM; + } + const roomName = get(boxData, 'room.name'); - return ; + return ( + + ); } } diff --git a/front/src/components/house/RoomSelector.jsx b/front/src/components/house/RoomSelector.jsx index b2f35e538d..89da017f67 100644 --- a/front/src/components/house/RoomSelector.jsx +++ b/front/src/components/house/RoomSelector.jsx @@ -70,7 +70,18 @@ class RoomSelector extends Component { } render({}, { selectedRoom, houseOptions }) { - return ({ ...provided, zIndex: 100 }) + }} + /> + ); } } diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 28b8647a97..1d62ccce83 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -257,7 +257,8 @@ }, "humidityInRoom": { "editRoomLabel": "Select the room you want to display here.", - "noHumidityRecorded": "No humidity recorded recently." + "noHumidityRecorded": "No humidity recorded recently.", + "thresholdsLabel": "Configure custom thresholds" }, "userPresence": { "description": "Display who's at home and who is not. You can change the user presence in scenes, and select here the users displayed.", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 38918180c7..3288aa9514 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -257,7 +257,8 @@ }, "humidityInRoom": { "editRoomLabel": "Sélectionnez la pièce que vous souhaitez afficher ici.", - "noHumidityRecorded": "Aucune mesure d'humidité enregistrée récemment." + "noHumidityRecorded": "Aucune mesure d'humidité enregistrée récemment.", + "thresholdsLabel": "Configurer des seuils personnalisés" }, "userPresence": { "description": "Cette box affiche qui est à la maison et qui ne l'est pas. Vous pouvez changer la présence d'un utilisateur dans les scènes, et sélectionner ici quels utilisateurs afficher.", diff --git a/front/src/style/index.css b/front/src/style/index.css index f5a3d1e499..def1688683 100644 --- a/front/src/style/index.css +++ b/front/src/style/index.css @@ -307,3 +307,54 @@ body { .react-clock__second-hand { transition: transform cubic-bezier(0.68, 0, 0.27, 1.55) 0.2s; } + +.opacity-60 { + opacity: 60%; +} + + +.humidity-slider { + width: 100%; + max-width: 500px; + height: 30px; +} + +.humidity-slider-thumb { + font-size: 0.9em; + text-align: center; + background-color: #fafafa; + #color: white; + cursor: pointer; + border: 1px solid gray; + border-radius: 10px; + box-sizing: border-box; +} + +.humidity-slider-thumb.active { + border: 1px solid #467fcf; +} + +.humidity-slider-track { + position: relative; + background: #f1c40f; +} + +.humidity-slider-track-1 { + background: #5eba00; +} + +.humidity-slider-track-2 { + background: #467fcf ; +} + +.humidity-slider .humidity-slider-track { + top: 10px; + height: 10px; +} + +.humidity-slider .humidity-slider-thumb { + top: 3px; + width: 41px; + height: 24px; + line-height: 22px; +} diff --git a/server/models/dashboard.js b/server/models/dashboard.js index bb092f774f..e32929342d 100644 --- a/server/models/dashboard.js +++ b/server/models/dashboard.js @@ -29,6 +29,9 @@ const boxesSchema = Joi.array().items( camera_latency: Joi.string(), camera_live_auto_start: Joi.boolean(), scenes: Joi.array().items(Joi.string()), + humidity_use_custom_value: Joi.boolean(), + humidity_min: Joi.number(), + humidity_max: Joi.number(), }), ), ); diff --git a/server/utils/constants.js b/server/utils/constants.js index 4de848f105..ab4b91e31d 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -944,6 +944,11 @@ const JOB_ERROR_TYPES = { UNKNOWN_ERROR: 'unknown-error', }; +const DEFAULT_VALUE_HUMIDITY = { + MINIMUM: 45, + MAXIMUM: 60, +}; + const createList = (obj) => { const list = []; Object.keys(obj).forEach((key) => { @@ -1039,3 +1044,5 @@ module.exports.JOB_STATUS = JOB_STATUS; module.exports.JOB_STATUS_LIST = JOB_STATUS_LIST; module.exports.JOB_ERROR_TYPES = JOB_ERROR_TYPES; module.exports.JOB_ERROR_TYPES_LIST = JOB_ERROR_TYPES_LIST; + +module.exports.DEFAULT_VALUE_HUMIDITY = DEFAULT_VALUE_HUMIDITY;