Distinguishing Trigger Source: Device Interaction vs. sendCommand

Hi everyone,

I’m using a Google Nest Audio to implement a radio alarm clock. I would like to add features to allow SNOOZE and disabling the alarm.

  • PLAY/PAUSE: Disable alarm for today
  • VOLUME +/-: Snooze the alarm

However, I am facing a challenge: In the changed event, I cannot distinguish whether the volume change was triggered by my rule using .sendCommand or by manually pressing the volume buttons on the Google Nest Audio. The Item ChromeNestVolume changed event is triggered in both cases.

Is there a way to differentiate between these triggers within a rule?

Ideally, I would like to specify something like:

Item changed and not Item received command

Any suggestions or workarounds would be greatly appreciated!

Thanks, Julian

  • Platform information:
    • Hardware: Raspi PI 5 8GB
    • OS: Linux openhabian
    • Java Runtime Environment: latest Version
    • openHAB version: 4.3.3 Main UI Commit 1cc0b830

No, you’ll have to work around - e.g. setup a marker, set when sending the command, then start a timer to reset the marker.
To elaborate, openHAB is event driven. Rules start via triggers, there is no AND in the trigger part of the rule.
But even if there was an AND, it’s very unlikely that a changed event and a received command event will take place at the very same moment.

Hi Toni,
thanks for your answer.

Any other idea to realize this Funktion?

Can you explain more what you need to do in the rule, or perhaps paste the rule here?

I want to recreate a radio alarm clock with additional functions.

I am already using openHAB and a Google Nest Audio Wi-Fi speaker for this.
The rule checks every minute whether the alarm time matches the current time.
If they match, a predefined song starts playing and gradually increases in volume.
When the song ends, a text-to-speech message announces the current time.
After the announcement finishes, the web radio starts playing.

This rule/alarm clock works so far, but the code is not presentable yet :wink:

Now I would like to add smart functions:

  1. Deactivate the current alarm using the play/pause button.
  2. SNOOZE for x minutes using the volume up/down buttons.

However, I cannot distinguish whether the song/speech was stopped automatically (Trigger Changed "NestAudioControl") or if someone pressed a button on the device.

The same issue occurs with the volume up/down buttons—I cannot differentiate between a manual button press and the gradual volume increase at the beginning.
Each action always triggers a Changed "NestAudioVolume" event.

While I can use an internal flag to track if the event was triggered by my rule, a Changed event is also triggered when the first song or the text announcement ends.

These are the requirements I would like to implement.

Unfortunately the origin of an event is not tracked in OH. All your rule can know is that a command or a change or an update occurred, but there is no information about what caused that event.

Sometimes you can work around this using proxy Items or tricks using timing. But that can sometimes be brittle.

Presumably if you press a button on the device OH sees that only as an update and change and not as a command. If so then you can keep track of when your rule last issued a command and if the update and change come within a reasonable number of milliseconds after that ignore the change as a response from the command. You could even set a flag to ignore the first update/change after the command. But there are lots of edge cases to handle and we don’t have enough info to do more than just suggest a general overall approach.

In the rule that commands the volume, before you sendCommand to the volume Item store a timestamp in the shared cache (or a global variable). In the rule that triggers on changes to the volume Item check that timestamp and simply do nothing if it’s too soon.

Or set a boolean flag in the cache instead of a timestamp. In the rule that triggers on changes to the volume Item, check the boolean and if it’s true set it to false and exit since this event is the first one after the last command.

You might need to use updates instead of changes though to handle the cases where the volume is already 0 or 100 in which case commands won’t actually change the volume.

Hi all,
thanks for the answers.
Try to implement some time-based functions for filtering.
I will report the outcome soon.

Thanks

Every time my Rule fires a command I will start a timer with 3 seconds and set a FLAG boolean.
When the timer expired the Flag is reset.
Works well till now.

Best reagrds
Julian

Hi all,

with one user the Timer base trigger was working. Since I upgraded to more user, it is not working any longer.

her is my code, where I do have trouble.

var now = time.ZonedDateTime.now();
userStatus["Jonas"].timerID = actions.ScriptExecution.createTimer('My Timer Jonas', now.plusSeconds(1), resetOutputTriggerJonas);
userStatus["Luisa"].timerID = actions.ScriptExecution.createTimer('My Timer Luisa', now.plusSeconds(1), resetOutputTriggerLuisa);
userStatus["Juli"].timerID = actions.ScriptExecution.createTimer('My Timer_ Juli', now.plusSeconds(1), resetOutputTriggerJuli);

function setOutputTriggered(user) {
    userStatus[user].outputTriggered = true;
    console.info(`NestWecker /setOutputTriggered/:✅ OutputTrigger für  ${user} auf TRUE geändert`);
    
    let nowTime = time.ZonedDateTime.now(); // Vermeidung von Race Conditions

    switch(user) {
        case "Jonas":
            console.error(`NestWecker /setOutputTriggered/:✅ CASE Jonas`);
            if (userStatus["Jonas"].timerID !== null && !userStatus["Jonas"].timerID.hasTerminated()) {
                userStatus["Jonas"].timerID.cancel();
                console.error(`NestWecker /setOutputTriggered/:✅ Timer Jonas gecancelt`);
            }
            userStatus["Jonas"].timerID = actions.ScriptExecution.createTimer('My Timer Jonas', nowTime.plusSeconds(2), resetOutputTriggerJonas);
            console.error(`NestWecker /setOutputTriggered/:✅ Neuer Timer Jonas gestartet`);
            break;
        case "Luisa":
            console.error(`NestWecker /setOutputTriggered/:✅ CASE Luisa`);
            if (userStatus["Luisa"].timerID !== null && !userStatus["Luisa"].timerID.hasTerminated()) {
                userStatus["Luisa"].timerID.cancel();
                console.error(`NestWecker /setOutputTriggered/:✅ Timer Luisa gecancelt`);
            }
            userStatus["Luisa"].timerID = actions.ScriptExecution.createTimer('My Timer Luisa', nowTime.plusSeconds(2), resetOutputTriggerLuisa);
            console.error(`NestWecker /setOutputTriggered/:✅ Neuer Timer Luisa gestartet`);
            break;
        case "Juli":
            console.error(`NestWecker /setOutputTriggered/:✅ CASE Juli`);
            if (userStatus["Juli"].timerID !== null && !userStatus["Juli"].timerID.hasTerminated()) {
                userStatus["Juli"].timerID.cancel();
                console.error(`NestWecker /setOutputTriggered/:✅ Timer Juli gecancelt`);
            }
            userStatus["Juli"].timerID = actions.ScriptExecution.createTimer('My Timer Juli', nowTime.plusSeconds(2), resetOutputTriggerJuli);
            console.error(`NestWecker /setOutputTriggered/:✅ Neuer Timer Juli gestartet`);
            break;
        default:
            return;
    }

    console.error(`NestWecker /setOutputTriggered/: 🚨 TimerID ${userStatus[user].timerID} für ${user}`);
}

function resetOutputTriggerJonas() {
    let userRE = "Jonas";
    userStatus[userRE].outputTriggered = false;
    userStatus[userRE].timerID = null;
    console.error(`NestWecker /setOutputTriggered/:❌ Timer ${userRE} gelöscht und OutputTrigger auf FALSE gesetzt`);
}

function resetOutputTriggerLuisa() {
    let userRE = "Luisa";
    userStatus[userRE].outputTriggered = false;
    userStatus[userRE].timerID = null;
    console.error(`NestWecker /setOutputTriggered/:❌ Timer ${userRE} gelöscht und OutputTrigger auf FALSE gesetzt`);
}

function resetOutputTriggerJuli() {
    let userRE = "Juli";
    userStatus[userRE].outputTriggered = false;
    userStatus[userRE].timerID = null;
    console.error(`NestWecker /setOutputTriggered/:❌ Timer ${userRE} gelöscht und OutputTrigger auf FALSE gesetzt`);
}

I do get following errors.

