IoT-Erweiterung der Regelung einer Junker-Therme (Teil 2: Software)
https://oer-informatik.de/esp32-iot-heizung-software
tl/dr; (ca. 10 min Lesezeit): Für eine Heizungsregelung (ESP32 basierend auf 1-2-4-Bus) sollen Heizungsthermostate (Fritz SmartHome, Dect ULE) per AHA- oder TR-064 Schnittstelle ausgelesen werden. Basierend auf den Werten soll die Steuerspannung am Analog-Ausgang des ESP32 eingestellt werden.
Das Gesamtprojekt ist in drei HowTos unterteilt: 1. die Hardware, dazwischen ein Exkurs zur ESP-Vorbereitung und dieser Teil zur eigentlichen Projekt-Software.
Teil 2: die Software
Im ersten Teil dieses HowTos wurde die Schaltung beschrieben, mit deren Hilfe ein ESP32 später die Steuerleitung eines 1-2-4-Busses auf die nötigen Potenziale zwischen 0 und 24V stellen soll. Woher die nötigen Temperaturwerte kommen und wie diese ausgelesen werden wurde bewusst offen gelassen. Das System soll dabei recht flexibel sein.
Der Artikel ist recht lang mit viel Codebeispielen. Für die Eiligen unter euch gibt es im Repository den fertigen Arduino-Sketch mit AHA-Schnittstelle (empfohlen) bzw. mit TR064-Schnittstelle (langsamer). Ich baue ihn hier Schritt für Schritt auf in folgenden Teilschritten:
Problemstellung
Auswahl der Thermostattechnik
Entwurf der Programmierung
Vorbereiten des ESP32
Eine WiFi-Verbindung aufbauen
Loggen der Werte und Debugging-Meldungen
Hardware konfigurieren: Pins der LED und des DAC
Fritzbox- / DECT-ULE- spezifische Einstellungen
Variante 1: Auslesen per AHA
Variante 2: Auslesen per TR064
Problemstellung
Damit der ESP bestimmen kann, wann ein Heizbedarf besteht, benötige ich Soll- und Istwerte für den Wärmebedarf. Einfache Temperatursensoren und ein Zeitplan kamen für mich nicht in Frage: zu unregelmäßig ist unser Heizverhalten in einigen Räumen. Ausserdem möchte ich verhindern, dass die Umwälzpumpe der Heizung anspringt, ohne dass ein Heizkörperventil geöffnet ist. Ausserdem sollte die Heizung raumindividuell und automatisch in die Nachtabsenkung (niedrigere Temperaturen) fahren.
Damit war die Richtung vorgegeben: es sollte ein “smartes” Thermostat werden: also fernsteuerbar und auslesbar. Welche Technik genau zum Einsatz kommt ist aber fast egal - mit wenigen Zeilen Codeänderung lässt sich das ganze beliebig übertragen.
Auswahl der Thermostattechnik
Wie bei allen IoT-Devices muss als erstes eine Technik/Hersteller-Entscheidung getroffen werden. Es gibt mittlerweile extrem viele Techniken und noch mehr Hersteller für smarte Thermostate. Die genutzten Funktechniken sind beispielsweise:
Zigbee
Z-Wave
WiFi
BluetoothLE
DECT ULE
Meine Entscheidungskriterien waren:
lange Batterielaufzeit (mind. 1 Heizsaison)
keine Datenabwanderung / kein Abomodell
Möglichkeit der lokalen zentrale Datenhaltung
möglichst keine zusätzliche Endgeräte (z.B. Bridge)
möglichst kein Lock-in-Effekt (gerne OpenSource)
große Reichweite oder Repeateroption (zwei Gebäudeteile)
Bedienung am Thermostat ohne Handy/Computer muss möglich sein
am Markt verfügbar (ja, das war Herbst 2022 ein Thema!)
veröffentlichte Schnittstelle, sowohl zum Auslesen als auch zum Einstellen der Thermostate
Um es kurz zu machen: nach etwas lesen war klar, dass ich nicht alle Anforderungen werde umsetzen können. Ich bin in die Lock-in-Falle getappt und habe mich für die FritzDECT bzw. CometDECT-Variante entschieden. Es wäre mit vielen anderen Thermostaten ebenso gegangen. Eine Übersicht, welche smarten Thermostate es gibt, hat u.a. die Stiftung Warentest veröffentlicht (hinter Bezahlschranke).
Ich habe zweierlei Thermostate im Einsatz, die allesamt DECT-ULE bzw. AVM AHA/TR064-kompatibel sind:
Eurotronic CometDECT (der Zusatz DECT ist wichtig): 2 AA-Batterien, einfaches LCD-Display, Batterien halten scheinbar mit Ach und Krach eine Saison. Wurde auch als AVM FRITZ!DECT 300 vertrieben. Manchmal findet man die noch günstig, aber eigentlich vergriffen.
AVM FRITZ!DECT 302: Mit eInk-Display, 3 AA-Batterien, längerer Laufzeit und etwas komfortabler zu bedienen (aber sehr teuer)
Es gibt noch das AVM FRITZ!DECT 301, eine verbesserte Version des CometDECT, mit eInk-Display. Kosten momentan oft so viel wie die 302, daher habe ich die anderen gewählt.
In den folgenden Absätzen werde ich zunächst versuchen, den hardwareunabhängigen Teil zu beschreiben. Später wird es dann speziell um die AVM/Fritz Spezifika gehen.
Entwurf der Programmierung
Wie bei den meisten Arduino-Microcontrollerprogrammen gibt es zwei zentrale Funktionen: die einmal zu Beginn ausgeführte setup()
und die danach immer wiederholte loop()
. Für den ersten Überblick über das Gesamtprogramm werfe ich zunächst einen Blick auf den Aufbau die Gliederung dieser beiden Funktionen.
Hinter jeder Aktivität in loop()
und setup()
stehen Abläufe, die weiter unten beschrieben werden. Um eine erste Vorstellung davon zu erhalten, was bei den Unterpunkten jeweils passiert habe ich Codeausschnitte oder Beschreibungen als Notiz angefügt.
Die setup()
-Funktion

