From 27b2aa04c9ce9330cbabbb29ccc84ca452c08282 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 13 Nov 2021 12:45:42 +0000 Subject: [PATCH] Add System Bridge keyboard services (#53893) * Add keyboard services * Extract to voluptuous validator * Cleanup * Lint * Catch StopIteration * Match validator Co-authored-by: Martin Hjelmare * Raise from Co-authored-by: Martin Hjelmare --- .../components/system_bridge/__init__.py | 167 ++++++++++++------ .../components/system_bridge/services.yaml | 46 +++++ 2 files changed, 161 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index b3f481141b8..cf50368c34c 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -10,6 +10,7 @@ from systembridge import Bridge 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 @@ -21,7 +22,11 @@ from homeassistant.const import ( 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, @@ -39,20 +44,15 @@ PLATFORMS = ["binary_sensor", "sensor"] 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: @@ -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", @@ -141,55 +146,113 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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. diff --git a/homeassistant/components/system_bridge/services.yaml b/homeassistant/components/system_bridge/services.yaml index 3f79f441415..aff0094501e 100644 --- a/homeassistant/components/system_bridge/services.yaml +++ b/homeassistant/components/system_bridge/services.yaml @@ -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: