Extending the WebSocket API

As a component you might have information that you want to make available to the frontend. For example, the media player will want to make album covers available for the frontend to show. Our frontend is communicating with the backend over the websocket API, which can be extended with custom commands.

Registering a command (Python)

To register a command, you need to have a message type, a message schema and a message handler. Your component does not have to add the websocket API as a dependency. You register your command, and if the user is using the websocket API, the command will be made available.

Message Types

Message types are made up the domain and the message type, separated by a forward slash. In the below example, we're defining media_player/thumbnail.

# The type of the message
WS_TYPE_MEDIA_PLAYER_THUMBNAIL = "media_player/thumbnail"

Message Schema

The message schema defines what type of data we expect when the message is invoked. It is defined as a voluptuous schema and has to extend the base web socket command schema.

import voluptuous as vol
from homeassistant.components import websocket_api
import homeassistant.helpers.config_validation as cv
# The schema for the message
SCHEMA_WEBSOCKET_GET_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{
"type": WS_TYPE_MEDIA_PLAYER_THUMBNAIL,
# The entity that we want to retrieve the thumbnail for.
"entity_id": cv.entity_id,
}
)

Defining a handler

Message handlers are callback functions that are run inside the event loop. If you want to do I/O or have to wait for your result, create a new function and queue it up using hass.async_add_job. This is done so that the websocket API can get back to handling the next message as soon as possible.

Sending a direct response

If you are defining a command that is querying simple information, you might be able to fulfill the request while the handler is being called by the websocket API. To do this, use connection.to_write.put_nowait.

@callback
def websocket_handle_thumbnail(hass, connection, msg):
"""Handle getting a thumbnail."""
# We know the answer without having to fetch any information,
# so we send it directly.
connection.to_write.put_nowait(
websocket_api.result_message(
msg["id"], {"thumbnail": "http://via.placeholder.com/350x150"}
)
)

Sending a delayed response

If your command needs to interact with the network, a device or needs to compute information, you will need to queue a job to do the work and send the response. To do this, use connection.send_message_outside.

@callback
def websocket_handle_thumbnail(hass, connection, msg):
"""Handle get media player cover command."""
# Retrieve media player using passed in entity id.
player = hass.data[DOMAIN].get_entity(msg["entity_id"])
# If the player does not exist, send an error message.
if player is None:
connection.to_write.put_nowait(
websocket_api.error_message(
msg["id"], "entity_not_found", "Entity not found"
)
)
return
# Define a function to be enqueued.
async def send_image():
"""Send image."""
data, content_type = await player.async_get_media_image()
# No media player thumbnail available
if data is None:
connection.send_message_outside(
websocket_api.error_message(
msg["id"], "thumbnail_fetch_failed", "Failed to fetch thumbnail"
)
)
return
connection.send_message_outside(
websocket_api.result_message(
msg["id"],
{
"content_type": content_type,
"content": base64.b64encode(data).decode("utf-8"),
},
)
)
# Player exist. Queue up a job to send the thumbnail.
hass.async_add_job(send_image())

Registering with the Websocket API

With all pieces defined, it's time to register the command. This is done inside your setup method.

async def async_setup(hass, config):
"""Setup of your component."""
hass.components.websocket_api.async_register_command(
WS_TYPE_MEDIA_PLAYER_THUMBNAIL,
websocket_handle_thumbnail,
SCHEMA_WEBSOCKET_GET_THUMBNAIL,
)

Calling the command from the frontend (JavaScript)

With your command defined, it's time to call it from the frontend! This is done using JavaScript. You will need access to the hass object which holds the WebSocket connection to the backend. Then just call hass.connection.sendMessagePromise. This will return a promise that will resolve if the command succeeds and errors if the command fails.

hass.connection.sendMessagePromise({
type: 'media_player/thumbnail',
entity_id: 'media_player.living_room_tv',
}).then(
(resp) => {
console.log('Message success!', resp.result);
},
(err) => {
console.error('Message failed!', err);
}
);

If your command is not sending a response, you can use hass.connection.sendMessage.