setup()
-FunktionKein Hexenwerk in der setup()
- erwartbar wird alles initialisiert, ggf. auch die genutzten Thermostate. Die Details stelle ich im Rahmen der jeweiligen Funktionalitäten vor.
Die loop()
-Funktion

loop()
-FunktionZentral ist hier die Schleife, die Soll- und Ist-Wert jedes Thermostats ausliest. Es wird nur die Differenz aus Soll- und Ist-Wert bestimmt. Die maximale Differenz bestimmt, ob und wie die Heizung anspringt.
Wurde eine neue Differenz bestimmt, wird die Funktion aufgerufen, die am Analog-Ausgang die erforderliche Spannung einstellt (siehe dazu die Beschreibung des 1-2-4-Busses im ersten Teil).
Vorbereiten des ESP32
Damit die Software vernünftig erstellt werden kann sind ein paar Vorarbeiten nötig. Wer schon mit dem ESP32 gearbeitet hat, kann diesen Teil hier überspringen/quer lesen:
Der ESP32 sollte installiert und getestet sein. Um ein erstes Gefühl zu erhalten, wie ein Programm mit der Arduino-IDE für den ESP32 geschrieben wird und aus welchen Komponenten es besteht, sollte diese kleine dort verlinkte Blink-Spielerei mal gemacht haben.
Der ESP sollte in ein WLAN integriert sein und Debugging-Nachrichten ausgeben können (ich nutze hierfür eine Webserver-Bibliothek, um unabhängig von USB zu sein). Darüber hinaus habe ich die Möglichkeit, Firmware-Updates per WLAN aufzuspielen, integriert.
Ich habe diese Vorbereitungsarbeiten in einem gesonderten Tutorial-Teil beschrieben. Dort werden die Codeabschnitte genannt, die für WLAN, Debugging und OTA für dieses Projekt verwendet wurden. Die oben verlinkten Tutorials können ergänzend dabei helfen. In den folgenden Programmabschnitten werden die Funktionen aus obiger Anleitung aufgerufen. Wenn also z.B. keine debugOutput()
oder ensureWIFIConnection()
gefunden wird hilft ein Blick in die verlinkten Dateien (oder den oben verlinkten Code).
In den folgenden Beschreibungen wird Code in einen der folgenden fünf Bereiche eines Arduino-Programms eingefügt:
/* 1 INSERT #include IMPORTS HERE*/
/* 2 INSERT VARIABLE DECLARATIONS HERE*/
void setup() {
/* 3 INSERT SETUP-OPERATIONS HERE*/
}
void loop() {
/* 4 INSERT LOOP-OPERATIONS HERE*/
}
/* 5 INSERT NEW FUNCTIONS HERE*/
Hardware konfigurieren: Pins der LED und des DAC
Am ESP habe ich vier LED angeschlossen, die den grundlegenden Status des Geräts ausgeben. Ein Pin ist reserviert für einen Jumper, der im Betrieb Debug-Ausgaben und Firmwareupdates unterdrückt. Darüber hinaus gibt es den analogen Ausgang, an dem die Operationsverstärkerschaltung für die Steuerleitung angeschlossen wird.
Welche relevanten Variablendeklarationen (Abschnitt 2) kommen jetzt neu hinzu?
//-------------------------------------------------------------------------------------
// List of Input- and Output-devices and Pins
//-------------------------------------------------------------------------------------
// Datatype | Name of Variable | Pin No. connected | Name, Behaviour*/
const int PIN_UPDATE_ACTIVE = 4; // Pullup, HIGH = Update active
const int PIN_LOGGING_ACTIVE = 4; // Pullup, HIGH = Logging active (Same as UPDATE)
const int DAC_THERMOSTAT = 25; // DAC setting V for OP-circuit (heating)
const int PIN_OPERATING_LED = 23; // green LED, HIGH-active
const int PIN_HEATING_MAX = 32; // red LED, HIGH-active
const int PIN_HEATING_ON = 33; // yellow LED, HIGH-active
const int PIN_HEATING_OFF = 22; // blue LED, HIGH-active
Ich lege per Software fest, welche Mindesttemperatur und welche Höchsttemperatur berücksichtigt werden soll: Wärmenachfrage über 21 Grad werden wie 21 Grad gewertet (Begrenzung aus Energiespargründen). Bei Soll-Temperaturen unter 8 Grad wird immer 8 Grad herangezogen (um komplette Auskühlung, Schimmelbildung und Frostschäden zu verhindern).
Alle drei Minuten werden die Werte eingelesen: das hat sich in der Praxis als nicht zu träge und nicht zu häufig bewährt.
//-------------------------------------------------------------------------------------
// App-Einstellungen
//-------------------------------------------------------------------------------------
const float MAX_TEMP = 21; // Höchsttemperatur, auf die die Heizung ausgelegt werden soll
const float MIN_TEMP = 8; // Mindestemperatur, auf die geheizt wird
const int CHECK_MILLISECONDS = 180000; // Intervall in Millisekunden, in denen neue Werte eingelesen werden
int currentMillis = 0;
In der setup()
(Abschnitt 3) werden schließlich die einzelnen Ein- und Ausgangspins konfiguriert (es werden die Datenrichtungsregister-Bit auf in/out gesetzt) und Startwerte gesetzt:
void setup() {
pinMode(PIN_OPERATING_LED, OUTPUT);
pinMode(PIN_HEATING_OFF, OUTPUT);
pinMode(PIN_HEATING_ON, OUTPUT);
pinMode(PIN_HEATING_MAX, OUTPUT);
pinMode(PIN_UPDATE_ACTIVE, INPUT_PULLUP);
pinMode(PIN_LOGGING_ACTIVE, INPUT_PULLUP);
digitalWrite(PIN_OPERATING_LED, HIGH);
digitalWrite(PIN_HEATING_OFF, HIGH);
...
// Startwert: DAC = 34 für ca. 0,45V also 5V hinter OPV
dacWrite(DAC_THERMOSTAT, 34);
digitalWrite(PIN_HEATING_OFF, LOW);
digitalWrite(PIN_HEATING_ON, LOW);
digitalWrite(PIN_HEATING_MAX, LOW);
digitalWrite(PIN_OPERATING_LED, LOW);
}
Es folgt in der loop()
(Abschnitt 4) das eigentliche Herzstück: nach den Funktionsaufrufen für WiFi, Webserver und OTA (die natürlich vorhanden bleiben) wird wie im obigen UML-Aktivitätsdiagramm beschrieben:
zunächst geprüft, ob das Zeitintervall, in dem die Temperaturen geprüft werden sollen abgelaufen ist (z.B. 3min)
Defaultwerte gesetzt (z.B. die Maximal-Soll/Ist-Temperaturdifferenz auf 0)
für jedes Thermostat Soll- und Ist-Temperatur abgerufen und die Differenz bestimmt
die maximale Temperaturdifferenz aller Termostate bestimmt
die Funktion aufgerufen, die für diese maximale Temperaturdifferenz die Steuerspannung der Heizung anpasst.
Ich habe an dieser Stelle die Funktionsaufrufe für die Thermostats bewusst nicht genannt, bis hierhin soll es hardwareunabhängig sein. Auch die Anzahl der Thermostate (anzahlHKR
) muss gesondert definiert werden (folgt im kommenden Fritz-spezifischen Abschnitt).
void loop() {
if ((digitalRead(PIN_UPDATE_ACTIVE) == HIGH)||(!ENABLE_UPDATE_JUMPER)){ // UPDATE Jumper not set or flag set
ArduinoOTA.handle();
}
if (digitalRead(PIN_LOGGING_ACTIVE) == HIGH){ // LOGGING Jumper not set or flag set
server.handleClient();
}
///...
if (((millis() - currentMillis) > CHECK_MILLISECONDS) || (currentMillis == 0)) {
loopLogText = "";
digitalWrite(PIN_OPERATING_LED, HIGH); //zeigt an, dass er TempDiff abfragt
float sollTemp = 0; // alle Variablen zurücksetzen
float istTemp = 0;
float maxDiff = 0;
float hkrDiff = 0;
String response = "";
for (int i = 0; i < anzahlHKR; i++) { /*anzahlHKR wird später angegeben, ist nur die Anzahl der Heizkreisläufe / Thermostate */
sollTemp = /*Hier kommt die Abfragemethode der genutzten Thermostattechnik hin*/;
istTemp = /*Hier kommt die Abfragemethode der genutzten Thermostattechnik hin*/;
hkrDiff = relevantTempDiff(istTemp, sollTemp);
if (hkrDiff > maxDiff) {
maxDiff = hkrDiff;
debugOutput("Neue Maximaldifferenz: " + String(maxDiff), 5);
}
}
writeTemperature(maxDiff);
digitalWrite(PIN_OPERATING_LED, LOW);
currentMillis = millis();
}
}
Dazu werden noch zwei Funktionen in Abschnitt 5 benötigt. Die eine Funktion bestimmt die zu berücksichtigende Temperaturdifferenz. Hier wird beispielsweise die oben definierte Maximal- und Minimaltemperatur eingesetzt sowie Extremwerte angepasst (z.B. die Einstellung “BOOST” und “FENSTER OFFEN” bei den Fritz-Thermostaten):
float relevantTempDiff(float hkrIsTemp, float hkrSetTemp) {
float tempDiff = 0;
//grobe Plausibilisierung der Werte
if ((hkrIsTemp > -15) && (hkrIsTemp < 45) && (hkrSetTemp > -15) && (hkrSetTemp < 256)) {
float usedSetTemp = hkrSetTemp;
if (int(hkrSetTemp * 10) == 1265) {
usedSetTemp = MIN_TEMP;
} else if (hkrSetTemp > MAX_TEMP) {
usedSetTemp = MAX_TEMP;
}
tempDiff = (usedSetTemp)-hkrIsTemp;
} else {
debugOutput("Etwas stimmt mit den Werten nicht", 3);
}
return tempDiff;
}
Eine weitere Funktion setzt die Temperaturdifferenz in eine Spannung um - zum Hintergrund dazu bitte einfach einen Blick in den ersten Teil dieses Tutorials werfen.
Letztendlich ist es ein Mapping der benötigten Spannungen (0-24V) auf den Bereich 0-3,3V und dann wieder auf den vom DAC darstellbaren Bereich 0-255:
void writeTemperature(float tempDiff) {
/*Gemessene Werte am Juncker-Raumthermostat bei eingestellte Raumtemp 18°C:
- Raum 1°C oder mehr wärmer als eingestellte SollTemperatur: 340mV
- wenn der Raum nur 0,5°C wärmer ist alls SollTemperatur springt das Thermostat auf 9-10V
- bei Solltemp = IstTemp ca. 11-15V
- Raum 0.5°C zu kalt: 18-20V
- ab 1°C zu kalt: 20V
*/
byte minDACValue = 0;
byte maxDACValue = 255;
float minVoltageValue = 0;
float maxVoltageValue = 3.3;
float voltageHeatingOff = 0.3; // aus (5V nach OPV) - Das Orginal Raumthermostat (ORT) geht auf 300mV
float voltageHeatingStart = 0.7; // start Regelung (8V nach OPV) - Das ORT startet mit 10V bei
float voltageHeatingMax = 1.9; // Regelung voll (20V nach OPV)
float tempDiffMax = 4;
float tempDiffMin = 0;
// 2.1V: Sättigung (22V)
float setVoltage = voltageHeatingOff;
digitalWrite(PIN_HEATING_OFF, LOW);
digitalWrite(PIN_HEATING_ON, LOW);
digitalWrite(PIN_HEATING_MAX, LOW);
if (tempDiff <= 0.2) {
setVoltage = voltageHeatingOff;
debugOutput("Heizung aus", 5);
digitalWrite(PIN_HEATING_OFF, HIGH);
} else {
if (tempDiff > tempDiffMax) {
setVoltage = voltageHeatingMax;
debugOutput("Heizung max", 5);
digitalWrite(PIN_HEATING_MAX, HIGH);
} else {
setVoltage = ((tempDiff - tempDiffMin) * (voltageHeatingMax - voltageHeatingStart) / (tempDiffMax - tempDiffMin) + voltageHeatingStart);
digitalWrite(PIN_HEATING_ON, HIGH);
}
}
byte dacValue = byte(((setVoltage - minVoltageValue) * (maxDACValue - minDACValue) / (maxVoltageValue - minVoltageValue) + minDACValue));
dacWrite(DAC_THERMOSTAT, dacValue);
debugOutput("Stelle den DAC-Wert auf " + String(dacValue), 4);
debugOutput(" fuer eine TempDiff von " + String(tempDiff) + " K", 4);
debugOutput(" fuer eine Spannung von " + String(setVoltage) + " V", 4);
}
Fritzbox- / DECT-ULE- spezifische Einstellungen
Hier endet der hardwareunabhängige Teil. Ab jetzt geht es um das Auslesen der Thermostate von AVM/Eurotronic.
Die Identifizierung der Thermostate erfolgt über die AIN-Nummern. Diese muss man einmal im Webinterface der Fritzbox nachschlagen. Ich speichere sie im Array HEIZKOERPERREGLER
und nutze für die Iteration durch das Array die Anzahl der Array-Elemente anzahlHKR
. Die relevanten Variablendeklarationen (Abschnitt 2):
// Die AIN (Actor Identificaion Number) der Devices findet sich im FritzBox Webinterface
const String ain_wohnzimmer = "12345 9876541";
const String ain_kueche = "12345 9876542";
const String ain_bad = "12345 9876543";
const String ain_esszimmer = "12345 9876544";
const String ain_schlafzimmer = "12345 9876545";
String HEIZKOERPERREGLER[] = { ain_wohnzimmer, ain_kueche, ain_bad, ain_esszimmer, ain_schlafzimmer};
int anzahlHKR = (sizeof(HEIZKOERPERREGLER) / sizeof(HEIZKOERPERREGLER[0]));
Zum Auslesen per API benötigen alle Schnittstellen Zugangsdaten eines Fritzbox-Nutzers. Ich habe einen gesonderten Nutzer angelegt. Die Daten hinterlege ich (wie die WLAN Credentials) in der Datei secrets.h
(siehe ESP-WLAN-Tutorial). Geschmackssache. Wer will, kann die Werte auch direkt hier eingeben.
const char* fuser = SECRET_FUSER; // The username if you created an account, "admin" otherwise
const char* fpass = SECRET_FPASS; // The password for the aforementioned account.
Soweit der allgemeine Teil. Ab jetzt gibt es zwei Möglichkeiten, auf die AVM- und Eurotronic-DECT-Thermostate zuzugreifen: AVM Home Automation (AHA)-Schnittstelle oder TR064-Schnittstelle. Es gibt zwei Varianten:
“AVM Home Automation” (AHA): Ein AVM-eigener SOAP-Webservice (XML-basiert). Fertige Bibliotheken habe ich dafür nicht gefunden, aber es gab einige Projekte, die die nötigen Methoden implementiert hatten. Es sind 5 Funktionen nötig, um eine Session sinnvoll aufzubauen. Bei mir dauert das Auslesen von 10 Thermostaten insgesamt ca. 12s. Es können auch neue Soll-Temperaturen über dieses Protokoll geschrieben werden (nicht Bestandteil dieses Projekts). Es ist schneller und flexibler als TR-064, dafür aber etwas mehr Code nötig (keine fertigen Bibliotheken).
TR-064: Ein Standard, um über das Netzwerk Geräte zu konfigurieren. Nutzt ebenso SOAP. Es gibt fertige Bibliotheken, so dass man mit dem eigentlichen Protokoll gar nicht viel mitbekommt: mit ca. zehn zusätzlichen Zeilen Code ist das Projekt schnell fertig. Bei mir dauert es allerdings 1:30min, wenn ich die 10 Thermostate auslese. Das Einstellen von neuen Soll-Temperaturen geht hiermit auch nicht. In der Praxis setze ich das nicht ein - für “mal schnell” etwas auslesen sind diese paar Zeilen Code vielleicht der bessere Weg als AHA.
/* Befehle
Funktion | TR064-Befehl | AHA Befehl:
===========================================================================
Gemessene Temperatur am Sensor | isTemp
manuell eingestellte Temperatur | hkrSetTemp | hkrtsoll
Konforttemperatur | thkrkomfort
gethkrabsenk*/
Auslesen per AHA
Die erste Alternative, die zur Auslesung genutzt werden kann, sind die AHA-Kommandos.
Der fertige Programmcode findet sich hier.
AVM nutzt mit seinen Smarthome-Geräten eine eigene “AVM Home Automation” (AHA)-Schnittstelle. Die Authentifizierung läuft hier über das Challenge-Response-Verfahren ab.
Hierbei wird nicht das Passwort selbst übermittelt, sondern das Passwort und eine zufällige vom AHA-Server vergebene Zeichenfolge werden gemeinsam gehasht:
Der Client ruft die zufällige Zeichenfolge (“Challenge”) beim Server ab.
Der Client wandelt die Zeichenfolge + Passwort in einen Hash um (Response).
Der Client sendet diesen Hash (Response) an den Server, der per bekanntem Passwort und Hashmethode prüft, ob die Response mit dem korrekten Passwort gebildet wurde.
Der Server vergibt eine SessionID und sendet diese an den Client.
Der Client nutzt diese SesseionID für alle weiteren Anfragen, die er an den Server hat.
Am Ende meldet sich der Client ab.
Im UML-Sequenzdiagramm lässt sich das etwa so darstellen:

Ich habe Ablauf und den Code v.a. aus dem Projkt von Sergej Müller eine Auslesung einer schaltbaren Steckdose per AHA exemplarisch durchgeführt
//-------------------------------------------------------------------------------------
// AHA ruft per HTTPClient die Daten ab
//-------------------------------------------------------------------------------------
#include <HTTPClient.h>
Die relevanten Einträge in die loop()
(Abschnitt 4):
void loop() {
//...
if (((millis() - currentMillis) > CHECK_MILLISECONDS) || (currentMillis == 0)) {
/* Variante Auslesen über AHA Interface*/
response = getAhaInfo(HEIZKOERPERREGLER[i], "hkrtsoll");
sollTemp = response.toFloat() / 2;
response = getAhaInfo(HEIZKOERPERREGLER[i], "temperature");
istTemp = response.toFloat() / 10;
hkrDiff = relevantTempDiff(istTemp, sollTemp);
response = getAhaInfo(HEIZKOERPERREGLER[i], "switchname");
debugOutput("Name: " + String(response) + " / AHA-Soll: " + String(sollTemp) + " / AHA-Ist: " + String(istTemp), false);
if (hkrDiff > maxDiff) {
maxDiff = hkrDiff;
debugOutput("Neue Maximaldifferenz: " + String(maxDiff), false);
}
}
writeTemperature(maxDiff);
loopLogText = "";
digitalWrite(PIN_OPERATING_LED, HIGH); //zeigt an, dass er TempDiff abfragt
float sollTemp = 0; // alle Variablen zurücksetzen
float istTemp = 0;
float maxDiff = 0;
float hkrDiff = 0;
String hkrName = "";
HTTPClient http;
String sessionID = getNewLoginSessionID(http);
for (int i = 0; i < anzahlHKR; i++) {
// Die eingelesenen Rohwerte müssen noch umgewandelt werden, um jeweils in Grad vorzuliegen
sollTemp = getAhaAttribute("hkrtsoll", HEIZKOERPERREGLER[i], sessionID, http).toFloat() / 2;
istTemp = getAhaAttribute("temperature", HEIZKOERPERREGLER[i], sessionID, http).toFloat() / 10;
hkrDiff = relevantTempDiff(istTemp, sollTemp);
hkrName = getAhaAttribute("switchname", HEIZKOERPERREGLER[i], sessionID, http);
debugOutput("Name: " + String(hkrName) + " / AHA-Soll: " + String(sollTemp) + " / AHA-Ist: " + String(istTemp), 4);
if (hkrDiff > maxDiff) {
maxDiff = hkrDiff;
debugOutput("Neue Maximaldifferenz: " + String(maxDiff), 4);
}
}
ahaLogOut(sessionID, http);
writeTemperature(maxDiff);
digitalWrite(PIN_OPERATING_LED, LOW);
currentMillis = millis();
}
}
Zunächst muss für die Kommunikation eine neue Session erstellt werden, dazu gehört, dass der Client vom Server
eine Challenge abfragt (eine zufällige Zeichenfolge): FUnktion
getAhaChallenge()
aus dieser mit Hilfe seines Passworts eine Response erstellt: Funktion
getCalculatedResponse()
. Mit Hilfe der Response kann der Server püber die bekannten Challenge die Credentials verifizieren.Bei erfolgreicher Verifizierung und Authentifizierung durch den Server kann der Client die Response gegen eine SessionID tauschen:
getAhaSessionID()
Mit Hilfe der SessionID kann der Client dann weitere Abfragen gegen die API vornehmen: Funktion:
getAhaAttribute()
Am Ende wird die Session beendet: Funktion
ahaLogOut()
Die einzelnen Funktionen (alle müssen in Abschnitt 5):
getNewLoginSessionID()
fasst die Schritte der Session-Erstellung zusammen:
String getNewLoginSessionID(HTTPClient& http) {
String sessionId= "";
debugOutput("Starte AHA Session ", 5);
String challenge = getAhaChallenge(http);
if (challenge != "") {
String response = getCalculatedResponse(challenge);
sessionId = getAhaSessionID(response, http);
}
return sessionId;
}
Die Challenge holt diese Methode per http ab:
String getAhaChallenge(HTTPClient& http) {
// Get Challenge
http.begin("http://fritz.box/login_sid.lua");
int retCode = http.GET();
if (retCode != 200) {
debugOutput("[AHA] Get Challenge failed! " + String(retCode), 3);
return "";
}
debugOutput("[AHA] Getting Challenge ", 6);
String result = http.getString();
String challenge = result.substring(result.indexOf("<Challenge>") + 11, result.indexOf("<Challenge>") + 19);
return challenge;
}
Den Antwort Hashwert berechnet die folgende Funktion:
String getCalculatedResponse(String challenge) {
// Calculate Response
debugOutput("[AHA] Calculating response ", 6);
String reponseASCII = challenge + "-" + fpass;
String responseHEX = "";
for (unsigned int i = 0; i < reponseASCII.length(); i++) {
responseHEX = responseHEX + String(reponseASCII.charAt(i), HEX) + "00";
}
MD5Builder md5;
md5.begin();
md5.addHexString(responseHEX);
md5.calculate();
String response = challenge + "-" + md5.toString();
return response;
}
Hier wir die Response gegen die SessionID eingetauscht:
String getAhaSessionID(String response, HTTPClient& http) {
// Login and get SID
debugOutput("[AHA] Get Session ID ", 6);
http.begin("http://fritz.box/login_sid.lua?user=" + String(fuser) + "&response=" + response);
int retCode = http.GET();
if (retCode != 200) {
debugOutput("[AHA] Get SessionID failed! " + String(retCode), 3);
return "";
}
String result = http.getString();
String sid = result.substring(result.indexOf("<SID>") + 5, result.indexOf("<SID>") + 21);
debugOutput("[AHA] Session established", 6);
return sid;
}
Die Logout-Funktion:
void ahaLogOut(String sid, HTTPClient& http) {
http.begin("http://fritz.box/login_sid.lua?logout=1&sid=" + sid);
http.GET();
String result = http.getString();
http.end();
//debugOutput("Antwort auf Logout:");
//debugOutput(result);
}
Und schließlich: die Abfrage-Methode. Deswegen wollen wir ja die Session eröffnen:
String getAhaAttribute(String command, String AIN, String sid, HTTPClient& http) {
String ainparam = "";
if (AIN != "") {
ainparam = "&ain=" + AIN;
}
String request = "http://fritz.box/webservices/homeautoswitch.lua?switchcmd=get" + command + ainparam + "&sid=" + sid;
http.begin(request);
int retCode = http.GET();
if (retCode != 200) {
debugOutput("[AHA] Den Wert " + command + " zu lesen (" + request + ") ist gescheitert mit Fehlercode: " + String(retCode), 3);
return "";
}
String result = http.getString();
debugOutput("[AHA] Lese "+command+": " + result, 6);
return result;
}
Damit ist das Programm eigentlich fertig und Einsatzbereit. Aber wo ich schon so weit war, wollte ich doch wenigstens mal ausprobieren, ob ich auch Werte ändern kann:
Exkurs: Einstellen der Temperaturen an den Thermostaten
Diesen Teil habe ich nur probehalber aktiviert - mittelfristig wird ein anderer ESP sich vielleicht um die Thermostate kümmern. Wünschenswert wäre z.B. eine Zeitschaltung, die am Vortag aktiviert wird (damit sie nicht im Urlaub und bei Abwesenheit durchläuft).
Wie funktioniert das setzen neuer Soll-Temperaturen? Im Prinzip genauso wie das Auslesen: ich eröffne eine Session und sende ein Kommando.
Bei bereits geöffneter Session wären es diese beiden Zeilen Code, die die Funktion unten aufruft:
int newTemp = 11;
setAhaAttribute( "hkrtsoll", String(int(newTemp * 2)), HEIZKOERPERREGLER[i], sessionID, http);
String setAhaAttribute(String command, String param, String AIN, String sid, HTTPClient& http) {
http.begin("http://fritz.box/webservices/homeautoswitch.lua?switchcmd=set" + command + "&ain=" + AIN + "¶m=" + param + "&sid=" + sid);
int retCode = http.GET();
if (retCode != 200) {
debugOutput("Den Wert " + command + " auf " + param + " setzen ist gescheitert mit Fehlercode: " + String(retCode), 3);
//return "";
}
String result = http.getString();
debugOutput("Antwort des Schreibens :" + result, 5);
return result;
}
Es gibt im Code eine zweite Variante, die für einzelne Aufrufe besser geeignet ist: diese baut selbständig eine Session auf: setTempAhaoInSingleSession()
.
Auslesen per TR064
Das Auslesen der Schnittstelle TR064 ist vergleichsweise einfach und Dank der vorhandenen Bibliothek mit wenig Code möglich.
Der fertige Programmcode für diese Variante findet sich hier.
Ich habe mich an das gut dokumentierte Beispiel zum Auslesen über TR-064 von fay.tv gehalten (findet sich unter diesem Link).
Die relevanten Imports + Variablendeklarationen (Abschnitt 1 + 2): Es müssen noch die IP-Adresse der Fritzbox, und der Port der TR-064 Schnittstelle angegeben werden (hier wieder über die Datei secrets.h
, kann aber auch direkt eingegeben werden).
//-------------------------------------------------------------------------------------
// FritzBox Smarthome-Settings für TR064
// Die TR064-Dokumentation habe ich folgendem Projekt entnommen: http://www.fay.tv/fritzbox-api-mit-esp32/
//-------------------------------------------------------------------------------------
#include <tr064.h>
const char* IP = SECRET_IP; // IP address of your router. This should be "192.168.179.1" for most FRITZ!Boxes
const int PORT = SECRET_PORT; // Port of the API of your router. This should be 49000 for all TR-064 devices.
TR064 connection(PORT, IP, fuser, fpass); // TR-064 connection
Die relevanten Einträge in die setup()
(Abschnitt 3):
void setup() {
//... Nachdem alle WLAN-Verbindungen stehen usw. muss das hier eingefügt werden:
connection.init(); // TR064
// Bei Problemen kann hier die Debug Ausgabe aktiviert werden
// connection.debug_level = DEBUG_VERBOSE;
}
In der loop()
Methode (Abschnitt 4) muss eigentlich nur ein Methodenaufruf ergänzt werden, der die jeweils relevante Temperaturdifferenz zurückgibt: hkrDiff = getTempDiffTr064(HEIZKOERPERREGLER[i]);
.
void loop() {
//...
if (((millis() - currentMillis) > CHECK_MILLISECONDS) || (currentMillis == 0)) {
//...
for (int i = 0; i < anzahlHKR; i++) {
/* Variante Auslesen über AHA Interface*/
hkrDiff = getTempDiffTr064(HEIZKOERPERREGLER[i]);
if (hkrDiff > maxDiff) {
maxDiff = hkrDiff;
debugOutput("Neue Maximaldifferenz: " + String(maxDiff), false);
}
}
writeTemperature(maxDiff);
}
}
Die relevante neue Funktionen (Abschnitt 5):
float getTempDiffTr064(String AIN) { // nutzt die TR064-Schnittstelle
String paramsb[][2] = { { "NewAIN", AIN } };
String reqb[][2] = { { "NewDeviceName", "" }, { "NewHkrIsTemperature", "" }, { "NewHkrSetTemperature", "" } };
connection.action("urn:dslforum-org:service:X_AVM-DE_Homeauto:1", "GetSpecificDeviceInfos", paramsb, 1, reqb, 3);
String name = reqb[0][1];
float hkrIsTemp = reqb[1][1].toInt() / 10.0;
float hkrSetTemp = reqb[2][1].toInt() / 10.0;
float diff = relevantTempDiff(hkrIsTemp, hkrSetTemp);
debugOutput(" -----------------------", 5);
debugOutput("Name: " + String(name), 5);
debugOutput("Ist: " + String(hkrIsTemp) + "C / Soll: " + String(hkrSetTemp) + "C / Diff: "+ String(diff) + "", 5);
debugOutput(" -----------------------", 5);
return diff;
}
Ohne die debugOutput()
-Zeilen bleibt fast nichts übrig von der Funktion: ist also wirklich schnell implementiert!
Die Log-Einträge sehen etwa so aus:
[2023-02-28_17:44:33] [DEBUG] -----------------------
[2023-02-28_17:44:33] [DEBUG] Name: Küche
[2023-02-28_17:44:33] [DEBUG] Ist: 19.50C / Soll: 20.00C / Diff: 0.50
[2023-02-28_17:44:33] [DEBUG] -----------------------
[2023-02-28_17:44:33] [DEBUG] Neue Maximaldifferenz: 0.50
[2023-02-28_17:44:43] [DEBUG] -----------------------
[2023-02-28_17:44:43] [DEBUG] Name: Vorne
[2023-02-28_17:44:43] [DEBUG] Ist: 20.50C / Soll: 20.00C / Diff: -0.50
[2023-02-28_17:44:43] [DEBUG] -----------------------
[2023-02-28_17:44:53] [DEBUG] -----------------------
[2023-02-28_17:44:53] [DEBUG] Name: Esszimmer
[2023-02-28_17:44:53] [DEBUG] Ist: 17.00C / Soll: 17.00C / Diff: 0.00
[2023-02-28_17:44:53] [DEBUG] -----------------------
[2023-02-28_17:44:54] [DEBUG] -----------------------
[2023-02-28_17:44:54] [INFO] Stelle den DAC-Wert auf 65
[2023-02-28_17:44:54] [INFO] fuer eine TempDiff von 0.50 K
[2023-02-28_17:44:54] [INFO] fuer eine Spannung von 0.85 V
Links und weitere Infos
AVM-Libraries allgemein:
- https://avm.de/service/schnittstellen/
TR064:
Paul Fay hat eine gute Beschreibung, wie die Kommunikation per TR064-Schnittstelle funktioniert in seinem Blog unter diesem Link.
Ein weiteres, ähnliches Projekt mit weiteren Methoden hat RoSchmi hireveröffentlicht](https://github.com/RoSchmi/Esp32_Fritzbox_TR064_FritzDect_Controller)
Die einzelnen angebotenen Aktionen und Variablen dieser Schnittstelle findet sich auf der eigenen Fritzbox unter: http://fritz.box:49000/x_homeautoSCPD.xml (Darin findet sich z.B. dass lediglich der AIN, DeviceName und Index über diese Schnittstelle änderbar sind:
<direction>in</direction>
)Die Veröffentlichte Dokumentation von AVM: https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/x_homeauto.pdf
AHA-Schnittstelle:
Sergej Müller hat hier eine Auslesung einer schaltbaren Steckdose per AHA exemplarisch durchgeführt
Die zweite Quelle hat auch Sergej Müller verlinkt: Sven s Blog open4me.de Ich fange hier mal mit der Liste der Tutorials an, die ich mir durchgesehen hatte und von denen ich hier mitunter schamlos abgeschrieben haben:
Die wichtigste Quelle ist natürlich die Schnittstellendokumentation der AHA-Schnittstelle von AVM selbst
Eine Übersicht zahlloser Implementierungen dieser Schnittstelle findet sich im Wiki von boxmatrix.info
Sehr weitreichendes Tutorial zu AHA-Schnittstelle: https://github.com/tidklaas/Hephaistos-AHA
Ebenso gutes Projekt: https://github.com/planetk/ArduinoFritzApi
Quellen und offene Ressourcen (OER)
Die Ursprungstexte (als Markdown), Grafiken und zugrunde liegende Diagrammquelltexte finden sich (soweit möglich in weiterbearbeitbarer Form) in folgendem git-Repository:
https://gitlab.com/oer-informatik/mcu/iot-therme.
Sofern nicht explizit anderweitig angegeben sind sie zur Nutzung als Open Education Resource (OER) unter Namensnennung (H. Stein, oer-informatik.de) freigegeben gemäß der Creative Commons Namensnennung 4.0 International Lizenz (CC BY 4.0).