Power Meter
The Power meter for our building measures the whole building, including B.Nektar. Installation of separate service for the two halves of the building is prohibitively expensive. Thus we have installed our own meter at the big disconnect right by the door to B. Nektar, at the southwest corner of the space.
Contents
The Meter
The EKM-OmniMeter II UL v.3 #25 from EKM Metering. Purchased 2013-05-07 along with three BCT-045-600 #35 current transformers.
RS-485 Interface
The meter can be read and configured via a RS-485 interface on top of the box. The serial protocol is documented here: http://documents.ekmmetering.com/Meter_Communication_Parsing_Submeter_v3.pdf
The port settings are unusual: 9600 baud 7 data bits and EVEN parity.
RS-485 <-> Ethernet
The meter's serial port is connected to the network with a Serial to ethernet adapter. The manual is here: File:RocketPortSoloManual.pdf
The IP address is 10.13.0.20. It should also be recorded on the Network page of this wiki.
There is a web interface for serial port configuration, but please don't monkey with it. Changing the IP address is more tricky, see the manual above.
The adapter is software configurable for RS-232, RS-422 and RS-485. But be careful, the pinout is non-standard. Don't assume that you can re-use the cable that is attached to the adapter.
To connect to the meter's serial port, open a TCP socket on port 8000.
The Serial Protocol
The serial protocol is documented here: http://documents.ekmmetering.com/Meter_Communication_Parsing_Submeter_v3.pdf Further discussion can be found on the EKM forums here: http://forum.ekmmetering.com/viewtopic.php?f=4&t=5
Current Python Script
This script (on GitHub, too) currently just grabs the data from the meter, parses and verifies it, drops it into a Python dictionary, then prints it to console and exits. Future improvements would be to pass the dictionary into MQTT (maybe the one on Mcclellan?) and do some graphing.
#!/usr/bin/env python # -*- coding: utf-8 -*- import socket from datetime import datetime from pprint import pprint class PowerMeter: '''Define methods to talk with an EKM power meter over socket''' # RocketPort Ethernet <-> RS-485 device HOST = '10.13.0.20' PORT = 8000 # string to send to ask the meter for a reading QUERY = '/?000010000863\r\n' # string to close a communicaiton with the meter CLOSE = '\x01B0\x03u' # EKM-published CRC16 table 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 ] def __init__(self): self.connection = None def connect(self): '''Set up a blocking socket with a timeout''' self.connection = socket.socket(socket.AF_INET,socket.SOCK_STREAM) self.connection.connect((self.HOST,self.PORT)) self.connection.settimeout(30) def calc_crc16(self,packet): '''Compute the EKM-published CRC16 for a packet''' test = [ord(c) for c in packet['data']] crc = 0xFFFF for c in test: crc = ((crc >> 8) & 0xFF) ^ self.CRC_LOOKUP[(crc ^ c) & 0xFF] swapped = ((crc << 8) | (crc >> 8)) & 0xFFFF return swapped & 0x7F7F def isvalid(self,packet): '''Determine if the packet is valid using the CRC16''' return packet['crc'] == self.calc_crc16(packet) def update(self): '''Ask for a reading''' self.connection.sendall(self.QUERY) # The RocketPort seems to be slow about sending all the data at once, # so spin until we have the whole 255-byte packet raw = '' while len(raw) < 255: raw += self.connection.recv(255) # hand off the parsed packet return self.parse(raw) def parse(self,raw): '''Parse a data packet from an EKM power meter''' try: # comments on lines are expected values if parsing the example # packet in the EKM official communications document # offsets calculated by hand from that document until things aligned packet = { 'model': raw[0x01:0x03], # '\x10\x17' 'fw_version': ord(raw[0x03]), # 19 'address': int(raw[0x04:0x10]), #10015 'total_kWh': float(raw[0x10:0x18])/10, # 3056.3 't1_kWh': float(raw[0x18:0x20])/10, # 1437.4 't2_kWh': float(raw[0x20:0x28])/10, # 831.2 't3_kWh': float(raw[0x28:0x30])/10, # 321.2 't4_kWh': float(raw[0x30:0x38])/10, # 466.5 'total_rev_kWh':float(raw[0x38:0x40])/10, # 0.0 't1_rev_kWh': float(raw[0x40:0x48])/10, # 0.0 't2_rev_kWh': float(raw[0x48:0x50])/10, # 0.0 't3_rev_kWh': float(raw[0x50:0x58])/10, # 0.0 't4_rev_kWh': float(raw[0x58:0x60])/10, # 0.0 'voltage1': float(raw[0x60:0x64])/10, # 118.8 'voltage2': float(raw[0x64:0x68])/10, # 118.9 'voltage3': float(raw[0x68:0x6C])/10, # 120.8 'current1': float(raw[0x6C:0x71])/10, # 18.0 'current2': float(raw[0x71:0x76])/10, # 18.0 'current3': float(raw[0x76:0x7B])/10, # 1.0 'power1': int(raw[0x7B:0x82]), # 2050 'power2': int(raw[0x82:0x89]), # 2050 'power3': int(raw[0x89:0x90]), # 160 'total_power': int(raw[0x90:0x97]), # 4270 'cos1': float(raw[0x98:0x9B])/100, # 1.00 'cos2': float(raw[0x9C:0x9F])/100, # 1.00 'cos3': float(raw[0xA0:0xA3])/100, # 0.83 'max_demand': float(raw[0xA3:0xAB])/10, #14275.0 'demand_period': int(raw[0xAB]), # 1 'timestamp':datetime.strptime( raw[0xAC:0xB2]+ #'110217' => 2011-02-17 raw[0xB4:0xBA], #'114637' => 11:46:37 '%y%m%d%H%M%S'), 'CT_rating': int(raw[0xBA:0xBE]), # 1000 'pulse1_count': int(raw[0xBE:0xC6]), # 0 'pulse2_count': int(raw[0xC6:0xCE]), # 0 'pulse3_count': int(raw[0xCE:0xD6]), # 0 'pulse1_ratio': int(raw[0xD6:0xDA]), # 0 'pulse2_ratio': int(raw[0xDA:0xDE]), # 0 'pulse3_ratio': int(raw[0xDE:0xE2]), # 0 'pulse_HL': int(raw[0xE2:0xE5]), # 0 'reserved': raw[0xE5:0xF9], # '00000000000000000000' 'unknown': raw[0xF9:0xFD], # '!\x0D\x0A\x03' 'data': raw[0x01:0xFD], 'crc': ((ord(raw[0xFD])<<8)+ # 0x77 ord(raw[0xFE])) & 0xFFFF # 0x3F } # if the packet passes CRC16 validation, return it if self.isvalid(packet): return packet # otherwise, the packet is worthless so return nothing else: return None except ValueError: # maybe a bad character or other corruption in the packet; do nothing return None def close(self): '''Close the connection to the power meter''' self.connection.send(self.CLOSE) self.connection.close() if __name__ == '__main__': # internal testing, to validate the parsing method against the documentation pm = PowerMeter() '''expected = { 'model': '\x10\x17', 'fw_version': 19, 'address': 10015, 'total_kWh': 3056.3, 't1_kWh': 1437.4, 't2_kWh': 831.2, 't3_kWh': 321.2, 't4_kWh': 466.5, 'total_rev_kWh':0.0, 't1_rev_kWh': 0.0, 't2_rev_kWh': 0.0, 't3_rev_kWh': 0.0, 't4_rev_kWh': 0.0, 'voltage1': 118.8, 'voltage2': 118.9, 'voltage3': 120.8, 'current1': 18.0, 'current2': 18.0, 'current3': 1.0, 'power1': 2050, 'power2': 2050, 'power3': 160, 'total_power': 4270, 'cos1': 1.00, 'cos2': 1.00, 'cos3': 0.83, 'max_demand': 14275.0, 'timestamp':datetime(2011,2,17,11,46,37), 'CT_rating': 1000, 'pulse1_count': 0, 'pulse2_count': 0, 'pulse3_count': 0, 'pulse1_ratio': 0, 'pulse2_ratio': 0, 'pulse3_ratio': 0, 'pulse_HL': 0, 'reserved': '0'*20, 'unknown': '!\x0D\x0A\x03', 'data': '\x10\x17\x13000000010015000305630001437400008312000032120000466500000000000000000000000000000000000000001188118912080018000180000100002050000205000001600004270 100 100L08300142750111021705114637100000000000000000000000000000000000000000000000000000000000000!\x0D\x0A\x03', 'crc':((ord('w')<<8)+ord('?')) & 0xFFFF } parsed = pm.parse( '\x02\x10\x17\x13000000010015000305630001437400008312000032120000466500000000000000000000000000000000000000001188118912080018000180000100002050000205000001600004270 100 100L08300142750111021705114637100000000000000000000000000000000000000000000000000000000000000!\x0d\x0a\x03w?') for key in expected.keys(): if expected[key] != parsed[key]: print key print expected[key] print parsed[key]''' # live testing, try to grab a data packet from the meter and print it off pm.connect() while True: packet = pm.update() if packet is not None: pprint(packet) break pm.close()
Outdated Ruby script
Here's an example of how to talk to this thing with ruby:
class OmniMeter attr_reader :raw, :time, :address, :total_kWh, :t1_kWh, :t2_kWh, :t3_kWh, :voltage1, :voltage2, :voltage3, :current1, :current2, :current3, :power1, :power2, :power3, :total_power, :cos1, :cos2, :cos3, :t1_rev_kWh, :t2_rev_kWh, :t3_rev_kWh, :total_rev_kWh, :max_demand, :crc def initialize(connection) @connection = connection end def update @connection.write "/?000010000863\r\n" @raw = @connection.read(255) @crc = @raw[-2..-1].unpack('n').first return false unless self.valid? @address = @raw[4..15] @total_kWh = @raw[16..23].to_f / 10 @t1_kWh = @raw[24..31].to_f / 10 @t2_kWh = @raw[32..39].to_f / 10 @t3_kWh = @raw[40..47].to_f / 10 @t4_kWh = @raw[48..57].to_f / 10 @total_rev_kWh = @raw[58..63].to_f / 10 @t1_rev_kWh = @raw[64..71].to_f / 10 @t2_rev_kWh = @raw[72..79].to_f / 10 @t3_rev_kWh = @raw[80..87].to_f / 10 @voltage1 = @raw[96..99].to_f / 10 @voltage2 = @raw[100..103].to_f / 10 @voltage3 = @raw[104..107].to_f / 10 @current1 = @raw[108..112].to_f / 10 @current2 = @raw[113..117].to_f / 10 @current3 = @raw[118..122].to_f / 10 @power1 = @raw[123..129].to_f @power2 = @raw[130..136].to_f @power3 = @raw[137..143].to_f @total_power = @raw[144..150].to_f @cos1 = @raw[151..154] @cos2 = @raw[155..158] @cos3 = @raw[159..162] @max_demand = @raw[163..170].to_f / 10 date = @raw[172..177] time = @raw[180..185] @time = Time.strptime(date + time, '%y%m%d%H%M%S') true end def get_hash hsh = {} [:time, :address, :total_kWh, :t1_kWh, :t2_kWh, :t3_kWh, :voltage1, :voltage2, :voltage3, :current1, :current2, :current3, :power1, :power2, :power3, :total_power, :cos1, :cos2, :cos3, :t1_rev_kWh, :t2_rev_kWh, :t3_rev_kWh, :total_rev_kWh, :max_demand, :crc ].each do |symbol| hsh[symbol] = self.send(symbol) end hsh end def inspect self.get_hash.inspect end def valid? @crc == self.calc_crc16 end def calc_crc16 crc = 0xFFFF @raw[1..-3].each_byte do |b| crc = ((crc >> 8) & 0xff) ^ CRC_LOOKUP[(crc ^ b) & 0xff] end swapped = ((crc << 8) | (crc >> 8)) & 0xFFFF swapped & 0x7F7F 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
Logging Script
The logging script runs onis not currently running on the skynet.i3detroit.local server in the space.
It can be found at /home/ted/omnimeter/omnimeter_logger.rb and works like so:
- Script reads data from the meter
- Script parses data and writes a new document to the local CouchDB database at http://localhost:5984/power_meter
- The local CouchDB continually replicates itself to the cloud at: https://i3detroit.iriscouch.com:6984/power_meter
- Profit
The Database
The CouchDB database is world-readable at: https://i3detroit.iriscouch.com:6984/power_meter
Here are some example queries:
- The most recent meter reading: https://i3detroit.iriscouch.com:6984/power_meter/_design/readings/_view/time_sorted?limit=1&descending=true
- The most recent 10 meter readings: https://i3detroit.iriscouch.com:6984/power_meter/_design/readings/_view/time_sorted?limit=10&descending=true