aemo 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +14 -10
- 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 +21 -443
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
|