aemo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +19 -0
  3. data/Gemfile.lock +7 -0
  4. data/README.md +0 -0
  5. data/aemo.gemspec +27 -0
  6. data/lib/aemo.rb +9 -0
  7. data/lib/aemo/dispatchable.rb +7 -0
  8. data/lib/aemo/market.rb +40 -0
  9. data/lib/aemo/market/interval.rb +73 -0
  10. data/lib/aemo/nem12.rb +487 -0
  11. data/lib/aemo/nem13.rb +5 -0
  12. data/lib/aemo/region.rb +61 -0
  13. data/spec/aemo/market_spec.rb +16 -0
  14. data/spec/aemo/nem12_spec.rb +58 -0
  15. data/spec/aemo/region_spec.rb +9 -0
  16. data/spec/aemo_spec.rb +9 -0
  17. data/spec/fixtures/GRAPH_30NSW1.csv +122 -0
  18. data/spec/fixtures/GRAPH_5NSW1.csv +289 -0
  19. data/spec/fixtures/NEM12-Errors/NEM12#000000000000021#CNRGYMDP#NEMMCO +3 -0
  20. data/spec/fixtures/NEM12-Errors/NEM12#000000000000022#CNRGYMDP#NEMMCO +4 -0
  21. data/spec/fixtures/NEM12-Errors/NEM12#000000000000023#CNRGYMDP#NEMMCO +6 -0
  22. data/spec/fixtures/NEM12-Errors/NEM12#000000000000024#CNRGYMDP#NEMMCO +6 -0
  23. data/spec/fixtures/NEM12-Errors/NEM12#000000000000025#CNRGYMDP#NEMMCO +3 -0
  24. data/spec/fixtures/NEM12/NEM12#000000000000001#CNRGYMDP#NEMMCO.csv +18 -0
  25. data/spec/fixtures/NEM12/NEM12#000000000000002#CNRGYMDP#NEMMCO.csv +34 -0
  26. data/spec/fixtures/NEM12/NEM12#000000000000003#CNRGYMDP#NEMMCO.csv +42 -0
  27. data/spec/fixtures/NEM12/NEM12#000000000000004#CNRGYMDP#NEMMCO.csv +10 -0
  28. data/spec/fixtures/NEM12/NEM12#000000000000005#CNRGYMDP#NEMMCO.csv +10 -0
  29. data/spec/fixtures/NEM12/NEM12#000000000000006#CNRGYMDP#NEMMCO.csv +18 -0
  30. data/spec/fixtures/NEM12/NEM12#000000000000007#CNRGYMDP#NEMMCO.csv +18 -0
  31. data/spec/fixtures/NEM12/NEM12#000000000000008#CNRGYMDP#NEMMCO.csv +12 -0
  32. data/spec/fixtures/NEM12/NEM12#000000000000009#CNRGYMDP#NEMMCO.csv +25 -0
  33. data/spec/fixtures/NEM12/NEM12#000000000000010#CNRGYMDP#NEMMCO.csv +12 -0
  34. data/spec/fixtures/NEM12/NEM12#01010_05030502#WBAYM#NEMMCO.V01 +12 -0
  35. data/spec/fixtures/NEM12/NEM12#02030_05030501#WBAYM#NEMMCO.V01 +22 -0
  36. data/spec/fixtures/NEM12/NEM12#03050_05031001#WBAYM#NEMMCO.V01 +12 -0
  37. data/spec/fixtures/NEM12/NEM12#05050200001000000#GLOBALM#NEMMCO +18 -0
  38. data/spec/fixtures/NEM12/NEM12#05050200002000000#GLOBALM#NEMMCO +34 -0
  39. data/spec/fixtures/NEM12/NEM12#05050200003000000#GLOBALM#NEMMCO +18 -0
  40. data/spec/fixtures/NEM12/NEM12#05050200004000000#GLOBALM#NEMMCO +18 -0
  41. data/spec/fixtures/NEM12/NEM12#05050200005000000#GLOBALM#NEMMCO +18 -0
  42. data/spec/fixtures/NEM12/NEM12#05050200008000000#GLOBALM#NEMMCO +19 -0
  43. data/spec/fixtures/NEM12/NEM12#05051100001000000#GLOBALM#NEMMCO +7 -0
  44. data/spec/fixtures/NEM12/NEM12#05051100002000000#GLOBALM#NEMMCO +5 -0
  45. data/spec/fixtures/NEM12/NEM12#05051100004000000#GLOBALM#NEMMCO +12 -0
  46. data/spec/fixtures/NEM12/NEM12#05051200001000000#GLOBALM#NEMMCO +13 -0
  47. data/spec/fixtures/NEM12/NEM12#05062000001000000#GLOBALM#EASTENGY +20 -0
  48. data/spec/fixtures/NEM12/NEM12#05090_05031401#WBAYM#NEMMCO.V01 +8 -0
  49. data/spec/fixtures/NEM12/NEM12#06110_05021206#WBAYM#NEMMCO.V01 +12 -0
  50. data/spec/fixtures/NEM12/NEM12#07130_05021202#WBAYM#NEMMCO.V01 +12 -0
  51. data/spec/fixtures/NEM12/NEM12#08150_05031502#WBAYM#NEMMCO.V01 +10 -0
  52. data/spec/fixtures/NEM12/NEM12#10190_05031401#WBAYM#NEMMCO.V01 +10 -0
  53. data/spec/fixtures/NEM12/NEM12#SCENARIO1#UNITEDDP#NEMMCO.csv +14 -0
  54. data/spec/fixtures/NEM12/NEM12#SCENARIO10#UNITEDDP#NEMMCO.csv +23 -0
  55. data/spec/fixtures/NEM12/NEM12#SCENARIO1005032705#ENERGEXM#NEMMCO.V05 +21 -0
  56. data/spec/fixtures/NEM12/NEM12#SCENARIO105032701#ENERGEXM#NEMMCO.V01 +12 -0
  57. data/spec/fixtures/NEM12/NEM12#SCENARIO2#UNITEDDP#NEMMCO.csv +22 -0
  58. data/spec/fixtures/NEM12/NEM12#SCENARIO205032701#ENERGEXM#NEMMCO.V01 +22 -0
  59. data/spec/fixtures/NEM12/NEM12#SCENARIO3#UNITEDDP#NEMMCO.csv +12 -0
  60. data/spec/fixtures/NEM12/NEM12#SCENARIO305032701#ENERGEXM#NEMMCO.V01 +12 -0
  61. data/spec/fixtures/NEM12/NEM12#SCENARIO4#UNITEDDP#NEMMCO.csv +9 -0
  62. data/spec/fixtures/NEM12/NEM12#SCENARIO5#UNITEDDP#NEMMCO.csv +9 -0
  63. data/spec/fixtures/NEM12/NEM12#SCENARIO505033001#ENERGEXM#NEMMCO.V01 +8 -0
  64. data/spec/fixtures/NEM12/NEM12#SCENARIO6#UNITEDDP#NEMMCO.csv +12 -0
  65. data/spec/fixtures/NEM12/NEM12#SCENARIO605033001#ENERGEXM#NEMMCO.V01 +12 -0
  66. data/spec/fixtures/NEM12/NEM12#SCENARIO7#UNITEDDP#NEMMCO.csv +12 -0
  67. data/spec/fixtures/NEM12/NEM12#SCENARIO705033001#ENERGEXM#NEMMCO.V01 +12 -0
  68. data/spec/fixtures/NEM12/NEM12#SCENARIO8#UNITEDDP#NEMMCO.csv +9 -0
  69. data/spec/fixtures/NEM12/NEM12#SCENARIO805040401#ENERGEXM#NEMMCO.V01 +11 -0
  70. data/spec/fixtures/NEM12/NEM12#SCENARIO9#UNITEDDP#NEMMCO.csv +13 -0
  71. data/spec/fixtures/NEM12/NEM12#Scenario01#ETSAMDP#NEMMCO.csv +12 -0
  72. data/spec/fixtures/NEM12/NEM12#Scenario01#POWERMDP#NEMMCO.csv +12 -0
  73. data/spec/fixtures/NEM12/NEM12#Scenario04#ETSAMDP#NEMMCO.csv +9 -0
  74. data/spec/fixtures/NEM12/NEM12#Scenario04#POWERMDP#NEMMCO.csv +9 -0
  75. data/spec/fixtures/NEM12/NEM12#Scenario05#ETSAMDP#NEMMCO.csv +9 -0
  76. data/spec/fixtures/NEM12/NEM12#Scenario05#POWERMDP#NEMMCO.csv +9 -0
  77. data/spec/fixtures/NEM12/NEM12#Scenario06#ETSAMDP#NEMMCO.csv +18 -0
  78. data/spec/fixtures/NEM12/NEM12#Scenario06#POWERMDP#NEMMCO.csv +18 -0
  79. data/spec/fixtures/NEM12/NEM12#Scenario07#ETSAMDP#NEMMCO.csv +18 -0
  80. data/spec/fixtures/NEM12/NEM12#Scenario07#POWERMDP#NEMMCO.csv +18 -0
  81. data/spec/fixtures/NEM12/NEM12#Scenario08#ETSAMDP#NEMMCO.csv +10 -0
  82. data/spec/fixtures/NEM12/NEM12#Scenario08#POWERMDP#NEMMCO.csv +10 -0
  83. data/spec/fixtures/NEM12/NEM12#Scenario09#ETSAMDP#NEMMCO.csv +13 -0
  84. data/spec/fixtures/NEM12/NEM12#Scenario09#POWERMDP#NEMMCO.csv +13 -0
  85. data/spec/fixtures/NEM12/NEM12#Scenario10#ETSAMDP#NEMMCO.csv +31 -0
  86. data/spec/fixtures/NEM12/NEM12#Scenario10#POWERMDP#NEMMCO.csv +31 -0
  87. data/spec/fixtures/NEM12/NEM12#mdffl0000000001#ACTEWM#NEMMCO.mdff +12 -0
  88. data/spec/fixtures/NEM12/NEM12#mdffl0000000004#ACTEWM#NEMMCO.txt +8 -0
  89. data/spec/fixtures/NEM12/NEM12#mdffl0000000008#ACTEWM#NEMMCO.txt +7 -0
  90. data/spec/fixtures/NEM12/nem12#S01#INTEGM#NEMMCO +14 -0
  91. data/spec/fixtures/NEM12/nem12#S02#INTEGM#NEMMCO +22 -0
  92. data/spec/fixtures/NEM12/nem12#S03#INTEGM#NEMMCO +12 -0
  93. data/spec/fixtures/NEM12/nem12#S04#INTEGM#NEMMCO +8 -0
  94. data/spec/fixtures/NEM12/nem12#S05#INTEGM#NEMMCO +8 -0
  95. data/spec/fixtures/NEM12/nem12#S06#INTEGM#NEMMCO +12 -0
  96. data/spec/fixtures/NEM12/nem12#S07#INTEGM#NEMMCO +12 -0
  97. data/spec/fixtures/NEM12/nem12#S08#INTEGM#NEMMCO +10 -0
  98. data/spec/fixtures/NEM12/nem12#S09#INTEGM#NEMMCO +13 -0
  99. data/spec/fixtures/NEM12/nem12#S10#INTEGM#NEMMCO +11 -0
  100. data/spec/fixtures/NEM12/nem12#SCENARIO01#TCAUSTM#NEMMCO.csv +14 -0
  101. data/spec/fixtures/NEM12/nem12#SCENARIO01NEM1201003#ELECTDSM#NEMMCO +14 -0
  102. data/spec/fixtures/NEM12/nem12#SCENARIO02#TCAUSTM#NEMMCO.csv +22 -0
  103. data/spec/fixtures/NEM12/nem12#SCENARIO02NEM1202023#ELECTDSM#NEMMCO +22 -0
  104. data/spec/fixtures/NEM12/nem12#SCENARIO03#TCAUSTM#NEMMCO.csv +12 -0
  105. data/spec/fixtures/NEM12/nem12#SCENARIO03NEM1203043#ELECTDSM#NEMMCO +12 -0
  106. data/spec/fixtures/NEM12/nem12#SCENARIO04#TCAUSTM#NEMMCO.csv +9 -0
  107. data/spec/fixtures/NEM12/nem12#SCENARIO05#TCAUSTM#NEMMCO.csv +8 -0
  108. data/spec/fixtures/NEM12/nem12#SCENARIO05NEM1205083#ELECTDSM#NEMMCO +8 -0
  109. data/spec/fixtures/NEM12/nem12#SCENARIO06#TCAUSTM#NEMMCO.csv +12 -0
  110. data/spec/fixtures/NEM12/nem12#SCENARIO06NEM1206103#ELECTDSM#NEMMCO +14 -0
  111. data/spec/fixtures/NEM12/nem12#SCENARIO07#TCAUSTM#NEMMCO.csv +12 -0
  112. data/spec/fixtures/NEM12/nem12#SCENARIO07NEM1206103#ELECTDSM#NEMMCO +14 -0
  113. data/spec/fixtures/NEM12/nem12#SCENARIO08#TCAUSTM#NEMMCO.csv +8 -0
  114. data/spec/fixtures/NEM12/nem12#SCENARIO08NEM1208143#ELECTDSM#NEMMCO +12 -0
  115. data/spec/fixtures/NEM12/nem12#SCENARIO09#TCAUSTM#NEMMCO.csv +13 -0
  116. data/spec/fixtures/NEM12/nem12#SCENARIO10#TCAUSTM#NEMMCO.csv +20 -0
  117. data/spec/fixtures/NEM12/nem12#SCENARIO10NEM1210183#ELECTDSM#NEMMCO +10 -0
  118. data/spec/fixtures/nmi_checksum.json +32 -0
  119. data/spec/spec.opts +2 -0
  120. data/spec/spec_helper.rb +4 -0
  121. metadata +326 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a6c51782a64249e4194c67303c84c6c934cdfb79
