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 @@bot = Irc::Utils.bot
19 def initialize(station)
21 @url = "http://www.nws.noaa.gov/data/current_obs/#{URI.encode @station.upcase}.xml"
22 @current_conditions = String.new
26 resp = @@bot.httputil.get_response(@url)
29 cc_doc = (REXML::Document.new resp.body).root
30 @current_conditions = parse(cc_doc)
32 raise Net::HTTPError.new(_("couldn't get data for %{station} (%{message})") % {
33 :station => @station, :message => resp.message
37 if Net::HTTPError === e
41 raise "error retrieving data: #{e}"
48 cc_doc.elements.each do |c|
49 cc[c.name.to_sym] = c.text
51 cc[:time] = cc[:observation_time_rfc822]
52 cc[:wind] = cc[:wind_string]
53 cc[:temperature] = cc[:temperature_string]
54 cc[:heatindexorwindchill] = heat_index_or_wind_chill(cc)
55 cc[:pressure] = cc[:pressure_string]
57 _("At %{time} the conditions at %{location} (%{station_id}) were %{weather} with a visibility of %{visibility_mi}mi. The wind was %{wind} with %{relative_humidity}%% relative humidity. The temperature was %{temperature}%{heatindexorwindchill}, and the pressure was %{pressure}.") % cc
60 def heat_index_or_wind_chill(cc)
61 hi = cc[:heat_index_string]
62 wc = cc[:windchill_string]
63 if hi and hi != 'NA' then
64 _(" with a heat index of %{hi}") % { :hi => hi }
65 elsif wc and wc != 'NA' then
66 _(" with a windchill of %{wc}") % { :wc => wc }
73 class WeatherPlugin < Plugin
75 Config.register Config::BooleanValue.new('weather.advisory',
77 :desc => "Should the bot report special weather advisories when any is present?")
78 Config.register Config::EnumValue.new('weather.units',
79 :values => ['metric', 'english', 'both'],
81 :desc => "Units to be used by default in Weather Underground reports")
84 def help(plugin, topic="")
87 "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/ )"
89 "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."
91 "weather information lookup. Looks up weather information for the last location you specified. See topics 'nws' and 'wu' for more information"
100 @wu_url = "http://mobile.wunderground.com/cgi-bin/findweather/getForecast?brand=mobile%s&query=%s"
101 @wu_station_url = "http://mobile.wunderground.com/auto/mobile%s/global/stations/%s.html"
104 def weather(m, params)
105 where = params[:where].to_s
106 service = params[:service].to_sym rescue nil
107 units = params[:units]
109 if where.empty? or !service or !units and @registry.has_key?(m.sourcenick)
110 reg = @registry[m.sourcenick]
111 debug "loaded weather info #{reg.inspect} for #{m.sourcenick}"
112 service = reg.first.to_sym if !service
113 where = reg[1].to_s if where.empty?
114 units = reg[2] rescue nil
118 if where.sub!(/^station\s+/,'')
126 debug "No weather location found for #{m.sourcenick}"
127 m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
131 wu_units = String.new
133 case (units || @bot.config['weather.units']).to_sym
134 when :english, :metric
135 wu_units = "_#{units}"
138 m.reply "Ignoring unknown units #{units}"
143 nws_describe(m, where)
145 wu_station(m, where, wu_units)
147 wu_weather(m, where, wu_units)
149 google_weather(m, where)
151 m.reply "I don't know the weather service #{service}, sorry"
155 @registry[m.sourcenick] = [service, where, units]
158 def nws_describe(m, where)
159 if @nws_cache.has_key?(where) then
160 met = @nws_cache[where]
162 met = CurrentConditions.new(where)
167 @nws_cache[where] = met
168 rescue Net::HTTPError => e
169 m.reply _("%{error}, will try WU service") % { :error => e.message }
175 m.reply "couldn't find weather data for #{where}"
179 def wu_station(m, where, units="")
181 xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
184 m.reply "couldn't retrieve weather information, sorry"
186 when /Search not found:/
187 m.reply "no such station found (#{where})"
189 when /<table border.*?>(.*?)<\/table>/m
191 m.reply wu_weather_filter(data)
192 wu_out_special(m, xml)
195 m.reply "something went wrong with the data for #{where}..."
198 m.reply "retrieving info about '#{where}' failed (#{e})"
202 def wu_weather(m, where, units="")
204 xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
207 m.reply "couldn't retrieve weather information, sorry"
208 when /City Not Found/
209 m.reply "no such location found (#{where})"
212 xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
213 data += wu_weather_filter(match.first)
218 m.reply "couldn't parse weather data from #{where}"
220 wu_out_special(m, xml)
221 when /<a href="\/auto\/mobile[^\/]+\/(?:global\/stations|[A-Z][A-Z])\//
222 wu_weather_multi(m, xml)
225 m.reply "something went wrong with the data from #{where}..."
228 m.reply "retrieving info about '#{where}' failed (#{e})"
232 def wu_weather_multi(m, xml)
234 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)
236 m.reply "multiple stations available, use 'weather station <code>' or 'weather <city, state>' as appropriate, for one of the following (current temp shown):"
243 if state # US station
244 (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, w.ircify_html])
245 else # non-US station
246 (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, w.ircify_html])
249 m.reply stations.join("; ")
252 def wu_check_special(xml)
254 # We only scan the first half to prevent getting the advisories twice
255 xml[0,xml.length/2].scan(%r{<a href="([^"]+\?[^"]*feature=warning#([^"]+))"[^>]*>([^<]+)</a>}) do
257 :url => "http://mobile.wunderground.com"+$1,
261 spec_rx = Regexp.new("<a name=\"#{special[:type]}\">(?:.+?)<td align=\"left\">\\s+(.+?)\\s+</td>\\s+</tr>\\s+</table>", Regexp::MULTILINE)
262 spec_xml = @bot.httputil.get(special[:url])
263 if spec_xml and spec_td = spec_xml.match(spec_rx)
264 special.merge!(:text => spec_td.captures.first.ircify_html)
271 def wu_out_special(m, xml)
272 return unless @bot.config['weather.advisory']
273 specials = wu_check_special(xml)
275 specials.each do |special|
276 special.merge!(:underline => Underline)
278 m.reply("%{underline}%{special}%{underline}: %{text}" % special)
280 m.reply("%{underline}%{special}%{underline} @ %{url}" % special)
285 def wu_weather_filter(stuff)
287 if stuff.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
288 result << ("Weather info for %s (updated on %s)" % [$2.ircify_html, $1.ircify_html])
290 stuff.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/m) { |k, v|
294 next if ["-", "- approx.", "N/A", "N/A approx."].include?(vv)
295 next if kk == "Raw METAR"
296 result << ("%s: %s" % [kk, vv])
298 return result.join('; ')
301 # TODO allow units choice other than lang, find how the API does it
302 def google_weather(m, where)
303 botlang = @bot.config['core.language'].intern
304 if Language::Lang2Locale.key?(botlang)
305 lang = Language::Lang2Locale[botlang].sub(/.UTF.?8$/,'')
307 lang = botlang.to_s[0,2]
310 debug "Google weather with language #{lang}"
311 xml = @bot.httputil.get("http://www.google.com/ig/api?hl=#{lang}&weather=#{CGI.escape where}")
313 weather = REXML::Document.new(xml).root.elements["weather"]
315 error = weather.elements["problem_cause"]
317 ermsg = error.attributes["data"]
318 ermsg = _("no reason specified") if ermsg.empty?
321 city = weather.elements["forecast_information/city"].attributes["data"]
322 date = Time.parse(weather.elements["forecast_information/current_date_time"].attributes["data"])
323 units = weather.elements["forecast_information/unit_system"].attributes["data"].intern
324 current_conditions = weather.elements["current_conditions"]
325 foreconds = weather.elements.to_a("forecast_conditions")
328 current_conditions.each { |el|
329 name = el.name.intern
330 value = el.attributes["data"].dup
346 foreconds.each { |forecast|
349 name = el.name.intern
350 value = el.attributes["data"]
355 value << (units == :SI ? "°C" : "°F")
356 value << " |" if name == :low
362 forecasts << cond.join(' ')
365 m.reply _("Google weather info for %{city} on %{date}: %{conds}. Three-day forecast: %{forecast}") % {
368 :conds => conds.join(', '),
369 :forecast => forecasts.join('; ')
373 m.reply _("Google weather failed: %{e}") % { :e => e}
380 plugin = WeatherPlugin.new
381 plugin.map 'weather :units :service *where',
388 :units => /metric|english|both/,
389 :service => /wu|nws|station|google/