Skip to content

Commit

Permalink
Add System Bridge keyboard services (home-assistant#53893)
Browse files Browse the repository at this point in the history
* Add keyboard services

* Extract to voluptuous validator

* Cleanup

* Lint

* Catch StopIteration

* Match validator

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Raise from

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
  • Loading branch information
timmo001 and MartinHjelmare authored Nov 13, 2021
1 parent 8ce3f18 commit 27b2aa0
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 52 deletions.
167 changes: 115 additions & 52 deletions homeassistant/components/system_bridge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from systembridge.client import BridgeClient
from systembridge.exceptions import BridgeAuthenticationException
from systembridge.objects.command.response import CommandResponse
from systembridge.objects.keyboard.payload import KeyboardPayload
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
Expand All @@ -21,7 +22,11 @@
CONF_PORT,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import (
aiohttp_client,
config_validation as cv,
Expand All @@ -39,20 +44,15 @@

CONF_ARGUMENTS = "arguments"
CONF_BRIDGE = "bridge"
CONF_KEY = "key"
CONF_MODIFIERS = "modifiers"
CONF_TEXT = "text"
CONF_WAIT = "wait"

SERVICE_SEND_COMMAND = "send_command"
SERVICE_SEND_COMMAND_SCHEMA = vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_COMMAND): cv.string,
vol.Optional(CONF_ARGUMENTS, []): cv.string,
}
)
SERVICE_OPEN = "open"
SERVICE_OPEN_SCHEMA = vol.Schema(
{vol.Required(CONF_BRIDGE): cv.string, vol.Required(CONF_PATH): cv.string}
)
SERVICE_SEND_KEYPRESS = "send_keypress"
SERVICE_SEND_TEXT = "send_text"


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Expand Down Expand Up @@ -113,25 +113,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if hass.services.has_service(DOMAIN, SERVICE_SEND_COMMAND):
return True

def valid_device(device: str):
"""Check device is valid."""
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get(device)
if device_entry is not None:
try:
return next(
entry.entry_id
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.entry_id in device_entry.config_entries
)
except StopIteration as exception:
raise vol.Invalid from exception
raise vol.Invalid(f"Device {device} does not exist")

async def handle_send_command(call):
"""Handle the send_command service call."""
device_registry = dr.async_get(hass)
device_id = call.data[CONF_BRIDGE]
device_entry = device_registry.async_get(device_id)
if device_entry is None:
_LOGGER.warning("Missing device: %s", device_id)
return
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
call.data[CONF_BRIDGE]
]
bridge: Bridge = coordinator.bridge

command = call.data[CONF_COMMAND]
arguments = shlex.split(call.data.get(CONF_ARGUMENTS, ""))

entry_id = next(
entry.entry_id
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.entry_id in device_entry.config_entries
)
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry_id]
bridge: Bridge = coordinator.bridge
arguments = shlex.split(call.data[CONF_ARGUMENTS])

_LOGGER.debug(
"Command payload: %s",
Expand All @@ -141,55 +146,113 @@ async def handle_send_command(call):
response: CommandResponse = await bridge.async_send_command(
{CONF_COMMAND: command, CONF_ARGUMENTS: arguments, CONF_WAIT: False}
)
if response.success:
_LOGGER.debug(
"Sent command. Response message was: %s", response.message
)
else:
_LOGGER.warning(
"Error sending command. Response message was: %s", response.message
if not response.success:
raise HomeAssistantError(
f"Error sending command. Response message was: {response.message}"
)
except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception:
_LOGGER.warning("Error sending command. Error was: %s", exception)
raise HomeAssistantError("Error sending command") from exception
_LOGGER.debug("Sent command. Response message was: %s", response.message)

async def handle_open(call):
"""Handle the open service call."""
device_registry = dr.async_get(hass)
device_id = call.data[CONF_BRIDGE]
device_entry = device_registry.async_get(device_id)
if device_entry is None:
_LOGGER.warning("Missing device: %s", device_id)
return
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
call.data[CONF_BRIDGE]
]
bridge: Bridge = coordinator.bridge

path = call.data[CONF_PATH]

entry_id = next(
entry.entry_id
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.entry_id in device_entry.config_entries
)
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry_id]
bridge: Bridge = coordinator.bridge

_LOGGER.debug("Open payload: %s", {CONF_PATH: path})
try:
await bridge.async_open({CONF_PATH: path})
_LOGGER.debug("Sent open request")
except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception:
_LOGGER.warning("Error sending. Error was: %s", exception)
raise HomeAssistantError("Error sending") from exception
_LOGGER.debug("Sent open request")

async def handle_send_keypress(call):
"""Handle the send_keypress service call."""
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
call.data[CONF_BRIDGE]
]
bridge: Bridge = coordinator.data

keyboard_payload: KeyboardPayload = {
CONF_KEY: call.data[CONF_KEY],
CONF_MODIFIERS: shlex.split(call.data.get(CONF_MODIFIERS, "")),
}

_LOGGER.debug("Keypress payload: %s", keyboard_payload)
try:
await bridge.async_send_keypress(keyboard_payload)
except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception:
raise HomeAssistantError("Error sending") from exception
_LOGGER.debug("Sent keypress request")

async def handle_send_text(call):
"""Handle the send_keypress service call."""
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
call.data[CONF_BRIDGE]
]
bridge: Bridge = coordinator.data

keyboard_payload: KeyboardPayload = {CONF_TEXT: call.data[CONF_TEXT]}

_LOGGER.debug("Text payload: %s", keyboard_payload)
try:
await bridge.async_send_keypress(keyboard_payload)
except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception:
raise HomeAssistantError("Error sending") from exception
_LOGGER.debug("Sent text request")

hass.services.async_register(
DOMAIN,
SERVICE_SEND_COMMAND,
handle_send_command,
schema=SERVICE_SEND_COMMAND_SCHEMA,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_COMMAND): cv.string,
vol.Optional(CONF_ARGUMENTS, ""): cv.string,
},
),
)

hass.services.async_register(
DOMAIN,
SERVICE_OPEN,
handle_open,
schema=SERVICE_OPEN_SCHEMA,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_PATH): cv.string,
},
),
)

hass.services.async_register(
DOMAIN,
SERVICE_SEND_KEYPRESS,
handle_send_keypress,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_KEY): cv.string,
vol.Optional(CONF_MODIFIERS): cv.string,
},
),
)

hass.services.async_register(
DOMAIN,
SERVICE_SEND_TEXT,
handle_send_text,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_TEXT): cv.string,
},
),
)

# Reload entry when its updated.
Expand Down
46 changes: 46 additions & 0 deletions homeassistant/components/system_bridge/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,49 @@ open:
example: "https://www.home-assistant.io"
selector:
text:
send_keypress:
name: Send Keyboard Keypress
description: Sends a keyboard keypress.
fields:
bridge:
name: Bridge
description: The server to send the command to.
required: true
selector:
device:
integration: system_bridge
key:
name: Key
description: "Key to press. List available here: http://robotjs.io/docs/syntax#keys"
required: true
example: "audio_play"
selector:
text:
modifiers:
name: Modifiers
description: "List of modifier(s). Accepts alt, command/win, control, and shift."
required: false
default: ""
example:
- "control"
- "shift"
selector:
text:
send_text:
name: Send Keyboard Text
description: Sends text for the server to type.
fields:
bridge:
name: Bridge
description: The server to send the command to.
required: true
selector:
device:
integration: system_bridge
text:
name: Text
description: "Text to type."
required: true
example: "Hello world"
selector:
text:

0 comments on commit 27b2aa0

Please sign in to comment.