regentanz 0.2.0

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.
Files changed (39) hide show
  1. data/.gitignore +5 -0
  2. data/CHANGELOG.rdoc +22 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +24 -0
  5. data/README.rdoc +46 -0
  6. data/Rakefile +23 -0
  7. data/lib/regentanz/astronomy.rb +69 -0
  8. data/lib/regentanz/cache/base.rb +51 -0
  9. data/lib/regentanz/cache/file.rb +86 -0
  10. data/lib/regentanz/cache.rb +2 -0
  11. data/lib/regentanz/callbacks.rb +18 -0
  12. data/lib/regentanz/conditions/base.rb +16 -0
  13. data/lib/regentanz/conditions/current.rb +14 -0
  14. data/lib/regentanz/conditions/forecast.rb +14 -0
  15. data/lib/regentanz/conditions.rb +3 -0
  16. data/lib/regentanz/configuration.rb +55 -0
  17. data/lib/regentanz/configurator.rb +22 -0
  18. data/lib/regentanz/google_weather.rb +151 -0
  19. data/lib/regentanz/parser/google_weather.rb +100 -0
  20. data/lib/regentanz/parser.rb +1 -0
  21. data/lib/regentanz/test_helper.rb +52 -0
  22. data/lib/regentanz/version.rb +4 -0
  23. data/lib/regentanz.rb +12 -0
  24. data/regentanz.gemspec +31 -0
  25. data/test/factories.rb +8 -0
  26. data/test/support/support_mailer.rb +7 -0
  27. data/test/support/tmp/.gitignore +1 -0
  28. data/test/support/valid_response.xml.erb +26 -0
  29. data/test/test_helper.rb +18 -0
  30. data/test/unit/astronomy_test.rb +26 -0
  31. data/test/unit/cache/base_test.rb +53 -0
  32. data/test/unit/cache/file_test.rb +141 -0
  33. data/test/unit/callbacks_test.rb +27 -0
  34. data/test/unit/configuration_test.rb +57 -0
  35. data/test/unit/current_condition_test.rb +33 -0
  36. data/test/unit/forecast_condition_test.rb +35 -0
  37. data/test/unit/google_weather_test.rb +131 -0
  38. data/test/unit/parser/google_weather_parser_test.rb +71 -0
  39. metadata +219 -0
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ pkg
3
+ *~
4
+ *.swp
5
+ Gemfile.lock
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,22 @@
1
+ == CHANGELOG
2
+
3
+ === v0.1.4 // 2011-10-14
4
+ * Fixed bug that caused forecast to have the current day only
5
+ * present? now properly returns its true state (as opposed to always false)
6
+ * Known issue: {current_date_time node always returns the beginning of The Epoch}[https://github.com/carpodaster/regentanz/issues/1] (API problem)
7
+
8
+ === v0.1.2 // 2011-04-19
9
+ * Retry state is tracked by cache backend
10
+ * Removed OpenStruct by adding Conditions::Forecast and Conditions::Current
11
+ * XML-Parsing is down in parser class
12
+
13
+ === v0.1.0 // 2011-04-16
14
+ * Moved caching into separate module
15
+
16
+ === v0.0.5 // 2011-04-15
17
+ * Removed RAILS_ROOT
18
+ * Moved constants into class-wide configuration
19
+ * Callback-stubs
20
+
21
+ === v0.0.1 // 2011-04-13
22
+ * Initially packaged model as a gem
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in regentanz.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2011, Carsten Zimmermann
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name of the original author / copyright holder nor the
12
+ names of its contributors may be used to endorse or promote products
13
+ derived from this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.rdoc ADDED
@@ -0,0 +1,46 @@
1
+ == Regentanz
2
+ *Regentanz* (German: <i>rain dance</i>) is a Ruby library to connect to Google's
3
+ innofficial (ie. undocumented and unsupported) weather API.
4
+
5
+ === Installation
6
+ The gem is not published yet and can be installed from github:
7
+ git clone git://github.com/kaupertmedia/regentanz.git
8
+ cd regentanz
9
+ gem build regentanz.gemspec
10
+ gem install regentanz-<gemversion>.gem
11
+
12
+ Much easier using bundler:
13
+ # Gemfile
14
+ gem 'regentanz', :git => "git://github.com/carpodaster/regentanz"
15
+
16
+ === Usage
17
+ Supply a location and a language to retrieve weather:
18
+ weather = Regentanz::GoogleWeather.new(:location => "Berlin, Germany", :lang => :en)
19
+ weather.current # current condition
20
+ weather.forecast # array with forecast conditions
21
+
22
+ It uses <b>file-based caching</b> by default, other cache backends will follow.
23
+ See {Regentanz::Cache::Base}[https://github.com/carpodaster/regentanz/blob/master/lib/regentanz/cache/base.rb]
24
+ for details (and if you want to create your own backend).
25
+
26
+ === Configuration
27
+ *Regentanz* can either be configured through a configure block or directly via
28
+ its configuration object. It uses sane defaults so there should be no need for
29
+ configuration to start right off. If you're using *Regentanz* with Rails,
30
+ a file in <tt>config/initializers</tt> is your friend.
31
+
32
+ Configure block:
33
+ Regentanz.configure do |config|
34
+ config.cache_backend Regentanz::Cache::File
35
+ config.cache_dir "/path/to/cache_file"
36
+ end
37
+
38
+ Direct configuration:
39
+ Regentanz.configuration.cache_dir = "/some/other/path"
40
+
41
+ See {Regentanz::Configuration}[https://github.com/carpodaster/regentanz/blob/master/lib/regentanz/configuration.rb]
42
+ for a full list of configurable options.
43
+
44
+ === Credits
45
+ *Regentanz* is based upon and extracted from a standalone Ruby class made for
46
+ {berlin.kauperts.de}[http://berlin.kauperts.de] by {kaupert media gmbh}[http://kaupertmedia.de].
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
4
+ require 'rake/testtask'
5
+ require 'rake/rdoctask'
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => :test
9
+
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << 'lib'
12
+ t.libs << 'test'
13
+ t.pattern = 'test/**/*_test.rb'
14
+ t.verbose = true
15
+ end
16
+
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'Regentanz'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README*')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
@@ -0,0 +1,69 @@
1
+ module Regentanz
2
+ # :nodoc:
3
+ # Code taken from http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/264573
4
+ module Astronomy
5
+ include Math
6
+
7
+ private
8
+ # :doc:
9
+
10
+ def sun_rise_set(mode, lat, lng, zenith = 90.8333)
11
+ #step 1: first calculate the day of the year
12
+ mode = mode.to_sym
13
+ date = Date.today
14
+ n=date.yday
15
+
16
+ #step 2: convert the longitude to hour value and calculate an approximate time
17
+ lng_hour=lng/15
18
+ t = n+ ((6-lng_hour)/24) if mode==:sunrise
19
+ t = n+ ((18-lng_hour)/24) if mode==:sunset
20
+
21
+ #step 3: calculate the sun's mean anomaly
22
+ m = (0.9856 * t) - 3.289
23
+
24
+ #step 4: calculate the sun's true longitude
25
+ l = (m+(1.1916 * sin(deg_to_rad(m))) + (0.020 * sin(deg_to_rad(2*m))) + 282.634) % 360
26
+
27
+ #step 5a: calculate the sun's right ascension
28
+ ra = rad_to_deg(atan(0.91764 * tan(deg_to_rad(l)))) % 360
29
+
30
+ #step 5b: right ascension value needs to be in the same quadrant as L
31
+ lquadrant = (l/90).floor*90
32
+ raquadrant = (ra/90).floor*90
33
+ ra = ra+(lquadrant-raquadrant)
34
+
35
+ #step 5c: right ascension value needs to be converted into hours
36
+ ra/=15
37
+
38
+ #step 6: calculate the sun's declination
39
+ sin_dec = 0.39782 * sin(deg_to_rad(l))
40
+ cos_dec = cos(asin(sin_dec))
41
+ #step 7a: calculate the sun's local hour angle
42
+ cos_h = (cos(deg_to_rad(zenith)) - (sin_dec * sin(deg_to_rad(lat)))) / (cos_dec * cos(deg_to_rad(lat)))
43
+
44
+ return nil if (not (-1..1).include? cos_h)
45
+
46
+ #step 7b: finish calculating H and convert into hours
47
+ h = (360 - rad_to_deg(acos(cos_h)))/15 if mode==:sunrise
48
+ h = (rad_to_deg(acos(cos_h)))/15 if mode==:sunset
49
+
50
+ #step 8: calculate local mean time
51
+ t = h + ra - (0.06571 * t) - 6.622
52
+ t %=24
53
+
54
+ #step 9: convert to UTC
55
+ return (date.to_datetime+(t - lng_hour)/24).to_time.getlocal
56
+ end
57
+
58
+ # Convenience helper
59
+ def deg_to_rad(degrees)
60
+ degrees*PI/180
61
+ end
62
+
63
+ # Convenience helper
64
+ def rad_to_deg(radians)
65
+ radians*180/PI
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,51 @@
1
+ module Regentanz
2
+
3
+ module Cache
4
+
5
+ class Base
6
+
7
+ # Checks if +instance_or_class+ complies to cache backend duck type,
8
+ # ie. if it reponds to all mandatory methods
9
+ def self.lint(instance_or_class)
10
+ instance = instance_or_class.is_a?(Class) ? instance_or_class.new : instance_or_class
11
+ [:set, :get, :available?, :expire!, :valid?].inject(true) do |memo, method|
12
+ memo && instance.respond_to?(method)
13
+ end
14
+ end
15
+
16
+ # Returns a alpha-numeric cache key
17
+ def self.sanitize_key(key)
18
+ Digest::SHA1.hexdigest(key.to_s)
19
+ end
20
+
21
+ # Stores cache +value+ as +key+.
22
+ def set(key, value); end
23
+
24
+ # Retrieves cached value from +key+.
25
+ def get(key); end
26
+
27
+ # Checks if cache under +key+ is available.
28
+ def available?(key); end
29
+
30
+ # Deletes cache under +key+.
31
+ def expire!(key); end
32
+
33
+ # Checks if cache under +key+ is still valid.
34
+ def valid?(key); end
35
+
36
+ # Returns whether or not weather retrieval from the API
37
+ # is currently waiting for a timeout to expire
38
+ def waiting_for_retry?; end
39
+
40
+ # Checks if we've waited enough. Unsets a possible retry
41
+ # state (and returns true) if so or returns false if not
42
+ def unset_retry_state!; end
43
+
44
+ # Persists a timeout state
45
+ def set_retry_state!; end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,86 @@
1
+ module Regentanz
2
+
3
+ module Cache
4
+
5
+ # Implements file-based caching. Cache files are stored in
6
+ # +Regentanz.configuration.cache_dir+ and are prefixed with
7
+ # +Regentanz.configuration.cache_prefix+.
8
+ class File < Regentanz::Cache::Base
9
+
10
+ # Cache is available (n.b. not necessarily #valid?) if a file
11
+ # exists for +key+
12
+ def available?(key)
13
+ ::File.exists?(filename(key)) rescue nil
14
+ end
15
+
16
+ # Unlinks the cache file for +key+
17
+ def expire!(key)
18
+ return false unless available?(key)
19
+ begin
20
+ ::File.delete(filename(key))
21
+ true
22
+ rescue
23
+ end
24
+ end
25
+
26
+ def filename(key)
27
+ ::File.join(Regentanz.configuration.cache_dir, "#{Regentanz.configuration.cache_prefix}_#{key}.xml")
28
+ end
29
+
30
+ # Retrieves content of #filename for +key+
31
+ def get(key)
32
+ if available?(key)
33
+ ::File.open(filename(key), "r") { |file| file.read } rescue nil
34
+ end
35
+ end
36
+
37
+ # Stores +value+ in #filename for +key+
38
+ def set(key, value)
39
+ begin
40
+ ::File.open(filename(key), "w") { |file| file.puts value }
41
+ filename(key)
42
+ rescue
43
+ end
44
+ end
45
+
46
+ def valid?(key)
47
+ return false unless available?(key)
48
+ begin
49
+ # TODO delegate XML parsing and verification
50
+ doc = REXML::Document.new(get(key))
51
+ node = doc.elements["xml_api_reply/weather/forecast_information/current_date_time"]
52
+ time = node.attribute("data").to_s.to_time if node
53
+ time > Regentanz.configuration.cache_ttl.seconds.ago
54
+ rescue
55
+ # TODO pass exception upstream until properly delegated in the first place?
56
+ end
57
+ end
58
+
59
+ # Returns whether or not weather retrieval from the API
60
+ # is currently waiting for a timeout to expire; here: existence of
61
+ # a retry marker file
62
+ def waiting_for_retry?
63
+ ::File.exists?(Regentanz.configuration.retry_marker)
64
+ end
65
+
66
+ # Checks if we've waited long enough. Deletes a possible retry
67
+ # marker file (and returns true) if so or returns false if not
68
+ def unset_retry_state!
69
+ marker = Regentanz.configuration.retry_marker
70
+ if waiting_for_retry? and ::File.new(marker).mtime < Regentanz.configuration.retry_ttl.seconds.ago
71
+ ::File.delete(marker) if ::File.exists?(marker)
72
+ true
73
+ end
74
+ end
75
+
76
+ # Persists the timeout state by writing a retry_marker file
77
+ def set_retry_state!
78
+ ::File.open(Regentanz.configuration.retry_marker, "w+").close
79
+ waiting_for_retry?
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,2 @@
1
+ require 'regentanz/cache/base'
2
+ require 'regentanz/cache/file'
@@ -0,0 +1,18 @@
1
+ module Regentanz
2
+ module Callbacks
3
+
4
+ CALLBACKS = [:api_failure_detected, :api_failure_resumed]
5
+
6
+ CALLBACKS.each do |callback_method|
7
+ # Define no-op stubs for all CALLBACKS
8
+ define_method(callback_method) {}
9
+ private callback_method
10
+ end
11
+
12
+ def self.included(base)
13
+ base.send :include, ActiveSupport::Callbacks
14
+ base.define_callbacks *CALLBACKS
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ module Regentanz
2
+ module Conditions
3
+ class Base
4
+
5
+ attr_accessor :condition, :style, :icon
6
+
7
+ def initialize(attributes = {})
8
+ attributes.symbolize_keys!
9
+ attributes.keys.each do |attr|
10
+ self.send(:"#{attr}=", attributes[attr]) if respond_to?(:"#{attr}=")
11
+ end
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ module Regentanz
2
+ module Conditions
3
+
4
+ class Current < Regentanz::Conditions::Base
5
+
6
+ attr_reader :temp_c, :temp_f
7
+ attr_accessor :humidity, :wind_condition
8
+
9
+ def temp_c=temp; @temp_c = temp.to_i; end
10
+ def temp_f=temp; @temp_f = temp.to_i; end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Regentanz
2
+ module Conditions
3
+
4
+ class Forecast < Regentanz::Conditions::Base
5
+
6
+ attr_reader :high, :low
7
+ attr_accessor :day_of_week
8
+
9
+ def high=temp; @high = temp.to_i; end
10
+ def low=temp; @low = temp.to_i; end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ require 'regentanz/conditions/base'
2
+ require 'regentanz/conditions/current'
3
+ require 'regentanz/conditions/forecast'
@@ -0,0 +1,55 @@
1
+ module Regentanz
2
+ require "tmpdir"
3
+
4
+ class Configuration
5
+
6
+ DEFAULT_OPTIONS = [
7
+ :base_url,
8
+ :cache_backend,
9
+ :cache_dir,
10
+ :cache_prefix,
11
+ :cache_ttl,
12
+ :retry_marker,
13
+ :retry_ttl
14
+ ]
15
+
16
+ OPTIONS = DEFAULT_OPTIONS + [
17
+ :do_not_get_weather,
18
+ :suppress_stderr_output
19
+ ]
20
+
21
+ # Define default values
22
+ @@default_base_url = "http://www.google.com/ig/api"
23
+ @@default_cache_backend = Regentanz::Cache::File
24
+ @@default_cache_dir = Dir.tmpdir
25
+ @@default_cache_prefix = "regentanz"
26
+ @@default_cache_ttl = 14400 # 4 hours
27
+ @@default_retry_ttl = 3600 # 1 hour
28
+ @@default_retry_marker = File.join(@@default_cache_dir, "#{@@default_cache_prefix}_api_retry.txt")
29
+
30
+ OPTIONS.each { |opt| attr_accessor(opt) }
31
+ DEFAULT_OPTIONS.each { |cvar| cattr_reader(:"default_#{cvar}", :instance_reader => false) } # class getter for all DEFAULT_OPTION cvars
32
+
33
+ # Stores global configuration information for +Regentanz+.
34
+ #
35
+ # == Default Options
36
+ # * +base_url+: HTTP API, request-specific calls will be appended (default: +http://www.google.com/ig/api+)
37
+ # * +cache_backend+: defaults to +Regentanz::Cache::File+
38
+ # * +cache_dir+: defaults to +Dir.tmpdir+
39
+ # * +cache_prefix+: String to prefix both cache file and retry_marker (if not specified otherwise) with
40
+ # * +cache_ttl+: time in seconds for which cache data is considered valid. Default: 14400 (4 hours).
41
+ # * +retry_ttl+: time in seconds Regentanz should wait until it tries to call the API again when it failed before. Default: 3600 (1 hour).
42
+ # * +retry_marker+: persist a marker-file here to indicate a failed API state. Supply a full pathname.
43
+ #
44
+ # == Options
45
+ # * +do_not_get_weather+: don't try to retrieve weather data, neither from cache nor remote API. Intended for testing.
46
+ # * +suppress_stderr_output+: called from GoogleWeather#error_output, silences Regentanz' output. Intended for testing.
47
+ def initialize(*args)
48
+ DEFAULT_OPTIONS.each do |option|
49
+ self.send(:"#{option}=", self.class.send(:class_variable_get, :"@@default_#{option}") )
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,22 @@
1
+ module Regentanz
2
+ class << self
3
+
4
+ attr_writer :configuration
5
+ def configuration #:nodoc:
6
+ @configuration ||= Configuration.new
7
+ end
8
+
9
+ # Call this method to modify defaults in your initializers.
10
+ # See Regentanz::Configuration for supported config options.
11
+ #
12
+ # === Example usage
13
+ # Regentanz.configure do |config|
14
+ # config.<supported_config_option> = :bar
15
+ # end
16
+ def configure
17
+ self.configuration ||= Configuration.new
18
+ yield(configuration)
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,151 @@
1
+ module Regentanz
2
+ class GoogleWeather
3
+ require 'net/http'
4
+ require 'rexml/document'
5
+ require 'ostruct'
6
+
7
+ include Astronomy
8
+ include Callbacks
9
+
10
+ attr_accessor :location, :cache_id
11
+ attr_reader :cache, :current, :forecast, :lang, :parser, :xml
12
+
13
+ # Creates an object and queries the weather API.
14
+ #
15
+ # === Parameters
16
+ # * +options+ A hash
17
+ #
18
+ # === Available options
19
+ # * +location+ String to pass to Google's weather API (mandatory)
20
+ # * +cache_id+ enables caching with a unique identifier to locate a cached file
21
+ # * +geodata+ a hash with keys :lat and :lng, required for sunset/-rise calculations
22
+ # * +lang+ Desired language for the returned results (Defaults to "de")
23
+ def initialize(*args)
24
+ options = args.extract_options!
25
+ @options = options.symbolize_keys
26
+ @parser = Parser::GoogleWeather.new
27
+ self.location = args.first || options[:location]
28
+ self.lang = options[:lang]
29
+
30
+ # Activate caching
31
+ if Regentanz.configuration.cache_backend
32
+ @cache = Regentanz.configuration.cache_backend.new if Regentanz.configuration.cache_backend
33
+ @cache_id = options[:cache_id] || Regentanz.configuration.cache_backend.sanitize_key(@location)
34
+ end
35
+
36
+ @geodata = options[:geodata] if options[:geodata] and options[:geodata][:lat] and options[:geodata][:lng]
37
+ get_weather() unless Regentanz.configuration.do_not_get_weather
38
+ end
39
+
40
+ # Loads weather data from known data sources (ie. cache or external API)
41
+ def get_weather!; get_weather(); end
42
+
43
+ # Input sanitizer-setter, defaults to "de"
44
+ def lang=(lang)
45
+ @lang = lang.present? ? lang.to_s : "de"
46
+ end
47
+
48
+ # Provide an accessor to see if we actually got weather info at all.
49
+ def present?
50
+ current.present? or forecast.present?
51
+ end
52
+
53
+ def sunrise
54
+ @sunrise ||= @geodata.blank? ? nil : sun_rise_set(:sunrise, @geodata[:lat], @geodata[:lng])
55
+ end
56
+
57
+ def sunset
58
+ @sunset ||= @geodata.blank? ? nil : sun_rise_set(:sunset, @geodata[:lat], @geodata[:lng])
59
+ end
60
+
61
+ def waiting_for_retry?
62
+ @cache && @cache.waiting_for_retry?
63
+ end
64
+
65
+ private
66
+
67
+ # Encapsulate output of error messages. Will output to $stderr unless
68
+ # Regentanz.configuration.suppress_stderr_output is set
69
+ #
70
+ # === Parameters
71
+ # * +output+: String to output
72
+ def error_output(output)
73
+ $stderr.puts output unless Regentanz.configuration.suppress_stderr_output
74
+ end
75
+
76
+ # Proxies +do_request+ and +parse_request+
77
+ def get_weather
78
+ if cache_valid?
79
+ @xml = @parser.convert_encoding(@cache.get(@cache_id))
80
+ else
81
+ @xml = @parser.convert_encoding(do_request(Regentanz.configuration.base_url + "?weather=#{CGI::escape(@location)}&hl=#{@lang}"))
82
+ if @cache
83
+ @cache.expire!(@cache_id)
84
+ @cache.set(@cache_id, @xml)
85
+ end
86
+ end
87
+ parse_xml() if @xml
88
+ end
89
+
90
+ # Makes an outbound HTTP-request and returns the request body (ie. the XML)
91
+ #
92
+ # === Parameters
93
+ # * +url+ API-URL with protocol, fqdn and URI
94
+ def do_request(url)
95
+ begin
96
+ Net::HTTP.get_response(URI.parse(url)).body
97
+ rescue => e
98
+ error_output(e.message)
99
+ end
100
+ end
101
+
102
+ # Parses the raw data and Sets the +current+ and +forecast+ variables.
103
+ #
104
+ # Assumes the instance var +xml+ is set.
105
+ def parse_xml
106
+ begin
107
+ @current = @parser.parse_current!(@xml)
108
+ @forecast = @parser.parse_forecast!(@xml)
109
+ rescue REXML::ParseException => e
110
+ error_output(e.message)
111
+ end
112
+ end
113
+
114
+ # Returns +true+ if a given cache file contains data not older than Regentanz::Configuration#cache_ttl seconds
115
+ # TODO properly use cache backend's #valid?
116
+ def cache_valid?
117
+ validity = false
118
+ if @cache and @cache.available?(@cache_id)
119
+ begin
120
+ doc = REXML::Document.new(@cache.get(@cache_id))
121
+ node = doc.elements["xml_api_reply/weather/forecast_information/current_date_time"]
122
+ time = node.attribute("data").to_s.to_time if node
123
+ validity = time ? time > Regentanz.configuration.cache_ttl.seconds.ago : false
124
+ rescue REXML::ParseException
125
+ retry_after_incorrect_api_reply
126
+ validity = waiting_for_retry? # not really valid, but we need to wait a bit.
127
+ end
128
+ end
129
+ validity
130
+ end
131
+
132
+ # Brings the outbound API-calls to a halt for Regentanz::Configuration#retry_ttl seconds by creating
133
+ # a marker file. Flushes the incorrect cached API response after Regentanz::Configuration#retry_ttl
134
+ # seconds.
135
+ def retry_after_incorrect_api_reply
136
+ if !waiting_for_retry? and @cache
137
+ # We are run for the first time, create the marker file
138
+ # TODO remove dependency to SupportMailer class
139
+ api_failure_detected # callback
140
+ SupportMailer.deliver_weather_retry_marker_notification!(self, :set)
141
+ @cache.set_retry_state!
142
+ elsif @cache and @cache.unset_retry_state!
143
+ # Marker file is old enough, delete the (invalid) cache file and remove the marker_file
144
+ @cache.expire!(@cache_id)
145
+ api_failure_resumed # callback
146
+ SupportMailer.deliver_weather_retry_marker_notification!(self, :unset)
147
+ end
148
+ end
149
+
150
+ end
151
+ end