Rule optimization: Window OPEN reminder

Hi,

whit the help of this Thread from @rlkoshak i build this:

Contact     FHT_Bad_Fenster 			            "Bad Fenster[MAP(fht_fenster.map):%s]"  				                <iftswindow1w>          (FHT, FHTfenster)
DateTime    FHT_Bad_Fenster_LastUpdate              "Bad Fenster letztes Update [%1$tm.%1$td.%1$tY %1$tH:%1$tM]"            <itimeclock>            (FHT, FHTfensterUpdate)
Switch      FHT_Bad_Fenster_Timer                   "Bad Fenster Timer"                                                     <itimetimer>            (FHT, FHTfensterTimer)   {expire="15m,command=OFF" }
Number      FHT_Bad_Fenster_offenZeit               "Bad Fenster offen Zeit[%s]"                                            <itimetimer>            (FHT, FHToffenZeit)
Number      FHT_Bad_Fenster_maxoffenZeit            "Bad Fenster max öffnungs Zeit[%s]"                                     <itimetimer>            (FHT, FHTmaxoffenZeit)

rule "Fensterüberwachung"
when
    Item FHT_Bad_Fenster changed or
    Item FHT_Buero_Fenster_Hof changed or
    Item FHT_Buero_Fenster_Strasse changed or
    Item FHT_Wohnzimmer_Fenster_Tur changed or
	Item FHT_Wohnzimmer_Fenster_Bal changed or
	Item FHT_Wohnzimmer_Fenster_Hof changed or
	Item FHT_Schlafzimmer_Fenster changed or
    Item FHT_Kueche_Fenster changed
then

    val logname = 'FENSTERÜBERWACHUNG'

    Thread::sleep(100)

    val fenster = FHTfenster.members.filter[s|s.lastUpdate("mapdb") !== null].sortBy[lastUpdate("mapdb")].last as ContactItem 
    val timer = FHTfensterTimer.members.filter[t | t.name == fenster.name+"_Timer"].head as SwitchItem

    val lastUpdate = FHTfensterUpdate.members.filter[dt | dt.name == fenster.name + "_LastUpdate"].head as DateTimeItem
    lastUpdate.postUpdate(new DateTimeType)

    val StringBuilder msg = new StringBuilder
    msg.append = transform("MAP", "fenster.map", fenster.name)

    if(fenster.state == OPEN) {
        timer.sendCommand(ON)
        msg.append(" wurde geöffnet")
    } 
    else {
        timer.postUpdate(OFF)
        sendCommand(fenster.name+"_offenZeit", "0")
         msg.append(" wurde geschlossen")
     }
    var alert = false
    if(Tageszeit.state.toString == "Nacht" && fenster.state == OPEN){
        alert = true
        msg.append(" und es ist Nacht")
    }
    if(Anwesend.state == OFF){
        alert = true
        msg.append(" und niemand Zuhause")
    }
    if(alert){
        msg.append("!")
        FensterAlarm.sendCommand(msg.toString)
    }
    logInfo(logname, msg.toString)
    
    if(Anwesend.state == OFF){
        sendNotification("xxx", msg.toString)
    }
end

rule "Fenster Timer ausgelaufen"
when
    Item FHT_Bad_Fenster_Timer received command OFF or
    Item FHT_Buero_Fenster_Hof_Timer received command OFF or
    Item FHT_Buero_Fenster_Strasse_Timer received command OFF or
    Item FHT_Wohnzimmer_Fenster_Tur_Timer received command OFF or
	Item FHT_Wohnzimmer_Fenster_Bal_Timer received command OFF or
	Item FHT_Kueche_Fenster_Timer received command OFF or
	Item FHT_Schlafzimmer_Fenster_Timer received command OFF or
    Item FHT_Wohnzimmer_Fenster_Hof_Timer received command OFF
