AppDaemon: callback to other module forgets about "self"

n00b here. Trying to get to grips with AppDaemon. Either my Google-fu is lacking or I’m doing something stupid noone’s ever tried before. I’m guessing the former :wink:
The AppDaemon manual also doesn’t cover this use-case. So, I’m kinda stuck.

I have an AppDaemon app that consists of various modules to split functionality into logical groups. I can have the app start a callback to a function in one of the other modules, but then I loose access to the class’s “self” object.

Below is a minimal example of what I’m trying to do. I’m using a fast-firing sensor (CPU load) for testing.

qlisten.yaml

---
listen_cpuload:
  module: qlisten
  class: QListen
  listen_for: sensor.system_monitor_load_1_min
  attribute: state
  priority: 100

qlisten.py

import appdaemon.plugins.hass.hassapi as hass

import qextern_cb as xt_cb


class QListen(hass.Hass):
    
    def initialize(self):
        entity = self.args.get("listen_for")
        attribute = self.args.get("attribute")

        self.listen_state(self.state_changed, entity, attribute=attribute). # <- this works
        self.listen_state(xt_cb.state_changed, entity, attribute=attribute)  # <- this doesn't work
        self.log(f"\n\t*** Listening to entity: {entity}, attribute: {attribute}")

    def state_changed(self, *args, **kwargs):
        self.log(f"State changed in class ...")

qextern_cb.py

def state_changed(fles, *args, **kwargs):
    fles.log(f"State changed in external ...")

Here’s the log that is generated:

2025-06-01 16:55:16.169566 INFO AppDaemon: Starting apps: {‘listen_cpuload’}
2025-06-01 16:55:16.172620 INFO AppDaemon: Calling initialize() for listen_cpuload
2025-06-01 16:55:16.175391 INFO listen_cpuload:
*** Listening to entity: sensor.system_monitor_load_1_min, attribute: state
2025-06-01 16:55:22.176067 INFO listen_cpuload: State changed in class …
2025-06-01 16:55:22.179788 ERROR listen_cpuload: ===== state_changed for listen_cpuload ==================================
2025-06-01 16:55:22.180068 ERROR listen_cpuload: StateCallbackFail: State callback failed for ‘sensor.system_monitor_load_1_min’ from ‘listen_cpuload’
2025-06-01 16:55:22.180220 ERROR listen_cpuload: args: (‘sensor.system_monitor_load_1_min’, ‘state’, ‘0.52’, ‘0.48’)
2025-06-01 16:55:22.180348 ERROR listen_cpuload: kwargs: {
2025-06-01 16:55:22.180463 ERROR listen_cpuload: “__thread_id”: “thread-5”
2025-06-01 16:55:22.180574 ERROR listen_cpuload: }
2025-06-01 16:55:22.181085 ERROR listen_cpuload: AttributeError: ‘str’ object has no attribute ‘log’
2025-06-01 16:55:22.181514 ERROR listen_cpuload: File “/usr/lib/python3.12/site-packages/appdaemon/threads.py”, line 1075, in safe_callback
2025-06-01 16:55:22.181633 ERROR listen_cpuload: funcref()
2025-06-01 16:55:22.181728 ERROR listen_cpuload: File “/config/apps/quicklisten/qextern_cb.py”, line 3, in state_changed
2025-06-01 16:55:22.181821 ERROR listen_cpuload: fles.log(f"State changed in external …")
2025-06-01 16:55:22.181907 ERROR listen_cpuload: ^^^^^^^^
2025-06-01 16:55:22.181993 ERROR listen_cpuload: ===========================================================================

Why is fles suddenly a str? I was expecting it to be the class’s self object. And how do I pass access to self to the other module?

Any insights and help appreciated.

In python, the first parameter is only a reference to the object if the method is defined in a class.

The method in qextern_cb.py doesn’t seem to be in a class, so self would have no meaning.

I have never tried using a method outside a class as a callback, so I don’t know if it can work. I would suggest creating a class in qextern_cb.py derived from Hass and writing your callback in that. Be aware though, that this will be a different object, so self will be a reference to the object defined in qextern_cb.py , not qlisten.py

You’d have to make a base class and inherit from that if you want to have shared class methods between objects. Otherwise, just keep duplicating code or make them “helper” methods that you import and call (that have no reference to self).

e.g.

class MyBaseClass(hass.Hass):
    def state_changed(self, *args, **kwargs):
        self.log(f"State changed in class ...")
class QListen(MyBaseClass):
    
    def initialize(self):
        entity = self.args.get("listen_for")
        attribute = self.args.get("attribute")

        self.listen_state(self.state_changed, entity, attribute=attribute). # <- this works
        self.listen_state(xt_cb.state_changed, entity, attribute=attribute)  # <- this doesn't work
        self.log(f"\n\t*** Listening to entity: {entity}, attribute: {attribute}")

I am now working on a different approach: making all separate scripts an app with each their own class and using the get_app() method to set-up a communication channel between the apps. That way they can exchange commands/data.

It feels a bit elaborate though.
@petro’s suggestion is interesting too. I’m going to try that later. But, access to a single “self” from all modules is kinda essential to me.

That’s not how self works. Self references “itself”. each automation is a different self. Hence why you have to create a base class that shares all the functionality you want to extend to all your “selfs”. So even when all the selfs are different, they share some of the same functionality.

I suggest you read up on self. This is a core concept in programming and not understanding object orientation will be a major thorn in your side.

You might want to look at using multiple inheritance to define your appdaemon app.

It will allow you to define different classes that will reference the same object. The technique is rather frowned on these days as its hard to avoid conflicts, but provided the inheritence tree isn’t complicated, it works ok.

What I’m developing now is a number of apps each with its own class. Since I can’t share a central object between the apps easily, I’m using a manager app that all other apps talk to to provide their state info. The manager then takes decisions on the combined states and instructs other apps to take approrpiate actions.