Forum KNX francophone / English KNX forum
Script PHP pour connexion par tunnel IP KNX - Version imprimable

+- Forum KNX francophone / English KNX forum (https://www.knx-fr.com)
+-- Forum : Français (https://www.knx-fr.com/forumdisplay.php?fid=3)
+--- Forum : Logiciels libres (linknx, knxweb, pKNyX,...) (https://www.knx-fr.com/forumdisplay.php?fid=10)
+--- Sujet : Script PHP pour connexion par tunnel IP KNX (/showthread.php?tid=16448)



Script PHP pour connexion par tunnel IP KNX - Smart Guido 73 - 15/09/2026

Bonjour,

Je mets à disposition ce petit code PHP pour ceux qui souhaitent créer davantage de choses avec PHP et KNX. Il établit une connexion par tunnel IP, envoie des données pour les DPT 1, 5 ou 9, puis se déconnecte.
Cela n'a rien à voir non plus avec mon wrapper, c'est totalement indépendant.


Code PHP :
<?php
header
('Content-Type: text/html; charset=utf-8');

class 
KNXTunnel {
    private string $ip;
    private int $port;
    private $socket null;
    private int $channelId 0;
    private int $sequenceNumber 0;

    public function __construct(string $ipint $port 3671) {
        $this->ip $ip;
        $this->port $port;
    }

    private function log(string $msg): void {
        echo $msg "<br>\n";
        if (ob_get_level() > 0) { @ob_flush(); }
        @flush();
    }

    private function getLocalIp(): string {
        $sock socket_create(AF_INETSOCK_DGRAMSOL_UDP);
        socket_connect($sock$this->ip$this->port);
        socket_getsockname($sock$addr);
        socket_close($sock);
        return $addr;
    }

    private function statusText(int $status): string {
        return match ($status) {
            0x00 => 'E_NO_ERROR',
            0x22 => 'E_CONNECTION_TYPE (Verbindungstyp nicht unterstützt)',
            0x23 => 'E_CONNECTION_OPTION (Option nicht unterstützt)',
            0x24 => 'E_NO_MORE_CONNECTIONS (max. Anzahl Tunnel-Verbindungen erreicht)',
            0x26 => 'E_DATA_CONNECTION',
            0x27 => 'E_KNX_CONNECTION',
            0x29 => 'E_NO_MORE_UNIQUE_CONNECTIONS',
            default => 'unbekannt (0x' sprintf('%02X'$status) . ')',
        };
    }

    public function connect(): bool {
        $this->socket socket_create(AF_INETSOCK_DGRAMSOL_UDP);
        if (!$this->socket) {
            $this->log("❌ Socket konnte nicht erstellt werden : " socket_strerror(socket_last_error()));
            return false;
        }

        socket_set_option($this->socketSOL_SOCKETSO_RCVTIMEO, ['sec' => 3'usec' => 0]);

        $myPcIp $this->getLocalIp();
        $this->log("ℹ️ Lokale IP : {$myPcIp}");

        if (!@socket_bind($this->socket$myPcIp0)) {
            $this->log("❌ Fehler beim Binden an PC IP {$myPcIp} : " socket_strerror(socket_last_error($this->socket)));
            return false;
        }

        $localPort 0;
        socket_getsockname($this->socket$myPcIp$localPort);
        $ipParts array_map('intval'explode('.'$myPcIp));

        $hpaiControl pack('C*'0x080x01$ipParts[0], $ipParts[1], $ipParts[2], $ipParts[3]) . pack('n'$localPort);

        $hpaiData pack('C*'0x080x01$ipParts[0], $ipParts[1], $ipParts[2], $ipParts[3]) . pack('n'$localPort);

        $header "\x06\x10\x02\x05\x00\x1a";

        $cri "\x04\x04\x02\x00";

        $connectRequest $header $hpaiControl $hpaiData $cri;

        socket_sendto($this->socket$connectRequeststrlen($connectRequest), 0$this->ip$this->port);
        $this->log("? CONNECT_REQUEST gesendet an {$this->ip}:{$this->port}");

        $buffer '';
        $from '';
        $fromPort 0;
        $bytes = @socket_recvfrom($this->socket$buffer10240$from$fromPort);

        if ($bytes === false) {
            $this->log("❌ recvfrom Fehler : " socket_strerror(socket_last_error($this->socket)));
            return false;
        }

        if ($bytes 8) {
            $this->log("❌ Zu kurze Antwort empfangen ({$bytes} Bytes).");
            return false;
        }

        $this->log("? Rohdaten empfangen von {$from}:{$fromPort} (" $bytes " Bytes): " bin2hex($buffer));

        $serviceType = (ord($buffer[2]) << 8) | ord($buffer[3]);

        if ($serviceType !== 0x0206) {
            $this->log("❌ Unerwarteter Service Type : 0x" sprintf('%04X'$serviceType) . " (erwartet : 0x0206 CONNECT_RESPONSE)");
            return false;
        }

        $status ord($buffer[7]);
        if ($status !== 0x00) {
            $this->log("❌ IP Gateway abbruch, Status : 0x" sprintf('%02X'$status) . " (" $this->statusText($status) . ")");
            return false;
        }

        $this->channelId ord($buffer[6]);
        $this->sequenceNumber 0;
        $this->log("✅ IP Gateway Tunnel erfolgreich aufgebaut! (Channel ID : {$this->channelId})");
        return true;
    }

    private function parseGroupAddress(string $ga): string {
        $parts explode('/'$ga);
        if (count($parts) === 3) {
            $main = (int)$parts[0] & 0x1F;
            $middle = (int)$parts[1] & 0x07;
            $sub = (int)$parts[2] & 0xFF;
            $val = ($main << 11) | ($middle << 8) | $sub;
            return pack('n'$val);
        }
        return "\x00\x00";
    }

    public function encodeDPT(string $dptType$value): string {
        switch (strtoupper($dptType)) {
            case 'DPT1':
                return pack('C'0x80 | ($value 0));

            case 'DPT5':
                $val max(0min(255, (int)$value));
                return pack('CC'0x80$val);

            case 'DPT9':
                $v = (float)$value;
                $sign = ($v 0) ? 0x8000 0x0000;
                $v abs($v);
                $mantissa = (int)round($v 100);
                $exponent 0;
                while ($mantissa 2047) {
                    $mantissa >>= 1;
                    $exponent++;
                }
                if ($sign$mantissa = -$mantissa;
                $raw $sign | (($exponent 0x0F) << 11) | ($mantissa 0x07FF);
                return pack('C*'0x80, ($raw >> 8) & 0xFF$raw 0xFF);

            default:
                throw new Exception("DPT Typ '{$dptType}' nicht unterstützt.");
        }
    }

    public function send(string $groupAddressstring $dptType$value): bool {
        if ($this->channelId === 0) {
            $this->log("❌ send() ohne aktiven Tunnel aufgerufen, zuerst connect() erfolgreich ausführen.");
            return false;
        }

        $gaBytes $this->parseGroupAddress($groupAddress);
        $payload $this->encodeDPT($dptType$value);
        $payloadLen strlen($payload);

        $cemi "\x11\x00\xbc\xe0\x00\x00" $gaBytes pack('C'$payloadLen) . "\x00" $payload;
        $totalLen strlen($cemi);

        $header "\x06\x10\x04\x20" pack('n'$totalLen);
        $connectionHeader pack('CC'0x04$this->channelId) . pack('CC'$this->sequenceNumber0x00);

        $packet $header $connectionHeader $cemi;

        socket_sendto($this->socket$packetstrlen($packet), 0$this->ip$this->port);
        $this->log("? Telegramm an <b>{$groupAddress}</b> gesendet! (Seq: {$this->sequenceNumber})");

        $ack $this->receiveACK();
        $this->sequenceNumber = ($this->sequenceNumber 1) % 256;

        return $ack;
    }

    private function receiveACK(): bool {
        $buffer '';
        $from '';
        $port 0;
        $bytes = @socket_recvfrom($this->socket$buffer10240$from$port);

        if ($bytes === false) {
            $this->log("❌ Kein ACK erhalten (Timeout) : " socket_strerror(socket_last_error($this->socket)));
            return false;
        }

        if ($bytes >= 10 && ord($buffer[2]) === 0x04 && ord($buffer[3]) === 0x21) {
            $status ord($buffer[9]);
            if ($status === 0x00) {
                $this->log("? ACK vom IP Gateway erhalten.");
                return true;
            }
            $this->log("⚠️ ACK erhalten, aber mit Fehlerstatus: 0x" sprintf('%02X'$status));
            return false;
        }

        $this->log("⚠️ Unerwartete Antwort statt ACK erhalten : " bin2hex($buffer));
        return false;
    }

    public function disconnect(): void {
        if ($this->channelId !== && $this->socket) {
            $disconnectRequest "\x06\x10\x02\x09\x00\x10" pack('CC'$this->channelId0x00) . "\x08\x01\x00\x00\x00\x00\x00\x00";
            @socket_sendto($this->socket$disconnectRequeststrlen($disconnectRequest), 0$this->ip$this->port);
            $this->log("? IP Gateway Tunnel Verbindung (Channel {$this->channelId}) ordnungsgemäß getrennt.");
            $this->channelId 0;
        }
        if ($this->socket) {
            @socket_close($this->socket);
            $this->socket null;
        }
    }
}

$Ip '192.168.178.42';
$knx = new KNXTunnel($Ip);

try {
    if ($knx->connect()) {
        $knx->send('1/0/0''DPT1'true);
    }
} finally {
    $knx->disconnect();