Extended Motion Sensor Rule: any Suggestions?

hmm.
I edited many things. I implemented Hashtables like you said.
Unfortunately, i get “null” errors, for example here:

try {
    mylock.lock()
    if (timers.get(Raum_Name) !== null){
            timers.get(Raum_Name).cancel()
            timers.put(Raum_Name, null)   
    }
}
catch(Throwable t) {
    logError("Error", "Keine Ahnung was passiert ist..." + t.toString)
    timers.put(Raum_Name, null) 
}
finally {
    mylock.unlock() 
}

Error:
17:58:19.613 [ERROR] [untime.internal.engine.RuleEngineImpl] - Rule ‘Motion’: null

It doesnt matter if I have locks or not, it was just a try.
Any ideas?

import java.util.Hashtable
import org.eclipse.smarthome.model.script.ScriptServiceUtil
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
////GLOBALE PARAMETER/////
val String[] Raumliste = newArrayList('Flur','Kueche','Bett')
val Log_An = true
//Textbausteine(Prefix + Raumname + Suffix)
val Light_Prefix = "gMotion_"
val Light_Suffix_Brightness = "_Licht_Helligkeit"
val Light_Suffix_Temperature = "_Licht_Farbtemperatur"

val Motion_Sensor_Prefix = "gMotion_"
val Motion_Sensor_Suffix = "_Sensor_Bewegung"

val Lux_Sensor_Prefix = "gMotion_"
val Lux_Sensor_Suffix = "_Sensor_Lux"
////GLOBALE PARAMETER/////


//intern
val Hashtable<String, Timer> timers = new Hashtable<String, Timer>()
val Hashtable<String, DateTime> last_Update_Time = new Hashtable<String, DateTime>()
val Hashtable<String, String> last_Update_Type = new Hashtable<String, String>()
val Hashtable<String, Number> last_Update_Brightness = new Hashtable<String, Number>()
val Hashtable<String, Number> last_Update_Temperature = new Hashtable<String, Number>()
var Lock mylock = new ReentrantLock()
rule "Motion"
when
    Member of gMotion_Sensor_Bewegung changed //Bewegungsmelder
    or Member of gMotion_Sensor_Lux received update //Helligkeit
then
///////Zeiten////////
val Zeit_Morgens = now.withTimeAtStartOfDay.plusDays(1).minusHours(24-10) //10 Uhr
val Zeit_Mittags = now.withTimeAtStartOfDay.plusDays(1).minusHours(24-13) //13 Uhr
val Zeit_Nachmittags = now.withTimeAtStartOfDay.plusDays(1).minusHours(24-16) //16 Uhr
val Zeit_Nachts = now.withTimeAtStartOfDay.plusDays(1).minusHours(24-22) //22 Uhr
var Tageszeit = "UNBEKANNT"

switch now {
    case now.isAfter(Zeit_Morgens) && now.isBefore(Zeit_Mittags): Tageszeit = "MORGENS"
    case now.isAfter(Zeit_Mittags) && now.isBefore(Zeit_Nachmittags): Tageszeit = "MITTAGS"
    case now.isAfter(Zeit_Nachmittags) && now.isBefore(Zeit_Nachts): Tageszeit = "NACHMITTAG"
    default: Tageszeit = "NACHTS"
}
///////Zeiten////////

///////RAUM bestimmen/////
var Trigger_Name = triggeringItem.name
var Raum_Name = ""
//Raum bestimmen
val i = Raumliste.iterator
while (i.hasNext){
    val str = i.next
    if (Trigger_Name.contains(str)){
        Raum_Name = str
    }
}
if (Raum_Name == "") {return;} //Raum nicht in der Raumliste
///////RAUM bestimmen/////
if (Log_An) {logWarn("MOTION", Raum_Name)}
///////PARAMETER//////////
    //Standardwerte
    var Par_Lux_Threshold = 5.0 //lx
    var Par_SP_Brightness = 70.0 //%
    var Par_SP_Temperature = 70.0 //%
    var Par_Timeout_Seconds = 30 //s
    var Par_Deadtime_Manual_Seconds = 20 //Totzeit für Bewegungsmelder nach man. Bedienung
    //Individuelle Werte
    switch (Raum_Name){
        case "Flur":{
            Par_Lux_Threshold = 40.0
            Par_Timeout_Seconds = 10

            switch(Tageszeit){
                case "MORGENS",
                case "NACHTS":{
                    Par_SP_Brightness = 30.0
                    Par_SP_Temperature = 80.0
                }
                case "NACHMITTAG":{
                    Par_SP_Brightness = 100.0
                    Par_SP_Temperature = 60.0 
                }
                default:{
                    Par_SP_Brightness = 0.0
                    Par_SP_Temperature = 60.0                 
                }
            }
        }
        ////Küche
        case "Kueche": {
            Par_Lux_Threshold = 180.0
            Par_Timeout_Seconds = 60

            switch(Tageszeit){
                case "MORGENS",
                case "NACHTS":{
                    Par_SP_Brightness = 50.0
                    Par_SP_Temperature = 80.0
                }
                case "NACHMITTAG":{
                    Par_SP_Brightness = 100.0
                    Par_SP_Temperature = 60.0 
                }
                default:{
                    Par_SP_Brightness = 100.0
                    Par_SP_Temperature = 40.0                 
                }
            }
        }
        ////Küche
        case "Bett": {
            Par_Lux_Threshold = 300.0
            Par_Timeout_Seconds = 0
            Par_Deadtime_Manual_Seconds = 0
            switch(Tageszeit){
                case "MORGENS",
                case "NACHTS":{
                    Par_SP_Brightness = 20.0
                    Par_SP_Temperature = 100.0
                }
                case "NACHMITTAG":{
                    Par_SP_Brightness = 100.0
                    Par_SP_Temperature = 60.0 
                }
                default:{
                    Par_SP_Brightness = 100.0
                    Par_SP_Temperature = 50.0                 
                }
            }
        }
    }

