aemo 0.7.1 → 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.
@@ -1,110 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AEMO
4
- # Namespace for classes and modules that handle AEMO Gem NEM12 interactions
5
- # @since 0.1.4
6
- class NEM12
7
- REASON_CODES = {
8
- 0 => 'Free Text Description',
9
- 1 => 'Meter/Equipment Changed',
10
- 2 => 'Extreme Weather/Wet',
11
- 3 => 'Quarantine',
12
- 4 => 'Savage Dog',
13
- 5 => 'Meter/Equipment Changed',
14
- 6 => 'Extreme Weather/Wet',
15
- 7 => 'Unable To Locate Meter',
16
- 8 => 'Vacant Premise',
17
- 9 => 'Meter/Equipment Changed',
18
- 10 => 'Lock Damaged/Seized',
19
- 11 => 'In Wrong Walk',
20
- 12 => 'Locked Premises',
21
- 13 => 'Locked Gate',
22
- 14 => 'Locked Meter Box',
23
- 15 => 'Access - Overgrown',
24
- 16 => 'Noxious Weeds',
25
- 17 => 'Unsafe Equipment/Location',
26
- 18 => 'Read Below Previous',
27
- 19 => 'Consumer Wanted',
28
- 20 => 'Damaged Equipment/Panel',
29
- 21 => 'Switched Off',
30
- 22 => 'Meter/Equipment Seals Missing',
31
- 23 => 'Meter/Equipment Seals Missing',
32
- 24 => 'Meter/Equipment Seals Missing',
33
- 25 => 'Meter/Equipment Seals Missing',
34
- 26 => 'Meter/Equipment Seals Missing',
35
- 27 => 'Meter/Equipment Seals Missing',
36
- 28 => 'Damaged Equipment/Panel',
37
- 29 => 'Relay Faulty/Damaged',
38
- 30 => 'Meter Stop Switch On',
39
- 31 => 'Meter/Equipment Seals Missing',
40
- 32 => 'Damaged Equipment/Panel',
41
- 33 => 'Relay Faulty/Damaged',
42
- 34 => 'Meter Not In Handheld',
43
- 35 => 'Timeswitch Faulty/Reset Required',
44
- 36 => 'Meter High/Ladder Required',
45
- 37 => 'Meter High/Ladder Required',
46
- 38 => 'Unsafe Equipment/Location',
47
- 39 => 'Reverse Energy Observed',
48
- 40 => 'Timeswitch Faulty/Reset Required',
49
- 41 => 'Faulty Equipment Display/Dials',
50
- 42 => 'Faulty Equipment Display/Dials',
51
- 43 => 'Power Outage',
52
- 44 => 'Unsafe Equipment/Location',
53
- 45 => 'Readings Failed To Validate',
54
- 46 => 'Extreme Weather/Hot',
55
- 47 => 'Refused Access',
56
- 48 => 'Timeswitch Faulty/Reset Required',
57
- 49 => 'Wet Paint',
58
- 50 => 'Wrong Tariff',
59
- 51 => 'Installation Demolished',
60
- 52 => 'Access - Blocked',
61
- 53 => 'Bees/Wasp In Meter Box',
62
- 54 => 'Meter Box Damaged/Faulty',
63
- 55 => 'Faulty Equipment Display/Dials',
64
- 56 => 'Meter Box Damaged/Faulty',
65
- 57 => 'Timeswitch Faulty/Reset Required',
66
- 58 => 'Meter Ok - Supply Failure',
67
- 59 => 'Faulty Equipment Display/Dials',
68
- 60 => 'Illegal Connection/Equipment Tampered',
69
- 61 => 'Meter Box Damaged/Faulty',
70
- 62 => 'Damaged Equipment/Panel',
71
- 63 => 'Illegal Connection/Equipment Tampered',
72
- 64 => 'Key Required',
73
- 65 => 'Wrong Key Provided',
74
- 66 => 'Lock Damaged/Seized',
75
- 67 => 'Extreme Weather/Wet',
76
- 68 => 'Zero Consumption',
77
- 69 => 'Reading Exceeds Estimate',
78
- 70 => 'Probe Reports Tampering',
79
- 71 => 'Probe Read Error',
80
- 72 => 'Meter/Equipment Changed',
81
- 73 => 'Low Consumption',
82
- 74 => 'High Consumption',
83
- 75 => 'Customer Read',
84
- 76 => 'Communications Fault',
85
- 77 => 'Estimation Forecast',
86
- 78 => 'Null Data',
87
- 79 => 'Power Outage Alarm',
88
- 80 => 'Short Interval Alarm',
89
- 81 => 'Long Interval Alarm',
90
- 82 => 'CRC Error',
91
- 83 => 'RAM Checksum Error',
92
- 84 => 'ROM Checksum Error',
93
- 85 => 'Data Missing Alarm',
94
- 86 => 'Clock Error Alarm',
95
- 87 => 'Reset Occurred',
96
- 88 => 'Watchdog Timeout Alarm',
97
- 89 => 'Time Reset Occurred',
98
- 90 => 'Test Mode',
99
- 91 => 'Load Control',
100
- 92 => 'Added Interval (Data Correction)',
101
- 93 => 'Replaced Interval (Data Correction)',
102
- 94 => 'Estimated Interval (Data Correction)',
103
- 95 => 'Pulse Overflow Alarm',
104
- 96 => 'Data Out Of Limits',
105
- 97 => 'Excluded Data',
106
- 98 => 'Parity Error',
107
- 99 => 'Energy Type (Register Changed)'
108
- }.freeze
109
- end
110
- end
3
+ # This file is kept for backward compatibility
4
+ # Constants have been moved to lib/aemo/nem12/meter_data/flag/reason_code.rb
5
+ # and are now accessible via AEMO::NEM12::REASON_CODES
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 = []
@@ -123,7 +129,7 @@ module AEMO
123
129
  default_nem12_100,
