Best Practice - Motion Sensor Rules

Like most I have a number of Motion Sensors placed around my home and use them to trigger events. One of the more active area’s is my Kitchen, especially first thing in the morning.

At present when the Motion sensor is activated I then look for a number of conditions. For example, when the Lux level is low I switch on cabinet lights, but also check to see what time the motion sensor is activated and if it’s between 7am and 8am the Sonos starts playing the local radio station. My current rule has a number of if statements for each condition and action required. I wonder if creating a rule for each condition is a better way and have less if statements. Just looking for peoples opinions. For context here’s my current rule.

rule "KITCHEN: Motion Sensor Activation"
when
    Item Kitchen_SP3102_vMotion changed
then
val vToday = Today.state.format("%1$tA")
if(Kitchen_SP3102_vMotion.state == ON) {
    if(Door_Sensor_Timer.state != OFF) Door_Sensor_Timer.sendCommand(OFF)
    if(Light_Level_Status.state < 6) Kitchen_Lightstrip_vSwitch.sendCommand(ON)
    if(now.getHour() > 6 && now.getHour() < 9 && Study_ZW089_vDoor.state == CLOSED) {
        Announcement_Location.sendCommand(1)
        Thread::sleep(1000)
        if(vToday == "Monday" && Sarah_Holiday.state == ON) {
            if(Sarah_Holiday_Announced.state != ON) Announcement_Type.sendCommand(8)
        }
//        if(date.getMonth() == 7 && date.getDay() == 25) Announcement_Type.sendCommand(23)
        if(Kitchen_Sonos_Stop.state == ON && Kitchen_Morning_Announcement.state == OFF) {
            Landing_Blind_vSwitch.sendCommand(OFF)
            Kitchen_Morning_Announcement.sendCommand(ON)
            Kitchen_Sonos_Volume.sendCommand(15)
            Thread::sleep(1000)
            Announcement_Type.sendCommand(22)
            Thread::sleep(5000)
            createTimer(now.plusSeconds(5)) [|
                Kitchen_Sonos_Volume.sendCommand(20)
                Kitchen_Sonos_Tuneinstationid.sendCommand("6894")
                Kitchen_Sonos_Control.sendCommand(PLAY)
                createTimer(now.plusSeconds(30)) [|
                    Announcement_Type.sendCommand(9)
                ]
            ]
        }
        if(Announcement_Location.state == 1 && Car_Mini_Range_Announced.state == OFF) {
            createTimer(now.plusSeconds(30)) [|
                Announcement_Type.sendCommand(6)
                logInfo(logName, "System Message: Announce remaining range in the Mini.")
                logInfo(logName, "Garage: The Mini has a range of {} Miles.", Car_Mini_RangeMiles.state)
            ]
        }
    }
} else if(Kitchen_SP3102_vMotion.state == OFF && Kitchen_Lightstrip_vSwitch.state == ON) {
    var int vTimeout = (new java.util.Random).nextInt(60)
    vTimeout = vTimeout + 60
    logInfo(logName, "System Message: Kitchen Lightstrip count down timer set to {} seconds.", vTimeout)
    createTimer(now.plusSeconds(vTimeout)) [|
        if(now.getHour() > 7 && Kitchen_SP3102_vMotion.state == OFF) Kitchen_Lightstrip_vSwitch.sendCommand(OFF);
    ]
}
end

first of all: if it works - and you know what and why you did it? Why change it then? It’s not, that you have to win a beauty contest or something! :wink:
second of all: if you feel more comfortable with seperate rules (because you feel you can change it later in a year or two more easily! speak: you remember better, what you wanted to achieve), then change it!

PS: you can always have more than one DSL “rule” with OH3 rules - if you manage rules via OH3-GUI, just add rules for the condition Item Kitchen_SP3102_vMotion changed
PS2: from performance perspective it’s nothing to worry. It’ll work out both ways.

But: having “Thread::sleeps” in your code is only the second best way to implement pauses. use timers for that too, if you have more rules in place. Timers work better with a bunch of parallel “waits” than sleeps do.

Refactoring code is a great way to learn though.

As @binderth indicates, it’s mostly a matter of preference. But for the sake of showing some more OH ways of managing something like this here’s one way I would do it using separate rules.

  1. Create a separate rule in the UI with just a Script Action for each of your top level if conditions with the code for just that one condition.

  2. I’d write my actions in JS Scripting so I can use Announcing the initial release of openhab_rules_tools (now available through openHABian) and the Gatekeeper to schedule the sequences of events. So, for example, the sonos ON and announcement OFF script would be:

var {Gatekeeper} = requires('openhab_rules_tools');