///////PARAMETER//////////
if (Log_An) {logWarn("MOTION", "Parameter bestimmt")}
///////Items bestimmen////
val GenericItem Motion_Sensor = ScriptServiceUtil.getItemRegistry?.getItem(Motion_Sensor_Prefix + Raum_Name + Motion_Sensor_Suffix) as GenericItem 
val GenericItem Lux_Sensor = ScriptServiceUtil.getItemRegistry?.getItem(Lux_Sensor_Prefix + Raum_Name + Lux_Sensor_Suffix) as GenericItem 
val GenericItem Light_Brightness = ScriptServiceUtil.getItemRegistry?.getItem(Light_Prefix + Raum_Name + Light_Suffix_Brightness) as GenericItem 
///////Items bestimmen////
if (Log_An) {logWarn("MOTION", "Items bestimmt")}
///////MOTION AN//////////
if (Motion_Sensor.state == ON){
    if (Log_An) {logWarn("MOTION", "Sensor Aktiv")}
    //Timer canceln, wenn bereits aktiv,da wieder Bewegung
    
    try {
        mylock.lock()
        if (timers.get(Raum_Name) !== null){
                timers.get(Raum_Name).cancel()
                timers.put(Raum_Name, null)   
        }
    }
    catch(Throwable t) {
        logError("Error", "Keine Ahnung was passiert ist..." + t.toString)
        timers.put(Raum_Name, null) 
    }
    finally {
        mylock.unlock() 
    }
    

    if (Log_An) {logWarn("MOTION", "Timer gecancelt")}
    ///////BEDINGUNGEN ZUM ANSCHALTEN////
    //nur wenn Lampen aus
    if (Light_Brightness.state >= 1.0) {
        if (Log_An) {logWarn("MOTION", "BEDINGUNG Licht an nicht OK")}
        return;
    }
    if (Log_An) {logWarn("MOTION", "BEDINGUNG Licht aus OK")}
    //wenn letzter Befehl von Automatik, dann nur wenn letzter Befehl älter als X Sekunden (Da der Sensor bis zu 15 Sekunden lang noch AN senden kann)
    //verhindert, dass das Licht nach dem Ausschalten direkt wieder an geht
    if (last_Update_Type.get(Raum_Name) === null || last_Update_Type.get(Raum_Name).contains("MANU")){
        if (last_Update_Time.get(Raum_Name) !== null && now.minusSeconds(Par_Deadtime_Manual_Seconds).isBefore(last_Update_Time.get(Raum_Name))) {
        
        if (Log_An) {logWarn("MOTION","BEDINGUNG Totzeit nach manuell aus nicht OK")}
        return;
        }
    }
    if (Log_An) {logWarn("MOTION","BEDINGUNG Totzeit nach manuell aus OK")}


    //Helligkeitswert gering genug, um zu aktivieren?
    var Cur_Lux = (Lux_Sensor.state as DecimalType).floatValue
    if (Cur_Lux == UNDEF){ Cur_Lux = (1000.0).floatValue }

    if ((Cur_Lux > Par_Lux_Threshold)) {
        if (Log_An) {logWarn("MOTION","BEDINGUNG Helligkeit nicht OK")}
        return; //nichts tun.
    }
    if (Log_An) {logWarn("MOTION","BEDINGUNG Helligkeit OK")}
    //Wenn Sollhelligkeit quasi 0 ist, ist der Bewegungsmelder auch nicht aktiv.
    if (Par_SP_Brightness <= 0.1){
        if (Log_An) {logWarn("MOTION","BEDINGUNG Sensor Sollhelligkeit >0 nicht OK")}
        return;
    }
    if (Log_An) {logWarn("MOTION","BEDINGUNG Sensor Sollhelligkeit >0 OK")}
    ///////BEDINGUNGEN ZUM ANSCHALTEN////

    ///////AKTION AN//////
    //Rücksetzen der Befehle durch Automatik verhindern
    
    last_Update_Time.put(Raum_Name,now)
    last_Update_Type.put(Raum_Name,"AUTO")
    
    Thread::sleep(100)

    //Ausführen der jeweiligen Aktion
    //Aktion an
    sendCommand(Light_Prefix+Raum_Name+Light_Suffix_Brightness,Par_SP_Brightness.toString())
    createTimer(now.plusMillis(300), [ | sendCommand(Light_Prefix+Raum_Name+Light_Suffix_Temperature,Par_SP_Temperature.toString())])
    createTimer(now.plusMillis(600), [ | sendCommand(Light_Prefix+Raum_Name+Light_Suffix_Brightness,Par_SP_Brightness.toString())])
    createTimer(now.plusMillis(900), [ | sendCommand(Light_Prefix+Raum_Name+Light_Suffix_Temperature,Par_SP_Temperature.toString())])
    if (Log_An) {logWarn("MOTION","AKTION AN")}
    ///////AKTION AN//////
}
///////MOTION AN//////
///////MOTION AUS//////////
else {
    if (Log_An) {logWarn("MOTION", "Sensor Inaktiv")}
    ///////BEDINGUNGEN ZUM AUSCHALTEN/////
    //nur wenn letzte Aktion nicht Manuell war
    if (last_Update_Type.get(Raum_Name) === null || last_Update_Type.get(Raum_Name).contains("MANU")){
        if (Log_An) {logWarn("MOTION","BEDINGUNG letzte Aktion Automatik nicht OK")}
        return;
    }
    if (Log_An) {logWarn("MOTION","BEDINGUNG letzte Aktion Automatik OK")}
    //nur wenn Timer nicht vorhanden
    
    if (timers.get(Raum_Name) !== null) {
        if (Log_An) {logWarn("MOTION","BEDINGUNG Timer noch nicht vorhanden nicht OK")}
        return;
    }
    if (Log_An) {logWarn("MOTION","BEDINGUNG Timer noch nicht vorhanden OK")}
    ///////BEDINGUNGEN ZUM AUSCHALTEN/////

    ///////AKTION AUS//////
    ////Aktion aus
        if (Log_An) {logWarn("MOTION","AKTION AUS TIMER AKTIVIERT")}
        timers.put(Raum_Name, createTimer(now.plusSeconds(Par_Timeout_Seconds), [|
        
        last_Update_Time.put(Raum_Name,now)
        last_Update_Type.put(Raum_Name,"AUTO")
        
        sendCommand(Light_Prefix+Raum_Name+Light_Suffix_Brightness,0.toString())
        if (Log_An) {logWarn("MOTION","AKTION AUS")}
        ]))
    ///////AKTION AUS//////
     
}
///////MOTION AUS//////////
end

rule "Motion Cancel" //bei manuellem Eingriff
when
    Member of gMotion_Licht_Helligkeit changed
    or Member of gMotion_Licht_Farbtemperatur changed
then
///////RAUM bestimmen/////
var Trigger_Name = triggeringItem.name
var Raum_Name = ""
//Raum bestimmen
val i = Raumliste.iterator
while (i.hasNext){
    val str = i.next
    if (Trigger_Name.contains(str)){
        Raum_Name = str
    }
}
if (Raum_Name == "") {return;} //Raum nicht in der Raumliste
///////RAUM bestimmen/////
if (Log_An) {logWarn("MOTIONCANCEL", Raum_Name)}
///////BEDINGUNG ABBRUCH///////

//Bedingung Mindeständerung für Abbruch oder Lampen aus
if (triggeringItem.state == UNDEF ||triggeringItem.state == NULL ){
    return;
}
val Current_Value = (triggeringItem.state as DecimalType).floatValue
if (triggeringItem.name.contains(Light_Suffix_Brightness)){
    if (last_Update_Brightness.get(Raum_Name) === null){
        last_Update_Brightness.put(Raum_Name,Current_Value)
    }

    if (Current_Value+0.1 <= last_Update_Brightness.get(Raum_Name) 
    || Current_Value-0.1 >= last_Update_Brightness.get(Raum_Name)
    || Current_Value <=0.1){
        last_Update_Brightness.put(Raum_Name,Current_Value)
    }
    else{
        if (Log_An) {logWarn("MOTIONCANCEL","BEDINGUNG Mindestaenderung Helligkeit nicht OK")}
        return;
    }
    if (Log_An) {logWarn("MOTIONCANCEL","BEDINGUNG Mindestaenderung Helligkeit OK")}
    
}
else{
    if (last_Update_Temperature.get(Raum_Name) === null){
        last_Update_Temperature.put(Raum_Name,Current_Value)
    }

    if (Current_Value+0.1 <= last_Update_Temperature.get(Raum_Name) 
    || Current_Value-0.1 >= last_Update_Temperature.get(Raum_Name)
    || Current_Value <=0.1){
        last_Update_Temperature.put(Raum_Name,Current_Value)
    }
    else{
        if (Log_An) {logWarn("MOTIONCANCEL","BEDINGUNG Mindestaenderung Farbtemp nicht OK")}
        return;
    }
    if (Log_An) {logWarn("MOTIONCANCEL","BEDINGUNG Mindestaenderung Farbtemp OK")}
}

//nur wenn letzter Befehl von Automatik
if (last_Update_Type.get(Raum_Name) === null || last_Update_Type.get(Raum_Name).contains("MANU")){
    if (Log_An) {logWarn("MOTIONCANCEL","BEDINGUNG Timer von Automatik nicht OK")}
    return;
}
if (Log_An) {logWarn("MOTIONCANCEL","BEDINGUNG Timer von Automatik OK")}
//nur wenn letzter Befehl älter als 5 Sekunden
if (last_Update_Time.get(Raum_Name) !== null && now.minusSeconds(5).isBefore(last_Update_Time.get(Raum_Name))) {
    if (Log_An) {logWarn("MOTIONCANCEL","BEDINGUNG letzter Befehl alt genug nicht OK")}
    
    return;
}
if (Log_An) {logWarn("MOTIONCANCEL","BEDINGUNG letzter Befehl alt genug OK")}

///////BEDINGUNG ABBRUCH///////

///////AKTION ABBRUCH///////
//Timer stoppen, wenn aktiv
if (timers.get(Raum_Name) !== null){
    timers.get(Raum_Name)?.cancel()
    timers.put(Raum_Name, null)
}
last_Update_Time.put(Raum_Name,now)
last_Update_Type.put(Raum_Name,"MANU")


if (Log_An) {logWarn("MOTIONCANCEL","MOTION UNTERBROCHEN")}

///////AKTION ABBRUCH///////
end

And this is why ReentrantLocks are dangerous. I’m going to bet that your finally never ran and your lock never got unlocked because of the error. Notice your logError never ran.

Unfortunately, null errors mean lots of different things in Rules DSL. Most often it indicates a type error (e.g. you try to cast a Number Item’s state to Number but the Item’s state is NULL or UNDEF).

You will need to add logging statements to your Rule to figure out which line it’s failing on and then check the value of everything being used on that line to see if something is unexpected.

