Zählerwerte vom Stromzähler über D0-Schnittstelle (IEC 62056-21) auslesen

Live-Demo: home.404.at/?page=powerConsumption

Vor einiger Zeit habe ich mich dazu entschlossen, auch den Stromverbrauch und den Leistungsbedarf im Haus ein wenig zu überwachen. Für meine Grundwasser-Wärmepumpe habe ich separate Subzähler installiert, deren S0-Impuls habe ich per Digitaleingang bereits auf den KNX-Bus gebracht, das war dann aber auch schon alles was damit umgesetzt wurde. Das Interesse am Gesamtstrombedarf war grösser. Da der Stromzähler, welchen die Tiwag bei mir installiert hat (EMH, Typ ITZ) zwar über eine S0-Schnittstelle verfügt, diese aber dank Verplombung nicht zugänglich ist, war anfangs nur an einen quick-Hack zu denken. So habe ich einfach einen LDR über die LED am Zähler geklebt, ausreichend optisch von der Umgebung abgeschirmt (großer Fleck an Isolierband, welcher auch gleichzeitig der Befestigung dient) und mit einem pull-down Widerstand gegen 3.3V an einen Digitaleingang eines Raspberry Pi gehängt. Entgegen allen Erwartungen hat das sogar (super) funktioniert. Gestört hat dabei nur der große Fleck Isolierband, da dieser auch teilweise das Display am Zähler verdeckt hat und im Lauf der Zeit seiner Funktion als mechanische Befestigung nicht mehr nachkam. Nachdem dann schließlich auch noch die SD-Karte im Raspberry Pi sich von ihrer Berufung als Datenspeicher verabschiedet hat, war die Motivation gegen /dev/null, an eine Fortführung des Projektes in damals aktueller Form war nicht mehr zu denken. Also Zeit für etwas neues.

Hardware:
Bei der optischen D0-Schnittstelle handelt es sich um eine serielle Schnittstelle über Infrarot. Ein Telegramm besteht dabei aus einem Startbit, 7 Datenbits, einem Paritätsbit und einem Stopbit. Die standardisierten Baudraten betragen 300, 600, 1.200, 2.400, 4.800, 9.600 und 19.200 baud, wobei die Initialisierung einer Verbindung immer mit 300 baud erfolgt, später können sich die Geräte auf eine höhere Übertragungsrate einigen, oder weiterhin mit 300 baud rumgurken.
Anhand dieser Informationen sollte klar sein, dass sich das sehr einfach über einen UART umsetzen lässt, welchen der Raspberry Pi natürlich mitliefert.
Die optische Ankopplung an den Stromzähler erfolgt über einen Lesekopf. Dieser ist meist mit Magneten ausgestattet um an der runden Eisenscheibe hinter der Zählerfrontplatte zu halten.
Hier ein Stomlaufplan der gesamten Schaltung incl. der Dimensionen eines Lesekopfes lt. IEC 65056-21.

schematic
Stromlaufplan

Die Infrarot-LED zum Senden hatte ich seit Jahren rumliegen, für den Fototransistor als Empfänger habe ich eine alte Gabellichtschranke geopfert. Somit kann ich leider auch keine genauen Bauteilbezeichnungen liefern. Es sollten sich dafür aber nahezu alle gängigen Standardbauteile eignen. Bei dem Empfangselement (Fototransistor/Fotodiode) kann es sein dass sich IR-Empfänger für Fernbedienungen nicht eignen, da diese oft ein Filter für die jeweilige eingesetzte Frequenz (meist ca. 30-60kHz) bzw. einen Demodulator eingebaut haben. Der Schaltungsaufbau ist offensichtlich sehr trivial und bedarf keiner weiteren Erklärung. Zusätzliche Bauelemente zur Signalaufbereitung (Schmitt-Trigger, etc.) werden nicht benötigt, da die Signalform (zumindest bei mir) bereits mit dieser Grundschaltung sehr ansehnlich ist.

Oszillogramm
Oszillogramm

Für die Befestigung der Bauteile wurde als Prototyp ein Alurest (5mm dickes Stangenmaterial) zurechtgesägt und mit zwei 5mm Bohrungen für die IR-LED und den Fototransistor ausgestattet. Zusätzlich habe ich mit Flüssigmetall noch einige kleine Magneten eingeklebt, welche den Lesekopf am Eisenring hinter der Frontplatte des Zählers festhalten. Diese sollten zahlenmäßig nicht unterlegen sein, immerhin gilt es für den magnetischen Fluss auch noch einen Spalt (Gehäusefront und Gummiring an der Frontplatte) zu überwinden. Die LED und der Fototransistor werden mitsamt den Widerständen auf der Halterung verlötet und mit jeder Menge Heißkleber fixiert.

