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.
@@ -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
@@ -0,0 +1,5 @@
1
+ notes.txt
2
+ test.rb
3
+ .project
4
+ .DS_Store
5
+ .bundle/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1 @@
1
+ greenbuttonparser
@@ -0,0 +1 @@
1
+ ruby-2.1.0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -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)
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,10 @@
1
+
2
+ ## Sample Green Button Parser Implemented in Ruby ##
3
+
4
+ [![Code Climate](https://codeclimate.com/github/cew821/greenbutton.png)](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.
@@ -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
@@ -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