Making sure that only one timer exists per itemName

Hi there,

in my JavaScript (ECMAScript 2022+) rule I’m using a function to set a timer for different windows or doors to remind me that these are still open.
My goal is I want that for every itemName there is only one timer.

In the moment this is not the case. If I close and open a window three times and leave it open in the end I have 3 timers running in the end.

Can some more professional programmer explain me how I can make the timers unique per itemName. Only the last timer started should exist.

this is the function were I start the timer:

function FensterErinnerung(itemName, delay, nachricht) {
    timeoutReminderId = setTimeout(FensterNochOffen(itemName, nachricht), delay);
    // ich möchte gerne das pro itemName nur ein Timer existieren kann. 
    // Aktuelles Problem, wenn ich das Fenster 3x zu und auf mache und es am ende auf ist, 
    // dann habe ich 3 aktive Timer laufen und dann 3 Durchsagen.
}

This is the whole code of my oeffnungen.js:

var { someFunction, durchsage } = require('oh_lib')

// Fensterauffunktion Heizung
function HeizungFensterAuf(raum) {
    return function () {
        console.debug("Heizung Fenster Auf Test", items.getItem("OC_" + raum).members.find(item => item.state === "OPEN"));

        // Wenn immer noch OPEN wenn der Timer abgelaufen ist dann:
        if (items.getItem("OC_" + raum).members.find(item => item.state === "OPEN") != undefined) // Prüft ob einer der Kinder von OC_Raum einen Zustand "ON" hat.
        {
            if (items.getItem("HT_" + raum + "_Fensterzustand", true) !== null) {
                if (items.getItem("HT_" + raum + "_Fensterzustand").state === "CLOSED") {
                    items.getItem("HT_" + raum + "_Fensterzustand").sendCommand("OPEN");
                    console.debug("Fenster auf Modus aktiviert für", "HT_" + raum + "_Fensterzustand");
                }
            }
            if (items.getItem("WT_" + raum + "_Fensterzustand", true) !== null) {
                if (items.getItem("WT_" + raum + "_Fensterzustand").state === "CLOSED") {
                    items.getItem("WT_" + raum + "_Fensterzustand").sendCommand("OPEN");
                    console.debug("Fenster auf Modus aktiviert für", "WT_" + raum + "_Fensterzustand");
                }
            }
        }
    };
}

// Fensterzufunktion Heizung
function HeizungFensterZu(raum) {
    if (items.getItem("HT_" + raum + "_Fensterzustand", true) !== null) {
        if (items.getItem("HT_" + raum + "_Fensterzustand").state === "OPEN") {
            items.getItem("HT_" + raum + "_Fensterzustand").sendCommand("CLOSED");
            console.debug("Fenster auf Modus deaktiviert für", "HT_" + raum + "_Fensterzustand");
        }
    }
    if (items.getItem("WT_" + raum + "_Fensterzustand", true) !== null) {
        if (items.getItem("WT_" + raum + "_Fensterzustand").state === "OPEN") {
            items.getItem("WT_" + raum + "_Fensterzustand").sendCommand("CLOSED");
            console.debug("Fenster auf Modus deaktiviert für", "WT_" + raum + "_Fensterzustand");
        }
    }
}

// Fenstersteuerung Heizung Master
function HeizungFensterZustand(newState, raum, delay) {
    // Wenn länger als 2min OPEN dann Heizung FensterAufModus aktivieren
    if (newState === "OPEN") {
        // Timer wenn Tür länger auf als delay ms, dann Fenster auf Zustand aktivieren
        timeoutId = setTimeout(HeizungFensterAuf(raum), delay)

    }
    // Wenn CLOSED Heizung FensterAufModus deaktivieren
    if (newState === "CLOSED") {
        const members = items.getItem("OC_" + raum).members
        const found = members.some(el => el.state === "OPEN");
        if (!found) {
            HeizungFensterZu(raum)
        }
    }
}


/* vvvvvvvvvv FENSTER AUF REMINDER FUNKTIONEN vvvvvvvvvv */
// Prüft ob nach Ablauf des Timers das Fenster noch offen ist
function FensterNochOffen(itemName, nachricht) {
    return function () {
        if (items.getItem(itemName).state === "OPEN") {
            durchsage(["Arbeitszimmer", "Diele", "Werkstatt", "Wohnzimmer"], nachricht)
        }
    };
}

