JSR223 Python: Timer in dict doesn't work

I’m on the latest snapshot (as of today) and I’m in the process of converting one of my more complex Rules DSL Rules over to Python. This Rule requires the use and tracking of Timers. I’m trying to use Python Timers, not the createTimer Action for now. I’m also not looking to use Expire Timers. It’s a learning exercise.

My problem is I can create Timers that work just fine if I store the Timer in a single variable. But as soon as I try to put the Timer into a dict it doesn’t work. I’ve distilled the code down to the following:

testTimers = { }
testTimer = None

def timer_body(msg):
    logging.info("Timer expired: " + msg)

@rule("Test timers")
@when("System started")
def timer_test(event):
    timer_test.log.info("Importing global")
    global testTimers
    global testTimer

    timer_test.log.info("Creating a singular test timer")
    testTimer = Timer(2, lambda: timer_body("Singular"))
    timer_test.log.info("Timer state is " + str(testTimer.getState()))
    testTimer.start()
    timer_test.log.info("Timer state is " + str(testTimer.getState()))

    timer_test.log.info("Creating a test timer dict")
    testTimers["foo"] = Timer(2, lambda: timer_body("Dict"))
    timer_test.log.info("Timer state is " + str(testTimers["foo"].getState()))
    testTimers["foo"].start()
    timer_test.log.info("Timer state is " + str(testTimers["foo"].getState()))

The output is:

2019-07-22 15:45:10.438 [INFO ] [jsr223.jython.rules.Test timers     ] - Importing global
2019-07-22 15:45:10.439 [INFO ] [jsr223.jython.rules.Test timers     ] - Creating a singular test timer
2019-07-22 15:45:10.441 [INFO ] [jsr223.jython.rules.Test timers     ] - Timer state is NEW
2019-07-22 15:45:10.455 [INFO ] [jsr223.jython.rules.Test timers     ] - Timer state is RUNNABLE
2019-07-22 15:45:10.456 [INFO ] [jsr223.jython.rules.Test timers     ] - Creating a test timer dict
2019-07-22 15:45:10.462 [ERROR] [jsr223.jython.rules.Test timers     ] - Traceback (most recent call last):
  File "/openhab/conf/automation/lib/python/core/log.py", line 51, in wrapper
    return fn(*args, **kwargs)
  File "<script>", line 57, in timer_test
AttributeError: '_Timer' object has no attribute 'getState'

2019-07-22 15:45:10.463 [ERROR] [e.automation.internal.RuleEngineImpl] - Failed to execute rule '31b10faf-f436-4f57-b9f1-45cf3e484d0f': Fail to execute action: 1
2019-07-22 15:45:12.479 [INFO ] [ROOT                                ] - Timer expired: Singular

I’ve tried a bunch of variations but I always get the same result. As soon as the Timer gets anywhere close to the dict it fails.

I’ve found a couple of examples on the forum and I don’t see where I’m doing anything different. What am I missing?

You don’t need to use global with lists and dicts… … https://www.e-education.psu.edu/geog489/node/2280.

I still don’t have a good enough understanding of timers to give you a good explanation. Sometimes their type will be threading._Timer (these don’t have a getState method). But if you raise them to global, they will have type org.python.core.FunctionThread (these have getState).

from threading import Timer

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

testTimers = {}

def timer_body(msg):
    timer_test.log.info("Timer expired: {}".format(msg))

@rule("Test timers")
@when("System started")
def timer_test(event):
    timer_test.log.info("Creating a singular test timer")
    singular_timer = Timer(2, lambda: timer_body("Singular"))
    global singular_timer

    timer_test.log.info("Timer state is {}".format(singular_timer.getState()))
    singular_timer.start()
    timer_test.log.info("Timer state is {}".format(singular_timer.getState()))

    timer_test.log.info("Creating a test timer dict")
    dict_timer = Timer(2, lambda: timer_body("Dict"))
    global dict_timer
    
    testTimers["foo"] = dict_timer
    # comment out the global and compare these logs
    timer_test.log.info("type(testTimers[\"foo\"]) = {}".format(type(testTimers["foo"])))
    timer_test.log.info("dir(testTimers[\"foo\"]) = \n{}".format(dir(testTimers["foo"])))
    timer_test.log.info("Timer state is {}".format(testTimers["foo"].getState()))
    testTimers["foo"].start()
    timer_test.log.info("Timer state is {}".format(testTimers["foo"].getState()))

OK, I can live with that. Using global on the dict was one of my “lets try pseudo-random stuff and see if it works.” I’ll back that out for sure.

Assigning the Timer to a global before assigning it to the dictc was actually going to be my next test. I’m glad to see I wasn’t totally off base. I wonder if it’s smart enough to see that if the Timer is declared in a local context it doesn’t make sense to have a status method because the variable would be gone before you can test it.

Thanks!

I’ve been compiling notes and will start contributing to the docs as I build my experience a bit more.

I have a bunch of updates to push. I’ll prioritize this for tomorrow, but you may want to hold off until you see it come through so you don’t have to rebase. I’m glowing to see you getting into Jython!

You don’t need to global the dict if you declare it has an empty dict at the top of the file, as you have done. You can’t change the value of variable from a different context than it was declared, but you can modify a dict or list.

test_dict = {} # set value to empty dict
test_var = 0

def test_func():
  # modify dict from parent scope
  test_dict["test_key"] = "test_value"
  # set var from parent scope
  test_var = 1

test_func()
print(test_dict)
# prints {"test_key": "test_value"}
print(test_var)
# prints 0

What you can’t do is build a new dict in a function and try to assign it to the global variable name.

2 Likes

Right, but the more I think about threading in Python the more I think that getting _Timer back is correct. To you can’t getState() a thread, you isAlive() to check if a thread has finished running (also returns True if the thread has not been started yet). My understanding is that the threading.Timer class is a wrapper for a thread that sleeps for the period given and then calls the function given.