JSR223 Jython Create a Temporary Rule

This is probably me jumping in too deep before I’m ready but I have the following use case and the following idea.

Use Case:

I’ve at least three places scattered throughout my Rules where I set and monitor an anti-flapping timer for one of many Items (i.e. there can be many antiflapping timers going at a time). What I want to do is wait to do something until an Item has remained in a state for a given amount of time, and do something else if it changes state before that time. Often that “do something else” is to really do nothing.

Idea:

It seems like this would make a good library class. And I’ve already started on a class that I will find useful. I’ll create an instance of this class when ever I have a bunch of Items that I want to watch for flapping (see my alerting and entry reminder Rules I’ve posted) I’ll call a method when the Item changes to one state (e.g. ON) then call another method when it changes to OFF. I’ll pass a couple of functions, one to run when it is determined to be flapping and other to be called when it is determined not to be flapping.

So far so good and so far it’s pretty straight forward. And if this is all I can do that is still pretty useful, at least for me. But it occurred to me that if I can create the Rule that listens for the Item to change within the flapping period in this class, and delete that Rule when I’m done this class would be wholly self contained.

I know I can create a Rule in the class (at least I’m pretty sure I can). And I know I can disable a Rule. But can a delete a Rule from the JSR223 code?

What I’d like to do is create the Timer and a temporary Rule to listen for the Item changing to some other state. If the Item changes to some other state before the Timer goes off I’ll call the “flapping_function” passed to the object. If the Timer goes off without the Item changing state I’ll call the “notflapping_function” passed to the object. In either case, once that function is called, the Rule that listened for changes to the Item would need to be deleted.

I’d rather not settle just for disabling the Rule because they will build up fairly quickly over time. I’ve found what I think I need to create these Rules dynamically but not how to delete them.

Any advice is most welcome.

Deleting a rule

Yes! I just finished the brunt of the work on my new lighting system (I will be posting a PR to the HL repo when I have some docs written up on its use, still needs testing though) where I am doing something similar. It creates 2 rules for itself when the init script loads and deletes them when it is uninit-ed. It also deletes any leftover rules when it is init-ing if it finds them. Here’s that code:

# not needed in a script file, only in a module
#from core.jsr223.scope import scriptExtension
#rules = scriptExtension.get("ruleRegistry")

for objRule in [objRule for objRule in rules.getAll() if objRule.name == RULE_NAME]:
    log.warn("Found existing {rule} with UID '{uid}'".format(rule=objRule.name, uid=objRule.UID))
    rules.remove(objRule.UID)

You could just as easily cache the UID in your class for your use case, or if you use the rule decorator on the class instance it will add it as an attribute class.UID

Class instance as rule target

I tried to get someone else to do this but never got any good feedback from them. I am very interested in seeing if we can get this to work! Give me some concrete stuff to work with and I’ll put together a simple class to test.

Things to remember:

  • Class must have a method execute(event) (similar to a rule function) that is called when the rule is triggered
  • You can’t build more than one rule per class instance or method (this includes aliases)
1 Like

