Tuesday 14 October 2014

Slimme electriciteitsmeter (Kamstrup 162JxC) uitlezen met de Raspberry Pi (dutch how-to)

Inleiding

Begin oktober 2014 besloot de netwerkbeheerder dat het tijd was om mijn vertrouwde, ouderwetse electriciteitsmeter te vervangen door een 'slimme' meter.
De nieuwe meter betreft een Kamstrup 162JxC.
Omdat ik al diverse projecten heb uitgevoerd met de Raspberry Pi (MediaCenter, UPS monitor/controller, data-logging met RRDtool) en omdat ik tot op heden handmatig de meterstanden bijhield, vond ik het wel een leuke uitdaging om eens te kijken of ik deze meter met een Raspberry Pi kon uitlezen. Er zit immers een communicatie-poort op deze meter die erom vraagt om uitgelezen te worden.
Het manual van de meter vond ik op kamstrup.nl. Helaas staat er weinig bruikbare informatie in.
Informatie over de P1 poort vond ik hier (DSMR v4) of hier (DSMR v3) Dit documentje lijkt meer nuttige informatie te bevatten.

Boodschappenlijstje

  • Om de verbinding te kunnen maken tussen de meter en de Raspberry Pi is een speciale kabel nodig. Deze "P1 Converter Cable" is te koop op de website smartmeterdashboard.nl. Ik heb daar de P1 Converter Cable v2 gekocht voor 22.50 euro.
  • Voor de Raspberry Pi zelf shop ik altijd bij The Pi Hut. Ik heb daar een Raspberry Pi Model B+, een 8GB (kleiner is al  bijna niet meer te krijgen...) micro-SDHC kaart (met NOOBS), een 5V 2A micro-USB voeding en een Multicomp Case op de kop getikt voor totaal ongeveer 59.00 euro (incl. verzendkosten).
  • Een UTP-kabel (voor deze toepassing is een Cat.5 al voldoende) heb ik nog liggen en de router staat in de meterkast, dus de verbinding met mijn thuisnetwerk is makkelijk gemaakt.
  • Enige kennis van Python, Linux (bij voorkeur Debian of een afgeleide daarvan) en ervaring met de Raspberry Pi zijn een pré. 
  • Mocht onderstaande beschrijving op punten onduidelijk zijn... Google is je vriend.
De totale kosten voor dit project zijn dus 81.50 euro.

De Raspberry Pi model B+ vervangt een al bestaande Pi model B. Het B-model ga ik gebruiken voor dit project. Het B-plus-model krijgt een andere bestemming.
Omdat de Raspberry Pi geen monitor of toetsenbord heeft, is een computer nodig die via het (thuis-)netwerk verbinding kan maken met de Pi. Voor Windows kun je gebruik maken van ssh via Putty; in OSX is Terminal.app of iTerm.app een goede keuze en is ssh standaard beschikbaar. Ik neem even aan dat het IP-adres bekend is. Op mijn router heb ik het MAC-adres van de Raspberry Pi verbonden aan een vast IP-adres. Dat maakt het inloggen later een stuk gemakkelijker.

NOOBS

NOOBS staat voor "New Out OBox Software" en is een pakket bestaande uit 6 operating systems die erg populair zijn voor de Raspberry Pi (ArchLinux, Raspian, Pidora, RaspBMC, OpenELEC en RiscOS). Als je een SD-kaart specifiek voor de Raspberry Pi koopt, wordt die momenteel zo'n beetje standaard met dit pakket erop geleverd. Leuk voor de beginners om eens mee te experimenteren, maar omdat ik voor dit project geen behoefte heb aan de geboden keuzemogelijkheden, zal ik de SD-kaart leeg maken en er een andere bestemming aan geven.

Raspbian

De nieuwste versie van Raspbian download ik altijd vanaf de RaspberryPi.org website. Hoe een SD-kaart voorzien moet worden van dit image is daar ook te vinden in de installatie instructie. Een alternatieve installatie methode voor Apple gebruikers is, om gebruik te maken van de OSX app "Apple-Pi-Baker.app"
Als de image op de kaart staat zijn we klaar om van start te gaan.

