green-button-data 0.2.1 → 0.3.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 (54) hide show
  1. checksums.yaml +13 -5
  2. data/README.md +116 -8
  3. data/green-button-data.gemspec +6 -1
  4. data/lib/green-button-data.rb +26 -1
  5. data/lib/green-button-data/application_information.rb +112 -0
  6. data/lib/green-button-data/authorization.rb +35 -0
  7. data/lib/green-button-data/configuration.rb +133 -0
  8. data/lib/green-button-data/core_ext.rb +1 -0
  9. data/lib/green-button-data/core_ext/string.rb +35 -0
  10. data/lib/green-button-data/dst.rb +57 -0
  11. data/lib/green-button-data/entry.rb +87 -0
  12. data/lib/green-button-data/feed.rb +5 -0
  13. data/lib/green-button-data/fetchable.rb +242 -0
  14. data/lib/green-button-data/interval_block.rb +38 -0
  15. data/lib/green-button-data/local_time_parameters.rb +31 -0
  16. data/lib/green-button-data/meter_reading.rb +4 -0
  17. data/lib/green-button-data/model_collection.rb +33 -0
  18. data/lib/green-button-data/parser/application_information.rb +2 -11
  19. data/lib/green-button-data/parser/authorization.rb +0 -4
  20. data/lib/green-button-data/parser/interval.rb +8 -4
  21. data/lib/green-button-data/parser/interval_reading.rb +4 -1
  22. data/lib/green-button-data/parser/local_time_parameters.rb +0 -57
  23. data/lib/green-button-data/parser/summary_measurement.rb +12 -0
  24. data/lib/green-button-data/reading_type.rb +82 -0
  25. data/lib/green-button-data/relations.rb +24 -0
  26. data/lib/green-button-data/usage_point.rb +27 -0
  27. data/lib/green-button-data/usage_summary.rb +36 -0
  28. data/lib/green-button-data/utilities.rb +27 -0
  29. data/lib/green-button-data/version.rb +1 -1
  30. data/spec/fixtures.rb +5 -0
  31. data/spec/fixtures/ESPIReadingType.xml +23 -535
  32. data/spec/fixtures/ESPIReadingTypes.xml +535 -0
  33. data/spec/fixtures/ESPIUsagePoint.xml +3 -3
  34. data/spec/fixtures/ESPIUsagePointMeterReading.xml +13 -0
  35. data/spec/fixtures/ESPIUsagePointMeterReadings.xml +21 -0
  36. data/spec/fixtures/ESPIUsagePoints.xml +822 -0
  37. data/spec/lib/green-button-data/application_information_spec.rb +389 -0
  38. data/spec/lib/green-button-data/authorization_spec.rb +91 -0
  39. data/spec/{green-button-data → lib/green-button-data}/core_ext/date_spec.rb +0 -0
  40. data/spec/{green-button-data → lib/green-button-data}/core_ext/fixnum_spec.rb +0 -0
  41. data/spec/lib/green-button-data/local_time_parameters_spec.rb +106 -0
  42. data/spec/{green-button-data → lib/green-button-data}/parser/application_information_spec.rb +4 -4
  43. data/spec/{green-button-data → lib/green-button-data}/parser/authorization_spec.rb +0 -12
  44. data/spec/{green-button-data → lib/green-button-data}/parser/entry_spec.rb +1 -1
  45. data/spec/{green-button-data → lib/green-button-data}/parser/interval_block_spec.rb +4 -4
  46. data/spec/{green-button-data → lib/green-button-data}/parser/local_time_parameter_spec.rb +0 -0
  47. data/spec/{green-button-data → lib/green-button-data}/parser/reading_type_spec.rb +0 -0
  48. data/spec/{green-button-data → lib/green-button-data}/parser/usage_point_spec.rb +0 -0
  49. data/spec/{green-button-data → lib/green-button-data}/parser/usage_summary_spec.rb +0 -0
  50. data/spec/lib/green-button-data/reading_type_spec.rb +127 -0
  51. data/spec/lib/green-button-data/usage_point_spec.rb +167 -0
  52. data/spec/{green-button-data → lib/green-button-data}/utilities_spec.rb +1 -1
  53. data/spec/spec_helper.rb +5 -0
  54. metadata +70 -17
@@ -1,2 +1,3 @@
1
1
  require 'green-button-data/core_ext/date'
2
2
  require 'green-button-data/core_ext/fixnum'
3
+ require 'green-button-data/core_ext/string'
@@ -0,0 +1,35 @@
1
+ class String
2
+ def camelize
3
+ self.gsub(/(?<=_|^)(\w)/) { $1.upcase }
4
+ .gsub(/(?:_)(\w)/,'\1')
5
+ end
6
+
7
+ def underscore
8
+ # Shamelessly copied from Rails' ActiveSupport gem
9
+ self.gsub(/::/, '/')
10
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
11
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
12
+ .tr("-", "_")
13
+ .downcase
14
+ end
15
+
16
+ def pluralize
17
+ result = self.dup
18
+
19
+ if self.empty?
20
+ return result
21
+ else
22
+ [
23
+ [/([^aeiouy]|qu)y$/i, '\1ies'],
24
+ [/s$/i, 's'],
25
+ [/$/, 's']
26
+ ].each { |(rule, replacement)| break if result.sub!(rule, replacement) }
27
+
28
+ return result
29
+ end
30
+ end
31
+
32
+ def dasherize
33
+ self.tr('_', '-')
34
+ end
35
+ end
@@ -1,5 +1,7 @@
1
1
  module GreenButtonData
2
2
  module Dst
3
+ include Utilities
4
+
3
5
  # From ESPI XML schema:
4
6
  # [extension] Bit map encoded rule from which is calculated the start or
5
7
  # end time, within the current year, to which daylight savings time offset
@@ -45,5 +47,60 @@ module GreenButtonData
45
47
  BITSHIFT_DAY_OF_MONTH = 20
46
48
  BITSHIFT_DST_RULE = 25
47
49
  BITSHIFT_MONTH = 28
50
+
51
+ def byte_to_dst_datetime(byte, year = Time.now.year)
52
+ # Bits 0 - 11: seconds 0 - 3599
53
+ seconds = byte & BITMASK_SECOND
54
+
55
+ # Bits 12 - 16: hours 0 - 23
56
+ hour = (byte & BITMASK_HOUR) >> BITSHIFT_HOUR
57
+
58
+ # Bits 17 - 19: day of the week; 0 = NA, 1 - 7 (Monday = 1)
59
+ weekday = (byte & BITMASK_DAY_OF_WEEK) >> BITSHIFT_DAY_OF_WEEK
60
+
61
+ # Bits 20 - 24: day of the month; 0 = NA, 1 - 31
62
+ day = (byte & BITMASK_DAY_OF_MONTH) >> BITSHIFT_DAY_OF_MONTH
63
+
64
+ # Bits 25 - 27: DST rule 0 - 7
65
+ dst_rule = (byte & BITMASK_DST_RULE) >> BITSHIFT_DST_RULE
66
+
67
+ # Bits 28 - 31: month 1 - 12
68
+ month = (byte & BITMASK_MONTH) >> BITSHIFT_MONTH
69
+
70
+ # Raise an error unless all the values are in valid range
71
+ seconds.between?(0, 3599) and hour.between?(0, 23) and
72
+ weekday.between?(1, 7) and day.between?(0, 31) and
73
+ dst_rule.between?(0, 7) and month.between?(1, 12) or
74
+ raise RangeError, 'Invalid value range'
75
+
76
+ # In Ruby, Sunday = 0 not 7
77
+ weekday = weekday == 7 ? 0 : weekday
78
+
79
+ # Check the DST rule
80
+ dst_day = if dst_rule == 1
81
+ # Rule 1: DST starts/ends on Day of Week on or after the Day of Month
82
+ day_of_month = DateTime.new year, month, day
83
+ day_offset = if weekday >= day_of_month.wday
84
+ weekday - day_of_month.wday
85
+ else
86
+ 7 + weekday - day_of_month.wday
87
+ end
88
+
89
+ day_of_month + day_offset
90
+ elsif dst_rule.between?(2, 6)
91
+ # Rule 2 - 6: DST starts/ends on Nth Day of Week in given month
92
+ # Nth Day of Week (e.g. third Friday of July)
93
+ nth_weekday_of year, month, weekday, dst_rule - 1
94
+ elsif dst_rule == 7
95
+ # Rule 7: DST starts/ends on last Day of Week in given month
96
+ last_weekday_of year, month, weekday
97
+ else
98
+ # Rule 0: DST starts/ends on the Day of Month
99
+ DateTime.new year, month, day
100
+ end
101
+
102
+ # Add the hour and seconds component to the day
103
+ dst_day + Rational(hour * 3600 + seconds, 86400)
104
+ end
48
105
  end
