[SOLVED] OH3 JSR 223 Jython - No module named core in AND how to manipulate a cron trigger rule

I am starting to write my first Jython JSR223 rule in OH3

I started with installing the JSR223 engine and the following “Hello World” works as expected

from org.slf4j import LoggerFactory

LoggerFactory.getLogger("org.openhab.core.automation.examples").info("Hello world!")

However when I start adding more imports

from org.slf4j import LoggerFactory
from core.rules import rule
from core.triggers import when
from core.utils import sendCommand

LoggerFactory.getLogger("org.openhab.core.automation.examples").info("Hello world!")

it fails with

Script execution of rule with UID ‘jythonTestRule’ failed: ImportError: No module named core in at line number 2

I looked around in the forum and saw some similar but usually rather old threads or non-related to OH3 (correct if I am wrong :slight_smile: )

With respect to Rules — openHAB Helper Libraries documentation adding

scriptExtension.importPreset("RuleSimple")
scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleFactories")

didn’t help either.

Can anyone point me to a simple working rule jython script that checks a state and sends a command?

thanks in advance
Stefan

How did you initially set up openHAB with Jython? The following is about the only sure up-to-date way to get it working with OH3:

1 Like

Thank a lot! I wasn’t aware that there needs to be some more work to be done before it will work. I will try to take my time tomorrow as I think this needs to be done carefully!

I think what is most relevant are the steps written here:

https://crazyivan359.github.io/openhab-helper-libraries/Getting%20Started/Installation.html

with the exception mentioned as

  • Step 5 - Install the Jython Scripting add-on.
  • Step 7 - Download the contents of this repository.
  • Step 10 & 11 - skip these steps.

In the meantime, can you share an example of a simple working rule so I can make sure I can verify that everything works as expected and as working point to start from?

Even though here is some documentation but I would love to see some simple rule like (PSEUDO code)

if (myitem.state == ON)
  myOtherItem.post("OFF")

Update: see But How Do I…? — openHAB Helper Libraries documentation for examples

if (items["myitem"]==ON):
  events.postUpdate("myOtherItem", "OFF")

TIA
Stefan

Check steps 12 & 13 - those give you a simple script which repeats every 10 seconds and writes something in the logs!

EDIT: Another boat load of examples in the Script Examples folder:

Stephan, you will not need to change much to have your rules working in OH3.

For installation, looks like you got it working, so you just need to download my fork instead of the main Helper Libraries.

For rules all that has really changed is that when dealing with DateTime you now need to use ZonedDateTime instead of Joda.

The examples are a good starting point, just be aware of the above about DateTime, I have not updated any examples, documentation, or community scripts yet.

Feel free to ask any questions you have, there are a growing number of us that are successfully using Jython rules in OH3 who are happy to help!

2 Likes

I really appreciate that and for all your effort, Michael!
I will let you know how it goes.

I looked through all the examples. Still…:wink:

Do you mind providing me the above really simple pseudo code as a Jython openhab rule (only the script part, see below, as I am triggering the script via the UI trigger rules)

Another question: when using the script through the UI I do not need the @rule and @when and the event variable is automatically provided? Correct?

What I am also interested in is how I could create a dynamic time trigger that triggers based on a provided time string like “11:30” (not a cron expression). Is that possible with ScriptExecution.createTimerWithArgument ?

Thanks, Stefan

Can’t be done. You can trigger a rule from any arbitrary event (such as setting an Item to a target time) and have the rule set up a block of code to execute at your designated time, using the Timer method you indicate.

i have a jython-rule i call “shutter-management”. here i dynamically create rules for individual opening-times for each shutter. in sitemap i use setpoint items to be able to change the times. always when i change the value the existing rule will be removed and a new rule will be created.

