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 "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 def heat_index_or_wind_chill(cc)
55 hi = cc[:heat_index_string]
56 wc = cc[:windchill_string]
58 " with a heat index of #{hi}"
60 " with a windchill of #{wc}"
67 class WeatherPlugin < Plugin
69 Config.register Config::BooleanValue.new('weather.advisory',
71 :desc => "Should the bot report special weather advisories when any is present?")
72 Config.register Config::EnumValue.new('weather.units',
73 :values => ['metric', 'english', 'both'],
75 :desc => "Units to be used by default in Weather Underground reports")
78 def help(plugin, topic="")
81 "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/ )"
83 "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."
85 "weather information lookup. Looks up weather information for the last location you specified. See topics 'nws' and 'wu' for more information"
94 @wu_url = "http://mobile.wunderground.com/cgi-bin/findweather/getForecast?brand=mobile%s&query=%s"
95 @wu_station_url = "http://mobile.wunderground.com/auto/mobile%s/global/stations/%s.html"
98 def weather(m, params)
99 where = params[:where].to_s
100 service = params[:service].to_sym rescue nil
101 units = params[:units]
103 if where.empty? or !service or !units and @registry.has_key?(m.sourcenick)
104 reg = @registry[m.sourcenick]
105 debug "loaded weather info #{reg.inspect} for #{m.sourcenick}"
106 service = reg.first.to_sym if !service
107 where = reg[1].to_s if where.empty?
108 units = reg[2] rescue nil
112 if where.sub!(/^station\s+/,'')
120 debug "No weather location found for #{m.sourcenick}"
121 m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
125 wu_units = String.new
127 case (units || @bot.config['weather.units']).to_sym
128 when :english, :metric
129 wu_units = "_#{units}"
132 m.reply "Ignoring unknown units #{units}"
137 nws_describe(m, where)
139 wu_station(m, where, wu_units)
141 wu_weather(m, where, wu_units)
143 google_weather(m, where)
145 m.reply "I don't know the weather service #{service}, sorry"
149 @registry[m.sourcenick] = [service, where, units]
152 def nws_describe(m, where)
153 if @nws_cache.has_key?(where) then
154 met = @nws_cache[where]
156 met = CurrentConditions.new(where)
161 @nws_cache[where] = met
162 rescue Net::HTTPError => e
163 m.reply _("%{error}, will try WU service") % { :error => e.message }
169 m.reply "couldn't find weather data for #{where}"
173 def wu_station(m, where, units="")
175 xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
178 m.reply "couldn't retrieve weather information, sorry"
180 when /Search not found:/
181 m.reply "no such station found (#{where})"
183 when /<table border.*?>(.*?)<\/table>/m
185 m.reply wu_weather_filter(data)
186 wu_out_special(m, xml)
189 m.reply "something went wrong with the data for #{where}..."
192 m.reply "retrieving info about '#{where}' failed (#{e})"
196 def wu_weather(m, where, units="")
198 xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
201 m.reply "couldn't retrieve weather information, sorry"
202 when /City Not Found/
203 m.reply "no such location found (#{where})"
206 xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
207 data += wu_weather_filter(match.first)
212 m.reply "couldn't parse weather data from #{where}"
214 wu_out_special(m, xml)
215 when /<a href="\/auto\/mobile[^\/]+\/(?:global\/stations|[A-Z][A-Z])\//
216 wu_weather_multi(m, xml)
219 m.reply "something went wrong with the data from #{where}..."
222 m.reply "retrieving info about '#{where}' failed (#{e})"
226 def wu_weather_multi(m, xml)
228 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)
230 m.reply "multiple stations available, use 'weather station <code>' or 'weather <city, state>' as appropriate, for one of the following (current temp shown):"
237 if state # US station
238 (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, w.ircify_html])
239 else # non-US station
240 (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, w.ircify_html])
243 m.reply stations.join("; ")
246 def wu_check_special(xml)
248 # We only scan the first half to prevent getting the advisories twice
249 xml[0,xml.length/2].scan(%r{<a href="([^"]+\?[^"]*feature=warning#([^"]+))"[^>]*>([^<]+)</a>}) do
251 :url => "http://mobile.wunderground.com"+$1,
255 spec_rx = Regexp.new("<a name=\"#{special[:type]}\">(?:.+?)<td align=\"left\">\\s+(.+?)\\s+</td>\\s+</tr>\\s+</table>", Regexp::MULTILINE)
256 spec_xml = @bot.httputil.get(special[:url])
257 if spec_xml and spec_td = spec_xml.match(spec_rx)
258 special.merge!(:text => spec_td.captures.first.ircify_html)
265 def wu_out_special(m, xml)
266 return unless @bot.config['weather.advisory']
267 specials = wu_check_special(xml)
269 specials.each do |special|
270 special.merge!(:underline => Underline)
272 m.reply("%{underline}%{special}%{underline}: %{text}" % special)
274 m.reply("%{underline}%{special}%{underline} @ %{url}" % special)
279 def wu_weather_filter(stuff)
281 if stuff.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
282 result << ("Weather info for %s (updated on %s)" % [$2.ircify_html, $1.ircify_html])
284 stuff.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/m) { |k, v|
288 next if ["-", "- approx.", "N/A", "N/A approx."].include?(vv)
289 next if kk == "Raw METAR"
290 result << ("%s: %s" % [kk, vv])
292 return result.join('; ')
295 # TODO allow units choice other than lang, find how the API does it
296 def google_weather(m, where)
297 botlang = @bot.config['core.language'].intern
298 if Language::Lang2Locale.key?(botlang)
299 lang = Language::Lang2Locale[botlang].sub(/.UTF.?8$/,'')
301 lang = botlang.to_s[0,2]
304 debug "Google weather with language #{lang}"
305 xml = @bot.httputil.get("http://www.google.com/ig/api?hl=#{lang}&weather=#{CGI.escape where}")
307 weather = REXML::Document.new(xml).root.elements["weather"]
309 error = weather.elements["problem_cause"]
311 ermsg = error.attributes["data"]
312 ermsg = _("no reason specified") if ermsg.empty?
315 city = weather.elements["forecast_information/city"].attributes["data"]
316 date = Time.parse(weather.elements["forecast_information/current_date_time"].attributes["data"])
317 units = weather.elements["forecast_information/unit_system"].attributes["data"].intern
318 current_conditions = weather.elements["current_conditions"]
319 foreconds = weather.elements.to_a("forecast_conditions")
322 current_conditions.each { |el|
323 name = el.name.intern
324 value = el.attributes["data"].dup
340 foreconds.each { |forecast|
343 name = el.name.intern
344 value = el.attributes["data"]
349 value << (units == :SI ? "°C" : "°F")
350 value << " |" if name == :low
356 forecasts << cond.join(' ')
359 m.reply _("Google weather info for %{city} on %{date}: %{conds}. Three-day forecast: %{forecast}") % {
362 :conds => conds.join(', '),
363 :forecast => forecasts.join('; ')
367 m.reply _("Google weather failed: %{e}") % { :e => e}
374 plugin = WeatherPlugin.new
375 plugin.map 'weather :units :service *where',
382 :units => /metric|english|both/,
383 :service => /wu|nws|station|google/