function FensterErinnerung(itemName, delay, nachricht) {
    timeoutReminderId = setTimeout(FensterNochOffen(itemName, nachricht), delay);
    // ich möchte gerne das pro itemName nur ein Timer existieren kann. 
    // Aktuelles Problem, wenn ich das Fenster 3x zu und auf mache und es am ende auf ist, 
    // dann habe ich 3 aktive Timer laufen und dann 3 Durchsagen.
}
/* ^^^^^^^^^^ FENSTER AUF REMINDER FUNKTIONEN ^^^^^^^^^^ */



rules.JSRule({
    name: "Außentür oder Fenster auf oder zu",
    description: "Automatisierungen abhängig von der Öffnungen von Außentüren und Fenstern",
    triggers: [
        triggers.GroupStateChangeTrigger('Aussentueren'),
        triggers.GroupStateChangeTrigger('Fenster')
    ],
    execute: (event) => {
        loggerName = 'tueren.js.Oeffnungen';
        console.debug("Automatisierung gestartet durch:", event.itemName, event.newState);
        const triggerTyp = event.itemName.split('_')[0]
        const raum = event.itemName.split('_')[1]
        const triggerEigenschaft = event.itemName.split('_')[2]
        var delay = 0

        if (items.getItem(event.itemName).type === "ContactItem" && triggerTyp === "Aussentuer" || triggerTyp === "Fenster") {

            if (triggerTyp === "Fenster" && event.newState === "OPEN") {

                // Wenn (Lüften nicht empfohlen) => Zusätzliche Durchsage, dass Lüften nicht gut aktuell
                if (items.getItem("LuftfeuchteEmpfehlung_" + raum, true) !== null) {
                    if (items.getItem("LuftfeuchteEmpfehlung_" + raum).state == 0) {
                        durchsage([raum], "Lüften im Raum " + raum + " wird bei der aktuellen Luftfeuchtigkeit außen nicht empfohlen.")
                    }
                }

                if (items.getItem("TemperaturIst_Outdoor").numericState < 10 || items.getItem("TemperaturIst_Outdoor").numericState > 25) {
                    // Wenn draussen sehr kalt (<10 Grad) || sehr heiss (> 25  Grad) => Timer nach 10 Min                                
                    if (items.getItem("TemperaturIst_Outdoor").numericState < 10) {
                        // Wenn draussen < 10 Grad
                        delay = 10 * 60 * 1000; //Erste Zahl entspricht den Minuten        
                        FensterErinnerung(event.itemName, delay, "Das Fenster in Raum " + raum + " ist seit " + Math.round(delay / 60000) + " Minuten offen und draussen ist es sehr kalt.")
                        delay = 15 * 60 * 1000; //Erste Zahl entspricht den Minuten        
                        FensterErinnerung(event.itemName, delay, "Das Fenster in Raum " + raum + " ist immer noch auf. Nun seit " + Math.round(delay / 60000) + " Minuten, obwohl es draussen sehr kalt ist.")
                    }
                    else {
                        // Wenn draussen > 25 Grad
                        delay = 10 * 60 * 1000; //Erste Zahl entspricht den Minuten        
                        FensterErinnerung(event.itemName, delay, "Das Fenster in Raum " + raum + " ist seit " + Math.round(delay / 60000) + " Minuten offen und draussen ist es sehr warm.")
                        delay = 15 * 60 * 1000; //Erste Zahl entspricht den Minuten        
                        FensterErinnerung(event.itemName, delay, "Das Fenster in Raum " + raum + " ist immer noch auf. Nun seit " + Math.round(delay / 60000) + " Minuten, obwohl es draussen sehr warm ist.")
                    }
                }
                else if (items.getItem("TemperaturIst_Outdoor").numericState < 17 || items.getItem("TemperaturIst_Outdoor").numericState > 23) {
                    // Wenn draussen kalt (<17 Grad) || heiss (> 23 Grad) => Timer nach 20 Min
                    delay = 20 * 60 * 1000; //Erste Zahl entspricht den Minuten
                    FensterErinnerung(event.itemName, delay, "Das Fenster in Raum " + raum + " ist seit " + Math.round(delay / 60000) + " Minuten offen.")
                }
                else {
                    // Wenn draussen 17 - 23  Grad => Timer nach 60 Min
                    delay = 60 * 60 * 1000; //Erste Zahl entspricht den Minuten
                    FensterErinnerung(event.itemName, delay, "Das Fenster in Raum " + raum + " ist seit " + Math.round(delay / 60000) + " Minuten offen.")
                }
            }

            // Verschiedene Regeln je nach Raum dessen Fenster/Tür geöffnet wurde:
            switch (raum) {
                case "Arbeitszimmer":

                    if (triggerTyp === "Aussentuer") {
                        HeizungFensterZustand(event.newState, raum, 120000)
                    }
                    else {
                        HeizungFensterZustand(event.newState, raum, 0)

                    }
                    //Regeln nur für Türen:
                    if (triggerTyp === "Aussentuer") {
                        // Wenn OPEN  & dunkel dann Lichttimer an
                        if (event.newState === "OPEN" && items.getItem("LS_Outdoor_DunkelMax").state === "ON") {
                            console.debug("Licht_Terrasse_Wand_Timer ON, da Tür auf und draussen dunkel")
                            items.getItem("Licht_Terrasse_Wand_Timer").sendCommand("ON");
                            console.debug("Licht_Westfassade_Spots_Timer ON, da Tür auf und draussen dunkel")
                            items.getItem("Licht_Westfassade_Spots_Timer").sendCommand("ON");
                        }
                    }
                    break;
                case "Diele":
                    if (triggerTyp === "Aussentuer") {
                        HeizungFensterZustand(event.newState, raum, 120000)
                    }
                    else {
                        HeizungFensterZustand(event.newState, raum, 0)
                    }
                    //Regeln nur für Türen:
                    if (triggerTyp === "Aussentuer") {
                        // Wenn OPEN  & dunkel dann Lichttimer an
                        if (event.newState === "OPEN" && items.getItem("LS_Outdoor_DunkelMax").state === "ON") {
                            console.debug("Licht_Einfahrt_Hauseingang_Timer ON, da Tür auf und draussen dunkel")
                            items.getItem("Licht_Einfahrt_Hauseingang_Timer").sendCommand("ON");
                            items.getItem("Licht_Einfahrt_Poller1_Timer").sendCommand(35);
                            items.getItem("Licht_Einfahrt_Poller2_Timer").sendCommand(35);
                        }
                    }
                    break;
                case "Duschbad":
                    HeizungFensterZustand(event.newState, raum, 0)
                    break;
                case "Hauswirtschaftsraum":
                    HeizungFensterZustand(event.newState, raum, 120000)
                    //Regeln nur für Türen:
                    if (triggerTyp === "Aussentuer") {
                        // Wenn OPEN  & dunkel dann Lichttimer an
                        if (event.newState === "OPEN" && items.getItem("LS_Outdoor_DunkelMax").state === "ON") {
                            console.debug("Licht_Einfahrt_Hauseingang_Timer ON, da Tür auf und draussen dunkel")
                            items.getItem("Licht_Einfahrt_Hauseingang_Timer").sendCommand("ON");
                        }
                    }
                    break;
                case "Kinderzimmer":
                    HeizungFensterZustand(event.newState, raum, 0)
                    //Regeln nur für Fenster:
                    if (triggerTyp === "Fenster") {
                        // Wenn OPEN Ost Dann Rolladen hoch
                        if (event.newState === "OPEN" && triggerEigenschaft === "Ost") {
                            console.debug("Rolladen hoch in Raum " + raum)
                            items.getItem("Verdunkelung_Kinderzimmer_LevelSoll").sendCommand(0);
                        }
                    }
                    break;
                case "Kueche":
                    HeizungFensterZustand(event.newState, raum, 0)
                    break;
                case "Schlafzimmer":
                    HeizungFensterZustand(event.newState, raum, 0)
                    //Regeln nur für Fenster:
                    if (triggerTyp === "Fenster") {
                        // Wenn OPEN Ost Dann Rolladen hoch
                        if (event.newState === "OPEN" && triggerEigenschaft === "Ost") {
                            console.debug("Rolladen hoch in Raum " + raum)
                            items.getItem("Verdunkelung_Schlafzimmer_LevelSoll").sendCommand(0);
                        }
                    }
                    break;
                case "Wohnzimmer":
                    HeizungFensterZustand(event.newState, raum, 120000)
                    //Regeln nur für Türen:
                    if (triggerTyp === "Aussentuer") {
                        // Wenn OPEN  & dunkel dann Lichttimer an
                        if (event.newState === "OPEN" && items.getItem("LS_Outdoor_DunkelMax").state === "ON") {
                            console.debug("Licht_Terrasse_Wand_Timer ON, da Tür auf und draussen dunkel")
                            items.getItem("Licht_Terrasse_Wand_Timer").sendCommand("ON");
                        }
                    }
                    break;
                default:
                    console.warn("Raum " + raum + " ist für Öffnungsregeln aktuell noch nicht vorgesehen.")
            }
        }
        else {
            console.error("Automatisierung beendet, da erstes Segment des auslösenden Items nicht `Aussentuer` oder `Fenster` lautet.")
        }

    },
    tags: ["Oeffnungen", "Aussentueren", "Fenster", "Licht", "Heizung", "Alarm"],
    id: "Oeffnungen"
});


