smartHome Eigenbau – Projekt Spezifikationen (0.2)

letztes Update: 18.03.2016

Mit diesem Dokument will ich Projekt Spezifikationen festlegen und aufzuzeigen wie genau die Software miteinander arbeitet.
Deshalb versuche ich Schnittstellen zu definieren und Programmteile zu standardisieren.

Damit hoffe ich den ein oder anderen für die Mitarbeit an diesem Projekt gewinnen zu können.
Und sei es nur um kleinere Fehler zu beseitigen.

  1. Aufbau und Zusammenspiel
  2. Begriffsdefinitionen
  3. Datenbank Aufbau
  4. Abhängigkeiten
  5. json-Austausch-Format
  6. Einen neuen Typ von Schalter hinzufügen
  7. Eine neue Sprache hinzufügen

 

Aufbau und Zusammenspiel der smartHome System Komponenten

Unten abgebildet ist eine schematische Darstellung wie das smartHome Systems aufgebaut ist und wie die einzelne Programmteile miteinander zusammenspielen, bzw. kommunizieren.

Aufbau des smartHome Systems (v0.1)
Aufbau des smartHome Systems (v0.1)

Wie man sehen kann, besteht das System aus 3 x Teilen welche untereinander über das websocket-Protokoll miteinander kommunizieren.
Dabei spielt es keine Rolle ob alle Teile auf einem System laufen oder alle auf unterschiedlichen Systemen.

 

Begriffsdefinitionen

  • clients
    Darunter verstehe ich die Hardware der Schalter-/Sensorplattform.
    Das wird in der Regel ein Raspberry Pi sein, genauso gut kann es aber auch ein Intel Galileo Board sein, der Tinkerforge RED Brick oder auch ein ganz normaler PC.
  • (G)UI / html-frontend
    Hiermit ist die Weboberfläche des Systems gemeint.
    Diese wird mit Hilfe von html5, css, php und javascript programmiert.
    Dabei muss auf Responsive Design geachtet werden, damit die UI sowohl am Desktop PC, aber auch auf Tablets & Smartphones gut aussieht.
  • server
    Hiermit ist das python-Server-Script gemeint welches als Mittler zwischen den clients und dem html-frontend dient.
    Dazu macht das Script 2 x websocket-Server auf, einen für die clients und einen für die html-frontends.
  • database
    Darunter verstehe ich die Datenbank inklusiv dem Datenbank Schema.
    Diese sollte im Idealfall auf dem gleichen System wie der server laufen.

 

Datenbank-Aufbau

Unten abgebildet ist das Datenbank-Schema des smartHome Systems.

database schema 2.0

 

Abhängigkeiten / Software-Voraussetzungen

Folgende Software-Voraussetzungen müssen erfüllt sein:

server

client

datenbank

  • Die Datenbank Software mysql

UI / html-frontend

  • webserver mit php (apache/nginx)
  • html5 fähiger Browser (Chrome/Firefox/Safari)

json-Austausch-Format

Bei der Kommunikation zwischen client, server und html-frontend kommt das json-Format zum Einsatz.
Der json-String besteht dabei aus 4 x Feldern und baut sich wie folgt auf (im Beispiel meldet ein GPIO-Pin das sich sein Zustand geändert hat):

{
    "usage": "switch_changed_status",
    "ip": self.ip,
    "id": self.switch_id,
    "value": switch_to
}

Aus der folgenden Tabelle wird ersichtlich wie der Inhalt der einzelnen Felder bei bestimmten Aktionen definiert wird:

usage (string) ip (string) id (int) value (various)
sender
receiver
einen Schalter auslösen switch_turn 192.168.128.66 8 (switches.id) on server, gui client
einen Schalter abfragen switch_status 192.168.128.66 8 (switches.id) server, gui client
ein Schaltzustand übertragen switch_send_status 192.168.128.66 8 (switches.id) on client server, gui
ein Schaltzustand ändert sich switch_changed_status 192.168.128.66 8 (switches.id) off client server, gui
einen neuen Schaltvorgang anlegen timerswitch_new 47 (scheduler.id) gui server
einen Schaltvorgang löschen timerswitch_delete 47 (scheduler.id) gui server
einen Schaltvorgang ändern timerswitch_update 47 (scheduler.id) gui server
die Zeitschaltuhr neu starten timerswitch_restart gui server
einen Sensorwert abfragen sensor_status 192.168.128.69 12 (sensors.id) server, gui client
einen Sensorwert übertragen sensor_send_status 192.168.128.69 12 (sensors.id) 14,5 client server, gui
einen Sensorwert ändert sich sensor_changed_status 192.168.128.69 12 (sensors.id) 27,8 client server, gui

Einen neue Art von Schalter hinzufügen

Möchte man das System um eine neue Art von Schalter erweitern sind dazu 3 Schritte notwendig:

  1. es muss eine neue Klasse für diese Art von Schalter programmiert werden
  2. die Client-Software muss um einen kleinen Abschnitt erweitert werden
  3. für die Datenbank muss über das html-frontend ein neuer Typ von Schalter angelegt werden

Howto: neue Klasse für Schalter-Typen

Grundsätzlich muss für jede Art von Schalter eine entsprechende Klasse programmiert werden.
Ich werde das Ganze anhand der „lib_gpio.py“ erklären, über welche ich die GPIO-Pins schalte.

Diese wird später per import in die Client-Software geladen.

import lib_gpio

Jede „Schalter-Klasse“ muss über genau definierte öffentliche Funktionen/Methoden verfügen, welche als Schnittstelle zum restlichen Programm fungieren.

Als erstes müssen wir uns um die „__init__“-Methode kümmern.
Hier übergeben wir der Klasse wichtige Parameter.

def __init__(self, switch_id, ip, logging_daemon, queue):
    self.switch_id = switch_id
    self.ip = ip
    self._logging_daemon = logging_daemon
    self._queue = queue
    self._logging_daemon.info('RaspiGPIO ..... initialisiert')

Hier haben wir auch gleichzeitig ein Beispiel dafür welche Parameter immer übergeben werden müssen.

  • switch_id
    Die Datenbank-ID des Schalters.
  • ip
    Die IP des Hardware-Clients aus der config.py.
  • logging_daemon
    Damit die Klasse logging-Infos ausgeben kann.
  • queue
    Damit ist die websockets-Warteschlange gemeint und über diesen Weg geben die Schalter „Rückmeldung“ an die Server-Software.

Im Beispiel oben wird nicht mehr gemacht als die Parameter übergeben und eine Logging-Information ausgegeben.
Bei anderen Schalter können hier durchaus noch andere Befehle notwendig sein.
Schaut euch dazu einfach mal die anderen libs an, wie z.Bsp. lib_sispm.

Kommen wir zur wichtigsten Funktion „set_switch„, über welche der jeweilige Schalter letztendlich geschaltet wird.

def set_switch(self, switch_to, arg_a, arg_b, arg_c, arg_d):

    GPIO.setmode(GPIO.BOARD) 
    GPIO.setup(int(arg_a), GPIO.OUT)
    
    if switch_to:
        GPIO.output(int(arg_a), GPIO.HIGH)
    else:
        GPIO.output(int(arg_a), GPIO.LOW)

    self._logging_daemon.debug('RaspiGPIO ..... geschaltet Pin %s , SOLL = %s , IST = %s' % (arg_a, switch_to, self.status(arg_a)))

    tmp_json = json.dumps(["switch_changed_status", self.ip, self.switch_id, switch_to])

    for consumer in self._queue:
        consumer(tmp_json)
        self._logging_daemon.info('RaspiGPIO ..... Pin %s , send %s -> SocketServer Warteschlange ' % (arg_a, self.status(arg_a)))

Die Funktion hat immer die gleichen 5 x  Übergabeparameter:

  • switch_to
    Kann „True“ oder „False“ sein, je nachdem ob an- oder ausgeschaltet wird und wird immer gebraucht.
  • arg_aarg_b, arg_c, arg_d
    Diese Parameter enthalten notwendige Informationen zum jeweiligen Schalter.

Dabei kann es vorkommen, das eine neue Art von Schalter nur die Parameter „switch_to“ und „arg_a“ nutzt, trotzdem muss die Funktion alle(!) Parameter entgegen nehmen.
Dadurch  kann ich in der Client-Software den gleichen Aufruf zum schalten benutzen, egal um welche Art von Schalter es geht.

