aemo 0.1.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.
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