Klimaanlage (TCL) über Infra Rot Steuern (Fernbedienungssimulator)

Hallo,

habe meine Klimaanlage via Infra-Rot (Hersteller TCL) an Symcon angebunden.

Die (indirekte) Protokollbeschreibung lässt sich hier aus den Quellcodes entziffern:

Ich habe die IR-Diode allerdings an meinem Symcon Raspi hängen, deshalb läuft es nicht so, wie für den ESP beschrieben.

Raspian hat das Program LIRC, welches zum IR-Senden und Empfangen gedacht ist. Anleitungen, wie die Schaltung dafür aussehen sollte (Widerstände, Transistor, IR Sende und Empfangsdioden) gibt es einige, ich habe mich hier belesen: Raspberry Pi Fernbedienung: Infrarot Steuerung einrichten. Vorwiderstand für die Dioden (habe 2 in Reihe, da ich noch eine andere Klimaanlage mit anderem Protokoll ansteuere) habe ich deutlich kleiner gewählt, die Sendedioden sind durch Verkabelung im Haus bis vor die Empfangsdioden der Innenteile der Klima verlegt.

Was ich hier mit Euch teilen möchte, ist der Code für das „Zusammenstellen“ des IR-Signals.

Lirc funktioniert so, dass in einer im Ordner /etc/lirc/lircd.conf.d/ abzulegenden Datei mit der Endung *.lircd.conf der „raw“-Code abgelegt wird und dann das Senden via Console-Befehl gestartet und gestoppt wird. Die Datei wird durch Symcon bei meiner Vorgehensweise jedes mal für den gewünschten Sendebefehl neu „überschrieben“ und dann die Übertragung gestartet. Funktioniert sehr zuverlässig.

So, hier nun der Code zur Ermittlung der „Rohdaten“ für die LIRC-Datei:

<?php


/*
Berechnet Befehlszeile für IR TCL in Stube und sendet diese via Lirc zur Klima
Entschlüsselung der Fernbedienung via GitHub und eigenen Messungen
https://github.com/crankyoldgit/IRremoteESP8266/blob/master/src/ir_Tcl.h

Idee:
IPS berechnet den Raw-Code, schreibt diesen auf den Raspi in das Lirc-Konf-Verzeichnis in eine extra datei, Fernbedienung TCL_Stube und der Befehl heisst command
Lirc wird neu gestartet, um die Datei zu laden
Mit Shellexec wird Lirc senden gestartet und gestoppt

Der Befehl besteht aus 14 Bytes und zwei Zeichen davor. Die 14 Bytes und die Vor-Zeichen sind entweder in Konstanten bzw. im Code gespeichert oder werden in Laufzeit berechnet


*/

#Konstanten einlesen

$Header_const= getvalue(16528);
$Byte00_const = getvalue(53131);
$Byte01_const = getvalue(14315);
$Byte02_const = getvalue(14661);
$Byte03_const = getvalue(20338);
$Byte04_const = getvalue(28144);
$Byte05_const = getvalue(36829);
$Byte06_const = getvalue(40818);
$Byte07_const = getvalue(53732);
$Byte08_const = getvalue(54586);
$Byte09_const = getvalue(55356);
$Byte10_const = getvalue(55605);
$Byte11_const = getvalue(59717);
$Byte12_const = getvalue(21451);
$DauerBitMark_const = getvalue(36844);
$Dauerhigh_const = getvalue(16522);
$Dauerlow_const = getvalue(50269);


#Code

#Byte 0-2 - Konstanten
    $Byte00 = $Byte00_const; #Konstant
    $Byte01 = $Byte01_const; #Konstant
    $Byte02 = $Byte02_const; #Konstant

# Byte 3, Bits 6-7 - Message Type (anderen Bits konstant)
    $MsgType_AcNormal  = "01";
    $MsgType_AcSpecial = "10";

    #$Byte03 = $MsgType_AcNormal.$Byte03_const; #Unklar, wann normale nachricht genutzt wird, hier nur Spezial
    $Byte03 = $MsgType_AcSpecial.$Byte03_const;
    
