Browse files

Fixed date parsing powerfactor formatting. Added CRC16 algo but its n…

…ot working yet.
  • Loading branch information...
1 parent dfc61f6 commit d3612f9eaf621376dc75b47d698bc7c9c986ab7a @jwtd committed Apr 4, 2014
Showing with 131 additions and 98 deletions.
  1. +8 −8 README.md
  2. +3 −1 ekm-omnimeter.gemspec
  3. +1 −0 lib/ekm-omnimeter.rb
  4. +54 −0 lib/ekm-omnimeter/crc16.rb
  5. +64 −88 lib/ekm-omnimeter/meter.rb
  6. +1 −1 lib/ekm-omnimeter/version.rb
View
16 README.md
@@ -49,16 +49,16 @@ m = EkmOmnimeter::Meter.new(
:remote_port => 50000) # The port on which your iSerial device is listening
# These values change based on the value of :power_configuration in the constructor
+m.meter_number # 000300000234
+m.remote_address # 192.168.0.125
+m.remote_port # 50000
m.volts # 248.7
m.amps # 21.6
m.watts # 2664
-# Warning: Dates seem off
-# My computer clock said 2014-04-03 3:32PM when these outputs came out
-m.meter_timestamp # 2014-04-03 05:15:32
-m.computer_timestamp # Time.now() = 2014-04-03 15:32:10 -0400
-
# Read meter values
+m.meter_timestamp # 2014-04-04 12:48:06
+m.last_read_timestamp # 2014-04-04 12:48:17 -0400
m.remote_address # 192.168.1.125
m.remote_port # 50000
m.meter_number # 000300000001
@@ -85,9 +85,9 @@ m.watts_l1 # 2276
m.watts_l2 # 384
m.watts_l3 # 0
m.watts_total # 2664
-m.power_factor_1 # 1.0
-m.power_factor_2 # 0.0
-m.power_factor_3 # 0.0
+m.power_factor_1 # 1.0
+m.power_factor_2 # C0.99
+m.power_factor_3 # C0.0
m.maximum_demand # 22640.0
m.maximum_demand_period # 15 (in minutes, can be 15, 30 or 60)
m.ct_ratio # 400
View
4 ekm-omnimeter.gemspec
@@ -25,7 +25,9 @@ Gem::Specification.new do |spec|
# Runtime dependencies
spec.add_runtime_dependency "log4r", "~> 1.1"
- spec.add_runtime_dependency "xively-rb-connector", "~> 0.1"
+ #spec.add_runtime_dependency "xively-rb-connector", "~> 0.1"
+ #spec.add_runtime_dependency "trollop"
+ #spec.add_runtime_dependency "daemons"
end
View
1 lib/ekm-omnimeter.rb
@@ -5,4 +5,5 @@ class EkmOmnimeterError < ::Exception; end
require "ekm-omnimeter/version"
require "ekm-omnimeter/logging"
+require "ekm-omnimeter/crc16"
require "ekm-omnimeter/meter"
View
54 lib/ekm-omnimeter/crc16.rb
@@ -0,0 +1,54 @@
+module Crc16
+
+ # Returns boolean true or false if checksum of string s matches expected value
+ def self.check_crc16(s, expecting)
+ (Crc16.crc16(s) == expecting)
+ end
+
+ # Calculates CRC16 checksum of string buf
+ def self.crc16(buf)
+ crc = 0x00
+ buf.each_byte do |b|
+ crc = ((crc >> 8) & 0xff) ^ CRC_LOOKUP[(crc ^ b) & 0xff]
+ end
+ crc
+ end
+
+ private
+
+ CRC_LOOKUP = [
+ 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
+ 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
+ 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
+ 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
+ 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
+ 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
+ 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
+ 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
+ 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
+ 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
+ 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
+ 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
+ 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
+ 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
+ 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
+ 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
+ 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
+ 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
+ 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
+ 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
+ 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
+ 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
+ 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
+ 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
+ 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
+ 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
+ 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
+ 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
+ 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
+ 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
+ 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
+ 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
+ ]
+
+end
View
152 lib/ekm-omnimeter/meter.rb
@@ -13,14 +13,17 @@ class Meter
VALID_POWER_CONFIGURATIONS = [:single_phase_2wire, :single_phase_3wire, :three_phase_3wire, :three_phase_4wire]
# Initialization attributes
- attr_reader :meter_number, :remote_address, :remote_port, :power_configuration, :last_read_timestamp
+ attr_reader :meter_number, :remote_address, :remote_port, :verify_checksums, :power_configuration, :last_read_timestamp
# Request A
#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,
# Request B
#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
+ # iSerial v4 Spec From http://documents.ekmmetering.com/Omnimeter-Pulse-v.4-Protocol.pdf
+ # %w(01 52 31 02 30 30 31 31 28 29 03 13 16).map{|a| a.to_i(16).chr}.join
+
# Mix in the ability to log
include Logging
@@ -32,16 +35,19 @@ def initialize(options)
@logger.info "Initializing Meter"
# Prepend the meter number with the correct amount of leading zeros
- @meter_number = options[:meter_number].to_s.rjust(12, '0')
- @remote_address = options[:remote_address] || '192.168.0.125'
- @remote_port = options[:remote_port] || 50000
+ @meter_number = options[:meter_number].to_s.rjust(12, '0')
+ @remote_address = options[:remote_address] || '192.168.0.125'
+ @remote_port = options[:remote_port] || 50000
+ @verify_checksums = options[:verify_checksums] || false
@logger.debug "meter_number: #{meter_number}"
@logger.debug "remote_address: #{remote_address}"
@logger.debug "remote_port: #{remote_port}"
+ @logger.debug "verify_checksums: #{verify_checksums}"
# Collect the power configurations
if VALID_POWER_CONFIGURATIONS.index(options[:power_configuration])
@power_configuration = options[:power_configuration]
+ @logger.debug "power_configuration: #{@power_configuration}"
else
raise EkmOmnimeterError, "Invalid power configuration #{options[:power_configuration]}. Valid values are #{VALID_POWER_CONFIGURATIONS.join(', ')}"
end
@@ -68,29 +74,21 @@ def read
@values
end
- # Formatted datetime reported by meter during last read
- def meter_timestamp
- "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]}"
- end
# Attribute handler that delegates attribute reads to the values hash
def method_missing(method_sym, *arguments, &block)
-
- #@logger.debug "method_missing #{method_sym.inspect}"
-
# Only refresh data if its more than 0.25 seconds old
et = @last_read_timestamp.nil? ? 0 : (Time.now - @last_read_timestamp)
- #logger.debug "Elapsed time since last read #{et}"
+ @logger.debug "Elapsed time since last read #{et}"
if et > 250
- @logger.info "More than 250 milliseconds have passed, updating data"
+ @logger.info "More than 250 milliseconds have passed since last read. Triggering refresh."
read()
end
if @values.include? method_sym
- #logger.debug "Found #{method_sym}"
@values[method_sym]
else
- #logger.debug "Didn't find #{method_sym}"
+ @logger.debug "method_missing failed to find #{method_sym} in the Meter.values cache"
super
end
end
@@ -105,59 +103,6 @@ def respond_to?(method_sym, include_private = false)
end
- ## Request A
- #d[:meter_type] # 2 Byte Meter Type
- #d[:meter_firmware] # 1 Byte Meter Firmware
- #d[:address] # 12 Bytes Address
- #d[:total_active_kwh] # 8 Bytes total Active kWh
- #d[:total_kvarh] # 8 Bytes Total kVARh
- #d[:total_rev_kwh] # 8 Bytes Total Rev.kWh
- #d[:three_phase_kwh] # 24 Bytes 3 phase kWh
- #d[:three_phase_rev_kwh] # 24 Bytes 3 phase Rev.kWh
- #d[:resettable_kwh] # 8 Bytes Resettable kWh
- #d[:resettable_reverse_kwh] # 8 bytes Resettable Reverse kWh
- #d[:volts_l1] # 4 Bytes Volts L1
- #d[:volts_l2] # 4 Bytes Volts L2
- #d[:volts_l3] # 4 Bytes Volts L3
- #d[:amps_l1] # 5 Bytes Amps L1
- #d[:amps_l2] # 5 Bytes Amps L2
- #d[:amps_l3] # 5 Bytes Amps L3
- #d[:watts_l1] # 7 Bytes Watts L1
- #d[:watts_l2] # 7 Bytes Watts L2
- #d[:watts_l3] # 7 Bytes Watts L3
- #d[:watts_total] # 7 Bytes Watts Total
- #d[:cosϴ_l1] # 4 Bytes Cosϴ L1
- #d[:cosϴ_l2] # 4 Bytes Cosϴ L2
- #d[:cosϴ_l3] # 4 Bytes Cosϴ L3
- #d[:var_l1] # 7 Bytes VAR L1
- #d[:var_l2] # 7 Bytes VAR L2
- #d[:var_l3] # 7 Bytes VAR L3
- #d[:var_total] # 7 Bytes VAR Total
- #d[:freq] # 4 Bytes Freq
- #d[:pulse_count_1] # 8 Bytes Pulse Count 1
- #d[:pulse_count_2] # 8 Bytes Pulse Count 2
- #d[:pulse_count_3] # 8 Bytes Pulse Count 3
- #d[:pulse_input_hilo] # 1 Byte Pulse Input Hi/Lo
- #d[:direction_of_current] # 1 Bytes direction of current
- #d[:outputs_onoff] # 1 Byte Outputs On/Off
- #d[:kwh_data_decimal_places] # 1 Byte kWh Data Decimal Places
-
- ## Request B
- #d[:t1_t2_t3_t4_kwh] # 32 Bytes T1, T2, T3, T4 kwh
- #d[:t1_t2_t3_t4_rev_kwh] # 32 Bytes T1, T2, T3, T4 Rev kWh
- #d[:maximum_demand] # 8 Bytes Maximum Demand
- #d[:maximum_demand_time] # 1 Byte Maximum Demand Time
- #d[:pulse_ratio_1] # 4 Bytes Pulse Ratio 1
- #d[:pulse_ratio_2] # 4 Bytes Pulse Ratio 2
- #d[:pulse_ratio_3] # 4 Bytes Pulse Ratio 3
- #d[:ct_ratio] # 4 Bytes CT Ratio
- #d[:auto_reset_md] # 1 Bytes Auto Reset MD
- #d[:settable_imp_per_kWh_constant] # 4 Bytes Settable Imp/kWh Constant
-
-
- # iSerial v4 Spec From http://documents.ekmmetering.com/Omnimeter-Pulse-v.4-Protocol.pdf
- # %w(01 52 31 02 30 30 31 31 28 29 03 13 16).map{|a| a.to_i(16).chr}.join
-
# Returns the correct measurement for voltage, current, and power based on the corresponding power_configuration
def calculate_measurement(m1, m2, m3)
if power_configuration == :single_phase_2wire
@@ -185,6 +130,20 @@ def to_f_with_decimal_places(s, p=1)
end
end
+ # Power factor values come back as C099 which need to be cast to C0.99
+ def cast_power_factor(s)
+ "#{s[0]}#{s[1,3].to_f / 100.0}"
+ end
+
+ # Formatted datetime reported by meter during last read.
+ # Raw string is formatted as YYMMDDWWHHMMSS where YY is year without century, and WW is week day with Sunday as the first day of the week
+ #"20#{current_time[0,2]}-#{current_time[2,2]}-#{current_time[4,2]} #{current_time[8,2]}:#{current_time[ 10,2]}:#{current_time[12,2]}"
+ def as_datetime(s)
+ DateTime.new("20#{s[0,2]}".to_i, s[2,2].to_i, s[4,2].to_i, s[8,2].to_i, s[10,2].to_i, s[12,2].to_i, '-4')
+ end
+
+ # All values are returned without decimals. This method loops over all
+ # the values and sets them to the correct precision
def cast_response_to_correct_types(d)
# Integers
@@ -224,10 +183,7 @@ def cast_response_to_correct_types(d)
end
# Floats with precision 2
- [:power_factor_1,
- :power_factor_2,
- :power_factor_3,
- :frequency
+ [:frequency
].each do |k|
logger.debug "Casting #{k}"
d[k] = to_f_with_decimal_places(d[k], 2) if d.has_key?(k)
@@ -329,20 +285,31 @@ def request_a
d[:outputs_onoff] = a.shift(1) # 1 Byte Outputs On/Off
d[:kwh_data_decimal_places] = a.shift(1) # 1 Byte kWh Data Decimal Places
a.shift(2) # 2 Bytes Reserved
- d[:current_time] = a.shift(14) # 14 Bytes Current Time
+ meter_timestamp = a.shift(14).join('') # 14 Bytes Current Time
a.shift(6) # 30 30 21 0D 0A 03
- d[:CRC16] = a.shift(2) # 2 Bytes CRC16
+ d[:checksum] = a.shift(2) # 2 Bytes CRC16
# Smash arrays into strungs
d.each {|k,v| d[k] = v.join('')}
+ if verify_checksums
+ if Crc16.check_crc16(response, d[:checksum])
+ @logger.debug "Checksum matches"
+ else
+ @logger.error "CRC16 Checksum doesn't match. Expecting #{d[:checksum]} but was #{Crc16.crc16(response)}"
+ #raise EkmOmnimeterError, "Checksum doesn't match"
+ end
+ end
+
# Cast types
@values[:kwh_data_decimal_places] = d[:kwh_data_decimal_places].to_i
+ d[:power_factor_1] = cast_power_factor(d[:power_factor_1])
+ d[:power_factor_2] = cast_power_factor(d[:power_factor_2])
+ d[:power_factor_3] = cast_power_factor(d[:power_factor_3])
+ d[:meter_timestamp] = as_datetime(meter_timestamp)
cast_response_to_correct_types(d)
# Lookup mapped values
- puts "d[:pulse_input_hilo] = #{d[:pulse_input_hilo].inspect}"
-
d[:pulse_1_input], d[:pulse_2_input], d[:pulse_3_input] = lookup_pulse_input_states(d[:pulse_input_hilo])
d[:current_direction_l1], d[:current_direction_l2], d[:current_direction_l3] = lookup_direction_of_current(d[:direction_of_current])
d[:output_1], d[:output_2] = lookup_output_states(d[:outputs_onoff])
@@ -352,7 +319,6 @@ def request_a
@last_read_timestamp = Time.now
# Calculate totals based on wiring configuration
- @values[:meter_timestamp] = meter_timestamp
@values[:volts] = calculate_measurement(volts_l1, volts_l2, volts_l3)
@values[:amps] = calculate_measurement(amps_l1, amps_l2, amps_l3)
@values[:watts] = calculate_measurement(watts_l1, watts_l2, watts_l3)
@@ -364,7 +330,6 @@ def request_a
end
-
# Request B
# TODO: Instead of pre-parsing and casting everything, refactor this so that only the response string gets saved, and parse out values that are accessed.
def request_b
@@ -428,14 +393,27 @@ def request_b
d[:settable_pulse_per_kwh_ratio] = a.shift(4) # 4 Bytes Settable Imp/kWh Constant
# Diff from request A end
a.shift(56) # 56 Bytes Reserved
- d[:current_time] = a.shift(14) # 14 Bytes Current Time
+ meter_timestamp = a.shift(14).join('') # 14 Bytes Current Time
a.shift(6) # 30 30 21 0D 0A 03
- d[:checksum] = a.shift(2) # 2 Bytes CRC16
+ d[:checksum] = a.shift(2) # 2 Bytes CRC16
# Smash arrays into strungs
d.each {|k,v| d[k] = v.join('')}
+ if verify_checksums
+ if Crc16.check_crc16(response, d[:checksum])
+ @logger.debug "Checksum matches"
+ else
+ @logger.error "CRC16 Checksum doesn't match. Expecting #{d[:checksum]} but was #{Crc16.crc16(response)}"
+ #raise EkmOmnimeterError, "Checksum doesn't match"
+ end
+ end
+
# Cast types
+ d[:power_factor_1] = cast_power_factor(d[:power_factor_1])
+ d[:power_factor_2] = cast_power_factor(d[:power_factor_2])
+ d[:power_factor_3] = cast_power_factor(d[:power_factor_3])
+ d[:meter_timestamp] = as_datetime(meter_timestamp)
cast_response_to_correct_types(d)
# Lookup mapped values
@@ -447,7 +425,6 @@ def request_b
@last_read_timestamp = Time.now
# Calculate totals based on wiring configuration
- @values[:meter_timestamp] = meter_timestamp
@values[:volts] = calculate_measurement(volts_l1, volts_l2, volts_l3)
@values[:amps] = calculate_measurement(amps_l1, amps_l2, amps_l3)
@values[:watts] = calculate_measurement(watts_l1, watts_l2, watts_l3)
@@ -458,7 +435,6 @@ def request_b
end
-
# Gets remote EKM meter data using iSerial defaults
# meter_number is the meters serial number. leading 0s not required.
# remote_address is the IP address of the ethernet-RS485 converter
@@ -473,24 +449,24 @@ def get_remote_meter_data(request, read_bytes)
begin
socket = TCPSocket.new(remote_address, remote_port)
- logger.debug "Socket open" unless logger.nil?
+ @logger.debug "Socket open"
# Send request to the meter
- logger.debug "Request: #{request}" unless logger.nil?
+ @logger.debug "Request: #{request}"
socket.write(request)
# Receive a response of 255 bytes
response = socket.read(read_bytes)
- logger.debug "Socket response #{response.length}" unless logger.nil?
- logger.debug response unless logger.nil?
+ @logger.debug "Socket response #{response.length}"
+ @logger.debug response
rescue Exception => ex
- logger.error "Exception\n#{ex.message}\n#{ex.backtrace.join("\n")}" unless logger.nil?
+ @logger.error "Exception\n#{ex.message}\n#{ex.backtrace.join("\n")}"
ensure
# EKM Meter software sends this just before closing the connection, so we will too
socket.write "\x0a\x03\x32\x3d"
socket.close
- logger.debug "Socket closed" unless logger.nil?
+ @logger.debug "Socket closed"
end
return response
View
2 lib/ekm-omnimeter/version.rb
@@ -7,7 +7,7 @@ def self.version_string
module VERSION #:nodoc:
MAJOR = 0
MINOR = 2
- PATCH = 3
+ PATCH = 4
STRING = [MAJOR, MINOR, PATCH].join('.')
end

0 comments on commit d3612f9

Please sign in to comment.