Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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) {
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
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)) {
await this.dailyUpdate();
}
return scene;
}

module.exports = {
addScene,
hasSunriseSunsetTrigger,
};
2 changes: 1 addition & 1 deletion server/lib/scene/scene.create.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
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
2 changes: 1 addition & 1 deletion server/lib/scene/scene.update.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
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