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:
parent
6e4ce35a69
commit
2e9132873a
20 changed files with 119 additions and 54 deletions
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Reference in a new issue