diff --git a/front/package-lock.json b/front/package-lock.json index 93337684d3..2dd86f51ce 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -3740,6 +3740,15 @@ "sha.js": "^2.4.8" } }, + "create-react-context": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz", + "integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==", + "requires": { + "gud": "^1.0.0", + "warning": "^4.0.3" + } + }, "cropperjs": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.5.6.tgz", @@ -4209,6 +4218,11 @@ "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.0.1.tgz", "integrity": "sha512-fawhJU3ajJud093das8L3PSXqDb+LjclKhRTIbVX1xC+QeHtL/30kNTkS8s/lOiKMGMngxKvwEzQhBEmK/KQnQ==" }, + "date-fns": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.10.0.tgz", + "integrity": "sha512-EhfEKevYGWhWlZbNeplfhIU/+N+x0iCIx7VzKlXma2EdQyznVlZhCptXUY+BegNpPW2kjdx15Rvq503YcXXrcA==" + }, "dayjs": { "version": "1.8.17", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.17.tgz", @@ -4370,7 +4384,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, "requires": { "is-arguments": "^1.0.4", "is-date-object": "^1.0.1", @@ -4405,7 +4418,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -6580,8 +6592,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -6796,6 +6807,11 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", "dev": true }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "hammerjs": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", @@ -6879,7 +6895,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -7889,8 +7904,7 @@ "is-arguments": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" }, "is-arrayish": { "version": "0.2.1", @@ -7947,8 +7961,7 @@ "is-date-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" }, "is-descriptor": { "version": "0.1.6", @@ -8134,7 +8147,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, "requires": { "has": "^1.0.1" } @@ -10369,14 +10381,12 @@ "object-is": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", - "dev": true + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=" }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -14129,6 +14139,18 @@ "warning": "^4.0.2" } }, + "react-datepicker": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-2.13.0.tgz", + "integrity": "sha512-vUp3tGGT8SwJX5c6hKnhSJFOFjjBgwKQUv9nl7dkhe/viraxrihLikDr/KVDRGYyj/Q1MBvTpOog4fsSly9o9A==", + "requires": { + "classnames": "^2.2.6", + "date-fns": "^2.0.1", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.9.0", + "react-popper": "^1.3.4" + } + }, "react-input-autosize": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz", @@ -14147,6 +14169,11 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-onclickoutside": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz", + "integrity": "sha512-8ltIY3bC7oGhj2nPAvWOGi+xGFybPNhJM0V1H8hY/whNcXgmDeaeoCMPPd8VatrpTsUWjb/vGzrmu6SrXVty3A==" + }, "react-overlays": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-2.1.0.tgz", @@ -14161,6 +14188,20 @@ "warning": "^4.0.3" } }, + "react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==", + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "^0.3.0", + "deep-equal": "^1.1.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + } + }, "react-select": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/react-select/-/react-select-3.0.8.tgz", @@ -14656,7 +14697,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", - "dev": true, "requires": { "define-properties": "^1.1.2" } @@ -16785,6 +16825,11 @@ "mime-types": "~2.1.24" } }, + "typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", diff --git a/front/package.json b/front/package.json index bb0dcc650a..08ed387c55 100644 --- a/front/package.json +++ b/front/package.json @@ -21,7 +21,7 @@ "identity-obj-proxy": "^3.0.0", "jest": "^21.2.1", "per-env": "^1.0.2", - "preact-cli": "^2.1.0", + "preact-cli": "^2.2.1", "preact-render-spy": "^1.2.1", "prettier": "^1.17.1" }, @@ -30,6 +30,7 @@ "axios": "^0.18.0", "classnames": "^2.2.6", "cropperjs": "^1.5.1", + "date-fns": "^2.10.0", "dayjs": "^1.8.14", "debounce": "^1.2.0", "dotenv": "^6.2.0", @@ -44,6 +45,7 @@ "preact-router": "^3.2.1", "qrcode": "^1.4.2", "react-big-calendar": "^0.22.1", + "react-datepicker": "^2.13.0", "react-select": "^3.0.8", "react-stripe-elements": "^5.0.1", "set-value": "^3.0.0", diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 5d899f118d..bdc0c428c6 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -508,6 +508,9 @@ "triggers": { "device": { "new-state": "Device state change" + }, + "time": { + "changed": "Scheduled trigger" } }, "triggersCard": { @@ -521,6 +524,35 @@ "valuePlaceholder": "Value", "on": "On", "off": "Off" + }, + "scheduledTrigger": { + "everyMonth": "Every month", + "everyWeek": "Every week", + "everyDay": "Every day", + "interval": "Interval", + "customTime": "On a specific day", + "dateLabel": "Date", + "timeLabel": "Time", + "unitLabel": "Unit", + "intervalLabel": "Interval", + "daysOfTheWeekLabel": "Days of the week", + "dayOfTheMonthLabel": "Day of the month", + "dateFormat": "MM-dd-yyyy", + "timeCaption": "Time", + "units": { + "second": "seconds", + "minute": "minutes", + "hour": "hours" + }, + "daysOfTheWeek": { + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday" + } } } }, diff --git a/front/src/routes/scene/edit-scene/TriggerCard.jsx b/front/src/routes/scene/edit-scene/TriggerCard.jsx index 4a0f64502b..12e53b48a3 100644 --- a/front/src/routes/scene/edit-scene/TriggerCard.jsx +++ b/front/src/routes/scene/edit-scene/TriggerCard.jsx @@ -1,8 +1,11 @@ import { h } from 'preact'; import { Text } from 'preact-i18n'; import DeviceFeatureState from './triggers/DeviceFeatureState'; +import ScheduledTrigger from './triggers/ScheduledTrigger'; import ChooseTriggerType from './triggers/ChooseTriggerTypeCard'; +import { EVENTS } from '../../../../../server/utils/constants'; + const deleteTriggerFromList = (deleteTrigger, index) => () => { deleteTrigger(index); }; @@ -10,7 +13,8 @@ const deleteTriggerFromList = (deleteTrigger, index) => () => { const TriggerCard = ({ children, ...props }) => (
- {props.trigger.type === 'device.new-state' && } + {props.trigger.type === EVENTS.DEVICE.NEW_STATE && } + {props.trigger.type === EVENTS.TIME.CHANGED && } {props.trigger.type === null && }
( {props.trigger.type === null && ( )} - {props.trigger.type === 'device.new-state' && ( + {props.trigger.type === EVENTS.DEVICE.NEW_STATE && ( )} + {props.trigger.type === EVENTS.TIME.CHANGED && ( + + )}
); diff --git a/front/src/routes/scene/edit-scene/triggers/ChooseTriggerTypeCard.jsx b/front/src/routes/scene/edit-scene/triggers/ChooseTriggerTypeCard.jsx index 2fff00db65..22d238f8f9 100644 --- a/front/src/routes/scene/edit-scene/triggers/ChooseTriggerTypeCard.jsx +++ b/front/src/routes/scene/edit-scene/triggers/ChooseTriggerTypeCard.jsx @@ -1,9 +1,12 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; import { Text } from 'preact-i18n'; -const TRIGGER_LIST = ['device.new-state']; import Select from 'react-select'; +import { EVENTS } from '../../../../../../server/utils/constants'; + +const TRIGGER_LIST = [EVENTS.DEVICE.NEW_STATE, EVENTS.TIME.CHANGED]; + @connect('httpClient', {}) class ChooseTriggerType extends Component { state = { diff --git a/front/src/routes/scene/edit-scene/triggers/ScheduledTrigger.jsx b/front/src/routes/scene/edit-scene/triggers/ScheduledTrigger.jsx new file mode 100644 index 0000000000..e16ac64b27 --- /dev/null +++ b/front/src/routes/scene/edit-scene/triggers/ScheduledTrigger.jsx @@ -0,0 +1,320 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import DatePicker from 'react-datepicker'; +import get from 'get-value'; +import { Text, Localizer } from 'preact-i18n'; +import { format } from 'date-fns'; +import Select from 'react-select'; + +import fr from 'date-fns/locale/fr'; + +import 'react-datepicker/dist/react-datepicker.css'; + +const DAYS_OF_THE_MONTH = new Array(31).fill(0, 0, 31).map((val, index) => index + 1); + +@connect('httpClient,user', {}) +class TurnOnLight extends Component { + resetForm = () => { + this.props.updateTriggerProperty(this.props.index, 'date', undefined); + this.props.updateTriggerProperty(this.props.index, 'time', undefined); + this.props.updateTriggerProperty(this.props.index, 'interval', undefined); + this.props.updateTriggerProperty(this.props.index, 'unit', undefined); + this.props.updateTriggerProperty(this.props.index, 'days_of_the_week', undefined); + this.props.updateTriggerProperty(this.props.index, 'day_of_the_month', undefined); + }; + handleTypeChange = e => { + const schedulerType = e.target.value; + this.props.updateTriggerProperty(this.props.index, 'scheduler_type', schedulerType); + this.resetForm(); + if (schedulerType === 'every-month') { + this.props.updateTriggerProperty(this.props.index, 'day_of_the_month', 1); + } else if (schedulerType === 'every-week') { + this.props.updateTriggerProperty(this.props.index, 'days_of_the_week', []); + } else if (schedulerType === 'interval') { + this.props.updateTriggerProperty(this.props.index, 'unit', 'second'); + } + }; + handleDateChange = date => { + this.props.updateTriggerProperty(this.props.index, 'date', format(date, 'yyyy-MM-dd')); + }; + handleTimeChange = time => { + this.props.updateTriggerProperty(this.props.index, 'time', format(time, 'HH:mm')); + }; + handleIntervalChange = e => { + if (!isNaN(parseInt(e.target.value, 10))) { + this.props.updateTriggerProperty(this.props.index, 'interval', parseInt(e.target.value, 10)); + } else { + this.props.updateTriggerProperty(this.props.index, 'interval', null); + } + }; + handleUnitChange = e => { + this.props.updateTriggerProperty(this.props.index, 'unit', e.target.value); + }; + handleDayOfTheWeekChange = options => { + const values = options ? options.map(option => option.value) : []; + this.props.updateTriggerProperty(this.props.index, 'days_of_the_week', values); + }; + handleDayOfTheMonthChange = e => { + this.props.updateTriggerProperty(this.props.index, 'day_of_the_month', e.target.value); + }; + + render({}, {}) { + const language = get(this.props, 'user.language'); + const localeSet = language === 'fr' ? fr : 'en'; + const time = this.props.trigger.time + ? new Date().setHours(this.props.trigger.time.substr(0, 2), this.props.trigger.time.substr(3, 2)) + : null; + const selectedWeekDaysOptions = this.props.trigger.days_of_the_week + ? this.props.trigger.days_of_the_week.map(day => ({ + value: day, + label: + })) + : []; + const date = this.props.trigger.date ? new Date(this.props.trigger.date) : null; + return ( +
+
+
+
+
Type
+ +
+
+ {this.props.trigger.scheduler_type === 'custom-time' && ( +
+
+
+ +
+ + } + locale={localeSet} + onChange={this.handleDateChange} + dateFormat={} + /> + +
+
+ )} + {this.props.trigger.scheduler_type === 'custom-time' && ( +
+
+
+ +
+ + } + showTimeSelect + showTimeSelectOnly + timeIntervals={5} + timeCaption={} + dateFormat="HH:mm" + /> + +
+
+ )} + {this.props.trigger.scheduler_type === 'interval' && ( +
+
+
+ +
+ + } + /> + +
+
+ )} + {this.props.trigger.scheduler_type === 'interval' && ( +
+
+
+ +
+ +
+
+ )} + {this.props.trigger.scheduler_type === 'every-day' && ( +
+
+
+ +
+ + } + showTimeSelect + showTimeSelectOnly + timeIntervals={5} + timeCaption={} + dateFormat="HH:mm" + /> + +
+
+ )} + {this.props.trigger.scheduler_type === 'every-week' && ( +
+
+
+ +
+ + {DAYS_OF_THE_MONTH.map(value => ( + + ))} + +
+
+ )} + {this.props.trigger.scheduler_type === 'every-month' && ( +
+
+
+ +
+ + } + showTimeSelect + showTimeSelectOnly + timeIntervals={5} + timeCaption={} + dateFormat="HH:mm" + /> + +
+
+ )} +
+
+ ); + } +} + +export default TurnOnLight; diff --git a/server/api/websockets/index.js b/server/api/websockets/index.js index 7f7c980d84..0e85cf8b26 100644 --- a/server/api/websockets/index.js +++ b/server/api/websockets/index.js @@ -92,7 +92,6 @@ function userDisconnected(user, client) { */ function init() { this.wss.on('connection', (ws) => { - logger.debug(`New user connected in websocket, ${ws}`); let user; let authenticated = false; ws.on('close', () => { @@ -114,7 +113,6 @@ function init() { authenticated = true; this.userConnected(user, ws); } catch (e) { - logger.debug(e); ws.close(4000, ERROR_MESSAGES.INVALID_ACCESS_TOKEN); } break; diff --git a/server/lib/scene/index.js b/server/lib/scene/index.js index 98c43964c3..6d924bf8d0 100644 --- a/server/lib/scene/index.js +++ b/server/lib/scene/index.js @@ -3,6 +3,7 @@ const { addScene } = require('./scene.addScene'); const { create } = require('./scene.create'); const { checkTrigger } = require('./scene.checkTrigger'); const { init } = require('./scene.init'); +const { cancelTriggers } = require('./scene.cancelTriggers'); const { destroy } = require('./scene.destroy'); const { execute } = require('./scene.execute'); const { get } = require('./scene.get'); @@ -29,6 +30,7 @@ const SceneManager = function SceneManager(stateManager, event, device, message) }; SceneManager.prototype.addScene = addScene; +SceneManager.prototype.cancelTriggers = cancelTriggers; SceneManager.prototype.create = create; SceneManager.prototype.checkTrigger = checkTrigger; SceneManager.prototype.destroy = destroy; diff --git a/server/lib/scene/scene.addScene.js b/server/lib/scene/scene.addScene.js index 3d3d9fcefc..8cf1c41416 100644 --- a/server/lib/scene/scene.addScene.js +++ b/server/lib/scene/scene.addScene.js @@ -1,13 +1,92 @@ +const schedule = require('node-schedule'); +const cloneDeep = require('lodash.clonedeep'); + +const { BadParameters } = require('../../utils/coreErrors'); +const { EVENTS } = require('../../utils/constants'); + +const MAX_VALUE_SET_INTERVAL = 2 ** 31 - 1; + +const nodeScheduleDaysOfWeek = { + sunday: 0, + monday: 1, + tuesday: 2, + wednesday: 3, + thursday: 4, + friday: 5, + saturday: 6, +}; + /** * @description Add a scene to the scene manager. - * @param {Object} scene - Scene object from DB. + * @param {Object} sceneRaw - Scene object from DB. + * @returns {Object} Return the scene. * @example * addScene({ * selector: 'test' * }); */ -async function addScene(scene) { +function addScene(sceneRaw) { + // deep clone the scene so that we don't modify the same object which will be returned to the client + const scene = cloneDeep(sceneRaw); + // first, if the scene actually exist, we cancel all triggers + this.cancelTriggers(scene.selector); + // Foreach triggger, we schedule jobs for triggers that need to be scheduled + scene.triggers.forEach((trigger) => { + if (trigger.type === EVENTS.TIME.CHANGED && trigger.scheduler_type !== 'interval') { + const rule = new schedule.RecurrenceRule(); + switch (trigger.scheduler_type) { + case 'every-month': + rule.date = trigger.day_of_the_month; + rule.hour = parseInt(trigger.time.substr(0, 2), 10); + rule.minute = parseInt(trigger.time.substr(3, 2), 10); + rule.second = 0; + break; + case 'every-week': + rule.dayOfWeek = trigger.days_of_the_week.map((day) => nodeScheduleDaysOfWeek[day]); + rule.hour = parseInt(trigger.time.substr(0, 2), 10); + rule.minute = parseInt(trigger.time.substr(3, 2), 10); + rule.second = 0; + break; + case 'every-day': + rule.hour = parseInt(trigger.time.substr(0, 2), 10); + rule.minute = parseInt(trigger.time.substr(3, 2), 10); + rule.second = 0; + break; + case 'custom-time': + rule.year = parseInt(trigger.date.substr(0, 4), 10); + rule.month = parseInt(trigger.date.substr(5, 2), 10) - 1; + rule.date = parseInt(trigger.date.substr(8, 4), 10); + rule.hour = parseInt(trigger.time.substr(0, 2), 10); + rule.minute = parseInt(trigger.time.substr(3, 2), 10); + rule.second = 0; + break; + default: + throw new BadParameters(`${trigger.scheduler_type} not supported`); + } + trigger.nodeScheduleJob = schedule.scheduleJob(rule, () => this.event.emit(EVENTS.TRIGGERS.CHECK, trigger)); + } else if (trigger.type === EVENTS.TIME.CHANGED && trigger.scheduler_type === 'interval') { + let intervalMilliseconds; + switch (trigger.unit) { + case 'second': + intervalMilliseconds = trigger.interval * 1000; + break; + case 'minute': + intervalMilliseconds = trigger.interval * 60 * 1000; + break; + case 'hour': + intervalMilliseconds = trigger.interval * 60 * 60 * 1000; + break; + default: + throw new BadParameters(`${trigger.unit} not supported`); + } + if (intervalMilliseconds > MAX_VALUE_SET_INTERVAL) { + throw new BadParameters(`${trigger.interval} ${trigger.unit} is too big for an interval`); + } + trigger.jsInterval = setInterval(() => this.event.emit(EVENTS.TRIGGERS.CHECK, trigger), intervalMilliseconds); + } + }); this.scenes[scene.selector] = scene; + return scene; } module.exports = { diff --git a/server/lib/scene/scene.cancelTriggers.js b/server/lib/scene/scene.cancelTriggers.js new file mode 100644 index 0000000000..7841814771 --- /dev/null +++ b/server/lib/scene/scene.cancelTriggers.js @@ -0,0 +1,24 @@ +/** + * @description Cancel a trigger. + * @param {Object} sceneSelector - The selector of the scene to clean. + * @example + * this.cancelTriggers('test-scene'); + */ +function cancelTriggers(sceneSelector) { + if (this.scenes[sceneSelector]) { + this.scenes[sceneSelector].triggers.forEach((trigger) => { + if (trigger.nodeScheduleJob) { + trigger.nodeScheduleJob.cancel(); + delete trigger.nodeScheduleJob; + } + if (trigger.jsInterval) { + clearInterval(trigger.jsInterval); + delete trigger.jsInterval; + } + }); + } +} + +module.exports = { + cancelTriggers, +}; diff --git a/server/lib/scene/scene.destroy.js b/server/lib/scene/scene.destroy.js index e703a279db..e5da4e119e 100644 --- a/server/lib/scene/scene.destroy.js +++ b/server/lib/scene/scene.destroy.js @@ -19,7 +19,9 @@ async function destroy(selector) { } await existingScene.destroy(); - + // we cancel triggers linked to the scene + this.cancelTriggers(selector); + // then we delete the scene in RAM delete this.scenes[selector]; } diff --git a/server/lib/scene/scene.triggers.js b/server/lib/scene/scene.triggers.js index b7096d3f44..a20ecff91f 100644 --- a/server/lib/scene/scene.triggers.js +++ b/server/lib/scene/scene.triggers.js @@ -10,6 +10,7 @@ const triggersFunc = { // we compare the value with the expected value return compare(trigger.operator, event.last_value, trigger.value); }, + [EVENTS.TIME.CHANGED]: (event, trigger) => true, }; module.exports = { diff --git a/server/models/scene.js b/server/models/scene.js index a0e5cae7f0..bdcc254b23 100644 --- a/server/models/scene.js +++ b/server/models/scene.js @@ -1,4 +1,4 @@ -const Joi = require('joi'); +const Joi = require('@hapi/joi').extend(require('@hapi/joi-date')); const { ACTION_LIST, EVENT_LIST } = require('../utils/constants'); const { addSelector } = require('../utils/addSelector'); const iconList = require('../config/icons.json'); @@ -7,7 +7,7 @@ const actionSchema = Joi.array().items( Joi.array().items( Joi.object().keys({ type: Joi.string() - .valid(ACTION_LIST) + .valid(...ACTION_LIST) .required(), device_feature: Joi.string(), device_features: Joi.array().items(Joi.string()), @@ -20,7 +20,7 @@ const actionSchema = Joi.array().items( conditions: Joi.array().items({ variable: Joi.string().required(), operator: Joi.string() - .valid(['=', '!=', '>', '>=', '<', '<=']) + .valid('=', '!=', '>', '>=', '<', '<=') .required(), value: Joi.number(), }), @@ -31,14 +31,25 @@ const actionSchema = Joi.array().items( const triggersSchema = Joi.array().items( Joi.object().keys({ type: Joi.string() - .valid(EVENT_LIST) + .valid(...EVENT_LIST) .required(), house: Joi.string(), device: Joi.string(), device_feature: Joi.string(), - operator: Joi.string().valid(['=', '!=', '>', '>=', '<', '<=']), + operator: Joi.string().valid('=', '!=', '>', '>=', '<', '<='), value: Joi.number(), user: Joi.string(), + scheduler_type: Joi.string().valid('every-month', 'every-week', 'every-day', 'interval', 'custom-time'), + date: Joi.date().format('YYYY-MM-DD'), + time: Joi.string().regex(/^([0-9]{2}):([0-9]{2})$/), + interval: Joi.number(), + unit: Joi.string(), + days_of_the_week: Joi.array().items( + Joi.string().valid('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'), + ), + day_of_the_month: Joi.number() + .min(1) + .max(31), }), ); @@ -72,7 +83,7 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.JSON, validate: { isEven(value) { - const result = Joi.validate(value, actionSchema); + const result = actionSchema.validate(value); if (result.error) { throw new Error(result.error.details[0].message); } @@ -83,7 +94,7 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.JSON, validate: { isEven(value) { - const result = Joi.validate(value, triggersSchema); + const result = triggersSchema.validate(value); if (result.error) { throw new Error(result.error.details[0].message); } diff --git a/server/package-lock.json b/server/package-lock.json index 633e2a7f00..b655ec85f3 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -150,6 +150,57 @@ "socket.io-client": "^2.1.1" } }, + "@hapi/address": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.0.0.tgz", + "integrity": "sha512-GDDpkCdSUfkQCznmWUHh9dDN85BWf/V8TFKQ2JLuHdGB4Yy3YTEGBzZxoBNxfNBEvreSR/o+ZxBBSNNEVzY+lQ==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@hapi/formula": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-2.0.0.tgz", + "integrity": "sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==" + }, + "@hapi/hoek": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.0.3.tgz", + "integrity": "sha512-jKtjLLDiH95b002sJVc5c74PE6KKYftuyVdVmsuYId5stTaWcRFqE+5ukZI4gDUKjGn8wv2C3zPn3/nyjEI7gg==" + }, + "@hapi/joi": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-17.1.0.tgz", + "integrity": "sha512-ob67RcPlwRWxBzLCnWvcwx5qbwf88I3ykD7gcJLWOTRfLLgosK7r6aeChz4thA3XRvuBfI0KB1tPVl2EQFlPXw==", + "requires": { + "@hapi/address": "^4.0.0", + "@hapi/formula": "^2.0.0", + "@hapi/hoek": "^9.0.0", + "@hapi/pinpoint": "^2.0.0", + "@hapi/topo": "^5.0.0" + } + }, + "@hapi/joi-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/joi-date/-/joi-date-2.0.1.tgz", + "integrity": "sha512-8be8JUEC8Wm1Do3ryJy+TXmkAL13b2JwXn7gILBoor8LopY/M+hJskodzOOxfJdckkfWnbmbnMEyJW3d/gZMfA==", + "requires": { + "moment": "2.x.x" + } + }, + "@hapi/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==" + }, + "@hapi/topo": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", + "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, "@microsoft/recognizers-text": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text/-/recognizers-text-1.1.4.tgz", @@ -1875,6 +1926,15 @@ "resolved": "https://registry.npmjs.org/crlf-normalize/-/crlf-normalize-1.0.3.tgz", "integrity": "sha512-n+H8helYU03hMHL1W380VHNHdUW61WVpVTADjZoTzHMLyyu3IKAurTQyCG+neq61txELetenaXQWCPCtukcZBA==" }, + "cron-parser": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-2.13.0.tgz", + "integrity": "sha512-UWeIpnRb0eyoWPVk+pD3TDpNx3KCFQeezO224oJIkktBrcW6RoAPOx5zIKprZGfk6vcYSmA8yQXItejSaDBhbQ==", + "requires": { + "is-nan": "^1.2.1", + "moment-timezone": "^0.5.25" + } + }, "cross-env": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.1.tgz", @@ -2039,7 +2099,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -4721,6 +4780,14 @@ "resolved": "https://registry.npmjs.org/is-mergeable-object/-/is-mergeable-object-1.1.0.tgz", "integrity": "sha512-JfyDDwUdtS4yHCgUpxOyKB9dnfZ0gecufxB0eytX6BmSXSE+8dbxDGt+V7CNRIRJ9sYFV/WQt2KJG6hNob2sBw==" }, + "is-nan": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.0.tgz", + "integrity": "sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ==", + "requires": { + "define-properties": "^1.1.3" + } + }, "is-npm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", @@ -5349,6 +5416,11 @@ "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", "dev": true }, + "long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ=" + }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -5919,6 +5991,16 @@ } } }, + "node-schedule": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-1.3.2.tgz", + "integrity": "sha512-GIND2pHMHiReSZSvS6dpZcDH7pGPGFfWBIEud6S00Q8zEIzAs9ommdyRK1ZbQt8y1LyZsJYZgPnyi7gpU2lcdw==", + "requires": { + "cron-parser": "^2.7.3", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.0.0" + } + }, "node-webcrypto-ossl": { "version": "1.0.48", "resolved": "https://registry.npmjs.org/node-webcrypto-ossl/-/node-webcrypto-ossl-1.0.48.tgz", @@ -7243,8 +7325,7 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -8784,6 +8865,11 @@ "resolved": "https://registry.npmjs.org/sort-object-keys2/-/sort-object-keys2-1.1.0.tgz", "integrity": "sha512-yV3GAWYQp+xOqDw8f484cmZY8NACX1fITTM/dUlXteRAdZHOmdJUI0ibuDHH+454kIvorXuJiJCA5evUtPfoVw==" }, + "sorted-array-functions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz", + "integrity": "sha512-sWpjPhIZJtqO77GN+LD8dDsDKcWZ9GCOJNqKzi1tvtjGIzwfoyuRH8S0psunmc6Z5P+qfDqztSbwYR5X/e1UTg==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/server/package.json b/server/package.json index c358e70531..dd7eae564f 100644 --- a/server/package.json +++ b/server/package.json @@ -67,6 +67,8 @@ }, "dependencies": { "@gladysassistant/gladys-gateway-js": "^3.2.2", + "@hapi/joi": "^17.1.0", + "@hapi/joi-date": "^2.0.1", "bcrypt": "^3.0.3", "bluebird": "^3.5.3", "compression": "^1.7.4", @@ -80,7 +82,9 @@ "handlebars": "^4.1.0", "joi": "^14.3.1", "jsonwebtoken": "^8.4.0", + "lodash.clonedeep": "^4.5.0", "node-nlp": "^3.0.3", + "node-schedule": "^1.3.2", "node-webcrypto-ossl": "^1.0.48", "path-to-regexp": "^3.0.0", "queue": "^6.0.0", diff --git a/server/seeders/20190227043234-scene.js b/server/seeders/20190227043234-scene.js index 759a88e85c..b55a369ba7 100644 --- a/server/seeders/20190227043234-scene.js +++ b/server/seeders/20190227043234-scene.js @@ -18,6 +18,7 @@ module.exports = { }, ], ]), + triggers: '[]', created_at: '2019-02-12 07:49:07.556 +00:00', updated_at: '2019-02-12 07:49:07.556 +00:00', }, diff --git a/server/test/controllers/scene/scene.test.js b/server/test/controllers/scene/scene.test.js index a1acfd417a..e00196e1b5 100644 --- a/server/test/controllers/scene/scene.test.js +++ b/server/test/controllers/scene/scene.test.js @@ -9,6 +9,7 @@ describe('POST /api/v1/scene', () => { .send({ name: 'New Scene', icon: 'bell', + triggers: [], actions: [ [ { diff --git a/server/test/lib/scene/scene.addScene.test.js b/server/test/lib/scene/scene.addScene.test.js new file mode 100644 index 0000000000..6960560029 --- /dev/null +++ b/server/test/lib/scene/scene.addScene.test.js @@ -0,0 +1,133 @@ +const { expect } = require('chai'); +const EventEmitter = require('events'); +const { EVENTS } = require('../../../utils/constants'); +const { BadParameters } = require('../../../utils/coreErrors'); +const SceneManager = require('../../../lib/scene'); + +const event = new EventEmitter(); + +describe('SceneManager.addScene', () => { + it('should add a scene with a scheduled trigger, every-month', async () => { + const sceneManager = new SceneManager({}, event); + const scene = sceneManager.addScene({ + name: 'a-test-scene', + icon: 'bell', + triggers: [ + { + type: EVENTS.TIME.CHANGED, + scheduler_type: 'every-month', + day_of_the_month: 1, + time: '12:00', + }, + ], + actions: [], + }); + expect(sceneManager.scenes[scene.selector].triggers[0]).to.have.property('nodeScheduleJob'); + }); + it('should add a scene with a scheduled trigger, every-week', async () => { + const sceneManager = new SceneManager({}, event); + const scene = sceneManager.addScene({ + name: 'a-test-scene', + icon: 'bell', + triggers: [ + { + type: EVENTS.TIME.CHANGED, + scheduler_type: 'every-week', + days_of_the_week: ['monday'], + time: '12:00', + }, + ], + actions: [], + }); + expect(sceneManager.scenes[scene.selector].triggers[0]).to.have.property('nodeScheduleJob'); + }); + it('should add a scene with a scheduled trigger, every-day', async () => { + const sceneManager = new SceneManager({}, event); + const scene = sceneManager.addScene({ + name: 'a-test-scene', + icon: 'bell', + triggers: [ + { + type: EVENTS.TIME.CHANGED, + scheduler_type: 'every-day', + time: '12:00', + }, + ], + actions: [], + }); + expect(sceneManager.scenes[scene.selector].triggers[0]).to.have.property('nodeScheduleJob'); + }); + it('should add a scene with a scheduled trigger, custom-time', async () => { + const sceneManager = new SceneManager({}, event); + const in30Minutes = new Date(new Date().getTime() + 30 * 60 * 1000); + const date = in30Minutes.toISOString().slice(0, 10); + const time = in30Minutes.toLocaleTimeString('en-US', { hour12: false }).slice(0, 5); + const scene = sceneManager.addScene({ + name: 'a-test-scene', + icon: 'bell', + triggers: [ + { + type: EVENTS.TIME.CHANGED, + scheduler_type: 'custom-time', + date, + time, + }, + ], + actions: [], + }); + expect(sceneManager.scenes[scene.selector].triggers[0]).to.have.property('nodeScheduleJob'); + }); + it('should add a scene with a scheduled trigger, interval', async () => { + const sceneManager = new SceneManager({}, event); + const scene = sceneManager.addScene({ + name: 'a-test-scene', + icon: 'bell', + triggers: [ + { + type: EVENTS.TIME.CHANGED, + scheduler_type: 'interval', + interval: 10, + unit: 'hour', + }, + ], + actions: [], + }); + expect(sceneManager.scenes[scene.selector].triggers[0]).to.have.property('jsInterval'); + }); + it('should throw an error, interval is too big', async () => { + const sceneManager = new SceneManager({}, event); + expect(() => + sceneManager.addScene({ + name: 'a-test-scene', + icon: 'bell', + triggers: [ + { + type: EVENTS.TIME.CHANGED, + scheduler_type: 'interval', + interval: 10000, + unit: 'hour', + }, + ], + actions: [], + }), + ).to.throw(BadParameters, '10000 hour is too big for an interval'); + }); + it('should return error, interval not supported', async () => { + const sceneManager = new SceneManager({}, event); + expect(() => + sceneManager.addScene({ + name: 'a-test-scene', + icon: 'bell', + triggers: [ + { + type: EVENTS.TIME.CHANGED, + scheduler_type: 'interval', + interval: 10, + unit: 'not-supported', + }, + ], + actions: [], + }), + ).to.throw(BadParameters, 'not-supported not supported'); + }); +}); diff --git a/server/test/lib/scene/scene.cancelTriggers.test.js b/server/test/lib/scene/scene.cancelTriggers.test.js new file mode 100644 index 0000000000..e620de8078 --- /dev/null +++ b/server/test/lib/scene/scene.cancelTriggers.test.js @@ -0,0 +1,48 @@ +const { expect } = require('chai'); +const EventEmitter = require('events'); +const { EVENTS } = require('../../../utils/constants'); +const SceneManager = require('../../../lib/scene'); + +const event = new EventEmitter(); + +describe('SceneManager.cancelTriggers', () => { + it('should cancel a node-schedule trigger', async () => { + const sceneManager = new SceneManager({}, event); + const scene = await sceneManager.create({ + name: 'a-test-scene', + icon: 'bell', + triggers: [ + { + type: EVENTS.TIME.CHANGED, + scheduler_type: 'custom-time', + date: '2100-01-01', + time: '12:00', + }, + ], + actions: [], + }); + expect(sceneManager.scenes[scene.selector].triggers[0]).to.have.property('nodeScheduleJob'); + sceneManager.cancelTriggers(scene.selector); + expect(sceneManager.scenes[scene.selector].triggers[0]).not.to.have.property('nodeScheduleJob'); + }); + it('should cancel a js interval trigger', async () => { + const sceneManager = new SceneManager({}, event); + const scene = await sceneManager.create({ + name: 'a-test-scene', + icon: 'bell', + triggers: [ + { + type: EVENTS.TIME.CHANGED, + scheduler_type: 'interval', + interval: 1000, + unit: 'minute', + }, + ], + actions: [], + }); + expect(sceneManager.scenes[scene.selector].triggers[0]).not.to.have.property('nodeScheduleJob'); + expect(sceneManager.scenes[scene.selector].triggers[0]).to.have.property('jsInterval'); + sceneManager.cancelTriggers(scene.selector); + expect(sceneManager.scenes[scene.selector].triggers[0]).not.to.have.property('jsInterval'); + }); +}); diff --git a/server/test/lib/scene/scene.checkTrigger.test.js b/server/test/lib/scene/scene.checkTrigger.test.js index 4616a436ba..880f20727d 100644 --- a/server/test/lib/scene/scene.checkTrigger.test.js +++ b/server/test/lib/scene/scene.checkTrigger.test.js @@ -50,6 +50,45 @@ describe('scene.checkTrigger', () => { }); }); }); + it('should execute scene', async () => { + const stateManager = new StateManager(); + const device = { + setValue: fake.resolves(null), + }; + const sceneManager = new SceneManager(stateManager, event, device); + sceneManager.addScene({ + selector: 'my-scene', + actions: [ + [ + { + type: ACTIONS.LIGHT.TURN_ON, + devices: ['light-1'], + }, + ], + ], + triggers: [ + { + type: EVENTS.TIME.CHANGED, + scheduler_type: 'custom-time', + date: '01-01-1990', + time: '12:00', + }, + ], + }); + sceneManager.checkTrigger({ + type: EVENTS.TIME.CHANGED, + }); + return new Promise((resolve, reject) => { + sceneManager.queue.start(() => { + try { + assert.calledOnce(device.setValue); + resolve(); + } catch (e) { + reject(e); + } + }); + }); + }); it('should not execute scene, condition not verified', async () => { const stateManager = new StateManager(); const device = { diff --git a/server/test/lib/scene/scene.create.test.js b/server/test/lib/scene/scene.create.test.js index d044f04a63..6fd58c724c 100644 --- a/server/test/lib/scene/scene.create.test.js +++ b/server/test/lib/scene/scene.create.test.js @@ -11,6 +11,7 @@ describe('SceneManager', () => { const scene = await sceneManager.create({ name: 'My living room', icon: 'bell', + triggers: [], actions: [ [ { @@ -27,6 +28,7 @@ describe('SceneManager', () => { name: 'My living room', icon: 'bell', selector: 'my-custom-selector', + triggers: [], actions: [ [ { @@ -42,6 +44,7 @@ describe('SceneManager', () => { const promise = sceneManager.create({ name: 'My living room', icon: 'bell', + triggers: [], actions: [ { type: ACTIONS.LIGHT.TURN_ON, diff --git a/server/test/lib/scene/scene.destroy.test.js b/server/test/lib/scene/scene.destroy.test.js index 2b4826267a..be168d911a 100644 --- a/server/test/lib/scene/scene.destroy.test.js +++ b/server/test/lib/scene/scene.destroy.test.js @@ -9,6 +9,7 @@ describe('SceneManager', () => { const sceneManager = new SceneManager({}, event); sceneManager.scenes['test-scene'] = { name: 'Test Scene', + triggers: [], }; await sceneManager.destroy('test-scene'); expect(sceneManager.scenes['test-scene']).to.be.undefined; // eslint-disable-line diff --git a/server/test/lib/scene/scene.execute.test.js b/server/test/lib/scene/scene.execute.test.js index 494a5b94c9..56c72e816e 100644 --- a/server/test/lib/scene/scene.execute.test.js +++ b/server/test/lib/scene/scene.execute.test.js @@ -19,6 +19,7 @@ describe('SceneManager', () => { const sceneManager = new SceneManager(stateManager, event, device); const scene = { selector: 'my-scene', + triggers: [], actions: [ [ { diff --git a/server/utils/constants.js b/server/utils/constants.js index 66f8e34e16..8c29a65210 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -88,6 +88,9 @@ const EVENTS = { HUE_CHANGED: 'light.hue-changed', SATURATION_CHANGED: 'light.saturation-changed', }, + TIME: { + CHANGED: 'time.changed', + }, TRIGGERS: { CHECK: 'trigger.check', },