AD and register_service() but getting service not found

Hi all,

I’m trying to register a service through an app like this:

def registerServices(self):
        self.register_service("alarmclock/toggletest", self.service_toggle, kwargs={})
        self.fire_event('service_registered', domain="alarmclock", service="toggletest")        
        self.log("Services registered")

and then the caller function:

def service_toggle(self, kwargs):
        self.workday = not self.workday
        self.log("Service toggle test")

I see from the AD log that the service is registered, and it also appears in the Developer Tools in hass.io, but when I try to call it I just get a message that the service is not found:

2020-04-09 20:07:18 DEBUG (MainThread) 
[homeassistant.components.websocket_api.http.connection.139774625198032] Received {'type': 'call_service', 'domain': 'alarmclock', 'service': 'toggletest', 'service_data': {}, 'id': 20}
2020-04-09 20:07:18 DEBUG (MainThread) [homeassistant.components.websocket_api.http.connection.139774625198032] Sending {'id': 20, 'type': 'result', 'success': False, 'error': {'code': 'not_found', 'message': 'Service not found.'}}

Anyone kind enough give me a hint on what is wrong?

Thanks.

Please, anyone that could help me figure out what I’m doing wrong?

@tormagj did you solve it? I’m having a similar issue.

I do not believe the self.fire_event() should be necessary. That function simply creates an “empty text service name” in HA, with no link back to AppDaemon. Looking at how HADashboard-widgets have done it, I have created a minimal example below, which does not work. Also, HADashboard doesn’t work for me neither, but that obviously worked at least at some point in time in the authors system based on the screenshots in the git repo.

Minimal example:

import appdaemon.plugins.hass.hassapi as hass

class TestService(hass.Hass):

    def initialize(self):
        self.set_log_level("DEBUG")
        logger = self.get_main_log()
        logger.setLevel("DEBUG")
        lg2=self.AD.logging.get_child("_services")
        lg2.setLevel("DEBUG")
        self.register_service("debug/testservice", self.testservice)
        self.register_service("debug/testservice_s", self.testservice_sync)
        self.register_service("automation/testservice", self.testservice)

    def testservice_sync(self, plugin, domain, service, data):        
        self.log(domain)
        self.log(service)

    async def testservice(self, plugin, domain, service, data):        
        self.log(domain)
        self.log(service)

The output from the appdaemon.log indicates no errors:

2020-04-18 22:06:20.845941 INFO AppDaemon: Initializing app testservice using class TestService from module testservice
2020-04-18 22:06:20.863777 DEBUG testservice: register_service: debug/testservice, {}
2020-04-18 22:06:20.864656 DEBUG AppDaemon: register_service called: default.debug.testservice → <bound method TestService.testservice of <testservice.TestService object at 0xb592ae38>>
2020-04-18 22:06:20.865473 DEBUG testservice: register_service: debug/testservice_s, {}
2020-04-18 22:06:20.866234 DEBUG AppDaemon: register_service called: default.debug.testservice_s → <bound method TestService.testservice_sync of <testservice.TestService object at 0xb592ae38>>
2020-04-18 22:06:20.867259 DEBUG testservice: register_service: automation/testservice, {}
2020-04-18 22:06:20.868075 DEBUG AppDaemon: register_service called: default.automation.testservice → <bound method TestService.testservice of <testservice.TestService object at 0xb592ae38>>

Calling self.list_services() yields (in the huge list of services):

{'namespace': 'default', 'domain': 'automation', 'service': 'testservice'}, {'namespace': 'default', 'domain': 'debug', 'service': 'testservice'}, {'namespace': 'default', 'domain': 'debug', 'service': 'testservice_s'}

Another interesting fact is that adding a listener to service_registered event in HA (devtools->events) shows no events being fired.

Thus, it seems like the services are created within AppDaemon, but never connected to HA. Specifically, everything up to the line linked below seems to execute correctly, but after that my knowledge in AppDaemon internals and asyncio is to shallow to grasp what’s going wrong.

self.AD.loop.create_task(self.AD.events.process_event(namespace, data))

I have tried to create a workaround based on self.fire_event(), and attaching a listener for the service call event, but since the service call is empty, no call is ever made from within HA.

HA version: 0.108.5
AppDaemon 4, version 0.2.4

I found the answer on discord:
"register_service is created to create AD services, not HA services"

2 Likes

Hi and sorry for my late reply.

Yes, I asked around on the Discord and was informed that the register_service is basically only for AD services and not HA services. Seems that HA does not support adding services this way, perhaps it should be? Anyone able to shed some light on the subject?

