From aeddefcab45f779022900bfed11bf2ace0a37ea1 Mon Sep 17 00:00:00 2001 From: callemand Date: Fri, 1 Dec 2023 10:46:47 +0100 Subject: [PATCH] Add system warning for battery level (#1953) --- front/src/config/i18n/en.json | 4 +- front/src/config/i18n/fr.json | 4 +- .../SettingsSystemBatteryLevelWarning.jsx | 135 ++++++++++ .../SettingsSystemContainers.jsx | 54 ++++ .../SettingsSystemDatabaseCleaning.jsx | 54 ++++ .../SettingsSystemKeepAggregatedStates.jsx | 98 ++++++++ .../SettingsSystemKeepDeviceHistory.jsx | 83 ++++++ .../SettingsSystemOperations.jsx | 49 ++++ .../settings-system/SettingsSystemPage.jsx | 237 ++---------------- .../SettingsSystemTimeExpiryState.jsx | 81 ++++++ .../SettingsSystemTimezone.jsx | 61 +++++ .../routes/settings/settings-system/index.js | 207 +-------------- .../brain/battery-threshold/answers.en.json | 8 + .../brain/battery-threshold/answers.fr.json | 8 + server/config/scheduler-jobs.js | 5 + server/lib/device/device.checkBatteries.js | 51 ++++ server/lib/device/index.js | 5 + server/lib/index.js | 2 +- server/lib/user/index.js | 2 + server/lib/user/user.getByRole.js | 34 +++ ...-default-system-variable-device-battery.js | 17 ++ .../seeders/20190227081700-device-feature.js | 17 ++ .../lib/device/device.checkBatteries.test.js | 82 ++++++ server/test/lib/user/user.getByRole.test.js | 18 ++ server/utils/constants.js | 3 + 25 files changed, 892 insertions(+), 427 deletions(-) create mode 100644 front/src/routes/settings/settings-system/SettingsSystemBatteryLevelWarning.jsx create mode 100644 front/src/routes/settings/settings-system/SettingsSystemContainers.jsx create mode 100644 front/src/routes/settings/settings-system/SettingsSystemDatabaseCleaning.jsx create mode 100644 front/src/routes/settings/settings-system/SettingsSystemKeepAggregatedStates.jsx create mode 100644 front/src/routes/settings/settings-system/SettingsSystemKeepDeviceHistory.jsx create mode 100644 front/src/routes/settings/settings-system/SettingsSystemOperations.jsx create mode 100644 front/src/routes/settings/settings-system/SettingsSystemTimeExpiryState.jsx create mode 100644 front/src/routes/settings/settings-system/SettingsSystemTimezone.jsx create mode 100644 server/config/brain/battery-threshold/answers.en.json create mode 100644 server/config/brain/battery-threshold/answers.fr.json create mode 100644 server/lib/device/device.checkBatteries.js create mode 100644 server/lib/user/user.getByRole.js create mode 100644 server/migrations/20231115163530-default-system-variable-device-battery.js create mode 100644 server/test/lib/device/device.checkBatteries.test.js create mode 100644 server/test/lib/user/user.getByRole.test.js diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 1bf5a2c078..40ac835d15 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -2063,7 +2063,9 @@ "paused": "Paused", "exited": "Exited", "dead": "Dead" - } + }, + "batteryLevel": "Battery level alert", + "batteryLevelDescription": "Every Saturday at 9:00 am, a message will be sent to all administrators if a device's battery level falls below the chosen threshold." }, "newArea": { "createNewZoneButton": "Create new zone", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 042b1b357c..1c0bfd67c9 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -2064,7 +2064,9 @@ "paused": "En Pause", "exited": "Arrêté", "dead": "Mort" - } + }, + "batteryLevel": "Alerte sur le niveau de batterie", + "batteryLevelDescription": "Tous les samedis à 9h00, un message sera envoyé à tous les administrateurs si le niveau de batterie d'un appareil passe en dessous du seuil choisi." }, "newArea": { "createNewZoneButton": "Créer une zone", diff --git a/front/src/routes/settings/settings-system/SettingsSystemBatteryLevelWarning.jsx b/front/src/routes/settings/settings-system/SettingsSystemBatteryLevelWarning.jsx new file mode 100644 index 0000000000..a3040baaeb --- /dev/null +++ b/front/src/routes/settings/settings-system/SettingsSystemBatteryLevelWarning.jsx @@ -0,0 +1,135 @@ +import cx from 'classnames'; +import { Text } from 'preact-i18n'; +import { Component } from 'preact'; +import { SYSTEM_VARIABLE_NAMES } from '../../../../../server/utils/constants'; +import debounce from 'debounce'; +import { connect } from 'unistore/preact'; + +class SettingsSystemBatteryLevelWarning extends Component { + getBatteryLevelUnderWarning = async () => { + try { + const { value: batteryLevelUnderWarningThreshold } = await this.props.httpClient.get( + `/api/v1/variable/${SYSTEM_VARIABLE_NAMES.DEVICE_BATTERY_LEVEL_WARNING_THRESHOLD}` + ); + + const { value: batteryLevelUnderWarningEnabled } = await this.props.httpClient.get( + `/api/v1/variable/${SYSTEM_VARIABLE_NAMES.DEVICE_BATTERY_LEVEL_WARNING_ENABLED}` + ); + + this.setState({ + batteryLevelUnderWarningThreshold, + batteryLevelUnderWarningEnabled: batteryLevelUnderWarningEnabled === '1' + }); + } catch (e) { + console.error(e); + } + }; + + updateBatteryLevelUnderWarningThreshold = async e => { + let { value, min, max } = e.target; + if (value !== undefined && value !== null && value !== '') { + value = Math.max(Number(min), Math.min(Number(max), Number(value))); + } + + await this.setState({ + batteryLevelUnderWarningThreshold: value, + savingBatteryLevelUnderWarning: true + }); + try { + await this.props.httpClient.post( + `/api/v1/variable/${SYSTEM_VARIABLE_NAMES.DEVICE_BATTERY_LEVEL_WARNING_THRESHOLD}`, + { + value + } + ); + } catch (e) { + console.error(e); + } + await this.setState({ + savingBatteryLevelUnderWarning: false + }); + }; + + debouncedUpdateBatteryLevelUnderWarningThreshold = debounce(this.updateBatteryLevelUnderWarningThreshold, 200); + + updateBatteryLevelUnderWarningEnabled = async () => { + const value = !this.state.batteryLevelUnderWarningEnabled; + await this.setState({ + batteryLevelUnderWarningEnabled: value, + savingBatteryLevelUnderWarning: true + }); + try { + await this.props.httpClient.post( + `/api/v1/variable/${SYSTEM_VARIABLE_NAMES.DEVICE_BATTERY_LEVEL_WARNING_ENABLED}`, + { + value + } + ); + } catch (e) { + console.error(e); + } + await this.setState({ + savingBatteryLevelUnderWarning: false + }); + }; + + componentDidMount() { + this.getBatteryLevelUnderWarning(); + } + + render({}, { batteryLevelUnderWarningThreshold, batteryLevelUnderWarningEnabled }) { + return ( +
+

+ + +

+
+
+

+ +

+
+ +
+ + + +
+
+
+
+
+ ); + } +} + +export default connect('httpClient', null)(SettingsSystemBatteryLevelWarning); diff --git a/front/src/routes/settings/settings-system/SettingsSystemContainers.jsx b/front/src/routes/settings/settings-system/SettingsSystemContainers.jsx new file mode 100644 index 0000000000..006326f053 --- /dev/null +++ b/front/src/routes/settings/settings-system/SettingsSystemContainers.jsx @@ -0,0 +1,54 @@ +import { connect } from 'unistore/preact'; +import { Component } from 'preact'; +import { Text } from 'preact-i18n'; +import cx from 'classnames'; + +class SettingsSystemContainers extends Component { + render({ systemContainers }, {}) { + return ( +
+

+ +

+
+ + + + + + + + + + {systemContainers && + systemContainers.map(container => ( + + + + + + ))} + +
+ + + + + +
{container.name}{container.created_at_formatted} + + + +
+
+
+ ); + } +} + +export default connect('systemContainers', null)(SettingsSystemContainers); diff --git a/front/src/routes/settings/settings-system/SettingsSystemDatabaseCleaning.jsx b/front/src/routes/settings/settings-system/SettingsSystemDatabaseCleaning.jsx new file mode 100644 index 0000000000..1f790f34b0 --- /dev/null +++ b/front/src/routes/settings/settings-system/SettingsSystemDatabaseCleaning.jsx @@ -0,0 +1,54 @@ +import { connect } from 'unistore/preact'; +import { Component } from 'preact'; +import { Text } from 'preact-i18n'; + +class SettingsSystemDatabaseCleaning extends Component { + constructor(props) { + super(props); + this.state = { + vacuumStarted: false + }; + } + + vacuumDatabase = async e => { + e.preventDefault(); + this.setState({ + vacuumStarted: true + }); + try { + await this.props.httpClient.post('/api/v1/system/vacuum'); + } catch (e) { + console.error(e); + } + }; + + render({}, { vacuumStarted }) { + return ( +
+

+ +

+ +
+
+

+ +

+

+ {vacuumStarted && ( +

+ +
+ )} + +

+
+
+
+ ); + } +} + +export default connect('httpClient', null)(SettingsSystemDatabaseCleaning); diff --git a/front/src/routes/settings/settings-system/SettingsSystemKeepAggregatedStates.jsx b/front/src/routes/settings/settings-system/SettingsSystemKeepAggregatedStates.jsx new file mode 100644 index 0000000000..ded8ce8a76 --- /dev/null +++ b/front/src/routes/settings/settings-system/SettingsSystemKeepAggregatedStates.jsx @@ -0,0 +1,98 @@ +import { connect } from 'unistore/preact'; +import { Component } from 'preact'; +import { Text } from 'preact-i18n'; +import { SYSTEM_VARIABLE_NAMES } from '../../../../../server/utils/constants'; +import get from 'get-value'; + +class SettingsSystemKeepAggregatedStates extends Component { + getDeviceAggregateStateHistoryPreference = async () => { + try { + const { value } = await this.props.httpClient.get( + `/api/v1/variable/${SYSTEM_VARIABLE_NAMES.DEVICE_AGGREGATE_STATE_HISTORY_IN_DAYS}` + ); + this.setState({ + deviceAggregateStateHistoryInDays: value + }); + } catch (e) { + console.error(e); + const status = get(e, 'response.status'); + if (status === 404) { + // Default value is -1 + this.setState({ + deviceAggregateStateHistoryInDays: '-1' + }); + } + } + }; + + updateDeviceAggregateStateHistory = async e => { + await this.setState({ + deviceAggregateStateHistoryInDays: e.target.value, + savingDeviceStateHistory: true + }); + try { + await this.props.httpClient.post( + `/api/v1/variable/${SYSTEM_VARIABLE_NAMES.DEVICE_AGGREGATE_STATE_HISTORY_IN_DAYS}`, + { + value: e.target.value + } + ); + } catch (e) { + console.error(e); + } + await this.setState({ + savingDeviceStateHistory: false + }); + }; + + componentDidMount() { + this.getDeviceAggregateStateHistoryPreference(); + } + + render({}, { deviceAggregateStateHistoryInDays }) { + return ( +
+

+ +

+ +
+
+

+ +

+ +
+
+
+ ); + } +} + +export default connect('httpClient', null)(SettingsSystemKeepAggregatedStates); diff --git a/front/src/routes/settings/settings-system/SettingsSystemKeepDeviceHistory.jsx b/front/src/routes/settings/settings-system/SettingsSystemKeepDeviceHistory.jsx new file mode 100644 index 0000000000..eed8498fb9 --- /dev/null +++ b/front/src/routes/settings/settings-system/SettingsSystemKeepDeviceHistory.jsx @@ -0,0 +1,83 @@ +import { connect } from 'unistore/preact'; +import { Component } from 'preact'; +import { Text } from 'preact-i18n'; +import { SYSTEM_VARIABLE_NAMES } from '../../../../../server/utils/constants'; + +class SettingsSystemKeepDeviceHistory extends Component { + getDeviceStateHistoryPreference = async () => { + try { + const { value } = await this.props.httpClient.get( + `/api/v1/variable/${SYSTEM_VARIABLE_NAMES.DEVICE_STATE_HISTORY_IN_DAYS}` + ); + this.setState({ + deviceStateHistoryInDays: value + }); + } catch (e) { + console.error(e); + } + }; + + updateDeviceStateHistory = async e => { + await this.setState({ + deviceStateHistoryInDays: e.target.value, + savingDeviceStateHistory: true + }); + try { + await this.props.httpClient.post(`/api/v1/variable/${SYSTEM_VARIABLE_NAMES.DEVICE_STATE_HISTORY_IN_DAYS}`, { + value: e.target.value + }); + } catch (e) { + console.error(e); + } + await this.setState({ + savingDeviceStateHistory: false + }); + }; + + componentDidMount() { + this.getDeviceStateHistoryPreference(); + } + + render({}, { deviceStateHistoryInDays }) { + return ( +
+

+ +

+ +
+
+

+ +

+ +
+
+
+ ); + } +} + +export default connect('httpClient', null)(SettingsSystemKeepDeviceHistory); diff --git a/front/src/routes/settings/settings-system/SettingsSystemOperations.jsx b/front/src/routes/settings/settings-system/SettingsSystemOperations.jsx new file mode 100644 index 0000000000..4aeef229cc --- /dev/null +++ b/front/src/routes/settings/settings-system/SettingsSystemOperations.jsx @@ -0,0 +1,49 @@ +import { connect } from 'unistore/preact'; +import { Component } from 'preact'; +import { Text } from 'preact-i18n'; + +class SettingsSystemOperations extends Component { + render({ systemInfos }, {}) { + return ( +
+

+ +

+ + {systemInfos && systemInfos.new_release_available === true && ( +
+
+

+ +

+

+ +

+
+
+ )} + + {systemInfos && systemInfos.new_release_available === false && ( +
+ + + + + + + +
+ + + + + +
+
+ )} +
+ ); + } +} + +export default connect('systemInfos', null)(SettingsSystemOperations); diff --git a/front/src/routes/settings/settings-system/SettingsSystemPage.jsx b/front/src/routes/settings/settings-system/SettingsSystemPage.jsx index 73ca6502c4..35901fada5 100644 --- a/front/src/routes/settings/settings-system/SettingsSystemPage.jsx +++ b/front/src/routes/settings/settings-system/SettingsSystemPage.jsx @@ -1,7 +1,13 @@ import { Text } from 'preact-i18n'; import SettingsLayout from '../SettingsLayout'; -import cx from 'classnames'; -import Select from 'react-select'; +import SettingsSystemBatteryLevelWarning from './SettingsSystemBatteryLevelWarning'; +import SettingsSystemContainers from './SettingsSystemContainers'; +import SettingsSystemOperations from './SettingsSystemOperations'; +import SettingsSystemTimezone from './SettingsSystemTimezone'; +import SettingsSystemKeepDeviceHistory from './SettingsSystemKeepDeviceHistory'; +import SettingsSystemKeepAggregatedStates from './SettingsSystemKeepAggregatedStates'; +import SettingsSystemTimeExpiryState from './SettingsSystemTimeExpiryState'; +import SettingsSystemDatabaseCleaning from './SettingsSystemDatabaseCleaning'; const SystemPage = ({ children, ...props }) => ( @@ -79,227 +85,16 @@ const SystemPage = ({ children, ...props }) => (
-
-

- -

- - {props.systemInfos && props.systemInfos.new_release_available === true && ( -
-
-

- -

-

- -

-
-
- )} - - {props.systemInfos && props.systemInfos.new_release_available === false && ( -
- - - - - - - -
- - - - - -
-
- )} -
- -
-

- -

-
-
- -

- - - -

- - - - - - - - - -
-
- -

- - - -

- -
-
- -

- - - -

-
- -
- - - -
-
-
-
- -

- - - -

-

- {props.vacuumStarted && ( -

- -
- )} - -

-
-
-
+ + + +
-
-

- -

-
- - - - - - - - - - {props.systemContainers && - props.systemContainers.map(container => ( - - - - - - ))} - -
- - - - - -
{container.name}{container.created_at_formatted} - - - -
-
-
+ + + +
diff --git a/front/src/routes/settings/settings-system/SettingsSystemTimeExpiryState.jsx b/front/src/routes/settings/settings-system/SettingsSystemTimeExpiryState.jsx new file mode 100644 index 0000000000..32fa60914e --- /dev/null +++ b/front/src/routes/settings/settings-system/SettingsSystemTimeExpiryState.jsx @@ -0,0 +1,81 @@ +import { connect } from 'unistore/preact'; +import { Component } from 'preact'; +import { Text } from 'preact-i18n'; +import { SYSTEM_VARIABLE_NAMES } from '../../../../../server/utils/constants'; + +class SettingsSystemTimeExpiryState extends Component { + getNumberOfHoursBeforeStateIsOutdated = async () => { + try { + const { value } = await this.props.httpClient.get( + `/api/v1/variable/${SYSTEM_VARIABLE_NAMES.DEVICE_STATE_NUMBER_OF_HOURS_BEFORE_STATE_IS_OUTDATED}` + ); + this.setState({ + numberOfHoursBeforeStateIsOutdated: value + }); + } catch (e) { + console.error(e); + // if variable doesn't exist, value is 48 + this.setState({ + numberOfHoursBeforeStateIsOutdated: 48 + }); + } + }; + + updateNumberOfHoursBeforeStateIsOutdated = async e => { + await this.setState({ + numberOfHoursBeforeStateIsOutdated: e.target.value, + savingNumberOfHourseBeforeStateIsOutdated: true + }); + try { + await this.props.httpClient.post( + `/api/v1/variable/${SYSTEM_VARIABLE_NAMES.DEVICE_STATE_NUMBER_OF_HOURS_BEFORE_STATE_IS_OUTDATED}`, + { + value: e.target.value + } + ); + } catch (e) { + console.error(e); + } + await this.setState({ + savingNumberOfHourseBeforeStateIsOutdated: false + }); + }; + + componentDidMount() { + this.getNumberOfHoursBeforeStateIsOutdated(); + } + + render({}, { numberOfHoursBeforeStateIsOutdated }) { + return ( +
+

+ +

+ +
+
+

+ +

+
+ +
+ + + +
+
+
+
+
+ ); + } +} + +export default connect('httpClient', null)(SettingsSystemTimeExpiryState); diff --git a/front/src/routes/settings/settings-system/SettingsSystemTimezone.jsx b/front/src/routes/settings/settings-system/SettingsSystemTimezone.jsx new file mode 100644 index 0000000000..692c551657 --- /dev/null +++ b/front/src/routes/settings/settings-system/SettingsSystemTimezone.jsx @@ -0,0 +1,61 @@ +import { connect } from 'unistore/preact'; +import { Component } from 'preact'; +import { Text } from 'preact-i18n'; +import Select from 'react-select'; +import timezones from '../../../config/timezones'; +import { SYSTEM_VARIABLE_NAMES } from '../../../../../server/utils/constants'; + +class SettingsSystemTimezone extends Component { + getTimezone = async () => { + try { + const { value } = await this.props.httpClient.get(`/api/v1/variable/${SYSTEM_VARIABLE_NAMES.TIMEZONE}`); + const selectedTimezone = timezones.find(tz => tz.value === value); + if (selectedTimezone) { + this.setState({ + selectedTimezone + }); + } + } catch (e) { + console.error(e); + } + }; + + updateTimezone = async option => { + this.setState({ + savingTimezone: true, + selectedTimezone: option + }); + try { + await this.props.httpClient.post(`/api/v1/variable/${SYSTEM_VARIABLE_NAMES.TIMEZONE}`, { + value: option.value + }); + } catch (e) { + console.error(e); + } + }; + + componentDidMount() { + this.getTimezone(); + } + + render({}, { selectedTimezone }) { + return ( +
+

+ +

+ +
+
+

+ +

+