]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - data/rbot/plugins/weather.rb
weather: refactor NWS output
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / weather.rb
index afcf394c1a41edfd0d2b8f22be5d1b388c585e45..9d2b8aed5e3c368b49b62e30a3f6511d3fa8e4cd 100644 (file)
@@ -15,51 +15,55 @@ require 'rexml/document'
 
 # Wraps NOAA National Weather Service information
 class CurrentConditions
+  @@bot = Irc::Utils.bot
     def initialize(station)
         @station = station
-        @url = "http://www.nws.noaa.gov/data/current_obs/#{@station.upcase}.xml"
-        @etag = String.new
-        @mtime = Time.mktime(0)
+        @url = "http://www.nws.noaa.gov/data/current_obs/#{URI.encode @station.upcase}.xml"
         @current_conditions = String.new
-        @iscached = false
     end
     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
+      begin
+        resp = @@bot.httputil.get_response(@url)
+        case resp
+        when Net::HTTPSuccess
+          cc_doc = (REXML::Document.new resp.body).root
+          @current_conditions = parse(cc_doc)
+        else
+          raise Net::HTTPError.new(_("couldn't get data for %{station} (%{message})") % {
+            :station => @station, :message => resp.message
+          }, resp)
+        end
+      rescue => e
+        if Net::HTTPError === e
+          raise
+        else
+          error e
+          raise "error retrieving data: #{e}"
         end
-        @current_conditions # +" Cached? "+ ((@iscached) ? "Y" : "N")
+      end
+      @current_conditions
     end
     def parse(cc_doc)
         cc = Hash.new
         cc_doc.elements.each do |c|
             cc[c.name.to_sym] = c.text
         end
-        "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."
+        cc[:time] = cc[:observation_time_rfc822]
+        cc[:wind] = cc[:wind_string]
+        cc[:temperature] = cc[:temperature_string]
+        cc[:heatindexorwindchill] = heat_index_or_wind_chill(cc)
+        cc[:pressure] = cc[:pressure_string]
+
+        _("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
     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}"
+        if hi and hi != 'NA' then
+            _(" with a heat index of %{hi}") % { :hi => hi }
+        elsif wc and wc != 'NA' then
+            _(" with a windchill of %{wc}") % { :wc => wc }
         else
             ""
         end
@@ -98,32 +102,27 @@ class WeatherPlugin < Plugin
   end
 
   def weather(m, params)
-    if params[:where].empty?
-      if @registry.has_key?(m.sourcenick)
-        where = @registry[m.sourcenick]
-        debug "Loaded weather info #{where.inspect} for #{m.sourcenick}"
-
-        service = where.first.to_sym
-        loc = where[1].to_s
-        units = params[:units] || where[2] rescue nil
-      else
-        debug "No weather info 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
-    else
-      where = params[:where]
-      if ['nws','station'].include?(where.first)
-        service = where.first.to_sym
-        loc = where[1].to_s
+    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
-        loc = where.to_s
       end
-      units = params[:units]
     end
 
-    if loc.empty?
+    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
@@ -131,8 +130,7 @@ class WeatherPlugin < Plugin
 
     wu_units = String.new
 
-    units ||= @bot.config['weather.units']
-    case units.to_sym
+    case (units || @bot.config['weather.units']).to_sym
     when :english, :metric
       wu_units = "_#{units}"
     when :both
@@ -142,14 +140,19 @@ class WeatherPlugin < Plugin
 
     case service
     when :nws
-      nws_describe(m, loc)
+      nws_describe(m, where)
     when :station
-      wu_station(m, loc, wu_units)
+      wu_station(m, where, wu_units)
     when :wu
-      wu_weather(m, loc, wu_units)
+      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, loc, units]
+    @registry[m.sourcenick] = [service, where, units]
   end
 
   def nws_describe(m, where)
@@ -162,6 +165,9 @@ class WeatherPlugin < Plugin
       begin
         m.reply met.update
         @nws_cache[where] = met
+      rescue Net::HTTPError => e
+        m.reply _("%{error}, will try WU service") % { :error => e.message }
+        wu_weather(m, where)
       rescue => e
         m.reply e.message
       end
@@ -170,7 +176,7 @@ class WeatherPlugin < Plugin
     end
   end
 
-  def wu_station(m, where, units)
+  def wu_station(m, where, units="")
     begin
       xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
       case xml
@@ -193,7 +199,7 @@ class WeatherPlugin < Plugin
     end
   end
 
-  def wu_weather(m, where, units)
+  def wu_weather(m, where, units="")
     begin
       xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
       case xml
@@ -212,7 +218,7 @@ class WeatherPlugin < Plugin
           m.reply "couldn't parse weather data from #{where}"
         end
         wu_out_special(m, xml)
-      when /<a href="\/(?:global\/stations|US\/\w\w)\//
+      when /<a href="\/auto\/mobile[^\/]+\/(?:global\/stations|[A-Z][A-Z])\//
         wu_weather_multi(m, xml)
       else
         debug xml
@@ -223,21 +229,9 @@ class WeatherPlugin < Plugin
     end
   end
 
-  def wu_clean(stuff)
-    txt = stuff
-    txt.gsub!(/[\n\s]+/,' ')
-    txt.gsub!(/&nbsp;/, ' ')
-    txt.gsub!(/&#176;/, ' ') # degree sign
-    txt.gsub!(/<\/?b>/,'')
-    txt.gsub!(/<\/?span[^<>]*?>/,'')
-    txt.gsub!(/<img\s*[^<>]*?>/,'')
-    txt.gsub!(/<br\s?\/?>/,'')
-    txt
-  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)
+    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)
     # 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|
@@ -247,9 +241,9 @@ class WeatherPlugin < Plugin
       par = ar[3]
       w = ar[4]
       if state # US station
-        (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, wu_clean(w)])
+        (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, w.ircify_html])
       else # non-US station
-        (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, wu_clean(w)])
+        (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, w.ircify_html])
       end
     }
     m.reply stations.join("; ")
@@ -289,21 +283,108 @@ class WeatherPlugin < Plugin
   end
 
   def wu_weather_filter(stuff)
-    txt = wu_clean(stuff)
-
     result = Array.new
-    if txt.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
-      result << ("Weather info for %s (updated on %s)" % [$2, $1])
+    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
-    txt.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/) { |k, v|
-      next if v.empty?
-      next if ["-", "- approx.", "N/A", "N/A approx."].include?(v)
-      next if k == "Raw METAR"
-      result << ("%s: %s" % [k, v])
+    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 = WeatherPlugin.new
-plugin.map 'weather :units *where', :defaults => {:where => false, :units => false}, :requirements => {:units => /metric|english|both/}
+plugin.map 'weather :units :service *where',
+  :defaults => {
+    :where => false,
+    :units => false,
+    :service => false
+  },
+  :requirements => {
+    :units => /metric|english|both/,
+    :service => /wu|nws|station|google/
+  }