aemo 0.5.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/aemo/dispatchable.rb +2 -2
- data/lib/aemo/exceptions/invalid_nmi_allocation_type.rb +1 -1
- data/lib/aemo/exceptions/time_error.rb +20 -0
- data/lib/aemo/market/interval.rb +4 -4
- data/lib/aemo/market/node.rb +9 -3
- data/lib/aemo/market.rb +2 -4
- data/lib/aemo/msats.rb +70 -54
- data/lib/aemo/nem12/data_stream_suffix.rb +1 -1
- data/lib/aemo/nem12/quality_method.rb +25 -13
- data/lib/aemo/nem12/unit_of_measurement.rb +42 -42
- data/lib/aemo/nem12.rb +300 -94
- data/lib/aemo/nem13.rb +1 -2
- data/lib/aemo/nmi/allocation.rb +6 -7
- data/lib/aemo/nmi.rb +48 -38
- data/lib/aemo/region.rb +4 -3
- data/lib/aemo/register.rb +7 -7
- data/lib/aemo/struct.rb +3 -0
- data/lib/aemo/time.rb +112 -0
- data/lib/aemo/version.rb +1 -2
- data/lib/aemo.rb +13 -11
- data/lib/data/xml_to_json.rb +2 -4
- data/spec/aemo_spec.rb +0 -7
- data/spec/lib/aemo/market/interval_spec.rb +30 -12
- data/spec/lib/aemo/market/node_spec.rb +11 -9
- data/spec/lib/aemo/market_spec.rb +5 -4
- data/spec/lib/aemo/meter_spec.rb +2 -2
- data/spec/lib/aemo/msats_spec.rb +68 -56
- data/spec/lib/aemo/nem12_spec.rb +154 -43
- data/spec/lib/aemo/nmi/allocation_spec.rb +23 -18
- data/spec/lib/aemo/nmi_spec.rb +98 -72
- data/spec/lib/aemo/region_spec.rb +23 -18
- data/spec/spec_helper.rb +13 -13
- metadata +13 -435
data/lib/aemo/nem12.rb
CHANGED
@@ -14,6 +14,9 @@ module AEMO
|
|
14
14
|
# Namespace for classes and modules that handle AEMO Gem NEM12 interactions
|
15
15
|
# @since 0.1.4
|
16
16
|
class NEM12
|
17
|
+
CRLF = "\r\n"
|
18
|
+
CSV_SEPARATOR = ','
|
19
|
+
|
17
20
|
@file_contents = nil
|
18
21
|
@header = nil
|
19
22
|
@nmi_data_details = []
|
@@ -26,10 +29,108 @@ module AEMO
|
|
26
29
|
attr_reader :data_details, :interval_data, :interval_events
|
27
30
|
attr_accessor :file_contents, :header, :nmi_data_details, :nmi
|
28
31
|
|
32
|
+
# Class methods.
|
33
|
+
class << self
|
34
|
+
# @param [String] path_to_file the path to a file
|
35
|
+
# @return [Array<AEMO::NEM12>] NEM12 object
|
36
|
+
def parse_nem12_file(path_to_file, strict: true)
|
37
|
+
parse_nem12(File.read(path_to_file), strict:)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param [String] contents the path to a file
|
41
|
+
# @param [Boolean] strict
|
42
|
+
# @return [Array<AEMO::NEM12>] An array of NEM12 objects
|
43
|
+
def parse_nem12(contents, strict: true)
|
44
|
+
file_contents = contents.tr("\r", "\n").tr("\n\n", "\n").split("\n").delete_if(&:empty?)
|
45
|
+
# nothing to further process
|
46
|
+
return [] if file_contents.empty?
|
47
|
+
|
48
|
+
unless file_contents.first.parse_csv[0] == '100'
|
49
|
+
raise ArgumentError,
|
50
|
+
'First row should be have a RecordIndicator of 100 and be of type Header Record'
|
51
|
+
end
|
52
|
+
|
53
|
+
nem12s = []
|
54
|
+
header = AEMO::NEM12.parse_nem12_100(file_contents.first, strict:)
|
55
|
+
file_contents.each do |line|
|
56
|
+
case line[0..2].to_i
|
57
|
+
when 200
|
58
|
+
nem12s << AEMO::NEM12.new('')
|
59
|
+
nem12s.last.header = header
|
60
|
+
nem12s.last.parse_nem12_200(line, strict:)
|
61
|
+
when 300
|
62
|
+
nem12s.last.parse_nem12_300(line, strict:)
|
63
|
+
when 400
|
64
|
+
nem12s.last.parse_nem12_400(line, strict:)
|
65
|
+
# when 500
|
66
|
+
# nem12s.last.parse_nem12_500(line, strict: strict)
|
67
|
+
# when 900
|
68
|
+
# nem12s.last.parse_nem12_900(line, strict: strict)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
# Return the array of NEM12 groups
|
72
|
+
nem12s
|
73
|
+
end
|
74
|
+
|
75
|
+
# Parses the header record
|
76
|
+
# @param [String] line A single line in string format
|
77
|
+
# @param [Boolean] strict
|
78
|
+
# @return [Hash] the line parsed into a hash of information
|
79
|
+
def parse_nem12_100(line, strict: true) # rubocop:disable Naming/VariableNumber
|
80
|
+
csv = line.parse_csv
|
81
|
+
|
82
|
+
raise ArgumentError, 'RecordIndicator is not 100' if csv[0] != '100'
|
83
|
+
raise ArgumentError, 'VersionHeader is not NEM12' if csv[1] != 'NEM12'
|
84
|
+
|
85
|
+
raise ArgumentError, 'Time is not valid' if strict && !AEMO::Time.valid_timestamp12?(csv[2])
|
86
|
+
|
87
|
+
raise ArgumentError, 'FromParticipant is not valid' if csv[3].match(/.{1,10}/).nil?
|
88
|
+
raise ArgumentError, 'ToParticipant is not valid' if csv[4].match(/.{1,10}/).nil?
|
89
|
+
|
90
|
+
datetime = strict && AEMO::Time.valid_timestamp12?(csv[2]) ? AEMO::Time.parse_timestamp12(csv[2]) : nil
|
91
|
+
|
92
|
+
{
|
93
|
+
record_indicator: csv[0].to_i,
|
94
|
+
version_header: csv[1],
|
95
|
+
datetime:,
|
96
|
+
from_participant: csv[3],
|
97
|
+
to_participant: csv[4]
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
# Default NEM12 100 row record.
|
102
|
+
#
|
103
|
+
# @return [String]
|
104
|
+
def default_nem12_100 # rubocop:disable Naming/VariableNumber
|
105
|
+
timestamp = AEMO::Time.format_timestamp12(::Time.now)
|
106
|
+
|
107
|
+
"100,NEM12,#{timestamp},ENOSI,ENOSI#{CRLF}"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Default NEM12 100 row record.
|
111
|
+
#
|
112
|
+
# @return [String]
|
113
|
+
def default_nem12_900 # rubocop:disable Naming/VariableNumber
|
114
|
+
"900#{CRLF}"
|
115
|
+
end
|
116
|
+
|
117
|
+
# For a list of nem12s, turn into a single NEM12 CSV string with default header row.
|
118
|
+
#
|
119
|
+
# @param [Array<AEMO::NEM12>] nem12s
|
120
|
+
# @return [String]
|
121
|
+
def to_nem12_csv(nem12s:)
|
122
|
+
[
|
123
|
+
default_nem12_100,
|
124
|
+
nem12s.map(&:to_nem12_200_csv),
|
125
|
+
default_nem12_900
|
126
|
+
].flatten.join
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
29
130
|
# Initialize a NEM12 file
|
30
131
|
# @param [string] nmi
|
31
132
|
# @param [Hash] options
|
32
|
-
def initialize(nmi, options
|
133
|
+
def initialize(nmi, options: {})
|
33
134
|
@nmi = AEMO::NMI.new(nmi) unless nmi.empty?
|
34
135
|
@data_details = []
|
35
136
|
@interval_data = []
|
@@ -41,49 +142,42 @@ module AEMO
|
|
41
142
|
|
42
143
|
# Returns the NMI Identifier or nil
|
43
144
|
def nmi_identifier
|
44
|
-
@nmi
|
45
|
-
end
|
46
|
-
|
47
|
-
# Parses the header record
|
48
|
-
# @param [String] line A single line in string format
|
49
|
-
# @param [Hash] options
|
50
|
-
# @return [Hash] the line parsed into a hash of information
|
51
|
-
def self.parse_nem12_100(line, options = {})
|
52
|
-
csv = line.parse_csv
|
53
|
-
|
54
|
-
raise ArgumentError, 'RecordIndicator is not 100' if csv[0] != '100'
|
55
|
-
raise ArgumentError, 'VersionHeader is not NEM12' if csv[1] != 'NEM12'
|
56
|
-
raise ArgumentError, 'Time is not valid' if options[:strict] && (csv[2].match(/\d{12}/).nil? || csv[2] != Time.parse("#{csv[2]}00").strftime('%Y%m%d%H%M'))
|
57
|
-
raise ArgumentError, 'FromParticipant is not valid' if csv[3].match(/.{1,10}/).nil?
|
58
|
-
raise ArgumentError, 'ToParticipant is not valid' if csv[4].match(/.{1,10}/).nil?
|
59
|
-
|
60
|
-
{
|
61
|
-
record_indicator: csv[0].to_i,
|
62
|
-
version_header: csv[1],
|
63
|
-
datetime: Time.parse("#{csv[2]}+1000"),
|
64
|
-
from_participant: csv[3],
|
65
|
-
to_participant: csv[4]
|
66
|
-
}
|
145
|
+
@nmi&.nmi
|
67
146
|
end
|
68
147
|
|
69
148
|
# Parses the NMI Data Details
|
70
149
|
# @param [String] line A single line in string format
|
71
|
-
# @param [
|
150
|
+
# @param [Boolean] strict
|
72
151
|
# @return [Hash] the line parsed into a hash of information
|
73
|
-
def parse_nem12_200(line,
|
152
|
+
def parse_nem12_200(line, strict: true) # rubocop:disable Naming/VariableNumber
|
74
153
|
csv = line.parse_csv
|
75
154
|
|
76
155
|
raise ArgumentError, 'RecordIndicator is not 200' if csv[0] != '200'
|
77
156
|
raise ArgumentError, 'NMI is not valid' unless AEMO::NMI.valid_nmi?(csv[1])
|
78
|
-
|
157
|
+
|
158
|
+
if strict && (csv[2].nil? || csv[2].match(/.{1,240}/).nil?)
|
159
|
+
raise ArgumentError,
|
160
|
+
'NMIConfiguration is not valid'
|
161
|
+
end
|
162
|
+
|
79
163
|
raise ArgumentError, 'RegisterID is not valid' if !csv[3].nil? && csv[3].match(/.{1,10}/).nil?
|
80
164
|
raise ArgumentError, 'NMISuffix is not valid' if csv[4].nil? || csv[4].match(/[A-HJ-NP-Z][1-9A-HJ-NP-Z]/).nil?
|
81
|
-
|
82
|
-
|
165
|
+
|
166
|
+
if !csv[5].nil? && !csv[5].empty? && !csv[5].match(/^\s*$/) && csv[5].match(/[A-Z0-9]{2}/).nil?
|
167
|
+
raise ArgumentError,
|
168
|
+
'MDMDataStreamIdentifier is not valid'
|
169
|
+
end
|
170
|
+
|
171
|
+
if !csv[6].nil? && !csv[6].empty? && !csv[6].match(/^\s*$/) && csv[6].match(/[A-Z0-9]{2}/).nil?
|
172
|
+
raise ArgumentError,
|
173
|
+
'MeterSerialNumber is not valid'
|
174
|
+
end
|
175
|
+
|
83
176
|
raise ArgumentError, 'UOM is not valid' if csv[7].nil? || csv[7].upcase.match(/[A-Z0-9]{2}/).nil?
|
84
177
|
raise ArgumentError, 'UOM is not valid' unless UOM.keys.map(&:upcase).include?(csv[7].upcase)
|
85
178
|
raise ArgumentError, 'IntervalLength is not valid' unless %w[1 5 10 15 30].include?(csv[8])
|
86
|
-
|
179
|
+
|
180
|
+
# raise ArgumentError, 'NextScheduledReadDate is not valid' if !AEMO::Time.valid_timestamp8?(csv[9])
|
87
181
|
|
88
182
|
@nmi = AEMO::NMI.new(csv[1])
|
89
183
|
|
@@ -103,43 +197,64 @@ module AEMO
|
|
103
197
|
end
|
104
198
|
|
105
199
|
# @param [String] line A single line in string format
|
106
|
-
# @param [
|
200
|
+
# @param [Boolean] strict
|
107
201
|
# @return [Array of hashes] the line parsed into a hash of information
|
108
|
-
def parse_nem12_300(line,
|
202
|
+
def parse_nem12_300(line, strict: true) # rubocop:disable Naming/VariableNumber
|
109
203
|
csv = line.parse_csv
|
110
|
-
|
204
|
+
|
205
|
+
if @data_details.last.nil? || @data_details.last[:interval_length].nil?
|
206
|
+
raise TypeError,
|
207
|
+
'Expected NMI Data Details to exist with IntervalLength specified'
|
208
|
+
end
|
111
209
|
|
112
210
|
# ref: AEMO's MDFF Spec NEM12 and NEM13 v1.01 (2014-05-14)
|
113
211
|
record_fixed_fields = %w[RecordIndicator IntervalDate QualityMethod ReasonCode ReasonDescription UpdateDatetime MSATSLoadDateTime]
|
114
212
|
number_of_intervals = 1440 / @data_details.last[:interval_length]
|
213
|
+
|
115
214
|
raise TypeError, 'Invalid record length' if csv.length != record_fixed_fields.length + number_of_intervals
|
116
215
|
|
117
216
|
intervals_offset = number_of_intervals + 2
|
118
217
|
|
119
218
|
raise ArgumentError, 'RecordIndicator is not 300' if csv[0] != '300'
|
120
|
-
raise ArgumentError, 'IntervalDate is not valid'
|
219
|
+
raise ArgumentError, 'IntervalDate is not valid' unless AEMO::Time.valid_timestamp8?(csv[1])
|
220
|
+
|
121
221
|
(2..(number_of_intervals + 1)).each do |i|
|
122
222
|
raise ArgumentError, "Interval number #{i - 1} is not valid" if csv[i].nil? || csv[i].match(/\d+(\.\d+)?/).nil?
|
123
223
|
end
|
124
|
-
|
224
|
+
|
225
|
+
raise ArgumentError, 'QualityMethod is not valid' unless csv[intervals_offset + 0].instance_of?(String)
|
125
226
|
raise ArgumentError, 'QualityMethod does not have valid length' unless [1, 3].include?(csv[intervals_offset + 0].length)
|
126
|
-
|
227
|
+
|
228
|
+
unless QUALITY_FLAGS.keys.include?(csv[intervals_offset + 0][0])
|
229
|
+
raise ArgumentError,
|
230
|
+
'QualityMethod does not have valid QualityFlag'
|
231
|
+
end
|
232
|
+
|
127
233
|
unless %w[A N V].include?(csv[intervals_offset + 0][0])
|
128
234
|
raise ArgumentError, 'QualityMethod does not have valid length' unless csv[intervals_offset + 0].length == 3
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
235
|
+
|
236
|
+
unless METHOD_FLAGS.keys.include?(csv[intervals_offset + 0][1..2].to_i)
|
237
|
+
raise ArgumentError,
|
238
|
+
'QualityMethod does not have valid MethodFlag'
|
239
|
+
end
|
133
240
|
end
|
134
|
-
|
135
|
-
|
241
|
+
|
242
|
+
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)
|
243
|
+
|
244
|
+
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?)
|
245
|
+
raise ArgumentError,
|
246
|
+
'ReasonDescription is not valid'
|
136
247
|
end
|
137
|
-
|
138
|
-
|
139
|
-
|
248
|
+
|
249
|
+
if strict
|
250
|
+
unless AEMO::Time.valid_timestamp14?(csv[intervals_offset + 3])
|
251
|
+
raise ArgumentError,
|
252
|
+
'UpdateDateTime is not valid'
|
140
253
|
end
|
141
|
-
|
142
|
-
|
254
|
+
|
255
|
+
if !csv[intervals_offset + 4].blank? && !AEMO::Time.valid_timestamp14?(csv[intervals_offset + 4])
|
256
|
+
raise ArgumentError,
|
257
|
+
'MSATSLoadDateTime is not valid'
|
143
258
|
end
|
144
259
|
end
|
145
260
|
|
@@ -159,18 +274,18 @@ module AEMO
|
|
159
274
|
updated_at = nil
|
160
275
|
msats_load_at = nil
|
161
276
|
|
162
|
-
if
|
163
|
-
updated_at = Time.
|
164
|
-
msats_load_at = Time.
|
277
|
+
if strict
|
278
|
+
updated_at = AEMO::Time.parse_timestamp14(csv[intervals_offset + 3]) unless csv[intervals_offset + 3].blank?
|
279
|
+
msats_load_at = AEMO::Time.parse_timestamp14(csv[intervals_offset + 4]) unless csv[intervals_offset + 4].blank?
|
165
280
|
end
|
166
281
|
|
167
282
|
base_interval = {
|
168
283
|
data_details: @data_details.last,
|
169
|
-
datetime: Time.
|
284
|
+
datetime: AEMO::Time.parse_timestamp8(csv[1]),
|
170
285
|
value: nil,
|
171
|
-
flag
|
172
|
-
updated_at
|
173
|
-
msats_load_at:
|
286
|
+
flag:,
|
287
|
+
updated_at:,
|
288
|
+
msats_load_at:
|
174
289
|
}
|
175
290
|
|
176
291
|
intervals = []
|
@@ -185,14 +300,19 @@ module AEMO
|
|
185
300
|
end
|
186
301
|
|
187
302
|
# @param [String] line A single line in string format
|
188
|
-
# @param [
|
303
|
+
# @param [Boolean] strict
|
189
304
|
# @return [Hash] the line parsed into a hash of information
|
190
|
-
def parse_nem12_400(line,
|
305
|
+
def parse_nem12_400(line, strict: true) # rubocop:disable Lint/UnusedMethodArgument,Naming/VariableNumber
|
191
306
|
csv = line.parse_csv
|
192
307
|
raise ArgumentError, 'RecordIndicator is not 400' if csv[0] != '400'
|
193
308
|
raise ArgumentError, 'StartInterval is not valid' if csv[1].nil? || csv[1].match(/^\d+$/).nil?
|
194
309
|
raise ArgumentError, 'EndInterval is not valid' if csv[2].nil? || csv[2].match(/^\d+$/).nil?
|
195
|
-
|
310
|
+
|
311
|
+
if csv[3].nil? || csv[3].match(/^([AN]|([AEFNSV]\d{2}))$/).nil?
|
312
|
+
raise ArgumentError,
|
313
|
+
'QualityMethod is not valid'
|
314
|
+
end
|
315
|
+
|
196
316
|
# raise ArgumentError, 'ReasonCode is not valid' if (csv[4].nil? && csv[3].match(/^ANE/)) || csv[4].match(/^\d{3}?$/) || csv[3].match(/^ANE/)
|
197
317
|
# raise ArgumentError, 'ReasonDescription is not valid' if (csv[4].nil? && csv[3].match(/^ANE/)) || ( csv[5].match(/^$/) && csv[4].match(/^0$/) )
|
198
318
|
|
@@ -204,7 +324,8 @@ module AEMO
|
|
204
324
|
interval_start_point = @interval_data.length - number_of_intervals
|
205
325
|
|
206
326
|
# For each of these
|
207
|
-
base_interval_event = { datetime: nil, quality_method: csv[3], reason_code:
|
327
|
+
base_interval_event = { datetime: nil, quality_method: csv[3], reason_code: csv[4]&.to_i,
|
328
|
+
reason_description: csv[5] }
|
208
329
|
|
209
330
|
# Interval Numbers are 1-indexed
|
210
331
|
((csv[1].to_i)..(csv[2].to_i)).each do |i|
|
@@ -229,16 +350,16 @@ module AEMO
|
|
229
350
|
# What even is a 500 row?
|
230
351
|
#
|
231
352
|
# @param [String] line A single line in string format
|
232
|
-
# @param [
|
353
|
+
# @param [Boolean] strict
|
233
354
|
# @return [Hash] the line parsed into a hash of information
|
234
|
-
def parse_nem12_500(_line,
|
355
|
+
def parse_nem12_500(_line, strict: true); end # rubocop:disable Naming/VariableNumber
|
235
356
|
|
236
357
|
# 900 is the last row a NEM12 should see...
|
237
358
|
#
|
238
359
|
# @param [String] line A single line in string format
|
239
|
-
# @param [
|
360
|
+
# @param [Boolean] strict
|
240
361
|
# @return [Hash] the line parsed into a hash of information
|
241
|
-
def parse_nem12_900(_line,
|
362
|
+
def parse_nem12_900(_line, strict: true); end # rubocop:disable Naming/VariableNumber
|
242
363
|
|
243
364
|
# Turns the flag to a string
|
244
365
|
#
|
@@ -272,48 +393,133 @@ module AEMO
|
|
272
393
|
def to_csv
|
273
394
|
headers = %w[nmi suffix units datetime value flags]
|
274
395
|
([headers] + to_a.map do |row|
|
275
|
-
row[3] = row[3].strftime('%Y%m%d%
|
396
|
+
row[3] = row[3].strftime('%Y%m%d%TH%M%S%z')
|
276
397
|
row
|
277
398
|
end).map do |row|
|
278
399
|
row.join(', ')
|
279
400
|
end.join("\n")
|
280
401
|
end
|
281
402
|
|
282
|
-
#
|
283
|
-
#
|
284
|
-
|
285
|
-
|
403
|
+
# Output the AEMO::NEM12 to a valid NEM12 CSV string.
|
404
|
+
#
|
405
|
+
# @return [String]
|
406
|
+
def to_nem12_csv
|
407
|
+
[
|
408
|
+
to_nem12_100_csv,
|
409
|
+
to_nem12_200_csv,
|
410
|
+
to_nem12_900_csv
|
411
|
+
].flatten.join
|
286
412
|
end
|
287
413
|
|
288
|
-
#
|
289
|
-
#
|
290
|
-
# @return [
|
291
|
-
def
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
414
|
+
# Output the AEMO::NEM12 to a valid NEM12 100 row CSV string.
|
415
|
+
#
|
416
|
+
# @return [String]
|
417
|
+
def to_nem12_100_csv
|
418
|
+
return self.class.default_nem12_100 if header.nil?
|
419
|
+
|
420
|
+
[
|
421
|
+
header[:record_indicator],
|
422
|
+
header[:version_header],
|
423
|
+
AEMO::Time.format_timestamp12(header[:datetime]),
|
424
|
+
header[:from_participant],
|
425
|
+
header[:to_participant]
|
426
|
+
].join(CSV_SEPARATOR) + CRLF
|
427
|
+
end
|
428
|
+
|
429
|
+
# Output the AEMO::NEM12 to a valid NEM12 200 row CSV string.
|
430
|
+
#
|
431
|
+
# @return [String]
|
432
|
+
def to_nem12_200_csv
|
433
|
+
return nil if data_details.length != 1
|
434
|
+
|
435
|
+
data_detail = data_details.first
|
436
|
+
|
437
|
+
[
|
438
|
+
[
|
439
|
+
data_detail[:record_indicator],
|
440
|
+
data_detail[:nmi],
|
441
|
+
data_detail[:nmi_configuration],
|
442
|
+
data_detail[:register_id],
|
443
|
+
data_detail[:nmi_suffix],
|
444
|
+
data_detail[:mdm_data_streaming_identifier],
|
445
|
+
data_detail[:meter_serial_number],
|
446
|
+
data_detail[:uom],
|
447
|
+
data_detail[:interval_length],
|
448
|
+
data_detail[:next_scheduled_read_date] # NOTE: this is not turned into a timestamp.
|
449
|
+
].join(CSV_SEPARATOR),
|
450
|
+
to_nem12_300_csv
|
451
|
+
].flatten.join(CRLF)
|
452
|
+
end
|
453
|
+
|
454
|
+
# Output the AEMO::NEM12 to a valid NEM12 300 row CSV string.
|
455
|
+
#
|
456
|
+
# @return [String]
|
457
|
+
def to_nem12_300_csv
|
458
|
+
lines = []
|
459
|
+
|
460
|
+
daily_datas = interval_data.group_by do |x|
|
461
|
+
AEMO::Time.format_timestamp8(x[:datetime] - 1.second)
|
462
|
+
end
|
463
|
+
daily_datas.keys.sort.each do |key|
|
464
|
+
daily_data = daily_datas[key].sort_by { |x| x[:datetime] }
|
465
|
+
has_flags = daily_data.map { |x| x[:flag]&.any? }.uniq.include?(true)
|
466
|
+
|
467
|
+
lines << [
|
468
|
+
'300',
|
469
|
+
key,
|
470
|
+
daily_data.map { |x| x[:value] },
|
471
|
+
has_flags ? 'V' : 'A',
|
472
|
+
'',
|
473
|
+
'',
|
474
|
+
daily_data.first[:updated_at] ? AEMO::Time.format_timestamp14(daily_data.first[:updated_at]) : nil,
|
475
|
+
daily_data.first[:msats_load_at] ? AEMO::Time.format_timestamp14(daily_data.first[:msats_load_at]) : nil
|
476
|
+
].flatten.join(CSV_SEPARATOR)
|
477
|
+
|
478
|
+
next unless has_flags
|
479
|
+
|
480
|
+
lines << to_nem12_400_csv(daily_data:)
|
481
|
+
end
|
482
|
+
|
483
|
+
lines.join(CRLF) + CRLF
|
484
|
+
end
|
485
|
+
|
486
|
+
# Output the AEMO::NEM12 to a valid NEM12 400 row CSV string.
|
487
|
+
#
|
488
|
+
# @param [Array<Hash>] daily_data
|
489
|
+
# @return [String]
|
490
|
+
def to_nem12_400_csv(daily_data:)
|
491
|
+
daily_data.sort_by! { |x| x[:datetime] }
|
492
|
+
|
493
|
+
nem12_400_rows = []
|
494
|
+
|
495
|
+
daily_data.each_with_index do |x, i|
|
496
|
+
nem12_400_rows << { flag: x[:flag], start_index: i + 1, finish_index: i + 1 } if nem12_400_rows.empty?
|
497
|
+
|
498
|
+
if nem12_400_rows.last[:flag] == x[:flag]
|
499
|
+
nem12_400_rows.last[:finish_index] = i + 1
|
500
|
+
next
|
313
501
|
end
|
502
|
+
|
503
|
+
nem12_400_rows << { flag: x[:flag], start_index: i + 1, finish_index: i + 1 }
|
314
504
|
end
|
315
|
-
|
316
|
-
|
505
|
+
|
506
|
+
nem12_400_rows.map do |row|
|
507
|
+
[
|
508
|
+
'400',
|
509
|
+
row[:start_index],
|
510
|
+
row[:finish_index],
|
511
|
+
row[:flag].nil? ? 'A' : "#{row[:flag][:quality_flag]}#{row[:flag][:method_flag]}",
|
512
|
+
row[:flag].nil? ? '' : row[:flag][:reason_code],
|
513
|
+
''
|
514
|
+
].join(CSV_SEPARATOR)
|
515
|
+
end.join(CRLF)
|
516
|
+
end
|
517
|
+
|
518
|
+
# Output the AEMO::NEM12 to a valid NEM12 900 row CSV string.
|
519
|
+
#
|
520
|
+
# @return [String]
|
521
|
+
def to_nem12_900_csv
|
522
|
+
self.class.default_nem12_900
|
317
523
|
end
|
318
524
|
end
|
319
525
|
end
|
data/lib/aemo/nem13.rb
CHANGED
data/lib/aemo/nmi/allocation.rb
CHANGED
@@ -155,7 +155,7 @@ module AEMO
|
|
155
155
|
type: 'electricity',
|
156
156
|
includes: [
|
157
157
|
/^(SAAA[A-HJ-NP-VX-Z\d][A-HJ-NP-Z\d]{5})$/,
|
158
|
-
/^(SASMPL
|
158
|
+
/^(SASMPL\d{4})$/,
|
159
159
|
/^(200[12]\d{6})$/
|
160
160
|
],
|
161
161
|
excludes: []
|
@@ -405,8 +405,8 @@ module AEMO
|
|
405
405
|
# Enumerable support
|
406
406
|
#
|
407
407
|
# @return [Enumerator]
|
408
|
-
def each(&
|
409
|
-
all.each(&
|
408
|
+
def each(&)
|
409
|
+
all.each(&)
|
410
410
|
end
|
411
411
|
|
412
412
|
# Finds the Allocation that encompasses a given NMI
|
@@ -444,7 +444,7 @@ module AEMO
|
|
444
444
|
@friendly_title = opts.fetch(:friendly_title, title)
|
445
445
|
@exclude_nmi_patterns = opts.fetch(:excludes, [])
|
446
446
|
@include_nmi_patterns = opts.fetch(:includes, [])
|
447
|
-
@region = AEMO::Region.new(opts.fetch(:region)) if opts
|
447
|
+
@region = AEMO::Region.new(opts.fetch(:region)) if opts[:region]
|
448
448
|
end
|
449
449
|
|
450
450
|
private
|
@@ -456,9 +456,8 @@ module AEMO
|
|
456
456
|
# @return [Symbol]
|
457
457
|
def parse_allocation_type(type)
|
458
458
|
type_sym = type.to_sym
|
459
|
-
unless SUPPORTED_TYPES.include?(type_sym)
|
460
|
-
|
461
|
-
end
|
459
|
+
raise AEMO::InvalidNMIAllocationType unless SUPPORTED_TYPES.include?(type_sym)
|
460
|
+
|
462
461
|
type_sym
|
463
462
|
rescue NoMethodError
|
464
463
|
raise AEMO::InvalidNMIAllocationType
|