HABApp - Easy automation with openHAB

Sure! I started with a very simple rule:

...
class ZigbeeRule(HABApp.Rule):
    def __init__(self, base_name: str):
        super().__init__()

        soc = NumberItem.get_item(base_name + "_SoC")
        available = SwitchItem.get_item(base_name + "_Available")

        soc.listen_event(self.handle_soc, ItemStateChangedEventFilter())
        available.listen_event(self.handle_available, ItemStateChangedEventFilter())

        self.gotify = StringItem.get_item("Gotify")

    def handle_soc(self, event: ItemStateChangedEvent):
        if event.value < SOC_THRESH and event.old_value >= SOC_THRESH:
            self.gotify.oh_send_command(
                f"{event.name} bald leer! (SoC = {event.value}%)"
            )

    def handle_available(self, event: ItemStateChangedEvent):
        if not event.value:
            self.gotify.oh_send_command(f"{event.name} nicht verfĂŒgbar!")
...

I found the concept of mock items in the docs but I am missing some kind of ‘test fixture’.
I could imagine something like the following:

# setup
gotify = StringItem("Gotify", "init")
soc = NumberItem("Test_SoC", 50)
available = SwitchItem("Test_Available", "ON")

Items.add_item(gotify)
Items.add_item(soc)
Items.add_item(available)

ZigbeeRule("Test")

# test
soc.set_value(4)

assert gotify.get_value() == "expected"

# tear down
Items.pop_item("Gotify")
Items.pop_item("Test_SoC")
Items.pop_item("Test_Available")

Currently we have a mixture of rule definition and rule creation.
E.g. in one file there is

class MyRule(Rule):
   ...

MyRule()

This makes it impossible to import the rule class (rule definition) without creating an instance.
But for testing the rule should only be created in the test case so it’s independent of other test cases which might have been running before.
This is something that has to be solved before it’s possible to write clean and easy to use tests.
I can think of two three ways:

  • Separate rule definition and rule creation in different files. However this creates much overhead and I think it’s not work the additional effort for the HABApp users. E.g.
class MyRule(Rule):
   ...
from file_a import MyRule
MyRule()
  • Write some kind of pre-processor that removes the rule creation statements and dynamically imports the rule module (difficult)
  • Patch the environment so the automatically created rules will automatically get unloaded again before the tests run (difficult, because rule creation can depend on existing items)

I think it would be nice to have an easy way to test the rules but while it sounds easy at first when done properly an implementation is quite hard.
With a static code checker you can already catch many issues now so I didn’t think that an easy way to test was that important.

@nobbi123 I know you already do some testing, how have you solved these issues?

1 Like

Sorry, my time is very limited, these days :frowning:
I will try to post an example on weekend.

Short: I have separate files for rule creation and definition.

If I create a module, lets say mymodule.py with the following code:

def greeting(name):
  return ("Hello, " + name)

And then I use the module in a HABApp rules file by using the import statement:

import mymodule

mymodule.greeting("Adam")

I can now use that mode from several HABApp rules file. However if I’d like to allow that module to use the openhab object I could probably use a class like this in my module file:

import HABApp
class MyClass(HABApp.Rule):
    def __init__(self):
        super().__init__()
    def greeting(name):
        self.log.debug("Hello, " + name)
        self.openhab.post_update('MySwitch', OFF)
        return ("Hello, " + name)

I’m not sure but it feels wrong to create a HABApp.Rule instance for this because it’s not really a HABApp.Rule. Would there be any other way to access the openhab object that I get access to by using the HABApp.Rule? Small code example would be really helpful. Thanks in advance!

In modules I use

import HABApp.openhab.interface as interface
...
in code:
    interface.post_update( item_name, value )
    interface.send_command ( item_name, cmd )

To be warned, at least today it is not officially documented.

If you are worried about it, you could pass self.openhab as parameter to greeting().

1 Like

Be aware that you should put the file mymodule.py in the lib folder.
Have you done that?

It’s not officially documented because it’s not officially supported. :wink:
There are so many things that can actually go wrong and so many things that you have to ensure.
It’s easy to build something that works sometimes but also breaks sometimes and error search can be very very hard.

There is almost always a more elegant option to achieve the same, e.g.

  • using the HABApp event bus to pass data between rule instances where one rule instance acts as e.g. a worker
  • self.get_rule()

@RRoe What are you actually trying to do? Maybe there is an easier way 
 .