Als je al een SD-kaartje hebt (en vooral als die kleiner is dan 4GB) dan valt het te overwegen om er een afgeslankte versie van Raspbian op te installeren. Zie deze handleiding (engelstalig). Ga daarna hieronder verder bij het hoofdstukje "Pakketten installeren".

Power On!

In principe kun je nu al meteen de boel in de meterkast aansluiten. Plaats de SD-kaart in de Pi, sluit de P1 Converterkabel aan op de electriciteitsmeter en steek de USB-stekker in de Pi. Sluit dan de voeding aan en steek de stekker in het stopcontact.
In mijn netwerk heb ik voor de Raspberry Pi het IP-adres 10.0.1.228 gealloceerd.
Op mijn laptop (Putty of terminal) wacht ik op het actief worden van de Raspberry Pi met het ping commando.
$ ping 10.0.1.228
PING 10.0.1.228 (10.0.1.228): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
64 bytes from 10.0.1.228: icmp_seq=64 ttl=64 time=4.366 ms
Nu kan ingelogd worden met
$ ssh pi@10.0.1.228
Als eerste zal nu de installatie afgemaakt moeten worden.
$ sudo raspi-config

In het menu dat nu verschijnt (zie afbeelding) maak ik de volgende selecties:
1. Expand Filesystem -> OK
2. Change User Password -> OK -> volg de aanwijzingen op het scherm
3. Enable Boot to Desktop/Scratch -> Console text console -> OK
4. Internationalisation Options -> I1 Change locale -> Er verschijnt nu een lange lijst met locales (regios/talen). Bij en_GB.UTF8 staat al een sterretje. Haal dat weg door erop te gaan staan met de cursor en op de spatiebalk te drukken.  Zoek vervolgens de gewenste locale en selecteer die met de spatiebalk. Zelf selecteer ik altijd en_US.UTF8. De kans dat mijn US toetsenbord dan goed werkt is dan het grootst ;-) Selecteer daarna de gewenste locale als de default. -> OK.
5. Internationalisation Options -> I2 Change timezone -> Ik kies hier Europe/Amsterdam.
8. Advanced Options -> A2 Hostname -> OK -> Verander desgewenst de standaard hostname (raspberrypi) in iets anders. Omdat alle servers binnen mijn netwerk de naam hebben van een elementair deeltje kies ik voor de voor de hand liggende naam electron.
9. Advanced Options -> A3 Memory Split -> Verander de hoeveelheid videogeheugen naar 16. Meer hebben we niet nodig.
10. Advanced Options -> A4 SSH -> Enable : SSH staat al aan, maar voor de zekerheid enable ik deze optie alsnog.

Finish -> Would you like to reboot now? -> NO : Ik ben nog niet klaar...

Terug op de command-line zien we een hoop foutmeldingen. Ongeveer zoiets als dit:
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LC_CTYPE = "UTF-8",
LANG = "en_GB.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
locale: Cannot set LC_CTYPE to default locale: No such file or directory
locale: Cannot set LC_MESSAGES to default locale: No such file or directory
locale: Cannot set LC_ALL to default locale: No such file or directory
/usr/bin/locale: Cannot set LC_CTYPE to default locale: No such file or directory
/usr/bin/locale: Cannot set LC_MESSAGES to default locale: No such file or directory
/usr/bin/locale: Cannot set LC_ALL to default locale: No such file or directory
Daar ga ik eerst iets aan doen. De oorzaak van deze meldingen is gelegen in het feit dat de terminal via ssh een locale probeert in te stellen.
$ sudo nano /etc/ssh/sshd_config
Bijna onderaan dit bestand staan de regels
# Allow client to pass locale environment variables
AcceptEnv LANG LC_*
Plaats een # voor die tweede regel (comment out), zodat je er dit staat:
# Allow client to pass locale environment variables
# AcceptEnv LANG LC_*
Om de SD-kaart te sparen verplaatsen we de /tmp directory naar tmpfs:
$ sudo nano /etc/fstab
Voeg deze regel toe:
tmpfs /tmp tmpfs nodev,nosuid,size=30M,mode=1777 0 0 
Ook de swap hebben we niet nodig:
$ sudo dphys-swapfile uninstall
$ sudo rm /etc/init.d/dphys-swapfile
En de gebruiker pi moet ook toegevoegd worden aan de groep dialout.
$ sudo usermod -a -G dialout pi
Rest ons nog om de ssh sleutels te regenereren gevolgd door een reboot.
$ sudo rm /etc/ssh/ssh_host_*
$ sudo dpkg-reconfigure openssh-server

