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 debug "No weather location found for #{m.sourcenick}"
115 m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
119 wu_units = String.new
121 case (units || @bot.config['weather.units']).to_sym
122 when :english, :metric
123 wu_units = "_#{units}"
126 m.reply "Ignoring unknown units #{units}"
131 nws_describe(m, where)
133 wu_station(m, where, wu_units)
135 wu_weather(m, where, wu_units)
137 google_weather(m, where)
139 m.reply "I don't know the weather service #{service}, sorry"
143 @registry[m.sourcenick] = [service, where, units]
146 def nws_describe(m, where)
147 if @nws_cache.has_key?(where) then
148 met = @nws_cache[where]
150 met = CurrentConditions.new(where)
155 @nws_cache[where] = met
160 m.reply "couldn't find weather data for #{where}"
164 def wu_station(m, where, units)
166 xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
169 m.reply "couldn't retrieve weather information, sorry"
171 when /Search not found:/
172 m.reply "no such station found (#{where})"
174 when /<table border.*?>(.*?)<\/table>/m
176 m.reply wu_weather_filter(data)
177 wu_out_special(m, xml)
180 m.reply "something went wrong with the data for #{where}..."
183 m.reply "retrieving info about '#{where}' failed (#{e})"
187 def wu_weather(m, where, units)
189 xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
192 m.reply "couldn't retrieve weather information, sorry"
193 when /City Not Found/
194 m.reply "no such location found (#{where})"
197 xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
198 data += wu_weather_filter(match.first)
203 m.reply "couldn't parse weather data from #{where}"
205 wu_out_special(m, xml)
206 when /<a href="\/(?:global\/stations|US\/\w\w)\//
207 wu_weather_multi(m, xml)
210 m.reply "something went wrong with the data from #{where}..."
213 m.reply "retrieving info about '#{where}' failed (#{e})"
217 def wu_weather_multi(m, xml)
219 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)
221 m.reply "multiple stations available, use 'weather station <code>' or 'weather <city, state>' as appropriate, for one of the following (current temp shown):"
228 if state # US station
229 (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, w.ircify_html])
230 else # non-US station
231 (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, w.ircify_html])
234 m.reply stations.join("; ")
237 def wu_check_special(xml)
239 # We only scan the first half to prevent getting the advisories twice
240 xml[0,xml.length/2].scan(%r{<a href="([^"]+\?[^"]*feature=warning#([^"]+))"[^>]*>([^<]+)</a>}) do
242 :url => "http://mobile.wunderground.com"+$1,
246 spec_rx = Regexp.new("<a name=\"#{special[:type]}\">(?:.+?)<td align=\"left\">\\s+(.+?)\\s+</td>\\s+</tr>\\s+</table>", Regexp::MULTILINE)
247 spec_xml = @bot.httputil.get(special[:url])
248 if spec_xml and spec_td = spec_xml.match(spec_rx)
249 special.merge!(:text => spec_td.captures.first.ircify_html)
256 def wu_out_special(m, xml)
257 return unless @bot.config['weather.advisory']
258 specials = wu_check_special(xml)
260 specials.each do |special|
261 special.merge!(:underline => Underline)
263 m.reply("%{underline}%{special}%{underline}: %{text}" % special)
265 m.reply("%{underline}%{special}%{underline} @ %{url}" % special)
270 def wu_weather_filter(stuff)
272 if stuff.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
273 result << ("Weather info for %s (updated on %s)" % [$2.ircify_html, $1.ircify_html])
275 stuff.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/m) { |k, v|
279 next if ["-", "- approx.", "N/A", "N/A approx."].include?(vv)
280 next if kk == "Raw METAR"
281 result << ("%s: %s" % [kk, vv])
283 return result.join('; ')
286 # TODO allow units choice other than lang, find how the API does it
287 def google_weather(m, where)
288 botlang = @bot.config['core.language'].intern
289 if Language::Lang2Locale.key?(botlang)
290 lang = Language::Lang2Locale[botlang].sub(/.UTF.?8$/,'')
292 lang = botlang.to_s[0,2]
295 debug "Google weather with language #{lang}"
296 xml = @bot.httputil.get("http://www.google.com/ig/api?hl=#{lang}&weather=#{CGI.escape where}")
298 weather = REXML::Document.new(xml).root.elements["weather"]
300 error = weather.elements["problem_cause"]
302 ermsg = error.attributes["data"]
303 ermsg = _("no reason specified") if ermsg.empty?
306 city = weather.elements["forecast_information/city"].attributes["data"]
307 date = Time.parse(weather.elements["forecast_information/current_date_time"].attributes["data"])
308 units = weather.elements["forecast_information/unit_system"].attributes["data"].intern
309 current_conditions = weather.elements["current_conditions"]
310 foreconds = weather.elements.to_a("forecast_conditions")
313 current_conditions.each { |el|
314 name = el.name.intern
315 value = el.attributes["data"].dup
331 foreconds.each { |forecast|
334 name = el.name.intern
335 value = el.attributes["data"]
340 value << (units == :SI ? "°C" : "°F")
341 value << " |" if name == :low
347 forecasts << cond.join(' ')
350 m.reply _("Google weather info for %{city} on %{date}: %{conds}. Three-day forecast: %{forecast}") % {
353 :conds => conds.join(', '),
354 :forecast => forecasts.join('; ')
358 m.reply _("Google weather failed: %{e}") % { :e => e}
365 plugin = WeatherPlugin.new
366 plugin.map 'weather :units :service *where',
373 :units => /metric|english|both/,
374 :service => /wu|nws|station|google/