Add System Bridge keyboard services (#53893)

* 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>
This commit is contained in:
Aidan Timson 2021-11-13 12:45:42 +00:00 committed by GitHub
parent 8ce3f18295
commit 27b2aa04c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 161 additions and 52 deletions

View File

@ -10,6 +10,7 @@ from systembridge import Bridge
from systembridge.client import BridgeClient from systembridge.client import BridgeClient
from systembridge.exceptions import BridgeAuthenticationException from systembridge.exceptions import BridgeAuthenticationException
from systembridge.objects.command.response import CommandResponse from systembridge.objects.command.response import CommandResponse
from systembridge.objects.keyboard.payload import KeyboardPayload
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -21,7 +22,11 @@ from homeassistant.const import (
CONF_PORT, CONF_PORT,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import ( from homeassistant.helpers import (
aiohttp_client, aiohttp_client,
config_validation as cv, config_validation as cv,
@ -39,20 +44,15 @@ PLATFORMS = ["binary_sensor", "sensor"]
CONF_ARGUMENTS = "arguments" CONF_ARGUMENTS = "arguments"
CONF_BRIDGE = "bridge" CONF_BRIDGE = "bridge"
CONF_KEY = "key"
CONF_MODIFIERS = "modifiers"
CONF_TEXT = "text"
CONF_WAIT = "wait" CONF_WAIT = "wait"
SERVICE_SEND_COMMAND = "send_command" 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 = "open"
SERVICE_OPEN_SCHEMA = vol.Schema( SERVICE_SEND_KEYPRESS = "send_keypress"
{vol.Required(CONF_BRIDGE): cv.string, vol.Required(CONF_PATH): cv.string} SERVICE_SEND_TEXT = "send_text"
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -113,25 +113,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if hass.services.has_service(DOMAIN, SERVICE_SEND_COMMAND): if hass.services.has_service(DOMAIN, SERVICE_SEND_COMMAND):
return True 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): async def handle_send_command(call):
"""Handle the send_command service call.""" """Handle the send_command service call."""
device_registry = dr.async_get(hass) coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
device_id = call.data[CONF_BRIDGE] call.data[CONF_BRIDGE]
device_entry = device_registry.async_get(device_id) ]
if device_entry is None: bridge: Bridge = coordinator.bridge
_LOGGER.warning("Missing device: %s", device_id)
return
command = call.data[CONF_COMMAND] command = call.data[CONF_COMMAND]
arguments = shlex.split(call.data.get(CONF_ARGUMENTS, "")) arguments = shlex.split(call.data[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
_LOGGER.debug( _LOGGER.debug(
"Command payload: %s", "Command payload: %s",
@ -141,55 +146,113 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
response: CommandResponse = await bridge.async_send_command( response: CommandResponse = await bridge.async_send_command(
{CONF_COMMAND: command, CONF_ARGUMENTS: arguments, CONF_WAIT: False} {CONF_COMMAND: command, CONF_ARGUMENTS: arguments, CONF_WAIT: False}
) )
if response.success: if not response.success:
_LOGGER.debug( raise HomeAssistantError(
"Sent command. Response message was: %s", response.message f"Error sending command. Response message was: {response.message}"
)
else:
_LOGGER.warning(
"Error sending command. Response message was: %s", response.message
) )
except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: 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): async def handle_open(call):
"""Handle the open service call.""" """Handle the open service call."""
device_registry = dr.async_get(hass) coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
device_id = call.data[CONF_BRIDGE] call.data[CONF_BRIDGE]
device_entry = device_registry.async_get(device_id) ]
if device_entry is None: bridge: Bridge = coordinator.bridge
_LOGGER.warning("Missing device: %s", device_id)
return
path = call.data[CONF_PATH] 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}) _LOGGER.debug("Open payload: %s", {CONF_PATH: path})
try: try:
await bridge.async_open({CONF_PATH: path}) await bridge.async_open({CONF_PATH: path})
_LOGGER.debug("Sent open request")
except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: 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( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_SEND_COMMAND, SERVICE_SEND_COMMAND,
handle_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( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_OPEN, SERVICE_OPEN,
handle_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. # Reload entry when its updated.

View File

@ -42,3 +42,49 @@ open:
example: "https://www.home-assistant.io" example: "https://www.home-assistant.io"
selector: selector:
text: 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: