mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
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:
parent
8ce3f18295
commit
27b2aa04c9
@ -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.
|
||||||
|
@ -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:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user