Der Anfang der mechanischen Bearbeitung
Der Anfang der mechanischen Bearbeitung
Hier mit eingeklebten Neodym-Magneten
Hier mit eingeklebten Neodym-Magneten
Der fertige und bereits installierte Lesekopf nebst RaspberryPi
Der fertige und bereits installierte Lesekopf nebst RaspberryPi

Software:
Auf einer neuen SD-Karte habe ich die damals aktuellste minimale Version (2017-03-02-raspbian-jessie-lite) von Raspbian installiert. Nach dem ersten erfolgreichen Hochfahren kam dann mal das Triviale dran:

sudo apt-get -y install vim
sudo raspi-config

Nachdem nun ein sicheres Kennwort gesetzt, das Dateisystem auf die maximale Größe erweitert, und hoffentlich auch die germanische Tastatur eingerichtet ist, kann nun vernünftig auf der Himbeere gearbeitet werden. Zuerst muss die serielle Schnittstelle von ihrer Funktion als Standard-Ausgabegerät gelöst werden.
Dazu entweder im Punkt 5 von raspi-config (Interfacing Options), P6 (Serial) die Frage „Would you like a login shell to be accessible over serial?“ mit „No“ und die Frage „Would you like the serial port hardware to be enabled?“ mit „Yes“ beantworten, oder alternativ folgende Dateien entsprechend angepassen:

sudo vim /boot/config.txt

enable_uart=1 am Ende hinzufügen.

sudo vim /boot/cmdline.txt

console=serial0,115200 entfernen oder auskommentieren.
Nach einem

sudo reboot

sollte aus Sicht des Betriebssystems alles erledigt sein.

Hier der relativ schnell zusammengeschusterte C-Code zum Auslesen der Zählerwerte:

/*
 * 2017 - blog.404.at
 *
 * Read values from IEC 62056-21 powermeter.
 * Working with RaspberryPi / BananaPi
 *
 * ToDo:
 * Needs some errorhandling.
 * Negotiation of higher baudrate not working on EMH ITZ powermeter.
 *
 * */
 
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <stdbool.h>
#include <mysql.h>
#include <my_global.h>
#include <stdint.h>
#include <time.h>
 
#define SQL_HOST "homeAutomation"
#define SQL_USER "homeAutomation"
#define SQL_PASS "yourpasswordhere"
#define SQL_DB   "homeAutomation"
 
 
typedef struct values_t {
	uint32_t electricityID;
	uint8_t billingPeriod;
	uint32_t activeFirmware;
	uint32_t timeSwitchPrgmNo;
	uint32_t localTime;
	uint32_t localDate;
	float sumPower1;
	float sumPower2;
	float sumPower3;
	float power;
	float U_L1;
	float U_L2;
	float U_L3;
	uint32_t deviceNo;
	uint8_t freqL1;
	uint8_t freqL2;
	uint8_t freqL3;
} values_t;
 
 
 
int init_serial ( int baudrate )
  {
 
   int fd;
   struct termios options;
 
   // For termios options see http://linux.die.net/man/3/termios
 
   fd = open ( "/dev/ttyAMA0", O_RDWR | O_NOCTTY | O_NDELAY );     // For use on BPI-R1 change to /dev/ttyS2
 
   if ( fd &gt;= 0 ) {
     // Get current options
     fcntl ( fd, F_SETFL, 0 );
     if ( tcgetattr ( fd, &amp;options ) != 0 ) return (-1);
     memset ( &amp;options, 0, sizeof ( options ) ); // Save options to possibly restore them later
 
     // Set baudrate
     if (baudrate == 300) {
	     cfsetispeed ( &amp;options, B300 );
	     cfsetospeed ( &amp;options, B300 );
     }
 
     if ( baudrate == 4800 ) {
	     cfsetispeed ( &amp;options, B4800 );
	     cfsetospeed ( &amp;options, B4800 );
     }
 
 
     options.c_cflag |= PARENB;          // Enable parity generation on output and parity checking on input
     options.c_cflag &amp;= ~PARODD;         // Unset odd parity
     options.c_cflag &amp;= ~CSTOPB;         // Delete Flag for 2 stop bits, rather than one
     options.c_cflag &amp;= ~CSIZE;          // Unset character size mask
     options.c_cflag |= CS7;             // Character size mask (CS5, CS6, CS7 or CS8)
 
     options.c_cflag |= CLOCAL;          // Ignore modem control lines
     options.c_cflag |= CREAD;           // Enable receiver
 
     options.c_lflag &amp;= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
     options.c_iflag &amp;= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
 
     options.c_iflag = IGNPAR;           // Ignore framing errors and parity errors
     options.c_oflag &amp;= ~OPOST;          // Disable implementation-defined output processing, set to "raw" Input
     options.c_cc[VMIN]  = 0;            // Minimum number of characters for noncanonical read
     options.c_cc[VTIME] = 10;           // Timeout in deciseconds for noncanonical read
     tcflush ( fd,TCIOFLUSH );          // flushes both data received but not read, and data written but not transmitted
 
     // if (tcsetattr(fd, TCSAFLUSH, &amp;options) != 0) return(-1); // the change occurs after all output written to the object referred by fd has been transmitted, and all input that has been received but not read will be discarded before the change is made.
     if ( tcsetattr ( fd, TCSANOW, &amp;options ) != 0 ) return ( -1) ; // The change occurs immediately
 
   }
   return ( fd );
}
 
