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