then

    val logname = 'FENSTERALARM'

    Thread::sleep(100)

    val timer = FHTfensterTimer.members.filter[t|t.lastUpdate("mapdb") !== null].sortBy[lastUpdate("mapdb")].last as SwitchItem
    var fensterName = timer.name.split("_Timer").get(0)
    val fenster = FHTfenster.members.filter[z | z.name == fensterName].head as ContactItem
    var offenZeit = FHToffenZeit.members.filter[o | o.name == fenster.name+"_offenZeit"].head as NumberItem
    val maxoffenZeit = FHTmaxoffenZeit.members.filter[m | m.name == fenster.name+"_maxoffenZeit"].head as NumberItem
    var erlaubt = maxoffenZeit.state as Number
    var dauer = offenZeit.state as Number

    val StringBuilder msg = new StringBuilder
    msg.append = transform("MAP", "fenster.map", fenster.name)

    if(fenster.state == OPEN) {
        dauer = dauer + erlaubt
        sendCommand(offenZeit, dauer)
        if( dauer == erlaubt ) {
            msg.append(" schon ")
            msg.append(dauer)
            msg.append(" Minuten offen!")
        } else {
            msg.append(" immer noch offen! (")
            msg.append(dauer)
            msg.append("min)")
        }
        logInfo(logname, msg.toString)
        if( Anwesend.state == OFF && dauer == erlaubt ) {
           sendNotification("xxx", msg.toString)
           sendNotification("xxx", msg.toString)
        }
        if( Virtuell_x1_Zuhause.state == ON ) {
            sendNotification("xxx", msg.toString)
        }
        if( Virtuell_x2_Zuhause.state == ON ) {
            sendNotification("xxx", msg.toString)
        }
        sendCommand(timer, ON)
    }
end

After a couple of days it seems to work :slight_smile:

i hope it helps somebody

Cheers
Chris

2 Likes

With the addition of the triggeringItem implicit variable, you no longer need to use the persistence hack to get the Item that triggered the Rule. Replace the first two lines after the val logname ... with:

val fenster = triggeringItem
2 Likes

Do you have any insight on the state of triggeringItem working with groups ?

I’ve successfully used jython logic to surface the triggering item from a group via a virtual item “interface”. Sorta-like a pass-by-reference (VERY loosely). It works but does create more (but simpler) maintenance points.

In work but not yet available. There will be a new trigger type to populate triggeringItem with the member that triggered the update to the Group. I couldn’t find the issue but I’m subscribed to it and don’t remember seeing that it has closed.

Once it is done we will have to wait for a new release of ESH to be merged with OH.

FWIW, here’s how I monitor my garage (if open for 10 mins)… It sends me a text message, and HABPanel speaker will say "Warning: Garage remained open."

val ruleForGarageInSeconds = 60 * 10 // 10 minutes
var Timer alarmReminderTimer = null
var DateTime garageLastOpened = null
val String emailRecipients = "__MYPHONENUMBER__@mms.att.net"

rule "Send Reminder for Garage - OPEN"
	
when
	Item alarmContactZone23GarageDoorDriveway changed to OPEN
then

	if (alarmReminderTimer == null) {
		garageLastOpened = now;
		logInfo("GarageCheck-isOpen", "Garage Opened @ " + garageLastOpened.toString("MM/dd/yy HH:mm:ss a") +
				" . Will check if still open in " + ruleForGarageInSeconds + " seconds."
		);
		
		alarmReminderTimer = createTimer(now.plusSeconds(ruleForGarageInSeconds), [|
			// Check every X seconds if OPEN
			
			// Garage is still open
			if (alarmContactZone23GarageDoorDriveway.state == OPEN) {
				postUpdate(hp_s_voicespeaker, "Warning: Garage remained open.");
				sendMail(
					emailRecipients, 
					"Warning: Garage remained open.", 
					"Warning: Garage is still open after " + ruleForGarageInSeconds + "seconds.\r\n"
						+ "Garage Opened @ " + garageLastOpened.toString("MM/dd/yy HH:mm:ss a") + ".\r\n"
						+ "Will re-check and re-send reminder after "+ ruleForGarageInSeconds + " seconds.");
				
				alarmReminderTimer.reschedule(now.plusSeconds(ruleForGarageInSeconds));	
				logInfo("GarageCheck-isOpen","Garage still open after " + ruleForGarageInSeconds + " seconds. Sending reminder.");
			}
			// Garage is still closed now			
			else {
				if (alarmReminderTimer != null) {
					alarmReminderTimer.cancel();
				}
					
				alarmReminderTimer = null				
			}
             
		]);
	} else {
		alarmReminderTimer.reschedule(now.plusSeconds(ruleForGarageInSeconds))
	}
end

rule "Send Reminder for Garage - CLOSED" 
when
	Item alarmContactZone23GarageDoorDriveway changed to CLOSED
then
	if (alarmReminderTimer != null)
		alarmReminderTimer.cancel();
		
	alarmReminderTimer = null;
	logInfo("GarageCheck-isClose","Garage closed. Timer stopped.");
end

Does this work? I’ve a vague recollection that I tried to reschedule a Timer from inside itself and it not working or not working consistently because you cannot reschedule a Timer that has expired and if you are inside the Timer body the Timer is by definition expired. It is one of the reasons I wrote Design Pattern: Recursive Timers. Maybe it works better now or the behavior changed. This would have been back in the 1.8 days I think.