2025-03-09 20:33:49.369 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /setOutputTriggered/:✅ OutputTrigger für  Luisa auf TRUE geändert
2025-03-09 20:33:49.370 [ERROR] [tomation.script.file.Wecker_Audio.js] - NestWecker /setOutputTriggered/:✅ CASE Luisa
2025-03-09 20:33:49.372 [ERROR] [tomation.script.file.Wecker_Audio.js] - NestWecker /setOutputTriggered/:✅ Neuer Timer Luisa gestartet
2025-03-09 20:33:49.373 [ERROR] [tomation.script.file.Wecker_Audio.js] - NestWecker /setOutputTriggered/: 🚨 TimerID org.openhab.core.automation.module.script.internal.action.TimerImpl@6f622dc5 für Luisa
2025-03-09 20:33:49.839 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /waitForAnnouncementToEnd/:⏳ Warte auf Ende der Ansage für Luisa auf chromecast:audio:ChromeNestLuisa...
2025-03-09 20:33:49.931 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /Wecker Stop/Snooze/:🔔 Regel getriggert durch ChromeNestLuisaControl
2025-03-09 20:33:49.933 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /isSystemStarted/:System läuft seit: 423 Minuten
2025-03-09 20:33:49.933 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /Wecker Stop/Snooze/:⚠️ Ignoriert für Luisa, da kein Alarm aktiv ist.
2025-03-09 20:33:49.934 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /isSystemStarted/:System läuft seit: 423 Minuten
2025-03-09 20:33:49.935 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:⚠️ Ignoriert für Luisa, da kein Alarm aktiv ist.
2025-03-09 20:33:51.091 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /isSystemStarted/:System läuft seit: 423 Minuten
2025-03-09 20:33:51.092 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:⚠️ Ignoriert für Luisa, da kein Alarm aktiv ist.
2025-03-09 20:33:51.369 [ERROR] [tomation.script.file.Wecker_Audio.js] - NestWecker /setOutputTriggered/:❌ Timer Luisa gelöscht und OutputTrigger auf FALSE gesetzt
2025-03-09 20:33:53.308 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /isSystemStarted/:System läuft seit: 423 Minuten
2025-03-09 20:33:53.309 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:⚠️ Ignoriert für Luisa, da kein Alarm aktiv ist.
2025-03-09 20:33:53.841 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /waitForAnnouncementToEnd/:✅ Ansage für Luisa beendet. Starte Stream https://liveradio.swr.de/sw282p3/swr3/play.mp3 auf chromecast:audio:ChromeNestLuisa
2025-03-09 20:33:53.841 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /setOutputTriggered/:✅ OutputTrigger für  Luisa auf TRUE geändert
2025-03-09 20:33:53.843 [WARN ] [ore.internal.scheduler.SchedulerImpl] - Scheduled job 'org.openhab.automation.script.file.Wecker_Audio.js.interval.1' failed and stopped
org.graalvm.polyglot.PolyglotException: Thread was interrupted.
	at <js>.n(@openhab-globals.js:2) ~[?:?]
	at <js>.r.offsetOfEpochMilli(@openhab-globals.js:2) ~[?:?]
	at <js>.r.offsetOfInstant(@openhab-globals.js:2) ~[?:?]
	at <js>.e.offset(@openhab-globals.js:2) ~[?:?]
	at <js>.e._create(@openhab-globals.js:2) ~[?:?]
	at <js>.e.ofInstant2(@openhab-globals.js:2) ~[?:?]
	at <js>.e.ofInstant(@openhab-globals.js:2) ~[?:?]
	at <js>.e.now(@openhab-globals.js:2) ~[?:?]
	at <js>.setOutputTriggered(Wecker_Audio.js:61) ~[?:?]
	at <js>.:=>(Wecker_Audio.js:247) ~[?:?]
	at <js>.:=>(@jsscripting-globals.js:189) ~[?:?]
	at com.oracle.truffle.polyglot.PolyglotFunctionProxyHandler.invoke(PolyglotFunctionProxyHandler.java:154) ~[?:?]
	at jdk.proxy1.$Proxy1596.run(Unknown Source) ~[?:?]
	at org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers.lambda$2(ThreadsafeTimers.java:159) ~[?:?]
	at org.openhab.core.internal.scheduler.SchedulerImpl.lambda$12(SchedulerImpl.java:189) ~[?:?]
	at org.openhab.core.internal.scheduler.SchedulerImpl.lambda$1(SchedulerImpl.java:88) ~[?:?]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) [?:?]
	at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) [?:?]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) [?:?]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) [?:?]
	at java.lang.Thread.run(Thread.java:840) [?:?]
2025-03-09 20:34:00.725 [INFO ] [tomation.script.file.Wecker_Audio.js] - NestWecker /Weckersteuerung/:🔔 Wecker-Check gestartet.

The second time I receive a function call for

setOutputTriggered

from the same User I run into this issue.

What I´m doing wrong?

I tried also with setTimeout.

I now will try the solution with the timestamp.

Please post the full rule. If this is a UI rule, click the code tab and post that.

Assuming this is a UI rule, you are not saving the timers from one run of the rule to the next. You need to save the timers to the cache, not variables. I don’t see userStatus defined anywhere so I assume this is in a .js file and userStatus is a global in the file.

Every time this rule is triggered it creates three timers regardless of whether ot not there are already timers created and running. Normally you’d only want to create one timer and which timer to create depends on the Item that triggered the rule.

I don’t see here where that function is ever called so :person_shrugging:

I´m Sorry.

You are totally right.
Half of the it is moreless impossible to help.

The hole rule is a long one an written in .js. I will post it here. All the comments are in German.

Best regards Julian

const { rules, triggers, items, actions } = require('openhab');

let userStatus = {}; // Speichert den Wecker-Status pro Benutzer
let yourTimeItem = {};

// Benutzer initialisieren
["Juli", "Luisa", "Jonas"].forEach(user => {
if (!userStatus[user]) {
    userStatus[user] = { isAlarmTriggered: false, announcementDone: false, outputTriggered: false, timeTrigger: time.ZonedDateTime.now() };
    console.info(`NestWecker /isSystemStarted/: System Variable angelegt: ${user} mit den Werten ${JSON.stringify(userStatus[user])}`);
}
});


let checkAnnouncementEnd = {}; // Speichert Timeouts pro Nutzer
let checkPlayed = {}; // Speichert Timeouts pro Nutzer
let waitInterval = {};
let waitedTime = {};


const streamMappings = {
"https://liveradio.swr.de/sw282p3/swr3/play.mp3": "SWR3",
"https://stream.sunshine-live.de/edm/mp3-192/stream.sunshine-live.de/": "Sunshine EDM",
"https://stream.regenbogen.de/karlsruhe/mp3-128": "Radio Regenbogen",
"https://stream.rockantenne.de/rockantenne/stream/mp3": "RockAntenne"
};


// **🚀 Mapping von Benutzern auf ihre Chromecast-Geräte**
const userDevices = {
    "Juli": "chromecast:audio:ChromeNestJuli",
    "Luisa": "chromecast:audio:ChromeNestLuisa",
    "Jonas": "chromecast:audio:ChromeNestJonas"
};


// **🚀 Prüft, ob OpenHAB länger als 5 Minuten läuft**
function isSystemStarted() {
let uptime = parseInt(items["RP_Betriebszeit"]?.state ?? 0);
console.info(`NestWecker /isSystemStarted/:System läuft seit: ${uptime} Minuten`);
return uptime > 5;
};

// **🚀 Sound-Ordner für jeden Benutzer**
// const soundFolders = {
//     "Juli": "etc/openhab/sounds/Juli/",
//     "Luisa": "etc/openhab/sounds/Luisa/",
//     "Jonas": "etc/openhab/sounds/Jonas/"
// };



function setOutputTriggered(user) {

    // let nowTime = time.ZonedDateTime.now();
    // userStatus[user].timeTrigger = nowTime;
    // console.error(`NestWecker /setOutputTriggered/:✅ Neuer Time ${userStatus[user].timeTrigger} für ${user} gesetzt`);

};

function isTimeTriggerOlderThan2Seconds(user) {
    // let nowTime = time.ZonedDateTime.now();
    // let timeTrigger = userStatus[user]?.timeTrigger; // Stelle sicher, dass der Wert existiert

    // if (!timeTrigger) {
    //     console.warn(`NestWecker /sTimeTriggerOlderThan2Seconds/:🚨 Kein TimeTrigger für ${user} gefunden.`);
    //     return false;
    // }
    // console.warn(`NestWecker /sTimeTriggerOlderThan2Seconds/:🚨 Berechne Zeitunterschied für ${user}.`);
    // let duration = nowTime.toInstant().toEpochMilli() - timeTrigger.toInstant().toEpochMilli();
    // console.warn(`NestWecker /sTimeTriggerOlderThan2Seconds/:🚨 Zeitunterschied ${duration} in Millisekunden für ${user} .`);
   // return duration > 2000;
   return true;
};

var now = time.ZonedDateTime.now();
userStatus["Jonas"].timerID = actions.ScriptExecution.createTimer('My Timer Jonas', now.plusSeconds(1), resetOutputTriggerJonas);
userStatus["Luisa"].timerID = actions.ScriptExecution.createTimer('My Timer Luisa', now.plusSeconds(1), resetOutputTriggerLuisa);
userStatus["Juli"].timerID = actions.ScriptExecution.createTimer('My Timer_ Juli', now.plusSeconds(1), resetOutputTriggerJuli);

