Class: AEMO::NEM12

Inherits:
Object
  • Object
show all
Defined in:
lib/aemo/nem12.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

CRLF =

Since:

  • 0.1.4

"\r\n"
CSV_SEPARATOR =

Since:

  • 0.1.4

','
QUALITY_FLAGS =

Backward compatibility: delegate constants to Flag class

Since:

  • 0.1.4

::AEMO::MeterData::Flag::QUALITY_FLAGS
METHOD_FLAGS =

Since:

  • 0.1.4

::AEMO::MeterData::Flag::METHOD_FLAGS
REASON_CODES =

Since:

  • 0.1.4

::AEMO::MeterData::Flag::REASON_CODES
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



139
140
141
142
143
144
145
146
147
# File 'lib/aemo/nem12.rb', line 139

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



35
36
37
# File 'lib/aemo/nem12.rb', line 35

def data_details
  @data_details
end

#file_contentsObject

Since:

  • 0.1.4



36
37
38
# File 'lib/aemo/nem12.rb', line 36

def file_contents
  @file_contents
end

#headerObject

Since:

  • 0.1.4



36
37
38
# File 'lib/aemo/nem12.rb', line 36

def header
  @header
end

#interval_dataObject (readonly)

Since:

  • 0.1.4



35
36
37
# File 'lib/aemo/nem12.rb', line 35

def interval_data
  @interval_data
end

#interval_eventsObject (readonly)

Since:

  • 0.1.4



35
36
37
# File 'lib/aemo/nem12.rb', line 35

def interval_events
  @interval_events
end

#nmiObject

Since:

  • 0.1.4



36
37
38
# File 'lib/aemo/nem12.rb', line 36

def nmi
  @nmi
end

#nmi_data_detailsObject

Since:

  • 0.1.4



36
37
38
# File 'lib/aemo/nem12.rb', line 36

def nmi_data_details
  @nmi_data_details
end

Class Method Details

.default_nem12_100String

Default NEM12 100 row record.

Returns:

  • (String)

Since:

  • 0.1.4



110
111
112
113
114
# File 'lib/aemo/nem12.rb', line 110

def default_nem12_100 # rubocop:disable Naming/VariableNumber
  timestamp = AEMO::Time.format_timestamp12(::Time.now)

  "100,NEM12,#{timestamp},ENOSI,ENOSI#{CRLF}"
end

.default_nem12_900String

Default NEM12 100 row record.

Returns:

  • (String)

Since:

  • 0.1.4



119
120
121
# File 'lib/aemo/nem12.rb', line 119

def default_nem12_900 # rubocop:disable Naming/VariableNumber
  "900#{CRLF}"
end

.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:

Since:

  • 0.1.4



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/aemo/nem12.rb', line 49

def 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?

  unless file_contents.first.parse_csv[0] == '100'
    raise ArgumentError,
          'First row should be have a RecordIndicator of 100 and be of type Header Record'
  end

  nem12s = []
  header = AEMO::NEM12.parse_nem12_100(file_contents.first, strict:)
  file_contents.each do |line|
    case line[0..2].to_i
    when 200
      nem12s << AEMO::NEM12.new('')
      nem12s.last.header = header
      nem12s.last.parse_nem12_200(line, strict:)
    when 300
      nem12s.last.parse_nem12_300(line, strict:)
    when 400
      nem12s.last.parse_nem12_400(line, 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, strict: true) ⇒ Hash

Parses the header record

Parameters:

  • line (String)

    A single line in string format

  • strict (Boolean) (defaults to: true)

Returns:

  • (Hash)

    the line parsed into a hash of information

Raises:

  • (ArgumentError)

Since:

  • 0.1.4



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/aemo/nem12.rb', line 85

