Class: AEMO::NMI Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/aemo/nmi.rb,
lib/aemo/nmi/allocation.rb

Overview

This class is abstract.

Model for a National Metering Identifier.

AEMO::NMI

AEMO::NMI acts as an object to simplify access to data and information

about a NMI and provide verification of the NMI value

Author:

  • Joel Courtney

Since:

  • 2014-12-05

Defined Under Namespace

Classes: Allocation

Constant Summary collapse

REGIONS =

Operational Regions for the NMI

Since:

  • 2014-12-05

{
  'ACT' => 'Australian Capital Territory',
  'NSW' => 'New South Wales',
  'QLD' => 'Queensland',
  'SA' => 'South Australia',
  'TAS' => 'Tasmania',
  'VIC' => 'Victoria',
  'WA' => 'Western Australia',
  'NT' => 'Northern Territory'
}.freeze
TNI_CODES =

Transmission Node Identifier Codes are loaded from a json file

Obtained from http://www.nemweb.com.au/

See /lib/data for further data manipulation required

Since:

  • 2014-12-05

JSON.parse(
  File.read(
    File.join(File.dirname(__FILE__), '..', 'data', 'aemo-tni.json')
  )
).freeze
DLF_CODES =

Distribution Loss Factor Codes are loaded from a json file

Obtained from MSATS, matching to DNSP from file

www.aemo.com.au/-/media/Files/Electricity/NEM/

 Security_and_Reliability/Loss_Factors_and_Regional_Boundaries/
 2016/DLF_V3_2016_2017.pdf

Last accessed 2017-08-01
See /lib/data for further data manipulation required

Since:

  • 2014-12-05

JSON.parse(
  File.read(
    File.join(File.dirname(__FILE__), '..', 'data', 'aemo-dlf.json')
  )
).freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(nmi, options = {}) ⇒ AEMO::NMI

Initialize a NMI file

Parameters:

  • nmi (String)

    the National Meter Identifier (NMI)

  • options (Hash) (defaults to: {})

    a hash of options

Options Hash (options):

  • :msats_detail (Hash)

    MSATS details as per #parse_msats_detail requirements

Raises:

  • (ArgumentError)

Since:

  • 2014-12-05



115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/aemo/nmi.rb', line 115

def initialize(nmi, options = {})
  raise ArgumentError, 'NMI is not a string' unless nmi.is_a?(String)
  raise ArgumentError, 'NMI is not 10 characters' unless nmi.length == 10
  raise ArgumentError, 'NMI is not constructed with valid characters' unless AEMO::NMI.valid_nmi?(nmi)

  @nmi          = nmi
  @meters       = []
  @roles        = {}
  @data_streams = []
  @msats_detail = options[:msats_detail]

  parse_msats_detail unless @msats_detail.nil?
end

Instance Attribute Details

#addressObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def address
  @address
end

#classification_codeObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def classification_code
  @classification_code
end

#customer_classification_codeObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def customer_classification_code
  @customer_classification_code
end

#customer_threshold_codeObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def customer_threshold_code
  @customer_threshold_code
end

#data_streamsObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def data_streams
  @data_streams
end

#dlfObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def dlf
  @dlf
end

#jurisdiction_codeObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def jurisdiction_code
  @jurisdiction_code
end

#metersObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def meters
  @meters
end

#msats_detailObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def msats_detail
  @msats_detail
end

#nmiObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def nmi
  @nmi
end

#rolesObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def roles
  @roles
end

#statusObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def status
  @status
end

#tniObject

Since:

  • 2014-12-05



71
72
73
# File 'lib/aemo/nmi.rb', line 71

def tni
  @tni
end

Class Method Details

.allocationObject

Find the Network for a given NMI

Parameters:

  • nmi (String)

    NMI

Since:

  • 2014-12-05



105
106
107
# File 'lib/aemo/nmi.rb', line 105

def network(nmi)
  AEMO::NMI::Allocation.find_by_nmi(nmi)
end

.network(nmi) ⇒ Object

Find the Network for a given NMI

Parameters:

  • nmi (String)

    NMI

Since:

  • 2014-12-05



101
102
103
# File 'lib/aemo/nmi.rb', line 101

def network(nmi)
  AEMO::NMI::Allocation.find_by_nmi(nmi)
end

.valid_checksum?(nmi, checksum_value) ⇒ Boolean

A function to calculate the checksum value for a given National Meter Identifier

