HABAPP - What happens with running Threads when rule reloaded

I have a question:

What happens with running threads e.g. time threads when a rule gets reloaded?
In Jython, there was a function which gets called: scriptUnloaded.

Is there something similar or should the class destructor be used?

You should not schedule threads at all and there is hardly ever the need for it.
If it’s a long running stand alone script you can run it as a subprocess, everything else can run directly as a function.

Maybe when starting from scratch, though I am porting from Jython and have to bring over a lot of stuff I have written before like usage of Threading.Timers.

Therefore it’s vital for me to know what happens with threads created in the rule file scope.

Just a hint, otherwise I have to find it out myself the hard way

Maybe you should rework instead of fighting the Framework all the time.
With asyncio and threads is so easy to break things and get flaky behavior.

If you do things like that I can not and will not support you.

I thought the whole HABApp’s selling proposal is that there fewer restrictions as imposed by JSR223/Jython. Why should it be that asyncio and threads are a fight against the framework?

I asked a simple question if the answer is complex then ok :+1:, otherwise please do not evangelize your view of the topic.

Don’t get me wrong HABApp is great, but I have to be prepared to maintain my own fork if things go south as they did with Jython and its main and only maintainer 5iver.

There are two methods of the class Rule which can be overwritten similar as scriptUnloaded() in Jython Rules:

    def on_rule_loaded(self):
        """Override this to implement logic that will be called when the rule and the file has been successfully loaded
        """

    def on_rule_removed(self):
        """Override this to implement logic that will be called when the rule has been unloaded.
        """

In on_rule_removed I am able to cancel all running timers.

No - the whole selling point is it’s an easy way to create home automation rules in python 3.
And to achieve that it’s necessary to work in a certain way so everything plays together nicely.

Since you want to do all the things “your way” and not the “HABApp way” I can only encourage you to fork and work on your own rule engine. Clearly HABApp is not the right tool for you since you already have very determined ideas of how things should work.
Who knows - maybe you’ll come up with something better. :person_shrugging:

2 Likes

@lukics Maybe you can show us why you need threading.Timer. As @Spaceman_Spiff already stated, I’m sure we will find a solution without threading :slight_smile:

@nobbi123

My aim is to “lift-and-shift” my existing JSR223 code, utilizing HABApp as a minimally invasive layer rather than depending on any specific framework. This is rooted in a pragmatic concern: for instance, should the maintainer of HABApp be suddenly unavailable (like hypothetically being hit by a lottery bus), I don’t want to be left in a lurch.

A notable benefit of HABApp is that it interfaces effectively with a well-defined openhab API and operates as native Python.

Only yesterday, I set up my debugging and development environment on Visual Studio Code, running both HABApp and JSR223 concurrently. I have discovered that debugging HABApp rules is a significant advancement over the JSR223 code.

The solutions that I’ve programmed are quite intricate. For instance, my blinds control is configured by a JSON definition:

RS_ATTRIBUTES='''      
{
    "StudyRoomRoSh": 
        
        [
            {
                "ORIN" : 180,
                "FUSU" : 53,
                "FUSD" : 53,
                "AUTO" : 0,
                "WDCO" : "LivingRoomRoWO"
            }   
            ,
            {
                "FUP" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "6001", ""]
                ],
                "FDN" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "5001", ""]
                ],
                "45DEG" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "3002", ""],
                    ["CET", "homematic:HM-PB-6-WM55:3014F711A0001F58A992FB22:NEQ1001224:3#BUTTON", "SHORT_PRESSED", ""]
                ],
                "90DEG" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "4002", ""],
                    ["CET", "homematic:HM-PB-6-WM55:3014F711A0001F58A992FB22:NEQ1001224:4#BUTTON", "SHORT_PRESSED", ""]
                ],
                "45DEGFD" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "3001", ""],
                    ["CET", "homematic:HM-PB-6-WM55:3014F711A0001F58A992FB22:NEQ1001224:3#BUTTON", "LONG_PRESSED", ""]
                ],
                "90DEGFD" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "4001", ""],
                    ["CET", "homematic:HM-PB-6-WM55:3014F711A0001F58A992FB22:NEQ1001224:4#BUTTON", "LONG_PRESSED", ""]
                ],
               "SOP" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "6002", ""]
                ],
                "SCL" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "5002", ""]
                ],
                "UPO" : [
                    ["ISCT", "StudyRoomRoSh", "", ""]
                ],
                "WDC" : [
                    ["ISCT", "LivingRoomRoWO", "", ""]
                ]

            }
        ],
    "GuetsRoomRoSh": 
        
        [
            {
                "ORIN" : 180,
                "FUSU" : 60,
                "FUSD" : 60,
                "AUTO" : 0,
                "WDCO" : "LivingRoomRoWO"
            }   
            ,
            {
                "45DEG" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "3002", ""],
                    ["CET", "homematic:HM-PB-6-WM55:3014F711A0001F58A992FB22:NEQ1001224:3#BUTTON", "SHORT_PRESSED", ""]
                ],
                "90DEG" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "4002", ""],
                    ["CET", "homematic:HM-PB-6-WM55:3014F711A0001F58A992FB22:NEQ1001224:4#BUTTON", "SHORT_PRESSED", ""]
                ],
                "45DEGFD" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "3001", ""],
                    ["CET", "homematic:HM-PB-6-WM55:3014F711A0001F58A992FB22:NEQ1001224:3#BUTTON", "LONG_PRESSED", ""]
                ],
                "90DEGFD" : [
                    ["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "4001", ""],
                    ["CET", "homematic:HM-PB-6-WM55:3014F711A0001F58A992FB22:NEQ1001224:4#BUTTON", "LONG_PRESSED", ""]
                ],
                "UPO" : [
                    ["ISCT", "GuetsRoomRoSh", "", ""]
                ],
                "WDC" : [
                    ["ISCT", "LivingRoomRoWO", "", ""]
                ]

            }
        ],
    "KitchenRoShL":  
        
        [
            {
                "ORIN" : 180,
                "FUSU" : 18,
                "FUSD" : 18,
                "AUTO" : 0,
                "WDCO" : "LivingRoomRoWO"
            }   
            ,
            {
                "UPO" : [
                    ["ISCT", "KitchenRoShL", "", ""]
                ],
                "WDC" : [
                    ["ISCT", "LivingRoomRoWO", "", ""]
                ]

            }
        ],
    "LivingRoomRoShL": 
        
        [
            {
                "ORIN" : 180,
                "FUSU" : 65,
                "FUSD" : 60,
                "AUTO" : 0,
                "WDCO" : "LivingRoomRoWO"
            }   
            ,
            {
                "UPO" : [
                    ["ISCT", "LivingRoomRoShL", "", ""]
                ],
                "WDC" : [
                    ["ISCT", "LivingRoomRoWO", "", ""]
                ]

            }
        ],
    "LivingRoomRoShR": 
        
        [
            {
                "ORIN" : 180,
                "FUSU" : 65,
                "FUSD" : 60,
                "AUTO" : 0,
                "WDCO" : "LivingRoomRoWO"
            }   
            ,
            {
                "UPO" : [
                    ["ISCT", "LivingRoomRoShR", "", ""]
                ],
                "WDC" : [
                    ["ISCT", "LivingRoomRoWO", "", ""]
                ]

            }
        ],
    "BedRoomRoSh":  
        
        [
            {
                "ORIN" : 180,
                "FUSU" : 56,
                "FUSD" : 56,
                "AUTO" : 0,
                "WDCO" : "LivingRoomRoWO" 
            }   
            ,
            {
                "UPO" : [
                    ["ISCT", "BedRoomRoSh", "", ""]
                ],
                "WDC" : [
                    ["ISCT", "LivingRoomRoWO", "", ""]
                ]

            }
        ]
}
'''  
           
wbMgr = wbItemManager(RS_ATTRIBUTES)       

class wbManagerRule(RuleJSR):   
    def __init__(self):
        super().__init__() 

        self.set_triggers(self.execute, wbMgr.getAllTriggers())

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

    
wbManagerRule()  

in the JSON definition I have per blind:

parameters of blind

  • geographical orientation
  • full strokeup time
  • full stroke down time
  • automatic operation flag
  • window/door contact to prevent automatic operation

operation of blind with corresponding item triggers

  • FUP - full up
  • FUD - Full down
  • …
  • 90DEDFD - Go full down and set flaps to 90 degree
  • …

From this definition, the wbItemManager extracts the trigger definition, sends it back, and anticipates being invoked through the execute method. I’ve created a wrapper class around the Rule, which is designed to manage this trigger definition effectively.

Since the entire functionality is embedded in library modules, I’ve incorporated elements such as the timer within these modules:

import threading, sys, uuid
from threading import Timer
from datetime import datetime
from personal.log_stack import getLogger

log = getLogger("timers")