def parse_nem12_100(line, strict: true) # rubocop:disable Naming/VariableNumber
  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 strict && !AEMO::Time.valid_timestamp12?(csv[2])

  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?

  datetime = strict && AEMO::Time.valid_timestamp12?(csv[2]) ? AEMO::Time.parse_timestamp12(csv[2]) : nil

  {
    record_indicator: csv[0].to_i,
    version_header: csv[1],
    datetime:,
    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



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

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

.to_nem12_csv(nem12s:) ⇒ String

For a list of nem12s, turn into a single NEM12 CSV string with default header row.

Parameters:

Returns:

  • (String)

Since:

  • 0.1.4



127
128
129
130
131
132
133
# File 'lib/aemo/nem12.rb', line 127

def to_nem12_csv(nem12s:)
  [
    default_nem12_100,
    nem12s.map(&:to_nem12_200_csv),
    default_nem12_900
  ].join
end

Instance Method Details

#nmi_identifierObject

Returns the NMI Identifier or nil

Since:

  • 0.1.4



150
151
152
# File 'lib/aemo/nem12.rb', line 150

def nmi_identifier
  @nmi&.nmi
end

#parse_nem12_200(line, strict: true) ⇒ Hash

Parses the NMI Data Details

Parameters:

  • line (String)

    A single line in string format

  • strict (Boolean) (defaults to: true)

Returns:

  • (Hash)

    the line parsed into a hash of information

Raises:

  • (ArgumentError)

Since:

  • 0.1.4



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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/aemo/nem12.rb', line 158

def parse_nem12_200(line, strict: true) # rubocop:disable Naming/VariableNumber
  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])

  if strict && (csv[2].nil? || csv[2].match(/.{1,240}/).nil?)
    raise ArgumentError,
          'NMIConfiguration is not valid'
  end

  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?

  if !csv[5].nil? && !csv[5].empty? && !csv[5].match(/^\s*$/) && csv[5].match(/[A-Z0-9]{2}/).nil?
    raise ArgumentError,
          'MDMDataStreamIdentifier is not valid'
  end

  if !csv[6].nil? && !csv[6].empty? && !csv[6].match(/^\s*$/) && csv[6].match(/[A-Z0-9]{2}/).nil?
    raise ArgumentError,
          'MeterSerialNumber is not valid'
  end

  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 !AEMO::Time.valid_timestamp8?(csv[9])

  @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, strict: true) ⇒ Array of hashes

Returns the line parsed into a hash of information.

Parameters:

  • line (String)

    A single line in string format

  • strict (Boolean) (defaults to: true)

Returns:

  • (Array of hashes)

    the line parsed into a hash of information

Raises:

  • (TypeError)

Since:

  • 0.1.4



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/aemo/nem12.rb', line 208

def parse_nem12_300(line, strict: true) # rubocop:disable Naming/VariableNumber
  csv = line.parse_csv

  if @data_details.last.nil? || @data_details.last[:interval_length].nil?
    raise TypeError,
          'Expected NMI Data Details to exist with IntervalLength specified'
  end

  # 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' unless AEMO::Time.valid_timestamp8?(csv[1])

  (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].instance_of?(String)
  raise ArgumentError, 'QualityMethod does not have valid length' unless [1, 3].include?(csv[intervals_offset + 0].length)

  unless QUALITY_FLAGS.keys.include?(csv[intervals_offset + 0][0])
    raise ArgumentError,
          'QualityMethod does not have valid QualityFlag'
  end

  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

    unless METHOD_FLAGS.keys.include?(csv[intervals_offset + 0][1..2].to_i)
      raise ArgumentError,
            'QualityMethod does not have valid MethodFlag'
    end
  end

  raise ArgumentError, 'ReasonCode is not valid' if !%w[A N E].include?(csv[intervals_offset + 0][0]) && !REASON_CODES.keys.include?(csv[intervals_offset + 1].to_i)

  if !csv[intervals_offset + 1].nil? && csv[intervals_offset + 1].to_i.zero? && !(csv[intervals_offset + 2].instance_of?(String) && !csv[intervals_offset + 2].empty?)
    raise ArgumentError,
          'ReasonDescription is not valid'
  end

  if strict
    unless AEMO::Time.valid_timestamp14?(csv[intervals_offset + 3])
      raise ArgumentError,
            'UpdateDateTime is not valid'
    end

    if !csv[intervals_offset + 4].blank? && !AEMO::Time.valid_timestamp14?(csv[intervals_offset + 4])
      raise ArgumentError,
            'MSATSLoadDateTime is not valid'
    end
  end

  # Deal with flags - explicitly set all flag values
  flag = AEMO::MeterData::Flag.from_quality_method_reason_code(quality_method: csv[intervals_offset + 0], reason_code: csv[intervals_offset + 1], validate: strict)

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

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

  base_interval = {
    data_details: @data_details.last,
    datetime: AEMO::Time.parse_timestamp8(csv[1]),
    value: nil,
    flag:,
    updated_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, strict: true) ⇒ Hash

Returns the line parsed into a hash of information.

Parameters:

  • line (String)

    A single line in string format

  • strict (Boolean) (defaults to: true)

Returns:

  • (Hash)

    the line parsed into a hash of information

Raises:

  • (ArgumentError)

Since:

  • 0.1.4



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/aemo/nem12.rb', line 302

def parse_nem12_400(line, strict: true) # rubocop:disable Naming/VariableNumber
  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?

  if csv[3].nil? || csv[3].match(/^([AN]|([AEFNSV]\d{2}))$/).nil?
    raise ArgumentError,
          'QualityMethod is not valid'
  end

  # 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 = []

  number_of_intervals = 1440 / @data_details.last[:interval_length]
  interval_start_point = @interval_data.length - number_of_intervals

  # For each of these
  # Parse reason code - only set if present and not empty
  parsed_reason_code = nil
  parsed_reason_code = csv[4].to_i unless csv[4].nil? || csv[4].empty?

  base_interval_event = { datetime: nil, quality_method: csv[3], reason_code: parsed_reason_code,
                          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

    flag = AEMO::MeterData::Flag.from_quality_method_reason_code(quality_method: interval_event[:quality_method], reason_code: interval_event[:reason_code], validate: strict)

    # Update with flag details
    @interval_data[interval_start_point + (i - 1)][:flag] = flag
  end
  @interval_events += interval_events

  interval_events
end

#parse_nem12_500(_line, strict: true) ⇒ Hash

What even is a 500 row?

Parameters:

  • line (String)

    A single line in string format

  • strict (Boolean) (defaults to: true)

Returns:

  • (Hash)

    the line parsed into a hash of information

Since:

  • 0.1.4



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

def parse_nem12_500(_line, strict: true); end

#parse_nem12_900(_line, strict: true) ⇒ Hash

900 is the last row a NEM12 should see…

Parameters:

  • line (String)

    A single line in string format

  • strict (Boolean) (defaults to: true)

Returns:

  • (Hash)

    the line parsed into a hash of information

Since:

  • 0.1.4



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

def parse_nem12_900(_line, strict: true); 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



360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/aemo/nem12.rb', line 360

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],
      d[:flag]&.to_s
    ]
  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



374
375
376
377
378
379
380
381
382
# File 'lib/aemo/nem12.rb', line 374

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%TH%M%S%z')
    row
  end).map do |row|
    row.join(', ')
  end.join("\n")
end

#to_nem12_100_csvString

Output the AEMO::NEM12 to a valid NEM12 100 row CSV string.

Returns:

  • (String)

Since:

  • 0.1.4



398
399
400
401
402
403
404
405
406
407
408
# File 'lib/aemo/nem12.rb', line 398

def to_nem12_100_csv
  return self.class.default_nem12_100 if header.nil?

  [
    header[:record_indicator],
    header[:version_header],
    AEMO::Time.format_timestamp12(header[:datetime]),
    header[:from_participant],
    header[:to_participant]
  ].join(CSV_SEPARATOR) + CRLF
end

#to_nem12_200_csvString

Output the AEMO::NEM12 to a valid NEM12 200 row CSV string.

Returns:

  • (String)

Since:

  • 0.1.4



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/aemo/nem12.rb', line 413

def to_nem12_200_csv
  return nil if data_details.length != 1

  data_detail = data_details.first

  [
    [
      data_detail[:record_indicator],
      data_detail[:nmi],
      data_detail[:nmi_configuration],
      data_detail[:register_id],
      data_detail[:nmi_suffix],
      data_detail[:mdm_data_streaming_identifier],
      data_detail[:meter_serial_number],
      data_detail[:uom],
      data_detail[:interval_length],
      data_detail[:next_scheduled_read_date] # NOTE: this is not turned into a timestamp.
    ].join(CSV_SEPARATOR),
    to_nem12_300_csv
  ].flatten.join(CRLF)