this is the code-snippet, perhaps it might point you to a desired direction:

    def __gen_cron(self, time_in_min):
        hour = time_in_min / 60
        minute = time_in_min % 60
        # LogAction.logInfo(u"shutter_management", u"Zeit in Minuten = {} -> {}:{} Uhr". format(time_in_min, hour, minute))

        cron = "0 {} {} 1/1 * ? *".format(minute, hour)
        return cron

    def __adjust_rule_trigger_body(self):
        shutter_name = self.__get_instance_name()
        LogAction.logInfo(u"shutter_management", u"Cron Rule neu erstellen für {}".format(shutter_name))
        self.trigger_timer = None

        cron_work = self.__gen_cron(self.get_opentime_work())
        cron_holiday = self.__gen_cron(self.__get_opentime_holiday())

        try:                                                                    # if rule exists, remove it
            rule_name = "{} check morning open".format(shutter_name)
            rule_uid = [rule_object for rule_object in rules.getAll() if rule_object.name == rule_name][0].UID
            LogAction.logInfo(u"shutter_management", u"Rule '{} check morning open' existiert schon, wird entfernt".format(shutter_name))
            scriptExtension.importPreset("RuleSupport")
            ruleRegistry.remove(rule_uid)
        except:
            pass

        # Rule erstellen
        @rule("{} check morning open".format(shutter_name))
        @when("Time cron {}".format(cron_work))
        @when("Time cron {}".format(cron_holiday))
        def check_morning_open(event):
            self.__set_prio_flag(True)
            shutter_name = self.__get_instance_name()
            # LogAction.logInfo(u"shutter_management", u"############## Shutter UP - CHECK ##############")
...
1 Like

Thanks Stefan, looks like a great start to work. As soon as I have the basic rule working, I will dive into your code. Very much appreciated!

Stefan

Correct. All the stuff related to setting up the Rule is done outside of Python when building the rules in the UI. This means the rule name, tags, description, and triggers are all outside the Python code. All you need to write is the stuff in the function that gets called when the rule is triggered.

Note that because you are not and cannot use the @rule decorator you will have to set up and configure the logger yourself. You don’t get it for free like happens when using the decorator.

You’ll probably have to explain more about what you are really after here because this sounds a whole lot like an XY problem. All the createTimerWithArgument does is allow you to pass a value to the Timer function. It doesn’t change anything about how the Timer is scheduled.

It is possible to change the time that a rule is triggered but it’s not going to be worth the effort. Two approaches I can think of:

  • create a rule that triggers on system started or when some Item containing your “11:30” String changes, this rule programmatically creates a new rule using that parsed time converted to a cron trigger format as the trigger

  • create a rule that triggers when some Item containing your “11:30” String changes, this rule make the REST API calls to pull the existing rules JSON, modifies the trigger and then makes the REST API calls to change the Rule

For something like this that is kind of a lot of complicated code to write and there is some additional book keeping and cleanup required. You can achieve the same functionality using just plain old Timers.

Note: if you compare my Python and JavaScript versions of the Ephemeris Time of Day implementations you will see that I dynamically create the Rule to update it’s triggers in the Python version. I’ve abandoned that approach in the JavaScript version and went back to a more “standard” approach using Groups.

https://github.com/rkoshak/openhab-rules-tools/tree/main/ephem_tod

Thanks Rich and everyone else here. I’m pretty loaded this week with work but I will definitely start pursuing all of the above and will eventually come back!

Thanks!
Stefan

The good news is that basically the setup works now. When I create a rule and trigger it, I see

2021-03-09 22:37:21.182 [DEBUG] [ipt.internal.ScriptEngineManagerImpl] - Added ScriptEngine for language 'application/python' with identifier: 5a09406a-41d0-43d0-b2e2-4f0ec9a86684
2021-03-09 22:37:22.009 [INFO ] [jsr223.jython.mylib.log             ] - Hello world!
2021-03-09 22:37:22.013 [DEBUG] [e.automation.internal.RuleEngineImpl] - The rule 'firstPythonRule' is executed.

However with the following code I do not see anything in the normal openhab logs nor do I see an additional log file appearing in the log directory nor anywhere else.

