lib/ekm-omnimeter/meter.rb in ekm-omnimeter-0.1.0 vs lib/ekm-omnimeter/meter.rb in ekm-omnimeter-0.2.0

- old
+ new

@@ -11,11 +11,11 @@ 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 + attr_reader :meter_number, :remote_address, :remote_port, :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 @@ -52,45 +52,47 @@ #@pulse_input_1_device = options[:pulse_input_1_device] || nil #@pulse_input_2_device = options[:pulse_input_3_device] || nil #@pulse_input_3_device = options[:pulse_input_2_device] || nil @values = {} - @last_update = nil + @last_read_timestamp = nil # Get values - request_a() + read() end - # Alias request_a with read + # A complete read spans two protocol requests def read request_a() + request_b() + @values end # Formatted datetime reported by meter during last read - def measurement_timestamp + 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}" + #@logger.debug "method_missing #{method_sym.inspect}" # Only refresh data if its more than 0.25 seconds old - et = @last_update.nil? ? 0 : (Time.now - @last_update) - @logger.debug "Elapsed time since last read #{et}" + et = @last_read_timestamp.nil? ? 0 : (Time.now - @last_read_timestamp) + #logger.debug "Elapsed time since last read #{et}" if et > 250 @logger.info "More than 250 milliseconds have passed, updating data" read() end if @values.include? method_sym - @logger.debug "Found #{method_sym}" + #logger.debug "Found #{method_sym}" @values[method_sym] else - @logger.debug "Didn't find #{method_sym}" + #logger.debug "Didn't find #{method_sym}" super end end # Attribute responder that delegates check of attribute existence to the values hash @@ -167,36 +169,141 @@ elsif power_configuration == :three_phase_4wire (volts_l1 + volts_l2 + volts_l3) end end - #Request A: + def to_kwh_float(s) + to_f_with_decimal_places(s, @values[:kwh_data_decimal_places]) + end + + def to_f_with_decimal_places(s, p=1) + unless s.nil? + v = (s.to_f / (10 ** p)) + logger.debug "Casting #{s.inspect} -> #{v.inspect}" + v + else + logger.error "Could not cast #{s} to #{p} decimal places" + end + end + + def cast_response_to_correct_types(d) + + # Integers + [:kwh_data_decimal_places, + :watts_l1, + :watts_l2, + :watts_l3, + :watts_total, + :maximum_demand, + :ct_ratio, + :pulse_1_count, + :pulse_1_ratio, + :pulse_2_count, + :pulse_2_ratio, + :pulse_3_count, + :pulse_3_ratio, + :reactive_power_1, + :reactive_power_2, + :reactive_power_3, + :total_reactive_power, + :settable_pulse_per_kwh_ratio + ].each do |k| + logger.debug "Casting #{k} = #{d[k].inspect} -> #{d[k].to_i}" + d[k] = d[k].to_i if d.has_key?(k) + end + + # Floats with precision 1 + [:volts_l1, + :volts_l2, + :volts_l3, + :amps_l1, + :amps_l2, + :amps_l3 + ].each do |k| + logger.debug "Casting #{k}" + d[k] = to_f_with_decimal_places(d[k], 1) if d.has_key?(k) + end + + # Floats with precision 2 + [:power_factor_1, + :power_factor_2, + :power_factor_3, + :frequency + ].each do |k| + logger.debug "Casting #{k}" + d[k] = to_f_with_decimal_places(d[k], 2) if d.has_key?(k) + end + + # Floats with precision set by kwh_data_decimal_places + [:total_kwh, + :reactive_kwh_kvarh, + :total_forward_kwh, + :total_reverse_kwh, + :net_kwh, + :total_kwh_l1, + :total_kwh_l2, + :total_kwh_l3, + :reverse_kwh_l1, + :reverse_kwh_l2, + :reverse_kwh_l3, + :resettable_total_kwh, + :resettable_reverse_kwh, + :total_kwh_t1, + :total_kwh_t2, + :total_kwh_t3, + :total_kwh_t4, + :reverse_kwh_t1, + :reverse_kwh_t2, + :reverse_kwh_t3, + :reverse_kwh_t4 + ].each do |k| + logger.debug "Casting #{k}" + d[k] = to_kwh_float(d[k]) if d.has_key?(k) + end + + end + + + # Request A + # 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_a # 2F 3F 12 Bytes Address 30 30 21 0D 0A # /?00000000012300! then a CRLF request = "/?" + meter_number + "00!\r\n" read_bytes = 255 logger.debug "Socket write #{request}" unless logger.nil? response = get_remote_meter_data(request, read_bytes) - raise EkmError if response.nil? + if response.nil? + log.error "No response to request_a from meter #{address}" + raise EkmOmnimeter, "No response from meter." + end + # Split the response string into an array and prepare a hash to store the values a = response.split('') d = {} # Return (255 Bytes total) : a.shift(1) # 02 d[:meter_type] = a.shift(2) # 2 Byte Meter Type d[:meter_firmware] = a.shift(1) # 1 Byte Meter Firmware d[:address] = a.shift(12) # 12 Bytes Address - d[:total_active_kwh] = a.shift(8) # 8 Bytes total Active kWh - d[:total_kvarh] = a.shift(8) # 8 Bytes Total kVARh - d[:total_rev_kwh] = a.shift(8) # 8 Bytes Total Rev.kWh - d[:three_phase_kwh] = a.shift(24) # 24 Bytes 3 phase kWh - d[:three_phase_rev_kwh] = a.shift(24) # 24 Bytes 3 phase Rev.kWh - d[:resettable_kwh] = a.shift(8) # 8 Bytes Resettable kWh + d[:total_kwh] = a.shift(8) # 8 Bytes total Active kWh + d[:reactive_kwh_kvarh] = a.shift(8) # 8 Bytes Total kVARh + d[:total_reverse_kwh] = a.shift(8) # 8 Bytes Total Rev.kWh + + # 24 Bytes 3 phase kWh + d[:total_kwh_l1] = a.shift(8) + d[:total_kwh_l2] = a.shift(8) + d[:total_kwh_l3] = a.shift(8) + # 24 Bytes 3 phase Rev.kWh + d[:reverse_kwh_l1] = a.shift(8) + d[:reverse_kwh_l2] = a.shift(8) + d[:reverse_kwh_l3] = a.shift(8) + + d[:resettable_total_kwh] = a.shift(8) # 8 Bytes Resettable kWh d[:resettable_reverse_kwh] = a.shift(8) # 8 bytes Resettable Reverse kWh d[:volts_l1] = a.shift(4) # 4 Bytes Volts L1 d[:volts_l2] = a.shift(4) # 4 Bytes Volts L2 d[:volts_l3] = a.shift(4) # 4 Bytes Volts L3 d[:amps_l1] = a.shift(5) # 5 Bytes Amps L1 @@ -204,72 +311,93 @@ d[:amps_l3] = a.shift(5) # 5 Bytes Amps L3 d[:watts_l1] = a.shift(7) # 7 Bytes Watts L1 d[:watts_l2] = a.shift(7) # 7 Bytes Watts L2 d[:watts_l3] = a.shift(7) # 7 Bytes Watts L3 d[:watts_total] = a.shift(7) # 7 Bytes Watts Total - d[:cosϴ_l1] = a.shift(4) # 4 Bytes Cosϴ L1 - d[:cosϴ_l2] = a.shift(4) # 4 Bytes Cosϴ L2 - d[:cosϴ_l3] = a.shift(4) # 4 Bytes Cosϴ L3 - d[:var_l1] = a.shift(7) # 7 Bytes VAR L1 - d[:var_l2] = a.shift(7) # 7 Bytes VAR L2 - d[:var_l3] = a.shift(7) # 7 Bytes VAR L3 - d[:var_total] = a.shift(7) # 7 Bytes VAR Total - d[:freq] = a.shift(4) # 4 Bytes Freq - d[:pulse_count_1] = a.shift(8) # 8 Bytes Pulse Count 1 - d[:pulse_count_2] = a.shift(8) # 8 Bytes Pulse Count 2 - d[:pulse_count_3] = a.shift(8) # 8 Bytes Pulse Count 3 + d[:power_factor_1] = a.shift(4) # 4 Bytes Cosϴ L1 + d[:power_factor_2] = a.shift(4) # 4 Bytes Cosϴ L2 + d[:power_factor_3] = a.shift(4) # 4 Bytes Cosϴ L3 + d[:reactive_power_1] = a.shift(7) # 7 Bytes VAR L1 + d[:reactive_power_2] = a.shift(7) # 7 Bytes VAR L2 + d[:reactive_power_3] = a.shift(7) # 7 Bytes VAR L3 + d[:total_reactive_power] = a.shift(7) # 7 Bytes VAR Total + d[:frequency] = a.shift(4) # 4 Bytes Freq + d[:pulse_1_count] = a.shift(8) # 8 Bytes Pulse Count 1 + d[:pulse_2_count] = a.shift(8) # 8 Bytes Pulse Count 2 + d[:pulse_3_count] = a.shift(8) # 8 Bytes Pulse Count 3 d[:pulse_input_hilo] = a.shift(1) # 1 Byte Pulse Input Hi/Lo d[:direction_of_current] = a.shift(1) # 1 Bytes direction of current 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 a.shift(6) # 30 30 21 0D 0A 03 - #d[] = a.shift(2) # 2 Bytes CRC16 + d[:CRC16] = a.shift(2) # 2 Bytes CRC16 # Smash arrays into strungs d.each {|k,v| d[k] = v.join('')} + # Cast types + @values[:kwh_data_decimal_places] = d[:kwh_data_decimal_places].to_i + cast_response_to_correct_types(d) + # Merge to values and reset time @values.merge!(d) - @last_update = Time.now + @last_read_timestamp = Time.now # Calculate totals based on wiring configuration - @values[:measurement_timestamp] = measurement_timestamp + @values[:meter_timestamp] = meter_timestamp @values[:volts] = calculate_measurement(d[:volts_l1], d[:volts_l2], d[:volts_l3]) @values[:amps] = calculate_measurement(d[:amps_l1], d[:amps_l2], d[:amps_l3]) @values[:watts] = calculate_measurement(d[:watts_l1], d[:watts_l2], d[:watts_l3]) + @values[:total_forward_kwh] = total_kwh - total_reverse_kwh + @values[:net_kwh] = total_forward_kwh - total_reverse_kwh # Return the hash as an open struct return d end - # Request B: + # 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 # 2F 3F 12 Bytes Address 30 31 21 0D 0A # /?00000000012301! then a CRLF request = "/?" + meter_number + "01!\r\n" read_bytes = 255 logger.debug "Socket write #{request}" unless logger.nil? response = get_remote_meter_data(request, read_bytes) - raise EkmError if response.nil? + if response.nil? + log.error "No response to request_a from meter #{address}" + raise EkmOmnimeter, "No response from meter." + end # Split the response string into an array and prepare a hash to store the values - a = s.split('') + a = response.split('') d = {} # Return (255 Bytes total) : a.shift(1) # 02 d[:meter_type] = a.shift(2) # 2 Byte Meter Type d[:meter_firmware] = a.shift(1) # 1 Byte Meter Firmware d[:address] = a.shift(12) # 12 Bytes Address + # Diff from request A start - d[:t1_t2_t3_t4_kwh] = a.shift(32) # 32 Bytes T1, T2, T3, T4 kwh - d[:t1_t2_t3_t4_rev_kwh] = a.shift(32) # 32 Bytes T1, T2, T3, T4 Rev kWh + #d[:t1_t2_t3_t4_kwh] = a.shift(32) # 32 Bytes T1, T2, T3, T4 kwh + d[:total_kwh_t1] = a.shift(8) + d[:total_kwh_t2] = a.shift(8) + d[:total_kwh_t3] = a.shift(8) + d[:total_kwh_t4] = a.shift(8) + + #d[:t1_t2_t3_t4_rev_kwh] = a.shift(32) # 32 Bytes T1, T2, T3, T4 Rev kWh + d[:reverse_kwh_t1] = a.shift(8) + d[:reverse_kwh_t2] = a.shift(8) + d[:reverse_kwh_t3] = a.shift(8) + d[:reverse_kwh_t4] = a.shift(8) + # Diff from request A end d[:volts_l1] = a.shift(4) # 4 Bytes Volts L1 d[:volts_l2] = a.shift(4) # 4 Bytes Volts L2 d[:volts_l3] = a.shift(4) # 4 Bytes Volts L3 d[:amps_l1] = a.shift(5) # 5 Bytes Amps L1 @@ -277,36 +405,39 @@ d[:amps_l3] = a.shift(5) # 5 Bytes Amps L3 d[:watts_l1] = a.shift(7) # 7 Bytes Watts L1 d[:watts_l2] = a.shift(7) # 7 Bytes Watts L2 d[:watts_l3] = a.shift(7) # 7 Bytes Watts L3 d[:watts_total] = a.shift(7) # 7 Bytes Watts Total - d[:cosϴ_l1] = a.shift(4) # 4 Bytes Cosϴ L1 - d[:cosϴ_l2] = a.shift(4) # 4 Bytes Cosϴ L2 - d[:cosϴ_l3] = a.shift(4) # 4 Bytes Cosϴ L3 + d[:power_factor_1] = a.shift(4) # 4 Bytes Cosϴ L1 + d[:power_factor_2] = a.shift(4) # 4 Bytes Cosϴ L2 + d[:power_factor_3] = a.shift(4) # 4 Bytes Cosϴ L3 # Diff from request A start d[:maximum_demand] = a.shift(8) # 8 Bytes Maximum Demand - d[:maximum_demand_time] = a.shift(1) # 1 Byte Maximum Demand Time - d[:pulse_ratio_1] = a.shift(4) # 4 Bytes Pulse Ratio 1 - d[:pulse_ratio_2] = a.shift(4) # 4 Bytes Pulse Ratio 2 - d[:pulse_ratio_3] = a.shift(4) # 4 Bytes Pulse Ratio 3 + d[:maximum_demand_period] = a.shift(1) # 1 Byte Maximum Demand Time + d[:pulse_1_ratio] = a.shift(4) # 4 Bytes Pulse Ratio 1 + d[:pulse_2_ratio] = a.shift(4) # 4 Bytes Pulse Ratio 2 + d[:pulse_3_ratio] = a.shift(4) # 4 Bytes Pulse Ratio 3 d[:ct_ratio] = a.shift(4) # 4 Bytes CT Ratio - d[:auto_reset_md] = a.shift(1) # 1 Bytes Auto Reset MD - d[:settable_imp_per_kWh_constant] = a.shift(4) # 4 Bytes Settable Imp/kWh Constant + d[:auto_reset_max_demand] = a.shift(1) # 1 Bytes Auto Reset MD + 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 + a.shift(56) # 56 Bytes Reserved d[:current_time] = a.shift(14) # 14 Bytes Current Time a.shift(6) # 30 30 21 0D 0A 03 - d[] = 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('')} + # Cast types + cast_response_to_correct_types(d) + # Merge to values and reset time @values.merge!(d) - @last_update = Time.now + @last_read_timestamp = Time.now # Calculate totals based on wiring configuration - @values[:measurement_timestamp] = measurement_timestamp + @values[:meter_timestamp] = meter_timestamp @values[:volts] = calculate_measurement(d[:volts_l1], d[:volts_l2], d[:volts_l3]) @values[:amps] = calculate_measurement(d[:amps_l1], d[:amps_l2], d[:amps_l3]) @values[:watts] = calculate_measurement(d[:watts_l1], d[:watts_l2], d[:watts_l3]) # Return the hash as an open struct