]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/weather.rb
4f89e08d37cf83fbe6d3610547bcd51fffac3b33
[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.dup
177         m.reply wu_weather_filter(data)
178         wu_out_special(m, xml)
179       else
180         debug xml
181         m.reply "something went wrong with the data for #{where}..."
182       end
183     rescue => e
184       m.reply "retrieving info about '#{where}' failed (#{e})"
185     end
186   end
187
188   def wu_weather(m, where, units)
189     begin
190       xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
191       case xml
192       when nil
193         m.reply "couldn't retrieve weather information, sorry"
194       when /City Not Found/
195         m.reply "no such location found (#{where})"
196       when /Current<\/a>/
197         data = ""
198         xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
199           data += wu_weather_filter(match.first)
200         end
201         if data.length > 0
202           m.reply data
203         else
204           m.reply "couldn't parse weather data from #{where}"
205         end
206         wu_out_special(m, xml)
207       when /<a href="\/(?:global\/stations|US\/\w\w)\//
208         wu_weather_multi(m, xml)
209       else
210         debug xml
211         m.reply "something went wrong with the data from #{where}..."
212       end
213     rescue => e
214       m.reply "retrieving info about '#{where}' failed (#{e})"
215     end
216   end
217
218   def wu_clean(stuff)
219     txt = stuff
220     txt.gsub!(/[\n\s]+/,' ')
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     txt
228   end
229
230   def wu_weather_multi(m, xml)
231     # debug xml
232     stations = xml.scan(/<td>\s*(?:<a href="([^?"]+\?feature=[^"]+)"\s*[^>]*><img [^>]+><\/a>\s*)?<a href="\/(?:global\/stations|US\/(\w\w))\/([^"]*?)\.html">(.*?)<\/a>\s*:\s*(.*?)<\/td>/m)
233     # debug stations
234     m.reply "multiple stations available, use 'weather station <code>' or 'weather <city, state>' as appropriate, for one of the following (current temp shown):"
235     stations.map! { |ar|
236       warning = ar[0]
237       loc = ar[2]
238       state = ar[1]
239       par = ar[3]
240       w = ar[4]
241       if state # US station
242         (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, wu_clean(w)])
243       else # non-US station
244         (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, wu_clean(w)])
245       end
246     }
247     m.reply stations.join("; ")
248   end
249
250   def wu_check_special(xml)
251     if spec_match = xml.match(%r{<a href="([^"]+\?[^"]*feature=warning[^"]+)"[^>]*>([^<]+)</a>})
252       special = {
253         :url => "http://mobile.wunderground.com"+$1,
254         :special => $2.dup
255       }
256       spec_xml = @bot.httputil.get(special[:url])
257       if spec_xml and spec_td = spec_xml.match(/<tr>\s*<td align="left">\s*(.*?)\s*<\/td>\s*<\/tr>\s*<\/table>/m)
258         return special.merge(:text => $1.ircify_html)
259       else
260         return special
261       end
262     else
263       return nil
264     end
265   end
266
267   def wu_out_special(m, xml)
268     special = wu_check_special(xml).merge(:underline => Underline)
269     if special
270       if special[:text]
271         m.reply("%{underline}%{special}%{underline}: %{text}" % special)
272       else
273         m.reply("%{underline}%{special}%{underline} @ %{url}" % special)
274       end
275     end
276   end
277
278   def wu_weather_filter(stuff)
279     txt = wu_clean(stuff)
280
281     result = Array.new
282     if txt.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
283       result << ("Weather info for %s (updated on %s)" % [$2, $1])
284     end
285     txt.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/) { |k, v|
286       next if v.empty?
287       next if ["-", "- approx.", "N/A", "N/A approx."].include?(v)
288       next if k == "Raw METAR"
289       result << ("%s: %s" % [k, v])
290     }
291     return result.join('; ')
292   end
293 end
294
295 plugin = WeatherPlugin.new
296 plugin.map 'weather :units *where', :defaults => {:where => false, :units => false}, :requirements => {:units => /metric|english|both/}