var gk = cache.get(ruleUID+'_gatekeeper', () => { new gatekeeper.Gatekeeper('kitchen motion morning'); });

items.getItem['Landing_Blind_vSwitch'].sendCommand('OFF');
items.getItem['Kitchen_Morning_Announcement'].sendCommand('ON');
gk.addCommand('PT1s', () => { items.getItem['Kitchen_Sonos_Volume'].sendCommand('15'); });
gk.addCommand('PT10s', () => { items.getItem['Announcement_Type'].sendCommand('22'); });
gk.addCommand('PT30s', () => {
  items.getItem('Kitchen_Sonos_Volume').sendCommand('20');
  items.getItem('Kitchen_Sonos_Tuneinstationid').sendCommand('6894');
  items.getItem('Kitchen_Sonos_Control').sendCommand('PLAY');
}
gk.addCommand(0, () => { items.getItem('Announcement_Type').sendCommand('9'); });

Gatekeeper takes two arguments, the first is how long to wait after executing the second argument before running the next command. addCommand means “run this now then wait to run the next command”. The PT30s is and ISO8601 formatted duration string (this one means 30 seconds). It uses timers so it won’t monopolize the rule for seconds at a time. All the timer management stuff is handled for you so all you have to worry about is the times and the functions to run.

Then add a Script Condition to each that tests for whether it’s OK to run that rule now. You wouldn’t even need code for this and can do it all using the built in UI conditions. But if you were to write it as code it’d look something like:

time.toZDT().isBetweenTimes('6:00 AM', '9:00 AM')
&& items.getItem['Study_ZW089_vDoor'].state == 'CLOSED'

Though again, I’d probably just use the UI conditions.

  1. Add a trigger to each separate rule to trigger when motion is detected. But but use the appropriate trigger so those rules that should only run when it changes to ON run (e.g. in Rules DSL Item MotionSensor changed to ON) and the one rule that should run when it changes to OFF runs when it changes to OFF. You don’t need to make that a condition if the rule only triggers when it’s appropriate.

There are advantages and disadvantages here. Adding new behaviors is as simple as adding a new rule and you don’t need to mess with what’s already working. But it breaks up the logic so you can’t just look everything on the screen a the same time.

Another advantage is that rule Conditions are likely to be easier to read and understand than complex if/else if clauses as you can add additional documentation in the UI.

But of course you could add comments to achieve the same here.

Note: all code written above is untested and almost certainly contains typos and errors. It’s more just for illustration purposes. Cargo cult programmers beware!

For the benefit of non-programmers (like me) I want to emphasize the phrases I’ve bolded above, based on the following thoughts:

  1. It’s frustrating when you did something long ago and can’t figure out what you did or why you did it that way.
  2. It’s even more frustrating when you spend time changing something (because you’re obviously smarter now), only to realize exactly why you did what you did before.

(This applies to much more than just openHAB, but I’ll avoid going on a tangent for once.)

Looking at my rules, I’ve realized that they’re less about items/triggers and more about real-world activities.

If something is supposed to happen 100% of the time, I’ll make a rule for that exact activity. For example, I have a rule that runs every time my kitchen lights turn on to set the brightness. This means that I don’t ever think about “kitchen brightness” in other rules, because the brightness has nothing to do with what happens in those cases. This rule is easy to understand/change in the future (even without inline comments), and other rules are easier to comprehend since they don’t have extraneous logic.

For everything else, I use if statements with detailed comments. As Rich pointed out, I’m avoiding breaking up the logic so that Future Russ can quickly understand what’s happening within the context of the real-world activity that the rule represents. This makes it easier for me to visualize everything without getting confused by something that’s not really related to the activity (e.g. kitchen brightness).

I’m still using DSL rules, and I may rethink this approach a bit if/when I move to UI rules.

@Maximo, based on this perspective I’d split your rule up into:

  • The things you want to happen all of the time
  • The things you want to happen in the morning
  • The things you want to happen when you return home in your car

But as the others note, it’s all about personal preference.

Just note that you do not need to abandon Rules DSL to move to UI rules. You can write your script actions and script conditions in Rules DSL as well as any supported langauge. With the creation of sharedCache and privateCache in the last week and support being added to Rules DSL as we speak, the problem of sharing variables (like Timers) between rules and/or preserving them between runs of the same rule will be possible, meaning that there will not be nothing you can do in text based .rules files that you cannot be in UI rules using Rules DSL in the scripts.

I believe it will even let you share variables between scripts written in different languages.

Though I’d encourage anyone looking to do this to have a look at Blockly.

1 Like

Thanks guys, very insightful information.

1 Like