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