Time is <item> trigger condition in Jython Rules

Hi,

I honestly haven’t tested it, but I think I have a solution to the following problem:

Under Time-based Triggers you can find the trigger condition:

Time is <item> [timeOnly]

timeOnly means:

When using an item and you want to ignore the date-portion of that item the timeOnly option can be used.

Specifically, you use a DateTime item and if this DateTime or only the Time (timeOnly) is reached, this rule is triggered!

So as example you have following two options as rule:

rule "Morning Greeting"
when
    Time is Morning_Time
then
    // Aktionen, die am Morgen ausgeführt werden sollen
    logInfo("Morning Greeting", "Guten Morgen!")
end

or:

rule "Morning Greeting"
when
    Time is Morning_Time timeOnly
then
    // Aktionen, die am Morgen ausgeführt werden sollen
    logInfo("Morning Greeting", "Guten Morgen!")
end

In Jython you don’t have this trigger condition.

You have:

# Cron
@when("Time cron 55 55 5 * * ?")

# Items and Groups
@when("Item ITEM_NAME received update [NEW_STATE]")
@when("Item GROUP_NAME received update [NEW_STATE]")
@when("Member of GROUP_NAME received update [NEW_STATE]")
@when("Descendent of GROUP_NAME received update [NEW_STATE]")

@when("Item ITEM_NAME changed [from OLD_STATE] [to NEW_STATE]")
@when("Item GROUP_NAME changed [from OLD_STATE] [to NEW_STATE]")
@when("Member of GROUP_NAME changed [from OLD_STATE] [to NEW_STATE]")
@when("Descendent of GROUP_NAME changed [from OLD_STATE] [to NEW_STATE]")

@when("Item ITEM_NAME received command [COMMAND]")
@when("Item GROUP_NAME received command [COMMAND]")
@when("Member of GROUP_NAME received command [COMMAND]")
@when("Descendent of GROUP_NAME received command [COMMAND]")

# Thing Event
@when("Thing THING:NAME received update [NEW_STATE]")
@when("Thing THING:NAME changed [from OLD_STATE] [to NEW_STATE]")

# Channel Event
@when("Channel CHANNEL:NAME triggered [EVENT]")

# System based
@when("System started")

I am trying a workaround and my approach looks like this:

import core
from core.rules import rule
from core.triggers import when
from core.actions import LogAction
from datetime import datetime

def datetime_item_to_cron_expression(item_name, timeOnly=False):
    """
    Convert a DateTime Item to a cron expression.

    Args:
        item_name (str): The name of the DateTime Item.
        timeOnly (bool): When using an item and you want to ignore the date-portion of that item the timeOnly option can be used.

    Returns:
        str: The generated cron expression.
    """
    # Annahme: Hier wird der Zustand des DateTime-Items abgerufen
    item_state = ir.getItem(str(item_name)).state

    # Annahme: Hier wird der Zustand des DateTime-Items in ein datetime-Objekt konvertiert
    date_time = datetime.strptime(item_state, "%Y-%m-%dT%H:%M:%S.%f%z")

    # Extrahiere Jahr, Monat, Tag, Stunde und Minute aus dem datetime-Objekt
    if timeOnly:
        year = 0
        month = 0
        day = 0
    else:
        year = date_time.year
        month = date_time.month
        day = date_time.day
    hour = date_time.hour
    minute = date_time.minute

    # Erstelle die Cron-Expression im Format: "Minute Stunde Tag Monat ? Jahr"
    cron_expression = "{} {} {} {} ? {}".format(minute, hour, day, month, year)

    return cron_expression

@rule("Time is <item> time test rule")
@when("Time cron {}".format(datetime_item_to_cron_expression("testDateTimeItem")))  # Beispiel für 8 Uhr morgens
def time_is_datetime_item(event):
    # Aktionen, die am Morgen ausgeführt werden sollen
    LogAction.logInfo("Time is <item> test rule", "Rule successfully triggered!")

What other problem is there?
Right, I create the trigger for the rule when loading the rule. So if the state of my DateTime item changes, the trigger would not be changed. This makes the dependency on a DateTime item for a trigger in the DSL rules very efficient.

What solution could there be?
I delete the old rule as soon as the state of the DateTime item changes and create a new rule. My approach would be to create this rule using a function and delete it accordingly. Because nothing changes in the actual rule. So I only have to time when I create or recreate this rule.