OK, the remove is definitely possible. But it occurred to me that I might not need to create a separate Rule for each Item I’m watching for flapping. I really only need to change the trigger on an existing Rule. I’d start off with a single Rule with no triggers what-so-ever. Then as I add the flapping timers, I’d add "Item {} changed from {}".format(itemName, items[itemName] as a trigger. The Rule does the same thing for every Item so a completely separate Rule per Item is a bit of overkill.

Is it possible to add/remove triggers to a Rule dynamically?

Something like

from core.rules import rule
from core.triggers import when

@rule("Device online/offline", description="A device we track it's online/offline status changed state", tags=["admin"])
def flappingTimer(event):
    # do stuff when the Item ends up flapping
    flappingTimer.removeTrigger("Item {} changed from {}".format(itemName, items[itemName])
# ...

    def set_timer(itemName, blah blah balh):
        flappingTimer.setTrigger("Item {} changed from {}".format(itemName, items[itemName])

Obviously the above code is bogus. Treat it as pseudo code. But if I can monkey with the trigger there would be no need to create a bunch of all but identical Rules.

I suppose another approach could be to leverage Groups. I can dynamically add Items to a Group and have a statically defined Rule triggered by Member of. I could even do that in Rules DSL. And this would keep it self contained. Maybe this is the best approach of all?

I’m not sure… I’ll investigate

Unfortunately this is impossible with the rule decorator, you have to have at least one valid trigger or it won’t create the rule.

Short answer: yes, but it wouldn’t be pretty. I think it would be better to have the class create and destroy rules as needed.

This is not needed, since the rule registry is provided in the default scope as rules. There is an example of getting a rule UID by the name of the rule in the docs. BTW, the RuleSupport preset is not needed in that example… I’ve removed it.

Are you planning to create the rule after the first state change and how fast does the flapping occur? The creation of rules takes some time, so is it possible that the flapping could occur before the rule is created?

Yes, triggers can be added/removed from rules, but this also takes time.

I’m not sure this would be needed. You have devices that flap, and timers that start when the Items change state. You also have rules that trigger on the Items’ state changes. In those rules, could you just check if the timer is active to signify that it was flapping, and then the state change could be ignored or you could log, notify, etc.?

Besides that, the sort of functionality that you’re attempting to build may be overly complicated or not work because of using the decorators. I suggest looking into using the extensions or just use the raw API.

I could but the main thing I’m after is to consolidate all the antiflapping logic in one place. In the rules that need an antiflapping timer if just have:

flapping_timer.set(itemName, interval, flapping_function, notflapping_function)

And that’s it (ignoring creation of the flapping_timer Object itself). As a user I can expect flapping_function to be called if the Item changes state too fast and notflapping_function to be called if not.

Creation and management of the timer and listening for the Item to change state would all take place inside flapping_timer.

Having slept on it, ignoring the amount if time it would take to modify or change a ruke, I think the better approach would be to do this using a Group anyway.

Duh… I’m doing it in a module not a script file, that’s why it wasn’t working for me without the import.

@rlkoshak, would you be able to approach this by assigning items to an antiflapping group, catching group events and use the triggering item to instantiate a class to do the work?

I’m actually in the middle of coding something along these lines. But I’m still experimenting to see if I can make it just a tad more dynamic.

One of the things I want to centralize is not just the Timer creation but the logic that keeps track of everything in the dict. At a lower level I’m centralizing the following:

  1. create a dict to store the active Timers for each Item
  2. on a given Item event, create a Timer, store that in the dict
  3. if the Item changes state while the Timer exists, cancel the timer, clean it from the dict, and execute one set of code
  4. if the Item doesn’t change state before the Timer expires, execute a different set of code.

Because I’m also centralizing the dict logic as well as the Timer logic, I’m putting it all in a class and I’d have each file that depends on this logic create it’s own instance of the class. The reason I’m hesitant to create one central Rule used by all instances of the class is how to handle that case where there are two Timers on one Item. I’ve not yet convinced myself that this is a use case that doesn’t not need to be supported. And that is one reason I’ve shied away from using just one central Rule for all Items.

And I’m also pursuing this as a way to learn. I’m pretty positive that the one Rule approach would work but I won’t learn much coding it that way. :wink: I may ultimately end up coding it that way if I run into trouble or maybe end up doing it that way anyway because it will probably be simpler and more efficient in the end.

Anyway, what I’m doing now is when I call the set function, I will add that Item to a Group (a Group I create in the init of the class if it doesn’t exist). Then I catch the Group events in a Rule and execute the right lambda.

I ran into some trouble with the scope yesterday so now I’m looking to figure out how to create that Rule that triggers on the Group dynamically. Because I create the Group Item dynamically I can’t use the decorators to create the Rule. But I’m pretty sure I can use the lower level stuff to create the Rule and then add the Member of trigger once I’ve created the Group.

The current version of the code (unfinished) is:

"""
A class to centralize antiflapping timer logic for multiple Items
"""
from core.jsr223.scope import ir
from core.items import add_item
from core.rules import rule
from core.triggers import when
from core.actions import ScriptExecution
from org.joda.time import DateTime
from core.log import log_traceback

class Antiflapping_Timers(object):
    """Creates an openHAB Timer and calls the passed in method. It manages
    keeping track of the timers and cleaning up after they expire
    """

    def __init__(self, log):
        self.timers = {}
        self.log = log

        # Create the Group if it doesn't already exist
        # TODO, pass the name of the Group to the function
        if ir.getItems("AntiflappingTimers") == []:
            log.info("Creating AntiflappingTimers Group")
            add_item("AntiflappingTimers", item_type="Group", label="AntiflappingTimers")

    @log_traceback
    def not_flapping(self, scope, itemName):
        self.log.info("{} is not flapping!".format(itemName))
        ir.getItem("AntiflappingTimers").removeMember(ir.getItem(itemName))
        self.log.info("Removed {} from group".format(itemName))
        self.timers[itemName]["not_flapping"]()
        self.log.info("Called function")

    @log_traceback
    def set(self, itemName, interval, not_function, flapping_function):
        """
        If a Timer alreday exists, cancel it and delete it and create a new one:

        Args
            itemName: The name of the Item the Timer is watching for flapping
            interval: how long the item need to remain in the same state to be considered not flapping in milliseconds
            not_function: function to call when the Item is determined to not be flapping
            flapping_function: function to call when the Item is determined to be flapping
        """
        if itemName in self.timers:
            self.log.warning("There is already an antiflapping timer for {}".format(itemName))
            self.timers[itemName]['timer'].cancel()
            del self.timers[itemName]

        self.log.info("Creating the antiflapping timers for {}".format(itemName))
        item = ir.getItem(itemName)
        ir.getItem("AntiflappingTimers").addMember(item)
        self.timers[itemName] = {'orig_state':   item.state,
                                 'flapping':     flapping_function,
                                 'not_flapping': not_function,
                                 'timer':        ScriptExecution.createTimer(DateTime.now().plusMillis(interval), lambda: self.not_flapping(scope, itemName))}

    # @rule("Listen for flapping", description="A Rule to listen for flapping of Items")
    # @when("Member of AntiflappingTimers changed")
    # def flapping(self, event):
    #     self.log("{} is flapping!".format(event.itemName))
    #     self.timers[itemName][flapping]()
    #     scope.getItem("AntiflappingTimers").removeMember(event.itemName)

My semi-final version of this idea follows. I’ve veered a little bit from my original plans. Instead of creating something to help manage just flapping timers, I’ve created a class that helps manage lots of Timers when you need one or more Timer per Item. The important features are:

  • each Item can have more than one Timer
  • moves all of the book keeping logic to the new class
  • minimal set of functions
  • as encapsulated as possible.
"""
A class to centralize management of multiple Timers in cases where one needs to keep
track of one timer per Item
"""
from core.jsr223.scope import ir
from core.jsr223.scope import items
from core.actions import ScriptExecution
from org.joda.time import DateTime
from core.log import log_traceback

class timer_mgr(object):
    """
    Keeps and manages a dictionary of Timers keyed on an Item name.
    """

    def __init__(self):
        self.timers = {}

    @log_traceback
    def __not_flapping(self, itemName):
        """
        Called when the Timer expires. Call the function and delete the timer from the dict.
        This function ensures that the dict get's cleaned up.

        Args:
            itemName: the name of the Item associated with the Timer
        """
        try:
            self.timers[itemName]['not_flapping']()
        finally:
            del self.timers[itemName]

    @log_traceback
    def timer_check(self, itemName, interval, function, flapping_function=None, reschedule=False):
        """
        Call to check whether an Item is flapping. If a Timer exists, create a new timer to run the passed
        in function. 
        If a Timer exists, reschedule it if reschedule is True. If a flapping_function was passed, run it.

        Args:
            itemName: The name of the Item we are monitoring for flapping
            interval: How long the Item needs to remain the same state before calling function, in milliseconds
            function: Function to call when the timer expired
            flapping_function: Optional function to call if the Item is found to be flapping
            reschedule: Optional flag that causes the Timer to be rescheduled when the Item is flapping
        """

        # Timer exists, reschedule and call flapping_lambda if necessary
        if itemName in self.timers:
            # Reschedule the Timer if desired
            if reschedule: self.timers[itemName]['timer'].reschedule(DateTime.now().plusMillis(interval))

            # Cancel the Timer if not rescheduling
            else:
                self.timers[itemName]['timer'].cancel()
                del self.timers[itemName]

            # Call the flapping function
            if flapping_function: flapping_function()

        # No timer exists, create the Timer
        else:
            item = ir.getItem(itemName)
            self.timers[itemName] = { 'orig_state':   item.state,
                                      'timer':        ScriptExecution.createTimer(DateTime.now().plusMillis(interval), lambda: self.__not_flapping(itemName)),
                                      'flapping':     flapping_function,
                                      'not_flapping': function }

    def has_timer(self, itemName): 
        """
        Returns True if there is a Timer for this Item, False otherwise
        """
        return True if itemName in self.timers else False

    @log_traceback
    def cancel(self, itemName):
        """
        Cancels the Timer assocaited with this Timer if one exists
        """
        if not self.has_timer(itemName): return
        self.timers[itemName]['timer'].cancel() 
        del self.timers[itemName]

And a test script showing how to use it:

from core.log import logging, LOG_PREFIX
log = logging.getLogger("{}.TEST".format(LOG_PREFIX))
 
import personal.timer_mgr
reload(personal.timer_mgr)
from personal.timer_mgr import timer_mgr

timers = timer_mgr()

def flapping():
    log.info("Test was found to be flapping")

def not_flapping():
    log.info("Test was found not to be flapping")

from time import sleep

log.info("TEST: Flapping, no lambda, no reschedule: no output")
timers.timer_check("Test", 1000, not_flapping)
sleep(0.5)
timers.timer_check("Test", 1000, not_flapping)
sleep(0.5)
    
log.info("TEST: Flapping, with lambda, no reschedule: Test as found to be flapping")
timers.timer_check("Test", 1000, not_flapping, flapping_function=flapping)
sleep(0.5)
timers.timer_check("Test", 1000, not_flapping, flapping_function=flapping)
sleep(0.5)

log.info("TEST: Flapping, with lambda and reschedule: Test was found to be flapping, Test was found not to be flapping")
timers.timer_check("Test", 1000, not_flapping, flapping_function=flapping, reschedule=True)
sleep(0.5)
timers.timer_check("Test", 1000, not_flapping, flapping_function=flapping, reschedule=True)
sleep(1.1)

log.info("TEST: Not flapping: Test was found not to be flapping")
timers.timer_check("Test", 1000, not_flapping)
sleep(1.1)

log.info("TEST: hasTimer and cancel")
timers.timer_check("Test", 1000, not_flapping)
sleep(0.5)
if timers.has_timer("Test"): log.info("Timer exists")
log.info("Cancelling timer")
timers.cancel("Test")
sleep(0.6) 
 
log.info("Cancelling non-existant timer")
if not timers.has_timer("Test"): log.info("No timer exists")
timers.cancel("Test")
log.info("Done")