OpenHausIO / backend

HTTP API for the OpenHaus SmartHome/IoT solution
https://docs.open-haus.io
6 stars 2 forks source link

Add "events" & "timestamps" for scenes #496

Closed mStirner closed 1 month ago

mStirner commented 2 months ago

Would be nice when you can react upon scenes executions steps.

Also would be nice to have timestamps in /state routes.

mStirner commented 2 months ago

Draft:

class.scene.js

const Joi = require("joi");
const mongodb = require("mongodb");
const { EventEmitter } = require("events");

const { setTimeout } = require("timers/promises");

const Makro = require("./class.makro.js");
const Trigger = require("./class.trigger.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, "running", {
            value: false,
            enumerable: false,
            configurable: false,
            writable: true
        });

        Object.defineProperty(this, "aborted", {
            value: false,
            enumerable: false,
            configurable: false,
            writable: true
        });

        Object.defineProperty(this, "index", {
            value: 0,
            enumerable: false,
            configurable: false,
            writable: true
        });

        Object.defineProperty(this, "finished", {
            value: false,
            enumerable: false,
            configurable: false,
            writable: true
        });

        Object.defineProperty(this, "_ac", {
            value: null,
            enumerable: false,
            configurable: false,
            writable: true
        });

        this.events = new EventEmitter();

    }

    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([]),
            visible: Joi.boolean().default(true),
            icon: Joi.string().allow(null).default(null)
        });
    }

    static validate(data) {
        return Scene.schema().validate(data);
    }

    trigger() {

        this.events.emit("started", this);

        let ac = new AbortController();
        this._ac = ac;

        // wrap this in a custom method
        // that returns the state?
        // `getStates()` or so...
        this.running = true;
        this.aborted = false;
        this.finished = false;
        this.index = 0;

        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.aborted) {

                        return Promise.reject("Aborted!");

                    } else {

                        // NOTE: Intended to be a workaround for #329 & #312
                        // But the general idea of this is not bad
                        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.index = i;

                        this.events.emit("makro", cur);

                        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.finished = true;
            this.events.emit("finished", this);
        }).catch((err) => {
            console.log("Makro stack aborted", err);
            this.finished = false;
            this.events.emit("aborted", this);
        }).finally(() => {
            console.log("Finaly");
            this.running = false;
        });

    }

    abort() {

        console.log("Aborted called");

        this._ac.abort();
        this.running = false;
        this.aborted = true;
        this.finished = false;

    }

};