function setOutputTriggered(user) {
    userStatus[user]?.outputTriggered = true;
    console.info(`NestWecker /setOutputTriggered/:✅ OutputTrigger für  ${user} auf TRUE geändert`);
    
    let nowTime = time.ZonedDateTime.now(); // Vermeidung von Race Conditions

    switch(user) {
        case "Jonas":
            console.error(`NestWecker /setOutputTriggered/:✅ CASE Jonas`);
            if (userStatus["Jonas"].timerID !== null && !userStatus["Jonas"].timerID.hasTerminated()) {
                userStatus["Jonas"].timerID.cancel();
                console.error(`NestWecker /setOutputTriggered/:✅ Timer Jonas gecancelt`);
            }
            userStatus["Jonas"].timerID = actions.ScriptExecution.createTimer('My Timer Jonas', nowTime.plusSeconds(2), resetOutputTriggerJonas);
            console.error(`NestWecker /setOutputTriggered/:✅ Neuer Timer Jonas gestartet`);
            break;
        case "Luisa":
            console.error(`NestWecker /setOutputTriggered/:✅ CASE Luisa`);
            if (userStatus["Luisa"].timerID !== null && !userStatus["Luisa"].timerID.hasTerminated()) {
                userStatus["Luisa"].timerID.cancel();
                console.error(`NestWecker /setOutputTriggered/:✅ Timer Luisa gecancelt`);
            }
            userStatus["Luisa"].timerID = actions.ScriptExecution.createTimer('My Timer Luisa', nowTime.plusSeconds(2), resetOutputTriggerLuisa);
            console.error(`NestWecker /setOutputTriggered/:✅ Neuer Timer Luisa gestartet`);
            break;
        case "Juli":
            console.error(`NestWecker /setOutputTriggered/:✅ CASE Juli`);
            if (userStatus["Juli"].timerID !== null && !userStatus["Juli"].timerID.hasTerminated()) {
                userStatus["Juli"].timerID.cancel();
                console.error(`NestWecker /setOutputTriggered/:✅ Timer Juli gecancelt`);
            }
            userStatus["Juli"].timerID = actions.ScriptExecution.createTimer('My Timer Juli', nowTime.plusSeconds(2), resetOutputTriggerJuli);
            console.error(`NestWecker /setOutputTriggered/:✅ Neuer Timer Juli gestartet`);
            break;
        default:
            return;
    }

    console.error(`NestWecker /setOutputTriggered/: 🚨 TimerID ${userStatus[user].timerID} für ${user}`);
}

function resetOutputTriggerJonas() {
    let userRE = "Jonas";
    userStatus[userRE].outputTriggered = false;
    userStatus[userRE].timerID = null;
    console.error(`NestWecker /setOutputTriggered/:❌ Timer ${userRE} gelöscht und OutputTrigger auf FALSE gesetzt`);
}

function resetOutputTriggerLuisa() {
    let userRE = "Luisa";
    userStatus[userRE].outputTriggered = false;
    userStatus[userRE].timerID = null;
    console.error(`NestWecker /setOutputTriggered/:❌ Timer ${userRE} gelöscht und OutputTrigger auf FALSE gesetzt`);
}

function resetOutputTriggerJuli() {
    let userRE = "Juli";
    userStatus[userRE].outputTriggered = false;
    userStatus[userRE].timerID = null;
    console.error(`NestWecker /setOutputTriggered/:❌ Timer ${userRE} gelöscht und OutputTrigger auf FALSE gesetzt`);
}




// function resetOutputTrigger(user) {
//     //userStatus[user]?.outputTriggered = false;
//     // console.error(`NestWecker /setOutputTriggered/: 🚨 Trigger gelöscht für ${user}`);
//     //clearTimeout(userStatus[user].timerID);
// };

// Falls nötig, kann man den Timeout vor Ablauf abbrechen
// clearTimeout(timeoutId);




rules.JSRule({
    name: "Nest Audio Control",
    description: "Steuert Nest Audio Geräte und löst Streams aus",
    triggers: [
        triggers.ItemStateChangeTrigger("ChromeNestJuliPlayer"),
        triggers.ItemStateChangeTrigger("ChromeNestJuliVolume"),
        triggers.ItemStateChangeTrigger("ChromeNestLuisaPlayer"),
        triggers.ItemStateChangeTrigger("ChromeNestLuisaVolume"),
        triggers.ItemStateChangeTrigger("ChromeNestJonasPlayer"),
        triggers.ItemStateChangeTrigger("ChromeNestJonasVolume")
    ],
    execute: (event) => {
        if (!isSystemStarted()) {
            console.info("NestWecker /Nest Audio Control/⚠️: OpenHAB ist noch im Startvorgang (< 5 min). Ignoriere Regel.");
            return;
        }

        let user = event.itemName.replace("ChromeNest", "").replace(/(Player|Volume)/, "");

        if (userStatus[user].isAlarmTriggered) {
            console.info(`NestWecker /Nest Audio Control/⚠️: Wecker für ${user} läuft, Befehle werden ignoriert.`);
            return;
        }

        // //if (items[outputTriggerItem] === "ON") {

        // if (isTimeTriggerOlderThan2Seconds(user)) {
        //     console.log(`Die gespeicherte Zeit für ${user} liegt mehr als 2 Sekunden zurück.`);
        // } else {
        //     console.log(`NestWecker /Nest Audio Control/⚠️: Die gespeicherte Zeit für ${user} ist noch aktuell.`);
        //     console.info(`NestWecker /Nest Audio Control/⚠️: Ignoriert für ${user}, da OutputTriggered aktiv ist.`);
        //     return;
        // }
        // if (userStatus[user]?.outputTriggered == true) {
        //     console.info(`NestWecker /Nest Audio Control/⚠️: Ignoriert für ${user}, da OutputTriggered aktiv ist.`);
        //     return;
        // }
        console.info(`NestWecker /Nest Audio Control/: Regel getriggert durch ${event.itemName}`);
        let playerControlItem = `ChromeNest${user}Control`;
        //let outputTriggerItem = `${user}_OutputTriggered`;
        let device = userDevices[user];

        if (!items.existsItem(playerControlItem)) {
            console.warn(`NestWecker /Nest Audio Control/⚠️: Item '${playerControlItem}' existiert nicht.`);
            return;
        }

        if (items[playerControlItem].state !== "PAUSE") {
            console.info(`NestWecker /Nest Audio Control/⚠️: Player für ${user} läuft bereits, neuer Stream wird nicht gestartet.`);
            return;
        }

        if (event.itemName.endsWith("Volume") && event.newState !== null) {
            let prevVolume = parseFloat(event.oldState);
            let currentVolume = parseFloat(event.newState);

            if (!isNaN(prevVolume) && !isNaN(currentVolume)) {
                let streams = {
                    "Juli": [items.ChromeNestJuliStreamLaut?.state, items.ChromeNestJuliStreamLeise?.state],
                    "Luisa": [items.ChromeNestLuisaStreamLaut?.state, items.ChromeNestLuisaStreamLeise?.state],
                    "Jonas": [items.ChromeNestJonasStreamLaut?.state, items.ChromeNestJonasStreamLeise?.state]
                };

                if (streams[user] && streams[user][0] && streams[user][1]) {
                    let streamURL = prevVolume > currentVolume ? streams[user][1] : streams[user][0];
                    let streamName = streamMappings[streamURL] || "Unbekannter Stream";
                    let lautLeise = prevVolume > currentVolume ? "leiser" : "lauter";

                    console.info(`NestWecker /Nest Audio Control/🎶: Lautstärke Änderung erkannt. Starte ${lautLeise} Stream: ${streamName} (${streamURL}) auf ${device}`);

                    announceStreamStart(user, streamName, device);
                    waitForAnnouncementToEnd(user, device, streamURL);
                } else {
                    console.warn(`NestWecker /Nest Audio Control/⚠️: Keine gültige Stream-Konfiguration für ${user} gefunden`);
                }
            }
        } else if (event.itemName.endsWith("Player")) {
            let action = event.newState;
            console.info(`NestWecker /Nest Audio Control/🎵: Aktion erkannt für ${user}: ${action}`);

            let radioStreams = {
                "SWR3": "https://liveradio.swr.de/sw282p3/swr3/play.mp3",
                "RockAntenne": "https://stream.rockantenne.de/rockantenne/stream/mp3",
                "Regenbogen": "https://stream.regenbogen.de/karlsruhe/mp3-128",
                "Sunshine_EDM": "https://stream.sunshine-live.de/edm/mp3-192/stream.sunshine-live.de/"
            };

            if (radioStreams[action]) {
                console.info(`NestWecker /Nest Audio Control/: Starte Radiostream ${action} mit der URL ${radioStreams[action]} auf ${device}`);

                announceStreamStart(user, action, device);
                waitForAnnouncementToEnd(user, device, radioStreams[action]);
            } else {
                console.warn(`NestWecker /Nest Audio Control/: ⚠️ Unbekannte Aktion ${action}`);
            }
        }
    }
});