I also get them without locks, i tried it before…
It fails for example on one of those after the check for null…
timers.get(Raum_Name).cancel()
timers.put(Raum_Name, null

I have no idea why…

Are you certain that Raum_Name is not null at that point?

Honestly, I’ve no idea what is causing the problem but this code is so long and complicated I’m more inclined to fix the whole thing using Rules DSL best practices rather than trying to figure this part out.

import org.eclipse.smarthome.model.script.ScriptServiceUtil
import java.util.Map

val Map<String, Timer> timers = createHashMap
val Map<String, DateTime> last_update_time = createHashMap
val Map<String, String> last_update_type = createHashMap
val Map<String, Number> last_update_brightness = createHashMap
val Map<String, Number> last_update_temperature = createHashMap

val PAR_DEADTIME_MANUAL_SECONDS = 20
val PAR_LUX_THRESHOLD = 5.0 //lx

// Move the time of day stuff to it's own Rule.
rule "Time of Day"
when
    System started or
    Time cron "0 0 10 * * ? *" or
    Time cron "0 0 13 * * ? *" or
    Time cron "0 0 16 * * ? *" or
    Time cron "0 0 22 * * ? *"
then
    val ziet_morgens = now.withTime(10, 0, 0, 0)
    val ziet_mittags = now.withTime(13, 0, 0, 0)
    val ziet_nachmittags = now.withTime(16, 0, 0, 0)
    val ziet_nachts = now.withTime(22, 0, 0, 0)

    var curr_tod = "UNKNOWN"
    switch now {
        case now.isAfter(ziet_morgens) && now.isBefore(ziet_mittags): curr_tod = "MORGENS"
        case now.isAfter(ziet_mittags) && now.isBefore(ziet_nachmittags): curr_tod = "MITTAGS"
        case now.isAfter(ziet_nachmittags) && now.isBefore(ziet_nachts): curr_tod = "NACHMITTAG"
        case now.isAfter(ziet_nachts): curr_tod == "NACHTS"
    }

    if(curr_tod == "UNKNOWN") logError("TOD", "Error calculating time of day!")

    if(TimeOfDay.state.toString != curr_tod){
        logInfo("TOD", "Transitioning time of day to " + curr_tod)
        TimeOfDay.postUpdate(curr_tod)
    }
end

rule "Motion"
when
    Member of gMotion_Sensor_Bewegung changed or
    Member of gMotion_Sensor_Lux received update
then
    // don't see your Item names so I'll assume it follows the pattern <something>_<room name>_<other stuff>
    val raum_name = triggeringItem.name.split("_").get(1)
    val light_name = "gMotion_"+raum_name+"_Licht_Helligkeit"
    if(Log_An) logWarn("MOTION", raum_name)

    // Consider using Items and persistence for last_update stuff. I'll leave them as maps for now
    // Fail fast, both cases exit in this situation, just put the check once at the top
    if(last_update_type.get(raum_name) === null || last_update_type.get(raum_name) == "MANU"){
        if(last_update_time.get(raum_name) !== null && if(now.minusSeconds(PAR_DEADTIME_MANUAL_SECONDS).isBefore(last_update_time.get(raum_name))){
            if(Log_An) logWarn("MOTION", "BEDINGUNG Totzeit nach manuell aus nicht OK")
            return;
        }
    }

    // I recommend putting the Lux and Timeout parameters in Items which will give you the 
    // opportunity to adjust them from your sitemap instead of requiring changing code.
    val par_lux_threshold = ScriptServiceUtil.getItemRegistry.getItem(raum_name+"_Lux")
    val par_timeout_seconds = ScriptServiceUtil.getItemRegistry.getItem(raum_name+"_Timeout_Seconds")

    // Similarly, put the time of day brightness and temps into Items
    val bright_item = ScriptServiceUtil.getItemRegistry.getItem(raum_name+"_"+TimeOfDay.state.toString+"_Brightness")
    val par_sp_brightness = if(bright_item == null) 0 else bright_item.state as Number
    val temp_item = ScriptServiceUtil.getItemRegistry.getItem(raum_name+"_"+TimeOfDay.state.toString+"_Temperature")
    val par_sp_temperature = if(temp_item == null) 60 else temp_item.state as Number

    val motion_sensor = ScriptServiceUtil.getItem.getItemRegistry.getItem("gMotion_"+raum_name+"_Sensor_Bewegung")
    val lux_sensor = ScriptServiceUtil.getItem.getItemRegistry.getItem("gMotion_"+raum_name+"_Sensor_Lux")
    val light_brightness = ScriptServiceUtil.getItemRegistry.getItem(light_name)

    // Motion sensor is ON
    if(motion_sensor.state == ON){
        if(Log_An) logWarn("MOTION", "Sensor Aktiv")
        // Cancel the timer if it exists
        timers.get(raum_name)?.cancel
        timers.put(raum_name, null)

        // Exit if brightness is over 1
        if(light_brightness.state as Number >= 1) {
            if(Log_An) logWarn("MOTION", "BEDINGUNG Licht an nicht OK")
            return;
        }

        // You will get an error if you try to cast an UNDEF to DecimalType. You have to check for UNDEF first
        // Don't use primitives except where necessary
        val cur_lux = if(Lux_Sensor.state instanceof DecimalType) lux_sensor.state as Number else 1000

        // Check if the room is already too bright
        if(cur_lux > PAR_LUX_THRESHOLD) {
            if(Log_An) logWarn("MOTION", "BEDINGUNG Helligkeit nicht OK")
            return;
        }

        // Check that the target brightness is high enough
        if(par_sp_brightness <= 0.1){
            if(Log_An) logWarn("MOTION", "BEDINGUNG Sensor Sollhelligkeit > 0 nicht OK")
            return;
        }

        // Record the update
        last_update_time.put(raum_name, now)
        last_update_type.put(raum_name, "AUTO")

        // why sleep? There is no reason to sleep here.

        // Command the light
        sendCommand(light_name, par_sp_brightness)
        createTimer(now.plusMillis(300), [ | sendCommand("gMotion_"+raum_name+"_Licht_Farbtemperature", par_sp_temperature.toString)
        // Why do you have to sendCommand twice?
    }

    else {
        if(Log_An) logWarn("MOTION", "Sensor Inaktiv")

        if(timers.get(raum_name) !== null){
            if(Log_An) logWarn("MOTION", "BEDINGUNG Timer noch nicht vorhanden nicht OK")
            return;
        }

        if(Log_An) logWarn("MOTION", "AKTOIN AUS TIMER AKTIVIERT")
        timers.put(raum_name, createTimer(now.plusSeconds(par_timeout_seconds.intValue), [ |
            last_update_time.put(raum_name, now)
            last_update_type.put(raum_name, "AUTO")
            sendCommand(light_name, "OFF") // you can send ON/OFF to Dimmer Items
            if(Log_An) logWarn("MOTION", "AKTION AUS")
        ])
    }
end
3 Likes

Wow, this is amazing! I will definitely try it out.
Thank you!

I triggered the lights multiple times, because philips seems to ignore some commands sometimes if it cant senf it to the bulb at that time so the cmd gets lost. In rare cases, the light stays off, has the wrong color or brightness.

By the way, is there a simpler method in my motion cancel rule to have some deadband, by which the item triggers on change?
The brightness sometimes changes like 0.001% by itself, triggers the rule 5 times, and cancels my motion sensor allthough nothing happened. Is there nothing like item x changed by 0.1 or more as trigger?

Theoretically there is something more missing…if motion cancel cancels the timer, and the light goes off, it should reevaluate the rule once after deadtime_seconds for that room Can i somehow trigger an update to an item (the motion sensor) without changing its state? Im afraid writing on to it, because if the motion sensor changes to off right before the state would then be wrong.
The motion sensor only Updates to on if moving once, and off once 16 seconds or so after the last movement. I want the lights to come back on after the deadtime, even if there was motion the whole time (no trigger because the state does not change). Tl;dr i want to push an update to the item without changing its state so the rule gets triggerer on that room.

Thank you so much again!

Some words about EOS: the scaling by sensor is nice, we have that in our Office. But at home i want darker lights at night, and maximum brightness if it is too dark, or just some ambient light. And i want it to go on if the shutters are down as well, etc.
So, the position inside the room is better thsn outside, because it measures brightness where it is needed. The downside is, you can only read it while the lights are off (thats what i do) or implement some regulator with the brightness as feedback. Im not sure if that would be overkill… but it would be the most precise brightness control possible. :slight_smile:

This can be done using the time of day mechanism I explained above.


This can be done by using the shutters open/close as a motion trigger to change the scene being evaluated, also explained above.


This is possible, but how would you measure the feedback? Openhab allows you to connect any kind of light you want, so brightness from each light cannot be known. I cannot think of a way to do this without requiring many adjustments by the user, which I think would make undesirable to use.

Do you mean hysteresis? The only approach I know of is the set of if/elif statements. I’ve submitted a generic implementation of that to the JSR223 Helper Libraries though. https://github.com/openhab-scripters/openhab-helper-libraries/pull/222 For Rules DSL, you just have to do it yourself though.

No. You would have to do this yourself. It’s not hard though.

rule "Brightness changes"
when
    Item MyBrightnessItem changed
then
    val diff = (newState as Number) - (previousState as Number)
    if(diff < 0.001) return;

    // do stuff
end

newState and previousState are implicit variables that only exist for Rules triggered by a changed trigger.

MyItem.postUpdate(MyItem.state)

That will update the Item with it’s current state. If your Rule is triggered with received update the Rule will trigger as a result.

You can determine this experimentally. It would be tedious but possible.

My point exactly. Tedium is the anti-purpose of home automation

Sometimes you have to do some tedious work up front to enable savings in the long run.

Indeed. If I have some time I will look into it. I have other things on the go and no sensor to test with right now

I thought of a real regulator. Actually, it is not that hard to implement. https://en.m.wikipedia.org/wiki/PID_controller

You theoretically could do that, im an engineer for process plants and that way you normally regulate temperatures, pressures, levels whatever.

You basically check every second or 2 or so and adjust your (light) output based on setpoint and current value.

In the most simple form, you just do (Setpoint in lux - current value in lux) x Factor P= output in %, limited to 0-100.

You then choose P experimentally (in this case) until it works alright. If its too big, it could start to swing.

You can make it better by using a more complex form. Im not sure if that is something which works good for lightning, would be fun for sure.

Basically it periodically readjusts the Brightness till it measures the point you want.

That way you dont have to calculate each part into your output yourself. It just fixes the brightness no matter what influences it or how the lamps behave.
Its the difference between controlling and regulating.
You need a closed loop feedback for that, what means that your output has to influence your input, like it does with a luminance sensor inside the room.
Measuring the influence of each lamp would be a dead end. Just use a regulator algorithm…

But personally i dont think i need that, it works fine with fixed values.

But hey, maybe i try it out if i find some time somewhere in the Future :smiley: you can try it if you want to, too :slight_smile: but back to topic.

Wow, really? It just gives me the before and after values?? Nice :sunglasses:

But of course! I hadn’t thought of that.

I will see about adding a level target setting and scene type.

One problem that always stumped me when I was thinking about this type of scene is how to handle switched lights. We wouldn’t want them turning on and off repeatedly. A delay after changing the light would prevent flickering, but it’s possible it would still try to turn on and off repeatedly at the end of the delay.

Look at https://www.openhab.org/docs/configuration/rules-dsl.html#implicit-variables-inside-the-execution-block and pay attention to which variables appear under which circumstances (e.g. those will not appear for a received command triggered Rule but receivedCommand will).

A change is a change for rule triggers. Depending on the binding in use, you may be able to apply a transformation to your raw input data to round it and eliminate some jitter, so that the Item changes less frequently.

Hey, Í implemented your suggestions.
but the NULL Problem with the Hashtables persists…

21:49:36.646 [WARN ] [eclipse.smarthome.model.script.MOTION] - HIER ANFANG                                                               
21:49:36.647 [WARN ] [eclipse.smarthome.model.script.MOTION] - Flur                                                                      
21:49:36.649 [ERROR] [untime.internal.engine.RuleEngineImpl] - Rule 'Motion': null

Code to that:

        // Cancel the timer if it exists
        logWarn("MOTION", "HIER ANFANG")
        logWarn("MOTION", raum_name)
        timers.get(raum_name)?.cancel
        timers.put(raum_name, null)
        logWarn("MOTION", "HIER ENDE")

Any hopes for Hashtables? Are they initialized wrong?

Full Code:
Rules:

import java.util.Hashtable
import org.eclipse.smarthome.model.script.ScriptServiceUtil

val Hashtable<String, Timer> timers = new Hashtable<String, Timer>()
val Hashtable<String, DateTime> last_update_time = new Hashtable<String, DateTime>()
val Hashtable<String, String> last_update_type = new Hashtable<String, String>()
val Hashtable<String, Number> last_update_brightness = new Hashtable<String, Number>()
val Hashtable<String, Number> last_update_temperature = new Hashtable<String, Number>()

val PAR_DEADTIME_MANUAL_SECONDS = 20

val Log_An = false
rule "Time of Day"
when
    System started or
    Time cron "0 0 10 * * ? *" or
    Time cron "0 0 13 * * ? *" or
    Time cron "0 0 16 * * ? *" or
    Time cron "0 0 22 * * ? *"
then
    val Zeit_morgens = now.withTime(10, 0, 0, 0)
    val Zeit_mittags = now.withTime(13, 0, 0, 0)
    val Zeit_nachmittags = now.withTime(16, 0, 0, 0)
    val Zeit_nachts = now.withTime(22, 0, 0, 0)

    var curr_TOD = "UNKNOWN"
    switch now {
        case now.isAfter(Zeit_morgens) && now.isBefore(Zeit_mittags): curr_TOD = "MORGENS"
        case now.isAfter(Zeit_mittags) && now.isBefore(Zeit_nachmittags): curr_TOD = "MITTAGS"
        case now.isAfter(Zeit_nachmittags) && now.isBefore(Zeit_nachts): curr_TOD = "NACHMITTAG"
        default: curr_TOD = "NACHTS"
    }

    if(Time_of_Day.state.toString != curr_TOD){
        logInfo("TOD", "Transitioning time of day to " + curr_TOD)
        Time_of_Day.postUpdate(curr_TOD)
    }
end

rule "Motion"
when
    Member of gMotion_Sensor_Bewegung changed or
    Member of gMotion_Sensor_Lux received update
then
    // Raum bestimmen
    val raum_name = triggeringItem.name.split("_").get(1)
    val light_name = "gMotion_"+raum_name+"_Licht_Helligkeit"
    if(Log_An) logWarn("MOTION", raum_name)

 val motion_sensor = ScriptServiceUtil.getItemRegistry.getItem("gMotion_"+raum_name+"_Sensor_Bewegung")

    //wenn letzter Befehl von Manuell, dann nur wenn letzter Befehl älter als X Sekunden (Da der Sensor bis zu 15 Sekunden lang noch AN senden kann)
    //verhindert, dass das Licht nach dem Ausschalten direkt wieder an geht. Wenn Manuell und Sensor ist inaktiv, dann ist auch nichts zu tun.
    if(last_update_type.get(raum_name) === null || last_update_type.get(raum_name) == "MANU"){       
        if(motion_sensor.state ==OFF){
            if(Log_An) logWarn("MOTION", "BEDINGUNG Bewegungsmelder aus und letzter Befehl manuell nicht OK")
            return;  
        }   

        if(last_update_time.get(raum_name) !== null && now.minusSeconds(PAR_DEADTIME_MANUAL_SECONDS).isBefore(last_update_time.get(raum_name))){
            if(Log_An) logWarn("MOTION", "BEDINGUNG Totzeit nach manuell aus nicht OK")
            return;
        }
    }

    // Parameter
    val par_lux_threshold = ScriptServiceUtil.getItemRegistry.getItem("Motion_"+raum_name+"_SP_Lux")
    val par_timeout_seconds = ScriptServiceUtil.getItemRegistry.getItem("Motion_"+raum_name+"_SP_Timeout")

    val bright_item = ScriptServiceUtil.getItemRegistry.getItem("Motion_"+raum_name+"_"+Time_of_Day.state.toString+"_Brightness")
    val par_sp_brightness = if(bright_item === null) 0 else bright_item.state as Number
    val temp_item = ScriptServiceUtil.getItemRegistry.getItem("Motion_"+raum_name+"_"+Time_of_Day.state.toString+"_Temperature")
    val par_sp_temperature = if(temp_item === null) 60 else temp_item.state as Number

   
    val lux_sensor = ScriptServiceUtil.getItemRegistry.getItem("gMotion_"+raum_name+"_Sensor_Lux")
    val light_brightness = ScriptServiceUtil.getItemRegistry.getItem(light_name)

    // Motion sensor is ON
    if(motion_sensor.state == ON){
        if(Log_An) logWarn("MOTION", "Sensor Aktiv")
        // Cancel the timer if it exists
        logWarn("MOTION", "HIER ANFANG")
        logWarn("MOTION", raum_name)
        timers.get(raum_name)?.cancel
        timers.put(raum_name, null)
        logWarn("MOTION", "HIER ENDE")
        // Exit wenn Licht an
        if(light_brightness.state as Number >= 1) {
            if(Log_An) logWarn("MOTION", "BEDINGUNG Licht an nicht OK")
            return;
        }

        // check for UNDEF first
        val cur_lux = if(lux_sensor.state instanceof DecimalType) lux_sensor.state as Number else 1000.0
        val SP_lux = if(par_lux_threshold.state instanceof DecimalType) par_lux_threshold.state as Number else 0.0
        // Check if the room is already too bright
        if(cur_lux > SP_lux) {
            if(Log_An) logWarn("MOTION", "BEDINGUNG Helligkeit nicht OK")
            return;
        }

        // Check that the target brightness is high enough
        if(par_sp_brightness <= 0.1){
            if(Log_An) logWarn("MOTION", "BEDINGUNG Sensor Sollhelligkeit > 0 nicht OK")
            return;
        }

        // Record the update
        last_update_time.put(raum_name, now)
        last_update_type.put(raum_name, "AUTO")

        // Command the light
        sendCommand(light_name, par_sp_brightness.toString)
        createTimer(now.plusMillis(400), [ | sendCommand("gMotion_"+raum_name+"_Licht_Farbtemperature", par_sp_temperature.toString)])
        createTimer(now.plusMillis(800), [ | sendCommand(light_name, par_sp_brightness.toString)])
        createTimer(now.plusMillis(1200), [ | sendCommand("gMotion_"+raum_name+"_Licht_Farbtemperature", par_sp_temperature.toString)])      
    }

    else {
        if(Log_An) logWarn("MOTION", "Sensor Inaktiv")

        if(timers.get(raum_name) !== null){
            if(Log_An) logWarn("MOTION", "BEDINGUNG Timer noch nicht vorhanden nicht OK")
            return;
        }

        var par_timeout = if(par_timeout_seconds.state instanceof DecimalType) (par_timeout_seconds.state as Number).intValue else 30
        if(Log_An) logWarn("MOTION", "AKTION AUS TIMER AKTIVIERT")
        timers.put(raum_name, createTimer(now.plusSeconds(par_timeout), [ |
            last_update_time.put(raum_name, now)
            last_update_type.put(raum_name, "AUTO")
            sendCommand(light_name, "OFF") // you can send ON/OFF to Dimmer Items
            if(Log_An) logWarn("MOTION", "AKTION AUS")
        ]))
    }
end

//TODO: MOTION CANCEL

Items:

 Items:
//Global

String Time_of_Day "Tageszeit"

//Overall Groups

Group: Dimmer:AVG gMotion_Licht_Helligkeit

Group: Dimmer:AVG gMotion_Licht_Farbtemperatur

Group: Switch:OR(ON,OFF) gMotion_Sensor_Bewegung

Group: Number:AVG gMotion_Sensor_Lux

Group gMotion_SP_Lux "Bewegungsmelder Schwellwert Helligeit"

Group gMotion_SP_Timeout "Bewegungsmelder Timeouts"

Group gMotion_SP_Brightness "Bewegungsmelder Sollhelligkeit"

Group gMotion_SP_Temperature "Bewegungsmelder Farbtemperaturen"

//Flur

Group: Dimmer:AVG gMotion_Flur_Licht_Helligkeit (gMotion_Licht_Helligkeit)

Group: Dimmer:AVG gMotion_Flur_Licht_Farbtemperatur (gMotion_Licht_Farbtemperatur)

Group: Switch:OR(ON,OFF) gMotion_Flur_Sensor_Bewegung (gMotion_Sensor_Bewegung)

Group: Number:AVG gMotion_Flur_Sensor_Lux (gMotion_Sensor_Lux)

Group Motion_Flur_SP_Brightness "Bewegungsmelder Flur Helligkeit" (gMotion_SP_Brightness)

Group Motion_Flur_SP_Temperature "Bewegungsmelder Flur Farbtemperatur" (gMotion_SP_Temperature)

Number Motion_Flur_SP_Lux "Schwellwert Helligkeit Flur (lx)" (gMotion_SP_Lux)

Number Motion_Flur_SP_Timeout "Timeout Flur (s)" (gMotion_SP_Timeout)

Dimmer Motion_Flur_MORGENS_Brightness "Helligkeit morgens" (Motion_Flur_SP_Brightness)

Dimmer Motion_Flur_MORGENS_Temperature "Farbtemperatur morgens" (Motion_Flur_SP_Temperature)

Dimmer Motion_Flur_MITTAGS_Brightness "Helligkeit mittags" (Motion_Flur_SP_Brightness)

Dimmer Motion_Flur_MITTAGS_Temperature "Farbtemperatur mittags" (Motion_Flur_SP_Temperature)

Dimmer Motion_Flur_NACHMITTAG_Brightness "Helligkeit nachmittags" (Motion_Flur_SP_Brightness)

Dimmer Motion_Flur_NACHMITTAG_Temperature "Farbtemperatur nachmittags" (Motion_Flur_SP_Temperature)

Dimmer Motion_Flur_NACHTS_Brightness "Helligkeit nachts" (Motion_Flur_SP_Brightness)

Dimmer Motion_Flur_NACHTS_Temperature "Farbtemperatur nachts" (Motion_Flur_SP_Temperature)

//Küche

Group: Dimmer:AVG gMotion_Kueche_Licht_Helligkeit (gMotion_Licht_Helligkeit)

Group: Dimmer:AVG gMotion_Kueche_Licht_Farbtemperatur (gMotion_Licht_Farbtemperatur)

Group: Switch:OR(ON,OFF) gMotion_Kueche_Sensor_Bewegung (gMotion_Sensor_Bewegung)

Group: Number:AVG gMotion_Kueche_Sensor_Lux (gMotion_Sensor_Lux)

Group Motion_Kueche_SP_Brightness "Bewegungsmelder Küche Helligkeit" (gMotion_SP_Brightness)

Group Motion_Kueche_SP_Temperature "Bewegungsmelder Küche Farbtemperatur" (gMotion_SP_Temperature)

Number Motion_Kueche_SP_Lux "Schwellwert Helligkeit Küche (lx)" (gMotion_SP_Lux)

Number Motion_Kueche_SP_Timeout "Timeout Küche (s)" (gMotion_SP_Timeout)

Dimmer Motion_Kueche_MORGENS_Brightness "Helligkeit morgens" (Motion_Kueche_SP_Brightness)

Dimmer Motion_Kueche_MORGENS_Temperature "Farbtemperatur morgens" (Motion_Kueche_SP_Temperature)

Dimmer Motion_Kueche_MITTAGS_Brightness "Helligkeit mittags" (Motion_Kueche_SP_Brightness)

Dimmer Motion_Kueche_MITTAGS_Temperature "Farbtemperatur mittags" (Motion_Kueche_SP_Temperature)

Dimmer Motion_Kueche_NACHMITTAG_Brightness "Helligkeit nachmittags" (Motion_Kueche_SP_Brightness)

Dimmer Motion_Kueche_NACHMITTAG_Temperature "Farbtemperatur nachmittags" (Motion_Kueche_SP_Temperature)

Dimmer Motion_Kueche_NACHTS_Brightness "Helligkeit nachts" (Motion_Kueche_SP_Brightness)

Dimmer Motion_Kueche_NACHTS_Temperature "Farbtemperatur nachts" (Motion_Kueche_SP_Temperature)

//Bett

Group: Dimmer:AVG gMotion_Bett_Licht_Helligkeit (gMotion_Licht_Helligkeit)

Group: Dimmer:AVG gMotion_Bett_Licht_Farbtemperatur (gMotion_Licht_Farbtemperatur)

Group: Switch:OR(ON,OFF) gMotion_Bett_Sensor_Bewegung (gMotion_Sensor_Bewegung)

Group: Number:AVG gMotion_Bett_Sensor_Lux (gMotion_Sensor_Lux)

Group Motion_Bett_SP_Brightness "Bewegungsmelder Bett Helligkeit" (gMotion_SP_Brightness)

Group Motion_Bett_SP_Temperature "Bewegungsmelder Bett Farbtemperatur" (gMotion_SP_Temperature)

Number Motion_Bett_SP_Lux "Schwellwert Helligkeit Bett (lx)" (gMotion_SP_Lux)

Number Motion_Bett_SP_Timeout "Timeout Bett (s)" (gMotion_SP_Timeout)

Dimmer Motion_Bett_MORGENS_Brightness "Helligkeit morgens" (Motion_Bett_SP_Brightness)

Dimmer Motion_Bett_MORGENS_Temperature "Farbtemperatur morgens" (Motion_Bett_SP_Temperature)

Dimmer Motion_Bett_MITTAGS_Brightness "Helligkeit mittags" (Motion_Bett_SP_Brightness)

Dimmer Motion_Bett_MITTAGS_Temperature "Farbtemperatur mittags" (Motion_Bett_SP_Temperature)

Dimmer Motion_Bett_NACHMITTAG_Brightness "Helligkeit nachmittags" (Motion_Bett_SP_Brightness)

Dimmer Motion_Bett_NACHMITTAG_Temperature "Farbtemperatur nachmittags" (Motion_Bett_SP_Temperature)

Dimmer Motion_Bett_NACHTS_Brightness "Helligkeit nachts" (Motion_Bett_SP_Brightness)

Dimmer Motion_Bett_NACHTS_Temperature "Farbtemperatur nachts" (Motion_Bett_SP_Temperature)

Any ideas? :confused: sooooo close…


If you only have on off switches, it doesnt make any sense to regulate, you could only use it for turning it on (off does not work, because you wont notice it if it gets darker)

I’d open it up to see what’s going on

var blah = timers.get(raum_name)
if (blah !== null) {
   logInfo("MOTION", "something in hashtable")
   blah.cancel
   logInfo("MOTION", "trying to PUT null")
   timers.put(raum_name, null)
} else {
   logInfo("MOTION", "nothing in hashtable")
}

I don’t think you’re allowed to put null in a hashtable (unlike a hashmap).
You may need to .remove() instead

You are completely right. Hashtable doesn’t allow null keys or values. My Java is getting really rusty. HashMap does allow one key to be null and it allows null values.

Here is an all-in-one solution that I think meets all of your requirements OOTB, but it can also be customized…

This solution requires the use of the new rule engine, scripted automation, Jython, and the helper libraries. I have incorporated individual room luminance and it looks to be ready to commit.

Ok, thanks for your help! I will try that.

Looks nice, i will bookmark that. Im not sure i want to upgrade my running automation system for now.

Things missing i think would be:

-lux sensor should only be used for turning on, not turning off in my case, as it is inside the room.

-better than only checking for >98% as manual action would be any state change of that group/lamp, so i can dim lights or change color without turning off automatically, until the light is turned off manually. That way i can adjust lamps however i want without manually changing the mode first, and not only via switches but also through alexa or app or ui. A Timeout before turning back to autlmatic mode would be needed, as my detectors are slow and are on for at least 15 econds after ladt movement, so lights dont turn right back on if i want to turn them off manually.
What do you think?

The new and old rule engines can be used at the same time, so upgrading is currently optional. There are many benefits to the new rule engine and there is a section of the helper library repository for community contributed scripts and libraries, like the one I referred to, that users can make use of with some minor configuration.

This is the part of the code (and documentation) that I mentioned I have yet to commit.

In the last couple years, the only manual adjustments that we needed were to keep the lights bright, like for reading a book, which the light_action function handles. You could customize the light_action function to not change a dimmer or color light if it has a value different than any of the predefined values in any of the modes. Let me know if you’d like me to code this… should only take a few minutes. I don’t see any way to handle this for switches though.

Keep in mind that you can manually adjust your lights and they will not change back to the predefined settings until either a Mode change or a lux level change where the current and previous lux values span a low_lux_trigger. Please provide some specific scenarios that are you imagining where this would be an issue.

I don’t understand what you mean here. Are your motion sensors only ON with motion for 15s!? If you can’t adjust this, which I think you can, then you can set an OFF timer so that the OFF action will be delayed, or it will be cancelled if motion is detected again before the timer expires.

BTW, the Philips Hue Motion Sensors (and others) use Zigbee and are compatible with the Zigbee binding.