]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/weather.rb
weather plugin: fix detection of weather page
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / weather.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Weather plugin for rbot
5 #
6 # Author:: MrChucho (mrchucho@mrchucho.net): NOAA National Weather Service support
7 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
8 #
9 # Copyright:: (C) 2006 Ralph M. Churchill
10 # Copyright:: (C) 2006-2007 Giuseppe Bilotta
11 #
12 # License:: GPL v2
13
14 require 'rexml/document'
15
16 # Wraps NOAA National Weather Service information
17 class CurrentConditions
18     def initialize(station)
19         @station = station
20         @url = "http://www.nws.noaa.gov/data/current_obs/#{@station.upcase}.xml"
21         @etag = String.new
22         @mtime = Time.mktime(0)
23         @current_conditions = String.new
24         @iscached = false
25     end
26     def update
27         begin
28             open(@url,"If-Modified-Since" => @mtime.rfc2822) do |feed|
29             # open(@url,"If-None-Match"=>@etag) do |feed|
30                 @etag = feed.meta['etag']
31                 @mtime = feed.last_modified
32                 cc_doc = (REXML::Document.new feed).root
33                 @iscached = false
34                 @current_conditions = parse(cc_doc)
35             end
36         rescue OpenURI::HTTPError => e
37             case e
38             when /304/:
39                 @iscached = true
40             when /404/:
41                 raise "Data for #{@station} not found"
42             else
43                 raise "Error retrieving data: #{e}"
44             end
45         end
46         @current_conditions # +" Cached? "+ ((@iscached) ? "Y" : "N")
47     end
48     def parse(cc_doc)
49         cc = Hash.new
50         cc_doc.elements.each do |c|
51             cc[c.name.to_sym] = c.text
52         end
53         "At #{cc[:observation_time_rfc822]}, the wind was #{cc[:wind_string]} at #{cc[:location]} (#{cc[:station_id]}). The temperature was #{cc[:temperature_string]}#{heat_index_or_wind_chill(cc)}, and the pressure was #{cc[:pressure_string]}. The relative humidity was #{cc[:relative_humidity]}%. Current conditions are #{cc[:weather]} with #{cc[:visibility_mi]}mi visibility."
54     end
55 private
56     def heat_index_or_wind_chill(cc)
57         hi = cc[:heat_index_string]
58         wc = cc[:windchill_string]
59         if hi != 'NA' then
60             " with a heat index of #{hi}"
61         elsif wc != 'NA' then
62             " with a windchill of #{wc}"
63         else
64             ""
65         end
66     end
67 end
68
69 class WeatherPlugin < Plugin
70   
71   def help(plugin, topic="")
72     case topic
73     when "nws"
74       "weather nws <station> => display the current conditions at the location specified by the NOAA National Weather Service station code <station> ( lookup your station code at http://www.nws.noaa.gov/data/current_obs/ )"
75     when "station", "wu"
76       "weather [<units>] <location> => display the current conditions at the location specified, looking it up on the Weather Underground site; you can use 'station <code>' to look up data by station code ( lookup your station code at http://www.weatherunderground.com/ ); you can optionally set <units>  to 'metric' or 'english' if you only want data with the units; use 'both' for units to go back to having both." 
77     else
78       "weather information lookup. Looks up weather information for the last location you specified. See topics 'nws' and 'wu' for more information"
79     end
80   end
81   
82   def initialize
83     super
84
85     @nws_cache = Hash.new
86
87     @wu_url         = "http://mobile.wunderground.com/cgi-bin/findweather/getForecast?brand=mobile%s&query=%s"
88     @wu_station_url = "http://mobile.wunderground.com/auto/mobile%s/global/stations/%s.html"
89   end
90   
91   def weather(m, params)
92     if params[:where].empty?
93       if @registry.has_key?(m.sourcenick)
94         where = @registry[m.sourcenick]
95         debug "Loaded weather info #{where.inspect} for #{m.sourcenick}"
96
97         service = where.first.to_sym
98         loc = where[1].to_s
99         units = params[:units] || where[2] rescue nil
100       else
101         debug "No weather info for #{m.sourcenick}"
102         m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
103         return
104       end
105     else
106       where = params[:where]
107       if ['nws','station'].include?(where.first)
108         service = where.first.to_sym
109         loc = where[1].to_s
110       else
111         service = :wu
112         loc = where.to_s
113       end
114       units = params[:units]
115     end
116
117     if loc.empty?
118       debug "No weather location found for #{m.sourcenick}"
119       m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
120       return
121     end
122
123     wu_units = String.new
124     if units
125       case units.to_sym
126       when :english, :metric
127         wu_units = "_#{units}"
128       when :both
129       else
130         m.reply "Ignoring unknown units #{units}"
131         wu_units = String.new
132       end
133     end
134
135     case service
136     when :nws
137       nws_describe(m, loc)
138     when :station
139       wu_station(m, loc, wu_units)
140     when :wu
141       wu_weather(m, loc, wu_units)
142     end
143
144     @registry[m.sourcenick] = [service, loc, units]
145   end
146
147   def nws_describe(m, where)
148     if @nws_cache.has_key?(where) then
149         met = @nws_cache[where]
150     else
151         met = CurrentConditions.new(where)
152     end
153     if met
154       begin
155         m.reply met.update
156         @nws_cache[where] = met
157       rescue => e
158         m.reply e.message
159       end
160     else
161       m.reply "couldn't find weather data for #{where}"
162     end
163   end
164
165   def wu_station(m, where, units)
166     begin
167       xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
168       case xml
169       when nil
170         m.reply "couldn't retrieve weather information, sorry"
171         return
172       when /Search not found:/
173         m.reply "no such station found (#{where})"
174         return
175       when /<table border.*?>(.*?)<\/table>/m
176         data = $1
177         m.reply wu_weather_filter(data)
178       else
179         debug xml
180         m.reply "something went wrong with the data for #{where}..."
181       end
182     rescue => e
183       m.reply "retrieving info about '#{where}' failed (#{e})"
184     end
185   end
186
187   def wu_weather(m, where, units)
188     begin
189       xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
190       case xml
191       when nil
192         m.reply "couldn't retrieve weather information, sorry"
193       when /City Not Found/
194         m.reply "no such location found (#{where})"
195       when /Current<\/a>/
196         data = ""
197         xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
198           data += wu_weather_filter(match.first)
199         end
200         if data.length > 0
201           m.reply data
202         else
203           m.reply "couldn't parse weather data from #{where}"
204         end
205       when /<a href="\/global\/stations\//
206         stations = xml.scan(/<a href="\/global\/stations\/(.*?)\.html">/)
207         m.reply "multiple stations available, use 'weather station <code>' where code is one of " + stations.join(", ")
208       else
209         debug xml
210         m.reply "something went wrong with the data from #{where}..."
211       end
212     rescue => e
213       m.reply "retrieving info about '#{where}' failed (#{e})"
214     end
215   end
216
217   def wu_weather_filter(stuff)
218     txt = stuff
219     txt.gsub!(/[\n\s]+/,' ')
220     data = Hash.new
221     txt.gsub!(/&nbsp;/, ' ')
222     txt.gsub!(/&#176;/, ' ') # degree sign
223     txt.gsub!(/<\/?b>/,'')
224     txt.gsub!(/<\/?span[^<>]*?>/,'')
225     txt.gsub!(/<img\s*[^<>]*?>/,'')
226     txt.gsub!(/<br\s?\/?>/,'')
227
228     result = Array.new
229     if txt.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
230       result << ("Weather info for %s (updated on %s)" % [$2, $1])
231     end
232     txt.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/) { |k, v|
233       next if v.empty?
234       next if ["-", "- approx.", "N/A", "N/A approx."].include?(v)
235       next if k == "Raw METAR"
236       result << ("%s: %s" % [k, v])
237     }
238     return result.join('; ')
239   end
240 end
241
242 plugin = WeatherPlugin.new
243 plugin.map 'weather :units *where', :defaults => {:where => false, :units => false}, :requirements => {:units => /metric|english|both/}