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 }) => (
);
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 (
+
+
+
+ {this.props.trigger.scheduler_type === 'custom-time' && (
+
+ )}
+ {this.props.trigger.scheduler_type === 'custom-time' && (
+
+ )}
+ {this.props.trigger.scheduler_type === 'interval' && (
+
+ )}
+ {this.props.trigger.scheduler_type === 'interval' && (
+
+ )}
+ {this.props.trigger.scheduler_type === 'every-day' && (
+
+ )}
+ {this.props.trigger.scheduler_type === 'every-week' && (
+
+ )}
+ {this.props.trigger.scheduler_type === 'every-week' && (
+
+ )}
+ {this.props.trigger.scheduler_type === 'every-month' && (
+
+ )}
+ {this.props.trigger.scheduler_type === 'every-month' && (
+
+ )}
+
+
+ );
+ }
+}
+
+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',
},