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