Files
node-red-contrib-xilica/nodes/xilica-command.js
2025-12-31 08:28:11 +01:00

244 lines
6.0 KiB
JavaScript

const net = require("net");
module.exports = function (RED) {
function parseResponseLines(lines) {
const results = [];
lines.forEach((line) => {
if (!line) {
return;
}
if (line === "OK") {
results.push({ type: "ok" });
return;
}
if (line.startsWith("ERROR=")) {
const code = parseInt(line.substring(6), 10);
results.push({ type: "error", code, raw: line });
return;
}
if (line[0] === "#") {
const payload = line.slice(1);
const idx = payload.indexOf("=");
if (idx !== -1) {
const control = payload.slice(0, idx);
const data = payload.slice(idx + 1);
results.push({
type: "notification",
control,
data,
});
return;
}
}
const idx = line.indexOf("=");
if (idx !== -1) {
const control = line.slice(0, idx);
const data = line.slice(idx + 1);
results.push({
type: "value",
control,
data,
});
return;
}
results.push({ type: "raw", raw: line });
});
if (results.length === 1) {
return results[0];
}
return results;
}
function XilicaCommand(config) {
RED.nodes.createNode(this, config);
const node = this;
node.connectionConfig = RED.nodes.getNode(config.connection);
node.command = config.command || "";
node.control = config.control || "";
node.data = config.data || "";
node.parseResponse = config.parseResponse !== false;
if (!node.connectionConfig) {
node.status({ fill: "red", shape: "ring", text: "no connection" });
}
node.on("input", (msg, send, done) => {
send =
send ||
function () {
node.send.apply(node, arguments);
};
done = done || function () {};
const conn = node.connectionConfig;
if (!conn || !conn.host) {
node.status({ fill: "red", shape: "ring", text: "no host" });
node.error("No Xilica connection configured", msg);
done();
return;
}
let rawCommand = "";
if (typeof msg.payload === "string" && msg.payload.trim()) {
rawCommand = msg.payload.trim();
}
let cmd = node.command;
let control = node.control;
let data = node.data;
if (msg.command) {
cmd = msg.command;
}
if (msg.control) {
control = msg.control;
}
if (Object.prototype.hasOwnProperty.call(msg, "data")) {
data = msg.data;
}
if (!rawCommand) {
if (!cmd) {
node.status({ fill: "red", shape: "ring", text: "missing command" });
node.error("Missing command", msg);
done();
return;
}
rawCommand = String(cmd).trim();
if (control) {
rawCommand += " " + String(control).trim();
}
if (data !== undefined && data !== "") {
rawCommand += " " + String(data).trim();
}
}
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";
const responseLines = [];
let finished = false;
let idleTimer = null;
const idleTimeout = 100;
function clearIdle() {
if (idleTimer) {
clearTimeout(idleTimer);
idleTimer = null;
}
}
function scheduleIdleFinish() {
clearIdle();
idleTimer = setTimeout(() => {
finishSuccess();
}, idleTimeout);
}
function finishSuccess() {
if (finished) {
return;
}
finished = true;
clearIdle();
client.end();
node.status({});
msg.raw = responseLines.slice();
if (node.parseResponse) {
msg.payload = parseResponseLines(responseLines);
} else {
msg.payload = responseLines.join("\n");
}
send(msg);
done();
}
function finishError(err) {
if (finished) {
return;
}
finished = true;
clearIdle();
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);
}
} else {
if (line) {
responseLines.push(line);
}
scheduleIdleFinish();
}
idx = buffer.indexOf("\r");
}
});
client.on("timeout", () => {
finishError(new Error("Timeout waiting for response from Xilica"));
});
client.on("error", (err) => {
finishError(err);
});
client.on("end", () => {
if (!finished && responseLines.length > 0) {
finishSuccess();
}
});
});
}
RED.nodes.registerType("xilica-command", XilicaCommand);
};