Parameters:

  • nmi (String)

    the NMI to check the checksum against

  • checksum_value (Integer)

    the checksum value to check against the current National Meter Identifier’s checksum value

Returns:

  • (Boolean)

    whether or not the checksum is valid

Since:

  • 2014-12-05



92
93
94
95
# File 'lib/aemo/nmi.rb', line 92

def valid_checksum?(nmi, checksum_value)
  nmi = AEMO::NMI.new(nmi)
  nmi.valid_checksum?(checksum_value)
end

.valid_nmi?(nmi) ⇒ Boolean

A function to validate the NMI provided

Parameters:

  • nmi (String)

    the nmi to be checked

Returns:

  • (Boolean)

    whether or not the nmi is valid

Since:

  • 2014-12-05



81
82
83
# File 'lib/aemo/nmi.rb', line 81

def valid_nmi?(nmi)
  (nmi.length == 10) && !nmi.match(/^([A-HJ-NP-Z\d]{10})/).nil?
end

Instance Method Details

#checksumInteger

Checksum is a function to calculate the checksum value for a given National Meter Identifier

Returns:

  • (Integer)

    the checksum value for the current National Meter Identifier

Since:

  • 2014-12-05



160
161
162
163
164
165
166
167
168
169
# File 'lib/aemo/nmi.rb', line 160

def checksum
  summation = 0
  @nmi.reverse.chars.each_index do |i|
    value = nmi[nmi.length - i - 1].ord
    value *= 2 if i.even?
    value = value.to_s.chars.map(&:to_i).reduce(:+)
    summation += value
  end
  (10 - (summation % 10)) % 10
end

#current_annual_loadInteger

TODO:

Use TimeDifference for more accurate annualised load

The current annual load in MWh

Returns:

  • (Integer)

    the current annual load for the meter in MWh

Since:

  • 2014-12-05



294
295
296
# File 'lib/aemo/nmi.rb', line 294

def current_annual_load
  (current_daily_load * 365.2425 / 1000).to_i
end

#current_daily_loadInteger

The current daily load in kWh

Returns:

  • (Integer)

    the current daily load for the meter in kWh

Since:

  • 2014-12-05



285
286
287
288
# File 'lib/aemo/nmi.rb', line 285

def current_daily_load
  data_streams_by_status.map { |x| x.averaged_daily_load.to_i }
                        .inject(0, :+)
end

#data_streams_by_status(status = 'A') ⇒ Array<Struct>

Returns the data_stream Structs for the requested status (A/I)

Parameters:

  • status (String) (defaults to: 'A')

    the stateus [A|I]

Returns:

  • (Array<Struct>)

    Returns an array of Structs for the current Meters

Since:

  • 2014-12-05



278
279
280
# File 'lib/aemo/nmi.rb', line 278

def data_streams_by_status(status = 'A')
  @data_streams.select { |x| x.status == status.to_s }
end

#dlfc_value(datetime = ::Time.now) ⇒ nil, float

A function to return the distribution loss factor value for a given date

Parameters:

  • datetime (DateTime, ::Time) (defaults to: ::Time.now)

    the date for the distribution loss factor value

Returns:

  • (nil, float)

    the distribution loss factor value

Since:

  • 2014-12-05



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/aemo/nmi.rb', line 303

def dlfc_value(datetime = ::Time.now)
  if @dlf.nil?
    raise 'No DLF set, ensure that you have set the value either via the' \
          'update_from_msats! function or manually'
  end
  raise 'DLF is invalid' unless DLF_CODES.keys.include?(@dlf)
  raise 'Invalid date' unless [DateTime, ::Time].include?(datetime.class)

  possible_values = DLF_CODES[@dlf].select do |x|
    ::Time.parse(x['FromDate']) <= datetime &&
      ::Time.parse(x['ToDate']) >= datetime
  end
  if possible_values.empty?
    nil
  else
    possible_values.first['Value'].to_f
  end
end

#dlfc_values(start = ::Time.now, finish = ::Time.now) ⇒ Array(Hash)

A function to return the distribution loss factor value for a given date

Parameters:

  • start (DateTime, ::Time) (defaults to: ::Time.now)

    the date for the distribution loss factor value

  • finish (DateTime, ::Time) (defaults to: ::Time.now)

    the date for the distribution loss factor value

Returns:

  • (Array(Hash))

    array of hashes of start, finish and value

Since:

  • 2014-12-05



327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/aemo/nmi.rb', line 327

