Class: AEMO::NEM12

Inherits:
Object
  • Object
show all
Defined in:
lib/aemo/nem12.rb,
lib/aemo/nem12/reason_codes.rb,
lib/aemo/nem12/quality_method.rb,
lib/aemo/nem12/record_indicators.rb,
lib/aemo/nem12/data_stream_suffix.rb,
lib/aemo/nem12/unit_of_measurement.rb,
lib/aemo/nem12/transaction_code_flags.rb

Overview

Namespace for classes and modules that handle AEMO Gem NEM12 interactions

Since:

  • 0.1.4

Constant Summary collapse

REASON_CODES =

Since:

  • 0.1.4

{
  0 => 'Free Text Description',
  1 => 'Meter/Equipment Changed',
  2 => 'Extreme Weather/Wet',
  3 => 'Quarantine',
  4 => 'Savage Dog',
  5 => 'Meter/Equipment Changed',
  6 => 'Extreme Weather/Wet',
  7 => 'Unable To Locate Meter',
  8 => 'Vacant Premise',
  9 => 'Meter/Equipment Changed',
  10 => 'Lock Damaged/Seized',
  11 => 'In Wrong Walk',
  12 => 'Locked Premises',
  13 => 'Locked Gate',
  14 => 'Locked Meter Box',
  15 => 'Access - Overgrown',
  16 => 'Noxious Weeds',
  17 => 'Unsafe Equipment/Location',
  18 => 'Read Below Previous',
  19 => 'Consumer Wanted',
  20 => 'Damaged Equipment/Panel',
  21 => 'Switched Off',
  22 => 'Meter/Equipment Seals Missing',
  23 => 'Meter/Equipment Seals Missing',
  24 => 'Meter/Equipment Seals Missing',
  25 => 'Meter/Equipment Seals Missing',
  26 => 'Meter/Equipment Seals Missing',
  27 => 'Meter/Equipment Seals Missing',
  28 => 'Damaged Equipment/Panel',
  29 => 'Relay Faulty/Damaged',
  30 => 'Meter Stop Switch On',
  31 => 'Meter/Equipment Seals Missing',
  32 => 'Damaged Equipment/Panel',
  33 => 'Relay Faulty/Damaged',
  34 => 'Meter Not In Handheld',
  35 => 'Timeswitch Faulty/Reset Required',
  36 => 'Meter High/Ladder Required',
  37 => 'Meter High/Ladder Required',
  38 => 'Unsafe Equipment/Location',
  39 => 'Reverse Energy Observed',
  40 => 'Timeswitch Faulty/Reset Required',
  41 => 'Faulty Equipment Display/Dials',
  42 => 'Faulty Equipment Display/Dials',
  43 => 'Power Outage',
  44 => 'Unsafe Equipment/Location',
  45 => 'Readings Failed To Validate',
  46 => 'Extreme Weather/Hot',
  47 => 'Refused Access',
  48 => 'Timeswitch Faulty/Reset Required',
  49 => 'Wet Paint',
  50 => 'Wrong Tariff',
  51 => 'Installation Demolished',
  52 => 'Access - Blocked',
  53 => 'Bees/Wasp In Meter Box',
  54 => 'Meter Box Damaged/Faulty',
  55 => 'Faulty Equipment Display/Dials',
  56 => 'Meter Box Damaged/Faulty',
  57 => 'Timeswitch Faulty/Reset Required',
  58 => 'Meter Ok - Supply Failure',
  59 => 'Faulty Equipment Display/Dials',
  60 => 'Illegal Connection/Equipment Tampered',
  61 => 'Meter Box Damaged/Faulty',
  62 => 'Damaged Equipment/Panel',
  63 => 'Illegal Connection/Equipment Tampered',
  64 => 'Key Required',
  65 => 'Wrong Key Provided',
  66 => 'Lock Damaged/Seized',
  67 => 'Extreme Weather/Wet',
  68 => 'Zero Consumption',
  69 => 'Reading Exceeds Estimate',
  70 => 'Probe Reports Tampering',
  71 => 'Probe Read Error',
  72 => 'Meter/Equipment Changed',
  73 => 'Low Consumption',
  74 => 'High Consumption',
  75 => 'Customer Read',
  76 => 'Communications Fault',
  77 => 'Estimation Forecast',
  78 => 'Null Data',
  79 => 'Power Outage Alarm',
  80 => 'Short Interval Alarm',
  81 => 'Long Interval Alarm',
  82 => 'CRC Error',
  83 => 'RAM Checksum Error',
  84 => 'ROM Checksum Error',
  85 => 'Data Missing Alarm',
  86 => 'Clock Error Alarm',
  87 => 'Reset Occurred',
  88 => 'Watchdog Timeout Alarm',
  89 => 'Time Reset Occurred',
  90 => 'Test Mode',
  91 => 'Load Control',
  92 => 'Added Interval (Data Correction)',
  93 => 'Replaced Interval (Data Correction)',
  94 => 'Estimated Interval (Data Correction)',
  95 => 'Pulse Overflow Alarm',
  96 => 'Data Out Of Limits',
  97 => 'Excluded Data',
  98 => 'Parity Error',
  99 => 'Energy Type (Register Changed)'
}.freeze
QUALITY_FLAGS =

