Callback "kwargs" usage

Hi guys,

I recently started using Home Assistant and quickly came across AppDaemon as a (very) powerful environment to program my automations.

And like many others I discovered in the docs (and experimenting) that the kwargs in the three types of callbacks is a positional argument and not a collection of named arguments in the form **kwargs.

Which led me to find a couple of discussions here about the topic and a fruitful one on GitHub in which even a potential backwards compatible switching mechanism could have been implemented with the release of 4.0, but this apparently never happened.

The referenced discussion: Callback signature validation fails for (*args, **kwargs) · Issue #462 · AppDaemon/appdaemon · GitHub

A fix for that issue at least alleviated the situation by implementing a lazy evaluation of the callback signature.

In any case and given I haven’t found anything like the two lines I have implemented to overcome the situation, I would like to share them here with the community.

A typical class in my AppDaemon apps looks like this:

import hassapi as hass

from datetime import time


class SampleImplementation(hass.Hass):
    # cb faker, to bypass the positional 'kwargs' argument which isn't a real
    # **kwargs
    # it's presence its guaranteed as the last positional argument, so one can
    # take the positional arguments [0:-1] and then [-1] (-1 will always exist),
    # knowing it's a dictionary and convert it to **kwargs
    def cb(self, realcb):
        return lambda *args: realcb(*args[0:-1], **args[-1])

With this two-liner (which could actually be a one-liner) one can now do something like this (heavily redacted for the sake of brevity and clarity)

    def initialize(self):
        shutter = 'cover.in_the_kitchen'
        # Define the period of activity
        tup, tdown, tnow = time(7, 15), time(20, 0), self.time()
        # Initialize the state
        self.shutter_updown(shutter, tup < self.time() < tdown)

        # Schedule callbacks
        self.run_daily(self.cb(self.shutter_updown), tup, shutter=shutter, state=True)
        self.run_daily(self.cb(self.shutter_updown), tdown, shutter=shutter, state=False)

    def shutter_updown(self, shutter, state, **kwargs):
        self.log(f'UpDown Shutter: {shutter} {state}')
        self.call_service(f'cover/{"open" * state or "close"}_cover', entity_id=shutter)

Et voilá, the magic is done. Notice that shutter and state in the callback can be used as both positional and named arguments. This thanks to Python and not to the two-liner, of course.

When initializing the state in initialize, used as positional arguments

        self.shutter_updown(shutter, time(7, 15) < self.time() < time(20, 0))

Via the callback, but first passing them as named arguments to run_daily

        self.run_daily(self.cb(self.shutter_updown), tup, shutter=shutter, state=True)

which will later be taken from the kwargs supplied to the callback faker and passed to the real callback as named arguments (which Python will find due to the given name, even if they pose as positional arguments)

The real callback needs, in any case, a final placeholder in the form of **kwargs. Becase AppDaemon is supplying some secret named arguments via kwargs like __threading_id.

With this in mind I believe that AppDaemon could offer a quick and dirty callback mechanism by having overloaded run_xxx methods which could wrap the actual callback in a callback faker mechanism (it must not be a two-liner) which passes the arguments down to the real callback with the usual Python notation.

The proposed mechanism in the GitHub discussion reference above was to set an instance member to True. Personally I would rather implement that as a configuration option for AppDaemon, which could be read by hass. It may also well be offered with different options to activate it.

Best regards