UNPKG

knxnetjs

Version:

A TypeScript library for KNXnet/IP communication

226 lines 9.64 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.KNXNetDiscovery = void 0; const events_1 = require("events"); const dgram_1 = require("dgram"); const types_1 = require("./types"); const hpai_1 = require("./types/hpai"); const constants_1 = require("./constants"); const frames_1 = require("./frames"); 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() { // Discovery endpoint HPAI (8 bytes) const hpai = this.createHPAI(); const frame = new frames_1.KNXnetIPFrame(constants_1.KNX_CONSTANTS.SERVICE_TYPES.SEARCH_REQUEST, hpai); return frame.toBuffer(); } createHPAI() { const port = this.socket?.address()?.port || 0; const hpai = new types_1.HPAI(hpai_1.HostProtocol.IPV4_UDP, "0.0.0.0", port); return hpai.toBuffer(); } handleSearchResponse(msg, rinfo) { try { const frame = frames_1.KNXnetIPFrame.fromBuffer(msg); if (frame.service !== constants_1.KNX_CONSTANTS.SERVICE_TYPES.SEARCH_RESPONSE) { return; } const endpoint = this.parseSearchResponse(frame.payload, 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(payload, rinfo) { let offset = 0; // Parse Control Endpoint HPAI const controlHpaiLength = payload.readUInt8(offset); const controlEndpoint = this.parseHPAI(payload, offset); offset += controlHpaiLength; // Parse Device Information DIB const deviceInfoLength = payload.readUInt8(offset); const deviceInfoDescType = payload.readUInt8(offset + 1); if (deviceInfoDescType !== 0x01) { throw new Error(`Expected Device Info DIB (0x01), got 0x${deviceInfoDescType.toString(16)}`); } const deviceInfo = this.parseDeviceInfoDIB(payload, offset + 2, deviceInfoLength - 2); offset += deviceInfoLength; // Parse Supported Service Families DIB const serviceFamiliesLength = payload.readUInt8(offset); const serviceFamiliesDescType = payload.readUInt8(offset + 1); if (serviceFamiliesDescType !== 0x02) { throw new Error(`Expected Service Families DIB (0x02), got 0x${serviceFamiliesDescType.toString(16)}`); } const serviceFamilies = this.parseServiceFamiliesDIB(payload, offset + 2, serviceFamiliesLength - 2); return { name: deviceInfo.friendlyName || `KNX Device ${rinfo.address}`, ip: rinfo.address, port: controlEndpoint.port, protocol: controlEndpoint.hostProtocol, 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 hpaiBuffer = msg.subarray(offset, offset + 8); return types_1.HPAI.fromBuffer(hpaiBuffer); } 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