Elektronischer Gaszähler

Aus Bennys Wiki
Wechseln zu: Navigation, Suche

Die Idee[Bearbeiten]

Die Grundidee war den Gasverbrauch über den Tag von einem mechanischen Zähler auszulesen und von einem kleinen Rechner (ThinClient) oder Router grafisch aufbereiten zu lassen um eine Statistik über den Monat hinweg zu erhalten. Die Optische-Schaltung macht sich zu nutze, dass die im Gaszähler kleinste Stelle anstatt der 0 ein reflektierendes Material enthält. Die Veränderung der Intensität des reflektierenden Lichtes, kann durch einen Reflexkoppler ausgelesen und zur späteren Verarbeitung elektronisch aufbereitet werden. Die Auswertung erfolgt über den unbenutzen Parallelport eines ThinClients, welcher nur sehr wenig Strom verbraucht.

Eingesetzte Bauteile[Bearbeiten]

  • Einen ThinClient der Marke Igel (128 MB RAM, 4 GB Compact Flash als Festplatte) mit einem Debian Linux
  • Diverse Bauteile für den Bau der Leseeinheit
  • Ein Steckernetzteil mit ca 12 Volt zum Betrieb der Leseeinheit
  • Einen Stecker für den Parallelport und entsprechende Kabel zum Verbinden der Leseeinheit mit dem Computer

Technische Umsetzung der Leseeinheit[Bearbeiten]

Die Leseeinheit wurde wie im Forum nach dem Schaltplan von Martin J. beschrieben auf einer kleinen Lochrasterplatine wie in nachfolgender Abbildung dargestellt aufgebaut.

Der Schaltungsaufbau von Martin J.

Der Reflexkoppler (CNY70) dient zum Einlesen der Werte und wird direkt auf dem Gaszähler über der kleinsten dargestellten Stelle angebracht. Am einfachsten ist den Reflexkoppler auf z.B. einer passgenau zugeschnittenen, ausgedienten EC oder Kreditkarte anzubringen. Die Karte sollte an der passenden Stelle (kleinsten dargestellten Stelle des Gaszählers) mit einem Loch versehen sein, worauf der Reflexkoppler angebracht wird. Das bringt den Vorteil mit sich das die Elektronik gegen Einstrahlung von anderen Lichtquellen geschützt ist und problemlos vom Zähler abgenommen oder wieder angebracht werden kann. Wenn die Reflexkoppler justiert ist, sollte die Einheit über den regelbaren Wiederstand so justiert werden, das die auf der Schaltung befindliche LED bei jedem Durchlauf des reflektierenden Bereich des Gaszählers kurz leuchtet (ca. 1 Sekunde) und danach wieder erlischt, bis zum nächsten Durchlauf. Die LED sollte nicht flackern, sondern durchgängig leuchten, da sonst falsche Werte erfasst werden. Des weiteren ist es wichtig beim Anbringen des Reflexkoppler darauf zu achten, das dieser möglichst direkt auf den reflektierenden Teil des Gaszählers "schaut".

Verbinden der Schaltung mit dem PC[Bearbeiten]

Die Ausgänge 4 und 5 des Optokoppler (CNY17) wurden mit dem Pin 11 welchem ein Vorwiederstand von ca 150 kOhm vorgeschaltet ist und Pin 25 des Parallelports verbunden. Pin 25 dient als Masse und Pin 11 (BUSY) dient als Eingang, um am PC per Software die Daten auslesen zu können.

Die Software[Bearbeiten]

Einlesen der Impulse[Bearbeiten]

Die Software bereitet zunächst die parallele Schnittstelle für das einlesen von Werten vor. Dann wird ggf. eine Sqlite Datenbank an der Stelle FILE_NAME erstellt und initialisiert. Die eigentliche Arbeitet erledigt der Code in der main Sektion beginnen ab der While Schleife, welcher auf ein LOW Pegel auf der Schnittstelle am Port 11 wartet, dann 80000 Mikrosekunden wartet und dann wieder auf einen HIGH Pegel prüft und wieder 80000 Mikrosekunden wartet. Wenn ein solches Event erkannt wurde, wird in die Sqlite Datenbank und in das Logfile counter.log die aktuelle Unix-Timestamp geschrieben. Der Dienst sollte zu Systemstart als Hintergrundprozess gestartet werden.

#include <sys/io.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <time.h>
#include <sqlite3.h>

