X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=data%2Frbot%2Fplugins%2Fweather.rb;h=8f59a9ba693eea6f98c61a3f42d69fe53f4a29a2;hb=052217de30c59206d7025b582d4604557a747470;hp=f2c3e3683887fde074eb2267da96af0e47086b5d;hpb=676dd61e6b0bea5f506d064039a685944aefd6fb;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/data/rbot/plugins/weather.rb b/data/rbot/plugins/weather.rb index f2c3e368..8f59a9ba 100644 --- a/data/rbot/plugins/weather.rb +++ b/data/rbot/plugins/weather.rb @@ -1,649 +1,383 @@ -# This is nasty-ass. I hate writing parsers. -class Metar - attr_reader :decoded - attr_reader :input - attr_reader :date - attr_reader :nodata - def initialize(string) - str = nil - @nodata = false - string.each_line {|l| - if str == nil - # grab first line (date) - @date = l.chomp.strip - str = "" - else - if(str == "") - str = l.chomp.strip - else - str += " " + l.chomp.strip - end - end - } - if @date && @date =~ /^(\d+)\/(\d+)\/(\d+) (\d+):(\d+)$/ - # 2002/02/26 05:00 - @date = Time.gm($1, $2, $3, $4, $5, 0) - else - @date = Time.now - end - @input = str.chomp - @cloud_layers = 0 - @cloud_coverage = { - 'SKC' => '0', - 'CLR' => '0', - 'VV' => '8/8', - 'FEW' => '1/8 - 2/8', - 'SCT' => '3/8 - 4/8', - 'BKN' => '5/8 - 7/8', - 'OVC' => '8/8' - } - @wind_dir_texts = [ - 'North', - 'North/Northeast', - 'Northeast', - 'East/Northeast', - 'East', - 'East/Southeast', - 'Southeast', - 'South/Southeast', - 'South', - 'South/Southwest', - 'Southwest', - 'West/Southwest', - 'West', - 'West/Northwest', - 'Northwest', - 'North/Northwest', - 'North' - ] - @wind_dir_texts_short = [ - 'N', - 'N/NE', - 'NE', - 'E/NE', - 'E', - 'E/SE', - 'SE', - 'S/SE', - 'S', - 'S/SW', - 'SW', - 'W/SW', - 'W', - 'W/NW', - 'NW', - 'N/NW', - 'N' - ] - @weather_array = { - 'MI' => 'Mild ', - 'PR' => 'Partial ', - 'BC' => 'Patches ', - 'DR' => 'Low Drifting ', - 'BL' => 'Blowing ', - 'SH' => 'Shower(s) ', - 'TS' => 'Thunderstorm ', - 'FZ' => 'Freezing', - 'DZ' => 'Drizzle ', - 'RA' => 'Rain ', - 'SN' => 'Snow ', - 'SG' => 'Snow Grains ', - 'IC' => 'Ice Crystals ', - 'PE' => 'Ice Pellets ', - 'GR' => 'Hail ', - 'GS' => 'Small Hail and/or Snow Pellets ', - 'UP' => 'Unknown ', - 'BR' => 'Mist ', - 'FG' => 'Fog ', - 'FU' => 'Smoke ', - 'VA' => 'Volcanic Ash ', - 'DU' => 'Widespread Dust ', - 'SA' => 'Sand ', - 'HZ' => 'Haze ', - 'PY' => 'Spray', - 'PO' => 'Well-Developed Dust/Sand Whirls ', - 'SQ' => 'Squalls ', - 'FC' => 'Funnel Cloud Tornado Waterspout ', - 'SS' => 'Sandstorm/Duststorm ' - } - @cloud_condition_array = { - 'SKC' => 'clear', - 'CLR' => 'clear', - 'VV' => 'vertical visibility', - 'FEW' => 'a few', - 'SCT' => 'scattered', - 'BKN' => 'broken', - 'OVC' => 'overcast' - } - @strings = { - 'mm_inches' => '%s mm (%s inches)', - 'precip_a_trace' => 'a trace', - 'precip_there_was' => 'There was %s of precipitation ', - 'sky_str_format1' => 'There were %s at a height of %s meters (%s feet)', - 'sky_str_clear' => 'The sky was clear', - 'sky_str_format2' => ', %s at a height of %s meter (%s feet) and %s at a height of %s meters (%s feet)', - 'sky_str_format3' => ' and %s at a height of %s meters (%s feet)', - 'clouds' => ' clouds', - 'clouds_cb' => ' cumulonimbus clouds', - 'clouds_tcu' => ' towering cumulus clouds', - 'visibility_format' => 'The visibility was %s kilometers (%s miles).', - 'wind_str_format1' => 'blowing at a speed of %s meters per second (%s miles per hour)', - 'wind_str_format2' => ', with gusts to %s meters per second (%s miles per hour),', - 'wind_str_format3' => ' from the %s', - 'wind_str_calm' => 'calm', - 'precip_last_hour' => 'in the last hour. ', - 'precip_last_6_hours' => 'in the last 3 to 6 hours. ', - 'precip_last_24_hours' => 'in the last 24 hours. ', - 'precip_snow' => 'There is %s mm (%s inches) of snow on the ground. ', - 'temp_min_max_6_hours' => 'The maximum and minimum temperatures over the last 6 hours were %s and %s degrees Celsius (%s and %s degrees Fahrenheit).', - 'temp_max_6_hours' => 'The maximum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ', - 'temp_min_6_hours' => 'The minimum temperature over the last 6 hours was %s degrees Celsius (%s degrees Fahrenheit). ', - 'temp_min_max_24_hours' => 'The maximum and minimum temperatures over the last 24 hours were %s and %s degrees Celsius (%s and %s degrees Fahrenheit). ', - 'light' => 'Light ', - 'moderate' => 'Moderate ', - 'heavy' => 'Heavy ', - 'mild' => 'Mild ', - 'nearby' => 'Nearby ', - 'current_weather' => 'Current weather is %s. ', - 'pretty_print_metar' => '%s on %s, the wind was %s at %s. The temperature was %s degrees Celsius (%s degrees Fahrenheit), and the pressure was %s hPa (%s inHg). The relative humidity was %s%%. %s %s %s %s %s' - } +#-- vim:sw=2:et +#++ +# +# :title: Weather plugin for rbot +# +# Author:: MrChucho (mrchucho@mrchucho.net): NOAA National Weather Service support +# Author:: Giuseppe "Oblomov" Bilotta +# +# Copyright:: (C) 2006 Ralph M. Churchill +# Copyright:: (C) 2006-2007 Giuseppe Bilotta +# +# License:: GPL v2 - parse - end +require 'rexml/document' - def store_speed(value, windunit, meterspersec, knots, milesperhour) - # Helper function to convert and store speed based on unit. - # &$meterspersec, &$knots and &$milesperhour are passed on - # reference - if (windunit == 'KT') - # The windspeed measured in knots: - @decoded[knots] = sprintf("%.2f", value) - # The windspeed measured in meters per second, rounded to one decimal place: - @decoded[meterspersec] = sprintf("%.2f", value.to_f * 0.51444) - # The windspeed measured in miles per hour, rounded to one decimal place: */ - @decoded[milesperhour] = sprintf("%.2f", value.to_f * 1.1507695060844667) - elsif (windunit == 'MPS') - # The windspeed measured in meters per second: - @decoded[meterspersec] = sprintf("%.2f", value) - # The windspeed measured in knots, rounded to one decimal place: - @decoded[knots] = sprintf("%.2f", value.to_f / 0.51444) - #The windspeed measured in miles per hour, rounded to one decimal place: - @decoded[milesperhour] = sprintf("%.1f", value.to_f / 0.51444 * 1.1507695060844667) - elsif (windunit == 'KMH') - # The windspeed measured in kilometers per hour: - @decoded[meterspersec] = sprintf("%.1f", value.to_f * 1000 / 3600) - @decoded[knots] = sprintf("%.1f", value.to_f * 1000 / 3600 / 0.51444) - # The windspeed measured in miles per hour, rounded to one decimal place: - @decoded[milesperhour] = sprintf("%.1f", knots.to_f * 1.1507695060844667) +# Wraps NOAA National Weather Service information +class CurrentConditions + def initialize(station) + @station = station + @url = "http://www.nws.noaa.gov/data/current_obs/#{@station.upcase}.xml" + @etag = String.new + @mtime = Time.mktime(0) + @current_conditions = String.new + @iscached = false end - end - - def parse - @decoded = Hash.new - puts @input - @input.split(" ").each {|part| - if (part == 'METAR') - # Type of Report: METAR - @decoded['type'] = 'METAR' - elsif (part == 'SPECI') - # Type of Report: SPECI - @decoded['type'] = 'SPECI' - elsif (part == 'AUTO') - # Report Modifier: AUTO - @decoded['report_mod'] = 'AUTO' - elsif (part == 'NIL') - @nodata = true - elsif (part =~ /^\S{4}$/ && ! (@decoded.has_key?('station'))) - # Station Identifier - @decoded['station'] = part - elsif (part =~ /([0-9]{2})([0-9]{2})([0-9]{2})Z/) - # ignore this bit, it's useless without month/year. some of these - # things are hideously out of date. - # now = Time.new - # time = Time.gm(now.year, now.month, $1, $2, $3, 0) - # Date and Time of Report - # @decoded['time'] = time - elsif (part == 'COR') - # Report Modifier: COR - @decoded['report_mod'] = 'COR' - elsif (part =~ /([0-9]{3}|VRB)([0-9]{2,3}).*(KT|MPS|KMH)/) - # Wind Group - windunit = $3 - # now do ereg to get the actual values - part =~ /([0-9]{3}|VRB)([0-9]{2,3})((G[0-9]{2,3})?#{windunit})/ - if ($1 == 'VRB') - @decoded['wind_deg'] = 'variable directions' - @decoded['wind_dir_text'] = 'variable directions' - @decoded['wind_dir_text_short'] = 'VAR' - else - @decoded['wind_deg'] = $1 - @decoded['wind_dir_text'] = @wind_dir_texts[($1.to_i/22.5).round] - @decoded['wind_dir_text_short'] = @wind_dir_texts_short[($1.to_i/22.5).round] + def update + begin + open(@url,"If-Modified-Since" => @mtime.rfc2822) do |feed| + # open(@url,"If-None-Match"=>@etag) do |feed| + @etag = feed.meta['etag'] + @mtime = feed.last_modified + cc_doc = (REXML::Document.new feed).root + @iscached = false + @current_conditions = parse(cc_doc) + end + rescue OpenURI::HTTPError => e + case e + when /304/ + @iscached = true + when /404/ + raise "Data for #{@station} not found" + else + raise "Error retrieving data: #{e}" + end end - store_speed($2, windunit, - 'wind_meters_per_second', - 'wind_knots', - 'wind_miles_per_hour') - - if ($4 != nil) - # We have a report with information about the gust. - # First we have the gust measured in knots - if ($4 =~ /G([0-9]{2,3})/) - store_speed($1,windunit, - 'wind_gust_meters_per_second', - 'wind_gust_knots', - 'wind_gust_miles_per_hour') + @current_conditions # +" Cached? "+ ((@iscached) ? "Y" : "N") end + def parse(cc_doc) + cc = Hash.new + cc_doc.elements.each do |c| + cc[c.name.to_sym] = c.text end - elsif (part =~ /([0-9]{3})V([0-9]{3})/) - # Variable wind-direction - @decoded['wind_var_beg'] = $1 - @decoded['wind_var_end'] = $2 - elsif (part == "9999") - # A strange value. When you look at other pages you see it - # interpreted like this (where I use > to signify 'Greater - # than'): - @decoded['visibility_miles'] = '>7'; - @decoded['visibility_km'] = '>11.3'; - elsif (part =~ /^([0-9]{4})$/) - # Visibility in meters (4 digits only) - # The visibility measured in kilometers, rounded to one decimal place. - @decoded['visibility_km'] = sprintf("%.1f", $1.to_i / 1000) - # The visibility measured in miles, rounded to one decimal place. - @decoded['visibility_miles'] = sprintf("%.1f", $1.to_i / 1000 / 1.609344) - elsif (part =~ /^[0-9]$/) - # Temp Visibility Group, single digit followed by space - @decoded['temp_visibility_miles'] = part - elsif (@decoded['temp_visibility_miles'] && (@decoded['temp_visibility_miles']+' '+part) =~ /^M?(([0-9]?)[ ]?([0-9])(\/?)([0-9]*))SM$/) - # Visibility Group - if ($4 == '/') - vis_miles = $2.to_i + $3.to_i/$5.to_i - else - vis_miles = $1.to_i; - end - if (@decoded['temp_visibility_miles'][0] == 'M') - # The visibility measured in miles, prefixed with < to indicate 'Less than' - @decoded['visibility_miles'] = '<' + sprintf("%.1f", vis_miles) - # The visibility measured in kilometers. The value is rounded - # to one decimal place, prefixed with < to indicate 'Less than' */ - @decoded['visibility_km'] = '<' . sprintf("%.1f", vis_miles * 1.609344) - else - # The visibility measured in mile.s */ - @decoded['visibility_miles'] = sprintf("%.1f", vis_miles) - # The visibility measured in kilometers, rounded to one decimal place. - @decoded['visibility_km'] = sprintf("%.1f", vis_miles * 1.609344) - end - elsif (part =~ /^(-|\+|VC|MI)?(TS|SH|FZ|BL|DR|BC|PR|RA|DZ|SN|SG|GR|GS|PE|IC|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS)+$/) - # Current weather-group - @decoded['weather'] = '' unless @decoded.has_key?('weather') - if (part[0].chr == '-') - # A light phenomenon - @decoded['weather'] += @strings['light'] - part = part[1,part.length] - elsif (part[0].chr == '+') - # A heavy phenomenon - @decoded['weather'] += @strings['heavy'] - part = part[1,part.length] - elsif (part[0,2] == 'VC') - # Proximity Qualifier - @decoded['weather'] += @strings['nearby'] - part = part[2,part.length] - elsif (part[0,2] == 'MI') - @decoded['weather'] += @strings['mild'] - part = part[2,part.length] - else - # no intensity code => moderate phenomenon - @decoded['weather'] += @strings['moderate'] - end - - while (part && bite = part[0,2]) do - # Now we take the first two letters and determine what they - # mean. We append this to the variable so that we gradually - # build up a phrase. - - @decoded['weather'] += @weather_array[bite] - # Here we chop off the two first letters, so that we can take - # a new bite at top of the while-loop. - part = part[2,-1] - end - elsif (part =~ /(SKC|CLR)/) - # Cloud-layer-group. - # There can be up to three of these groups, so we store them as - # cloud_layer1, cloud_layer2 and cloud_layer3. - - @cloud_layers += 1; - # Again we have to translate the code-characters to a - # meaningful string. - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] = @cloud_condition_array[$1] - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1] - elsif (part =~ /^(VV|FEW|SCT|BKN|OVC)([0-9]{3})(CB|TCU)?$/) - # We have found (another) a cloud-layer-group. There can be up - # to three of these groups, so we store them as cloud_layer1, - # cloud_layer2 and cloud_layer3. - @cloud_layers += 1; - # Again we have to translate the code-characters to a meaningful string. - if ($3 == 'CB') - # cumulonimbus (CB) clouds were observed. */ - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] = - @cloud_condition_array[$1] + @strings['clouds_cb'] - elsif ($3 == 'TCU') - # towering cumulus (TCU) clouds were observed. - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] = - @cloud_condition_array[$1] + @strings['clouds_tcu'] - else - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_condition'] = - @cloud_condition_array[$1] + @strings['clouds'] - end - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_coverage'] = @cloud_coverage[$1] - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_ft'] = $2.to_i * 100 - @decoded['cloud_layer'+ (@cloud_layers.to_s) +'_altitude_m'] = ($2.to_f * 30.48).round - elsif (part =~ /^T([0-9]{4})$/) - store_temp($1,'temp_c','temp_f') - elsif (part =~ /^T?(M?[0-9]{2})\/(M?[0-9\/]{1,2})?$/) - # Temperature/Dew Point Group - # The temperature and dew-point measured in Celsius. - @decoded['temp_c'] = sprintf("%d", $1.tr('M', '-')) - if $2 == "//" || !$2 - @decoded['dew_c'] = 0 + "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." + end +private + def heat_index_or_wind_chill(cc) + hi = cc[:heat_index_string] + wc = cc[:windchill_string] + if hi != 'NA' then + " with a heat index of #{hi}" + elsif wc != 'NA' then + " with a windchill of #{wc}" else - @decoded['dew_c'] = sprintf("%.1f", $2.tr('M', '-')) + "" end - # The temperature and dew-point measured in Fahrenheit, rounded to - # the nearest degree. - @decoded['temp_f'] = ((@decoded['temp_c'].to_f * 9 / 5) + 32).round - @decoded['dew_f'] = ((@decoded['dew_c'].to_f * 9 / 5) + 32).round - elsif(part =~ /A([0-9]{4})/) - # Altimeter - # The pressure measured in inHg - @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_i/100) - # The pressure measured in mmHg, hPa and atm - @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.254) - @decoded['altimeter_hpa'] = sprintf("%d", ($1.to_f * 0.33863881578947).to_i) - @decoded['altimeter_atm'] = sprintf("%.3f", $1.to_f * 3.3421052631579e-4) - elsif(part =~ /Q([0-9]{4})/) - # Altimeter - # This is strange, the specification doesnt say anything about - # the Qxxxx-form, but it's in the METARs. - # The pressure measured in hPa - @decoded['altimeter_hpa'] = sprintf("%d", $1.to_i) - # The pressure measured in mmHg, inHg and atm - @decoded['altimeter_mmhg'] = sprintf("%.1f", $1.to_f * 0.7500616827) - @decoded['altimeter_inhg'] = sprintf("%.2f", $1.to_f * 0.0295299875) - @decoded['altimeter_atm'] = sprintf("%.3f", $1.to_f * 9.869232667e-4) - elsif (part =~ /^T([0-9]{4})([0-9]{4})/) - # Temperature/Dew Point Group, coded to tenth of degree. - # The temperature and dew-point measured in Celsius. - store_temp($1,'temp_c','temp_f') - store_temp($2,'dew_c','dew_f') - elsif (part =~ /^1([0-9]{4}$)/) - # 6 hour maximum temperature Celsius, coded to tenth of degree - store_temp($1,'temp_max6h_c','temp_max6h_f') - elsif (part =~ /^2([0-9]{4}$)/) - # 6 hour minimum temperature Celsius, coded to tenth of degree - store_temp($1,'temp_min6h_c','temp_min6h_f') - elsif (part =~ /^4([0-9]{4})([0-9]{4})$/) - # 24 hour maximum and minimum temperature Celsius, coded to - # tenth of degree - store_temp($1,'temp_max24h_c','temp_max24h_f') - store_temp($2,'temp_min24h_c','temp_min24h_f') - elsif (part =~ /^P([0-9]{4})/) - # Precipitation during last hour in hundredths of an inch - # (store as inches) - @decoded['precip_in'] = sprintf("%.2f", $1.to_f/100) - @decoded['precip_mm'] = sprintf("%.2f", $1.to_f * 0.254) - elsif (part =~ /^6([0-9]{4})/) - # Precipitation during last 3 or 6 hours in hundredths of an - # inch (store as inches) - @decoded['precip_6h_in'] = sprintf("%.2f", $1.to_f/100) - @decoded['precip_6h_mm'] = sprintf("%.2f", $1.to_f * 0.254) - elsif (part =~ /^7([0-9]{4})/) - # Precipitation during last 24 hours in hundredths of an inch - # (store as inches) - @decoded['precip_24h_in'] = sprintf("%.2f", $1.to_f/100) - @decoded['precip_24h_mm'] = sprintf("%.2f", $1.to_f * 0.254) - elsif(part =~ /^4\/([0-9]{3})/) - # Snow depth in inches - @decoded['snow_in'] = sprintf("%.2f", $1); - @decoded['snow_mm'] = sprintf("%.2f", $1.to_f * 25.4) - else - # If we couldn't match the group, we assume that it was a - # remark. - @decoded['remarks'] = '' unless @decoded.has_key?("remarks") - @decoded['remarks'] += ' ' + part; - end - } - - # Relative humidity - # p @decoded['dew_c'] # 11.0 - # p @decoded['temp_c'] # 21.0 - # => 56.1 - @decoded['rel_humidity'] = sprintf("%.1f",100 * - (6.11 * (10.0**(7.5 * @decoded['dew_c'].to_f / (237.7 + @decoded['dew_c'].to_f)))) / (6.11 * (10.0 ** (7.5 * @decoded['temp_c'].to_f / (237.7 + @decoded['temp_c'].to_f))))) if @decoded.has_key?('dew_c') - end + end +end + +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 store_temp(temp,temp_cname,temp_fname) - # Given a numerical temperature temp in Celsius, coded to tenth of - # degree, store in @decoded[temp_cname], convert to Fahrenheit - # and store in @decoded[temp_fname] - # Note: temp is converted to negative if temp > 100.0 (See - # Federal Meteorological Handbook for groups T, 1, 2 and 4) - - # Temperature measured in Celsius, coded to tenth of degree - temp = temp.to_f/10 - if (temp >100.0) - # first digit = 1 means minus temperature - temp = -(temp - 100.0) + def help(plugin, topic="") + case topic + when "nws" + "weather nws => display the current conditions at the location specified by the NOAA National Weather Service station code ( lookup your station code at http://www.nws.noaa.gov/data/current_obs/ )" + when "station", "wu" + "weather [] => display the current conditions at the location specified, looking it up on the Weather Underground site; you can use 'station ' to look up data by station code ( lookup your station code at http://www.weatherunderground.com/ ); you can optionally set 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 - @decoded[temp_cname] = sprintf("%.1f", temp) - # The temperature in Fahrenheit. - @decoded[temp_fname] = sprintf("%.1f", (temp * 9 / 5) + 32) end - def pretty_print_precip(precip_mm, precip_in) - # Returns amount if $precip_mm > 0, otherwise "trace" (see Federal - # Meteorological Handbook No. 1 for code groups P, 6 and 7) used in - # several places, so standardized in one function. - if (precip_mm.to_i > 0) - amount = sprintf(@strings['mm_inches'], precip_mm, precip_in) - else - amount = @strings['a_trace'] - end - return sprintf(@strings['precip_there_was'], amount) + def initialize + super + + @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 pretty_print - if @nodata - return "The weather stored for #{@decoded['station']} consists of the string 'NIL' :(" - end + def weather(m, params) + where = params[:where].to_s + service = params[:service].to_sym rescue nil + units = params[:units] - ["temp_c", "altimeter_hpa"].each {|key| - if !@decoded.has_key?(key) - return "The weather stored for #{@decoded['station']} could not be parsed (#{@input})" - end - } - - mins_old = ((Time.now - @date.to_i).to_f/60).round - if (mins_old <= 60) - weather_age = mins_old.to_s + " minutes ago," - elsif (mins_old <= 60 * 25) - weather_age = (mins_old / 60).to_s + " hours, " - weather_age += (mins_old % 60).to_s + " minutes ago," - else - # return "The weather stored for #{@decoded['station']} is hideously out of date :( (Last update #{@date})" - weather_age = "The weather stored for #{@decoded['station']} is hideously out of date :( here it is anyway:" - end - - if(@decoded.has_key?("cloud_layer1_altitude_ft")) - sky_str = sprintf(@strings['sky_str_format1'], - @decoded["cloud_layer1_condition"], - @decoded["cloud_layer1_altitude_m"], - @decoded["cloud_layer1_altitude_ft"]) - else - sky_str = @strings['sky_str_clear'] + 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(@decoded.has_key?("cloud_layer2_altitude_ft")) - if(@decoded.has_key?("cloud_layer3_altitude_ft")) - sky_str += sprintf(@strings['sky_str_format2'], - @decoded["cloud_layer2_condition"], - @decoded["cloud_layer2_altitude_m"], - @decoded["cloud_layer2_altitude_ft"], - @decoded["cloud_layer3_condition"], - @decoded["cloud_layer3_altitude_m"], - @decoded["cloud_layer3_altitude_ft"]) + if !service + if where.sub!(/^station\s+/,'') + service = :nws else - sky_str += sprintf(@strings['sky_str_format3'], - @decoded["cloud_layer2_condition"], - @decoded["cloud_layer2_altitude_m"], - @decoded["cloud_layer2_altitude_ft"]) + service = :wu end end - sky_str += "." - if(@decoded.has_key?("visibility_miles")) - visibility = sprintf(@strings['visibility_format'], - @decoded["visibility_km"], - @decoded["visibility_miles"]) - else - visibility = "" + 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 - if (@decoded.has_key?("wind_meters_per_second") && @decoded["wind_meters_per_second"].to_i > 0) - wind_str = sprintf(@strings['wind_str_format1'], - @decoded["wind_meters_per_second"], - @decoded["wind_miles_per_hour"]) - if (@decoded.has_key?("wind_gust_meters_per_second") && @decoded["wind_gust_meters_per_second"].to_i > 0) - wind_str += sprintf(@strings['wind_str_format2'], - @decoded["wind_gust_meters_per_second"], - @decoded["wind_gust_miles_per_hour"]) - end - wind_str += sprintf(@strings['wind_str_format3'], - @decoded["wind_dir_text"]) + wu_units = String.new + + case (units || @bot.config['weather.units']).to_sym + when :english, :metric + wu_units = "_#{units}" + when :both else - wind_str = @strings['wind_str_calm'] + m.reply "Ignoring unknown units #{units}" end - prec_str = "" - if (@decoded.has_key?("precip_in")) - prec_str += pretty_print_precip(@decoded["precip_mm"], @decoded["precip_in"]) + @strings['precip_last_hour'] - end - if (@decoded.has_key?("precip_6h_in")) - prec_str += pretty_print_precip(@decoded["precip_6h_mm"], @decoded["precip_6h_in"]) + @strings['precip_last_6_hours'] - end - if (@decoded.has_key?("precip_24h_in")) - prec_str += pretty_print_precip(@decoded["precip_24h_mm"], @decoded["precip_24h_in"]) + @strings['precip_last_24_hours'] - end - if (@decoded.has_key?("snow_in")) - prec_str += sprintf(@strings['precip_snow'], @decoded["snow_mm"], @decoded["snow_in"]) + 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 - temp_str = "" - if (@decoded.has_key?("temp_max6h_c") && @decoded.has_key?("temp_min6h_c")) - temp_str += sprintf(@strings['temp_min_max_6_hours'], - @decoded["temp_max6h_c"], - @decoded["temp_min6h_c"], - @decoded["temp_max6h_f"], - @decoded["temp_min6h_f"]) + @registry[m.sourcenick] = [service, where, units] + end + + def nws_describe(m, where) + if @nws_cache.has_key?(where) then + met = @nws_cache[where] else - if (@decoded.has_key?("temp_max6h_c")) - temp_str += sprintf(@strings['temp_max_6_hours'], - @decoded["temp_max6h_c"], - @decoded["temp_max6h_f"]) - end - if (@decoded.has_key?("temp_min6h_c")) - temp_str += sprintf(@strings['temp_max_6_hours'], - @decoded["temp_min6h_c"], - @decoded["temp_min6h_f"]) - end + met = CurrentConditions.new(where) end - if (@decoded.has_key?("temp_max24h_c")) - temp_str += sprintf(@strings['temp_min_max_24_hours'], - @decoded["temp_max24h_c"], - @decoded["temp_min24h_c"], - @decoded["temp_max24h_f"], - @decoded["temp_min24h_f"]) - end - - if (@decoded.has_key?("weather")) - weather_str = sprintf(@strings['current_weather'], @decoded["weather"]) + if met + begin + m.reply met.update + @nws_cache[where] = met + rescue => e + m.reply e.message + end else - weather_str = '' + m.reply "couldn't find weather data for #{where}" end - - return sprintf(@strings['pretty_print_metar'], - weather_age, - @date, - wind_str, @decoded["station"], @decoded["temp_c"], - @decoded["temp_f"], @decoded["altimeter_hpa"], - @decoded["altimeter_inhg"], - @decoded["rel_humidity"], sky_str, - visibility, weather_str, prec_str, temp_str).strip end - def to_s - @input + 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>/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 -end - -class WeatherPlugin < Plugin - - def help(plugin, topic="") - "weather => display the current weather at the location specified by the ICAO code [Lookup your ICAO code at http://www.nws.noaa.gov/tg/siteloc.shtml - this will also store the ICAO against your nick, so you can later just say \"weather\", weather => display the current weather at the location you last asked for" + 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>/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 / e + m.reply "retrieving info about '#{where}' failed (#{e})" + end end - def get_metar(station) - station.upcase! - - result = @bot.httputil.get(URI.parse("http://weather.noaa.gov/pub/data/observations/metar/stations/#{station}.TXT")) - return nil unless result - return Metar.new(result) + def wu_weather_multi(m, xml) + # debug xml + stations = xml.scan(/\\s+\\s+
\s*(?:]*>]+><\/a>\s*)?(.*?)<\/a>\s*:\s*(.*?)<\/td>/m) + # debug stations + m.reply "multiple stations available, use 'weather station ' or 'weather ' 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 initialize - super - # this plugin only wants to store strings - class << @registry - def store(val) - val - end - def restore(val) - val + 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{]*>([^<]+)}) do + special = { + :url => "http://mobile.wunderground.com"+$1, + :type => $2.dup, + :special => $3.dup + } + spec_rx = Regexp.new("(?:.+?)\\s+(.+?)\\s+
", 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 - @metar_cache = Hash.new + return specials end - - def describe(m, where) - if @metar_cache.has_key?(where) && - Time.now - @metar_cache[where].date < 3600 - met = @metar_cache[where] - else - met = get_metar(where) + + 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("%{underline}%{special}%{underline} @ %{url}" % special) + end end - - if met - m.reply met.pretty_print - @metar_cache[where] = met - else - m.reply "couldn't find weather data for #{where}" + 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(/\s*\s*(.*?)\s*<\/td>\s*\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 - def weather(m, params) - if params[:where] - @registry[m.sourcenick] = params[:where] - describe(m,params[:where]) + # 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 - if @registry.has_key?(m.sourcenick) - where = @registry[m.sourcenick] - describe(m,where) - else - m.reply "I don't know where you are yet! Lookup your code at http://www.nws.noaa.gov/tg/siteloc.shtml and tell me 'weather ', then I'll know." + 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 = WeatherPlugin.new -plugin.map 'weather :where', :defaults => {:where => false} +plugin.map 'weather :units :service *where', + :defaults => { + :where => false, + :units => false, + :service => false + }, + :requirements => { + :units => /metric|english|both/, + :service => /wu|nws|station|google/ + }