const net = require("net"); module.exports = function (RED) { function buildNamesFromList(targets) { return targets .split(/\r?\n/) .map((l) => l.trim()) .filter((l) => l.length > 0); } function buildNamesStartsWith(targets, startIndex, endIndex) { const prefixes = buildNamesFromList(targets); const names = []; if (startIndex > endIndex) { const tmp = startIndex; startIndex = endIndex; endIndex = tmp; } prefixes.forEach((prefix) => { for (let i = startIndex; i <= endIndex; i += 1) { names.push(prefix + i); } }); return names; } function buildNamesEndsWith(targets, startIndex, endIndex) { const suffixes = buildNamesFromList(targets); const names = []; if (startIndex > endIndex) { const tmp = startIndex; startIndex = endIndex; endIndex = tmp; } suffixes.forEach((suffix) => { for (let i = startIndex; i <= endIndex; i += 1) { names.push(String(i) + suffix); } }); return names; } function buildNamesStartsAndEnds(targets, startIndex, endIndex) { const patterns = buildNamesFromList(targets); const names = []; if (startIndex > endIndex) { const tmp = startIndex; startIndex = endIndex; endIndex = tmp; } patterns.forEach((pattern) => { const parts = pattern.split("|"); const prefix = (parts[0] || "").trim(); const suffix = (parts[1] || "").trim(); if (!prefix && !suffix) { return; } for (let i = startIndex; i <= endIndex; i += 1) { names.push(prefix + i + suffix); } }); return names; } function buildControlNames(mode, targets, startIndex, endIndex) { if (mode === "startsWith") { return buildNamesStartsWith(targets, startIndex, endIndex); } if (mode === "endsWith") { return buildNamesEndsWith(targets, startIndex, endIndex); } if (mode === "startsAndEnds") { return buildNamesStartsAndEnds(targets, startIndex, endIndex); } return buildNamesFromList(targets); } function normaliseAction(action) { const a = (action || "").toString().toLowerCase(); if (a === "unsubscribe" || a === "unsub") { return "UNSUBSCRIBE"; } return "SUBSCRIBE"; } function XilicaSubscribe(config) { RED.nodes.createNode(this, config); const node = this; node.action = config.action || "subscribe"; node.connectionConfig = RED.nodes.getNode(config.connection); node.mode = config.mode || "list"; node.targets = config.targets || ""; node.startIndex = parseInt(config.startIndex, 10) || 1; node.endIndex = parseInt(config.endIndex, 10) || node.startIndex; node.transport = config.transport || "TCP"; node.on("input", (msg, send, done) => { send = send || function () { node.send.apply(node, arguments); }; done = done || function () {}; const action = normaliseAction(msg.action || node.action); const mode = (msg.mode || node.mode || "list").toString(); const targets = typeof msg.targets === "string" && msg.targets.trim().length ? msg.targets : node.targets; let startIndex = node.startIndex; let endIndex = node.endIndex; if (Object.prototype.hasOwnProperty.call(msg, "startIndex")) { const v = parseInt(msg.startIndex, 10); if (!Number.isNaN(v)) { startIndex = v; } } if (Object.prototype.hasOwnProperty.call(msg, "endIndex")) { const v = parseInt(msg.endIndex, 10); if (!Number.isNaN(v)) { endIndex = v; } } const transport = typeof msg.transport === "string" && msg.transport.trim().length ? msg.transport.trim() : node.transport; const names = buildControlNames(mode, targets || "", startIndex, endIndex); if (!names.length) { node.warn("xilica-subscribe: no rules defined"); send(msg); done(); return; } const lines = names.map( (name) => action + " " + name + ' "' + transport + '"', ); const rawCommand = lines.join("\r"); const conn = node.connectionConfig; if (!conn || !conn.host) { node.status({ fill: "red", shape: "ring", text: "no connection", }); node.warn( "xilica-subscribe: no connection configured, outputting commands only", ); msg.payload = rawCommand; send(msg); done(); return; } const host = conn.host; const port = conn.port || 10007; const timeout = conn.timeout || 5000; const password = conn.credentials && conn.credentials.password ? conn.credentials.password : null; const client = net.createConnection({ host, port }); let buffer = ""; let phase = password ? "login" : "command"; let finished = false; function finishSuccess() { if (finished) { return; } finished = true; client.end(); node.status({}); msg.raw = buffer; msg.payload = rawCommand; send(msg); done(); } function finishError(err) { if (finished) { return; } finished = true; client.destroy(); node.status({ fill: "red", shape: "ring", text: "error" }); node.error(err, msg); done(err); } client.setTimeout(timeout); client.on("connect", () => { node.status({ fill: "green", shape: "dot", text: "connected" }); try { if (phase === "login") { client.write("LOGIN " + password + "\r"); } else { client.write(rawCommand + "\r"); } } catch (err) { finishError(err); } }); client.on("data", (dataBuf) => { buffer += dataBuf.toString("utf8"); let idx = buffer.indexOf("\r"); while (idx !== -1) { const line = buffer.slice(0, idx); buffer = buffer.slice(idx + 1); if (phase === "login") { if (line && line.startsWith("ERROR=")) { finishError(new Error("Login failed: " + line)); return; } phase = "command"; try { client.write(rawCommand + "\r"); } catch (err) { finishError(err); return; } } idx = buffer.indexOf("\r"); } }); client.on("timeout", () => { if (phase === "command") { finishSuccess(); } else { finishError(new Error("Timeout sending SUBSCRIBE to Xilica")); } }); client.on("error", (err) => { finishError(err); }); client.on("end", () => { if (!finished) { finishSuccess(); } }); }); } RED.nodes.registerType("xilica-subscribe", XilicaSubscribe); };