How to create own devices/items, (IKEA remote double click) beginner-coding question

As a learn-habapp/python-project I trying to do a generic item/class for a Ikea-remote.

At the moment input is a string (from openhab Item, in future I intend to take it from MQTT instead) string could be "on, off, brightness_move_up, brightness_stop… " etc. I would like to write a class that instead gives me event-update for changes and boolean outputs for .on_click/.on_dbl_click/.on_hold etc.

My start is something like this:

class IkeaRemoteE2002(Item):
    def __init__(self, link_item_name, hab_item_name):
        super().__init__(hab_item_name)

        self.remote_action_string = StringItem.get_item(link_item_name)
        self.remote_action_string.listen_event(self.action_string_update, ValueUpdateEventFilter())
        self._on_click = False
        self._on_dbl_click = False

    def action_string_update(self, event: ValueUpdateEvent):
        self._on_click = (event.value == "on")

    @property
    def act_on_click(self) -> bool:
        return self._on_click

However as double click is not supported by the ikea-device I would need to implement this is Python. Habapp gives some generic method for .watch_update and watch_change, however they are supposed to be used inside rules, where I would like to use them in my own class.

Function should be something like this
if input string = “on”:
previous string != ‘on’ or older than -400ms, and latest input older thatn 400ms:
set on_click = true and update item event

if last input string =“on” and previous string = “on” less than 400ms later
set on_dbl_click = true

I struggle somewhat over the structure of I should implement this (without writing a lot of code). Is there some inbuilt functions in HABApp suitable for this or should I maybe use the .last_update/change-properties?

Why do you want to use a property to access the double click and why do you want to create a new item?
Imho it’s more of an event so you can

  • create an HABApp internal item that holds True and post events to it
  • use a custom event on the HABApp event bus

When there is no need to debounce detecting the double click should be easiest through last_change

I also don’t think it’s necessary to create an item.

from HABApp.core.items import Item
from HABApp.openhab.items import StringItem
from HABApp.core.events import ValueUpdateEvent, ValueUpdateEventFilter
from datetime import datetime, timedelta

class IkeaRemoteE2002(HABApp.Rule):
    def __init__(self, hab_item_name: str, event_name: str):
        super().__init__()

        self.remote_action_string = StringItem.get_item(hab_item_name)
        self.remote_action_string.listen_event(self.action_string_update, ValueUpdateEventFilter())

        # Two HABApp internal items
        self.button_click = Item.get_create_item(f'{event_name:s}_Click')
        self.button_double_click = Item.get_create_item(f'{event_name:s}_Doubleclick')

        self.last_click = datetime.now()

    def action_string_update(self, event: ValueUpdateEvent):
        if event.value == "on":
            self.button_click.post_value(True)

            now = datetime.now()
            if now - self.last_click > timedelta(seconds=1):
                self.button_double_click.post_value(True)
            self.last_click = now
    

IkeaRemoteE2002('OpenhabStrItem', 'MyEvent')


class OnDoubleClickRule(HABApp.Rule):
    def __init__(self):
        super().__init__()

        # notice the get_item
        Item.get_item('MyEvent_Doubleclick').listen_event(self.on_dbl_click, ValueUpdateEventFilter())
        
    def on_dbl_click(self, event: ValueUpdateEvent):
        # on doubleclick

OnDoubleClickRule()

I just wrote it down so of course it’s untested.
I use the HABApp internal items to be able to listen to the events.
If you want to make the double click detection more robust you can e.g. keep the timestamp of the click events of the last 2 seconds in a list and do further processing.
What do you think of this approach?

Thanks for your fast help! Please, bare with me :slight_smile: Im trying to get my head around both Python and how to best set up the structure using HABApp, your answer helps alot in this. much appreciated!

I think your solution will work, yes I will still want a debounce detection so thats needs sorting but your code should be enough start for me to dig into those part myself (or I shout for help again).

Why I wanted class? I may be wrong here, but I was after a usage-functionality similar to this:

class MyRule(HABApp.Rule)
    def __init__(self):
        super().__init__()

        self.my_remote = IkeaRemoteE2002('link_to_openhab_item')
        self.my_remote.listen_event(self.button_pressed, ValueUpdateEventFilter)

def button_pressed(self, event: ValueUpdateEvent):
    if self.my_remote.button_double_click():
        # do something
    elif self.my_remote.button_single_click():
        # do somehting else

In this case I would not be dependent on the “internal” naming of item_event_names (which I assume I must know for your proposed solution. ie the ‘MyEvent_Doubleclick’), and an IDE would give me the optional attributes of my_remote (.button_double_click, .button_single_click, button_hold etc).

In short I wanted to create a generic ‘HABApp’-item with the added properties of the type of button-action, similar to for instance the SwitchItem which presents is_on()/is_off().

But the click or doubleclick is only valid for a short amount of time so it’s not a good fit for a property. If you don’t want to trigger on the double click from another rule file you can just do:

from HABApp import Rule
from HABApp.openhab.items import StringItem
from HABApp.core.events import ValueUpdateEvent, ValueUpdateEventFilter
from datetime import datetime, timedelta


class IkeaRemoteE2002(Rule):
    def __init__(self, hab_item_name: str):
        super().__init__()

        self.remote_action_string = StringItem.get_item(hab_item_name)
        self.remote_action_string.listen_event(self.action_string_update, ValueUpdateEventFilter())

        self.last_click = datetime.now()

    def action_string_update(self, event: ValueUpdateEvent):
        if event.value == "on":
            self.on_single_click()  # <-- Just call it directly

            now = datetime.now()
            if now - self.last_click > timedelta(seconds=1):
                self.on_double_click()    # <-- Just call it directly
            self.last_click = now

    def on_single_click(self):
        pass

    def on_double_click(self):
        pass


IkeaRemoteE2002('OpenhabStrItem')

Note this rule will trigger on_single_click and on_double_click if you hit the button two times.
If you want to trigger just on one of them you have to do it with self.remote_action_string.watch_change

Valid point, though quite easy to overcome as well I think. If class-implementation it should be implemented in more dynamic and elegant way compared to what I proposed.

As before, your input is much appreciated and a help to figure out and understand how to best use HABApp. I will play around and learn hopefully!

Yes - I can only encourage you to try things out and find out what works well and what doesn’t.
And HABApp is ever evolving and I’m always happy to find contributors, especially for the docs :wink:

1 Like

Ok, I think I reached something that I can use and of course you were correct in using class Rule instead of class Item! Im sure improvements can be made, but Im leaving code here anyway.

A note that I seem to violate one of you given advices when I assign the Rule to a class member (see usage-part). Is there a solution around this? I could not get it to work properly using self.get_rule(filename.py)
Rule — HABApp beta documentation

import logging
from HABApp.openhab.items import StringItem
from HABApp.core.items import Item
from HABApp import Rule
from HABApp.core.events import ValueUpdateEvent, ValueUpdateEventFilter, OrFilterGroup
from datetime import datetime, timedelta

log = logging.getLogger('TestDebug')


class ButtonClick:
    # Checks for nbr of consecutive clicks
    def __init__(self, delay_ms):
        self.last_click = datetime.now()
        self.nbr_consecutive_clicks = 0
        self.dbl_click_max_delay_ms = delay_ms
        self.id_last= ''

    def click(self, id_click):
        """ checks/returns nbr_consecutive_clicks within timeout """
        if id_click != self.id_last:
            self.nbr_consecutive_clicks = 1
        elif datetime.now() - self.last_click > timedelta(milliseconds=self.dbl_click_max_delay_ms):
            self.nbr_consecutive_clicks = 1
        else:
            self.nbr_consecutive_clicks += 1
        self.last_click = datetime.now()
        self.id_last = id_click
        return self.nbr_consecutive_clicks