from core.log import getLogger,log_traceback, logging, LOG_PREFIX
scriptExtension.importPreset("RuleSimple")
scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleFactories")


log1 = getLogger("mylib.log") # will log to '{LOG_PREFIX}.mylib.log'
log2 = logging.getLogger("Test")
log1.info("Hello world!")
log2.info("Hello world2!")

How do I achieve the logs to appear?

TIA
Stefan

When you call getLogger I think you need to pass the LOG_PREFIX as part of the logger name. In other words I do not think your comment is correct.

log1 = getLogger(LOG_PREFIX+".myLib.log")

Honestly I just copied the whole line as is (including the comment). the log2-approach was taken from one of your code snippets.

but shouldn’t

log1 = getLogger(“mylib.log”)
log1.info(“test”)

just appear in the normal openhab log then? Or where would I have to expect it?

The log4j2.xml file works by configuring the settings for a logger based on its name. The name of a logger is usually the package name and class name. When using openHAB’s logger Actions (e.g. logInfo from Rules DSL) it uses org.openhab.model.script.Rules.<logger name> as the logger name and so you will find an entry in log4j.xml to configure a logger.

When one configures a logger in log4j2.xml it applies to everything at that log name and below. So you’ll see a line

<Logger level="INFO" name="org.openhab"/>

That sets every logger that starts with “org.openhab” and below to the INFO level.

So far so good.

Step 3 in the installation instructions for the Helper Libraries has you run a command in the Karaf console that adds a line to log4j2.xml for you to set a DEBUG logging level for all the loggers that start with jsr223.

So now that I’ve worked through this one of two things are going on.

  1. The comment is correct and the call the getLogger will prepend “jsr223” to the logger name for you. The problem is you failed to run step 3 when you installed the Helper Libraries so there is no configuration for loggers that start with “jsr223”.

  2. My original statement is correct and your logger name is “mylib.log” without “org.openhab” or “jsr223” so, again, there is no logger configured.

So look in /var/lib/openhab/etc/log4j2.xml to see if you have a logger config that mentions jsr223. If so you need to add the LOG_PREFIX to the logger name yourself. If not you need do step 3 on the installation instructions (or manually add the following line):

<Logger level="INFO" name="jsr223"/>

Alternately, you can use the openHAB logger Actions instead.

from core.actions import LogAction

LogAction.logInfo("mylib.log", "Hello World!")

That will put the logger name at org.openhab.model.script.mylib.log I think.

A final note, this does not make it log to a separate file called mylib.log. To achieve that you need to modify log4j2.xml to add a new file appender and a new logger to use that file appender.

2 Likes

Just to clarify, I added a function in core.log called getLogger that adds the prefix automatically.

You would still need to have the logger configured to see the logs, as Rich states.

Thx Rih and Michael for the comprehensive explanation:

I checked (hopefully) everything you mentioned, so I don’t think that this step failed.

Step 3 of instructions:

  • openhab-console shows with log:list
    jsr223 │ DEBUG :white_check_mark:
  • log4j2.xml in /var/lib/openhab/etc has the following entry
    :white_check_mark:

Now I am using

LogAction.logInfo(“mylib.log”, “Hello World!”)

which indeed results into

[INFO ] [.openhab.core.model.script.mylib.log] - Hello World!

So LogAction :white_check_mark: works well

Like mentioned below by Michael I added then

LogAction.logInfo("mylib.log", "Hello World!")

log1 = getLogger("mylib.log") # will log to '{LOG_PREFIX}.mylib.log'
log1.info("Hello JSR!")

which works as well.

For the sake of completeness (I don’t think it really is an issue and not related to the code but rather strange) it then appears as well but with very small letters and very much indented in frontail, so it works but I am not sure why with a different font

It seems that “something” is adding a style to the log output

However it is is not reflected in the normal log (see below), so there is “something” in the log that makes it appear differently in frontail :man_shrugging: but nothing necessarily to be investigated (at least here)