def dlfc_values(start = ::Time.now, finish = ::Time.now)
  if @dlf.nil?
    raise 'No DLF set, ensure that you have set the value either via the ' \
          'update_from_msats! function or manually'
  end
  raise 'DLF is invalid' unless DLF_CODES.keys.include?(@dlf)
  raise 'Invalid start' unless [DateTime, ::Time].include?(start.class)
  raise 'Invalid finish' unless [DateTime, ::Time].include?(finish.class)
  raise 'start cannot be after finish' if start > finish

  DLF_CODES[@dlf].reject { |x| start > ::Time.parse(x['ToDate']) || finish < ::Time.parse(x['FromDate']) }
                 .map { |x| { 'start' => x['FromDate'], 'finish' => x['ToDate'], 'value' => x['Value'].to_f } }
end

#friendly_addressString

Returns a nice address from the structured one AEMO sends us

Returns:

  • (String)

Since:

  • 2014-12-05



251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/aemo/nmi.rb', line 251

def friendly_address
  friendly_address = ''
  if @address.is_a?(Hash)
    friendly_address = @address.values.map do |x|
      if x.is_a?(Hash)
        x = x.values.map { |y| y.is_a?(Hash) ? y.values.join(' ') : y }.join(' ')
      end
      x
    end.join(', ')
  end
  friendly_address
end

#meters_by_status(status = 'C') ⇒ Array<AEMO::Meter>

Returns the meters for the requested status (C/R)

Parameters:

  • status (String) (defaults to: 'C')

    the stateus [C|R]

Returns:

  • (Array<AEMO::Meter>)

    Returns an array of AEMO::Meters with the status provided

Since:

  • 2014-12-05



269
270
271
# File 'lib/aemo/nmi.rb', line 269

def meters_by_status(status = 'C')
  @meters.select { |x| x.status == status.to_s }
end

#networkObject Also known as: allocation

Find the Network of NMI

Since:

  • 2014-12-05



139
140
141
# File 'lib/aemo/nmi.rb', line 139

def network
  AEMO::NMI.network(@nmi)
end

#parse_msats_detailself

Turns raw MSATS junk into useful things

Returns:

  • (self)

    returns self

Since:

  • 2014-12-05



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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/aemo/nmi.rb', line 194

def parse_msats_detail
  # Set the details if there are any
  unless @msats_detail['MasterData'].nil?
    @tni                          = @msats_detail['MasterData']['TransmissionNodeIdentifier']
    @dlf                          = @msats_detail['MasterData']['DistributionLossFactorCode']
    @customer_classification_code = @msats_detail['MasterData']['CustomerClassificationCode']
    @customer_threshold_code      = @msats_detail['MasterData']['CustomerThresholdCode']
    @jurisdiction_code            = @msats_detail['MasterData']['JurisdictionCode']
    @classification_code          = @msats_detail['MasterData']['NMIClassificationCode']
    @status                       = @msats_detail['MasterData']['Status']
    @address                      = @msats_detail['MasterData']['Address']
  end
  @meters                       ||= []
  @roles                        ||= {}
  @data_streams                 ||= []
  # Meters
  unless @msats_detail['MeterRegister'].nil?
    meters = @msats_detail['MeterRegister']['Meter']
    meters = [meters] if meters.is_a?(Hash)
    meters.reject { |x| x['Status'].nil? }.each do |meter|
      @meters << AEMO::Meter.from_hash(meter)
    end
    meters.select { |x| x['Status'].nil? }.each do |registers|
      m = @meters.find { |x| x.serial_number == registers['SerialNumber'] }
      m.registers << AEMO::Register.from_hash(
        registers['RegisterConfiguration']['Register']
      )
    end
  end
  # Roles
  unless @msats_detail['RoleAssignments'].nil?
    role_assignments = @msats_detail['RoleAssignments']['RoleAssignment']
    role_assignments = [role_assignments] if role_assignments.is_a?(Hash)
    role_assignments.each do |role|
      @roles[role['Role']] = role['Party']
    end
  end
  # DataStreams
  unless @msats_detail['DataStreams'].nil?
    data_streams = @msats_detail['DataStreams']['DataStream']
    data_streams = [data_streams] if data_streams.is_a?(Hash) # Deal with issue of only one existing
    data_streams.each do |stream|
      @data_streams << Struct::DataStream.new(
        suffix: stream['Suffix'],
        profile_name: stream['ProfileName'],
        averaged_daily_load: stream['AveragedDailyLoad'],
        data_stream_type: stream['DataStreamType'],
        status: stream['Status']
      )
    end
  end
  self