# Byte 4 - Konstante    
    $Byte04 = $Byte04_const; #Konstant

# Byte 5
/* Jeweils 1 Bit in Byte 5
    uint8_t Power           :1; Bit 5 - 1(an), 0(aus)
    uint8_t OffTimerEnabled :1; Bit 4 - 0 (aus) --> Standard in diesem Code, Timer wird über Symcon geregelt
    uint8_t OnTimerEnabled  :1; Bit 3 - 0 (aus) --> Standard in diesem Code, Timer wird über Symcon geregelt
    uint8_t Quiet           :1; Bit 2 - Piepsen an wenn 1
    uint8_t Light           :1; Bit 1 - Anzeige aus wenn 1
    uint8_t Econo           :1; Bit 0 - Economy-Modus wenn 1
*/
    $Power = intval(getvalue(55043));
    $Quiet = intval(getvalue(51313));
    $Light = intval(getvalue(32638));
    $Econo = intval(getvalue(15029));

    $OffTimerEnabled = 0; #Konstant
    $OnTimerEnabled= 0; #Konstant

    $Byte05 = $Byte05_const.$Power.$OffTimerEnabled.$OnTimerEnabled.$Quiet.$Light.$Econo;

#Byte 6
/* Belegung Byte 6 (Bit 0 und 1 konstant)

    uint8_t Mode            :4; Bit 4-7
    uint8_t Health          :1; Bit 3 --> an=1
    uint8_t Turbo           :1; Bit 2 --> an=1
    
    Wobei
    const uint8_t kTcl112AcHeat = 1;
    const uint8_t kTcl112AcDry =  2;
    const uint8_t kTcl112AcCool = 3;
    const uint8_t kTcl112AcFan =  7;
    const uint8_t kTcl112AcAuto = 8;
    
    #Big Endian
    $Mode_Heat = "1000"; #1
    $Mode_Dry = "0100"; #2
    $Mode_Cool = "1100"; #3
    $Mode_Fan = "1110"; #7
    $Mode_Auto = "0001"; #8
*/

    #Mode als Integer, dann nach 4-bit Binär und dann Big Endian
    $Mode = strrev(sprintf('%04b',getvalue(46860)));

    $Health = intval(getvalue(53039)); #Boolean nach Integer umwandeln
    $Turbo = intval(getvalue(27114)); #Boolean nach Integer umwandeln

    $Byte06 = $Mode.$Health.$Turbo.$Byte06_const;

#Byte 7
/* Belegung Byte 7

    uint8_t Temp            :4; Bit 4-7, Big endian und dann noch verkehrt herum
    uint8_t                 :4; const

    Temp
    Big Endian
    0 = 31°C
    15 = 16°C

    Integer ist in diese Werte umzurechnen    
    0000 (0)	31
    1000	30
    0100	29
    1100	28
    0010	27
    1010	26
    0110	25
    1110	24
    0001	23
    1001	22
    0101	21
    1101	20
    0011	19
    1011	18
    0111	17
    1111 (15)	16
*/
    #Temperatur von °C nach umgekehrtenwert (31-Temp), dann nach 4-bit Binär und dann Big Endian
    $Temperatur = strrev(sprintf('%04b',31 - getvalue(42469)));
    
    $Byte07 = $Temperatur.$Byte07_const;