#define PARL    0x378
#define FILE_NAME "/var/www/htdocs/wohnung/gas_counter.sqlite"

#define SQL_STMT_BUFFER 60

/*
 ** The database table has a structure like this
 */
static const char schema[] =
"CREATE TABLE time(\n"
"  id INTEGER PRIMARY KEY ASC,\n"
"  time long\n"
")"
;

void writeTimestamp(long time) 
{
	sqlite3 *db;
	sqlite3_stmt *pStmt = 0;
	int rc = SQLITE_OK;
	
	rc = sqlite3_open(FILE_NAME, &db);
	if (SQLITE_OK != rc){
		printf("ERROR: Could not open db [%d].\n", rc);
		return;
	}
	
	char buffer[SQL_STMT_BUFFER];
	sprintf (buffer, "INSERT INTO time (\"time\") VALUES (\"%ld\")",time);
	
	rc = sqlite3_prepare(db, buffer, SQL_STMT_BUFFER, &pStmt, NULL);
	if (SQLITE_OK != rc){
		printf("ERROR: Could not insert something into the db [%d].\n", rc);
		rc = sqlite3_prepare(db, schema, sizeof(schema), &pStmt, NULL);
		if (SQLITE_OK != rc){
			printf("! Could not create the table structure\n");
			return;
		}
	}
	
	rc = sqlite3_step(pStmt);
	if (SQLITE_OK != rc && SQLITE_DONE != rc) {
		printf("ERROR: Could not exectute excuteing [%d].\n", rc);	
	}
	
	rc = sqlite3_finalize(pStmt);
	rc = sqlite3_close(db);
}

void ex_program(int sig) {
//	printf("Wake up call ... !!! - Catched signal: %d ... !!\n", sig);
	(void) signal(SIGINT, SIG_DFL);
	iopl (0); /* no I/O permission */
	exit(1);

}

int main(int argc, char **argv)
{
	(void) signal(SIGINT, ex_program);
	unsigned char a = 0;
	unsigned int lastSignal = 0;
	time_t now;
	FILE *file;
	if (geteuid() != 0)
	{
		printf ("\a\n\nError: $EUID==%d!=0 (you are not a superuser).\n\n", getuid());
		exit(-EPERM);
	}

	if (ioperm(PARL,1,1))
		fprintf(stderr, "Couldn't get the port at %x\n", PARL), exit(1);

	iopl(3); /* unlimited I/O access permission, nessesary above the 0x3ff limit e. g. at 0x9800=38912 */
	outb(0x04, PARL + 2); /* write mode, interrupt disable, all controll pins high */
	outb(0xb7, PARL + 1); // try to set the status pins high
	outb(0x00, PARL); // write 0x00
	outb(0x24, PARL + 2); /* read mode */

	while (1) {
		// wait for open switch
		while (inb(PARL + 1) == 254)
			usleep(80000);
		// wait that switch closes
		while (inb(PARL + 1) == 126)
			usleep(80000);
		// now check if we have todo something
		a = inb(PARL + 1);
		time(&now);
		// some buffer to remove peaks
		if (lastSignal + 5 < (int)now) {
			lastSignal = (int)now;
			printf ("%i\n", lastSignal);
			writeTimestamp(lastSignal);
			file = fopen("/var/www/htdocs/wohnung/counter.log","a+"); /* append file (add text to a file or create a file if it does not exist.*/
			fprintf(file,"%i\n", lastSignal); /*writes*/
			fclose(file); /*done!*/
		}
	}

	return 0;
}

Die Weboberfläche[Bearbeiten]

Screenshot der Weboberfläche

Die grafische Aufbereitung erfolgt im Prinzip auf dem Client, weil die Google BarChart API verwendet wird. Dieser werden die Werte aus der Sqlite Datenbank und der dazugehörige Zeitraum mitgeteilt. Für eine korrekte Berechnung sollte der $ppKwStd, welcher die dynamischen und fixen Kosten pro Kilowatt Stunde enthält angepasst werden. Der $kwFaktor lässt sich anhanden der auf dem Zähler angegebenen m³ Umrechnung ausrechnen (FIXME: BEISPIELE FÜR ALLES). Als letztes sollte der aktuelle Zählerstand vor der Installation erfasst werden, so das man zu einem späteren Zeitpunkt prüfen kann, ob die Werte welcher der Rechner erfasst hat mit dem realen Zählerstand übereinstimmen.

<?php