end

#to_nem12_300_csvString

Output the AEMO::NEM12 to a valid NEM12 300 row CSV string.

Returns:

  • (String)

Since:

  • 0.1.4



438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# File 'lib/aemo/nem12.rb', line 438

def to_nem12_300_csv
  lines = []

  daily_datas = interval_data.group_by do |x|
    AEMO::Time.format_timestamp8(x[:datetime] - 1.second)
  end
  daily_datas.keys.sort.each do |key|
    daily_data = daily_datas[key].sort_by { |x| x[:datetime] }

    # Check if all intervals have identical flags
    # If so, use that flag's quality method in the 300 record
    # Otherwise use 'V' (variable data) and generate 400 records
    first_flag = daily_data.first[:flag]
    all_same_flag = daily_data.all? { |x| x[:flag] == first_flag }

    if all_same_flag
      # All intervals have the same flag - use it in the 300 record
      quality_method = first_flag&.to_quality_method || ''
      reason_code = first_flag&.reason_code || ''
    else
      # Intervals have different flags - use 'V' and generate 400 records
      quality_method = 'V'
      reason_code = ''
    end

    lines << [
      '300',
      key,
      daily_data.map { |x| x[:value] },
      quality_method,
      reason_code,
      '',
      daily_data.first[:updated_at] ? AEMO::Time.format_timestamp14(daily_data.first[:updated_at]) : nil,
      daily_data.first[:msats_load_at] ? AEMO::Time.format_timestamp14(daily_data.first[:msats_load_at]) : nil
    ].flatten.join(CSV_SEPARATOR)

    # Generate 400 records only if we have variable data
    next if all_same_flag

    lines << to_nem12_400_csv(daily_data:)
  end

  lines.join(CRLF) + CRLF
end

#to_nem12_400_csv(daily_data:) ⇒ String

Output the AEMO::NEM12 to a valid NEM12 400 row CSV string.

Parameters:

  • daily_data (Array<Hash>)

Returns:

  • (String)

Since:

  • 0.1.4



487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/aemo/nem12.rb', line 487

def to_nem12_400_csv(daily_data:)
  daily_data.sort_by! { |x| x[:datetime] }

  nem12_400_rows = []

  daily_data.each_with_index do |x, i|
    nem12_400_rows << { flag: x[:flag], start_index: i + 1, finish_index: i + 1 } if nem12_400_rows.empty?

    if nem12_400_rows.last[:flag] == x[:flag]
      nem12_400_rows.last[:finish_index] = i + 1
      next
    end

    nem12_400_rows << { flag: x[:flag], start_index: i + 1, finish_index: i + 1 }
  end

  nem12_400_rows.map do |row|
    flag = row[:flag]
    # Default to 'A' if flag is nil, otherwise use explicit quality flag
    quality_flag = flag.nil? ? 'A' : flag.quality_flag
    method_flag = flag&.method_flag
    reason_code = flag&.reason_code

    # Build quality method string (quality flag + optional method flag)
    quality_method = if method_flag.nil?
                       quality_flag
                     else
                       "#{quality_flag}#{format('%02d', method_flag)}"
                     end

    [
      '400',
      row[:start_index],
      row[:finish_index],
      quality_method,
      reason_code || '',
      ''
    ].join(CSV_SEPARATOR)
  end.join(CRLF)
end

#to_nem12_900_csvString

Output the AEMO::NEM12 to a valid NEM12 900 row CSV string.

Returns:

  • (String)

Since:

  • 0.1.4



531
532
533
# File 'lib/aemo/nem12.rb', line 531

def to_nem12_900_csv
  self.class.default_nem12_900
end

#to_nem12_csvString

Output the AEMO::NEM12 to a valid NEM12 CSV string.

Returns:

  • (String)

Since:

  • 0.1.4



387
388
389
390
391
392
393
# File 'lib/aemo/nem12.rb', line 387

def to_nem12_csv
  [
    to_nem12_100_csv,
    to_nem12_200_csv,
    to_nem12_900_csv
  ].join
end