end

#raw_msats_nmi_detail(options = {}) ⇒ Hash

Provided MSATS is configured, gets the MSATS data for the NMI

Returns:

  • (Hash)

    MSATS NMI Detail data

Raises:

  • (ArgumentError)

Since:

  • 2014-12-05



174
175
176
177
178
# File 'lib/aemo/nmi.rb', line 174

def raw_msats_nmi_detail(options = {})
  raise ArgumentError, 'MSATS has no authentication credentials' unless AEMO::MSATS.can_authenticate?

  AEMO::MSATS.nmi_detail(@nmi, options)
end

#tni_value(datetime = ::Time.now) ⇒ nil, float

A function to return the transmission node identifier loss factor value for a given date

Parameters:

  • datetime (DateTime, ::Time) (defaults to: ::Time.now)

    the date for the distribution loss factor value

Returns:

  • (nil, float)

    the transmission node identifier loss factor value

Since:

  • 2014-12-05



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/aemo/nmi.rb', line 345

def tni_value(datetime = ::Time.now)
  if @tni.nil?
    raise 'No TNI set, ensure that you have set the value either via the ' \
          'update_from_msats! function or manually'
  end
  raise 'TNI is invalid' unless TNI_CODES.keys.include?(@tni)
  raise 'Invalid date' unless [DateTime, ::Time].include?(datetime.class)

  possible_values = TNI_CODES[@tni].select do |x|
    ::Time.parse(x['FromDate']) <= datetime && datetime <= ::Time.parse(x['ToDate'])
  end
  return nil if possible_values.empty?

  possible_values = possible_values.first['mlf_data']['loss_factors'].select do |x|
    ::Time.parse(x['start']) <= datetime && datetime <= ::Time.parse(x['finish'])
  end
  return nil if possible_values.empty?

  possible_values.first['value'].to_f
end

#tni_values(start = ::Time.now, finish = ::Time.now) ⇒ Array(Hash)

A function to return the transmission node identifier loss factor value for a given date

Parameters:

  • start (DateTime, ::Time) (defaults to: ::Time.now)

    the date for the distribution loss factor value

  • finish (DateTime, ::Time) (defaults to: ::Time.now)

    the date for the distribution loss factor value

Returns:

  • (Array(Hash))

    array of hashes of start, finish and value

Since:

  • 2014-12-05



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/aemo/nmi.rb', line 371

def tni_values(start = ::Time.now, finish = ::Time.now)
  if @tni.nil?
    raise 'No TNI set, ensure that you have set the value either via the ' \
          'update_from_msats! function or manually'
  end
  raise 'TNI is invalid' unless TNI_CODES.keys.include?(@tni)
  raise 'Invalid start' unless [DateTime, ::Time].include?(start.class)
  raise 'Invalid finish' unless [DateTime, ::Time].include?(finish.class)
  raise 'start cannot be after finish' if start > finish

  possible_values = TNI_CODES[@tni].reject do |tni_code|
    start > ::Time.parse(tni_code['ToDate']) ||
      finish < ::Time.parse(tni_code['FromDate'])
  end

  return nil if possible_values.empty?

  possible_values.map { |x| x['mlf_data']['loss_factors'] }
end

#update_from_msats!(options = {}) ⇒ self

Provided MSATS is configured, uses the raw MSATS data to augment NMI information

Returns:

  • (self)

    returns self

Since:

  • 2014-12-05



184
185
186
187
188
189
# File 'lib/aemo/nmi.rb', line 184

def update_from_msats!(options = {})
  # Update local cache
  @msats_detail = raw_msats_nmi_detail(options)
  parse_msats_detail
  self
end

#valid_checksum?(checksum_value) ⇒ Boolean

A function to calculate the checksum value for a given National Meter Identifier

Parameters:

  • checksum_value (Integer)

    the checksum value to check against the current National Meter Identifier’s checksum value

Returns:

  • (Boolean)

    whether or not the checksum is valid

Since:

  • 2014-12-05



151
152
153
# File 'lib/aemo/nmi.rb', line 151

def valid_checksum?(checksum_value)
  checksum_value == checksum
end

#valid_nmi?Boolean

A function to validate the instance’s nmi value

Returns:

  • (Boolean)

    whether or not the nmi is valid

Since:

  • 2014-12-05



132
133
134
# File 'lib/aemo/nmi.rb', line 132

def valid_nmi?
  AEMO::NMI.valid_nmi?(@nmi)
end