aemo 0.1.27 → 0.1.28
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 +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
|