$ sudo reboot; exit

Pakketten installeren

De volgende stap is het installeren van diverse software die niet standaard op de Raspberry Pi aanwezig is, maar wel handig kan zijn. Om te beginnen voeren we een update uit van de reeds aanwezige software:
$ sudo apt-get update
$ sudo apt-get autoclean
$ sudo apt-get autoremove
$ sudo apt-get -yuV upgrade
Het is zeer aanbevelenswaardig, om bovenstaande vier commando's met enige regelmaat op het systeem uit te voeren, zodat het systeem up-to-date blijft.
Dan installeer ik:
  • htop; een grafische versie van top,
  • screen; hiermee kan een terminal-sessie worden voortgezet zonder dat je daarvoor ingelogd hoeft te zijn,
  • lsof; toont open bestanden EN door welke programma ze geopend zijn,
  • bc; command-line calculator,
  • python-serial; een Python module die we nodig hebben om met P1 poort op de electriciteitsmeter te kunnen communiceren,
  • cu; programma om de communicatie met de P1-poort te kunnen testen en troubleshooten.
$ sudo apt-get install bc htop screen lsof python-serial cu
Pas de opstartparameters aan, zodat de serieele poort niet door de kernel misbruikt wordt:
$ sudo nano /boot/cmdline.txt
In dit bestand verwijder ik de console= en kgdboc= parameters (hier doorgestreept):

dwc_otg.lpm_enable=0 console=ttyAMA0,115200 kgdboc=ttyAMA0,115200 console=tty1 elevator=deadline root=/dev/mmcblk0p2 rootfstype=ext4 rootwait

$ sudo reboot; exit

Communicatie test

Nu is het tijd om te kijken of de Raspberry Pi onze slimme meter via de P1 Converter kabel kan verstaan. Als eerste dient gecontroleerd te worden of de P1 Converter Cable goed gedetecteerd is:
$ dmesg | grep usb
Deze output laat zien dat de kabel goed gedetecteerd wordt:
[2014-10-28 16:30:15]  usb 1-1.2: new full-speed USB device number 4 using dwc_otg
[2014-10-28 16:30:15]  usb 1-1.2: New USB device found, idVendor=0403, idProduct=6001
[2014-10-28 16:30:15]  usb 1-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[2014-10-28 16:30:15]  usb 1-1.2: Product: P1 Converter Cable
[2014-10-28 16:30:15]  usb 1-1.2: Manufacturer: FTDI
[2014-10-28 16:30:15]  usb 1-1.2: SerialNumber: A6Y1OLG2
[2014-10-28 16:30:15]  usbcore: registered new interface driver usbserial
[2014-10-28 16:30:15]  usbcore: registered new interface driver usbserial_generic
[2014-10-28 16:30:15]  usbserial: USB Serial support registered for generic
[2014-10-28 16:30:16]  usbcore: registered new interface driver ftdi_sio
[2014-10-28 16:30:16]  usbserial: USB Serial support registered for FTDI USB Serial Device
[2014-10-28 16:30:16]  usb 1-1.2: Detected FT232RL
[2014-10-28 16:30:16]  usb 1-1.2: Number of endpoints 2
[2014-10-28 16:30:16]  usb 1-1.2: Endpoint 1 MaxPacketSize 64
[2014-10-28 16:30:16]  usb 1-1.2: Endpoint 2 MaxPacketSize 64
[2014-10-28 16:30:16]  usb 1-1.2: Setting MaxPacketSize 64
[2014-10-28 16:30:16]  usb 1-1.2: FTDI USB Serial Device converter now attached to ttyUSB0

