aemo 0.7.2 → 0.8.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 +4 -4
- data/lib/aemo/meter_data/flag/method.rb +58 -0
- data/lib/aemo/meter_data/flag/quality.rb +18 -0
- data/lib/aemo/meter_data/flag/reason_code.rb +112 -0
- data/lib/aemo/meter_data/flag.rb +280 -0
- data/lib/aemo/nem12/quality_method.rb +3 -63
- data/lib/aemo/nem12/reason_codes.rb +3 -108
- data/lib/aemo/nem12.rb +68 -58
- data/lib/aemo/version.rb +1 -1
- data/lib/data/xml_to_json.rb +32 -32
- data/spec/fixtures/NEM12/NEM12.actual_with_reason_code.csv +14 -0
- data/spec/lib/aemo/meter_data/flag_spec.rb +142 -0
- data/spec/lib/aemo/nem12_spec.rb +159 -13
- metadata +11 -5
data/lib/aemo/nem12.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'csv'
|
|
4
4
|
require 'time'
|
|
5
5
|
|
|
6
|
+
require 'aemo/meter_data/flag'
|
|
6
7
|
require 'aemo/nem12/data_stream_suffix'
|
|
7
8
|
require 'aemo/nem12/quality_method'
|
|
8
9
|
require 'aemo/nem12/reason_codes'
|
|
@@ -17,6 +18,11 @@ module AEMO
|
|
|
17
18
|
CRLF = "\r\n"
|
|
18
19
|
CSV_SEPARATOR = ','
|
|
19
20
|
|
|
21
|
+
# Backward compatibility: delegate constants to Flag class
|
|
22
|
+
QUALITY_FLAGS = ::AEMO::MeterData::Flag::QUALITY_FLAGS
|
|
23
|
+
METHOD_FLAGS = ::AEMO::MeterData::Flag::METHOD_FLAGS
|
|
24
|
+
REASON_CODES = ::AEMO::MeterData::Flag::REASON_CODES
|
|
25
|
+
|
|
20
26
|
@file_contents = nil
|
|
21
27
|
@header = nil
|
|
22
28
|
@nmi_data_details = []
|
|
@@ -258,17 +264,8 @@ module AEMO
|
|
|
258
264
|
end
|
|
259
265
|
end
|
|
260
266
|
|
|
261
|
-
# Deal with flags
|
|
262
|
-
flag =
|
|
263
|
-
# Based on QualityMethod and ReasonCode
|
|
264
|
-
if csv[intervals_offset + 0].length == 3 || !csv[intervals_offset + 1].nil?
|
|
265
|
-
flag ||= { quality_flag: nil, method_flag: nil, reason_code: nil }
|
|
266
|
-
if csv[intervals_offset + 0].length == 3
|
|
267
|
-
flag[:quality_flag] = csv[intervals_offset + 0][0]
|
|
268
|
-
flag[:method_flag] = csv[intervals_offset + 0][1, 2].to_i
|
|
269
|
-
end
|
|
270
|
-
flag[:reason_code] = csv[intervals_offset + 1].to_i unless csv[intervals_offset + 1].nil?
|
|
271
|
-
end
|
|
267
|
+
# Deal with flags - explicitly set all flag values
|
|
268
|
+
flag = AEMO::MeterData::Flag.from_quality_method_reason_code(quality_method: csv[intervals_offset + 0], reason_code: csv[intervals_offset + 1], validate: strict)
|
|
272
269
|
|
|
273
270
|
# Deal with updated_at & msats_load_at
|
|
274
271
|
updated_at = nil
|
|
@@ -302,7 +299,7 @@ module AEMO
|
|
|
302
299
|
# @param [String] line A single line in string format
|
|
303
300
|
# @param [Boolean] strict
|
|
304
301
|
# @return [Hash] the line parsed into a hash of information
|
|
305
|
-
def parse_nem12_400(line, strict: true) # rubocop:disable
|
|
302
|
+
def parse_nem12_400(line, strict: true) # rubocop:disable Naming/VariableNumber
|
|
306
303
|
csv = line.parse_csv
|
|
307
304
|
raise ArgumentError, 'RecordIndicator is not 400' if csv[0] != '400'
|
|
308
305
|
raise ArgumentError, 'StartInterval is not valid' if csv[1].nil? || csv[1].match(/^\d+$/).nil?
|
|
@@ -318,32 +315,30 @@ module AEMO
|
|
|
318
315
|
|
|
319
316
|
interval_events = []
|
|
320
317
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
# Update with flag details
|
|
343
|
-
@interval_data[interval_start_point + (i - 1)][:flag] = flag
|
|
344
|
-
end
|
|
345
|
-
@interval_events += interval_events
|
|
318
|
+
number_of_intervals = 1440 / @data_details.last[:interval_length]
|
|
319
|
+
interval_start_point = @interval_data.length - number_of_intervals
|
|
320
|
+
|
|
321
|
+
# For each of these
|
|
322
|
+
# Parse reason code - only set if present and not empty
|
|
323
|
+
parsed_reason_code = nil
|
|
324
|
+
parsed_reason_code = csv[4].to_i unless csv[4].nil? || csv[4].empty?
|
|
325
|
+
|
|
326
|
+
base_interval_event = { datetime: nil, quality_method: csv[3], reason_code: parsed_reason_code,
|
|
327
|
+
reason_description: csv[5] }
|
|
328
|
+
|
|
329
|
+
# Interval Numbers are 1-indexed
|
|
330
|
+
((csv[1].to_i)..(csv[2].to_i)).each do |i|
|
|
331
|
+
interval_event = base_interval_event.dup
|
|
332
|
+
interval_event[:datetime] = @interval_data[interval_start_point + (i - 1)][:datetime]
|
|
333
|
+
interval_events << interval_event
|
|
334
|
+
|
|
335
|
+
flag = AEMO::MeterData::Flag.from_quality_method_reason_code(quality_method: interval_event[:quality_method], reason_code: interval_event[:reason_code], validate: strict)
|
|
336
|
+
|
|
337
|
+
# Update with flag details
|
|
338
|
+
@interval_data[interval_start_point + (i - 1)][:flag] = flag
|
|
346
339
|
end
|
|
340
|
+
@interval_events += interval_events
|
|
341
|
+
|
|
347
342
|
interval_events
|
|
348
343
|
end
|
|
349
344
|
|
|
@@ -361,20 +356,6 @@ module AEMO
|
|
|
361
356
|
# @return [Hash] the line parsed into a hash of information
|
|
362
357
|
def parse_nem12_900(_line, strict: true); end # rubocop:disable Naming/VariableNumber
|
|
363
358
|
|
|
364
|
-
# Turns the flag to a string
|
|
365
|
-
#
|
|
366
|
-
# @param [Hash] flag the object of a flag
|
|
367
|
-
# @return [nil, String] a hyphenated string for the flag or nil
|
|
368
|
-
def flag_to_s(flag)
|
|
369
|
-
flag_to_s = []
|
|
370
|
-
unless flag.nil?
|
|
371
|
-
flag_to_s << QUALITY_FLAGS[flag[:quality_flag]] unless QUALITY_FLAGS[flag[:quality_flag]].nil?
|
|
372
|
-
flag_to_s << METHOD_FLAGS[flag[:method_flag]][:short_descriptor] unless METHOD_FLAGS[flag[:method_flag]].nil?
|
|
373
|
-
flag_to_s << REASON_CODES[flag[:reason_code]] unless REASON_CODES[flag[:reason_code]].nil?
|
|
374
|
-
end
|
|
375
|
-
flag_to_s.empty? ? nil : flag_to_s.join(' - ')
|
|
376
|
-
end
|
|
377
|
-
|
|
378
359
|
# @return [Array] array of a NEM12 file a given Meter + Data Stream for easy reading
|
|
379
360
|
def to_a
|
|
380
361
|
@interval_data.map do |d|
|
|
@@ -384,7 +365,7 @@ module AEMO
|
|
|
384
365
|
d[:data_details][:uom],
|
|
385
366
|
d[:datetime],
|
|
386
367
|
d[:value],
|
|
387
|
-
|
|
368
|
+
d[:flag]&.to_s
|
|
388
369
|
]
|
|
389
370
|
end
|
|
390
371
|
end
|
|
@@ -462,20 +443,36 @@ module AEMO
|
|
|
462
443
|
end
|
|
463
444
|
daily_datas.keys.sort.each do |key|
|
|
464
445
|
daily_data = daily_datas[key].sort_by { |x| x[:datetime] }
|
|
465
|
-
|
|
446
|
+
|
|
447
|
+
# Check if all intervals have identical flags
|
|
448
|
+
# If so, use that flag's quality method in the 300 record
|
|
449
|
+
# Otherwise use 'V' (variable data) and generate 400 records
|
|
450
|
+
first_flag = daily_data.first[:flag]
|
|
451
|
+
all_same_flag = daily_data.all? { |x| x[:flag] == first_flag }
|
|
452
|
+
|
|
453
|
+
if all_same_flag
|
|
454
|
+
# All intervals have the same flag - use it in the 300 record
|
|
455
|
+
quality_method = first_flag&.to_quality_method || ''
|
|
456
|
+
reason_code = first_flag&.reason_code || ''
|
|
457
|
+
else
|
|
458
|
+
# Intervals have different flags - use 'V' and generate 400 records
|
|
459
|
+
quality_method = 'V'
|
|
460
|
+
reason_code = ''
|
|
461
|
+
end
|
|
466
462
|
|
|
467
463
|
lines << [
|
|
468
464
|
'300',
|
|
469
465
|
key,
|
|
470
466
|
daily_data.map { |x| x[:value] },
|
|
471
|
-
|
|
472
|
-
|
|
467
|
+
quality_method,
|
|
468
|
+
reason_code,
|
|
473
469
|
'',
|
|
474
470
|
daily_data.first[:updated_at] ? AEMO::Time.format_timestamp14(daily_data.first[:updated_at]) : nil,
|
|
475
471
|
daily_data.first[:msats_load_at] ? AEMO::Time.format_timestamp14(daily_data.first[:msats_load_at]) : nil
|
|
476
472
|
].flatten.join(CSV_SEPARATOR)
|
|
477
473
|
|
|
478
|
-
|
|
474
|
+
# Generate 400 records only if we have variable data
|
|
475
|
+
next if all_same_flag
|
|
479
476
|
|
|
480
477
|
lines << to_nem12_400_csv(daily_data:)
|
|
481
478
|
end
|
|
@@ -504,12 +501,25 @@ module AEMO
|
|
|
504
501
|
end
|
|
505
502
|
|
|
506
503
|
nem12_400_rows.map do |row|
|
|
504
|
+
flag = row[:flag]
|
|
505
|
+
# Default to 'A' if flag is nil, otherwise use explicit quality flag
|
|
506
|
+
quality_flag = flag.nil? ? 'A' : flag.quality_flag
|
|
507
|
+
method_flag = flag&.method_flag
|
|
508
|
+
reason_code = flag&.reason_code
|
|
509
|
+
|
|
510
|
+
# Build quality method string (quality flag + optional method flag)
|
|
511
|
+
quality_method = if method_flag.nil?
|
|
512
|
+
quality_flag
|
|
513
|
+
else
|
|
514
|
+
"#{quality_flag}#{format('%02d', method_flag)}"
|
|
515
|
+
end
|
|
516
|
+
|
|
507
517
|
[
|
|
508
518
|
'400',
|
|
509
519
|
row[:start_index],
|
|
510
520
|
row[:finish_index],
|
|
511
|
-
|
|
512
|
-
|
|
521
|
+
quality_method,
|
|
522
|
+
reason_code || '',
|
|
513
523
|
''
|
|
514
524
|
].join(CSV_SEPARATOR)
|
|
515
525
|
end.join(CRLF)
|
data/lib/aemo/version.rb
CHANGED
data/lib/data/xml_to_json.rb
CHANGED
|
@@ -40,39 +40,39 @@ end
|
|
|
40
40
|
# Now to create the DLF and TNI output JSON files for use
|
|
41
41
|
@files.select { |x| ['aemo-tni.xml', 'aemo-dlf.xml'].include?(x) }
|
|
42
42
|
.each do |file|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
output_file = file.gsub('.xml', '.json')
|
|
44
|
+
output_data = {}
|
|
45
|
+
open_file = File.open(File.join(@path, file))
|
|
46
|
+
xml = Nokogiri::XML(open_file) do |c|
|
|
47
|
+
c.options = Nokogiri::XML::ParseOptions::NOBLANKS
|
|
48
|
+
end
|
|
49
|
+
open_file.close
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
xml.xpath('//Row').each do |row|
|
|
52
|
+
row_children = row.children
|
|
53
|
+
code = row_children.find { |x| x.name == 'Code' }.children.first.text
|
|
54
|
+
output_data[code] ||= []
|
|
55
|
+
output_data_instance = {}
|
|
56
|
+
row_children.each do |row_child|
|
|
57
|
+
output_data_instance[row_child.name] = row_child.children.first.text
|
|
58
|
+
end
|
|
59
|
+
if file =~ /tni/
|
|
60
|
+
puts "output_data_instance: #{output_data_instance.inspect}"
|
|
61
|
+
output_data_instance[:mlf_data] = {}
|
|
62
|
+
unless @mlf_data[code].nil?
|
|
63
|
+
output_data_instance[:mlf_data] = @mlf_data[code].deep_dup
|
|
64
|
+
output_data_instance[:mlf_data][:loss_factors].reject! do |x|
|
|
65
|
+
Time.parse(output_data_instance['ToDate']) < x[:start] ||
|
|
66
|
+
Time.parse(output_data_instance['FromDate']) >= x[:finish]
|
|
67
|
+
end
|
|
68
|
+
puts 'output_data_instance[:mlf_data][:loss_factors]: ' \
|
|
69
|
+
"#{output_data_instance[:mlf_data][:loss_factors].inspect}"
|
|
70
|
+
end
|
|
71
|
+
elsif file =~ /dlf/
|
|
72
|
+
output_data_instance[:nsp_code] = @dlf_data[code]
|
|
73
|
+
end
|
|
74
|
+
output_data[code] << output_data_instance
|
|
67
75
|
end
|
|
68
|
-
puts 'output_data_instance[:mlf_data][:loss_factors]: ' \
|
|
69
|
-
"#{output_data_instance[:mlf_data][:loss_factors].inspect}"
|
|
70
|
-
end
|
|
71
|
-
elsif file =~ /dlf/
|
|
72
|
-
output_data_instance[:nsp_code] = @dlf_data[code]
|
|
73
|
-
end
|
|
74
|
-
output_data[code] << output_data_instance
|
|
75
|
-
end
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
File.write(File.join(@path, output_file), output_data.to_json)
|
|
78
78
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
100,NEM12,200505181432,ENOSI,NEMMCO
|
|
2
|
+
200,4123456789,E1B1,E1,E1,E1,B1234567,KWH,5,20260211
|
|
3
|
+
300,20251227,0.5120,0.4930,0.4170,0.3730,0.3850,0.4170,0.3900,0.4020,0.3910,0.3730,0.4830,0.5080,0.4060,0.4040,0.3640,0.3780,0.3070,0.2620,0.3280,0.2940,0.2930,0.3320,0.2960,0.3290,0.3060,0.2550,0.2920,0.2890,0.4040,0.4200,0.4030,0.4130,0.3780,0.3430,0.4050,0.3440,0.3590,0.3320,0.3050,0.3180,0.2850,0.2770,0.3250,0.2960,0.3140,0.3390,0.2970,0.3240,0.3060,0.3010,0.3900,0.3250,0.3110,0.3310,0.3200,0.3810,0.3060,0.2720,0.3150,0.2980,0.2760,0.2470,0.2160,0.3190,0.3180,0.3060,0.3310,0.2980,0.3140,0.3180,0.2420,0.2760,0.2570,0.2330,0.2700,0.2430,0.2530,0.2610,0.2200,0.3910,0.3660,0.3130,0.2800,0.2280,0.2330,0.2430,0.1850,0.2040,0.2230,0.1830,0.1930,0.1150,0.0730,0.0940,0.0230,0.0070,0.0040,0.0040,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0320,0.0090,0.1130,0.0280,0.0230,0.0000,0.0000,0.0040,0.0010,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.1710,0.0000,0.0000,0.1780,0.0000,0.0000,0.1930,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0190,0.0020,0.0000,0.0000,0.0000,0.0050,0.0140,0.0220,0.0130,0.0640,0.0440,0.0700,0.2380,0.1860,0.1960,0.2000,0.1410,0.0580,0.0170,0.0030,0.0170,0.0750,0.0370,0.0820,0.1060,0.1000,0.1240,0.0900,0.1430,0.1460,0.1510,0.2060,0.1830,0.2180,0.2280,0.1970,0.2380,0.2190,0.1940,0.2420,0.2130,0.2200,0.2300,0.1940,0.2660,0.4730,0.4500,0.5530,0.5600,0.5680,0.6040,0.5620,0.5870,0.6150,0.5770,0.6100,0.6000,0.5710,0.6140,0.5810,0.5820,0.6090,0.5720,0.6590,0.6520,0.7140,0.8750,0.7740,0.8690,0.9730,0.9240,1.0040,0.9260,0.9540,0.9420,0.9540,0.9040,0.9130,0.8790,0.9200,0.8610,0.8650,0.9030,0.8840,0.9090,0.8910,0.7720,0.7800,0.7550,0.7470,0.8830,0.8760,0.8310,0.7710,0.7040,0.7240,0.7540,0.8530,0.8490,0.7540,0.7180,V,,,20251228011206,
|
|
4
|
+
400,1,60,A,,
|
|
5
|
+
400,61,61,A,79,
|
|
6
|
+
400,62,288,A,,
|
|
7
|
+
500,O,,20251228000000,079499.641
|
|
8
|
+
200,4123456789,E1B1,B1,B1,B1,B1234567,KWH,5,20260211
|
|
9
|
+
300,20251227,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0170,0.0400,0.0540,0.1690,0.1730,0.2440,0.2320,0.2400,0.3070,0.2730,0.0240,0.0480,0.0170,0.0340,0.1590,0.3290,0.4210,0.1430,0.1240,0.1670,0.2100,0.3860,0.3710,0.2060,0.1680,0.3940,0.4270,0.3920,0.4480,0.4470,0.4280,0.4480,0.4390,0.4310,0.0640,0.3180,0.4450,0.0430,0.3560,0.4490,0.0590,0.3600,0.5000,0.4820,0.5650,0.5720,0.5390,0.5650,0.5430,0.5470,0.5780,0.5330,0.5690,0.5760,0.5530,0.6200,0.5850,0.5880,0.5870,0.5780,0.6130,0.6050,0.5490,0.6050,0.5570,0.4580,0.4770,0.5630,0.5390,0.5510,0.5830,0.4430,0.5850,0.5460,0.5410,0.5600,0.4840,0.4970,0.4790,0.3430,0.1860,0.2020,0.1000,0.0410,0.1300,0.1250,0.1820,0.1490,0.2100,0.1510,0.1440,0.1530,0.0960,0.0860,0.0860,0.0540,0.0810,0.0150,0.0500,0.0150,0.0180,0.0140,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0210,0.0150,0.0410,0.0290,0.0220,0.0000,0.0040,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,V,,,20251228011206,
|
|
10
|
+
400,1,60,A,,
|
|
11
|
+
400,61,61,A,79,
|
|
12
|
+
400,62,288,A,,
|
|
13
|
+
500,O,,20251228000000,027575.424
|
|
14
|
+
900
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe AEMO::MeterData::Flag do
|
|
6
|
+
describe '.new' do
|
|
7
|
+
it 'creates a flag with all attributes' do
|
|
8
|
+
flag = described_class.new(quality_flag: 'E', method_flag: 52, reason_code: 1)
|
|
9
|
+
expect(flag.quality_flag).to eq('E')
|
|
10
|
+
expect(flag.method_flag).to eq(52)
|
|
11
|
+
expect(flag.reason_code).to eq(1)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'creates a flag with nil attributes' do
|
|
15
|
+
flag = described_class.new
|
|
16
|
+
expect(flag.quality_flag).to be_nil
|
|
17
|
+
expect(flag.method_flag).to be_nil
|
|
18
|
+
expect(flag.reason_code).to be_nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'validates quality flag' do
|
|
22
|
+
expect { described_class.new(quality_flag: 'X') }.to raise_error(ArgumentError, /Invalid quality flag/)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'validates method flag' do
|
|
26
|
+
expect { described_class.new(method_flag: 999) }.to raise_error(ArgumentError, /Invalid method flag/)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'validates reason code' do
|
|
30
|
+
expect { described_class.new(reason_code: 999) }.to raise_error(ArgumentError, /Invalid reason code/)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'validates flag combinations' do
|
|
34
|
+
expect { described_class.new(quality_flag: 'E') }.to raise_error(ArgumentError, /requires a method flag/)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '.from_hash' do
|
|
39
|
+
it 'creates a flag from a hash' do
|
|
40
|
+
flag = described_class.from_hash({ quality_flag: 'E', method_flag: 52, reason_code: 1 })
|
|
41
|
+
expect(flag.quality_flag).to eq('E')
|
|
42
|
+
expect(flag.method_flag).to eq(52)
|
|
43
|
+
expect(flag.reason_code).to eq(1)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'returns nil for nil input' do
|
|
47
|
+
expect(described_class.from_hash(nil)).to be_nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'returns the flag if already a Flag instance' do
|
|
51
|
+
flag = described_class.new(quality_flag: 'A')
|
|
52
|
+
expect(described_class.from_hash(flag)).to eq(flag)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '.normalize' do
|
|
57
|
+
it 'normalizes nil to actual data flag' do
|
|
58
|
+
flag = described_class.normalize(nil)
|
|
59
|
+
expect(flag.quality_flag).to eq('A')
|
|
60
|
+
expect(flag.method_flag).to be_nil
|
|
61
|
+
expect(flag.reason_code).to be_nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'removes method flag from A quality flag' do
|
|
65
|
+
flag = described_class.normalize({ quality_flag: 'A', method_flag: 52 })
|
|
66
|
+
expect(flag.quality_flag).to eq('A')
|
|
67
|
+
expect(flag.method_flag).to be_nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'removes method flag and reason code from V quality flag' do
|
|
71
|
+
flag = described_class.normalize({ quality_flag: 'V', method_flag: 52, reason_code: 1 })
|
|
72
|
+
expect(flag.quality_flag).to eq('V')
|
|
73
|
+
expect(flag.method_flag).to be_nil
|
|
74
|
+
expect(flag.reason_code).to be_nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'preserves E quality flag with method flag' do
|
|
78
|
+
flag = described_class.normalize({ quality_flag: 'E', method_flag: 52 })
|
|
79
|
+
expect(flag.quality_flag).to eq('E')
|
|
80
|
+
expect(flag.method_flag).to eq(52)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe '#to_quality_method' do
|
|
85
|
+
it 'returns A for nil quality flag' do
|
|
86
|
+
flag = described_class.new
|
|
87
|
+
expect(flag.to_quality_method).to eq('A')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'returns quality flag when method flag is nil' do
|
|
91
|
+
flag = described_class.new(quality_flag: 'A')
|
|
92
|
+
expect(flag.to_quality_method).to eq('A')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'returns quality flag with formatted method flag' do
|
|
96
|
+
flag = described_class.new(quality_flag: 'E', method_flag: 52)
|
|
97
|
+
expect(flag.to_quality_method).to eq('E52')
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe '#to_s' do
|
|
102
|
+
it 'returns nil for nil flag' do
|
|
103
|
+
flag = described_class.new
|
|
104
|
+
expect(flag.to_s).to be_nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'returns quality flag description' do
|
|
108
|
+
flag = described_class.new(quality_flag: 'A')
|
|
109
|
+
expect(flag.to_s).to eq('Actual Data')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'returns quality flag and method flag description' do
|
|
113
|
+
flag = described_class.new(quality_flag: 'E', method_flag: 52)
|
|
114
|
+
expect(flag.to_s).to eq('Forward Estimated Data - Previous Read')
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'returns quality flag, method flag, and reason code description' do
|
|
118
|
+
flag = described_class.new(quality_flag: 'E', method_flag: 52, reason_code: 1)
|
|
119
|
+
expect(flag.to_s).to eq('Forward Estimated Data - Previous Read - Meter/Equipment Changed')
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
describe '#to_h' do
|
|
124
|
+
it 'returns a hash representation' do
|
|
125
|
+
flag = described_class.new(quality_flag: 'E', method_flag: 52, reason_code: 1)
|
|
126
|
+
expect(flag.to_h).to eq({ quality_flag: 'E', method_flag: 52, reason_code: 1 })
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe '#==' do
|
|
131
|
+
it 'compares two flags' do
|
|
132
|
+
flag1 = described_class.new(quality_flag: 'E', method_flag: 52, reason_code: 1)
|
|
133
|
+
flag2 = described_class.new(quality_flag: 'E', method_flag: 52, reason_code: 1)
|
|
134
|
+
expect(flag1).to eq(flag2)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'compares flag with hash' do
|
|
138
|
+
flag = described_class.new(quality_flag: 'E', method_flag: 52, reason_code: 1)
|
|
139
|
+
expect(flag).to eq({ quality_flag: 'E', method_flag: 52, reason_code: 1 })
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/spec/lib/aemo/nem12_spec.rb
CHANGED
|
@@ -80,7 +80,8 @@ describe AEMO::NEM12 do
|
|
|
80
80
|
'200,NEM1201002,E1E2,E1,E1,N1,01002,KWH,30,',
|
|
81
81
|
'300,20050318,315.15,313.8,296.55,298.5,295.2,298.95,300.75,322.95,330.45,350.7,345.75,346.95,345.9,348.6,300.15,337.5,336.75,345.9,330.45,327.15,334.8,345.75,335.85,320.1,325.5,325.2,326.4,330.6,332.7,332.25,321.0,316.5,299.85,302.4,301.05,263.85,255.45,142.05,138.3,138.3,136.8,138.3,136.05,135.75,135.75,136.65,136.05,130.8,A,,,20050319014041,',
|
|
82
82
|
'200,NEM1201002,E1E2,E2,E2,N2,01002,KWH,30,',
|
|
83
|
-
'300,20050318,103.05,98.85,81.15,75.6,72.15,73.95,74.55,81.15,89.25,124.2,125.7,128.7,136.8,151.05,177.9,174.45,204.0,210.15,180.15,164.4,187.95,211.95,193.8,122.85,124.8,121.2,129.3,131.25,130.95,130.05,118.05,105.75,77.1,75.9,75.0,51.15,44.4,12.3,12.75,12.15,12.45,11.55,14.4,14.55,15.0,14.7,15.75,21.9,A,,,20050319014041,',
|
|
83
|
+
'300,20050318,103.05,98.85,81.15,75.6,72.15,73.95,74.55,81.15,89.25,124.2,125.7,128.7,136.8,151.05,177.9,174.45,204.0,210.15,180.15,164.4,187.95,211.95,193.8,122.85,124.8,121.2,129.3,131.25,130.95,130.05,118.05,105.75,77.1,75.9,75.0,51.15,44.4,12.3,12.75,12.15,12.45,11.55,14.4,14.55,15.0,14.7,15.75,21.9,A,,,20050319014041,',
|
|
84
|
+
'900',
|
|
84
85
|
''
|
|
85
86
|
].join("\r\n")
|
|
86
87
|
end
|
|
@@ -106,11 +107,9 @@ describe AEMO::NEM12 do
|
|
|
106
107
|
'400,1,10,F52,71,',
|
|
107
108
|
'400,11,48,E52,,',
|
|
108
109
|
'200,NEM1204062,E1,E1,E1,N1,04062,KWH,30,20050503',
|
|
109
|
-
'300,20040528,0.68,0.653,0.62,0.623,0.618,0.625,0.613,0.623,0.618,0.615,0.613,0.76,0.665,0.638,0.61,0.648,0.65,0.645,0.895,0.668,0.645,0.648,0.655,0.73,0.695,0.67,0.638,0.643,0.64,0.723,0.653,0.645,0.633,0.71,0.683,0.648,0.625,0.63,0.625,0.63,0.638,0.635,0.633,0.638,0.673,0.765,0.65,0.628,
|
|
110
|
-
'400,1,48,E52,,',
|
|
110
|
+
'300,20040528,0.68,0.653,0.62,0.623,0.618,0.625,0.613,0.623,0.618,0.615,0.613,0.76,0.665,0.638,0.61,0.648,0.65,0.645,0.895,0.668,0.645,0.648,0.655,0.73,0.695,0.67,0.638,0.643,0.64,0.723,0.653,0.645,0.633,0.71,0.683,0.648,0.625,0.63,0.625,0.63,0.638,0.635,0.633,0.638,0.673,0.765,0.65,0.628,E52,,,20040609000001,',
|
|
111
111
|
'200,NEM1204062,E1,E1,E1,N1,04062,KWH,30,20050503',
|
|
112
|
-
'300,20040529,0.633,0.613,0.628,0.618,0.625,0.623,0.623,0.613,0.655,0.663,0.645,0.708,0.608,0.618,0.63,0.625,0.62,0.635,0.63,0.638,0.693,0.71,0.683,0.645,0.638,0.653,0.653,0.648,0.655,0.745,0.69,0.695,0.68,0.643,0.645,0.635,0.628,0.625,0.635,0.628,0.673,0.688,0.685,0.66,0.638,0.718,0.638,0.63,
|
|
113
|
-
'400,1,48,E52,,',
|
|
112
|
+
'300,20040529,0.633,0.613,0.628,0.618,0.625,0.623,0.623,0.613,0.655,0.663,0.645,0.708,0.608,0.618,0.63,0.625,0.62,0.635,0.63,0.638,0.693,0.71,0.683,0.645,0.638,0.653,0.653,0.648,0.655,0.745,0.69,0.695,0.68,0.643,0.645,0.635,0.628,0.625,0.635,0.628,0.673,0.688,0.685,0.66,0.638,0.718,0.638,0.63,E52,,,20040609000001,',
|
|
114
113
|
'900',
|
|
115
114
|
''
|
|
116
115
|
].join("\r\n")
|
|
@@ -136,9 +135,9 @@ describe AEMO::NEM12 do
|
|
|
136
135
|
Dir.entries(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'NEM12'))
|
|
137
136
|
.reject { |f| %w[. .. .DS_Store].include?(f) }
|
|
138
137
|
.each do |file|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
described_class.parse_nem12_file(fixture(File.join('NEM12', file))).each do |nem12|
|
|
139
|
+
expect(nem12.nmi_identifier).to be_a String
|
|
140
|
+
end
|
|
142
141
|
end
|
|
143
142
|
end
|
|
144
143
|
end
|
|
@@ -173,6 +172,31 @@ describe AEMO::NEM12 do
|
|
|
173
172
|
nem12_empty_cells_300_record = fixture(File.join('NEM12-Errors', 'NEM12#EmptyCells300Record#CNRGYMDP#NEMMCO.csv'))
|
|
174
173
|
expect { described_class.parse_nem12_file(nem12_empty_cells_300_record) }.to raise_error(ArgumentError)
|
|
175
174
|
end
|
|
175
|
+
|
|
176
|
+
it 'parses actual data flags explicitly' do
|
|
177
|
+
nem12_filepath = fixture(File.join('NEM12', 'NEM12#000000000000001#CNRGYMDP#NEMMCO.csv'))
|
|
178
|
+
nem12s = described_class.parse_nem12_file(nem12_filepath)
|
|
179
|
+
expect(nem12s.first.interval_data.first[:flag].quality_flag).to eq('A')
|
|
180
|
+
expect(nem12s.first.interval_data.first[:flag].method_flag).to be_nil
|
|
181
|
+
expect(nem12s.first.interval_data.first[:flag].reason_code).to be_nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'parses substituted data flags with method flags' do
|
|
185
|
+
nem12_filepath = fixture(File.join('NEM12', 'NEM12#000000000000004#CNRGYMDP#NEMMCO.csv'))
|
|
186
|
+
nem12s = described_class.parse_nem12_file(nem12_filepath)
|
|
187
|
+
# First 10 intervals have F52 flag
|
|
188
|
+
expect(nem12s.first.interval_data.first[:flag].quality_flag).to eq('F')
|
|
189
|
+
expect(nem12s.first.interval_data.first[:flag].method_flag).to eq(52)
|
|
190
|
+
expect(nem12s.first.interval_data.first[:flag].reason_code).to eq(71)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'parses estimated data flags' do
|
|
194
|
+
nem12_filepath = fixture(File.join('NEM12', 'NEM12#000000000000004#CNRGYMDP#NEMMCO.csv'))
|
|
195
|
+
nem12s = described_class.parse_nem12_file(nem12_filepath)
|
|
196
|
+
# Intervals 11-48 have E52 flag
|
|
197
|
+
expect(nem12s.first.interval_data[10][:flag].quality_flag).to eq('E')
|
|
198
|
+
expect(nem12s.first.interval_data[10][:flag].method_flag).to eq(52)
|
|
199
|
+
end
|
|
176
200
|
end
|
|
177
201
|
|
|
178
202
|
describe '#parse_nem12_400' do
|
|
@@ -180,14 +204,36 @@ describe AEMO::NEM12 do
|
|
|
180
204
|
nem12_empty_cells_400_record = fixture(File.join('NEM12-Errors', 'NEM12#EmptyCells400Record#CNRGYMDP#NEMMCO.csv'))
|
|
181
205
|
expect { described_class.parse_nem12_file(nem12_empty_cells_400_record) }.to raise_error(ArgumentError)
|
|
182
206
|
end
|
|
207
|
+
|
|
208
|
+
it 'parses 400 records and updates interval flags' do
|
|
209
|
+
nem12_filepath = fixture(File.join('NEM12', 'NEM12#000000000000004#CNRGYMDP#NEMMCO.csv'))
|
|
210
|
+
nem12s = described_class.parse_nem12_file(nem12_filepath)
|
|
211
|
+
# Verify that 400 records override the V flag from 300 record
|
|
212
|
+
expect(nem12s.first.interval_data.first[:flag].quality_flag).to eq('F')
|
|
213
|
+
expect(nem12s.first.interval_data[10][:flag].quality_flag).to eq('E')
|
|
214
|
+
end
|
|
183
215
|
end
|
|
184
216
|
|
|
185
|
-
describe '
|
|
186
|
-
it '
|
|
187
|
-
|
|
217
|
+
describe 'flag handling edge cases' do
|
|
218
|
+
it 'handles actual data with reason code' do
|
|
219
|
+
nem12_filepath = fixture(File.join('NEM12', 'NEM12.actual_with_reason_code.csv'))
|
|
220
|
+
nem12s = described_class.parse_nem12_file(nem12_filepath)
|
|
221
|
+
# Check that actual data with reason code is parsed correctly
|
|
222
|
+
interval_with_reason = nem12s.first.interval_data.find { |x| x[:flag].reason_code == 79 }
|
|
223
|
+
expect(interval_with_reason).not_to be_nil
|
|
224
|
+
expect(interval_with_reason[:flag].quality_flag).to eq('A')
|
|
225
|
+
expect(interval_with_reason[:flag].method_flag).to be_nil
|
|
226
|
+
expect(interval_with_reason[:flag].reason_code).to eq(79)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it 'maintains backward compatibility with nil flags' do
|
|
188
230
|
nem12 = described_class.new('NEEE000010')
|
|
189
|
-
|
|
190
|
-
|
|
231
|
+
nem12.instance_variable_set(:@data_details, [{ interval_length: 30 }])
|
|
232
|
+
nem12.instance_variable_set(:@interval_data, [
|
|
233
|
+
{ flag: nil, value: 1.0, datetime: Time.now, data_details: { interval_length: 30 } }
|
|
234
|
+
])
|
|
235
|
+
# Should not raise error
|
|
236
|
+
expect { nem12.to_nem12_300_csv }.not_to raise_error
|
|
191
237
|
end
|
|
192
238
|
end
|
|
193
239
|
|
|
@@ -210,4 +256,104 @@ describe AEMO::NEM12 do
|
|
|
210
256
|
expect(nem12.to_nem12_csv).to eq(expected)
|
|
211
257
|
end
|
|
212
258
|
end
|
|
259
|
+
|
|
260
|
+
describe '#to_nem12_300_csv with uniform flags' do
|
|
261
|
+
let(:nem12) { described_class.new('NEEE000010') }
|
|
262
|
+
let(:base_time) { Time.parse('2024-01-01 00:30:00 +1000') }
|
|
263
|
+
|
|
264
|
+
before do
|
|
265
|
+
nem12.instance_variable_set(:@data_details, [{ interval_length: 30 }])
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'uses quality method from uniform actual data flags without 400 records' do
|
|
269
|
+
intervals = (0...48).map do |i|
|
|
270
|
+
{
|
|
271
|
+
flag: AEMO::MeterData::Flag.new(quality_flag: 'A', method_flag: nil, reason_code: nil),
|
|
272
|
+
value: 1.0 + i,
|
|
273
|
+
datetime: base_time + (i * 30 * 60),
|
|
274
|
+
data_details: { interval_length: 30 }
|
|
275
|
+
}
|
|
276
|
+
end
|
|
277
|
+
nem12.instance_variable_set(:@interval_data, intervals)
|
|
278
|
+
|
|
279
|
+
output = nem12.to_nem12_300_csv
|
|
280
|
+
expect(output).to include('300,20240101')
|
|
281
|
+
expect(output).to include(',A,,,')
|
|
282
|
+
expect(output).not_to include('400,')
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it 'uses quality method from uniform estimated data flags without 400 records' do
|
|
286
|
+
intervals = (0...48).map do |i|
|
|
287
|
+
{
|
|
288
|
+
flag: AEMO::MeterData::Flag.new(quality_flag: 'E', method_flag: 52, reason_code: nil),
|
|
289
|
+
value: 1.0 + i,
|
|
290
|
+
datetime: base_time + (i * 30 * 60),
|
|
291
|
+
data_details: { interval_length: 30 }
|
|
292
|
+
}
|
|
293
|
+
end
|
|
294
|
+
nem12.instance_variable_set(:@interval_data, intervals)
|
|
295
|
+
|
|
296
|
+
output = nem12.to_nem12_300_csv
|
|
297
|
+
expect(output).to include('300,20240101')
|
|
298
|
+
expect(output).to include(',E52,,,')
|
|
299
|
+
expect(output).not_to include('400,')
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
it 'uses quality method from uniform substituted data flags with reason code' do
|
|
303
|
+
intervals = (0...48).map do |i|
|
|
304
|
+
{
|
|
305
|
+
flag: AEMO::MeterData::Flag.new(quality_flag: 'S', method_flag: 11, reason_code: 53),
|
|
306
|
+
value: 1.0 + i,
|
|
307
|
+
datetime: base_time + (i * 30 * 60),
|
|
308
|
+
data_details: { interval_length: 30 }
|
|
309
|
+
}
|
|
310
|
+
end
|
|
311
|
+
nem12.instance_variable_set(:@interval_data, intervals)
|
|
312
|
+
|
|
313
|
+
output = nem12.to_nem12_300_csv
|
|
314
|
+
expect(output).to include('300,20240101')
|
|
315
|
+
expect(output).to include(',S11,53,')
|
|
316
|
+
expect(output).not_to include('400,')
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
it 'uses V and generates 400 records when flags differ' do
|
|
320
|
+
intervals = (0...48).map do |i|
|
|
321
|
+
flag = if i < 10
|
|
322
|
+
AEMO::MeterData::Flag.new(quality_flag: 'F', method_flag: 52, reason_code: 71)
|
|
323
|
+
else
|
|
324
|
+
AEMO::MeterData::Flag.new(quality_flag: 'E', method_flag: 52, reason_code: nil)
|
|
325
|
+
end
|
|
326
|
+
{
|
|
327
|
+
flag:,
|
|
328
|
+
value: 1.0 + i,
|
|
329
|
+
datetime: base_time + (i * 30 * 60),
|
|
330
|
+
data_details: { interval_length: 30 }
|
|
331
|
+
}
|
|
332
|
+
end
|
|
333
|
+
nem12.instance_variable_set(:@interval_data, intervals)
|
|
334
|
+
|
|
335
|
+
output = nem12.to_nem12_300_csv
|
|
336
|
+
expect(output).to include('300,20240101')
|
|
337
|
+
expect(output).to include(',V,,,')
|
|
338
|
+
expect(output).to include('400,1,10,F52,71,')
|
|
339
|
+
expect(output).to include('400,11,48,E52,,')
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
it 'handles nil flags as nil data' do
|
|
343
|
+
intervals = (0...48).map do |i|
|
|
344
|
+
{
|
|
345
|
+
flag: nil,
|
|
346
|
+
value: 1.0 + i,
|
|
347
|
+
datetime: base_time + (i * 30 * 60),
|
|
348
|
+
data_details: { interval_length: 30 }
|
|
349
|
+
}
|
|
350
|
+
end
|
|
351
|
+
nem12.instance_variable_set(:@interval_data, intervals)
|
|
352
|
+
|
|
353
|
+
output = nem12.to_nem12_300_csv
|
|
354
|
+
expect(output).to include('300,20240101')
|
|
355
|
+
expect(output).to include(',,,,')
|
|
356
|
+
expect(output).not_to include('400,')
|
|
357
|
+
end
|
|
358
|
+
end
|
|
213
359
|
end
|