I’d recommend installing openhab_rules_tools (through openhabian-config or by running npm install openhab_rules_tools from the $OH_CONF/automation/js folder. Then you can use TimerMgr which handles all that stuff for you.

var {TimerMgr} = require('openhab_rules_tools');

var tm = cache.private.get('timers', () => TimerMgr());

...

function FensterErinnerung(itemName, delay, nachricht) {
    tm.check(itemName, delay, nachricht);
    // ich möchte gerne das pro itemName nur ein Timer existieren kann. 
    // Aktuelles Problem, wenn ich das Fenster 3x zu und auf mache und es am ende auf ist, 
    // dann habe ich 3 aktive Timer laufen und dann 3 Durchsagen.
}

If you want to reschedule the timer if it already exists

tm.check(itemName, delay, nachricht, true);

The reason it doesn’t work now the way you’ve implemented it is you throw away the reference to the Timer. You don’t save it anywhere. You don’t associate it with the Item that created it. You just create the timer and immediately forget about it.

1 Like

@rlkoshak thanks for the solution. I understood the problem but didn’t know how to solve it.

Can I call the function FensterNochOffen(itemName, nachricht)
in
tm.check(itemName, delay, nachricht);

like this:
tm.check(itemName, delay, FensterNochOffen(itemName, nachricht));
?

the first parameter of the tm.check function is the uniqueIdentifier of the timer right?

Thanks so much!

Yes

But not like that.

You have to pass the function. You are calling the function and passing the result of the function.

What you’d need is to create a function generator which is a function that returns a function. This is discussed in the JS Scripting docs in detail. It would look something like this:

function timerFunctionGenerator(itemName, nachricht) {
  return function() { FensterNochOffen(item, nachricht); }
}

That is a function that returns a function that calls a function. But a timer can only handle a function that doesn’t take arguments so you have to go through this redirection.

1 Like

Thank you so much! You keep OpenHab living!

I have more questions about the use of the openhab_rules_tools. If I have this installed via “npm install”, what do I have to take into account for the future? I always try to have as few constraints as possible. And this is a pretty new one for me.

  1. How are updates to these openhab_rules_tools installed?
  2. If my system crashes on an openhab update. Can I then just copy back my backup and openhab will run again, or will I have to reinstall this package again?
  3. Is this library somehow synced with the actual OpenHAB version to be compatible? For example, I just changed all my rules from DSL rules to Javascript rules. Do I have any risk here in the future?

Or is there a possibility to do just what I need with the on-board resources of OpenHAB 4?

Thank you again for your great support!

It depends. If you are happy with it as it is you need do nothing. It should work for the foreseeable future (at least until OH 5). If you want to keep up with the few bug fixes that crop up and new features you’ll want to periodically update it. I’d recommend updating it when you update OH as that’s when you’ll get a new openhab-js version.

If you are running openHABian, the updates come automatically as part of the upgrade process.

npm update

run from $OH_CONF/automation/js.

If there is some reason I need to do a major version change (e.g. go from 2.1.4 to 3.0.0) there is additional manual step imposed by npm.

I can’t imagine that happening except for maybe for OH 5 which is years away.

OHRT should be captured by your backup assuming a standard backup, so you don’t have to install it again. It will be part of the backup. Everything is in $OH_CONF/automation/js/node_modules.

You are kind of talking apples to oranges here. OHRT only is a thing for JS Scripting. It has nothing to do with Rules DSL, jRuby, or anything else. I have on my todo list creating a Blockly library so Blockly users can use it too, but this far it’s low priority.

Whether you have a risk of JS Scripting going away? :person_shrugging: No one can predict the future but if that happens OHRT won’t be the reason.

As for compatibility, it’s primarily tied to the version of openhab-js (i.e. the JS Scripting helper library). For now, as long as you are on OH 4.0.0.0 or later everything is fine. In the future I intend to add more versioning checks when adding features that depend on new features in openhab-js so the library does things the new way only where the new way is supported.

It’s just code. You can write the code yourself instead of using a library. That doesn’t really change anything said above. It just means you are responsible for keeping up with openhab-js and adjusting everything as necessary. You also have to do more work since you can’t reuse anything anyone else has done.

When you install the library, you get the actual code. It’s not compiled or anything, so you could just install it and take on maintaining your own personal version on your own.

From one perspective, OHRT does come with OH as it comes with openHABian. But if you are not using openHABian, well you are already taking on more manual work to install and maintain OH.

1 Like

Thank for the detailed explanation which helped me a lot.
I will start using the OHRT. Thank you for your work!

I think with my hardware (Synology NAS DS716+) it is the correct way to run it in a docker container, right?

Yes, running on Synology needs a VM or container.

1 Like

In cases like this I use an item with expire. Sending multiple ON commands restarts the timer, and it expires only once.

@rlkoshak : Maybe I found a bug in OHRT.

I implemented it now with the help of OHRT.

The main part looks likes this:

var { someFunction, durchsage } = require('oh_lib')
var { TimerMgr } = require('openhab_rules_tools');
var tm = cache.private.get('timers', () => TimerMgr());

// Prüft ob nach Ablauf des Timers das Fenster noch offen ist
function FensterNochOffen(itemName, nachricht) {
    return function () {
        if (items.getItem(itemName).state === "OPEN") {
            durchsage(["Arbeitszimmer", "Diele", "Werkstatt", "Wohnzimmer"], nachricht)
        }
    };
}

function FensterErinnerung(itemName, delay, nachricht) {
    //timeoutReminderId = setTimeout(FensterNochOffen(itemName, nachricht), delay);
    tm.check(itemName, delay, FensterNochOffen(itemName, nachricht));
}

If I now call the following I get an Message after 5 seconds:

FensterErinnerung(event.itemName, 5000, "Test 1")

But if I call the following I would expect it to cancel the first timer and send me the message “Test 2” after 10 seconds. But nothing happens at all? The timer is correctly canceled but no now timer is set. Why is that? This looks like a bug for me.

FensterErinnerung(event.itemName, 5000, "Test 1")
FensterErinnerung(event.itemName, 10000, "Test 2")

If I change the timer call to this:

tm.check(itemName, delay, FensterNochOffen(itemName, nachricht));

The result is after 10 seconds “Test 1” because the first timer gets rescheduled. This makes sense to me.

Can you change the implementation so that with the flag is false after canceling the timer a new timer with the new delay and message is created?

I assume this is a place holder for the actual function you are passing? Passing a String as the third argument isn’t going to work because it’s not a function.

This is a unique use case that Timer Manager doesn’t handle I think and I’m not sure how to handle it without a breaking change. In pseudocode the logic of TimerMgr is as follows:

if timer exists:
  if reschedule:
    timer.reschedule(when) # obviously it's going to use the old function
  else:
    timer.cancel
  if flappingFunc exists:
    flappingFunc()
else:
  timer = createTimer(when, func)

The assumption is that a reschedule is just that, a reschedule, not a recreation. Passing a different func would be a new timer. TimerMgr was modeled after the typical way Timers are used and created.

I’d have to think for a way to handle a way to reschedule and replace the func in a way that would be non-breaking.

In the mean time, If you want to replace the timer, call cancel and then call check to create a new one.

No. It is not a placeholder. But the string is then used as the second parameter in the function I send to the timer. So it is working.

I have done it like this for now:

function FensterErinnerung(itemName, delay, nachricht) {
    // old version:
    //timeoutReminderId = setTimeout(FensterNochOffen(itemName, nachricht), delay);
    tm.cancel(itemName);
    tm.check(itemName, delay, FensterNochOffen(itemName, nachricht));
}

I think you are right. The would need a new function in your libary so it would be non-breaking. Or additional optional parameter in the very end of the check function.

Thank you for your help!

??? How is it converting “Test 1” into anything usable as a ZonedDateTime?

And that explanation doesn’t make sense because you have a second argument for the time already with the 5000 or 10000.