From 61ad11fcd74311d854bea2021cf6d7a8288600eb Mon Sep 17 00:00:00 2001 From: fakezeta Date: Fri, 27 Jan 2017 00:32:01 +0100 Subject: [PATCH 1/9] Added notify.twilio_call component for Voice calling with Twilio --- .../components/notify/twilio_call.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 homeassistant/components/notify/twilio_call.py diff --git a/homeassistant/components/notify/twilio_call.py b/homeassistant/components/notify/twilio_call.py new file mode 100644 index 00000000000..8bc9185e789 --- /dev/null +++ b/homeassistant/components/notify/twilio_call.py @@ -0,0 +1,65 @@ +""" +Twilio Call platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.twilio_call/ +""" +import logging +import urllib + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ["twilio==5.4.0"] + + +CONF_ACCOUNT_SID = "account_sid" +CONF_AUTH_TOKEN = "auth_token" +CONF_FROM_NUMBER = "from_number" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ACCOUNT_SID): cv.string, + vol.Required(CONF_AUTH_TOKEN): cv.string, + vol.Required(CONF_FROM_NUMBER): + vol.All(cv.string, vol.Match(r"^\+?[1-9]\d{1,14}$")), +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Twilio Call notification service.""" + # pylint: disable=import-error + from twilio.rest import TwilioRestClient + + twilio_client = TwilioRestClient(config[CONF_ACCOUNT_SID], + config[CONF_AUTH_TOKEN]) + + return TwilioCallNotificationService(twilio_client, + config[CONF_FROM_NUMBER]) + + +class TwilioCallNotificationService(BaseNotificationService): + """Implement the notification service for the Twilio Call service.""" + + def __init__(self, twilio_client, from_number): + """Initialize the service.""" + self.client = twilio_client + self.from_number = from_number + + def send_message(self, message="", **kwargs): + """Call to specified target users.""" + targets = kwargs.get(ATTR_TARGET) + + if not targets: + _LOGGER.info("At least 1 target is required") + return + + for target in targets: + twimlet_url = 'http://twimlets.com/message?Me=' + twimlet_url += urllib.parse.quote(message, safe='') + self.client.calls.create(to=target, + url=twimlet_url, + from_=self.from_number) From 3f13bdb1f73973ef12102c1e48cf6025ceea229d Mon Sep 17 00:00:00 2001 From: fakezeta Date: Fri, 27 Jan 2017 00:48:58 +0100 Subject: [PATCH 2/9] Modified .coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index b5dda5b6f43..d23a4cc2ce4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -271,6 +271,7 @@ omit = homeassistant/components/notify/telegram.py homeassistant/components/notify/telstra.py homeassistant/components/notify/twilio_sms.py + homeassistant/components/notify/twilio_call.py homeassistant/components/notify/twitter.py homeassistant/components/notify/xmpp.py homeassistant/components/nuimo_controller.py From e4f0b0a57f8b2e01493a53668d84ba76bc3e9346 Mon Sep 17 00:00:00 2001 From: fakezeta Date: Fri, 27 Jan 2017 01:12:28 +0100 Subject: [PATCH 3/9] Updated requirements_all.txt --- requirements_all.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_all.txt b/requirements_all.txt index de4a2cbeab3..1a6d008ab35 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -634,6 +634,7 @@ tikteck==0.4 # homeassistant.components.switch.transmission transmissionrpc==0.11 +# homeassistant.components.notify.twilio_call # homeassistant.components.notify.twilio_sms twilio==5.4.0 From db85e2bc2a573579208e3ded4a62d64115b15664 Mon Sep 17 00:00:00 2001 From: fakezeta Date: Sat, 28 Jan 2017 00:51:30 +0100 Subject: [PATCH 4/9] Checking message if valid url or a string for TTS --- .../components/notify/twilio_call.py | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/twilio_call.py b/homeassistant/components/notify/twilio_call.py index 8bc9185e789..37068d6e220 100644 --- a/homeassistant/components/notify/twilio_call.py +++ b/homeassistant/components/notify/twilio_call.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/notify.twilio_call/ import logging import urllib + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -41,6 +42,57 @@ def get_service(hass, config, discovery_info=None): config[CONF_FROM_NUMBER]) +def is_validurl(url): + """Check if the passed url is valid using dperini regex.""" + import re + + ip_middle_oct = u"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5]))" + ip_last_oct = u"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))" + + regex = re.compile( + u"^" + # protocol identifier + u"(?:(?:https?|ftp)://)" + # user:pass authentication + u"(?:\S+(?::\S*)?@)?" + u"(?:" + u"(?P" + # IP address exclusion + # private & local networks + u"(?:(?:10|127)" + ip_middle_oct + u"{2}" + ip_last_oct + u")|" + u"(?:(?:169\.254|192\.168)" + ip_middle_oct + ip_last_oct + u")|" + u"(?:172\.(?:1[6-9]|2\d|3[0-1])" + ip_middle_oct + ip_last_oct + u"))" + u"|" + # IP address dotted notation octets + # excludes loopback network 0.0.0.0 + # excludes reserved space >= 224.0.0.0 + # excludes network & broadcast addresses + # (first & last IP address of each class) + u"(?P" + u"(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])" + u"" + ip_middle_oct + u"{2}" + u"" + ip_last_oct + u")" + u"|" + # host name + u"(?:(?:[a-z\u00a1-\uffff0-9]-?)*[a-z\u00a1-\uffff0-9]+)" + # domain name + u"(?:\.(?:[a-z\u00a1-\uffff0-9]-?)*[a-z\u00a1-\uffff0-9]+)*" + # TLD identifier + u"(?:\.(?:[a-z\u00a1-\uffff]{2,}))" + u")" + # port number + u"(?::\d{2,5})?" + # resource path + u"(?:/\S*)?" + # query string + u"(?:\?\S*)?" + u"$", + re.UNICODE | re.IGNORECASE + ) + + return regex.match(url) + + class TwilioCallNotificationService(BaseNotificationService): """Implement the notification service for the Twilio Call service.""" @@ -57,9 +109,13 @@ class TwilioCallNotificationService(BaseNotificationService): _LOGGER.info("At least 1 target is required") return - for target in targets: - twimlet_url = 'http://twimlets.com/message?Me=' + if is_validurl(message): + twimlet_url = message + else: + twimlet_url = 'http://twimlets.com/message?Message=' twimlet_url += urllib.parse.quote(message, safe='') + + for target in targets: self.client.calls.create(to=target, url=twimlet_url, from_=self.from_number) From 549c3b2c84d60fe92bd0b57e5062cf0e6600acef Mon Sep 17 00:00:00 2001 From: fakezeta Date: Sat, 28 Jan 2017 01:06:01 +0100 Subject: [PATCH 5/9] Minor changes to pass lint check --- .../components/notify/twilio_call.py | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/notify/twilio_call.py b/homeassistant/components/notify/twilio_call.py index 37068d6e220..59da15aeebc 100644 --- a/homeassistant/components/notify/twilio_call.py +++ b/homeassistant/components/notify/twilio_call.py @@ -46,47 +46,47 @@ def is_validurl(url): """Check if the passed url is valid using dperini regex.""" import re - ip_middle_oct = u"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5]))" - ip_last_oct = u"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))" + ip_middle_oct = r"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5]))" + ip_last_oct = r"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))" regex = re.compile( - u"^" + r"^" # protocol identifier - u"(?:(?:https?|ftp)://)" + r"(?:(?:https?|ftp)://)" # user:pass authentication - u"(?:\S+(?::\S*)?@)?" - u"(?:" - u"(?P" + r"(?:\S+(?::\S*)?@)?" + r"(?:" + r"(?P" # IP address exclusion # private & local networks - u"(?:(?:10|127)" + ip_middle_oct + u"{2}" + ip_last_oct + u")|" - u"(?:(?:169\.254|192\.168)" + ip_middle_oct + ip_last_oct + u")|" - u"(?:172\.(?:1[6-9]|2\d|3[0-1])" + ip_middle_oct + ip_last_oct + u"))" - u"|" + r"(?:(?:10|127)" + ip_middle_oct + u"{2}" + ip_last_oct + u")|" + r"(?:(?:169\.254|192\.168)" + ip_middle_oct + ip_last_oct + u")|" + r"(?:172\.(?:1[6-9]|2\d|3[0-1])" + ip_middle_oct + ip_last_oct + u"))" + r"|" # IP address dotted notation octets # excludes loopback network 0.0.0.0 # excludes reserved space >= 224.0.0.0 # excludes network & broadcast addresses # (first & last IP address of each class) - u"(?P" - u"(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])" - u"" + ip_middle_oct + u"{2}" - u"" + ip_last_oct + u")" - u"|" + r"(?P" + r"(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])" + r"" + ip_middle_oct + u"{2}" + r"" + ip_last_oct + u")" + r"|" # host name - u"(?:(?:[a-z\u00a1-\uffff0-9]-?)*[a-z\u00a1-\uffff0-9]+)" + r"(?:(?:[a-z\u00a1-\uffff0-9]-?)*[a-z\u00a1-\uffff0-9]+)" # domain name - u"(?:\.(?:[a-z\u00a1-\uffff0-9]-?)*[a-z\u00a1-\uffff0-9]+)*" + r"(?:\.(?:[a-z\u00a1-\uffff0-9]-?)*[a-z\u00a1-\uffff0-9]+)*" # TLD identifier - u"(?:\.(?:[a-z\u00a1-\uffff]{2,}))" - u")" + r"(?:\.(?:[a-z\u00a1-\uffff]{2,}))" + r")" # port number - u"(?::\d{2,5})?" + r"(?::\d{2,5})?" # resource path - u"(?:/\S*)?" + r"(?:/\S*)?" # query string - u"(?:\?\S*)?" - u"$", + r"(?:\?\S*)?" + r"$", re.UNICODE | re.IGNORECASE ) From 564aad0ab8318929a06f29bf98dc88800275308d Mon Sep 17 00:00:00 2001 From: fakezeta Date: Sat, 28 Jan 2017 01:35:54 +0100 Subject: [PATCH 6/9] Removed Regexp and added error logging --- .../components/notify/twilio_call.py | 64 +++---------------- 1 file changed, 9 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/notify/twilio_call.py b/homeassistant/components/notify/twilio_call.py index 59da15aeebc..14e6fe27aa7 100644 --- a/homeassistant/components/notify/twilio_call.py +++ b/homeassistant/components/notify/twilio_call.py @@ -42,57 +42,6 @@ def get_service(hass, config, discovery_info=None): config[CONF_FROM_NUMBER]) -def is_validurl(url): - """Check if the passed url is valid using dperini regex.""" - import re - - ip_middle_oct = r"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5]))" - ip_last_oct = r"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))" - - regex = re.compile( - r"^" - # protocol identifier - r"(?:(?:https?|ftp)://)" - # user:pass authentication - r"(?:\S+(?::\S*)?@)?" - r"(?:" - r"(?P" - # IP address exclusion - # private & local networks - r"(?:(?:10|127)" + ip_middle_oct + u"{2}" + ip_last_oct + u")|" - r"(?:(?:169\.254|192\.168)" + ip_middle_oct + ip_last_oct + u")|" - r"(?:172\.(?:1[6-9]|2\d|3[0-1])" + ip_middle_oct + ip_last_oct + u"))" - r"|" - # IP address dotted notation octets - # excludes loopback network 0.0.0.0 - # excludes reserved space >= 224.0.0.0 - # excludes network & broadcast addresses - # (first & last IP address of each class) - r"(?P" - r"(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])" - r"" + ip_middle_oct + u"{2}" - r"" + ip_last_oct + u")" - r"|" - # host name - r"(?:(?:[a-z\u00a1-\uffff0-9]-?)*[a-z\u00a1-\uffff0-9]+)" - # domain name - r"(?:\.(?:[a-z\u00a1-\uffff0-9]-?)*[a-z\u00a1-\uffff0-9]+)*" - # TLD identifier - r"(?:\.(?:[a-z\u00a1-\uffff]{2,}))" - r")" - # port number - r"(?::\d{2,5})?" - # resource path - r"(?:/\S*)?" - # query string - r"(?:\?\S*)?" - r"$", - re.UNICODE | re.IGNORECASE - ) - - return regex.match(url) - - class TwilioCallNotificationService(BaseNotificationService): """Implement the notification service for the Twilio Call service.""" @@ -103,19 +52,24 @@ class TwilioCallNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Call to specified target users.""" + from twilio import TwilioRestException + targets = kwargs.get(ATTR_TARGET) if not targets: _LOGGER.info("At least 1 target is required") return - if is_validurl(message): + if message.startswith("http://"): twimlet_url = message else: twimlet_url = 'http://twimlets.com/message?Message=' twimlet_url += urllib.parse.quote(message, safe='') for target in targets: - self.client.calls.create(to=target, - url=twimlet_url, - from_=self.from_number) + try: + self.client.calls.create(to=target, + url=twimlet_url, + from_=self.from_number) + except TwilioRestException as exc: + _LOGGER.error(exc) From 8f418831a12dfef34a2392c3b5c63d71423925ca Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 12:45:32 -0800 Subject: [PATCH 7/9] Update Twilio SDK version --- homeassistant/components/notify/twilio_call.py | 2 +- homeassistant/components/notify/twilio_sms.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notify/twilio_call.py b/homeassistant/components/notify/twilio_call.py index 14e6fe27aa7..b1cc8e5eeed 100644 --- a/homeassistant/components/notify/twilio_call.py +++ b/homeassistant/components/notify/twilio_call.py @@ -15,7 +15,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["twilio==5.4.0"] +REQUIREMENTS = ["twilio==5.7.0"] CONF_ACCOUNT_SID = "account_sid" diff --git a/homeassistant/components/notify/twilio_sms.py b/homeassistant/components/notify/twilio_sms.py index 950e0eed221..ab3ac89e6b2 100644 --- a/homeassistant/components/notify/twilio_sms.py +++ b/homeassistant/components/notify/twilio_sms.py @@ -13,7 +13,7 @@ from homeassistant.components.notify import ( ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["twilio==5.4.0"] +REQUIREMENTS = ["twilio==5.7.0"] CONF_ACCOUNT_SID = "account_sid" diff --git a/requirements_all.txt b/requirements_all.txt index 1a6d008ab35..ea4ebc95ecb 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -636,7 +636,7 @@ transmissionrpc==0.11 # homeassistant.components.notify.twilio_call # homeassistant.components.notify.twilio_sms -twilio==5.4.0 +twilio==5.7.0 # homeassistant.components.sensor.uber uber_rides==0.2.7 From 379ae11405a2bcd775784e1e61de98682d9778c9 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 12:45:52 -0800 Subject: [PATCH 8/9] Minor style fixes --- homeassistant/components/notify/twilio_call.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notify/twilio_call.py b/homeassistant/components/notify/twilio_call.py index b1cc8e5eeed..2ae00081240 100644 --- a/homeassistant/components/notify/twilio_call.py +++ b/homeassistant/components/notify/twilio_call.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/notify.twilio_call/ import logging import urllib - import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -63,8 +62,8 @@ class TwilioCallNotificationService(BaseNotificationService): if message.startswith("http://"): twimlet_url = message else: - twimlet_url = 'http://twimlets.com/message?Message=' - twimlet_url += urllib.parse.quote(message, safe='') + twimlet_url = "http://twimlets.com/message?Message=" + twimlet_url += urllib.parse.quote(message, safe="") for target in targets: try: From b7bb31cb95070f0b485cd4348426ec8ef10318ab Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 12:46:34 -0800 Subject: [PATCH 9/9] Allow both http and https URLs --- homeassistant/components/notify/twilio_call.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notify/twilio_call.py b/homeassistant/components/notify/twilio_call.py index 2ae00081240..374e77b9507 100644 --- a/homeassistant/components/notify/twilio_call.py +++ b/homeassistant/components/notify/twilio_call.py @@ -59,7 +59,7 @@ class TwilioCallNotificationService(BaseNotificationService): _LOGGER.info("At least 1 target is required") return - if message.startswith("http://"): + if message.startswith(("http://", "https://")): twimlet_url = message else: twimlet_url = "http://twimlets.com/message?Message="