diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c550aa4f2c7..970653cd4df 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -377,7 +377,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): else: 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. This method is a coroutine. diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 9fc78746a7c..2c247280e06 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -47,10 +47,9 @@ async def async_attach_trigger( return hass.async_run_job( - action( - {"trigger": {"platform": platform_type, "event": event}}, - context=event.context, - ) + action, + {"trigger": {"platform": platform_type, "event": event}}, + event.context, ) return hass.bus.async_listen(event_type, handle_event) diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index 91b67e28c7c..6ccb54d034e 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -30,10 +30,9 @@ async def async_attach_trigger(hass, config, action, automation_info): def hass_shutdown(event): """Execute when Home Assistant is shutting down.""" hass.async_run_job( - action( - {"trigger": {"platform": "homeassistant", "event": event}}, - context=event.context, - ) + action, + {"trigger": {"platform": "homeassistant", "event": event}}, + event.context, ) return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 1429fa7ce7b..b2958f4d63a 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -103,20 +103,19 @@ async def async_attach_trigger( def call_action(): """Call action with right context.""" hass.async_run_job( - action( - { - "trigger": { - "platform": platform_type, - "entity_id": entity, - "below": below, - "above": above, - "from_state": from_s, - "to_state": to_s, - "for": time_delta if not time_delta else period[entity], - } - }, - context=to_s.context, - ) + action, + { + "trigger": { + "platform": platform_type, + "entity_id": entity, + "below": below, + "above": above, + "from_state": from_s, + "to_state": to_s, + "for": time_delta if not time_delta else period[entity], + } + }, + to_s.context, ) matching = check_numeric_state(entity, from_s, to_s) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 603fff5993e..9ddd1b3fcb8 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -83,18 +83,17 @@ async def async_attach_trigger( def call_action(): """Call action with right context.""" hass.async_run_job( - action( - { - "trigger": { - "platform": platform_type, - "entity_id": entity, - "from_state": from_s, - "to_state": to_s, - "for": time_delta if not time_delta else period[entity], - } - }, - context=event.context, - ) + action, + { + "trigger": { + "platform": platform_type, + "entity_id": entity, + "from_state": from_s, + "to_state": to_s, + "for": time_delta if not time_delta else period[entity], + } + }, + event.context, ) if not time_delta: diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 032ffa813f5..b6e6c974807 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -54,18 +54,17 @@ async def async_attach_trigger( def call_action(*_): """Call action with right context.""" hass.async_run_job( - action( - { - "trigger": { - "platform": "template", - "entity_id": entity_id, - "from_state": from_s, - "to_state": to_s, - "for": time_delta if not time_delta else period, - } - }, - context=(to_s.context if to_s else None), - ) + action, + { + "trigger": { + "platform": "template", + "entity_id": entity_id, + "from_state": from_s, + "to_state": to_s, + "for": time_delta if not time_delta else period, + } + }, + (to_s.context if to_s else None), ) if not time_delta: diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index b44ca45ce03..4ed0292a9f4 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -40,6 +40,8 @@ def async_register_commands(hass, async_reg): async_reg(hass, handle_manifest_list) async_reg(hass, handle_manifest_get) async_reg(hass, handle_entity_source) + async_reg(hass, handle_subscribe_trigger) + async_reg(hass, handle_test_condition) def pong_message(iden): @@ -315,3 +317,69 @@ def handle_entity_source(hass, connection, msg): sources[entity_id] = source 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"))} + ) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 3b794f698a1..f53af0e5d7f 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -57,19 +57,18 @@ async def async_attach_trigger(hass, config, action, automation_info): and not to_match ): hass.async_run_job( - action( - { - "trigger": { - "platform": "zone", - "entity_id": entity, - "from_state": from_s, - "to_state": to_s, - "zone": zone_state, - "event": event, - } - }, - context=to_s.context, - ) + action, + { + "trigger": { + "platform": "zone", + "entity_id": entity, + "from_state": from_s, + "to_state": to_s, + "zone": zone_state, + "event": event, + } + }, + to_s.context, ) return async_track_state_change_event(hass, entity_id, zone_automation_listener) diff --git a/homeassistant/core.py b/homeassistant/core.py index 31e132eec1a..c5a54f0374f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -302,6 +302,9 @@ class HomeAssistant: target: target to call. args: parameters for method to call. """ + if target is None: + raise ValueError("Don't call async_add_job with None") + task = None # Check for partials to properly determine if coroutine function diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 5e35f2de04f..4113a833872 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -8,7 +8,7 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH_REQUIRED, ) 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.helpers import entity 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 not msg["success"] 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