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?")
74 Config.register Config::EnumValue.new('weather.units',
75 :values => ['metric', 'english', 'both'],
77 :desc => "Units to be used by default in Weather Underground reports")
80 def help(plugin, topic="")
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/ )"
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."
87 "weather information lookup. Looks up weather information for the last location you specified. See topics 'nws' and 'wu' for more information"
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"
100 def weather(m, params)
101 where = params[:where].to_s
102 service = params[:service].to_sym rescue nil
103 units = params[:units]
105 if where.empty? or !service or !units and @registry.has_key?(m.sourcenick)
106 reg = @registry[m.sourcenick]
107 debug "loaded weather info #{reg.inspect} for #{m.sourcenick}"
108 service = reg.first.to_sym if !service
109 where = reg[1].to_s if where.empty?
110 units = reg[2] rescue nil
114 if where.sub!(/^station\s+/,'')
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
129 case (units || @bot.config['weather.units']).to_sym
130 when :english, :metric
131 wu_units = "_#{units}"
134 m.reply "Ignoring unknown units #{units}"
139 nws_describe(m, where)
141 wu_station(m, where, wu_units)
143 wu_weather(m, where, wu_units)
145 google_weather(m, where)
147 m.reply "I don't know the weather service #{service}, sorry"
151 @registry[m.sourcenick] = [service, where, units]
154 def nws_describe(m, where)
155 if @nws_cache.has_key?(where) then
156 met = @nws_cache[where]
158 met = CurrentConditions.new(where)
163 @nws_cache[where] = met
168 m.reply "couldn't find weather data for #{where}"
172 def wu_station(m, where, units)
174 xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
177 m.reply "couldn't retrieve weather information, sorry"
179 when /Search not found:/
180 m.reply "no such station found (#{where})"
182 when /<table border.*?>(.*?)<\/table>/m
184 m.reply wu_weather_filter(data)
185 wu_out_special(m, xml)
188 m.reply "something went wrong with the data for #{where}..."
191 m.reply "retrieving info about '#{where}' failed (#{e})"
195 def wu_weather(m, where, units)
197 xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
200 m.reply "couldn't retrieve weather information, sorry"
201 when /City Not Found/
202 m.reply "no such location found (#{where})"
205 xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
206 data += wu_weather_filter(match.first)
211 m.reply "couldn't parse weather data from #{where}"
213 wu_out_special(m, xml)
214 when /<a href="\/auto\/mobile[^\/]+\/(?:global\/stations|[A-Z][A-Z])\//
215 wu_weather_multi(m, xml)
218 m.reply "something went wrong with the data from #{where}..."
221 m.reply "retrieving info about '#{where}' failed (#{e})"
225 def wu_weather_multi(m, xml)
227 stations = xml.scan(/<td>\s*(?:<a href="([^?"]+\?feature=[^"]+)"\s*[^>]*><img [^>]+><\/a>\s*)?<a href="\/auto\/mobile[^\/]+\/(?:global\/stations|([A-Z][A-Z]))\/([^"]*?)\.html">(.*?)<\/a>\s*:\s*(.*?)<\/td>/m)
229 m.reply "multiple stations available, use 'weather station <code>' or 'weather <city, state>' as appropriate, for one of the following (current temp shown):"
236 if state # US station
237 (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, w.ircify_html])
238 else # non-US station
239 (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, w.ircify_html])
242 m.reply stations.join("; ")
245 def wu_check_special(xml)
247 # We only scan the first half to prevent getting the advisories twice
248 xml[0,xml.length/2].scan(%r{<a href="([^"]+\?[^"]*feature=warning#([^"]+))"[^>]*>([^<]+)</a>}) do
250 :url => "http://mobile.wunderground.com"+$1,
254 spec_rx = Regexp.new("<a name=\"#{special[:type]}\">(?:.+?)<td align=\"left\">\\s+(.+?)\\s+</td>\\s+</tr>\\s+</table>", Regexp::MULTILINE)
255 spec_xml = @bot.httputil.get(special[:url])
256 if spec_xml and spec_td = spec_xml.match(spec_rx)
257 special.merge!(:text => spec_td.captures.first.ircify_html)
264 def wu_out_special(m, xml)
265 return unless @bot.config['weather.advisory']
266 specials = wu_check_special(xml)
268 specials.each do |special|
269 special.merge!(:underline => Underline)
271 m.reply("%{underline}%{special}%{underline}: %{text}" % special)
273 m.reply("%{underline}%{special}%{underline} @ %{url}" % special)
278 def wu_weather_filter(stuff)
280 if stuff.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
281 result << ("Weather info for %s (updated on %s)" % [$2.ircify_html, $1.ircify_html])
283 stuff.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/m) { |k, v|
287 next if ["-", "- approx.", "N/A", "N/A approx."].include?(vv)
288 next if kk == "Raw METAR"
289 result << ("%s: %s" % [kk, vv])
291 return result.join('; ')
294 # TODO allow units choice other than lang, find how the API does it
295 def google_weather(m, where)
296 botlang = @bot.config['core.language'].intern
297 if Language::Lang2Locale.key?(botlang)
298 lang = Language::Lang2Locale[botlang].sub(/.UTF.?8$/,'')
300 lang = botlang.to_s[0,2]
303 debug "Google weather with language #{lang}"
304 xml = @bot.httputil.get("http://www.google.com/ig/api?hl=#{lang}&weather=#{CGI.escape where}")
306 weather = REXML::Document.new(xml).root.elements["weather"]
308 error = weather.elements["problem_cause"]
310 ermsg = error.attributes["data"]
311 ermsg = _("no reason specified") if ermsg.empty?
314 city = weather.elements["forecast_information/city"].attributes["data"]
315 date = Time.parse(weather.elements["forecast_information/current_date_time"].attributes["data"])
316 units = weather.elements["forecast_information/unit_system"].attributes["data"].intern
317 current_conditions = weather.elements["current_conditions"]
318 foreconds = weather.elements.to_a("forecast_conditions")
321 current_conditions.each { |el|
322 name = el.name.intern
323 value = el.attributes["data"].dup
339 foreconds.each { |forecast|
342 name = el.name.intern
343 value = el.attributes["data"]
348 value << (units == :SI ? "°C" : "°F")
349 value << " |" if name == :low
355 forecasts << cond.join(' ')
358 m.reply _("Google weather info for %{city} on %{date}: %{conds}. Three-day forecast: %{forecast}") % {
361 :conds => conds.join(', '),
362 :forecast => forecasts.join('; ')
366 m.reply _("Google weather failed: %{e}") % { :e => e}
373 plugin = WeatherPlugin.new
374 plugin.map 'weather :units :service *where',
381 :units => /metric|english|both/,
382 :service => /wu|nws|station|google/