$counterOffset = 14679.404;
$kwFaktor = 10.675;
$ppKwStd = 6.33; // betrag in Cent

?>
<html>
<head>
<title>Wohnung</title>
<meta http-equiv="expires" content="Sat, 01 Dec 2001 00:00:00 GMT">
<!-- <meta http-equiv="refresh" content="30"> -->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> 
<link href="csschart.css" rel="stylesheet" type="text/css" media="screen" />
    <script type="text/javascript" src="http://www.google.com/jsapi"></script>
    <script type="text/javascript">
      google.load("visualization", "1", {packages:["areachart"]});
//      google.load("visualization", "1", {packages:["barchart"]});
      google.setOnLoadCallback(drawChart);
      function drawChart() {
        var dataMonth = new google.visualization.DataTable();
        dataMonth.addColumn('string', 'Tag');
        dataMonth.addColumn('number', 'Verbrauch (in kWh)');

        var dataToday = new google.visualization.DataTable();
        dataToday.addColumn('string', 'Stunde');
        dataToday.addColumn('number', 'Verbrauch Heute (in kWh)');
        dataToday.addColumn('number', 'Verbrauch Gestern (in kWh)');

<?php

/*
if (!file_exists("counter.log"))
	die("No counter logfile");
$data = file("counter.log");
$linesInFile = sizeof($data);
*/

$year = date("Y");
$month = date("m");
$day = date("d");

$tstampToday = mktime(0,0,0,$month,$day,$year);
$tstampYesterday = $tstampToday - (60*60*24);
$tstampMonth = mktime(0,0,0,$month,1,$year);
$tstampLastDayOfMonth = mktime(0,0,0,$month+1,1,$year)-(60);

try {
	/*** connect to SQLite database ***/
	$dbh = new PDO("sqlite:/var/www/htdocs/wohnung/gas_counter.sqlite");

	$sql = "SELECT COUNT(time) AS 'time' FROM time";
	$stmt = $dbh->query($sql);
	$result = $stmt->fetch(PDO::FETCH_ASSOC);
	$eventCounter = $result['time'];

	$sql = "SELECT COUNT(time) AS 'time' FROM time WHERE time >= $tstampToday";
	$stmt = $dbh->query($sql);
	$result = $stmt->fetch(PDO::FETCH_ASSOC);
	$vToday = $result['time'];

	$sql = "SELECT COUNT(time) AS 'time' FROM time WHERE time >= $tstampYesterday AND time <= $tstampToday";
	$stmt = $dbh->query($sql);
	$result = $stmt->fetch(PDO::FETCH_ASSOC);
	$vYesterday = $result['time'];

	$sql = "SELECT COUNT(time) AS 'time' FROM time WHERE time >= $tstampMonth";
	$stmt = $dbh->query($sql);
	$result = $stmt->fetch(PDO::FETCH_ASSOC);
	$vMonth = $result['time'];

for ($i=$tstampMonth; $i<$tstampLastDayOfMonth; $i+=(60*60*24)) {
	$curDay = date("Y-m-d",$i);
	$nextI = $i+(60*60*24);
	$sql = "SELECT COUNT(time) AS 'time' FROM time WHERE time >= $i AND time <= $nextI";
	$stmt = $dbh->query($sql);
	$result = $stmt->fetch(PDO::FETCH_ASSOC);
	$sMonth[$curDay] = $result['time']/100*$kwFaktor;
}

for ($i=$tstampToday; $i<$tstampToday+(60*60*24); $i+=(60*60)) {
	$curHour = date("H",$i);
	$nextI = $i+(60*60);
	$sql = "SELECT COUNT(time) AS 'time' FROM time WHERE time >= $i AND time <= $nextI";
	$stmt = $dbh->query($sql);
	$result = $stmt->fetch(PDO::FETCH_ASSOC);
	$sDay[$curHour] = $result['time']/100*$kwFaktor;
}

for ($i=$tstampYesterday; $i<$tstampYesterday+(60*60*24); $i+=(60*60)) {
	$curHour = date("H",$i);
	$nextI = $i+(60*60);
	$sql = "SELECT COUNT(time) AS 'time' FROM time WHERE time >= $i AND time <= $nextI";
	$stmt = $dbh->query($sql);
	$result = $stmt->fetch(PDO::FETCH_ASSOC);
	$sYesterday[$curHour] = $result['time']/100*$kwFaktor;
}
	$dbh = null;
}
catch(PDOException $e)
{
	echo $e->getMessage();
}

$cCounter = $counterOffset + ($eventCounter/100);
$cToday = $vToday/100*$kwFaktor;
$cYesterday = $vYesterday/100*$kwFaktor;
$cMonth = $vMonth/100*$kwFaktor;


$cPToday = round($cToday * $ppKwStd, 2);
$cPYesterday = round($cYesterday * $ppKwStd, 2);
$cPMonth = round(($cMonth * $ppKwStd)/100, 2);

echo "dataMonth.addRows(".count($sMonth).");\n";
$counter = 0;
foreach ($sMonth as $key=>$val) {
        echo "dataMonth.setValue($counter, 0, '".$key."');\n";
        echo "dataMonth.setValue($counter, 1, ".$val.");\n";
        $counter++;
}

echo "dataToday.addRows(".count($sDay).");\n";
$counter = 0;
foreach ($sDay as $key=>$val) {
        echo "dataToday.setValue($counter, 0, '".$key."');\n";
        echo "dataToday.setValue($counter, 1, ".$val.");\n";
        $counter++;
}

$counter = 0;
foreach ($sYesterday as $key=>$val) {
        echo "dataToday.setValue($counter, 2, ".$val.");\n";
        $counter++;
}

?>
       var chartToday = new google.visualization.AreaChart(document.getElementById('chart_div_today'));
        chartToday.draw(dataToday, {width: 900, height: 240, is3D: false, title: 'Tagesstatistiken'});

       var chartMonth = new google.visualization.AreaChart(document.getElementById('chart_div_month'));
        chartMonth.draw(dataMonth, {width: 900, height: 240, is3D: false, title: 'Monatsstatistiken'});
      }
    </script>