The rule looks like this:

import core
#from core import osgi
from core.rules import rule
from core.triggers import when
from core.actions import LogAction
from datetime import datetime
from core.jsr223.scope import scriptExtension
import time

ruleRegistry = scriptExtension.get("ruleRegistry")
RULE_NAME = "Time is <item> time test rule"
ITEM = "testDateTime"

def datetime_item_to_cron_expression(item_name, timeOnly=False):
    """
    Convert a DateTime Item to a cron expression.

    Args:
        item_name (str): The name of the DateTime Item.
        timeOnly (bool): When using an item and you want to ignore the date-portion of that item the timeOnly option can be used.

    Returns:
        str: The generated cron expression.
    """
    # Annahme: Hier wird der Zustand des DateTime-Items abgerufen
    item_state = str(ir.getItem(str(item_name)).state)

    # Annahme: Hier wird der Zustand des DateTime-Items in ein datetime-Objekt konvertiert
    # Annahme: Hier wird der Zustand des DateTime-Items in ein datetime-Objekt konvertiert
    date_time = datetime.strptime(item_state[:-5], "%Y-%m-%dT%H:%M:%S.%f")

    # Extrahiere Jahr, Monat, Tag, Stunde und Minute aus dem datetime-Objekt
    if timeOnly:
        year = "*"
        month = "*"
        day = "*"
    else:
        year = date_time.year
        month = date_time.month
        day = date_time.day
    
    day_of_week = "?"
    hour = date_time.hour
    minute = date_time.minute
    second = date_time.second

    # Erstelle die Cron-Expression im Format: "Sekunden Minuten Stunden Tag Monat ? Jahr"
    cron_expression = "{} {} {} {} {} {} {}".format(second, minute, hour, day, month, day_of_week, year)

    return cron_expression

@rule("Time is <time> creation at system start")
@when("System started")
def init(event):
    create_rule()

@rule("Change Time is <item> trigger condition")
@when("Item {} changed".format(ITEM))
def change_rule_trigger(event):
    delete_rule()
    time.sleep(2)
    create_rule()

def create_rule():
    @rule(RULE_NAME)
    @when("Time cron {}".format(datetime_item_to_cron_expression(ITEM)))
    def time_is_datetime_item(event):
        # Aktionen, die am Morgen ausgeführt werden sollen
        LogAction.logInfo("Time is <item> test rule", "Rule successfully triggered!")

def delete_rule():
    for objRule in [objRule for objRule in ruleRegistry.getAll() if objRule.name == RULE_NAME]:
        ruleRegistry.remove(objRule.UID)

As I said, it is still untested. I won’t get round to testing it until tomorrow or the day after.

What could be implemented in a similar way?
“Time is midnight” or “Time is noon” and the like could also be implemented using functions that are then simply converted to cron expressions.

What can be done better?

  • I can of course import my functions for Time is , Midnight, Noon, etc. So I can use it in different Python/Jython files.
  • Of course, I can also write functions for Create and Delete in a more generalised way and thus reuse them more skilfully.
  • I can of course check whether the state is empty.
  • Of course, I can also delete the rule for a date that has already expired if no timeOnly is set

Here the tested rule

import core
#from core import osgi
from core.rules import rule
from core.triggers import when
from core.actions import LogAction
from datetime import datetime
from core.jsr223.scope import scriptExtension
import time

ruleRegistry = scriptExtension.get("ruleRegistry")
RULE_NAME = "Time is <item> time test rule"
ITEM = "testDateTime"

def datetime_item_to_cron_expression(item_name, timeOnly=False):
    """
    Convert a DateTime Item to a cron expression.

    Args:
        item_name (str): The name of the DateTime Item.
        timeOnly (bool): When using an item and you want to ignore the date-portion of that item the timeOnly option can be used.

    Returns:
        str: The generated cron expression.
    """
    # Annahme: Hier wird der Zustand des DateTime-Items abgerufen
    item_state = str(ir.getItem(str(item_name)).state)

    # Annahme: Hier wird der Zustand des DateTime-Items in ein datetime-Objekt konvertiert
    # Annahme: Hier wird der Zustand des DateTime-Items in ein datetime-Objekt konvertiert
    date_time = datetime.strptime(item_state[:-5], "%Y-%m-%dT%H:%M:%S.%f")

    # Extrahiere Jahr, Monat, Tag, Stunde und Minute aus dem datetime-Objekt
    if timeOnly:
        year = "*"
        month = "*"
        day = "*"
    else:
        year = date_time.year
        month = date_time.month
        day = date_time.day
    
    day_of_week = "?"
    hour = date_time.hour
    minute = date_time.minute
    second = date_time.second

    # Erstelle die Cron-Expression im Format: "Sekunden Minuten Stunden Tag Monat ? Jahr"
    cron_expression = "{} {} {} {} {} {} {}".format(second, minute, hour, day, month, day_of_week, year)

    return cron_expression

