]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/weather.rb
36a5a88f4e78f4f15108c9bd1d32fd254e950214
[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     units = @bot.config['weather.units'] unless units
134
135     case units.to_sym
136     when :english, :metric
137       wu_units = "_#{units}"
138     when :both
139     else
140       m.reply "Ignoring unknown units #{units}"
141     end
142
143     case service
144     when :nws
145       nws_describe(m, where)
146     when :station
147       wu_station(m, where, wu_units)
148     when :wu
149       wu_weather(m, where, wu_units)
150     when :google
151       google_weather(m, where)
152     else
153       m.reply "I don't know the weather service #{service}, sorry"
154       return
155     end
156
157     @registry[m.sourcenick] = [service, where, units]
158   end
159
160   def nws_describe(m, where)
161     if @nws_cache.has_key?(where) then
162         met = @nws_cache[where]
163     else
164         met = CurrentConditions.new(where)
165     end
166     if met
167       begin
168         m.reply met.update
169         @nws_cache[where] = met
170       rescue Net::HTTPError => e
171         m.reply _("%{error}, will try WU service") % { :error => e.message }
172         wu_weather(m, where)
173       rescue => e
174         m.reply e.message
175       end
176     else
177       m.reply "couldn't find weather data for #{where}"
178     end
179   end
180
181   def wu_station(m, where, units="")
182     begin
183       xml = @bot.httputil.get(@wu_station_url % [units, CGI.escape(where)])
184       case xml
185       when nil
186         m.reply "couldn't retrieve weather information, sorry"
187         return
188       when /Search not found:/
189         m.reply "no such station found (#{where})"
190         return
191       when /<table border.*?>(.*?)<\/table>/m
192         data = $1.dup
193         m.reply wu_weather_filter(data)
194         wu_out_special(m, xml)
195       else
196         debug xml
197         m.reply "something went wrong with the data for #{where}..."
198       end
199     rescue => e
200       m.reply "retrieving info about '#{where}' failed (#{e})"
201     end
202   end
203
204   def wu_weather(m, where, units="")
205     begin
206       xml = @bot.httputil.get(@wu_url % [units, CGI.escape(where)])
207       case xml
208       when nil
209         m.reply "couldn't retrieve weather information, sorry"
210       when /City Not Found/
211         m.reply "no such location found (#{where})"
212       when /Current<\/a>/
213         data = ""
214         xml.scan(/<table border.*?>(.*?)<\/table>/m).each do |match|
215           data += wu_weather_filter(match.first)
216         end
217         if data.length > 0
218           m.reply data
219         else
220           m.reply "couldn't parse weather data from #{where}"
221         end
222         wu_out_special(m, xml)
223       when /<a href="\/auto\/mobile[^\/]+\/(?:global\/stations|[A-Z][A-Z])\//
224         wu_weather_multi(m, xml)
225       else
226         debug xml
227         m.reply "something went wrong with the data from #{where}..."
228       end
229     rescue => e
230       m.reply "retrieving info about '#{where}' failed (#{e})"
231     end
232   end
233
234   def wu_weather_multi(m, xml)
235     # debug xml
236     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)
237     # debug stations
238     m.reply "multiple stations available, use 'weather station <code>' or 'weather <city, state>' as appropriate, for one of the following (current temp shown):"
239     stations.map! { |ar|
240       warning = ar[0]
241       loc = ar[2]
242       state = ar[1]
243       par = ar[3]
244       w = ar[4]
245       if state # US station
246         (warning ? "*" : "") + ("%s, %s (%s): %s" % [loc, state, par, w.ircify_html])
247       else # non-US station
248         (warning ? "*" : "") + ("station %s (%s): %s" % [loc, par, w.ircify_html])
249       end
250     }
251     m.reply stations.join("; ")
252   end
253
254   def wu_check_special(xml)
255     specials = []
256     # We only scan the first half to prevent getting the advisories twice
257     xml[0,xml.length/2].scan(%r{<a href="([^"]+\?[^"]*feature=warning#([^"]+))"[^>]*>([^<]+)</a>}) do
258       special = {
259         :url => "http://mobile.wunderground.com"+$1,
260         :type => $2.dup,
261         :special => $3.dup
262       }
263       spec_rx = Regexp.new("<a name=\"#{special[:type]}\">(?:.+?)<td align=\"left\">\\s+(.+?)\\s+</td>\\s+</tr>\\s+</table>", Regexp::MULTILINE)
264       spec_xml = @bot.httputil.get(special[:url])
265       if spec_xml and spec_td = spec_xml.match(spec_rx)
266         special.merge!(:text => spec_td.captures.first.ircify_html)
267       end
268       specials << special
269     end
270     return specials
271   end
272
273   def wu_out_special(m, xml)
274     return unless @bot.config['weather.advisory']
275     specials = wu_check_special(xml)
276     debug specials
277     specials.each do |special|
278       special.merge!(:underline => Underline)
279       if special[:text]
280         m.reply("%{underline}%{special}%{underline}: %{text}" % special)
281       else
282         m.reply("%{underline}%{special}%{underline} @ %{url}" % special)
283       end
284     end
285   end
286
287   def wu_weather_filter(stuff)
288     result = Array.new
289     if stuff.match(/<\/a>\s*Updated:\s*(.*?)\s*Observed at\s*(.*?)\s*<\/td>/)
290       result << ("Weather info for %s (updated on %s)" % [$2.ircify_html, $1.ircify_html])
291     end
292     stuff.scan(/<tr>\s*<td>\s*(.*?)\s*<\/td>\s*<td>\s*(.*?)\s*<\/td>\s*<\/tr>/m) { |k, v|
293       kk = k.riphtml
294       vv = v.riphtml
295       next if vv.empty?
296       next if ["-", "- approx.", "N/A", "N/A approx."].include?(vv)
297       next if kk == "Raw METAR"
298       result << ("%s: %s" % [kk, vv])
299     }
300     return result.join('; ')
301   end
302
303   # TODO allow units choice other than lang, find how the API does it
304   def google_weather(m, where)
305     botlang = @bot.config['core.language'].intern
306     if Language::Lang2Locale.key?(botlang)
307       lang = Language::Lang2Locale[botlang].sub(/.UTF.?8$/,'')
308     else
309       lang = botlang.to_s[0,2]
310     end
311
312     debug "Google weather with language #{lang}"
313     xml = @bot.httputil.get("http://www.google.com/ig/api?hl=#{lang}&weather=#{CGI.escape where}")
314     debug xml
315     weather = REXML::Document.new(xml).root.elements["weather"]
316     begin
317       error = weather.elements["problem_cause"]
318       if error
319         ermsg = error.attributes["data"]
320         ermsg = _("no reason specified") if ermsg.empty?
321         raise ermsg
322       end
323       city = weather.elements["forecast_information/city"].attributes["data"]
324       date = Time.parse(weather.elements["forecast_information/current_date_time"].attributes["data"])
325       units = weather.elements["forecast_information/unit_system"].attributes["data"].intern
326       current_conditions = weather.elements["current_conditions"]
327       foreconds = weather.elements.to_a("forecast_conditions")
328
329       conds = []
330       current_conditions.each { |el|
331         name = el.name.intern
332         value = el.attributes["data"].dup
333         debug [name, value]
334         case name
335         when :icon
336           next
337         when :temp_f
338           next if units == :SI
339           value << "°F"
340         when :temp_c
341           next if units == :US
342           value << "°C"
343         end
344         conds << value
345       }
346
347       forecasts = []
348       foreconds.each { |forecast|
349         cond = []
350         forecast.each { |el|
351           name = el.name.intern
352           value = el.attributes["data"]
353           case name
354           when :icon
355             next
356           when :high, :low
357             value << (units == :SI ? "°C" : "°F")
358             value << " |" if name == :low
359           when :condition
360             value = "(#{value})"
361           end
362           cond << value
363         }
364         forecasts << cond.join(' ')
365       }
366
367       m.reply _("Google weather info for %{city} on %{date}: %{conds}. Three-day forecast: %{forecast}") % {
368         :city => city,
369         :date => date,
370         :conds => conds.join(', '),
371         :forecast => forecasts.join('; ')
372       }
373     rescue => e
374       debug e
375       m.reply _("Google weather failed: %{e}") % { :e => e}
376     end
377
378   end
379
380 end
381
382 plugin = WeatherPlugin.new
383 plugin.map 'weather :units :service *where',
384   :defaults => {
385     :where => false,
386     :units => false,
387     :service => false
388   },
389   :requirements => {
390     :units => /metric|english|both/,
391     :service => /wu|nws|station|google/
392   }