int sendbytes ( int fd, char * Buffer, int Count )
 
  {
  int sent;
 
  sent = write ( fd, Buffer, Count );
  if ( sent &lt; 0 )
    {
    perror ( "sendbytes failed - error!" );
    return false;
    }
  if ( sent &lt; Count ) { perror ( "sendbytes failed - truncated!" ); } return sent; } int receiveBytes ( int fd, char * retBuffer ) { char buf[101], c; int count, i = 0; do { count = read ( fd, (void*)&amp;c, 1 ); if ( c == 0x3 ) { // ETX return false; } if ( count &gt; 0 ) {
			if ( c  != '\r' &amp;&amp; c != '\n')
				buf[i++] = c;
		}
 
	} while ( c != '\n' &amp;&amp; i &lt; 100 &amp;&amp; count &gt;= 0 );
 
	if ( count &lt; 0 ) perror ( "Read failed!" );
	else if ( i == 0 ) perror ( "No data!" );
	else {
	  buf[i] = '\0';
	  snprintf ( retBuffer, i + 1, buf );
	  //printf (" %i Bytes: %s", i, buf );
	}
 
	return true;
 
}
 
 
 
int main( void ) {
 
	MYSQL *con;
	char sql[1000] = "\0";
 
	my_bool my_true = TRUE;
	con = mysql_init ( NULL );
 
	if ( con == NULL )
		fprintf ( stderr, "%s\n", mysql_error ( con ) );
	if ( mysql_options ( con, MYSQL_OPT_RECONNECT, &amp;my_true ) )
		fprintf ( stderr, "MySQL_options failed: unknown MYSQL_OPT_RECONNECT." );
	if ( mysql_real_connect ( con, SQL_HOST, SQL_USER, SQL_PASS, SQL_DB, 0, NULL, 0 ) == 0 )
		fprintf ( stderr, "Connect to DB failed." );
 
    int fd;
 
	char sendBuffer[100];
	char retBuffer[101];
	char *p_retBuffer = retBuffer;
	char tmpVal[9];	// 8 Char + \0
 
	values_t values;
 
	time_t systime;
 
	for ( ; ; ) {
		systime = time( NULL );
 
		if ( systime  % 30 == 0 ) {
 
			fd = init_serial ( 300 );
 
			printf ( "Sending Request...\r\n" );
			sprintf ( sendBuffer, "/?!\r\n" );
			sendbytes ( fd, sendBuffer, 5 );
 
			printf ( "Identification: " );
			receiveBytes ( fd, p_retBuffer );
			printf ( "%s", retBuffer );
			printf ( "\r\n" );
 
			usleep ( 3000000 );	// 300msec
 
			printf ( "Sending ACK...   ");
			sprintf ( sendBuffer, "%c040\r\n", 0x06 );
			sendbytes ( fd, sendBuffer, strlen ( sendBuffer ) );
 
		//	close ( fd );
		//	fd = init_serial ( 4800 );
		//	printf ( "New connection with 4800 Baud\r\n" );
 
			receiveBytes ( fd, p_retBuffer );
			printf ( "Response: %s\r\n", retBuffer );
 
			//usleep(300000);	// 300msec
 
			while ( receiveBytes ( fd, p_retBuffer ) ) {
				if ( strncasecmp ( retBuffer, "0.0.0", 5) == 0 ) {
					snprintf ( tmpVal, 9, "%.*s", 8, retBuffer + 6 );
					printf ( "Electricity id: %i  rawdata: %s\n", atoi( tmpVal ), retBuffer );
					values.electricityID = atoi ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "0.1.0", 5) == 0 ) {
					snprintf ( tmpVal, 3, "%.*s", 2, retBuffer + 6 );
					printf ( "Billing period: %i  rawdata: %s\n", atoi( tmpVal ), retBuffer );
					values.billingPeriod = atoi ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "0.2.0", 5) == 0 ) {
					snprintf ( tmpVal, 9, "%.*s", 8, retBuffer + 6 );
					printf ( "Active firmware: %i  rawdata: %s\n", atoi( tmpVal ), retBuffer );
					values.activeFirmware = atoi ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "0.2.2", 5) == 0 ) {
					snprintf ( tmpVal, 9, "%.*s", 8, retBuffer + 6 );
					printf ( "Time switch prgm no.: %i  rawdata: %s\n", atoi( tmpVal ), retBuffer );
					values.timeSwitchPrgmNo = atoi ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "0.9.1", 5) == 0 ) {
					snprintf ( tmpVal, 8, "%.*s", 7, retBuffer + 6 );
					printf ( "Time: %i  rawdata: %s\n", atoi( tmpVal ), retBuffer );
					values.localTime = atoi ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "0.9.2", 5) == 0 ) {
					snprintf ( tmpVal, 8, "%.*s", 7, retBuffer + 6 );
					printf ( "Date: %i  rawdata: %s\n", atoi( tmpVal ), retBuffer );
					values.localDate = atoi ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "1.8.0", 5) == 0 ) {
					snprintf ( tmpVal, 9, "%.*s", 8, retBuffer + 6 );
					printf ( "Sum Power 1: %08.1f  rawdata: %s\n", atof( tmpVal ), retBuffer );
					values.sumPower1 = atof ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "1.8.1", 5) == 0 ) {
					snprintf ( tmpVal, 9, "%.*s", 8, retBuffer + 6 );
					printf ( "Sum Power 2: %08.1f  rawdata: %s\n", atof( tmpVal ), retBuffer );
					values.sumPower2 = atof ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "1.8.2", 5) == 0 ) {
					snprintf ( tmpVal, 9, "%.*s", 8, retBuffer + 6 );
					printf ( "Sum Power 3: %08.1f  rawdata: %s\n", atof( tmpVal ), retBuffer );
					values.sumPower3 = atof ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "1.25", 4) == 0 )  {
					snprintf ( tmpVal, 6, "%.*s", 5, retBuffer + 5 );
					printf ( "Power: %05.2f  rawdata: %s\n", atof( tmpVal ), retBuffer );
					values.power = atof ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "32.25", 5) == 0 ) {
					snprintf ( tmpVal, 6, "%.*s", 5, retBuffer + 6 );
					printf ( "U L1: %05.1f  rawdata: %s\n", atof( tmpVal ), retBuffer );
					values.U_L1 = atof ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "52.25", 5) == 0 ) {
					snprintf ( tmpVal, 6, "%.*s", 5, retBuffer + 6 );
					printf ( "U L2: %05.1f  rawdata: %s\n", atof( tmpVal ), retBuffer );
					values.U_L2 = atof ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "72.25", 5) == 0 ) {
					snprintf ( tmpVal, 6, "%.*s", 5, retBuffer + 6 );
					printf ( "U L3: %05.1f  rawdata: %s\n", atof( tmpVal ), retBuffer );
					values.U_L3 = atof ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "C.1.0", 5) == 0 ) {
					snprintf ( tmpVal, 9, "%.*s", 8, retBuffer + 6 );
					printf ( "Device No.: %i  rawdata: %s\n", atoi( tmpVal ), retBuffer );
					values.deviceNo = atoi ( tmpVal );
				}
				if ( strncasecmp ( retBuffer, "C.7.1", 5) == 0 ) {
					snprintf ( tmpVal, 5, "%.*s", 4, retBuffer + 6 );
					printf ( "Freq. L1: %li  rawdata: %s\n", strtol( tmpVal, NULL, 16 ), retBuffer );
					values.freqL1 = strtol( tmpVal, NULL, 16 );
				}
				if ( strncasecmp ( retBuffer, "C.7.2", 5) == 0 ) {
					snprintf ( tmpVal, 5, "%.*s", 4, retBuffer + 6 );
					printf ( "Freq. L2: %li  rawdata: %s\n", strtol( tmpVal, NULL, 16 ), retBuffer );
					values.freqL2 = strtol( tmpVal, NULL, 16 );
				}
				if ( strncasecmp ( retBuffer, "C.7.3", 5) == 0 ) {
					snprintf ( tmpVal, 5, "%.*s", 4, retBuffer + 6 );
					printf ( "Freq. L3: %li  rawdata: %s\n", strtol( tmpVal, NULL, 16 ), retBuffer );
					values.freqL3 = strtol( tmpVal, NULL, 16 );
				}
 
			}
 
			close ( fd );
 
			sprintf ( sql, "INSERT INTO D0 (dbTime, electricityID, billingPeriod, activeFirmware, timeSwitchPrgmNo, `localTime`, localDate"\
							", sumPower1, sumPower2, sumPower3, power, U_L1, U_L2, U_L3, deviceNo, freqL1, freqL2, freqL3) "\
							"VALUES (now(), %i, %i, %i, %i, %i, %i, %8.1f, %8.1f, %8.1f, %5.2f, %5.1f, %5.1f, %5.1f, %i, %i, %i, %i)"
							,values.electricityID
							, values.billingPeriod
							, values.activeFirmware
							, values.timeSwitchPrgmNo
							, values.localTime
							, values.localDate
							, values.sumPower1
							, values.sumPower2
							, values.sumPower3
							, values.power
							, values.U_L1
							, values.U_L2
							, values.U_L3
							, values.deviceNo
							, values.freqL1
							, values.freqL2
							, values.freqL3);
			if ( mysql_query ( con, sql ) ) {
				fprintf ( stderr, "Error at statement: %s\n", mysql_error ( con ) );
			}
		}
	}
 
    return true;
}

