Sofern Dir der neue Name „FRITZ!Smart Thermo 301“ nichts sagt, bis vor kurzem hießen die smarten Heizkörperventile von AVM noch „FRITZ!DECT 301“. Dieser Beitrag soll die Steuerbarkeit der Heizungsventile über php-Skripte beschreiben. Prinzipiell können alle Funktionen der Ventile nicht nur über die GUIs der FRITZ!Boxen und der Apps gesteuert werden, sondern auch über geeignete Netzwerkbefehle. Ich betrachte hier ausschließlich die PBKDF2-Verschlüsselung (ab FRITZ!OS 7.24), da diese Methode den aktuellen Stand der Sicherheit bei den AVM-Geräten bildet. Die MD5-Methode der Anmeldung funktioniert nur noch in sehr alten Geräten mit dementsprechenden Softwareständen vor FRITZ!OS 7.24.
Die Notwendigkeit der Steuerung ergab sich in meinem Haus dadurch, dass ich unterschiedliche Smart-Home-Komponenten unterschiedlicher Hersteller verwende. Die Heizungsventile sind ausschließlich von AVM, – eben die „FRITZ!Smart Thermo 301“ oder in alter Nomenklatur „FRITZ!DECT 301“. Unter dem alten Namen habe ich die smarten Ventile auch noch gekauft. Es gibt von AVM natürlich auch passende Fensterkontakte, die die Ventilstellung beeinflussen können. Beispielsweise soll bei geöffnetem Fenster das Ventil zugedreht werden. Sobald das Fenster wieder geschlossen wird, soll die ursprüngliche Einsstellung wiederhergestellt werden.
Meine Tür-Fensterkontakte sind aber von HomematicIP. Das HomematicIP-System hat aber nativ keine Steuerungsmöglichkeit für AVM-Komponenten. Mir ist schon bewusst, dass man das über komplette Homeautomatisierungssysteme leicht in den Griff kriegt, aber das wollte ich nicht. Da spielt auch der „Forschergeist“ eine wichtige Rolle. Der Weg der Steuerung läuft also folgendermaßen:
- Tür-Fenster-Kontakt der HomematicIP sendet seinen geänderten Zustand (auf/zu) an die Homematic-Zentrale, bei mir eine CCU3
- Neben der Anzeigemöglichkeit der Fensterzustände im ganzen Haus ist die Zentrale unter bestimmten Umständen in der Lage ein Skript auf einem Server aufzurufen.
- Das Skript auf dem Server spricht gezielt das passende Ventil an und fragt auch notwendige Daten von dem Ventil ab.
Welche Befehle zur Verfügung stehen, darüber gibt einDokument von AVM Auskunft: AVM Home Automation HTTP Interface
Ich will hier nicht das ganze Dokument wiederholen, aber wenigstens den für das Projekt wichtigen Abschnitt zeigen:

Neben den Befehlen an sich und eventuellen Parametern fallen zwei weitere Abkürzungen auf, die der Erklärung bedürfen:
ain
Hierbei handelt es sich um den eindeutigen Identifizierer des zu steuernden Devices. Die ain kann in der FRITZ!Box-GUI abgelesen werden. Interessanterweise hält sich AVM aber selbst nicht an seine Nomenklatur. Der Wert heißt dort nämlich „Identifikationsnummer (IPEI). Und noch etwas passt für unsere Zwecke nicht ganz. Die IPEI enthält eine Leerstelle, die in den Befehlen zu entfernen ist.

