├── README └── kamstrup.py /README: -------------------------------------------------------------------------------- 1 | 2020-12-02 Update: 2 | ------------------ 3 | 4 | The local utility has replaced my electricity-meter, so I no longer have 5 | any Kamstrup devices to test against. 6 | 7 | I would really appreciate if somebody else could take over this project. 8 | 9 | Thanks, 10 | 11 | Poul-Henning 12 | 13 | 14 | 15 | This is an implementation of the Kamstrup Meter Protocol (KMP) based 16 | on reverse engineering of a traffic dump. 17 | 18 | There is very little information about this protocol on the net, and 19 | despite calling it an "open protocol", Kamstrup has not wanted to 20 | release the documentation for it to me. 21 | 22 | Thanks to Erik Jensen for details about units and exponents. 23 | 24 | Tested on a Kamstrup 382J electricity-meter using a home-built 25 | optical head. 26 | 27 | Enjoy, 28 | 29 | Poul-Henning 30 | 31 | PS: Sample output: 32 | 33 | Energy in 6753.0 kWh 34 | Energy out 0.0 kWh 35 | Energy in hi-res 6753.3242 kWh 36 | Energy out hi-res 0.0 kWh 37 | Voltage p1 229.0 V 38 | Voltage p2 227.0 V 39 | Voltage p3 229.0 V 40 | Current p1 5.41 A 41 | Current p2 2.12 A 42 | Current p3 3.07 A 43 | Power p1 0.976 kW 44 | Power p2 0.475 kW 45 | Power p3 0.595 kW 46 | 47 | -------------------------------------------------------------------------------- /kamstrup.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # 3 | # ---------------------------------------------------------------------------- 4 | # "THE BEER-WARE LICENSE" (Revision 42): 5 | # wrote this file. As long as you retain this notice you 6 | # can do whatever you want with this stuff. If we meet some day, and you think 7 | # this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp 8 | # ---------------------------------------------------------------------------- 9 | # 10 | 11 | from __future__ import print_function 12 | 13 | # You need pySerial 14 | import serial 15 | 16 | import math 17 | 18 | ####################################################################### 19 | # These are the variables I have managed to identify 20 | # Submissions welcome. 21 | 22 | kamstrup_382_var = { 23 | 24 | 0x0001: "Energy in", 25 | 0x0002: "Energy out", 26 | 27 | 0x000d: "Energy in hi-res", 28 | 0x000e: "Energy out hi-res", 29 | 30 | 0x041e: "Voltage p1", 31 | 0x041f: "Voltage p2", 32 | 0x0420: "Voltage p3", 33 | 34 | 0x0434: "Current p1", 35 | 0x0435: "Current p2", 36 | 0x0436: "Current p3", 37 | 38 | 0x0438: "Power p1", 39 | 0x0439: "Power p2", 40 | 0x043a: "Power p3", 41 | 42 | 0x0056: "Current flow temperature", 43 | 0x0057: "Current return flow temperature", 44 | 0x0058: "Current temperature T3", 45 | 0x007A: "Current temperature T4", 46 | 0x0059: "Current temperature difference", 47 | 0x005B: "Pressure in flow", 48 | 0x005C: "Pressure in return flow", 49 | 0x004A: "Current flow in flow", 50 | 0x004B: "Current flow in return flow" 51 | 0x03ff: "Power In", 52 | 0x0438: "Power p1 In", 53 | 0x0439: "Power p2 In", 54 | 0x043a: "Power p3 In", 55 | 56 | 0x0400: "Power In", 57 | 0x0540: "Power p1 Out", 58 | 0x0541: "Power p2 Out", 59 | 0x0542: "Power p3 Out", 60 | } 61 | 62 | kamstrup_681_var = { 63 | 1: "Date", 64 | 60: "Heat", 65 | 61: "x", 66 | 62: "x", 67 | 63: "x", 68 | 95: "x", 69 | 96: "x", 70 | 97: "x", 71 | } 72 | 73 | kamstrup_MC601_var = { 74 | 0x003C: "Energy register 1: Heat energy", 75 | 0x0044: "Volume register V1", 76 | 0x0058: "Current temperature T3", 77 | 0x03EC: "Operation hours counter", 78 | } 79 | 80 | 81 | ####################################################################### 82 | # Units, provided by Erik Jensen 83 | 84 | units = { 85 | 0: '', 1: 'Wh', 2: 'kWh', 3: 'MWh', 4: 'GWh', 5: 'j', 6: 'kj', 7: 'Mj', 86 | 8: 'Gj', 9: 'Cal', 10: 'kCal', 11: 'Mcal', 12: 'Gcal', 13: 'varh', 87 | 14: 'kvarh', 15: 'Mvarh', 16: 'Gvarh', 17: 'VAh', 18: 'kVAh', 88 | 19: 'MVAh', 20: 'GVAh', 21: 'kW', 22: 'kW', 23: 'MW', 24: 'GW', 89 | 25: 'kvar', 26: 'kvar', 27: 'Mvar', 28: 'Gvar', 29: 'VA', 30: 'kVA', 90 | 31: 'MVA', 32: 'GVA', 33: 'V', 34: 'A', 35: 'kV',36: 'kA', 37: 'C', 91 | 38: 'K', 39: 'l', 40: 'm3', 41: 'l/h', 42: 'm3/h', 43: 'm3xC', 92 | 44: 'ton', 45: 'ton/h', 46: 'h', 47: 'hh:mm:ss', 48: 'yy:mm:dd', 93 | 49: 'yyyy:mm:dd', 50: 'mm:dd', 51: '', 52: 'bar', 53: 'RTC', 94 | 54: 'ASCII', 55: 'm3 x 10', 56: 'ton x 10', 57: 'GJ x 10', 95 | 58: 'minutes', 59: 'Bitfield', 60: 's', 61: 'ms', 62: 'days', 96 | 63: 'RTC-Q', 64: 'Datetime' 97 | } 98 | 99 | ####################################################################### 100 | # Kamstrup uses the "true" CCITT CRC-16 101 | # 102 | 103 | def crc_1021(message): 104 | poly = 0x1021 105 | reg = 0x0000 106 | for byte in message: 107 | mask = 0x80 108 | while(mask > 0): 109 | reg<<=1 110 | if byte & mask: 111 | reg |= 1 112 | mask>>=1 113 | if reg & 0x10000: 114 | reg &= 0xffff 115 | reg ^= poly 116 | return reg 117 | 118 | ####################################################################### 119 | # Byte values which must be escaped before transmission 120 | # 121 | 122 | escapes = { 123 | 0x06: True, 124 | 0x0d: True, 125 | 0x1b: True, 126 | 0x40: True, 127 | 0x80: True, 128 | } 129 | 130 | ####################################################################### 131 | # And here we go.... 132 | # 133 | class kamstrup(object): 134 | 135 | def __init__(self, serial_port = "/dev/cuaU0"): 136 | self.debug_fd = open("/tmp/_kamstrup", "a") 137 | self.debug_fd.write("\n\nStart\n") 138 | self.debug_id = None 139 | 140 | self.ser = serial.Serial( 141 | port = serial_port, 142 | baudrate = 1200, 143 | timeout = 1.0) 144 | 145 | def debug(self, dir, b): 146 | for i in b: 147 | if dir != self.debug_id: 148 | if self.debug_id != None: 149 | self.debug_fd.write("\n") 150 | self.debug_fd.write(dir + "\t") 151 | self.debug_id = dir 152 | self.debug_fd.write(" %02x " % i) 153 | self.debug_fd.flush() 154 | 155 | def debug_msg(self, msg): 156 | if self.debug_id != None: 157 | self.debug_fd.write("\n") 158 | self.debug_id = "Msg" 159 | self.debug_fd.write("Msg\t" + msg) 160 | self.debug_fd.flush() 161 | 162 | def wr(self, b): 163 | b = bytearray(b) 164 | self.debug("Wr", b); 165 | self.ser.write(b) 166 | 167 | def rd(self): 168 | a = self.ser.read(1) 169 | if len(a) == 0: 170 | self.debug_msg("Rx Timeout") 171 | return None 172 | b = bytearray(a)[0] 173 | self.debug("Rd", bytearray((b,))); 174 | return b 175 | 176 | def send(self, pfx, msg): 177 | b = bytearray(msg) 178 | 179 | b.append(0) 180 | b.append(0) 181 | c = crc_1021(b) 182 | b[-2] = c >> 8 183 | b[-1] = c & 0xff 184 | 185 | c = bytearray() 186 | c.append(pfx) 187 | for i in b: 188 | if i in escapes: 189 | c.append(0x1b) 190 | c.append(i ^ 0xff) 191 | else: 192 | c.append(i) 193 | c.append(0x0d) 194 | self.wr(c) 195 | 196 | def recv(self): 197 | b = bytearray() 198 | while True: 199 | d = self.rd() 200 | if d == None: 201 | return None 202 | if d == 0x40: 203 | b = bytearray() 204 | b.append(d) 205 | if d == 0x0d: 206 | break 207 | c = bytearray() 208 | i = 1; 209 | while i < len(b) - 1: 210 | if b[i] == 0x1b: 211 | v = b[i + 1] ^ 0xff 212 | if v not in escapes: 213 | self.debug_msg( 214 | "Missing Escape %02x" % v) 215 | c.append(v) 216 | i += 2 217 | else: 218 | c.append(b[i]) 219 | i += 1 220 | if crc_1021(c): 221 | self.debug_msg("CRC error") 222 | return c[:-2] 223 | 224 | def readvar(self, nbr): 225 | # I wouldn't be surprised if you can ask for more than 226 | # one variable at the time, given that the length is 227 | # encoded in the response. Havn't tried. 228 | 229 | self.send(0x80, (0x3f, 0x10, 0x01, nbr >> 8, nbr & 0xff)) 230 | 231 | b = self.recv() 232 | if b == None: 233 | return (None, None) 234 | 235 | if b[0] != 0x3f or b[1] != 0x10: 236 | return (None, None) 237 | 238 | if b[2] != nbr >> 8 or b[3] != nbr & 0xff: 239 | return (None, None) 240 | 241 | if b[4] in units: 242 | u = units[b[4]] 243 | else: 244 | u = None 245 | 246 | # Decode the mantissa 247 | x = 0 248 | for i in range(0,b[5]): 249 | x <<= 8 250 | x |= b[i + 7] 251 | 252 | # Decode the exponent 253 | i = b[6] & 0x3f 254 | if b[6] & 0x40: 255 | i = -i 256 | i = math.pow(10,i) 257 | if b[6] & 0x80: 258 | i = -i 259 | x *= i 260 | 261 | if False: 262 | # Debug print 263 | s = "" 264 | for i in b[:4]: 265 | s += " %02x" % i 266 | s += " |" 267 | for i in b[4:7]: 268 | s += " %02x" % i 269 | s += " |" 270 | for i in b[7:]: 271 | s += " %02x" % i 272 | 273 | print(s, "=", x, units[b[4]]) 274 | 275 | return (x, u) 276 | 277 | 278 | if __name__ == "__main__": 279 | 280 | import time 281 | 282 | foo = kamstrup() 283 | 284 | for i in kamstrup_382_var: 285 | x,u = foo.readvar(i) 286 | print("%-25s" % kamstrup_382_var[i], x, u) 287 | --------------------------------------------------------------------------------