regentanz 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/CHANGELOG.rdoc +22 -0
- data/Gemfile +4 -0
- data/LICENSE +24 -0
- data/README.rdoc +46 -0
- data/Rakefile +23 -0
- data/lib/regentanz/astronomy.rb +69 -0
- data/lib/regentanz/cache/base.rb +51 -0
- data/lib/regentanz/cache/file.rb +86 -0
- data/lib/regentanz/cache.rb +2 -0
- data/lib/regentanz/callbacks.rb +18 -0
- data/lib/regentanz/conditions/base.rb +16 -0
- data/lib/regentanz/conditions/current.rb +14 -0
- data/lib/regentanz/conditions/forecast.rb +14 -0
- data/lib/regentanz/conditions.rb +3 -0
- data/lib/regentanz/configuration.rb +55 -0
- data/lib/regentanz/configurator.rb +22 -0
- data/lib/regentanz/google_weather.rb +151 -0
- data/lib/regentanz/parser/google_weather.rb +100 -0
- data/lib/regentanz/parser.rb +1 -0
- data/lib/regentanz/test_helper.rb +52 -0
- data/lib/regentanz/version.rb +4 -0
- data/lib/regentanz.rb +12 -0
- data/regentanz.gemspec +31 -0
- data/test/factories.rb +8 -0
- data/test/support/support_mailer.rb +7 -0
- data/test/support/tmp/.gitignore +1 -0
- data/test/support/valid_response.xml.erb +26 -0
- data/test/test_helper.rb +18 -0
- data/test/unit/astronomy_test.rb +26 -0
- data/test/unit/cache/base_test.rb +53 -0
- data/test/unit/cache/file_test.rb +141 -0
- data/test/unit/callbacks_test.rb +27 -0
- data/test/unit/configuration_test.rb +57 -0
- data/test/unit/current_condition_test.rb +33 -0
- data/test/unit/forecast_condition_test.rb +35 -0
- data/test/unit/google_weather_test.rb +131 -0
- data/test/unit/parser/google_weather_parser_test.rb +71 -0
- metadata +219 -0
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
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,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,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
|