</head>

<body bgcolor="white" text="black">
<?php

echo "Zählerstand: {$cCounter}<br /><br />\n";
echo "Verbrauch Heute: ".round($cToday, 2)." in kWh ({$cPToday} Cent)<br />\n";
echo "Verbrauch Gestern: ".round($cYesterday, 2)." in kWh ({$cPYesterday} Cent)<br />\n";
echo "Verbrauch Monat: ".round($cMonth, 2)." in kWh ({$cPMonth} Euro)<br /><br />\n";

?>
    <div id="chart_div_today"></div>
    <div id="chart_div_month"></div>

</body>
</html>

Verbesserungsvorschläge und alternative Ideen[Bearbeiten]

Alte Maus als Leseeinheit[Bearbeiten]

Ein Bekannter hatte die Idee anstatt der selbst gebauten elektronischen Schaltung, eine alte Maus mit einem Mausball zu verwenden, welche per seriellem oder PS/2 Anschluss angebunden ist. Die Mauskugel im inneren treibt üblicherweise zwei Gabellichtschranken (jeweils für eine Achse) an, welche evtl. durch Modifikation zum Auslesen des reflektierenden Bereichs am Zähler geeignet sind. Für diese Lösung liegen keine Erfahrungen vor und sie müsste erst getestet werden.

Auswertung über einen Mikrocontroller[Bearbeiten]

Anstatt die oben vorgeschlagene Schaltung aufzubauen, könnte man den CNY70 direkt an einem A/D Port eines Mikrocontrollers anschließen, was den Stromverbrauch der Schaltung deutlich senken würde. Des weiteren hätte es den Vorteil, dass der Mikrocontroller die Werte zwischenspeichern könnte und nur auf Anfrage des Rechners, z.B. über die serielle oder USB Schnittstelle zur Verfügung stellen könnte. Dem Mikrocontroller müsste für diese Funktionalität die korrekte Uhrzeit bekannt sein, welche zum Beispiel über ein RTC Modul zur Verfügung gestellt werden kann.

Erweiterung der Funktionalität der Weboberfläche[Bearbeiten]

Die Weboberfläche könnte als statische html Seite auskommen, welche die Informationen über das JSON Format in komprimierter Form (gzip) von einem php Skript erhält. Dadurch wäre es möglich verschiedene Wertebereiche abzudecken, welche beliebig feingranular von dem PHP Skript aufbereitet werden. Dem PHP Skript, wird dazu der jeweilige Zeitraum und die Granularität mitgeteilt, welches dann die jeweiligen Daten im JSON Format aufbereitet.

Referenzen[Bearbeiten]

Allgemein[Bearbeiten]

Details zur Leseeinheit[Bearbeiten]