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
- CRLF =
"\r\n"
- CSV_SEPARATOR =
','
- 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: 'Retrospective Like Day', description: 'Updated v7.8' }, 15 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Retrospective Average Like Day', description: 'Updated v7.8' }, 16 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Agreed', description: '[OBSOLETE] v7.8' }, 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: 'Prospective Like Day', description: 'Updated v7.8' }, 21 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Five-minute No Historical Data', description: 'Added v7.8' }, 22 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Prospective Ave Like Day', description: 'Added v7.8' }, 23 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Previous Year', description: 'Added v7.8' }, 24 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'Data Scaling', description: 'Added v7.8' }, 25 => { type: %w[SUB], installation_type: [1, 2, 3, 4], short_descriptor: 'ADL', description: 'Added v7.8' }, 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
-
.default_nem12_100 ⇒ String
Default NEM12 100 row record.
-
.default_nem12_900 ⇒ String
Default NEM12 100 row record.
-
.parse_nem12(contents, strict: true) ⇒ Array<AEMO::NEM12>
An array of NEM12 objects.
-
.parse_nem12_100(line, strict: true) ⇒ Hash
Parses the header record.
-
.parse_nem12_file(path_to_file, strict: true) ⇒ Array<AEMO::NEM12>
NEM12 object.
-
.to_nem12_csv(nem12s:) ⇒ String
For a list of nem12s, turn into a single NEM12 CSV string with default header row.
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, strict: true) ⇒ Hash
Parses the NMI Data Details.
-
#parse_nem12_300(line, strict: true) ⇒ Array of hashes
The line parsed into a hash of information.
-
#parse_nem12_400(line, strict: true) ⇒ Hash
The line parsed into a hash of information.
-
#parse_nem12_500(_line, strict: true) ⇒ Hash
What even is a 500 row?.
-
#parse_nem12_900(_line, strict: true) ⇒ 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.
-
#to_nem12_100_csv ⇒ String
Output the AEMO::NEM12 to a valid NEM12 100 row CSV string.
-
#to_nem12_200_csv ⇒ String
Output the AEMO::NEM12 to a valid NEM12 200 row CSV string.
-
#to_nem12_300_csv ⇒ String
Output the AEMO::NEM12 to a valid NEM12 300 row CSV string.
-
#to_nem12_400_csv(daily_data:) ⇒ String
Output the AEMO::NEM12 to a valid NEM12 400 row CSV string.
-
#to_nem12_900_csv ⇒ String
Output the AEMO::NEM12 to a valid NEM12 900 row CSV string.
-
#to_nem12_csv ⇒ String
Output the AEMO::NEM12 to a valid NEM12 CSV string.
Constructor Details
#initialize(nmi, options: {}) ⇒ NEM12
Initialize a NEM12 file
133 134 135 136 137 138 139 140 141 |
# File 'lib/aemo/nem12.rb', line 133 def initialize(nmi, options: {}) @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)
29 30 31 |
# File 'lib/aemo/nem12.rb', line 29 def data_details @data_details end |
#file_contents ⇒ Object
30 31 32 |
# File 'lib/aemo/nem12.rb', line 30 def file_contents @file_contents end |
#header ⇒ Object
30 31 32 |
# File 'lib/aemo/nem12.rb', line 30 def header @header end |
#interval_data ⇒ Object (readonly)
29 30 31 |
# File 'lib/aemo/nem12.rb', line 29 def interval_data @interval_data end |
#interval_events ⇒ Object (readonly)
29 30 31 |
# File 'lib/aemo/nem12.rb', line 29 def interval_events @interval_events end |
#nmi ⇒ Object
30 31 32 |
# File 'lib/aemo/nem12.rb', line 30 def nmi @nmi end |
#nmi_data_details ⇒ Object
30 31 32 |
# File 'lib/aemo/nem12.rb', line 30 def nmi_data_details @nmi_data_details end |
Class Method Details
.default_nem12_100 ⇒ String
Default NEM12 100 row record.
104 105 106 107 108 |
# File 'lib/aemo/nem12.rb', line 104 def default_nem12_100 # rubocop:disable Naming/VariableNumber = AEMO::Time.(::Time.now) "100,NEM12,#{},ENOSI,ENOSI#{CRLF}" end |
.default_nem12_900 ⇒ String
Default NEM12 100 row record.
113 114 115 |
# File 'lib/aemo/nem12.rb', line 113 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.
43 44 45 46 47 48 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 |
# File 'lib/aemo/nem12.rb', line 43 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
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/aemo/nem12.rb', line 79 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.(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.(csv[2]) ? AEMO::Time.(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.
36 37 38 |
# File 'lib/aemo/nem12.rb', line 36 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.
121 122 123 124 125 126 127 |
# File 'lib/aemo/nem12.rb', line 121 def to_nem12_csv(nem12s:) [ default_nem12_100, nem12s.map(&:to_nem12_200_csv), default_nem12_900 ].flatten.join end |
Instance Method Details
#flag_to_s(flag) ⇒ nil, String
Turns the flag to a string
368 369 370 371 372 373 374 375 376 |
# File 'lib/aemo/nem12.rb', line 368 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
144 145 146 |
# File 'lib/aemo/nem12.rb', line 144 def nmi_identifier @nmi&.nmi end |
#parse_nem12_200(line, strict: true) ⇒ Hash
Parses the NMI Data Details
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 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/aemo/nem12.rb', line 152 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.
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 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 298 299 300 |
# File 'lib/aemo/nem12.rb', line 202 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.(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.(csv[intervals_offset + 3]) raise ArgumentError, 'UpdateDateTime is not valid' end if !csv[intervals_offset + 4].blank? && !AEMO::Time.(csv[intervals_offset + 4]) 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 = AEMO::Time.(csv[intervals_offset + 3]) unless csv[intervals_offset + 3].blank? msats_load_at = AEMO::Time.(csv[intervals_offset + 4]) unless csv[intervals_offset + 4].blank? end base_interval = { data_details: @data_details.last, datetime: AEMO::Time.(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.
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 344 345 346 347 348 |
# File 'lib/aemo/nem12.rb', line 305 def parse_nem12_400(line, strict: true) # rubocop:disable Lint/UnusedMethodArgument,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 = [] # 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]&.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, strict: true) ⇒ Hash
What even is a 500 row?
355 |
# File 'lib/aemo/nem12.rb', line 355 def parse_nem12_500(_line, strict: true); end |
#parse_nem12_900(_line, strict: true) ⇒ Hash
900 is the last row a NEM12 should seeā¦
362 |
# File 'lib/aemo/nem12.rb', line 362 def parse_nem12_900(_line, strict: true); end |
#to_a ⇒ Array
Returns array of a NEM12 file a given Meter + Data Stream for easy reading.
379 380 381 382 383 384 385 386 387 388 389 390 |
# File 'lib/aemo/nem12.rb', line 379 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.
393 394 395 396 397 398 399 400 401 |
# File 'lib/aemo/nem12.rb', line 393 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_csv ⇒ String
Output the AEMO::NEM12 to a valid NEM12 100 row CSV string.
417 418 419 420 421 422 423 424 425 426 427 |
# File 'lib/aemo/nem12.rb', line 417 def to_nem12_100_csv return self.class.default_nem12_100 if header.nil? [ header[:record_indicator], header[:version_header], AEMO::Time.(header[:datetime]), header[:from_participant], header[:to_participant] ].join(CSV_SEPARATOR) + CRLF end |
#to_nem12_200_csv ⇒ String
Output the AEMO::NEM12 to a valid NEM12 200 row CSV string.
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 |
# File 'lib/aemo/nem12.rb', line 432 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_csv ⇒ String
Output the AEMO::NEM12 to a valid NEM12 300 row CSV string.
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 482 483 484 |
# File 'lib/aemo/nem12.rb', line 457 def to_nem12_300_csv lines = [] daily_datas = interval_data.group_by do |x| AEMO::Time.(x[:datetime] - 1.second) end daily_datas.keys.sort.each do |key| daily_data = daily_datas[key].sort_by { |x| x[:datetime] } has_flags = daily_data.map { |x| x[:flag]&.any? }.uniq.include?(true) lines << [ '300', key, daily_data.map { |x| x[:value] }, has_flags ? 'V' : 'A', '', '', daily_data.first[:updated_at] ? AEMO::Time.(daily_data.first[:updated_at]) : nil, daily_data.first[:msats_load_at] ? AEMO::Time.(daily_data.first[:msats_load_at]) : nil ].flatten.join(CSV_SEPARATOR) next unless has_flags 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.
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 |
# File 'lib/aemo/nem12.rb', line 490 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| [ '400', row[:start_index], row[:finish_index], row[:flag].nil? ? 'A' : "#{row[:flag][:quality_flag]}#{row[:flag][:method_flag]}", row[:flag].nil? ? '' : row[:flag][:reason_code], '' ].join(CSV_SEPARATOR) end.join(CRLF) end |
#to_nem12_900_csv ⇒ String
Output the AEMO::NEM12 to a valid NEM12 900 row CSV string.
521 522 523 |
# File 'lib/aemo/nem12.rb', line 521 def to_nem12_900_csv self.class.default_nem12_900 end |
#to_nem12_csv ⇒ String
Output the AEMO::NEM12 to a valid NEM12 CSV string.
406 407 408 409 410 411 412 |
# File 'lib/aemo/nem12.rb', line 406 def to_nem12_csv [ to_nem12_100_csv, to_nem12_200_csv, to_nem12_900_csv ].flatten.join end |