4 # :title: Weather plugin for rbot
6 # Author:: MrChucho (mrchucho@mrchucho.net): NOAA National Weather Service support
7 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
9 # Copyright:: (C) 2006 Ralph M. Churchill
10 # Copyright:: (C) 2006-2007 Giuseppe Bilotta
14 require 'rexml/document'
16 # Wraps NOAA National Weather Service information
17 class CurrentConditions
18 def initialize(station)
20 @url = "http://www.nws.noaa.gov/data/current_obs/#{@station.upcase}.xml"
22 @mtime = Time.mktime(0)
23 @current_conditions = String.new
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
34 @current_conditions = parse(cc_doc)
36 rescue OpenURI::HTTPError => e
41 raise "Data for #{@station} not found"
43 raise "Error retrieving data: #{e}"
46 @current_conditions # +" Cached? "+ ((@iscached) ? "Y" : "N")
50 cc_doc.elements.each do |c|
51 cc[c.name.to_sym] = c.text
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."
56 def heat_index_or_wind_chill(cc)
57 hi = cc[:heat_index_string]
58 wc = cc[:windchill_string]
60 " with a heat index of #{hi}"
62 " with a windchill of #{wc}"
69 class WeatherPlugin < Plugin
71 Config.register Config::BooleanValue.new('weather.advisory',
73 :desc => "Should the bot report special weather advisories when any is present?")
75 def help(plugin, topic="")
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/ )"
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."
82 "weather information lookup. Looks up weather information for the last location you specified. See topics 'nws' and 'wu' for more information"
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"
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}"
101 service = where.first.to_sym
103 units = params[:units] || where[2] rescue nil
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"
110 where = params[:where]
111 if ['nws','station'].include?(where.first)
112 service = where.first.to_sym
118 units = params[:units]
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"
127 wu_units = String.new
130 when :english, :metric
131 wu_units = "_#{units}"
134 m.reply "Ignoring unknown units #{units}"
135 wu_units = String.new
143 wu_station(m, loc, wu_units)
145 wu_weather(m, loc, wu_units)
148 @registry[m.sourcenick] = [service, loc, units]
151 def nws_describe(m, where)
152 if @nws_cache.has_key?(where) then
153 met = @nws_cache[where]
155 met = CurrentConditions.new(where)
160 @nws_cache[where] = met
165 m.reply "couldn't find weather data for #{where}"
169 def wu_station(m, where, units)
171 xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
174 m.reply "couldn't retrieve weather information, sorry"
176 when /Search not found:/
177 m.reply "no such station found (#{where})"
179 when /<table border.*?>(.*?)<\/table>/m
181 m.reply wu_weather_filter(data)
182 wu_out_special(m, xml)
185 m.reply "something went wrong with the data for #{where}..."
188 m.reply "retrieving info about '#{where}' failed (#{e})"
192 def wu_weather(m, where, units)
194 xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
197 m.reply "couldn't retrieve weather information, sorry"
198 when /City Not Found/
199 m.reply "no such location found (#{where})"
202 xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
203 data += wu_weather_filter(match.first)
208 m.reply "couldn't parse weather data from #{where}"
210 wu_out_special(m, xml)
211 when /<a href="\/(?:global\/stations|US\/\w\w)\//
212 wu_weather_multi(m, xml)
215 m.reply "something went wrong with the data from #{where}..."
218 m.reply "retrieving info about '#{where}' failed (#{e})"
224 txt.gsub!(/[\n\s]+/,' ')
225 txt.gsub!(/ /, ' ')
226 txt.gsub!(/°/, ' ') # degree sign
227 txt.gsub!(/<\/?b>/,'')
228 txt.gsub!(/<\/?span[^<>]*?>/,'')
229 txt.gsub!(/<img\s*[^<>]*?>/,'')
230 txt.gsub!(/<br\s?\/?>/,'')
234 def wu_weather_multi(m, 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)
238 m.reply "multiple stations available, use 'weather station <code>' or 'weather <city, state>' as appropriate, for one of the following (current temp shown):"
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)])
251 m.reply stations.join("; ")
254 def wu_check_special(xml)
256 # We only scan the first half to prevent getting the advisories twice
257 xml[0,xml.length/2].scan(%r{<a href="([^"]+\?[^"]*feature=warning#([^"]+))"[^>]*>([^<]+)</a>}) do
259 :url => "http://mobile.wunderground.com"+$1,
263 spec_rx = Regexp.new("<a name=\"#{special[:type]}\">(?:.+?)<td align=\"left\">\\s+(.+?)\\s+</td>\\s+</tr>\\s+</table>", Regexp::MULTILINE)
264 spec_xml = @bot.httputil.get(special[:url])
265 if spec_xml and spec_td = spec_xml.match(spec_rx)
266 special.merge!(:text => spec_td.captures.first.ircify_html)
273 def wu_out_special(m, xml)
274 return unless @bot.config['weather.advisory']
275 specials = wu_check_special(xml)
277 specials.each do |special|
278 special.merge!(:underline => Underline)
280 m.reply("%{underline}%{special}%{underline}: %{text}" % special)
282 m.reply("%{underline}%{special}%{underline} @ %{url}" % special)
287 def wu_weather_filter(stuff)
288 txt = wu_clean(stuff)
291 if txt.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
292 result << ("Weather info for %s (updated on %s)" % [$2, $1])
294 txt.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/) { |k, v|
296 next if ["-", "- approx.", "N/A", "N/A approx."].include?(v)
297 next if k == "Raw METAR"
298 result << ("%s: %s" % [k, v])
300 return result.join('; ')
304 plugin = WeatherPlugin.new
305 plugin.map 'weather :units *where', :defaults => {:where => false, :units => false}, :requirements => {:units => /metric|english|both/}