From 4eca35286594901c3a2864b914fa98d80bd3743c Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Thu, 26 Oct 2023 11:11:18 +0200 Subject: [PATCH] Add alarm set mode action --- front/src/config/i18n/en.json | 8 +- front/src/config/i18n/fr.json | 8 +- .../routes/scene/edit-scene/ActionCard.jsx | 12 +- .../actions/ChooseActionTypeCard.jsx | 3 +- .../scene/edit-scene/actions/SetAlarmMode.jsx | 123 ++++++++++++++++++ server/lib/house/house.arm.js | 25 +++- server/lib/scene/scene.actions.js | 16 ++- server/test/lib/house/house.arm.test.js | 38 +++++- .../actions/scene.action.setAlarmMode.test.js | 102 +++++++++++++++ server/utils/constants.js | 1 + 10 files changed, 323 insertions(+), 13 deletions(-) create mode 100644 front/src/routes/scene/edit-scene/actions/SetAlarmMode.jsx create mode 100644 server/test/lib/scene/actions/scene.action.setAlarmMode.test.js diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 8a4e536143..ed1b45743d 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -1531,6 +1531,11 @@ "description": "The scene will continue if the alarm is in the selected mode.", "houseLabel": "House", "alarmModeLabel": "Alarm Mode" + }, + "alarmSetMode": { + "description": "This action will set the selected house to the selected alarm mode.", + "houseLabel": "House", + "alarmModeLabel": "Alarm Mode" } }, "actions": { @@ -1583,7 +1588,8 @@ "condition": "Condition on Ecowatt (France)" }, "alarm": { - "check-alarm-mode": "If the alarm is in mode" + "check-alarm-mode": "If the alarm is in mode", + "set-alarm-mode": "Set alarm mode to" } }, "variables": { diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 881529debe..6df931f62c 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -1533,6 +1533,11 @@ "description": "La scène continuera si l'alarme est dans le mode sélectionné.", "houseLabel": "Maison", "alarmModeLabel": "Mode de l'alarme" + }, + "alarmSetMode": { + "description": "Cette action passera la maison sélectionnée dans le mode d'alarme sélectionné.", + "houseLabel": "Maison", + "alarmModeLabel": "Mode de l'alarme" } }, "actions": { @@ -1585,7 +1590,8 @@ "condition": "Condition sur Ecowatt ( France )" }, "alarm": { - "check-alarm-mode": "Si l'alarme est en mode" + "check-alarm-mode": "Si l'alarme est en mode", + "set-alarm-mode": "Passer l'alarme en mode" } }, "variables": { diff --git a/front/src/routes/scene/edit-scene/ActionCard.jsx b/front/src/routes/scene/edit-scene/ActionCard.jsx index 5ce52bc8e5..e9e08ecc67 100644 --- a/front/src/routes/scene/edit-scene/ActionCard.jsx +++ b/front/src/routes/scene/edit-scene/ActionCard.jsx @@ -26,6 +26,7 @@ import CalendarIsEventRunning from './actions/CalendarIsEventRunning'; import EcowattCondition from './actions/EcowattCondition'; import SendMessageCameraParams from './actions/SendMessageCameraParams'; import CheckAlarmMode from './actions/CheckAlarmMode'; +import SetAlarmMode from './actions/SetAlarmMode'; const deleteActionFromColumn = (columnIndex, rowIndex, deleteAction) => () => { deleteAction(columnIndex, rowIndex); @@ -54,7 +55,8 @@ const ACTION_ICON = { [ACTIONS.DEVICE.SET_VALUE]: 'fe fe-radio', [ACTIONS.CALENDAR.IS_EVENT_RUNNING]: 'fe fe-calendar', [ACTIONS.ECOWATT.CONDITION]: 'fe fe-zap', - [ACTIONS.ALARM.CHECK_ALARM_MODE]: 'fe fe-bell' + [ACTIONS.ALARM.CHECK_ALARM_MODE]: 'fe fe-bell', + [ACTIONS.ALARM.SET_ALARM_MODE]: 'fe fe-bell' }; const ACTION_CARD_TYPE = 'ACTION_CARD_TYPE'; @@ -342,6 +344,14 @@ const ActionCard = ({ children, ...props }) => { updateActionProperty={props.updateActionProperty} /> )} + {props.action.type === ACTIONS.ALARM.SET_ALARM_MODE && ( + + )} diff --git a/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx b/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx index 8574af8627..ed34672725 100644 --- a/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx +++ b/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx @@ -28,7 +28,8 @@ const ACTION_LIST = [ ACTIONS.DEVICE.SET_VALUE, ACTIONS.CALENDAR.IS_EVENT_RUNNING, ACTIONS.ECOWATT.CONDITION, - ACTIONS.ALARM.CHECK_ALARM_MODE + ACTIONS.ALARM.CHECK_ALARM_MODE, + ACTIONS.ALARM.SET_ALARM_MODE ]; const TRANSLATIONS = ACTION_LIST.reduce((acc, action) => { diff --git a/front/src/routes/scene/edit-scene/actions/SetAlarmMode.jsx b/front/src/routes/scene/edit-scene/actions/SetAlarmMode.jsx new file mode 100644 index 0000000000..654ea54226 --- /dev/null +++ b/front/src/routes/scene/edit-scene/actions/SetAlarmMode.jsx @@ -0,0 +1,123 @@ +import Select from 'react-select'; +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import { Text } from 'preact-i18n'; +import withIntlAsProp from '../../../../utils/withIntlAsProp'; +import get from 'get-value'; + +import { ALARM_MODES_LIST } from '../../../../../../server/utils/constants'; + +const capitalizeFirstLetter = string => { + return string.charAt(0).toUpperCase() + string.slice(1); +}; + +class SetAlarmMode extends Component { + getOptions = async () => { + try { + const houses = await this.props.httpClient.get('/api/v1/house'); + const houseOptions = []; + houses.forEach(house => { + houseOptions.push({ + label: house.name, + value: house.selector + }); + }); + await this.setState({ houseOptions }); + this.refreshSelectedOptions(this.props); + } catch (e) { + console.error(e); + } + }; + handleHouseChange = selectedOption => { + if (selectedOption && selectedOption.value) { + this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'house', selectedOption.value); + } else { + this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'house', null); + } + }; + handleAlarmModeChange = selectedOption => { + if (selectedOption && selectedOption.value) { + this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'alarm_mode', selectedOption.value); + } else { + this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'alarm_mode', null); + } + }; + refreshSelectedOptions = nextProps => { + let selectedHouseOption = ''; + if (nextProps.action.house && this.state.houseOptions) { + const houseOption = this.state.houseOptions.find(option => option.value === nextProps.action.house); + + if (houseOption) { + selectedHouseOption = houseOption; + } + } + let selectedAlarmModeOption = ''; + if (nextProps.action.alarm_mode && this.state.alarmModesOptions) { + const alarmModeOption = this.state.alarmModesOptions.find(option => option.value === nextProps.action.alarm_mode); + + if (alarmModeOption) { + selectedAlarmModeOption = alarmModeOption; + } + } + this.setState({ selectedHouseOption, selectedAlarmModeOption }); + }; + constructor(props) { + super(props); + this.props = props; + const alarmModesOptions = ALARM_MODES_LIST.map(alarmMode => { + return { + value: alarmMode, + label: capitalizeFirstLetter(get(props.intl.dictionary, `alarmModes.${alarmMode}`, { default: alarmMode })) + }; + }); + this.state = { + alarmModesOptions, + selectedHouseOption: '' + }; + } + componentDidMount() { + this.getOptions(); + } + componentWillReceiveProps(nextProps) { + this.refreshSelectedOptions(nextProps); + } + render(props, { alarmModesOptions, houseOptions, selectedHouseOption, selectedAlarmModeOption }) { + return ( +
+

+ +

+
+ + +
+
+ ); + } +} + +export default withIntlAsProp(connect('httpClient', {})(SetAlarmMode)); diff --git a/server/lib/house/house.arm.js b/server/lib/house/house.arm.js index 90062da07d..2b93545b9d 100644 --- a/server/lib/house/house.arm.js +++ b/server/lib/house/house.arm.js @@ -6,12 +6,13 @@ const { NotFoundError, ConflictError } = require('../../utils/coreErrors'); /** * @public * @description Arm house Alarm. - * @param {object} selector - Selector of the house. + * @param {string} selector - Selector of the house. + * @param {boolean} disableWaitTime - Should not wait to arm. * @returns {Promise} Resolve with house object. * @example * const mainHouse = await gladys.house.arm('main-house'); */ -async function arm(selector) { +async function arm(selector, disableWaitTime = false) { const house = await db.House.findOne({ where: { selector, @@ -38,8 +39,10 @@ async function arm(selector) { type: EVENTS.ALARM.ARMING, house: selector, }); - // Wait the delay before arming - const currentTimeout = setTimeout(async () => { + + const waitTimeInMs = disableWaitTime ? 0 : house.alarm_delay_before_arming * 1000; + + const armHouse = async () => { // Update database await house.update({ alarm_mode: ALARM_MODES.ARMED }); // Lock all tablets in this house @@ -56,10 +59,18 @@ async function arm(selector) { house: selector, }, }); - }, house.alarm_delay_before_arming * 1000); + }; - // store the timeout so we can cancel it if needed - this.armingHouseTimeout.set(selector, currentTimeout); + // if the wait time is 0, just arm now + if (waitTimeInMs === 0) { + await armHouse(); + } else { + // Wait the delay before arming + const currentTimeout = setTimeout(armHouse, waitTimeInMs); + + // store the timeout so we can cancel it if needed + this.armingHouseTimeout.set(selector, currentTimeout); + } } module.exports = { diff --git a/server/lib/scene/scene.actions.js b/server/lib/scene/scene.actions.js index d4138422da..fc22120c00 100644 --- a/server/lib/scene/scene.actions.js +++ b/server/lib/scene/scene.actions.js @@ -18,7 +18,7 @@ const get = require('get-value'); const dayjs = require('dayjs'); const utc = require('dayjs/plugin/utc'); const timezone = require('dayjs/plugin/timezone'); -const { ACTIONS, DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../utils/constants'); +const { ACTIONS, DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES, ALARM_MODES } = require('../../utils/constants'); const { getDeviceFeature } = require('../../utils/device'); const { AbortScene } = require('../../utils/coreErrors'); const { compare } = require('../../utils/compare'); @@ -443,6 +443,20 @@ const actionsFunc = { throw new AbortScene(`House "${house.name}" is not in mode ${action.alarm_mode}`); } }, + [ACTIONS.ALARM.SET_ALARM_MODE]: async (self, action) => { + if (action.alarm_mode === ALARM_MODES.ARMED) { + await self.house.arm(action.house, true); + } + if (action.alarm_mode === ALARM_MODES.DISARMED) { + await self.house.disarm(action.house); + } + if (action.alarm_mode === ALARM_MODES.PARTIALLY_ARMED) { + await self.house.partialArm(action.house); + } + if (action.alarm_mode === ALARM_MODES.PANIC) { + await self.house.panic(action.house); + } + }, }; module.exports = { diff --git a/server/test/lib/house/house.arm.test.js b/server/test/lib/house/house.arm.test.js index 875bb9c7d1..d7b052c35d 100644 --- a/server/test/lib/house/house.arm.test.js +++ b/server/test/lib/house/house.arm.test.js @@ -20,7 +20,7 @@ describe('house.arm', () => { const house = new House(event, {}, session); beforeEach(async () => { await house.update('test-house', { - alarm_delay_before_arming: 0, + alarm_delay_before_arming: 0.001, }); sinon.reset(); }); @@ -64,6 +64,42 @@ describe('house.arm', () => { }, ]); }); + it('should arm a house immediately', async () => { + await house.arm('test-house', true); + assert.callCount(event.emit, 4); + expect(event.emit.firstCall.args).to.deep.equal([ + EVENTS.WEBSOCKET.SEND_ALL, + { + type: WEBSOCKET_MESSAGE_TYPES.ALARM.ARMING, + payload: { + house: 'test-house', + }, + }, + ]); + expect(event.emit.secondCall.args).to.deep.equal([ + EVENTS.TRIGGERS.CHECK, + { + type: EVENTS.ALARM.ARMING, + house: 'test-house', + }, + ]); + expect(event.emit.thirdCall.args).to.deep.equal([ + EVENTS.TRIGGERS.CHECK, + { + type: EVENTS.ALARM.ARM, + house: 'test-house', + }, + ]); + expect(event.emit.args[3]).to.deep.equal([ + EVENTS.WEBSOCKET.SEND_ALL, + { + type: WEBSOCKET_MESSAGE_TYPES.ALARM.ARMED, + payload: { + house: 'test-house', + }, + }, + ]); + }); it('should return house not found', async () => { const promise = house.arm('house-not-found'); return assertChai.isRejected(promise, 'House not found'); diff --git a/server/test/lib/scene/actions/scene.action.setAlarmMode.test.js b/server/test/lib/scene/actions/scene.action.setAlarmMode.test.js new file mode 100644 index 0000000000..60481bb8f5 --- /dev/null +++ b/server/test/lib/scene/actions/scene.action.setAlarmMode.test.js @@ -0,0 +1,102 @@ +const { fake, assert } = require('sinon'); +const EventEmitter = require('events'); + +const { ACTIONS } = require('../../../../utils/constants'); +const { executeActions } = require('../../../../lib/scene/scene.executeActions'); + +const StateManager = require('../../../../lib/state'); + +describe('scene.set-alarm-mode', () => { + let event; + let stateManager; + + beforeEach(() => { + event = new EventEmitter(); + stateManager = new StateManager(event); + }); + + it('should arm house', async () => { + const house = { + getBySelector: fake.resolves({ name: 'my house', alarm_mode: 'disarmed' }), + arm: fake.resolves(null), + }; + const scope = {}; + await executeActions( + { stateManager, event, house }, + [ + [ + { + type: ACTIONS.ALARM.SET_ALARM_MODE, + house: 'my-house', + alarm_mode: 'armed', + }, + ], + ], + scope, + ); + assert.calledWith(house.arm, 'my-house', true); + }); + it('should disarm house', async () => { + const house = { + getBySelector: fake.resolves({ name: 'my house', alarm_mode: 'armed' }), + disarm: fake.resolves(null), + }; + const scope = {}; + await executeActions( + { stateManager, event, house }, + [ + [ + { + type: ACTIONS.ALARM.SET_ALARM_MODE, + house: 'my-house', + alarm_mode: 'disarmed', + }, + ], + ], + scope, + ); + assert.calledWith(house.disarm, 'my-house'); + }); + it('should partially arm house', async () => { + const house = { + getBySelector: fake.resolves({ name: 'my house', alarm_mode: 'disarmed' }), + partialArm: fake.resolves(null), + }; + const scope = {}; + await executeActions( + { stateManager, event, house }, + [ + [ + { + type: ACTIONS.ALARM.SET_ALARM_MODE, + house: 'my-house', + alarm_mode: 'partially-armed', + }, + ], + ], + scope, + ); + assert.calledWith(house.partialArm, 'my-house'); + }); + it('should put house in panic mode', async () => { + const house = { + getBySelector: fake.resolves({ name: 'my house', alarm_mode: 'disarmed' }), + panic: fake.resolves(null), + }; + const scope = {}; + await executeActions( + { stateManager, event, house }, + [ + [ + { + type: ACTIONS.ALARM.SET_ALARM_MODE, + house: 'my-house', + alarm_mode: 'panic', + }, + ], + ], + scope, + ); + assert.calledWith(house.panic, 'my-house'); + }); +}); diff --git a/server/utils/constants.js b/server/utils/constants.js index fc0ed065ef..a69b135b9b 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -295,6 +295,7 @@ const CONDITIONS = { const ACTIONS = { ALARM: { CHECK_ALARM_MODE: 'alarm.check-alarm-mode', + SET_ALARM_MODE: 'alarm.set-alarm-mode', }, CALENDAR: { IS_EVENT_RUNNING: 'calendar.is-event-running',