AppleTV integration keyboard commands

I would like to be able to send text to my Apple TV.

The Apple TV integration uses the pyatv library, which already supports this feature.

However, remote.send_command of the Apple TV integration only interacts with the atv.remote_control interface. It would be great to support the list of commands in atv.keyboard.

Here’s an updated implementation that supports all methods on atv with up to 1 string or int argument. That makes e.g. keyword.text_set callable via remote.send_command.

    async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
        """Send a command to one device."""
        num_repeats = kwargs[ATTR_NUM_REPEATS]
        delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)

        if not self.is_on:
            _LOGGER.error("Unable to send commands, not connected to %s", self.name)
            return

        for _ in range(num_repeats):
            for single_command in command:
                split = single_command.split(' ')
                command_name = split[0]
                if '.' in command_name:
                    attr_value = self.atv
                    for command_path in command_name.split('.'):
                        attr_value = getattr(attr_value, command_path, None)
                else:
                    attr_value = getattr(self.atv.remote_control, single_command, None)
                if not attr_value:
                    raise ValueError("Command not found. Exiting sequence!!!")

                _LOGGER.info("Sending command %s", single_command)
                arg = ' '.join(split[1:])
                args = []
                if arg:
                    args.append(arg)
                await attr_value(*args)
                await asyncio.sleep(delay)

And another thing: The commands have hold actions. For example, holding the right button enables the fast forwarding mode. pyatv supports this feature, but it is not usable from the Apple TV integration. Here’s a slight update that makes e.g. remote_control.right 2 enable fast forward mode. It’s a little bit hacky.

    async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
        """Send a command to one device."""
        num_repeats = kwargs[ATTR_NUM_REPEATS]
        delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)

        if not self.is_on:
            _LOGGER.error("Unable to send commands, not connected to %s", self.name)
            return

        for _ in range(num_repeats):
            for single_command in command:
                split = single_command.split(' ')
                command_name = split[0]
                if '.' in command_name:
                    attr_value = self.atv
                    for command_path in command_name.split('.'):
                        attr_value = getattr(attr_value, command_path, None)
                else:
                    attr_value = getattr(self.atv.remote_control, single_command, None)
                if not attr_value:
                    raise ValueError("Command not found. Exiting sequence!!!")

                _LOGGER.info("Sending command %s", single_command)
                arg = ' '.join(split[1:])
                args = []
                if arg:
                    if command_name.startswith('remote_control') and arg.isdigit():
                        arg = InputAction(int(arg))
                    args.append(arg)
                await attr_value(*args)
                await asyncio.sleep(delay)

I have added this to the apple_tv component under remote.py. However, when I call the service, it shows failed to call service, command not found. Here is my service call

service: remote.send_command
target:
entity_id: remote.living_room_apple_4k
data:
num_repeats: 1
delay_secs: 0.4
hold_secs: 0
command: remote_control.right 2

if I just add remote_control.right it skips 10 seconds forward as expected.
Your help is appreciated.

I answered my own question, adding

from pyatv.const import InputAction

worked like a charm. Thanks for the hack. Great addition!

Where would I add this? :slight_smile:

I have this question too! I’m not sure where or how to find the remote.py file or make the update. Would love any tips! Been trying to figure it out with google but I haven’t found any obvious links / docs on updating the code(?) in an integration!

Hi @klopyrev would you mind providing instructions on where this should be added? This is exactly what I’d like to so with my Apple TV and text input. Thanks

1 Like

Ok - So I was finally able to get it working. Pretty sure I tried the same thing last time, so not sure what I did different this time.

Step 1:
Got all the files for the Apple TV Component and put them in the custom_component folder. So I wouldn’t have to change the original one. core/homeassistant/components/apple_tv at dev · home-assistant/core · GitHub

Step 2:
Change the manifest, I just changed the name and added a version:

