User Tools

Site Tools


weatherbot

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
weatherbot [2014/08/23 13:28]
sekula created
weatherbot [2014/08/30 23:23] (current)
sekula [Talking to WeatherBot]
Line 1: Line 1:
 ====== WeatherBot ====== ====== WeatherBot ======
  
-This is a simple bot script, written in Python and based on the PyPump pump.io toolkit. It looks at the inbox of a pump.io account (e.g. [[https://​hub.polari.us/​weather]]) and looks for new major messages. If the message contains a viable "​City,​Region"​ string, the weather information for that location will be returned by the bot as a reply to the original inbox message.+{{ :​weatherbot.png?​direct&​200|}}This is a simple bot script, written in Python and based on the PyPump pump.io toolkit. It looks at the inbox of a pump.io account (e.g. [[https://​hub.polari.us/​weather]]) and looks for new major messages. If the message contains a viable "​City,​Region"​ string, the weather information for that location will be returned by the bot as a reply to the original inbox message.
  
 This script uses the [[http://​openweathermap.org/​api]] OpenWeatherMap API. This script uses the [[http://​openweathermap.org/​api]] OpenWeatherMap API.
 +
 +The logo is derived from two OpenClipArt.org images: [[http://​openclipart.org/​detail/​191072/​blue-robot-by-scout-191072]] (Blue Robot, by Scout [[http://​openclipart.org/​user-detail/​Scout]]),​ and [[http://​openclipart.org/​detail/​190891/​clouds-by-arking-190891]] (Clouds by Arking [[http://​openclipart.org/​user-detail/​arking]])
  
 ===== License ===== ===== License =====
Line 15: Line 17:
  
 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 +
 +===== External Software Dependencies =====
 +
 +  * PyPump [[https://​github.com/​xray7224/​PyPump.git]]
 +  * LYNX (for converting HTML to plain text - some pump clients don't send messages in HTML, some do... this saved me having to write my own converter)
 +
 +===== Bug Reporting and Feature Requests =====
 +
 +  * [[community:​WeatherBotBugTracker]]
 +
  
 ===== Source Code ===== ===== Source Code =====
  
 <​code>​ <​code>​
 +#​!/​usr/​bin/​env python
 +
 +# Originally authored by Stephen Sekula (https://​hub.polari.us/​steve)
 +# Distributed under an Apache 2.0 license
 +# For more information ,see 
 +
 +import re
 +import pycurl
 +import cStringIO
 +import json
 +import sys
 +import time
 +import subprocess ​
 +import os
 +import unicodedata
 +
 +from datetime import datetime
 +
 +from pypump import PyPump, Client
 +from pypump.models.image import Image
 +from pypump.models.collection import Collection
 +from pypump.models.collection import Public
 +from pypump.models.person import Person
 +from pypump.exception import PyPumpException
 +
 +
 +# city,region search pattern
 +citypattern = re.compile('​.*?​([A-Za-z]+,​.*)',​re.DOTALL)
 +
 +tripletpattern = re.compile('​.*?​([A-Za-z\-\s]+,​\s{0,​1}[A-Za-z]+,​\s{0,​1}[A-Za-z]+).*',​re.DOTALL)
 +doubletpattern = re.compile('​.*?​([A-Za-z\-\s]+,​\s{0,​1}[A-Za-z]+).*',​re.DOTALL)
 +
 +for_tripletpattern = re.compile('​^.*?​\sfor %s' % (tripletpattern.pattern),​re.DOTALL)
 +in_tripletpattern = re.compile('​^.*?​\sin %s' % (tripletpattern.pattern),​re.DOTALL)
 +
 +for_doubletpattern = re.compile('​^.*?​\sfor %s' % (doubletpattern.pattern),​re.DOTALL)
 +in_doubletpattern = re.compile('​^.*?​\sin %s' % (doubletpattern.pattern),​re.DOTALL)
 +
 +
 +# Place the credentials below for the pump.io account
 +# These can be obtained by using: http://​polari.us/​dokuwiki/​doku.php?​id=navierstokes#​a_simple_program_to_authenticate_pypump_against_your_pumpio_instance
 +
 +client_credentials = ['​XXX',​ '​XXX'​]
 +client_tokens = ['​XXX',​ '​XXX'​]
 +
 +# Webfinger for your account
 +webfinger = "​name@example.com"​
 +
 +# Important: define a log file that contains a list of activities to which
 +# we have already responded.
 +logfile_name = "​weatherbot.activity.log"​
 +
 +def simple_verifier(url):​
 +    print 'Go to: ' + url
 +    return raw_input('​Verifier:​ ') # they will get a code back
 +
 +def HTMLToText( html_text ):
 +    pid = os.getpid()
 +
 +    htmlfile = open('/​tmp/​%d_msg.html'​ % (pid),'​w'​)
 +    try:
 +        htmlfile.write( html_text )
 +    except UnicodeEncodeError:​
 +        htmlfile.write( unicodedata.normalize('​NFKD',​ html_text).encode('​ascii','​ignore'​) )
 +        pass
 +
 +    htmlfile.close()
 +        ​
 +    try:
 +        ascii_text = subprocess.check_output(["​lynx",​ "​--dump",​ "​-width 2048", "​-nolist",​ "/​tmp/​%d_msg.html"​ % (pid)])
 +    except subprocess.CalledProcessError:​
 +        print "There was a problem trying to call lynx - make sure it is installed correctly."​
 +        return html_text
 +
 +    return ascii_text
 +
 +
 +def reply(activity,​my_reply):​
 +    try:
 +        activity.obj.comment(my_reply)
 +    except PyPumpException:​
 +        print " ​  ... PyPumpException - ERROR - I will try to get this request next time!"
 +        return False
 +        pass
 +        ​
 +    return True
 +
 +def KelvinToCelsius(temp_kelvin):​
 +    return temp_kelvin - 273.15
 +
 +def CelsiusToFarenheit(temp_celsius):​
 +    return temp_celsius*1.8 + 32.0
 +
 +def FarenheitToCelsius(temp_farenheit):​
 +    return (temp_farenheit - 32.0)/1.8
 +
 +def KPHToMPH(speed_kph):​
 +    return speed_kph/​0.6214
 +
 +def current_conditions(weather_data):​
 +    humidity = weather_data['​main'​]['​humidity'​]
 +    ​
 +    temp_kelvin = weather_data['​main'​]['​temp'​]
 +    temp_celsius = KelvinToCelsius(temp_kelvin)
 +    temp_farenheit = CelsiusToFarenheit(temp_celsius)
 +    ​
 +    wind_speed_kmh = weather_data['​wind'​]['​speed'​]
 +    wind_speed_mph = KPHToMPH(wind_speed_kmh)
 +    ​
 +    heat_index_farenheit = -42.379 + 2.04901523*temp_farenheit + 10.14333127*humidity - .22475541*temp_farenheit*humidity - .00683783*temp_farenheit*temp_farenheit - .05481717*humidity*humidity + .00122874*temp_farenheit*temp_farenheit*humidity + .00085282*temp_farenheit*humidity*humidity - .00000199*temp_farenheit*temp_farenheit*humidity*humidity
 +    heat_index_celsius = FarenheitToCelsius(heat_index_farenheit)
 +    ​
 +    response = ""​
 +    ​
 +    response += "<​p>​The weather in %s is currently: </​p>​\n"​ % weather_data["​name"​]
 +    response += "<​p><​img src=\"​http://​openweathermap.org/​img/​w/​%s.png\"/></​p>"​ % (weather_data['​weather'​][0]['​icon'​]) ​
 +    response += "<​ul>​\n"​
 +    response += "<​li>​ Temperature:​ %0.1fC (%0.1fF) </​li>​\n"​ % (temp_celsius,​ temp_farenheit)
 +    response += "<​li>​ Humidity: %0.1f%% </li> \n" % (humidity)
 +    response += "<​li>​ Heat Index: %0.1fC (%0.1fF) </​li>​\n"​ % (heat_index_celsius,​heat_index_farenheit) ​   ​
 +    response += "<​li>​ Wind Speed: %.1fkm/h (%.1fmph) </​li>​\n"​ % (wind_speed_kmh,​wind_speed_mph)
 +    response += "<​li>​ Conditions: %s - %s</​li>​ \n" % (weather_data['​weather'​][0]['​main'​],​ weather_data['​weather'​][0]['​description'​])
 +    response += "</​ul>​\n"​
 +
 +
 +    return response
 +
 +def forecast(weather_data):​
 +    list_of_data = weather_data['​list'​]
 +    response = ""​
 +
 +    response += "<​p>​The forecast for %s is: </​p>​\n"​ % weather_data["​city"​]["​name"​]
 +    response += "<​table style=\"​border-collapse:​ collapse;​border:​ 1px solid black;​\">​\n"​
 +    response += "<​tr><​td style=\"​border:​ 1px solid black;​\"><​b>​TIME</​b></​td><​td style=\"​border:​ 1px solid black;​\"><​b>​Temp</​b></​td><​td style=\"​border:​ 1px solid black;​\"><​b>​Humidity</​b></​td><​td style=\"​border:​ 1px solid black;​\"><​b>​Description</​b></​td></​tr>​\n"​
 +    for moment in list_of_data:​
 +        dt = datetime.fromtimestamp(moment["​dt"​])
 +        timestamp = dt.strftime('​%Y-%m-%d %H:​%M:​%S'​)
 +        temp_celsius = KelvinToCelsius(moment["​main"​]["​temp"​])
 +        temp_farenheit = CelsiusToFarenheit(temp_celsius)
 +        humidity = moment["​main"​]["​humidity"​]
 +        description = "%s - %s" % (moment["​weather"​][0]["​main"​],​ moment["​weather"​][0]["​description"​])
 +        icon = "​http://​openweathermap.org/​img/​w/​%s.png"​ % (moment["​weather"​][0]["​icon"​])
 +        response += "<​tr><​td style=\"​border:​ 1px solid black;​\">​%s</​td><​td style=\"​border:​ 1px solid black;​\">​%s C (%s F)</​td><​td style=\"​border:​ 1px solid black;​\">​%s%%</​td><​td style=\"​border:​ 1px solid black;​\"><​img src=\"​%s\"/>​%s</​td></​tr>"​ % (timestamp,​temp_celsius,​temp_farenheit,​humidity,​icon,​description)
 +    response += "</​table>"​
 +
 +    return response
 +
 +
 +client = Client(
 +    webfinger,
 +    name="​PyPump",​
 +    type="​native",​
 +    key=client_credentials[0],​ # client key
 +    secret=client_credentials[1] # client secret
 +)
 +
 +# archive of activities I have already processed
 +
 +process_log = open(logfile_name,'​a+'​)
 +
 +while 1==1:
 +    pump = PyPump(
 +        client=client,​
 +        token=client_tokens[0],​ # the token key
 +        secret=client_tokens[1],​ # the token secret
 +        verifier_callback=simple_verifier
 +    )
 +    my_inbox = pump.me.inbox
 +    ​
 +    for activity in my_inbox.major[:​100]:​
 +
 +        author = None
 +        to = None
 +        cc = None
 +        content = None
 +        id = None
 +
 +        try:
 +            author = activity.obj.author
 +            to = getattr(activity,​ "​to",​ [])
 +            cc = getattr(activity,​ "​cc",​ [])
 +            content = activity.obj.content
 +            id = activity.obj.id
 +        except AttributeError:​
 +            continue
 +
 +        if content == None:
 +            continue
 +
 +        for recipient in to:
 +            if isinstance(recipient,​ Person):
 +                if recipient.webfinger == pump.client.webfinger:​
 +                    # check to see if we already handled this request
 +                    do_i_respond = True
 +                    ​
 +                    process_log.seek(0)
 +                    for processed_id in process_log:​
 +                        processed_id = processed_id.rstrip()
 +                        if activity.obj.id == processed_id:​
 +                            do_i_respond = False
 +                            pass
 +                        pass
 +
 +                    if not do_i_respond:​
 +                        continue
 +                ​
 +                    # handle this content
 +                    ​
 +                    text_content = HTMLToText(content)
 +                    text_content = text_content.lower()
 +
 +                    query = -1
 +                    if text_content.find('​current'​) != -1 or \
 +                       ​text_content.find('​conditions'​) != -1 or \
 +                       ​text_content.find('​currently'​) != -1:
 +                        query = 0
 +                    elif text_content.find('​forecast'​) != -1:
 +                        query = 1
 +                        pass
 +                    else:
 +                        # default to current conditions
 +                        query = 0
 +
 +
 +                    search_result = in_tripletpattern.match(text_content)
 +                    if not search_result:​
 +                        print " ... Search failed for in + triplet"​
 +                        search_result = for_tripletpattern.match(text_content)
 +                    if not search_result:​
 +                        print " ... Search failed for for + triplet"​
 +                        search_result = tripletpattern.match(text_content)
 +                        ​
 +                    if not search_result:​
 +                        print " ... Search failed for pure triplet"​
 +                        search_result = in_doubletpattern.match(text_content)
 +                    if not search_result:​
 +                        print " ... Search failed for in + doublet"​
 +                        search_result = for_doubletpattern.match(text_content)
 +                    if not search_result:​
 +                        print " ... Search failed for for + doublet"​
 +                        search_result = doubletpattern.match(text_content)
 +                    if not search_result:​
 +                        print " ... Search failed for pure doublet"​
 +                        pass
 +
 +                    if query >= 0 and search_result:​
 +                        cityrequest = search_result.group(1)
 +
 +                        query_type = "​current conditions"​
 +                        if query == 1:
 +                            query_type = "​forecast"​
 +                            pass
 +                        ​
 +                        author = ""​
 +                        if type(activity.obj.author) == Person:
 +                            author = activity.obj.author.display_name
 +                        elif type(activity.obj.author) == unicode:
 +                            author = unicodedata.normalize('​NFKD',​ activity.obj.author).encode('​ascii','​ignore'​)
 +                        else:
 +                            author = activity.obj.author
 +                            pass
 +                                                ​
 +                        print "%s sent a weather request for the %s for %s, which I will now process..."​ % (author, query_type, cityrequest)
 +                        ​
 +                        # clean up poor formatting in content
 +                        cityrequest = cityrequest.replace(",​ ", ","​)
 +
 +                        # send city information to openweathermap.org
 +                        curl_tries = 0
 +                        curl_success = False
 +                        curl_buffer = cStringIO.StringIO()
 +
 +
 +                        while curl_success == False and curl_tries < 5:
 +                        ​
 +                            c = pycurl.Curl()
 +                            if query == 0:
 +                                # current conditions
 +                                c.setopt(c.URL,​ '​http://​api.openweathermap.org/​data/​2.5/​weather?​q=%s'​ % (cityrequest))
 +                            elif query == 1:
 +                                # forecast
 +                                c.setopt(c.URL,​ '​http://​api.openweathermap.org/​data/​2.5/​forecast?​q=%s'​ % (cityrequest))
 +                                pass
 +                            c.setopt(c.WRITEFUNCTION,​ curl_buffer.write)
 +
 +                        ​
 +
 +                            try:
 +                                c.perform()
 +                            except pycurl.error:​
 +                                curl_tries += 1
 +                                time.sleep(10)
 +                                pass
 +
 +                            curl_success = True
 +
 +                            c.close()
 +                        ​
 +                        if curl_success == False:
 +                            response = "<​p>​There was a problem getting the response from OpenWeatherMaps.org. It could be them; or, it could be that you did not give me a valid \"​City,​Region\"​. Please try again.</​p>"​
 +                            my_reply = pump.Comment(response)
 +                            success = reply(activity,​my_reply)
 +                            if success:
 +                                process_log.write(activity.obj.id+"​\n"​)
 +                                print " ​  ... user did not send a valid City,Region pair or there is a problem with OpenWeatherMaps.org."​
 +                            else:
 +                                continue
 +                            pass
 +
 +
 +                        # Try loading the data from the JSON
 +                        try:
 +                            weather_data = json.loads(curl_buffer.getvalue())
 +                            curl_buffer.close()
 +                        except ValueError:
 +                            response = "<​p>​There was a problem getting the response from OpenWeatherMaps.org. It could be them; or, it could be that you did not give me a valid \"​City,​Region\"​ pair from you. Please try again.</​p>"​
 +                            my_reply = pump.Comment(response)
 +                            success = reply(activity,​my_reply)
 +                            curl_buffer.close()
 +                            ​
 +                            process_log.write(activity.obj.id+"​\n"​)
 +                            print " ​  ... your request resulted in a bad response by the OpenWeatherMap.org API. Check it for funny business. Sorry!"​
 +                            continue
 +
 +
 +
 +                        try:
 +
 +                            if query == 0:
 +                                response = current_conditions(weather_data)
 +                            elif query == 1:
 +                                response = forecast(weather_data)
 +                            ​
 +                                pass
 +
 +                            my_reply = pump.Comment(response)
 +                            my_reply.cc = cc
 +                            my_reply.cc.append( pump.Public )
 +                            ​
 +                            try:
 +                                activity.obj.comment(my_reply)
 +                                process_log.write(activity.obj.id+"​\n"​)
 +                                print " ​  ... successfully handled the request and responded"​
 +                            except PyPumpException:​
 +                                print " ​  ... PyPumpException - ERROR - I will try to get this request next time!"
 +                                continue
 +
 +                        except KeyError:
 +                            response = "<​p>​There was a problem getting the response from OpenWeatherMaps.org. It could be them; or, it could be that you did not give me a valid \"​City,​Region\"​ string. Please try again.</​p>"​
 +                            my_reply = pump.Comment(response)
 +                            success = reply(activity,​my_reply)
 +                            curl_buffer.close()
 +                            ​
 +                            process_log.write(activity.obj.id+"​\n"​)
 +                            print " ​  ... got bad data from the API, it seems. Letting the user know."
 +                            continue
 +                        pass
 +                    pass
 +                pass
 +            pass
 +        pass
 +    pass
 +    print "​Sleeping until next cycle..."​
 +    time.sleep(30)
 +
 +process_log.close()
 +    ​
 +
 +
  
 </​code>​ </​code>​
  
 +===== Talking to WeatherBot =====
 +
 +WeatherBot (e.g. [[https://​hub.polari.us/​weather]]) can now take various request forms. Sending these:
 +
 +<​code>​
 +New York,NY,US
 +or
 +Stockholm,​SE
 +</​code>​
 +
 +results in the original behavior - you get the current conditions.
 +
 +Sending any of these:
 +
 +<​code>​
 +Current conditions in Jefferson City,MO,US
 +Currently Uppsala,SE
 +Current in London,UK
 +Conditions for Moscow,RU
 +etc.
 +</​code>​
 +
 +also results in the current conditions.
 +
 +But sending any of these:
 +
 +<​code>​
 +Forecast for London,UK
 +Forecast in Dallas,​TX,​US
 +</​code>​
 +
 +will return a table of forecast data for the city in question, going out 3 days. This is just a first attempt to do simple request processing based on language, and, yes, it's only in English for now. But now you can at least get a forecast!
 +
 +{{ :​weatherbot_forecast.png?​direct&​300 |}}
  
weatherbot.1408814928.txt.gz ยท Last modified: 2014/08/23 13:28 by sekula