124
130
  nem12s.map(&:to_nem12_200_csv),
125
131
  default_nem12_900
126
- ].flatten.join
132
+ ].join
127
133
  end
128
134
  end
129
135
 
@@ -258,17 +264,8 @@ module AEMO
258
264
  end
259
265
  end
260
266
 
261
- # Deal with flags if necessary
262
- flag = nil
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 Lint/UnusedMethodArgument,Naming/VariableNumber
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
- # Only need to update flags for EFSV
322
- unless %w[A N].include? csv[3]
323
- number_of_intervals = 1440 / @data_details.last[:interval_length]
324
- interval_start_point = @interval_data.length - number_of_intervals
325
-
326
- # For each of these
327
- base_interval_event = { datetime: nil, quality_method: csv[3], reason_code: csv[4]&.to_i,
328
- reason_description: csv[5] }
329
-
330
- # Interval Numbers are 1-indexed
331
- ((csv[1].to_i)..(csv[2].to_i)).each do |i|
332
- interval_event = base_interval_event.dup
333
- interval_event[:datetime] = @interval_data[interval_start_point + (i - 1)][:datetime]
334
- interval_events << interval_event
335
- # Create flag details
336
- flag ||= { quality_flag: nil, method_flag: nil, reason_code: nil }
337
- unless interval_event[:quality_method].nil?
338
- flag[:quality_flag] = interval_event[:quality_method][0]
339
- flag[:method_flag] = interval_event[:quality_method][1, 2].to_i
340
- end
341
- flag[:reason_code] = interval_event[:reason_code] unless interval_event[:reason_code].nil?
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
- flag_to_s(d[:flag])
368
+ d[:flag]&.to_s
388
369
  ]
389
370
  end
390
371
  end
@@ -408,7 +389,7 @@ module AEMO
408
389
  to_nem12_100_csv,
409
390
  to_nem12_200_csv,
410
391
  to_nem12_900_csv
411
- ].flatten.join
392
+ ].join
412
393
  end
413
394
 
414
395
  # Output the AEMO::NEM12 to a valid NEM12 100 row CSV string.
@@ -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
- has_flags = daily_data.map { |x| x[:flag]&.any? }.uniq.include?(true)
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
- has_flags ? 'V' : 'A',
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
- next unless has_flags
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
- row[:flag].nil? ? 'A' : "#{row[:flag][:quality_flag]}#{row[:flag][:method_flag]}",
512
- row[:flag].nil? ? '' : row[:flag][:reason_code],
521
+ quality_method,
522
+ reason_code || '',
513
523
  ''
514
524
  ].join(CSV_SEPARATOR)
515
525
  end.join(CRLF)
data/lib/aemo/time.rb CHANGED
@@ -10,11 +10,11 @@ module AEMO
10
10
  module Time
11
11
  NEMTIMEZONE = 'Australia/Brisbane'
12
12
  TIMESTAMP14 = '%Y%m%d%H%M%S'
13
- TIMESTAMP14_PATTERN = /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}$/
13
+ TIMESTAMP14_PATTERN = /\A\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}\z/
14
14
  TIMESTAMP12 = '%Y%m%d%H%M'
15
- TIMESTAMP12_PATTERN = /^\d{4}\d{2}\d{2}\d{2}\d{2}$/
15
+ TIMESTAMP12_PATTERN = /\A\d{4}\d{2}\d{2}\d{2}\d{2}\z/
16
16
  TIMESTAMP8 = '%Y%m%d'
17
- TIMESTAMP8_PATTERN = /^\d{4}\d{2}\d{2}$/
17
+ TIMESTAMP8_PATTERN = /\A\d{4}\d{2}\d{2}\z/
18
18
 
19
19
  class << self
20
20
  # Format a time to a timestamp 14.
data/lib/aemo/version.rb CHANGED
@@ -23,7 +23,7 @@
23
23
  # @author Joel Courtney <euphemize@gmail.com>
24
24
  module AEMO
25
25
  # aemo version
26
- VERSION = '0.7.1'
26
+ VERSION = '0.8.0'
27
27
 
28
28
  # aemo version split amongst different revisions
29
29
  MAJOR_VERSION, MINOR_VERSION, REVISION = VERSION.split('.').map(&:to_i)
@@ -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
- 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
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
- 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]
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
- File.write(File.join(@path, output_file), output_data.to_json)
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