mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add webhook + IFTTT example (#16817)
* Add webhook + IFTTT example * Abort if not externally accessible * Abort on local url * Add description to create entry * Make body optional * Allow ifttt setup without config * Add tests * Lint * Fix Lint + Tests * Fix typing
This commit is contained in:
parent
06d959ed43
commit
f5632a5da5
@ -1,24 +1,13 @@
|
|||||||
"""Helpers to resolve client ID/secret."""
|
"""Helpers to resolve client ID/secret."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from ipaddress import ip_address
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from ipaddress import ip_address, ip_network
|
|
||||||
from urllib.parse import urlparse, urljoin
|
from urllib.parse import urlparse, urljoin
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp.client_exceptions import ClientError
|
from aiohttp.client_exceptions import ClientError
|
||||||
|
|
||||||
# IP addresses of loopback interfaces
|
from homeassistant.util.network import is_local
|
||||||
ALLOWED_IPS = (
|
|
||||||
ip_address('127.0.0.1'),
|
|
||||||
ip_address('::1'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# RFC1918 - Address allocation for Private Internets
|
|
||||||
ALLOWED_NETWORKS = (
|
|
||||||
ip_network('10.0.0.0/8'),
|
|
||||||
ip_network('172.16.0.0/12'),
|
|
||||||
ip_network('192.168.0.0/16'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def verify_redirect_uri(hass, client_id, redirect_uri):
|
async def verify_redirect_uri(hass, client_id, redirect_uri):
|
||||||
@ -185,9 +174,7 @@ def _parse_client_id(client_id):
|
|||||||
# Not an ip address
|
# Not an ip address
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if (address is None or
|
if address is None or is_local(address):
|
||||||
address in ALLOWED_IPS or
|
|
||||||
any(address in network for network in ALLOWED_NETWORKS)):
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
raise ValueError('Hostname should be a domain name or local IP address')
|
raise ValueError('Hostname should be a domain name or local IP address')
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
"""
|
|
||||||
Support to trigger Maker IFTTT recipes.
|
|
||||||
|
|
||||||
For more details about this component, please refer to the documentation at
|
|
||||||
https://home-assistant.io/components/ifttt/
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
|
|
||||||
REQUIREMENTS = ['pyfttt==0.3']
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
ATTR_EVENT = 'event'
|
|
||||||
ATTR_VALUE1 = 'value1'
|
|
||||||
ATTR_VALUE2 = 'value2'
|
|
||||||
ATTR_VALUE3 = 'value3'
|
|
||||||
|
|
||||||
CONF_KEY = 'key'
|
|
||||||
|
|
||||||
DOMAIN = 'ifttt'
|
|
||||||
|
|
||||||
SERVICE_TRIGGER = 'trigger'
|
|
||||||
|
|
||||||
SERVICE_TRIGGER_SCHEMA = vol.Schema({
|
|
||||||
vol.Required(ATTR_EVENT): cv.string,
|
|
||||||
vol.Optional(ATTR_VALUE1): cv.string,
|
|
||||||
vol.Optional(ATTR_VALUE2): cv.string,
|
|
||||||
vol.Optional(ATTR_VALUE3): cv.string,
|
|
||||||
})
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
|
||||||
DOMAIN: vol.Schema({
|
|
||||||
vol.Required(CONF_KEY): cv.string,
|
|
||||||
}),
|
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
|
||||||
|
|
||||||
|
|
||||||
def trigger(hass, event, value1=None, value2=None, value3=None):
|
|
||||||
"""Trigger a Maker IFTTT recipe."""
|
|
||||||
data = {
|
|
||||||
ATTR_EVENT: event,
|
|
||||||
ATTR_VALUE1: value1,
|
|
||||||
ATTR_VALUE2: value2,
|
|
||||||
ATTR_VALUE3: value3,
|
|
||||||
}
|
|
||||||
hass.services.call(DOMAIN, SERVICE_TRIGGER, data)
|
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
|
||||||
"""Set up the IFTTT service component."""
|
|
||||||
key = config[DOMAIN][CONF_KEY]
|
|
||||||
|
|
||||||
def trigger_service(call):
|
|
||||||
"""Handle IFTTT trigger service calls."""
|
|
||||||
event = call.data[ATTR_EVENT]
|
|
||||||
value1 = call.data.get(ATTR_VALUE1)
|
|
||||||
value2 = call.data.get(ATTR_VALUE2)
|
|
||||||
value3 = call.data.get(ATTR_VALUE3)
|
|
||||||
|
|
||||||
try:
|
|
||||||
import pyfttt
|
|
||||||
pyfttt.send_event(key, event, value1, value2, value3)
|
|
||||||
except requests.exceptions.RequestException:
|
|
||||||
_LOGGER.exception("Error communicating with IFTTT")
|
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service,
|
|
||||||
schema=SERVICE_TRIGGER_SCHEMA)
|
|
||||||
|
|
||||||
return True
|
|
18
homeassistant/components/ifttt/.translations/en.json
Normal file
18
homeassistant/components/ifttt/.translations/en.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages.",
|
||||||
|
"one_instance_allowed": "Only a single instance is necessary."
|
||||||
|
},
|
||||||
|
"create_entry": {
|
||||||
|
"default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"description": "Are you sure you want to set up IFTTT?",
|
||||||
|
"title": "Set up the IFTTT Webhook Applet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "IFTTT"
|
||||||
|
}
|
||||||
|
}
|
135
homeassistant/components/ifttt/__init__.py
Normal file
135
homeassistant/components/ifttt/__init__.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
"""
|
||||||
|
Support to trigger Maker IFTTT recipes.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/ifttt/
|
||||||
|
"""
|
||||||
|
from ipaddress import ip_address
|
||||||
|
import logging
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.util.network import is_local
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pyfttt==0.3']
|
||||||
|
DEPENDENCIES = ['webhook']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
EVENT_RECEIVED = 'ifttt_webhook_received'
|
||||||
|
|
||||||
|
ATTR_EVENT = 'event'
|
||||||
|
ATTR_VALUE1 = 'value1'
|
||||||
|
ATTR_VALUE2 = 'value2'
|
||||||
|
ATTR_VALUE3 = 'value3'
|
||||||
|
|
||||||
|
CONF_KEY = 'key'
|
||||||
|
CONF_WEBHOOK_ID = 'webhook_id'
|
||||||
|
|
||||||
|
DOMAIN = 'ifttt'
|
||||||
|
|
||||||
|
SERVICE_TRIGGER = 'trigger'
|
||||||
|
|
||||||
|
SERVICE_TRIGGER_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(ATTR_EVENT): cv.string,
|
||||||
|
vol.Optional(ATTR_VALUE1): cv.string,
|
||||||
|
vol.Optional(ATTR_VALUE2): cv.string,
|
||||||
|
vol.Optional(ATTR_VALUE3): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(DOMAIN): vol.Schema({
|
||||||
|
vol.Required(CONF_KEY): cv.string,
|
||||||
|
}),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass, config):
|
||||||
|
"""Set up the IFTTT service component."""
|
||||||
|
if DOMAIN not in config:
|
||||||
|
return True
|
||||||
|
|
||||||
|
key = config[DOMAIN][CONF_KEY]
|
||||||
|
|
||||||
|
def trigger_service(call):
|
||||||
|
"""Handle IFTTT trigger service calls."""
|
||||||
|
event = call.data[ATTR_EVENT]
|
||||||
|
value1 = call.data.get(ATTR_VALUE1)
|
||||||
|
value2 = call.data.get(ATTR_VALUE2)
|
||||||
|
value3 = call.data.get(ATTR_VALUE3)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyfttt
|
||||||
|
pyfttt.send_event(key, event, value1, value2, value3)
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
_LOGGER.exception("Error communicating with IFTTT")
|
||||||
|
|
||||||
|
hass.services.async_register(DOMAIN, SERVICE_TRIGGER, trigger_service,
|
||||||
|
schema=SERVICE_TRIGGER_SCHEMA)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_webhook(hass, webhook_id, data):
|
||||||
|
"""Handle webhook callback."""
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data['webhook_id'] = webhook_id
|
||||||
|
hass.bus.async_fire(EVENT_RECEIVED, data)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Configure based on config entry."""
|
||||||
|
hass.components.webhook.async_register(
|
||||||
|
entry.data['webhook_id'], handle_webhook)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
hass.components.webhook.async_unregister(entry.data['webhook_id'])
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@config_entries.HANDLERS.register(DOMAIN)
|
||||||
|
class ConfigFlow(config_entries.ConfigFlow):
|
||||||
|
"""Handle an IFTTT config flow."""
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a user initiated set up flow."""
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(reason='one_instance_allowed')
|
||||||
|
|
||||||
|
try:
|
||||||
|
url_parts = urlparse(self.hass.config.api.base_url)
|
||||||
|
|
||||||
|
if is_local(ip_address(url_parts.hostname)):
|
||||||
|
return self.async_abort(reason='not_internet_accessible')
|
||||||
|
except ValueError:
|
||||||
|
# If it's not an IP address, it's very likely publicly accessible
|
||||||
|
pass
|
||||||
|
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='user',
|
||||||
|
)
|
||||||
|
|
||||||
|
webhook_id = self.hass.components.webhook.async_generate_id()
|
||||||
|
webhook_url = \
|
||||||
|
self.hass.components.webhook.async_generate_url(webhook_id)
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title='IFTTT Webhook',
|
||||||
|
data={
|
||||||
|
CONF_WEBHOOK_ID: webhook_id
|
||||||
|
},
|
||||||
|
description_placeholders={
|
||||||
|
'applet_url': 'https://ifttt.com/maker_webhooks',
|
||||||
|
'webhook_url': webhook_url,
|
||||||
|
'docs_url':
|
||||||
|
'https://www.home-assistant.io/components/ifttt/'
|
||||||
|
}
|
||||||
|
)
|
18
homeassistant/components/ifttt/strings.json
Normal file
18
homeassistant/components/ifttt/strings.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "IFTTT",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Set up the IFTTT Webhook Applet",
|
||||||
|
"description": "Are you sure you want to set up IFTTT?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"one_instance_allowed": "Only a single instance is necessary.",
|
||||||
|
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages."
|
||||||
|
},
|
||||||
|
"create_entry": {
|
||||||
|
"default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
homeassistant/components/webhook.py
Normal file
94
homeassistant/components/webhook.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
"""Webhooks for Home Assistant.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/webhook/
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiohttp.web import Response
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.loader import bind_hass
|
||||||
|
from homeassistant.auth.util import generate_secret
|
||||||
|
from homeassistant.components.http.view import HomeAssistantView
|
||||||
|
|
||||||
|
DOMAIN = 'webhook'
|
||||||
|
DEPENDENCIES = ['http']
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@bind_hass
|
||||||
|
def async_register(hass, 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
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@bind_hass
|
||||||
|
def async_unregister(hass, webhook_id):
|
||||||
|
"""Remove a webhook."""
|
||||||
|
handlers = hass.data.setdefault(DOMAIN, {})
|
||||||
|
handlers.pop(webhook_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_generate_id():
|
||||||
|
"""Generate a webhook_id."""
|
||||||
|
return generate_secret(entropy=32)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@bind_hass
|
||||||
|
def async_generate_url(hass, webhook_id):
|
||||||
|
"""Generate a webhook_id."""
|
||||||
|
return "{}/api/webhook/{}".format(hass.config.api.base_url, webhook_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass, config):
|
||||||
|
"""Initialize the webhook component."""
|
||||||
|
hass.http.register_view(WebhookView)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookView(HomeAssistantView):
|
||||||
|
"""Handle incoming webhook requests."""
|
||||||
|
|
||||||
|
url = "/api/webhook/{webhook_id}"
|
||||||
|
name = "api:webhook"
|
||||||
|
requires_auth = False
|
||||||
|
|
||||||
|
async def post(self, request, webhook_id):
|
||||||
|
"""Handle webhook call."""
|
||||||
|
hass = request.app['hass']
|
||||||
|
handlers = hass.data.setdefault(DOMAIN, {})
|
||||||
|
handler = handlers.get(webhook_id)
|
||||||
|
|
||||||
|
# Always respond successfully to not give away if a hook exists or not.
|
||||||
|
if handler is None:
|
||||||
|
_LOGGER.warning(
|
||||||
|
'Received message for unregistered webhook %s', webhook_id)
|
||||||
|
return Response(status=200)
|
||||||
|
|
||||||
|
body = await request.text()
|
||||||
|
try:
|
||||||
|
data = json.loads(body) if body else {}
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
'Received webhook %s with invalid JSON', webhook_id)
|
||||||
|
return Response(status=200)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await handler(hass, webhook_id, data)
|
||||||
|
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)
|
@ -141,6 +141,7 @@ FLOWS = [
|
|||||||
'deconz',
|
'deconz',
|
||||||
'homematicip_cloud',
|
'homematicip_cloud',
|
||||||
'hue',
|
'hue',
|
||||||
|
'ifttt',
|
||||||
'ios',
|
'ios',
|
||||||
'mqtt',
|
'mqtt',
|
||||||
'nest',
|
'nest',
|
||||||
|
@ -153,7 +153,10 @@ class FlowHandler:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_create_entry(self, *, title: str, data: Dict) -> Dict:
|
def async_create_entry(self, *, title: str, data: Dict,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
description_placeholders: Optional[Dict] = None) \
|
||||||
|
-> Dict:
|
||||||
"""Finish config flow and create a config entry."""
|
"""Finish config flow and create a config entry."""
|
||||||
return {
|
return {
|
||||||
'version': self.VERSION,
|
'version': self.VERSION,
|
||||||
@ -162,6 +165,8 @@ class FlowHandler:
|
|||||||
'handler': self.handler,
|
'handler': self.handler,
|
||||||
'title': title,
|
'title': title,
|
||||||
'data': data,
|
'data': data,
|
||||||
|
'description': description,
|
||||||
|
'description_placeholders': description_placeholders,
|
||||||
}
|
}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
22
homeassistant/util/network.py
Normal file
22
homeassistant/util/network.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""Network utilities."""
|
||||||
|
from ipaddress import IPv4Address, IPv6Address, ip_address, ip_network
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
# IP addresses of loopback interfaces
|
||||||
|
LOCAL_IPS = (
|
||||||
|
ip_address('127.0.0.1'),
|
||||||
|
ip_address('::1'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# RFC1918 - Address allocation for Private Internets
|
||||||
|
LOCAL_NETWORKS = (
|
||||||
|
ip_network('10.0.0.0/8'),
|
||||||
|
ip_network('172.16.0.0/12'),
|
||||||
|
ip_network('192.168.0.0/16'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_local(address: Union[IPv4Address, IPv6Address]) -> bool:
|
||||||
|
"""Check if an address is local."""
|
||||||
|
return address in LOCAL_IPS or \
|
||||||
|
any(address in network for network in LOCAL_NETWORKS)
|
@ -206,6 +206,8 @@ def test_create_account(hass, client):
|
|||||||
'title': 'Test Entry',
|
'title': 'Test Entry',
|
||||||
'type': 'create_entry',
|
'type': 'create_entry',
|
||||||
'version': 1,
|
'version': 1,
|
||||||
|
'description': None,
|
||||||
|
'description_placeholders': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -266,6 +268,8 @@ def test_two_step_flow(hass, client):
|
|||||||
'type': 'create_entry',
|
'type': 'create_entry',
|
||||||
'title': 'user-title',
|
'title': 'user-title',
|
||||||
'version': 1,
|
'version': 1,
|
||||||
|
'description': None,
|
||||||
|
'description_placeholders': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
1
tests/components/ifttt/__init__.py
Normal file
1
tests/components/ifttt/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the IFTTT component."""
|
48
tests/components/ifttt/test_init.py
Normal file
48
tests/components/ifttt/test_init.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"""Test the init file of IFTTT."""
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.components import ifttt
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_flow_registers_webhook(hass, aiohttp_client):
|
||||||
|
"""Test setting up IFTTT and sending webhook."""
|
||||||
|
with patch('homeassistant.util.get_local_ip', return_value='example.com'):
|
||||||
|
result = await hass.config_entries.flow.async_init('ifttt', 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']
|
||||||
|
|
||||||
|
ifttt_events = []
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def handle_event(event):
|
||||||
|
"""Handle IFTTT event."""
|
||||||
|
ifttt_events.append(event)
|
||||||
|
|
||||||
|
hass.bus.async_listen(ifttt.EVENT_RECEIVED, handle_event)
|
||||||
|
|
||||||
|
client = await aiohttp_client(hass.http.app)
|
||||||
|
await client.post('/api/webhook/{}'.format(webhook_id), json={
|
||||||
|
'hello': 'ifttt'
|
||||||
|
})
|
||||||
|
|
||||||
|
assert len(ifttt_events) == 1
|
||||||
|
assert ifttt_events[0].data['webhook_id'] == webhook_id
|
||||||
|
assert ifttt_events[0].data['hello'] == 'ifttt'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_flow_aborts_external_url(hass, aiohttp_client):
|
||||||
|
"""Test setting up IFTTT and sending webhook."""
|
||||||
|
hass.config.api = Mock(base_url='http://192.168.1.10')
|
||||||
|
result = await hass.config_entries.flow.async_init('ifttt', context={
|
||||||
|
'source': 'user'
|
||||||
|
})
|
||||||
|
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result['reason'] == 'not_internet_accessible'
|
98
tests/components/test_webhook.py
Normal file
98
tests/components/test_webhook.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
"""Test the webhook component."""
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_client(hass, aiohttp_client):
|
||||||
|
"""Create http client for webhooks."""
|
||||||
|
hass.loop.run_until_complete(async_setup_component(hass, 'webhook', {}))
|
||||||
|
return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unregistering_webhook(hass, mock_client):
|
||||||
|
"""Test unregistering a webhook."""
|
||||||
|
hooks = []
|
||||||
|
webhook_id = hass.components.webhook.async_generate_id()
|
||||||
|
|
||||||
|
async def handle(*args):
|
||||||
|
"""Handle webhook."""
|
||||||
|
hooks.append(args)
|
||||||
|
|
||||||
|
hass.components.webhook.async_register(webhook_id, handle)
|
||||||
|
|
||||||
|
resp = await mock_client.post('/api/webhook/{}'.format(webhook_id))
|
||||||
|
assert resp.status == 200
|
||||||
|
assert len(hooks) == 1
|
||||||
|
|
||||||
|
hass.components.webhook.async_unregister(webhook_id)
|
||||||
|
|
||||||
|
resp = await mock_client.post('/api/webhook/{}'.format(webhook_id))
|
||||||
|
assert resp.status == 200
|
||||||
|
assert len(hooks) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_generate_webhook_url(hass):
|
||||||
|
"""Test we generate a webhook url correctly."""
|
||||||
|
hass.config.api = Mock(base_url='https://example.com')
|
||||||
|
url = hass.components.webhook.async_generate_url('some_id')
|
||||||
|
|
||||||
|
assert url == 'https://example.com/api/webhook/some_id'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_posting_webhook_nonexisting(hass, mock_client):
|
||||||
|
"""Test posting to a nonexisting webhook."""
|
||||||
|
resp = await mock_client.post('/api/webhook/non-existing')
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
async def test_posting_webhook_invalid_json(hass, mock_client):
|
||||||
|
"""Test posting to a nonexisting webhook."""
|
||||||
|
hass.components.webhook.async_register('hello', None)
|
||||||
|
resp = await mock_client.post('/api/webhook/hello', data='not-json')
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
async def test_posting_webhook_json(hass, mock_client):
|
||||||
|
"""Test posting a webhook with JSON data."""
|
||||||
|
hooks = []
|
||||||
|
webhook_id = hass.components.webhook.async_generate_id()
|
||||||
|
|
||||||
|
async def handle(*args):
|
||||||
|
"""Handle webhook."""
|
||||||
|
hooks.append(args)
|
||||||
|
|
||||||
|
hass.components.webhook.async_register(webhook_id, handle)
|
||||||
|
|
||||||
|
resp = await mock_client.post('/api/webhook/{}'.format(webhook_id), json={
|
||||||
|
'data': True
|
||||||
|
})
|
||||||
|
assert resp.status == 200
|
||||||
|
assert len(hooks) == 1
|
||||||
|
assert hooks[0][0] is hass
|
||||||
|
assert hooks[0][1] == webhook_id
|
||||||
|
assert hooks[0][2] == {
|
||||||
|
'data': True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_posting_webhook_no_data(hass, mock_client):
|
||||||
|
"""Test posting a webhook with no data."""
|
||||||
|
hooks = []
|
||||||
|
webhook_id = hass.components.webhook.async_generate_id()
|
||||||
|
|
||||||
|
async def handle(*args):
|
||||||
|
"""Handle webhook."""
|
||||||
|
hooks.append(args)
|
||||||
|
|
||||||
|
hass.components.webhook.async_register(webhook_id, handle)
|
||||||
|
|
||||||
|
resp = await mock_client.post('/api/webhook/{}'.format(webhook_id))
|
||||||
|
assert resp.status == 200
|
||||||
|
assert len(hooks) == 1
|
||||||
|
assert hooks[0][0] is hass
|
||||||
|
assert hooks[0][1] == webhook_id
|
||||||
|
assert hooks[0][2] == {}
|
Loading…
x
Reference in New Issue
Block a user