{
    "domain": "apple_tv",
    "name": "Eple TV",
    "version": "1.0.0",
    "codeowners": ["@postlund"],
    "config_flow": true,
    "dependencies": ["zeroconf"],
    "documentation": "https://www.home-assistant.io/integrations/apple_tv",
    "iot_class": "local_push",
    "loggers": ["pyatv", "srptools"],
    "requirements": ["pyatv==0.15.1"],
    "zeroconf": [
      "_mediaremotetv._tcp.local.",
      "_companion-link._tcp.local.",
      "_airport._tcp.local.",
      "_sleep-proxy._udp.local.",
      "_touch-able._tcp.local.",
      "_appletv-v2._tcp.local.",
      "_hscp._tcp.local.",
      {
        "type": "_airplay._tcp.local.",
        "properties": {
          "model": "appletv*"
        }
      },
      {
        "type": "_airplay._tcp.local.",
        "properties": {
          "model": "audioaccessory*"
        }
      },
      {
        "type": "_airplay._tcp.local.",
        "properties": {
          "am": "airport*"
        }
      },
      {
        "type": "_raop._tcp.local.",
        "properties": {
          "am": "appletv*"
        }
      },
      {
        "type": "_raop._tcp.local.",
        "properties": {
          "am": "audioaccessory*"
        }
      },
      {
        "type": "_raop._tcp.local.",
        "properties": {
          "am": "airport*"
        }
      }
    ]
  }

Step 3:

Change the remote.py to this:

"""Remote control support for Apple TV."""

import asyncio
from collections.abc import Iterable
import logging
from typing import Any

from pyatv.const import InputAction

from homeassistant.components.remote import (
    ATTR_DELAY_SECS,
    ATTR_HOLD_SECS,
    ATTR_NUM_REPEATS,
    DEFAULT_DELAY_SECS,
    DEFAULT_HOLD_SECS,
    RemoteEntity,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import AppleTvConfigEntry
from .entity import AppleTVEntity

_LOGGER = logging.getLogger(__name__)

PARALLEL_UPDATES = 0
COMMAND_TO_ATTRIBUTE = {
    "wakeup": ("power", "turn_on"),
    "suspend": ("power", "turn_off"),
    "turn_on": ("power", "turn_on"),
    "turn_off": ("power", "turn_off"),
    "volume_up": ("audio", "volume_up"),
    "volume_down": ("audio", "volume_down"),
}


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: AppleTvConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Load Apple TV remote based on a config entry."""
    name: str = config_entry.data[CONF_NAME]
    # apple_tv config entries always have a unique id
    assert config_entry.unique_id is not None
    manager = config_entry.runtime_data
    async_add_entities([AppleTVRemote(name, config_entry.unique_id, manager)])


class AppleTVRemote(AppleTVEntity, RemoteEntity):
    """Device that sends commands to an Apple TV."""

    @property
    def is_on(self) -> bool:
        """Return true if device is on."""
        return self.atv is not None

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the device on."""
        await self.manager.connect()

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn the device off."""
        await self.manager.disconnect()

    async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
        """Send a command to one device."""
        num_repeats = kwargs[ATTR_NUM_REPEATS]
        delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)

        if not self.is_on:
            _LOGGER.error("Unable to send commands, not connected to %s", self.name)
            return

        for _ in range(num_repeats):
            for single_command in command:
                split = single_command.split(' ')
                command_name = split[0]
                if '.' in command_name:
                    attr_value = self.atv
                    for command_path in command_name.split('.'):
                        attr_value = getattr(attr_value, command_path, None)
                else:
                    attr_value = getattr(self.atv.remote_control, single_command, None)
                if not attr_value:
                    raise ValueError("Command not found. Exiting sequence!!!")

                _LOGGER.info("Sending command %s", single_command)
                arg = ' '.join(split[1:])
                args = []
                if arg:
                    args.append(arg)
                await attr_value(*args)
                await asyncio.sleep(delay)

Step 4
Restart Home Assistant - You should then see in your integration that the Apple TV Component has changed to the new name, in my case it changed from Apple TV to Eple TV (Apple in Norwegian for lack of imagination)

Skjermbilde 2024-10-09 kl. 14.58.50

Step 5:
You should now be able to call an action like this:

action: remote.send_command
target:
  entity_id: remote.living_room
data:
  command: 
  - 'keyboard.text_set "Lord of the Rings"'

Note: For it to work you would have to have the search open on the Apple TV, so you could put it as part of a script.

alias: Nytt skript
sequence:
  - action: media_player.select_source
    target:
      entity_id: media_player.living_room
    data:
      source: Søk
  - action: remote.send_command
    target:
      entity_id: remote.living_room
    data:
      command:
        - keyboard.text_set "Lord of the Rings"