diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json
index f41db2910e..b77a2eb8d1 100644
--- a/front/src/config/i18n/de.json
+++ b/front/src/config/i18n/de.json
@@ -2055,8 +2055,8 @@
"sunset": "Jeder Sonnenuntergang"
},
"user": {
- "back-home": "Benutzer hat das Zuhause verlassen",
- "left-home": "Benutzer ist zurück zu Hause"
+ "back-home": "Benutzer ist zurück zu Hause",
+ "left-home": "Benutzer hat das Zuhause verlassen"
},
"house": {
"empty": "Das Zuhause ist leer",
@@ -2096,7 +2096,8 @@
"on": "Ein",
"off": "Aus",
"deviceSeen": "Wenn das Gerät erkannt wird",
- "onlyExecuteAtThreshold": "Nur ausführen, wenn der Schwellenwert überschritten wird (und nicht bei jedem vom Gerät gesendeten Wert)"
+ "onlyExecuteAtThreshold": "Nur ausführen, wenn der Schwellenwert überschritten wird (und nicht bei jedem vom Gerät gesendeten Wert)",
+ "activateOrDeactivateForDuration": "Szene ausführen, nachdem die Bedingung für die Dauer gültig war:"
},
"scheduledTrigger": {
"typeLabel": "Typ",
diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json
index 77dc498e7f..f1b2cd6fea 100644
--- a/front/src/config/i18n/en.json
+++ b/front/src/config/i18n/en.json
@@ -2055,8 +2055,8 @@
"sunset": "Every sunset"
},
"user": {
- "back-home": "User left home",
- "left-home": "User back at home"
+ "back-home": "User back at home",
+ "left-home": "User left home"
},
"house": {
"empty": "House is empty",
@@ -2096,7 +2096,8 @@
"on": "On",
"off": "Off",
"deviceSeen": "If the device is detected",
- "onlyExecuteAtThreshold": "Execute only when threshold is passed (and not at every value sent by the device)"
+ "onlyExecuteAtThreshold": "Execute only when threshold is passed (and not at every value sent by the device)",
+ "activateOrDeactivateForDuration": "Run the scene after the condition has been valid for:"
},
"scheduledTrigger": {
"typeLabel": "Type",
diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json
index 9b7f9151e0..bdbb410e41 100644
--- a/front/src/config/i18n/fr.json
+++ b/front/src/config/i18n/fr.json
@@ -2096,7 +2096,8 @@
"on": "On",
"off": "Off",
"deviceSeen": "Si l'appareil est détecté",
- "onlyExecuteAtThreshold": "Exécuter seulement lorsque le seuil est passé ( et non pas à chaque valeur envoyée )"
+ "onlyExecuteAtThreshold": "Exécuter seulement lorsque le seuil est passé ( et non pas à chaque valeur envoyée )",
+ "activateOrDeactivateForDuration": "Exécuter la scène après que la condition ait été valide pendant : "
},
"scheduledTrigger": {
"typeLabel": "Type",
diff --git a/front/src/routes/scene/edit-scene/triggers/DeviceFeatureState.jsx b/front/src/routes/scene/edit-scene/triggers/DeviceFeatureState.jsx
index 2cac11f1c8..3e6474bdd1 100644
--- a/front/src/routes/scene/edit-scene/triggers/DeviceFeatureState.jsx
+++ b/front/src/routes/scene/edit-scene/triggers/DeviceFeatureState.jsx
@@ -1,5 +1,6 @@
import { Component } from 'preact';
import { connect } from 'unistore/preact';
+import { Text, Localizer } from 'preact-i18n';
import { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } from '../../../../../../server/utils/constants';
@@ -24,6 +25,24 @@ class TurnOnLight extends Component {
}
};
+ onForDurationChange = e => {
+ e.preventDefault();
+ if (e.target.value) {
+ this.props.updateTriggerProperty(this.props.index, 'for_duration', Number(e.target.value) * 60 * 1000);
+ } else {
+ this.props.updateTriggerProperty(this.props.index, 'for_duration', '');
+ }
+ };
+
+ enableOrDisableForDuration = e => {
+ e.preventDefault();
+ if (e.target.checked) {
+ this.props.updateTriggerProperty(this.props.index, 'for_duration', 60 * 1000);
+ } else {
+ this.props.updateTriggerProperty(this.props.index, 'for_duration', undefined);
+ }
+ };
+
render(props, { selectedDeviceFeature }) {
let binaryDevice = false;
let presenceDevice = false;
@@ -55,12 +74,55 @@ class TurnOnLight extends Component {
{binaryDevice && }
- {presenceDevice && }
+ {presenceDevice && }
{buttonClickDevice && }
{pilotWireModeDevice && }
{defaultDevice && }
{thresholdDevice && }
+
+ {props.trigger.for_duration !== undefined && (
+
+ )}
);
}
diff --git a/front/src/routes/scene/edit-scene/triggers/device-states/PresenceSensorDeviceState.jsx b/front/src/routes/scene/edit-scene/triggers/device-states/PresenceSensorDeviceState.jsx
index e1a4d63307..8e086069f2 100644
--- a/front/src/routes/scene/edit-scene/triggers/device-states/PresenceSensorDeviceState.jsx
+++ b/front/src/routes/scene/edit-scene/triggers/device-states/PresenceSensorDeviceState.jsx
@@ -8,6 +8,18 @@ class PresenceSensorDeviceState extends Component {
this.props.updateTriggerProperty(this.props.index, 'threshold_only', false);
}
+ componentDidUpdate(prevProps) {
+ if (
+ prevProps.selectedDeviceFeature &&
+ this.props.selectedDeviceFeature &&
+ prevProps.selectedDeviceFeature.selector !== this.props.selectedDeviceFeature.selector
+ ) {
+ this.props.updateTriggerProperty(this.props.index, 'operator', '=');
+ this.props.updateTriggerProperty(this.props.index, 'value', 1);
+ this.props.updateTriggerProperty(this.props.index, 'threshold_only', false);
+ }
+ }
+
render() {
return (
diff --git a/server/lib/gateway/gateway.forwardMessageToOpenAI.js b/server/lib/gateway/gateway.forwardMessageToOpenAI.js
index bcdae2a0f9..29e84052c3 100644
--- a/server/lib/gateway/gateway.forwardMessageToOpenAI.js
+++ b/server/lib/gateway/gateway.forwardMessageToOpenAI.js
@@ -11,7 +11,7 @@ const intentTranslation = {
INFO: 'info.get-info',
};
-const disableOpenAiFirstReply = new Set(['GET_TEMPERATURE', 'GET_HUMIDITY']);
+const disableOpenAiFirstReply = new Set(['GET_TEMPERATURE', 'GET_HUMIDITY', 'NO_RESPONSE']);
/**
* @public
diff --git a/server/lib/scene/index.js b/server/lib/scene/index.js
index 1e1f26298d..eb141e567c 100644
--- a/server/lib/scene/index.js
+++ b/server/lib/scene/index.js
@@ -57,6 +57,7 @@ const SceneManager = function SceneManager(
this.sunCalc = sunCalc;
this.scheduler = scheduler;
this.jobs = [];
+ this.checkTriggersDurationTimer = new Map();
this.event.on(EVENTS.TRIGGERS.CHECK, eventFunctionWrapper(this.checkTrigger.bind(this)));
this.event.on(EVENTS.ACTION.TRIGGERED, eventFunctionWrapper(this.executeSingleAction.bind(this)));
// on timezone change, reload all scenes
diff --git a/server/lib/scene/scene.checkTrigger.js b/server/lib/scene/scene.checkTrigger.js
index 8143e760e8..3d93c25b49 100644
--- a/server/lib/scene/scene.checkTrigger.js
+++ b/server/lib/scene/scene.checkTrigger.js
@@ -30,7 +30,7 @@ function checkTrigger(event) {
if (event.type === trigger.type) {
logger.debug(`Trigger ${trigger.type} is matching with event`);
// then we check the condition is verified
- const conditionVerified = triggersFunc[event.type](event, trigger);
+ const conditionVerified = triggersFunc[event.type](this, sceneSelector, event, trigger);
logger.debug(`Trigger ${trigger.type}, conditionVerified = ${conditionVerified}...`);
// if yes, we execute the scene
diff --git a/server/lib/scene/scene.triggers.js b/server/lib/scene/scene.triggers.js
index 6761088484..a557459d61 100644
--- a/server/lib/scene/scene.triggers.js
+++ b/server/lib/scene/scene.triggers.js
@@ -1,38 +1,106 @@
+const cloneDeep = require('lodash.clonedeep');
+
+const logger = require('../../utils/logger');
const { EVENTS } = require('../../utils/constants');
const { compare } = require('../../utils/compare');
const triggersFunc = {
- [EVENTS.DEVICE.NEW_STATE]: (event, trigger) => {
- // we check that we are talking about the same event
+ [EVENTS.DEVICE.NEW_STATE]: (self, sceneSelector, event, trigger) => {
+ // we check that we are talking about the same device feature
if (event.device_feature !== trigger.device_feature) {
return false;
}
+
+ // We verify if both old value and new value validate the rule
const newValueValidateRule = compare(trigger.operator, event.last_value, trigger.value);
- // if the trigger is only a threshold_only, we only validate the trigger is the rule has been validated
- // and was not validated with the previous value
- if (trigger.threshold_only === true && !Number.isNaN(event.previous_value)) {
- const previousValueValidateRule = compare(trigger.operator, event.previous_value, trigger.value);
- return newValueValidateRule && !previousValueValidateRule;
+ const previousValueValidateRule = compare(trigger.operator, event.previous_value, trigger.value);
+
+ const triggerDurationKey = `device.new-state.${sceneSelector}.${trigger.device_feature}:${trigger.operator}:${trigger.value}`;
+
+ // If the previous value was validating the rule, and the new value is not
+ // We clear any timeout for this trigger
+ if (previousValueValidateRule && !newValueValidateRule && self.checkTriggersDurationTimer.get(triggerDurationKey)) {
+ logger.info(
+ `Cancelling timer on trigger for device_feature ${trigger.device_feature}, because condition is no longer valid`,
+ );
+ clearTimeout(self.checkTriggersDurationTimer.get(triggerDurationKey));
+ self.checkTriggersDurationTimer.delete(triggerDurationKey);
+ }
+
+ if (trigger.for_duration === undefined) {
+ // If the trigger is only a threshold_only, we only validate the trigger is the rule has been validated
+ // and was not validated with the previous value
+ if (trigger.threshold_only === true && !Number.isNaN(event.previous_value)) {
+ return newValueValidateRule && !previousValueValidateRule;
+ }
+
+ return newValueValidateRule;
}
- return newValueValidateRule;
+
+ // If the "for_duration_finished" is here, it means we are
+ // checking the state after the timeout
+ if (event.for_duration_finished && triggerDurationKey === event.trigger_duration_key) {
+ logger.info(`Scene trigger device.new-state: Timer for sensor ${trigger.device_feature} has finished.`);
+ clearTimeout(self.checkTriggersDurationTimer.get(triggerDurationKey));
+ self.checkTriggersDurationTimer.delete(triggerDurationKey);
+ return newValueValidateRule;
+ }
+
+ const isValidatedIfThresholdOnly =
+ trigger.threshold_only && !Number.isNaN(event.previous_value)
+ ? newValueValidateRule && !previousValueValidateRule
+ : true;
+
+ if (newValueValidateRule && isValidatedIfThresholdOnly) {
+ // If the timeout already exist, don't re-create it
+ const timeoutAlreadyExist = self.checkTriggersDurationTimer.get(triggerDurationKey);
+ if (timeoutAlreadyExist) {
+ logger.info(`Timer for "${trigger.device_feature}" already exist, not re-creating.`);
+ return false;
+ }
+ logger.info(
+ `Scheduling timer to check for device_feature "${trigger.device_feature}" state in ${trigger.for_duration}ms`,
+ );
+ // Create a timeout
+ const timeoutId = setTimeout(() => {
+ const lastValue = self.stateManager.get('deviceFeature', trigger.device_feature).last_value;
+ self.event.emit(EVENTS.TRIGGERS.CHECK, {
+ ...cloneDeep(event),
+ previous_value: event.last_value,
+ last_value: lastValue,
+ for_duration_finished: true,
+ trigger_duration_key: triggerDurationKey,
+ });
+ }, trigger.for_duration);
+ // Save the timeoutId in case we need to cancel it later
+ self.checkTriggersDurationTimer.set(triggerDurationKey, timeoutId);
+ // Return false, as we'll check this only in the future
+ return false;
+ }
+
+ return false;
},
- [EVENTS.TIME.CHANGED]: (event, trigger) => event.key === trigger.key,
- [EVENTS.TIME.SUNRISE]: (event, trigger) => event.house.selector === trigger.house,
- [EVENTS.TIME.SUNSET]: (event, trigger) => event.house.selector === trigger.house,
- [EVENTS.USER_PRESENCE.BACK_HOME]: (event, trigger) => event.house === trigger.house && event.user === trigger.user,
- [EVENTS.USER_PRESENCE.LEFT_HOME]: (event, trigger) => event.house === trigger.house && event.user === trigger.user,
- [EVENTS.HOUSE.EMPTY]: (event, trigger) => event.house === trigger.house,
- [EVENTS.HOUSE.NO_LONGER_EMPTY]: (event, trigger) => event.house === trigger.house,
- [EVENTS.AREA.USER_ENTERED]: (event, trigger) => event.user === trigger.user && event.area === trigger.area,
- [EVENTS.AREA.USER_LEFT]: (event, trigger) => event.user === trigger.user && event.area === trigger.area,
- [EVENTS.ALARM.ARM]: (event, trigger) => event.house === trigger.house,
- [EVENTS.ALARM.ARMING]: (event, trigger) => event.house === trigger.house,
- [EVENTS.ALARM.DISARM]: (event, trigger) => event.house === trigger.house,
- [EVENTS.ALARM.PARTIAL_ARM]: (event, trigger) => event.house === trigger.house,
- [EVENTS.ALARM.PANIC]: (event, trigger) => event.house === trigger.house,
- [EVENTS.ALARM.TOO_MANY_CODES_TESTS]: (event, trigger) => event.house === trigger.house,
+ [EVENTS.TIME.CHANGED]: (self, sceneSelector, event, trigger) => event.key === trigger.key,
+ [EVENTS.TIME.SUNRISE]: (self, sceneSelector, event, trigger) => event.house.selector === trigger.house,
+ [EVENTS.TIME.SUNSET]: (self, sceneSelector, event, trigger) => event.house.selector === trigger.house,
+ [EVENTS.USER_PRESENCE.BACK_HOME]: (self, sceneSelector, event, trigger) =>
+ event.house === trigger.house && event.user === trigger.user,
+ [EVENTS.USER_PRESENCE.LEFT_HOME]: (self, sceneSelector, event, trigger) =>
+ event.house === trigger.house && event.user === trigger.user,
+ [EVENTS.HOUSE.EMPTY]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
+ [EVENTS.HOUSE.NO_LONGER_EMPTY]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
+ [EVENTS.AREA.USER_ENTERED]: (self, sceneSelector, event, trigger) =>
+ event.user === trigger.user && event.area === trigger.area,
+ [EVENTS.AREA.USER_LEFT]: (self, sceneSelector, event, trigger) =>
+ event.user === trigger.user && event.area === trigger.area,
+ [EVENTS.ALARM.ARM]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
+ [EVENTS.ALARM.ARMING]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
+ [EVENTS.ALARM.DISARM]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
+ [EVENTS.ALARM.PARTIAL_ARM]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
+ [EVENTS.ALARM.PANIC]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
+ [EVENTS.ALARM.TOO_MANY_CODES_TESTS]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
[EVENTS.SYSTEM.START]: () => true,
- [EVENTS.MQTT.RECEIVED]: (event, trigger) =>
+ [EVENTS.MQTT.RECEIVED]: (self, sceneSelector, event, trigger) =>
event.topic === trigger.topic && (trigger.message === '' || trigger.message === event.message),
};
diff --git a/server/models/scene.js b/server/models/scene.js
index 8143ab3097..7c91a90c7b 100644
--- a/server/models/scene.js
+++ b/server/models/scene.js
@@ -99,6 +99,7 @@ const triggersSchema = Joi.array().items(
time: Joi.string().regex(/^([0-9]{2}):([0-9]{2})$/),
interval: Joi.number(),
unit: Joi.string(),
+ for_duration: Joi.number(),
days_of_the_week: Joi.array().items(
Joi.string().valid('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'),
),
diff --git a/server/test/lib/gateway/gateway.forwardMessageToOpenAI.test.js b/server/test/lib/gateway/gateway.forwardMessageToOpenAI.test.js
index e6913af973..8eeca35d4e 100644
--- a/server/test/lib/gateway/gateway.forwardMessageToOpenAI.test.js
+++ b/server/test/lib/gateway/gateway.forwardMessageToOpenAI.test.js
@@ -205,6 +205,18 @@ describe('gateway.forwardMessageToOpenAI', () => {
intent: 'temperature-sensor.get-in-room',
});
});
+ it('should do nothing, no-response was sent', async () => {
+ gateway.gladysGatewayClient.openAIAsk = fake.resolves({
+ type: 'NO_RESPONSE',
+ answer: '',
+ room: '',
+ });
+ const classification = await gateway.forwardMessageToOpenAI({ message, previousQuestions, context });
+ expect(classification).to.deep.equal({
+ intent: undefined,
+ });
+ assert.notCalled(messageManager.reply);
+ });
it('should start scene from OpenAI', async () => {
gateway.gladysGatewayClient.openAIAsk = fake.resolves({
type: 'SCENE_START',
diff --git a/server/test/lib/scene/scene.checkTrigger.test.js b/server/test/lib/scene/scene.checkTrigger.test.js
index 4ab3be0d81..7b43695395 100644
--- a/server/test/lib/scene/scene.checkTrigger.test.js
+++ b/server/test/lib/scene/scene.checkTrigger.test.js
@@ -57,80 +57,6 @@ describe('scene.checkTrigger', () => {
sinon.reset();
});
- it('should execute scene', async () => {
- sceneManager.addScene({
- selector: 'my-scene',
- active: true,
- actions: [
- [
- {
- type: ACTIONS.LIGHT.TURN_ON,
- devices: ['light-1'],
- },
- ],
- ],
- triggers: [
- {
- type: EVENTS.DEVICE.NEW_STATE,
- device_feature: 'light-1',
- value: 12,
- operator: '=',
- },
- ],
- });
- sceneManager.checkTrigger({
- type: EVENTS.DEVICE.NEW_STATE,
- device_feature: 'light-1',
- last_value: 12,
- });
- return new Promise((resolve, reject) => {
- sceneManager.queue.start(() => {
- try {
- assert.calledOnce(device.setValue);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- });
- });
- it('should not execute scene, scene not active', async () => {
- sceneManager.addScene({
- selector: 'my-scene',
- active: false,
- actions: [
- [
- {
- type: ACTIONS.LIGHT.TURN_ON,
- devices: ['light-1'],
- },
- ],
- ],
- triggers: [
- {
- type: EVENTS.DEVICE.NEW_STATE,
- device_feature: 'light-1',
- value: 12,
- operator: '=',
- },
- ],
- });
- sceneManager.checkTrigger({
- type: EVENTS.DEVICE.NEW_STATE,
- device_feature: 'light-1',
- last_value: 12,
- });
- return new Promise((resolve, reject) => {
- sceneManager.queue.start(() => {
- try {
- assert.notCalled(device.setValue);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- });
- });
it('should execute scene', async () => {
const addedScene = sceneManager.addScene({
selector: 'my-scene',
@@ -489,122 +415,6 @@ describe('scene.checkTrigger', () => {
});
});
});
-
- it('should not execute scene, condition not verified', async () => {
- sceneManager.addScene({
- selector: 'my-scene',
- active: true,
- actions: [
- [
- {
- type: ACTIONS.LIGHT.TURN_ON,
- devices: ['light-1'],
- },
- ],
- ],
- triggers: [
- {
- type: EVENTS.DEVICE.NEW_STATE,
- device_feature: 'light-1',
- value: 12,
- operator: '=',
- },
- ],
- });
- sceneManager.checkTrigger({
- type: EVENTS.DEVICE.NEW_STATE,
- device_feature: 'light-1',
- last_value: 14,
- });
- return new Promise((resolve, reject) => {
- sceneManager.queue.start(() => {
- try {
- assert.notCalled(device.setValue);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- });
- });
- it('should not execute scene, threshold already passed', async () => {
- sceneManager.addScene({
- selector: 'my-scene',
- active: true,
- actions: [
- [
- {
- type: ACTIONS.LIGHT.TURN_ON,
- devices: ['light-1'],
- },
- ],
- ],
- triggers: [
- {
- type: EVENTS.DEVICE.NEW_STATE,
- device_feature: 'light-1',
- value: 12,
- operator: '>',
- threshold_only: true,
- },
- ],
- });
- sceneManager.checkTrigger({
- type: EVENTS.DEVICE.NEW_STATE,
- device_feature: 'light-1',
- previous_value: 14,
- last_value: 14,
- });
- return new Promise((resolve, reject) => {
- sceneManager.queue.start(() => {
- try {
- assert.notCalled(device.setValue);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- });
- });
- it('should execute scene, threshold passed for the first time', async () => {
- sceneManager.addScene({
- selector: 'my-scene',
- active: true,
- actions: [
- [
- {
- type: ACTIONS.LIGHT.TURN_ON,
- devices: ['light-1'],
- },
- ],
- ],
- triggers: [
- {
- type: EVENTS.DEVICE.NEW_STATE,
- device_feature: 'light-1',
- value: 12,
- operator: '>',
- threshold_only: true,
- },
- ],
- });
- sceneManager.checkTrigger({
- type: EVENTS.DEVICE.NEW_STATE,
- device_feature: 'light-1',
- previous_value: 11,
- last_value: 14,
- });
- return new Promise((resolve, reject) => {
- sceneManager.queue.start(() => {
- try {
- assert.calledOnce(device.setValue);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- });
- });
it('should not execute scene, event not matching', async () => {
sceneManager.addScene({
selector: 'my-scene',
diff --git a/server/test/lib/scene/triggers/scene.trigger.alarmMode.test.js b/server/test/lib/scene/triggers/scene.trigger.alarmMode.test.js
index 283bdf61f7..e8db1371e4 100644
--- a/server/test/lib/scene/triggers/scene.trigger.alarmMode.test.js
+++ b/server/test/lib/scene/triggers/scene.trigger.alarmMode.test.js
@@ -215,6 +215,40 @@ describe('Scene.triggers.alarmMode', () => {
});
});
});
+ it('should execute scene with alarm.too-many-codes-tests trigger', async () => {
+ sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_OFF,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.ALARM.TOO_MANY_CODES_TESTS,
+ house: 'house-1',
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.ALARM.TOO_MANY_CODES_TESTS,
+ house: 'house-1',
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.calledOnce(device.setValue);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
it('should not execute scene (house not matching)', async () => {
sceneManager.addScene({
selector: 'my-scene',
diff --git a/server/test/lib/scene/triggers/scene.trigger.deviceNewState.test.js b/server/test/lib/scene/triggers/scene.trigger.deviceNewState.test.js
new file mode 100644
index 0000000000..374661573f
--- /dev/null
+++ b/server/test/lib/scene/triggers/scene.trigger.deviceNewState.test.js
@@ -0,0 +1,470 @@
+const sinon = require('sinon');
+const { expect } = require('chai');
+const EventEmitter = require('events');
+const Promise = require('bluebird');
+
+const { assert, fake } = sinon;
+
+const { EVENTS, ACTIONS } = require('../../../../utils/constants');
+const SceneManager = require('../../../../lib/scene');
+const StateManager = require('../../../../lib/state');
+
+const event = new EventEmitter();
+
+describe('scene.triggers.deviceNewState', () => {
+ let sceneManager;
+ let device;
+
+ const brain = {};
+
+ const service = {
+ getService: fake.returns({
+ device: {
+ subscribe: fake.returns(null),
+ },
+ }),
+ };
+
+ beforeEach(() => {
+ const house = {
+ get: fake.resolves([]),
+ };
+
+ device = {
+ setValue: fake.resolves(null),
+ };
+
+ const scheduler = {
+ scheduleJob: (date, callback) => {
+ return {
+ callback,
+ date,
+ cancel: () => {},
+ };
+ },
+ };
+
+ brain.addNamedEntity = fake.returns(null);
+ brain.removeNamedEntity = fake.returns(null);
+
+ const stateManager = new StateManager();
+ stateManager.setState('deviceFeature', 'light-1', {
+ last_value: 14,
+ });
+ sceneManager = new SceneManager(stateManager, event, device, {}, {}, house, {}, {}, {}, scheduler, brain, service);
+ });
+
+ afterEach(() => {
+ sinon.reset();
+ });
+
+ it('should execute scene', async () => {
+ sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ value: 12,
+ operator: '=',
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ last_value: 12,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.calledOnce(device.setValue);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+ it('should not execute scene, scene not active', async () => {
+ sceneManager.addScene({
+ selector: 'my-scene',
+ active: false,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ value: 12,
+ operator: '=',
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ last_value: 12,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.notCalled(device.setValue);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+ it('should not execute scene, condition not verified', async () => {
+ sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ value: 12,
+ operator: '=',
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ last_value: 14,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.notCalled(device.setValue);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+ it('should not execute scene, device feature is not the same', async () => {
+ sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ value: 12,
+ operator: '=',
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-2',
+ last_value: 14,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.notCalled(device.setValue);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+ it('should not execute scene, threshold already passed', async () => {
+ sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ value: 12,
+ operator: '>',
+ threshold_only: true,
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ previous_value: 14,
+ last_value: 14,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.notCalled(device.setValue);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+ it('should execute scene, threshold passed for the first time', async () => {
+ sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ value: 12,
+ operator: '>',
+ threshold_only: true,
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ previous_value: 11,
+ last_value: 14,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.calledOnce(device.setValue);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+ it('should start timer to check later for state and not follow current scene', async () => {
+ sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ value: 12,
+ operator: '>',
+ threshold_only: true,
+ for_duration: 10 * 60 * 1000,
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ previous_value: 11,
+ last_value: 14,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.notCalled(device.setValue);
+ expect(sceneManager.checkTriggersDurationTimer.size).to.equal(1);
+ sceneManager.checkTriggersDurationTimer.forEach((value, timeoutKey) => {
+ expect(timeoutKey).to.equal('device.new-state.my-scene.light-1:>:12');
+ clearTimeout(value);
+ });
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+ it('should start timer to check now and condition should still be valid on second call', async () => {
+ sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ value: 12,
+ operator: '>',
+ threshold_only: true,
+ for_duration: 0, // now
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ previous_value: 11,
+ last_value: 14,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(async () => {
+ try {
+ await Promise.delay(5);
+ assert.calledOnce(device.setValue);
+ expect(sceneManager.checkTriggersDurationTimer.size).to.equal(0);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+ it('should start timer to check now and re-send new value still validating the condition', async () => {
+ sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ value: 12,
+ operator: '>',
+ threshold_only: false,
+ for_duration: 5,
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ previous_value: 11,
+ last_value: 14,
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ previous_value: 14,
+ last_value: 14,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(async () => {
+ try {
+ await Promise.delay(10);
+ assert.calledOnce(device.setValue);
+ expect(sceneManager.checkTriggersDurationTimer.size).to.equal(0);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+ it('should start timer to check now and condition should not be valid on second call', async () => {
+ sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ value: 12,
+ operator: '>',
+ threshold_only: true,
+ for_duration: 10, // In 10ms
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ previous_value: 11,
+ last_value: 14,
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.DEVICE.NEW_STATE,
+ device_feature: 'light-1',
+ previous_value: 14,
+ last_value: 5,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(async () => {
+ try {
+ await Promise.delay(5);
+ assert.notCalled(device.setValue);
+ expect(sceneManager.checkTriggersDurationTimer.size).to.equal(0);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+});