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