Since:

  • 0.1.4

{
  'A'     => 'Actual Data',
  'E'     => 'Forward Estimated Data',
  'F'     => 'Final Substituted Data',
  'N'     => 'Null Data',
  'S'     => 'Substituted Data',
  'V'     => 'Variable Data'
}.freeze
METHOD_FLAGS =

Since:

  • 0.1.4

{
  11 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Check', description: '' },
  12 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Calculated', description: '' },
  13 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'SCADA', description: '' },
  14 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Like Day', description: '' },
  15 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Average Like Day', description: '' },
  16 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Agreed', description: '' },
  17 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Linear', description: '' },
  18 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Alternate', description: '' },
  19 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Zero', description: '' },
  20 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Churn Correction (Like Day)', description: '' },
  21 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Five-minute No Historical Data', description: '' },
  51 => { type: %w[EST SUB], installation_type: 5, short_descriptor: 'Previous Year', description: '' },
  52 => { type: %w[EST SUB], installation_type: 5, short_descriptor: 'Previous Read', description: '' },
  53 => { type: %w[SUB], installation_type: 5, short_descriptor: 'Revision', description: '' },
  54 => { type: %w[SUB], installation_type: 5, short_descriptor: 'Linear', description: '' },
  55 => { type: %w[SUB], installation_type: 5, short_descriptor: 'Agreed', description: '' },
  56 => { type: %w[EST SUB], installation_type: 5, short_descriptor: 'Prior to First Read - Agreed', description: '' },
  57 => { type: %w[EST SUB], installation_type: 5, short_descriptor: 'Customer Class', description: '' },
  58 => { type: %w[EST SUB], installation_type: 5, short_descriptor: 'Zero', description: '' },
  59 => { type: %w[EST SUB], installation_type: 5, short_descriptor: 'Five-minute No Historical Data', description: '' },
  61 => { type: %w[EST SUB], installation_type: 6, short_descriptor: 'Previous Year', description: '' },
  62 => { type: %w[EST SUB], installation_type: 6, short_descriptor: 'Previous Read', description: '' },
  63 => { type: %w[EST SUB], installation_type: 6, short_descriptor: 'Customer Class', description: '' },
  64 => { type: %w[SUB], installation_type: 6, short_descriptor: 'Agreed', description: '' },
  65 => { type: %w[EST], installation_type: 6, short_descriptor: 'ADL', description: '' },
  66 => { type: %w[SUB], installation_type: 6, short_descriptor: 'Revision', description: '' },
  67 => { type: %w[SUB], installation_type: 6, short_descriptor: 'Customer Read', description: '' },
  68 => { type: %w[EST SUB], installation_type: 6, short_descriptor: 'Zero', description: '' },
  69 => { type: %w[SUB], installation_type: 6, short_descriptor: 'Linear extrapolation', description: '' },
  71 => { type: %w[SUB], installation_type: 7, short_descriptor: 'Recalculation', description: '' },
  72 => { type: %w[SUB], installation_type: 7, short_descriptor: 'Revised Table', description: '' },
  73 => { type: %w[SUB], installation_type: 7, short_descriptor: 'Revised Algorithm', description: '' },
  74 => { type: %w[SUB], installation_type: 7, short_descriptor: 'Agreed', description: '' },
  75 => { type: %w[EST], installation_type: 7, short_descriptor: 'Existing Table', description: '' }
}.freeze
RECORD_INDICATORS =

