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
12 changes: 11 additions & 1 deletion front/src/config/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -3479,7 +3479,8 @@
"time": {
"changed": "Zeitabhängiger Auslöser",
"sunrise": "Jeder Sonnenaufgang",
"sunset": "Jeder Sonnenuntergang"
"sunset": "Jeder Sonnenuntergang",
"sun-position": "Sonnenstand"
},
"user": {
"back-home": "Benutzer ist zurück zu Hause",
Expand Down Expand Up @@ -3575,6 +3576,15 @@
"after": "Nach",
"minutes": "Minuten"
},
"sunPosition": {
"description": "Dies wird ausgelöst, wenn die Sonne die konfigurierte Position erreicht, zum Beispiel um die Rollläden der von der Sonne beschienenen Fassade zu öffnen oder zu schließen. Die Position wird jede Minute geprüft, und die Szene wird einmal gestartet, wenn die Sonne den konfigurierten Bereich erreicht.",
"houseLabel": "Zuhause",
"noCondition": "Keine Bedingung",
"altitudeLabel": "Sonnenhöhe",
"altitudeHelp": "Höhenwinkel der Sonne über dem Horizont, in Grad: 0° bei Sonnenaufgang und Sonnenuntergang, 90° im Zenit, nachts negativ.",
"azimuthLabel": "Sonnenazimut",
"azimuthHelp": "Himmelsrichtung der Sonne, in Grad ab Norden: 0° = Norden, 90° = Osten, 180° = Süden, 270° = Westen. Um die wahre Mitternacht herum steht die Sonne genau im Norden, unter dem Horizont: Kombinieren Sie den Azimut mit einer Bedingung für die Sonnenhöhe (zum Beispiel Höhe > 0), wenn die Szene nur tagsüber starten soll. Eine einzelne Bedingung kann keinen Bereich beschreiben, der über Norden hinweggeht, zum Beispiel von 350° bis 10°."
},
"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
12 changes: 11 additions & 1 deletion front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3479,7 +3479,8 @@
"time": {
"changed": "Scheduled trigger",
"sunrise": "Every sunrise",
"sunset": "Every sunset"
"sunset": "Every sunset",
"sun-position": "Sun position"
},
"user": {
"back-home": "User back at home",
Expand Down Expand Up @@ -3575,6 +3576,15 @@
"after": "After",
"minutes": "minutes"
},
"sunPosition": {
"description": "This will trigger when the sun reaches the position you configured, for example to open or close the shutters of the facade the sun is hitting. The position is checked every minute, and the scene is started once, when the sun enters the configured area.",
"houseLabel": "House",
"noCondition": "No condition",
"altitudeLabel": "Sun altitude",
"altitudeHelp": "Elevation angle of the sun above the horizon, in degrees: 0° at sunrise and sunset, 90° at the zenith, negative at night.",
"azimuthLabel": "Sun azimuth",
"azimuthHelp": "Compass direction of the sun, in degrees from North: 0° = North, 90° = East, 180° = South, 270° = West. Around solar midnight the sun is due North, below the horizon: combine the azimuth with an altitude condition (for example altitude > 0) if the scene must only start during the day. A single condition cannot describe an area crossing North, for example from 350° to 10°."
},
"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
12 changes: 11 additions & 1 deletion front/src/config/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -3479,7 +3479,8 @@
"time": {
"changed": "Déclenchement programmé",
"sunrise": "A chaque lever de soleil",
"sunset": "A chaque coucher de soleil"
"sunset": "A chaque coucher de soleil",
"sun-position": "Position du soleil"
},
"user": {
"back-home": "Retour à la maison",
Expand Down Expand Up @@ -3575,6 +3576,15 @@
"after": "Après",
"minutes": "minutes"
},
"sunPosition": {
"description": "Cette scène se déclenchera lorsque le soleil atteindra la position configurée, par exemple pour ouvrir ou fermer les volets de la façade que le soleil éclaire. La position est vérifiée chaque minute, et la scène est démarrée une fois, lorsque le soleil entre dans la zone configurée.",
"houseLabel": "Maison",
"noCondition": "Aucune condition",
"altitudeLabel": "Altitude du soleil",
"altitudeHelp": "Hauteur du soleil au-dessus de l'horizon, en degrés : 0° au lever et au coucher du soleil, 90° au zénith, négative la nuit.",
"azimuthLabel": "Azimut du soleil",
"azimuthHelp": "Direction du soleil à la boussole, en degrés depuis le Nord : 0° = Nord, 90° = Est, 180° = Sud, 270° = Ouest. Autour du minuit solaire, le soleil est plein Nord, sous l'horizon : combinez l'azimut avec une condition sur l'altitude (par exemple altitude > 0) si la scène ne doit démarrer que le jour. Une seule condition ne peut pas décrire une zone qui traverse le Nord, par exemple de 350° à 10°."
},
"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
9 changes: 9 additions & 0 deletions front/src/routes/scene/edit-scene/TriggerCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import DeviceFeatureState from './triggers/DeviceFeatureState';
import ScheduledTrigger from './triggers/ScheduledTrigger';
import ChooseTriggerType from './triggers/ChooseTriggerTypeCard';
import SunriseSunsetTrigger from './triggers/SunriseSunsetTrigger';
import SunPositionTrigger from './triggers/SunPositionTrigger';
import UserPresenceTrigger from './triggers/UserPresenceTrigger';
import HouseEmptyOrNot from './triggers/HouseEmptyOrNot';
import UserEnteredOrLeftArea from './triggers/UserEnteredOrLeftArea';
Expand All @@ -21,6 +22,7 @@ const TRIGGER_ICON = {
[EVENTS.TIME.CHANGED]: 'fe-watch',
[EVENTS.TIME.SUNSET]: 'fe-sunset',
[EVENTS.TIME.SUNRISE]: 'fe-sunrise',
[EVENTS.TIME.SUN_POSITION]: 'fe-sun',
[EVENTS.USER_PRESENCE.BACK_HOME]: 'fe-home',
[EVENTS.USER_PRESENCE.LEFT_HOME]: 'fe-home',
[EVENTS.HOUSE.NO_LONGER_EMPTY]: 'fe-home',
Expand Down Expand Up @@ -107,6 +109,13 @@ const TriggerCard = ({ children, ...props }) => (
trigger={props.trigger}
/>
)}
{props.trigger.type === EVENTS.TIME.SUN_POSITION && (
<SunPositionTrigger
updateTriggerProperty={props.updateTriggerProperty}
index={props.index}
trigger={props.trigger}
/>
)}
{props.trigger.type === EVENTS.HOUSE.EMPTY && (
<HouseEmptyOrNot
updateTriggerProperty={props.updateTriggerProperty}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const TRIGGER_LIST = [
EVENTS.TIME.CHANGED,
EVENTS.TIME.SUNRISE,
EVENTS.TIME.SUNSET,
EVENTS.TIME.SUN_POSITION,
EVENTS.USER_PRESENCE.BACK_HOME,
EVENTS.USER_PRESENCE.LEFT_HOME,
EVENTS.HOUSE.EMPTY,
Expand Down
198 changes: 198 additions & 0 deletions front/src/routes/scene/edit-scene/triggers/SunPositionTrigger.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { Component } from 'preact';
import { connect } from 'unistore/preact';
import { Text } from 'preact-i18n';

import { RequestStatus } from '../../../../utils/consts';

const SUN_POSITION_OPERATORS = ['>', '<', '='];

class SunPositionTrigger extends Component {
getHouses = async () => {
this.setState({
SceneGetHouses: RequestStatus.Getting
});
try {
const houses = await this.props.httpClient.get('/api/v1/house');
this.setState({
houses,
SceneGetHouses: RequestStatus.Success
});
} catch (e) {
this.setState({
SceneGetHouses: RequestStatus.Error
});
}
};

onHouseChange = e => {
this.props.updateTriggerProperty(this.props.index, 'house', e.target.value);
};

onAltitudeOperatorChange = e => {
this.props.updateTriggerProperty(this.props.index, 'sun_altitude_operator', e.target.value);
};

onAzimuthOperatorChange = e => {
this.props.updateTriggerProperty(this.props.index, 'sun_azimuth_operator', e.target.value);
};

onAltitudeChange = e => {
this.props.updateTriggerProperty(this.props.index, 'sun_altitude', this.parseDegree(e.target.value));
};

onAzimuthChange = e => {
this.props.updateTriggerProperty(this.props.index, 'sun_azimuth', this.parseDegree(e.target.value));
};

// An empty input is saved as null, so that the trigger stays valid while the
// user is typing, and the condition is simply not checked.
parseDegree = value => {
const degree = parseFloat(value);
return Number.isFinite(degree) ? degree : null;
};

houseIsValid(houseSelector) {
const { houses } = this.state;
let houseValid = false;
if (houseSelector === undefined || houseSelector === '') {
houseValid = true;
} else if (houses) {
const selectedHouse = houses.find(house => house.selector === houseSelector);
if (selectedHouse !== undefined) {
houseValid = selectedHouse.latitude && selectedHouse.longitude;
}
}
this.setState({ houseValid });
}

constructor(props) {
super(props);
this.state = {
houses: [],
houseValid: true
};
}

componentDidMount() {
this.getHouses();
}

componentDidUpdate(prevProps, prevState) {
if (prevState.houses !== this.state.houses || prevProps.trigger.house !== this.props.trigger.house) {
this.houseIsValid(this.props.trigger.house);
}
}

render({ trigger }, { houses, houseValid }) {
return (
<div>
<p>
<Text id="editScene.triggersCard.sunPosition.description" />
</p>
<div className="form-group">
<div className="form-label">
<Text id="editScene.triggersCard.sunPosition.houseLabel" />
</div>
{!houseValid && (
<div className="alert alert-danger">
<Text id="editScene.triggersCard.warning.houseWithoutCoordinate" />
</div>
)}
<select onChange={this.onHouseChange} className="form-control">
<option value="">
<Text id="global.emptySelectOption" />
</option>
{houses &&
houses.map(house => (
<option selected={house.selector === trigger.house} value={house.selector}>
{house.name}
</option>
))}
</select>
</div>
<div className="form-group">
<div className="form-label">
<Text id="editScene.triggersCard.sunPosition.altitudeLabel" />
</div>
<div className="row">
<div className="col-5">
<select onChange={this.onAltitudeOperatorChange} className="form-control">
<option value="">
<Text id="editScene.triggersCard.sunPosition.noCondition" />
</option>
{SUN_POSITION_OPERATORS.map(operator => (
<option selected={operator === trigger.sun_altitude_operator} value={operator}>
{operator}
</option>
))}
</select>
</div>
<div className="col-7">
<div className="input-group">
<input
type="number"
className="form-control"
min="-90"
max="90"
step="0.1"
disabled={!trigger.sun_altitude_operator}
value={
trigger.sun_altitude === null || trigger.sun_altitude === undefined ? '' : trigger.sun_altitude
}
onInput={this.onAltitudeChange}
/>
<span className="input-group-append">
<span className="input-group-text">°</span>
</span>
</div>
</div>
</div>
<small className="form-text text-muted">
<Text id="editScene.triggersCard.sunPosition.altitudeHelp" />
</small>
</div>
<div className="form-group">
<div className="form-label">
<Text id="editScene.triggersCard.sunPosition.azimuthLabel" />
</div>
<div className="row">
<div className="col-5">
<select onChange={this.onAzimuthOperatorChange} className="form-control">
<option value="">
<Text id="editScene.triggersCard.sunPosition.noCondition" />
</option>
{SUN_POSITION_OPERATORS.map(operator => (
<option selected={operator === trigger.sun_azimuth_operator} value={operator}>
{operator}
</option>
))}
</select>
</div>
<div className="col-7">
<div className="input-group">
<input
type="number"
className="form-control"
min="0"
max="360"
step="0.1"
disabled={!trigger.sun_azimuth_operator}
value={trigger.sun_azimuth === null || trigger.sun_azimuth === undefined ? '' : trigger.sun_azimuth}
onInput={this.onAzimuthChange}
/>
<span className="input-group-append">
<span className="input-group-text">°</span>
</span>
</div>
</div>
</div>
<small className="form-text text-muted">
<Text id="editScene.triggersCard.sunPosition.azimuthHelp" />
</small>
</div>
</div>
);
}
}

export default connect('httpClient,user', {})(SunPositionTrigger);
5 changes: 5 additions & 0 deletions server/lib/scene/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { addScene } = require('./scene.addScene');
const { create } = require('./scene.create');
const { checkTrigger } = require('./scene.checkTrigger');
const { checkCalendarTriggers } = require('./scene.checkCalendarTriggers');
const { checkSunPositionTriggers } = require('./scene.checkSunPositionTriggers');
const { init } = require('./scene.init');
const { cancelTriggers } = require('./scene.cancelTriggers');
const { destroy } = require('./scene.destroy');
Expand Down Expand Up @@ -56,6 +57,8 @@ const SceneManager = function SceneManager(
this.sunCalc = sunCalc;
this.scheduler = scheduler;
this.jobs = [];
// Last known sun position (in degrees) per house selector
this.sunPositions = new Map();
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)));
Expand All @@ -66,6 +69,7 @@ const SceneManager = function SceneManager(
this.event.on(EVENTS.HOUSE.UPDATED, eventFunctionWrapper(this.dailyUpdate.bind(this)));
this.event.on(EVENTS.HOUSE.DELETED, eventFunctionWrapper(this.dailyUpdate.bind(this)));
this.event.on(EVENTS.CALENDAR.CHECK_IF_EVENT_IS_COMING, eventFunctionWrapper(this.checkCalendarTriggers.bind(this)));
this.event.on(EVENTS.TIME.CHECK_SUN_POSITION, eventFunctionWrapper(this.checkSunPositionTriggers.bind(this)));

this.event.on(INTENTS.SCENE.START, this.command.bind(this));

Expand All @@ -76,6 +80,7 @@ SceneManager.prototype.addScene = addScene;
SceneManager.prototype.cancelTriggers = cancelTriggers;
SceneManager.prototype.create = create;
SceneManager.prototype.checkCalendarTriggers = checkCalendarTriggers;
SceneManager.prototype.checkSunPositionTriggers = checkSunPositionTriggers;
SceneManager.prototype.checkTrigger = checkTrigger;
SceneManager.prototype.destroy = destroy;
SceneManager.prototype.get = get;
Expand Down
Loading
Loading