-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Change scenes schema #505
Labels
Comments
Draft (Condition)Codeclass.condition.js const Joi = require("joi");
const mongodb = require("mongodb");
module.exports = class Condition {
constructor(obj) {
Object.assign(this, obj);
this._id = String(obj._id);
}
check() {
if (this.type === "timerange") {
let { startTime, endTime } = this.operation;
return Condition.isWithinTimeWindow(`${startTime}-${endTime}`);
} else if (this.type === "daterange") {
let { startDate, endDate } = this.operation;
return Condition.isWithinTimeWindow(`${startDate}-${endDate}`);
} else {
// not supported
return false;
}
}
static schema() {
return Joi.object({
_id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).default(() => {
return String(new mongodb.ObjectId());
}),
type: Joi.string().valid("timerange", "daterange").required(),
enabled: Joi.boolean().default(true),
operation: Joi.object().when("type", {
switch: [{
is: "timerange",
then: Joi.object({
startTime: Joi.string().required(),
endTime: Joi.string().required()
})
}, {
is: "daterange",
then: Joi.object({
startDate: Joi.string().required(),
endDate: Joi.string().required()
})
}],
otherwise: Joi.forbidden()
})
});
}
/**
* @function validate
* Validate schema object
*
* @static
*
* @param {Object} obj Input data that matches the schema
*
* @returns {Object} Joi validation object
*
* @link https://joi.dev/api/?v=17.6.0#anyvalidatevalue-options
*/
static validate(obj) {
return Condition.schema().validate(obj);
}
static isWithinTimeWindow(input) {
const now = new Date();
// Prüfen, ob der Input ein Zeitbereich oder ein Datumsbereich ist
if (input.includes("-")) {
// Whitespace entfernen und den Input in Start und Stop aufteilen
const [start, stop] = input.split("-").map(part => part.trim());
// Zeitfenster im Format "HH:MM-HH:MM"
if (start.includes(":") && stop.includes(":")) {
const [startHour, startMinute] = start.split(":").map(Number);
const [endHour, endMinute] = stop.split(":").map(Number);
return Condition.checkTimeWindow(startHour, startMinute, endHour, endMinute, now);
}
// Datumsbereich im Format "YYYY.MM.DD - YYYY.MM.DD"
else if (start.match(/^\d{4}\.\d{2}\.\d{2}$/) && stop.match(/^\d{4}\.\d{2}\.\d{2}$/)) {
const startDate = new Date(start.replace(/\./g, "-")); // Punkte durch Bindestriche ersetzen
const endDate = new Date(stop.replace(/\./g, "-")); // Punkte durch Bindestriche ersetzen
return now >= startDate && now <= endDate;
}
}
return false;
}
static checkTimeWindow(startHour, startMinute, endHour, endMinute, currentDate) {
// Aktuelle Stunde und Minute
const currentHour = currentDate.getHours();
const currentMinute = currentDate.getMinutes();
// Erstelle die Start- und Endzeit im Format "HHMM" (z.B. 0830 für 8:30)
const startTime = startHour * 100 + startMinute;
const endTime = endHour * 100 + endMinute;
const currentTime = currentHour * 100 + currentMinute;
// Überprüfen, ob aktuelle Zeit im Zeitfenster liegt
if (startTime <= endTime) {
// Zeitfenster liegt innerhalb eines Tages (z.B. 08:00 bis 18:00)
return currentTime >= startTime && currentTime <= endTime;
} else {
// Zeitfenster geht über Mitternacht (z.B. 22:00 bis 06:00)
return currentTime >= startTime || currentTime <= endTime;
}
}
}; class.scene.js const Joi = require("joi");
const mongodb = require("mongodb");
const { setTimeout } = require("timers/promises");
const debounce = require("../../helper/debounce.js");
const Makro = require("./class.makro.js");
const Trigger = require("./class.trigger.js");
const Condition = require("./class.condition.js");
const Item = require("../../system/component/class.item.js");
module.exports = class Scene extends Item {
constructor(obj) {
super(obj);
// removed for #356
//Object.assign(this, obj);
//this._id = String(obj._id);
this.makros = obj.makros.map((makro) => {
return new Makro(makro);
});
this.triggers = obj.triggers.map((data) => {
let trigger = new Trigger(data);
trigger.signal.on("fire", () => {
this.trigger();
});
return trigger;
});
Object.defineProperty(this, "states", {
value: {
running: false,
aborted: false,
finished: false,
index: 0
},
enumerable: false,
configurable: false,
writable: true
});
Object.defineProperty(this, "_ac", {
value: null,
enumerable: false,
configurable: false,
writable: true
});
// like in state updated
// see components/endpoints/class.state.js
let updater = debounce((prop, value) => {
let { update, logger } = Scene.scope;
update(this._id, this, (err) => {
if (err) {
// feedback
logger.warn(err, `Could not save timestamp ${prop}=${value}`);
} else {
// feedback
logger.debug(`Updated timestamps in database: ${prop}=${value}`);
}
});
}, 100);
// wrap timestamps in proxy set trap
// update item in database when the timestamps
// "started", "finished" or "aborted" set
// this ensures that theay are not `null` after a restart
this.timestamps = new Proxy(obj.timestamps, {
set: (target, prop, value, receiver) => {
let { logger } = Scene.scope;
if (["started", "finished", "aborted"].includes(prop) && value !== target[prop]) {
// feedback
logger.debug(`Update timestamp: ${prop}=${value}`);
// call debounced `.update()`
updater(prop, value);
}
return Reflect.set(target, prop, value, receiver);
}
});
}
static schema() {
return Joi.object({
_id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).default(() => {
return String(new mongodb.ObjectId());
}),
name: Joi.string().required(),
makros: Joi.array().items(Makro.schema()).default([]),
triggers: Joi.array().items(Trigger.schema()).default([]),
conditions: Joi.array().items(Condition.schema()).default([]),
visible: Joi.boolean().default(true),
icon: Joi.string().allow(null).default(null),
timestamps: {
started: Joi.number().allow(null).default(null),
aborted: Joi.number().allow(null).default(null),
finished: Joi.number().allow(null).default(null)
}
});
}
static validate(data) {
return Scene.schema().validate(data);
}
trigger() {
// fix #507
// stop previous running scene
if (this.states.running && this._ac) {
this._ac.abort();
}
this.timestamps.started = Date.now();
let ac = new AbortController();
this._ac = ac;
// wrap this in a custom method
// that returns the state?
// `getStates()` or so...
this.states.running = true;
this.states.aborted = false;
this.states.finished = false;
this.states.index = 0;
let execute = this.conditions.every(({ check }) => {
return check() === true;
});
if (!execute) {
console.log("Condition not true, do nothing!");
this.states.running = false;
return;
} else {
console.log("Condition true, execute scene");
}
let init = this.makros.filter(({
// enabled is per default "true"
// when a marko should be disabled
// this has explicit to be set to false
enabled = true
}) => {
// execute only enabled makros
return enabled;
}).map((makro) => {
// bind scope to method
return makro.execute.bind(makro);
}).reduce((acc, cur, i) => {
return (result) => {
return acc(result, this._ac.signal).then(async (r) => {
if (this.states.aborted) {
return Promise.reject("Aborted!");
} else {
// NOTE: Intended to be a workaround for #329 & #312
// But the general idea of this is not bad
// TODO: Add abort signal
await setTimeout(Number(process.env.SCENES_MAKRO_DELAY));
// represents the current index of makro
// e.g. timer takes 90min to finish,
// index = timer makro in `makros` array
this.states.index = i;
return cur(r, this._ac.signal);
}
}).catch((err) => {
console.log("Catched", i, err);
return Promise.reject(err);
});
};
});
return init(true, this._ac).then((result) => {
console.log("Makro stack done", result);
this.timestamps.finished = Date.now();
this.states.finished = true;
}).catch((err) => {
console.log("Makro stack aborted", err);
this.states.finished = false;
}).finally(() => {
console.log("Finaly");
this.states.running = false;
});
}
abort() {
// fix #507
if (this.states.running && this._ac) {
this._ac.abort();
}
this.states.running = false;
this.states.aborted = true;
this.states.finished = false;
this.timestamps.aborted = Date.now();
}
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add the following properties:
Triggers are defined indpendelty.
But with "conditions" you could create a appeal, that the trigger does not fire or is ignored.
E.g. A LUX Sensor trigger a light scenes based on the light outside. But only between 17:00 & 20:00.
This would prevent that the scenes fires in the morning, when its cloudy and the values jumps/jitter.
Currently the makro "command" does execute each command regardless if the previous command is done.
(
resolve()
outside the commad handler callback):backend/components/scenes/makro-types.js
Line 40 in dbf499e
This can be a problem when one command dependet on the other command successful execution.
But its not needed to wait for the previous command when, diffrent lights are turned on. Why wait for a light to turn on to turn another one on?
So there should be a option where you specify if it should execute them "simultaneous" or in a synchronous way.
The text was updated successfully, but these errors were encountered: