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.
- checksums.yaml +13 -5
- data/README.md +116 -8
- data/green-button-data.gemspec +6 -1
- data/lib/green-button-data.rb +26 -1
- data/lib/green-button-data/application_information.rb +112 -0
- data/lib/green-button-data/authorization.rb +35 -0
- data/lib/green-button-data/configuration.rb +133 -0
- data/lib/green-button-data/core_ext.rb +1 -0
- data/lib/green-button-data/core_ext/string.rb +35 -0
- data/lib/green-button-data/dst.rb +57 -0
- data/lib/green-button-data/entry.rb +87 -0
- data/lib/green-button-data/feed.rb +5 -0
- data/lib/green-button-data/fetchable.rb +242 -0
- data/lib/green-button-data/interval_block.rb +38 -0
- data/lib/green-button-data/local_time_parameters.rb +31 -0
- data/lib/green-button-data/meter_reading.rb +4 -0
- data/lib/green-button-data/model_collection.rb +33 -0
- data/lib/green-button-data/parser/application_information.rb +2 -11
- data/lib/green-button-data/parser/authorization.rb +0 -4
- data/lib/green-button-data/parser/interval.rb +8 -4
- data/lib/green-button-data/parser/interval_reading.rb +4 -1
- data/lib/green-button-data/parser/local_time_parameters.rb +0 -57
- data/lib/green-button-data/parser/summary_measurement.rb +12 -0
- data/lib/green-button-data/reading_type.rb +82 -0
- data/lib/green-button-data/relations.rb +24 -0
- data/lib/green-button-data/usage_point.rb +27 -0
- data/lib/green-button-data/usage_summary.rb +36 -0
- data/lib/green-button-data/utilities.rb +27 -0
- data/lib/green-button-data/version.rb +1 -1
- data/spec/fixtures.rb +5 -0
- data/spec/fixtures/ESPIReadingType.xml +23 -535
- data/spec/fixtures/ESPIReadingTypes.xml +535 -0
- data/spec/fixtures/ESPIUsagePoint.xml +3 -3
- data/spec/fixtures/ESPIUsagePointMeterReading.xml +13 -0
- data/spec/fixtures/ESPIUsagePointMeterReadings.xml +21 -0
- data/spec/fixtures/ESPIUsagePoints.xml +822 -0
- data/spec/lib/green-button-data/application_information_spec.rb +389 -0
- data/spec/lib/green-button-data/authorization_spec.rb +91 -0
- data/spec/{green-button-data → lib/green-button-data}/core_ext/date_spec.rb +0 -0
- data/spec/{green-button-data → lib/green-button-data}/core_ext/fixnum_spec.rb +0 -0
- data/spec/lib/green-button-data/local_time_parameters_spec.rb +106 -0
- data/spec/{green-button-data → lib/green-button-data}/parser/application_information_spec.rb +4 -4
- data/spec/{green-button-data → lib/green-button-data}/parser/authorization_spec.rb +0 -12
- data/spec/{green-button-data → lib/green-button-data}/parser/entry_spec.rb +1 -1
- data/spec/{green-button-data → lib/green-button-data}/parser/interval_block_spec.rb +4 -4
- data/spec/{green-button-data → lib/green-button-data}/parser/local_time_parameter_spec.rb +0 -0
- data/spec/{green-button-data → lib/green-button-data}/parser/reading_type_spec.rb +0 -0
- data/spec/{green-button-data → lib/green-button-data}/parser/usage_point_spec.rb +0 -0
- data/spec/{green-button-data → lib/green-button-data}/parser/usage_summary_spec.rb +0 -0
- data/spec/lib/green-button-data/reading_type_spec.rb +127 -0
- data/spec/lib/green-button-data/usage_point_spec.rb +167 -0
- data/spec/{green-button-data → lib/green-button-data}/utilities_spec.rb +1 -1
- data/spec/spec_helper.rb +5 -0
- metadata +70 -17
@@ -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
|
@@ -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
|