Twilio

Weather By Phone

Weather By Phone lets a caller check the weather by phone. Twilio answers the call, the application asks the caller for their US zip code, the application looks up the weather using a remote web service and reads the response to the caller.

This demo is written in Python for Google App Engine. This code can be adapted for other Python web frameworks such as Django.

Usage

A user enters his/her phone number. Twilio will call a specified third party and initialize a call between that party and the user. It's that simple.

Concepts

This demo shows the usage of the TwiML <Play>, <Say>, and <Gather> verbs.

Download

weatherbyphone.zip

Implementation

  • 1

    • When a call is answered by Twilio, Twilio fetches the '/' page which is configures to query the GatherPage object.

      • howtos/weatherbyphone/weatherbyphone.py
        import os
        import wsgiref.handlers
        from xml.dom import minidom
        
        from google.appengine.ext.webapp import template
        from google.appengine.ext import webapp
        from google.appengine.api import urlfetch
        
        BASE_URL = "http://weatherbyphone.appspot.com/"
        WEATHER_API_URL = "http://weather.yahooapis.com/forecastrss?p="
        WEATHER_API_NS = "http://xml.weather.yahoo.com/ns/rss/1.0"
        
        class WeatherPage(webapp.RequestHandler):
            """
            Accepts input digits from the caller, fetches the weather from an
            external site, and reads back the weather to the caller
            """
            def get(self):
                self.post()
            
            def _error(self, msg, redirecturl=None):
                templatevalues = {
                    'msg': msg,
                    'redirecturl': redirecturl
                }
                xml_response(self, 'error.xml', templatevalues)
            
            def _fetch(self, zipcode):
                url = WEATHER_API_URL + zipcode
                result = urlfetch.fetch(url)
                if result.status_code != 200:
                    return None
                return result.content
            
            def _parse(self, xml):
                dom = minidom.parseString(xml)
                conditions = dom.getElementsByTagNameNS(WEATHER_API_NS,
                    'condition')[0]
                location = dom.getElementsByTagNameNS(WEATHER_API_NS,
                    'location')[0]
                return {
                    'location': '%s, %s' % (location.getAttribute('city'),
                        location.getAttribute('region')),
                    'conditions': conditions.getAttribute('text'),
                    'temp': conditions.getAttribute('temp')
                }
            # @start snippet
            def post(self):
                zipcode = self.request.get('Digits')
                if not zipcode:
                    self._error("Invalid zip code.", BASE_URL)
                    return
                
                # strip off extra digits and keys from the Digits we got back
                zipcode = zipcode.replace('#', '').replace('*', '')[:5]
                
                weatherxml = self._fetch(zipcode)
                if not weatherxml:
                    self._error("Error fetching weather. Good Bye.")
                    return
                
                try:
                    xml_response(self, 'weather.xml', self._parse(weatherxml))
                except:
                    self._error("Error parsing weather. Good Bye.")
                # @end snippet
        
        # @start snippet
        def xml_response(handler, page, templatevalues=None):
            """
            Renders an XML response using a provided template page and values
            """
            path = os.path.join(os.path.dirname(__file__), page)
            handler.response.headers["Content-Type"] = "text/xml"
            handler.response.out.write(template.render(path, templatevalues))
        
        class GatherPage(webapp.RequestHandler):
            """
            Initial user greeting.  Plays the welcome audio file then reads the
            "enter zip code" message.  The Play and Say are wrapped in a Gather
            verb to collect the 5 digit zip code from the caller.  The Gather
            will post the results to /weather
            """
            def get(self):
                self.post()
            
            def post(self):
                templatevalues = {
                    'postprefix': BASE_URL,
                }
                xml_response(self, 'gather.xml', templatevalues)
        # @end snippet
        
        def main():
        	# @start snippet
            application = webapp.WSGIApplication([ \
                ('/', GatherPage),
                ('/weather', WeatherPage)],
                debug=True)
            # @end snippet
            wsgiref.handlers.CGIHandler().run(application)
        
        if __name__ == "__main__":
            main()
            
  • 2

    • The GatherPage object contains two methods get() and post() which map to the same function and return a page generated by the template gather.xml

      • howtos/weatherbyphone/weatherbyphone.py
        import os
        import wsgiref.handlers
        from xml.dom import minidom
        
        from google.appengine.ext.webapp import template
        from google.appengine.ext import webapp
        from google.appengine.api import urlfetch
        
        BASE_URL = "http://weatherbyphone.appspot.com/"
        WEATHER_API_URL = "http://weather.yahooapis.com/forecastrss?p="
        WEATHER_API_NS = "http://xml.weather.yahoo.com/ns/rss/1.0"
        
        class WeatherPage(webapp.RequestHandler):
            """
            Accepts input digits from the caller, fetches the weather from an
            external site, and reads back the weather to the caller
            """
            def get(self):
                self.post()
            
            def _error(self, msg, redirecturl=None):
                templatevalues = {
                    'msg': msg,
                    'redirecturl': redirecturl
                }
                xml_response(self, 'error.xml', templatevalues)
            
            def _fetch(self, zipcode):
                url = WEATHER_API_URL + zipcode
                result = urlfetch.fetch(url)
                if result.status_code != 200:
                    return None
                return result.content
            
            def _parse(self, xml):
                dom = minidom.parseString(xml)
                conditions = dom.getElementsByTagNameNS(WEATHER_API_NS,
                    'condition')[0]
                location = dom.getElementsByTagNameNS(WEATHER_API_NS,
                    'location')[0]
                return {
                    'location': '%s, %s' % (location.getAttribute('city'),
                        location.getAttribute('region')),
                    'conditions': conditions.getAttribute('text'),
                    'temp': conditions.getAttribute('temp')
                }
            # @start snippet
            def post(self):
                zipcode = self.request.get('Digits')
                if not zipcode:
                    self._error("Invalid zip code.", BASE_URL)
                    return
                
                # strip off extra digits and keys from the Digits we got back
                zipcode = zipcode.replace('#', '').replace('*', '')[:5]
                
                weatherxml = self._fetch(zipcode)
                if not weatherxml:
                    self._error("Error fetching weather. Good Bye.")
                    return
                
                try:
                    xml_response(self, 'weather.xml', self._parse(weatherxml))
                except:
                    self._error("Error parsing weather. Good Bye.")
                # @end snippet
        
        # @start snippet
        def xml_response(handler, page, templatevalues=None):
            """
            Renders an XML response using a provided template page and values
            """
            path = os.path.join(os.path.dirname(__file__), page)
            handler.response.headers["Content-Type"] = "text/xml"
            handler.response.out.write(template.render(path, templatevalues))
        
        class GatherPage(webapp.RequestHandler):
            """
            Initial user greeting.  Plays the welcome audio file then reads the
            "enter zip code" message.  The Play and Say are wrapped in a Gather
            verb to collect the 5 digit zip code from the caller.  The Gather
            will post the results to /weather
            """
            def get(self):
                self.post()
            
            def post(self):
                templatevalues = {
                    'postprefix': BASE_URL,
                }
                xml_response(self, 'gather.xml', templatevalues)
        # @end snippet
        
        def main():
        	# @start snippet
            application = webapp.WSGIApplication([ \
                ('/', GatherPage),
                ('/weather', WeatherPage)],
                debug=True)
            # @end snippet
            wsgiref.handlers.CGIHandler().run(application)
        
        if __name__ == "__main__":
            main()
            

      Here is the template used to render the initial TwiML greeting.

      • howtos/weatherbyphone/gather.xml
        <?xml version="1.0" encoding="UTF-8"?>
        <Response>
            <Gather method="POST" numDigits="5" action="{{ postprefix }}weather">
                <Play>greeting.wav</Play>
                <Say>Please enter your 5 digit zipcode to hear the weather.</Say>
            </Gather>
        </Response>
            
  • 3

    • Twilio then plays the greeting sound file, reads the "Please enter..." text to the caller and waits for 5 digits from the caller. When 5 digits have been entered by the caller, Twilio performs an HTTP POST with the results back to the 'action' handler. In this case, the hander is configured to be 'http://weatherbyphone.appspot.com/weather' Control then passes to the /weather portion of the application and the WeatherPage object.

      • howtos/weatherbyphone/weatherbyphone.py
        import os
        import wsgiref.handlers
        from xml.dom import minidom
        
        from google.appengine.ext.webapp import template
        from google.appengine.ext import webapp
        from google.appengine.api import urlfetch
        
        BASE_URL = "http://weatherbyphone.appspot.com/"
        WEATHER_API_URL = "http://weather.yahooapis.com/forecastrss?p="
        WEATHER_API_NS = "http://xml.weather.yahoo.com/ns/rss/1.0"
        
        class WeatherPage(webapp.RequestHandler):
            """
            Accepts input digits from the caller, fetches the weather from an
            external site, and reads back the weather to the caller
            """
            def get(self):
                self.post()
            
            def _error(self, msg, redirecturl=None):
                templatevalues = {
                    'msg': msg,
                    'redirecturl': redirecturl
                }
                xml_response(self, 'error.xml', templatevalues)
            
            def _fetch(self, zipcode):
                url = WEATHER_API_URL + zipcode
                result = urlfetch.fetch(url)
                if result.status_code != 200:
                    return None
                return result.content
            
            def _parse(self, xml):
                dom = minidom.parseString(xml)
                conditions = dom.getElementsByTagNameNS(WEATHER_API_NS,
                    'condition')[0]
                location = dom.getElementsByTagNameNS(WEATHER_API_NS,
                    'location')[0]
                return {
                    'location': '%s, %s' % (location.getAttribute('city'),
                        location.getAttribute('region')),
                    'conditions': conditions.getAttribute('text'),
                    'temp': conditions.getAttribute('temp')
                }
            # @start snippet
            def post(self):
                zipcode = self.request.get('Digits')
                if not zipcode:
                    self._error("Invalid zip code.", BASE_URL)
                    return
                
                # strip off extra digits and keys from the Digits we got back
                zipcode = zipcode.replace('#', '').replace('*', '')[:5]
                
                weatherxml = self._fetch(zipcode)
                if not weatherxml:
                    self._error("Error fetching weather. Good Bye.")
                    return
                
                try:
                    xml_response(self, 'weather.xml', self._parse(weatherxml))
                except:
                    self._error("Error parsing weather. Good Bye.")
                # @end snippet
        
        # @start snippet
        def xml_response(handler, page, templatevalues=None):
            """
            Renders an XML response using a provided template page and values
            """
            path = os.path.join(os.path.dirname(__file__), page)
            handler.response.headers["Content-Type"] = "text/xml"
            handler.response.out.write(template.render(path, templatevalues))
        
        class GatherPage(webapp.RequestHandler):
            """
            Initial user greeting.  Plays the welcome audio file then reads the
            "enter zip code" message.  The Play and Say are wrapped in a Gather
            verb to collect the 5 digit zip code from the caller.  The Gather
            will post the results to /weather
            """
            def get(self):
                self.post()
            
            def post(self):
                templatevalues = {
                    'postprefix': BASE_URL,
                }
                xml_response(self, 'gather.xml', templatevalues)
        # @end snippet
        
        def main():
        	# @start snippet
            application = webapp.WSGIApplication([ \
                ('/', GatherPage),
                ('/weather', WeatherPage)],
                debug=True)
            # @end snippet
            wsgiref.handlers.CGIHandler().run(application)
        
        if __name__ == "__main__":
            main()
            

      Because we have mapped the get() handler in the WeatherPage object we can simulate digits pressed by the caller directly in a web browser. Twilio passes back digits pressed using the 'Digits' parameter. Here is link simulating the situation if someone entered the zip code 02138:

  • 4

    • The final response is generated by fetching the weather conditions in XML format from the remote weather web service, parsing the XML, and feeding the values back in to the weather.xml template.

      • howtos/weatherbyphone/weather.xml
        <?xml version="1.0" encoding="UTF-8"?>
        <Response>
            <Say>It is currently {{ temp }} degrees fahrenheit and {{ conditions }}
                in {{ location }}.</Say>
        </Response>
            

      And that's it! The rest of the code is input validation and error handling.