Im Code oben wird nur der Parameter „arg_a“ verwendet, da ich zur Identifizierung und zum Schalten von GPIO-Pins nur eine Angabe brauche – die Nummer des Pins.
Im Code zum schalten mit Hilfe des Tinkerforge RemoteSwitch (Funk) brauche ich dagegen alle 4 x Parameter um den genauen Funk-Code zu definieren.

Wie man im Beispiel sieht wird zuerst der entsprechende Pin als Ausgang gesetzt.

GPIO.setmode(GPIO.BOARD) 
GPIO.setup(int(arg_a), GPIO.OUT)

Im nächsten Schritt wird dann der Inhalt von „switch_to“ ausgewertet und je nach Inhalt der entsprechende Pin an- oder ausgeschaltet.

if switch_to:
        GPIO.output(int(arg_a), GPIO.HIGH)
    else:
        GPIO.output(int(arg_a), GPIO.LOW)

Jetzt kommen wieder ein wenig Logging-Informationen.
Die Ausgabe auf der Logging-Console ist optional, ich rate aber dazu.

Nun setzen wir den json-String zusammen um dem Server Rückmeldung zu geben.

tmp_json = json.dumps(["switch_changed_status", self.ip, self.switch_id, switch_to])

Der Aufbau des json-String folgt einem bestimmten Muster.
Siehe auch hier -> json-Austausch-Format

Schlussendlich übergeben wir den json-String dann an die websockets-Warteschlange zum senden an den Server.

for consumer in self._queue:
    consumer(tmp_json)
    self._logging_daemon.info('RaspiGPIO ..... Pin %s , send %s -> SocketServer Warteschlange ' % (arg_a, self.status(arg_a)))

Die Rückmeldung an den Server ist zwingend notwendig und muss in dem vorgegeben Format stattfinden.
Nur so kann in späteren Versionen ein Trigger eingebaut werden.
Am Ende kommen noch einmal Logging-Informationen. Deren Inhalt ist weitestgehend euch überlassen.

Damit sind alle notwendigen Methoden/Funktionen der Klassen besprochen.
Ansonsten kann die Klasse noch beliebig viele weitere Funktionen/Methoden haben.
Wichtig sind nur die drei oben.

Howto: Anpassung an der Client-Software bei neuen Schalter-Typen

Die Client-Software „sh-client.py“ muss nur an einer Stelle angepasst werden.
Und zwar in der Methode „get_switches()„, in welcher die einzelnen Schalter initialisiert werden.

Zur besseren Übersicht erst einmal die ganze Methode, mit derzeit 5 x verschiedenen Schaltertypen.

