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