Samstag, 30. Januar 2016

RWE Smart Home - Notifications mit PHP Library

Während ich noch vor ein paar Jahren eine Gastherme aus den 90ern zum Heizen der Wohnung per kontrolliertem Strom ein/aus über eine Funksteckdose schalten musste, habe ich nun den "Luxus" einer Fußbodenheizung.
Da ich bis vor zwei Jahren bei ELV (FS20) nichts gefunden hatte, was mir diesbezüglich zusagte,hatte ich bei einem BlackFriday Angobot zugeschlagen und günstig eine RWE SmartHome Zentrale Controller für Fußbodenheizungsventile und vier Raumthermostate erstanden.

Der allgemeine Glauben, dass man sich wegen der "geschlossenen" Lösung in Bezug auf die Benutzung vom Hersteller abhängig macht, stimmt hier nicht - es gibt eine Reihe von Ansätzen, mit der Anlage genau das zu machen, was der werte HomeAutomation Hacker möchte. Die API ist inzwischen rechtgut dokumentiert.

Aus Platz- und Speicherspargründen habe ich mich für die PHP-Implementierung der API entschieden. Alternativen sind im SmartHomeForum beschrieben.

Das Installieren und Ausprobieren funktioniert mit den Beispielen schon ganz gut allerdings kommt man schnell an die Grenzen, wenn es mehr als nur ein paar Temperaturen abzufragen gilt.
So hatte ich über ein Jahr lang per 5 minütigem Polling alle Temperatur- und Luftfeuchtewerte abgefragt und geloggt - per PHP-Shell Programm.

Das alte Webinterface war ein Relikt aus den 90er Jahren - eine clickable Map, die über einem Bild liegt, welches per BASH und "fly" bzw "flydraw" erzeugt wurde.
altes Webinterface
Der Wunsch, Temperaturen einstellen zu können, hat mich dann nach über 15Jahren doch noch veranlasst, auch das Interface ins Web 2.0 Zeitalter zu hieven.
Dazu verteile ich per CSS eine Reihe von Controls auf einem Hintergrund, die alle einen AJAX Event-Handler besitzen.
modernisiertes Webinterface
Auf die runden Temperatur Controls bin ich besonders stolz. Ich nutze dafür das jQuery Knob (für das CSS habe ich bestimmt einen Tag gebraucht).

Als zum letzten BlackFriday ein RWE Bewegungsmelder hinzukam, war mein Polling-Script am Ende. Schließlich sollte die Musik im Bad zeitnah eingeschaltet werden und nicht erst nach max. 5min.

Die Lösung lautet "Notifications"


Eigentlich hatte ich die API für meine Zwecke missbraucht. Jede meiner Anfragen an die Zentrale führte dazu, dass sämtliche Daten ausgelesen und übertragen werden mussten - alle Räume, Sensoren und Aktoren. Leider ist in der Doku zur PHP-API auch nicht ersichtlich, welche Methode eine Kommunikation mit der Zentrale auslöst, wie lange eine Session gültig ist, und wann sich die API ggfs. wieder neu an der Zentrale anmelden muss.

Die SmartHomeZentrale (SHC) unterstützt das Senden von Änderungen an Notification-Subscriber, sobald sich ein Status ändert. Die Dauer dieses Abonnement beträgt nach meinen Erfahrungen ca. 4h, dann muss es erneuert werden, indem man sich erneut an der SHC anmeldet. Normalerweise sollte zuvor allerdings eine "LogoutNotification" kommen, so dass die 4h nur als TimeOut Parameter dienen.

An die Stelle meines per CRON aufgerufenen "GetTemperature.php" Scripts tritt nun ein PHP Programm welches  bei Systemstart über rc.local aufgerufen und in den Hintergrund gelegt wird und so permanent läuft. Um ganz ehrlich zu sein, handelt es sich auch bei den Notifications um Polling - schließlich werden die Daten per HTTP(S) übertragen. Allerdings handelt es sich dabei um eine sehr knappe Angelegenheit, sofern man korrekt den NotificationsID Schlüssel präsentiert. Mein Update-Intervall beträgt 5 Sekunden, was selbst für den Bewegungsensor im Bad ausreichend schnell ist.

#!/usr/bin/php
<?php

use Bubelbub\SmartHomePHP\SmartHome;
use Bubelbub\SmartHomePHP\Entity\LogicalDevice;

