aemo 0.1.27 → 0.1.28
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/aemo/dispatchable.rb +4 -4
- data/lib/aemo/market.rb +30 -27
- data/lib/aemo/market/interval.rb +27 -23
- data/lib/aemo/msats.rb +218 -210
- data/lib/aemo/nem12.rb +165 -160
- data/lib/aemo/nem13.rb +1 -2
- data/lib/aemo/nmi.rb +91 -85
- data/lib/aemo/region.rb +12 -11
- data/lib/aemo/version.rb +3 -3
- data/lib/data/xml_to_json.rb +62 -0
- data/spec/aemo_spec.rb +1 -2
- data/spec/lib/aemo/market/interval_spec.rb +13 -13
- data/spec/lib/aemo/market_spec.rb +8 -6
- data/spec/lib/aemo/msats_spec.rb +14 -15
- data/spec/lib/aemo/nem12_spec.rb +8 -10
- data/spec/lib/aemo/nmi_spec.rb +79 -71
- data/spec/lib/aemo/region_spec.rb +5 -6
- data/spec/spec_helper.rb +38 -35
- metadata +16 -2
- data/lib/data/xml-to-json.rb +0 -61
data/lib/aemo/nem12.rb
CHANGED
@@ -6,7 +6,9 @@ module AEMO
|
|
6
6
|
# @since 0.1.4
|
7
7
|
class NEM12
|
8
8
|
# As per AEMO NEM12 Specification
|
9
|
-
# http://www.aemo.com.au/Consultations/National-Electricity-Market/Open/~/media/
|
9
|
+
# http://www.aemo.com.au/Consultations/National-Electricity-Market/Open/~/media/
|
10
|
+
# Files/Other/consultations/nem/Meter% 20Data% 20File% 20Format% 20Specification% 20
|
11
|
+
# NEM12_NEM13/MDFF_Specification_NEM12_NEM13_Final_v102_clean.ashx
|
10
12
|
RECORD_INDICATORS = {
|
11
13
|
100 => 'Header',
|
12
14
|
200 => 'NMI Data Details',
|
@@ -14,7 +16,7 @@ module AEMO
|
|
14
16
|
400 => 'Interval Event',
|
15
17
|
500 => 'B2B Details',
|
16
18
|
900 => 'End'
|
17
|
-
}
|
19
|
+
}.freeze
|
18
20
|
|
19
21
|
TRANSACTION_CODE_FLAGS = {
|
20
22
|
'A' => 'Alteration',
|
@@ -26,33 +28,33 @@ module AEMO
|
|
26
28
|
'O' => 'Other',
|
27
29
|
'S' => 'Special Read',
|
28
30
|
'R' => 'Removal of Meter'
|
29
|
-
}
|
31
|
+
}.freeze
|
30
32
|
|
31
33
|
UOM = {
|
32
|
-
'MWh' => { :
|
33
|
-
'kWh' => { :
|
34
|
-
'Wh' => { :
|
35
|
-
'MW' => { :
|
36
|
-
'kW' => { :
|
37
|
-
'W' => { :
|
38
|
-
'MVArh' => { :
|
39
|
-
'kVArh' => { :
|
40
|
-
'VArh' => { :
|
41
|
-
'MVAr' => { :
|
42
|
-
'kVAr' => { :
|
43
|
-
'VAr' => { :
|
44
|
-
'MVAh' => { :
|
45
|
-
'kVAh' => { :
|
46
|
-
'VAh' => { :
|
47
|
-
'MVA' => { :
|
48
|
-
'kVA' => { :
|
49
|
-
'VA' => { :
|
50
|
-
'kV' => { :
|
51
|
-
'V' => { :
|
52
|
-
'kA' => { :
|
53
|
-
'A' => { :
|
54
|
-
'pf' => { :
|
55
|
-
}
|
34
|
+
'MWh' => { name: 'Megawatt Hour', multiplier: 1e6 },
|
35
|
+
'kWh' => { name: 'Kilowatt Hour', multiplier: 1e3 },
|
36
|
+
'Wh' => { name: 'Watt Hour', multiplier: 1 },
|
37
|
+
'MW' => { name: 'Megawatt', multiplier: 1e6 },
|
38
|
+
'kW' => { name: 'Kilowatt', multiplier: 1e3 },
|
39
|
+
'W' => { name: 'Watt', multiplier: 1 },
|
40
|
+
'MVArh' => { name: 'Megavolt Ampere Reactive Hour', multiplier: 1e6 },
|
41
|
+
'kVArh' => { name: 'Kilovolt Ampere Reactive Hour', multiplier: 1e3 },
|
42
|
+
'VArh' => { name: 'Volt Ampere Reactive Hour', multiplier: 1 },
|
43
|
+
'MVAr' => { name: 'Megavolt Ampere Reactive', multiplier: 1e6 },
|
44
|
+
'kVAr' => { name: 'Kilovolt Ampere Reactive', multiplier: 1e3 },
|
45
|
+
'VAr' => { name: 'Volt Ampere Reactive', multiplier: 1 },
|
46
|
+
'MVAh' => { name: 'Megavolt Ampere Hour', multiplier: 1e6 },
|
47
|
+
'kVAh' => { name: 'Kilovolt Ampere Hour', multiplier: 1e3 },
|
48
|
+
'VAh' => { name: 'Volt Ampere Hour', multiplier: 1 },
|
49
|
+
'MVA' => { name: 'Megavolt Ampere', multiplier: 1e6 },
|
50
|
+
'kVA' => { name: 'Kilovolt Ampere', multiplier: 1e3 },
|
51
|
+
'VA' => { name: 'Volt Ampere', multiplier: 1 },
|
52
|
+
'kV' => { name: 'Kilovolt', multiplier: 1e3 },
|
53
|
+
'V' => { name: 'Volt', multiplier: 1 },
|
54
|
+
'kA' => { name: 'Kiloampere', multiplier: 1e3 },
|
55
|
+
'A' => { name: 'Ampere', multiplier: 1 },
|
56
|
+
'pf' => { name: 'Power Factor', multiplier: 1 }
|
57
|
+
}.freeze
|
56
58
|
|
57
59
|
UOM_NON_SPEC_MAPPING = {
|
58
60
|
'MWH' => 'MWh',
|
@@ -78,7 +80,7 @@ module AEMO
|
|
78
80
|
'KA' => 'kA',
|
79
81
|
'A' => 'A',
|
80
82
|
'PF' => 'pf'
|
81
|
-
}
|
83
|
+
}.freeze
|
82
84
|
|
83
85
|
QUALITY_FLAGS = {
|
84
86
|
'A' => 'Actual Data',
|
@@ -86,41 +88,41 @@ module AEMO
|
|
86
88
|
'F' => 'Final Substituted Data',
|
87
89
|
'N' => 'Null Data',
|
88
90
|
'S' => 'Substituted Data',
|
89
|
-
'V' => 'Variable Data'
|
90
|
-
}
|
91
|
+
'V' => 'Variable Data'
|
92
|
+
}.freeze
|
91
93
|
|
92
94
|
METHOD_FLAGS = {
|
93
|
-
11 => { type:
|
94
|
-
12 => { type:
|
95
|
-
13 => { type:
|
96
|
-
14 => { type:
|
97
|
-
15 => { type:
|
98
|
-
16 => { type:
|
99
|
-
17 => { type:
|
100
|
-
18 => { type:
|
101
|
-
19 => { type:
|
102
|
-
51 => { type:
|
103
|
-
52 => { type:
|
104
|
-
53 => { type:
|
105
|
-
54 => { type:
|
106
|
-
55 => { type:
|
107
|
-
56 => { type:
|
108
|
-
57 => { type:
|
109
|
-
58 => { type:
|
110
|
-
61 => { type:
|
111
|
-
62 => { type:
|
112
|
-
63 => { type:
|
113
|
-
64 => { type:
|
114
|
-
65 => { type:
|
115
|
-
66 => { type:
|
116
|
-
67 => { type:
|
117
|
-
68 => { type:
|
118
|
-
71 => { type:
|
119
|
-
72 => { type:
|
120
|
-
73 => { type:
|
121
|
-
74 => { type:
|
122
|
-
75 => { type:
|
123
|
-
}
|
95
|
+
11 => { type: %w(SUB), installation_type: [1, 2, 3, 4], short_descriptor: 'Check', description: '' },
|
96
|
+
12 => { type: %w(SUB), installation_type: [1, 2, 3, 4], short_descriptor: 'Calculated', description: '' },
|
97
|
+
13 => { type: %w(SUB), installation_type: [1, 2, 3, 4], short_descriptor: 'SCADA', description: '' },
|
98
|
+
14 => { type: %w(SUB), installation_type: [1, 2, 3, 4], short_descriptor: 'Like Day', description: '' },
|
99
|
+
15 => { type: %w(SUB), installation_type: [1, 2, 3, 4], short_descriptor: 'Average Like Day', description: '' },
|
100
|
+
16 => { type: %w(SUB), installation_type: [1, 2, 3, 4], short_descriptor: 'Agreed', description: '' },
|
101
|
+
17 => { type: %w(SUB), installation_type: [1, 2, 3, 4], short_descriptor: 'Linear', description: '' },
|
102
|
+
18 => { type: %w(SUB), installation_type: [1, 2, 3, 4], short_descriptor: 'Alternate', description: '' },
|
103
|
+
19 => { type: %w(SUB), installation_type: [1, 2, 3, 4], short_descriptor: 'Zero', description: '' },
|
104
|
+
51 => { type: %w(EST SUB), installation_type: 5, short_descriptor: 'Previous Year', description: '' },
|
105
|
+
52 => { type: %w(EST SUB), installation_type: 5, short_descriptor: 'Previous Read', description: '' },
|
106
|
+
53 => { type: %w(SUB), installation_type: 5, short_descriptor: 'Revision', description: '' },
|
107
|
+
54 => { type: %w(SUB), installation_type: 5, short_descriptor: 'Linear', description: '' },
|
108
|
+
55 => { type: %w(SUB), installation_type: 5, short_descriptor: 'Agreed', description: '' },
|
109
|
+
56 => { type: %w(EST SUB), installation_type: 5, short_descriptor: 'Prior to First Read - Agreed', description: '' },
|
110
|
+
57 => { type: %w(EST SUB), installation_type: 5, short_descriptor: 'Customer Class', description: '' },
|
111
|
+
58 => { type: %w(EST SUB), installation_type: 5, short_descriptor: 'Zero', description: '' },
|
112
|
+
61 => { type: %w(EST SUB), installation_type: 6, short_descriptor: 'Previous Year', description: '' },
|
113
|
+
62 => { type: %w(EST SUB), installation_type: 6, short_descriptor: 'Previous Read', description: '' },
|
114
|
+
63 => { type: %w(EST SUB), installation_type: 6, short_descriptor: 'Customer Class', description: '' },
|
115
|
+
64 => { type: %w(SUB), installation_type: 6, short_descriptor: 'Agreed', description: '' },
|
116
|
+
65 => { type: %w(EST), installation_type: 6, short_descriptor: 'ADL', description: '' },
|
117
|
+
66 => { type: %w(SUB), installation_type: 6, short_descriptor: 'Revision', description: '' },
|
118
|
+
67 => { type: %w(SUB), installation_type: 6, short_descriptor: 'Customer Read', description: '' },
|
119
|
+
68 => { type: %w(EST SUB), installation_type: 6, short_descriptor: 'Zero', description: '' },
|
120
|
+
71 => { type: %w(SUB), installation_type: 7, short_descriptor: 'Recalculation', description: '' },
|
121
|
+
72 => { type: %w(SUB), installation_type: 7, short_descriptor: 'Revised Table', description: '' },
|
122
|
+
73 => { type: %w(SUB), installation_type: 7, short_descriptor: 'Revised Algorithm', description: '' },
|
123
|
+
74 => { type: %w(SUB), installation_type: 7, short_descriptor: 'Agreed', description: '' },
|
124
|
+
75 => { type: %w(EST), installation_type: 7, short_descriptor: 'Existing Table', description: '' }
|
125
|
+
}.freeze
|
124
126
|
|
125
127
|
REASON_CODES = {
|
126
128
|
0 => 'Free Text Description',
|
@@ -223,40 +225,39 @@ module AEMO
|
|
223
225
|
97 => 'Excluded Data',
|
224
226
|
98 => 'Parity Error',
|
225
227
|
99 => 'Energy Type (Register Changed)'
|
226
|
-
|
227
|
-
}
|
228
|
+
}.freeze
|
228
229
|
|
229
230
|
DATA_STREAM_SUFFIX = {
|
230
231
|
# Averaged Data Streams
|
231
|
-
'A' => { :
|
232
|
-
'D' => { :
|
233
|
-
'J' => { :
|
234
|
-
'P' => { :
|
235
|
-
'S' => { :
|
232
|
+
'A' => { stream: 'Average', description: 'Import', units: 'kWh' },
|
233
|
+
'D' => { stream: 'Average', description: 'Export', units: 'kWh' },
|
234
|
+
'J' => { stream: 'Average', description: 'Import', units: 'kVArh' },
|
235
|
+
'P' => { stream: 'Average', description: 'Export', units: 'kVArh' },
|
236
|
+
'S' => { stream: 'Average', description: '', units: 'kVAh' },
|
236
237
|
# Master Data Streams
|
237
|
-
'B' => { :
|
238
|
-
'E' => { :
|
239
|
-
'K' => { :
|
240
|
-
'Q' => { :
|
241
|
-
'T' => { :
|
242
|
-
'G' => { :
|
243
|
-
'H' => { :
|
244
|
-
'M' => { :
|
245
|
-
'V' => { :
|
238
|
+
'B' => { stream: 'Master', description: 'Import', units: 'kWh' },
|
239
|
+
'E' => { stream: 'Master', description: 'Export', units: 'kWh' },
|
240
|
+
'K' => { stream: 'Master', description: 'Import', units: 'kVArh' },
|
241
|
+
'Q' => { stream: 'Master', description: 'Export', units: 'kVArh' },
|
242
|
+
'T' => { stream: 'Master', description: '', units: 'kVAh' },
|
243
|
+
'G' => { stream: 'Master', description: 'Power Factor', units: 'PF' },
|
244
|
+
'H' => { stream: 'Master', description: 'Q Metering', units: 'Qh' },
|
245
|
+
'M' => { stream: 'Master', description: 'Par Metering', units: 'parh' },
|
246
|
+
'V' => { stream: 'Master', description: 'Volts or V2h or Amps or A2h', units: '' },
|
246
247
|
# Check Meter Streams
|
247
|
-
'C' => { :
|
248
|
-
'F' => { :
|
249
|
-
'L' => { :
|
250
|
-
'R' => { :
|
251
|
-
'U' => { :
|
252
|
-
'Y' => { :
|
253
|
-
'W' => { :
|
254
|
-
'Z' => { :
|
248
|
+
'C' => { stream: 'Check', description: 'Import', units: 'kWh' },
|
249
|
+
'F' => { stream: 'Check', description: 'Export', units: 'kWh' },
|
250
|
+
'L' => { stream: 'Check', description: 'Import', units: 'kVArh' },
|
251
|
+
'R' => { stream: 'Check', description: 'Export', units: 'kVArh' },
|
252
|
+
'U' => { stream: 'Check', description: '', units: 'kVAh' },
|
253
|
+
'Y' => { stream: 'Check', description: 'Q Metering', units: 'Qh' },
|
254
|
+
'W' => { stream: 'Check', description: 'Par Metering Path', units: '' },
|
255
|
+
'Z' => { stream: 'Check', description: 'Volts or V2h or Amps or A2h', units: '' },
|
255
256
|
# Net Meter Streams
|
256
257
|
# AEMO: NOTE THAT D AND J ARE PREVIOUSLY DEFINED
|
257
|
-
# 'D' => { :
|
258
|
-
# 'J' => { :
|
259
|
-
}
|
258
|
+
# 'D' => { stream: 'Net', description: 'Net', units: 'kWh' },
|
259
|
+
# 'J' => { stream: 'Net', description: 'Net', units: 'kVArh' }
|
260
|
+
}.freeze
|
260
261
|
|
261
262
|
@file_contents = nil
|
262
263
|
@header = nil
|
@@ -267,19 +268,18 @@ module AEMO
|
|
267
268
|
@interval_data = []
|
268
269
|
@interval_events = []
|
269
270
|
|
270
|
-
|
271
271
|
attr_accessor :nmi, :file_contents
|
272
272
|
attr_reader :data_details, :interval_data, :interval_events
|
273
273
|
attr_accessor :file_contents, :header, :nmi_data_details, :nmi
|
274
274
|
|
275
275
|
# Initialize a NEM12 file
|
276
|
-
def initialize(nmi,options={})
|
276
|
+
def initialize(nmi, options = {})
|
277
277
|
@nmi = AEMO::NMI.new(nmi) unless nmi.empty?
|
278
278
|
@data_details = []
|
279
279
|
@interval_data = []
|
280
280
|
@interval_events = []
|
281
281
|
options.keys.each do |key|
|
282
|
-
|
282
|
+
send 'key=', options[key]
|
283
283
|
end
|
284
284
|
end
|
285
285
|
|
@@ -291,39 +291,39 @@ module AEMO
|
|
291
291
|
# Parses the header record
|
292
292
|
# @param line [String] A single line in string format
|
293
293
|
# @return [Hash] the line parsed into a hash of information
|
294
|
-
def self.parse_nem12_100(line,options={})
|
294
|
+
def self.parse_nem12_100(line, options = {})
|
295
295
|
csv = line.parse_csv
|
296
296
|
|
297
297
|
raise ArgumentError, 'RecordIndicator is not 100' if csv[0] != '100'
|
298
298
|
raise ArgumentError, 'VersionHeader is not NEM12' if csv[1] != 'NEM12'
|
299
|
-
if options[:strict]
|
300
|
-
raise ArgumentError, 'DateTime is not valid'
|
299
|
+
if options[:strict] && (csv[2].match(/\d{12}/).nil? || csv[2] != Time.parse("#{csv[2]}00").strftime('%Y%m%d%H%M'))
|
300
|
+
raise ArgumentError, 'DateTime is not valid'
|
301
301
|
end
|
302
302
|
raise ArgumentError, 'FromParticispant is not valid' if csv[3].match(/.{1,10}/).nil?
|
303
303
|
raise ArgumentError, 'ToParticispant is not valid' if csv[4].match(/.{1,10}/).nil?
|
304
304
|
|
305
|
-
|
306
|
-
:
|
307
|
-
:
|
308
|
-
:
|
309
|
-
:
|
310
|
-
:
|
305
|
+
{
|
306
|
+
record_indicator: csv[0].to_i,
|
307
|
+
version_header: csv[1],
|
308
|
+
datetime: Time.parse("#{csv[2]}+1000"),
|
309
|
+
from_participant: csv[3],
|
310
|
+
to_participant: csv[4]
|
311
311
|
}
|
312
312
|
end
|
313
313
|
|
314
314
|
# Parses the NMI Data Details
|
315
315
|
# @param line [String] A single line in string format
|
316
316
|
# @return [Hash] the line parsed into a hash of information
|
317
|
-
def parse_nem12_200(line,
|
317
|
+
def parse_nem12_200(line, _options = {})
|
318
318
|
csv = line.parse_csv
|
319
319
|
|
320
320
|
raise ArgumentError, 'RecordIndicator is not 200' if csv[0] != '200'
|
321
|
-
raise ArgumentError, 'NMI is not valid'
|
321
|
+
raise ArgumentError, 'NMI is not valid' unless AEMO::NMI.valid_nmi?(csv[1])
|
322
322
|
raise ArgumentError, 'NMIConfiguration is not valid' if csv[2].match(/.{1,240}/).nil?
|
323
|
-
|
324
|
-
raise ArgumentError, 'RegisterID is not valid'
|
323
|
+
if !csv[3].nil? && csv[3].match(/.{1,10}/).nil?
|
324
|
+
raise ArgumentError, 'RegisterID is not valid'
|
325
325
|
end
|
326
|
-
raise ArgumentError, 'NMISuffix is not valid'
|
326
|
+
raise ArgumentError, 'NMISuffix is not valid' if csv[4].match(/[A-HJ-NP-Z][1-9A-HJ-NP-Z]/).nil?
|
327
327
|
if !csv[5].nil? && !csv[5].empty? && !csv[5].match(/^\s*$/)
|
328
328
|
raise ArgumentError, 'MDMDataStreamIdentifier is not valid' if csv[5].match(/[A-Z0-9]{2}/).nil?
|
329
329
|
end
|
@@ -331,58 +331,60 @@ module AEMO
|
|
331
331
|
raise ArgumentError, 'MeterSerialNumber is not valid' if csv[6].match(/[A-Z0-9]{2}/).nil?
|
332
332
|
end
|
333
333
|
raise ArgumentError, 'UOM is not valid' if csv[7].upcase.match(/[A-Z0-9]{2}/).nil?
|
334
|
-
raise ArgumentError, 'UOM is not valid' unless UOM.keys.map
|
334
|
+
raise ArgumentError, 'UOM is not valid' unless UOM.keys.map(&:upcase).include?(csv[7].upcase)
|
335
335
|
raise ArgumentError, 'IntervalLength is not valid' unless %w(1 5 10 15 30).include?(csv[8])
|
336
|
-
# raise ArgumentError, 'NextScheduledReadDate is not valid' if csv[9].match(/\d{8}/).nil? || csv[9] != Time.parse(
|
336
|
+
# raise ArgumentError, 'NextScheduledReadDate is not valid' if csv[9].match(/\d{8}/).nil? || csv[9] != Time.parse('#{csv[9]}').strftime('%Y%m%d')
|
337
337
|
|
338
338
|
@nmi = AEMO::NMI.new(csv[1])
|
339
339
|
|
340
340
|
# Push onto the stack
|
341
341
|
@data_details << {
|
342
|
-
:
|
343
|
-
:
|
344
|
-
:
|
345
|
-
:
|
346
|
-
:
|
347
|
-
:
|
348
|
-
:
|
349
|
-
:
|
350
|
-
:
|
351
|
-
:
|
342
|
+
record_indicator: csv[0].to_i,
|
343
|
+
nmi: csv[1],
|
344
|
+
nmi_configuration: csv[2],
|
345
|
+
register_id: csv[3],
|
346
|
+
nmi_suffix: csv[4],
|
347
|
+
mdm_data_streaming_identifier: csv[5],
|
348
|
+
meter_serial_nubmer: csv[6],
|
349
|
+
uom: csv[7].upcase,
|
350
|
+
interval_length: csv[8].to_i,
|
351
|
+
next_scheduled_read_date: csv[9]
|
352
352
|
}
|
353
353
|
end
|
354
354
|
|
355
355
|
# @param line [String] A single line in string format
|
356
356
|
# @return [Array of hashes] the line parsed into a hash of information
|
357
|
-
def parse_nem12_300(line,options={})
|
357
|
+
def parse_nem12_300(line, options = {})
|
358
358
|
csv = line.parse_csv
|
359
359
|
|
360
360
|
raise TypeError, 'Expected NMI Data Details to exist with IntervalLength specified' if @data_details.last.nil? || @data_details.last[:interval_length].nil?
|
361
361
|
number_of_intervals = 1440 / @data_details.last[:interval_length]
|
362
362
|
intervals_offset = number_of_intervals + 2
|
363
363
|
|
364
|
-
raise ArgumentError, 'RecordIndicator is not 300'
|
365
|
-
raise ArgumentError, 'IntervalDate is not valid'
|
366
|
-
(2..(number_of_intervals+1)).each do |i|
|
367
|
-
raise ArgumentError, "Interval number #{i-1} is not valid"
|
364
|
+
raise ArgumentError, 'RecordIndicator is not 300' if csv[0] != '300'
|
365
|
+
raise ArgumentError, 'IntervalDate is not valid' if csv[1].match(/\d{8}/).nil? || csv[1] != Time.parse(csv[1].to_s).strftime('%Y%m%d')
|
366
|
+
(2..(number_of_intervals + 1)).each do |i|
|
367
|
+
raise ArgumentError, "Interval number #{i - 1} is not valid" if csv[i].match(/\d+(\.\d+)?/).nil?
|
368
368
|
end
|
369
|
-
raise ArgumentError, 'QualityMethod is not valid'
|
370
|
-
raise ArgumentError, 'QualityMethod does not have valid length'
|
371
|
-
raise ArgumentError, 'QualityMethod does not have valid QualityFlag'
|
369
|
+
raise ArgumentError, 'QualityMethod is not valid' unless csv[intervals_offset + 0].class == String
|
370
|
+
raise ArgumentError, 'QualityMethod does not have valid length' unless [1, 3].include?(csv[intervals_offset + 0].length)
|
371
|
+
raise ArgumentError, 'QualityMethod does not have valid QualityFlag' unless QUALITY_FLAGS.keys.include?(csv[intervals_offset + 0][0])
|
372
372
|
unless %w(A N V).include?(csv[intervals_offset + 0][0])
|
373
|
-
raise ArgumentError, 'QualityMethod does not have valid length'
|
374
|
-
raise ArgumentError, 'QualityMethod does not have valid MethodFlag'
|
373
|
+
raise ArgumentError, 'QualityMethod does not have valid length' unless csv[intervals_offset + 0].length == 3
|
374
|
+
raise ArgumentError, 'QualityMethod does not have valid MethodFlag' unless METHOD_FLAGS.keys.include?(csv[intervals_offset + 0][1..2].to_i)
|
375
375
|
end
|
376
376
|
unless %w(A N E).include?(csv[intervals_offset + 0][0])
|
377
|
-
raise ArgumentError, 'ReasonCode is not valid'
|
377
|
+
raise ArgumentError, 'ReasonCode is not valid' unless REASON_CODES.keys.include?(csv[intervals_offset + 1].to_i)
|
378
378
|
end
|
379
379
|
if !csv[intervals_offset + 1].nil? && csv[intervals_offset + 1].to_i == 0
|
380
|
-
raise ArgumentError, 'ReasonDescription is not valid'
|
380
|
+
raise ArgumentError, 'ReasonDescription is not valid' unless csv[intervals_offset + 2].class == String && !csv[intervals_offset + 2].empty?
|
381
381
|
end
|
382
382
|
if options[:strict]
|
383
|
-
|
384
|
-
|
385
|
-
|
383
|
+
if csv[intervals_offset + 3].match(/\d{14}/).nil? || csv[intervals_offset + 3] != Time.parse(csv[intervals_offset + 3].to_s).strftime('%Y%m%d%H%M%S')
|
384
|
+
raise ArgumentError, 'UpdateDateTime is not valid'
|
385
|
+
end
|
386
|
+
if !csv[intervals_offset + 4].nil? && csv[intervals_offset + 4].match(/\d{14}/).nil? || csv[intervals_offset + 4] != Time.parse(csv[intervals_offset + 4].to_s).strftime('%Y%m%d%H%M%S')
|
387
|
+
raise ArgumentError, 'MSATSLoadDateTime is not valid'
|
386
388
|
end
|
387
389
|
end
|
388
390
|
|
@@ -393,19 +395,19 @@ module AEMO
|
|
393
395
|
flag ||= { quality_flag: nil, method_flag: nil, reason_code: nil }
|
394
396
|
if csv[intervals_offset + 0].length == 3
|
395
397
|
flag[:quality_flag] = csv[intervals_offset + 0][0]
|
396
|
-
flag[:method_flag] = csv[intervals_offset + 0][1,2].to_i
|
398
|
+
flag[:method_flag] = csv[intervals_offset + 0][1, 2].to_i
|
397
399
|
end
|
398
400
|
unless csv[intervals_offset + 1].nil?
|
399
401
|
flag[:reason_code] = csv[intervals_offset + 1].to_i
|
400
402
|
end
|
401
403
|
end
|
402
404
|
|
403
|
-
base_interval = { data_details: @data_details.last, datetime: Time.parse("#{csv[1]}000000+1000"), value: nil, flag: flag}
|
405
|
+
base_interval = { data_details: @data_details.last, datetime: Time.parse("#{csv[1]}000000+1000"), value: nil, flag: flag }
|
404
406
|
|
405
407
|
intervals = []
|
406
|
-
(2..(number_of_intervals+1)).each do |i|
|
408
|
+
(2..(number_of_intervals + 1)).each do |i|
|
407
409
|
interval = base_interval.dup
|
408
|
-
interval[:datetime] += (i-1) * interval[:data_details][:interval_length] * 60
|
410
|
+
interval[:datetime] += (i - 1) * interval[:data_details][:interval_length] * 60
|
409
411
|
interval[:value] = csv[i].to_f
|
410
412
|
intervals << interval
|
411
413
|
end
|
@@ -437,19 +439,19 @@ module AEMO
|
|
437
439
|
# Interval Numbers are 1-indexed
|
438
440
|
((csv[1].to_i)..(csv[2].to_i)).each do |i|
|
439
441
|
interval_event = base_interval_event.dup
|
440
|
-
interval_event[:datetime] = @interval_data[interval_start_point + (i-1)][:datetime]
|
442
|
+
interval_event[:datetime] = @interval_data[interval_start_point + (i - 1)][:datetime]
|
441
443
|
interval_events << interval_event
|
442
444
|
# Create flag details
|
443
445
|
flag ||= { quality_flag: nil, method_flag: nil, reason_code: nil }
|
444
446
|
unless interval_event[:quality_method].nil?
|
445
447
|
flag[:quality_flag] = interval_event[:quality_method][0]
|
446
|
-
flag[:method_flag] = interval_event[:quality_method][1,2].to_i
|
448
|
+
flag[:method_flag] = interval_event[:quality_method][1, 2].to_i
|
447
449
|
end
|
448
450
|
unless interval_event[:reason_code].nil?
|
449
451
|
flag[:reason_code] = interval_event[:reason_code]
|
450
452
|
end
|
451
453
|
# Update with flag details
|
452
|
-
@interval_data[interval_start_point + (i-1)][:flag] = flag
|
454
|
+
@interval_data[interval_start_point + (i - 1)][:flag] = flag
|
453
455
|
end
|
454
456
|
@interval_events += interval_events
|
455
457
|
end
|
@@ -458,18 +460,18 @@ module AEMO
|
|
458
460
|
|
459
461
|
# @param line [String] A single line in string format
|
460
462
|
# @return [Hash] the line parsed into a hash of information
|
461
|
-
def parse_nem12_500(
|
463
|
+
def parse_nem12_500(_line, _options = {})
|
462
464
|
end
|
463
465
|
|
464
466
|
# @param line [String] A single line in string format
|
465
467
|
# @return [Hash] the line parsed into a hash of information
|
466
|
-
def parse_nem12_900(
|
468
|
+
def parse_nem12_900(_line, _options = {})
|
467
469
|
end
|
468
470
|
|
469
471
|
# Turns the flag to a string
|
470
472
|
#
|
471
473
|
# @param [Hash] the object of a flag
|
472
|
-
# @return [nil,String] a hyphenated string for the flag or nil
|
474
|
+
# @return [nil, String] a hyphenated string for the flag or nil
|
473
475
|
def flag_to_s(flag)
|
474
476
|
flag_to_s = []
|
475
477
|
unless flag.nil?
|
@@ -477,44 +479,48 @@ module AEMO
|
|
477
479
|
flag_to_s << METHOD_FLAGS[flag[:method_flag]][:short_descriptor] unless METHOD_FLAGS[flag[:method_flag]].nil?
|
478
480
|
flag_to_s << REASON_CODES[flag[:reason_code]] unless REASON_CODES[flag[:reason_code]].nil?
|
479
481
|
end
|
480
|
-
|
482
|
+
flag_to_s.empty? ? nil : flag_to_s.join(' - ')
|
481
483
|
end
|
482
484
|
|
483
485
|
# @return [Array] array of a NEM12 file a given Meter + Data Stream for easy reading
|
484
486
|
def to_a
|
485
|
-
|
486
|
-
|
487
|
-
|
487
|
+
@interval_data.map do |d|
|
488
|
+
[
|
489
|
+
d[:data_details][:nmi],
|
488
490
|
d[:data_details][:nmi_suffix].upcase,
|
489
491
|
d[:data_details][:uom],
|
490
492
|
d[:datetime],
|
491
493
|
d[:value],
|
492
|
-
flag_to_s(d[:flag])
|
494
|
+
flag_to_s(d[:flag])
|
495
|
+
]
|
493
496
|
end
|
494
|
-
values
|
495
497
|
end
|
496
498
|
|
497
499
|
# @return [Array] CSV of a NEM12 file a given Meter + Data Stream for easy reading
|
498
500
|
def to_csv
|
499
|
-
headers =
|
500
|
-
([headers]+
|
501
|
+
headers = %w(nmi suffix units datetime value flags)
|
502
|
+
([headers] + to_a.map do |row|
|
503
|
+
row[3] = row[3].strftime('%Y%m%d%H%M%S%z')
|
504
|
+
row
|
505
|
+
end).map do |row|
|
506
|
+
row.join(', ')
|
507
|
+
end.join('\n')
|
501
508
|
end
|
502
509
|
|
503
510
|
# @param path_to_file [String] the path to a file
|
504
511
|
# @return [] NEM12 object
|
505
512
|
def self.parse_nem12_file(path_to_file, strict = false)
|
506
|
-
parse_nem12(File.read(path_to_file),strict)
|
513
|
+
parse_nem12(File.read(path_to_file), strict)
|
507
514
|
end
|
508
515
|
|
509
516
|
# @param contents [String] the path to a file
|
510
517
|
# @return [Array[AEMO::NEM12]] An array of NEM12 objects
|
511
|
-
def self.parse_nem12(contents,
|
512
|
-
file_contents = contents.
|
518
|
+
def self.parse_nem12(contents, _strict = false)
|
519
|
+
file_contents = contents.tr('\r', '\n').tr('\n\n', '\n').split('\n').delete_if(&:empty?)
|
513
520
|
raise ArgumentError, 'First row should be have a RecordIndicator of 100 and be of type Header Record' unless file_contents.first.parse_csv[0] == '100'
|
514
521
|
|
515
522
|
nem12s = []
|
516
|
-
|
517
|
-
nem12 = nil
|
523
|
+
AEMO::NEM12.parse_nem12_100(file_contents.first, strict: strict)
|
518
524
|
file_contents.each do |line|
|
519
525
|
case line[0..2].to_i
|
520
526
|
when 200
|
@@ -524,15 +530,14 @@ module AEMO
|
|
524
530
|
nem12s.last.parse_nem12_300(line)
|
525
531
|
when 400
|
526
532
|
nem12s.last.parse_nem12_400(line)
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
533
|
+
# when 500
|
534
|
+
# nem12s.last.parse_nem12_500(line)
|
535
|
+
# when 900
|
536
|
+
# nem12s.last.parse_nem12_900(line)
|
531
537
|
end
|
532
538
|
end
|
533
539
|
# Return the array of NEM12 groups
|
534
540
|
nem12s
|
535
541
|
end
|
536
|
-
|
537
542
|
end
|
538
543
|
end
|