Class: AEMO::NEM12
- Inherits:
-
Object
- Object
- AEMO::NEM12
- 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
Constant Summary collapse
- 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
- 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: '' }, 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
{ 100 => 'Header', 200 => 'NMI Data Details', 300 => 'Interval Data', 400 => 'Interval Event', 500 => 'B2B Details', 900 => 'End' }.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
- 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
- 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
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
32 33 34 35 36 37 38 39 40 |
# File 'lib/aemo/nem12.rb', line 32 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)
26 27 28 |
# File 'lib/aemo/nem12.rb', line 26 def data_details @data_details end |
#file_contents ⇒ Object
27 28 29 |
# File 'lib/aemo/nem12.rb', line 27 def file_contents @file_contents end |
#header ⇒ Object
27 28 29 |
# File 'lib/aemo/nem12.rb', line 27 def header @header end |
#interval_data ⇒ Object (readonly)
26 27 28 |
# File 'lib/aemo/nem12.rb', line 26 def interval_data @interval_data end |
#interval_events ⇒ Object (readonly)
26 27 28 |
# File 'lib/aemo/nem12.rb', line 26 def interval_events @interval_events end |
#nmi ⇒ Object
27 28 29 |
# File 'lib/aemo/nem12.rb', line 27 def nmi @nmi end |
#nmi_data_details ⇒ Object
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.
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
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, = {}) 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.
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
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_identifier ⇒ Object
Returns the NMI Identifier or nil
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
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, = {}) 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_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.
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, = {}) 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.
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, = {}) 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?
234 |
# File 'lib/aemo/nem12.rb', line 234 def parse_nem12_500(_line, = {}); end |
#parse_nem12_900(_line, _options = {}) ⇒ Hash
900 is the last row a NEM12 should seeā¦
241 |
# File 'lib/aemo/nem12.rb', line 241 def parse_nem12_900(_line, = {}); end |
#to_a ⇒ Array
Returns array of a NEM12 file a given Meter + Data Stream for easy reading.
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_csv ⇒ Array
Returns CSV of a NEM12 file a given Meter + Data Stream for easy reading.
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 |