4
+ data.tar.gz: b87f5bfb7d5a38deede4a0751e46f257bf6840f0
5
+ SHA512:
6
+ metadata.gz: f3106500593f93c92b30faf2fb5df869cedea03bcafb3c14037c6eb994c1ba4af1489d674b511aef97f1732394985f2f36d7aeaa9e355c7f12c31fbf1dd32d82
7
+ data.tar.gz: eca1a6b0db4795e6d9776da382237c9ccc011356d4b2e31a4e526877caa7174dfe329c753fd8accca728425a4250c3233c57100c0b4635948991f51215773864
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ gem 'httparty', '~0.13'
7
+ gem 'json', '~1.8'
8
+ gem 'multi_xml', '>= 0.5.2'
9
+ gem 'coveralls', :require => false
10
+
11
+ # Add dependencies to develop your gem here.
12
+ # Include everything needed to run rake, tests, features, etc.
13
+ # group :development do
14
+ # gem "shoulda", ">= 0"
15
+ # gem "rdoc", "~> 3.12"
16
+ # gem "bundler", ">= 1.0.0"
17
+ # gem "jeweler", ">= 1.8.4"
18
+ # gem "simplecov", ">= 0"
19
+ # end
@@ -0,0 +1,7 @@
1
+ GEM
2
+ specs:
3
+
4
+ PLATFORMS
5
+ ruby
6
+
7
+ DEPENDENCIES
File without changes
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'aemo'
6
+ s.version = '0.1.0'
7
+ s.platform = Gem::Platform::RUBY
8
+ s.date = '2014-08-19'
9
+ s.summary = 'AEMO Gem'
10
+ s.description = 'Gem providing functionality for the Australian Energy Market Operator data'
11
+ s.authors = ['Joel Courtney']
12
+ s.email = ['jcourtney@cozero.com.au']
13
+ s.homepage =
14
+ 'https://bitbucket.org/jufemaiz/aemo-gem'
15
+ s.license = 'MIT'
16
+
17
+ s.required_ruby_version = '>= 1.9.3'
18
+
19
+ s.add_dependency 'json', '~> 1.8'
20
+ s.add_runtime_dependency 'multi_xml', '~> 0.5', '>= 0.5.2'
21
+ s.add_runtime_dependency 'httparty', '~> 0.13', '>= 0.13.1'
22
+
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
26
+ s.require_paths = ['lib']
27
+ end
@@ -0,0 +1,9 @@
1
+ require 'httparty'
2
+
3
+ require 'aemo/market.rb'
4
+ require 'aemo/market/interval.rb'
5
+ require 'aemo/region.rb'
6
+ require 'aemo/nem12.rb'
7
+
8
+ module AEMO
9
+ end
@@ -0,0 +1,7 @@
1
+ module AEMO
2
+ class Region
3
+ DISPATCH_TYPE = ['Generator','Load Norm Off','Network Service Provider']
4
+ CATEGORY = ['Market','Non-Market']
5
+ CLASSIFICATION = ['Scheduled','Semi-Scheduled','Non-Scheduled']
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ module AEMO
2
+ module Market
3
+ include HTTParty
4
+ base_uri 'www.nemweb.com.au'
5
+
6
+ def self.regions
7
+ AEMO::Region::REGIONS
8
+ end
9
+
10
+ def self.current_dispatch(region)
11
+ response = AEMO::Market.get "/mms.GRAPHS/GRAPHS/GRAPH_5#{region}1.csv"
12
+ values = AEMO::Market.parse_response(response)
13
+ values
14
+ end
15
+
16
+ def self.current_trading(region)
17
+ response = AEMO::Market.get "/mms.GRAPHS/GRAPHS/GRAPH_30#{region}1.csv"
18
+ values = AEMO::Market.parse_response(response)
19
+ values
20
+ end
21
+
22
+ # ######### #
23
+ protected
24
+ # ######### #
25
+
26
+ def self.parse_response(response)
27
+ values = []
28
+ if response.response.code == '200'
29
+ CSV.parse(response.body, :headers => true, :converters => :numeric) do |row|
30
+ row = row.to_h
31
+ values.push AEMO::Market::Interval.new(row['SETTLEMENTDATE'],row)
32
+ end
33
+ end
34
+ values
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
@@ -0,0 +1,73 @@
1
+ module AEMO
2
+ module Market
3
+ class Interval
4
+ INTERVALS = {
5
+ :trading => 'Trading',
6
+ :dispatch => 'Dispatch'
7
+ }
8
+
9
+ attr_accessor :datetime, :region, :total_demand, :rrp, :period_type
10
+
11
+ # @param datetime [Time]
12
+ # @param options [Hash] Hash of optional data values
13
+ # @return [AEMO::Market::Interval]
14
+ def initialize(datetime,options={})
15
+ @datetime = Time.parse("#{datetime} +1000")
16
+ @region = options['REGION']
17
+ @total_demand = options['TOTALDEMAND']
18
+ @rrp = options['RRP']
19
+ @period_type = options['PERIODTYPE']
20
+ end
21
+
22
+ # Instance Variable Getters
23
+
24
+ # All AEMO Data operates in Australian Eastern Standard Time
25
+ # All AEMO Data aggregates to the trailing edge of the period (this makes it difficult to do daily aggregations :( )
26
+ # @param trailing_edge [Boolean] selection of either the trailing edge of the period or the rising edge of the period for the date time
27
+ # @return [Time] a time object of the trailing edge of the interval
28
+ def datetime(trailing_edge = true)
29
+ t = @datetime
30
+ # If the datetime requested is the trailing edge, offset as per interval requirement
31
+ unless trailing_edge
32
+ # This is for dispatch intervals of five minutes
33
+ if self.is_dispatch?
34
+ t -= 5 * 60
35
+ elsif self.is_trading?
36
+ t -= 30 * 60
37
+ end
38
+ end
39
+ t
40
+ end
41
+
42
+ # @return [Time] the time of the
43
+ def interval_length
44
+ Time.at(300)
45
+ end
46
+
47
+ # @return [Symbol] :dispatch or :trading
48
+ def interval_type
49
+ self.is_dispatch? ? :dispatch : :trading
50
+ end
51
+
52
+ # @return [Boolean] returns true if the interval type is dispatch
53
+ def is_dispatch?
54
+ @period_type.nil? || @period_type.empty?
55
+ end
56
+
57
+ # @return [Boolean] returns true if the interval type is trading
58
+ def is_trading?
59
+ !(self.is_dispatch?)
60
+ end
61
+
62
+ # @return [Float] the value of the interval in Australian Dollars
63
+ def value
64
+ @value ||= Float::NAN
65
+ if @total_demand.class == Float && @rrp.class == Float
66
+ @value = (@total_demand * @rrp).round(2)
67
+ end
68
+ @value
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,487 @@
1
+ require 'csv'
2
+ require 'time'
3
+ module AEMO
4
+ class NEM12
5
+ # As per AEMO NEM12 Specification
6
+ # http://www.aemo.com.au/Consultations/National-Electricity-Market/Open/~/media/Files/Other/consultations/nem/Meter%20Data%20File%20Format%20Specification%20NEM12_NEM13/MDFF_Specification_NEM12_NEM13_Final_v102_clean.ashx
7
+ RECORD_INDICATORS = {
8
+ 100 => 'Header',
9
+ 200 => 'NMI Data Details',
10
+ 300 => 'Interval Data',
11
+ 400 => 'Interval Event',
12
+ 500 => 'B2B Details',
13
+ 900 => 'End'
14
+ }
15
+
16
+ TRANSACTION_CODE_FLAGS = {
17
+ 'A' => 'Alteration',
18
+ 'C' => 'Meter Reconfiguration',
19
+ 'G' => 'Re-energisation',
20
+ 'D' => 'De-energisation',
21
+ 'E' => 'Forward Estimate',
22
+ 'N' => 'Normal Read',
23
+ 'O' => 'Other',
24
+ 'S' => 'Special Read',
25
+ 'R' => 'Removal of Meter'
26
+ }
27
+
28
+ UOM = {
29
+ 'MWh' => { :name => 'Megawatt Hour', :multiplier => 1e6 },
30
+ 'kWh' => { :name => 'Kilowatt Hour', :multiplier => 1e3 },
31
+ 'Wh' => { :name => 'Watt Hour', :multiplier => 1 },
32
+ 'MW' => { :name => 'Megawatt', :multiplier => 1e6 },
33
+ 'kW' => { :name => 'Kilowatt', :multiplier => 1e3 },
34
+ 'W' => { :name => 'Watt', :multiplier => 1 },
35
+ 'MVArh' => { :name => 'Megavolt Ampere Reactive Hour', :multiplier => 1e6 },
36
+ 'kVArh' => { :name => 'Kilovolt Ampere Reactive Hour', :multiplier => 1e3 },
37
+ 'VArh' => { :name => 'Volt Ampere Reactive Hour', :multiplier => 1 },
38
+ 'MVAr' => { :name => 'Megavolt Ampere Reactive', :multiplier => 1e6 },
39
+ 'kVAr' => { :name => 'Kilovolt Ampere Reactive', :multiplier => 1e3 },
40
+ 'VAr' => { :name => 'Volt Ampere Reactive', :multiplier => 1 },
41
+ 'MVAh' => { :name => 'Megavolt Ampere Hour', :multiplier => 1e6 },
42
+ 'kVAh' => { :name => 'Kilovolt Ampere Hour', :multiplier => 1e3 },
43
+ 'VAh' => { :name => 'Volt Ampere Hour', :multiplier => 1 },
44
+ 'MVA' => { :name => 'Megavolt Ampere', :multiplier => 1e6 },
45
+ 'kVA' => { :name => 'Kilovolt Ampere', :multiplier => 1e3 },
46
+ 'VA' => { :name => 'Volt Ampere', :multiplier => 1 },
47
+ 'kV' => { :name => 'Kilovolt', :multiplier => 1e3 },
48
+ 'V' => { :name => 'Volt', :multiplier => 1 },
49
+ 'kA' => { :name => 'Kiloampere', :multiplier => 1e3 },
50
+ 'A' => { :name => 'Ampere', :multiplier => 1 },
51
+ 'pf' => { :name => 'Power Factor', :multiplier => 1 }
52
+ }
53
+
54
+ QUALITY_FLAGS = {
55
+ 'A' => 'Actual Data',
56
+ 'E' => 'Forward Estimated Data',
57
+ 'F' => 'Final Substituted Data',
58
+ 'N' => 'Null Data',
59
+ 'S' => 'Substituted Data',
60
+ 'V' => 'Variable Data',
61
+ }
62
+
63
+ METHOD_FLAGS = {
64
+ "11" => { type: ["SUB"], installation_type: [1,2,3,4], short_descriptor: "Check", description: "" },
65
+ "12" => { type: ["SUB"], installation_type: [1,2,3,4], short_descriptor: "Calculated", description: "" },
66
+ "13" => { type: ["SUB"], installation_type: [1,2,3,4], short_descriptor: "SCADA", description: "" },
67
+ "14" => { type: ["SUB"], installation_type: [1,2,3,4], short_descriptor: "Like Day", description: "" },
68
+ "15" => { type: ["SUB"], installation_type: [1,2,3,4], short_descriptor: "Average Like Day", description: "" },
69
+ "16" => { type: ["SUB"], installation_type: [1,2,3,4], short_descriptor: "Agreed", description: "" },
70
+ "17" => { type: ["SUB"], installation_type: [1,2,3,4], short_descriptor: "Linear", description: "" },
71
+ "18" => { type: ["SUB"], installation_type: [1,2,3,4], short_descriptor: "Alternate", description: "" },
72
+ "19" => { type: ["SUB"], installation_type: [1,2,3,4], short_descriptor: "Zero", description: "" },
73
+ "51" => { type: ["EST","SUB"], installation_type: 5, short_descriptor: "Previous Year", description: "" },
74
+ "52" => { type: ["EST","SUB"], installation_type: 5, short_descriptor: "Previous Read", description: "" },
75
+ "53" => { type: ["SUB"], installation_type: 5, short_descriptor: "Revision", description: "" },
76
+ "54" => { type: ["SUB"], installation_type: 5, short_descriptor: "Linear", description: "" },
77
+ "55" => { type: ["SUB"], installation_type: 5, short_descriptor: "Agreed", description: "" },
78
+ "56" => { type: ["EST","SUB"], installation_type: 5, short_descriptor: "Prior to First Read – Agreed", description: "" },
79
+ "57" => { type: ["EST","SUB"], installation_type: 5, short_descriptor: "Customer Class", description: "" },
80
+ "58" => { type: ["EST","SUB"], installation_type: 5, short_descriptor: "Zero", description: "" },
81
+ "61" => { type: ["EST","SUB"], installation_type: 6, short_descriptor: "Previous Year", description: "" },
82
+ "62" => { type: ["EST","SUB"], installation_type: 6, short_descriptor: "Previous Read", description: "" },
83
+ "63" => { type: ["EST","SUB"], installation_type: 6, short_descriptor: "Customer Class", description: "" },
84
+ "64" => { type: ["SUB"], installation_type: 6, short_descriptor: "Agreed", description: "" },
85
+ "65" => { type: ["EST"], installation_type: 6, short_descriptor: "ADL", description: "" },
86
+ "66" => { type: ["SUB"], installation_type: 6, short_descriptor: "Revision", description: "" },
87
+ "67" => { type: ["SUB"], installation_type: 6, short_descriptor: "Customer Read", description: "" },
88
+ "68" => { type: ["EST","SUB"], installation_type: 6, short_descriptor: "Zero", description: "" },
89
+ "71" => { type: ["SUB"], installation_type: 7, short_descriptor: "Recalculation", description: "" },
90
+ "72" => { type: ["SUB"], installation_type: 7, short_descriptor: "Revised Table", description: "" },
91
+ "73" => { type: ["SUB"], installation_type: 7, short_descriptor: "Revised Algorithm", description: "" },
92
+ "74" => { type: ["SUB"], installation_type: 7, short_descriptor: "Agreed", description: "" },
93
+ "75" => { type: ["EST"], installation_type: 7, short_descriptor: "Existing Table", description: "" }
94
+ }
95
+
96
+ REASON_CODES = {
97
+ 0 => 'Free Text Description',
98
+ 1 => 'Meter/Equipment Changed',
99
+ 2 => 'Extreme Weather/Wet',
100
+ 3 => 'Quarantine',
101
+ 4 => 'Savage Dog',
102
+ 5 => 'Meter/Equipment Changed',
103
+ 6 => 'Extreme Weather/Wet',
104
+ 7 => 'Unable To Locate Meter',
105
+ 8 => 'Vacant Premise',
106
+ 9 => 'Meter/Equipment Changed',
107
+ 10 => 'Lock Damaged/Seized',
108
+ 11 => 'In Wrong Walk',
109
+ 12 => 'Locked Premises',
110
+ 13 => 'Locked Gate',
111
+ 14 => 'Locked Meter Box',
112
+ 15 => 'Access - Overgrown',
113
+ 16 => 'Noxious Weeds',
114
+ 17 => 'Unsafe Equipment/Location',
115
+ 18 => 'Read Below Previous',
116
+ 19 => 'Consumer Wanted',
117
+ 20 => 'Damaged Equipment/Panel',
118
+ 21 => 'Switched Off',
119
+ 22 => 'Meter/Equipment Seals Missing',
120
+ 23 => 'Meter/Equipment Seals Missing',
121
+ 24 => 'Meter/Equipment Seals Missing',
122
+ 25 => 'Meter/Equipment Seals Missing',
123
+ 26 => 'Meter/Equipment Seals Missing',
124
+ 27 => 'Meter/Equipment Seals Missing',
125
+ 28 => 'Damaged Equipment/Panel',
126
+ 29 => 'Relay Faulty/Damaged',
127
+ 30 => 'Meter Stop Switch On',
128
+ 31 => 'Meter/Equipment Seals Missing',
129
+ 32 => 'Damaged Equipment/Panel',
130
+ 33 => 'Relay Faulty/Damaged',
131
+ 34 => 'Meter Not In Handheld',
132
+ 35 => 'Timeswitch Faulty/Reset Required',
133
+ 36 => 'Meter High/Ladder Required',
134
+ 37 => 'Meter High/Ladder Required',
135
+ 38 => 'Unsafe Equipment/Location',
136
+ 39 => 'Reverse Energy Observed',
137
+ 40 => 'Timeswitch Faulty/Reset Required',
138
+ 41 => 'Faulty Equipment Display/Dials',
139
+ 42 => 'Faulty Equipment Display/Dials',
140
+ 43 => 'Power Outage',
141
+ 44 => 'Unsafe Equipment/Location',
142
+ 45 => 'Readings Failed To Validate',
143
+ 46 => 'Extreme Weather/Hot',
144
+ 47 => 'Refused Access',
145
+ 48 => 'Timeswitch Faulty/Reset Required',
146
+ 49 => 'Wet Paint',
147
+ 50 => 'Wrong Tariff',
148
+ 51 => 'Installation Demolished',
149
+ 52 => 'Access - Blocked',
150
+ 53 => 'Bees/Wasp In Meter Box',
151
+ 54 => 'Meter Box Damaged/Faulty',
152
+ 55 => 'Faulty Equipment Display/Dials',
153
+ 56 => 'Meter Box Damaged/Faulty',
154
+ 57 => 'Timeswitch Faulty/Reset Required',
155
+ 58 => 'Meter Ok - Supply Failure',
156
+ 59 => 'Faulty Equipment Display/Dials',
157
+ 60 => 'Illegal Connection/Equipment Tampered',
158
+ 61 => 'Meter Box Damaged/Faulty',
159
+ 62 => 'Damaged Equipment/Panel',
160
+ 63 => 'Illegal Connection/Equipment Tampered',
161
+ 64 => 'Key Required',
162
+ 65 => 'Wrong Key Provided',
163
+ 66 => 'Lock Damaged/Seized',
164
+ 67 => 'Extreme Weather/Wet',
165
+ 68 => 'Zero Consumption',
166
+ 69 => 'Reading Exceeds Estimate',
167
+ 70 => 'Probe Reports Tampering',
168
+ 71 => 'Probe Read Error',
169
+ 72 => 'Meter/Equipment Changed',
170
+ 73 => 'Low Consumption',
171
+ 74 => 'High Consumption',
172
+ 75 => 'Customer Read',
173
+ 76 => 'Communications Fault',
174
+ 77 => 'Estimation Forecast',
175
+ 78 => 'Null Data',
176
+ 79 => 'Power Outage Alarm',
177
+ 80 => 'Short Interval Alarm',
178
+ 81 => 'Long Interval Alarm',
179
+ 82 => 'CRC Error',
180
+ 83 => 'RAM Checksum Error',
181
+ 84 => 'ROM Checksum Error',
182
+ 85 => 'Data Missing Alarm',
183
+ 86 => 'Clock Error Alarm',
184
+ 87 => 'Reset Occurred',
185
+ 88 => 'Watchdog Timeout Alarm',
186
+ 89 => 'Time Reset Occurred',
187
+ 90 => 'Test Mode',
188
+ 91 => 'Load Control',
189
+ 92 => 'Added Interval (Data Correction)',
190
+ 93 => 'Replaced Interval (Data Correction)',
191
+ 94 => 'Estimated Interval (Data Correction)',
192
+ 95 => 'Pulse Overflow Alarm',
193
+ 96 => 'Data Out Of Limits',
194
+ 97 => 'Excluded Data',
195
+ 98 => 'Parity Error',
196
+ 99 => 'Energy Type (Register Changed)'
197
+ }
198
+
199
+ DATA_STREAM_SUFFIX = {
200
+ # Averaged Data Streams
201
+ 'A' => { :stream => 'Average', :description => 'Import', :units => 'kWh' },
202
+ 'D' => { :stream => 'Average', :description => 'Export', :units => 'kWh' },
203
+ 'J' => { :stream => 'Average', :description => 'Import', :units => 'kVArh' },
204
+ 'P' => { :stream => 'Average', :description => 'Export', :units => 'kVArh' },
205
+ 'S' => { :stream => 'Average', :description => '', :units => 'kVAh' },
206
+ # Master Data Streams
207
+ 'B' => { :stream => 'Master', :description => 'Import', :units => 'kWh' },
208
+ 'E' => { :stream => 'Master', :description => 'Export', :units => 'kWh' },
209
+ 'K' => { :stream => 'Master', :description => 'Import', :units => 'kVArh' },
210
+ 'Q' => { :stream => 'Master', :description => 'Export', :units => 'kVArh' },
211
+ 'T' => { :stream => 'Master', :description => '', :units => 'kVAh' },
212
+ 'G' => { :stream => 'Master', :description => 'Power Factor', :units => 'PF' },
213
+ 'H' => { :stream => 'Master', :description => 'Q Metering', :units => 'Qh' },
214
+ 'M' => { :stream => 'Master', :description => 'Par Metering', :units => 'parh' },
215
+ 'V' => { :stream => 'Master', :description => 'Volts or V2h or Amps or A2h', :units => '' },
216
+ # Check Meter Streams
217
+ 'C' => { :stream => 'Check', :description => 'Import', :units => 'kWh' },
218
+ 'F' => { :stream => 'Check', :description => 'Export', :units => 'kWh' },
219
+ 'L' => { :stream => 'Check', :description => 'Import', :units => 'kVArh' },
220
+ 'R' => { :stream => 'Check', :description => 'Export', :units => 'kVArh' },
221
+ 'U' => { :stream => 'Check', :description => '', :units => 'kVAh' },
222
+ 'Y' => { :stream => 'Check', :description => 'Q Metering', :units => 'Qh' },
223
+ 'W' => { :stream => 'Check', :description => 'Par Metering Path', :units => '' },
224
+ 'Z' => { :stream => 'Check', :description => 'Volts or V2h or Amps or A2h', :units => '' },
225
+ # Net Meter Streams
226
+ 'D' => { :stream => 'Net', :description => 'Net', :units => 'kWh' },
227
+ 'J' => { :stream => 'Net', :description => 'Net', :units => 'kVArh' }
228
+ }
229
+
230
+ @nmi = nil
231
+ @data_details = []
232
+ @interval_data = []
233
+ @interval_events = []
234
+
235
+ attr_accessor :nmi, :file_contents
236
+ attr_reader :data_details, :interval_data, :interval_events
237
+
238
+ # Initialize a NEM12 file
239
+ def initialize(nmi,options={})
240
+ @nmi = nmi
241
+ @data_details = []
242
+ @interval_data = []
243
+ @interval_events = []
244
+ options.keys.each do |key|
245
+ eval "self.#{key} = #{options[key]}"
246
+ end
247
+ end
248
+
249
+ # @return [Integer] checksum of the NMI
250
+ def nmi_checksum
251
+ summation = 0
252
+ @nmi.reverse.split(//).each_index do |i|
253
+ value = nmi[nmi.length - i - 1].ord
254
+ if(i % 2 == 0)
255
+ value = value * 2
256
+ end
257
+ value = value.to_s.split(//).map{|i| i.to_i}.reduce(:+)
258
+ summation += value
259
+ end
260
+ checksum = (10 - (summation % 10)) % 10
261
+ checksum
262
+ end
263
+
264
+ # Parses the header record
265
+ # @param line [String] A single line in string format
266
+ # @return [Hash] the line parsed into a hash of information
267
+ def self.parse_nem12_100(line,options={})
268
+ csv = line.parse_csv
269
+
270
+ raise ArgumentError, 'RecordIndicator is not 100' if csv[0] != '100'
271
+ raise ArgumentError, 'VersionHeader is not NEM12' if csv[1] != 'NEM12'
272
+ if options[:strict]
273
+ raise ArgumentError, 'DateTime is not valid' if csv[2].match(/\d{12}/).nil? || csv[2] != Time.parse("#{csv[2]}00").strftime('%Y%m%d%H%M')
274
+ end
275
+ raise ArgumentError, 'FromParticispant is not valid' if csv[3].match(/.{1,10}/).nil?
276
+ raise ArgumentError, 'ToParticispant is not valid' if csv[4].match(/.{1,10}/).nil?
277
+
278
+ nem12_100 = {
279
+ :record_indicator => csv[0].to_i,
280
+ :version_header => csv[1],
281
+ :datetime => Time.parse("#{csv[2]}+1000"),
282
+ :from_participant => csv[3],
283
+ :to_participant => csv[4]
284
+ }
285
+ end
286
+
287
+ # Parses the NMI Data Details
288
+ # @param line [String] A single line in string format
289
+ # @return [Hash] the line parsed into a hash of information
290
+ def parse_nem12_200(line,options={})
291
+ csv = line.parse_csv
292
+
293
+ raise ArgumentError, 'RecordIndicator is not 200' if csv[0] != '200'
294
+ raise ArgumentError, 'NMI is not valid' if csv[1].match(/[A-Z0-9]{10}/).nil?
295
+ raise ArgumentError, 'NMIConfiguration is not valid' if csv[2].match(/.{1,240}/).nil?
296
+ unless csv[3].nil?
297
+ raise ArgumentError, 'RegisterID is not valid' if csv[3].match(/.{1,10}/).nil?
298
+ end
299
+ raise ArgumentError, 'NMISuffix is not valid' if csv[4].match(/[A-HJ-NP-Z][1-9A-HJ-NP-Z]/).nil?
300
+ if !csv[5].nil? && !csv[5].empty? && !csv[5].match(/^\s*$/)
301
+ raise ArgumentError, 'MDMDataStreamIdentifier is not valid' if csv[5].match(/[A-Z0-9]{2}/).nil?
302
+ end
303
+ if !csv[6].nil? && !csv[6].empty? && !csv[6].match(/^\s*$/)
304
+ raise ArgumentError, 'MeterSerialNumber is not valid' if csv[6].match(/[A-Z0-9]{2}/).nil?
305
+ end
306
+ raise ArgumentError, 'UOM is not valid' if csv[7].upcase.match(/[A-Z0-9]{2}/).nil?
307
+ raise ArgumentError, 'UOM is not valid' unless UOM.keys.map{|k| k.upcase}.include?(csv[7].upcase)
308
+ raise ArgumentError, 'IntervalLength is not valid' unless %w(1 5 10 15 30).include?(csv[8])
309
+ # raise ArgumentError, 'NextScheduledReadDate is not valid' if csv[9].match(/\d{8}/).nil? || csv[9] != Time.parse("#{csv[9]}").strftime('%Y%m%d')
310
+
311
+ @nmi = csv[1]
312
+
313
+ # Push onto the stack
314
+ @data_details << {
315
+ :record_indicator => csv[0].to_i,
316
+ :nmi => csv[1],
317
+ :nmi_configuration => csv[2],
318
+ :register_id => csv[3],
319
+ :nmi_suffix => csv[4],
320
+ :mdm_data_streaming_identifier => csv[5],
321
+ :meter_serial_nubmer => csv[6],
322
+ :uom => csv[7].upcase,
323
+ :interval_length => csv[8].to_i,
324
+ :next_scheduled_read_date => csv[9],
325
+ }
326
+ end
327
+
328
+ # @param line [String] A single line in string format
329
+ # @return [Array of hashes] the line parsed into a hash of information
330
+ def parse_nem12_300(line,options={})
331
+ csv = line.parse_csv
332
+
333
+ raise TypeError, 'Expected NMI Data Details to exist with IntervalLength specified' if @data_details.last.nil? || @data_details.last[:interval_length].nil?
334
+ number_of_intervals = 1440 / @data_details.last[:interval_length]
335
+ intervals_offset = number_of_intervals + 2
336
+
337
+ raise ArgumentError, 'RecordIndicator is not 300' if csv[0] != '300'
338
+ raise ArgumentError, 'IntervalDate is not valid' if csv[1].match(/\d{8}/).nil? || csv[1] != Time.parse("#{csv[1]}").strftime('%Y%m%d')
339
+ (2..(number_of_intervals+1)).each do |i|
340
+ raise ArgumentError, "Interval number #{i-1} is not valid" if csv[i].match(/\d+(\.\d+)?/).nil?
341
+ end
342
+ raise ArgumentError, 'QualityMethod is not valid' unless csv[intervals_offset + 0].class == String
343
+ raise ArgumentError, 'QualityMethod does not have valid length' unless [1,3].include?(csv[intervals_offset + 0].length)
344
+ raise ArgumentError, 'QualityMethod does not have valid QualityFlag' unless QUALITY_FLAGS.keys.include?(csv[intervals_offset + 0][0])
345
+ unless %w(A N V).include?(csv[intervals_offset + 0][0])
346
+ raise ArgumentError, 'QualityMethod does not have valid length' unless csv[intervals_offset + 0].length == 3
347
+ raise ArgumentError, 'QualityMethod does not have valid MethodFlag' unless METHOD_FLAGS.keys.include?(csv[intervals_offset + 0][1..2])
348
+ end
349
+ unless %w(A N E).include?(csv[intervals_offset + 0][0])
350
+ raise ArgumentError, 'ReasonCode is not valid' unless REASON_CODES.keys.include?(csv[intervals_offset + 1].to_i)
351
+ end
352
+ if !csv[intervals_offset + 1].nil? && csv[intervals_offset + 1].to_i == 0
353
+ raise ArgumentError, 'ReasonDescription is not valid' unless csv[intervals_offset + 2].class == String && csv[intervals_offset + 2].length > 0
354
+ end
355
+ if options[:strict]
356
+ raise ArgumentError, 'UpdateDateTime is not valid' if csv[intervals_offset + 3].match(/\d{14}/).nil? || csv[intervals_offset + 3] != Time.parse("#{csv[intervals_offset + 3]}").strftime('%Y%m%d%H%M%S')
357
+ unless csv[intervals_offset + 4].nil?
358
+ raise ArgumentError, 'MSATSLoadDateTime is not valid' if csv[intervals_offset + 4].match(/\d{14}/).nil? || csv[intervals_offset + 4] != Time.parse("#{csv[intervals_offset + 4]}").strftime('%Y%m%d%H%M%S')
359
+ end
360
+ end
361
+
362
+ base_interval = { :data_details => @data_details.last, :datetime => Time.parse("#{csv[1]}000000+1000"), :value => nil, :flag => nil}
363
+ intervals = []
364
+ (2..(number_of_intervals+1)).each do |i|
365
+ interval = base_interval.dup
366
+ interval[:datetime] += (i-1) * interval[:data_details][:interval_length] * 60
367
+ interval[:value] = csv[i].to_f
368
+ intervals << interval
369
+ end
370
+ @interval_data += intervals
371
+ intervals
372
+ end
373
+
374
+ # @param line [String] A single line in string format
375
+ # @return [Hash] the line parsed into a hash of information
376
+ def parse_nem12_400(line)
377
+ csv = line.parse_csv
378
+ raise ArgumentError, 'RecordIndicator is not 400' if csv[0] != '400'
379
+ raise ArgumentError, 'StartInterval is not valid' if csv[1].match(/^\d+$/).nil?
380
+ raise ArgumentError, 'EndInterval is not valid' if csv[2].match(/^\d+$/).nil?
381
+ raise ArgumentError, 'QualityMethod is not valid' if csv[3].match(/^([AN]|([AEFNSV]\d{2}))$/).nil?
382
+ # raise ArgumentError, 'ReasonCode is not valid' if (csv[4].nil? && csv[3].match(/^ANE/)) || csv[4].match(/^\d{3}?$/) || csv[3].match(/^ANE/)
383
+ # raise ArgumentError, 'ReasonDescription is not valid' if (csv[4].nil? && csv[3].match(/^ANE/)) || ( csv[5].match(/^$/) && csv[4].match(/^0$/) )
384
+
385
+ interval_events = []
386
+
387
+ # Only need to update flags for EFSV
388
+ unless %w(A N).include?csv[3]
389
+ number_of_intervals = 1440 / @data_details.last[:interval_length]
390
+ interval_start_point = @interval_data.length - number_of_intervals
391
+
392
+ # For each of these
393
+ base_interval_event = { datetime: nil, quality_method: csv[3], reason_code: csv[4], reason_description: csv[5] }
394
+
395
+ # Interval Numbers are 1-indexed
396
+ ((csv[1].to_i)..(csv[2].to_i)).each do |i|
397
+ interval_event = base_interval_event.dup
398
+ interval_event[:datetime] = @interval_data[interval_start_point + (i-1)][:datetime]
399
+ interval_events << interval_event
400
+
401
+ method_flag = nil
402
+ unless (quality_method = interval_event[:quality_method].match(/(\d+)/)[1]).nil?
403
+ method_flag = METHOD_FLAGS[quality_method][:short_descriptor]
404
+ end
405
+ reason_code = nil
406
+ unless (reason_code = interval_event[:reason_code]).nil?
407
+ reason_code = REASON_CODES[reason_code.to_i]
408
+ end
409
+
410
+ case csv[3][0]
411
+ when 'E'
412
+ @interval_data[interval_start_point + (i-1)][:flag] = ['Estimate',method_flag,reason_code].compact.join(' - ')
413
+ when 'F'
414
+ @interval_data[interval_start_point + (i-1)][:flag] = nil
415
+ when 'S'
416
+ @interval_data[interval_start_point + (i-1)][:flag] = ['Substitute',method_flag,reason_code].compact.join(' - ')
417
+ end
418
+ end
419
+ @interval_events += interval_events
420
+ end
421
+ interval_events
422
+ end
423
+
424
+ # @param line [String] A single line in string format
425
+ # @return [Hash] the line parsed into a hash of information
426
+ def parse_nem12_500(line,options={})
427
+ end
428
+
429
+ # @param line [String] A single line in string format
430
+ # @return [Hash] the line parsed into a hash of information
431
+ def parse_nem12_900(line,options={})
432
+ end
433
+
434
+ # @param nmi [String] a NMI that is to be checked for validity
435
+ # @return [Boolean] determines if the NMI is valid
436
+ def self.valid_nmi?(nmi)
437
+ (nmi.class == String && nmi.length == 10 && !nmi.match(/^[A-Z1-9][A-Z0-9]{9}$/).nil?)
438
+ end
439
+
440
+ # @param path_to_file [String] the path to a file
441
+ # @return [] NEM12 object
442
+ def self.parse_nem12_file(path_to_file, strict = false)
443
+ parse_nem12(File.read(path_to_file),strict)
444
+ end
445
+
446
+ # @return [Array] array of a NEM12 file a given Meter + Data Stream for easy reading
447
+ def to_a
448
+ values = @interval_data.map{|d| [d[:data_details][:nmi],d[:data_details][:nmi_suffix].upcase,d[:data_details][:uom],d[:datetime],d[:value]]}
449
+ values
450
+ end
451
+
452
+ # @return [Array] CSV of a NEM12 file a given Meter + Data Stream for easy reading
453
+ def to_csv
454
+ headers = ['nmi','suffix','units','datetime','value']
455
+ ([headers]+self.to_a.map{|row| row[3]=row[3].strftime("%Y%m%d%H%M%S%z"); row}).map{|row| row.join(',')}.join("\n")
456
+ end
457
+
458
+ # @param contents [String] the path to a file
459
+ # @return [Array[AEMO::NEM12]] An array of NEM12 objects
460
+ def self.parse_nem12(contents, strict=false)
461
+ file_contents = contents.gsub(/\r/,"\n").gsub(/\n\n/,"\n").split("\n").delete_if{|line| line.empty? }
462
+ raise ArgumentError, 'First row should be have a RecordIndicator of 100 and be of type Header Record' unless file_contents.first.parse_csv[0] == '100'
463
+
464
+ nem12s = []
465
+ nem12_100 = AEMO::NEM12.parse_nem12_100(file_contents.first,:strict => strict)
466
+ nem12 = nil
467
+ file_contents.each do |line|
468
+ case line[0..2].to_i
469
+ when 200
470
+ nem12s << AEMO::NEM12.new('')
471
+ nem12s.last.parse_nem12_200(line)
472
+ when 300
473
+ nem12s.last.parse_nem12_300(line)
474
+ when 400
475
+ nem12s.last.parse_nem12_400(line)
476
+ # when 500
477
+ # nem12s.last.parse_nem12_500(line)
478
+ # when 900
479
+ # nem12s.last.parse_nem12_900(line)
480
+ end
481
+ end
482
+ # Return the array of NEM12 groups
483
+ nem12s
484
+ end
485
+
486
+ end
487
+ end