def get_switches():
    tinkerforge_connection = IPConnection()
    tinkerforge = False
    #
    # get a list of all switches for this client
    #
    sql = """SELECT 
          switches.id AS switches_id, 
          switches.title AS switches_title, 
          switches.argA, 
          switches.argB, 
          switches.argC, 
          switches.argD, 
          switch_types.title AS switches_typ 
          FROM switches, switch_types, clients 
          WHERE clients.ip = %s 
          AND switches.switch_types_id = switch_types.id 
          AND switches.clients_id = clients.id"""
    mysql_cursor.execute(sql, str(DEVICE_IP))
    results = mysql_cursor.fetchall()
    for result in results:
        switches_info[result['switches_id'], "title"] = result['switches_title']
        switches_info[result['switches_id'], "argA"] = result['argA']
        switches_info[result['switches_id'], "argB"] = result['argB']
        switches_info[result['switches_id'], "argC"] = result['argC']
        switches_info[result['switches_id'], "argD"] = result['argD']

        logger.debug(
            'get_switches... %s id=%s (%s)' % (result['switches_title'], result['switches_id'], result['switches_typ']))

        #
        # set up Tinkerforge switches
        #
        if result['switches_typ'] == "tf_ind_quad_relay":
            switches_info[result['switches_id'], "index"] = result['argA']

            switches[switches_info[result['switches_id'], "index"]] = BrickletQuadRelay(result['argA'],
                                                                                        result['switches_id'],
                                                                                        DEVICE_IP,
                                                                                        tinkerforge_connection, logger,
                                                                                        consumers)
            tinkerforge = True
        elif result['switches_typ'] == "tf_dual":
            switches_info[result['switches_id'], "index"] = result['argA']

            switches[switches_info[result['switches_id'], "index"]] = BrickletDualRelay(result['argA'],
                                                                                        result['switches_id'],
                                                                                        DEVICE_IP,
                                                                                        tinkerforge_connection, logger,
                                                                                        consumers)
            tinkerforge = True
        elif result['switches_typ'] == "tf_remote":
            switches_info[result['switches_id'], "index"] = result['argA']

            switches[switches_info[result['switches_id'], "index"]] = BrickletRemote(result['argA'],
                                                                                     result['switches_id'], DEVICE_IP,
                                                                                     tinkerforge_connection, logger,
                                                                                     consumers)
            tinkerforge = True

        #
        # set up SIS USB switch
        #
        elif result['switches_typ'] == "sis_usb_socket":
            switches_info[result['switches_id'], "index"] = "sis_usb_socket"
            switches[switches_info[result['switches_id'], "index"]] = lib_sispm.Sispm(result['switches_id'], DEVICE_IP,
                                                                                      logger, consumers)

        #
        # set up raspi gpio pins
        #
        elif result['switches_typ'] == "raspi_gpio":
            switches_info[result['switches_id'], "index"] = "raspi_gpio"
            switches[switches_info[result['switches_id'], "index"]] = lib_gpio.RaspiGPIO(result['switches_id'],
                                                                                         DEVICE_IP, logger, consumers)

    if tinkerforge:
        tinkerforge_connection.connect(DEVICE_IP, 4223)
        logger.info('Tinkerforge ... System online')

Zuerst besorge ich mir aus der mysql-Datenbank eine Liste aller Schalter die an diesem Hardware-Client angeschlossen sind.
Dann folgt eine Schleife, in der ich die Liste Schritt für Schritt durchgehe.

Hier ist auch der Punkt wo wir den Sourcecode erweitern müssen.

elif result['switches_typ'] == "raspi_gpio":
    switches_info[result['switches_id'], "index"] = "raspi_gpio"
    switches[switches_info[result['switches_id'], "index"]] = lib_gpio.RaspiGPIO(result['switches_id'],DEVICE_IP, logger, consumers)

Zur Erklärung:
Wir sehen nach welcher Wert in der Datenbank für „switches_typ“ hinterlegten wurde und zweigen dann mit einer if-Bedingung entsprechend ab.
Im Beispiel oben schauen wir nach ob GPIO-Pins genutzt werden, wenn ja, dann legen wir als erstes einen Index für diesen Schalter fest.

switches_info[result['switches_id'], "index"] = "raspi_gpio"

Warum ein extra Index manuell festlegen?
Ganz einfach, es gibt Hardware, z.Bsp. die Relais von Tinkerforge oder die SIS-PM Steckdosenleiste, welche über ein Objekt mehrere Schalter zur Verfügung stellen.
Außerdem kann ich aber auch noch 4 x Tinkerforge Relais Module an einen Hardware-Client anschließen (macht dann 4 x 4 = 16 Schalter).
Ich muss (und kann) die Tinkerforge Module und die Steckdosenleiste genau einmal per Software ansprechen, habe aber eventuell 4 x Schalter.
Deshalb müssen diese 4 x Schalter sich das gleiche Objekt teilen und damit auch den gleichen Index.
Gleichzeitig muss ich aber so flexibel sein und die Möglichkeit schaffen das von einem Hardware-Modul mehrere Exemplare angeschlossen werden.
Deshalb lege ich das Ganze manuell fest.

Im aktuellen Sourcecode kann ich genau 1 x GPIO-Leiste und genau 1 x SIS-Steckdosenleiste ansprechen, aber beliebig viele Tinkerforge Module.

Als nächstes wird eine Instanz des Schalters erzeugt.

switches[switches_info[result['switches_id'], "index"]] = lib_gpio.RaspiGPIO(result['switches_id'],DEVICE_IP, logger, consumers)

Dazu speichere ich die neue Instanz in einer Liste unter dem eben erzeugten Index ab.

Gerne würde ich diesen Abschnitt irgendwie verallgemeinern, so das kein Eingriff mehr notwendig ist.
Leider weiß ich nicht wie.