Anzupassen sind noch die Einträge für SQL_HOST, SQL_USER, SQL_PASS und SQL_DB
Wer lieber einen Banana Pi verwendet, muss in der Funktion init_serial noch das Gerät der seriellen Schnittstelle von /dev/ttyAMA0 auf /dev/ttyS2 ändern.
Der Code läuft bei mir nun schon seit ein paar Monaten ohne Probleme, und das obwohl bezüglich Errorhandling noch einiges zu implementieren wäre. Lediglich die Aushandlung einer höheren Baudrate nach dem Request funktioniert nicht. Laut Debug vom Oszilloskop scheint aber alles korrekt übertragen zu werden. Keine Ahnung warum das nicht klappt, vielleicht unterstützt der Zähler das auch einfach nicht.
Wem’s interessiert, der kann sich das bei Freizeitüberschuss gerne mal ansehen. Bis dahin läuft die komplette Kommunikation einfach mit 300Baud. Basta.

Zum compilieren fehlen noch die MySQL-Header:

sudo apt-get -y install libmysqlclient-dev

Mittels

gcc -o D0 D0.c -I/usr/include/mysql -L/usr/lib/arm-linux-gnueabihf -lmysqlclient

verarbeitet uns der treue gcc den Code in eine ausführbare Bitsuppe, welche nach dem Ausführen alle 30 Sekunden die aktuellen Zählerwerte abfragt.
Wer lustig ist, kann sich dazu noch ein schönes Startscript basteln, und Vorkehrungen treffen, um die Anwendung automatisch beim Booten zu starten.