I was able to register services from AD and have them appear in HA by also firing a service registered event. See image below. The service also appears when using the list_services() method. However, when trying to call the service, HA just says “service not found”. So I concluded it is not possible, although it seems it could be…

One thing is adding some weird services, but what I was actually trying to achieve was having my AD app add for instance input_boolean or input_number on the fly in my code, and then use them for input in a custom Lovelace card. This didn’t work for some reason since the fields don’t seem to be “fully qualified” although they appeared in the States overview, they’re state changes were not registered properly. However, by following a thread I found on the topic, I was able to do what I wanted by implementing a listener in AD for those funky inputs that I added by code. So all in all it seems to be working fine.

I believe this was the post that helped me on my way:

Here is more or less what it boiled down to in my case:


    def create_input_booleans(self):
        self.input_booleans = [
            {"entity_id" : "input_boolean.app_alarmclock_skipnext", "state":"off", "attr": {"editable": "true", "friendly_name": "AlarmClock skipnext", "icon": "mdi:debug-step-over"}},
            {"entity_id" : "input_boolean.app_alarmclock_stop", "state":"off", "attr": {"editable": "true", "friendly_name": "AlarmClock stop", "icon": "mdi:stop"}},
        ]

        for ib in self.input_booleans:
            self.set_state(ib["entity_id"], state=ib["state"], attributes=ib["attr"])
        self.listen_event(self.ib_change_state, event = "call_service")
        self.listen_state(self.stop, "input_boolean.app_alarmclock_stop")

    def stop(self, entity, attribute, old, new, kwargs={}):
        if new == "on":
            self.log("Stop requested")
            self.switchoff()
            self.call_service("input_boolean/turn_off", entity_id="input_boolean.app_alarmclock_stop")

    def ib_change_state(self,event_name,data, kwargs):
        entity_id = data.get("service_data").get("entity_id", None)

        if entity_id is not None and any(entity_id in e["entity_id"] for e in self.input_booleans):
            entity_data = self.get_state(entity_id, attribute="all")
            if(data["service"] == "turn_off"):
                self.log(entity_id + " switched off")
                self.set_state(entity_id, state = "off", attributes=entity_data["attributes"])
            if(data["service"] == "turn_on"):
                self.log(entity_id + " switched on")
                self.set_state(entity_id, state = "on", attributes=entity_data["attributes"])
            if(data["service"] == "toggle"):
                self.log(entity_id + " toggled")
                togglestate = "on" if entity_data["state"] == "off" else "off"
                self.set_state(entity_id, state = togglestate, attributes=entity_data["attributes"])

    def createsensorsetup(self):
        """Create custom sensor"""        
        self.create_input_booleans()

So, in conclusion, I wasn’t able to register services freely, but I was able to achieve something similar by listening to changes. Hope it helps someone else looking around!

Yes, thank you. The docs definitely need some TLC in this area, in the appdaemon docs it doesn’t even define the callback args or mention that this isn’t a service that can be called from HA. You just saved me a bunch of time, thanks again.

-Fred

Hello, any progress in this?

I need to define a “service” in AppDeamon/python and call it from my automations.

Is this now possible?

I was able to work around this limitation by creating a blank script:

ad_presence_light_controller:
  alias: 'AppDaemon: Presence Light Controller'
  sequence: []

Then, in my app, I listen to events that call that script. You can easily access the data that is passed to the script too, so you could choose what action your app takes depending on that data.

def initialize(self):
    # Listen to the script being called which acts as the interface between
    # HA and AppDaemon
    self.listen_event(
        self.app_service_call,
        event="call_service",
        service="ad_presence_light_controller",
    )

def app_service_call(self, event_name, data, kwargs):
    service_data = data["service_data"]
    try:
        assert "method" in service_data, "Missing data field: 'method'"
    except AssertionError as exception:
        self.log(f"Service data invalid: {exception}", level="ERROR")
        return
    if service_data["method"] == "reset_room":
        self._reset_room()

Hope that helps! It would be great to have a direct way of creating services in HA, but I think this workaround is fairly clean.

3 Likes

Listening to events work.
For example I have an Appdaemon “service” that does TTS announce, optionally with an MP3 sound effect played before the text - all this as a HA event, basically like this:

class tts_announce(hass.Hass):

    def initialize(self):
        self.listen_event(self.tts_announce,  "tts_announce")

    def tts_announce(self, event, data, args):
        if data is not None and 'message' in data:
            text = data.get("message")

            filename = None
            if "filename" in data:
                filename = data.get("filename")
            ...

Which is callled like this from HA:

    - event: tts_announce
      event_data_template:
        filename: dog_bark.mp3
        message: >-
          {{ [
          "Funny message 1",
          "Funny message 2",
          "Funny message 3",
          ] |random }}
1 Like