/**
 * 🚀 **Warte auf Ende der Sprachausgabe, dann starte Stream**
 */
function waitForAnnouncementToEnd(user, device, streamURL) {
let currentTimeItem = `ChromeNest${user}CurrentTime`;
let maxWaitTime = 10000; // Max. 10 Sekunden warten
waitInterval[user] = 4000; // Prüfe Anfangs nach 4 s und dann alle 500ms
waitedTime[user] = 0;

console.info(`NestWecker /waitForAnnouncementToEnd/:⏳ Warte auf Ende der Ansage für ${user} auf ${device}...`);

// Falls eine alte Instanz läuft, vorher beenden
if (checkAnnouncementEnd[user]) clearInterval(checkAnnouncementEnd[user]);
if (checkPlayed[user]) clearTimeout(checkPlayed[user]);

checkAnnouncementEnd[user] = setInterval(() => {
    let currentTime = items[currentTimeItem]?.state?.toString() || "UNDEF";

    if (currentTime === "0 s" || currentTime === "UNDEF") {
    if (checkAnnouncementEnd[user]) {
        clearInterval(checkAnnouncementEnd[user]);
        delete checkAnnouncementEnd[user];
    }

        console.info(`NestWecker /waitForAnnouncementToEnd/:✅ Ansage für ${user} beendet. Starte Stream ${streamURL} auf ${device}`);
        
        setOutputTriggered(user);
        actions.Audio.playStream(device, null); // Stoppt aktuellen Stream
        console.info(`NestWecker /waitForAnnouncementToEnd/:✅ Stream gelöscht für ${user}`);
        // Falls bereits ein Timeout existiert, löschen
        if (checkPlayed[user]) clearTimeout(checkPlayed[user]);
        setTimeout(() => {
            setOutputTriggered(user);
            actions.Audio.playStream(device, streamURL);
            console.info(`NestWecker /waitForAnnouncementToEnd/:✅ Stream aktiviert für ${user}`);
        }, 1000); // 1 Sekunde warten, bevor der neue Stream startet
        console.info(`NestWecker /waitForAnnouncementToEnd/:✅ TimeOut gestartet für ${user}`);
    } else if (waitedTime[user] >= maxWaitTime) {
    if (checkAnnouncementEnd[user]) {
        clearInterval(checkAnnouncementEnd[user]);
        delete checkAnnouncementEnd[user];
    }
        console.warn(`NestWecker /waitForAnnouncementToEnd/:⚠️ Timeout erreicht für ${user} auf ${device}! Starte Stream trotzdem.`);
        setOutputTriggered(user);
        actions.Audio.playStream(device, null); // Stoppt aktuellen Stream
        
        // Falls bereits ein Timeout existiert, löschen
        if (checkPlayed[user]) clearTimeout(checkPlayed[user]);
        setTimeout(() => {
            setOutputTriggered(user);
            actions.Audio.playStream(device, streamURL);
        }, 1000); // 1 Sekunde warten, bevor der neue Stream startet
    }

    waitedTime[user] += waitInterval[user];
    waitInterval[user] = 500; // Nach dem ersten Durchlauf auf 500ms reduzieren
}, waitInterval[user]);
};

/**
 * 🚀 **Sprachausgabe vor dem Stream**
 * Diese Methode kündigt an, dass ein Stream für den Benutzer gestartet wird.
 */
function announceStreamStart(user, streamName, device) {
    console.info(`NestWecker /announceStreamStart/:📢 Ankündigung für ${user} auf ${device}: ${streamName}`);
    // **Setzt OutputTriggered bei Lautstärkeänderung**
    setOutputTriggered(user);
    actions.Voice.say(`Der Stream ${streamName} für ${user} wird gespielt.`, "pipertts:ramona-de_DE", device);
};






// // **🚀 Methode zum Abrufen einer zufälligen lokalen Audiodatei für einen Benutzer**
// function getRandomSoundFile(user) {
//     let folder = soundFolders[user];
//     if (!folder) {
//         console.warn(`NestWecker /getRandomSoundFile/:⚠️ Kein Sound-Ordner für ${user} definiert.`);
//         return null;
//     }

//     try {
//         let files = os.listFiles(folder);
//         let audioFiles = files.filter(file => file.endsWith(".mp3") || file.endsWith(".wav"));
//         if (audioFiles.length > 0) {
//             let selectedFile = "file://" + folder + audioFiles[Math.floor(Math.random() * audioFiles.length)];
//             console.info(`NestWecker /getRandomSoundFile/:🎵 Zufällige Datei für ${user}: ${selectedFile}`);
//             return selectedFile;
//         } else {
//             console.warn(`NestWecker /getRandomSoundFile/:⚠️ Keine Audiodateien im Ordner von ${user} gefunden.`);
//             return null;
//         }
//     } catch (error) {
//         console.error(`NestWecker /getRandomSoundFile/:🚨 Fehler beim Abrufen der Audiodateien für ${user}: ${error}`);
//         return null;
//     }
// };

// **🚀 Methode zum Abrufen des aktuellen Wochentags**
function getCurrentWeekday() {
    const weekdays = ["SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY"];
    return weekdays[new Date().getDay()]; // Gibt immer einen verlässlichen Wochentag zurück
}

rules.JSRule({
    name: "Weckersteuerung",
    description: "Prüft jede Minute, ob ein Wecker aktiviert ist und gestartet werden muss",
    triggers: [triggers.GenericCronTrigger("0 * * * * ?")], // Läuft jede Minute
    execute: function () {
        // console.info(`NestWecker /Weckersteuerung/:🔔 Wecker-Check gestartet.`);
        let now = new Date();
        let hour = now.getHours();
        let minute = now.getMinutes();
        let weekday = getCurrentWeekday(); // "MONDAY", "TUESDAY", etc.
    // Ausgabe des kompletten Status aller Benutzer
        // console.info(`NestWecker /Weckersteuerung/: ⏰ Aktueller Benutzerstatus: ${JSON.stringify(userStatus)}`);
        // console.info(`NestWecker /Weckersteuerung/:⏰ Aktuelle Uhrzeit ${hour}:${minute}.`);

        ["Juli", "Luisa", "Jonas"].forEach(user => {
            let weckerItem = `${user}_Wecker_${weekday}`;
            let weckerHourItem = `${user}_Wecker_h_${weekday}`;
            let weckerMinuteItem = `${user}_Wecker_m_${weekday}`;

            // **Sichere Werte für Wecker, Stunde & Minute setzen**
            let weckerState = items[weckerItem]?.state?.toString() ?? "OFF";
            let weckerHour = parseInt(items[weckerHourItem]?.state ?? NaN);
            let weckerMinute = parseInt(items[weckerMinuteItem]?.state ?? NaN);

            // **Fehlermeldung, falls Werte ungültig sind**
            if (isNaN(weckerHour) || isNaN(weckerMinute)) {
                console.warn(`NestWecker /Weckersteuerung/:⚠️ Wecker-Fehler für ${user}: Ungültige Werte für Stunde (${items[weckerHourItem]?.state ?? "NULL"}) oder Minute (${items[weckerMinuteItem]?.state ?? "NULL"})`);
                return;
            }

            // console.info(`NestWecker /Weckersteuerung/:⏰ Prüfe Wecker für ${user} am ${weekday} mit ${weckerHour}:${weckerMinute}.`);

            if (weckerState === "ON" && weckerHour === hour && weckerMinute === minute) {
                startAlarm(user);
                console.info(`NestWecker /Weckersteuerung/:⏰ Wecker für ${user} am ${weekday} wird gestartet.`);
            }
        });
    }
});


// /**
//  * 🚀 **Prüft, ob eine Audiodatei existiert**
//  */
// function isValidAudioFile(filePath) {
//     if (!filePath || typeof filePath !== "string") {
//         return false;
//     }

//     let fileSystemPath = filePath.replace("file://", ""); // Entfernt `file://` für lokale Datei-Pfade
//     let directory = fileSystemPath.substring(0, fileSystemPath.lastIndexOf("/")); // Extrahiere den Ordner
//     let fileName = fileSystemPath.split("/").pop(); // Nur den Dateinamen extrahieren

//     try {
//         let files = os.listFiles(directory); // Liste alle Dateien im Verzeichnis
//         return files.includes(fileName); // Prüfe, ob die Datei in der Liste enthalten ist
//     } catch (error) {
//         console.error(`Player Wecker: ❌ Fehler bei der Datei-Prüfung (${filePath}): ${error}`);
//         return false;
//     }
// };


/**
 * 🚀 **Wecker-Start mit Lautstärkeregelung & Random**
 */
