Add websocket trigger/condition commands (#39109)

This commit is contained in:
Paulus Schoutsen 2020-08-24 23:01:57 +02:00 committed by GitHub
parent d5193e64de
commit 2a1fe9d29a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 204 additions and 61 deletions

View File

@ -377,7 +377,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
else: else:
await self.async_disable() await self.async_disable()
async def async_trigger(self, variables, skip_condition=False, context=None): async def async_trigger(self, variables, context=None, skip_condition=False):
"""Trigger automation. """Trigger automation.
This method is a coroutine. This method is a coroutine.

View File

@ -47,10 +47,9 @@ async def async_attach_trigger(
return return
hass.async_run_job( hass.async_run_job(
action( action,
{"trigger": {"platform": platform_type, "event": event}}, {"trigger": {"platform": platform_type, "event": event}},
context=event.context, event.context,
)
) )
return hass.bus.async_listen(event_type, handle_event) return hass.bus.async_listen(event_type, handle_event)

View File

@ -30,10 +30,9 @@ async def async_attach_trigger(hass, config, action, automation_info):
def hass_shutdown(event): def hass_shutdown(event):
"""Execute when Home Assistant is shutting down.""" """Execute when Home Assistant is shutting down."""
hass.async_run_job( hass.async_run_job(
action( action,
{"trigger": {"platform": "homeassistant", "event": event}}, {"trigger": {"platform": "homeassistant", "event": event}},
context=event.context, event.context,
)
) )
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown) return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown)

View File

