ekm-omnimeter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 10dccf5f6ac3dcbab08c817d8efe39f9781352d4
4
+ data.tar.gz: c890f15fa21dae3bee6478505dbde6f366318de0
5
+ SHA512:
6
+ metadata.gz: c78895c1cb06608cd7c23f0a489866c12305bef1532ee610cb8e6e69976868a96b5a16f977e080416dc8bdde0334346eced2ab543e593b4177bc9b04e6913578
7
+ data.tar.gz: cee6d71ff4faaec035eaae6daa1b798063814e53363a2b1a142ce7a3c5cc189f98324edf6202c87c505f6f5eff10a273991d8564b1a2f57ca337d9c18219dd38
data/.gitignore ADDED
@@ -0,0 +1,34 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /lib/bundler/man/
26
+
27
+ # for a library or gem, you might want to ignore these files since the code is
28
+ # intended to run in multiple environments; otherwise, check them in:
29
+ # Gemfile.lock
30
+ # .ruby-version
31
+ # .ruby-gemset
32
+
33
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
34
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ekm-meter.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,58 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ekm-omnimeter (0.1.0)
5
+ bigdecimal
6
+ daemons
7
+ log4r (~> 1.1)
8
+ trollop
9
+ xively-rb-connector (~> 0.1)
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ bigdecimal (1.2.5)
15
+ daemons (1.1.9)
16
+ diff-lcs (1.2.5)
17
+ httparty (0.13.0)
18
+ json (~> 1.8)
19
+ multi_xml (>= 0.5.2)
20
+ json (1.8.1)
21
+ log4r (1.1.10)
22
+ mini_portile (0.5.3)
23
+ multi_json (1.9.2)
24
+ multi_xml (0.5.5)
25
+ nokogiri (1.6.1)
26
+ mini_portile (~> 0.5.0)
27
+ ox (2.1.1)
28
+ rake (10.2.2)
29
+ rspec (2.14.1)
30
+ rspec-core (~> 2.14.0)
31
+ rspec-expectations (~> 2.14.0)
32
+ rspec-mocks (~> 2.14.0)
33
+ rspec-core (2.14.8)
34
+ rspec-expectations (2.14.5)
35
+ diff-lcs (>= 1.1.3, < 2.0)
36
+ rspec-mocks (2.14.6)
37
+ trollop (2.0)
38
+ xively-rb (0.2.10)
39
+ httparty (>= 0.10.0)
40
+ multi_json (>= 1.3.6)
41
+ multi_xml (>= 0.5.2)
42
+ nokogiri (>= 1.5.6)
43
+ ox (>= 1.5.9)
44
+ yajl-ruby (>= 1.1.0)
45
+ xively-rb-connector (0.1.1)
46
+ bigdecimal (~> 1.2)
47
+ log4r (~> 1.1)
48
+ xively-rb (~> 0.2)
49
+ yajl-ruby (1.2.0)
50
+
51
+ PLATFORMS
52
+ ruby
53
+
54
+ DEPENDENCIES
55
+ bundler (~> 1.3)
56
+ ekm-omnimeter!
57
+ rake (~> 10.2)
58
+ rspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Jordan Duggan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ ekm-omnimeter
2
+ =============
3
+
4
+ This ruby gem provides an interface to [EKM Omnimeter Pulse meters](http://www.ekmmetering.com/ekm-metering-products/electric-meters-kwh-meters/smart-meters-read-meter-remotely-automatically/omnimeter-pulse.html)
5
+ that are connected to a [EKM iSerial TCP/IP Ethernet to RS-485 Serial Converter](http://www.ekmmetering.com/ekm-metering-products/remote-meter-reading-solutions/ekm-iserial-v-2-tcp-ip-to-serial-converter-ethernet-connection.html).
6
+
7
+ [EKM Metering](http://www.ekmmetering.com) offers a range of digital utility metering and control devices, including the
8
+ Omnimeter Pulse Meter line of smart meters. The
9
+ [Omnimeter Pulse Meter](http://www.ekmmetering.com/ekm-metering-products/electric-meters-kwh-meters/smart-meters-read-meter-remotely-automatically/omnimeter-pulse.html)
10
+ contains a revenue-grade universal kWh power meter, three pulse counting inputs for
11
+ [water](http://www.ekmmetering.com/ekm-metering-products/water-meters.html) and/or
12
+ [gas](http://www.ekmmetering.com/ekm-metering-products/gas-meters/pulse-output-gas-meter-pgm-1-read-gas-consumption-remotely.html) metering,
13
+ and 2 controllable 50 mA at 12 volts relay outputs that can toggle 120v circuits when connected to their [EKM Switch120 power cord](http://www.ekmmetering.com/ekm-metering-products/accessories/switch120.html).
14
+
15
+ TODO:
16
+ * Cast values to their proper type and precision
17
+ * Add specs
18
+ * Add daemon to monitor meter's output at regular intervals
19
+
20
+ NOTE:
21
+ ## Requirements
22
+
23
+ * Ruby 2.0.0 or higher
24
+ * A [EKM Omnimeter Pulse meters](http://www.ekmmetering.com/ekm-metering-products/electric-meters-kwh-meters/smart-meters-read-meter-remotely-automatically/omnimeter-pulse.html)
25
+ * A [EKM iSerial TCP/IP Ethernet to RS-485 Serial Converter](http://www.ekmmetering.com/ekm-metering-products/remote-meter-reading-solutions/ekm-iserial-v-2-tcp-ip-to-serial-converter-ethernet-connection.html)
26
+
27
+
28
+ ## Contact, feedback and bugs
29
+
30
+ This interface was not developed or reviewed by EKM Metering. They bare no responsibility for its quality, performance, or results. Use at your own risk.
31
+
32
+ Please file bugs / issues and feature requests on the [issue tracker](https://github.com/jwtd/ekm-omnimeter/issues)
33
+
34
+ ## Install
35
+
36
+ ```
37
+ gem install ekm-omnimeter
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ ```ruby
43
+
44
+ require 'ekm-omnimeter'
45
+
46
+ # Connect to the meter
47
+ m = EkmOmnimeter::Meter.new(
48
+ :power_configuration => :single_phase_3wire, # Valid values are :single_phase_2wire, :single_phase_3wire, :three_phase_3wire, :three_phase_4wire
49
+ :meter_number=>300000001, # Your nine digit meter id
50
+ :remote_address=>'192.168.1.125', # The IP address of your iSerial device
51
+ :remote_port => 50000) # The port on which your iSerial device is listening
52
+
53
+ # Read some values
54
+ m.remote_address # 192.168.1.125
55
+ m.remote_port # 50000
56
+ m.meter_number # 000300000001
57
+ m.address # 000300000001
58
+ m.measurement_timestamp # 2014-04-03 05:03:01
59
+ m.total_active_kwh # 00450609
60
+ m.total_kvarh # 00080417
61
+ m.total_rev_kwh # 00007590
62
+ m.three_phase_kwh # 002115660023898000000000
63
+ m.three_phase_rev_kwh # 000000000000759000000000
64
+ m.resettable_kwh # 00450609
65
+ m.resettable_reverse_kwh # 00007590
66
+ m.volts_l1 # 1245
67
+ m.volts_l2 # 1254
68
+ m.volts_l3 # 0000
69
+ m.amps_l1 # 00200
70
+ m.amps_l2 # 00056
71
+ m.amps_l3 # 00000
72
+ m.watts_l1 # 0002504
73
+ m.watts_l2 # 0000664
74
+ m.watts_l3 # 0000000
75
+ m.watts_total # 0003172
76
+ m.cosϴ_l1 # 100
77
+ m.cosϴ_l2 # 100
78
+ m.cosϴ_l3 # C000
79
+ m.var_l1 # 0000052
80
+ m.var_l2 # 0000000
81
+ m.var_l3 # 0000000
82
+ m.var_total # 0000052
83
+ m.freq # 6006
84
+ m.pulse_count_1 # 00000003
85
+ m.pulse_count_2 # 00000006
86
+ m.pulse_count_3 # 00000000
87
+ m.pulse_input_hilo # 4
88
+ m.direction_of_current # 1
89
+ m.outputs_onoff # 1
90
+ m.kwh_data_decimal_places # 2
91
+
92
+
93
+ ```
94
+
95
+ ## Special Thanks
96
+
97
+ * EKM Metering - for an awesome piece of hardware
98
+ * Ian Duggan - for introducing me to ruby
99
+
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ekm-omnimeter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ekm-omnimeter"
8
+ spec.version = EkmOmnimeter::VERSION::STRING
9
+ spec.authors = ["Jordan Duggan"]
10
+ spec.email = ["Jordan.Duggan@gmail.com"]
11
+ spec.description = %q{Ruby interface to the EKM Omnimeter Pulse}
12
+ spec.summary = %q{This ruby gem provides an interface to EKM Omnimeter Pulse meters that are connected to a EKM iSerial TCP/IP Ethernet to RS-485 Serial Converter. Omnimeter Pulse meters contain a revenue-grade universal kWh power meter, three pulse counting inputs for water and/or gas metering, and 2 controllable 50 mA at 12 volts relay outputs.}
13
+ spec.homepage = "https://github.com/jwtd/ekm-omnimeter"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ # Development dependencies
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake", "~> 10.2"
24
+ spec.add_development_dependency "rspec", "~> 2.14"
25
+
26
+ # Runtime dependencies
27
+ spec.add_runtime_dependency "log4r", "~> 1.1"
28
+ spec.add_runtime_dependency "xively-rb-connector", "~> 0.1"
29
+ spec.add_runtime_dependency "bigdecimal", "~> 1.2"
30
+
31
+
32
+ end
@@ -0,0 +1,8 @@
1
+ module EkmOmnimeter
2
+ # Base Ekm exception class
3
+ class EkmOmnimeterError < ::Exception; end
4
+ end
5
+
6
+ require "ekm-omnimeter/version"
7
+ require "ekm-omnimeter/logging"
8
+ require "ekm-omnimeter/meter"
@@ -0,0 +1,25 @@
1
+ require 'log4r'
2
+ include Log4r
3
+ module EkmOmnimeter
4
+
5
+ module Logging
6
+
7
+ def logger
8
+ @logger ||= EkmOmnimeter::Logging.logger
9
+ end
10
+
11
+ def self.logger
12
+ @logger ||= self.configure_logger_for(self.class.name)
13
+ end
14
+
15
+ def self.configure_logger_for(classname)
16
+ l = Logger.new(classname)
17
+ l.level = ERROR
18
+ l.trace = false
19
+ l.add Log4r::Outputter.stderr
20
+ l
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,359 @@
1
+ require 'socket'
2
+ require 'ostruct'
3
+
4
+ # Convert byte array to string
5
+ # %w(01 52 31 02 30 30 31 31 28 29 03 13 16).map{|a| a.to_i(16).chr}.join
6
+
7
+ module EkmOmnimeter
8
+ # EKM Omnimeter Pulse v.4 - Pulse Counting, Relay Controlling, Universal Smart Electric Meter
9
+ # EKM Developer Portal http://www.ekmmetering.com/developer-portal
10
+ # PHP Client http://forum.ekmmetering.com/viewtopic.php?f=4&t=3206
11
+ class Meter
12
+
13
+ VALID_POWER_CONFIGURATIONS = [:single_phase_2wire, :single_phase_3wire, :three_phase_3wire, :three_phase_4wire]
14
+
15
+ # Initialization attributes
16
+ attr_reader :meter_number, :remote_address, :remote_port, :power_configuration
17
+
18
+ # Request A
19
+ #attr_reader :meter_type, :meter_firmware, :address, :total_active_kwh, :total_kvarh, :total_rev_kwh, :three_phase_kwh, :three_phase_rev_kwh, :resettable_kwh, :resettable_reverse_kwh, :volts_l1, :volts_l2, :volts_l3, :amps_l1, :amps_l2, :amps_l3, :watts_l1, :watts_l2, :watts_l3, :watts_total, :cosϴ_l1, :cosϴ_l2, :cosϴ_l3, :var_l1, :var_l2, :var_l3, :var_total, :freq, :pulse_count_1, :pulse_count_2, :pulse_count_3, :pulse_input_hilo, :direction_of_current, :outputs_onoff, :kwh_data_decimal_places,
20
+
21
+ # Request B
22
+ #attr_reader :t1_t2_t3_t4_kwh, :t1_t2_t3_t4_rev_kwh, :maximum_demand, :maximum_demand_time, :pulse_ratio_1, :pulse_ratio_2, :pulse_ratio_3, :ct_ratio, :auto_reset_md, :settable_imp_per_kWh_constant
23
+
24
+ # Mix in the ability to log
25
+ include Logging
26
+
27
+ def initialize(options)
28
+
29
+ @logger = logger || options[:logger]
30
+
31
+ # Test logging call
32
+ @logger.info "Initializing Meter"
33
+
34
+ # Prepend the meter number with the correct amount of leading zeros
35
+ @meter_number = options[:meter_number].to_s.rjust(12, '0')
36
+ @remote_address = options[:remote_address] || '192.168.0.125'
37
+ @remote_port = options[:remote_port] || 50000
38
+ @logger.debug "meter_number: #{meter_number}"
39
+ @logger.debug "remote_address: #{remote_address}"
40
+ @logger.debug "remote_port: #{remote_port}"
41
+
42
+ # Collect the power configurations
43
+ if VALID_POWER_CONFIGURATIONS.index(options[:power_configuration])
44
+ power_configuration = options[:power_configuration]
45
+ else
46
+ raise EkmOmnimeterError, "Invalid power configuration #{options[:power_configuration]}. Valid values are #{VALID_POWER_CONFIGURATIONS.join(', ')}"
47
+ end
48
+
49
+ # Collect pulse inputs
50
+ #:ekm_gas_meter
51
+ #:ekm_water_meter
52
+ #@pulse_input_1_device = options[:pulse_input_1_device] || nil
53
+ #@pulse_input_2_device = options[:pulse_input_3_device] || nil
54
+ #@pulse_input_3_device = options[:pulse_input_2_device] || nil
55
+
56
+ @values = {}
57
+ @last_update = nil
58
+
59
+ # Get values
60
+ request_a()
61
+
62
+ end
63
+
64
+ # Alias request_a with read
65
+ def read
66
+ request_a()
67
+ end
68
+
69
+ # Formatted datetime reported by meter during last read
70
+ def measurement_timestamp
71
+ "20#{current_time[0,2]}-#{current_time[2,2]}-#{current_time[4,2]} #{current_time[6,2]}:#{current_time[ 8,2]}:#{current_time[10,2]}"
72
+ end
73
+
74
+ # Attribute handler that delegates attribute reads to the values hash
75
+ def method_missing(method_sym, *arguments, &block)
76
+
77
+ @logger.debug "method_missing #{method_sym.inspect}"
78
+
79
+ # Only refresh data if its more than 0.25 seconds old
80
+ et = @last_update.nil? ? 0 : (Time.now - @last_update)
81
+ @logger.debug "Elapsed time since last read #{et}"
82
+ if et > 250
83
+ @logger.info "More than 250 milliseconds have passed, updating data"
84
+ read()
85
+ end
86
+
87
+ if @values.include? method_sym
88
+ @logger.debug "Found #{method_sym}"
89
+ @values[method_sym]
90
+ else
91
+ @logger.debug "Didn't find #{method_sym}"
92
+ super
93
+ end
94
+ end
95
+
96
+ # Attribute responder that delegates check of attribute existence to the values hash
97
+ def respond_to?(method_sym, include_private = false)
98
+ if @values.include? method_sym
99
+ true
100
+ else
101
+ super
102
+ end
103
+ end
104
+
105
+
106
+ ## Request A
107
+ #d[:meter_type] # 2 Byte Meter Type
108
+ #d[:meter_firmware] # 1 Byte Meter Firmware
109
+ #d[:address] # 12 Bytes Address
110
+ #d[:total_active_kwh] # 8 Bytes total Active kWh
111
+ #d[:total_kvarh] # 8 Bytes Total kVARh
112
+ #d[:total_rev_kwh] # 8 Bytes Total Rev.kWh
113
+ #d[:three_phase_kwh] # 24 Bytes 3 phase kWh
114
+ #d[:three_phase_rev_kwh] # 24 Bytes 3 phase Rev.kWh
115
+ #d[:resettable_kwh] # 8 Bytes Resettable kWh
116
+ #d[:resettable_reverse_kwh] # 8 bytes Resettable Reverse kWh
117
+ #d[:volts_l1] # 4 Bytes Volts L1
118
+ #d[:volts_l2] # 4 Bytes Volts L2
119
+ #d[:volts_l3] # 4 Bytes Volts L3
120
+ #d[:amps_l1] # 5 Bytes Amps L1
121
+ #d[:amps_l2] # 5 Bytes Amps L2
122
+ #d[:amps_l3] # 5 Bytes Amps L3
123
+ #d[:watts_l1] # 7 Bytes Watts L1
124
+ #d[:watts_l2] # 7 Bytes Watts L2
125
+ #d[:watts_l3] # 7 Bytes Watts L3
126
+ #d[:watts_total] # 7 Bytes Watts Total
127
+ #d[:cosϴ_l1] # 4 Bytes Cosϴ L1
128
+ #d[:cosϴ_l2] # 4 Bytes Cosϴ L2
129
+ #d[:cosϴ_l3] # 4 Bytes Cosϴ L3
130
+ #d[:var_l1] # 7 Bytes VAR L1
131
+ #d[:var_l2] # 7 Bytes VAR L2
132
+ #d[:var_l3] # 7 Bytes VAR L3
133
+ #d[:var_total] # 7 Bytes VAR Total
134
+ #d[:freq] # 4 Bytes Freq
135
+ #d[:pulse_count_1] # 8 Bytes Pulse Count 1
136
+ #d[:pulse_count_2] # 8 Bytes Pulse Count 2
137
+ #d[:pulse_count_3] # 8 Bytes Pulse Count 3
138
+ #d[:pulse_input_hilo] # 1 Byte Pulse Input Hi/Lo
139
+ #d[:direction_of_current] # 1 Bytes direction of current
140
+ #d[:outputs_onoff] # 1 Byte Outputs On/Off
141
+ #d[:kwh_data_decimal_places] # 1 Byte kWh Data Decimal Places
142
+
143
+ ## Request B
144
+ #d[:t1_t2_t3_t4_kwh] # 32 Bytes T1, T2, T3, T4 kwh
145
+ #d[:t1_t2_t3_t4_rev_kwh] # 32 Bytes T1, T2, T3, T4 Rev kWh
146
+ #d[:maximum_demand] # 8 Bytes Maximum Demand
147
+ #d[:maximum_demand_time] # 1 Byte Maximum Demand Time
148
+ #d[:pulse_ratio_1] # 4 Bytes Pulse Ratio 1
149
+ #d[:pulse_ratio_2] # 4 Bytes Pulse Ratio 2
150
+ #d[:pulse_ratio_3] # 4 Bytes Pulse Ratio 3
151
+ #d[:ct_ratio] # 4 Bytes CT Ratio
152
+ #d[:auto_reset_md] # 1 Bytes Auto Reset MD
153
+ #d[:settable_imp_per_kWh_constant] # 4 Bytes Settable Imp/kWh Constant
154
+
155
+
156
+ # iSerial v4 Spec From http://documents.ekmmetering.com/Omnimeter-Pulse-v.4-Protocol.pdf
157
+ # %w(01 52 31 02 30 30 31 31 28 29 03 13 16).map{|a| a.to_i(16).chr}.join
158
+
159
+ # Returns the correct measurement for voltage, current, and power based on the corresponding power_configuration
160
+ def calculate_measurement(m1, m2, m3)
161
+ if power_configuration == :single_phase_2wire
162
+ volts_l1
163
+ elsif power_configuration == :single_phase_3wire
164
+ (volts_l1 + volts_l2)
165
+ elsif power_configuration == :three_phase_3wire
166
+ (volts_l1 + :volts_l3)
167
+ elsif power_configuration == :three_phase_4wire
168
+ (volts_l1 + volts_l2 + volts_l3)
169
+ end
170
+ end
171
+
172
+ #Request A:
173
+ def request_a
174
+
175
+ # 2F 3F 12 Bytes Address 30 30 21 0D 0A
176
+ # /?00000000012300! then a CRLF
177
+ request = "/?" + meter_number + "00!\r\n"
178
+ read_bytes = 255
179
+ logger.debug "Socket write #{request}" unless logger.nil?
180
+ response = get_remote_meter_data(request, read_bytes)
181
+ raise EkmError if response.nil?
182
+
183
+ # Split the response string into an array and prepare a hash to store the values
184
+ a = response.split('')
185
+ d = {}
186
+
187
+ # Return (255 Bytes total) :
188
+ a.shift(1) # 02
189
+ d[:meter_type] = a.shift(2) # 2 Byte Meter Type
190
+ d[:meter_firmware] = a.shift(1) # 1 Byte Meter Firmware
191
+ d[:address] = a.shift(12) # 12 Bytes Address
192
+ d[:total_active_kwh] = a.shift(8) # 8 Bytes total Active kWh
193
+ d[:total_kvarh] = a.shift(8) # 8 Bytes Total kVARh
194
+ d[:total_rev_kwh] = a.shift(8) # 8 Bytes Total Rev.kWh
195
+ d[:three_phase_kwh] = a.shift(24) # 24 Bytes 3 phase kWh
196
+ d[:three_phase_rev_kwh] = a.shift(24) # 24 Bytes 3 phase Rev.kWh
197
+ d[:resettable_kwh] = a.shift(8) # 8 Bytes Resettable kWh
198
+ d[:resettable_reverse_kwh] = a.shift(8) # 8 bytes Resettable Reverse kWh
199
+ d[:volts_l1] = a.shift(4) # 4 Bytes Volts L1
200
+ d[:volts_l2] = a.shift(4) # 4 Bytes Volts L2
201
+ d[:volts_l3] = a.shift(4) # 4 Bytes Volts L3
202
+ d[:amps_l1] = a.shift(5) # 5 Bytes Amps L1
203
+ d[:amps_l2] = a.shift(5) # 5 Bytes Amps L2
204
+ d[:amps_l3] = a.shift(5) # 5 Bytes Amps L3
205
+ d[:watts_l1] = a.shift(7) # 7 Bytes Watts L1
206
+ d[:watts_l2] = a.shift(7) # 7 Bytes Watts L2
207
+ d[:watts_l3] = a.shift(7) # 7 Bytes Watts L3
208
+ d[:watts_total] = a.shift(7) # 7 Bytes Watts Total
209
+ d[:cosϴ_l1] = a.shift(4) # 4 Bytes Cosϴ L1
210
+ d[:cosϴ_l2] = a.shift(4) # 4 Bytes Cosϴ L2
211
+ d[:cosϴ_l3] = a.shift(4) # 4 Bytes Cosϴ L3
212
+ d[:var_l1] = a.shift(7) # 7 Bytes VAR L1
213
+ d[:var_l2] = a.shift(7) # 7 Bytes VAR L2
214
+ d[:var_l3] = a.shift(7) # 7 Bytes VAR L3
215
+ d[:var_total] = a.shift(7) # 7 Bytes VAR Total
216
+ d[:freq] = a.shift(4) # 4 Bytes Freq
217
+ d[:pulse_count_1] = a.shift(8) # 8 Bytes Pulse Count 1
218
+ d[:pulse_count_2] = a.shift(8) # 8 Bytes Pulse Count 2
219
+ d[:pulse_count_3] = a.shift(8) # 8 Bytes Pulse Count 3
220
+ d[:pulse_input_hilo] = a.shift(1) # 1 Byte Pulse Input Hi/Lo
221
+ d[:direction_of_current] = a.shift(1) # 1 Bytes direction of current
222
+ d[:outputs_onoff] = a.shift(1) # 1 Byte Outputs On/Off
223
+ d[:kwh_data_decimal_places] = a.shift(1) # 1 Byte kWh Data Decimal Places
224
+ a.shift(2) # 2 Bytes Reserved
225
+ d[:current_time] = a.shift(14) # 14 Bytes Current Time
226
+ a.shift(6) # 30 30 21 0D 0A 03
227
+ #d[] = a.shift(2) # 2 Bytes CRC16
228
+
229
+ # Smash arrays into strungs
230
+ d.each {|k,v| d[k] = v.join('')}
231
+
232
+ # Merge to values and reset time
233
+ @values.merge!(d)
234
+ @last_update = Time.now
235
+
236
+ # Calculate totals based on wiring configuration
237
+ @values[:measurement_timestamp] = measurement_timestamp
238
+ @values[:volts] = calculate_measurement(d[:volts_l1], d[:volts_l2], d[:volts_l3])
239
+ @values[:amps] = calculate_measurement(d[:amps_l1], d[:amps_l2], d[:amps_l3])
240
+ @values[:watts] = calculate_measurement(d[:watts_l1], d[:watts_l2], d[:watts_l3])
241
+
242
+ # Return the hash as an open struct
243
+ return d
244
+
245
+ end
246
+
247
+
248
+ # Request B:
249
+ def request_b
250
+
251
+ # 2F 3F 12 Bytes Address 30 31 21 0D 0A
252
+ # /?00000000012301! then a CRLF
253
+ request = "/?" + meter_number + "01!\r\n"
254
+ read_bytes = 255
255
+ logger.debug "Socket write #{request}" unless logger.nil?
256
+ response = get_remote_meter_data(request, read_bytes)
257
+ raise EkmError if response.nil?
258
+
259
+ # Split the response string into an array and prepare a hash to store the values
260
+ a = s.split('')
261
+ d = {}
262
+
263
+ # Return (255 Bytes total) :
264
+ a.shift(1) # 02
265
+ d[:meter_type] = a.shift(2) # 2 Byte Meter Type
266
+ d[:meter_firmware] = a.shift(1) # 1 Byte Meter Firmware
267
+ d[:address] = a.shift(12) # 12 Bytes Address
268
+ # Diff from request A start
269
+ d[:t1_t2_t3_t4_kwh] = a.shift(32) # 32 Bytes T1, T2, T3, T4 kwh
270
+ d[:t1_t2_t3_t4_rev_kwh] = a.shift(32) # 32 Bytes T1, T2, T3, T4 Rev kWh
271
+ # Diff from request A end
272
+ d[:volts_l1] = a.shift(4) # 4 Bytes Volts L1
273
+ d[:volts_l2] = a.shift(4) # 4 Bytes Volts L2
274
+ d[:volts_l3] = a.shift(4) # 4 Bytes Volts L3
275
+ d[:amps_l1] = a.shift(5) # 5 Bytes Amps L1
276
+ d[:amps_l2] = a.shift(5) # 5 Bytes Amps L2
277
+ d[:amps_l3] = a.shift(5) # 5 Bytes Amps L3
278
+ d[:watts_l1] = a.shift(7) # 7 Bytes Watts L1
279
+ d[:watts_l2] = a.shift(7) # 7 Bytes Watts L2
280
+ d[:watts_l3] = a.shift(7) # 7 Bytes Watts L3
281
+ d[:watts_total] = a.shift(7) # 7 Bytes Watts Total
282
+ d[:cosϴ_l1] = a.shift(4) # 4 Bytes Cosϴ L1
283
+ d[:cosϴ_l2] = a.shift(4) # 4 Bytes Cosϴ L2
284
+ d[:cosϴ_l3] = a.shift(4) # 4 Bytes Cosϴ L3
285
+ # Diff from request A start
286
+ d[:maximum_demand] = a.shift(8) # 8 Bytes Maximum Demand
287
+ d[:maximum_demand_time] = a.shift(1) # 1 Byte Maximum Demand Time
288
+ d[:pulse_ratio_1] = a.shift(4) # 4 Bytes Pulse Ratio 1
289
+ d[:pulse_ratio_2] = a.shift(4) # 4 Bytes Pulse Ratio 2
290
+ d[:pulse_ratio_3] = a.shift(4) # 4 Bytes Pulse Ratio 3
291
+ d[:ct_ratio] = a.shift(4) # 4 Bytes CT Ratio
292
+ d[:auto_reset_md] = a.shift(1) # 1 Bytes Auto Reset MD
293
+ d[:settable_imp_per_kWh_constant] = a.shift(4) # 4 Bytes Settable Imp/kWh Constant
294
+ # Diff from request A end
295
+ a.shift(56) # 56 Bytes Reserved
296
+ d[:current_time] = a.shift(14) # 14 Bytes Current Time
297
+ a.shift(6) # 30 30 21 0D 0A 03
298
+ d[] = a.shift(2) # 2 Bytes CRC16
299
+
300
+ # Smash arrays into strungs
301
+ d.each {|k,v| d[k] = v.join('')}
302
+
303
+ # Merge to values and reset time
304
+ @values.merge!(d)
305
+ @last_update = Time.now
306
+
307
+ # Calculate totals based on wiring configuration
308
+ @values[:measurement_timestamp] = measurement_timestamp
309
+ @values[:volts] = calculate_measurement(d[:volts_l1], d[:volts_l2], d[:volts_l3])
310
+ @values[:amps] = calculate_measurement(d[:amps_l1], d[:amps_l2], d[:amps_l3])
311
+ @values[:watts] = calculate_measurement(d[:watts_l1], d[:watts_l2], d[:watts_l3])
312
+
313
+ # Return the hash as an open struct
314
+ return d
315
+
316
+ end
317
+
318
+
319
+
320
+ # Gets remote EKM meter data using iSerial defaults
321
+ # meter_number is the meters serial number. leading 0s not required.
322
+ # remote_address is the IP address of the ethernet-RS485 converter
323
+ # remote_port is the TCP port number the converter is listening to (50000 in my case)
324
+ # We do not check the checksum - I'm lazy. Probably should. Running for 8 months, querying
325
+ # once per minute..I've encountered one or two bad results from the meter.
326
+ def get_remote_meter_data(request, read_bytes)
327
+
328
+ logger.debug "get_remote_meter_data #{request} from meter# #{meter_number}" unless logger.nil?
329
+
330
+ # connect to the meter and check to make sure we connected
331
+ begin
332
+
333
+ socket = TCPSocket.new(remote_address, remote_port)
334
+ logger.debug "Socket open" unless logger.nil?
335
+
336
+ # Send request to the meter
337
+ logger.debug "Request: #{request}" unless logger.nil?
338
+ socket.write(request)
339
+
340
+ # Receive a response of 255 bytes
341
+ response = socket.read(read_bytes)
342
+ logger.debug "Socket response #{response.length}" unless logger.nil?
343
+ logger.debug response unless logger.nil?
344
+
345
+ rescue Exception => ex
346
+ logger.error "Exception\n#{ex.message}\n#{ex.backtrace.join("\n")}" unless logger.nil?
347
+ ensure
348
+ # EKM Meter software sends this just before closing the connection, so we will too
349
+ socket.write "\x0a\x03\x32\x3d"
350
+ socket.close
351
+ logger.debug "Socket closed" unless logger.nil?
352
+ end
353
+
354
+ return response
355
+
356
+ end
357
+
358
+ end
359
+ end
@@ -0,0 +1,14 @@
1
+ module EkmOmnimeter
2
+
3
+ def self.version_string
4
+ "ekm-omnimeter version #{EkmOmnimeter::VERSION::STRING}"
5
+ end
6
+
7
+ module VERSION #:nodoc:
8
+ MAJOR = 0
9
+ MINOR = 1
10
+ PATCH = 0
11
+
12
+ STRING = [MAJOR, MINOR, PATCH].join('.')
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe EkmOmnimeter::Meter do
4
+
5
+ describe "#initialize" do
6
+ it "should ..." do
7
+
8
+ #:meter_number
9
+ #:remote_address
10
+ #:remote_port
11
+
12
+ end
13
+ end
14
+
15
+ describe "#read" do
16
+ it "should ..." do
17
+ end
18
+ end
19
+
20
+ describe "#meter_timestamp" do
21
+ it "should ..." do
22
+ end
23
+ end
24
+
25
+ end
26
+
@@ -0,0 +1,42 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ Bundler.setup
4
+
5
+ require 'rspec'
6
+ require 'time'
7
+
8
+ # figure out where we are being loaded from
9
+ if $LOADED_FEATURES.grep(/spec\/spec_helper\.rb/).any?
10
+ begin
11
+ raise "foo"
12
+ rescue => e
13
+ puts <<-MSG
14
+ ===================================================
15
+ It looks like spec_helper.rb has been loaded
16
+ multiple times. Normalize the require to:
17
+
18
+ require "spec/spec_helper"
19
+
20
+ Things like File.join and File.expand_path will
21
+ cause it to be loaded multiple times.
22
+
23
+ Loaded this time from:
24
+
25
+ #{e.backtrace.join("\n ")}
26
+ ===================================================
27
+ MSG
28
+ end
29
+ end
30
+
31
+ $:.push File.expand_path("../lib", __FILE__)
32
+ require 'ekm-omnimeter'
33
+
34
+ RSpec.configure do |config|
35
+ config.color_enabled = true
36
+ config.formatter = 'documentation'
37
+ end
38
+
39
+
40
+
41
+
42
+
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ekm-omnimeter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jordan Duggan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '10.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '10.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '2.14'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '2.14'
55
+ - !ruby/object:Gem::Dependency
56
+ name: log4r
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: xively-rb-connector
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '0.1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '0.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bigdecimal
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '1.2'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '1.2'
97
+ description: Ruby interface to the EKM Omnimeter Pulse
98
+ email:
99
+ - Jordan.Duggan@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - .gitignore
105
+ - Gemfile
106
+ - Gemfile.lock
107
+ - LICENSE
108
+ - README.md
109
+ - Rakefile
110
+ - ekm-omnimeter.gemspec
111
+ - lib/ekm-omnimeter.rb
112
+ - lib/ekm-omnimeter/logging.rb
113
+ - lib/ekm-omnimeter/meter.rb
114
+ - lib/ekm-omnimeter/version.rb
115
+ - spec/meter_spec.rb
116
+ - spec/spec_helper.rb
117
+ homepage: https://github.com/jwtd/ekm-omnimeter
118
+ licenses:
119
+ - MIT
120
+ metadata: {}
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubyforge_project:
137
+ rubygems_version: 2.2.2
138
+ signing_key:
139
+ specification_version: 4
140
+ summary: This ruby gem provides an interface to EKM Omnimeter Pulse meters that are
141
+ connected to a EKM iSerial TCP/IP Ethernet to RS-485 Serial Converter. Omnimeter
142
+ Pulse meters contain a revenue-grade universal kWh power meter, three pulse counting
143
+ inputs for water and/or gas metering, and 2 controllable 50 mA at 12 volts relay
144
+ outputs.
145
+ test_files:
146
+ - spec/meter_spec.rb
147
+ - spec/spec_helper.rb