49
106
  end
@@ -0,0 +1,87 @@
1
+ module GreenButtonData
2
+ class Entry
3
+ include Fetchable
4
+
5
+ attr_reader :id
6
+ attr_accessor :token
7
+
8
+ def initialize(attributes)
9
+ # Automagically sets instance variables from attribute hash parsed from
10
+ # the GreenButtonData::Parser classes
11
+ attributes.each do |key, value|
12
+ self.instance_variable_set :"@#{key}", value
13
+ end
14
+
15
+ # Handle relations via related_urls
16
+ @related_urls.is_a?(Hash) and @related_urls.each do |key, value|
17
+ self.instance_variable_set :"@#{key}_url", value
18
+ singleton_class.class_eval do
19
+ attr_accessor "#{key}_url".to_sym
20
+ end
21
+
22
+ # Define accessor methods from pluralized resource names
23
+ self.class.send :define_method, "#{key.to_s.pluralize}" do |*args|
24
+ id = args[0]
25
+ options = args[1]
26
+
27
+ klazz_name = "GreenButtonData::#{key.to_s.camelize}"
28
+
29
+ # Handle deprecations
30
+ klazz_name.gsub! /ElectricPowerUsageSummary/, 'UsageSummary'
31
+
32
+ klazz = klazz_name.split('::')
33
+ .inject(Object) { |obj, cls| obj.const_get cls }
34
+
35
+ collection = self.instance_variable_get "@#{key.to_s.pluralize}"
36
+ url = self.instance_variable_get "@#{key}_url"
37
+
38
+ # Make the ID argument optional
39
+ options ||= if id.is_a?(Hash)
40
+ id
41
+ else
42
+ {}
43
+ end
44
+
45
+ result = if id.is_a?(Numeric) || id.is_a?(String) || id.is_a?(Symbol)
46
+ # Try returning cached results first
47
+ collection and instance = collection.find_by_id(id)
48
+ cache_miss = instance.nil?
49
+
50
+ # On cache miss or forced reload, send API request
51
+ instance = if !options[:reload] && instance
52
+ instance
53
+ else
54
+ klazz.find "#{url}/#{id}", options
55
+ end
56
+
57
+ # Cache the result
58
+ collection ||= ModelCollection.new
59
+ collection << instance if cache_miss
60
+
61
+ instance
62
+ else
63
+ if !options[:reload] && collection
64
+ collection
65
+ else
66
+ collection = klazz.all url, options
67
+ end
68
+ end
69
+
70
+ self.instance_variable_set :"@#{key.to_s.pluralize}", collection
71
+
72
+ return result
73
+ end
74
+ end
75
+ end # initialize
76
+
77
+ protected
78
+
79
+ def get_enum_symbol(enum, value)
80
+ if value.is_a? Numeric
81
+ enum[value]
82
+ else
83
+ value
84
+ end
85
+ end
86
+ end # Entry
87
+ end # GreenButtonData
@@ -1,5 +1,10 @@
1
1
  module GreenButtonData
2
2
  class Feed
3
+ def self.fetch(url, options)
4
+ block = block_given? ? Proc.new : nil
5
+ conn = Faraday.new url, options, &block
6
+ end
7
+
3
8
  def self.parse(xml)
4
9
  GreenButtonData::Parser::Feed.parse xml
5
10
  end
@@ -0,0 +1,242 @@
1
+ require 'uri'
2
+
3
+ module GreenButtonData
4
+ module Fetchable
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ include Relations
11
+ include Utilities
12
+
13
+ ##
14
+ # Returns all entries
15
+ #
16
+ # ==== Arguments
17
+ #
18
+ # * +url+ (OPTIONAL) - URL to fetch from. If not supplied, it defaults to
19
+ # the URLs in configuration.
20
+ # * +options+ - Options hash
21
+ def all(url = nil, options = nil)
22
+ if url.is_a?(Hash)
23
+ # Assume it is an options Hash
24
+ options = url
25
+ url = nil
26
+ end
27
+
28
+ url_options = url_options(options)
29
+
30
+ url ||= if url_options.keys.size > 0
31
+ GreenButtonData.configuration.send(
32
+ "#{class_name.underscore}_url", url_options(options)
33
+ )
34
+ else
35
+ GreenButtonData.configuration.send "#{class_name.underscore}_url"
36
+ end
37
+
38
+ @url = url
39
+ @options = options
40
+ return records
41
+ end
42
+
43
+ ##
44
+ # Returns the first item in the ModelCollection from all entries returned
45
+ # by the API endpoint
46
+ #
47
+ # ==== Arguments
48
+ #
49
+ # * +url+ (OPTIONAL) - URL to fetch from. If not supplied, it defaults to
50
+ # the URLs in configuration.
51
+ # * +options+ - Options hash
52
+ def first(url = nil, options = nil)
53
+ return all(url, options).first
54
+ end
55
+
56
+ ##
57
+ # Finds an entry that matches the {id} in the URL of the form:
58
+ # https://services.greenbuttondata.org/DataCustodian/espi/1_1/resource/{model_name}/{id}
59
+ #
60
+ # ==== Arguments
61
+ #
62
+ # * +id+ - ID of the entry content. URL matching a single entry content
63
+ # can be supplied in place of ID.
64
+ # * +url+ (OPTIONAL) - If specified, this URL is used to fetch data in
65
+ # place of global configuration.
66
+ # * +options+ - Options hash
67
+ #
68
+ # ==== Examples
69
+ #
70
+ # The following fetches data identically.
71
+ #
72
+ # ReadingType.find(1, token: "12345678-1024-2048-abcdef001234") # use global config
73
+ # ReadingType.find("https://services.greenbuttondata.org/DataCustodian/espi/1_1/resource/ReadingType/1", token: "12345678-1024-2048-abcdef001234") # override global config and use specific url
74
+ def find(id = nil, options = nil)
75
+ # If +id+ argument is a URL, set the url
76
+ url = if id =~ /\A#{URI::regexp}\z/
77
+ id
78
+ else
79
+ url_options = url_options options
80
+
81
+ path_prefix = if url_options.keys.size > 0
82
+ GreenButtonData.configuration.send(
83
+ "#{class_name.underscore}_url", url_options(options)
84
+ )
85
+ else
86
+ GreenButtonData.configuration.send "#{class_name.underscore}_url"
87
+ end
88
+
89
+ URI.join(path_prefix, "#{id}/").to_s
90
+ end
91
+
92
+ return populate_models(fetch(url, options)).first
93
+ end
94
+
95
+ def last(url = nil, options = nil)
96
+ return all(url, options).last
97
+ end
98
+
99
+ def fetch(url = nil, options = nil)
100
+ url or raise ArgumentError.new "url is required to fetch data"
101
+
102
+ connection_options = {}
103
+
104
+ options ||= {}
105
+ connection_options[:ssl] = options[:ssl] if options[:ssl]
106
+
107
+ conn = Faraday.new connection_options do |connection|
108
+ connection.response :logger
109
+ connection.adapter Faraday.default_adapter
110
+ end
111
+ conn.authorization :Bearer, options[:token] if options[:token]
112
+
113
+ response = conn.get do |req|
114
+ req.url url
115
+ req.params = build_query_params options
116
+ end
117
+
118
+ if response.status == 200
119
+ GreenButtonData::Feed.parse response.body
120
+ elsif response.status == 401
121
+ raise "Unauthorized API call; check authorization token and try again"
122
+ elsif response.status == 500
123
+ raise "500 Server Error:\n#{response.body}"
124
+ else
125
+ raise "Status: #{response.status}"
126
+ end
127
+ end
128
+
129
+ def feed
130
+ if !options[:reload] && @feed
131
+ @feed
132
+ else
133
+ @feed = fetch url, options
134
+ end
135
+ end
136
+
137
+ def records
138
+ if !options[:reload] && @records
139
+ @records
140
+ else
141
+ @records = populate_models(feed)
142
+ end
143
+ end
144
+
145
+ def options
146
+ @options
147
+ end
148
+
149
+ def url
150
+ @url
151
+ end
152
+
153
+ private
154
+
155
+ def class_name
156
+ self.name.split('::').last
157
+ end
158
+
159
+ def each_entry_content(feed)
160
+ entry_content = nil
161
+
162
+ feed.entries.each do |entry|
163
+ match_data = /\/(([a-zA-Z]+)|([a-zA-Z]+)\/(\w+=*|(\d+,*\d*)+))\/*\z/.
164
+ match(entry.self)
165
+
166
+ unless match_data.nil?
167
+ id = match_data[4] || entry.id
168
+ type = match_data[2] || match_data[3]
169
+
170
+ entry_content = case type.downcase
171
+
172
+ when 'applicationinformation'
173
+ entry.content.application_information
174
+ when 'authorization'
175
+ entry.content.authorization
176
+ when 'electricpowerusagesummary'
177
+ entry.content.electric_power_usage_summary
178
+ when 'intervalblock'
179
+ entry.content.interval_block
180
+ when 'localtimeparameters'
181
+ entry.content.local_time_parameters
182
+ when 'readingtype'
183
+ entry.content.reading_type
184
+ when 'usagepoint'
185
+ entry.content.usage_point
186
+ when 'usagesummary'
187
+ entry.content.usage_summary
188
+ else
189
+ nil
190
+ end
191
+
192
+ yield id, entry_content, entry
193
+ end
194
+ end
195
+ end
196
+
197
+ def populate_models(feed)
198
+ models = GreenButtonData::ModelCollection.new
199
+
200
+ each_entry_content feed do |id, content, entry|
201
+ attributes_hash = attributes_to_hash(content)
202
+ attributes_hash[:id] = id
203
+
204
+ attributes_hash[:related_urls] = construct_related_urls entry
205
+ models << self.new(attributes_hash)
206
+ end
207
+
208
+ return models
209
+ end
210
+
211
+ def build_query_params(options)
212
+ params = {}
213
+
214
+ options.each do |key, value|
215
+ if key == :published_min || key == :published_max
216
+ if value.respond_to? :to_time
217
+ value = value.to_time.to_i
218
+ end
219
+ end
220
+
221
+ unless key == :ssl || key == :token || key == :subscription_id
222
+ params[key.to_s.dasherize] = value
223
+ end
224
+ end
225
+
226
+ return params
227
+ end
228
+
229
+ def url_options(options)
230
+ url_options = {}
231
+
232
+ options.each do |key, value|
233
+ if /_id$/ =~ key
234
+ url_options[key] = value
235
+ end
236
+ end
237
+
238
+ return url_options
239
+ end
240
+ end # ClassMethods
241
+ end # Fetchable
242
+ end # GreenButtonData