Limit how often a rule can be run

Hi,
I have a rule that runs whenever my or my partners iPhone location changes. This info is fetched from iCloud and both locations usualy change at the same time, even if by just a little. This causes the rule to fire twice within a second. How can i make it run no more than for example once a minute? I tried a lock but that didnt work since i guess it finishes in less than a second and it gets unlocked before running again.

Also, if i manage to limit this, how can i make sure the rule uses the latest data when running? It has to act on both positions, so ideally whenever any location changes i want to wait 30 seconds and then run the rule once. Any ideas please?

It might be helpful is you provide why you need this. It can change the recommendations significantly.

IMHO, one will get a much better recommendation when describing their ultimate goal rather than focusing on what they think is the solution. For example, if the problem is you are getting too many notifications, it is easy to throttle the notifications; easier in fact than stopping the rule itself from running in some cases.

From what you describe, I am not thinking that stopping the rule from running is the right solution and there is likely a better overall approach. But I can’t know that without knowing what your ultimate goal is. Perhaps seeing the actual rule will also help.

Hi, here is the .rules file https://pastebin.com/N76FBe6r and here is the problem. If the notification fires twice, all commands will fire twice with unknown consequences.

IMG_1115

Decide if you want to queue or discard the ‘excess’ requests.

Your issue is related to both your phones coming home at the same time launches 2 instances of the rule and therefore two notifications. But you probably know that :slight_smile:

I have created switches that will lockout a specific rule that cause havoc if run twice in rapid succession such as garage door control. When the rule runs, have it turn on that switch. Then have another rule that turns the switch off after a set period of time such as 5 minutes when you know everything has settled. Actions within your rule should check if that switch is on before firing commands.

You could get more granular (and keep to a single rule) with creating datetime items and updating it with times when you fire off parts of the rule and check that datetime before firing the rule bits.

Here is a slight variation on Moxified solution using a timestamp instead of a switch.

import java.util.concurrent.locks.ReentrantLock
val ReentrantLock iPhoneLocationLock = new ReentrantLock() // keep the lock
var distanceThreshold = 125.0
var lastAlert = now.minusMinutes(5) // set to five minutes ago so the Rule can run when OH first starts
 
rule "Presence"
when
    Item MaciejsLocation changed or
    Item PatrycjasLocation changed
