Webhook names (#18206)

* Add new automation_info param to async_trigger

* Add domain and name to webhook registration and add WS command
This commit is contained in:
Paulus Schoutsen 2018-11-05 09:23:58 +01:00 committed by Pascal Vizeli
parent 6e4ce35a69
commit 2e9132873a
20 changed files with 119 additions and 54 deletions

View file

@ -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)

View file

@ -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(

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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'
}
]

View file

@ -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'