]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/weather.rb
weather: URI-encode station
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / weather.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Weather plugin for rbot
5 #
6 # Author:: MrChucho (mrchucho@mrchucho.net): NOAA National Weather Service support
7 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
8 #
9 # Copyright:: (C) 2006 Ralph M. Churchill
10 # Copyright:: (C) 2006-2007 Giuseppe Bilotta
11 #
12 # License:: GPL v2
13
14 require 'rexml/document'
15
16 # Wraps NOAA National Weather Service information
17 class CurrentConditions
18     def initialize(station)
19         @station = station
20         @url = "http://www.nws.noaa.gov/data/current_obs/#{URI.encode @station.upcase}.xml"
21         @etag = String.new
22         @mtime = Time.mktime(0)
23         @current_conditions = String.new
24         @iscached = false
25     end
26     def update
27         begin
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
33                 @iscached = false
34                 @current_conditions = parse(cc_doc)
35             end
36         rescue OpenURI::HTTPError => e
37             case e
38             when /304/
39                 @iscached = true
40             when /404/
41                 raise "Data for #{@station} not found"
42             else
43                 raise "Error retrieving data: #{e}"
44             end
45         end
46         @current_conditions # +" Cached? "+ ((@iscached) ? "Y" : "N")
47     end
48     def parse(cc_doc)
49         cc = Hash.new
50         cc_doc.elements.each do |c|
51             cc[c.name.to_sym] = c.text
52         end
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."
54     end
55 private
56     def heat_index_or_wind_chill(cc)
57         hi = cc[:heat_index_string]
58         wc = cc[:windchill_string]
59         if hi != 'NA' then
60             " with a heat index of #{hi}"
61         elsif wc != 'NA' then
62             " with a windchill of #{wc}"
63         else
64             ""
65         end
66     end
67 end
68
69 class WeatherPlugin < Plugin
70
71   Config.register Config::BooleanValue.new('weather.advisory',
72     :default => true,
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'],
76     :default => 'both',
77     :desc => "Units to be used by default in Weather Underground reports")
78
79
80   def help(plugin, topic="")
81     case topic
82     when "nws"
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/ )"
84     when "station", "wu"
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."
86     else
87       "weather information lookup. Looks up weather information for the last location you specified. See topics 'nws' and 'wu' for more information"
88     end
89   end
90
91   def initialize
92     super
93
94     @nws_cache = Hash.new
95
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"
98   end
99
100   def weather(m, params)
101     where = params[:where].to_s
102     service = params[:service].to_sym rescue nil
103     units = params[:units]
104
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
111     end
112
113     if !service
114       if where.sub!(/^station\s+/,'')
115         service = :nws
116       else
117         service = :wu
118       end
119     end
120
121     if where.empty?
122       debug "No weather location found for #{m.sourcenick}"
123       m.reply "I don't know where you are yet, #{m.sourcenick}. See 'help weather nws' or 'help weather wu' for additional help"
124       return
125     end
126
127     wu_units = String.new
128
129     case (units || @bot.config['weather.units']).to_sym
130     when :english, :metric
131       wu_units = "_#{units}"
132     when :both
133     else
134       m.reply "Ignoring unknown units #{units}"
135     end
136
137     case service
138     when :nws
139       nws_describe(m, where)
140     when :station
141       wu_station(m, where, wu_units)
142     when :wu
143       wu_weather(m, where, wu_units)
144     when :google
145       google_weather(m, where)
146     else
147       m.reply "I don't know the weather service #{service}, sorry"
148       return
149     end
150
151     @registry[m.sourcenick] = [service, where, units]
152   end
153
154   def nws_describe(m, where)
155     if @nws_cache.has_key?(where) then
156         met = @nws_cache[where]
157     else
158         met = CurrentConditions.new(where)
159     end
160     if met
161       begin
162         m.reply met.update
163         @nws_cache[where] = met
164       rescue => e
165         m.reply e.message
166       end
167     else
168       m.reply "couldn't find weather data for #{where}"
169     end
170   end
171
172   def wu_station(m, where, units)
173     begin
174       xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
175       case xml
176       when nil
177         m.reply "couldn't retrieve weather information, sorry"
178         return
179       when /Search not found:/
180         m.reply "no such station found (#{where})"
181         return
182       when /<table border.*?>(.*?)<\/table>/m
183         data = $1.dup
184         m.reply wu_weather_filter(data)
185         wu_out_special(m, xml)
186       else
187         debug xml
188         m.reply "something went wrong with the data for #{where}..."
189       end
190     rescue => e
191       m.reply "retrieving info about '#{where}' failed (#{e})"
192     end
193   end
194
195   def wu_weather(m, where, units)
196     begin
197       xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
198       case xml
199       when nil
200         m.reply "couldn't retrieve weather information, sorry"
201       when /City Not Found/
202         m.reply "no such location found (#{where})"
203       when /Current<\/a>/
204         data = ""
205         xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
206           data += wu_weather_filter(match.first)
207         end
208         if data.length > 0
209           m.reply data
210         else
211           m.reply "couldn't parse weather data from #{where}"
212         end
213         wu_out_special(m, xml)
214       when /<a href="\/auto\/mobile[^\/]+\/(?:global\/stations|[A-Z][A-Z])\//
215         wu_weather_multi(m, xml)
216       else
217         debug xml
218         m.reply "something went wrong with the data from #{where}..."
219       end
220     rescue => e
221       m.reply "retrieving info about '#{where}' failed (#{e})"
222     end
223   end
224
225   def wu_weather_multi(m, xml)
226     # debug xml
227     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)
228     # debug stations
229     m.reply "multiple stations available, use 'weather station <code>' or 'weather <city, state>' as appropriate, for one of the following (current temp shown):"
230     stations.map! { |ar|
231       warning = ar[0]
232       loc = ar[2]
233       state = ar[1]
234       par = ar[3]
235       w = ar[4]
236       if state # US station
237         (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, w.ircify_html])
238       else # non-US station
239         (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, w.ircify_html])
240       end
241     }
242     m.reply stations.join("; ")
243   end
244
245   def wu_check_special(xml)
246     specials = []
247     # We only scan the first half to prevent getting the advisories twice
248     xml[0,xml.length/2].scan(%r{<a href="([^"]+\?[^"]*feature=warning#([^"]+))"[^>]*>([^<]+)</a>}) do
249       special = {
250         :url => "http://mobile.wunderground.com"+$1,
251         :type => $2.dup,
252         :special => $3.dup
253       }
254       spec_rx = Regexp.new("<a name=\"#{special[:type]}\">(?:.+?)<td align=\"left\">\\s+(.+?)\\s+</td>\\s+</tr>\\s+</table>", Regexp::MULTILINE)
255       spec_xml = @bot.httputil.get(special[:url])
256       if spec_xml and spec_td = spec_xml.match(spec_rx)
257         special.merge!(:text => spec_td.captures.first.ircify_html)
258       end
259       specials << special
260     end
261     return specials
262   end
263
264   def wu_out_special(m, xml)
265     return unless @bot.config['weather.advisory']
266     specials = wu_check_special(xml)
267     debug specials
268     specials.each do |special|
269       special.merge!(:underline => Underline)
270       if special[:text]
271         m.reply("%{underline}%{special}%{underline}: %{text}" % special)
272       else
273         m.reply("%{underline}%{special}%{underline} @ %{url}" % special)
274       end
275     end
276   end
277
278   def wu_weather_filter(stuff)
279     result = Array.new
280     if stuff.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
281       result << ("Weather info for %s (updated on %s)" % [$2.ircify_html, $1.ircify_html])
282     end
283     stuff.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/m) { |k, v|
284       kk = k.riphtml
285       vv = v.riphtml
286       next if vv.empty?
287       next if ["-", "- approx.", "N/A", "N/A approx."].include?(vv)
288       next if kk == "Raw METAR"
289       result << ("%s: %s" % [kk, vv])
290     }
291     return result.join('; ')
292   end
293
294   # TODO allow units choice other than lang, find how the API does it
295   def google_weather(m, where)
296     botlang = @bot.config['core.language'].intern
297     if Language::Lang2Locale.key?(botlang)
298       lang = Language::Lang2Locale[botlang].sub(/.UTF.?8$/,'')
299     else
300       lang = botlang.to_s[0,2]
301     end
302
303     debug "Google weather with language #{lang}"
304     xml = @bot.httputil.get("http://www.google.com/ig/api?hl=#{lang}&weather=#{CGI.escape where}")
305     debug xml
306     weather = REXML::Document.new(xml).root.elements["weather"]
307     begin
308       error = weather.elements["problem_cause"]
309       if error
310         ermsg = error.attributes["data"]
311         ermsg = _("no reason specified") if ermsg.empty?
312         raise ermsg
313       end
314       city = weather.elements["forecast_information/city"].attributes["data"]
315       date = Time.parse(weather.elements["forecast_information/current_date_time"].attributes["data"])
316       units = weather.elements["forecast_information/unit_system"].attributes["data"].intern
317       current_conditions = weather.elements["current_conditions"]
318       foreconds = weather.elements.to_a("forecast_conditions")
319
320       conds = []
321       current_conditions.each { |el|
322         name = el.name.intern
323         value = el.attributes["data"].dup
324         debug [name, value]
325         case name
326         when :icon
327           next
328         when :temp_f
329           next if units == :SI
330           value << "°F"
331         when :temp_c
332           next if units == :US
333           value << "°C"
334         end
335         conds << value
336       }
337
338       forecasts = []
339       foreconds.each { |forecast|
340         cond = []
341         forecast.each { |el|
342           name = el.name.intern
343           value = el.attributes["data"]
344           case name
345           when :icon
346             next
347           when :high, :low
348             value << (units == :SI ? "°C" : "°F")
349             value << " |" if name == :low
350           when :condition
351             value = "(#{value})"
352           end
353           cond << value
354         }
355         forecasts << cond.join(' ')
356       }
357
358       m.reply _("Google weather info for %{city} on %{date}: %{conds}. Three-day forecast: %{forecast}") % {
359         :city => city,
360         :date => date,
361         :conds => conds.join(', '),
362         :forecast => forecasts.join('; ')
363       }
364     rescue => e
365       debug e
366       m.reply _("Google weather failed: %{e}") % { :e => e}
367     end
368
369   end
370
371 end
372
373 plugin = WeatherPlugin.new
374 plugin.map 'weather :units :service *where',
375   :defaults => {
376     :where => false,
377     :units => false,
378     :service => false
379   },
380   :requirements => {
381     :units => /metric|english|both/,
382     :service => /wu|nws|station|google/
383   }