class IkeaRemoteE2002(Rule):
    __str_click_up = 'on'
    __str_click_down = 'off'
    __str_click_left = 'arrow_left_click'
    __str_click_right = 'arrow_right_click'

    __str_hold_up = 'brightness_move_up'
    __str_hold_down = 'brightness_move_down'
    __str_hold_left = 'arrow_left_hold'
    __str_hold_right = 'arrow_right_hold'
    __str_hold_release = 'brightness_stop'

    def __init__(self, link_item_name, enable_dbl_click=True, click_delay_ms=500):
        super().__init__()
        if not enable_dbl_click:
            click_delay_ms = 1

        self.remote_action_string = StringItem.get_item(link_item_name)

        self.click_event_filter = OrFilterGroup(
            ValueUpdateEventFilter(value=self.__str_click_up),
            ValueUpdateEventFilter(value=self.__str_click_down),
            ValueUpdateEventFilter(value=self.__str_click_left),
            ValueUpdateEventFilter(value=self.__str_click_right)
        )
        self.remote_action_string.listen_event(callback=self.button_click_update, event_filter=self.click_event_filter)

        self.hold_event_filter = OrFilterGroup(
            ValueUpdateEventFilter(value=self.__str_hold_up),
            ValueUpdateEventFilter(value=self.__str_hold_down),
            ValueUpdateEventFilter(value=self.__str_hold_left),
            ValueUpdateEventFilter(value=self.__str_hold_right),
            ValueUpdateEventFilter(value=self.__str_hold_release)
        )
        self.remote_action_string.listen_event(callback=self.button_hold_update, event_filter=self.hold_event_filter)
        self.countdown_click_timeout = self.run.countdown(expire_time=timedelta(milliseconds=click_delay_ms),
                                                          callback=self.button_click_timeout)

        self.__button = ButtonClick(delay_ms=click_delay_ms)

        self.up_single_click = Item.get_create_item('up_single_click')
        self.down_single_click = Item.get_create_item('down_single_click')
        self.left_single_click = Item.get_create_item('left_single_click')
        self.right_single_click = Item.get_create_item('right_single_click')

        self.up_hold = Item.get_create_item('up_hold')
        self.down_hold = Item.get_create_item('down_hold')
        self.left_hold = Item.get_create_item('left_hold')
        self.right_hold = Item.get_create_item('right_hold')
        self.hold_release = Item.get_create_item('hold_release')

        self.up_double_click = Item.get_create_item('up_double_click')
        self.down_double_click = Item.get_create_item('down_double_click')
        self.left_double_click = Item.get_create_item('left_double_click')
        self.right_double_click = Item.get_create_item('right_double_click')

        self.up_multi_click = Item.get_create_item('up_multi_click')
        self.down_multi_click = Item.get_create_item('down_multi_click')
        self.left_multi_click = Item.get_create_item('left_multi_click')
        self.right_multi_click = Item.get_create_item('right_multi_click')

    def button_click_update(self, event: ValueUpdateEvent):
        self.countdown_click_timeout.stop()
        self.__button.click(id_click=event.value)
        if self.__button.nbr_consecutive_clicks == 1:
            self.countdown_click_timeout.reset()
        elif self.__button.nbr_consecutive_clicks == 2:
            match event.value:
                case self.__str_click_up:
                    self.up_double_click.post_value(True)
                case self.__str_click_down:
                    self.down_double_click.post_value(True)
                case self.__str_click_left:
                    self.left_double_click.post_value(True)
                case self.__str_click_right:
                    self.right_double_click.post_value(True)
        elif self.__button.nbr_consecutive_clicks > 2:
            match event.value:
                case self.__str_click_up:
                    self.up_multi_click.post_value(True)
                case self.__str_click_down:
                    self.down_multi_click.post_value(True)
                case self.__str_click_left:
                    self.left_multi_click.post_value(True)
                case self.__str_click_right:
                    self.right_multi_click.post_value(True)

    def button_hold_update(self, event: ValueUpdateEvent):
        if event.value == self.__str_hold_release:
            self.hold_release.post_value(new_value=True)
            self.up_hold.post_value_if(equal=True, new_value=False)
            self.down_hold.post_value_if(equal=True, new_value=False)
            self.left_hold.post_value_if(equal=True, new_value=False)
            self.right_hold.post_value_if(equal=True, new_value=False)
        else:
            self.hold_release.post_value_if(equal=True, new_value=False)
            match self.remote_action_string.value:
                case self.__str_hold_up:
                    self.up_hold.post_value(True)
                case self.__str_hold_down:
                    self.down_hold.post_value(True)
                case self.__str_hold_left:
                    self.left_hold.post_value(True)
                case self.__str_hold_right:
                    self.right_hold.post_value(True)

    def button_click_timeout(self):
        match self.remote_action_string.value:
            case self.__str_click_up:
                self.up_single_click.post_value(True)
            case self.__str_click_down:
                self.down_single_click.post_value(True)
            case self.__str_click_left:
                self.left_single_click.post_value(True)
            case self.__str_click_right:
                self.right_single_click.post_value(True)