This can be replaced with

alarmReminderTimer?.cancel

The ? operator will cause the line to simply be skipped if alarmReminderTimer is null. It is one of the nice pieces of a syntactic sugar the Xtext language added that I actually like.

Since we are posting examples :wink:

I use Design Pattern: Expire Binding Based Timers, Design Pattern: Associated Items, Design Pattern: Human Readable Names in Messages, and Design Pattern: Separation of Behaviors (for the alerts) to make the rule generic for all my doors:

Items:

Group:Contact:OR(OPEN,CLOSED) gDoorSensors "The doors are [MAP(en.map):%s]"
        <door>

Group:Number:SUM gDoorCounts "Open Doors [%d]"
  <door>

Contact vFrontDoor "Front Door is [MAP(en.map):%s]"
  <door> (gDoorSensors,gDoorCounts)
  { mqtt="<[mosquitto:entry_sensors/main/front_door:state:default]" }

Switch vFrontDoor_Timer
  (gDoorsTimers, gResetExpire)
  { expire="1h,command=OFF" }

DateTime vFrontDoor_LastUpdate "Front Door [%1$tm/%1$td %1$tH:%1$tM]"
  <time> (gDoorsLast)

// these three Items for remaining doors and windows

Rules:

rule "Keep track of the last time a door was opened or closed"
when
  Item vGarageOpener1 changed from CLOSED to OPEN or
  Item vGarageOpener1 changed from OPEN to CLOSED or
  Item vGarageOpener2 changed from CLOSED to OPEN or
  Item vGarageOpener2 changed from OPEN to CLOSED or
  Item vFrontDoor changed from CLOSED to OPEN or
  Item vFrontDoor changed from OPEN to CLOSED or
  Item vBackDoor changed from CLOSED to OPEN or
  Item vBackDoor changed from OPEN to CLOSED or
  Item vGarageDoor changed from CLOSED to OPEN or
  Item vGarageDoor changed from OPEN to CLOSED
then
  val door = triggeringItem as ContactItem

  // Update LastUpdate
  val last = gDoorsLast.members.filter[dt | dt.name == door.name+"_LastUpdate"].head as DateTimeItem // Associated Items DP
  last.postUpdate(new DateTimeType)

  // Set/cancel the Timer
  if(door.state == OPEN) sendCommand(door.name+"_Timer", "ON") // Associated Items DP, Expire Based Timers DP
  else postUpdate(door.name+"_Timer", "OFF") // Associated Items DP, Expired Based Timers DP

  // Log and alert
  val StringBuilder msg = new StringBuilder
  val doorName = transform("MAP", "en.map", door.name) // Human Readable Names in Messages

  msg.append(doorName)
  msg.append(" was")
  msg.append(if(door.state == OPEN) " opened" else " closed")

  var alert = false
  if(vTimeOfDay.state.toString == "NIGHT" || vTimeOfDay.state.toString == "BED"){ // Time of Day DP
        alert = true
        msg.append(" and it is night")
  }

  if(vPresent.state == OFF) { // Generic Based Presence Example
        alert = true
        msg.append(" and no one is home")
  }

  if(alert){
    // create Timer and wait before sending alert (I've a bad sensor, this prevents a barrage of alerts when it starts flapping)
    if(flappingTimers.get(triggeringItem.name) === null) {
      flappingTimers.put(triggeringItem.name, createTimer(now.plusSeconds(1), [|
            msg.append("!")
        aAlert.sendCommand(msg.toString) // Separation of Behaviors DP
        flappingTimers.put(triggeringItem.name, null)
      ]))
    }
    // Flapping, cancel the timer before the alert can be sent
    else {
      flappingTimers.get(triggeringItem.name).cancel
      flappingTimers.put(triggeringItem.name, null)
      logWarn(logName, triggeringItem.name + " is flapping!")
    }
  }
  else logInfo(logName, msg.toString)

end

// Expire Based Timers DP
rule "Timer expired for a door"
when
  Item vGarageOpener1_Timer received command OFF or
  Item vGarageOpener2_Timer received command OFF or
  Item vFrontDoor_Timer received command OFF or
  Item vBackDoor_Timer received command OFF or
  Item vGarageDoor_Timer received command OFF
then
  val doorName = transform("MAP", "en.map", triggeringItem.name) // Human Readable Names in Messages DP

  aAlert.sendCommand(doorName + " has been open for over an hour") // Separation of Behaviors DP

  if(vTimeOfDay.state.toString == "NIGHT" || vTimeOfDay.state.toString == "BED") { // Time of Day DP
        triggeringItem.sendCommand(ON) // reschedule the timer, Expire Based Timer DP
  }