function startAlarm(user) {
    console.info(`NestWecker /startAlarm/:🎵 Wecker: Starte für ${user}`);

    //if (!userStatus[user]) userStatus[user] = { isAlarmTriggered: true, announcementDone: false };
    // jwu
userStatus[user].isAlarmTriggered = true;
userStatus[user].announcementDone = false;
    // let userFolder = soundFolders[user] || "/etc/openhab/sounds/";
    // let isRandom = (items[`${user}_Wecker_Random`]?.state?.toString() === "ON");
    // let songToPlay = isRandom ? getRandomSoundFile(user) : items[`${user}_Wecker_StartSong`]?.state?.toString() ?? null;
    let songToPlay = items[`${user}_Wecker_StartSong`]?.state?.toString() ?? null;
    let device = userDevices[user];
    // **Prüfen, ob die Datei existiert, sonst Fallback auf "relax.mp3"**
    // let songPath = `${userFolder}${songToPlay}`;
    // if (!isValidAudioFile(songPath)) {
    //     console.warn(`NestWecker //:⚠️ Datei ${songPath} nicht gefunden. Verwende Ersatzdatei relax.mp3.`);
    //     songPath = `relax.mp3`;
    // }

    let startVolume = parseInt(items[`${user}_Wecker_StartVolume`]?.state ?? 5);
    let maxVolume = parseInt(items[`${user}_Wecker_MaxVolume`]?.state ?? 40);
    let upVolume = parseInt(items[`${user}_Wecker_UpVolume`]?.state ?? 5);
    let upTimeVolume = (parseInt(items[`${user}_Wecker_UpTimeVolume`]?.state ?? 10) * 1000);

    let volumeItem = `ChromeNest${user}Volume`;

    console.info(`NestWecker /startAlarm/:🔊 Starte mit Lautstärke: ${startVolume}% (Max: ${maxVolume}%) für ${user} Steigerung alle ${upTimeVolume} Millisekunden um ${upVolume}%`);

    // **Setzt OutputTriggered**
    setOutputTriggered(user);
    // **Setze initiale Lautstärke mit `sendCommand`**
    items[volumeItem].sendCommand(startVolume);

    //console.info(`Player Wecker: ⏰ Spiele Wecklied für ${user} auf Engerät §{device}: ${songPath}`);
    console.info(`NestWecker /startAlarm/: ⏰ Spiele Wecklied für ${user} auf ${device}: ${songToPlay}`);
    setOutputTriggered(user);
    actions.Audio.playSound(device, songToPlay);

    // **🚀 Schrittweise Lautstärkenerhöhung (Snooze-Erkennung)**
    let volumeIncrease = setInterval(() => {
        if (!userStatus[user]?.isAlarmTriggered) {
            console.info(`NestWecker /startAlarm/:🛑 Lautstärkenanpassung gestoppt, da Wecker für ${user} deaktiviert wurde.`);
            clearInterval(volumeIncrease);
            return;
        }

        let currentVolume = parseInt(items[volumeItem]?.state) ?? startVolume;
        if (currentVolume < maxVolume) {
            let newVolume = Math.min(currentVolume + upVolume, maxVolume);
            // **Setzt OutputTriggered bei Lautstärkeänderung**
            setOutputTriggered(user);
            // **Setzt neue Lautstärke mit `sendCommand`**
            items[volumeItem].sendCommand(newVolume);
            console.info(`NestWecker /startAlarm/:🔊 Erhöhe Lautstärke auf: ${newVolume}% für ${user}`);
        } else {
            console.info(`NestWecker /startAlarm/:🔊 Maximale Lautstärke erreicht: ${maxVolume}% für ${user}`);
            clearInterval(volumeIncrease);
        }
    }, upTimeVolume);
};



// **🚀 Sprachausgabe**
function makeAnnouncement(user) {
    if (!userStatus[user]?.isAlarmTriggered) {
        console.warn(`NestWecker /makeAnnouncement/:❌ Wecker wurde deaktiviert. Sprachausgabe abgebrochen.`);
        return;
    }

    let now = new Date();
    let currentTime = `${now.getHours()} Uhr ${now.getMinutes().toString().padStart(2, '0')}`;
    let device = userDevices[user];

    let genderItem = `${user}_Wecker_Gender`;
    let gender = (items[genderItem] === "ON") ? "Lieber" : "Liebe";

    console.info(`NestWecker /makeAnnouncement/:📢 Wecker: Sprachausgabe für ${user} mit Gender: ${gender} auf  ${device}`);
    setOutputTriggered(user);
    actions.Voice.say(`Guten Morgen ${gender} ${user}, es ist ${currentTime}. Bitte werde langsam wach! Guten morgen ${user}!`, "pipertts:ramona-de_DE", device);
};

// **🚀 Radiostream starten mit Fallback**
function startRadio(user) {
    if (!userStatus[user]?.isAlarmTriggered) {
        console.warn(`NestWecker /startRadio/:❌ Wecker wurde deaktiviert. Radio nicht gestartet.`);
        return;
    }
    let device = userDevices[user];
    let streamURL = items[`${user}_Wecker_Stream`]?.state?.toString() ?? "https://stream.rockantenne.de/rockantenne/stream/mp3";

    console.info(`NestWecker /startRadio/:📻 Starte Radiostream für ${user} mit folgendem Stream: ${streamURL} auf ${device}`);
    setOutputTriggered(user);
    actions.Audio.playStream(device, null); // Stoppt aktuellen Stream
    setTimeout(() => {
        if (!userStatus[user]?.isAlarmTriggered) {
            console.warn(`NestWecker /startRadio_Warten/:❌ Wecker wurde deaktiviert. Radio nicht gestartet.`);
            return;
        }
        userStatus[user].isAlarmTriggered = false;
        userStatus[user].announcementDone = false;
        setOutputTriggered(user);
        actions.Audio.playStream(device, streamURL);
    }, 1000); // 1 Sekunde warten, bevor der neue Stream startet
};



// **🚀 Gemeinsame Regel für Lied-Ende & Sprachausgabe-Ende**
rules.JSRule({
    name: "Wecker Lied- oder Sprachausgabe-Ende",
    description: "Reagiert, wenn ein Song oder die Sprachausgabe endet",
    triggers: [
        triggers.ItemStateChangeTrigger("ChromeNestJuliCurrentTime"),
        triggers.ItemStateChangeTrigger("ChromeNestJonasCurrentTime"),
        triggers.ItemStateChangeTrigger("ChromeNestLuisaCurrentTime")
    ],
    execute: handleWeckerEvent
});