#Byte 8
/* Belegung Byte 8 (Fan und Swing Vertikal)

    // Byte 8
    uint8_t Fan             :3; Bit 5-7
    uint8_t SwingV          :3; Bit 2-4
    uint8_t TimerIndicator  :1; Bit 1 #Konstant = 0
    uint8_t                 :1; Bit 0 #Konstant = 0

    const uint8_t kTcl112AcFanAuto = 000; = 0
    const uint8_t kTcl112AcFanMin  = 001; = 1
    const uint8_t kTcl112AcFanLow  = 010; = 2
    const uint8_t kTcl112AcFanMed  = 011; = 3
    const uint8_t kTcl112AcFanHigh = 101; = 5
    
    const uint8_t kTcl112AcSwingVOff =     000; = 0
    const uint8_t kTcl112AcSwingVHighest = 001; = 1 --> geht nicht
    const uint8_t kTcl112AcSwingVHigh =    010; = 2 --> geht nicht
    const uint8_t kTcl112AcSwingVMiddle =  011; = 3 --> geht nicht
    const uint8_t kTcl112AcSwingVLow =     100; = 4 --> geht nicht
    const uint8_t kTcl112AcSwingVLowest =  101; = 5 --> geht nicht
    const uint8_t kTcl112AcSwingVOn =      111; = 7

*/

    $Fan = sprintf('%03b',getvalue(54992)); #Integer in 3bit - Binär umwandeln
    
    $SwingV = sprintf('%03b',getvalue(31118));; #Integer in 3bit - Binär umwandeln
    
    $Byte08 = $Fan.$SwingV."0".$Byte08_const;

#Byte 9 bis 12 - Konstant
    $Byte09 = $Byte09_const; #als Konstant gewählt
    $Byte10 = $Byte10_const; #als Konstant gewählt
    $Byte11 = $Byte11_const; #Konstant
    $Byte12 = $Byte12_const; #Konstant

#Byte 13 = Berechnung Checksumme (jedes Byte wird erst Big endianisiert, dann zu Dezimal umgerechnet, dann alle addiert, dann wieder zu 8bit Bin und danach wieder little Endian)

    $Checksum = bindec(strrev($Byte00))+bindec(strrev($Byte01))+bindec(strrev($Byte02))+bindec(strrev($Byte03))+bindec(strrev($Byte04))+bindec(strrev($Byte05))+bindec(strrev($Byte06))+bindec(strrev($Byte07))+bindec(strrev($Byte08))+bindec(strrev($Byte09))+bindec(strrev($Byte10))+bindec(strrev($Byte11))+bindec(strrev($Byte12));
    $Checksum = substr(sprintf('%08b',$Checksum), -8);
    $Checksum = strrev($Checksum);
    setvalue(57787,$Checksum); #Checksumme für Debug in Variable schreiben

    $Byte13 = $Byte00.$Byte01.$Byte02.$Byte03.$Byte04.$Byte05.$Byte06.$Byte07.$Byte08.$Byte09.$Byte10.$Byte11.$Byte12.$Checksum; #komplette Nachricht als Binär
    
# Umwandeln in Impulszeiten
   
    $StrLength = strlen($Byte13); #Länge des Bit-Strings ermitteln
    
    #String durchlaufen und durch Pulszeiten ersetzen
        $StrIR = ""; #Stringvariable initialisieren

        for ($i = 0; $i < $StrLength; $i++) 
        {
            $StrIR = $StrIR.$DauerBitMark_const." "; # Start-BitMark setzen

            if ($Byte13[$i] == "1")
            {
                $StrIR=$StrIR.$Dauerhigh_const." "; #Pause für "1"
            }
            else
            {
                $StrIR=$StrIR.$Dauerlow_const." "; #Pause für "0"
            }

        }

    $StrIR = $Header_const." ".$StrIR.$DauerBitMark_const; #Header zufügen und Abschluß-Abtasten

    setvalue(45337,$StrIR); #Befehl für Debug in Variable schreiben