end

Once I get back to the AIY, I’ll update my aAlert rule to also announce the message on the speaker.

I actually like this set of Rules because it illustrates how the DPs are not intended to be used in isolation. They can and should be used together. If you include the Generic Presence Detection tutorial, the above two rules use seven separate tutorials and examples (if you include the Design Pattern: Working with Groups in Rules which is really just a generic application of the way Groups are used in the other DPs).

The theory of operation is when a door changes state I update a DateTime Item to store when the last time the door was opened/closed.

If the door is opened I start a Timer. If the door is closed, I cancel the Timer.

The rest of the main rule is constructing and sending an alert message. I use a map file to convert the Item name into a more friendly name for the message, indicate whether the door was opened or closed, and then add additional warnings if it is night and/or no one is home.

If it is night or no one is home I want an alert. I’ve a bad door sensor that periodically goes nuts flapping between open and closed several times a second and it gets worse in the cold. So I added a little anti-flapping Timer code to wait a second before sending the alert. If the door changes state while this antiflapping timer is active we throw out the alert. Otherwise we send on the alert using the Separation of Behaviors DP.

Remember that Timer we set? When it goes OFF this second rule triggers where we again use the map to get a nicer version of the Item name and send an alert that the door has been open too long. If it is NIGHT or BED time, we reschedule the Timer so we get the alert repeatedly until the door closes or MORNING comes.

I’ve posted this before which is probably why it looks very similar to Chris’s.

2 Likes

Yeah I’m not 100% with xtend but ?. is actually common even for .Net and typescript. Looks like they are extending the language to match more popular ones. What I want them to add is the consistency of the " ; " as I am used to end everything with it. Lol. I often make that habit in python too hehehe

Out of curiosity, why not just trigger the rule based in group itself.

Working 100% .here’s the last text I got from it:

Warning: Garage is still open after 600seconds.

Garage Opened @ 01/26/18 07:41:38 AM.

Will re-check and re-send reminder after 600 seconds.

^direct copy paste from cellphone (yes I’m replying on my phone lol. Waiting in d parking lot for my son to come out from school hehehe).

Just a fyi… google assistants (even AIY) are also Chromecast, so you can use the Chromecast binding also. You can pipe text to a vlc and use that to stream it to the AIY. Or just send this command to the AIY:
repeat after me, ....the text...

Well, it doesn’t hurt to have them in the Rules DSL. But the only time it is required is if you want to use return false; to exit early.

There are several reasons, some of which will go away in the not too distant future.

There are only two ways to trigger a Rule using a Group and have the Rule trigger when members of the Group change or receive an update: changed or received update.

One can use changed if the Group’s aggregation function is properly configured so when the members of the Group change state the Group changes state in a detectable way which is usually possible using SUM. And this would probably work in this case but see below for why I don’t.

If one uses received update the Rule will trigger any time any member of the Group receives and update. However, as a side effect of how the Group’s state is calculated, the Rule will actually trigger N-1 times for that one update where N is the number of members of the Group. This would probably work here as well but it will take a lot of extra code to deal with the multiple triggers to avoid getting lots of alerts for one update to a member. Also, since the Rule triggers for all updates it means the Rule will trigger when I restoreOnStartup the members of the Group as the Items goe from NULL to what ever their restored states are.

So let’s assume I used changed with a sane aggregation function like SUM. Then I have to figure out which Item actually changed. There are two ways I can handle this. The first is to just generate an alert/set Timers, etc for everything that is OPEN and ignore which Item triggered the Rule. I use this approach in lots of places and it does work successfully. However, in this case it will take a lot more code to implement, more than the extra lines caused by listing all the Items as separate triggers.

The second is to use the Persistence LastUpdate hack (see the Working with Groups in Rules for details). That hack requires Persistence and a Thread:sleep at the front to give persistence a chance to save the latest states. It’s only two line of code so it isn’t that big of a deal, but I really don’t like the dependency on timing.

So, to ensure that the Rule only triggers once per Item change and to avoid needing to use the persistence hack and instead use the triggeringItem implicit variable I need to list each Item separately as triggers to the Rule.

Soon there will be introduced a new Rule trigger that we can use on Groups that will populate triggeringItem with the member of the Group that caused the Group to change or update. At that point I can use that new trigger with the Group without a lot of extra work.

About 90% of my posts, including this one, is from my phone. :slight_smile:

I can’t get it to show up as a device I can cast to. That was one of my first ideas but I dropped it as not supported. Maybe I need to look into it more. Searching online reveals mixed results with some saying it works and some saying it used to work but is no longer supported.

