rubyweather 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,113 @@
1
+ = RubyWeather
2
+
3
+ Author:: Matt Zukowski (http://blog.roughest.net)
4
+ Copyright:: Copyright (c) 2006 Urbacon Ltd.
5
+ License:: GNU Lesser General Public License v2.1 (LGPL 2.1)
6
+
7
+ <b>RubyWeather is a Ruby[http://ruby-lang.org] library for fetching weather-related data from weather.com[http://www.weather.com/services/xmloap.html].</b>
8
+
9
+ Simple usage example:
10
+
11
+ require 'rubygems'
12
+ require_gem 'rubyweather'
13
+
14
+ require 'weather'
15
+
16
+ service = Weather::Service.new
17
+ service.partner_id = <b>your partner id</b>
18
+ service.license_key = <b>your license key</b>
19
+
20
+ locations = service.find_location('Toronto')
21
+ puts "Matching Locations: " + locations.inspect
22
+
23
+ The above will print out a list of available locations and their codes. We can
24
+ now use these codes to fetch the weather data for our city:
25
+
26
+ forecast = service.fetch_forecast("CAXX0504", 5)
27
+
28
+ puts "Location: %s" % forecast.location_name
29
+
30
+ puts "Current Temperature: %s" % forecast.current.temperature
31
+ puts "Current Windspeed: %s" % forecast.current.wind.speed
32
+
33
+ puts "Tomorrow's High: %s" % forecast.tomorrow.high
34
+ puts "Tomorrow's Outlook: %s" % forecast.tomorrow.outlook
35
+ puts "Tomorrow's Wind Direction: %s" % forecast.tomorrow.wind.direction
36
+
37
+ Forecasts for days in the future are accessed via <tt>forecast.day(#)</tt> where <tt>#</tt> is the number of day sinto the future
38
+ (assuming that you've fetched data for as many days in your <tt>service.fetch_forecast</tt> request):
39
+
40
+ puts "High 3 days from now: %s" % forecast.day(3).high
41
+ puts "Probability of precipitation 4 days from now: %s" % forecast.day(4).pop
42
+
43
+ There are a lot of attributes you can fetch for a forecast day. Here are just a few:
44
+
45
+ <tt>temp</tt>, <tt>temperature</tt>, <tt>temp</tt>::
46
+ The temperature. For future days this is equivalent to the low for nighttime and high for daytime.
47
+ <tt>icon</tt>::
48
+ The number of the icon gif file from the weather.com SDK[http://www.weather.com/services/xmloap.html] that
49
+ identifies the conditions (e.g. a little icon of a cloud with rain, or sun, or whatever).
50
+ <tt>outlook</tt>, <tt>outlook_brief</tt>::
51
+ Brief text describing the conditions (e.g. "Mostly Cloudy", "Rain", "Scattered T-Storms"). An abbreviated
52
+ version is available in <tt>outlook_brief</tt> (e.g. "M Cloudy", "Scat T-Storms").
53
+ <tt>low</tt>, <tt>lo</tt>::
54
+ The forecasted low temperature. (Not available for current conditions)
55
+ <tt>high</tt>, <tt>hi</tt>::
56
+ The forecasted high temperature. (Not available for current conditions)
57
+ <tt>wind</tt>, <tt>wind.direction</tt>, <tt>wind.speed</tt>, <tt>wind.heading</tt>::
58
+ Wind conditions. The <tt>wind</tt> attribute returns an object with sub-attributes.
59
+ <tt>pop</tt>, <tt>ppcp</tt>::
60
+ Probability of precipitation.
61
+ <tt>date</tt>::
62
+ The date that this forecast is for, returned as a ruby Time object.
63
+ <tt>sunrise</tt>::
64
+ The time of sunrise on the day of the forecast.
65
+ <tt>sunset</tt>::
66
+ The time of sunset on the day of the forecast.
67
+
68
+ Additionally, all (or at least most) of the attributes for a given day in the raw weather.com xml data are
69
+ also available. For example, you can call forecast.tomorrow.dewp to get the dewpoint, because the xml file
70
+ contains a <tt>dewp</tt> element for that day. Have a look at #test/test_weather.xml to see what data is
71
+ available in the xml file. Note though that raw xml elements will be returned as a string, without any nice
72
+ class casting or unit conversion.
73
+
74
+ I encourage other programmers to add more functionality to the lib/forecast.rb module to provide better
75
+ accessor methods for the underlying xml data.
76
+
77
+ == Download
78
+
79
+ You can download the latest stable version of RubyWeather from http://rubyforge.org/projects/rubyweather/.
80
+ Alternatively, the files <em>may</em> be available from http://roughest.net/rubyweather (but no promises -- the
81
+ rubyforge site is a better bet).
82
+
83
+ You can also check out the latest copy via subversion from svn://rubyforge.org//var/svn/rubyweather/trunk. If you would
84
+ like to contribute back your changes to the code, please contact me via the rubyforge project site to obtain a subversion
85
+ account.
86
+
87
+ == Note
88
+
89
+ To use this library you will need a partner id and license key from
90
+ weather.com. The service is completely free, but requires that you agree
91
+ to weather.com's legal stuff, which, among other things, requires your software
92
+ to include a link back to the weather.com website (although this is not actually
93
+ enforced in any way by the service).
94
+
95
+ To obtain the free license visit http://www.weather.com/services/xmloap.html.
96
+ This will also allow you to download an SDK that includes nice weather icons for
97
+ use with the data.
98
+
99
+ == License
100
+
101
+ This program is free software; you can redistribute it and/or modify
102
+ it under the terms of the GNU General Public License as published by
103
+ the Free Software Foundation; either version 2 of the License, or
104
+ (at your option) any later version.
105
+
106
+ This program is distributed in the hope that it will be useful,
107
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
108
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
109
+ GNU General Public License for more details.
110
+
111
+ You should have received a copy of the GNU General Public License
112
+ along with this program (see the file called LICENSE); if not, write to the
113
+ Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
data/lib/forecast.rb ADDED
@@ -0,0 +1,318 @@
1
+ #
2
+ # Author:: Matt Zukowski (http://blog.roughest.net)
3
+ # Copyright:: Copyright (c) 2006 Urbacon Ltd.
4
+ # License:: GNU Lesser General Public License v2.1 (LGPL 2.1)
5
+ #
6
+
7
+ require File.dirname(__FILE__) + '/util'
8
+
9
+ module Weather
10
+
11
+ # Namespace module for weather data objects.
12
+ module Forecast
13
+
14
+ # Contains several days of weather data.
15
+ # The forecast object includes the Enumerable mixin, so that you can iterate
16
+ # over all of the days in the forecast using the standard ruby mechanisms
17
+ # (i.e. <tt>myforecast.each { |d| print d.outlook }</tt>).
18
+ class Forecast
19
+ include Enumerable
20
+
21
+ attr_reader :xml
22
+
23
+ # Instantiate a Forecast object from the specified Weather.com REXML::Document.
24
+ def initialize(weather_xmldoc)
25
+ if (not weather_xmldoc.kind_of? REXML::Document)
26
+ raise "The xml document given to the Forecast constructor must be a valid REXML::Document"
27
+ end
28
+
29
+ @xml = weather_xmldoc
30
+ end
31
+
32
+ # Returns the current conditions as a Conditions object.
33
+ def current
34
+ CurrentConditions.new(xml.root.elements['cc'])
35
+ end
36
+
37
+ # Alias for day(1)
38
+ def tomorrow
39
+ day(1)
40
+ end
41
+
42
+ # Returns the conditions for the given day (0 = today, 1 = tomorrow, etc.)
43
+ # The maximum day number depends on the data available in the xml that was used to create this Forecast.
44
+ def day(num)
45
+ element = xml.root.elements['dayf'].elements["day[@d='#{num.to_s}']"]
46
+ if not element
47
+ case num
48
+ when 0 then daydisplay = "today"
49
+ when 1 then daydisplay = "tomorrow"
50
+ else daydisplay = "#{num} days from now"
51
+ end
52
+ raise "Sorry, there is no data available for #{daydisplay}"
53
+ else
54
+ Day.new(element)
55
+ end
56
+ end
57
+
58
+ # Returns the conditions for the given night (0 = tonight, 1 = tomorrow night, etc.)
59
+ # The maximum day number depends on the data available in the xml that was used to create this Forecast.
60
+ def night(num)
61
+ element = xml.root.elements['dayf'].elements["day[@d='#{num.to_s}']"]
62
+ Night.new(element)
63
+ end
64
+
65
+ # Iterates over all of the days in this Forecast.
66
+ def each(&block)
67
+ first = true
68
+ REXML::XPath.match(xml, "//dayf/day").each do |dxml|
69
+ d = Day.new(dxml)
70
+
71
+ # if it is after 3 PM, use current conditions
72
+ if Time.now > Time.local(d.date.year, d.date.month, d.date.day, 15)
73
+ d = current
74
+ end
75
+
76
+ yield d
77
+
78
+ first = false if first
79
+ end
80
+ end
81
+
82
+ # Returns the full human-readable name of the place that this Forecast is for.
83
+ def location_name
84
+ xml.root.elements['loc'].elements['dnam'].text
85
+ end
86
+
87
+ # Returns the name of the city that this Forecast is for.
88
+ def location_city
89
+ xml.root.elements['loc'].elements['dnam'].text.split(",").first
90
+ end
91
+
92
+ # The location code of the weather station that this Forecast is for.
93
+ def location_code
94
+ xml.root.elements['loc'].attributes['id']
95
+ end
96
+
97
+ # Returns true if the units returned by this Forecast will be in the metric system (i.e. Celcius).
98
+ def metric?
99
+ xml.root.elements['head'].elements['ut'].text == "C"
100
+ end
101
+ end
102
+
103
+ # Abstract class that all Forecast entities (roughly "days") are based on.
104
+ class Conditions
105
+
106
+ # For elements in the forecast that we have not defined an explicit accessor,
107
+ # this allows accessing the raw underlying data in the forecast xml.
108
+ def method_missing(symbol)
109
+ begin
110
+ return @xml.elements[symbol.to_s].text
111
+ rescue NoMethodError
112
+ return "N/A"
113
+ end
114
+ end
115
+
116
+ # Returns the wind conditions as an anonymous object (i.e. wind.d for wind
117
+ # direction, wind.s for wind speed, etc.) See the <wind> element in the
118
+ # weather.com XML data spec for more info.
119
+ def wind
120
+ fix_wind(complex_attribute(@xml.elements['wind']))
121
+ end
122
+
123
+ private
124
+ # Returns the element specified by name as a cleaned-up temperature value.
125
+ # That is, if the temperature is "N/A", then nil is returned; otherwise
126
+ # the value is converted to an integer.
127
+ def clean_temp(name)
128
+ temp = @xml.elements[name].text
129
+
130
+ if (temp == "N/A")
131
+ return nil
132
+ else
133
+ return temp.to_i
134
+ end
135
+ end
136
+
137
+ # Return the given xml element as an anonymous object, with the text nodes of the element's
138
+ # immediate children available as accessor methods.
139
+ # This allows for accessing attributes that have child elements (i.e. wind, bar, etc.)
140
+ # as anonymous objects (i.e. wind.d for wind direction, wind.s for wind speed, etc.)
141
+ def complex_attribute(objxml)
142
+ obj = {}
143
+ class << obj
144
+ def method_missing(name)
145
+ return self[name.to_s]
146
+ end
147
+ end
148
+
149
+ objxml.elements.each do |element|
150
+ obj[element.name] = element.text
151
+ end
152
+
153
+ return obj
154
+ end
155
+
156
+ def fix_wind(obj)
157
+ obj['heading'] = obj.t
158
+ obj['direction'] = obj.d.to_i
159
+ obj['speed'] = obj.s.to_i
160
+
161
+ return obj
162
+ end
163
+ end
164
+
165
+ # Represents the current weather conditions.
166
+ class CurrentConditions < Conditions
167
+ attr_reader :xml
168
+
169
+ @xml
170
+
171
+ def initialize(element)
172
+ if (not element.kind_of? REXML::Element)
173
+ raise "The xml element given to the Day/Night constructor must be a valid REXML::Element"
174
+ end
175
+ @xml = element
176
+ end
177
+
178
+ def icon
179
+ xml.elements['icon'].text.to_i
180
+ end
181
+
182
+ def temp
183
+ clean_temp('tmp')
184
+ end
185
+ alias_method :tmp, :temp
186
+ alias_method :temperature, :temp
187
+
188
+ def outlook
189
+ xml.elements['t'].text
190
+ end
191
+
192
+ def outlook_brief
193
+ xml.elements['bt'].text
194
+ end
195
+
196
+ def pop
197
+ nil
198
+ end
199
+ alias_method :ppcp, :pop
200
+
201
+ def date
202
+ Time.now
203
+ end
204
+ end
205
+
206
+ # Abstract class representing either a day or night in the daily forecast (note that "future" can include today).
207
+ class FutureConditions < Conditions
208
+ attr_reader :xml
209
+
210
+ @xml
211
+
212
+ def initialize(element)
213
+ if (not element.kind_of? REXML::Element)
214
+ raise "The xml element given to the Day/Night constructor must be a valid REXML::Element"
215
+ end
216
+ @xml = element
217
+ end
218
+
219
+ def method_missing(name)
220
+ begin
221
+ return mypart.elements[name.to_s].text
222
+ rescue NoMethodError
223
+ return "N/A"
224
+ end
225
+ end
226
+
227
+ def wind
228
+ fix_wind(complex_attribute(mypart.elements['wind']))
229
+ end
230
+
231
+ def date
232
+ # FIXME: this will break if rolling over to next year (i.e. fetched 5 days into the future on Dec 30), since today's year is assumed
233
+ mon, day = @xml.attributes['dt'].split(" ")
234
+ Time.local(Time.now.year, mon, day)
235
+ end
236
+
237
+ def icon
238
+ mypart.elements['icon'].text.to_i
239
+ end
240
+
241
+ def high
242
+ clean_temp('hi')
243
+ end
244
+ alias_method :hi, :high
245
+
246
+ def low
247
+ clean_temp('low')
248
+ end
249
+ alias_method :lo, :low
250
+
251
+
252
+ def outlook
253
+ mypart.elements['t'].text
254
+ end
255
+
256
+ def outlook_brief
257
+ mypart.elements['bt'].text
258
+ end
259
+
260
+ def pop
261
+ mypart.elements['ppcp'].text.to_i
262
+ end
263
+ alias_method :ppcp, :pop
264
+
265
+ def sunrise
266
+ hour,minute = @xml.elements['sunr'].text.split(" ").first.split(":")
267
+ hour = hour.to_i
268
+ minute = minute.to_i
269
+ Time.local(date.year, date.month, date.day, hour, minute)
270
+ end
271
+
272
+ def sunset
273
+ hour,minute = @xml.elements['suns'].text.split(" ").first.split(":")
274
+ hour = hour.to_i + 12 # add 12 since we need 24 hour clock and sunset is always (?) in the PM
275
+ minute = minute.to_i
276
+ Time.local(date.year, date.month, date.day, hour, minute)
277
+ end
278
+
279
+ end
280
+
281
+ # The daytime part of the forecast for a given day.
282
+ class Day < FutureConditions
283
+ def initialize(element)
284
+ super(element)
285
+ end
286
+
287
+ def temp
288
+ high
289
+ end
290
+ alias_method :tmp, :temp
291
+ alias_method :temperature, :temp
292
+
293
+ private
294
+ def mypart
295
+ @xml.elements['part[@p="d"]']
296
+ end
297
+ end
298
+
299
+ # The nighttime part of a forecast for a given day.
300
+ class Night < FutureConditions
301
+ def initialize(element)
302
+ super(element)
303
+ end
304
+
305
+ def temp
306
+ low
307
+ end
308
+ alias_method :tmp, :temp
309
+ alias_method :temperature, :temp
310
+
311
+ private
312
+ def mypart
313
+ @xml.elements['part[@p="n"]']
314
+ end
315
+ end
316
+ end
317
+
318
+ end
data/lib/service.rb ADDED
@@ -0,0 +1,84 @@
1
+ #
2
+ # Author:: Matt Zukowski (http://blog.roughest.net)
3
+ # Copyright:: Copyright (c) 2006 Urbacon Ltd.
4
+ # License:: GNU Lesser General Public License v2.1 (LGPL 2.1)
5
+ #
6
+
7
+ require 'net/http'
8
+ require 'rexml/document'
9
+
10
+ require File.dirname(__FILE__) + '/forecast'
11
+
12
+ module Weather
13
+
14
+ # Interface for interacting with the weather.com service.
15
+ class Service
16
+ attr_writer :partner_id, :license_key, :imperial
17
+ attr_reader :partner_id, :license_key, :imperial
18
+
19
+ # Returns the forecast data fetched from the weather.com xoap service for the given location and number of days.
20
+ def fetch_forecast(location_id, days = 5)
21
+
22
+ # try to pull the partner_id and license_key from the environment if not already set
23
+ partner_id = ENV['WEATHER_COM_PARTNER_ID'] unless partner_id
24
+ license_key = ENV['WEATHER_COM_LICENSE_KEY'] unless license_key
25
+
26
+ if imperial or (ENV.has_key? 'USE_IMPERIAL_UNITS' and ENV['USE_IMPERIAL_UNITS'])
27
+ imperial = true
28
+ else
29
+ imperial = false
30
+ end
31
+
32
+ # NOTE: Strangely enough, weather.com doesn't seem to be enforcing the partner_id/license_key stuff. You can specify blank values for both
33
+ # and the service will return the data just fine (actually, it will accept any value as valid). I'm commenting out these checks
34
+ # for now, but we may need to re-enable these once weather.com figures out what's going on.
35
+ #if not partner_id
36
+ # puts "WARNING: No partner ID has been set. Please obtain a partner ID from weather.com before attempting to fetch a forecast, otherwise the data you requested may not be available."
37
+ #end
38
+ #
39
+ #if not license_key
40
+ # puts "WARNING: No license key has been set. Please obtain a license key from weather.com before attempting to fetch a forecast, otherwise the data you requested may not be available"
41
+ #end
42
+
43
+ # default to metric (degrees fahrenheit are just silly :)
44
+ unit = imperial ? "s" : "m"
45
+
46
+ host = "xoap.weather.com"
47
+ url = "/weather/local/#{location_id}?cc=*&dayf=#{days}&prod=xoap&par=#{partner_id}&key=#{license_key}&unit=#{unit}"
48
+
49
+ # puts "Using url: "+url
50
+
51
+ xml = Net::HTTP.get(host, url);
52
+ doc = REXML::Document.new(xml)
53
+
54
+ Forecast::Forecast.new(doc)
55
+ end
56
+
57
+ # Returns the forecast data loaded from a file. This is useful for testing.
58
+ def load_forecast(filename)
59
+ file = File.new(filename)
60
+ doc = REXML::Document.new(file)
61
+
62
+ Forecast::Forecast.new(doc)
63
+ end
64
+
65
+ # Returns a hash containing location_code => location_name key-value pairs for the given location search string.
66
+ # In other words, you can use this to find a location code based on a city name, ZIP code, etc.
67
+ def find_location(search_string)
68
+ host = "xoap.weather.com"
69
+ # FIXME: need to do url encoding of the search string!
70
+ url = "/weather/search/search?where=#{search_string}"
71
+
72
+ xml = Net::HTTP.get(host, url);
73
+ doc = REXML::Document.new(xml)
74
+
75
+ locations = {}
76
+
77
+ REXML::XPath.match(doc.root, "//loc").each do |loc|
78
+ locations[loc.attributes['id']] = loc.text
79
+ end
80
+
81
+ return locations
82
+ end
83
+ end
84
+ end