#Lirc-Datei Schreiben

    $Pfad         ="/etc/lirc/lircd.conf.d/"; # Lirc-Ordner auf dem Raspi
    $DateiPfad = $Pfad."tcl_temp.lircd.conf"; # Pfad und Name der Lirc-Datei

    /* Beispielaufbau der Lirc Datei

        begin remote
        
        name   TCL_Stube
        flags  RAW_CODES
        eps     30
        aeps   100
        
        ptrail   0
        repeat 0 0
        gap 100000
        
        begin raw_codes

        name command

        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx hier kommt der Pulse-Code hin

        end raw_codes
        end remote
    */

    #Text der Datei (Inhalt) zusammenstellen - \n ist immer ein Zeilenumbruch
        $LircText  = "begin remote";
        $LircText .= "\n";
        $LircText .= "\nname   TCL_Stube"; #Watchdog stoppen
        $LircText .= "\nflags  RAW_CODES";
        $LircText .= "\neps     30";
        $LircText .= "\naeps   100";
        $LircText .= "\n";
        $LircText .= "\nptrail   0";
        $LircText .= "\nrepeat 0 0";
        $LircText .= "\ngap 100000";
        $LircText .= "\n";
        $LircText .= "\nbegin raw_codes";
        $LircText .= "\n";
        $LircText .= "\nname command";
        $LircText .= "\n";
        $LircText .= "\n".$StrIR;
        $LircText .= "\n";
        $LircText .= "\nend raw_codes";
        $LircText .= "\nend remote";
        $LircText = trim($LircText);

    #Datei anlegen + füllen + speichern
        $LircDatei = fopen ($DateiPfad, "wb");              //Datei anlegen bzw. vorhandene überschreiben ohne Rückfrage 
        fwrite($LircDatei, $LircText);                      //Text darin abspeichern
        fclose($LircDatei);                                 //und schließen

    IPS_Sleep(100); #Pause

# Befehl senden

    shell_exec ("sudo systemctl restart lircd"); #Lirc neu starten, um Datei neu zu laden
    IPS_Sleep(100); #Pause

    $MessageOn = "sudo irsend SEND_start TCL_Stube command"; //An-Befehl für Senden in Endlosschleife
    $MessageOff = "sudo irsend SEND_stop TCL_Stube command"; //Aus-Befehl Abschalten Endlosschleife

    shell_exec ($MessageOn); //Schaltet das Senden einer IR-Sequenz als Endlosschleife an
    IPS_Sleep (1000); // lässt Sende-Schleife 1 Sekunde laufen
    shell_exec ($MessageOff); //stellt Schleife wieder ab

#Ende

Hier die Variablen und Konstanten:

Ich habe bei meinem TCL Klimaanlagen einfach den internen ESP8266 für die Cloud abgeklemmt und dafür einen eigenen ESP mit Tasmota angesteckt.
Die IR Sendediode liegt in der Nähe der Klimaempfangsdiode und ich kann so über IPSymcon direkt die Klima steuern.
Einfacher geht es kaum.
Viele Grüße,
Doc

Das klingt gut. Somit sparst Du Dir die Kabel durch die Bude und kannst von jeder Symcon - Archiketur aus steuern. Clever.

Hatte auch überlegt, ob ich den ESP-Weg gehe.

Genial wäre natürlich, dass wenn Du schon den ESP ersetzt hast, diesen nicht zum Ansteuern einer IR nimmt, welche dann hintenrum die Klima steuert, sondern direkt (so wie es der Cloud-ESP auch macht) die Klima steuerst.

Bezüglich einfacher… Jetzt, wo Du weißt, wie es geht, ja.
Hast ja schon etwas gebraucht, um es zu lösen, wenn ich mir den Thread so ansehe…

Mit einfacher meinte ich die HW, da nur der ESP benötigt wird, keine Spannungsversorgung usw., auch das Anschlusskabel hatte ich verwendet, also der ESP nur gesteckt wurde.
Probleme hatte ich glaube ich, da das Protokoll erst nicht das richtige war.
Die Klimas werden bei mir auch vom Netz getrennt, wenn sie nicht benötigt werden da sie über das Jahr gesehen ohne Nutzung doch im Standby eine kWh verbrauchen.