Skip to content

Commit

Permalink
Merge branch 'master' into gladys-sec-2
Browse files Browse the repository at this point in the history
  • Loading branch information
bnematzadeh authored Nov 9, 2024
2 parents 08554b0 + 2540927 commit 2c55862
Show file tree
Hide file tree
Showing 14 changed files with 697 additions and 224 deletions.
7 changes: 4 additions & 3 deletions front/src/config/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2055,8 +2055,8 @@
"sunset": "Jeder Sonnenuntergang"
},
"user": {
"back-home": "Benutzer hat das Zuhause verlassen",
"left-home": "Benutzer ist zurück zu Hause"
"back-home": "Benutzer ist zurück zu Hause",
"left-home": "Benutzer hat das Zuhause verlassen"
},
"house": {
"empty": "Das Zuhause ist leer",
Expand Down Expand Up @@ -2096,7 +2096,8 @@
"on": "Ein",
"off": "Aus",
"deviceSeen": "Wenn das Gerät erkannt wird",
"onlyExecuteAtThreshold": "Nur ausführen, wenn der Schwellenwert überschritten wird (und nicht bei jedem vom Gerät gesendeten Wert)"
"onlyExecuteAtThreshold": "Nur ausführen, wenn der Schwellenwert überschritten wird (und nicht bei jedem vom Gerät gesendeten Wert)",
"activateOrDeactivateForDuration": "Szene ausführen, nachdem die Bedingung für die Dauer gültig war:"
},
"scheduledTrigger": {
"typeLabel": "Typ",
Expand Down
7 changes: 4 additions & 3 deletions front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2055,8 +2055,8 @@
"sunset": "Every sunset"
},
"user": {
"back-home": "User left home",
"left-home": "User back at home"
"back-home": "User back at home",
"left-home": "User left home"
},
"house": {
"empty": "House is empty",
Expand Down Expand Up @@ -2096,7 +2096,8 @@
"on": "On",
"off": "Off",
"deviceSeen": "If the device is detected",
"onlyExecuteAtThreshold": "Execute only when threshold is passed (and not at every value sent by the device)"
"onlyExecuteAtThreshold": "Execute only when threshold is passed (and not at every value sent by the device)",
"activateOrDeactivateForDuration": "Run the scene after the condition has been valid for:"
},
"scheduledTrigger": {
"typeLabel": "Type",
Expand Down
3 changes: 2 additions & 1 deletion front/src/config/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2096,7 +2096,8 @@
"on": "On",
"off": "Off",
"deviceSeen": "Si l'appareil est détecté",
"onlyExecuteAtThreshold": "Exécuter seulement lorsque le seuil est passé ( et non pas à chaque valeur envoyée )"
"onlyExecuteAtThreshold": "Exécuter seulement lorsque le seuil est passé ( et non pas à chaque valeur envoyée )",
"activateOrDeactivateForDuration": "Exécuter la scène après que la condition ait été valide pendant : "
},
"scheduledTrigger": {
"typeLabel": "Type",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component } from 'preact';
import { connect } from 'unistore/preact';
import { Text, Localizer } from 'preact-i18n';

import { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } from '../../../../../../server/utils/constants';

Expand All @@ -24,6 +25,24 @@ class TurnOnLight extends Component {
}
};

onForDurationChange = e => {
e.preventDefault();
if (e.target.value) {
this.props.updateTriggerProperty(this.props.index, 'for_duration', Number(e.target.value) * 60 * 1000);
} else {
this.props.updateTriggerProperty(this.props.index, 'for_duration', '');
}
};

enableOrDisableForDuration = e => {
e.preventDefault();
if (e.target.checked) {
this.props.updateTriggerProperty(this.props.index, 'for_duration', 60 * 1000);
} else {
this.props.updateTriggerProperty(this.props.index, 'for_duration', undefined);
}
};

