]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/weather.rb
weather plugin: advisories can be disabled
[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   Config.register Config::BooleanValue.new('weather.advisory',
72     :default => true,
73     :desc => "Should the bot report special weather advisories when any is present?")
74   
75   def help(plugin, topic="")
76     case topic
77     when "nws"
78       "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/ )"
79     when "station", "wu"
80       "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." 
81     else
82       "weather information lookup. Looks up weather information for the last location you specified. See topics 'nws' and 'wu' for more information"
83     end
84   end
85   
86   def initialize
87     super
88
89     @nws_cache = Hash.new
90
91     @wu_url         = "http://mobile.wunderground.com/cgi-bin/findweather/getForecast?brand=mobile%s&query=%s"
92     @wu_station_url = "http://mobile.wunderground.com/auto/mobile%s/global/stations/%s.html"
93   end
94   
95   def weather(m, params)
96     if params[:where].empty?
97       if @registry.has_key?(m.sourcenick)
98         where = @registry[m.sourcenick]
99         debug "Loaded weather info #{where.inspect} for #{m.sourcenick}"
100
101         service = where.first.to_sym
102         loc = where[1].to_s
103         units = params[:units] || where[2] rescue nil
104       else
105         debug "No weather info for #{m.sourcenick}"
106         m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
107         return
108       end
109     else
110       where = params[:where]
111       if ['nws','station'].include?(where.first)
112         service = where.first.to_sym
113         loc = where[1].to_s
114       else
115         service = :wu
116         loc = where.to_s
117       end
118       units = params[:units]
119     end
120
121     if loc.empty?
122       debug "No weather location found for #{m.sourcenick}"
123       m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
124       return
125     end
126
127     wu_units = String.new
128     if units
129       case units.to_sym
130       when :english, :metric
131         wu_units = "_#{units}"
132       when :both
133       else
134         m.reply "Ignoring unknown units #{units}"
135         wu_units = String.new
136       end
137     end
138
139     case service
140     when :nws
141       nws_describe(m, loc)
142     when :station
143       wu_station(m, loc, wu_units)
144     when :wu
145       wu_weather(m, loc, wu_units)
146     end
147
148     @registry[m.sourcenick] = [service, loc, units]
149   end
150
151   def nws_describe(m, where)
152     if @nws_cache.has_key?(where) then
153         met = @nws_cache[where]
154     else
155         met = CurrentConditions.new(where)
156     end
157     if met
158       begin
159         m.reply met.update
160         @nws_cache[where] = met
161       rescue => e
162         m.reply e.message
163       end
164     else
165       m.reply "couldn't find weather data for #{where}"
166     end
167   end
168
169   def wu_station(m, where, units)
170     begin
171       xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
172       case xml
173       when nil
174         m.reply "couldn't retrieve weather information, sorry"
175         return
176       when /Search not found:/
177         m.reply "no such station found (#{where})"
178         return
179       when /<table border.*?>(.*?)<\/table>/m
180         data = $1.dup
181         m.reply wu_weather_filter(data)
182         wu_out_special(m, xml)
183       else
184         debug xml
185         m.reply "something went wrong with the data for #{where}..."
186       end
187     rescue => e
188       m.reply "retrieving info about '#{where}' failed (#{e})"
189     end
190   end
191
192   def wu_weather(m, where, units)
193     begin
194       xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
195       case xml
196       when nil
197         m.reply "couldn't retrieve weather information, sorry"
198       when /City Not Found/
199         m.reply "no such location found (#{where})"
200       when /Current<\/a>/
201         data = ""
202         xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
203           data += wu_weather_filter(match.first)
204         end
205         if data.length > 0
206           m.reply data
207         else
208           m.reply "couldn't parse weather data from #{where}"
209         end
210         wu_out_special(m, xml)
211       when /<a href="\/(?:global\/stations|US\/\w\w)\//
212         wu_weather_multi(m, xml)
213       else
214         debug xml
215         m.reply "something went wrong with the data from #{where}..."
216       end
217     rescue => e
218       m.reply "retrieving info about '#{where}' failed (#{e})"
219     end
220   end
221
222   def wu_clean(stuff)
223     txt = stuff
224     txt.gsub!(/[\n\s]+/,' ')
225     txt.gsub!(/&nbsp;/, ' ')
226     txt.gsub!(/&#176;/, ' ') # degree sign
227     txt.gsub!(/<\/?b>/,'')
228     txt.gsub!(/<\/?span[^<>]*?>/,'')
229     txt.gsub!(/<img\s*[^<>]*?>/,'')
230     txt.gsub!(/<br\s?\/?>/,'')
231     txt
232   end
233
234   def wu_weather_multi(m, xml)
235     # debug xml
236     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)
237     # debug stations
238     m.reply "multiple stations available, use 'weather station <code>' or 'weather <city, state>' as appropriate, for one of the following (current temp shown):"
239     stations.map! { |ar|
240       warning = ar[0]
241       loc = ar[2]
242       state = ar[1]
243       par = ar[3]
244       w = ar[4]
245       if state # US station
246         (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, wu_clean(w)])
247       else # non-US station
248         (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, wu_clean(w)])
249       end
250     }
251     m.reply stations.join("; ")
252   end
253
254   def wu_check_special(xml)
255     if spec_match = xml.match(%r{<a href="([^"]+\?[^"]*feature=warning[^"]+)"[^>]*>([^<]+)</a>})
256       special = {
257         :url => "http://mobile.wunderground.com"+$1,
258         :special => $2.dup
259       }
260       spec_xml = @bot.httputil.get(special[:url])
261       if spec_xml and spec_td = spec_xml.match(/<tr>\s*<td align="left">\s*(.*?)\s*<\/td>\s*<\/tr>\s*<\/table>/m)
262         return special.merge(:text => $1.ircify_html)
263       else
264         return special
265       end
266     else
267       return nil
268     end
269   end
270
271   def wu_out_special(m, xml)
272     return unless @bot.config['weather.advisory']
273     special = wu_check_special(xml).merge(:underline => Underline)
274     if special
275       if special[:text]
276         m.reply("%{underline}%{special}%{underline}: %{text}" % special)
277       else
278         m.reply("%{underline}%{special}%{underline} @ %{url}" % special)
279       end
280     end
281   end
282
283   def wu_weather_filter(stuff)
284     txt = wu_clean(stuff)
285
286     result = Array.new
287     if txt.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
288       result << ("Weather info for %s (updated on %s)" % [$2, $1])
289     end
290     txt.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/) { |k, v|
291       next if v.empty?
292       next if ["-", "- approx.", "N/A", "N/A approx."].include?(v)
293       next if k == "Raw METAR"
294       result << ("%s: %s" % [k, v])
295     }
296     return result.join('; ')
297   end
298 end
299
300 plugin = WeatherPlugin.new
301 plugin.map 'weather :units *where', :defaults => {:where => false, :units => false}, :requirements => {:units => /metric|english|both/}