function handleWeckerEvent(event) {

    if (!isSystemStarted()) {
        console.info("NestWecker /Nest Audio Control/⚠️: OpenHAB ist noch im Startvorgang (< 5 min). Ignoriere Regel.");
        return;
    }

    let itemName = event.itemName;

    let match = itemName.match(/ChromeNest(.*)CurrentTime/);
    let user = match ? match[1] : null;
    if (!user) {
        console.warn(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:❌ Kein Benutzer aus ${itemName} extrahiert.`);
        return;
    }

    //let outputTriggerItem = `${user}_OutputTriggered`; // OutputTrigger-Check
    let outputDurationItem = `ChromeNest${user}Duration`;
    let maxVolumeItem = `${user}_Wecker_MaxVolume`;

    let volumeItem = `ChromeNest${user}Volume`;
    let outputDuration = items[outputDurationItem]?.state?.toString();

    if (!userStatus[user]?.isAlarmTriggered) {
        // console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:⚠️ Ignoriert für ${user}, da kein Alarm aktiv ist.`);
        return;
    }

    // **Prüfen, ob `OutputTriggered` aktiv ist**
            //if (items[outputTriggerItem] === "ON") {
    // if (isTimeTriggerOlderThan2Seconds(user)) {
    //     console.log(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:⚠️ Die gespeicherte Zeit für ${user} liegt mehr als 2 Sekunden zurück.`);
    // } else {
    //     console.log(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:⚠️ Die gespeicherte Zeit für ${user} ist noch aktuell.`);
    //     console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:⚠️ Ignoriert für ${user}, da OutputTriggered aktiv ist.`);
    //     return;
    // }

    // if (userStatus[user]?.outputTriggered == true) {
    //     console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:⚠️ Ignoriert für ${user}, da OutputTriggered aktiv ist.`);
    //     return;
    // }

    let newState = event.newState !== null ? event.newState.toString() : "UNDEF";
    let oldState = event.oldState !== null ? event.oldState.toString() : "0 s";

    let isStateTransitionToUndef = (oldState === "0 s" && newState === "UNDEF");
    console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:📻 Playtime für ${user} geändert: Alt=${oldState}, Neu=${newState}, Spielzeit: ${outputDuration}`);

    if (isStateTransitionToUndef) {
        console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:🔄 Zustand von 0 auf UNDEF erkannt für ${user}.`);
        
        if (!userStatus[user]?.announcementDone) {
            // **Sprachausgabe starten**
            userStatus[user].announcementDone = true;
            console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:🔊 Musik für ${user} beendet. Starte Sprachausgabe.`);
            // **Setzt OutputTriggered bei Lautstärkeänderung**
            setOutputTriggered(user);
            items[volumeItem].sendCommand(maxVolumeItem);
            makeAnnouncement(user);
        } else {
            // **Radio starten**
            console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:📻 Sprachausgabe für ${user} beendet. Starte Radiostream.`);
            startRadio(user);
        }
    }
};

 rules.JSRule({
     name: "Wecker Stop/Snooze",
     description: "Reagiert auf Benutzerinteraktion mit dem Wecker",
     triggers: [
         triggers.ItemStateChangeTrigger("ChromeNestLuisaControl"),
         triggers.ItemStateChangeTrigger("ChromeNestJonasControl"),
         triggers.ItemStateChangeTrigger("ChromeNestJuliControl"),
         triggers.ItemStateChangeTrigger("ChromeNestLuisaVolume"),
         triggers.ItemStateChangeTrigger("ChromeNestJonasVolume"),
         triggers.ItemStateChangeTrigger("ChromeNestJuliVolume")
     ],
     execute: function (event) {
        // **Ignorieren während OpenHAB-Start**
        if (!isSystemStarted()) {
            console.info("NestWecker /Wecker Stop/Snooze/:⚠️ OpenHAB ist noch im Startvorgang (< 5 min). Ignoriere Regel.");
            return;
        };
    
         let user = event.itemName.replace("ChromeNest", "").replace(/(Control|Volume)/, "");
         // **Prüfen, ob `userStatus` existiert**
        if (!userStatus[user]?.isAlarmTriggered) {
            console.info(`NestWecker /Wecker Stop/Snooze/:⚠️ Ignoriert für ${user}, da kein Alarm aktiv ist.`);
            return;
        };

        //  let outputTriggerItem = `${user}_OutputTriggered`;

        // // **Prüfen, ob `OutputTriggered` aktiv ist**

        if (isTimeTriggerOlderThan2Seconds(user)) {
            console.log(`NestWecker /Wecker Stop/Snooze/:⚠️ Die gespeicherte Zeit für ${user} liegt mehr als 2 Sekunden zurück.`);
        } else {
            console.log(`NestWecker /Wecker Stop/Snooze/:⚠️ Die gespeicherte Zeit für ${user} ist noch aktuell.`);
            console.info(`NestWecker /Wecker Stop/Snooze/:⚠️ Ignoriert für ${user}, da OutputTriggered aktiv ist.`);
            return;
        }

        // if (!userStatus[user]?.outputTriggered) {
        //     console.info(`NestWecker /Wecker Stop/Snooze/:⚠️ Ignoriert für ${user}, da OutputTriggered aktiv ist.`);
        //     userStatus[user]?.outputTriggered = false;
        //     console.info(`NestWecker /Wecker Stop/Snooze/:✅ OutputTrigger durch Timer für  ${user} auf FALSE geändert`);
        //     return;
        // };

        console.info(`NestWecker /Wecker Stop/Snooze/:🔔 Regel getriggert durch ${event.itemName}`);
        if (event.itemName.endsWith("Control")) {
            if (event.newState === "PAUSE" || event.newState === "STOP") {
                console.info(`NestWecker /Wecker Stop/Snooze/:🛑 Wecker für ${user} gestoppt.`);
                // userStatus[user].isAlarmTriggered = false;
                // userStatus[user].announcementDone = false;

                // **Setzt OutputTriggered bei Wecker-Stopp**
                setOutputTriggered(user);
            }
        } else if (event.itemName.endsWith("Volume")) {
            console.info(`NestWecker /Wecker Stop/Snooze/:⏳ Snooze für ${user} erkannt.`);
            // userStatus[user].isAlarmTriggered = false;
            // userStatus[user].announcementDone = false;

            // **Setzt OutputTriggered bei Snooze**
            setOutputTriggered(user);
        }
    }
 });





// **🚀 Mapping der Schulstundenzeiten**
let schoolStartTimes = {
    1: { time: "07:33", label: "erste Stunde" },
    2: { time: "08:23", label: "zweite Stunde" },
    3: { time: "09:03", label: "dritte Stunde" },
    4: { time: "10:07", label: "vierte Stunde" },
    5: { time: "10:57", label: "fünfte Stunde" },
    6: { time: "11:43", label: "sechste Stunde" }
};

// **🚀 Prüfen, ob Schulzeiten definiert sind**
if (!schoolStartTimes || Object.keys(schoolStartTimes).length === 0) {
    console.error("NestWecker /Schulansagen/: ❌ Fehler: Keine Schulzeiten definiert!");
} else {
    console.info(`NestWecker /Schulansagen/: ✅ Schulzeiten erfolgreich geladen.`);
};


// **🚀 Generiere Cron-Trigger für Wochentage und Schulstunden
const cronExpressions = Object.values(schoolStartTimes)
    .map(({ time }) => {
        let [hour, minute] = time.split(":");
        return `0 ${minute} ${hour} ? * MON-FRI *`; // Nur Montag bis Freitag
    });

// **🚀 Regel mit dynamischen CRON-Triggern für jede Schulstunde**
rules.JSRule({
    name: "Schulansagen",
    description: "Prüft zu den Schulzeiten, ob eine Schulansage nötig ist",
    triggers: cronExpressions.map(cron => triggers.GenericCronTrigger(cron)), // Dynamische CRONs
    execute: function () {
        console.info("NestWecker /Schulansagen/: 🔔 Schulansagen-Check gestartet.");
        let now = new Date();
        let weekday = getCurrentWeekday(); // "MONDAY", "TUESDAY", etc.
        let currentTime = now.getHours().toString().padStart(2, '0') + ":" + now.getMinutes().toString().padStart(2, '0');

        ["Jonas", "Luisa", "Juli"].forEach(user => {
            let weckerTagItem = `${user}_Wecker_${weekday}`;
            let weckerSchoolItem = `${user}_Wecker_s_${weekday}`;

            // **🚀 Sicherstellen, dass das Item existiert**
            if (!items.existsItem(weckerSchoolItem)) {
                console.warn(`NestWecker /Schulansagen/: ⚠️ Item ${weckerSchoolItem} existiert nicht! Überspringe ${user}.`);
                return;
            }

            let schoolArryElement = items[weckerSchoolItem]?.numericState ?? 0;
            // Prüfen ob eine Schulstunde ausgewählt wurde
            if (schoolArryElement == 0) return;
            let weckerState = items[weckerTagItem]?.state?.toString() ?? "OFF";

            if (weckerState === "ON" && schoolStartTimes[schoolArryElement]?.time === currentTime) {
                makeSchoolAnnouncement(user, schoolArryElement);
            }
        });
    }
});

// **🚀 Funktion für die Schulansage mit Geräteziel**
function makeSchoolAnnouncement(user, schoolHour) {
    let device = userDevices[user];
        
    if (!device) {
        console.warn(`NestWecker /Schulansagen/: ❌ Kein Gerät für ${user} gefunden. Schulansage abgebrochen.`);
        return;
    }

    let now = new Date();
    let currentTime = now.getHours() + ":" + now.getMinutes().toString().padStart(2, '0');

    if (schoolStartTimes[schoolHour]) {
        console.info(`NestWecker /Schulansagen/: 📢 Schulansage für ${user} auf ${device} - Start: ${schoolStartTimes[schoolHour].label}`);
        // **Setzt OutputTriggered bei Lautstärkeänderung**
        setOutputTriggered(user);
        actions.Audio.say(`Es ist ${currentTime}. Bitte stelle das Spielen ein und richte dich, damit du pünktlich zur ${schoolStartTimes[schoolHour].label} in die Schule kommst.`, "pipertts:ramona-de_DE", device, 75);
    }
};

Some of the rule is commented out, due to future improvements like random music file and the right now received Error.
Comments are mostly in German.

Best regards

There are so many timeouts and timers being created in this code all I can guess is that they are interfearing with eachother somehow.

This code seems overly complex and there’s a lot of repeated code and there are lines that make no sense.

For example:

Why create timers here? Why not just do the work? And except for the log statement, there’s no work to do anyway. When this file gets loaded, userStatus gets created. It doesn’t have any data in it to be reset. If you need to initialize it, just initialize it with the data you need. Don’t go through creating a bunch fo timers which will all trigger at the same time to do the work (note they won’t actually run at the same time anyway, they will run one after the other).

