From 5024a80d61e9307fab55e034a22bcf97deb6bc42 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Thu, 25 Oct 2018 00:46:22 -0700 Subject: [PATCH] Migrate twilio webhooks to the webhook component (#17715) * Migrate twilio webhooks to the webhook component * Fix typos in twilio * Mock out twilio in the tests * Lint * Fix regression in twilio response --- .coveragerc | 1 - homeassistant/components/twilio.py | 58 -------------- .../components/twilio/.translations/en.json | 18 +++++ homeassistant/components/twilio/__init__.py | 76 +++++++++++++++++++ homeassistant/components/twilio/strings.json | 18 +++++ homeassistant/config_entries.py | 1 + tests/components/twilio/__init__.py | 1 + tests/components/twilio/test_init.py | 41 ++++++++++ 8 files changed, 155 insertions(+), 59 deletions(-) delete mode 100644 homeassistant/components/twilio.py create mode 100644 homeassistant/components/twilio/.translations/en.json create mode 100644 homeassistant/components/twilio/__init__.py create mode 100644 homeassistant/components/twilio/strings.json create mode 100644 tests/components/twilio/__init__.py create mode 100644 tests/components/twilio/test_init.py diff --git a/.coveragerc b/.coveragerc index 1da53f21cb1..599e155f8f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -333,7 +333,6 @@ omit = homeassistant/components/tradfri.py homeassistant/components/*/tradfri.py - homeassistant/components/twilio.py homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twilio_call.py diff --git a/homeassistant/components/twilio.py b/homeassistant/components/twilio.py deleted file mode 100644 index 9f9767e4675..00000000000 --- a/homeassistant/components/twilio.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Support for Twilio. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/twilio/ -""" -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView - -REQUIREMENTS = ['twilio==6.19.1'] - -DOMAIN = 'twilio' - -API_PATH = '/api/{}'.format(DOMAIN) - -CONF_ACCOUNT_SID = 'account_sid' -CONF_AUTH_TOKEN = 'auth_token' - -DATA_TWILIO = DOMAIN -DEPENDENCIES = ['http'] - -RECEIVED_DATA = '{}_data_received'.format(DOMAIN) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_ACCOUNT_SID): cv.string, - vol.Required(CONF_AUTH_TOKEN): cv.string - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the Twilio component.""" - from twilio.rest import TwilioRestClient - conf = config[DOMAIN] - hass.data[DATA_TWILIO] = TwilioRestClient( - conf.get(CONF_ACCOUNT_SID), conf.get(CONF_AUTH_TOKEN)) - hass.http.register_view(TwilioReceiveDataView()) - return True - - -class TwilioReceiveDataView(HomeAssistantView): - """Handle data from Twilio inbound messages and calls.""" - - url = API_PATH - name = 'api:{}'.format(DOMAIN) - - @callback - def post(self, request): # pylint: disable=no-self-use - """Handle Twilio data post.""" - from twilio.twiml import TwiML - hass = request.app['hass'] - data = yield from request.post() - hass.bus.async_fire(RECEIVED_DATA, dict(data)) - return TwiML().to_xml() diff --git a/homeassistant/components/twilio/.translations/en.json b/homeassistant/components/twilio/.translations/en.json new file mode 100644 index 00000000000..ca75fff0737 --- /dev/null +++ b/homeassistant/components/twilio/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Twilio", + "step": { + "user": { + "title": "Set up the Twilio Webhook", + "description": "Are you sure you want to set up Twilio?" + } + }, + "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 Twilio messages." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup [Webhooks with Twilio]({twilio_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + } + } +} diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py new file mode 100644 index 00000000000..d7196a034ce --- /dev/null +++ b/homeassistant/components/twilio/__init__.py @@ -0,0 +1,76 @@ +""" +Support for Twilio. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/twilio/ +""" +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.helpers import config_entry_flow + +REQUIREMENTS = ['twilio==6.19.1'] +DEPENDENCIES = ['webhook'] + +DOMAIN = 'twilio' + +CONF_ACCOUNT_SID = 'account_sid' +CONF_AUTH_TOKEN = 'auth_token' + +DATA_TWILIO = DOMAIN + +RECEIVED_DATA = '{}_data_received'.format(DOMAIN) + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): vol.Schema({ + vol.Required(CONF_ACCOUNT_SID): cv.string, + vol.Required(CONF_AUTH_TOKEN): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Twilio component.""" + from twilio.rest import TwilioRestClient + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + hass.data[DATA_TWILIO] = TwilioRestClient( + conf.get(CONF_ACCOUNT_SID), conf.get(CONF_AUTH_TOKEN)) + return True + + +async def handle_webhook(hass, webhook_id, request): + """Handle incoming webhook from Twilio for inbound messages and calls.""" + from twilio.twiml import TwiML + + data = dict(await request.post()) + data['webhook_id'] = webhook_id + hass.bus.async_fire(RECEIVED_DATA, dict(data)) + + return TwiML().to_xml() + + +async def async_setup_entry(hass, entry): + """Configure based on config entry.""" + hass.components.webhook.async_register( + entry.data[CONF_WEBHOOK_ID], handle_webhook) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + return True + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'Twilio Webhook', + { + 'twilio_url': + 'https://www.twilio.com/docs/glossary/what-is-a-webhook', + 'docs_url': 'https://www.home-assistant.io/components/twilio/' + } +) diff --git a/homeassistant/components/twilio/strings.json b/homeassistant/components/twilio/strings.json new file mode 100644 index 00000000000..ca75fff0737 --- /dev/null +++ b/homeassistant/components/twilio/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Twilio", + "step": { + "user": { + "title": "Set up the Twilio Webhook", + "description": "Are you sure you want to set up Twilio?" + } + }, + "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 Twilio messages." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup [Webhooks with Twilio]({twilio_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + } + } +} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e00215b8126..d37bd8cb558 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -151,6 +151,7 @@ FLOWS = [ 'smhi', 'sonos', 'tradfri', + 'twilio', 'unifi', 'upnp', 'zone', diff --git a/tests/components/twilio/__init__.py b/tests/components/twilio/__init__.py new file mode 100644 index 00000000000..641a509ff4d --- /dev/null +++ b/tests/components/twilio/__init__.py @@ -0,0 +1 @@ +"""Tests for the Twilio component.""" diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py new file mode 100644 index 00000000000..c740783f4c0 --- /dev/null +++ b/tests/components/twilio/test_init.py @@ -0,0 +1,41 @@ +"""Test the init file of Twilio.""" +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components import twilio +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' + }) + 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'