For now I’m going down the MQTT text message to send the announcement to the AIY. I may go back and try chromecast again.

1 Like

Does this really works? I tried it and it gives me an error.
Void function cannot return a value

If I use only return(); same

rule "test of return"
when
	Item testofreturn changed to ON
then
    testofreturn.postUpdate(ON)
    return false; //does not work
    return(false); //does not work
    return(); //does not work
    return //does not work
    testofreturn.postUpdate(OFF)
end

found it:

must be part of an if clause

if (testofreturn.state == testofreturn.state) {return}

or

if (testofreturn.state == testofreturn.state) return;

The syntax jumped around a few months back and I must have misremembered where it ended up. It would be just return;

It isn’t that it is part of an if clause that lets it work in your examples, it is that it has no argument and the semicolon.

I have a different approach. I just trigger a check rule by cron, every few minutes. I don’t check for summer, or do an amazon reminder but that could easily done. If a window is open for some time, i send a telegram message. All Window contacts are in the group gfk.

rule "fenster offen"
when
    Time cron "0 */5 * * * ? *"     	         
then
	var String msg = ""
    gfk.members.filter[t|t.lastUpdate("mapdb") !== null].filter(s| s.state.toString == "OPEN").forEach(item | 
	{	
        var Number minutes = (now.millis - item.lastUpdate.millis) / 60000
        if (minutes > 10) {
		   msg = String::format("%s - %s: seit %s minuten.\n", msg,  item.name.split("_").get(0), minutes.toString)
        }  
	})   
	if (msg != "") 
	{
		msg = String::format("*Fenster offen*\n%s", msg)
	    logInfo(logger, msg)
    	sendTelegram("openHAB", msg)
	}
end 
1 Like

@rlkoshak
return; does not work too, gives an error in the next line of code.

But with an if clause it is working for me.
if(1 == 1) return;

As written here


it makes no sense to return without an if clause, and I think this is right.

I have troubles with the rules found in the first post:

else if ((Door.state == OPEN)) && (Sommer.state!=ON)) {

I replaced it with
else if ((Door.state == OPEN)) && (Temperature.state < 15)) {

but I get the error
2018-02-19 18:24:20.900 [WARN ] [el.core.internal.ModelRepositoryImpl] - Configuration model ‘fensteroffencheck.rules’ has errors, therefore ignoring it: [14,36]: no viable alternative at input ‘&&’

[14,75]: no viable alternative at input ‘15’

[14,78]: mismatched input ‘)’ expecting ‘]’

[25,3]: missing EOF at ‘}’

I just want to shoot the rule if any door is open AND outside temperature is below 15 deg celsius

item Door is a group with all windows and doors
item Temperature is a number item from the weather binding:
Number Temperature “Temperature [%.2f °C]” {weather=“locationId=home, type=temperature, property=current”}

thanks