Zum Erstellen der Datenbank inklusive Tabelle kann folgendes Script am Datenbank-Host verwendet werden:

CREATE DATABASE homeAutomation;
CREATE USER 'homeAutomation'@'%'IDENTIFIED BY 'yourpasswordhere';
GRANT SELECT, INSERT, ALTER, CREATE ON homeAutomation.* TO 'homeAutomation'@'%';
 
CREATE TABLE IF NOT EXISTS `homeAutomation`.`D0` (
`id` INT NOT NULL AUTO_INCREMENT,
`dbTime` DATETIME NOT NULL,
`electricityID` INT NOT NULL ,
`billingPeriod` INT NOT NULL ,
`activeFirmware` INT NOT NULL ,
`timeSwitchPrgmNo` INT NOT NULL ,
`localTime` INT NOT NULL ,
`localDate` INT NOT NULL ,
`sumPower1` DECIMAL(8,1) NOT NULL ,
`sumPower2` DECIMAL(8,1) NOT NULL ,
`sumPower3` DECIMAL(8,1) NOT NULL ,
`power` DECIMAL(5,2) NOT NULL ,
`U_L1` DECIMAL(5,1) NOT NULL ,
`U_L2` DECIMAL(5,1) NOT NULL ,
`U_L3` DECIMAL(5,1) NOT NULL ,
`deviceNo` INT NOT NULL ,
`freqL1` INT NOT NULL ,
`freqL2` INT NOT NULL ,
`freqL3` INT NOT NULL , PRIMARY KEY (`id`) , UNIQUE INDEX `id_UNIQUE` (`id` ASC) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=0;

Nun fehlt noch etwas, um den Datenhaufen vernünftig darzustellen.
Dazu ist folgendes PHP-Script entstanden:

 
<?php
 
/*
 * 2017 - blog.404.at
 *
 * Simple summary and graphs for collected values from the powermeter.
 * 
 * */
define ( "DB_HOST", "homeAutomation" );
define ( "DB_USER", "homeAutomation" );
define ( "DB_PASS", "yourpasswordhere" );
define ( "DB_NAME", "homeAutomation" );
 
 
 
$dataset = isset($_GET['dataset']) ? $_GET['dataset'] : null;
 
 
$link = @mysqli_connect(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ( ! $link ) {
    die ( "Fehler " . mysqli_connect_errno() . " bei der Verbindung zur MySQL-DB: " . mysqli_connect_error() );
}
 
 
if (!empty($dataset)) {
	// Return JSON
 
	switch ($dataset) {
 
		case 'power':
 
			$data = array();
			$sql = "SELECT a.* FROM (SELECT id, UNIX_TIMESTAMP(dbTime) * 1000 as rawdate, power FROM homeAutomation.D0 ORDER BY dbTime DESC LIMIT 120) AS a ORDER BY rawdate ASC";
			$result = mysqli_query ( $link, $sql );
			if ( $result === FALSE ) die ("DB-query failed." . mysql_error());		
			while ( ($row = mysqli_fetch_assoc($result)) !== NULL ) {
				$data[] = array( "id" => $row['id'], "date" => $row['rawdate'], "value" => $row['power']);
			}
			print json_encode($data, JSON_NUMERIC_CHECK);
			break;
 
		case 'voltages':
 
			$data = array();
			$sql = "SELECT a.* FROM (SELECT id, UNIX_TIMESTAMP(dbTime) * 1000 as rawdate, U_L1, U_L2, U_L3 FROM homeAutomation.D0 ORDER BY dbTime DESC LIMIT 120) AS a ORDER BY rawdate ASC"; 
			$result = mysqli_query ( $link, $sql );
			if ( $result === FALSE ) die ("DB-query failed." . mysql_error());		
			while ( ($row = mysqli_fetch_assoc($result)) !== NULL ) {
				$data[] = array( "id" => $row['id'], "date" => $row['rawdate'], "value1" => $row['U_L1'],  "value2" => $row['U_L2'],  "value3" => $row['U_L3']);
			}
			print json_encode($data, JSON_NUMERIC_CHECK);
			break;
 
		case 'currentValues':
 
			$sql = "SELECT dbTime
					, electricityID
					, billingPeriod
					, activeFirmware
					, timeSwitchPrgmNo
					, `localTime`
					, localDate
					, round(sumPower1, 1) AS sumPower1
					, round(sumPower2, 1) AS sumPower2
					, round(sumPower3, 1) AS sumPower3
					, round(power, 2) AS power
					, round(U_L1, 1) AS U_L1
					, round(U_L2, 1) AS U_L2
					, round(U_L3, 1) AS U_L3
					, deviceNo
					, freqL1
					, freqL2
					, freqL3
			FROM homeAutomation.D0
			ORDER BY dbTime DESC
			LIMIT 1"; 
			$result = mysqli_query ( $link, $sql );
			if ( $result === FALSE ) die ("DB-query failed." . mysql_error());		
			$row = mysqli_fetch_assoc($result);
			print json_encode($row);
			break;
 
	}
 
}
 
 
else {
	// Display page content
 
?>
 
<!DOCTYPE html>
<html lang="de">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
		<title>powermeter</title>
 
		<script type="text/javascript" language="javascript" src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
 
		<script type="text/javascript" language="javascript" src="https://www.amcharts.com/lib/3/amcharts.js"></script>
		<script type="text/javascript" language="javascript" src="https://www.amcharts.com/lib/3/serial.js"></script>
		<script type="text/javascript" language="javascript" src="https://www.amcharts.com/lib/3/plugins/export/export.min.js"></script>
		<script type="text/javascript" language="javascript" src="https://www.amcharts.com/lib/3/themes/light.js"></script>
		<script type="text/javascript" language="javascript" src="https://www.amcharts.com/lib/3/plugins/dataloader/dataloader.min.js"></script>
 
		<link rel="stylesheet" href="https://www.amcharts.com/lib/3/plugins/export/export.css" type="text/css" media="all" />
 
		<script src="res/odometer/odometer.min.js"></script>
		<script type="text/javascript" language="javascript">
			window.odometerOptions = {
	  			format: '(,ddd).dd' // Change how digit groups are formatted, and how many digits are shown after the decimal point
			};
		</script>
 
		<link rel="stylesheet" href="res/odometer/odometer-theme-car.css" />
 
 
		<style>
 
			body {
				padding: 0 2em;
				font-family: Montserrat, sans-serif;
				-webkit-font-smoothing: antialiased;
				text-rendering: optimizeLegibility;
				color: #444;
				background: steelblue;
			}
			table {
				width: 100%;
				/* max-width: 600px; */
				/*border-collapse: collapse;*/
				border-radius: .4em;
				border: 1px solid #38678f;
				margin: 50px auto;
				background: white;
			}		
			tr {
    			border-color: lighten(#34495E, 50%);
    			border-top: 1px solid #ddd;
    			border-bottom: 1px solid #ddd;
			}
			th {
				background: steelblue;
				border-radius: .4em;
				padding: 5px;
				font-weight: lighter;
				text-shadow: 0 1px 0 #38678f;
				color: white;
				border: 1px solid #38678f;
				box-shadow: inset 0px 1px 2px #568ebd;
				transition: all 0.2s;
			}
			td.label {
				text-align:right;
			}
			.serialchart {
				width	: 100%;
				height	: 350px;
				background: #fff;
			}
 
		</style>
 
	</head>
	<body>
		<div id="currentValues">
			<table>
				<tr>
					<th colspan="10">IEC 62056-21 Meter Values</th>
				</tr>
				<tr>
					<td class="label">Counter 1 (no rate):</td>
					<td><span id="sumPower1" class="odometer">0</span> kW/h</td>
					<td class="label">Voltage L1:</td>
					<td><span id="U_L1">-</span> V</td>
					<td class="label">Frequency L1:</td>
					<td><span id="freqL1">-</span> Hz</td>
					<td class="label">Local time:</td>
					<td><span id="localTime">-</span></td>
					<td class="label">Time switch prgram number:</td>
					<td><span id="timeSwitchPrgmNo">-</span></td>
				</tr>
				<tr>
					<td class="label">Counter 2 (rate 1):</td>
					<td><span id="sumPower2" class="odometer">0</span> kW/h</td>
					<td class="label">Voltage L2:</td>
					<td><span id="U_L2">-</span> V</td>
					<td class="label">Frequency L2:</td>
					<td><span id="freqL2">-</span> Hz</td>
					<td class="label">Local date:</td>
					<td><span id="localDate">-</span></td>
					<td class="label">Counter Type:</td>
					<td><span id="electricityID">-</span></td>
				</tr>
				<tr>
					<td class="label">Counter 3 (rate 2):</td>
					<td><span id="sumPower3" class="odometer">0</span> kW/h</td>
					<td class="label">Voltage L3:</td>
					<td><span id="U_L3">-</span> V</td>
					<td class="label">Frequency L3:</td>
					<td><span id="freqL3">-</span> Hz</td>
					<td class="label">Billing period:</td>
					<td><span id="billingPeriod">-</span></td>
					<td class="label">Active Firmware:</td>
					<td><span id="activeFirmware">-</span></td>
				</tr>
				<tr>
					<td class="label">Current load:</td>
					<td><span id="power" class="odometer">0</span> kW</td>
					<td colspan="6"></td>
					<td class="label">Device #:</td>
					<td><span id="deviceNo">-</span></td>
				</tr>
			</table>
		</div>
		<div class="serialchart" id="powerChart"></div>
		<br>
		<div class="serialchart" id="voltagesChart"></div>
 
		<script type="text/javascript" language="javascript">
 
			(function($)
			{
			    $(document).ready(function()
			    {
			       $.getJSON("<?php print $_SERVER['PHP_SELF'];?>?dataset=currentValues", function(data){
					    $.each(data, function (index, value) {
					    	$('#' + index).text(value);
					    });
					});
			        var refreshId = setInterval(function()
			        {
				       $.getJSON("<?php print $_SERVER['PHP_SELF'];?>?dataset=currentValues", function(data){
						    $.each(data, function (index, value) {
						    	$('#' + index).text(value);
						    });
						});
			        }, 5000);
			    });
			})(jQuery);
 
			var chart = AmCharts.makeChart("powerChart", {
			    "type": "serial",
				"dataLoader": {
    			  "url": "<?php print $_SERVER['PHP_SELF'];?>?dataset=power",
    			  "format": "json",
			      "async": true,
			      "showErrors": true,
			      "showCurtain": true,
			      "reload": 30
				},
				"titles": [{
					"text": "Live power usage"
				}],
			    //"theme": "light",
			    "marginRight": 80,
			    "autoMarginOffset": 20,
			    "marginTop": 7,
			    "valueAxes": [{
			        "axisAlpha": 0.2,
			        "dashLength": 1,
			        "position": "left",
			        "ignoreAxisWidth": true
			    }],
			    "mouseWheelZoomEnabled": true,
			    "valueAxes": [{
       		       "gridAlpha": 0.07,
			       "title": "Power [kW]"
			    }],
			    "graphs": [{
			        "id": "g1",
			        "balloonText": "[[value]]",
			        "bullet": "round",
			        "bulletBorderAlpha": 1,
			        "bulletColor": "#FFFFFF",
			        "hideBulletsCount": 50,
			        "title": "red line",
			        "valueField": "value",
			        "useLineColorForBulletBorder": true
			    }],
			    "chartScrollbar": {
			        "autoGridCount": true,
			        "graph": "g1",
			        "scrollbarHeight": 40
			    },
			    "chartCursor": {
					"categoryBalloonDateFormat":"DD.MM.YYYY JJ:NN",
					"limitToGraph":"g1"
			    },
			    "categoryField": "date",
			    "categoryAxis": {
			        "parseDates": true,
			        "minPeriod": "mm",
			        "axisColor": "#DADADA",
			        "dashLength": 1,
			        "minorGridEnabled": true
			    },
			    "export": {
			        "enabled": true
			    }
			});
 
 
			var chart = AmCharts.makeChart("voltagesChart", {
			    "type": "serial",
				"dataLoader": {
    			  "url": "<?php print $_SERVER['PHP_SELF'];?>?dataset=voltages",
    			  "format": "json",
			      "async": true,
			      "showErrors": true,
			      "showCurtain": true,
			      "reload": 30
				},
				"titles": [{
						"text": "Live Line Voltages"
				}],
				"theme": "light",
			    "marginRight": 80,
			    "autoMarginOffset": 20,
			    "marginTop": 7,
			    "valueAxes": [{
			        "axisAlpha": 0.2,
			        "dashLength": 1,
			        "position": "left",
			        "ignoreAxisWidth": true
			    }],
				"legend": {
    				"useGraphSettings": true,
    				"align": "center"
  				},
			    "mouseWheelZoomEnabled": true,
			    "valueAxes": [{
       		       "gridAlpha": 0.07,
			       "title": "Voltage [V]"
			    }],
			    "graphs": [{
			        "id": "g1",
			        "balloonText": "L1: [[value1]]V",
			        "bullet": "round",
			        "bulletBorderAlpha": 1,
			        "bulletColor": "#FFFFFF",
			        "hideBulletsCount": 50,
			        "title": "L1",
			        "valueField": "value1",
			        "useLineColorForBulletBorder": true
			    },
			    {
			        "id": "g2",
			        "balloonText": "L2: [[value2]]V",
			        "bullet": "round",
			        "bulletBorderAlpha": 1,
			        "bulletColor": "#FFFFFF",
			        "hideBulletsCount": 50,
			        "title": "L2",
			        "valueField": "value2",
			        "useLineColorForBulletBorder": true
			    },
			    {
			        "id": "g3",
			        "balloonText": "L3: [[value3]]V",
			        "bullet": "round",
			        "bulletBorderAlpha": 1,
			        "bulletColor": "#FFFFFF",
			        "hideBulletsCount": 50,
			        "title": "L3",
			        "valueField": "value3",
			        "useLineColorForBulletBorder": true
			    } 
			    ],
			    "chartScrollbar": {
			        "autoGridCount": true,
			        "graph": "g1",
			        "scrollbarHeight": 40
			    },
			    "chartCursor": {
					"categoryBalloonDateFormat":"DD.MM.YYYY JJ:NN",
					"limitToGraph":"g1"
			    },
			    "categoryField": "date",
			    "categoryAxis": {
			        "parseDates": true,
			        "minPeriod": "mm",
			        "axisColor": "#DADADA",
			        "dashLength": 1,
			        "minorGridEnabled": true
			    },
			    "export": {
			        "enabled": true
			    }
			});
 
		</script>
	</body>
</html>
<?php
 
}
 
 
mysqli_close ( $link );
 
?>

Hier gibt’s nochmal den C-Source und das PHP-Script incl. der benötigten Odometer-Library:
powermeter.zip

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.