Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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,89 @@ class SunriseSunsetTrigger extends Component {
this.props.updateTriggerProperty(this.props.index, 'house', houseSelector);
};

onOffsetDirectionChange = e => {
const direction = e.target.value;
const currentMinutes = parseInt(this.state.offsetMinutesInput, 10) || 30;
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) {
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"
value={offsetMinutesInput}
onInput={this.onOffsetMinutesChange}
/>
</div>
<div class="col-6">
<Text id="editScene.triggersCard.sunriseSunsetTrigger.minutes" />
</div>
</div>
)}
</div>
);
}
Expand Down
26 changes: 25 additions & 1 deletion server/lib/scene/scene.addScene.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
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 +143,13 @@ function addScene(sceneRaw) {

this.scenes[scene.selector] = scene;
this.brain.addNamedEntity('scene', scene.selector, scene.name);
if (!skipDailyUpdate && (hasSunriseSunsetTrigger(scene) || hadSunriseSunset)) {
this.dailyUpdate();
}
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
1 change: 1 addition & 0 deletions server/models/scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ const triggersSchema = Joi.array().items(
threshold_only: Joi.boolean(),
topic: Joi.string(),
message: Joi.string().allow(''),
offset: Joi.number().integer(),
}),
);

Expand Down
Loading
Loading