Class: AEMO::NEM12
- Inherits:
-
Object
- Object
- AEMO::NEM12
- Defined in:
- lib/aemo/nem12.rb
Overview
Namespace for classes and modules that handle AEMO Gem NEM12 interactions
Constant Summary collapse
- 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
{ 100 => 'Header', 200 => 'NMI Data Details', 300 => 'Interval Data', 400 => 'Interval Event', 500 => 'B2B Details', 900 => 'End' }.freeze
- TRANSACTION_CODE_FLAGS =
{ '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
- UOM =
{ '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 =
{ '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
- QUALITY_FLAGS =
{ 'A' => 'Actual Data', 'E' => 'Forward Estimated Data', 'F' => 'Final Substituted Data', 'N' => 'Null Data', 'S' => 'Substituted Data', 'V' => 'Variable Data' }.freeze
- METHOD_FLAGS =
{ 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: '' }, 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: '' }, 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: '' }, 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
- REASON_CODES =
{ 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
- DATA_STREAM_SUFFIX =
{ # 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
Instance Attribute Summary collapse
- #data_details ⇒ Object readonly
- #file_contents ⇒ Object
- #header ⇒ Object
- #interval_data ⇒ Object readonly
- #interval_events ⇒ Object readonly
- #nmi ⇒ Object
- #nmi_data_details ⇒ Object
Class Method Summary collapse
-
.parse_nem12(contents, strict = true) ⇒ Array<AEMO::NEM12>
An array of NEM12 objects.
-
.parse_nem12_100(line, options = {}) ⇒ Hash
Parses the header record.
-
.parse_nem12_file(path_to_file, strict = true) ⇒ Array<AEMO::NEM12>
NEM12 object.
Instance Method Summary collapse
-
#flag_to_s(flag) ⇒ nil, String
Turns the flag to a string.
-
#initialize(nmi, options = {}) ⇒ NEM12
constructor
Initialize a NEM12 file.
-
#nmi_identifier ⇒ Object
Returns the NMI Identifier or nil.
-
#parse_nem12_200(line, options = {}) ⇒ Hash
Parses the NMI Data Details.
-
#parse_nem12_300(line, options = {}) ⇒ Array of hashes
The line parsed into a hash of information.
-
#parse_nem12_400(line, options = {}) ⇒ Hash
The line parsed into a hash of information.
-
#parse_nem12_500(_line, _options = {}) ⇒ Hash
What even is a 500 row?.
-
#parse_nem12_900(_line, _options = {}) ⇒ Hash
900 is the last row a NEM12 should seeā¦
-
#to_a ⇒ Array
Array of a NEM12 file a given Meter + Data Stream for easy reading.
-
#to_csv ⇒ Array
CSV of a NEM12 file a given Meter + Data Stream for easy reading.
Constructor Details
#initialize(nmi, options = {}) ⇒ NEM12
Initialize a NEM12 file
279 280 281 282 283 284 285 286 287 |
# File 'lib/aemo/nem12.rb', line 279 def initialize(nmi, = {}) @nmi = AEMO::NMI.new(nmi) unless nmi.empty? @data_details = [] @interval_data = [] @interval_events = [] .each_key do |key| send 'key=', [key] end end |
Instance Attribute Details
#data_details ⇒ Object (readonly)
273 274 275 |
# File 'lib/aemo/nem12.rb', line 273 def data_details @data_details end |
#file_contents ⇒ Object
274 275 276 |
# File 'lib/aemo/nem12.rb', line 274 def file_contents @file_contents end |
#header ⇒ Object
274 275 276 |
# File 'lib/aemo/nem12.rb', line 274 def header @header end |
#interval_data ⇒ Object (readonly)
273 274 275 |
# File 'lib/aemo/nem12.rb', line 273 def interval_data @interval_data end |
#interval_events ⇒ Object (readonly)
273 274 275 |
# File 'lib/aemo/nem12.rb', line 273 def interval_events @interval_events end |
#nmi ⇒ Object
274 275 276 |
# File 'lib/aemo/nem12.rb', line 274 def nmi @nmi end |
#nmi_data_details ⇒ Object
274 275 276 |
# File 'lib/aemo/nem12.rb', line 274 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.
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 |
# File 'lib/aemo/nem12.rb', line 538 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
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'lib/aemo/nem12.rb', line 298 def self.parse_nem12_100(line, = {}) 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] && (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.
531 532 533 |
# File 'lib/aemo/nem12.rb', line 531 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
494 495 496 497 498 499 500 501 502 |
# File 'lib/aemo/nem12.rb', line 494 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_identifier ⇒ Object
Returns the NMI Identifier or nil
290 291 292 |
# File 'lib/aemo/nem12.rb', line 290 def nmi_identifier @nmi.nil? ? nil : @nmi.nmi end |
#parse_nem12_200(line, options = {}) ⇒ Hash
Parses the NMI Data Details
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
# File 'lib/aemo/nem12.rb', line 320 def parse_nem12_200(line, = {}) 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 [: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_nubmer: 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.
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 |
# File 'lib/aemo/nem12.rb', line 355 def parse_nem12_300(line, = {}) 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 [: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 [: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.
437 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 |
# File 'lib/aemo/nem12.rb', line 437 def parse_nem12_400(line, = {}) 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?
481 |
# File 'lib/aemo/nem12.rb', line 481 def parse_nem12_500(_line, = {}); end |
#parse_nem12_900(_line, _options = {}) ⇒ Hash
900 is the last row a NEM12 should seeā¦
488 |
# File 'lib/aemo/nem12.rb', line 488 def parse_nem12_900(_line, = {}); end |
#to_a ⇒ Array
Returns array of a NEM12 file a given Meter + Data Stream for easy reading.
505 506 507 508 509 510 511 512 513 514 515 516 |
# File 'lib/aemo/nem12.rb', line 505 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_csv ⇒ Array
Returns CSV of a NEM12 file a given Meter + Data Stream for easy reading.
519 520 521 522 523 524 525 526 527 |
# File 'lib/aemo/nem12.rb', line 519 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 |