Example of usage:

import logging

from HABApp import Rule
from HABApp.core.events import ValueUpdateEvent, ValueUpdateEventFilter
import typing
# if typing.TYPE_CHECKING:            # This is only here to allow
from rules.devices.ikea_remote_E2002 import IkeaRemoteE2002      # type hints for the IDE

log = logging.getLogger('TestDebug')


class TestRemote(Rule):
    def __init__(self):
        super().__init__()
        # I see it can be dangerous to assign class in this way (ie, if the IkeaRemoteE2002 is changed in any way, this Rule is lost?, instead
        # self.get_rule('IkeaRemoteE2002') is suggested?
        # is it possible to combine .get_rule-method while still keeping the variable-instantiation and its item-instances

        # Not encouraged to assign a Rule to a class member see:
        # https://habapp.readthedocs.io/en/latest/rule.html#how-to-properly-use-rules-from-other-rule-files
        self.my_remote = IkeaRemoteE2002(link_item_name='action_IKEA_ON_OFF_kontor', click_delay_ms=600)

        # Assign/create 'click-events'(listen_event) to the item-members (up_single_click, down_double_click etc) of the remote
        self.my_remote.up_single_click.listen_event(self.on_up_single_click, ValueUpdateEventFilter())
        self.my_remote.up_double_click.listen_event(self.on_up_dbl_click, ValueUpdateEventFilter())
        self.my_remote.up_hold.listen_event(self.on_up_hold, ValueUpdateEventFilter(value=True))
        self.my_remote.up_multi_click.listen_event(self.on_multi_click, ValueUpdateEventFilter())

        self.my_remote.down_single_click.listen_event(self.on_down_single_click, ValueUpdateEventFilter())
        self.my_remote.down_double_click.listen_event(self.on_down_dbl_click, ValueUpdateEventFilter())
        self.my_remote.down_hold.listen_event(self.on_down_hold, ValueUpdateEventFilter(value=True))
        self.my_remote.down_multi_click.listen_event(self.on_down_multi_click, ValueUpdateEventFilter())
        self.my_remote.hold_release.listen_event(self.on_hold_release, ValueUpdateEventFilter())

    def on_up_single_click(self, event: ValueUpdateEvent):
        print(f'{event}')

    def on_multi_click(self, event: ValueUpdateEvent):
        print(f'{event}')

    def on_up_dbl_click(self, event: ValueUpdateEvent):
        print(f'{event}')

    def on_up_hold(self, event: ValueUpdateEvent):
        print(f'{event}')

    def on_down_single_click(self, event: ValueUpdateEvent):
        print(f'{event}')

    def on_down_dbl_click(self, event: ValueUpdateEvent):
        print(f'{event}')

    def on_down_multi_click(self, event: ValueUpdateEvent):
        print(f'{event}')

    def on_down_hold(self, event: ValueUpdateEvent):
        print(f'{event}')

    def on_hold_release(self, event: ValueUpdateEvent):
        print(f'{event}')


TestRemote()

What you are sowing doesn’t make any sense.
Why doesn’t your Testrule Inherit from IkeaRemoteE2002?
Why do you use intermediate HABApp items if you don’t trigger on them outside of the defining rule?
If you want to use class functions you can do it like I showed you above.

It’s either

  • Calling class functions that are empty in the base class and contain logic in the child classes
  • Triggering on events

but never both

Also this will only work as long as you have one IkeaRemoteE2002, as the second rule overwrites/uses the same HABApp items as the first rule.

Obviously the rule has to exist, otherwise this will fail.

One of the drawbacks of openHab as I see it is the item/thing-naming requirement. You have to define all items in text also when in some cases this is not needed. Main reason to define/instantiate name should be for persistence and for connection to GUI.

In this case Im only interested the click-events from the remote. I think this will be a give a more simple and clear solution for me. Ie, in which ever Rule where I want to use the remote-input, instantiate the remote inside the rule and create events for the actions Im interested in (instead of trying to remember the specific name for each and every defined remote item).

True, and actually I would like the possibility not to name them at all or have the system give them a random name. A unique hash.tag of some kind can/should be added to the item-names in my remote-solution.

But the intermediate items provide no benefit here - you don’t need them at all!
You should call the function directly as shown in the example.
You then create one :bangbang: Rule which inherits from IkeaRemoteE2002 for every remote you have and put your logic there.
If you need to propagate an button press of the remote across multiple files you then create an HABApp item in the derived class and use it accordingly.

Feeling a bit stupid I admit even If i much appreciate your help. I will try to implement in the inherit way as well.

One example of how I intend to use the functionality:
In our laundry room we have a drier. Currently the drier is controlled by some different rh-reference rel. to the current el. price (ie try to run the drier harder when el. price is cheap). In some occasions it would is preferable to manually enable the drier for say one hour. Lets use the lights button (or a free choice of remote) and assign this functionality to a double-click. Ie. the rules are not built around the remote.

Let me ask in a another way. Is my suggested remote-code-style bad because of bad coding style/habit or because HABApp was not designed for such approach and you see a danger of low performance/system limitations or maybe both :)??

My intention is not to make you feel stupid and I apologize if I made you feel that way.
It’s rather my lack of understanding of what you are trying to do.

But at some point you have to decide which remote does what and imho a good place where you do this would be a rule.

Pseudocode:

class RemoteNr1(IkeaRemoteE2002):
    def __init__(self):
        super().__init__('RemoteNr1')
        self.dryer = SwitchItem.get_item('Dryer')

    def on_double_click(self):
        self.dryer.on()

RemoteNr1()

Performance should be fine - don’t worry about it.
But:

  • Every rule that wants to trigger on a double click has the complete logic for click detection running
  • You assigned the rule to a class member. You’ll get very hard to track down errors when rules get unloaded/reloaded. It’s in the docs for a reason :wink:
  • usage seems unnecessary complex and thus it makes the code hard to maintain
1 Like

Ok, think I got the hang of inheritance now. I was not fully aware of how the IDE treated override methods, I thought I had to look up their names, but PyCharm does that for me. This method will work for me independent of if the remote is the central piece of my rule or brought in just as an extra interface to the rule.

In your psuedecode above you set the class name to the rule in the super().init-method (assigned to self.rule_name i assume?)

class MyRemote(IkeaRemoteE2002): # IkeaRemoteE2002 is inherited from class HABApp.Rule
    def __init__(self):
        super().__init__('MyRemote') # 

Whats the importance of assigning the name to the Rule? I have tested without and this seems to work, but maybe I miss something?

class IkeaRemoteE2002(HABApp.Rule):
    def __init__(self, link_item_name, enable_dbl_click=True, click_delay_ms=500):
        super().__init__()

class MyRemote(IkeaRemoteE2002):
    def on_up_single_click(self):
        print(f'Up single click')

MyRemote(link_item_name='action_IKEA_ON_OFF_kontor', enable_dbl_click=False, click_delay_ms=500)

So again, big thank you for your quick support and help, I feel I take steps in the right direction understanding both HABApp and Python! Next issue for me is to get the mqtt-connection to work, I have some issues but I will ask you in separate thread

No, it’s meant to be the openhab item that gets the status updates from the device.

If you don’t assign a name HABApp will make a suggestion and use that.