greenbutton 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +29 -0
- data/Rakefile +2 -0
- data/Readme.md +10 -0
- data/greenbutton.gemspec +18 -0
- data/lib/greenbutton.rb +60 -0
- data/lib/greenbutton/gb_classes.rb +361 -0
- data/lib/greenbutton/helpers.rb +58 -0
- data/license.txt +20 -0
- data/pkg/greenbutton-0.0.1.gem +0 -0
- data/spec/fixtures/sample_greenbutton_data.xml +2728 -0
- data/spec/helper_spec.rb +46 -0
- data/spec/parser_spec.rb +183 -0
- data/spec/spec_helper.rb +23 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 52012aa618542579f16c93ee0ad2c7a14509423a
|
4
|
+
data.tar.gz: 5c66932af049ff6ed7bb5b01bf8c9c149bce6065
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1ee88c5b6cf336011e92f7a175c2a7ee0903eb96076c0c098030d919bf7b8a5ccab9e43af69eb34f2307b0442644cfdffae1f0f5b4467e18bf6177cd00ac6c25
|
7
|
+
data.tar.gz: 230b9e587e05057f66689d57d1d9e3362dc1d82ba1afcd472fea5ea38e151b920b61bb64cae05cfcd65707adb1ac5506c987333b60329e996f4db8a492afbdda
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
greenbuttonparser
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.1.0
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
greenbutton (0.0.1)
|
5
|
+
nokogiri (~> 1.6)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.2.5)
|
11
|
+
mini_portile (0.5.2)
|
12
|
+
nokogiri (1.6.1)
|
13
|
+
mini_portile (~> 0.5.0)
|
14
|
+
rspec (2.14.1)
|
15
|
+
rspec-core (~> 2.14.0)
|
16
|
+
rspec-expectations (~> 2.14.0)
|
17
|
+
rspec-mocks (~> 2.14.0)
|
18
|
+
rspec-core (2.14.7)
|
19
|
+
rspec-expectations (2.14.4)
|
20
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
21
|
+
rspec-mocks (2.14.4)
|
22
|
+
|
23
|
+
PLATFORMS
|
24
|
+
ruby
|
25
|
+
|
26
|
+
DEPENDENCIES
|
27
|
+
bundler (~> 1.5)
|
28
|
+
greenbutton!
|
29
|
+
rspec (~> 2.14)
|
data/Rakefile
ADDED
data/Readme.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
|
2
|
+
## Sample Green Button Parser Implemented in Ruby ##
|
3
|
+
|
4
|
+
[](https://codeclimate.com/github/cew821/greenbutton)
|
5
|
+
|
6
|
+
This is a sample implementation of a parser for [Green Button Data](http://services.greenbuttondata.org/), written in Ruby.
|
7
|
+
|
8
|
+
This parser parsers Green Button XML into ruby objects.
|
9
|
+
|
10
|
+
This software is free, and is released as open source under the MIT license. See license.txt for complete details.
|
data/greenbutton.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'greenbutton'
|
3
|
+
s.version = '0.0.1'
|
4
|
+
s.date = '2014-03-17'
|
5
|
+
s.summary = "Ruby parser for the GreenButton data standard."
|
6
|
+
s.description = "This parser programmatically creates a Ruby object from a GreenButton data file. See https://collaborate.nist.gov/twiki-sggrid/bin/view/SmartGrid/GreenButtonSDK for more information on GreenButton. It is under active development and participation is encouraged. It is not yet stable."
|
7
|
+
s.authors = ["Charles Worthington", "Eric Hulburd"]
|
8
|
+
s.email = ['c.e.worthington@gmail.com','eric@arbol.org']
|
9
|
+
s.files = `git ls-files`.split("\n")
|
10
|
+
s.homepage = 'https://github.com/cew821/greenbutton'
|
11
|
+
s.license = 'MIT'
|
12
|
+
|
13
|
+
s.add_dependency "nokogiri", "~>1.6"
|
14
|
+
s.add_development_dependency "bundler", "~>1.5"
|
15
|
+
s.add_development_dependency "rspec", "~>2.14"
|
16
|
+
|
17
|
+
s.test_files = Dir.glob('spec/*_spec.rb')
|
18
|
+
end
|
data/lib/greenbutton.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module GreenButton
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'greenbutton/gb_classes.rb'
|
4
|
+
|
5
|
+
UsagePoint = GreenButtonClasses::UsagePoint
|
6
|
+
|
7
|
+
# could also load this from the data custodian:feed
|
8
|
+
# url = "http://services.greenbuttondata.org:80/DataCustodian/espi/1_1/resource/RetailCustomer/1/DownloadMyData"
|
9
|
+
|
10
|
+
def self.load_xml_from_web(url)
|
11
|
+
xml_file = Nokogiri.XML(open(url))
|
12
|
+
xml_file.remove_namespaces!
|
13
|
+
Parser.new(xml_file)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.load_xml_from_file(path)
|
17
|
+
xml_file = Nokogiri.XML(File.open(path, 'rb'))
|
18
|
+
xml_file.remove_namespaces!
|
19
|
+
Parser.new(xml_file)
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
class Parser
|
24
|
+
attr_accessor :doc, :usage_points
|
25
|
+
|
26
|
+
def initialize(doc)
|
27
|
+
@doc = doc
|
28
|
+
@usage_points = []
|
29
|
+
doc.xpath('//UsagePoint').each do |usage_point|
|
30
|
+
@usage_points << UsagePoint.new(usage_point.parent.parent, self)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def filter_usage_points(params)
|
35
|
+
# customer_id, service_kind, title, id, href
|
36
|
+
filtered = []
|
37
|
+
@usage_points.each do |usage_point|
|
38
|
+
params.each_pair do |key, value|
|
39
|
+
filtered << usage_point if usage_point.send(key) == value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
filtered
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_unique(attr)
|
46
|
+
#customer_id, service_kind, title
|
47
|
+
unique = []
|
48
|
+
@usage_points.each do |usage_point|
|
49
|
+
val = usage_point.send(attr)
|
50
|
+
if !unique.include?(val)
|
51
|
+
unique << val
|
52
|
+
end
|
53
|
+
end
|
54
|
+
unique
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
@@ -0,0 +1,361 @@
|
|
1
|
+
module GreenButtonClasses
|
2
|
+
require 'greenbutton/helpers.rb'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
Rule = Helper::Rule
|
6
|
+
RULES = [
|
7
|
+
Rule.new(:href, "./link[@rel='self']/@href", :string),
|
8
|
+
Rule.new(:parent_href, "./link[@rel='up']/@href", :string),
|
9
|
+
Rule.new(:id, "./id", :string),
|
10
|
+
Rule.new(:title, "./title", :string),
|
11
|
+
Rule.new(:date_published, "./published", :datetime),
|
12
|
+
Rule.new(:date_updated, "./updated", :datetime)
|
13
|
+
]
|
14
|
+
|
15
|
+
|
16
|
+
class GreenButtonEntry
|
17
|
+
attr_accessor :id, :title, :href, :published, :updated, :parent_href, :related_hrefs, :other_related
|
18
|
+
|
19
|
+
def initialize(entry_xml, parent)
|
20
|
+
if !entry_xml.nil?
|
21
|
+
@entry_xml = entry_xml
|
22
|
+
self.related_hrefs = []
|
23
|
+
self.other_related = []
|
24
|
+
pre_rule_assignment(parent)
|
25
|
+
assign_rules
|
26
|
+
find_related_entries
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def pre_rule_assignment(parent)
|
31
|
+
raise self.class + 'failed to implement pre_rule_assignment'
|
32
|
+
end
|
33
|
+
|
34
|
+
def additional_rules
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
|
38
|
+
def doc
|
39
|
+
self.usage_point.doc
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_by_href(href)
|
43
|
+
doc.xpath("//link[@rel='self' and @href='#{href}']/..")[0]
|
44
|
+
end
|
45
|
+
|
46
|
+
def assign_rules
|
47
|
+
(RULES + additional_rules).each do |rule|
|
48
|
+
create_attr(rule.attr_name)
|
49
|
+
rule_xml = @entry_xml.xpath(rule.xpath)
|
50
|
+
value = rule_xml.empty? ? nil : rule_xml.text
|
51
|
+
translated_value = value.nil? ? nil : Helper.translate(rule.type, value)
|
52
|
+
self.send(rule.attr_name.to_s+"=", translated_value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_related_entries
|
57
|
+
self.related_hrefs = []
|
58
|
+
@entry_xml.xpath("./link[@rel='related']/@href").each do |href|
|
59
|
+
if /\/\d+$/i.match(href.text)
|
60
|
+
related_entry = find_by_href(href.text)
|
61
|
+
if related_entry
|
62
|
+
parse_related_entry(related_entry)
|
63
|
+
self.related_hrefs << href.text
|
64
|
+
else
|
65
|
+
warn 'no link found for href: ' + href.text
|
66
|
+
end
|
67
|
+
else
|
68
|
+
doc.xpath("//link[@rel='up' and @href='#{href.text}']").each do |link|
|
69
|
+
self.related_hrefs << link.attr('href')
|
70
|
+
parse_related_entry(link.parent)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse_related_entry(entry_xml)
|
77
|
+
name = get_related_name(entry_xml)
|
78
|
+
classParser = GreenButtonClasses.const_get(name)
|
79
|
+
if !classParser.nil?
|
80
|
+
self.add_related(Helper.underscore(name), classParser.new(entry_xml, self))
|
81
|
+
else
|
82
|
+
other_related.push(xml)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def add_related(type, parser)
|
87
|
+
raise self.class + ' does not have any recognized relations.'
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def get_related_name(xml)
|
93
|
+
name = nil
|
94
|
+
xml.xpath('./content').children.each do |elem|
|
95
|
+
if elem.name != 'text'
|
96
|
+
name = elem.name
|
97
|
+
break
|
98
|
+
end
|
99
|
+
end
|
100
|
+
name
|
101
|
+
end
|
102
|
+
|
103
|
+
def alt_link(href)
|
104
|
+
# SDGE links map as .../MeterReading to .../MeterReading/\d+
|
105
|
+
regex = Regexp.new(href + '\/\d+$')
|
106
|
+
related_link = doc.xpath("//link[@rel='self']").select do |e|
|
107
|
+
if e['href'] =~ regex
|
108
|
+
e.parent
|
109
|
+
end
|
110
|
+
end
|
111
|
+
related_link[0]
|
112
|
+
end
|
113
|
+
|
114
|
+
def create_method( name, &block )
|
115
|
+
self.class.send( :define_method, name, &block )
|
116
|
+
end
|
117
|
+
|
118
|
+
def create_attr( name )
|
119
|
+
create_method( "#{name.to_s}=".to_sym ) { |val|
|
120
|
+
instance_variable_set( "@" + name.to_s, val)
|
121
|
+
}
|
122
|
+
create_method( name.to_sym ) {
|
123
|
+
instance_variable_get( "@" + name.to_s )
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
class UsagePoint < GreenButtonEntry
|
130
|
+
attr_accessor :local_time_parameters, :meter_readings, :electric_power_usage_summaries,
|
131
|
+
:electric_power_quality_summaries, :green_button
|
132
|
+
|
133
|
+
def pre_rule_assignment(parent)
|
134
|
+
self.green_button = parent
|
135
|
+
self.meter_readings = []
|
136
|
+
self.electric_power_quality_summaries = []
|
137
|
+
self.electric_power_usage_summaries = []
|
138
|
+
end
|
139
|
+
|
140
|
+
def doc
|
141
|
+
self.green_button.doc
|
142
|
+
end
|
143
|
+
|
144
|
+
def add_related(type, parser)
|
145
|
+
case type
|
146
|
+
when 'local_time_parameters'
|
147
|
+
self.local_time_parameters = parser
|
148
|
+
when 'meter_reading', 'electric_power_usage_summary', 'electric_power_quality_summary'
|
149
|
+
self.send(Helper.pluralize(type)) << parser
|
150
|
+
else
|
151
|
+
raise 'Not a recognized relation for UsagePoint: ' + type
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def additional_rules
|
156
|
+
[ Rule.new(:service_kind, "//ServiceCategory/kind", :ServiceKind) ]
|
157
|
+
end
|
158
|
+
|
159
|
+
def customer_id
|
160
|
+
if @customer_id.nil?
|
161
|
+
match = /\/([^\/]+)\/UsagePoint/i.match(self.href)
|
162
|
+
@customer_id = match.nil? ? nil : match[1]
|
163
|
+
end
|
164
|
+
@customer_id
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
class MeterReading < GreenButtonEntry
|
170
|
+
attr_accessor :reading_type, :interval_blocks, :usage_point
|
171
|
+
|
172
|
+
def pre_rule_assignment(parent)
|
173
|
+
self.usage_point = parent
|
174
|
+
self.interval_blocks = []
|
175
|
+
end
|
176
|
+
|
177
|
+
def add_related(type, parser)
|
178
|
+
case type
|
179
|
+
when 'reading_type'
|
180
|
+
self.reading_type = parser
|
181
|
+
when 'interval_block'
|
182
|
+
self.interval_blocks << parser
|
183
|
+
else
|
184
|
+
raise 'Not a recognized relation for MeterReading'
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class ReadingType < GreenButtonEntry
|
190
|
+
attr_accessor :meter_reading
|
191
|
+
ATTRS = ['accumulationBehaviour', 'commodity', 'currency', 'dataQualifier', 'flowDirection', 'intervalLength',
|
192
|
+
'kind', 'phase', 'powerOfTenMultiplier', 'timeAttribute', 'uom']
|
193
|
+
|
194
|
+
def pre_rule_assignment(parent)
|
195
|
+
self.meter_reading = parent
|
196
|
+
end
|
197
|
+
|
198
|
+
def doc
|
199
|
+
self.meter_reading.doc
|
200
|
+
end
|
201
|
+
|
202
|
+
def additional_rules
|
203
|
+
rules = []
|
204
|
+
ATTRS.each do |attr|
|
205
|
+
rules << Rule.new( Helper.underscore(attr).to_sym, './/'+attr, :integer )
|
206
|
+
end
|
207
|
+
rules
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
class ElectricPowerUsageSummary < GreenButtonEntry
|
212
|
+
attr_accessor :usage_point
|
213
|
+
ATTRS = ['billLastPeriod', 'billToDate', 'costAdditionalLastPeriod', 'currency',
|
214
|
+
'qualityOfReading', 'statusTimeStamp']
|
215
|
+
|
216
|
+
def pre_rule_assignment(parent)
|
217
|
+
self.usage_point = parent
|
218
|
+
end
|
219
|
+
|
220
|
+
def additional_rules
|
221
|
+
rules = [
|
222
|
+
Rule.new(:bill_duration, ".//duration", :integer),
|
223
|
+
Rule.new(:bill_start, ".//start", :unix_time),
|
224
|
+
Rule.new(:last_power_ten, ".//overallConsumptionLastPeriod/powerOfTenMultiplier", :integer),
|
225
|
+
Rule.new(:last_uom, ".//overallConsumptionLastPeriod/uom", :integer),
|
226
|
+
Rule.new(:last_value, ".//overallConsumptionLastPeriod/value", :integer),
|
227
|
+
Rule.new(:current_power_ten, ".//currentBillingPeriodOverAllConsumption/powerOfTenMultiplier", :integer),
|
228
|
+
Rule.new(:current_uom, ".//currentBillingPeriodOverAllConsumption/uom", :integer),
|
229
|
+
Rule.new(:current_value, ".//currentBillingPeriodOverAllConsumption/value", :integer),
|
230
|
+
Rule.new(:current_timestamp, ".//currentBillingPeriodOverAllConsumption/timeStamp", :unix_time)
|
231
|
+
]
|
232
|
+
ATTRS.each do |attr|
|
233
|
+
rules << Rule.new( Helper.underscore(attr).to_sym, '//'+attr, :integer )
|
234
|
+
end
|
235
|
+
rules
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
class ElectricPowerQualitySummary < GreenButtonEntry
|
240
|
+
attr_accessor :usage_point
|
241
|
+
def pre_rule_assignment(parent)
|
242
|
+
self.usage_point = parent
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class LocalTimeParameters < GreenButtonEntry
|
247
|
+
attr_accessor :usage_point
|
248
|
+
|
249
|
+
def pre_rule_assignment(parent)
|
250
|
+
self.usage_point = parent
|
251
|
+
end
|
252
|
+
|
253
|
+
def additional_rules
|
254
|
+
[
|
255
|
+
Rule.new(:dst_end_rule, ".//dstEndRule", :string),
|
256
|
+
Rule.new(:dst_offset, ".//dstOffset", :integer),
|
257
|
+
Rule.new(:dst_start_rule, ".//dstStartRule", :string),
|
258
|
+
Rule.new(:tz_offset, ".//tzOffset", :integer)
|
259
|
+
]
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
class IntervalBlock < GreenButtonEntry
|
264
|
+
attr_accessor :meter_reading
|
265
|
+
|
266
|
+
def pre_rule_assignment(parent)
|
267
|
+
self.meter_reading = parent
|
268
|
+
end
|
269
|
+
|
270
|
+
def additional_rules
|
271
|
+
[
|
272
|
+
Rule.new(:start_time, './/interval/start', :unix_time),
|
273
|
+
Rule.new(:duration, './/interval/duration', :integer)
|
274
|
+
]
|
275
|
+
end
|
276
|
+
|
277
|
+
def doc
|
278
|
+
self.meter_reading.doc
|
279
|
+
end
|
280
|
+
|
281
|
+
def end_time
|
282
|
+
self.start_time + self.duration
|
283
|
+
end
|
284
|
+
|
285
|
+
def power_of_ten_multiplier
|
286
|
+
self.meter_reading.reading_type.power_of_ten_multiplier
|
287
|
+
end
|
288
|
+
|
289
|
+
def reading_at_time(time)
|
290
|
+
if (time >= self.start_time) && (time < end_time)
|
291
|
+
@entry_xml.xpath('.//IntervalReading').each do |interval_reading|
|
292
|
+
intervalReading = IntervalReading.new(interval_reading)
|
293
|
+
if (intervalReading.start_time <= time) && (intervalReading.end_time > time)
|
294
|
+
return intervalReading
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def value_at_time(time)
|
301
|
+
reading_at_time(time).value*10**power_of_ten_multiplier
|
302
|
+
end
|
303
|
+
|
304
|
+
def total
|
305
|
+
if @total.nil?
|
306
|
+
@total = sum
|
307
|
+
end
|
308
|
+
@total
|
309
|
+
end
|
310
|
+
|
311
|
+
def average_interval_value
|
312
|
+
total/n_readings
|
313
|
+
end
|
314
|
+
|
315
|
+
def n_readings
|
316
|
+
@entry_xml.xpath('.//IntervalReading').length
|
317
|
+
end
|
318
|
+
|
319
|
+
def sum(starttime=nil, endtime=nil)
|
320
|
+
starttime = starttime.nil? ? self.start_time : starttime.utc
|
321
|
+
endtime = endtime.nil? ? self.end_time : endtime.utc
|
322
|
+
sum = 0
|
323
|
+
@entry_xml.xpath('.//IntervalReading').each do |interval_reading|
|
324
|
+
intervalReading = IntervalReading.new(interval_reading)
|
325
|
+
if intervalReading.start_time >= starttime && intervalReading.start_time < endtime
|
326
|
+
if intervalReading.end_time <= endtime
|
327
|
+
sum += intervalReading.value
|
328
|
+
else
|
329
|
+
ratio = (intervalReading.end_time.to_i - endtime.to_i)/intervalReading.duration
|
330
|
+
sum += ratio*intervalReading.value
|
331
|
+
break
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
sum*10**power_of_ten_multiplier
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
class IntervalReading
|
340
|
+
def initialize(reading_xml)
|
341
|
+
@reading_xml = reading_xml
|
342
|
+
end
|
343
|
+
|
344
|
+
def value
|
345
|
+
@reading_xml.xpath('./value').text.to_f
|
346
|
+
end
|
347
|
+
|
348
|
+
def start_time
|
349
|
+
Time.at(@reading_xml.xpath('./timePeriod/start').text.to_i).utc
|
350
|
+
end
|
351
|
+
|
352
|
+
def duration
|
353
|
+
@reading_xml.xpath('./timePeriod/duration').text.to_i
|
354
|
+
end
|
355
|
+
|
356
|
+
def end_time
|
357
|
+
start_time + duration
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
end
|