Hallo,
ich stelle diesen kleinen PHP Code bereit für die, die Interesse haben, mehr mit PHP und KNX zu erstellen. Er baut eine IP Tunneling Verbindung auf, sendet Daten für DPT 1, 5 oder 9 und trennt sich danach wieder.
Das hier hat auch nichts mit dem Wrapper von mir zu tun, es ist komplett eigenständig.
ich stelle diesen kleinen PHP Code bereit für die, die Interesse haben, mehr mit PHP und KNX zu erstellen. Er baut eine IP Tunneling Verbindung auf, sendet Daten für DPT 1, 5 oder 9 und trennt sich danach wieder.
Das hier hat auch nichts mit dem Wrapper von mir zu tun, es ist komplett eigenständig.
PHP-Code:
<?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();
}