There really is so much going on here I can’t begin to guess what might be happening. You’ve a mix of OH timers and timeouts. As far as I can tell many of these timers are not required. Lots of timers schedule to run at exactly the same time.

:person_shrugging:

Hi,
I try to express myself more clearly and define my requirements

I have implemented a working alarm control system with the following sequence:

  1. A predefined song starts playing (function startAlarm(user)) while the volume gradually increases.
  2. Once the song ends, a voice announcement (function makeAnnouncement(user)) is triggered.
  3. After the announcement finishes, a radio stream starts (startRadio(user)), which must be manually stopped.

This setup works exactly as intended.

Now, I want to add an additional feature:
While the wake-up song or the announcement is playing, the system should detect if a button on the speaker has been pressed.

  • Play/Pause button → Disable the alarm → stop anything
  • Volume UP/DOWN buttons → Activate a snooze timer → after Snooze Alarm starts again with (function makeAnnouncement(user))

Problem: Differentiating Between Rule-Based Commands and User Interaction

My approach was to solve this using timers:

  • Each time my rule sends a command (e.g., starting the song, adjusting the volume), a timer starts.
  • After two seconds, the timer expires – if a change is detected after this time, it can be considered a user interaction.
  • If another rule-based command is sent within these two seconds (e.g., increasing volume, song ending), the timer should restart.

Unfortunately, I’m facing an issue:
Every time I try to restart the timer, I get an error. It seems like I’m not handling the existing timer correctly.

Right now only on SetTimeOut is used to send of OFF comand to the speaker 1s prior starting a stream.
Maybe there are some declariation issus as well but it works right now.

Does anyone have an idea how to implement this properly?
Thanks in advance!

Best regards,
Julian

const { rules, triggers, items, actions } = require('openhab');

let userStatus = {}; // Speichert den Wecker-Status pro Benutzer

// Benutzer initialisieren
["Juli", "Luisa", "Jonas"].forEach(user => {
if (!userStatus[user]) {
    userStatus[user] = { isAlarmTriggered: false, announcementDone: false, outputTriggered: false};
    console.info(`NestWecker /isSystemStarted/: System Variable angelegt: ${user} mit den Werten ${JSON.stringify(userStatus[user])}`);
}
});

const streamMappings = {
"https://liveradio.swr.de/sw282p3/swr3/play.mp3": "SWR3",
"https://stream.sunshine-live.de/edm/mp3-192/stream.sunshine-live.de/": "Sunshine EDM",
"https://stream.regenbogen.de/karlsruhe/mp3-128": "Radio Regenbogen",
"https://stream.rockantenne.de/rockantenne/stream/mp3": "RockAntenne",
"https://stream.regenbogen2.de/bawue/mp3-128/private" : "Rock FM"
};


// **🚀 Mapping von Benutzern auf ihre Chromecast-Geräte**
const userDevices = {
    "Juli": "chromecast:audio:ChromeNestJuli",
    "Luisa": "chromecast:audio:ChromeNestLuisa",
    "Jonas": "chromecast:audio:ChromeNestJonas"
};


// **🚀 Prüft, ob OpenHAB länger als 5 Minuten läuft**
function isSystemStarted() {
let uptime = parseInt(items["RP_Betriebszeit"]?.state ?? 0);
console.info(`NestWecker /isSystemStarted/:System läuft seit: ${uptime} Minuten`);
return uptime > 5;
};

// **🚀 Methode zum Abrufen des aktuellen Wochentags**
function getCurrentWeekday() {
    const weekdays = ["SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY"];
    return weekdays[new Date().getDay()]; // Gibt immer einen verlässlichen Wochentag zurück
}

rules.JSRule({
    name: "Weckersteuerung",
    description: "Prüft jede Minute, ob ein Wecker aktiviert ist und gestartet werden muss",
    triggers: [triggers.GenericCronTrigger("0 * * * * ?")], // Läuft jede Minute
    execute: function () {
        // console.info(`NestWecker /Weckersteuerung/:🔔 Wecker-Check gestartet.`);
        let now = new Date();
        let hour = now.getHours();
        let minute = now.getMinutes();
        let weekday = getCurrentWeekday(); // "MONDAY", "TUESDAY", etc.
    // Ausgabe des kompletten Status aller Benutzer
        // console.info(`NestWecker /Weckersteuerung/: ⏰ Aktueller Benutzerstatus: ${JSON.stringify(userStatus)}`);
        // console.info(`NestWecker /Weckersteuerung/:⏰ Aktuelle Uhrzeit ${hour}:${minute}.`);

        ["Juli", "Luisa", "Jonas"].forEach(user => {
            let weckerItem = `${user}_Wecker_${weekday}`;
            let weckerHourItem = `${user}_Wecker_h_${weekday}`;
            let weckerMinuteItem = `${user}_Wecker_m_${weekday}`;

            // **Sichere Werte für Wecker, Stunde & Minute setzen**
            let weckerState = items[weckerItem]?.state?.toString() ?? "OFF";
            let weckerHour = parseInt(items[weckerHourItem]?.state ?? NaN);
            let weckerMinute = parseInt(items[weckerMinuteItem]?.state ?? NaN);

            // **Fehlermeldung, falls Werte ungültig sind**
            if (isNaN(weckerHour) || isNaN(weckerMinute)) {
                console.warn(`NestWecker /Weckersteuerung/:⚠️ Wecker-Fehler für ${user}: Ungültige Werte für Stunde (${items[weckerHourItem]?.state ?? "NULL"}) oder Minute (${items[weckerMinuteItem]?.state ?? "NULL"})`);
                return;
            }

            // console.info(`NestWecker /Weckersteuerung/:⏰ Prüfe Wecker für ${user} am ${weekday} mit ${weckerHour}:${weckerMinute}.`);

            if (weckerState === "ON" && weckerHour === hour && weckerMinute === minute) {
                startAlarm(user);
                console.info(`NestWecker /Weckersteuerung/:⏰ Wecker für ${user} am ${weekday} wird gestartet.`);
            }
        });
    }
});



/**
 * 🚀 **Wecker-Start mit Lautstärkeregelung & Random**
 */
function startAlarm(user) {
    console.info(`NestWecker /startAlarm/:🎵 Wecker: Starte für ${user}`);
    
	userStatus[user].isAlarmTriggered = true;
	userStatus[user].announcementDone = false;
    let songToPlay = items[`${user}_Wecker_StartSong`]?.state?.toString() ?? null;
    let device = userDevices[user];

    let startVolume = parseInt(items[`${user}_Wecker_StartVolume`]?.state ?? 5);
    let maxVolume = parseInt(items[`${user}_Wecker_MaxVolume`]?.state ?? 40);
    let upVolume = parseInt(items[`${user}_Wecker_UpVolume`]?.state ?? 5);
    let upTimeVolume = (parseInt(items[`${user}_Wecker_UpTimeVolume`]?.state ?? 10) * 1000);

    let volumeItem = `ChromeNest${user}Volume`;

    console.info(`NestWecker /startAlarm/:🔊 Starte mit Lautstärke: ${startVolume}% (Max: ${maxVolume}%) für ${user} Steigerung alle ${upTimeVolume} Millisekunden um ${upVolume}%`);

    // **Setzt OutputTriggered**
    setOutputTriggered(user);
    // **Setze initiale Lautstärke mit `sendCommand`**
    items[volumeItem].sendCommand(startVolume);

    //console.info(`Player Wecker: ⏰ Spiele Wecklied für ${user} auf Engerät §{device}: ${songPath}`);
    console.info(`NestWecker /startAlarm/: ⏰ Spiele Wecklied für ${user} auf ${device}: ${songToPlay}`);
    setOutputTriggered(user);
    actions.Audio.playSound(device, songToPlay);

    // **🚀 Schrittweise Lautstärkenerhöhung (Snooze-Erkennung)**
    let volumeIncrease = setInterval(() => {
        if (!userStatus[user]?.isAlarmTriggered) {
            console.info(`NestWecker /startAlarm/:🛑 Lautstärkenanpassung gestoppt, da Wecker für ${user} deaktiviert wurde.`);
            clearInterval(volumeIncrease);
            return;
        }

        let currentVolume = parseInt(items[volumeItem]?.state) ?? startVolume;
        if (currentVolume < maxVolume) {
            let newVolume = Math.min(currentVolume + upVolume, maxVolume);
            // **Setzt OutputTriggered bei Lautstärkeänderung**
            setOutputTriggered(user);
            // **Setzt neue Lautstärke mit `sendCommand`**
            items[volumeItem].sendCommand(newVolume);
            console.info(`NestWecker /startAlarm/:🔊 Erhöhe Lautstärke auf: ${newVolume}% für ${user}`);
        } else {
            console.info(`NestWecker /startAlarm/:🔊 Maximale Lautstärke erreicht: ${maxVolume}% für ${user}`);
            clearInterval(volumeIncrease);
        }
    }, upTimeVolume);
};