Thanks @Tokamak and @Spaceman_Spiff :smiley:, your kind help is surely appreciated!

Yeah, that’s correct I put the module in lib of course. In my example above, I just tried to keep the question uncomplicated. What I’m actually trying to do is to create a module for my heat pump and another module for Nord Pool market hourly energy spotprices. The logic and the properties of those have become so large that they don’t belong in a HABApp rule any longer. I need to move those things from regular HABApp rules, but the thing is, the new classes still need to do things that need access to the openhab object, e.g. turn on a switch or use a logger. There might be more needs, I haven’t found them yet but I think you can get my point.

As @Tokamak mentioned, I could pass the self.openhab object or even the whole self object from within a HABApp rule when initiating a new object from the new class but it just feels wrong.

However, I might be able to rework the code so that instead of:

def greeting(caller_object):
    return f'Good{caller_object.openhab.get_item(custom_item_names["clock_time_of_day_item"]).value.lower()}'

I could probably use:

def greeting():
    return f'God{StringItem.get_item(custom_item_names["clock_time_of_day_item"]).value.lower()}'

I just wondered if there was an easy way to be able to access the various openhab related items and functions that the HABApp.Rule object has.

I would create a subfolder heat_pump and a subfolder nord_pool in the rules directory and put multiple rule files there.
You can then create one rule that pulls the hourly energy spot prices and posts them to a HABApp internal item. Then next rule will listen to a update/change on that item and do the next calculation. You can even use dataclasses and pass these around (see docs on how to properly type hint them).
So the idea is use the HABApp internal item registry and event bus to build some kind of pipeline which allows you to have small parts of independent logic instead of a huge monolithic rule.
Additionally this makes debugging much easier.
E.g.:
I have a HABApp internal item is_frosty which will turn True if in the last two hours the temperature was below 1°C or in the last 24h the Temperature was < -2°C.
The other rules then build on the state of the internal item instead of e.g. a function call.

If you have static logic you can always put it in the lib folder but as a rule of thumb if it interacts with openHAB or HABApp items it belongs in a rule.
But you can always return e.g. a dict which contains items and the values which then shall be posted by the rule.
And if the logic is independent from HABApp testing the logic e.g. with pytest is much easier.
You’ll have some inputs and expect some output depending on the input.

I am aware of it :slight_smile:

My approach is totally different, and I am pretty sure you never intended to do that.

Each of my scripts is one rule, a singleton created from a own class derived from HABApp.Rule. That means, only one HABApp rule is instantiated, and the code executed in the script / rule is called in the __init__ of the derived class’ object, after calling super().__init__

Except for HABApp.Rule.on_rule_removed, which I use to clean up the objects I have created in the script, I don’t use any HABApp.Rule’s objects or functions.

That means, your HABApp.Rule.__init__ and HABApp.Rule.on_rule_reomved is the frame of all the code exectued in my scripts.

My own OHRule class, defined in a lib, uses the event_listener_group. With this I can activate or deactivate (cancel) listeners according to my needs. In each of my scripts I create several OHRule class objects, so in the script several event_listener_groups are created.

Because I don’t want to use and pass self.oh or self.openhab to all of my classes / objects in order to have access to it, I use interface.post_update and interface.send_command in my classes, as described.

But this is always called within the one framing HABApp.Rule, so it should work.

But you can

  • cancel and add the event listeners dynamically
  • use the EventListenerGroup to make adding/canceling really easy

from a normal HABApp Rule.

So I fail to see the benefit and tbh. from my point of view it doesn’t make any sense at all and seems to be an over complicated way of doing things.

But hey - if it works for you and you’re happy with it why not!
As long as you’re aware that it’s both unsupported and unintended I’m fine with it.

With OH 2.5 I implemented all my rules using Jython. Unfortunately it was a dead horse I was riding.

There I created some central classes like the Jython version of OHRule and a comfortable timer class. The latter supports tasks comparable to cron, but much more, by using python threads.
Timers are independent from OHRule, that means, they are not only created in a context of a rule.

During an unload of a Jython script I was able to clean up all thinks, especially canceling the timer threads.

With OH3 I migrated from Jython to your HABApp. In order to save most of the Jyhon code I adapted my former OHRule to your framework.

What I did not find was a possibility to clean up the timers created in the script. But I found on_rule_removed(), which is called during unload. So I decided to use a special, outer rule as replacement for the Jython script.