require_once '/usr/local/lib/php5/SmartHome-PHP/vendor/autoload.php';

// query SHC for notifications every ... seconds
$update_interval = 5;

// write out current stats every ... seconds
$save_interval = 60 * $update_interval;

// max. allowed login time (seconds)
$max_login = 60 * 60 * 4;

$path = '/tmp';
$rrdpath = "/var/log/rrd";
$rrd  = array( "wohnzimmer", "alex", "test", "bad" );
$raum = array( "WOHNZIMMER", "ALEX", "TEST", "BAD" );
$typ  = array( "soll", "ist", "hum" );

$sh = new \Bubelbub\SmartHomePHP\SmartHome('smarti', 'login', 'password');

getEntities();
$sh->getAllLogicalDeviceStates();

$lum = 0;
$room = array();

$dev = $sh->getLogicalDevices();

foreach( $dev as $ld )
{
  switch( $ld->getType() )
  {
    case LogicalDevice::DEVICE_TYPE_ROOM_TEMPERATURE_ACTUATOR:
      $room[$ld->getLocation()->getName()][$typ[0]] = $ld->getPointTemperature();
      break;
    case LogicalDevice::DEVICE_TYPE_ROOM_TEMPERATURE_SENSOR:
      $room[$ld->getLocation()->getName()][$typ[1]] = $ld->getTemperature();
      break;
    case LogicalDevice::DEVICE_TYPE_ROOM_HUMIDITY_SENSOR:
      $room[$ld->getLocation()->getName()][$typ[2]] = $ld->getHumidity();
      break;
    case LogicalDevice::DEVICE_TYPE_LUMINANCE_SENSOR:
      $lum = $ld->getLuminance();
      $f = fopen( $path.'/lum', 'a' );
      $s = date( "H:i", time() );
      fputs( $f, $s ." ". $lum ."\n" );
      fclose($f);
      break;
    case LogicalDevice::DEVICE_TYPE_GENERIC_ACTUATOR:
      $mov = ( $ld->getState() == 'ON' ) ? 'True' : 'False' ;
  }
}