def time_is_midnight():
    return "0 0 0 * * ? *"

def time_is_morning():
    return "0 0 8 * * ? *"

def time_is_noon():
    return "0 0 12 * * ? *"

def time_is_afternoon():
    return "0 0 15 * * ? *"

def time_is_evening():
    return "0 0 18 * * ? *"

def time_is_night():
    return "0 0 21 * * ? *"

@rule("Time is <time> creation at system start")
@when("System started")
def init(event):
    create_rule()

@rule("Change Time is <item> trigger condition")
@when("Item {} changed".format(ITEM))
def change_rule_trigger(event):
    delete_rule()
    time.sleep(2)
    create_rule()

def create_rule():
    @rule(RULE_NAME)
    @when("Time cron {}".format(datetime_item_to_cron_expression(ITEM)))
    def time_is_datetime_item(event):
        LogAction.logInfo("Time is <item> test rule", "Rule Time is <item> time successfully triggered!")

def delete_rule():
    for objRule in [objRule for objRule in ruleRegistry.getAll() if objRule.name == RULE_NAME]:
        ruleRegistry.remove(objRule.UID)

@rule("Rule Time is midnight")
@when("Time cron {}".format(time_is_midnight()))
def time_is_midnight_rule(event):
    LogAction.logInfo("Time is <item> test rule", "Rule Time is midnight successfully triggered!")

@rule("Rule Time is noon")
@when("Time cron {}".format(time_is_noon()))
def time_is_noon_rule(event):
    LogAction.logInfo("Time is <item> test rule", "Rule Time is noon successfully triggered!")

@rule("Rule Time is morning")
@when("Time cron {}".format(time_is_morning()))
def time_is_morning_rule(event):
    LogAction.logInfo("Time is <item> test rule", "Rule Time is morning successfully triggered!")

@rule("Rule Time is afternoon")
@when("Time cron {}".format(time_is_afternoon()))
def time_is_afternoon_rule(event):
    LogAction.logInfo("Time is <item> test rule", "Rule Time is afternoon successfully triggered!")

@rule("Rule Time is evening")
@when("Time cron {}".format(time_is_evening()))
def time_is_evening_rule(event):
    LogAction.logInfo("Time is <item> test rule", "Rule Time is evening successfully triggered!")

@rule("Rule Time is night")
@when("Time cron {}".format(time_is_night()))
def time_is_night_rule(event):
    LogAction.logInfo("Time is <item> test rule", "Rule Time is night successfully triggered!")

This works fine.

Perhaps this approach will help one or two people.

Kind regards
Michael

If you are going to go through all of this trouble, why not just implement the trigger? The existing helper library doesn’t but that doesn’t mean you can’t.

All you really need to do is figure out what to pass to the TriggerBuilder to create the Java classes. It probably will look something like:

configuration = {"item": item_name}
TriggerBuilder.create().withId(trigger_name).withTypeUID("timer.DateTimeTrigger").withConfiguration(Configuration(configuration)).build()

Look at the classes in triggers.py for examples on how to add this as a trigger. Then you get to use the full capabilities of the trigger instead of needing to create and recreate the rule and stuff like that.

Before there was this Time is Item trigger I used a rule that created Timers to run other rules.

Finally, the usual warning that Jython is deprecated, not maintained (hence the lack of the Time is Item trigger) and will break at some point. I do not recommend new development of rules in Jython and recommend to gradually start migrating to some other solution (HABApp is a good choice if you want to stay in Python, JS Scripting and jRuby are good choices to stay inside OH). It’s a lot easier to change now when it’s not broken than while under the gun after it breaks.