UNPKG

knxnetjs

Version:

A TypeScript library for KNXnet/IP communication

255 lines 10.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.KNXNetDiscovery = void 0; const events_1 = require("events"); const dgram_1 = require("dgram"); const constants_1 = require("./constants"); class KNXNetDiscovery extends events_1.EventEmitter { constructor() { super(); this.discoveredDevices = new Map(); } async discover(options = {}) { const timeout = options.timeout || constants_1.KNX_CONSTANTS.DISCOVERY.DEFAULT_SEARCH_TIMEOUT; const searchResponseTimeout = options.searchResponseTimeout || constants_1.KNX_CONSTANTS.DISCOVERY.SEARCH_RESPONSE_TIMEOUT; this.discoveredDevices.clear(); return new Promise((resolve, reject) => { this.setupSocket() .then(() => { this.sendSearchRequest(); // Set timeout for collecting responses this.searchTimeout = setTimeout(() => { this.cleanup(); const devices = Array.from(this.discoveredDevices.values()); resolve(devices); }, timeout); // Listen for responses this.socket.on("message", (msg, rinfo) => { try { this.handleSearchResponse(msg, rinfo); } catch (error) { this.emit("error", error); } }); }) .catch(reject); }); } async setupSocket() { return new Promise((resolve, reject) => { this.socket = (0, dgram_1.createSocket)({ type: "udp4", reuseAddr: true }); this.socket.on("error", (err) => { this.emit("error", err); reject(err); }); this.socket.on("listening", () => { resolve(); }); this.socket.bind(0); // Bind to any available port }); } sendSearchRequest() { if (!this.socket) { throw new Error("Socket not initialized"); } const searchFrame = this.createSearchRequestFrame(); this.socket.send(searchFrame, constants_1.KNX_CONSTANTS.DEFAULT_PORT, constants_1.KNX_CONSTANTS.DEFAULT_MULTICAST_ADDRESS, (err) => { if (err) { this.emit("error", err); } }); } createSearchRequestFrame() { // KNXnet/IP Header (6 bytes) const header = Buffer.allocUnsafe(6); header.writeUInt8(constants_1.KNX_CONSTANTS.HEADER_SIZE, 0); // Header size header.writeUInt8(constants_1.KNX_CONSTANTS.KNXNETIP_VERSION, 1); // Version header.writeUInt16BE(constants_1.KNX_CONSTANTS.SERVICE_TYPES.SEARCH_REQUEST, 2); // Service type header.writeUInt16BE(14, 4); // Total length (6 + 8) // Discovery endpoint HPAI (8 bytes) const hpai = this.createHPAI(); return Buffer.concat([header, hpai]); } createHPAI() { const hpai = Buffer.allocUnsafe(8); hpai.writeUInt8(8, 0); // Structure length hpai.writeUInt8(0x01, 1); // Host protocol (UDP) // Use 0.0.0.0 to indicate any interface hpai.writeUInt8(0, 2); // IP address hpai.writeUInt8(0, 3); hpai.writeUInt8(0, 4); hpai.writeUInt8(0, 5); const port = this.socket?.address()?.port || 0; hpai.writeUInt16BE(port, 6); // Port return hpai; } handleSearchResponse(msg, rinfo) { if (msg.length < constants_1.KNX_CONSTANTS.HEADER_SIZE) { return; } const headerSize = msg.readUInt8(0); const version = msg.readUInt8(1); const serviceType = msg.readUInt16BE(2); const totalLength = msg.readUInt16BE(4); if (headerSize !== constants_1.KNX_CONSTANTS.HEADER_SIZE || version !== constants_1.KNX_CONSTANTS.KNXNETIP_VERSION || serviceType !== constants_1.KNX_CONSTANTS.SERVICE_TYPES.SEARCH_RESPONSE) { return; } try { const endpoint = this.parseSearchResponse(msg, rinfo); const key = `${endpoint.ip}:${endpoint.port}`; if (!this.discoveredDevices.has(key)) { this.discoveredDevices.set(key, endpoint); this.emit("deviceFound", endpoint); } } catch (error) { this.emit("error", error); } } parseSearchResponse(msg, rinfo) { let offset = constants_1.KNX_CONSTANTS.HEADER_SIZE; // Parse Control Endpoint HPAI const controlHpaiLength = msg.readUInt8(offset); const controlEndpoint = this.parseHPAI(msg, offset); offset += controlHpaiLength; // Parse Device Information DIB const deviceInfoLength = msg.readUInt8(offset); const deviceInfoDescType = msg.readUInt8(offset + 1); if (deviceInfoDescType !== 0x01) { throw new Error(`Expected Device Info DIB (0x01), got 0x${deviceInfoDescType.toString(16)}`); } const deviceInfo = this.parseDeviceInfoDIB(msg, offset + 2, deviceInfoLength - 2); offset += deviceInfoLength; // Parse Supported Service Families DIB const serviceFamiliesLength = msg.readUInt8(offset); const serviceFamiliesDescType = msg.readUInt8(offset + 1); if (serviceFamiliesDescType !== 0x02) { throw new Error(`Expected Service Families DIB (0x02), got 0x${serviceFamiliesDescType.toString(16)}`); } const serviceFamilies = this.parseServiceFamiliesDIB(msg, offset + 2, serviceFamiliesLength - 2); return { name: deviceInfo.friendlyName || `KNX Device ${rinfo.address}`, ip: rinfo.address, port: controlEndpoint.port, capabilities: this.calculateCapabilities(serviceFamilies), deviceState: deviceInfo.deviceStatus || 0, knxAddress: deviceInfo.knxAddress, macAddress: deviceInfo.macAddress, serialNumber: deviceInfo.serialNumber, projectInstallationId: deviceInfo.projectInstallationId, friendlyName: deviceInfo.friendlyName, }; } parseHPAI(msg, offset) { const length = msg.readUInt8(offset); const hostProtocol = msg.readUInt8(offset + 1); const ip = [ msg.readUInt8(offset + 2), msg.readUInt8(offset + 3), msg.readUInt8(offset + 4), msg.readUInt8(offset + 5), ].join("."); const port = msg.readUInt16BE(offset + 6); return { hostProtocol, address: ip, port, }; } parseDeviceInfoDIB(msg, offset, length) { const info = {}; if (length < 50) { throw new Error(`Device Info DIB too short: ${length} bytes, expected at least 50`); } // KNX medium (1 byte) + Device status (1 byte) const knxMedium = msg.readUInt8(offset); info.deviceStatus = msg.readUInt8(offset + 1); // KNX Individual Address (2 bytes) const knxAddr = msg.readUInt16BE(offset + 2); info.knxAddress = `${(knxAddr >> 12) & 0x0f}.${(knxAddr >> 8) & 0x0f}.${knxAddr & 0xff}`; // Project Installation ID (2 bytes) info.projectInstallationId = msg.readUInt16BE(offset + 4); // Serial Number (6 bytes) const serialBytes = msg.subarray(offset + 6, offset + 12); info.serialNumber = Array.from(serialBytes) .map(b => b.toString(16).padStart(2, '0')) .join('') .toUpperCase(); // Routing Multicast Address (4 bytes) - skip for now // offset + 12 to offset + 16 // MAC Address (6 bytes) const mac = msg.subarray(offset + 16, offset + 22); info.macAddress = Array.from(mac) .map((b) => b.toString(16).padStart(2, "0")) .join(":") .toUpperCase(); // Device Friendly Name (30 bytes) const nameBytes = msg.subarray(offset + 22, offset + 52); const nullIndex = nameBytes.indexOf(0); const nameLength = nullIndex >= 0 ? nullIndex : nameBytes.length; info.friendlyName = nameBytes.subarray(0, nameLength).toString("utf8").trim(); return info; } parseServiceFamiliesDIB(msg, offset, length) { const families = []; // Each service family entry is 2 bytes (family ID + version) for (let i = 0; i < length; i += 2) { if (i + 1 < length) { const family = msg.readUInt8(offset + i); const version = msg.readUInt8(offset + i + 1); families.push(family); } } return families; } calculateCapabilities(serviceFamilies) { let capabilities = 0; for (const family of serviceFamilies) { switch (family) { case 0x02: // KNXnet/IP Core - basic protocol services // Core is fundamental but doesn't map to a specific capability flag break; case 0x03: // KNXnet/IP Device Management capabilities |= constants_1.KNX_CONSTANTS.DEVICE_CAPABILITIES.DEVICE_MANAGEMENT; break; case 0x04: // KNXnet/IP Tunnelling capabilities |= constants_1.KNX_CONSTANTS.DEVICE_CAPABILITIES.TUNNELLING; break; case 0x05: // KNXnet/IP Routing capabilities |= constants_1.KNX_CONSTANTS.DEVICE_CAPABILITIES.ROUTING; break; case 0x06: // KNXnet/IP Remote Logging capabilities |= constants_1.KNX_CONSTANTS.DEVICE_CAPABILITIES.REMOTE_LOGGING; break; case 0x07: // KNXnet/IP Remote Configuration and Diagnosis capabilities |= constants_1.KNX_CONSTANTS.DEVICE_CAPABILITIES.REMOTE_CONFIGURATION; break; case 0x08: // KNXnet/IP Object Server capabilities |= constants_1.KNX_CONSTANTS.DEVICE_CAPABILITIES.OBJECT_SERVER; break; } } return capabilities; } cleanup() { if (this.searchTimeout) { clearTimeout(this.searchTimeout); this.searchTimeout = undefined; } if (this.socket) { this.socket.removeAllListeners(); this.socket.close(); this.socket = undefined; } } close() { this.cleanup(); } } exports.KNXNetDiscovery = KNXNetDiscovery; //# sourceMappingURL=discovery.js.map