while( 1 == 1 )
{
  $sh->getNotificationID();

  // variable to indicate status updates
  $update = 0;
  // variable to indicate whether a LogOut Notification has been received
  $logout = 0;
  // variable to track the time period between last update and save_interval
  $lastupdate = 0;
  // state variable in RWE (shown as Generic_Actuator)
  $event = $mov;

  $lastevent = 0;
  $count = 0;

  while( ! $logout && ( ( $count * $update_interval ) < $max_login ) )
  {
    foreach( $sh->getUpdates()->Notifications as $a )
    {
      if( array_key_exists( 'LogicalDeviceStatesChangedNotification', $a) )
      {
        foreach( $a->LogicalDeviceStatesChangedNotification as $id )
        {

//           print $dev[(string) $id->LogicalDeviceStates->LogicalDeviceState['LID'] ]->getLocation()->getName() ."\t";
//           print $dev[(string) $id->LogicalDeviceStates->LogicalDeviceState['LID'] ]->getName() ."\t";
//           print $dev[(string) $id->LogicalDeviceStates->LogicalDeviceState['LID'] ]->getType() ."\t";

          switch( $dev[(string) $id->LogicalDeviceStates->LogicalDeviceState['LID'] ]->getType() )
          {
            case LogicalDevice::DEVICE_TYPE_ROOM_TEMPERATURE_ACTUATOR:
              $room[ $dev[(string) $id->LogicalDeviceStates->LogicalDeviceState['LID'] ]->getLocation()->getName() ][$typ[0]] = 
                  $id->LogicalDeviceStates->LogicalDeviceState['PtTmp'];
              $update = 1;
               print $id->LogicalDeviceStates->LogicalDeviceState['PtTmp'] ."\n";
              break;
            case LogicalDevice::DEVICE_TYPE_ROOM_TEMPERATURE_SENSOR:
              $room[ $dev[(string) $id->LogicalDeviceStates->LogicalDeviceState['LID'] ]->getLocation()->getName() ][$typ[1]] = 
                  $id->LogicalDeviceStates->LogicalDeviceState['Temperature'];
              $update = 1;
               print $id->LogicalDeviceStates->LogicalDeviceState['Temperature'] ."\n";
              break;
            case LogicalDevice::DEVICE_TYPE_ROOM_HUMIDITY_SENSOR:
              $room[ $dev[(string) $id->LogicalDeviceStates->LogicalDeviceState['LID'] ]->getLocation()->getName() ][$typ[2]] =
                  $id->LogicalDeviceStates->LogicalDeviceState['Humidity'];
              $update = 1;
               print $id->LogicalDeviceStates->LogicalDeviceState['Humidity'] ."\n";
              break;
            case LogicalDevice::DEVICE_TYPE_LUMINANCE_SENSOR:
              $lum = $id->LogicalDeviceStates->LogicalDeviceState->LuminanceProperty['Value'];
              $f = fopen( $path.'/lum', 'a' );
              $s = date( "H:i", time() );
              fputs( $f, $s ." ". $lum ."\n" );
              fclose($f);
               print $id->LogicalDeviceStates->LogicalDeviceState->LuminanceProperty['Value'] ."\n";
              break;
            case LogicalDevice::DEVICE_TYPE_GENERIC_ACTUATOR:
              $event = $id->LogicalDeviceStates->LogicalDeviceState->Ppts->Ppt['Value'];
               print $id->LogicalDeviceStates->LogicalDeviceState->Ppts->Ppt['Value'] ."\n";
              break;
             default:
               print $dev[(string) $id->LogicalDeviceStates->LogicalDeviceState['LID'] ]->getType() ."\n";
               print_r( $id->LogicalDeviceStates->LogicalDeviceState );
          }
           print "----------------------------------------------------------\n";

        }
      }
      elseif( array_key_exists( 'LogoutNotification', $a ) ){ $logout = 1; }
//       else{ print "...\n"; }
    }

    if( strcmp( $mov, $event ) )
    {
      if( $event != 'False' )
      {
        exec( '/usr/local/bin/movement_on.sh &' );
         print "movement on \n";
      }
      else
      {
        exec( '/usr/local/bin/movement_off.sh &' );
         print "movement off \n";
      }

      $lastevent = $count;
      $mov = $event;
    }

    if( $update || ( ( $count - $lastupdate ) * $update_interval >= $save_interval ) )
    {
//       print "Updating due to: update=". $update ." / save_interval= ". ( $count - $lastupdate ) * $update_interval ."\n";

      foreach( $typ as $j => $type )
      {
        $f = fopen( $path.'/'.$type, 'a' );
        $s = date( "H:i", time() );

        foreach( $raum as $i => $val )
        {
          $s .= " " . $room[$val][$type];
          if( $room[$val][$type] > 0 ){ rrd_update( $rrdpath ."/". $type ."_". $rrd[$i] .".rrd", array( "N:". $room[$val][$type] ) ); }
        }

        fputs( $f, $s . "\n" );
        fclose($f);
      }
      $lastupdate = $count;
      $update = 0;
    }

    sleep($update_interval);
    $count++;
  }
}

?>

Neben dem "movement_on/off.sh" Script bei Signal vom Bewegungsmelder, schreibe ich die Raumtemperaturen (soll/ist) und die Luftfeuchtewerte jeweils in Dateien im /tmp Ordner, aus denen sich das Web-Interface bedient. Zusätzlich schreibe ich ein RRD-Log sämtlicher Werte um den Tages-/Wochen-/Monats-/Jahresverlauf anzeigen zu können.

Leider sind meine objektorientierten Programmierkenntnisse fast nicht vorhanden, so dass ich für das Parsen der Notifications keine Klassenerweiterung schreiben konnte. Daher musste ich die Fälle direkt in meinem Code verarbeiten - der kommentierte Code veranschaulicht (wenn auskommentiert), welche Datenstrukturen in der NotificationResponse übertragen werden.

Um den Notifications Request richtig absetzen zu können bedarf es bei der aktuellen API-Version (Stand Januar 2016) noch eines kleinen Patches.

Update (2016-09-28):
Es gibt eine neue Firmware für das RWE Smarthome System, einhergehend mit der Umbenennung in Innogy. Im Forum wird diese Firmware UI2.0 genannt.
Aktuell gibt es keine Schnittstellenbeschreibung für UI2.0. Es ist dringend zu empfehlen zunächst auf UI1.0 zu bleiben!
Desweiteren sieht es so aus, als wäre mit der neuen Firmware keine Offline Verbindung merh möglich, d.h. die Zentrale kommuniziert nur noch mit dem innogy Server.