Skip to content

S0 Zähler mit ConfigurableFirmata

Christian edited this page Oct 23, 2018 · 3 revisions

S0-Zähler mit ConfigurableFirmata

Dieser Artikel beschreibt die Einbindung eines S0-Zählers in FHEM über das Modul FRM_IN.

Voraussetzungen

Konfiguration

Hardware

  • S0+ am Zähler wird mit einem digitalen Pin des Arduino verbunden.
  • S0- am Zähler wird mit Masse (GND) am Arduino verbunden.
  • Der Arduino wird mit dem Host-Rechner verbunden.

Arduino

  • ConfigurableFirmata oder RCSwitchFirmata in der Arduino IDE öffnen
  • Firmata-Features DigitalInputFirmata und FirmataExt einschalten:
#include <utility/DigitalInputFirmata.h>
DigitalInputFirmata digitalInput;

#include <utility/FirmataExt.h>
FirmataExt firmataExt;
  • Sketch auf den Arduino hochladen

FHEM

Zähler

Definition eines Gerätes counter, das die Signale des Stromzählers zählt und eines Logs log_counter, das den Stromverbrauch in einem Log speichert:

define log_counter FileLog %L/counter_%Y-%m.log counter.*power:.*
define firmata FRM /dev/ttyUSB0@57600
define counter FRM_IN 11
attr   counter internal-pullup on
attr   counter count-mode falling
attr   counter event-min-interval power:1
attr   counter oldreadings time
attr   counter stateFormat power
attr   counter userReadings\
                   time:reading:.off  { Time::HiRes::time() },\
                   power:time:.+ { sprintf("%.2f W", 3600 / (ReadingsVal('counter', 'time', undef) - OldReadingsVal('counter', 'time', undef))) }
Attribut internal-pullup

Dieses Attribut ist notwendig, wenn Zähler und Arduino wie oben beschrieben verbunden werden. Andernfalls ist das Signal hochohmig ("floating") und kann willkürlich zwischen LOWund HIGH hin- und herspringen.

Attribut event-min-interval

Dieses Attribut begrenzt den Zähler auf 1 Zählvorgang pro Sekunde. Das entspricht bei 1 Signal pro 1 Wh einem Verbrauch von 3,6 kW. Dadurch können ungewollte Fehlmessungen verhindert werden.

userReading time

Das userReading time reagiert auf FHEM-Benachrichtigungen, die auf den regulären Ausdruck reading:.off passen (. steht für 1 beliebiges Zeichen, also z.B. auch 1 Leerzeichen). Diese Benachrichtigungen werden vom Firmata-Modul versendet, sobald vom Arduino ein LOW-Signal (0 V) eintrifft. HIGH-Signale hingegen werden ignoriert. Im Sketch ConfigurableFirmata wird ein LOW immer nur gesendet, wenn der vorige Wert HIGH war, das entspricht also einer fallenden Flanke.

Time::HiRes::time() ruft den aktuellen Zeitstempel Mikrosekunden-genau vom System ab. Dieser Wert wird im Reading gespeichert.

Die Zeile attr counter oldreadings time bewirkt, dass für time nicht nur auf den aktuellen Wert, sondern auch auf den vorigen Wert zugegriffen werden kann.

userReading power

Das userReading power reagiert auf Änderungen von time. Der Wert für power wird wie folgt berechnet:

  • OldReadingsVal('counter', 'time', undef): den vorigen Wert des userReadings time laden, das ergibt also den Zeitstempel der letzten fallenden Flanke.
  • Den aktuellen Wert des userReadings time laden und die Differenz bilden; damit haben wir die Dauer zwischen zwei fallenden Flanken in Sekunden, z.B. 2.345
  • Berechnung des Stromverbrauchs. Der Stromzähler gibt 1 Signal pro Wh ab, also 1000 Signale pro kWh; eine Stunde hat 3600 Sekunden. Aus der gemessenen Dauer (x Sekunden) berechne ich die momentane Leistung p in W:
    p W = 1 Wh / x s
