HABApp migration from JSR223-Jython

I am in the middle of an evaluation for the migration of my Jython rules to the HABApp.

In my rules, I exclusively use the following sample setup:

triggers = []
triggers.append(ChannelEventTrigger("deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "1002", "SELECT").trigger)
triggers.append(ChannelEventTrigger("deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "2002", "OFF").trigger)
triggers.append(ChannelEventTrigger("deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "3002", "PREV").trigger)
triggers.append(ChannelEventTrigger("deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "4002", "NEXT").trigger)



@rule("Setting Scenes Rule)", description="This Rule controls the scene selection and execution")
class sceneSwitchRule(object):
    def __init__(self):
        self.triggers = triggers

    def execute(self, module, inputs):
        act = str(inputs["module"]).split("_")[0]

        if act == "SELECT":
            self.selectScene()
        elif act == "OFF":
            self.cancelScene()
        elif act == "PREV":
            self.prevScene()
        elif act == "NEXT":
            self.nextScene()

    def selectScene(self):
        pass

    def cancelScene(self):
        pass

    def prevScene(self):
        pass

    def nextScene(self):
        pass

I do not have a clue how I could map this to HABApp way of handling triggers of the form

        self.listen_event('deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent', self.on_channel, EventFilter(ChannelTriggeredEvent,  event="1002"))

as the listen_event does not accept a parameter like “event name” like “SELECT”, “OFF”, “ON”, “PREV”, NEXT", which I could evaluate in the callback function.

not sure if i understand correct, but if i want to trigger a special event i use

        self.listen_event('openwebnet:bus_cen_scenario_control:bticino:Hoftor_Cen:button#16',
        self.gate_close_activator_chn, EventFilter(ChannelTriggeredEvent, event='START_PRESS'))

of, if i do not add the event=... i handle it in the callback:

        OpenhabItem.get_item('iGarage_Ro_Kt').listen_event(self.garage_door_cnt,
        ItemStateChangedEventFilter())
...

    def garage_door_cnt(self, event):
        value = event.value
        if value == 'ON':
            pass
        elif value == 'OFF':
            pass

I’ make a simplified example:

In my living room, I have on the wall sets of switches that should switch lights on or dim etc.
The case is also that the same light might be switched by different switches positioned across the living room.

switches channel triggers for lamp 1:

deconz:switch:00212E00C488:04cd15fffe6f9d60011111:buttonevent triggered 1002
deconz:switch:00212E00C488:04cd15fffe6f9d60022222:buttonevent triggered 1002
deconz:switch:00212E00C488:04cd15fffe6f9d60033333:buttonevent triggered 1002

switches channel triggers for lamp 2:

deconz:switch:00212E00C488:04cd15fffe6f9d60044444:buttonevent triggered 1002
deconz:switch:00212E00C488:04cd15fffe6f9d60055555:buttonevent triggered 1002
deconz:switch:00212E00C488:04cd15fffe6f9d60066666:buttonevent triggered 1002

Now a sample HABApp sample rule:

class MyChannelRule(Rule):
    def __init__(self):
        super().__init__()
        self.listen_event('deconz:switch:00212E00C488:04cd15fffe6f9d60011111:buttonevent', self.on_channel, EventFilter(ChannelTriggeredEvent,  event="1002"))
        self.listen_event('deconz:switch:00212E00C488:04cd15fffe6f9d60022222:buttonevent', self.on_channel, EventFilter(ChannelTriggeredEvent,  event="1002"))
        self.listen_event('deconz:switch:00212E00C488:04cd15fffe6f9d60033333:buttonevent', self.on_channel, EventFilter(ChannelTriggeredEvent,  event="1002"))
        self.listen_event('deconz:switch:00212E00C488:04cd15fffe6f9d6044444:buttonevent', self.on_channel, EventFilter(ChannelTriggeredEvent,  event="1002"))
        self.listen_event('deconz:switch:00212E00C488:04cd15fffe6f9d60055555:buttonevent', self.on_channel, EventFilter(ChannelTriggeredEvent,  event="1002"))
        self.listen_event('deconz:switch:00212E00C488:04cd15fffe6f9d60066666:buttonevent', self.on_channel, EventFilter(ChannelTriggeredEvent,  event="1002"))

    def on_channel(self, event: ChannelTriggeredEvent):
        if  (event.name == "deconz:switch:00212E00C488:04cd15fffe6f9d60011111:buttonevent" or 
            event.name == "deconz:switch:00212E00C488:04cd15fffe6f9d60022222:buttonevent" or 
            event.name == "deconz:switch:00212E00C488:04cd15fffe6f9d60033333:buttonevent"):

            log.info("Toggle Light 1 on/off") 

        if  (event.name == "deconz:switch:00212E00C488:04cd15fffe6f9d6044444:buttonevent" or 
            event.name == "deconz:switch:00212E00C488:04cd15fffe6f9d60022222:buttonevent" or 
            event.name == "deconz:switch:00212E00C488:04cd15fffe6f9d60033333:buttonevent"):

            log.info("Toggle Light 2 on/off") 

MyChannelRule()

and how I did it in Jython:

triggers = []
triggers.append(ChannelEventTrigger("deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "1002", "TOG1").trigger)
triggers.append(ChannelEventTrigger("deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "1002", "TOG1").trigger)
triggers.append(ChannelEventTrigger("deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "1002", "TOG1").trigger)
triggers.append(ChannelEventTrigger("deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "1002", "TOG2").trigger)
triggers.append(ChannelEventTrigger("deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "1002", "TOG2").trigger)
triggers.append(ChannelEventTrigger("deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "1002", "TOG2").trigger)



@rule("Setting Scenes Rule)", description="This Rule controls the scene selection and execution")
class sceneSwitchRule(object):
    def __init__(self):
        self.triggers = triggers

    def execute(self, module, inputs):
        act = str(inputs["module"]).split("_")[0]

        if act == "TOG1":
            log.info("Toggle Light 1 on/off") 
        elif act == "TOG2":
            log.info("Toggle Light 2 on/off") 

the actual project is described here, which is based on a JSON string out of which all the trigger definition are created:

I have created as dirty test implementation a wrapper class around the Rule which mimics the behavior:

from HABApp import Rule
from HABApp.openhab.events.channel_events import ChannelTriggeredEvent
from HABApp.core.events.filter.event import EventFilter
import logging

class RuleJSR(Rule):

    def __init__(self):
        self.triggers = []
        self.trigger_dict = {}
        self.callback = None
        
        super().__init__()

    def set_triggers(self, callback, triggers):
        self.triggers = triggers
        self.callback = callback

        # Loop through the `triggers` array and populate the dictionary and add listeners
        for trigger_event, channel, value, action in triggers:
            key = (channel, value)
            self.trigger_dict[key] = action
            
            self.listen_event(channel, self.trigger_fnc, EventFilter(trigger_event,  event=value))

    def trigger_fnc(self, event: ChannelTriggeredEvent):
       self.callback((event.channel, event.event, self.trigger_dict[(event.channel, event.event)]))

This sample then works as expected:

import personal.migration_helper
import importlib
importlib.reload(personal.migration_helper)  

from personal.migration_helper import RuleJSR

from HABApp.openhab.events.channel_events import ChannelTriggeredEvent  
import logging

log = logging.getLogger('MyRule') 

triggers = []
triggers.append((ChannelTriggeredEvent, "deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "1002", "SELECT"))
triggers.append((ChannelTriggeredEvent, "deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "2002", "OFF"))
triggers.append((ChannelTriggeredEvent, "deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "3002", "PREV"))
triggers.append((ChannelTriggeredEvent, "deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent", "4002", "NEXT"))   


class MyChannelRule(RuleJSR):
    def __init__(self):
        super().__init__()
        self.set_triggers(self.execute, triggers)

    def execute(self, event):
        log.info('Channel Event: {}, {} ==> {}'.format(event[0], event[1], event[2]))

MyChannelRule()
[2023-07-28 22:33:42,900] [                   MyRule]     INFO | Channel Event: deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent, 1002 ==> SELECT
[2023-07-28 22:33:45,896] [                   MyRule]     INFO | Channel Event: deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent, 2002 ==> OFF
[2023-07-28 22:33:47,054] [                   MyRule]     INFO | Channel Event: deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent, 3002 ==> PREV
[2023-07-28 22:33:48,537] [                   MyRule]     INFO | Channel Event: deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent, 4002 ==> NEXT

Your implementation makes no sense!

First of all don’t use importlib.reload since it’ll create problems and very hard to track down issues.
It’s a super dirty hack that sometimes works but sometimes doesn’t.

Also there is no need to use it because you can just pass the values as an argument into your rule.
Inheritance is the wrong tool here.

Your examples of what you are trying to achieve are very inconsistent:
Sometimes you want to trigger on different channels, sometimes it’s the same channel with a different value and it’s all mixed.

Your last example is just a super complicated way to write

class MyChannelRule(RuleJSR):
    def __init__(self):
        super().__init__()
        channel = 'deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent'

        self.listen_event(channel, self.func_select, EventFilter(ChannelTriggeredEvent,  event='1002'))
        self.listen_event(channel, self.func_off, EventFilter(ChannelTriggeredEvent,  event='2002'))
        self.listen_event(channel, self.func_prev, EventFilter(ChannelTriggeredEvent,  event='3002'))
        self.listen_event(channel, self.func_next, EventFilter(ChannelTriggeredEvent,  event='4002'))

    def func_select(self, event):
        self.execute(event, 'SELECT')

    def func_off(self, event):
        self.execute(event, 'OFF')

    def func_prev(self, event):
        self.execute(event, 'PREV')

    def func_next(self, event):
        self.execute(event, 'NEXT')

    def execute(self, event, mode: str):
        log.info('Channel Event: {}, {} ==> {}'.format(event[0], event[1], event[2]))

MyChannelRule()

Thank you for your feedback!
The work you’ve done for HABApp is truly admirable!

During the development of a local library, this is the only (as I know) way to get the changes of the library get updated.

The hint of inheritance I do not get, related to importlib.reload?

a) What I want to achieve is to define at one place in the code what sort of triggers (channels-triggers, item state, or update changes) are connected to what desired actions.

b) When the rule triggers I would like to know what action needs to be done, regardless of which trigger ( (channels-triggers, item state, or update changes) initiated it.

How would you write the sample, but which would be scalable to 100+ trigger definitions?
And also open the possibility to define these 100+ triggers either by a generic JSON, or YAML definition.
I have seen only one possibility: one generic trigger-receiving function and then branching out to actions.

As I said - this might sometimes work but is not the proper way.
Put your whole definition in the rule file and work there.
Once you are done you can move the definition to another file.

You are using inheritance for something that is basically a shared function.
It might work but there are easier ways to achieve the same.


value_to_text = {
  '1002': 'SELECT',
  ...
}

class MyChannelRule(Rule):
    def __init__(self, channel: str):
        super().__init__()
        self.listen_event(channel, self.my_func, EventFilter(ChannelTriggeredEvent))

    def my_func(self, event: ChannelTriggeredEvent)
        text = value_to_text.get(event.event)
        if text is None:
            return None
        print(event, text)

# this can also be done depending on a HABApp Parameter file
MyChannelRule('deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent')

You can make the class instantiation depending on the HABApp Parameters.
This works out of the box and you can automtically reload the rule file when the parameter file changes.
See the docs for an example.

I think we are talking past each other.

The key for a unique key to trigger is not:

But rather:

If you have some spare time have a look at the link above I posted about my light control.
There is one big JSON definition for the light system behavior (toggle, dim, timeout,day-night time, movement presence detection, sunset, …
There is one rule handling all these different triggers (100+ lights, 100+ pushbutton switches)
There is one callback function for all these triggers.
I already ported the system from DSL rules to Jython.

I seems like it. Inheritance is not the right way to do it non the less.


This should work as expected with one big definition

class MyChannelRule(Rule):
    def __init__(self, channel: str, value_to_str: dict[str, str]):
        super().__init__()
        self.value_to_str = value_to_str
        self.listen_event(channel, self.my_func, EventFilter(ChannelTriggeredEvent))

    def my_func(self, event: ChannelTriggeredEvent):
        text = self.value_to_str.get(event.event)
        if text is None:
            return None
        print(event, text)

my_def = {
    'deconz:switch:00212E00C488:04cd15fffe6f9d60011000:buttonevent': {
        '1002': 'SELECT'
        ...
    }
}

for channel, value_to_str in my_def.items():
    # this can also be done depending on a HABApp Parameter file
    MyChannelRule(channel, value_to_str)