else if ((Door.state == OPEN)) && (Temperature.state < 15)) {

Should be

else if ((Door.state == OPEN) && (Temperature.state < 15)) {

Half of those parens are unnecessary

else if (Door.state == OPEN && Temperature.state < 15) {

I use duiffie´s rule and it work well. Is just extended it a little bit for my needs, but I´m still not familiar with javascript. Here is my code:

import java.util.Map

val Map<String, Timer> OpenWindowTimers = newHashMap

val Functions$Function2<ContactItem, Map<String, Timer>, Boolean> checkOpenWindow = [
	windowItem,
	timerMap |

	var String myTimerKey = windowItem.name.toString
	var String season_name = transform("MAP", "astro.map", Jahreszeit.state.toString)
	var winopentime = 0
	switch Jahreszeit {
		case Jahreszeit.state == "SPRING"	:	winopentime = 15
		case Jahreszeit.state == "SUMMER"	:	winopentime = 30
		case Jahreszeit.state == "AUTUMN"	:	winopentime = 10
		case Jahreszeit.state == "WINTER"	:	winopentime = 5
		default								:	winopentime = 5  //just in case if no season is set
	}
	
	logInfo("Fenster-Check", "Jahreszeit ist " + season_name )
	if (windowItem.state == CLOSED) {
		if (timerMap.get(myTimerKey) !== null) timerMap.get(myTimerKey).cancel()
	} else if (windowItem.state == OPEN) {
	timerMap.put(myTimerKey, createTimer(now.plusMinutes(winopentime)) [|
		timerMap.put(myTimerKey, null)

		val String shortName = transform("MAP", "windowShortName.map", windowItem.name.toString)
		val String longName = transform("MAP", "windowLongName.map", windowItem.name.toString)

		logInfo("Fenster-Check", shortName + " ist seit "+ winopentime +" Min. offen")
		sendBroadcastNotification(longName + " ist seit "+ winopentime +" Min. offen")
		echodotWZ_reminder.sendCommand(longName + " ist seit "+ winopentime +" Minuten offen")
	])
	}
	true
	]



rule "Fenster-Check"
when
	Item gWindoors received update
then
	logInfo("Fenster-Check", "Fenster/Tür hat sich verändert")
	Thread::sleep(500) // this gives the persistence service time to store the last update
	val lastUpdatedWindowItem = gWindoors.members.filter[s|s.lastUpdate("mapdb") !== null].sortBy[lastUpdate("mapdb")].last as ContactItem
	checkOpenWindow.apply(lastUpdatedWindowItem, OpenWindowTimers)
end

What i´ve done is simple, I just use the astro binding to check which season it is and depending on that, set a specific amount of minutes for the timer.

What I still want is a further extension to recheck if a window is still open after the first timer expired. I know there´s a command to reschedule a timer: https://docs.openhab.org/addons/actions.html#timers

Could someone assist to make this possible?

I monitore my windows status and send a message to alexa when a window is opened for longer than 15 minutes.
My rule looks like this:

rule "Window open while heat is on"

when
    Item gEGContacts changed to OPEN or
    Item gOGContacts changed to OPEN or
    Item gDBContacts changed to OPEN
then

if(EG_Vi_hkpump.state == ON){
    if(stopMotionTimer === null && gEGContacts.state == OPEN && gOGContacts.state == OPEN && gDBContacts.state == OPEN){
        stopMotionTimer = createTimer(now.plusMinutes(15)) [|
        Echo_TTS.sendCommand('Die Heizung ist an. Bitte Fenster schließen im Erd, Ober und Dachgeschoss.')
        stopMotionTimer = null
        ]
    }
    else if(stopMotionTimer === null && gEGContacts.state == CLOSED && gOGContacts.state == OPEN && gDBContacts.state == OPEN){
        stopMotionTimer = createTimer(now.plusMinutes(15)) [|
        Echo_TTS.sendCommand('Die Heizung ist an. Bitte Fenster schließen im Ober und Dachgeschoss.')
        stopMotionTimer = null
        ]
    }
        else if(stopMotionTimer === null && gEGContacts.state == OPEN && gOGContacts.state == CLOSED && gDBContacts.state == OPEN){
        stopMotionTimer = createTimer(now.plusMinutes(15)) [|
        Echo_TTS.sendCommand('Die Heizung ist an. Bitte Fenster schließen im Erd und Dachgeschoss.')
        stopMotionTimer = null
        ]
    }
        else if(stopMotionTimer === null && gEGContacts.state == OPEN && gOGContacts.state == OPEN && gDBContacts.state == CLOSED){
        stopMotionTimer = createTimer(now.plusMinutes(15)) [|
        Echo_TTS.sendCommand('Die Heizung ist an. Bitte Fenster schließen im Erd und Obergeschoss.')
        stopMotionTimer = null
        ]
    }
        else if(stopMotionTimer === null && gEGContacts.state == OPEN && gOGContacts.state == CLOSED && gDBContacts.state == CLOSED){
        stopMotionTimer = createTimer(now.plusMinutes(15)) [|
        Echo_TTS.sendCommand('Die Heizung ist an. Bitte Fenster schließen im Erdgeschoss.')
        stopMotionTimer = null
        ]
    }
        else if(stopMotionTimer === null && gEGContacts.state == CLOSED && gOGContacts.state == OPEN && gDBContacts.state == CLOSED){
        stopMotionTimer = createTimer(now.plusMinutes(15)) [|
        Echo_TTS.sendCommand('Die Heizung ist an. Bitte Fenster schließen im Obergeschoss.')
        stopMotionTimer = null
        ]
    }
        else if(stopMotionTimer === null && gEGContacts.state == CLOSED && gOGContacts.state == CLOSED && gDBContacts.state == OPEN){
        stopMotionTimer = createTimer(now.plusMinutes(15)) [|
        Echo_TTS.sendCommand('Die Heizung ist an. Bitte Fenster schließen im Dachgeschoss.')
        stopMotionTimer = null
        ]
    }
}

But the problem is, that when a window is closed while the timer is running, alexa still brings the message that a window is open after 15 minutes. So I need a second check, if a window is still open after 15 minutes and I don’t know how to do it.
An other problem is, that I would like to put alexas volume to a certain value before the window message is played. After the message I would like to restore the previous volume.
And Ideas?
By the way - I am a noob learning :wink:

This would have been better posted as a separate new posting.

OK, I see a whole lot of duplicated code here. Let’s see if we can make is simpler and then we can address the problem (which is that you need to cancel the Timer when the window closes.

First, put all your Contacts into a Group, I’ll call it Windows. Define is as Group:Windows:OR(OPEN, CLOSED). The state of Windows will be OPEN if any one of its members is OPEN, CLOSED otherwise.

Next change the Rule to trigger with Member of Windows. But, you probably want an alert if the heat turns on and a window is already OPEN so let’s trigger on the heat pump as well.

If the pump isn’t on, just exit the Rule. If there is already a Timer set, exit the Rule.

var Timer stopMotionTimer = null

rule "Window open while heat is on"
when
    Member of Windows changed to OPEN or
    Item EG_Vi_hkpump changed to ON
then
    if(EG_Vi_hkpump.state != ON) return;
    if(stopMotionTimer !== null) return;

    // Set the timer
    stopMotionTimer = createTimer(now.plusMinutes(15), [ |
        // Generate a list of all the open windows
        var openWins = Windows.members.filter[ w | w.state == OPEN ].map[ transform("MAP", "windows.map", name) ].reduce[ msg, winName | msg = msg + ", " + winName ] // get list of windows
        openWins = openWins.replaceFirst(", ", "") // delete the first comma
        openWins = openWins.replaceLast(", ", "und ") // replace the last comma with "und"

        Echo_TTS.sendCommand("Die Heizung ist an. Bitte Fenster schließen im " + openWins + ".")
        stopMotionTimer = null
    ])

end

Theory of operation: When a Window opens or the heat turns ON, set a timer if one doesn’t already exist. When the Timer goes off, construct a list of all the open Windows and TTS the list.

Now what if the heat turns off or all the windows close?

rule "Windows closed"
when
    Item Windows changed to CLOSED or // happens when all windows are closed
    Item  EG_Vi_hkpump changed to OFF
then
    stopMotionTimer?.cancel // if stopMotionTimer isn't null, cancel it.
end

Theory of operation: If all the windows close or the heat turns off, cancel the timer if it is running.

1 Like

Hello,
thank you so much for your help.
I already have a group for all windows. It is

Group:Contact:OR(OPEN,CLOSED) gHContacts "Fensterkontakte Haus [(%d)]" <window>

An example for a window item:

Contact EG_wo_fe_re1  "Wohnen Rechts Auf [MAP(de.map):%s]" <window> (gEGContactsOffen, gEGContacts) { channel="knx:device:bridge:Tasterschnittstellen:EG_wo_fe_re1" } 
Contact EG_wo_fe_re2  "Wohnen Rechts Kippe [MAP(de.map):%s]" <window> (gEGContactsKippe, gEGContacts) { channel="knx:device:bridge:Tasterschnittstellen:EG_wo_fe_re2" } 
String EG_wo_fe_re "Wohnen Rechts [MAP(de.map):%s]" <contact> (gHFenster, gEG, gEGFenster, gWO, gHContacts)

And the logfile shows an error:

019-02-25 20:45:51.532 [INFO ] [el.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model 'rules.rules', using it anyway:
Assignment to final parameter
2019-02-25 20:45:51.731 [INFO ] [el.core.internal.ModelRepositoryImpl] - Refreshing model 'rules.rules'
2019-02-25 20:45:51.742 [WARN ] [mmon.WrappedScheduledExecutorService] - Scheduled runnable ended with an exception: 
java.lang.NullPointerException: null
	at org.eclipse.smarthome.model.rule.runtime.internal.engine.RuleContextHelper.getContext(RuleContextHelper.java:68) ~[?:?]
	at org.eclipse.smarthome.model.rule.runtime.internal.engine.RuleEngineImpl.lambda$2(RuleEngineImpl.java:339) ~[?:?]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[?:?]
	at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[?:?]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) ~[?:?]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) ~[?:?]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:?]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:?]
	at java.lang.Thread.run(Thread.java:748) [?:?]
2019-02-25 20:46:35.456 [INFO ] [el.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model 'rules.rules', using it anyway:
Assignment to final parameter
2019-02-25 20:46:35.594 [INFO ] [el.core.internal.ModelRepositoryImpl] - Refreshing model 'rules.rules'
2019-02-25 20:48:20.193 [ERROR] [org.quartz.core.JobRunShell         ] - Job DEFAULT.2019-02-25T20:48:20.056+01:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: [ | {
  var openWins
  <null>.openWins = <XMemberFeatureCallImplCustom>
  <null>.openWins = <XMemberFeatureCallImplCustom>
  <XFeatureCallImplCustom>.sendCommand(<XBinaryOperationImplCustom>)
  <null>.stopMotionTimer = <XNullLiteralImplCustom>
} ] threw an unhandled Exception: 
java.lang.IllegalArgumentException: Couldn't invoke 'assignValueTo' for feature param msg
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.assignValueTo(XbaseInterpreter.java:1225) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:1213) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:216) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:204) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:447) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:228) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:204) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:190) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) ~[?:?]
	at com.sun.proxy.$Proxy155.apply(Unknown Source) ~[?:?]
	at org.eclipse.xtext.xbase.lib.IteratorExtensions.reduce(IteratorExtensions.java:647) ~[?:?]
	at org.eclipse.xtext.xbase.lib.IterableExtensions.reduce(IterableExtensions.java:545) ~[?:?]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:?]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1086) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1061) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._invokeFeature(XbaseInterpreter.java:1047) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeFeature(XbaseInterpreter.java:992) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:151) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:772) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:220) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:204) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:827) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:264) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:204) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:447) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:228) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:204) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:190) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) ~[?:?]
	at com.sun.proxy.$Proxy150.apply(Unknown Source) ~[?:?]
	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:49) ~[?:?]
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) [107:org.eclipse.smarthome.core.scheduler:0.10.0.oh240]
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [107:org.eclipse.smarthome.core.scheduler:0.10.0.oh240]
2019-02-25 20:48:20.308 [ERROR] [org.quartz.core.ErrorLogger         ] - Job (DEFAULT.2019-02-25T20:48:20.056+01:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: [ | {
  var openWins
  <null>.openWins = <XMemberFeatureCallImplCustom>
  <null>.openWins = <XMemberFeatureCallImplCustom>
  <XFeatureCallImplCustom>.sendCommand(<XBinaryOperationImplCustom>)
  <null>.stopMotionTimer = <XNullLiteralImplCustom>
} ] threw an exception.
org.quartz.SchedulerException: Job threw an unhandled exception.
	at org.quartz.core.JobRunShell.run(JobRunShell.java:213) [107:org.eclipse.smarthome.core.scheduler:0.10.0.oh240]
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [107:org.eclipse.smarthome.core.scheduler:0.10.0.oh240]
Caused by: java.lang.IllegalArgumentException: Couldn't invoke 'assignValueTo' for feature param msg
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.assignValueTo(XbaseInterpreter.java:1225) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:1213) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:216) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:204) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:447) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:228) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:204) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:190) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) ~[?:?]
	at com.sun.proxy.$Proxy155.apply(Unknown Source) ~[?:?]
	at org.eclipse.xtext.xbase.lib.IteratorExtensions.reduce(IteratorExtensions.java:647) ~[?:?]
	at org.eclipse.xtext.xbase.lib.IterableExtensions.reduce(IterableExtensions.java:545) ~[?:?]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:?]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1086) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeOperation(XbaseInterpreter.java:1061) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._invokeFeature(XbaseInterpreter.java:1047) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.invokeFeature(XbaseInterpreter.java:992) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:151) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:772) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:220) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:204) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:827) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:264) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:204) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:447) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:228) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:226) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:204) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:190) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) ~[?:?]
	at com.sun.proxy.$Proxy150.apply(Unknown Source) ~[?:?]
	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:49) ~[?:?]
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) ~[?:?]
	... 1 more
2019-02-25 20:50:32.060 [INFO ] [.smarthome.model.script.system.rules] - Uptime updated to 1 Std. / 52 Min.

Your rule adjusted to my group:

var Timer stopMotionTimer = null

rule "Window open while heat is on"
when
    Member of gHContacts changed to OPEN or
    Item EG_Vi_hkpump changed to ON
then
    if(EG_Vi_hkpump.state != ON) return;
    if(stopMotionTimer !== null) return;

    // Set the timer
    stopMotionTimer = createTimer(now.plusMinutes(1), [ |
        // Generate a list of all the open windows
        var openWins = gHContacts.members.filter[ w | w.state == OPEN ].map[ transform("MAP", "windows.map", name) ].reduce[ msg, winName | msg = msg + ", " + winName ] // get list of windows
        openWins = openWins.replaceFirst(", ", "") // delete the first comma
        openWins = openWins.replaceLast(", ", "und ") // replace the last comma with "und"

        Echo_TTS.sendCommand("Die Heizung ist an. Bitte Fenster schließen im " + openWins + ".")
        stopMotionTimer = null
    ])

end

There is an exception being thrown from inside the Timer.

I just typed in the code above so there is likely a typo.

Add some logging so you can figure out which line is throwing the error.