User Tools

Site Tools


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.

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

Copyright 2014, Stephen Jacob Sekula https://hub.polari.us/steve

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

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

  • 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

Source 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()
    


Talking to WeatherBot

WeatherBot (e.g. https://hub.polari.us/weather) can now take various request forms. Sending these:

New York,NY,US
or
Stockholm,SE

results in the original behavior - you get the current conditions.

Sending any of these:

Current conditions in Jefferson City,MO,US
Currently Uppsala,SE
Current in London,UK
Conditions for Moscow,RU
etc.

also results in the current conditions.

But sending any of these:

Forecast for London,UK
Forecast in Dallas,TX,US

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.txt · Last modified: 2014/08/30 22:23 by sekula