Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions front/src/config/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
7 changes: 7 additions & 0 deletions front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
7 changes: 7 additions & 0 deletions front/src/config/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,90 @@ 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'
};
}

componentDidMount() {
this.getHouses();
}

render({}, { houses }) {
render({}, { houses, offsetMinutesInput }) {
const offset = this.props.trigger.offset || 0;
const offsetDirection = offset === 0 ? 'exact' : offset > 0 ? 'after' : 'before';
return (
<div>
<div class="row">
<div class="col-sm-12">
<SelectSunriseSunset houses={houses} house={this.props.trigger.house} onHouseChange={this.onHouseChange} />
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label class="form-label">
<Text id="editScene.triggersCard.sunriseSunsetTrigger.offsetLabel" />
</label>
<select onChange={this.onOffsetDirectionChange} class="form-control">
<option value="exact" selected={offsetDirection === 'exact'}>
<Text id="editScene.triggersCard.sunriseSunsetTrigger.atExactTime" />
</option>
<option value="before" selected={offsetDirection === 'before'}>
<Text id="editScene.triggersCard.sunriseSunsetTrigger.before" />
</option>
<option value="after" selected={offsetDirection === 'after'}>
<Text id="editScene.triggersCard.sunriseSunsetTrigger.after" />
</option>
</select>
</div>
</div>
</div>
{offsetDirection !== 'exact' && (
<div class="row align-items-center">
<div class="col-6">
<input
type="number"
class="form-control"
min="1"
max="1440"
value={offsetMinutesInput}
onInput={this.onOffsetMinutesChange}
/>
</div>
<div class="col-6">
<Text id="editScene.triggersCard.sunriseSunsetTrigger.minutes" />
</div>
</div>
)}
</div>
);
}
Expand Down
27 changes: 26 additions & 1 deletion server/lib/scene/scene.addScene.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,26 @@

const { BadParameters } = require('../../utils/coreErrors');
const { EVENTS } = require('../../utils/constants');
const logger = require('../../utils/logger');

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,
Expand All @@ -19,16 +36,20 @@
/**
* @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) {
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
Expand Down Expand Up @@ -123,9 +144,13 @@

this.scenes[scene.selector] = scene;
this.brain.addNamedEntity('scene', scene.selector, scene.name);
if (!skipDailyUpdate && (hasSunriseSunsetTrigger(scene) || hadSunriseSunset)) {
this.dailyUpdate().catch((e) => logger.error(e));

Check warning on line 148 in server/lib/scene/scene.addScene.js

View workflow job for this annotation

GitHub Actions / Server test

Prefer await to then()/catch()/finally()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really the behavior we want? Shouldn’t addScene only resolve once dailyUpdate has completed?

I know addScene is currently synchronous, but is there any issue with making it async?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the dailyUpdate call behind an await + addScene async.
You can check impacts in all callers in the commit.
I decided to keep the call from scene.init.js without await (if you have 100 scenes, it can cause some latency, even if dailyUpdate is called once at the end).

}
return scene;
}

module.exports = {
addScene,
hasSunriseSunsetTrigger,
};
96 changes: 47 additions & 49 deletions server/lib/scene/scene.dailyUpdate.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,58 +31,56 @@ 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,
}),
// 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.get('hour')}:${sunriseBase.get('minute')}, in your timezone = ${
this.timezone
}`,
);
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.`);
}

const sunsetJob = this.scheduler.scheduleJob(sunsetTime, () =>
this.event.emit(EVENTS.TRIGGERS.CHECK, {
type: EVENTS.TIME.SUNSET,
house,
}),
logger.info(
`Sunset today is at ${sunsetBase.get('hour')}:${sunsetBase.get('minute')}, in your timezone = ${this.timezone}`,
);

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.`);
}
// 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);
}
});
});

// 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, ${dayjs(time).fromNow()}.`);
this.jobs.push(job);
} else {
logger.info(`${label} (offset ${offset}min): time is in the past, not scheduling for today.`);
}
});
};

scheduleForOffsets(sunriseOffsets, sunriseBase, EVENTS.TIME.SUNRISE, 'Sunrise');
scheduleForOffsets(sunsetOffsets, sunsetBase, EVENTS.TIME.SUNSET, 'Sunset');
}
});
}
Expand Down
6 changes: 6 additions & 0 deletions server/lib/scene/scene.destroy.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const db = require('../../models');
const { NotFoundError } = require('../../utils/coreErrors');
const { hasSunriseSunsetTrigger } = require('./scene.addScene');

/**
* @description Destroy a scene.
Expand Down Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion server/lib/scene/scene.init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 5 additions & 2 deletions server/lib/scene/scene.triggers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) =>
Expand Down
4 changes: 4 additions & 0 deletions server/models/scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}),
);

Expand Down
Loading
Loading