REST response handling, extending the rest_command

I’ve trying to extend the current rest_command service to include the response as a possible return and include it in the same way as a Automation does with the data_template:

My end goal is to have a service that looks something like this:

reposter:
  send_and_await_answer:
    url: 'http://192.168.1.10:12101/api/listen-for-command?nohass=false'
    method: 'POST'
    content_type: text/plain
    payload: '{{payload}}'
    action:
      service: persistent_notification.create
      data:
        message: "{{ value_json.response }}"
        title: "Getting post data"

In my case the plan is to use it for sending a question to Rhasspy and await an answer from which an actions would be performed.

I’ve got it working as far as being able to trigger an action, but I can’t figure out how to access/include the response data.

This is the currently working version, but without the response data

yaml

reposter:
  perform_action:
    url: 'http://your_api_to_call/api'
    method: 'POST'
    content_type: text/plain
    payload: '{{payload}}'
    action:
      service: persistent_notification.create
      data:
        message: "its working"
        title: "Getting post data"

python

"""Support for exposing regular REST commands as services."""
import asyncio
import logging

import aiohttp
from aiohttp import hdrs
import voluptuous as vol

from homeassistant.const import (
    CONF_HEADERS,
    CONF_METHOD,
    CONF_PASSWORD,
    CONF_PAYLOAD,
    CONF_TIMEOUT,
    CONF_URL,
    CONF_USERNAME,
    CONF_VERIFY_SSL,
    HTTP_BAD_REQUEST,
)
CONF_ACTION = "action"

from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_validation as cv, script

DOMAIN = "reposter"

_LOGGER = logging.getLogger(__name__)

DEFAULT_TIMEOUT = 10
DEFAULT_METHOD = "get"
DEFAULT_VERIFY_SSL = True

SUPPORT_REST_METHODS = ["get", "patch", "post", "put", "delete"]

CONF_CONTENT_TYPE = "content_type"

COMMAND_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_URL): cv.template,
        vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.All(
            vol.Lower, vol.In(SUPPORT_REST_METHODS)
        ),
        vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.template}),
        vol.Inclusive(CONF_USERNAME, "authentication"): cv.string,
        vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string,
        vol.Optional(CONF_PAYLOAD): cv.template,
        vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
        vol.Optional(CONF_CONTENT_TYPE): cv.string,
        vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
        vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
    }
)

CONFIG_SCHEMA = vol.Schema(
    {DOMAIN: cv.schema_with_slug_keys(COMMAND_SCHEMA)}, extra=vol.ALLOW_EXTRA
)


async def async_setup(hass, config):
    """Set up the REST repost component."""

    @callback
    def async_register_reposter(name, command_config):
        """Create service for rest command."""
        websession = async_get_clientsession(hass, command_config.get(CONF_VERIFY_SSL))
        timeout = command_config[CONF_TIMEOUT]
        method = command_config[CONF_METHOD]

        template_url = command_config[CONF_URL]
        template_url.hass = hass

        auth = None
        if CONF_USERNAME in command_config:
            username = command_config[CONF_USERNAME]
            password = command_config.get(CONF_PASSWORD, "")
            auth = aiohttp.BasicAuth(username, password=password)

        template_payload = None
        if CONF_PAYLOAD in command_config:
            template_payload = command_config[CONF_PAYLOAD]
            template_payload.hass = hass

        template_headers = None
        if CONF_HEADERS in command_config:
            template_headers = command_config[CONF_HEADERS]
            for template_header in template_headers.values():
                template_header.hass = hass

        content_type = None
        if CONF_CONTENT_TYPE in command_config:
            content_type = command_config[CONF_CONTENT_TYPE]

        async def async_service_handler(service):
            """Execute a shell command service."""
            payload = None
            if template_payload:
                payload = bytes(
                    template_payload.async_render(
                        variables=service.data, parse_result=False
                    ),
                    "utf-8",
                )

            request_url = template_url.async_render(
                variables=service.data, parse_result=False
            )

            headers = None
            if template_headers:
                headers = {}
                for header_name, template_header in template_headers.items():
                    headers[header_name] = template_header.async_render(
                        variables=service.data, parse_result=False
                    )

            if content_type:
                if headers is None:
                    headers = {}
                headers[hdrs.CONTENT_TYPE] = content_type

            try:
                async with getattr(websession, method)(
                    request_url,
                    data=payload,
                    auth=auth,
                    headers=headers,
                    timeout=timeout,
                ) as response:

                    if response.status < HTTP_BAD_REQUEST:
                        _LOGGER.debug(
                            "Success. Url: %s. Status code: %d. Payload: %s",
                            response.url,
                            response.status,
                            payload,
                        )
                        
                        if CONF_ACTION in command_config:
                            for action in command_config["actions"]:
                                attributes = {i[0]:i[1] for i in action}
                                #await response.text()
                                await command_config[CONF_ACTION].async_run(attributes, context=service.context)

                    else:
                        _LOGGER.warning(
                            "Error. Url: %s. Status code %d. Payload: %s",
                            response.url,
                            response.status,
                            payload,
                        )

            except asyncio.TimeoutError:
                _LOGGER.warning("Timeout call %s", request_url)

            except aiohttp.ClientError:
                _LOGGER.error("Client error %s", request_url)

        # register services
        hass.services.async_register(DOMAIN, name, async_service_handler)

    for command, command_config in config[DOMAIN].items():
        if CONF_ACTION in command_config:
            command_config["actions"] = command_config[CONF_ACTION]
            command_config[CONF_ACTION] = script.Script(
                hass, command_config[CONF_ACTION], f"reposter {command}", DOMAIN
            )

        async_register_reposter(command, command_config)

    return True

I’ve been trying to understand how the template data is handled, but after a few hours of work I don’t feel like I’m closer the actual solution.

All I got so far is that the template data for payload is handle by parsing whats stored in the service.data like this:

template_payload.async_render(
    variables=service.data, parse_result=False
)

but the service data is of the class mappingproxy
so I’m suspecting I should not try and force the the response information to that dictionary.

Maybe I’m trying to solve this in the completely wrong way, but any help would be appreciated

Why not just use a rest sensor?

the rest sensor updates on an interval right? I guess I could disable it as well of course between the times I want it to run.
The data being sent to it would also change depending on what the task to perform would be, meaning I would need to have multiple rest sensor performing similar tasks and on top of that having Automations handling the action of those.

I might of course be missing something obvious here that I haven’t though about how I could solve it.
But I will definitely look into the code on how that one works, since it probably contains exactly what I’m after.

No, that’s how it works. The sensor polls the resource on a fixed interval.

You could set a very long update interval (scan_interval == years) and call the home assistant update service manually.

You can also template the resource, which could cut down the number of sensors you need.

Ah alright, that might be a possible workaround in that case.
If I can’t find a better solution where I wouldn’t need to do all of those additional setups I will give that a try, but I much rather have as little yaml code to maintain as possible :slight_smile:

Thanks for the suggestion, I think it help point me in the right direction.

1 Like