SOLVED: Appdaemon and mqtt plugin problem

Hi all
I have a problem with AppDaemon and its MQTT plugin. I installed “Home Assistant Supervised” and installed AppDaemon from the App-Store within that. After that I changed appdaemon.yaml to this:

---
secrets: /config/secrets.yaml
appdaemon:
 latitude: 52.379189
 longitude: 4.899431
 elevation: 2
 time_zone: Europe/Amsterdam
 plugins:
   MQTT:
     type: mqtt
     namespace: mqtt
     client_host: '172.21.42.8'
     verbose: true
   HASS:
     type: hass
http:
 url: http://127.0.0.1:5050
admin:
api:
hadashboard:

and I made a small test program:

import mqttapi as mqtt

class NokLight(mqtt.Mqtt):

  def initialize(self):
#     self.mqtt_subscribe("home/binary_sensor/nielsole/state")
     self.listen_event(self.mqtt_message_received_event, "MQTT_MESSAGE", topic="home/#" )
     if self.is_client_connected():
         self.log('MQTT is connected')
     self.log('NOK Light started')

  def mqtt_message_received_event(self,eventname,data,kwargs):
     self.log(data)

However this test-program results in this output in the AppDaemon log:

2020-04-02 18:14:55.101811 WARNING nok_light: ------------------------------------------------------------
2020-04-02 18:14:55.100732 WARNING nok_light: Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/appdaemon/app_management.py", line 145, in initialize_app
    await utils.run_in_executor(self, init)
  File "/usr/lib/python3.8/site-packages/appdaemon/utils.py", line 276, in run_in_executor
    response = future.result()
  File "/usr/lib/python3.8/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/appdaemon/apps/nok_light.py", line 10, in initialize
    if self.is_client_connected():
  File "/usr/lib/python3.8/site-packages/appdaemon/utils.py", line 191, in inner_sync_wrapper
    f = run_coroutine_threadsafe(self, coro(self, *args, **kwargs))
  File "/usr/lib/python3.8/site-packages/appdaemon/utils.py", line 285, in run_coroutine_threadsafe
    result = future.result(self.AD.internal_function_timeout)
  File "/usr/lib/python3.8/concurrent/futures/_base.py", line 439, in result
    return self.__get_result()
  File "/usr/lib/python3.8/concurrent/futures/_base.py", line 388, in __get_result
    raise self._exception
  File "/usr/lib/python3.8/site-packages/appdaemon/plugins/mqtt/mqttapi.py", line 318, in is_client_connected
    return await plugin.mqtt_client_state()
AttributeError: 'HassPlugin' object has no attribute 'mqtt_client_state'

It is as if the MQTT plugin isn’t used at all. See the last line: “AttributeError: ‘HassPlugin’ object has no attribute ‘mqtt_client_state’”.

It also complains about mqtt_subscribe(), thats why it is commented out.
This is AppDaemon 4.0.3.

Does anyone have a clue? I’m a novice with respect to Home Assistant and AppDaemon

Hi

I solve it myself. It works by using do this:

import appdaemon.plugins.hass.hassapi as hass

class NokLight(hass.Hass):

  def initialize(self):
     self.mqtt = self.get_plugin_api("MQTT")
     self.mqtt.listen_event(self.mqtt_message_received_event, "MQTT_MESSAGE" )
     if self.mqtt.is_client_connected():
         self.log('MQTT is connected')
     self.log('NOK Light started')

  def mqtt_message_received_event(self,eventname,data,kwargs):
     self.log(data[0])

But still, why didn’t it work the other way since that’s what’s documented

I’m getting the same issue. I was hopeful your fix would be what I was looking for. But the fix is also giving me the same error. I don’t know if this still works. Does this still work for you?

Show your appdaemon.yaml, your code and your error messages please.

I ended up getting it working with some help on discord. It was a dumb error but in case anyone else has the same problem.

I didn’t have the client_host set. Because mqtt publishes were working I assumed it was pulling the info it needed from HA. But once I specifically set it the events starting being triggered.

Hello,

I was also unable to receive mqtt_events in ad following the doc. Your code saved me !

Here is a few more details for the others:

your appdaemon.yaml:

secrets: /config/secrets.yaml
appdaemon:
  latitude: zzz
  longitude: zzz
  elevation: 2
  time_zone: Europe/Amsterdam
  plugins:
    HASS:
      type: hass
    MQTT:
      type: mqtt
      namespace: mqtt # needed
      verbose: True
      client_host: core-mosquitto
      client_port: 1883
      client_id: localad
      client_user: usermqtt
      client_password: passmqtt
      event_name: MQTT_MESSAGE # the event you will listen in your module
      client_topics: NONE
