+#-- vim:sw=2:et
+#++
#
-# Weather plugin for rbot
-# by MrChucho (mrchucho@mrchucho.net)
-# Copyright (C) 2006 Ralph M. Churchill
+# :title: Weather plugin for rbot
#
-require 'open-uri'
+# Author:: MrChucho (mrchucho@mrchucho.net): NOAA National Weather Service support
+# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
+#
+# Copyright:: (C) 2006 Ralph M. Churchill
+# Copyright:: (C) 2006-2007 Giuseppe Bilotta
+#
+# License:: GPL v2
+
require 'rexml/document'
+# Wraps NOAA National Weather Service information
class CurrentConditions
def initialize(station)
@station = station
end
rescue OpenURI::HTTPError => e
case e
- when /304/:
+ when /304/
@iscached = true
- when /404/:
+ when /404/
raise "Data for #{@station} not found"
else
raise "Error retrieving data: #{e}"
end
end
-class MyWeatherPlugin < Plugin
-
+class WeatherPlugin < Plugin
+
+ Config.register Config::BooleanValue.new('weather.advisory',
+ :default => true,
+ :desc => "Should the bot report special weather advisories when any is present?")
+ Config.register Config::EnumValue.new('weather.units',
+ :values => ['metric', 'english', 'both'],
+ :default => 'both',
+ :desc => "Units to be used by default in Weather Underground reports")
+
+
def help(plugin, topic="")
- "weather <STATION> => display the current conditions at the location specified by the STATION code [Lookup your STATION code at http://www.nws.noaa.gov/data/current_obs/ - this will also store the STATION against your nick, so you can later just say \"weather\", weather => display the current weather at the location you last asked for"
+ case topic
+ when "nws"
+ "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/ )"
+ when "station", "wu"
+ "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."
+ else
+ "weather information lookup. Looks up weather information for the last location you specified. See topics 'nws' and 'wu' for more information"
+ end
end
-
+
def initialize
super
- # this plugin only wants to store strings
- class << @registry
- def store(val)
- val
- end
- def restore(val)
- val
+
+ @nws_cache = Hash.new
+
+ @wu_url = "http://mobile.wunderground.com/cgi-bin/findweather/getForecast?brand=mobile%s&query=%s"
+ @wu_station_url = "http://mobile.wunderground.com/auto/mobile%s/global/stations/%s.html"
+ end
+
+ def weather(m, params)
+ where = params[:where].to_s
+ service = params[:service].to_sym rescue nil
+ units = params[:units]
+
+ if where.empty? or !service or !units and @registry.has_key?(m.sourcenick)
+ reg = @registry[m.sourcenick]
+ debug "loaded weather info #{reg.inspect} for #{m.sourcenick}"
+ service = reg.first.to_sym if !service
+ where = reg[1].to_s if where.empty?
+ units = reg[2] rescue nil
+ end
+
+ if !service
+ if where.sub!(/^station\s+/,'')
+ service = :nws
+ else
+ service = :wu
end
end
- @cc_cache = Hash.new
+
+ if where.empty?
+ debug "No weather location found for #{m.sourcenick}"
+ m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
+ return
+ end
+
+ wu_units = String.new
+
+ case (units || @bot.config['weather.units']).to_sym
+ when :english, :metric
+ wu_units = "_#{units}"
+ when :both
+ else
+ m.reply "Ignoring unknown units #{units}"
+ end
+
+ case service
+ when :nws
+ nws_describe(m, where)
+ when :station
+ wu_station(m, where, wu_units)
+ when :wu
+ wu_weather(m, where, wu_units)
+ when :google
+ google_weather(m, where)
+ else
+ m.reply "I don't know the weather service #{service}, sorry"
+ return
+ end
+
+ @registry[m.sourcenick] = [service, where, units]
end
-
- def describe(m, where)
- if @cc_cache.has_key?(where) then
- met = @cc_cache[where]
+
+ def nws_describe(m, where)
+ if @nws_cache.has_key?(where) then
+ met = @nws_cache[where]
else
met = CurrentConditions.new(where)
end
if met
- # begin
- m.reply met.update
- @cc_cache[where] = met
- # rescue
- # end
+ begin
+ m.reply met.update
+ @nws_cache[where] = met
+ rescue => e
+ m.reply e.message
+ end
else
m.reply "couldn't find weather data for #{where}"
end
end
- def weather(m, params)
- if params[:where]
- @registry[m.sourcenick] = params[:where]
- describe(m,params[:where])
- else
- if @registry.has_key?(m.sourcenick)
- where = @registry[m.sourcenick]
- describe(m,where)
+ def wu_station(m, where, units)
+ begin
+ xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
+ case xml
+ when nil
+ m.reply "couldn't retrieve weather information, sorry"
+ return
+ when /Search not found:/
+ m.reply "no such station found (#{where})"
+ return
+ when /<table border.*?>(.*?)<\/table>/m
+ data = $1.dup
+ m.reply wu_weather_filter(data)
+ wu_out_special(m, xml)
+ else
+ debug xml
+ m.reply "something went wrong with the data for #{where}..."
+ end
+ rescue => e
+ m.reply "retrieving info about '#{where}' failed (#{e})"
+ end
+ end
+
+ def wu_weather(m, where, units)
+ begin
+ xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
+ case xml
+ when nil
+ m.reply "couldn't retrieve weather information, sorry"
+ when /City Not Found/
+ m.reply "no such location found (#{where})"
+ when /Current<\/a>/
+ data = ""
+ xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
+ data += wu_weather_filter(match.first)
+ end
+ if data.length > 0
+ m.reply data
+ else
+ m.reply "couldn't parse weather data from #{where}"
+ end
+ wu_out_special(m, xml)
+ when /<a href="\/(?:global\/stations|US\/\w\w)\//
+ wu_weather_multi(m, xml)
+ else
+ debug xml
+ m.reply "something went wrong with the data from #{where}..."
+ end
+ rescue => e
+ m.reply "retrieving info about '#{where}' failed (#{e})"
+ end
+ end
+
+ def wu_weather_multi(m, xml)
+ # debug xml
+ 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)
+ # debug stations
+ m.reply "multiple stations available, use 'weather station <code>' or 'weather <city, state>' as appropriate, for one of the following (current temp shown):"
+ stations.map! { |ar|
+ warning = ar[0]
+ loc = ar[2]
+ state = ar[1]
+ par = ar[3]
+ w = ar[4]
+ if state # US station
+ (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, w.ircify_html])
+ else # non-US station
+ (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, w.ircify_html])
+ end
+ }
+ m.reply stations.join("; ")
+ end
+
+ def wu_check_special(xml)
+ specials = []
+ # We only scan the first half to prevent getting the advisories twice
+ xml[0,xml.length/2].scan(%r{<a href="([^"]+\?[^"]*feature=warning#([^"]+))"[^>]*>([^<]+)</a>}) do
+ special = {
+ :url => "http://mobile.wunderground.com"+$1,
+ :type => $2.dup,
+ :special => $3.dup
+ }
+ spec_rx = Regexp.new("<a name=\"#{special[:type]}\">(?:.+?)<td align=\"left\">\\s+(.+?)\\s+</td>\\s+</tr>\\s+</table>", Regexp::MULTILINE)
+ spec_xml = @bot.httputil.get(special[:url])
+ if spec_xml and spec_td = spec_xml.match(spec_rx)
+ special.merge!(:text => spec_td.captures.first.ircify_html)
+ end
+ specials << special
+ end
+ return specials
+ end
+
+ def wu_out_special(m, xml)
+ return unless @bot.config['weather.advisory']
+ specials = wu_check_special(xml)
+ debug specials
+ specials.each do |special|
+ special.merge!(:underline => Underline)
+ if special[:text]
+ m.reply("%{underline}%{special}%{underline}: %{text}" % special)
else
- m.reply "I don't know where you are yet! Lookup your station at http://www.nws.noaa.gov/data/current_obs/ and tell me 'weather <station>', then I'll know."
+ m.reply("%{underline}%{special}%{underline} @ %{url}" % special)
end
end
end
+
+ def wu_weather_filter(stuff)
+ result = Array.new
+ if stuff.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
+ result << ("Weather info for %s (updated on %s)" % [$2.ircify_html, $1.ircify_html])
+ end
+ stuff.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/m) { |k, v|
+ kk = k.riphtml
+ vv = v.riphtml
+ next if vv.empty?
+ next if ["-", "- approx.", "N/A", "N/A approx."].include?(vv)
+ next if kk == "Raw METAR"
+ result << ("%s: %s" % [kk, vv])
+ }
+ return result.join('; ')
+ end
+
+ # TODO allow units choice other than lang, find how the API does it
+ def google_weather(m, where)
+ botlang = @bot.config['core.language'].intern
+ if Language::Lang2Locale.key?(botlang)
+ lang = Language::Lang2Locale[botlang].sub(/.UTF.?8$/,'')
+ else
+ lang = botlang.to_s[0,2]
+ end
+
+ debug "Google weather with language #{lang}"
+ xml = @bot.httputil.get("http://www.google.com/ig/api?hl=#{lang}&weather=#{CGI.escape where}")
+ debug xml
+ weather = REXML::Document.new(xml).root.elements["weather"]
+ begin
+ error = weather.elements["problem_cause"]
+ if error
+ ermsg = error.attributes["data"]
+ ermsg = _("no reason specified") if ermsg.empty?
+ raise ermsg
+ end
+ city = weather.elements["forecast_information/city"].attributes["data"]
+ date = Time.parse(weather.elements["forecast_information/current_date_time"].attributes["data"])
+ units = weather.elements["forecast_information/unit_system"].attributes["data"].intern
+ current_conditions = weather.elements["current_conditions"]
+ foreconds = weather.elements.to_a("forecast_conditions")
+
+ conds = []
+ current_conditions.each { |el|
+ name = el.name.intern
+ value = el.attributes["data"].dup
+ debug [name, value]
+ case name
+ when :icon
+ next
+ when :temp_f
+ next if units == :SI
+ value << "°F"
+ when :temp_c
+ next if units == :US
+ value << "°C"
+ end
+ conds << value
+ }
+
+ forecasts = []
+ foreconds.each { |forecast|
+ cond = []
+ forecast.each { |el|
+ name = el.name.intern
+ value = el.attributes["data"]
+ case name
+ when :icon
+ next
+ when :high, :low
+ value << (units == :SI ? "°C" : "°F")
+ value << " |" if name == :low
+ when :condition
+ value = "(#{value})"
+ end
+ cond << value
+ }
+ forecasts << cond.join(' ')
+ }
+
+ m.reply _("Google weather info for %{city} on %{date}: %{conds}. Three-day forecast: %{forecast}") % {
+ :city => city,
+ :date => date,
+ :conds => conds.join(', '),
+ :forecast => forecasts.join('; ')
+ }
+ rescue => e
+ debug e
+ m.reply _("Google weather failed: %{e}") % { :e => e}
+ end
+
+ end
+
end
-plugin = MyWeatherPlugin.new
-plugin.map 'weather :where', :defaults => {:where => false}
+
+plugin = WeatherPlugin.new
+plugin.map 'weather :units :service *where',
+ :defaults => {
+ :where => false,
+ :units => false,
+ :service => false
+ },
+ :requirements => {
+ :units => /metric|english|both/,
+ :service => /wu|nws|station|google/
+ }