knxnetjs
Version:
A TypeScript library for KNXnet/IP communication
226 lines • 9.64 kB
JavaScript
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
;