Zum einen muss ich beim Erzeugen der Instanz die Klasse mit Ihrem spezifischen Namen ansprechen (lib_gpio.RaspiGPIO).
Zum anderen muss ich daran denken, das manche Schalter-Typen bei der Initialisierung mehr Informationen brauchen (Tinkerforge-Module brauchen Ihre UID die in arg_a gespeichert wird).

Bitte nicht vergessen die neue Schalter-Klasse über

import lib_gpio

in die „sh-client.py“ zu importieren.

Damit wären alle Eingriffe erledigt.
Und die Software sollte funktionieren.

Eine neue Sprache hinzufügen

Eine neue Sprache hinzuzufügen ist ein Kinderspiel.

Dazu kopieren Sie einfach die Datei „/languages/de.php“ und geben ihr einen der Sprache entsprechenden Namen.
Für Spanisch wäre das „/languages/es.php“ und für Italienisch müsste die Datei „/languages/it.php“ heißen.

Danach öffnen wir diese neu erstellte Datei, der Inhalt sieht dann so aus:

    //  Menüeinträge
    $lang['dashboard'] = 'Dashboard';
    $lang['time_switch'] = 'Zeitschaltuhr';
    $lang['settings'] = 'Einstellungen';
    $lang['general'] = 'Allgemein';
    $lang['switches'] = 'Schalter';
    $lang['sensors'] = 'Sensoren';

    // Zeitschaltuhr 
    $lang['cycle_times'] = 'Schaltzeiten';
    $lang['new_cycle_time'] = 'Neue Schaltzeit';
    $lang['title'] = 'Bezeichnung';
    $lang['switch'] = 'Schalter';
    $lang['date'] = 'Datum';
    $lang['start'] = 'Start';
    $lang['stop'] = 'Stop';
    $lang['duration'] = 'Dauer';
    $lang['period'] = 'Zeitraum';
    $lang['finish'] = 'Ende';

Im nächsten Schritt ändern wir jetzt einfach Zeile für Zeile die ganze Datei und ersetzen die deutschen Wörter und Sätze durch ihre jeweiligen Übersetzungen.
Danach speichern wir unsere Änderungen.

Als nächstes müssen wir dem html-frontend an 2 x Stellen mitteilen das eine neue Sprache dazu gekommen ist.
Wir öffnen also die Datei „/languages/lang.php“ und gehen dort zur Funktion „is_language_supported

function is_language_supported( $lang_id )
{
    $langs = array( "en", "de" );
    return ( in_array( $lang_id, $langs ) ) ? true : false;
}

Hier tragen wir den Namen der soeben erstellten Datei ein, aber Achtung, ohne die Endung „.php“.
Um das Beispiel weiterzuführen, bei Italienisch würde die Sprach-Datei „/languages/it.php“ heißen, somit müssten wir hier „it“ eintragen

function is_language_supported( $lang_id )
{
    $langs = array( "en", "de", "it" );
    return ( in_array( $lang_id, $langs ) ) ? true : false;
}

Die zweite Stelle an der wir eine Änderungen durchführen müssen befindet sich in der Datei „/includes/navbar-top.php“.
Wir öffnen auch diese Datei und gehen in Zeile 13:

<ul class="dropdown-menu">
    <li><a href="javascript:setLang('de')">de</a></li>
    <li><a href="javascript:setLang('en')">en</a></li>
</ul>

An dieser Stelle müssen wir unsere neue Sprache nach dem gleichen Muster wie vorhin einfügen.
Für Italienisch würde das am Ende so aussehen:

<ul class="dropdown-menu">
    <li><a href="javascript:setLang('de')">de</a></li>
    <li><a href="javascript:setLang('en')">en</a></li>
    <li><a href="javascript:setLang('it')">it</a></li>
</ul>

Damit ist die neue Sprache für das html-frontend verfügbar.

Zusammengefasst besteht das Ganze aus 3 Schritten:

  • neue Sprachdatei erstellen (dazu eine alte kopieren) und Übersetzungen anlegen
  • Sprache (Dateiname ohne „.php“) in „lang.php“ hinzufügen
  • Sprache (Dateiname ohne „.php“) in „navbar-top.php“ hinzufügen

Das war dann auch schon alles.