http:
  url: http://127.0.0.1:5050
admin:
api:
hadashboard:

and the module.py:

import appdaemon.plugins.hass.hassapi as hass

class test_Mqtt(hass.Hass):
    def initialize(self):
        self.mqtt = self.get_plugin_api("MQTT")
        self.mqtt.mqtt_subscribe(topic=your_topic)      
        self.mqtt.listen_event(self.cb_event, "MQTT_MESSAGE", topic=your_topic, namespace='mqtt')
		
    def cb_event(self, event_name, data, kwargs):
	    self.log(data)
3 Likes

I see in:

it says:

At this point, it is not possible to use single level wildcard like using homeassistant/+/light instead of
homeassistant/bedroom/light. This could be added later, if need be.

I also tried getting it to respond to wildcards, both using TOPIC and WILDCARD settings, but no luck. It seems you can subscribe to either all messages, or a specific one.

Here’s a script I wrote for AppDaemon that shows how to:

  1. Subscribe to all MQTT Messages
  2. Parse through and only find the ones for the topic you want
  3. Send a new MQTT message extracting content from the original

Hopefully it’ll help get people started, as I’ve found the documentation to be fractured and not up to date:

import appdaemon.plugins.hass.hassapi as hass
import json

LISTEN_TOPIC = "BlueIris/+/Status"
PUBLISH_TOPIC = "homeassistant/camera_intents/description"


def does_needle_match_haystack_topic(needle_string, haystack_string):
    needle = needle_string.split("/")
    haystack = haystack_string.split("/")

    # Good solution if equal
    if needle == haystack:
        return True
    if len(needle) < len(haystack):
        return False

    # Check all subtopics for a match or wildcard match
    for i in range(len(needle)):
        s_needle = needle[i]
        if i < len(haystack):
            s_haystack = haystack[i]
        else:
            return False

        # Handle wildcards
        if s_haystack == "#":
            return True
        if not (s_haystack == "+" or s_haystack == s_needle):
            return False

    # Tried all steps, and nothing broke the pattern, so return true
    return True


def substring_after(s, delim):
    return s.partition(delim)[2]


class MqttExample(hass.Hass):

    def initialize(self):
        self.mqtt = self.get_plugin_api("MQTT")

        # Only seems to work with specific topics, no wildcards -
        #   described in https://buildmedia.readthedocs.org/media/pdf/appdaemon/stable/appdaemon.pdf
        self.mqtt.listen_event(self.mqtt_message_received_event, "MQTT_MESSAGE")
        if self.mqtt.is_client_connected():
            self.log("MQTT is connected")

        # This makes it easier to see in the logs whenever you update the file
        self.log("===============================MqttExample initialized")

    def mqtt_message_received_event(self, event_name, data, kwargs):
        try:
            _topic = data['topic']
            _payload = data['payload']

            if does_needle_match_haystack_topic(_topic, LISTEN_TOPIC):
                # This topic matches the camera feed lookup list
                self.handle_matched_message(_topic, _payload)

        except KeyError:
            self.log("KeyError trying to extract topic and payload from MQTT Message")

    # --- This is where the actual message is handled -------------
    def handle_matched_message(self, topic, payload):
        try:
            # Extract JSON data from the payload if it was sent as JSON. Change this if just text was sent
            payload_obj = json.loads(payload)

            # This is a simple example to take something out of JSON and the topic and resend it
            channel = topic.split("/")[1]  # Take the second part of the topic field
            status = payload_obj['status']  # Get the 'status' field from the JSON
            message = "The status of {} is {}".format(channel, status)  # Assemble them into a message

            # Send this modified message to a new channel            
            self.mqtt.mqtt_publish(PUBLISH_TOPIC, message)

        except KeyError as ex:
            self.log("Keyerror getting {} data:".format(topic))
            self.log(payload)
            self.log(ex)
        pass

4 Likes

Thanks all for the examples.

I found that I did have to derive my app from hass.Hass using self.get_plugin_api("MQTT") and not derive from mqtt.Mqtt as shown in the MQTT API reference. But I did not have to add the mqtt_host to the appdaemon.yaml, things worked fine without that.

FYI there’s a documentation request ticket for a working MQTT app.

1 Like