diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json
index a384f33efd..f979d58c49 100644
--- a/front/src/config/i18n/en.json
+++ b/front/src/config/i18n/en.json
@@ -2052,7 +2052,9 @@
"paused": "Paused",
"exited": "Exited",
"dead": "Dead"
- }
+ },
+ "batteryLevel": "Battery level alert",
+ "batteryLevelDescription": "At 09:00 AM, only on Saturday, a message will be sent to all admins as soon as the battery level drops below the chosen percentage"
},
"newArea": {
"createNewZoneButton": "Create new zone",
diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json
index b8bf98d7bf..f80bf628c2 100644
--- a/front/src/config/i18n/fr.json
+++ b/front/src/config/i18n/fr.json
@@ -2053,7 +2053,9 @@
"paused": "En Pause",
"exited": "Arrêté",
"dead": "Mort"
- }
+ },
+ "batteryLevel": "Alerte sur le niveau de batterie",
+ "batteryLevelDescription": "A 09h00, uniquement le samedi, un message sera envoyer à tous les admins dès que le niveau du batterie parsera en dessous du pourcentage 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..20f65516e4
--- /dev/null
+++ b/front/src/routes/settings/settings-system/SettingsSystemBatteryLevelWarning.jsx
@@ -0,0 +1,130 @@
+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 => {
+ await this.setState({
+ batteryLevelUnderWarningThreshold: e.target.value,
+ savingBatteryLevelUnderWarning: true
+ });
+ try {
+ await this.props.httpClient.post(
+ `/api/v1/variable/${SYSTEM_VARIABLE_NAMES.DEVICE_BATTERY_LEVEL_WARNING_THRESHOLD}`,
+ {
+ value: e.target.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, savingBatteryLevelUnderWarning }) {
+ 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 (
+
+ );
+ }
+}
+
+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.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..ff755910cf
--- /dev/null
+++ b/front/src/routes/settings/settings-system/SettingsSystemTimeExpiryState.jsx
@@ -0,0 +1,82 @@
+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, savingNumberOfHourseBeforeStateIsOutdated }) {
+ 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 (
+
+ );
+ }
+}
+
+export default connect('httpClient', null)(SettingsSystemTimezone);
diff --git a/front/src/routes/settings/settings-system/index.js b/front/src/routes/settings/settings-system/index.js
index fb91108fb9..548841be36 100644
--- a/front/src/routes/settings/settings-system/index.js
+++ b/front/src/routes/settings/settings-system/index.js
@@ -1,168 +1,14 @@
import { Component } from 'preact';
import { connect } from 'unistore/preact';
-import get from 'get-value';
-import timezones from '../../../config/timezones';
import SettingsSystemPage from './SettingsSystemPage';
import actions from '../../../actions/system';
-import { SYSTEM_VARIABLE_NAMES } from '../../../../../server/utils/constants';
-import { RequestStatus } from '../../../utils/consts';
class SettingsSystem extends Component {
- 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);
- }
- };
-
- vacuumDatabase = async e => {
- e.preventDefault();
- this.setState({
- vacuumStarted: true
- });
- try {
- await this.props.httpClient.post('/api/v1/system/vacuum');
- } catch (e) {
- console.error(e);
- }
- };
-
- 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);
- }
- };
-
- 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
- });
- };
-
- 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
- });
- };
-
- 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
- });
- };
-
- 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);
- }
- };
-
- 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'
- });
- }
- }
- };
-
- 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
- });
- }
- };
-
componentDidMount() {
this.props.getInfos();
this.props.getDiskSpace();
this.props.getContainers();
- this.getTimezone();
- this.getDeviceStateHistoryPreference();
- this.getDeviceAggregateStateHistoryPreference();
- this.getNumberOfHoursBeforeStateIsOutdated();
+
// we start the ping a little bit after to give it some time to breathe
this.refreshPingIntervalId = setInterval(() => {
this.props.ping();
@@ -173,54 +19,9 @@ class SettingsSystem extends Component {
clearInterval(this.refreshPingIntervalId);
}
- constructor(props) {
- super(props);
- this.state = {
- vacuumStarted: false
- };
- }
-
- render(
- props,
- {
- selectedTimezone,
- deviceStateHistoryInDays,
- deviceAggregateStateHistoryInDays,
- vacuumStarted,
- numberOfHoursBeforeStateIsOutdated,
- savingNumberOfHourseBeforeStateIsOutdated
- }
- ) {
- const isDocker = get(props, 'systemInfos.is_docker');
- const upgradeDownloadInProgress = props.downloadUpgradeStatus === RequestStatus.Getting;
- const upgradeDownloadFinished = props.downloadUpgradeStatus === RequestStatus.Success;
- const upgradeAvailable =
- !upgradeDownloadInProgress && !upgradeDownloadFinished && get(props, 'systemInfos.new_release_available');
- return (
-
- );
+ render(props, {}) {
+ return ;
}
}
-export default connect(
- 'httpClient,session,systemPing,systemInfos,systemDiskSpace,systemContainers,downloadUpgradeProgress,downloadUpgradeStatus',
- actions
-)(SettingsSystem);
+export default connect('httpClient,session,systemPing,systemDiskSpace,systemInfos', actions)(SettingsSystem);
diff --git a/server/config/scheduler-jobs.js b/server/config/scheduler-jobs.js
index 15dbe69f17..12fa9bb0b0 100644
--- a/server/config/scheduler-jobs.js
+++ b/server/config/scheduler-jobs.js
@@ -21,6 +21,11 @@ const jobs = [
rule: '0 0 22 * * *', // every day at 22:00
event: EVENTS.JOB.PURGE_OLD_JOBS,
},
+ {
+ name: 'hourly-device-check-batteries',
+ rule: '0 0 9 * * 6', // At 09:00 AM, only on Saturday
+ event: EVENTS.DEVICE.CHECK_BATTERIES,
+ },
];
module.exports = jobs;
diff --git a/server/lib/device/device.checkBatteries.js b/server/lib/device/device.checkBatteries.js
new file mode 100644
index 0000000000..317ecebc5c
--- /dev/null
+++ b/server/lib/device/device.checkBatteries.js
@@ -0,0 +1,51 @@
+const logger = require('../../utils/logger');
+const {
+ SYSTEM_VARIABLE_NAMES,
+ AVAILABLE_LANGUAGES,
+ DEVICE_FEATURE_CATEGORIES,
+ USER_ROLE,
+} = require('../../utils/constants');
+
+/**
+ * @description Check battery level and warn if needed.
+ * @returns {Promise} Resolve when finished.
+ * @example
+ * device.purgeStates();
+ */
+async function checkBatteries() {
+ const enabled = await this.variable.getValue(SYSTEM_VARIABLE_NAMES.DEVICE_BATTERY_LEVEL_WARNING_ENABLED);
+ if (!enabled) {
+ return;
+ }
+ logger.debug('Checking batteries ...');
+
+ const minPercentBattery = await this.variable.getValue(SYSTEM_VARIABLE_NAMES.DEVICE_BATTERY_LEVEL_WARNING_THRESHOLD);
+
+ const admins = await this.user.getByRole(USER_ROLE.ADMIN);
+
+ if (!minPercentBattery || !admins || admins.length === 0) {
+ return;
+ }
+
+ const devices = await this.get({ device_feature_category: DEVICE_FEATURE_CATEGORIES.BATTERY });
+
+ devices.forEach((device) => {
+ device.features
+ .filter((feature) => {
+ return feature.last_value < minPercentBattery;
+ })
+ .forEach((feature) => {
+ const messages = {
+ [AVAILABLE_LANGUAGES.EN]: `Warning !!! Battery level on ${device.name} is under ${minPercentBattery}% (current: ${feature.last_value}%)`,
+ [AVAILABLE_LANGUAGES.FR]: `Avertissement !!! Le niveau de la batterie de ${device.name} est inférieur à ${minPercentBattery} % (actuel : ${feature.last_value} %)`,
+ };
+ admins.forEach((admin) => {
+ this.messageManager.sendToUser(admin.selector, messages[admin.language]);
+ });
+ });
+ });
+}
+
+module.exports = {
+ checkBatteries,
+};
diff --git a/server/lib/device/index.js b/server/lib/device/index.js
index ab8faf2bc3..069948d22a 100644
--- a/server/lib/device/index.js
+++ b/server/lib/device/index.js
@@ -34,6 +34,7 @@ const { setValue } = require('./device.setValue');
const { setupPoll } = require('./device.setupPoll');
const { newStateEvent } = require('./device.newStateEvent');
const { notify } = require('./device.notify');
+const { checkBatteries } = require('./device.checkBatteries');
const DeviceManager = function DeviceManager(
eventManager,
@@ -44,6 +45,7 @@ const DeviceManager = function DeviceManager(
variable,
job,
brain,
+ user,
) {
this.eventManager = eventManager;
this.messageManager = messageManager;
@@ -53,6 +55,7 @@ const DeviceManager = function DeviceManager(
this.variable = variable;
this.job = job;
this.brain = brain;
+ this.user = user;
this.STATES_TO_PURGE_PER_DEVICE_FEATURE_CLEAN_BATCH = 1000;
this.WAIT_TIME_BETWEEN_DEVICE_FEATURE_CLEAN_BATCH = 100;
@@ -84,6 +87,7 @@ const DeviceManager = function DeviceManager(
EVENTS.DEVICE.PURGE_STATES_SINGLE_FEATURE,
eventFunctionWrapper(this.purgeStatesByFeatureId.bind(this)),
);
+ this.eventManager.on(EVENTS.DEVICE.CHECK_BATTERIES, eventFunctionWrapper(this.checkBatteries.bind(this)));
};
DeviceManager.prototype.add = add;
@@ -112,5 +116,6 @@ DeviceManager.prototype.setParam = setParam;
DeviceManager.prototype.setupPoll = setupPoll;
DeviceManager.prototype.setValue = setValue;
DeviceManager.prototype.notify = notify;
+DeviceManager.prototype.checkBatteries = checkBatteries;
module.exports = DeviceManager;
diff --git a/server/lib/index.js b/server/lib/index.js
index 0dfbd312a8..1222ba3527 100644
--- a/server/lib/index.js
+++ b/server/lib/index.js
@@ -66,7 +66,7 @@ function Gladys(params = {}) {
const message = new MessageHandler(event, brain, service, stateManager, variable);
const user = new User(session, stateManager, variable);
const location = new Location(user, event);
- const device = new Device(event, message, stateManager, service, room, variable, job, brain);
+ const device = new Device(event, message, stateManager, service, room, variable, job, brain, user);
const calendar = new Calendar(service);
const scheduler = new Scheduler(event);
const weather = new Weather(service, event, message, house);
diff --git a/server/lib/user/index.js b/server/lib/user/index.js
index 465c26ae54..d579aa82ea 100644
--- a/server/lib/user/index.js
+++ b/server/lib/user/index.js
@@ -12,6 +12,7 @@ const { forgotPassword } = require('./user.forgotPassword');
const { update } = require('./user.update');
const { updateBySelector } = require('./user.updateBySelector');
const { updatePassword } = require('./user.updatePassword');
+const { getByRole } = require('./user.getByRole');
const User = function User(session, stateManager, variable) {
this.session = session;
@@ -33,5 +34,6 @@ User.prototype.getByTelegramUserId = getByTelegramUserId;
User.prototype.update = update;
User.prototype.updateBySelector = updateBySelector;
User.prototype.updatePassword = updatePassword;
+User.prototype.getByRole = getByRole;
module.exports = User;
diff --git a/server/lib/user/user.getByRole.js b/server/lib/user/user.getByRole.js
new file mode 100644
index 0000000000..c0ea1344da
--- /dev/null
+++ b/server/lib/user/user.getByRole.js
@@ -0,0 +1,34 @@
+const db = require('../../models');
+
+/**
+ * @private
+ * @description Get list of users by role.
+ * @name gladys.user.getByRole
+ * @param {string} role - The role of the users.
+ * @returns {Promise} Promise.
+ * @example
+ * await gladys.user.getByRole('admin');
+ */
+async function getByRole(role) {
+ const queryParams = {
+ attributes: ['id', 'firstname', 'lastname', 'selector', 'email', 'language'],
+ offset: 0,
+ order: [['firstname', 'ASC']],
+ where: {
+ role,
+ },
+ };
+
+ const users = await db.User.findAll(queryParams);
+
+ const usersPlain = users.map((user) => {
+ // we converted the user to plain object
+ return user.get({ plain: true });
+ });
+
+ return usersPlain;
+}
+
+module.exports = {
+ getByRole,
+};
diff --git a/server/migrations/20231115163530-default-system-variable-device-battery.js b/server/migrations/20231115163530-default-system-variable-device-battery.js
new file mode 100644
index 0000000000..2df2f84e8d
--- /dev/null
+++ b/server/migrations/20231115163530-default-system-variable-device-battery.js
@@ -0,0 +1,17 @@
+const db = require('../models');
+const { SYSTEM_VARIABLE_NAMES } = require('../utils/constants');
+
+module.exports = {
+ up: async () => {
+ await db.Variable.create({
+ name: SYSTEM_VARIABLE_NAMES.DEVICE_BATTERY_LEVEL_WARNING_ENABLED,
+ value: true,
+ });
+ await db.Variable.create({
+ name: SYSTEM_VARIABLE_NAMES.DEVICE_BATTERY_LEVEL_WARNING_THRESHOLD,
+ value: '10',
+ });
+ },
+
+ down: async () => {},
+};
diff --git a/server/seeders/20190227081700-device-feature.js b/server/seeders/20190227081700-device-feature.js
index 066638af0c..fa9bfac98b 100644
--- a/server/seeders/20190227081700-device-feature.js
+++ b/server/seeders/20190227081700-device-feature.js
@@ -40,6 +40,23 @@ module.exports = {
created_at: '2019-02-12 07:49:07.556 +00:00',
updated_at: '2019-02-12 07:49:07.556 +00:00',
},
+ {
+ id: '3e1e3c30-18c6-4311-8ac3-7ebd2cea10d2',
+ name: 'Test device battery',
+ selector: 'test-device-feature-battery',
+ external_id: 'hue:battery:1',
+ category: 'battery',
+ type: 'integer',
+ read_only: true,
+ has_feedback: false,
+ min: 0,
+ max: 100,
+ last_value: 20,
+ last_value_changed: '2019-02-12 07:49:07.556 +00:00',
+ device_id: '7f85c2f8-86cc-4600-84db-6c074dadb4e8',
+ created_at: '2019-02-12 07:49:07.556 +00:00',
+ updated_at: '2019-02-12 07:49:07.556 +00:00',
+ },
{
id: 'bb1af3b9-f87d-4d9c-b5be-958cd9d28900',
name: 'Test temperature sensor celsius',
diff --git a/server/test/lib/device/device.checkBatteries.test.js b/server/test/lib/device/device.checkBatteries.test.js
new file mode 100644
index 0000000000..261da736de
--- /dev/null
+++ b/server/test/lib/device/device.checkBatteries.test.js
@@ -0,0 +1,79 @@
+const EventEmitter = require('events');
+const { assert, stub } = require('sinon');
+
+const Device = require('../../../lib/device');
+
+const StateManager = require('../../../lib/state');
+const Job = require('../../../lib/job');
+const { SYSTEM_VARIABLE_NAMES } = require('../../../utils/constants');
+
+const event = new EventEmitter();
+const job = new Job(event);
+
+const user = {
+ getByRole: stub().returns([{ selector: 'admin', language: 'fr' }]),
+};
+
+const messageManager = {
+ sendToUser: stub().returns(null),
+};
+
+describe('Device check batteries', () => {
+ it('should do nothing if is not enabled', async () => {
+ const stateManager = new StateManager(event);
+ const service = {
+ getService: () => null,
+ };
+ const variables = {
+ getValue: () => false,
+ };
+ const device = new Device(event, messageManager, stateManager, service, {}, variables, job, {}, user);
+
+ await device.checkBatteries();
+
+ assert.notCalled(user.getByRole);
+ assert.notCalled(messageManager.sendToUser);
+ });
+ it('should do nothing if the threshold is not set', async () => {
+ const stateManager = new StateManager(event);
+ const service = {
+ getService: () => null,
+ };
+ const variables = {
+ getValue: (key) => {
+ if (key === SYSTEM_VARIABLE_NAMES.DEVICE_BATTERY_LEVEL_WARNING_ENABLED) {
+ return true;
+ }
+ return undefined;
+ },
+ };
+ const device = new Device(event, messageManager, stateManager, service, {}, variables, job, {}, user);
+
+ await device.checkBatteries();
+
+ assert.notCalled(messageManager.sendToUser);
+ });
+ it('should send a message if battery is low', async () => {
+ const stateManager = new StateManager(event);
+ const service = {
+ getService: () => null,
+ };
+ const variables = {
+ getValue: (key) => {
+ if (key === SYSTEM_VARIABLE_NAMES.DEVICE_BATTERY_LEVEL_WARNING_ENABLED) {
+ return true;
+ }
+ return 30;
+ },
+ };
+ const device = new Device(event, messageManager, stateManager, service, {}, variables, job, {}, user);
+
+ await device.checkBatteries();
+
+ assert.calledWith(
+ messageManager.sendToUser,
+ 'admin',
+ 'Avertissement !!! Le niveau de la batterie de Test device est inférieur à 30 % (actuel : 20 %)',
+ );
+ });
+});
diff --git a/server/test/lib/user/user.getByRole.test.js b/server/test/lib/user/user.getByRole.test.js
new file mode 100644
index 0000000000..25b22781f3
--- /dev/null
+++ b/server/test/lib/user/user.getByRole.test.js
@@ -0,0 +1,18 @@
+const { expect } = require('chai');
+
+const User = require('../../../lib/user');
+
+describe('user.getByRole', () => {
+ const user = new User();
+ it('should return list of user', async () => {
+ const userFound = await user.getByRole('admin');
+ expect(userFound[0]).to.deep.equal({
+ id: '0cd30aef-9c4e-4a23-88e3-3547971296e5',
+ firstname: 'John',
+ lastname: 'Doe',
+ selector: 'john',
+ email: 'demo@demo.com',
+ language: 'en',
+ });
+ });
+});
diff --git a/server/utils/constants.js b/server/utils/constants.js
index 09a07b77c0..1a575748a8 100644
--- a/server/utils/constants.js
+++ b/server/utils/constants.js
@@ -99,6 +99,8 @@ const SYSTEM_VARIABLE_NAMES = {
GLADYS_GATEWAY_ALEXA_USER_IS_CONNECTED_WITH_GATEWAY: 'GLADYS_GATEWAY_ALEXA_USER_IS_CONNECTED_WITH_GATEWAY',
GLADYS_GATEWAY_OPEN_AI_ENABLED: 'GLADYS_GATEWAY_OPEN_AI_ENABLED',
TIMEZONE: 'TIMEZONE',
+ DEVICE_BATTERY_LEVEL_WARNING_THRESHOLD: 'DEVICE_BATTERY_LEVEL_WARNING_THRESHOLD',
+ DEVICE_BATTERY_LEVEL_WARNING_ENABLED: 'DEVICE_BATTERY_LEVEL_WARNING_ENABLED',
};
const EVENTS = {
@@ -125,6 +127,7 @@ const EVENTS = {
PURGE_STATES: 'device.purge-states',
CALCULATE_HOURLY_AGGREGATE: 'device.calculate-hourly-aggregate',
PURGE_STATES_SINGLE_FEATURE: 'device.purge-states-single-feature',
+ CHECK_BATTERIES: 'device.check-batteries',
},
GATEWAY: {
CREATE_BACKUP: 'gateway.create-backup',