diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json
index 6a7d80dff3..3dafd4bfcd 100644
--- a/front/src/config/i18n/de.json
+++ b/front/src/config/i18n/de.json
@@ -2633,6 +2633,13 @@
"warning": {
"houseWithoutCoordinate": "Dein Zuhause hat keine Koordinaten. Dieser Auslöser funktioniert nicht ohne Koordinaten. Bitte gehe zu Einstellungen -> Zuhause und klicke auf die Karte, um die Koordinaten des Zuhauses festzulegen."
},
+ "sunriseSunsetTrigger": {
+ "offsetLabel": "Wann",
+ "atExactTime": "Zur genauen Uhrzeit",
+ "before": "Vor",
+ "after": "Nach",
+ "minutes": "Minuten"
+ },
"userPresence": {
"backAtHomeDescription": "Dies wird ausgelöst, wenn der ausgewählte Benutzer wieder im ausgewählten Zuhause ist.",
"leftHomeDescription": "Dies wird ausgelöst, wenn der ausgewählte Benutzer das ausgewählte Zuhause verlässt.",
diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json
index f2a45b7092..0cfbca9a97 100644
--- a/front/src/config/i18n/en.json
+++ b/front/src/config/i18n/en.json
@@ -2633,6 +2633,13 @@
"warning": {
"houseWithoutCoordinate": "Your house has no coordinates. This trigger cannot work without this data. Please go to Settings / Houses and click on the map to set the house coordinates."
},
+ "sunriseSunsetTrigger": {
+ "offsetLabel": "When",
+ "atExactTime": "At exact time",
+ "before": "Before",
+ "after": "After",
+ "minutes": "minutes"
+ },
"userPresence": {
"backAtHomeDescription": "This will trigger when the selected user is back at the selected home.",
"leftHomeDescription": "This will trigger when the selected user is leaving the selected home.",
diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json
index f6da30046e..ee4b07687d 100644
--- a/front/src/config/i18n/fr.json
+++ b/front/src/config/i18n/fr.json
@@ -2633,6 +2633,13 @@
"warning": {
"houseWithoutCoordinate": "Votre maison n'a pas de coordonnées renseignées. Ce déclencheur ne peut fonctionner sans cette donnée. Veuillez-vous rendre dans Paramètres/Maisons et cliquer sur la carte pour définir les coordonnées de cette maison."
},
+ "sunriseSunsetTrigger": {
+ "offsetLabel": "Quand",
+ "atExactTime": "À l'heure exacte",
+ "before": "Avant",
+ "after": "Après",
+ "minutes": "minutes"
+ },
"userPresence": {
"backAtHomeDescription": "Cette scène s'exécutera lorsque l'utilisateur sélectionné rentrera à la maison sélectionnée.",
"leftHomeDescription": "Cette scène s'exécutera lorsque l'utilisateur sélectionné partira de la maison sélectionnée.",
diff --git a/front/src/routes/scene/edit-scene/triggers/SunriseSunsetTrigger.jsx b/front/src/routes/scene/edit-scene/triggers/SunriseSunsetTrigger.jsx
index 2a93e3da3c..cb8b2750a5 100644
--- a/front/src/routes/scene/edit-scene/triggers/SunriseSunsetTrigger.jsx
+++ b/front/src/routes/scene/edit-scene/triggers/SunriseSunsetTrigger.jsx
@@ -27,10 +27,36 @@ class SunriseSunsetTrigger extends Component {
this.props.updateTriggerProperty(this.props.index, 'house', houseSelector);
};
+ onOffsetDirectionChange = e => {
+ const direction = e.target.value;
+ const currentMinutes = Math.min(parseInt(this.state.offsetMinutesInput, 10) || 30, 1440);
+ if (direction === 'exact') {
+ this.props.updateTriggerProperty(this.props.index, 'offset', 0);
+ } else if (direction === 'before') {
+ this.props.updateTriggerProperty(this.props.index, 'offset', -currentMinutes);
+ } else {
+ this.props.updateTriggerProperty(this.props.index, 'offset', currentMinutes);
+ }
+ };
+
+ onOffsetMinutesChange = e => {
+ const raw = e.target.value;
+ this.setState({ offsetMinutesInput: raw });
+ const minutes = parseInt(raw, 10);
+ if (!minutes || minutes <= 0 || minutes > 1440) {
+ return;
+ }
+ const currentOffset = this.props.trigger.offset || 0;
+ const newOffset = currentOffset < 0 ? -minutes : minutes;
+ this.props.updateTriggerProperty(this.props.index, 'offset', newOffset);
+ };
+
constructor(props) {
super(props);
+ const initialMinutes = Math.abs(props.trigger.offset || 0);
this.state = {
- houses: []
+ houses: [],
+ offsetMinutesInput: initialMinutes > 0 ? String(initialMinutes) : '30'
};
}
@@ -38,7 +64,9 @@ class SunriseSunsetTrigger extends Component {
this.getHouses();
}
- render({}, { houses }) {
+ render({}, { houses, offsetMinutesInput }) {
+ const offset = this.props.trigger.offset || 0;
+ const offsetDirection = offset === 0 ? 'exact' : offset > 0 ? 'after' : 'before';
return (
@@ -46,6 +74,43 @@ class SunriseSunsetTrigger extends Component {
+
+
+
+
+
+
+
+
+ {offsetDirection !== 'exact' && (
+
+ )}
);
}
diff --git a/server/lib/scene/scene.addScene.js b/server/lib/scene/scene.addScene.js
index 36af10f277..94b825419e 100644
--- a/server/lib/scene/scene.addScene.js
+++ b/server/lib/scene/scene.addScene.js
@@ -6,6 +6,22 @@ const { EVENTS } = require('../../utils/constants');
const MAX_VALUE_SET_INTERVAL = 2 ** 31 - 1;
+/**
+ * @description Has sunrise or sunset trigger.
+ * @param {object} scene - Scene object.
+ * @returns {boolean} Return true if the scene has a sunrise or sunset trigger.
+ * @example
+ * hasSunriseSunsetTrigger({
+ * selector: 'test'
+ * });
+ */
+function hasSunriseSunsetTrigger(scene) {
+ if (!scene.triggers) {
+ return false;
+ }
+ return scene.triggers.some((trigger) => trigger.type === EVENTS.TIME.SUNRISE || trigger.type === EVENTS.TIME.SUNSET);
+}
+
const nodeScheduleDaysOfWeek = {
sunday: 0,
monday: 1,
@@ -19,16 +35,20 @@ const nodeScheduleDaysOfWeek = {
/**
* @description Add a scene to the scene manager.
* @param {object} sceneRaw - Scene object from DB.
+ * @param {object} [options] - Options.
+ * @param {boolean} [options.skipDailyUpdate=false] - Skip dailyUpdate call (e.g. During init).
* @returns {object} Return the scene.
* @example
* addScene({
* selector: 'test'
* });
*/
-function addScene(sceneRaw) {
+async function addScene(sceneRaw, { skipDailyUpdate = false } = {}) {
// 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
+ const previousScene = this.scenes[scene.selector];
+ const hadSunriseSunset = previousScene && hasSunriseSunsetTrigger(previousScene);
this.cancelTriggers(scene.selector);
// Foreach triggger, we schedule jobs for triggers that need to be scheduled
// only if the scene is active
@@ -123,9 +143,13 @@ function addScene(sceneRaw) {
this.scenes[scene.selector] = scene;
this.brain.addNamedEntity('scene', scene.selector, scene.name);
+ if (!skipDailyUpdate && (hasSunriseSunsetTrigger(scene) || hadSunriseSunset)) {
+ await this.dailyUpdate();
+ }
return scene;
}
module.exports = {
addScene,
+ hasSunriseSunsetTrigger,
};
diff --git a/server/lib/scene/scene.create.js b/server/lib/scene/scene.create.js
index 90880be9a2..478252a00f 100644
--- a/server/lib/scene/scene.create.js
+++ b/server/lib/scene/scene.create.js
@@ -32,7 +32,7 @@ async function create(scene) {
const plainScene = createdScene.get({ plain: true });
// add scene to live store
- this.addScene(plainScene);
+ await this.addScene(plainScene);
// return created scene
return plainScene;
}
diff --git a/server/lib/scene/scene.dailyUpdate.js b/server/lib/scene/scene.dailyUpdate.js
index 59b2027a8b..e803acfcce 100644
--- a/server/lib/scene/scene.dailyUpdate.js
+++ b/server/lib/scene/scene.dailyUpdate.js
@@ -31,58 +31,54 @@ async function dailyUpdate() {
.tz(this.timezone)
.toDate();
const times = this.sunCalc.getTimes(todayAt12InMyTimeZone, house.latitude, house.longitude);
- // Sunrise time
- const sunriseHour = dayjs(times.sunrise)
- .tz(this.timezone)
- .get('hour');
- const sunriseMinute = dayjs(times.sunrise)
- .tz(this.timezone)
- .get('minute');
- const sunriseTime = dayjs()
- .tz(this.timezone)
- .hour(sunriseHour)
- .minute(sunriseMinute)
- .toDate();
- // Sunset time
- const sunsetHour = dayjs(times.sunset)
- .tz(this.timezone)
- .get('hour');
- const sunsetMinute = dayjs(times.sunset)
- .tz(this.timezone)
- .get('minute');
- const sunsetTime = dayjs()
- .tz(this.timezone)
- .hour(sunsetHour)
- .minute(sunsetMinute)
- .toDate();
- logger.info(`Sunrise today is at ${sunriseHour}:${sunriseMinute} today, in your timezone = ${this.timezone}`);
- logger.info(`Sunset today is at ${sunsetHour}:${sunsetMinute} today, in your timezone = ${this.timezone}`);
- const sunriseJob = this.scheduler.scheduleJob(sunriseTime, () =>
- this.event.emit(EVENTS.TRIGGERS.CHECK, {
- type: EVENTS.TIME.SUNRISE,
- house,
- }),
- );
- if (sunriseJob) {
- logger.info(`Sunrise is scheduled, ${dayjs(sunriseTime).fromNow()}.`);
- this.jobs.push(sunriseJob);
- } else {
- logger.info(`The sun rose this morning. Not scheduling for today.`);
- }
+ // Sunrise and Sunset base times
+ const sunriseBase = dayjs(times.sunrise).tz(this.timezone);
+ const sunsetBase = dayjs(times.sunset).tz(this.timezone);
+ logger.info(`Sunrise today is at ${sunriseBase.format('HH:mm')}, in your timezone = ${this.timezone}`);
+ logger.info(`Sunset today is at ${sunsetBase.format('HH:mm')}, in your timezone = ${this.timezone}`);
+
+ // Collect all distinct offsets for this house from active scene triggers
+ const sunriseOffsets = new Set([0]);
+ const sunsetOffsets = new Set([0]);
+ Object.values(this.scenes).forEach((scene) => {
+ if (!scene.active || !scene.triggers) {
+ return;
+ }
+ scene.triggers.forEach((trigger) => {
+ const offset = Number(trigger.offset) || 0;
+ if (!Number.isInteger(offset) || Math.abs(offset) > 24 * 60) {
+ return;
+ }
+ if (trigger.type === EVENTS.TIME.SUNRISE && trigger.house === house.selector) {
+ sunriseOffsets.add(offset);
+ } else if (trigger.type === EVENTS.TIME.SUNSET && trigger.house === house.selector) {
+ sunsetOffsets.add(offset);
+ }
+ });
+ });
- const sunsetJob = this.scheduler.scheduleJob(sunsetTime, () =>
- this.event.emit(EVENTS.TRIGGERS.CHECK, {
- type: EVENTS.TIME.SUNSET,
- house,
- }),
- );
+ // Schedule one job per distinct (house, type, offset) combination
+ const scheduleForOffsets = (offsets, baseTime, eventType, label) => {
+ offsets.forEach((offset) => {
+ const time = baseTime.add(offset, 'minute').toDate();
+ const job = this.scheduler.scheduleJob(time, () =>
+ this.event.emit(EVENTS.TRIGGERS.CHECK, { type: eventType, house, offset }),
+ );
+ if (job) {
+ logger.info(
+ `${label} (offset ${offset}min) is scheduled at ${dayjs(time)
+ .tz(this.timezone)
+ .format('HH:mm')}, ${dayjs(time).fromNow()}.`,
+ );
+ this.jobs.push(job);
+ } else {
+ logger.info(`${label} (offset ${offset}min): time is in the past, not scheduling for today.`);
+ }
+ });
+ };
- if (sunsetJob) {
- logger.info(`Sunset is scheduled, ${dayjs(sunsetTime).fromNow()}.`);
- this.jobs.push(sunsetJob);
- } else {
- logger.info(`The sun has already set. Not scheduling for today.`);
- }
+ scheduleForOffsets(sunriseOffsets, sunriseBase, EVENTS.TIME.SUNRISE, 'Sunrise');
+ scheduleForOffsets(sunsetOffsets, sunsetBase, EVENTS.TIME.SUNSET, 'Sunset');
}
});
}
diff --git a/server/lib/scene/scene.destroy.js b/server/lib/scene/scene.destroy.js
index cbdbbf97ce..70d5098266 100644
--- a/server/lib/scene/scene.destroy.js
+++ b/server/lib/scene/scene.destroy.js
@@ -1,5 +1,6 @@
const db = require('../../models');
const { NotFoundError } = require('../../utils/coreErrors');
+const { hasSunriseSunsetTrigger } = require('./scene.addScene');
/**
* @description Destroy a scene.
@@ -35,10 +36,15 @@ async function destroy(selector) {
});
await existingScene.destroy();
+ // check if scene had sunrise/sunset triggers before deleting from RAM
+ const hadSunriseSunset = this.scenes[selector] && hasSunriseSunsetTrigger(this.scenes[selector]);
// we cancel triggers linked to the scene
this.cancelTriggers(selector);
// then we delete the scene in RAM
delete this.scenes[selector];
+ if (hadSunriseSunset) {
+ await this.dailyUpdate();
+ }
}
module.exports = {
diff --git a/server/lib/scene/scene.init.js b/server/lib/scene/scene.init.js
index cf5e280421..a4ad26495e 100644
--- a/server/lib/scene/scene.init.js
+++ b/server/lib/scene/scene.init.js
@@ -21,7 +21,7 @@ async function init() {
const plainScene = scene.get({ plain: true });
logger.debug(`Loading scene ${plainScene.name}`);
try {
- this.addScene(plainScene);
+ this.addScene(plainScene, { skipDailyUpdate: true });
this.brain.addNamedEntity('scene', plainScene.selector, plainScene.name);
logger.debug(`Scene loaded with success`);
} catch (e) {
diff --git a/server/lib/scene/scene.triggers.js b/server/lib/scene/scene.triggers.js
index feb1ea0aa3..15a81ad77c 100644
--- a/server/lib/scene/scene.triggers.js
+++ b/server/lib/scene/scene.triggers.js
@@ -4,6 +4,9 @@ const logger = require('../../utils/logger');
const { EVENTS } = require('../../utils/constants');
const { compare } = require('../../utils/compare');
+const matchSunEvent = (self, sceneSelector, event, trigger) =>
+ event.house.selector === trigger.house && (event.offset || 0) === (trigger.offset || 0);
+
const triggersFunc = {
[EVENTS.DEVICE.NEW_STATE]: (self, sceneSelector, event, trigger) => {
// we check that we are talking about the same device feature
@@ -81,8 +84,8 @@ const triggersFunc = {
return false;
},
[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.TIME.SUNRISE]: matchSunEvent,
+ [EVENTS.TIME.SUNSET]: matchSunEvent,
[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) =>
diff --git a/server/lib/scene/scene.update.js b/server/lib/scene/scene.update.js
index 7b64f72fff..c27eaf727d 100644
--- a/server/lib/scene/scene.update.js
+++ b/server/lib/scene/scene.update.js
@@ -54,7 +54,7 @@ async function update(selector, scene) {
this.brain.removeNamedEntity('scene', plainScene.selector, oldName);
}
// add scene to live store
- this.addScene(plainScene);
+ await this.addScene(plainScene);
// return updated scene
return plainScene;
}
diff --git a/server/models/scene.js b/server/models/scene.js
index 2626fb951c..671444457c 100644
--- a/server/models/scene.js
+++ b/server/models/scene.js
@@ -116,6 +116,10 @@ const triggersSchema = Joi.array().items(
threshold_only: Joi.boolean(),
topic: Joi.string(),
message: Joi.string().allow(''),
+ offset: Joi.number()
+ .integer()
+ .min(-1440)
+ .max(1440),
}),
);
diff --git a/server/test/lib/scene/scene.addScene.test.js b/server/test/lib/scene/scene.addScene.test.js
index e5a5a57f7c..ddf2be51e3 100644
--- a/server/test/lib/scene/scene.addScene.test.js
+++ b/server/test/lib/scene/scene.addScene.test.js
@@ -46,7 +46,7 @@ describe('SceneManager.addScene', () => {
it('should NOT add a scene with an invalid trigger', async () => {
try {
- sceneManager.addScene({
+ await sceneManager.addScene({
name: 'a-test-scene',
icon: 'bell',
active: true,
@@ -66,7 +66,7 @@ describe('SceneManager.addScene', () => {
}
});
it('should add a scene with a scheduled trigger, every-month', async () => {
- const scene = sceneManager.addScene({
+ const scene = await sceneManager.addScene({
name: 'a-test-scene',
icon: 'bell',
active: true,
@@ -89,7 +89,7 @@ describe('SceneManager.addScene', () => {
assert.calledOnceWithExactly(event.emit, EVENTS.TRIGGERS.CHECK, trigger);
});
it('should add a scene with a scheduled trigger, every-week', async () => {
- const scene = sceneManager.addScene({
+ const scene = await sceneManager.addScene({
name: 'a-test-scene',
icon: 'bell',
active: true,
@@ -106,7 +106,7 @@ describe('SceneManager.addScene', () => {
expect(sceneManager.scenes[scene.selector].triggers[0]).to.have.property('nodeScheduleJob');
});
it('should add a scene with a scheduled trigger, every-day', async () => {
- const scene = sceneManager.addScene({
+ const scene = await sceneManager.addScene({
name: 'a-test-scene',
icon: 'bell',
active: true,
@@ -125,7 +125,7 @@ describe('SceneManager.addScene', () => {
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({
+ const scene = await sceneManager.addScene({
name: 'a-test-scene',
icon: 'bell',
active: true,
@@ -147,7 +147,7 @@ describe('SceneManager.addScene', () => {
longitude: 50,
});
- const scene = sceneManager.addScene({
+ const scene = await sceneManager.addScene({
name: 'a-test-scene',
icon: 'bell',
active: true,
@@ -165,7 +165,7 @@ describe('SceneManager.addScene', () => {
});
it('should throw an error, interval is too big', async () => {
try {
- sceneManager.addScene({
+ await sceneManager.addScene({
name: 'a-test-scene',
icon: 'bell',
active: true,
@@ -186,7 +186,7 @@ describe('SceneManager.addScene', () => {
});
it('should return error, interval not supported', async () => {
try {
- sceneManager.addScene({
+ await sceneManager.addScene({
name: 'a-test-scene',
icon: 'bell',
active: true,
@@ -206,7 +206,8 @@ describe('SceneManager.addScene', () => {
}
});
it('should add a scene with a scheduled trigger, sunrise', async () => {
- const scene = sceneManager.addScene({
+ sceneManager.dailyUpdate = fake.resolves(null);
+ const scene = await sceneManager.addScene({
name: 'a-test-scene',
icon: 'bell',
active: true,
@@ -219,9 +220,11 @@ describe('SceneManager.addScene', () => {
actions: [],
});
expect(sceneManager.scenes[scene.selector].triggers[0]).to.not.have.property('nodeScheduleJob');
+ assert.calledOnce(sceneManager.dailyUpdate);
});
it('should add a scene with a scheduled trigger, sunset', async () => {
- const scene = sceneManager.addScene({
+ sceneManager.dailyUpdate = fake.resolves(null);
+ const scene = await sceneManager.addScene({
name: 'a-test-scene',
icon: 'bell',
active: true,
@@ -235,9 +238,80 @@ describe('SceneManager.addScene', () => {
actions: [],
});
expect(sceneManager.scenes[scene.selector].triggers[0]).to.not.have.property('nodeScheduleJob');
+ assert.calledOnce(sceneManager.dailyUpdate);
+ });
+ it('should NOT call dailyUpdate when adding a scene without sunrise/sunset triggers', async () => {
+ sceneManager.dailyUpdate = fake.resolves(null);
+ await sceneManager.addScene({
+ name: 'a-test-scene',
+ icon: 'bell',
+ active: true,
+ triggers: [
+ {
+ type: EVENTS.TIME.CHANGED,
+ scheduler_type: 'every-day',
+ time: '12:00',
+ },
+ ],
+ actions: [],
+ });
+ assert.notCalled(sceneManager.dailyUpdate);
+ });
+ it('should NOT call dailyUpdate when skipDailyUpdate option is true, even with a sunrise trigger', async () => {
+ sceneManager.dailyUpdate = fake.resolves(null);
+ await sceneManager.addScene(
+ {
+ name: 'a-test-scene',
+ icon: 'bell',
+ active: true,
+ triggers: [
+ {
+ type: EVENTS.TIME.SUNRISE,
+ house: 'house',
+ },
+ ],
+ actions: [],
+ },
+ { skipDailyUpdate: true },
+ );
+ assert.notCalled(sceneManager.dailyUpdate);
+ });
+ it('should call dailyUpdate when previous scene had sunrise trigger but new one does not', async () => {
+ sceneManager.dailyUpdate = fake.resolves(null);
+ const scene = await sceneManager.addScene({
+ selector: 'my-scene',
+ name: 'a-test-scene',
+ icon: 'bell',
+ active: true,
+ triggers: [
+ {
+ type: EVENTS.TIME.SUNRISE,
+ house: 'house',
+ },
+ ],
+ actions: [],
+ });
+ assert.calledOnce(sceneManager.dailyUpdate);
+ sceneManager.dailyUpdate.resetHistory();
+ // Update the scene to remove the sunrise trigger
+ await sceneManager.addScene({
+ selector: scene.selector,
+ name: 'a-test-scene',
+ icon: 'bell',
+ active: true,
+ triggers: [
+ {
+ type: EVENTS.TIME.CHANGED,
+ scheduler_type: 'every-day',
+ time: '08:00',
+ },
+ ],
+ actions: [],
+ });
+ assert.calledOnce(sceneManager.dailyUpdate);
});
it('should add a scene with a message received trigger', async () => {
- const scene = sceneManager.addScene({
+ const scene = await sceneManager.addScene({
name: 'a-test-scene',
icon: 'bell',
active: true,
diff --git a/server/test/lib/scene/scene.checkTrigger.test.js b/server/test/lib/scene/scene.checkTrigger.test.js
index 7b43695395..64838aa3c6 100644
--- a/server/test/lib/scene/scene.checkTrigger.test.js
+++ b/server/test/lib/scene/scene.checkTrigger.test.js
@@ -58,7 +58,7 @@ describe('scene.checkTrigger', () => {
});
it('should execute scene', async () => {
- const addedScene = sceneManager.addScene({
+ const addedScene = await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -93,80 +93,8 @@ describe('scene.checkTrigger', () => {
});
});
});
- it('should execute scene', async () => {
- const addedScene = sceneManager.addScene({
- selector: 'my-scene',
- active: true,
- actions: [
- [
- {
- type: ACTIONS.LIGHT.TURN_ON,
- devices: ['light-1'],
- },
- ],
- ],
- triggers: [
- {
- type: EVENTS.TIME.SUNRISE,
- house: 'house-1',
- },
- ],
- });
- sceneManager.checkTrigger({
- type: EVENTS.TIME.SUNRISE,
- house: {
- selector: addedScene.triggers[0].house,
- },
- });
- return new Promise((resolve, reject) => {
- sceneManager.queue.start(() => {
- try {
- assert.calledOnce(device.setValue);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- });
- });
- it('should execute scene', async () => {
- sceneManager.addScene({
- selector: 'my-scene',
- active: true,
- actions: [
- [
- {
- type: ACTIONS.LIGHT.TURN_OFF,
- devices: ['light-1'],
- },
- ],
- ],
- triggers: [
- {
- type: EVENTS.TIME.SUNSET,
- house: 'house-1',
- },
- ],
- });
- sceneManager.checkTrigger({
- type: EVENTS.TIME.SUNSET,
- house: {
- selector: 'house-1',
- },
- });
- return new Promise((resolve, reject) => {
- sceneManager.queue.start(() => {
- try {
- assert.calledOnce(device.setValue);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- });
- });
it('should execute scene with empty house trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -201,7 +129,7 @@ describe('scene.checkTrigger', () => {
});
it('should execute scene with no longer empty house trigger', async () => {
- const addedScene = sceneManager.addScene({
+ const addedScene = await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -236,7 +164,7 @@ describe('scene.checkTrigger', () => {
});
it('should execute scene with user back home trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -273,7 +201,7 @@ describe('scene.checkTrigger', () => {
});
it('should execute scene with user left home trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -310,7 +238,7 @@ describe('scene.checkTrigger', () => {
});
it('should execute scene with user entered area trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -347,7 +275,7 @@ describe('scene.checkTrigger', () => {
});
it('should execute scene with user left area trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -384,7 +312,7 @@ describe('scene.checkTrigger', () => {
});
it('should execute scene with system start trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -416,7 +344,7 @@ describe('scene.checkTrigger', () => {
});
});
it('should not execute scene, event not matching', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -462,7 +390,7 @@ describe('scene.checkTrigger', () => {
}).to.throw(Error, 'Trigger type "one-unknown-event" has no checker function.');
});
it('should execute scene, event & key matching', async () => {
- const addedScene = sceneManager.addScene({
+ const addedScene = await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -501,7 +429,7 @@ describe('scene.checkTrigger', () => {
});
});
it('should not execute scene, key not matching', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
diff --git a/server/test/lib/scene/scene.dailyUpdate.test.js b/server/test/lib/scene/scene.dailyUpdate.test.js
index bbee507e8b..a310d1efb6 100644
--- a/server/test/lib/scene/scene.dailyUpdate.test.js
+++ b/server/test/lib/scene/scene.dailyUpdate.test.js
@@ -3,6 +3,7 @@ const sinon = require('sinon');
const proxyquire = require('proxyquire').noCallThru();
const { fake, assert } = sinon;
+const { EVENTS } = require('../../../utils/constants');
const SceneManager = proxyquire('../../../lib/scene', {
suncalc: {
@@ -38,6 +39,7 @@ describe('SceneManager.dailyUpdate', () => {
beforeEach(async () => {
house.get = fake.resolves([
{
+ selector: 'house-1',
latitude: 12,
longitude: 13,
},
@@ -114,4 +116,156 @@ describe('SceneManager.dailyUpdate', () => {
await sceneManager.dailyUpdate();
expect(sceneManager.jobs).to.have.lengthOf(0);
});
+
+ it('should schedule extra job for sunrise when a scene has offset=30', async () => {
+ brain.addNamedEntity = fake.returns(null);
+ await sceneManager.addScene({
+ selector: 'scene-offset',
+ active: true,
+ actions: [],
+ triggers: [
+ {
+ type: EVENTS.TIME.SUNRISE,
+ house: 'house-1',
+ offset: 30,
+ },
+ ],
+ });
+ // addScene auto-triggers dailyUpdate(); flush microtask to let it complete
+ await Promise.resolve();
+ // offset=0 sunrise + offset=30 sunrise + offset=0 sunset = 3 jobs
+ expect(sceneManager.jobs).to.have.lengthOf(3);
+
+ // Trigger all jobs and verify events are emitted with correct offsets
+ const emittedOffsets = [];
+ event.emit = (eventName, payload) => {
+ emittedOffsets.push(payload.offset);
+ };
+ sceneManager.jobs.forEach((job) => {
+ job.callback();
+ });
+ expect(emittedOffsets).to.include(0);
+ expect(emittedOffsets).to.include(30);
+ });
+
+ it('should schedule extra job for sunset when a scene has negative offset=-15', async () => {
+ brain.addNamedEntity = fake.returns(null);
+ await sceneManager.addScene({
+ selector: 'scene-offset-neg',
+ active: true,
+ actions: [],
+ triggers: [
+ {
+ type: EVENTS.TIME.SUNSET,
+ house: 'house-1',
+ offset: -15,
+ },
+ ],
+ });
+ // addScene auto-triggers dailyUpdate(); flush microtask to let it complete
+ await Promise.resolve();
+ // offset=0 sunrise + offset=0 sunset + offset=-15 sunset = 3 jobs
+ expect(sceneManager.jobs).to.have.lengthOf(3);
+
+ const emittedOffsets = [];
+ event.emit = (eventName, payload) => {
+ emittedOffsets.push(payload.offset);
+ };
+ sceneManager.jobs.forEach((job) => {
+ job.callback();
+ });
+ expect(emittedOffsets).to.include(0);
+ expect(emittedOffsets).to.include(-15);
+ });
+
+ it('should not add extra jobs for an inactive scene with a sunrise trigger', async () => {
+ brain.addNamedEntity = fake.returns(null);
+ await sceneManager.addScene({
+ selector: 'scene-inactive',
+ active: false,
+ actions: [],
+ triggers: [{ type: EVENTS.TIME.SUNRISE, house: 'house-1', offset: 30 }],
+ });
+ // addScene auto-triggers dailyUpdate(); flush microtask to let it complete
+ await Promise.resolve();
+ // Inactive scene offsets must be ignored: only offset=0 sunrise + offset=0 sunset = 2 jobs
+ expect(sceneManager.jobs).to.have.lengthOf(2);
+ });
+
+ it('should not add extra jobs for a scene with no triggers', async () => {
+ brain.addNamedEntity = fake.returns(null);
+ await sceneManager.addScene({
+ selector: 'scene-no-triggers',
+ active: true,
+ actions: [],
+ triggers: null,
+ });
+ await sceneManager.dailyUpdate();
+ // Scene without triggers must be ignored: only offset=0 sunrise + offset=0 sunset = 2 jobs
+ expect(sceneManager.jobs).to.have.lengthOf(2);
+ });
+
+ it('should ignore a non-numeric offset (string)', async () => {
+ brain.addNamedEntity = fake.returns(null);
+ await sceneManager.addScene({
+ selector: 'scene-bad-offset',
+ active: true,
+ actions: [],
+ triggers: [{ type: EVENTS.TIME.SUNRISE, house: 'house-1', offset: 'abc' }],
+ });
+ // addScene auto-triggers dailyUpdate(); flush microtask to let it complete
+ await Promise.resolve();
+ // invalid offset must be ignored: only offset=0 sunrise + offset=0 sunset = 2 jobs
+ expect(sceneManager.jobs).to.have.lengthOf(2);
+ });
+
+ it('should ignore an offset exceeding 24h (offset > 1440)', async () => {
+ brain.addNamedEntity = fake.returns(null);
+ await sceneManager.addScene({
+ selector: 'scene-huge-offset',
+ active: true,
+ actions: [],
+ triggers: [{ type: EVENTS.TIME.SUNRISE, house: 'house-1', offset: 1500 }],
+ });
+ // addScene auto-triggers dailyUpdate(); flush microtask to let it complete
+ await Promise.resolve();
+ // out-of-day offset must be ignored: only offset=0 sunrise + offset=0 sunset = 2 jobs
+ expect(sceneManager.jobs).to.have.lengthOf(2);
+ });
+
+ it('should ignore a large negative offset exceeding 24h (offset < -1440)', async () => {
+ brain.addNamedEntity = fake.returns(null);
+ await sceneManager.addScene({
+ selector: 'scene-huge-neg-offset',
+ active: true,
+ actions: [],
+ triggers: [{ type: EVENTS.TIME.SUNSET, house: 'house-1', offset: -1500 }],
+ });
+ // addScene auto-triggers dailyUpdate(); flush microtask to let it complete
+ await Promise.resolve();
+ // out-of-day offset must be ignored: only offset=0 sunrise + offset=0 sunset = 2 jobs
+ expect(sceneManager.jobs).to.have.lengthOf(2);
+ });
+
+ it('should deduplicate offsets when multiple scenes share the same offset', async () => {
+ brain.addNamedEntity = fake.returns(null);
+ await sceneManager.addScene({
+ selector: 'scene-a',
+ active: true,
+ actions: [],
+ triggers: [{ type: EVENTS.TIME.SUNRISE, house: 'house-1', offset: 30 }],
+ });
+ // flush first addScene auto-triggered dailyUpdate
+ await Promise.resolve();
+ await sceneManager.addScene({
+ selector: 'scene-b',
+ active: true,
+ actions: [],
+ triggers: [{ type: EVENTS.TIME.SUNRISE, house: 'house-1', offset: 30 }],
+ });
+ // addScene auto-triggers dailyUpdate(); flush microtask to let it complete
+ await Promise.resolve();
+ // offset=0 sunrise + offset=30 sunrise (deduplicated) + offset=0 sunset = 3 jobs
+ expect(sceneManager.jobs).to.have.lengthOf(3);
+ });
});
diff --git a/server/test/lib/scene/scene.destroy.test.js b/server/test/lib/scene/scene.destroy.test.js
index 2c1042948f..4523d9727a 100644
--- a/server/test/lib/scene/scene.destroy.test.js
+++ b/server/test/lib/scene/scene.destroy.test.js
@@ -2,6 +2,7 @@ const { assert, expect } = require('chai');
const { fake, assert: assertSinon } = require('sinon');
const EventEmitter = require('events');
const SceneManager = require('../../../lib/scene');
+const { EVENTS } = require('../../../utils/constants');
const event = new EventEmitter();
@@ -26,4 +27,27 @@ describe('scene.destroy', () => {
const promise = sceneManager.destroy('not-found-scene');
return assert.isRejected(promise);
});
+ it('should call dailyUpdate when destroying a scene with sunrise trigger', async () => {
+ sceneManager.dailyUpdate = fake.resolves(null);
+ sceneManager.scenes['test-scene'] = {
+ name: 'Test Scene',
+ triggers: [
+ {
+ type: EVENTS.TIME.SUNRISE,
+ house: 'house',
+ },
+ ],
+ };
+ await sceneManager.destroy('test-scene');
+ assertSinon.calledOnce(sceneManager.dailyUpdate);
+ });
+ it('should NOT call dailyUpdate when destroying a scene without sunrise/sunset triggers', async () => {
+ sceneManager.dailyUpdate = fake.resolves(null);
+ sceneManager.scenes['test-scene'] = {
+ name: 'Test Scene',
+ triggers: [],
+ };
+ await sceneManager.destroy('test-scene');
+ assertSinon.notCalled(sceneManager.dailyUpdate);
+ });
});
diff --git a/server/test/lib/scene/scene.execute.test.js b/server/test/lib/scene/scene.execute.test.js
index 6bdd1ca4ac..bbe99d6f98 100644
--- a/server/test/lib/scene/scene.execute.test.js
+++ b/server/test/lib/scene/scene.execute.test.js
@@ -39,7 +39,7 @@ describe('scene.execute', () => {
],
],
};
- sceneManager.addScene(scene);
+ await sceneManager.addScene(scene);
await sceneManager.execute('my-scene');
return new Promise((resolve, reject) => {
sceneManager.queue.start(() => {
@@ -82,7 +82,7 @@ describe('scene.execute', () => {
],
],
};
- sceneManager.addScene(scene);
+ await sceneManager.addScene(scene);
const scope = {};
await sceneManager.execute('my-scene', scope);
return new Promise((resolve, reject) => {
@@ -116,7 +116,7 @@ describe('scene.execute', () => {
],
],
};
- sceneManager.addScene(scene);
+ await sceneManager.addScene(scene);
const scope = {};
await sceneManager.execute('my-scene', scope);
return new Promise((resolve, reject) => {
@@ -155,8 +155,8 @@ describe('scene.execute', () => {
],
],
};
- sceneManager.addScene(scene);
- sceneManager.addScene(secondScene);
+ await sceneManager.addScene(scene);
+ await sceneManager.addScene(secondScene);
await sceneManager.execute('my-scene', scope);
return new Promise((resolve, reject) => {
sceneManager.queue.start(() => {
@@ -203,8 +203,8 @@ describe('scene.execute', () => {
],
],
};
- sceneManager.addScene(scene);
- sceneManager.addScene(secondScene);
+ await sceneManager.addScene(scene);
+ await sceneManager.addScene(secondScene);
await sceneManager.execute('my-scene', scope);
return new Promise((resolve, reject) => {
sceneManager.queue.start(() => {
diff --git a/server/test/lib/scene/scene.init.test.js b/server/test/lib/scene/scene.init.test.js
index 74d69a20e4..919f7b9832 100644
--- a/server/test/lib/scene/scene.init.test.js
+++ b/server/test/lib/scene/scene.init.test.js
@@ -59,6 +59,23 @@ describe('scene.init', () => {
scheduler.scheduleJob.getCall(3).callback();
assert.calledOnceWithExactly(event.emit, 'calendar.check-if-event-is-coming');
});
+ it('should call dailyUpdate only once during init, even with multiple sunrise/sunset scenes', async () => {
+ await db.Scene.create({
+ name: 'sunrise-scene-1',
+ icon: 'activity',
+ triggers: [{ type: 'time.sunrise', house: 'my-house' }],
+ actions: [[]],
+ });
+ await db.Scene.create({
+ name: 'sunrise-scene-2',
+ icon: 'activity',
+ triggers: [{ type: 'time.sunset', house: 'my-house' }],
+ actions: [[]],
+ });
+ sceneManager.dailyUpdate = fake.resolves(null);
+ await sceneManager.init();
+ assert.calledOnce(sceneManager.dailyUpdate);
+ });
it('should init scene with failure but not crash', async () => {
await db.Scene.create({
name: 'broken-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 e8db1371e4..8bc684cd8b 100644
--- a/server/test/lib/scene/triggers/scene.trigger.alarmMode.test.js
+++ b/server/test/lib/scene/triggers/scene.trigger.alarmMode.test.js
@@ -46,7 +46,7 @@ describe('Scene.triggers.alarmMode', () => {
});
it('should execute scene with alarm.arm trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -80,7 +80,7 @@ describe('Scene.triggers.alarmMode', () => {
});
});
it('should execute scene with alarm.arming trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -114,7 +114,7 @@ describe('Scene.triggers.alarmMode', () => {
});
});
it('should execute scene with alarm.disarm trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -148,7 +148,7 @@ describe('Scene.triggers.alarmMode', () => {
});
});
it('should execute scene with alarm.partial-arm trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -182,7 +182,7 @@ describe('Scene.triggers.alarmMode', () => {
});
});
it('should execute scene with alarm.panic trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -216,7 +216,7 @@ describe('Scene.triggers.alarmMode', () => {
});
});
it('should execute scene with alarm.too-many-codes-tests trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -250,7 +250,7 @@ describe('Scene.triggers.alarmMode', () => {
});
});
it('should not execute scene (house not matching)', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
diff --git a/server/test/lib/scene/triggers/scene.trigger.deviceNewState.test.js b/server/test/lib/scene/triggers/scene.trigger.deviceNewState.test.js
index ee909c0fdf..67747ffde3 100644
--- a/server/test/lib/scene/triggers/scene.trigger.deviceNewState.test.js
+++ b/server/test/lib/scene/triggers/scene.trigger.deviceNewState.test.js
@@ -59,7 +59,7 @@ describe('scene.triggers.deviceNewState', () => {
});
it('should execute scene', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -96,7 +96,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should not execute scene, scene not active', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: false,
actions: [
@@ -133,7 +133,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should not execute scene, condition not verified', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -170,7 +170,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should not execute scene, device feature is not the same', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -207,7 +207,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should not execute scene, threshold already passed', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -246,7 +246,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should execute scene, threshold passed for the first time', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -285,7 +285,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should start timer to check later for state and not follow current scene', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -330,7 +330,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should start timer to check now and condition should still be valid on second call', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -372,7 +372,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should start timer to check now and re-send new value still validating the condition', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -420,7 +420,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should start timer to check now and condition should not be valid on second call', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -468,7 +468,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should execute scene with string value equality (text device feature like Shelly Button)', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -506,7 +506,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should not execute scene with string value equality when value does not match', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -544,7 +544,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should execute scene with string value inequality', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -582,7 +582,7 @@ describe('scene.triggers.deviceNewState', () => {
});
});
it('should not execute scene with string value inequality when value matches', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
diff --git a/server/test/lib/scene/triggers/scene.trigger.mqttReceived.test.js b/server/test/lib/scene/triggers/scene.trigger.mqttReceived.test.js
index 300639a704..de991d1362 100644
--- a/server/test/lib/scene/triggers/scene.trigger.mqttReceived.test.js
+++ b/server/test/lib/scene/triggers/scene.trigger.mqttReceived.test.js
@@ -54,7 +54,7 @@ describe('Scene.triggers.mqttReceived', () => {
});
it('should execute scene with message received trigger', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -91,7 +91,7 @@ describe('Scene.triggers.mqttReceived', () => {
});
it('should execute scene with message received trigger with undefined message (match any)', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
@@ -128,7 +128,7 @@ describe('Scene.triggers.mqttReceived', () => {
});
it('should execute scene with message received trigger whit message', async () => {
- sceneManager.addScene({
+ await sceneManager.addScene({
selector: 'my-scene',
active: true,
actions: [
diff --git a/server/test/lib/scene/triggers/scene.trigger.sunriseSunset.test.js b/server/test/lib/scene/triggers/scene.trigger.sunriseSunset.test.js
new file mode 100644
index 0000000000..4d3e664bc4
--- /dev/null
+++ b/server/test/lib/scene/triggers/scene.trigger.sunriseSunset.test.js
@@ -0,0 +1,246 @@
+const sinon = require('sinon');
+
+const { assert, fake } = sinon;
+
+const EventEmitter = require('events');
+const StateManager = require('../../../../lib/state');
+const SceneManager = require('../../../../lib/scene');
+const { ACTIONS, EVENTS } = require('../../../../utils/constants');
+
+const event = new EventEmitter();
+
+describe('Scene.triggers.sunriseSunset', () => {
+ let sceneManager;
+
+ const device = {
+ setValue: fake.resolves(null),
+ };
+
+ const brain = {};
+
+ const service = {
+ getService: fake.returns({
+ device: {
+ subscribe: fake.returns(null),
+ },
+ }),
+ };
+
+ beforeEach(() => {
+ const house = {
+ get: fake.resolves([]),
+ };
+
+ const scheduler = {
+ scheduleJob: (date, callback) => {
+ return {
+ callback,
+ date,
+ cancel: () => {},
+ };
+ },
+ };
+
+ brain.addNamedEntity = fake.returns(null);
+ brain.removeNamedEntity = fake.returns(null);
+
+ const stateManager = new StateManager();
+
+ sceneManager = new SceneManager(stateManager, event, device, {}, {}, house, {}, {}, {}, scheduler, brain, service);
+ });
+
+ afterEach(() => {
+ sinon.reset();
+ });
+
+ it('should execute scene with sunrise trigger', async () => {
+ const addedScene = await sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.TIME.SUNRISE,
+ house: 'house-1',
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.TIME.SUNRISE,
+ house: {
+ selector: addedScene.triggers[0].house,
+ },
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.calledOnce(device.setValue);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+
+ it('should execute scene with sunset trigger', async () => {
+ await sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_OFF,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.TIME.SUNSET,
+ house: 'house-1',
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.TIME.SUNSET,
+ house: {
+ selector: '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, sunrise trigger with offset=30 when event has offset=0', async () => {
+ await sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.TIME.SUNRISE,
+ house: 'house-1',
+ offset: 30,
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.TIME.SUNRISE,
+ house: {
+ selector: 'house-1',
+ },
+ offset: 0,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.notCalled(device.setValue);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+
+ it('should execute scene, sunrise trigger with offset=30 when event has offset=30', async () => {
+ await sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_ON,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.TIME.SUNRISE,
+ house: 'house-1',
+ offset: 30,
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.TIME.SUNRISE,
+ house: {
+ selector: 'house-1',
+ },
+ offset: 30,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.calledOnce(device.setValue);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+
+ it('should not execute scene, sunset trigger with offset=-15 when event has offset=0', async () => {
+ await sceneManager.addScene({
+ selector: 'my-scene',
+ active: true,
+ actions: [
+ [
+ {
+ type: ACTIONS.LIGHT.TURN_OFF,
+ devices: ['light-1'],
+ },
+ ],
+ ],
+ triggers: [
+ {
+ type: EVENTS.TIME.SUNSET,
+ house: 'house-1',
+ offset: -15,
+ },
+ ],
+ });
+ sceneManager.checkTrigger({
+ type: EVENTS.TIME.SUNSET,
+ house: {
+ selector: 'house-1',
+ },
+ offset: 0,
+ });
+ return new Promise((resolve, reject) => {
+ sceneManager.queue.start(() => {
+ try {
+ assert.notCalled(device.setValue);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ });
+});