Registering a service

In one of my custom components, I would like to register a service that calls methods within that component, but I’m not clear how best to do that.

In the example in the docs (https://home-assistant.io/developers/development_services/), services are registered in the platform’s setup function. The handler functions for those services simply set the state of some HomeAssistant sensors, which is done via the hass object (which is passed as a variable to the setup function).

Instead, how would you register a service that can call a method within the component itself? In other words, how can the handler function for the service get access to the custom component’s methods and internal state? I can think of some clunky ways of doing this (perhaps registering the services in the init function of the custom component and defining the handler as a mehod within the component?), but perhaps there is a ‘best’ way of doing this in HomeAssistant? Alternatively, perhaps this is not how services are intended to work?

1 Like

Don’t have access to my pc to copy/paste an example but I find looking at the github code for a simple component that creates a service helps. From memory I looked at the downloader component when trying to answer this same question.

What you are looking to do is quite simple.

Don’t take my word for it, but I think it wouldn’t be such a good idea. HA is using a lot of async code, and when you register a service that doesn’t work async, the HA core might have some trouble dealing with that.
For using regular Python functions / methods the Python Scripts should be used. Such a script could then make use of what your custom component provides.
But again, I’m no expert on this. So doing it directly in the component could be valid as well.

Something you can do is using the dispatcher_connect and dispatcher_send available in homeassistant.helpers.dispatcher (you register and subscribe to signals if you don’t need synchronous behaviours)

Is there not some confusion here. The last couple of responses seem to assume the post is about services in the linux system / hook kind of sense.

I’d assumed it was in the homeassistant sense of creating a service that can be called from an automation / script?

No, at least I’m talking about services components expose. Those shouldn’t do IO when they aren’t implemented in an async-friendly way while being accessed async. I may have failed to properly communicate this in my post. Sorry for that.

Thanks @andrewdolphin. I looked at the downloader component as you suggested, and you are correct that it contains a service. However, my requirement is slightly different - I’ll explain my situation in more detail here.

I have defined a custom sensor that represents a thermostat, which contains methods for changing the setpoint, querying the battery level, etc. I would like to be able to define and register a HomeAssistant service (that can be called from an automation or a script) that can act as a wrapper for those methods. Does that make sense? I think it should be fairly simple, but can’t find anywhere in the docs quite how to do it…

It would be something along these lines:

def setup(hass, config):
# or async_setup_platform(hass, config, async_add_devices, discovery_info=None):
    def handle_set_setpoint(call):
        # Code for setting setpoint within Thermostat
        # Something like:
        thermostat_instance = hass.get_component(call.get('entity_id')) # This is probably the key part - what is this function?
        thermostat.set_setpoint(call.get('temperature'))

    hass.services.register(DOMAIN, 'set_setpoint', handle_set_setpoint)
    return True

def Thermostat(Entity):
    def set_setpoint(self, temperature):
        # Add code for setting setpoint temperature...

I think it comes down to that one key line about how to access the component within the handle_set_setpoint function. Is it just via the hass object?

Did you ever figure this out? I am trying to do the exact same thing and a year later it’s still not documented anywhere.

Take a look at the climate platform code.

https://github.com/home-assistant/home-assistant/blob/d4c5cf396790e89aedc4693a91cc984e7a335bac/homeassistant/components/climate/init.py

You’ll see that the component has it’s own service register call.

@andrewdolphin I’ve looked at the example that you have reference, and that works even less then the “old” way of doing it.

Here is what I came up with based on the example you gave me.

async def async_setup(hass, config):
    """Setup Airscape component."""
    component = hass.data[DOMAIN] = EntityComponent(
        _LOGGER, DOMAIN, hass, SCAN_INTERVAL
    )
    component.async_setup(config)

    # for conf in config["fan"]:
    #    if conf["platform"] == DOMAIN:
    #         hass.data[DOMAIN] = {
    #            "host": conf.get("host"),
    #           "name": conf.get("name"),
    #          "timeout": conf.get("timeout"),
    #         "minimum": conf.get("minimum"),
    #    }
    hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config))
    component.async_register_entity_service("speed_up", None, service_speed_up)
    component.async_register_entity_service("slow_down", None, "slow_down")

    # Return boolean to indicate that initialization was successfully.
    return True


def service_speed_up(entity, service: ServiceDataType) -> None:
    _LOGGER.debug("Calling Service service_speed_up")
    if entity is None:
        _LOGGER.debug("Service service_speed up entity is None")
    entity.speed_up()

There isn’t any evidence that those service handlers are ever called (no debug log entries). I did notice in the example that the function call for the service was either a string literal or a function name. Based on the example I interpreted that to mean, a string literal would call the defined function of the entity class, and a function reference would call the function defined in the init. I tested both and neither is ever called. The platform does get loaded, I can control it from the UI. And the services do get registered, I can see them in the developer panel and the call service button “works” but nothing happens. I have to admit I am a little lost here. The example custom service is not very complete. And it only manipulates the state of an entity.