// **🚀 Sprachausgabe**
function makeAnnouncement(user) {
    if (!userStatus[user]?.isAlarmTriggered) {
        console.warn(`NestWecker /makeAnnouncement/:❌ Wecker wurde deaktiviert. Sprachausgabe abgebrochen.`);
        return;
    }

    let now = new Date();
    let currentTime = `${now.getHours()} Uhr ${now.getMinutes().toString().padStart(2, '0')}`;
    let device = userDevices[user];

    let genderItem = `${user}_Wecker_Gender`;
    let gender = (items[genderItem] === "ON") ? "Lieber" : "Liebe";

    console.info(`NestWecker /makeAnnouncement/:📢 Wecker: Sprachausgabe für ${user} mit Gender: ${gender} auf  ${device}`);
    setOutputTriggered(user);
    actions.Voice.say(`Guten Morgen ${gender} ${user}, es ist ${currentTime}. Bitte werde langsam wach! Guten morgen ${user}!`, "pipertts:ramona-de_DE", device);
};

// **🚀 Radiostream starten mit Fallback**
function startRadio(user) {
    if (!userStatus[user]?.isAlarmTriggered) {
        console.warn(`NestWecker /startRadio/:❌ Wecker wurde deaktiviert. Radio nicht gestartet.`);
        return;
    }
    let device = userDevices[user];
    let streamURL = items[`${user}_Wecker_Stream`]?.state?.toString() ?? "https://stream.rockantenne.de/rockantenne/stream/mp3";

    console.info(`NestWecker /startRadio/:📻 Starte Radiostream für ${user} mit folgendem Stream: ${streamURL} auf ${device}`);
    setOutputTriggered(user);
    actions.Audio.playStream(device, null); // Stoppt aktuellen Stream
    setTimeout(() => {
        if (!userStatus[user]?.isAlarmTriggered) {
            console.warn(`NestWecker /startRadio_Warten/:❌ Wecker wurde deaktiviert. Radio nicht gestartet.`);
            return;
        }
        userStatus[user].isAlarmTriggered = false;
        userStatus[user].announcementDone = false;
        setOutputTriggered(user);
        actions.Audio.playStream(device, streamURL);
    }, 1000); // 1 Sekunde warten, bevor der neue Stream startet
};


// **🚀 Gemeinsame Regel für Lied-Ende & Sprachausgabe-Ende**
rules.JSRule({
    name: "Wecker Lied- oder Sprachausgabe-Ende",
    description: "Reagiert, wenn ein Song oder die Sprachausgabe endet",
    triggers: [
        triggers.ItemStateChangeTrigger("ChromeNestJuliCurrentTime"),
        triggers.ItemStateChangeTrigger("ChromeNestJonasCurrentTime"),
        triggers.ItemStateChangeTrigger("ChromeNestLuisaCurrentTime")
    ],
    execute: handleWeckerEvent
});

function handleWeckerEvent(event) {

    if (!isSystemStarted()) {
        console.info("NestWecker /Nest Audio Control/⚠️: OpenHAB ist noch im Startvorgang (< 5 min). Ignoriere Regel.");
        return;
    }

    let itemName = event.itemName;

    let match = itemName.match(/ChromeNest(.*)CurrentTime/);
    let user = match ? match[1] : null;
    if (!user) {
        console.warn(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:❌ Kein Benutzer aus ${itemName} extrahiert.`);
        return;
    }

    let outputDurationItem = `ChromeNest${user}Duration`;
    let maxVolumeItem = `${user}_Wecker_MaxVolume`;

    let volumeItem = `ChromeNest${user}Volume`;
    let outputDuration = items[outputDurationItem]?.state?.toString();

    if (!userStatus[user]?.isAlarmTriggered) {
        // console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:⚠️ Ignoriert für ${user}, da kein Alarm aktiv ist.`);
        return;
    }
    
    let newState = event.newState !== null ? event.newState.toString() : "UNDEF";
    let oldState = event.oldState !== null ? event.oldState.toString() : "0 s";

    let isStateTransitionToUndef = (oldState === "0 s" && newState === "UNDEF");
    // console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:📻 Playtime für ${user} geändert: Alt=${oldState}, Neu=${newState}, Spielzeit: ${outputDuration}`);

    if (isStateTransitionToUndef) {
       // console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:🔄 Zustand von 0 auf UNDEF erkannt für ${user}.`);
        
        if (!userStatus[user]?.announcementDone) {
            // **Sprachausgabe starten**
            userStatus[user].announcementDone = true;
            console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:🔊 Musik für ${user} beendet. Starte Sprachausgabe.`);
            // **Setzt OutputTriggered bei Lautstärkeänderung**
            setOutputTriggered(user);
            items[volumeItem].sendCommand(maxVolumeItem);
            makeAnnouncement(user);
        } else {
            // **Radio starten**
            console.info(`NestWecker /Wecker Lied- oder Sprachausgabe-Ende/:📻 Sprachausgabe für ${user} beendet. Starte Radiostream.`);
            startRadio(user);
        }
    }
};

 rules.JSRule({
     name: "Wecker Stop/Snooze",
     description: "Reagiert auf Benutzerinteraktion mit dem Wecker",
     triggers: [
         triggers.ItemStateChangeTrigger("ChromeNestLuisaControl"),
         triggers.ItemStateChangeTrigger("ChromeNestJonasControl"),
         triggers.ItemStateChangeTrigger("ChromeNestJuliControl"),
         triggers.ItemStateChangeTrigger("ChromeNestLuisaVolume"),
         triggers.ItemStateChangeTrigger("ChromeNestJonasVolume"),
         triggers.ItemStateChangeTrigger("ChromeNestJuliVolume")
     ],
     execute: function (event) {
        // **Ignorieren während OpenHAB-Start**
        if (!isSystemStarted()) {
            console.info("NestWecker /Wecker Stop/Snooze/:⚠️ OpenHAB ist noch im Startvorgang (< 5 min). Ignoriere Regel.");
            return;
        };
    
         let user = event.itemName.replace("ChromeNest", "").replace(/(Control|Volume)/, "");
         // **Prüfen, ob `userStatus` existiert**
        if (!userStatus[user]?.isAlarmTriggered) {
            console.info(`NestWecker /Wecker Stop/Snooze/:⚠️ Ignoriert für ${user}, da kein Alarm aktiv ist.`);
            return;
        };

        //  let outputTriggerItem = `${user}_OutputTriggered`;

        // // **Prüfen, ob `OutputTriggered` aktiv ist**

        if (isTimeTriggerOlderThan2Seconds(user)) {
           // console.log(`NestWecker /Wecker Stop/Snooze/:⚠️ Die gespeicherte Zeit für ${user} liegt mehr als 2 Sekunden zurück.`);
        } else {
            console.log(`NestWecker /Wecker Stop/Snooze/:⚠️ Die gespeicherte Zeit für ${user} ist noch aktuell.`);
            console.info(`NestWecker /Wecker Stop/Snooze/:⚠️ Ignoriert für ${user}, da OutputTriggered aktiv ist.`);
            return;
        }
       
        console.info(`NestWecker /Wecker Stop/Snooze/:🔔 Regel getriggert durch ${event.itemName}`);
        if (event.itemName.endsWith("Control")) {
            if (event.newState === "PAUSE" || event.newState === "STOP") {
                console.info(`NestWecker /Wecker Stop/Snooze/:🛑 Wecker für ${user} gestoppt.`);
                // userStatus[user].isAlarmTriggered = false;
                // userStatus[user].announcementDone = false;

                // **Setzt OutputTriggered bei Wecker-Stopp**
                setOutputTriggered(user);
            }
        } else if (event.itemName.endsWith("Volume")) {
            console.info(`NestWecker /Wecker Stop/Snooze/:⏳ Snooze für ${user} erkannt.`);
            // userStatus[user].isAlarmTriggered = false;
            // userStatus[user].announcementDone = false;

            // **Setzt OutputTriggered bei Snooze**
            setOutputTriggered(user);
        }
    }
 });

While I fully understand your desire to achieve the stated objective, may I offer a much easier alternative? Get another device to act as the stop / snooze buttons. You could get a Zigbee button from Ikea, they are very inexpensive. Press to snooze, double click or press and hold or shake to stop the alarm.

It would greatly simplify your code logic.

You should not cancel a timer within the timer. Maybe that’s going on? If you want to reschedule a timer from inside the code that runs when a timer runs, you should rescedule instead of cancelling.

It would probably be a lot better to store timers in the cache rather than global variables.

Beyond that, I’d have to spend hours studying this code to trace the flow through and I don’t have that kind of time.

I’ll second @jimtng’s suggestion though. Remember that your time have value too.