Somewhere I read that it not possibile to create a HABApp.Rule within a HABApp.Rule. This was the reason to use the EventListenerGroup in my OHRule class.

To wrap it up you “hacked” something together to not loose your legacy code.

In HABApp we have the rule scheduler with self.run.XXX, is there actually no need for a custom (user-) timer implementation.
I can only recommend you try the plain HABApp for new rules so you can gradually switch over (which you could have done with your Jython rules, too) otherwise you’ll still be riding a half dead horse :smiley: :wink:

Yes - you should not do it because they will live forever and pollute your system and lead to hard to track down behavior. One of the next HABApp version will probably throw an error if you try it.

But the EventListenerGroup has nothing to with a HABApp Rule. :confused:
One is the rule implementation and the other one is a tool to enable/disable one or multiple event bus listeners comfortably.

Trust me, my timer class is a swiss army knife for all of my needs, much more than you are offering.
In addition, from my Jython history, I do not want to depend on a proprietary interface like your self.run.

For this reason I wrapped all of your interfaces, also the OpenhabItem.

This is why I am using it.

If there is a supported function which is called by your framework during unload of a script, I would not use any HABApp.Rule

You’re depending on my whole proprietary engine and wrappers so the argument is kind of void :wink:

To wrap it up:
It’s good that you found something that’s working for you but be careful when recommending unsupported solutions to other users who are not as tech savvy.

@RRoe
What do you think about splitting parts of the rule monolith in smaller sub rules?
Do you think that’s something that will work well for you?

After a very good conversation with @nobbi123 where he explained his testing implementation I created a demo repo to show the idea.
I extended the functionality with a time-aware rule runner so that testing of scheduled tasks is also possible.

I think such a testing functionality would be helpful for many users. @Spaceman_Spiff what do you think? I would gladly prepare a pull-request.

Yes - I think testing is a very good point and definitely something that should be possible and needs to be improved. I am also always looking for contributors so getting a PR would be very nice.

However I am unsure about the about the separation with files because it creates overhead and especially moving rules to the lib folder because they will not be dynamically reloaded. This would require a restart of HABApp to pick up a modified rule and that’s not something that we should aiming for.

But I’ll gladly have a discussion so why don’t you open a PR as is and we’ll discuss there?

1 Like

Hi All,
I am migrating from Windows to Openhabian, and installed HAPApp via the Openhabian user interface.
Version is 1.0.8. OS is Debian 5.10.162-1 (2023-01-21) i686

There is an error occurring at startup (two instances of same error):
Error loading logging config: Unable to configure handler ‘EventFile’
Error loading logging config: Unable to configure handler ‘EventFile’

Google didn’t show anything, but I did find a similar incident in this topic thread from around Nov 20, where the advice was to delete the logging configuration file and allow it to be recreated. That has not solved the issue for me. Any clues on what to consider next?

Is this the only error from a fresh HABApp install? Can you run the “fix permissions” from the openhabian-config menu and restart HABApp?

Hi Sebastian,
I ran fix permissions, and did a reboot. Full transcript below. The config handler error has changed and the first error after the config handler items is new.

                                        1.0.8
Error loading logging config: Unable to configure handler 'HABApp_default'
Error loading logging config: Unable to configure handler 'HABApp_default'
/opt/habapp/lib/python3.9/site-packages/ruamel/yaml/constructor.py:1405: ResourceWarning:unclosed file <_io.FileIO name='/var/log/openhab/HABApp_events.log' mode='ab' closefd=True>
6 validation errors for ApplicationConfig
mqtt -> connection -> tls
  value is not a valid dict (type=type_error.dict)
mqtt -> connection -> tls_ca_cert
  extra fields not permitted (type=value_error.extra)
mqtt -> connection -> tls_insecure
  extra fields not permitted (type=value_error.extra)
mqtt -> subscribe -> topics
  'int' object is not iterable (type=type_error)
openhab -> connection -> host
  extra fields not permitted (type=value_error.extra)
openhab -> connection -> port
  extra fields not permitted (type=value_error.extra)
Executing <Task finished name='Task-1' coro=<Runtime.start() done, defined at /opt/habapp/lib/python3.9/site-packages/HABApp/runtime/runtime.py:29> result=None created at /opt/habapp/lib/python3.9/site-packages/HABApp/__main__.py:38> took 0.506 seconds

Could you remove the config and let HABApp create a new one?
Then add your config entries. It seems you’re trying to load an old config scheme.