De laatste regel toont dat de kabel verbonden is met /dev/ttyUSB0. Handig om te weten.

Als tweede test proberen we de meter uit te lezen:
$ cu -l /dev/ttyUSB0 -s 9600 --parity=none
Binnen 10 seconden zou dan iets dergelijks op het scherm moeten verschijnen:
Connected.
/KMP5 KA6U00xxxxxxxx

0-0:96.1.1(xxxxxxx)
1-0:1.8.1(00170.393*kWh)
1-0:1.8.2(00122.009*kWh)
1-0:2.8.1(00000.000*kWh)
1-0:2.8.2(00000.000*kWh)
0-0:96.14.0(0002)
1-0:1.7.0(0000.38*kW)
1-0:2.7.0(0000.00*kW)
0-0:17.0.0(999*A)
0-0:96.3.10(1)
0-0:96.13.1()
0-0:96.13.0()
!

Sluit 'cu' af met ~. gevolgd door een enter. Als je via ssh ingelogd bent op de Pi kan het zijn dat nu de verbinding met de Pi verbroken wordt. Dit komt omdat ssh ook de ~. herkent als een "verbreek-de-verbinding" signaal. IN dat geval dient de tilde tweemaal getypt te worden: ~~., zodat ssh de tilde-punt negeert.

In de DSMR specificatie v3.0 op pagina 14 staat een tabel met de betekenis van de verschillende regels. Ik ben vooral geïntereseerd in het cumulatieve verbruik (1.8.1 en 1.8.2 in kWh) en in het actuele vermogen (1.7.0 in kW). Voor mensen die ook stroom terug leveren zijn respectievelijk 2.8.1, 2.8.2 en 2.7.0 ook interessant. 96.14.0 geeft aan of het hoge of lage tarief actief is (ook wel piek/dal of dag/nacht). Ook 96.3.10 kan interessant zijn. Deze geeft aan of er (1) stroom wordt afgenomen of (2) stroom wordt teruggeleverd.

Merk op dat de gegevens die de meter verstuurt voorafgegaan worden door een schuine streep ("/") en dat het einde van de data afgesloten wordt met een uitroepteken ("!"). Hier ga ik later in het programma dankbaar gebruik van maken.

Automatisch uitlezen van de meter


Als dat nog niet is gedaan, maak je nu een directory aan om de binary in op te slaan:
$ mkdir ~/bin

Dan maken we een bestand aan:
$ nano ~/bin/readkamstrup.py
En vullen het met deze code:
#! /usr/bin/python

import sys, serial, re

port          = serial.Serial()
port.baudrate = 9600
port.bytesize = serial.SEVENBITS
port.parity   = serial.PARITY_EVEN
port.stopbits = serial.STOPBITS_ONE
port.xonxoff  = 1
port.rtscts   = 0
port.dsrdtr   = 0
port.timeout  = 0
port.port     = "/dev/ttyUSB0"

