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


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

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


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def address
  @address
end

#classification_codeObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def classification_code
  @classification_code
end

#customer_classification_codeObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def customer_classification_code
  @customer_classification_code
end

#customer_threshold_codeObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def customer_threshold_code
  @customer_threshold_code
end

#data_streamsObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def data_streams
  @data_streams
end

#dlfObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def dlf
  @dlf
end

#jurisdiction_codeObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def jurisdiction_code
  @jurisdiction_code
end

#metersObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def meters
  @meters
end

#msats_detailObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def msats_detail
  @msats_detail
end

#nmiObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def nmi
  @nmi
end

#rolesObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def roles
  @roles
end

#statusObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def status
  @status
end

#tniObject

Since:

  • 2014-12-05


69
70
71
# File 'lib/aemo/nmi.rb', line 69

def tni
  @tni
end

Class Method Details

.allocationObject

Find the Network for a given NMI

Parameters:

  • nmi (String)

    NMI

Since:

  • 2014-12-05


103
104
105
# File 'lib/aemo/nmi.rb', line 103

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


99
100
101
# File 'lib/aemo/nmi.rb', line 99

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


90
91
92
93
# File 'lib/aemo/nmi.rb', line 90

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


79
80
81
# File 'lib/aemo/nmi.rb', line 79

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


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

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


292
293
294
# File 'lib/aemo/nmi.rb', line 292

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


283
284
285
286
# File 'lib/aemo/nmi.rb', line 283

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<OpenStruct>

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

Parameters:

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

    the stateus [A|I]

Returns:

  • (Array<OpenStruct>)

    Returns an array of OpenStructs for the current Meters

Since:

  • 2014-12-05


276
277
278
# File 'lib/aemo/nmi.rb', line 276

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


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

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


324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/aemo/nmi.rb', line 324

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


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

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


267
268
269
# File 'lib/aemo/nmi.rb', line 267

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


137
138
139
# File 'lib/aemo/nmi.rb', line 137

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


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/aemo/nmi.rb', line 192

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 << OpenStruct.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


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

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


341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/aemo/nmi.rb', line 341

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 { |x| Time.parse(x['FromDate']) <= datetime && datetime <= Time.parse(x['ToDate']) }
  return nil if possible_values.empty?
  possible_values = possible_values.first['mlf_data']['loss_factors'].select { |x| Time.parse(x['start']) <= datetime && datetime <= Time.parse(x['finish']) }
  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


360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/aemo/nmi.rb', line 360

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


182
183
184
185
186
187
# File 'lib/aemo/nmi.rb', line 182

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


149
150
151
# File 'lib/aemo/nmi.rb', line 149

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


130
131
132
# File 'lib/aemo/nmi.rb', line 130

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