Die wesentlich schwierigere Hürde stellt aber folgendes Datum dar.
sid
Die Session-ID ist für jeden Kommunikationsvorgang neu zu bilden. Stimmt nicht ganz, denn sie hat eine gewisse Lebensdauer und könnte theoretisch unter bestimmten Vorraussetzung mehrfach in diesem Zeitraum genutzt werden. Ich mache das allerdings nicht, da der Overhead zu groß wäre.
Ich will gar nicht schönreden, dass mich die Bildung der sid bald zur Weißglut getrieben hätte. Wiederum ein AVM-Dokument klärt darüber auf, wie die Session-ID gebildet wird: Anmeldung am FRITZ!Box Webinterface
- über die Adresse der eigenen FRITZ!Box wird mit http://[eigene_IP_der_Box]/login_sid.lua?version=2 eine Anwort abgerufen
- aus der Antwort (2$<iter1>$<salt1>$<iter2>$<salt2>) werden das Schlüsselverfahren (eigentlich immer 2=>PBKDF2) und die vier Schlüsselparameter abgerufen
- Zusammen mit dem eigenen Passwort und den vier Schlüsselparametern wird über zweimaliges Verschlüsseln die response erzeugt
- Die response wird über http-POST zusammen mit user-ID und password bei der FRITZ!Box die Session-ID abgefragt.
- Eine gültige Session-ID besteht niemals nur aus Nullen !!
Das genannte Dokument beschreibt den Prozess der response-Erzeugung eigentlich ganz gut. Trotzdem gibt es Fallstricke. Der gelieferte Python3-Quellcode brachte mich auch nicht wesentlich weiter, weil ich von Python nicht gerade viel Ahnung habe. Im Internet findet man zuhauf Programme und auch php-Skripte, die den Bildungsprozess für die response beschreiben. Leider haben alle Beispiele für php das gleiche Problem: Sie beschreiben nur die alte simple MD5-Verschlüsselung. Nach mehreren Anläufen ohne Erfolg, – die Session-ID bestand immer nur aus Nullen -, habe ich den AVM Entwicklersupport angeschrieben. Ich habe bei meiner Anfrage mein bis dahin erzeugtes php-Skript angehängt und um Unterstützung gebeten. Geschrieben um 15:42 des ersten Tages und die Antwort war am zweiten Tag um 12:49 da. An dieser Stelle möchte ich mal den Entwicklungssupport von AVM loben, denn die Antwort war nicht nur extrem schnell da, sondern erklärte im Text meinen Fehler und beinhaltete sogar das korrigierte Skript. Super!
Nun aber genug der Einführung und hinein in das php-Skript:
Zunächst mal der Hinweis, dass man alles noch viel besser machen könnte und auch konsequenter Variablen benutzen könnte. Ich war froh, dass des endlich lief! … und es soll helfen.
// *********************************
// Session-ID für FRITZ!Box erzeugen
// *********************************
$fritz_url = "[eigene_IP_der_Box]"; //Bsp: "192.178.1.23"
$fritz_user = "[UserID]";
$fritz_pwd = "[extrem geheimes Password]";
// Get Challenge-String
$l = simplexml_load_string(file_get_contents(sprintf('http://[eigene_IP_der_Box]/login_sid.lua?version=2')));
$c = $l->Challenge;
list($pre, $iter1, $salt1, $iter2, $salt2, $rest) = explode("$", $c);
Nach der Definition einiger Variablen wird über die Box die Antwort mit der sogenannten challenge geholt. Diese wird dann gleich in eine Liste mit den einzelnen Bestandteilen geladen.
//PW UTF8 encode
$fritz_pwd = mb_convert_encoding($fritz_pwd, "UTF-8");
//Hash1 bilden
$hash1 = hash_pbkdf2("sha256", $fritz_pwd, hex2bin($salt1), intval($iter1));
Das Password muss UTF-8-encoded sein. Gleich am Anfang daher die Anpassung. Das Password wird jetzt mit der PBKDF2-Methode verschlüsselt. Die SHA256-Verschlüsselung wird mit dem Verschlüsselungssatz salt1 sooft engewendet, wie es iter1 angibt. Und an dieser Stelle gibt es auch schon den ersten Fallstrick, den ich aus der Dokumentation so nicht herausgelesen habe. Der salt-Wert wird als hex geliefert. Damit kann der Verschlüsselungsalgorhytmus aber nicht sinnvolles anfangen. Daher muss eine hex2bin-Umwandlung erfolgen. Die Anzahl der Iterationen wird ebenfalls vom String in eine Zahl gewandelt. Damit ist hash1 erzeugt.
//Hash2 bilden
$hash2 = hash_pbkdf2("sha256", hex2bin($hash1), hex2bin($salt2), intval($iter2));
//response bilden
$response = $salt2 . '$' . $hash2;
$response = strtoupper($response);
Die Funktion hash-pbkdf2() erzeugt bei diesen Parametern einen HEX-Wert. In der nächsten Verschlüsselung für hash2 ist das zu berücksichtigen! Eigentlich ist das der gleiche Vorgang wie bei hash1. Allerdings wird hier statt des Passwords das in hash1 verschlüsselte Password als Basis genommen und das zweite Wertepaar salt2 und iter2 wird genutzt. Diesmal kann das hex-Ergebnis direkt genutzt werden. salt2 und hash2 werden nun mit einem eingeschlossenen ‚$‘ verkettet und zum Schluss werden die hex-typischen Buchstaben noch in Uppercase gewandelt. Fertig ist unsere response.
//Ziel definieren
$url = $fritz_url . '/login_sid.lua?version=2';
//Daten vorbereiten
$data = array('username' => $fritz_user, 'response' => $response);
//url-ify the data for the POST
$fields_string = http_build_query($data);
Die Entwickler sehen an dieser Stelle aus Sicherheitsgründen eine POST-Abfrage als notwendig an. Dazu werden Ziel der Anfrage und die Parameter in Arrays gepackt und dann noch aufbereitet.
$options = array(
CURLOPT_RETURNTRANSFER => true, // return web page
CURLOPT_HEADER => false, // don't return headers
CURLOPT_HTTPHEADER => array('Content-Type: application/x-www-form-urlencoded'),
CURLOPT_FOLLOWLOCATION => true, // follow redirects
CURLOPT_ENCODING => "", // handle all encodings
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => $fields_string
);
//open connection
$ch = curl_init($url);
curl_setopt_array( $ch, $options );
//execute post
$content = curl_exec($ch);
curl_close( $ch );
Die Technik einer POST-Abfrage unter php will ich hier nicht erläutern. Dazu gibt es im Internet genügend Quellen. Interessant für uns wird es erst mit der Antwort auf diese Abfrage. Diese liegt in der Struktur $content.
$xml_string = simplexml_load_string($content);
$session_id = $xml_string->SID;
Die XML-Struktur wird noch aufgebrochen und der SID-Anteil extrahiert. Damit haben wir unsere SessionID.
Bei der Ansteuerung der vielen Heizkörperventile wäre es unsinnig jedesmal ein Skript mit diesem ganzen Verschlüsselungskram in die eigentliche Steuerungslogik einzubauen. Deshalb verpackt man die Verschlüsselungslogik am besten in ein eigenes File und macht aus dem obigen Code eine Funktion.
<?php
function get_sid()
{
// *********************************
// Session-ID für FRITZ!Box erzeugen
// *********************************
Die Funktion wird natürlich am Fileanfang über dem oben ausgeführten Code vom php-Header <?php eingeleitet. Danach wird sie noch definiert mit dem function-Begriff und der öffnenden geschweiften Klammer. Texte zur Dokumentation sollten nicht fehlen.
return $session_id;
}
?>
Die Funktion liefert mit return die Session-ID zurück. Beendet wird sie mit der schließenden geschweiften Klammer und dem php-Ende ?>.
Der Aufruf dieser Funktion erfolgt dann aus dem in einem anderen php-File liegenden Hauptprogramm:
<?php
// Komforttemperatur für HK
// Session ID mit Funktion in der eigenen Funktionen-php holen
include('Fritz_Funktionen.php');
$ses_id = get_sid();
Damit das aufrufende Programm auch weiß, wo die betreffende Funktion zu finden ist, benötigt es ein include mit dem entsprechenden Filenamen. Der Aufruf selbst passiert dann mit get_sid().
Noch ein kleiner Tip zum Schluss, wenn Ihr php-Skripte durch die CCU3 aufrufen wollt: Umlaute in Dateinamen mag sie gar nicht.
In ein paar Tagen sollte die Fortsetzung dieser Beitragsreihe fertig sein. Ich werde dann den Weg beschreiben, wie man die HomematicIP-Komponenten zur Steuerung von AVM-Komponenten bewegt.