render(props, { selectedDeviceFeature }) {
let binaryDevice = false;
let presenceDevice = false;
Expand Down Expand Up @@ -55,12 +74,55 @@ class TurnOnLight extends Component {
</div>
</div>
{binaryDevice && <BinaryDeviceState {...props} selectedDeviceFeature={selectedDeviceFeature} />}
{presenceDevice && <PresenceSensorDeviceState {...props} />}
{presenceDevice && <PresenceSensorDeviceState {...props} selectedDeviceFeature={selectedDeviceFeature} />}
{buttonClickDevice && <ButtonClickDeviceState {...props} />}
{pilotWireModeDevice && <PilotWireModeDeviceState {...props} />}
{defaultDevice && <DefaultDeviceState {...props} selectedDeviceFeature={selectedDeviceFeature} />}
</div>
{thresholdDevice && <ThresholdDeviceState {...props} />}
<div class="row">
<div class="col-12">
<label class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
checked={props.trigger.for_duration !== undefined}
onChange={this.enableOrDisableForDuration}
/>
<span class="form-check-label">
<Text id="editScene.triggersCard.newState.activateOrDeactivateForDuration" />
</span>
</label>
</div>
</div>
{props.trigger.for_duration !== undefined && (
<div class="row">
<div class="col">
<div class="form-group">
<div class="input-group">
<Localizer>
<input
type="number"
class="form-control"
placeholder={<Text id="editScene.triggersCard.newState.valuePlaceholder" />}
value={
Number.isInteger(props.trigger.for_duration)
? props.trigger.for_duration / 60 / 1000
: props.trigger.for_duration
}
onChange={this.onForDurationChange}
/>
</Localizer>
<span class="input-group-append" id="basic-addon2">
<span class="input-group-text">
<Text id="deviceFeatureUnitShort.minutes" />
</span>
</span>
</div>
</div>
</div>
</div>
)}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ class PresenceSensorDeviceState extends Component {
this.props.updateTriggerProperty(this.props.index, 'threshold_only', false);
}

componentDidUpdate(prevProps) {
if (
prevProps.selectedDeviceFeature &&
this.props.selectedDeviceFeature &&
prevProps.selectedDeviceFeature.selector !== this.props.selectedDeviceFeature.selector
) {
this.props.updateTriggerProperty(this.props.index, 'operator', '=');
this.props.updateTriggerProperty(this.props.index, 'value', 1);
this.props.updateTriggerProperty(this.props.index, 'threshold_only', false);
}
}

render() {
return (
<div class="col-6">
Expand Down
2 changes: 1 addition & 1 deletion server/lib/gateway/gateway.forwardMessageToOpenAI.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const intentTranslation = {
INFO: 'info.get-info',
};

const disableOpenAiFirstReply = new Set(['GET_TEMPERATURE', 'GET_HUMIDITY']);
const disableOpenAiFirstReply = new Set(['GET_TEMPERATURE', 'GET_HUMIDITY', 'NO_RESPONSE']);

/**
* @public
Expand Down
1 change: 1 addition & 0 deletions server/lib/scene/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const SceneManager = function SceneManager(
this.sunCalc = sunCalc;
this.scheduler = scheduler;
this.jobs = [];
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)));
// on timezone change, reload all scenes
Expand Down
2 changes: 1 addition & 1 deletion server/lib/scene/scene.checkTrigger.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function checkTrigger(event) {
if (event.type === trigger.type) {
logger.debug(`Trigger ${trigger.type} is matching with event`);
// then we check the condition is verified
const conditionVerified = triggersFunc[event.type](event, trigger);
const conditionVerified = triggersFunc[event.type](this, sceneSelector, event, trigger);
logger.debug(`Trigger ${trigger.type}, conditionVerified = ${conditionVerified}...`);

// if yes, we execute the scene
Expand Down
116 changes: 92 additions & 24 deletions server/lib/scene/scene.triggers.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,106 @@
const cloneDeep = require('lodash.clonedeep');

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

const triggersFunc = {
[EVENTS.DEVICE.NEW_STATE]: (event, trigger) => {
// we check that we are talking about the same event
[EVENTS.DEVICE.NEW_STATE]: (self, sceneSelector, event, trigger) => {
// we check that we are talking about the same device feature
if (event.device_feature !== trigger.device_feature) {
return false;
}

// We verify if both old value and new value validate the rule
const newValueValidateRule = compare(trigger.operator, event.last_value, trigger.value);
// if the trigger is only a threshold_only, we only validate the trigger is the rule has been validated
// and was not validated with the previous value
if (trigger.threshold_only === true && !Number.isNaN(event.previous_value)) {
const previousValueValidateRule = compare(trigger.operator, event.previous_value, trigger.value);
return newValueValidateRule && !previousValueValidateRule;
const previousValueValidateRule = compare(trigger.operator, event.previous_value, trigger.value);

const triggerDurationKey = `device.new-state.${sceneSelector}.${trigger.device_feature}:${trigger.operator}:${trigger.value}`;

// If the previous value was validating the rule, and the new value is not
// We clear any timeout for this trigger
if (previousValueValidateRule && !newValueValidateRule && self.checkTriggersDurationTimer.get(triggerDurationKey)) {
logger.info(
`Cancelling timer on trigger for device_feature ${trigger.device_feature}, because condition is no longer valid`,
);
clearTimeout(self.checkTriggersDurationTimer.get(triggerDurationKey));
self.checkTriggersDurationTimer.delete(triggerDurationKey);
}

if (trigger.for_duration === undefined) {
// If the trigger is only a threshold_only, we only validate the trigger is the rule has been validated
// and was not validated with the previous value
if (trigger.threshold_only === true && !Number.isNaN(event.previous_value)) {
return newValueValidateRule && !previousValueValidateRule;
}

return newValueValidateRule;
}
return newValueValidateRule;

// If the "for_duration_finished" is here, it means we are
// checking the state after the timeout
if (event.for_duration_finished && triggerDurationKey === event.trigger_duration_key) {
logger.info(`Scene trigger device.new-state: Timer for sensor ${trigger.device_feature} has finished.`);
clearTimeout(self.checkTriggersDurationTimer.get(triggerDurationKey));
self.checkTriggersDurationTimer.delete(triggerDurationKey);
return newValueValidateRule;
}

const isValidatedIfThresholdOnly =
trigger.threshold_only && !Number.isNaN(event.previous_value)
? newValueValidateRule && !previousValueValidateRule
: true;

if (newValueValidateRule && isValidatedIfThresholdOnly) {
// If the timeout already exist, don't re-create it
const timeoutAlreadyExist = self.checkTriggersDurationTimer.get(triggerDurationKey);
if (timeoutAlreadyExist) {
logger.info(`Timer for "${trigger.device_feature}" already exist, not re-creating.`);
return false;
}
logger.info(
`Scheduling timer to check for device_feature "${trigger.device_feature}" state in ${trigger.for_duration}ms`,
);
// Create a timeout
const timeoutId = setTimeout(() => {
const lastValue = self.stateManager.get('deviceFeature', trigger.device_feature).last_value;
self.event.emit(EVENTS.TRIGGERS.CHECK, {
...cloneDeep(event),
previous_value: event.last_value,
last_value: lastValue,
for_duration_finished: true,
trigger_duration_key: triggerDurationKey,
});
}, trigger.for_duration);
// Save the timeoutId in case we need to cancel it later
self.checkTriggersDurationTimer.set(triggerDurationKey, timeoutId);
// Return false, as we'll check this only in the future
return false;
}

return false;
},
[EVENTS.TIME.CHANGED]: (event, trigger) => event.key === trigger.key,
[EVENTS.TIME.SUNRISE]: (event, trigger) => event.house.selector === trigger.house,
[EVENTS.TIME.SUNSET]: (event, trigger) => event.house.selector === trigger.house,
[EVENTS.USER_PRESENCE.BACK_HOME]: (event, trigger) => event.house === trigger.house && event.user === trigger.user,
[EVENTS.USER_PRESENCE.LEFT_HOME]: (event, trigger) => event.house === trigger.house && event.user === trigger.user,
[EVENTS.HOUSE.EMPTY]: (event, trigger) => event.house === trigger.house,
[EVENTS.HOUSE.NO_LONGER_EMPTY]: (event, trigger) => event.house === trigger.house,
[EVENTS.AREA.USER_ENTERED]: (event, trigger) => event.user === trigger.user && event.area === trigger.area,
[EVENTS.AREA.USER_LEFT]: (event, trigger) => event.user === trigger.user && event.area === trigger.area,
[EVENTS.ALARM.ARM]: (event, trigger) => event.house === trigger.house,
[EVENTS.ALARM.ARMING]: (event, trigger) => event.house === trigger.house,
[EVENTS.ALARM.DISARM]: (event, trigger) => event.house === trigger.house,
[EVENTS.ALARM.PARTIAL_ARM]: (event, trigger) => event.house === trigger.house,
[EVENTS.ALARM.PANIC]: (event, trigger) => event.house === trigger.house,
[EVENTS.ALARM.TOO_MANY_CODES_TESTS]: (event, trigger) => event.house === trigger.house,
[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.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) =>
event.house === trigger.house && event.user === trigger.user,
[EVENTS.HOUSE.EMPTY]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
[EVENTS.HOUSE.NO_LONGER_EMPTY]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
[EVENTS.AREA.USER_ENTERED]: (self, sceneSelector, event, trigger) =>
event.user === trigger.user && event.area === trigger.area,
[EVENTS.AREA.USER_LEFT]: (self, sceneSelector, event, trigger) =>
event.user === trigger.user && event.area === trigger.area,
[EVENTS.ALARM.ARM]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
[EVENTS.ALARM.ARMING]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
[EVENTS.ALARM.DISARM]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
[EVENTS.ALARM.PARTIAL_ARM]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
[EVENTS.ALARM.PANIC]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
[EVENTS.ALARM.TOO_MANY_CODES_TESTS]: (self, sceneSelector, event, trigger) => event.house === trigger.house,
[EVENTS.SYSTEM.START]: () => true,
[EVENTS.MQTT.RECEIVED]: (event, trigger) =>
[EVENTS.MQTT.RECEIVED]: (self, sceneSelector, event, trigger) =>
event.topic === trigger.topic && (trigger.message === '' || trigger.message === event.message),
};

Expand Down
1 change: 1 addition & 0 deletions server/models/scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const triggersSchema = Joi.array().items(
time: Joi.string().regex(/^([0-9]{2}):([0-9]{2})$/),
interval: Joi.number(),
unit: Joi.string(),
for_duration: Joi.number(),
days_of_the_week: Joi.array().items(
Joi.string().valid('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'),
),
Expand Down
12 changes: 12 additions & 0 deletions server/test/lib/gateway/gateway.forwardMessageToOpenAI.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,18 @@ describe('gateway.forwardMessageToOpenAI', () => {
intent: 'temperature-sensor.get-in-room',
});
});
it('should do nothing, no-response was sent', async () => {
gateway.gladysGatewayClient.openAIAsk = fake.resolves({
type: 'NO_RESPONSE',
answer: '',
room: '',
});
const classification = await gateway.forwardMessageToOpenAI({ message, previousQuestions, context });
expect(classification).to.deep.equal({
intent: undefined,
});
assert.notCalled(messageManager.reply);
});
it('should start scene from OpenAI', async () => {
gateway.gladysGatewayClient.openAIAsk = fake.resolves({
type: 'SCENE_START',
Expand Down
Loading

0 comments on commit 2c55862

Please sign in to comment.