Quantcast
Channel: Hauke’s Projects
Viewing all articles
Browse latest Browse all 41

Interfacing EasyMeter Q3M via Info-Interface

$
0
0

The installation of my fuel cell heating required a bi-directional power meter. Bonn Netz, my local power network provider, uses meters of type EasyMeter Q3M which have two infrared interfaces: A bidirectional D0 interface, and a read-only info interface. I use the info interface (INFO-DSS) to read out power consumption and production of the three phases. For this, I built an optical interface, a 3D printed housing for it, and use the UART of a Raspberry Pi with python to get the values.

The Power Meter

The EasyMeter Q3M’s documentation (in German) states that the infrared interfaces run at 9600,8,N,1 and use the protocol “Smart Meter Language” SML 1.03, which seems to be a pure German “Standard”. The bi-directional interface is sealed, so I could not access it, but the info interface is freely accessible from the front of the device.

Zähler
The EasyMeter Q3M

Unleashing the Full Power of the INFO-DSS Interface

When the power meter is delivered, it only outputs the summary energy consumption, but not the full set with the information on the individual phases. But the manual says that there is a PIN to unlock the full information set. I wrote an email to Bonn Netz and asked for the PIN, and they sent it out to me, professional in a sealed letter like an online banking PIN! Cool, thanks Bonn Netz! The PIN needs to be keyed in using a flashlight and moving it across a light sensor – a bit cumbersome, but a good idea to have a fully sealed housing and no mechanical parts. Worked very well in the end!

Info DSS
The Info DSS interface

Electronics

I started with the interface that Sven Jordan describes on his webpage (German), but I could not get the exact same components. With those I got, the signals coming out from the circuit, even after changing the resistors, were awful and did not register with the UART. So I used half the circiut that makes up the OptoLink adapter for my Viessmann heating, which works nicely. Here’s how I did it:

EasyMeter Circuit
EasyMeter infrared interface for the INFO-DSS

Parts add up to approximatly 1,- €. The IR phototransistor PT333-3C was the cheapest on stock at Conrad Bonn, so I took it – I guess more or less any IR phototransistor should work. From comparing a few datasheets, most are very similar in terms of specification, and the wavelength sensitivity covers such a broad window, that any should work.

And here’s how to connect it to the Raspberry Pi:

Circuit pin Function Raspberry GPIO pin Function
1 3.3 V 1 3.3 V
2 GND 6 GND
3 TxD 10 RxD

Starting now to make my own PCBs using the CNC mill in my Fabtotum Personal Fabricator, I advanced a bit in KiCad. Here’s what came out:

PCB simulated
The simulated PCB with parts

You can download the EasyMeter Interface KiCad files here.

Housing

Now having my own 3D printer, the aforementioned Fabtotum, I created a housing for the circuit:

EasyMeter Box
The housing for the interface board

The housing has the following features:

  • The KiCad designed PCB fits snugly into it, with the photo transistor matching up exactly with the IR emitter of the EasyMeter.
  • The bottom side has holes to match the fixation notches of the EasyMeter.
  • The bottom side has holes to put neodym magnets into which hold the box in place. However, the magnets I had (cylindrical with 5 mm diameter and 3 mm height) are barely strong enough, so perhaps you want to find stronger ones and modify the housing accordingly.
  • It has an outlet for a flat 4 wire telephone cable that can be put either on top or bottom of the housing.
  • Four 2 mm screws of 18 mm length with matching nuts hold everything together.

You may download the 3D model files for the housing here or from Thingiverse. I was a bit optimistic with regard to the notch and screw holes – I had to widen them with a drill. Perhaps you should modify them a bit before printing your own. By the way: I am totally surprised how well the 3D builder app that comes with Windows 10 works! It has a few nasty bugs, but all in all its surprisingly versatile and intuitive!

Interface Ready
The assembled interface

Software

As mentioned above, the meter “speaks” SML 1.03, which seems to be a German invention by the VDE. And personally I find it a rather crappy standard – I started to write a “generic” python implementation and gave up after half an hour, because a) the definition is over-complicated and b) the documentation is written in a *very* confusing way. So I followed the approach of Stefan Weigert (sorry, all in German) and just hard-coded the decodings. This is rather stupid and unflexible, but it does the job. Thanks to Stefan Weigert for publishing the code! The OBIS numbers used are explained in this document (you guessed it: in German…).

There is libSML, which is a generic implementation of SML written in C, and SMLlib for AVR, but I did not take the time to fiddle with these, since I am currently very python minded. There is also the Volkszähler project (surprise: German…) that’s worth a look if you want to do more.

So here is the code, based on Stefan Weigerts code from other smart meters:

#!/usr/bin/python
# -*- coding: iso-8859-15 -*-