2021-03-10 08:15:35.798 [DEBUG] [e.automation.internal.RuleEngineImpl] - The trigger '1' of rule 'firstPythonRule' is triggered.
2021-03-10 08:15:35.811 [INFO ] [.openhab.core.model.script.mylib.log] - Hello World!
2021-03-10 08:15:35.817 [INFO ] [jsr223.jython.mylib.log             ] - Hello JSR!
2021-03-10 08:15:35.818 [DEBUG] [e.automation.internal.RuleEngineImpl] - The rule 'firstPythonRule' is executed.

Update: For reference of the logging functionality see Logging — openHAB Helper Libraries documentation

So all good with logging now. On to the next adventures in JSR-land.

openHABian sets up some configuration for how the logs are presented in Frontail. I bet that that configuration is looking for org.openhab type log entries and does not recognize the jsr223.jython loggers so uses the default configuration.

Beyond that I’ve nothing to offer. I don’t use Frontail and have never seen how it’s configured.

I finally was able to dig deeper into Python rule implementation. As this might be interesting for other as well I document here my results I achieved so far

My intention was to

  • create a UI which allows me to set a String item rollershutter1_time to a time like “12:13”
  • create a CRON Trigger rule name with a tagName “cronRule1” via the UI which controls a specific rollershutter group to go down. So everything is done via the UI
  • I create a jython rule again via UI that triggers on the rollershutter1_time update and then takes the time value to manipulate the above cron trigger-time-setting to reschedule that trigger

And here is my first result which actually does what I want in that it changes the time of the cron-trigger
BUT even though if you look at the rule and verify that the time has changed it seems that openhab does not notice it and does a reschedule of the cron rule.

So I think there needs to be a way to tell openhab that the Rule actually was updated.

If I changed the timer rule manually in the UI I can see

2021-03-14 19:30:56.891 [DEBUG] [dule.handler.TimeOfDayTriggerHandler] - cancelled job for trigger 'cron1'.
2021-03-14 19:30:56.899 [DEBUG] [dule.handler.TimeOfDayTriggerHandler] - Scheduled job for trigger 'cron1' at '23:12' each day.

So it seems I need to cancel the job of the trigger and reschedule. Even by looking at the core code I haven’t quite understood how I could mimic that behavior.

Here is my code so far

from core.actions import LogAction
from core.log import getLogger

scriptExtension.importPreset("RuleSimple")
scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleFactories")
LogAction.logInfo("SimpleRule", "Starting Rule")

currentState = itemRegistry.getItem("rollershutter1_time").state
log = getLogger("Rollershutter-Time-Rule")
log.info("Starting RS-Rule. Time to set = {} {}".format(currentState, type(currentState.toString())))


rules = ruleRegistry.getByTag("cronRule1")

for rule in rules:
    log.info("Rule name=[{}], description=[{}], tags=[{}], UID=[{}]".format(rule.name, rule.description, rule.tags, rule.UID))
    triggers = rule.getTriggers()
    for trigger in triggers:
      log.info("Trigger id=[{}], label=[{}], description=[{}]".format(trigger.id, trigger.label, trigger.description))
      if (trigger.id=='cron1'):
        config = trigger.getConfiguration()
        properties = config.getProperties()
        for entry in properties.entrySet():
          log.info("Config key=[{}], value=[{}]".format(entry.key, entry.value))

        config.put("time",currentState.toString())

        # just to check if it changed the value of the trigger
        properties = config.getProperties()
        for entry in properties.entrySet():
          log.info("Config key=[{}], value=[{}]".format(entry.key, entry.value))

        # need to cancel and reschedule trigger job but how?

I did all those steps and see:

21:20:28.503 [INFO ] [hab.core.service.AbstractWatchService] - Loading script '/etc/openhab/automation/jsr223/python/personal/hello_world.py'
21:20:31.841 [ERROR] [ript.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/etc/openhab/automation/jsr223/python/personal/hello_world.py': ImportError: No module named joda in <script> at line number 6

any clue?