As per AEMO NEM12 Specification www.aemo.com.au/Consultations/National-Electricity-Market/Open/~/media/ Files/Other/consultations/nem/Meter% 20Data% 20File% 20Format% 20Specification% 20 NEM12_NEM13/MDFF_Specification_NEM12_NEM13_Final_v102_clean.ashx

Since:

  • 0.1.4

{
  100 => 'Header',
  200 => 'NMI Data Details',
  300 => 'Interval Data',
  400 => 'Interval Event',
  500 => 'B2B Details',
  900 => 'End'
}.freeze
DATA_STREAM_SUFFIX =

Since:

  • 0.1.4

{
  # Averaged Data Streams
  'A' => { stream: 'Average', description: 'Import', units: 'kWh' },
  'D' => { stream: 'Average', description: 'Export', units: 'kWh' },
  'J' => { stream: 'Average', description: 'Import', units: 'kVArh' },
  'P' => { stream: 'Average', description: 'Export', units: 'kVArh' },
  'S' => { stream: 'Average', description: '',       units: 'kVAh' },
  # Master Data Streams
  'B' => { stream: 'Master',  description: 'Import', units: 'kWh' },
  'E' => { stream: 'Master',  description: 'Export', units: 'kWh' },
  'K' => { stream: 'Master',  description: 'Import', units: 'kVArh' },
  'Q' => { stream: 'Master',  description: 'Export', units: 'kVArh' },
  'T' => { stream: 'Master',  description: '',       units: 'kVAh' },
  'G' => { stream: 'Master',  description: 'Power Factor', units: 'PF' },
  'H' => { stream: 'Master',  description: 'Q Metering', units: 'Qh' },
  'M' => { stream: 'Master',  description: 'Par Metering', units: 'parh' },
  'V' => { stream: 'Master',  description: 'Volts or V2h or Amps or A2h', units: '' },
  # Check Meter Streams
  'C' => { stream: 'Check',  description: 'Import', units: 'kWh' },
  'F' => { stream: 'Check',  description: 'Export', units: 'kWh' },
  'L' => { stream: 'Check',  description: 'Import', units: 'kVArh' },
  'R' => { stream: 'Check',  description: 'Export', units: 'kVArh' },
  'U' => { stream: 'Check',  description: '',       units: 'kVAh' },
  'Y' => { stream: 'Check',  description: 'Q Metering',         units: 'Qh' },
  'W' => { stream: 'Check',  description: 'Par Metering Path',  units: '' },
  'Z' => { stream: 'Check',  description: 'Volts or V2h or Amps or A2h',  units: '' },
  # Net Meter Streams
  # AEMO: NOTE THAT D AND J ARE PREVIOUSLY DEFINED
  # 'D' => { stream: 'Net',    description: 'Net', units: 'kWh' },
  # 'J' => { stream: 'Net',    description: 'Net', units: 'kVArh' }
}.freeze
UOM =

Since:

  • 0.1.4