then
    // Load previous state to determine change
    var maciejWasAway = MaciejIsHome.state == OFF
    var patrycjaWasAway = PatrycjaIsHome.state == OFF
 
    // Distance calculation
    var PointType maciejsiPhoneLocation = new PointType(MaciejsLocation.state.toString)
    var maciejDistance = maciejsiPhoneLocation.distanceFrom(new PointType(Home.state.toString))
    var maciejIsAwayNow = maciejDistance > distanceThreshold
   
    var PointType patrycjasiPhoneLocation = new PointType(PatrycjasLocation.state.toString)
    var patrycjaDistance = patrycjasiPhoneLocation.distanceFrom(new PointType(Home.state.toString))
    var patrycjaIsAwayNow = patrycjaDistance > distanceThreshold
 
    iPhoneLocationLock.lock() // this only keeps two copies of the rule from running at the same time
    try {
        // When last person leaves home
        if (maciejIsAwayNow && patrycjaIsAwayNow && (!maciejWasAway || !patrycjaWasAway)) {
            HarmonyLivingRoomCurrentActivity.sendCommand("PowerOff")
            PlexLivingRoomPause.sendCommand(ON)
            // TODO: Turn off lights
            // TODO: Start Neato Botvac
 
            logInfo("iphone.rules", "Everybody left Home (Maciej "+ maciejDistance +" m, Patrycja "+ patrycjaDistance +" m)")
            if(lastAlert.isBefore(now.minusMinutes(5)) {
                pushNotification("", "Everybody left Home!")
                lastAlert = now
            }
        } else
        // When everyone comes home together
        if (!maciejIsAwayNow && !patrycjaIsAwayNow && maciejWasAway && patrycjaWasAway) {
            // TODO: Turn on lights
            // TODO: Dock Neato Botvac
 
            logInfo("iphone.rules", "Everybody came Home now (Maciej "+ maciejDistance +" m, Patrycja "+ patrycjaDistance +" m)")
            if(lastAlert.isBefore(now.minusMinutes(5)) {
                pushNotification("", "Everybody came Home now!")
                lastAlert = now
            }
        } else
        // When one person comes home to the other
        if (!maciejIsAwayNow && !patrycjaIsAwayNow && (maciejWasAway || patrycjaWasAway)) {
            // Actions TBD
 
            logInfo("iphone.rules", "Everybody is Home now (Maciej "+ maciejDistance +" m, Patrycja "+ patrycjaDistance +" m)")
            if(lastAlert.isBefore(now.minusMinutes(5)) {
                pushNotification("", "Everybody is Home now!")
                lastAlert = now
            }
        } else
        // When somebody comes home to an empty house
        if (maciejWasAway && patrycjaWasAway && (!maciejIsAwayNow || !patrycjaIsAwayNow)) {
            // TODO: Turn on lights
            // TODO: Dock Neato Botvac
           
            logInfo("iphone.rules", "Someone came Home now (Maciej "+ maciejDistance +" m, Patrycja "+ patrycjaDistance +" m)")
            if(lastAlert.isBefore(now.minusMinutes(5)) {
                pushNotification("", "Someone came Home now!")
                lastAlert = now
            }
        }
 
        // Debug
        /*if (maciejIsAwayNow && patrycjaIsAwayNow && maciejWasAway && patrycjaWasAway) {
            logInfo("iphone.rules", "Everybody is away, nothing to do (Maciej "+ maciejDistance +" m, Patrycja "+ patrycjaDistance +" m)")
        } else if (!maciejIsAwayNow && !patrycjaIsAwayNow && !maciejWasAway && !patrycjaWasAway) {
            logInfo("iphone.rules", "Everybody is Home, nothing to do (Maciej "+ maciejDistance +" m, Patrycja "+ patrycjaDistance +" m)")
        } else if (!maciejIsAwayNow || !patrycjaIsAwayNow) {
            logInfo("iphone.rules", "Somebody is Home, nothing to do (Maciej "+ maciejDistance +" m, Patrycja "+ patrycjaDistance +" m)")
        }
 
        logInfo("iphone.rules", "Maciej Was Away: " + maciejWasAway + ", Patrycja Was Away: " + patrycjaWasAway)
        logInfo("iphone.rules", "Maciej Is Away: " + maciejIsAwayNow + ", Patrycja Is Away: " + patrycjaIsAwayNow)
        */
    } finally {
        iPhoneLocationLock.unlock()
    }
 
    // Update item states
    if (maciejIsAwayNow) {
        MaciejIsHome.postUpdate(OFF)
    } else {
        MaciejIsHome.postUpdate(ON)
    }
   
    if (patrycjaIsAwayNow) {
        PatrycjaIsHome.postUpdate(OFF)
    } else {
        PatrycjaIsHome.postUpdate(ON)
    }
end

The above only prevents alerts from being sent in too rapid succession. To make it so the other parts of the rule do not execute as well (i.e. turning on/off lights) move that behavior inside the if(lastAlert.isBefore(now.minusMinutes(5)).

1 Like

Exactly what I was saying except using a var instead of an item which is superior! In my defense… I’m sorting through patches for my infrastructure against spectre and meltdown :confused:

Thanks for the suggestions, but as i said originally i don’t want any of the actions to run until 30 seconds after the first change to make sure the second iPhone location has also been updated. Example of what i want:

10:00:00 First iPhone is recorded as at home.
10:00:00 Rule “fires” because the location changed, but it should not take action but start a 30 second timer, except updating the XXXIsHome item states on the bottom of the rule.
10:00:02 Second iPhone is recorded as at home.
10:00:02 Rule “fires” again because the location changed, but it will not do anything because there is a timer with 28 seconds remaining that will take action once that fires, except updating the XXXIsHome item states on the bottom of the rule.
10:00:30 Rule fires for real after waiting 30 seconds because now it can be sure that both locations have been updated.

OK, but what if for some reason the second phone isn’t updated in that 30 seconds?

timer = createTimer(now.plusSeconds30) [ | 
    // do stuff after 30 seconds
    timer = null
])
if(timer != null) // skip rule bcause there is already a Timer

Honestly, this Rule is already way overly complex IMHO. I would suggest breaking it up quite a bit.

  1. Put the top part of your current Presence rule in a rule and sendCommand to the IsHome Items based on the distance threshold. End of this Rule.
rule "Process Phone Location"
when
    Item MaciejsLocation changed or
    Item PatrycjasLocation changed
then
   // Distance calculation
    var PointType maciejsiPhoneLocation = new PointType(MaciejsLocation.state.toString)
    var maciejDistance = maciejsiPhoneLocation.distanceFrom(new PointType(Home.state.toString))
    MaciejIsHome.sendCommand(if(maciejDistance > distanceThreshold) OFF else ON)

    var PointType patrycjasiPhoneLocation = new PointType(PatrycjasLocation.state.toString)
    var patrycjaDistance = patrycjasiPhoneLocation.distanceFrom(new PointType(Home.state.toString))
    PatrycjaIsHome.sendCommand(if(patrycjaDistance > distanceThreshold) OFF else ON)
end
  1. Use Generic Presence Detection. Set the Present_Timer to 30 seconds instead of 5 minutes.

  2. Trigger a Rule based on Present receiving a command where you do your came/left logic

rule "Presence"
when
    Item Present received command
then

    // Everyone Left
    if(receivedCommand == OFF){
        HarmonyLivingRoomCurrentActivity.sendCommand("PowerOff")
        PlexLivingRoomPause.sendCommand(ON)
        // TODO: Turn off lights
        // TODO: Start Neato Botvac

        logInfo("iphone.rules", "Everybody left Home")
        pushNotification("", "Everybody left Home!")
    }

    // Everone came home in the last 30 seconds
    else if(MaciejIsHome.state == ON && PatrycjaIsHome.state == ON){
        // TODO: Turn on lights
        // TODO: Dock Neato Botvac
        logInfo("iphone.rules", "Everybody came Home now")
        pushNotification("", "Everybody came Home now!")
    }

    // Someone came home to the other, required Persistence
    else if(MaciejIsHome.lastUpdate.isAfter(now.minusSeconds(30) || PatrycjaIsHome.lastUpdate.isAfter(now.minusSeconds(30)) {
        // Actions TBD
        logInfo("iphone.rules", "Everybody is Home now")
    }

    // Somebody comes home to an empty house
    else {
        // TODO: Turn on lights
        // TODO: Dock Neato Botvac
        logInfo("iphone.rules", "Someone came Home now")
        pushNotification("", "Someone came Home now!")
    }
end

By separating the behaviors like this the rules become much simpler and easier to read, reason about, and understand. You can use MapDB for the persistence for this so you don’t have to worry about setting up anything complex or that will grow over time.

The above does exactly what you are after (i.e. will not execute the rule that does stuff until 30 seconds after the first person comes home or everyone leaves) plus it gives you a single Present Item you can use in your other Rules if desired and it lets you transparently add more presence sensors without needing to change the rules.

Since most of the behavior that you do when one person comes home verses both people except for your log statement and alert I recommend only writing that logic once and use a lambda or restructure your if/else statements to you don’t have duplicate code.