class SingleTimer(object):
    __lock = threading.Lock()

    def __init__(self, name=None, to=None, fnc=None):
        self.__name = name if name is not None else str(uuid.uuid1())
        self.__to = self.__calcTo(to)
        self.__fnc = fnc if fnc is not None else lambda: self.__emptyJob()
        self.__running = False
        if name is None and to is None and fnc is None:
            return
        try:
            with SingleTimer.__lock:
                log.debug("*** TIMER PAR TEST : {}, {}, {}".format(self.__name, self.__to, self.__fnc))
                for thread in threading.enumerate():
                    if isinstance(thread, Timer) and str(thread.name) == self.__name:
                        log.debug("*** STOPPING THREAD : {}".format(thread.name))
                        thread.cancel()

                log.debug("SingleTimer {} started with : {}s".format(self.__name, self.__to))
                self.__running = True
                tmr = Timer(self.__to, lambda: self.__job())
                tmr.setName(self.__name)
                tmr.start()
        except Exception as e:
            log.error("SingleTimer __init___() Exception: {}".format(e))

    def __calcTo(self, complexTo, recursiveDates=False):
        NoneType = type(None)
        log.debug("***** {} : type(complexTo) --> {} ******".format(self.__name, type(complexTo)))
        if type(complexTo) == int or type(complexTo) == float:
            log.debug("***** {} : type(complexTo) == int or float ******".format(self.__name))
            return complexTo
        elif type(complexTo) == str or type(complexTo) == str:
            log.debug("***** {} : type(complexTo) == str or str ******".format(self.__name))
            now = datetime.now()
            datetime_obj = datetime.strptime("{}-{:0>2}-{:0>2} ".format(now.year, now.month, now.day) + complexTo,
                                             "%Y-%m-%d %H:%M:%S")
            time_difference = datetime_obj - now
            return time_difference.total_seconds()
        else:
            return 0

    def __emptyJob(self):
        log.info("SingleTimer {} empty lambda function job: {} [time|s]".format(self.__name, self.__to))

    def __job(self):
        try:
            log.debug("SingleTimer {} executing job: {} [time|s]".format(self.__name, self.__to))
            self.__fnc()
            self.__running = False
        except Exception as e:
            log.error("SingleTimer __job() Exception: {}".format(e))

    def isRunning(self):
        return self.__running

    def cancel(self):
        try:
            with SingleTimer.__lock:
                for thread in threading.enumerate():
                    if isinstance(thread, Timer) and str(thread.name) == str(self.__name):
                        thread.cancel()
            self.__running = False
        except Exception as e:
            log.error(" SingleTimer cancel() Exception: {}".format(e))

    def __del__(self):
        log.debug(" SingleTimer destructor: {}".format(self.__name))


class RecursiveTimer(object):
    def __init__(self, name=None, to=None, fnc=None, cycles_to_run=None):
        self.__name = name if name is not None else str(uuid.uuid1())
        self.__to = to if to is not None else 0
        self.__fnc = fnc if fnc is not None else lambda: self.__emptyJob()
        self.__cycles_left = sys.maxsize if cycles_to_run is None else cycles_to_run
        self.__single_tmr = SingleTimer()
        self.__cycles = 0
        self.__first_run = True
        if name is None and to is None and fnc is None:
            return
        self.__job()

    def cancel(self):
        self.__single_tmr.cancel()

    def cyclesToRun(self, cycles_to_run):
        self.__cycles_left = cycles_to_run

    def cyclesLeft(self):
        return self.__cycles_left

    def cyclesDone(self):
        return self.__cycles

    def __emptyJob(self):
        log.warning("RecursiveTimer {} empty lambda function job: {} [time|s]".format(self.__name, self.__to))
        self.__cycles_left = 0

    def __job(self):
        try:
            if not self.__first_run:
                self.__cycles += 1
                self.__cycles_left -= 1
                self.__fnc()

            if self.__cycles_left > 0:
                self.__first_run = False
                self.__single_tmr = SingleTimer(self.__name, self.__to, lambda: self.__job())
        except Exception as e:
            log.error(" RecursiveTimer __job() Exception: {}".format(e))

I have module tests defined for most modules so I can do regression tests.

Yesterday I ported the blind control including the timer module. The test is still outstanding.

The help I need are rather basic questions like:

“Why this code does not send stuff to OH!”

OpenhabItem("MyItem").oh_send_command("ON")

And get the answer:

“Stupid, use instead!”

OpenhabItem.get_item("MyItem").oh_send_command("ON")

That is my point

As I’ve previously stated, HABApp is excellent! The hard work and effort behind it is truly admirable. Great job indeed!

There’s no need to fork anything.

My goal is simply to use HABApp in the most streamlined manner, without leaning towards any specific paradigm it might advocate. I don’t subscribe to the idea of a singular right solution - I believe in a multitude of individual solutions.

To be transparent, if the JSR223 Python was still being maintained and Jython hinted at a forthcoming version, I wouldn’t have transitioned to HABApp, a one-man show (based on GitHub insights) with minimal community support. That is the fact of the matter.