{
  'MWh'   => { name: 'Megawatt Hour', multiplier: 1e6 },
  'kWh'   => { name: 'Kilowatt Hour', multiplier: 1e3 },
  'Wh'    => { name: 'Watt Hour', multiplier: 1 },
  'MW'    => { name: 'Megawatt', multiplier: 1e6 },
  'kW'    => { name: 'Kilowatt', multiplier: 1e3 },
  'W'     => { name: 'Watt', multiplier: 1 },
  'MVArh' => { name: 'Megavolt Ampere Reactive Hour', multiplier: 1e6 },
  'kVArh' => { name: 'Kilovolt Ampere Reactive Hour', multiplier: 1e3 },
  'VArh'  => { name: 'Volt Ampere Reactive Hour', multiplier: 1 },
  'MVAr'  => { name: 'Megavolt Ampere Reactive', multiplier: 1e6 },
  'kVAr'  => { name: 'Kilovolt Ampere Reactive', multiplier: 1e3 },
  'VAr'   => { name: 'Volt Ampere Reactive', multiplier: 1 },
  'MVAh'  => { name: 'Megavolt Ampere Hour', multiplier: 1e6 },
  'kVAh'  => { name: 'Kilovolt Ampere Hour', multiplier: 1e3 },
  'VAh'   => { name: 'Volt Ampere Hour', multiplier: 1 },
  'MVA'   => { name: 'Megavolt Ampere', multiplier: 1e6 },
  'kVA'   => { name: 'Kilovolt Ampere', multiplier: 1e3 },
  'VA'    => { name: 'Volt Ampere', multiplier: 1 },
  'kV'    => { name: 'Kilovolt', multiplier: 1e3 },
  'V'     => { name: 'Volt', multiplier: 1 },
  'kA'    => { name: 'Kiloampere', multiplier: 1e3 },
  'A'     => { name: 'Ampere', multiplier: 1 },
  'pf'    => { name: 'Power Factor', multiplier: 1 }
}.freeze
UOM_NON_SPEC_MAPPING =

Since:

  • 0.1.4

{
  'MWH'   => 'MWh',
  'KWH'   => 'kWh',
  'WH'    => 'Wh',
  'MW'    => 'MW',
  'KW'    => 'kW',
  'W'     => 'W',
  'MVARH' => 'MVArh',
  'KVARH' => 'kVArh',
  'VARH'  => 'VArh',
  'MVAR'  => 'MVAr',
  'KVAR'  => 'kVAr',
  'VAR'   => 'VAr',
  'MVAH'  => 'MVAh',
  'KVAH'  => 'kVAh',
  'VAH'   => 'VAh',
  'MVA'   => 'MVA',
  'KVA'   => 'kVA',
  'VA'    => 'VA',
  'KV'    => 'kV',
  'V'     => 'V',
  'KA'    => 'kA',
  'A'     => 'A',
  'PF'    => 'pf'
}.freeze
TRANSACTION_CODE_FLAGS =

Since:

  • 0.1.4

