<?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 $ip, int $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_INET, SOCK_DGRAM, SOL_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_INET, SOCK_DGRAM, SOL_UDP);
if (!$this->socket) {
$this->log("❌ Socket konnte nicht erstellt werden : " . socket_strerror(socket_last_error()));
return false;
}
socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 3, 'usec' => 0]);
$myPcIp = $this->getLocalIp();
$this->log("ℹ️ Lokale IP : {$myPcIp}");
if (!@socket_bind($this->socket, $myPcIp, 0)) {
$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*', 0x08, 0x01, $ipParts[0], $ipParts[1], $ipParts[2], $ipParts[3]) . pack('n', $localPort);
$hpaiData = pack('C*', 0x08, 0x01, $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, $connectRequest, strlen($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, $buffer, 1024, 0, $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 ? 1 : 0));
case 'DPT5':
$val = max(0, min(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 $groupAddress, string $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 = 6 + 4 + strlen($cemi);
$header = "\x06\x10\x04\x20" . pack('n', $totalLen);
$connectionHeader = pack('CC', 0x04, $this->channelId) . pack('CC', $this->sequenceNumber, 0x00);
$packet = $header . $connectionHeader . $cemi;
socket_sendto($this->socket, $packet, strlen($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, $buffer, 1024, 0, $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 !== 0 && $this->socket) {
$disconnectRequest = "\x06\x10\x02\x09\x00\x10" . pack('CC', $this->channelId, 0x00) . "\x08\x01\x00\x00\x00\x00\x00\x00";
@socket_sendto($this->socket, $disconnectRequest, strlen($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();
}