<=> p W = 3600 Wh / x * 3600s
<=> p W = 3,6 kWh / x * 1h
<=> p W = 3,6 kW / x
<=> p W = 3600 W / x
<=> p = 3600 / x
  • sprintf("%.2f", ...: Das Ergebnis wird bei 2 Nachkommastellen abgeschnitten und in einen String umgewandelt.
  • " W": Einheit ergänzen (falls man später vielleicht mal auf kW umstellt)

Diagramm

Definition eines Diagramms unter Verwendung der Datei www/gplot/my_leistung_energie.gplot (siehe unten):

define counter_diagram weblink fileplot log_counter:my_leistung_energie:CURRENT
attr   counter_diagram title "Stromverbrauch"
attr   counter_diagram label "Leistung (W)"::"Energie (Wh)"
attr   counter_diagram plotfunction 4:counter.power::$fld[3]>3600?0:$fld[3] 4:counter.power::$cnt[0]
Attribut plotfunction

Das erste Argument 4:counter.power::$fld[3]>3600?0:$fld[3] beschreibt die Darstellung der Leistung. Ausgewertet wird der Wert in Spalte 4 des Logfiles in allen Zeilen, die den regulären Ausdruck counter.power erfüllen. Dargestellt wird dieser Wert ($fld[3]), sofern er höchstens 3600 ist, andernfalls 0.

Das zweite Argument 4:counter.power::$cnt[0] beschreibt die Darstellung der Energie. Der dargestellte Wert ist hier nicht der Wert aus dem Log, sondern die Summe der Vorkommnisse von Zeilen mit counter.power. Da der Stromzähler pro verbrauchter Wattstunde ein Signal abgibt, entspricht jede Zeile im Log einer Wattstunde. Die verbraucht Energie entspricht also der Summe aller Zeilen.

Achtung: Ein Argument von plotfunction darf keine Leerzeichen enthalten!

Modifikationen

Andere Zähler

Dieser Artikel basiert auf einem Stromzähler, der 1 Signal pro Wattstunde abgibt. Alternativ können

  • Stromzähler mit anderen Frequenzen, oder
  • andere Zähler, z.B. für Gas oder Wasser eingesetzt werden.

Raspberry Pi

Anstelle eines Arduinos ist ebenso ein Raspberry Pi mit dem Modul RPI_GPIO denkbar.

Vorteile:

  • FHEM kann auch auf dem Raspberry Pi laufen
  • Alle Pins unterstützen Interrupts

Nachteile:

  • Je nach Auslastung reagiert ein Raspberry Pi nicht sofort auf Signale, sodass die Zeitstempel verfälscht sein können

Beispiel-Konfiguration:

define counter RPI_GPIO 26
attr   counter direction input
attr   counter pud_resistor up
attr   counter interrupt falling
attr   counter event-min-interval power:1
attr   counter stateFormat time
attr   counter userReadings\
                   time:reading:.off  { Time::HiRes::time() },\
                   power:time:.+ { sprintf("%.2f W", 3600 / (ReadingsVal('counter', 'time', undef) - OldReadingsVal('counter', 'time', undef))) }

Bekannte Probleme

Ausreißer

Es kann passieren, dass ein FHEM-Modul die Messung eines S0-Signals verzögert. Es erscheint dann später im Log, und Berechnungen, die auf dem Zeitstempel basieren (z.B. die Ermittlung des Stroms (W) aus der Energie (Wh)) werden verfälscht. Ein Extrembeispiel ist ein Backup von FHEM im laufenden Betrieb. Eine Fritz!Box 7390 hat in einem Test dafür ca. 20 Minuten gebraucht. Einige S0-Signale wurden gepuffert und nach 20 Minuten verarbeitet (mit den verspäteten Zeitstempeln). Die meisten Signale sind aber verloren gegangen. Auch das Zeichnen von Diagrammen kann eine Verzögerung von einigen Sekunden bedeuten.

Verbindungsprobleme

  • USB: Ist das Kabel zwischen Arduino und Host-Rechner zu lang und/oder nicht ausreichend geschirmt, kann es zu Verbindungsabbrüchen kommen.
  • Ethernet: Durch die Ethernet-Unterstützung wird der Arduino-Sketch leicht sehr groß. Wenn zu wenig freier Speicher zur Verfügung steht, kann der Arduino sich instabil verhalten. Zu beachten ist, dass sowohl IP- als auch MAC-Adresse korrekt eingetragen sind und die Verbindung zwischen Arudino und Host-Rechner nicht durch eine Firewall o.ä. verhindert wird. Befindet sich ein Router zwischen den Geräten, kann ein Portforwarding-Eintrag notwendig sein.

Links

Praxis-Beispiel

Die folgende Konfiguration beschreibt Auslesen, Darstellung und Überwachung von 12 Stromzählern, die über einen Arduino Mega2560 an einer Fritz!Box 7390 angeschlossen sind.

Datei fhem.cfg

##
# Firmata Basis-Modul für die Kommunikation mit dem Arduino
##
define firmata FRM /dev/ttyACM0@57600
attr firmata room Zähler

##
# 1 Log pro Tag, für alle Zähler zusammen
##
define log_zaehler FileLog %L/zaehler_%Y-%m-%d.log counter_.*power:.*
attr log_zaehler room Zähler

##
# 1 Diagramm je Zähler und 1 Diagramm für den gesamten Haushalt
##
define erzeugung_zaehler notify erzeugung_zaehler {\
 createCounter("kueche",      23, "Küche",         "log_zaehler", "Bereiche", "02");;\
 createCounter("og",          25, "Obergeschoss",  "log_zaehler", "Bereiche", "04");;\
 createCounter("eg",          27, "Erdgeschoss",   "log_zaehler", "Bereiche", "01");;\
 createCounter("dg",          29, "Dachgeschoss",  "log_zaehler", "Bereiche", "05");;\
 createCounter("ug",          31, "Keller",        "log_zaehler", "Bereiche", "06");;\
 createCounter("heizung",     33, "Heizung",       "log_zaehler", "Geräte",   "11");;\
 createCounter("waschen",     35, "Waschmaschine", "log_zaehler", "Geräte",   "09");;\
 createCounter("trocknen",    37, "Trockner",      "log_zaehler", "Geräte",   "10");;\
 createCounter("haustechnik", 39, "Haustechnik",   "log_zaehler", "Geräte",   "12");;\
 createCounter("garten",      41, "Garten",        "log_zaehler", "Bereiche", "08");;\
 createCounter("spuelen",     43, "Spülmaschine",  "log_zaehler", "Geräte",   "03");;\
 createCounter("bad",         45, "Bäder",         "log_zaehler", "Bereiche", "07");;\
 createWeblink("haushalt", "counter_.*", "Haushalt", "log_zaehler", "Haushalt", "Haushalt");;\
}
attr erzeugung_zaehler room Zähler
trigger erzeugung_zaehler


##
# Aktueller Stromverbrauch
##
define zaehler_haushalt_jetzt Dummy
attr   zaehler_haushalt_jetzt room Aktuell
attr   zaehler_haushalt_jetzt alias Stromverbrauch (aktuell) 
define zaehler_haushalt_jetzt_erzeugung at +*00:01 { my $power = 12 * countLogEntries("300", "log_zaehler", "4:::");; fhem("set zaehler_haushalt_jetzt $power W") }
attr   zaehler_haushalt_jetzt_erzeugung room Zähler

##
# Zähler für den Energieverbrauch des gesamten Haushaltes
##
define energie_haushalt Dummy
attr   energie_haushalt room Zähler

##
# Log für den Energieverbrauch des gesamten Haushaltes
##
define log_energie FileLog %L/energie_%Y.log energie_haushalt.*
attr   log_energie room Zähler

##
# Ermittlung des Energieverbrauches pro Tag für den gesamten Haushalt
##
define ermittlung_energie_haushalt_tag at *23:59:00 { my $c = countLogEntries("86400", "log_zaehler", "4:::");; fhem("set energie_haushalt day $c Wh") }
attr   ermittlung_energie_haushalt_tag room Zähler

##
# Diagramm zur Anzeige des Energieverbrauches pro Tag für den gesamten Haushalt
##
define weblink_energie_haushalt_tag SVG log_energie:my_energie:CURRENT
attr   weblink_energie_haushalt_tag alias Diagramm-Stromverbrauch-Tag
attr   weblink_energie_haushalt_tag room Monatsübersicht
attr   weblink_energie_haushalt_tag fixedrange month
attr   weblink_energie_haushalt_tag label "Energie (Wh) | ⌀ $data{avg1}"
attr   weblink_energie_haushalt_tag title "Haushalt"
attr   weblink_energie_haushalt_tag plotfunction 4:energie_haushalt.day::

##
# Ermittlung des Energieverbrauches pro Woche für den gesamten Haushalt
##
define ermittlung_energie_haushalt_woche at *23:59:10 { if ($wday==0) { my $c = sumLogEntries("604800", "log_energie", "4:energie_haushalt.day::");; fhem("set energie_haushalt week $c Wh") } }
attr   ermittlung_energie_haushalt_woche room Zähler

##
# Automatischer Reset von Firmata, falls die Verbindung zum Arduino dauerhaft verloren geht
##
define firmata_disconnected_listener notify firmata:DISCONNECTED.* { Log(3, "firmata was disconnected, waiting 15 seconds for automatic reset...");; fhem "define firmata_reconnector at +00:00:15 trigger firmata_reconnect";; fhem "attr firmata_reconnector room Zähler" }
attr   firmata_disconnected_listener room Zähler
define firmata_reconnect notify firmata_reconnect { my $v = Value("firmata");; if ($v eq "disconnected") { Log(3, "firmata is still disconnected, now resetting");; fhem "set firmata reset" } else { Log(3, "No need to reset firmata: state is <$v>");; } }
attr   firmata_reconnect room Zähler
Erläuterung

zaehler_haushalt_jetzt_erzeugung berechnet den Durchschnittsverbrauch (in Watt) der letzten 5 Minuten. Beispiel: Wir nehmen einen Verbrauch von 500 W an. In einer Stunde wären das 500 Wh. In 5 Minuten kommen 500/12=41,66 Wh zusammen. Für jede Wattstunde steht eine Zeile im Log, für die Dauer von 5 Minuten also 41 Zeilen. zaehler_haushalt_jetzt_erzeugung zählt die Anzahl der Zeilen der letzten 300 Sekunden (5 Minuten) und rechnet die auf 1 Stunde hoch (*12). Damit kommt man auf 60*12=492. Das ist also der durchschnittliche Verbrauch der letzten 5 Minuten. Aktualisiert wird er jede Minute.

Datei FHEM/99_myUtils.pm

##############################################
# $Id: 99_myUtils.pm $
package main;

use strict;
use warnings;
use POSIX;

sub myUtils_Initialize() {
  my ($hash) = @_;
}

sub createCounter {
  my ($device_id, $pin, $name, $logdevice, $plotroom, $sortby) = @_;
  my $counter_id = "counter_$device_id";

  fhem "define $counter_id FRM_IN $pin";
  fhem "attr   $counter_id internal-pullup on";
  fhem "attr   $counter_id count-mode falling";
  fhem "attr   $counter_id event-min-interval power:1";
  fhem "attr   $counter_id oldreadings time";
  fhem "attr   $counter_id userReadings time:reading:.off { Time::HiRes::time() }, power:time:.+ { sprintf('%.2f W', 3600 / (ReadingsVal('$counter_id', 'time', undef) - OldReadingsVal('$counter_id', 'time', undef))) }";
  fhem "attr   $counter_id stateFormat { getCounterStateFormat('$counter_id') }";
  fhem "attr   $counter_id alias Stromzähler $name";
  fhem "attr   $counter_id room Zähler";
  
  createWeblink($device_id, $counter_id, $name, $logdevice, $plotroom, $sortby);
}

sub getCounterStateFormat {
  my ($device) = @_;
  my $reading = 'power';
  my $value = sprintf("%.0f", ReadingsNum($device, $reading, undef));
  my $age   = sprintf("%.0f", ReadingsAge($device, $reading, undef) / 60);
  return "< 1 W" if ($age >= 60);
  return "$value W" if ($age < 5);
  return sprintf("< %.0f W", 60 / $age);
}

sub createWeblink {
  my ($device_id, $counter_id, $name, $logdevice, $room, $sortby) = @_;
  my $diagram_id = "weblink_$device_id";

  fhem "define $diagram_id weblink fileplot $logdevice:my_leistung_energie:CURRENT";
  fhem "attr $diagram_id alias Diagramm-$name";
  fhem "attr $diagram_id sortby $sortby";
  fhem "attr $diagram_id room $room";
  fhem "attr $diagram_id title \"$name\"";
  fhem "attr $diagram_id label \"Leistung (W)\"::\"Energie (Wh)\"";
  fhem "attr $diagram_id plotfunction 4:$counter_id.power::\$fld[3]>3600?0:\$fld[3] 4:$counter_id.power::\$cnt[0]";
}

sub countLogEntries {
 my ($offset, $logfile, $cspec) = @_;
 my $count = getLogEntries($offset, $logfile, $cspec);
 return $count;
}

sub sumLogEntries {
 my ($offset, $logfile, $cspec) = @_;
 my @entries = getLogEntries($offset, $logfile, $cspec);
 my $sum = 0;
 foreach (@entries) {
  my @line = split(" ", $_);
  if (defined $line[1] && "$line[1]" ne "") {
   $sum += $line[1];
  }
 } 
 return $sum;
}

sub getLogEntries {
 my ($offset, $logfile, $cspec) = @_;
 my $start = strftime("%Y-%m-%d_%H:%M:%S", localtime(time()-$offset));
 my $end   = strftime("%Y-%m-%d_%H:%M:%S", localtime());
 my $loglevel = $attr{global}{verbose};
 $attr{global}{verbose} = 0; 
 my @logdata = split("\n", fhem("get $logfile - - $start $end $cspec"));
 $attr{global}{verbose} = $loglevel;
 return @logdata;
}

1;

Datei www/gplot/my_leistung_energie.gplot

set terminal size <SIZE>
set title '<TL>'
set ylabel  "Leistung (W)"
set y2label "Energie (Wh)"

#FileLog <SPEC1>
#FileLog <SPEC2>

plot axes x1y1 title '<L1>' with lines ls l0,\
     axes x1y2 title '<L2>' with lines ls l1fill

Datei www/gplot/my_energie.gplot

set terminal size <SIZE>
set title '<TL>'
set ylabel "Energie (Wh)"
set yrange [0:]
set y2range [0:]

#FileLog <SPEC1>

plot axes x1y2 title '<L1>' with lines ls l1fill