# from: http://www.stefan-weigert.de/php_loader/sml.php
# Modified for EasyMeter Q3M and translated to English by Hauke
# http://projects.webvoss.de/2019/01/04/interfacing-easymeter-q3m-via-info-interface/

import time
import serial
from threading import Timer
import sys
 
mystring = ""
crc16_x25_table = [
	0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD,	0x6536, 0x74BF,
	0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7,
	0x1081, 0x0108,	0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E,
	0x9CC9, 0x8D40, 0xBFDB, 0xAE52, 0xDAED, 0xCB64,	0xF9FF, 0xE876,
	0x2102, 0x308B, 0x0210, 0x1399,	0x6726, 0x76AF, 0x4434, 0x55BD,
	0xAD4A, 0xBCC3,	0x8E58, 0x9FD1, 0xEB6E, 0xFAE7, 0xC87C, 0xD9F5,
	0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E,	0x54B5, 0x453C,
	0xBDCB, 0xAC42, 0x9ED9, 0x8F50,	0xFBEF, 0xEA66, 0xD8FD, 0xC974,
	0x4204, 0x538D,	0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB,
	0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1,	0xAB7A, 0xBAF3,
	0x5285, 0x430C, 0x7197, 0x601E,	0x14A1, 0x0528, 0x37B3, 0x263A,
	0xDECD, 0xCF44,	0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72,
	0x6306, 0x728F, 0x4014, 0x519D, 0x2522, 0x34AB,	0x0630, 0x17B9,
	0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5,	0xA96A, 0xB8E3, 0x8A78, 0x9BF1,
	0x7387, 0x620E,	0x5095, 0x411C, 0x35A3, 0x242A, 0x16B1, 0x0738,
	0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862,	0x9AF9, 0x8B70,
	0x8408, 0x9581, 0xA71A, 0xB693,	0xC22C, 0xD3A5, 0xE13E, 0xF0B7,
	0x0840, 0x19C9,	0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF,
	0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324,	0xF1BF, 0xE036,
	0x18C1, 0x0948, 0x3BD3, 0x2A5A,	0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E,
	0xA50A, 0xB483,	0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5,
	0x2942, 0x38CB, 0x0A50, 0x1BD9, 0x6F66, 0x7EEF,	0x4C74, 0x5DFD,
	0xB58B, 0xA402, 0x9699, 0x8710,	0xF3AF, 0xE226, 0xD0BD, 0xC134,
	0x39C3, 0x284A,	0x1AD1, 0x0B58, 0x7FE7, 0x6E6E, 0x5CF5, 0x4D7C,
	0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1,	0xA33A, 0xB2B3,
	0x4A44, 0x5BCD, 0x6956, 0x78DF,	0x0C60, 0x1DE9, 0x2F72, 0x3EFB,
	0xD68D, 0xC704,	0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232,
	0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68,	0x3FF3, 0x2E7A,
	0xE70E, 0xF687, 0xC41C, 0xD595,	0xA12A, 0xB0A3, 0x8238, 0x93B1,
	0x6B46, 0x7ACF,	0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9,
	0xF78F, 0xE606, 0xD49D, 0xC514, 0xB1AB, 0xA022,	0x92B9, 0x8330,
	0x7BC7, 0x6A4E, 0x58D5, 0x495C,	0x3DE3, 0x2C6A, 0x1EF1, 0x0F78]
 
 
class Watchdog_timer:
	def __init__(self, timeout, userHandler=None):
		self.timeout = timeout
		self.handler = userHandler if userHandler is not None else self.defaultHandler
		self.timer = Timer(self.timeout, self.handler)
		self.timer.start()
 
	def reset(self):
		self.timer.cancel()
		self.timer = Timer(self.timeout, self.handler)
		self.timer.start()
 
	def stop(self):
		self.timer.cancel()
 
	def defaultHandler(self):
		raise self
 
 
def crc16_x25(Buffer):
	crcsum = 0xffff
	global crc16_x25_table
	for byte in Buffer:
		crcsum = crc16_x25_table[(ord(byte) ^ crcsum) & 0xff] ^ (crcsum >> 8 & 0xff)
	crcsum ^= 0xffff
	return crcsum
 
def signedintstr(hexstr):
	source = hexstr.encode('hex')
	sign_bit_mask = 1 << (len(source)*4-1)
	other_bits_mask = sign_bit_mask - 1
	value = int(source, 16)
	return -(value & sign_bit_mask) | (value & other_bits_mask)
 
