diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index a1f1563f5e1..f8563071fbc 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -400,6 +400,9 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): This method is a coroutine. """ removes = [] + info = { + 'name': name + } for conf in trigger_configs: platform = await async_prepare_setup_platform( @@ -408,7 +411,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): if platform is None: return None - remove = await platform.async_trigger(hass, conf, action) + remove = await platform.async_trigger(hass, conf, action, info) if not remove: _LOGGER.error("Error setting up trigger %s", name) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index a9605f343fd..ec47479eac8 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -24,7 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) event_data_schema = vol.Schema( diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index b2c9a9c093a..537646fefc1 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -33,7 +33,7 @@ def source_match(state, source): return state and state.attributes.get('source') == source -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" source = config.get(CONF_SOURCE).lower() zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index 30ab979d6f4..6d7a44291c9 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -22,7 +22,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index c0d2dd99ba2..70e01174078 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -32,7 +32,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 99d5ab8674c..67c538154e5 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -24,7 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" topic = config.get(CONF_TOPIC) payload = config.get(CONF_PAYLOAD) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 675b6f3653a..aa51e631026 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -28,7 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({ _LOGGER = logging.getLogger(__name__) -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 46c5cafa071..4e47026d8d1 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -26,7 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({ }), cv.key_dependency(CONF_FOR, CONF_TO)) -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 7cefe6953a1..509195689a1 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -24,7 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index c0d83b1067f..347b3f94e7d 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -22,7 +22,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({ }) -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = hass diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index eccc31581a0..116bfbdbc97 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -28,7 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({ }), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT)) -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" if CONF_AT in config: at_time = config.get(CONF_AT) diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index 345b0fe3249..f4afc8a601a 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -14,6 +14,8 @@ from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID import homeassistant.helpers.config_validation as cv +from . import DOMAIN as AUTOMATION_DOMAIN + DEPENDENCIES = ('webhook',) _LOGGER = logging.getLogger(__name__) @@ -39,10 +41,11 @@ async def _handle_webhook(action, hass, webhook_id, request): hass.async_run_job(action, {'trigger': result}) -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Trigger based on incoming webhooks.""" webhook_id = config.get(CONF_WEBHOOK_ID) hass.components.webhook.async_register( + AUTOMATION_DOMAIN, automation_info['name'], webhook_id, partial(_handle_webhook, action)) @callback diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index dfc9cc418bf..0c3c0941a9e 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -26,7 +26,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -async def async_trigger(hass, config, action): +async def async_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index 900dae5c7c1..3f3fbe7c14e 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -76,7 +76,7 @@ async def handle_webhook(hass, webhook_id, request): async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( - entry.data[CONF_WEBHOOK_ID], handle_webhook) + DOMAIN, 'DialogFlow', entry.data[CONF_WEBHOOK_ID], handle_webhook) return True diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 85ee6b9fa1c..209bbcef607 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -88,7 +88,7 @@ async def handle_webhook(hass, webhook_id, request): async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( - entry.data[CONF_WEBHOOK_ID], handle_webhook) + DOMAIN, 'IFTTT', entry.data[CONF_WEBHOOK_ID], handle_webhook) return True diff --git a/homeassistant/components/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py index e78dc0aa479..7fa08bb0f22 100644 --- a/homeassistant/components/mailgun/__init__.py +++ b/homeassistant/components/mailgun/__init__.py @@ -81,7 +81,7 @@ async def verify_webhook(hass, token=None, timestamp=None, signature=None): async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( - entry.data[CONF_WEBHOOK_ID], handle_webhook) + DOMAIN, 'Mailgun', entry.data[CONF_WEBHOOK_ID], handle_webhook) return True diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index c28f56a4b6c..9fcba4da817 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -56,7 +56,7 @@ async def handle_webhook(hass, webhook_id, request): async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( - entry.data[CONF_WEBHOOK_ID], handle_webhook) + DOMAIN, 'Twilio', entry.data[CONF_WEBHOOK_ID], handle_webhook) return True diff --git a/homeassistant/components/webhook.py b/homeassistant/components/webhook.py index 2a4c3f973f2..ad23ba6f544 100644 --- a/homeassistant/components/webhook.py +++ b/homeassistant/components/webhook.py @@ -6,10 +6,12 @@ https://home-assistant.io/components/webhook/ import logging from aiohttp.web import Response +import voluptuous as vol from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.auth.util import generate_secret +from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView DOMAIN = 'webhook' @@ -17,16 +19,26 @@ DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) +WS_TYPE_LIST = 'webhook/list' +SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_LIST, +}) + + @callback @bind_hass -def async_register(hass, webhook_id, handler): +def async_register(hass, domain, name, webhook_id, handler): """Register a webhook.""" handlers = hass.data.setdefault(DOMAIN, {}) if webhook_id in handlers: raise ValueError('Handler is already defined!') - handlers[webhook_id] = handler + handlers[webhook_id] = { + 'domain': domain, + 'name': name, + 'handler': handler + } @callback @@ -53,6 +65,10 @@ def async_generate_url(hass, webhook_id): async def async_setup(hass, config): """Initialize the webhook component.""" hass.http.register_view(WebhookView) + hass.components.websocket_api.async_register_command( + WS_TYPE_LIST, websocket_list, + SCHEMA_WS_LIST + ) return True @@ -67,19 +83,33 @@ class WebhookView(HomeAssistantView): """Handle webhook call.""" hass = request.app['hass'] handlers = hass.data.setdefault(DOMAIN, {}) - handler = handlers.get(webhook_id) + webhook = handlers.get(webhook_id) # Always respond successfully to not give away if a hook exists or not. - if handler is None: + if webhook is None: _LOGGER.warning( 'Received message for unregistered webhook %s', webhook_id) return Response(status=200) try: - response = await handler(hass, webhook_id, request) + response = await webhook['handler'](hass, webhook_id, request) if response is None: response = Response(status=200) return response except Exception: # pylint: disable=broad-except _LOGGER.exception("Error processing webhook %s", webhook_id) return Response(status=200) + + +@callback +def websocket_list(hass, connection, msg): + """Return a list of webhooks.""" + handlers = hass.data.setdefault(DOMAIN, {}) + result = [{ + 'webhook_id': webhook_id, + 'domain': info['domain'], + 'name': info['name'], + } for webhook_id, info in handlers.items()] + + connection.send_message( + websocket_api.result_message(msg['id'], result)) diff --git a/tests/components/test_webhook.py b/tests/components/test_webhook.py index 9434c3d98d5..c16fef3e059 100644 --- a/tests/components/test_webhook.py +++ b/tests/components/test_webhook.py @@ -22,7 +22,8 @@ async def test_unregistering_webhook(hass, mock_client): """Handle webhook.""" hooks.append(args) - hass.components.webhook.async_register(webhook_id, handle) + hass.components.webhook.async_register( + 'test', "Test hook", webhook_id, handle) resp = await mock_client.post('/api/webhook/{}'.format(webhook_id)) assert resp.status == 200 @@ -51,7 +52,7 @@ async def test_posting_webhook_nonexisting(hass, mock_client): async def test_posting_webhook_invalid_json(hass, mock_client): """Test posting to a nonexisting webhook.""" - hass.components.webhook.async_register('hello', None) + hass.components.webhook.async_register('test', "Test hook", 'hello', None) resp = await mock_client.post('/api/webhook/hello', data='not-json') assert resp.status == 200 @@ -65,7 +66,8 @@ async def test_posting_webhook_json(hass, mock_client): """Handle webhook.""" hooks.append((args[0], args[1], await args[2].text())) - hass.components.webhook.async_register(webhook_id, handle) + hass.components.webhook.async_register( + 'test', "Test hook", webhook_id, handle) resp = await mock_client.post('/api/webhook/{}'.format(webhook_id), json={ 'data': True @@ -86,7 +88,8 @@ async def test_posting_webhook_no_data(hass, mock_client): """Handle webhook.""" hooks.append(args) - hass.components.webhook.async_register(webhook_id, handle) + hass.components.webhook.async_register( + 'test', "Test hook", webhook_id, handle) resp = await mock_client.post('/api/webhook/{}'.format(webhook_id)) assert resp.status == 200 @@ -94,3 +97,28 @@ async def test_posting_webhook_no_data(hass, mock_client): assert hooks[0][0] is hass assert hooks[0][1] == webhook_id assert await hooks[0][2].text() == '' + + +async def test_listing_webhook(hass, hass_ws_client, hass_access_token): + """Test unregistering a webhook.""" + assert await async_setup_component(hass, 'webhook', {}) + client = await hass_ws_client(hass, hass_access_token) + + hass.components.webhook.async_register( + 'test', "Test hook", "my-id", None) + + await client.send_json({ + 'id': 5, + 'type': 'webhook/list', + }) + + msg = await client.receive_json() + assert msg['id'] == 5 + assert msg['success'] + assert msg['result'] == [ + { + 'webhook_id': 'my-id', + 'domain': 'test', + 'name': 'Test hook' + } + ] diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index c740783f4c0..3be211532ed 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -7,35 +7,36 @@ from homeassistant.core import callback from tests.common import MockDependency -@MockDependency('twilio', 'rest') -@MockDependency('twilio', 'twiml') async def test_config_flow_registers_webhook(hass, aiohttp_client): """Test setting up Twilio and sending webhook.""" - with patch('homeassistant.util.get_local_ip', return_value='example.com'): - result = await hass.config_entries.flow.async_init('twilio', context={ - 'source': 'user' + with MockDependency('twilio', 'rest'), MockDependency('twilio', 'twiml'): + with patch('homeassistant.util.get_local_ip', + return_value='example.com'): + result = await hass.config_entries.flow.async_init( + 'twilio', context={ + 'source': 'user' + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM, result + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], {}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + webhook_id = result['result'].data['webhook_id'] + + twilio_events = [] + + @callback + def handle_event(event): + """Handle Twilio event.""" + twilio_events.append(event) + + hass.bus.async_listen(twilio.RECEIVED_DATA, handle_event) + + client = await aiohttp_client(hass.http.app) + await client.post('/api/webhook/{}'.format(webhook_id), data={ + 'hello': 'twilio' }) - assert result['type'] == data_entry_flow.RESULT_TYPE_FORM, result - result = await hass.config_entries.flow.async_configure( - result['flow_id'], {}) - assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - webhook_id = result['result'].data['webhook_id'] - - twilio_events = [] - - @callback - def handle_event(event): - """Handle Twilio event.""" - twilio_events.append(event) - - hass.bus.async_listen(twilio.RECEIVED_DATA, handle_event) - - client = await aiohttp_client(hass.http.app) - await client.post('/api/webhook/{}'.format(webhook_id), data={ - 'hello': 'twilio' - }) - - assert len(twilio_events) == 1 - assert twilio_events[0].data['webhook_id'] == webhook_id - assert twilio_events[0].data['hello'] == 'twilio' + assert len(twilio_events) == 1 + assert twilio_events[0].data['webhook_id'] == webhook_id + assert twilio_events[0].data['hello'] == 'twilio'