From 62aaa1367884dd4d428619cb2b0c3e6d08a281dd Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Thu, 20 Feb 2020 10:11:37 +0100 Subject: [PATCH 01/14] Add light.turn-off action server-side --- server/lib/scene/scene.actions.js | 15 +++++++++++ .../scene/scene.executeSingleAction.test.js | 27 +++++++++++++++++-- server/utils/constants.js | 1 + 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/server/lib/scene/scene.actions.js b/server/lib/scene/scene.actions.js index c35be6fcc0..444d277c4a 100644 --- a/server/lib/scene/scene.actions.js +++ b/server/lib/scene/scene.actions.js @@ -31,6 +31,21 @@ const actionsFunc = { } }); }, + [ACTIONS.LIGHT.TURN_OFF]: async (self, action, scope) => { + await Promise.map(action.devices, async (deviceSelector) => { + try { + const device = self.stateManager.get('device', deviceSelector); + const deviceFeature = getDeviceFeature( + device, + DEVICE_FEATURE_CATEGORIES.LIGHT, + DEVICE_FEATURE_TYPES.LIGHT.BINARY, + ); + await self.device.setValue(device, deviceFeature, 0); + } catch (e) { + logger.warn(e); + } + }); + }, [ACTIONS.TIME.DELAY]: async (self, action, scope) => new Promise((resolve) => { let timeToWaitMilliseconds; diff --git a/server/test/lib/scene/scene.executeSingleAction.test.js b/server/test/lib/scene/scene.executeSingleAction.test.js index 2b7d898b0c..2c1a4367a9 100644 --- a/server/test/lib/scene/scene.executeSingleAction.test.js +++ b/server/test/lib/scene/scene.executeSingleAction.test.js @@ -1,22 +1,45 @@ const { assert, fake } = require('sinon'); const EventEmitter = require('events'); -const { ACTIONS } = require('../../../utils/constants'); +const { ACTIONS, DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../utils/constants'); const SceneManager = require('../../../lib/scene'); const StateManager = require('../../../lib/state'); const event = new EventEmitter(); +const deviceFeatureLightBinary = { + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.BINARY, +}; + +const lightDevice = { + features: [deviceFeatureLightBinary], +}; + describe('scene.executeSingleAction', () => { it('should execute one action', async () => { const device = { setValue: fake.resolves(null), }; const stateManager = new StateManager(); + stateManager.setState('device', 'light-1', lightDevice); const sceneManager = new SceneManager(stateManager, event, device); await sceneManager.executeSingleAction({ type: ACTIONS.LIGHT.TURN_ON, devices: ['light-1'], }); - assert.calledOnce(device.setValue); + assert.calledWith(device.setValue, lightDevice, deviceFeatureLightBinary, 1); + }); + it('should execute one action', async () => { + const device = { + setValue: fake.resolves(null), + }; + const stateManager = new StateManager(); + stateManager.setState('device', 'light-1', lightDevice); + const sceneManager = new SceneManager(stateManager, event, device); + await sceneManager.executeSingleAction({ + type: ACTIONS.LIGHT.TURN_OFF, + devices: ['light-1'], + }); + assert.calledWith(device.setValue, lightDevice, deviceFeatureLightBinary, 0); }); }); diff --git a/server/utils/constants.js b/server/utils/constants.js index 8835cc8fe5..0cc0595ae1 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -176,6 +176,7 @@ const ACTIONS = { }, LIGHT: { TURN_ON: 'light.turn-on', + TURN_OFF: 'light.turn-off', }, TIME: { DELAY: 'delay', From 6a99c9bdc0ead7e896b208327929ebad14b0bdb5 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Thu, 20 Feb 2020 10:24:27 +0100 Subject: [PATCH 02/14] Add turn off the lights box --- front/src/config/i18n/en.json | 9 ++++++- .../routes/scene/edit-scene/ActionCard.jsx | 26 +++++++++++++------ .../actions/ChooseActionTypeCard.jsx | 4 ++- ...ghtParams.jsx => TurnOnOffLightParams.jsx} | 12 ++++++--- 4 files changed, 38 insertions(+), 13 deletions(-) rename front/src/routes/scene/edit-scene/actions/{TurnOnLightParams.jsx => TurnOnOffLightParams.jsx} (81%) diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 4787650d68..e5eb76646f 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -454,6 +454,12 @@ "userLabel": "User", "textLabel": "Message", "textPlaceholder": "Message text" + }, + "turnOnLights": { + "label": "Select the lights you want to turn on" + }, + "turnOffLights": { + "label": "Select the lights you want to turn off" } }, "actions": { @@ -462,7 +468,8 @@ }, "delay": "Wait", "light": { - "turn-on": "Turn On the Lights" + "turn-on": "Turn On the Lights", + "turn-off": "Turn Off the Lights" }, "scene": { "start": "Start scene" diff --git a/front/src/routes/scene/edit-scene/ActionCard.jsx b/front/src/routes/scene/edit-scene/ActionCard.jsx index 0dcebaced1..36cbfd377e 100644 --- a/front/src/routes/scene/edit-scene/ActionCard.jsx +++ b/front/src/routes/scene/edit-scene/ActionCard.jsx @@ -1,7 +1,8 @@ import { h } from 'preact'; import { Text } from 'preact-i18n'; +import { ACTIONS } from '../../../../../server/utils/constants'; import DelayActionParams from './actions/DelayActionParams'; -import TurnOnLightParams from './actions/TurnOnLightParams'; +import TurnOnOffLightParams from './actions/TurnOnOffLightParams'; import SendMessageParams from './actions/SendMessageParams'; import ChooseActionTypeParams from './actions/ChooseActionTypeCard'; @@ -10,9 +11,10 @@ const deleteActionFromColumn = (columnIndex, rowIndex, deleteAction) => () => { }; const ACTION_ICON = { - 'light.turn-on': 'fe fe-sun', - delay: 'fe fe-clock', - 'message.send': 'fe fe-message-square' + [ACTIONS.LIGHT.TURN_ON]: 'fe fe-toggle-right', + [ACTIONS.LIGHT.TURN_OFF]: 'fe fe-toggle-left', + [ACTIONS.TIME.DELAY]: 'fe fe-clock', + [ACTIONS.MESSAGE.SEND]: 'fe fe-message-square' }; const ActionCard = ({ children, ...props }) => ( @@ -48,7 +50,7 @@ const ActionCard = ({ children, ...props }) => (
- {props.action.type === 'delay' && ( + {props.action.type === ACTIONS.TIME.DELAY && ( ( updateActionProperty={props.updateActionProperty} /> )} - {props.action.type === 'light.turn-on' && ( - )} - {props.action.type === 'message.send' && ( + {props.action.type === ACTIONS.LIGHT.TURN_OFF && ( + + )} + {props.action.type === ACTIONS.MESSAGE.SEND && ( { try { const devices = await this.props.httpClient.get('/api/v1/device', { @@ -59,7 +62,10 @@ class TurnOnLight extends Component { render(props, { selectedOptions, deviceOptions }) { return (
- + ; + } +} + +export default SelectDeviceFeature; diff --git a/front/src/routes/scene/edit-scene/triggers/DeviceFeatureState.jsx b/front/src/routes/scene/edit-scene/triggers/DeviceFeatureState.jsx index 92149dfd7d..4b96788a25 100644 --- a/front/src/routes/scene/edit-scene/triggers/DeviceFeatureState.jsx +++ b/front/src/routes/scene/edit-scene/triggers/DeviceFeatureState.jsx @@ -1,57 +1,23 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; import { Text, Localizer } from 'preact-i18n'; -import Select from 'react-select'; import cx from 'classnames'; +import SelectDeviceFeature from '../../../../components/device/SelectDeviceFeature'; + @connect('httpClient', {}) class TurnOnLight extends Component { - getOptions = async () => { - try { - const rooms = await this.props.httpClient.get('/api/v1/room?expand=devices'); - const deviceOptions = []; - rooms.forEach(room => { - const roomDeviceFeatures = []; - room.devices.forEach(device => { - device.features.forEach(feature => { - roomDeviceFeatures.push({ - value: feature.selector, - label: feature.name, - type: feature.type, - unit: feature.unit - }); - }); - }); - if (roomDeviceFeatures.length > 0) { - roomDeviceFeatures.sort((a, b) => { - if (a.label < b.label) { - return -1; - } else if (a.label > b.label) { - return 1; - } - return 0; - }); - deviceOptions.push({ - label: room.name, - options: roomDeviceFeatures - }); - } - }); - await this.setState({ deviceOptions }); - this.refreshSelectedOptions(this.props); - return deviceOptions; - } catch (e) { - console.log(e); - } - }; - handleChange = selectedOption => { - if (selectedOption && selectedOption.value) { - this.props.updateTriggerProperty(this.props.index, 'device_feature', selectedOption.value); - this.props.updateTriggerProperty(this.props.index, 'value', null); + onDeviceFeatureChange = deviceFeature => { + this.setState({ selectedDeviceFeature: deviceFeature }); + if (deviceFeature) { + this.props.updateTriggerProperty(this.props.index, 'device_feature', deviceFeature.selector); + if (deviceFeature.selector !== this.props.trigger.device_feature) { + this.props.updateTriggerProperty(this.props.index, 'value', null); + } } else { this.props.updateTriggerProperty(this.props.index, 'device_feature', null); } - if (selectedOption && selectedOption.type === 'binary') { + if (deviceFeature && deviceFeature.type === 'binary') { this.props.updateTriggerProperty(this.props.index, 'operator', '='); } }; @@ -68,24 +34,6 @@ class TurnOnLight extends Component { handleValueChangeBinary = newValue => () => { this.props.updateTriggerProperty(this.props.index, 'value', newValue); }; - refreshSelectedOptions = nextProps => { - let selectedOption = ''; - if (nextProps.trigger.device_feature && this.state.deviceOptions) { - let deviceOption; - let i = 0; - while (i < this.state.deviceOptions.length && deviceOption === undefined) { - deviceOption = this.state.deviceOptions[i].options.find( - option => option.value === nextProps.trigger.device_feature - ); - i++; - } - - if (deviceOption) { - selectedOption = deviceOption; - } - } - this.setState({ selectedOption }); - }; getBinaryOperator = () => (
@@ -125,32 +73,22 @@ class TurnOnLight extends Component {
); - constructor(props) { - super(props); - this.state = { - deviceOptions: null, - selectedOption: '' - }; - } - async componentDidMount() { - this.getOptions(); - } - componentWillReceiveProps(nextProps) { - this.refreshSelectedOptions(nextProps); - } - render(props, { selectedOption, deviceOptions }) { + render(props, { selectedDeviceFeature }) { return (
- @@ -177,7 +115,7 @@ class TurnOnLight extends Component {
)} - {selectedOption && selectedOption.type !== 'binary' && ( + {selectedDeviceFeature && selectedDeviceFeature.type !== 'binary' && (
@@ -190,10 +128,10 @@ class TurnOnLight extends Component { onChange={this.handleValueChange} /> - {selectedOption && selectedOption.unit && ( + {selectedDeviceFeature.unit && ( - + )} From e3487dfac4bcd9b279f639630177431e63cc7e12 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Thu, 20 Feb 2020 17:06:07 +0100 Subject: [PATCH 05/14] Add DeviceGetValue and OnlyContinueIf action in scenes --- .../components/device/SelectDeviceFeature.jsx | 7 +- front/src/config/i18n/en.json | 17 ++++ .../routes/scene/edit-scene/ActionCard.jsx | 29 +++++- .../routes/scene/edit-scene/ActionGroup.jsx | 1 + .../routes/scene/edit-scene/EditScenePage.jsx | 4 + .../actions/ChooseActionTypeCard.jsx | 9 +- .../actions/DeviceGetValueParams.jsx | 39 ++++++++ .../actions/only-continue-if/Condition.jsx | 99 +++++++++++++++++++ .../only-continue-if/OnlyContinueIfParams.jsx | 71 +++++++++++++ front/src/routes/scene/edit-scene/index.js | 4 +- server/models/scene.js | 6 +- server/utils/constants.js | 4 + 12 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 front/src/routes/scene/edit-scene/actions/DeviceGetValueParams.jsx create mode 100644 front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx create mode 100644 front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx diff --git a/front/src/components/device/SelectDeviceFeature.jsx b/front/src/components/device/SelectDeviceFeature.jsx index 3eb855c080..d5fb7882a8 100644 --- a/front/src/components/device/SelectDeviceFeature.jsx +++ b/front/src/components/device/SelectDeviceFeature.jsx @@ -45,7 +45,9 @@ class SelectDeviceFeature extends Component { }); await this.setState({ deviceOptions, deviceFeaturesDictionnary }); await this.refreshSelectedOptions(this.props); - this.props.onDeviceFeatureChange(this.state.deviceFeaturesDictionnary[this.state.selectedOption.value]); + if (this.state.selectedOption && this.state.selectedOption.value) { + this.props.onDeviceFeatureChange(this.state.deviceFeaturesDictionnary[this.state.selectedOption.value]); + } return deviceOptions; } catch (e) { console.log(e); @@ -92,6 +94,9 @@ class SelectDeviceFeature extends Component { } render(props, { selectedOption, deviceOptions }) { + if (!deviceOptions) { + return null; + } return +
+
+ +
+
+ + } + value={props.condition.value} + onChange={this.handleValueChange} + /> + +
+
+ +
+
+ ); + } +} + +export default Condition; diff --git a/front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx b/front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx new file mode 100644 index 0000000000..8445281136 --- /dev/null +++ b/front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx @@ -0,0 +1,71 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import update from 'immutability-helper'; +import get from 'get-value'; + +import Condition from './Condition'; + +import { ACTIONS } from '../../../../../../../server/utils/constants'; + +const ACTIONS_VARIABLES = { + [ACTIONS.DEVICE.GET_VALUE]: ['last_value'] +}; + +@connect('httpClient', {}) +class OnlyContinueIf extends Component { + handleConditionChange = (conditionIndex, condition) => { + const { columnIndex, index } = this.props; + const newConditions = update(this.props.action.conditions, { + [conditionIndex]: { + $set: condition + } + }); + this.props.updateActionProperty(columnIndex, index, 'conditions', newConditions); + }; + + deleteCondition = index => {}; + + componentDidMount() { + const { columnIndex, index } = this.props; + if (!this.props.action.conditions) { + this.props.updateActionProperty(columnIndex, index, 'conditions', [{}]); + } + } + + render(props, { selectedOptions, deviceOptions }) { + const variableOptions = []; + + props.actionsGroupsBefore.forEach((actionGroup, groupIndex) => { + actionGroup.forEach((action, index) => { + if (ACTIONS_VARIABLES[action.type]) { + variableOptions.push({ + label: `${groupIndex + 1}. ${get(this, `context.intl.dictionary.editScene.actions.${action.type}`)}`, + options: ACTIONS_VARIABLES[action.type].map(variable => { + const option = { + label: get(this, `context.intl.dictionary.editScene.variables.${action.type}.${variable}`), + value: `${groupIndex}.${index}.${variable}` + }; + return option; + }) + }); + } + }); + }); + + return ( +
+ {props.action.conditions && + props.action.conditions.map((condition, index) => ( + + ))} +
+ ); + } +} + +export default OnlyContinueIf; diff --git a/front/src/routes/scene/edit-scene/index.js b/front/src/routes/scene/edit-scene/index.js index bb6b81b06d..933c90d042 100644 --- a/front/src/routes/scene/edit-scene/index.js +++ b/front/src/routes/scene/edit-scene/index.js @@ -1,5 +1,6 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; + import EditScenePage from './EditScenePage'; import actions from '../../../actions/scene'; @@ -23,7 +24,8 @@ class EditScene extends Component { this.props.deleteScene(this.props.scene_selector); this.setState({ saving: false }); }; - componentWillMount() { + + componentDidMount() { this.props.getSceneBySelector(this.props.scene_selector); this.props.getUsers(); this.props.session.dispatcher.addListener('scene.executing-action', payload => diff --git a/server/models/scene.js b/server/models/scene.js index b52c9e702b..f667990fc3 100644 --- a/server/models/scene.js +++ b/server/models/scene.js @@ -9,14 +9,16 @@ const actionSchema = Joi.array().items( type: Joi.string() .valid(ACTION_LIST) .required(), - deviceFeature: Joi.string(), - deviceFeatures: Joi.array().items(Joi.string()), + device_feature: Joi.string(), + device_features: Joi.array().items(Joi.string()), device: Joi.string(), devices: Joi.array().items(Joi.string()), user: Joi.string(), text: Joi.string(), value: Joi.number(), unit: Joi.string(), + variable: Joi.string(), + operator: Joi.string().valid(['=', '!=', '>', '>=', '<', '<=']), }), ), ); diff --git a/server/utils/constants.js b/server/utils/constants.js index 0cc0595ae1..66f8e34e16 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -173,6 +173,7 @@ const CONDITIONS = { const ACTIONS = { DEVICE: { SET_VALUE: 'device.set-value', + GET_VALUE: 'device.get-value', }, LIGHT: { TURN_ON: 'light.turn-on', @@ -191,6 +192,9 @@ const ACTIONS = { MESSAGE: { SEND: 'message.send', }, + CONDITION: { + ONLY_CONTINUE_IF: 'condition.only-continue-if', + }, }; const INTENTS = { From ef8900dc95d1157e0ae664342ff8f69c3630a08b Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Thu, 20 Feb 2020 17:08:51 +0100 Subject: [PATCH 06/14] Fix condition variable names --- .../scene/edit-scene/actions/only-continue-if/Condition.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx b/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx index 01cb9503e8..b414af23fb 100644 --- a/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx +++ b/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx @@ -13,7 +13,7 @@ const ACTIONS_VARIABLES = { class Condition extends Component { handleChange = selectedOption => { const newCondition = update(this.props.condition, { - value: { + variable: { $set: selectedOption && selectedOption.value ? selectedOption.value : null } }); @@ -31,7 +31,7 @@ class Condition extends Component { handleValueChange = e => { const newCondition = update(this.props.condition, { - operator: { + value: { $set: e.target.value } }); From 785191c4b6574a18de09c01ff4114e7762afca7d Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 21 Feb 2020 15:02:34 +0100 Subject: [PATCH 07/14] Upgrade Preact --- front/package-lock.json | 6 +++--- front/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/front/package-lock.json b/front/package-lock.json index 10f7191b72..93337684d3 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -13271,9 +13271,9 @@ } }, "preact": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.2.1.tgz", - "integrity": "sha512-BUNvmQcVtNElku7mYHexIiM5JlGNSW2BY9O2t9xk1NqA43O8wbE0cah6PAlvT7PBHvyDRJ5TAj5Fewdi9DqLoQ==" + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.3.2.tgz", + "integrity": "sha512-yIx4i7gp45enhzX4SLkvvR20UZ+YOUbMdj2KEscU/dC70MHv/L6dpTcsP+4sXrU9SRbA3GjJQQCPfFa5sE17dQ==" }, "preact-cli": { "version": "2.2.1", diff --git a/front/package.json b/front/package.json index 3a9bd485ea..bb0dcc650a 100644 --- a/front/package.json +++ b/front/package.json @@ -38,7 +38,7 @@ "leaflet": "^1.4.0", "linkstate": "^1.1.1", "moment": "^2.24.0", - "preact": "^10.2.1", + "preact": "^10.3.2", "preact-cli-plugin-fast-async": "^1.0.1", "preact-i18n": "^2.0.0-preactx.2", "preact-router": "^3.2.1", From 4c78072d6b543c35eb435a233935ac0041901572 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 21 Feb 2020 15:02:48 +0100 Subject: [PATCH 08/14] Only continue if with multiple conditions working --- front/src/actions/scene.js | 198 -------------- .../components/device/SelectDeviceFeature.jsx | 26 +- .../routes/scene/edit-scene/ActionCard.jsx | 4 + .../routes/scene/edit-scene/ActionGroup.jsx | 2 + .../routes/scene/edit-scene/EditScenePage.jsx | 2 + .../actions/DeviceGetValueParams.jsx | 31 ++- .../actions/only-continue-if/Condition.jsx | 21 +- .../only-continue-if/OnlyContinueIfParams.jsx | 21 +- front/src/routes/scene/edit-scene/index.js | 253 +++++++++++++++++- front/src/utils/device.js | 8 + server/models/scene.js | 9 +- 11 files changed, 330 insertions(+), 245 deletions(-) create mode 100644 front/src/utils/device.js diff --git a/front/src/actions/scene.js b/front/src/actions/scene.js index 64671e8da3..687d233a23 100644 --- a/front/src/actions/scene.js +++ b/front/src/actions/scene.js @@ -1,7 +1,6 @@ import { RequestStatus } from '../utils/consts'; import update, { extend } from 'immutability-helper'; import debounce from 'debounce'; -import { route } from 'preact-router'; extend('$auto', function(value, object) { return object ? update(object, value) : update({}, value); @@ -43,203 +42,6 @@ function createActions(store) { getScenesOrderDir: e.target.value }); await actions.getScenes(store.getState()); - }, - async getSceneBySelector(state, sceneSelector) { - store.setState({ - SceneGetStatus: RequestStatus.Getting - }); - try { - const scene = await state.httpClient.get(`/api/v1/scene/${sceneSelector}`); - if (scene.actions[scene.actions.length - 1].length > 0) { - scene.actions.push([]); - } - if (!scene.triggers) { - scene.triggers = []; - } - store.setState({ - scene, - SceneGetStatus: RequestStatus.Success - }); - } catch (e) { - store.setState({ - SceneGetStatus: RequestStatus.Error - }); - } - }, - async startScene(state, selector) { - store.setState({ - SceneStartStatus: RequestStatus.Getting - }); - try { - await state.httpClient.post(`/api/v1/scene/${selector}/start`); - store.setState({ - SceneStartStatus: RequestStatus.Success - }); - } catch (e) { - store.setState({ - SceneStartStatus: RequestStatus.Error - }); - } - }, - async saveScene(state) { - await state.httpClient.patch(`/api/v1/scene/${state.scene.selector}`, state.scene); - }, - addAction(state, columnIndex) { - let newState = update(state, { - scene: { - actions: { - [columnIndex]: { - $push: [ - { - type: null - } - ] - } - } - } - }); - if (state.scene.actions[columnIndex].length === 0) { - newState = update(newState, { - scene: { - actions: { - $push: [[]] - } - } - }); - } - store.setState(newState); - }, - deleteAction(state, columnIndex, rowIndex) { - let newState = update(state, { - scene: { - actions: { - [columnIndex]: { - $splice: [[rowIndex, 1]] - } - } - } - }); - // if necessary, we remove the last action group - if (newState.scene.actions.length >= 2) { - if ( - newState.scene.actions[newState.scene.actions.length - 1].length === 0 && - newState.scene.actions[newState.scene.actions.length - 2].length === 0 - ) { - newState = update(newState, { - scene: { - actions: { - $splice: [[newState.scene.actions.length - 1, 1]] - } - } - }); - } - } - store.setState(newState); - }, - updateActionProperty(state, columnIndex, rowIndex, property, value) { - const newState = update(state, { - scene: { - actions: { - [columnIndex]: { - [rowIndex]: { - [property]: { - $set: value - } - } - } - } - } - }); - store.setState(newState); - }, - highlighCurrentlyExecutedAction(state, { columnIndex, rowIndex }) { - store.setState({ - highLightedActions: { - [`${columnIndex}:${rowIndex}`]: true - } - }); - }, - removeHighlighAction(state, { columnIndex, rowIndex }) { - setTimeout(() => { - store.setState({ - highLightedActions: { - [`${columnIndex}:${rowIndex}`]: false - } - }); - }, 500); - }, - async getUsers(state) { - store.setState({ - GetUsersStatus: RequestStatus.Getting - }); - try { - const users = await state.httpClient.get(`/api/v1/user`); - const sceneParamsData = { - users - }; - store.setState({ - sceneParamsData, - GetUsersStatus: RequestStatus.Success - }); - } catch (e) { - store.setState({ - GetUsersStatus: RequestStatus.Error - }); - } - }, - async deleteScene(state, selector) { - store.setState({ - deleteSceneStatus: RequestStatus.Getting - }); - try { - await state.httpClient.delete(`/api/v1/scene/${selector}`); - store.setState({ - deleteSceneStatus: RequestStatus.Success - }); - route('/dashboard/scene'); - } catch (e) { - store.setState({ - GetUsersStatus: RequestStatus.Error - }); - } - }, - addTrigger(state) { - const newState = update(state, { - scene: { - triggers: { - $push: [ - { - type: null - } - ] - } - } - }); - store.setState(newState); - }, - deleteTrigger(state, index) { - const newState = update(state, { - scene: { - triggers: { - $splice: [[index, 1]] - } - } - }); - store.setState(newState); - }, - updateTriggerProperty(state, index, property, value) { - const newState = update(state, { - scene: { - triggers: { - [index]: { - [property]: { - $set: value - } - } - } - } - }); - store.setState(newState); } }; actions.debouncedSearch = debounce(actions.search, 200); diff --git a/front/src/components/device/SelectDeviceFeature.jsx b/front/src/components/device/SelectDeviceFeature.jsx index d5fb7882a8..00ebb92772 100644 --- a/front/src/components/device/SelectDeviceFeature.jsx +++ b/front/src/components/device/SelectDeviceFeature.jsx @@ -1,7 +1,8 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; import Select from 'react-select'; -import get from 'get-value'; + +import { getDeviceFeatureName } from '../../utils/device'; @connect('httpClient', {}) class SelectDeviceFeature extends Component { @@ -11,6 +12,7 @@ class SelectDeviceFeature extends Component { const rooms = await this.props.httpClient.get('/api/v1/room?expand=devices'); const deviceOptions = []; + const deviceDictionnary = {}; const deviceFeaturesDictionnary = {}; // and compose the multi-level options @@ -18,13 +20,13 @@ class SelectDeviceFeature extends Component { const roomDeviceFeatures = []; room.devices.forEach(device => { device.features.forEach(feature => { + // keep device / deviceFeature in dictionnary deviceFeaturesDictionnary[feature.selector] = feature; + deviceDictionnary[feature.selector] = device; + roomDeviceFeatures.push({ value: feature.selector, - label: `${device.name} (${get( - this, - `context.intl.dictionary.deviceFeatureCategory.${feature.category}.${feature.type}` - )}) ` + label: getDeviceFeatureName(this.context.intl.dictionary, device, feature) }); }); }); @@ -43,10 +45,13 @@ class SelectDeviceFeature extends Component { }); } }); - await this.setState({ deviceOptions, deviceFeaturesDictionnary }); + await this.setState({ deviceOptions, deviceFeaturesDictionnary, deviceDictionnary }); await this.refreshSelectedOptions(this.props); if (this.state.selectedOption && this.state.selectedOption.value) { - this.props.onDeviceFeatureChange(this.state.deviceFeaturesDictionnary[this.state.selectedOption.value]); + this.props.onDeviceFeatureChange( + deviceFeaturesDictionnary[this.state.selectedOption.value], + deviceDictionnary[this.state.selectedOption.value] + ); } return deviceOptions; } catch (e) { @@ -54,9 +59,12 @@ class SelectDeviceFeature extends Component { } }; handleChange = selectedOption => { - const { deviceFeaturesDictionnary } = this.state; + const { deviceFeaturesDictionnary, deviceDictionnary } = this.state; if (selectedOption && selectedOption.value) { - this.props.onDeviceFeatureChange(deviceFeaturesDictionnary[selectedOption.value]); + this.props.onDeviceFeatureChange( + deviceFeaturesDictionnary[selectedOption.value], + deviceDictionnary[selectedOption.value] + ); } else { this.props.onDeviceFeatureChange(null); } diff --git a/front/src/routes/scene/edit-scene/ActionCard.jsx b/front/src/routes/scene/edit-scene/ActionCard.jsx index cfe3d6f61e..cbd0021002 100644 --- a/front/src/routes/scene/edit-scene/ActionCard.jsx +++ b/front/src/routes/scene/edit-scene/ActionCard.jsx @@ -102,6 +102,8 @@ const ActionCard = ({ children, ...props }) => ( index={props.index} updateActionProperty={props.updateActionProperty} actionsGroupsBefore={props.actionsGroupsBefore} + variables={props.variables} + setVariables={props.setVariables} /> )} {props.action.type === ACTIONS.DEVICE.GET_VALUE && ( @@ -110,6 +112,8 @@ const ActionCard = ({ children, ...props }) => ( columnIndex={props.columnIndex} index={props.index} updateActionProperty={props.updateActionProperty} + variables={props.variables} + setVariables={props.setVariables} /> )}
diff --git a/front/src/routes/scene/edit-scene/ActionGroup.jsx b/front/src/routes/scene/edit-scene/ActionGroup.jsx index 04b5e9d1ed..a1ea6dae94 100644 --- a/front/src/routes/scene/edit-scene/ActionGroup.jsx +++ b/front/src/routes/scene/edit-scene/ActionGroup.jsx @@ -49,6 +49,8 @@ const ActionGroup = ({ children, ...props }) => ( highLightedActions={props.highLightedActions} deleteAction={props.deleteAction} actionsGroupsBefore={props.actionsGroupsBefore} + variables={props.variables} + setVariables={props.setVariables} />
))} diff --git a/front/src/routes/scene/edit-scene/EditScenePage.jsx b/front/src/routes/scene/edit-scene/EditScenePage.jsx index e33396669b..d40e09c6d5 100644 --- a/front/src/routes/scene/edit-scene/EditScenePage.jsx +++ b/front/src/routes/scene/edit-scene/EditScenePage.jsx @@ -72,6 +72,8 @@ const EditScenePage = ({ children, ...props }) => ( actionsGroupsBefore={update(props.scene.actions, { $splice: [[index, props.scene.actions.length - index]] })} + variables={props.variables} + setVariables={props.setVariables} />
{index + 1 < props.scene.actions.length && ( diff --git a/front/src/routes/scene/edit-scene/actions/DeviceGetValueParams.jsx b/front/src/routes/scene/edit-scene/actions/DeviceGetValueParams.jsx index e090c1adbe..c511e211f9 100644 --- a/front/src/routes/scene/edit-scene/actions/DeviceGetValueParams.jsx +++ b/front/src/routes/scene/edit-scene/actions/DeviceGetValueParams.jsx @@ -1,20 +1,49 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; import { Text } from 'preact-i18n'; +import get from 'get-value'; import SelectDeviceFeature from '../../../../components/device/SelectDeviceFeature'; +import { getDeviceFeatureName } from '../../../../utils/device'; @connect('httpClient', {}) class DeviceGetValue extends Component { - onDeviceFeatureChange = deviceFeature => { + onDeviceFeatureChange = (deviceFeature, device) => { const { columnIndex, index } = this.props; if (deviceFeature) { this.props.updateActionProperty(columnIndex, index, 'device_feature', deviceFeature.selector); + this.setVariables(device, deviceFeature); } else { this.props.updateActionProperty(columnIndex, index, 'device_feature', null); + this.setVariables(); } + this.setState({ deviceFeature, device }); }; + setVariables = (device, deviceFeature) => { + const { columnIndex, index } = this.props; + const DEFAULT_VARIABLE_NAME = get(this.context.intl.dictionary, 'editScene.variables.device.get-value.last_value'); + this.props.setVariables(columnIndex, index, [ + { + name: 'last_value', + type: 'device_feature', + label: + device && deviceFeature + ? getDeviceFeatureName(this.context.intl.dictionary, device, deviceFeature) + : DEFAULT_VARIABLE_NAME, + data: { + device, + deviceFeature + } + } + ]); + }; + + componentDidMount() { + const { device, deviceFeature } = this.state; + this.setVariables(device, deviceFeature); + } + render(props, {}) { const { action } = props; return ( diff --git a/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx b/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx index b414af23fb..4f70805d3d 100644 --- a/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx +++ b/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx @@ -4,12 +4,6 @@ import { Text, Localizer } from 'preact-i18n'; import Select from 'react-select'; import update from 'immutability-helper'; -import { ACTIONS } from '../../../../../../../server/utils/constants'; - -const ACTIONS_VARIABLES = { - [ACTIONS.DEVICE.GET_VALUE]: ['last_value'] -}; - class Condition extends Component { handleChange = selectedOption => { const newCondition = update(this.props.condition, { @@ -38,8 +32,21 @@ class Condition extends Component { this.props.handleConditionChange(this.props.index, newCondition); }; + getSelectedOption = () => { + let selectedOption = null; + + this.props.variableOptions.forEach(variableOption => { + const foundOption = variableOption.options.find(option => this.props.condition.variable === option.value); + if (foundOption) { + selectedOption = foundOption; + } + }); + + return selectedOption; + }; + render(props, {}) { - let selectedOption = props.variableOptions.find(option => props.condition.variable === option); + const selectedOption = this.getSelectedOption(); return (
diff --git a/front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx b/front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx index 8445281136..873985f0f4 100644 --- a/front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx +++ b/front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx @@ -5,12 +5,6 @@ import get from 'get-value'; import Condition from './Condition'; -import { ACTIONS } from '../../../../../../../server/utils/constants'; - -const ACTIONS_VARIABLES = { - [ACTIONS.DEVICE.GET_VALUE]: ['last_value'] -}; - @connect('httpClient', {}) class OnlyContinueIf extends Component { handleConditionChange = (conditionIndex, condition) => { @@ -37,16 +31,15 @@ class OnlyContinueIf extends Component { props.actionsGroupsBefore.forEach((actionGroup, groupIndex) => { actionGroup.forEach((action, index) => { - if (ACTIONS_VARIABLES[action.type]) { + if (this.props.variables[groupIndex][index]) { variableOptions.push({ label: `${groupIndex + 1}. ${get(this, `context.intl.dictionary.editScene.actions.${action.type}`)}`, - options: ACTIONS_VARIABLES[action.type].map(variable => { - const option = { - label: get(this, `context.intl.dictionary.editScene.variables.${action.type}.${variable}`), - value: `${groupIndex}.${index}.${variable}` - }; - return option; - }) + options: this.props.variables[groupIndex][index].map(option => ({ + label: option.label, + value: `${groupIndex}.${index}.${option.name}`, + type: option.type, + data: option.data + })) }); } }); diff --git a/front/src/routes/scene/edit-scene/index.js b/front/src/routes/scene/edit-scene/index.js index 933c90d042..ad8e491288 100644 --- a/front/src/routes/scene/edit-scene/index.js +++ b/front/src/routes/scene/edit-scene/index.js @@ -1,51 +1,276 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; +import update from 'immutability-helper'; +import { route } from 'preact-router'; +import { RequestStatus } from '../../../utils/consts'; import EditScenePage from './EditScenePage'; -import actions from '../../../actions/scene'; -@connect('session,sceneParamsData,scene,highLightedActions', actions) +@connect('session,httpClient', {}) class EditScene extends Component { - startScene = () => { - this.props.startScene(this.props.scene_selector); + getSceneBySelector = async () => { + this.setState({ + SceneGetStatus: RequestStatus.Getting + }); + try { + const scene = await this.props.httpClient.get(`/api/v1/scene/${this.props.scene_selector}`); + if (scene.actions[scene.actions.length - 1].length > 0) { + scene.actions.push([]); + } + if (!scene.triggers) { + scene.triggers = []; + } + const variables = []; + scene.actions.forEach(actionGroup => { + variables.push(actionGroup.map(action => [])); + }); + this.setState({ + scene, + variables, + SceneGetStatus: RequestStatus.Success + }); + } catch (e) { + this.setState({ + SceneGetStatus: RequestStatus.Error + }); + } + }; + startScene = async () => { + this.setState({ saving: true }); + try { + await this.props.httpClient.post(`/api/v1/scene/${this.props.scene_selector}/start`); + this.setState({ saving: false }); + } catch (e) { + this.setState({ saving: false }); + } }; saveScene = async () => { this.setState({ saving: true, error: false }); try { - await this.props.saveScene(); + await this.props.httpClient.patch(`/api/v1/scene/${this.props.scene_selector}`, this.state.scene); } catch (e) { console.log(e); this.setState({ error: true }); } this.setState({ saving: false }); }; - deleteScene = () => { + addAction = columnIndex => { + this.setState(prevState => { + let newState = update(prevState, { + scene: { + actions: { + [columnIndex]: { + $push: [ + { + type: null + } + ] + } + } + }, + variables: { + [columnIndex]: { + $push: [[]] + } + } + }); + if (newState.scene.actions[columnIndex].length === 0) { + newState = update(newState, { + scene: { + actions: { + $push: [[]] + } + }, + variables: { + $push: [[]] + } + }); + } + return newState; + }); + }; + deleteAction = (columnIndex, rowIndex) => { + this.setState(prevState => { + let newState = update(prevState, { + scene: { + actions: { + [columnIndex]: { + $splice: [[rowIndex, 1]] + } + } + }, + variables: { + [columnIndex]: { + $splice: [[rowIndex, 1]] + } + } + }); + // if necessary, we remove the last action group + if (newState.scene.actions.length >= 2) { + if ( + newState.scene.actions[newState.scene.actions.length - 1].length === 0 && + newState.scene.actions[newState.scene.actions.length - 2].length === 0 + ) { + newState = update(newState, { + scene: { + actions: { + $splice: [[newState.scene.actions.length - 1, 1]] + } + }, + variables: { + $splice: [[newState.scene.actions.length - 1, 1]] + } + }); + } + } + + return newState; + }); + }; + updateActionProperty = (columnIndex, rowIndex, property, value) => { + this.setState(prevState => { + const newState = update(prevState, { + scene: { + actions: { + [columnIndex]: { + [rowIndex]: { + [property]: { + $set: value + } + } + } + } + } + }); + console.log(newState); + return newState; + }); + }; + highlighCurrentlyExecutedAction = ({ columnIndex, rowIndex }) => { + this.setState({ + highLightedActions: { + [`${columnIndex}:${rowIndex}`]: true + } + }); + }; + removeHighlighAction = ({ columnIndex, rowIndex }) => { + setTimeout(() => { + this.setState({ + highLightedActions: { + [`${columnIndex}:${rowIndex}`]: false + } + }); + }, 500); + }; + deleteScene = async () => { this.setState({ saving: true }); - this.props.deleteScene(this.props.scene_selector); - this.setState({ saving: false }); + try { + await this.props.httpClient.delete(`/api/v1/scene/${this.props.scene_selector}`); + this.setState({ saving: false }); + route('/dashboard/scene'); + } catch (e) { + this.setState({ saving: false }); + } + }; + addTrigger = () => { + this.setState(prevState => { + const newState = update(prevState, { + scene: { + triggers: { + $push: [ + { + type: null + } + ] + } + } + }); + return newState; + }); + + this.setState(newState); }; + deleteTrigger = index => { + this.setState(prevState => { + const newState = update(prevState, { + scene: { + triggers: { + $splice: [[index, 1]] + } + } + }); + return newState; + }); + }; + updateTriggerProperty = (index, property, value) => { + this.setState(prevState => { + const newState = update(prevState, { + scene: { + triggers: { + [index]: { + [property]: { + $set: value + } + } + } + } + }); + return newState; + }); + }; + + setVariables = (columnIndex, index, variables) => { + this.setState(prevState => { + const newState = update(prevState, { + variables: { + [columnIndex]: { + [index]: { + $set: variables + } + } + } + }); + return newState; + }); + }; + + constructor(props) { + super(props); + this.state = { + scene: null, + variables: {} + }; + } componentDidMount() { - this.props.getSceneBySelector(this.props.scene_selector); - this.props.getUsers(); + this.getSceneBySelector(); this.props.session.dispatcher.addListener('scene.executing-action', payload => - this.props.highlighCurrentlyExecutedAction(payload) + this.highlighCurrentlyExecutedAction(payload) ); this.props.session.dispatcher.addListener('scene.finished-executing-action', payload => - this.props.removeHighlighAction(payload) + this.removeHighlighAction(payload) ); } - render(props, { saving, error }) { + render(props, { saving, error, variables, scene }) { return ( - props.scene && ( + scene && ( ) ); diff --git a/front/src/utils/device.js b/front/src/utils/device.js new file mode 100644 index 0000000000..55a15f9278 --- /dev/null +++ b/front/src/utils/device.js @@ -0,0 +1,8 @@ +import get from 'get-value'; + +const getDeviceFeatureName = (dictionnary, device, deviceFeature) => { + const featureDescription = get(dictionnary, `deviceFeatureCategory.${deviceFeature.category}.${deviceFeature.type}`); + return `${device.name} (${featureDescription})`; +}; + +export { getDeviceFeatureName }; diff --git a/server/models/scene.js b/server/models/scene.js index f667990fc3..e4030bbc18 100644 --- a/server/models/scene.js +++ b/server/models/scene.js @@ -17,8 +17,13 @@ const actionSchema = Joi.array().items( text: Joi.string(), value: Joi.number(), unit: Joi.string(), - variable: Joi.string(), - operator: Joi.string().valid(['=', '!=', '>', '>=', '<', '<=']), + conditions: Joi.array().items({ + variable: Joi.string().required(), + operator: Joi.string() + .valid(['=', '!=', '>', '>=', '<', '<=']) + .required(), + value: Joi.string().required(), + }), }), ), ); From 06cbd180f847f255b5ff290fbc8da04be7b3d6c4 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 21 Feb 2020 15:47:22 +0100 Subject: [PATCH 09/14] Add condition / remove condition --- .../routes/scene/edit-scene/ActionCard.jsx | 180 +++++++++--------- .../routes/scene/edit-scene/ActionGroup.jsx | 26 ++- .../actions/only-continue-if/Condition.jsx | 131 ++++++++----- .../only-continue-if/OnlyContinueIfParams.jsx | 19 +- 4 files changed, 205 insertions(+), 151 deletions(-) diff --git a/front/src/routes/scene/edit-scene/ActionCard.jsx b/front/src/routes/scene/edit-scene/ActionCard.jsx index cbd0021002..a0dea9ea66 100644 --- a/front/src/routes/scene/edit-scene/ActionCard.jsx +++ b/front/src/routes/scene/edit-scene/ActionCard.jsx @@ -24,99 +24,101 @@ const ACTION_ICON = { }; const ActionCard = ({ children, ...props }) => ( -
-
- {props.action.type !== null && } - {props.action.type === null && } -
- {' '} - - {props.action.type === null && } -
- {props.highLightedActions && props.highLightedActions[`${props.columnIndex}:${props.index}`] && ( -
- )} -
- {false && ( - - +
+
+
+ {props.action.type !== null && } + {props.action.type === null && } +
+ {' '} + + {props.action.type === null && } +
+ {props.highLightedActions && props.highLightedActions[`${props.columnIndex}:${props.index}`] && ( +
+
+ {props.action.type === ACTIONS.TIME.DELAY && ( + + )} + {props.action.type === null && ( + + )} + {props.action.type === ACTIONS.LIGHT.TURN_ON && ( + + )} + {props.action.type === ACTIONS.LIGHT.TURN_OFF && ( + + )} + {props.action.type === ACTIONS.MESSAGE.SEND && ( + + )} + {props.action.type === ACTIONS.CONDITION.ONLY_CONTINUE_IF && ( + + )} + {props.action.type === ACTIONS.DEVICE.GET_VALUE && ( + )} - - -
-
- {props.action.type === ACTIONS.TIME.DELAY && ( - - )} - {props.action.type === null && ( - - )} - {props.action.type === ACTIONS.LIGHT.TURN_ON && ( - - )} - {props.action.type === ACTIONS.LIGHT.TURN_OFF && ( - - )} - {props.action.type === ACTIONS.MESSAGE.SEND && ( - - )} - {props.action.type === ACTIONS.CONDITION.ONLY_CONTINUE_IF && ( - - )} - {props.action.type === ACTIONS.DEVICE.GET_VALUE && ( - - )} -
); diff --git a/front/src/routes/scene/edit-scene/ActionGroup.jsx b/front/src/routes/scene/edit-scene/ActionGroup.jsx index a1ea6dae94..a8aea5072f 100644 --- a/front/src/routes/scene/edit-scene/ActionGroup.jsx +++ b/front/src/routes/scene/edit-scene/ActionGroup.jsx @@ -39,20 +39,18 @@ const ActionGroup = ({ children, ...props }) => ( )}
{props.actions.map((action, index) => ( -
- -
+ ))}
diff --git a/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx b/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx index 4f70805d3d..97c7177968 100644 --- a/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx +++ b/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx @@ -32,6 +32,10 @@ class Condition extends Component { this.props.handleConditionChange(this.props.index, newCondition); }; + deleteCondition = () => { + this.props.deleteCondition(this.props.index); + }; + getSelectedOption = () => { let selectedOption = null; @@ -49,54 +53,87 @@ class Condition extends Component { const selectedOption = this.getSelectedOption(); return (
-
- - - - - - - - - - -
-
- - } - value={props.condition.value} - onChange={this.handleValueChange} - /> - +
+
+
+ + + + + + + + + + +
+
+
+
+ + + } + value={props.condition.value} + onChange={this.handleValueChange} + /> + +
+
+
+ {props.index > 0 && ( +
+ + +
+ )} +
-
- +
+
+ {props.lastOne && ( + + )} + {!props.lastOne &&

OR

} +
); diff --git a/front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx b/front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx index 873985f0f4..0b1db3145e 100644 --- a/front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx +++ b/front/src/routes/scene/edit-scene/actions/only-continue-if/OnlyContinueIfParams.jsx @@ -17,7 +17,21 @@ class OnlyContinueIf extends Component { this.props.updateActionProperty(columnIndex, index, 'conditions', newConditions); }; - deleteCondition = index => {}; + addCondition = () => { + const { columnIndex, index } = this.props; + const newConditions = update(this.props.action.conditions, { + $push: [{}] + }); + this.props.updateActionProperty(columnIndex, index, 'conditions', newConditions); + }; + + deleteCondition = conditionIndex => { + const { columnIndex, index } = this.props; + const newConditions = update(this.props.action.conditions, { + $splice: [[conditionIndex, 1]] + }); + this.props.updateActionProperty(columnIndex, index, 'conditions', newConditions); + }; componentDidMount() { const { columnIndex, index } = this.props; @@ -54,6 +68,9 @@ class OnlyContinueIf extends Component { index={index} variableOptions={variableOptions} handleConditionChange={this.handleConditionChange} + addCondition={this.addCondition} + deleteCondition={this.deleteCondition} + lastOne={index + 1 === props.action.conditions.length} /> ))}
From 6bac54a76635530a728a510f2847b1d6d249256e Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 21 Feb 2020 15:48:24 +0100 Subject: [PATCH 10/14] Fix linting --- front/src/routes/scene/edit-scene/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/front/src/routes/scene/edit-scene/index.js b/front/src/routes/scene/edit-scene/index.js index ad8e491288..6af0525f73 100644 --- a/front/src/routes/scene/edit-scene/index.js +++ b/front/src/routes/scene/edit-scene/index.js @@ -187,8 +187,6 @@ class EditScene extends Component { }); return newState; }); - - this.setState(newState); }; deleteTrigger = index => { this.setState(prevState => { From 61a4d1529aa3d2cd199cdec0c5a066f0730722e5 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 21 Feb 2020 16:47:52 +0100 Subject: [PATCH 11/14] Add server-side code for continue only if and device.get-value --- .../actions/only-continue-if/Condition.jsx | 3 ++- server/lib/scene/scene.actions.js | 24 ++++++++++++++++++- server/lib/scene/scene.execute.js | 8 ++++++- server/lib/scene/scene.executeActions.js | 14 +++++++++-- server/lib/scene/scene.executeSingleAction.js | 2 +- server/models/scene.js | 2 +- server/utils/coreErrors.js | 8 +++++++ 7 files changed, 54 insertions(+), 7 deletions(-) diff --git a/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx b/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx index 97c7177968..612fc9bdd4 100644 --- a/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx +++ b/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx @@ -24,9 +24,10 @@ class Condition extends Component { }; handleValueChange = e => { + const newValue = Number.isInteger(parseInt(e.target.value, 10)) ? parseInt(e.target.value, 10) : null; const newCondition = update(this.props.condition, { value: { - $set: e.target.value + $set: newValue } }); this.props.handleConditionChange(this.props.index, newCondition); diff --git a/server/lib/scene/scene.actions.js b/server/lib/scene/scene.actions.js index 444d277c4a..f39052bd70 100644 --- a/server/lib/scene/scene.actions.js +++ b/server/lib/scene/scene.actions.js @@ -1,10 +1,12 @@ const Promise = require('bluebird'); const { ACTIONS, DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../utils/constants'); const { getDeviceFeature } = require('../../utils/device'); +const { AbortScene } = require('../../utils/coreErrors'); +const { compare } = require('../../utils/compare'); const logger = require('../../utils/logger'); const actionsFunc = { - [ACTIONS.DEVICE.SET_VALUE]: async (self, action, scope) => { + [ACTIONS.DEVICE.SET_VALUE]: async (self, action, scope, columnIndex, rowIndex) => { let device; let deviceFeature; if (action.device_feature) { @@ -73,6 +75,26 @@ const actionsFunc = { [ACTIONS.MESSAGE.SEND]: async (self, action, scope) => { await self.message.sendToUser(action.user, action.text); }, + [ACTIONS.DEVICE.GET_VALUE]: async (self, action, scope, columnIndex, rowIndex) => { + const deviceFeature = self.stateManager.get('deviceFeature', action.device_feature); + scope[`${columnIndex}.${rowIndex}.last_value`] = deviceFeature.last_value; + }, + [ACTIONS.CONDITION.ONLY_CONTINUE_IF]: async (self, action, scope) => { + let oneConditionVerified = false; + action.conditions.forEach((condition) => { + const conditionVerified = compare(condition.operator, scope[condition.variable], condition.value); + if (conditionVerified) { + oneConditionVerified = true; + } else { + logger.debug( + `Condition not verified. Condition = ${scope[condition.variable]} ${condition.operator} ${condition.value}`, + ); + } + }); + if (oneConditionVerified === false) { + throw new AbortScene(`Actions.condition.only_continue_if: Condition not verified.`); + } + }, }; module.exports = { diff --git a/server/lib/scene/scene.execute.js b/server/lib/scene/scene.execute.js index 34ec9a044f..91212cdefe 100644 --- a/server/lib/scene/scene.execute.js +++ b/server/lib/scene/scene.execute.js @@ -13,7 +13,13 @@ function execute(sceneSelector, scope) { if (!this.scenes[sceneSelector]) { throw new Error(`Scene with selector ${sceneSelector} not found.`); } - this.queue.push(() => executeActions(this, this.scenes[sceneSelector].actions, scope)); + this.queue.push(async () => { + try { + await executeActions(this, this.scenes[sceneSelector].actions, scope); + } catch (e) { + logger.error(e); + } + }); } catch (e) { logger.error(e); } diff --git a/server/lib/scene/scene.executeActions.js b/server/lib/scene/scene.executeActions.js index dfeae17df6..138e9bd503 100644 --- a/server/lib/scene/scene.executeActions.js +++ b/server/lib/scene/scene.executeActions.js @@ -1,6 +1,8 @@ const Promise = require('bluebird'); const { actionsFunc } = require('./scene.actions'); const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../utils/constants'); +const { AbortScene } = require('../../utils/coreErrors'); +const logger = require('../../utils/logger'); /** * @description Execute one action. @@ -25,8 +27,16 @@ async function executeAction(self, action, scope, columnIndex, rowIndex) { payload: { columnIndex, rowIndex }, }); } - // execute action - await actionsFunc[action.type](self, action, scope); + try { + // execute action + await actionsFunc[action.type](self, action, scope, columnIndex, rowIndex); + } catch (e) { + if (e instanceof AbortScene) { + throw e; + } else { + logger.warn(e); + } + } // send message to tell the UI the action has finished being executed if (columnIndex !== undefined && rowIndex !== undefined) { diff --git a/server/lib/scene/scene.executeSingleAction.js b/server/lib/scene/scene.executeSingleAction.js index a801f1f698..fa07d107ce 100644 --- a/server/lib/scene/scene.executeSingleAction.js +++ b/server/lib/scene/scene.executeSingleAction.js @@ -13,7 +13,7 @@ const { executeAction } = require('./scene.executeActions'); async function executeSingleAction(action) { logger.debug(`Executing action of type ${action.type}`); try { - await executeAction(this, action, action.scope); + await executeAction(this, action, {}); } catch (e) { logger.warn(`There was an error executing action ${action.type}`); logger.warn(e); diff --git a/server/models/scene.js b/server/models/scene.js index e4030bbc18..a0e5cae7f0 100644 --- a/server/models/scene.js +++ b/server/models/scene.js @@ -22,7 +22,7 @@ const actionSchema = Joi.array().items( operator: Joi.string() .valid(['=', '!=', '>', '>=', '<', '<=']) .required(), - value: Joi.string().required(), + value: Joi.number(), }), }), ), diff --git a/server/utils/coreErrors.js b/server/utils/coreErrors.js index 1b59d9933f..30204d144d 100644 --- a/server/utils/coreErrors.js +++ b/server/utils/coreErrors.js @@ -40,6 +40,13 @@ class BadParameters extends Error { } } +class AbortScene extends Error { + constructor(message) { + super(); + this.message = message; + } +} + module.exports = { PasswordNotMatchingError, NotFoundError, @@ -47,4 +54,5 @@ module.exports = { BadParameters, NoValuesFoundError, PlatformNotCompatible, + AbortScene, }; From 68fd2e5b9b54f585b64e96f802dddeb594b8c6a7 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 21 Feb 2020 17:32:51 +0100 Subject: [PATCH 12/14] Add tests to new actions --- server/lib/scene/scene.executeSingleAction.js | 5 +- .../lib/scene/scene.executeActions.test.js | 100 ++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/server/lib/scene/scene.executeSingleAction.js b/server/lib/scene/scene.executeSingleAction.js index fa07d107ce..f9490c6db5 100644 --- a/server/lib/scene/scene.executeSingleAction.js +++ b/server/lib/scene/scene.executeSingleAction.js @@ -4,16 +4,17 @@ const { executeAction } = require('./scene.executeActions'); /** * @description Execute an action coming from an event. * @param {Object} action - The action to execute. + * @param {Object} scope - The scope of the action. * @example * scene.executeSingleAction({ * type: 'light.turn-on', * device: 'light', * }); */ -async function executeSingleAction(action) { +async function executeSingleAction(action, scope = {}) { logger.debug(`Executing action of type ${action.type}`); try { - await executeAction(this, action, {}); + await executeAction(this, action, scope); } catch (e) { logger.warn(`There was an error executing action ${action.type}`); logger.warn(e); diff --git a/server/test/lib/scene/scene.executeActions.test.js b/server/test/lib/scene/scene.executeActions.test.js index 28d2067541..e1ec10436c 100644 --- a/server/test/lib/scene/scene.executeActions.test.js +++ b/server/test/lib/scene/scene.executeActions.test.js @@ -1,7 +1,9 @@ const { assert, fake } = require('sinon'); const chaiAssert = require('chai').assert; +const { expect } = require('chai'); const EventEmitter = require('events'); const { ACTIONS } = require('../../../utils/constants'); +const { AbortScene } = require('../../../utils/coreErrors'); const { executeActions } = require('../../../lib/scene/scene.executeActions'); const StateManager = require('../../../lib/state'); @@ -257,4 +259,102 @@ describe('scene.executeActions', () => { 1, ); }); + it('should execute action device.getValue', async () => { + const stateManager = new StateManager(event); + stateManager.setState('deviceFeature', 'my-device-feature', { + category: 'light', + type: 'binary', + last_value: 15, + }); + const device = { + setValue: fake.resolves(null), + }; + const scope = {}; + await executeActions( + { stateManager, event, device }, + [ + [ + { + type: ACTIONS.DEVICE.GET_VALUE, + device_feature: 'my-device-feature', + }, + ], + ], + scope, + ); + expect(scope).to.deep.equal({ '0.0.last_value': 15 }); + }); + it('should abort scene, condition is not verified', async () => { + const stateManager = new StateManager(event); + stateManager.setState('deviceFeature', 'my-device-feature', { + category: 'light', + type: 'binary', + last_value: 15, + }); + const device = { + setValue: fake.resolves(null), + }; + const scope = {}; + const promise = executeActions( + { stateManager, event, device }, + [ + [ + { + type: ACTIONS.DEVICE.GET_VALUE, + device_feature: 'my-device-feature', + }, + ], + [ + { + type: ACTIONS.CONDITION.ONLY_CONTINUE_IF, + conditions: [ + { + variable: '0.0.last_value', + operator: '=', + value: 20, + }, + ], + }, + ], + ], + scope, + ); + return chaiAssert.isRejected(promise, AbortScene); + }); + it('should finish scene, condition is verified', async () => { + const stateManager = new StateManager(event); + stateManager.setState('deviceFeature', 'my-device-feature', { + category: 'light', + type: 'binary', + last_value: 15, + }); + const device = { + setValue: fake.resolves(null), + }; + const scope = {}; + await executeActions( + { stateManager, event, device }, + [ + [ + { + type: ACTIONS.DEVICE.GET_VALUE, + device_feature: 'my-device-feature', + }, + ], + [ + { + type: ACTIONS.CONDITION.ONLY_CONTINUE_IF, + conditions: [ + { + variable: '0.0.last_value', + operator: '=', + value: 15, + }, + ], + }, + ], + ], + scope, + ); + }); }); From fcf99f9d08a227f94c1d71151a928f88c87c0ff1 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 21 Feb 2020 17:48:07 +0100 Subject: [PATCH 13/14] Add error message, name cannot be 'new' --- front/src/actions/createScene.js | 3 +++ front/src/config/i18n/en.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/front/src/actions/createScene.js b/front/src/actions/createScene.js index b5e22cb3d4..c78f01ec7a 100644 --- a/front/src/actions/createScene.js +++ b/front/src/actions/createScene.js @@ -10,6 +10,9 @@ function createActions(store) { if (!state.newScene.name) { newSceneErrors.name = true; } + if (state.newScene.name === 'new') { + newSceneErrors.name = true; + } if (!state.newScene.icon) { newSceneErrors.icon = true; } diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index cd31ad573d..fe5fde13bb 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -648,7 +648,7 @@ "namePlaceholder": "Enter the name of your scene", "iconLabel": "Select an icon for you scene", "createSceneButton": "Create scene", - "invalidName": "Name is required", + "invalidName": "Name is required and cannot be 'new'", "invalidIcon": "Icon is required", "sceneAlreadyExist": "A scene with that name already exist." }, From 712c0df8421c74d2e74571ebb62a96bee6063b15 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Fri, 21 Feb 2020 18:24:25 +0100 Subject: [PATCH 14/14] Add translations --- front/src/config/i18n/en.json | 8 ++++++++ .../actions/only-continue-if/Condition.jsx | 18 ++++++++++++------ front/src/routes/scene/edit-scene/index.js | 1 - server/lib/scene/scene.actions.js | 2 +- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index fe5fde13bb..cc47dde2aa 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -464,6 +464,14 @@ "deviceGetValue": { "description": "This will get the last value of the selected device, so you can use it later in the scene.", "deviceLabel": "Select a device" + }, + "onlyContinueIf": { + "variableLabel": "Variable", + "operatorLabel": "Operator", + "valueLabel": "Value", + "removeLabel": "Remove", + "orText": "OR", + "orButton": "OR" } }, "actions": { diff --git a/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx b/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx index 612fc9bdd4..eda9823951 100644 --- a/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx +++ b/front/src/routes/scene/edit-scene/actions/only-continue-if/Condition.jsx @@ -58,7 +58,7 @@ class Condition extends Component {
@@ -101,7 +101,7 @@ class Condition extends Component {
@@ -118,7 +118,9 @@ class Condition extends Component {
{props.index > 0 && (
- + @@ -130,10 +132,14 @@ class Condition extends Component {
{props.lastOne && ( )} - {!props.lastOne &&

OR

} + {!props.lastOne && ( +

+ +

+ )}
diff --git a/front/src/routes/scene/edit-scene/index.js b/front/src/routes/scene/edit-scene/index.js index 6af0525f73..622f73dca2 100644 --- a/front/src/routes/scene/edit-scene/index.js +++ b/front/src/routes/scene/edit-scene/index.js @@ -142,7 +142,6 @@ class EditScene extends Component { } } }); - console.log(newState); return newState; }); }; diff --git a/server/lib/scene/scene.actions.js b/server/lib/scene/scene.actions.js index f39052bd70..eb64ec7bc9 100644 --- a/server/lib/scene/scene.actions.js +++ b/server/lib/scene/scene.actions.js @@ -92,7 +92,7 @@ const actionsFunc = { } }); if (oneConditionVerified === false) { - throw new AbortScene(`Actions.condition.only_continue_if: Condition not verified.`); + throw new AbortScene('CONDITION_NOT_VERIFIED'); } }, };