def gettelegram():
  # flag used to exit the while-loop
  abort = 0
  # countdown counter used to prevent infinite loops
  loops2go = 40
  # storage space for the telegram
  telegram = []
  # end of line delimiter
  delim = "\x0d"

  try:
    port.open()
  except:
    abort == 4
    # open error terminates immediately
    return telegram, abort

  while abort == 0:
    try:
      line = "".join(iter(lambda:port.read(1),delim)).strip()
    except:
      # read error
      abort = 2
    if line == "!":
      # end of telegram uncountered
      abort = 1
    telegram.append(line)
    loops2go = loops2go - 1
    if loops2go < 0:
      abort = 3

  # test for correct start of telegram
  if telegram[0][0] != "/":
    abort = 2

  try:
    port.close()
  except:
    abort == 5

  # Return codes:
  # abort == 1 indicates a successful read
  # abort == 2 means that no valid data was read from the serial port
  # abort == 3 indicates a data overrun. More lines were received than expected.
  # abort == 4 indicates a serial port open error.
  # abort == 5 indicates a serial port close error.
  return (telegram, abort)

if __name__ == "__main__":

  electra1in = -1
  electra2in = -1
  electra1out = -1
  electra2out = -1
  tarif = -1
  swits = -1
  powerin = -1
  powerout = -1
  telegram, status = gettelegram()
  if status == 1:
    for element in range(3, len(telegram) - 1):
      line =  re.split( '[\(\*\)]', telegram[element] )
      #['1-0:1.8.1', '00175.402', 'kWh', '']
      if (line[0] == '1-0:1.8.1'):
        electra1in = float(line[1])
      #['1-0:1.8.2', '00136.043', 'kWh', '']
      if (line[0] == '1-0:1.8.2'):
        electra2in = float(line[1])
      #['1-0:2.8.1', '00000.000', 'kWh', '']
      if (line[0] == '1-0:2.8.1'):
        electra1out = float(line[1])
      #['1-0:2.8.2', '00000.000', 'kWh', '']
      if (line[0] == '1-0:2.8.2'):
        electra2out = float(line[1])
      #['0-0:96.14.0', '0002', '']
      if (line[0] == '0-0:96.14.0'):
        tarif = int(line[1])
      #['1-0:1.7.0', '0000.32', 'kW', '']
      if (line[0] == '1-0:1.7.0'):
        powerin = float(line[1])
      #['1-0:2.7.0', '0000.00', 'kW', '']
      if (line[0] == '1-0:2.7.0'):
        powerout = float(line[1])
      #['0-0:17.0.0', '999', 'A', '']
         # not recorded
      #['0-0:96.3.10', '1', '']
      if (line[0] == '0-0:96.3.10'):
        swits = int(line[1])
      #['0-0:96.13.1', '', '']
         # not recorded
      #['0-0:96.13.0', '', '']
         # not recorded

    # Print the data
    print '{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}'.format(electra1in, electra2in, powerin, electra1out, electra2out, powerout, tarif, swits)

Zet de rechten:
$ chmod 740 ~/bin/readkamstrup.py
$  readkamstrup.py
175.402, 137.382, 0.53, 0.0, 0.0, 0.0, 2, 1

Door nu met het script te spelen, zijn hier leuke dingen mee te doen. Zo laat ik ook de datum/tijd afdrukken, waarna ik de data in een rrdtool database opneem ten behoeve van een webpagina waarop de status wordt weergegeven. 

Veel succes en plezier. Laat gerust in de comments een opmerking achter als je bovenstaande code gebruikt.

3 comments:

Anonymous said...

Door dit artikel heb ik het uitlezen aan de praat gekregen. bedankt!

Heb het volgende extra commando uit moeten voeren (vanwege (/dev/ttyUSB0): Permission denied):
# adduser uucp dailout

En de baud rate verhoogd naar 115200:
$ cu -l /dev/ttyUSB0 -s 115200

Anonymousy said...

Opmerkelijk dat je zo'n hoge baudrate nodig hebt. De meeste meters gaan niet sneller dan 9600. Weet je zeker dat er geen gegevens verloren gaan?

Bedankt voor de tip van de group dialout. Die was ik vergeten.

Anonymous said...

Heb de connectie gegevens even opgezocht. Het is een DSMR4 meter en daarvoor moet de COM poort als volgt ingesteld worden:

* 115200baud
* Geen pariteit
* 8 Databits
* 1 Stopbit
* xon/off
* Geen arts/cts