[SOLVED] System started triggered whenever one single script is changed: is there a more specific hook/trigger that will only trigger when one single rules file changes?

Running openHAB 2…5 M5 on a RPi3B+ (openHABian Buster).

I realize that whenever I edit a single Jython rules file, System started is triggered. This results in an unwanted cascade of Jython rules file reload events. So it appears to me that System started may not be appropriate to trigger actions when a single script reloads.

There’s a scriptUnload() hook that can be used when a script is unloaded, but is there an equivalent scriptLoad() hook that could be triggered when one single script file has been updated (or at startup of the rules engine)?

The Jython helper library ‘System started’ trigger behaves just like it does in the rules DSL. There is also a Directory watcher trigger that could be used to monitor a directory of script files, but this trigger has not worked since the breaking API changes in S1319. So currently, there is no alternative.

When the Python file is loaded it executes the commands from top to bottom. You could put your code from the System started Rule just at the top level of the file and it should execute those lines when the file is loaded.

This is how I write a lot of my tests. For example, here is the test file I use to test my countdown_timer library.

import time
import personal.countdown_timer
reload (personal.countdown_timer)
from personal.countdown_timer import CountdownTimer
from core.log import log_traceback, logging, LOG_PREFIX
#from org.joda.time import DateTime
from datetime import datetime, timedelta
log = logging.getLogger("{}.TEST.countdown_timer".format(LOG_PREFIX))

func_called = False

def test():
    global func_called
    func_called = True

# Create a couple of Items to test with
from core.items import add_item
log.info("Creating test Items")
number = "Countdown_Timer_Test_Number"
string = "Countdown_Timer_Test_String"
add_item(number, item_type="Number")
add_item(string, item_type="String")

try:
    # Test that func_called on even seconds.
    log.info("--------------------------- seconds")
    timer = CountdownTimer(log, (datetime.now() + timedelta(seconds=2)), test, number)
    time.sleep(2.1)
    assert func_called

    # Test that func_called on fraction of seconds.
    log.info("--------------------------- milliseconds")
    func_called = False
    timer = CountdownTimer(log, (datetime.now() + timedelta(seconds=2, microseconds=100000)), test, number)
    time.sleep(2.2)
    assert func_called

    # Test that number gets updated properly
    log.info("--------------------------- number Item")
    log.info("number item is starting at {}".format(items[number]))
    assert items[number] == DecimalType(0)
    timer = CountdownTimer(log, (datetime.now() + timedelta(seconds=5)), test, number)
    time.sleep(0.1)
    log.info("number item is now {}".format(items[number]))
    assert items[number] == DecimalType(4)
    time.sleep(1)
    log.info("number item is now {}".format(items[number]))
    assert items[number] == DecimalType(3)
    time.sleep(1)
    log.info("number item is now {}".format(items[number]))
    assert items[number] == DecimalType(2)
    time.sleep(1)
    log.info("number item is now {}".format(items[number]))
    assert items[number] == DecimalType(1)
    time.sleep(1)
    log.info("number item is finally {}".format(items[number]))
    assert items[number] == DecimalType(0)

    # Test that string gets updated properly.
    log.info("--------------------------- string Item")
    log.info("string item is starting at {}".format(items[string]))
    timer = CountdownTimer(log, (datetime.now() + timedelta(seconds=5)), test, string)
    time.sleep(0.1)
    log.info("string item is now {}".format(items[string]))
    assert str(items[string]).startswith("0:00:04")

    time.sleep(1)
    log.info("string item is now {}".format(items[string]))
    assert str(items[string]).startswith("0:00:03")

    time.sleep(1)
    log.info("string item is now {}".format(items[string]))
    assert str(items[string]).startswith("0:00:02")

    time.sleep(1)
    log.info("string item is now {}".format(items[string]))
    assert str(items[string]).startswith("0:00:01")

    time.sleep(1)
    log.info("string item is finally {}".format(items[string]))
    assert str(items[string]) == "0:00:00"

    # Test that hasTerminated works
    log.info("--------------------------- hasTerminated()")
    timer = CountdownTimer(log, (datetime.now() + timedelta(seconds=2)), test, number)
    assert not timer.hasTerminated()
    time.sleep(2)
    assert timer.hasTerminated()

    # Test that cancel works.
    log.info("--------------------------- cancel()")
    timer = CountdownTimer(log, (datetime.now() + timedelta(seconds=2)), test, number)
    time.sleep(0.1)
    old_val = items[number]
    timer.cancel()
    time.sleep(2)
    assert items[number] == DecimalType(0)

except AssertionError:
    import traceback
    log.error("Exception: {}".format(traceback.format_exc()))
    timer.cancel()

else:
    log.info("CountdownTimer tests passed!")
finally:
    log.info("Deleting test Items")
    from core.items import remove_item
    remove_item(number)
    remove_item(string)

When this file is put into a folder where it’s picked up it immediately executes the tests, and it only executes them when the file is loaded.

So you can just put the code that you have in your System started rule outside of any functions and it will be executed when the file is loaded. Or you can put them into a function and put a call to that function at the top level of your script.


def on_load():
    # do stuff
    # do more stuff

on_load()

I don’t know if there is any sort of issue with doing it like this but based on my experience it should work.

2 Likes

^ This is exactly how I do it too.

Thanks!

I just removed the “System started” trigger from all my scripts since using the @when("System started)" trigger doesn’t make sense at all.

It does make sense when you have a rule that is triggered by System started and other triggers as well.

But when System started has been triggered, the rule engine has been restarted, and then all scripts are re-loaded.

Is there a case where System started would happen in which the rule engine is not reloaded, hence script reloading does not happen?

No, but if I have a rule that needs to run during other events too what are by choices?

  1. Delicate the rule at the top level.

  2. Put the role in a function and call the function from the to level and from the rule instead.

  3. Add a System started trigger to the rule.

1 is kind of stupid, 2 creates an unnecessary level of redirection. 3 keeps everything together in a much more intuitive and self documenting. You don’t have to look somewhere else to learn the rule is also called at system startup.

If the role only has a System started trigger, I agree, there isn’t much point to the rule trigger. But when you have a rule triggered by item or channel events in addition to System started, the code is clearer and more self contained when using the trigger.