I also tried the “old” method:

def setup(hass, config):
    """Setup Airscape Fan Component."""

    def service_speed_up(call):
        """Handle speed up service call."""
        _LOGGER.debug("Calling service_speed_up")
        # How do I reference to the entity to call
        # speed_up member

    def service_slow_down(call):
        """Handle speed up service call."""
        _LOGGER.debug("Calling service_speed_up")
        # How do I reference to the entity to call
        # slow_down member

    for conf in config["fan"]:
        if conf["platform"] == DOMAIN:
            hass.data[DOMAIN] = {
                "host": conf.get("host"),
                "name": conf.get("name"),
                "timeout": conf.get("timeout"),
                "minimum": conf.get("minimum"),
            }
    hass.helpers.discovery.load_platform("fan", DOMAIN, {}, config)
    hass.services.register(DOMAIN, "speed_up", service_slow_down)
    hass.services.register(DOMAIN, "slow_down", service_speed_up)

    return True

But the question with this style is how do I get access to the entity object to call one of its methods? At least using this style of service handler I do actually get the handler executed. I do see entries in debug.

Thanks to some help I was able to figure this out. The best way to do this is with the dispatcher. As part of your platform derived class use the lifecycle method async_added_to_hass. This fires after the entity is added to HA and you have access to the entity_id via self.entity_id. In that function connect to the dispatcher to listen to a specific signal. The function parameters include an element for what entity function to call when that message is received.

class SomeFan(FanEntity):
...
async def async_added_to_hass(self):
        """Run when about to be added to hass."""
        async_dispatcher_connect(
            # The Hass Object
            self.hass,
            # The Signal to listen for.
            # Try to make it unique per entity instance
            # so include something like entity_id 
            # or other unique data from the service call
            SIGNAL_TO_LISTEN_FOR.format(self.entity_id),
            # Function handle to call when signal is received
            self.some_function
        )

Once you are connected to the dispatcher then in your service handler you can send that message to the dispatcher.

def setup_platform(hass, config, add_entities, discovery_info=None):
...
    def handle_service_call(call):
        """Handle service call."""
        entity_id = call.data["entity_id"]
        async_dispatcher_send(hass, SIGNAL_TO_LISTEN_FOR.format(entity_id))

    hass.services.register(DOMAIN, "some_service", handle_service_call, SERVICE_SCHEMA)

Hi I’m trying to add a custom service to my custom_component sinope which contain many paltform, light, switch and climate but I’m stuck somewhere and the device is not updated when I call the service.
I’m registering a service sinope.set_outside_temperature with

component = hass.data[DOMAIN] = EntityComponent(
        _LOGGER, DOMAIN, hass, SCAN_INTERVAL
    )
    component.async_register_entity_service(
        SERVICE_SET_OUTSIDE_TEMPERATURE,
        {vol.Required(ATTR_OUTSIDE_TEMPERATURE): vol.All(
            vol.Coerce(float), vol.Range(min=-40, max=40)
            )
	},
        "set_outside_temperature",
        [SUPPORT_OUTSIDE_TEMPERATURE],
    )

I have created a services.yaml file like this

set_ouside_temperature:
  description: Send outside temperature to thermostats.
  fields:
    entity_id:
      description: Name(s) of thermostats that will receive outside temperature.
      example: "climate.sinope_climate_office"
    outside_temperature:
      description: outside temperature that will be sent to thermostat in oC or oF depending on your setup.
      example: "24"

in the dev tool the service created look like this

My service description is empty and there is no entity_id selection line.
if I specify the entity_id in the description data, when I call the service the entity_id is sent along with the outside_temperature. The log say
DEBUG (SyncWorker_4) [homeassistant.core] Bus:Handling <Event service_registered[L]: domain=sinope, service=set_outside_temperature>

DEBUG (MainThread) [custom_components.sinope.climate] Registered to service set_outside_temperature

DEBUG (MainThread) [homeassistant.components.websocket_api.http.connection.547363897696] Received {'type': 'call_service', 'domain': 'sinope', 'service': 'set_outside_temperature', 'service_data': {'entity_id': 'climate.sinope_climate_bureau', 'outside_temperature': 24}, 'id': 25}
DEBUG (MainThread) [homeassistant.core] Bus:Handling <Event call_service[L]: domain=sinope, service=set_outside_temperature, service_data=entity_id=climate.sinope_climate_bureau, outside_temperature=24>
WARNING (MainThread) [homeassistant.helpers.service] Unable to find referenced entities climate.sinope_climate_bureau

So I need to find out how to get that second line in dev tool for entity_id like what we get for other services and why my services.yaml is not loaded
Any help apreciated