@ -103,20 +103,19 @@ async def async_attach_trigger(
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
hass.async_run_job( hass.async_run_job(
action( action,
{ {
"trigger": { "trigger": {
"platform": platform_type, "platform": platform_type,
"entity_id": entity, "entity_id": entity,
"below": below, "below": below,
"above": above, "above": above,
"from_state": from_s, "from_state": from_s,
"to_state": to_s, "to_state": to_s,
"for": time_delta if not time_delta else period[entity], "for": time_delta if not time_delta else period[entity],
} }
}, },
context=to_s.context, to_s.context,
)
) )
matching = check_numeric_state(entity, from_s, to_s) matching = check_numeric_state(entity, from_s, to_s)

View File

@ -83,18 +83,17 @@ async def async_attach_trigger(
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
hass.async_run_job( hass.async_run_job(
action( action,
{ {
"trigger": { "trigger": {
"platform": platform_type, "platform": platform_type,
"entity_id": entity, "entity_id": entity,
"from_state": from_s, "from_state": from_s,
"to_state": to_s, "to_state": to_s,
"for": time_delta if not time_delta else period[entity], "for": time_delta if not time_delta else period[entity],
} }
}, },
context=event.context, event.context,
)
) )
if not time_delta: if not time_delta:

View File

@ -54,18 +54,17 @@ async def async_attach_trigger(
def call_action(*_): def call_action(*_):
"""Call action with right context.""" """Call action with right context."""
hass.async_run_job( hass.async_run_job(
action( action,
{ {
"trigger": { "trigger": {
"platform": "template", "platform": "template",
"entity_id": entity_id, "entity_id": entity_id,
"from_state": from_s, "from_state": from_s,
"to_state": to_s, "to_state": to_s,
"for": time_delta if not time_delta else period, "for": time_delta if not time_delta else period,
} }
}, },
context=(to_s.context if to_s else None), (to_s.context if to_s else None),
)
) )
if not time_delta: if not time_delta:

View File

@ -40,6 +40,8 @@ def async_register_commands(hass, async_reg):
async_reg(hass, handle_manifest_list) async_reg(hass, handle_manifest_list)
async_reg(hass, handle_manifest_get) async_reg(hass, handle_manifest_get)
async_reg(hass, handle_entity_source) async_reg(hass, handle_entity_source)
async_reg(hass, handle_subscribe_trigger)
async_reg(hass, handle_test_condition)
def pong_message(iden): def pong_message(iden):
@ -315,3 +317,69 @@ def handle_entity_source(hass, connection, msg):
sources[entity_id] = source sources[entity_id] = source
connection.send_result(msg["id"], sources) connection.send_result(msg["id"], sources)
@callback
@decorators.websocket_command(
{
vol.Required("type"): "subscribe_trigger",
vol.Required("trigger"): cv.TRIGGER_SCHEMA,
vol.Optional("variables"): dict,
}
)
@decorators.require_admin
@decorators.async_response
async def handle_subscribe_trigger(hass, connection, msg):
"""Handle subscribe trigger command."""
# Circular dep
# pylint: disable=import-outside-toplevel
from homeassistant.helpers import trigger
trigger_config = await trigger.async_validate_trigger_config(hass, msg["trigger"])
@callback
def forward_triggers(variables, context=None):
"""Forward events to websocket."""
connection.send_message(
messages.event_message(
msg["id"], {"variables": variables, "context": context}
)
)
connection.subscriptions[msg["id"]] = (
await trigger.async_initialize_triggers(
hass,
trigger_config,
forward_triggers,
const.DOMAIN,
const.DOMAIN,
connection.logger.log,
variables=msg.get("variables"),
)
) or (
# Some triggers won't return an unsub function. Since the caller expects
# a subscription, we're going to fake one.
lambda: None
)
connection.send_result(msg["id"])
@decorators.websocket_command(
{
vol.Required("type"): "test_condition",
vol.Required("condition"): cv.CONDITION_SCHEMA,
vol.Optional("variables"): dict,
}
)
@decorators.require_admin
@decorators.async_response
async def handle_test_condition(hass, connection, msg):
"""Handle test condition command."""
# Circular dep
# pylint: disable=import-outside-toplevel
from homeassistant.helpers import condition
check_condition = await condition.async_from_config(hass, msg["condition"])
connection.send_result(
msg["id"], {"result": check_condition(hass, msg.get("variables"))}
)

View File

@ -57,19 +57,18 @@ async def async_attach_trigger(hass, config, action, automation_info):
and not to_match and not to_match
): ):
hass.async_run_job( hass.async_run_job(
action( action,
{ {
"trigger": { "trigger": {
"platform": "zone", "platform": "zone",
"entity_id": entity, "entity_id": entity,
"from_state": from_s, "from_state": from_s,
"to_state": to_s, "to_state": to_s,
"zone": zone_state, "zone": zone_state,
"event": event, "event": event,
} }
}, },
context=to_s.context, to_s.context,
)
) )
return async_track_state_change_event(hass, entity_id, zone_automation_listener) return async_track_state_change_event(hass, entity_id, zone_automation_listener)

View File

@ -302,6 +302,9 @@ class HomeAssistant:
target: target to call. target: target to call.
args: parameters for method to call. args: parameters for method to call.
""" """
if target is None:
raise ValueError("Don't call async_add_job with None")
task = None task = None
# Check for partials to properly determine if coroutine function # Check for partials to properly determine if coroutine function

View File

@ -8,7 +8,7 @@ from homeassistant.components.websocket_api.auth import (
TYPE_AUTH_REQUIRED, TYPE_AUTH_REQUIRED,
) )
from homeassistant.components.websocket_api.const import URL from homeassistant.components.websocket_api.const import URL
from homeassistant.core import callback from homeassistant.core import Context, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity from homeassistant.helpers import entity
from homeassistant.loader import async_get_integration from homeassistant.loader import async_get_integration
@ -654,3 +654,81 @@ async def test_entity_source_admin(hass, websocket_client, hass_admin_user):
assert msg["type"] == const.TYPE_RESULT assert msg["type"] == const.TYPE_RESULT
assert not msg["success"] assert not msg["success"]
assert msg["error"]["code"] == const.ERR_UNAUTHORIZED assert msg["error"]["code"] == const.ERR_UNAUTHORIZED
async def test_subscribe_trigger(hass, websocket_client):
"""Test subscribing to a trigger."""
init_count = sum(hass.bus.async_listeners().values())
await websocket_client.send_json(
{
"id": 5,
"type": "subscribe_trigger",
"trigger": {"platform": "event", "event_type": "test_event"},
"variables": {"hello": "world"},
}
)
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
# Verify we have a new listener
assert sum(hass.bus.async_listeners().values()) == init_count + 1
context = Context()
hass.bus.async_fire("ignore_event")
hass.bus.async_fire("test_event", {"hello": "world"}, context=context)
hass.bus.async_fire("ignore_event")
with timeout(3):
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == "event"
assert msg["event"]["context"]["id"] == context.id
assert msg["event"]["variables"]["trigger"]["platform"] == "event"
event = msg["event"]["variables"]["trigger"]["event"]
assert event["event_type"] == "test_event"
assert event["data"] == {"hello": "world"}
assert event["origin"] == "LOCAL"
await websocket_client.send_json(
{"id": 6, "type": "unsubscribe_events", "subscription": 5}
)
msg = await websocket_client.receive_json()
assert msg["id"] == 6
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
# Check our listener got unsubscribed
assert sum(hass.bus.async_listeners().values()) == init_count
async def test_test_condition(hass, websocket_client):
"""Test testing a condition."""
hass.states.async_set("hello.world", "paulus")
await websocket_client.send_json(
{
"id": 5,
"type": "test_condition",
"condition": {
"condition": "state",
"entity_id": "hello.world",
"state": "paulus",
},
"variables": {"hello": "world"},
}
)
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
assert msg["result"]["result"] is True