rubyweather 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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