{
  'A' => 'Alteration',
  'C' => 'Meter Reconfiguration',
  'G' => 'Re-energisation',
  'D' => 'De-energisation',
  'E' => 'Forward Estimate',
  'N' => 'Normal Read',
  'O' => 'Other',
  'S' => 'Special Read',
  'R' => 'Removal of Meter'
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(nmi, options = {}) ⇒ NEM12

Initialize a NEM12 file

Parameters:

  • nmi (string)
  • options (Hash) (defaults to: {})

Since:

  • 0.1.4



32
33
34
35
36
37
38
39
40
# File 'lib/aemo/nem12.rb', line 32

def initialize(nmi, options = {})
  @nmi              = AEMO::NMI.new(nmi) unless nmi.empty?
  @data_details     = []
  @interval_data    = []
  @interval_events  = []
  options.each_key do |key|
    send 'key=', options[key]
  end
end

Instance Attribute Details

#data_detailsObject (readonly)

Since:

  • 0.1.4



26
27
28
# File 'lib/aemo/nem12.rb', line 26

def data_details
  @data_details
end

#file_contentsObject

Since:

  • 0.1.4



27
28
29
# File 'lib/aemo/nem12.rb', line 27

def file_contents
  @file_contents
end

#headerObject

Since:

  • 0.1.4



27
28
29
# File 'lib/aemo/nem12.rb', line 27

def header
  @header
end

#interval_dataObject (readonly)

Since:

  • 0.1.4



26
27
28
# File 'lib/aemo/nem12.rb', line 26

def interval_data
  @interval_data
end

#interval_eventsObject (readonly)

Since:

  • 0.1.4



26
27
28
# File 'lib/aemo/nem12.rb', line 26

def interval_events
  @interval_events
end

#nmiObject

Since:

  • 0.1.4



27
28
29
# File 'lib/aemo/nem12.rb', line 27

def nmi
  @nmi
end

#nmi_data_detailsObject

Since:

  • 0.1.4



27
28
29
# File 'lib/aemo/nem12.rb', line 27

def nmi_data_details
  @nmi_data_details
end

Class Method Details

.parse_nem12(contents, strict = true) ⇒ Array<AEMO::NEM12>

Returns An array of NEM12 objects.

Parameters:

  • contents (String)

    the path to a file

  • strict (Boolean) (defaults to: true)

Returns:

Raises:

  • (ArgumentError)

Since:

  • 0.1.4



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/aemo/nem12.rb', line 291

def self.parse_nem12(contents, strict = true)
  file_contents = contents.tr("\r", "\n").tr("\n\n", "\n").split("\n").delete_if(&:empty?)
  # nothing to further process
  return [] if file_contents.empty?

  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'

  nem12s = []
  AEMO::NEM12.parse_nem12_100(file_contents.first, strict: strict)
  file_contents.each do |line|
    case line[0..2].to_i
    when 200
      nem12s << AEMO::NEM12.new('')
      nem12s.last.parse_nem12_200(line, strict: strict)
    when 300
      nem12s.last.parse_nem12_300(line, strict: strict)
    when 400
      nem12s.last.parse_nem12_400(line, strict: strict)
      # when 500
      #   nem12s.last.parse_nem12_500(line, strict: strict)
      # when 900
      #   nem12s.last.parse_nem12_900(line, strict: strict)
    end
  end
  # Return the array of NEM12 groups
  nem12s
end

.parse_nem12_100(line, options = {}) ⇒ Hash

Parses the header record

Parameters:

  • line (String)

    A single line in string format

  • options (Hash) (defaults to: {})

Returns:

  • (Hash)

    the line parsed into a hash of information

Raises:

  • (ArgumentError)

Since:

  • 0.1.4



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/aemo/nem12.rb', line 51

def self.parse_nem12_100(line, options = {})
  csv = line.parse_csv

  raise ArgumentError, 'RecordIndicator is not 100'     if csv[0] != '100'
  raise ArgumentError, 'VersionHeader is not NEM12'     if csv[1] != 'NEM12'
  raise ArgumentError, 'Time is not valid' if options[:strict] && (csv[2].match(/\d{12}/).nil? || csv[2] != Time.parse("#{csv[2]}00").strftime('%Y%m%d%H%M'))
  raise ArgumentError, 'FromParticipant is not valid'  if csv[3].match(/.{1,10}/).nil?
  raise ArgumentError, 'ToParticipant is not valid'    if csv[4].match(/.{1,10}/).nil?

  {
    record_indicator: csv[0].to_i,
    version_header:   csv[1],
    datetime:         Time.parse("#{csv[2]}+1000"),
    from_participant: csv[3],
    to_participant:   csv[4]
  }
end

.parse_nem12_file(path_to_file, strict = true) ⇒ Array<AEMO::NEM12>

Returns NEM12 object.

Parameters:

  • path_to_file (String)

    the path to a file

Returns:

Since:

  • 0.1.4



284
285
286
# File 'lib/aemo/nem12.rb', line 284

def self.parse_nem12_file(path_to_file, strict = true)
  parse_nem12(File.read(path_to_file), strict)
end

Instance Method Details

#flag_to_s(flag) ⇒ nil, String

Turns the flag to a string

Parameters:

  • flag (Hash)

    the object of a flag

Returns:

  • (nil, String)

    a hyphenated string for the flag or nil

Since:

  • 0.1.4



247
248
249
250
251
252
253
254
255
# File 'lib/aemo/nem12.rb', line 247

def flag_to_s(flag)
  flag_to_s = []
  unless flag.nil?
    flag_to_s << QUALITY_FLAGS[flag[:quality_flag]]                   unless QUALITY_FLAGS[flag[:quality_flag]].nil?
    flag_to_s << METHOD_FLAGS[flag[:method_flag]][:short_descriptor]  unless METHOD_FLAGS[flag[:method_flag]].nil?
    flag_to_s << REASON_CODES[flag[:reason_code]]                     unless REASON_CODES[flag[:reason_code]].nil?
  end
  flag_to_s.empty? ? nil : flag_to_s.join(' - ')
end

#nmi_identifierObject

Returns the NMI Identifier or nil

Since:

  • 0.1.4



43
44
45
# File 'lib/aemo/nem12.rb', line 43

def nmi_identifier
  @nmi.nil? ? nil : @nmi.nmi
end

#parse_nem12_200(line, options = {}) ⇒ Hash

Parses the NMI Data Details

Parameters:

  • line (String)

    A single line in string format

  • options (Hash) (defaults to: {})

Returns:

  • (Hash)

    the line parsed into a hash of information

Raises:

  • (ArgumentError)

Since:

  • 0.1.4



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/aemo/nem12.rb', line 73

def parse_nem12_200(line, options = {})
  csv = line.parse_csv

  raise ArgumentError, 'RecordIndicator is not 200'     if csv[0] != '200'
  raise ArgumentError, 'NMI is not valid'               unless AEMO::NMI.valid_nmi?(csv[1])
  raise ArgumentError, 'NMIConfiguration is not valid' if options[:strict] && (csv[2].nil? || csv[2].match(/.{1,240}/).nil?)
  raise ArgumentError, 'RegisterID is not valid' if !csv[3].nil? && csv[3].match(/.{1,10}/).nil?
  raise ArgumentError, 'NMISuffix is not valid' if csv[4].nil? || csv[4].match(/[A-HJ-NP-Z][1-9A-HJ-NP-Z]/).nil?
  raise ArgumentError, 'MDMDataStreamIdentifier is not valid' if !csv[5].nil? && !csv[5].empty? && !csv[5].match(/^\s*$/) && csv[5].match(/[A-Z0-9]{2}/).nil?
  raise ArgumentError, 'MeterSerialNumber is not valid' if !csv[6].nil? && !csv[6].empty? && !csv[6].match(/^\s*$/) && csv[6].match(/[A-Z0-9]{2}/).nil?
  raise ArgumentError, 'UOM is not valid' if csv[7].nil? || csv[7].upcase.match(/[A-Z0-9]{2}/).nil?
  raise ArgumentError, 'UOM is not valid'               unless UOM.keys.map(&:upcase).include?(csv[7].upcase)
  raise ArgumentError, 'IntervalLength is not valid'    unless %w[1 5 10 15 30].include?(csv[8])
  # raise ArgumentError, 'NextScheduledReadDate is not valid' if csv[9].match(/\d{8}/).nil? || csv[9] != Time.parse('#{csv[9]}').strftime('%Y%m%d')

  @nmi = AEMO::NMI.new(csv[1])

  # Push onto the stack
  @data_details << {
    record_indicator: csv[0].to_i,
    nmi: csv[1],
    nmi_configuration: csv[2],
    register_id: csv[3],
    nmi_suffix: csv[4],
    mdm_data_streaming_identifier: csv[5],
    meter_serial_number: csv[6],
    uom: csv[7].upcase,
    interval_length: csv[8].to_i,
    next_scheduled_read_date: csv[9]
  }
end

#parse_nem12_300(line, options = {}) ⇒ Array of hashes

Returns the line parsed into a hash of information.

Parameters:

  • line (String)

    A single line in string format

  • options (Hash) (defaults to: {})

Returns:

  • (Array of hashes)

    the line parsed into a hash of information

Raises:

  • (TypeError)

Since:

  • 0.1.4



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/aemo/nem12.rb', line 108

def parse_nem12_300(line, options = {})
  csv = line.parse_csv
  raise TypeError, 'Expected NMI Data Details to exist with IntervalLength specified' if @data_details.last.nil? || @data_details.last[:interval_length].nil?

  # ref: AEMO's MDFF Spec NEM12 and NEM13 v1.01 (2014-05-14)
  record_fixed_fields = %w[RecordIndicator IntervalDate QualityMethod ReasonCode ReasonDescription UpdateDatetime MSATSLoadDateTime]
  number_of_intervals = 1440 / @data_details.last[:interval_length]
  raise TypeError, 'Invalid record length' if csv.length != record_fixed_fields.length + number_of_intervals

  intervals_offset = number_of_intervals + 2

  raise ArgumentError, 'RecordIndicator is not 300' if csv[0] != '300'
  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')
  (2..(number_of_intervals + 1)).each do |i|
    raise ArgumentError, "Interval number #{i - 1} is not valid" if csv[i].nil? || csv[i].match(/\d+(\.\d+)?/).nil?
  end
  raise ArgumentError, 'QualityMethod is not valid' unless csv[intervals_offset + 0].class == String
  raise ArgumentError, 'QualityMethod does not have valid length' unless [1, 3].include?(csv[intervals_offset + 0].length)
  raise ArgumentError, 'QualityMethod does not have valid QualityFlag' unless QUALITY_FLAGS.keys.include?(csv[intervals_offset + 0][0])
  unless %w[A N V].include?(csv[intervals_offset + 0][0])
    raise ArgumentError, 'QualityMethod does not have valid length' unless csv[intervals_offset + 0].length == 3
    raise ArgumentError, 'QualityMethod does not have valid MethodFlag' unless METHOD_FLAGS.keys.include?(csv[intervals_offset + 0][1..2].to_i)
  end
  unless %w[A N E].include?(csv[intervals_offset + 0][0])
    raise ArgumentError, 'ReasonCode is not valid' unless REASON_CODES.keys.include?(csv[intervals_offset + 1].to_i)
  end
  if !csv[intervals_offset + 1].nil? && csv[intervals_offset + 1].to_i.zero?
    raise ArgumentError, 'ReasonDescription is not valid' unless csv[intervals_offset + 2].class == String && !csv[intervals_offset + 2].empty?
  end
  if options[:strict]
    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')
      raise ArgumentError, 'UpdateDateTime is not valid'
    end
    if !csv[intervals_offset + 4].blank? && csv[intervals_offset + 4].match(/\d{14}/).nil? || !csv[intervals_offset + 4].blank? && csv[intervals_offset + 4] != Time.parse(csv[intervals_offset + 4].to_s).strftime('%Y%m%d%H%M%S')
      raise ArgumentError, 'MSATSLoadDateTime is not valid'
    end
  end

  # Deal with flags if necessary
  flag = nil
  # Based on QualityMethod and ReasonCode
  if csv[intervals_offset + 0].length == 3 || !csv[intervals_offset + 1].nil?
    flag ||= { quality_flag: nil, method_flag: nil, reason_code: nil }
    if csv[intervals_offset + 0].length == 3
      flag[:quality_flag] = csv[intervals_offset + 0][0]
      flag[:method_flag] = csv[intervals_offset + 0][1, 2].to_i
    end
    flag[:reason_code] = csv[intervals_offset + 1].to_i unless csv[intervals_offset + 1].nil?
  end

  # Deal with updated_at & msats_load_at
  updated_at = nil
  msats_load_at = nil

  if options[:strict]
    updated_at = Time.parse(csv[intervals_offset + 3]) unless csv[intervals_offset + 3].blank?
    msats_load_at = Time.parse(csv[intervals_offset + 4]) unless csv[intervals_offset + 4].blank?
  end

  base_interval = {
    data_details: @data_details.last,
    datetime: Time.parse("#{csv[1]}000000+1000"),
    value: nil,
    flag: flag,
    updated_at: updated_at,
    msats_load_at: msats_load_at
  }

  intervals = []
  (2..(number_of_intervals + 1)).each do |i|
    interval = base_interval.dup
    interval[:datetime] += (i - 1) * interval[:data_details][:interval_length] * 60
    interval[:value] = csv[i].to_f
    intervals << interval
  end
  @interval_data += intervals
  intervals
end

#parse_nem12_400(line, options = {}) ⇒ Hash

Returns the line parsed into a hash of information.

Parameters:

  • line (String)

    A single line in string format

  • options (Hash) (defaults to: {})

Returns:

  • (Hash)

    the line parsed into a hash of information

Raises:

  • (ArgumentError)

Since:

  • 0.1.4



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/aemo/nem12.rb', line 190

def parse_nem12_400(line, options = {})
  csv = line.parse_csv
  raise ArgumentError, 'RecordIndicator is not 400'     if csv[0] != '400'
  raise ArgumentError, 'StartInterval is not valid'     if csv[1].nil? || csv[1].match(/^\d+$/).nil?
  raise ArgumentError, 'EndInterval is not valid'       if csv[2].nil? || csv[2].match(/^\d+$/).nil?
  raise ArgumentError, 'QualityMethod is not valid'     if csv[3].nil? || csv[3].match(/^([AN]|([AEFNSV]\d{2}))$/).nil?
  # raise ArgumentError, 'ReasonCode is not valid'        if (csv[4].nil? && csv[3].match(/^ANE/)) || csv[4].match(/^\d{3}?$/) || csv[3].match(/^ANE/)
  # raise ArgumentError, 'ReasonDescription is not valid' if (csv[4].nil? && csv[3].match(/^ANE/)) || ( csv[5].match(/^$/) && csv[4].match(/^0$/) )

  interval_events = []

  # Only need to update flags for EFSV
  unless %w[A N].include? csv[3]
    number_of_intervals = 1440 / @data_details.last[:interval_length]
    interval_start_point = @interval_data.length - number_of_intervals

    # For each of these
    base_interval_event = { datetime: nil, quality_method: csv[3], reason_code: (csv[4].nil? ? nil : csv[4].to_i), reason_description: csv[5] }

    # Interval Numbers are 1-indexed
    ((csv[1].to_i)..(csv[2].to_i)).each do |i|
      interval_event = base_interval_event.dup
      interval_event[:datetime] = @interval_data[interval_start_point + (i - 1)][:datetime]
      interval_events << interval_event
      # Create flag details
      flag ||= { quality_flag: nil, method_flag: nil, reason_code: nil }
      unless interval_event[:quality_method].nil?
        flag[:quality_flag] = interval_event[:quality_method][0]
        flag[:method_flag] = interval_event[:quality_method][1, 2].to_i
      end
      flag[:reason_code] = interval_event[:reason_code] unless interval_event[:reason_code].nil?
      # Update with flag details
      @interval_data[interval_start_point + (i - 1)][:flag] = flag
    end
    @interval_events += interval_events
  end
  interval_events
end

#parse_nem12_500(_line, _options = {}) ⇒ Hash

What even is a 500 row?

Parameters:

  • line (String)

    A single line in string format

  • _options (Hash) (defaults to: {})

Returns:

  • (Hash)

    the line parsed into a hash of information

Since:

  • 0.1.4



234
# File 'lib/aemo/nem12.rb', line 234

def parse_nem12_500(_line, _options = {}); end

#parse_nem12_900(_line, _options = {}) ⇒ Hash

900 is the last row a NEM12 should seeā€¦

Parameters:

  • line (String)

    A single line in string format

  • _options (Hash) (defaults to: {})

Returns:

  • (Hash)

    the line parsed into a hash of information

Since:

  • 0.1.4



241
# File 'lib/aemo/nem12.rb', line 241

def parse_nem12_900(_line, _options = {}); end

#to_aArray

Returns array of a NEM12 file a given Meter + Data Stream for easy reading.

Returns:

  • (Array)

    array of a NEM12 file a given Meter + Data Stream for easy reading

Since:

  • 0.1.4



258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/aemo/nem12.rb', line 258

def to_a
  @interval_data.map do |d|
    [
      d[:data_details][:nmi],
      d[:data_details][:nmi_suffix].upcase,
      d[:data_details][:uom],
      d[:datetime],
      d[:value],
      flag_to_s(d[:flag])
    ]
  end
end

#to_csvArray

Returns CSV of a NEM12 file a given Meter + Data Stream for easy reading.

Returns:

  • (Array)

    CSV of a NEM12 file a given Meter + Data Stream for easy reading

Since:

  • 0.1.4



272
273
274
275
276
277
278
279
280
# File 'lib/aemo/nem12.rb', line 272

def to_csv
  headers = %w[nmi suffix units datetime value flags]
  ([headers] + to_a.map do |row|
    row[3] = row[3].strftime('%Y%m%d%H%M%S%z')
    row
  end).map do |row|
    row.join(', ')
  end.join("\n")
end