def watchdogtimer_ovf():
	global mystring
	sys.stdout.write("SML-Stream:\n" + mystring.encode('hex') + "\n\n")	# output full telegram
	message = mystring[0:-2]						# cut away the last bytes
	crc_rx = int((mystring[-1] + mystring[-2]).encode('hex'), 16)		# exchange CRC bytes and tore them
	crc_calc = crc16_x25(message)						# create own CRC for comparison
	if crc_rx == crc_calc:							# compare CRCs - continue on match
		sys.stdout.write("crc OK\n")
		if message[0:8] == '\x1b\x1b\x1b\x1b\x01\x01\x01\x01':		# check if first 8 bytes follow standard
			sys.stdout.write("SML start found\n\n")
			sys.stdout.write("____________________________________\n")
			sys.stdout.write("TransactionId: " + message[10:14] + message[14:20].encode('hex') + "\n")
			sys.stdout.write("GroupNo: " + message[21:22].encode('hex') + "\n")
			sys.stdout.write("abortOnError: " + message[23:24].encode('hex') + "\n")
			sys.stdout.write("getOpenResponse: " + message[31:34] + "\n")
			sys.stdout.write("reqFileId: " + message[35:38] + message[38:42].encode('hex') + "\n")
			sys.stdout.write("serverId: " + message[43:53].encode('hex') + "\n")
			sys.stdout.write("crc: " + message[56:58].encode('hex') + "\n")
			sys.stdout.write("____________________________________\n")
			sys.stdout.write("TransactionId: " + message[61:65] + message[65:71].encode('hex') + "\n")
			sys.stdout.write("GroupNo: " + message[72:73].encode('hex') + "\n")
			sys.stdout.write("abortOnError: " + message[74:75].encode('hex') + "\n")
			sys.stdout.write("getListResponse: " + message[77:79].encode('hex') + "\n")
			sys.stdout.write("serverId: " + message[82:92].encode('hex') + "\n")
			sys.stdout.write("listName: " + message[93:100].encode('hex') + "\n")
			sys.stdout.write("___\n")
			sys.stdout.write("choice(01=secIndex): " + message[102:103].encode('hex') + "\n")
			sys.stdout.write("secIndex(uptime): " + str(int(message[104:108].encode('hex'),16)) + "\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[111:117].encode('hex') + " = OBIS-number for vendor ID\n")
			sys.stdout.write("Vendor ID: " + message[122:125] + "\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[128:134].encode('hex') + " = OBIS-number for ServerID\n")
			sys.stdout.write("Server ID: " + message[139:149].encode('hex') + "\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[152:158].encode('hex') + " = OBIS-number for cumulated incoming effective power (no rate assigned)\n")
			sys.stdout.write("???: " + message[159:162].encode('hex') + "\n")
			sys.stdout.write("unit: " + message[164:165].encode('hex') + " (Unit 1E=Wh)\n")
			sys.stdout.write("scaler: " + message[166:167].encode('hex') + " (Factor FC = -4 = 10^-4 = /10000) - /1000 to convert to kWh\n")
			sys.stdout.write("Incoming: " + str(int(message[168:176].encode('hex'),16)/10000000.0) + " kWh\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[179:185].encode('hex') + " = OBIS-number for cumulated outgoing effective power (no rate assigned)\n")
			sys.stdout.write("???: " + message[186:189].encode('hex') + "\n")
			sys.stdout.write("unit: " + message[191:192].encode('hex') + " (Unit 1E=Wh)\n")
			sys.stdout.write("scaler: " + message[193:194].encode('hex') + " (Factor FC = -4 = 10^-4 = /10000) - /1000 to convert to kWh\n")
			sys.stdout.write("Outgoing: " + str(int(message[195:203].encode('hex'),16)/10000000.0) + " kWh\n")
			sys.stdout.write("___\n")	
			sys.stdout.write("objName: " + message[206:212].encode('hex') + " = OBIS-number for cumulated incoming effective power rate 1\n")
			sys.stdout.write("unit: " + message[215:216].encode('hex') + " (Unit 1E=Wh)\n")
			sys.stdout.write("scaler: " + message[217:218].encode('hex') + " (Factor FC = -4 = 10^-4 = /10000) - /1000 to convert to kWh\n")
			sys.stdout.write("Incoming: " + str(int(message[219:227].encode('hex'),16)/10000000.0) + " kWh\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[230:236].encode('hex') + " OBIS-number for cumulated outgoing effective power rate 1\n")
			sys.stdout.write("unit: " + message[239:240].encode('hex') + " (Unit 1E=Wh)\n")
			sys.stdout.write("scaler: " + message[241:242].encode('hex') + " (Factor FC = -4 = 10^-4 = /10000) - /1000 to convert to kWh\n")
			sys.stdout.write("Outgoing: " + str(int(message[243:251].encode('hex'),16)/10000000.0) + " kWh\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[254:260].encode('hex') + " = OBIS-number for cumulated incoming effective power rate 2\n")
			sys.stdout.write("unit: " + message[263:264].encode('hex') + " (Unit 1E=Wh)\n")
			sys.stdout.write("scaler: " + message[265:266].encode('hex') + " (Factor FC = -4 = 10^-4 = /10000) - /1000 to convert to kWh\n")
			sys.stdout.write("Incoming: " + str(int(message[267:275].encode('hex'),16)/10000000.0) + " kWh\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[278:284].encode('hex') + " OBIS-number for cumulated outgoing effective power rate 2\n")
			sys.stdout.write("unit: " + message[287:288].encode('hex') + " (Unit 1E=Wh)\n")
			sys.stdout.write("scaler: " + message[289:290].encode('hex') + " (Factor FC = -4 = 10^-4 = /10000) - /1000 to convert to kWh\n")
			sys.stdout.write("Outgoing: " + str(int(message[291:299].encode('hex'),16)/10000000.0) + " kWh\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[302:308].encode('hex') + " OBIS-number current effective power total\n")
			sys.stdout.write("unit: " + message[311:312].encode('hex') + " (Unit 1B=W)\n")
			sys.stdout.write("scaler: " + message[313:314].encode('hex')+ " Factor FE = -2 = 10^-2 = /100\n")
			sys.stdout.write("Current effective power total: " + str(signedintstr(message[315:323])/100.0)+ " W\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[326:332].encode('hex') + " OBIS-number  current effective power  L1\n")
			sys.stdout.write("unit: " + message[335:336].encode('hex') + " (Unit 1B=W)\n")
			sys.stdout.write("scaler: " + message[337:338].encode('hex')+ " Factor FE = -2 = 10^-2 = /100\n")
			sys.stdout.write("Current effective power L1: " + str(signedintstr(message[339:347])/100.0)+ " W\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[350:356].encode('hex') + " OBIS-number  current effective power  L2\n")
			sys.stdout.write("unit: " + message[359:360].encode('hex') + " (Unit 1B=W)\n")
			sys.stdout.write("scaler: " + message[361:362].encode('hex')+ " Factor FE = -2 = 10^-2 = /100\n")
			sys.stdout.write("Current effective power L2: " + str(signedintstr(message[363:371])/100.0)+ " W\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[374:380].encode('hex') + " OBIS-number current effective power L3\n")
			sys.stdout.write("unit: " + message[383:384].encode('hex') + " (Unit 1B=W)\n")
			sys.stdout.write("scaler: " + message[385:386].encode('hex')+ " Factor FE = -2 = 10^-2 = /100\n")
			sys.stdout.write("Current effective power L3: " + str(signedintstr(message[387:395])/100.0)+ " W\n")
			sys.stdout.write("___\n")
			sys.stdout.write("objName: " + message[398:404].encode('hex') + " OBIS-number for Public Key\n")
			sys.stdout.write("value: " + message[410:458].encode('hex') + " (Public Key)\n")
			sys.stdout.write("___\n")
			sys.stdout.write("crc: " + message[462:464].encode('hex') + "\n")
			sys.stdout.write("____________________________________\n")
			sys.stdout.write("TransactionId: " + message[467:471] + message[471:477].encode('hex') + "\n")
			sys.stdout.write("GroupNo: " + message[478:479].encode('hex') + "\n")
			sys.stdout.write("abortOnError: " + message[480:481].encode('hex') + "\n")
			sys.stdout.write("getCloseResponse: " + message[483:485].encode('hex') + "\n")
			sys.stdout.write("crc: " + message[488:490].encode('hex') + "\n")
			sys.stdout.write("\n")
			watchdog.stop()
			mystring=""
 
		else:
			sys.stdout.write("no SML\n\n")
			mystring=""
			watchdog.stop()
 
	else:
		sys.stdout.write("crc NOK\n\n")
		mystring=""
		watchdog.stop()
 
 
try:
	my_tty = serial.Serial(port='/dev/ttyAMA0', baudrate = 9600, parity =serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=0)
	sys.stdout.write(my_tty.portstr + " opened\n\n")
	my_tty.close()
	my_tty.open()
 
except Exception, e:
	sys.stdout.write("serial port could not be opened:\n" + str(e) + "\n\n")
	exit()
 
 
try:
	my_tty.reset_input_buffer()
	my_tty.reset_output_buffer()
	watchdog = Watchdog_timer(0.1, watchdogtimer_ovf)
	watchdog.stop()
	while True:
		while my_tty.in_waiting > 0:
			mystring += my_tty.read()
			watchdog.reset()
 
except KeyboardInterrupt:
	my_tty.close()
	sys.stdout.write("\nProgram stopped manually!\n")

In order for that to work you may need to modify the Raspberry Pi serial configuration as desribed in my OptoLink blog post.

Here’s a picture of the interface in place:

Interface in Place
The interface mounted to the meter

Final Remarks

My thanks go to Sven Jordan, Stefan Weigert, the authors of KiCad, Microsoft (sic!) for 3D builder, and to Bonn Netz!

 


Viewing all articles
Browse latest Browse all 41

Trending Articles