@isaac-platform/isaac-node-red
Version:
Set of Node-RED nodes to communicate with an ISAAC system
226 lines (225 loc) • 9.72 kB
JavaScript
const net = require("net"), axios = require("axios"), moment = require("moment"), split2 = require("split2"), utils = require("../../utils.js");
class Client {
node = null;
socket = null;
settings = null;
onData = null;
onConnect = null;
onDisconnect = null;
pingInterval = null;
subsystemExternalId = null;
constructor({ tcpHost, tcpPort, settings, node, subsystemExternalId, onConnect, onDisconnect, onData }) {
this.tcpHost = tcpHost, this.tcpPort = tcpPort, this.settings = settings, this.node = node, this.subsystemExternalId = subsystemExternalId, this.onConnect = onConnect, this.onDisconnect = onDisconnect, this.onData = onData;
}
isConnected = () => this.socket ? this.socket.readyState === "open" : !1;
send = ({ method, params, id }) => {
this.isConnected() || this.connect();
const message = {
jsonrpc: "2.0",
method: method.toString()
};
params !== void 0 && (message.params = params), id && (message.id = id), this.socket.write(JSON.stringify(message)), this.socket.write(`\r
`);
};
startPingInterval = () => {
clearInterval(this.pingInterval), this.pingInterval = setInterval(() => {
this.send({ method: "ping" });
}, 5e3);
};
connect = () => {
this.destroy();
const socket = net.createConnection(this.tcpPort, this.tcpHost).setEncoding("utf8");
return socket.on("connect", () => {
console.log("ISAAC Schedule Node Connected"), this.send({
method: "settings.set",
params: this.settings
}), this.send({
method: "subscriptions.add",
params: [this.subsystemExternalId]
}), this.startPingInterval(), this.onConnect && this.onConnect();
}), socket.on("end", () => {
this.node.warn("ISAAC Schedule Node ended"), this.onDisconnect && this.onDisconnect();
}), socket.on("close", () => {
console.log("ISAAC Schedule Node closed"), this.onDisconnect && this.onDisconnect();
}), socket.on("error", (e) => {
this.node.error("ISAAC Schedule Node error", e), this.onDisconnect && this.onDisconnect();
}), socket.on("timeout", () => {
socket.end(), this.onDisconnect && this.onDisconnect();
}), socket.pipe(split2()).on("data", (line) => {
this.onData(line);
}), this.socket = socket, this;
};
destroy = () => {
try {
clearInterval(this.pingInterval), this.socket && (this.socket.destroy(), this.socket = null);
} catch (e) {
console.error("Error destroying TCP client", e);
}
};
}
function is304(error) {
return !!error && typeof error == "object" && !!error.response && typeof error.response == "object" && error.response.status === 304;
}
let upcomingItemsLastFetchTime;
async function getUpcomingItems(server, mergedConfig) {
let upcomingCount = mergedConfig.upcomingCount || mergedConfig.count;
upcomingCount && typeof upcomingCount != "number" && (upcomingCount = parseInt(upcomingCount, 10));
let url = `${utils.getApiUrl(server)}/schedule?subsystemExternalId=${server.subsystemExternalId}&beforeCount=0`;
upcomingCount && (url = `${url}&afterCount=${upcomingCount}`);
let headers;
mergedConfig.useIfModifiedSince !== !0 ? upcomingItemsLastFetchTime = void 0 : upcomingItemsLastFetchTime && (headers = {
"If-Modified-Since": upcomingItemsLastFetchTime
});
const { data } = await axios.get(url, { headers });
if (mergedConfig.useIfModifiedSince === !0 && (upcomingItemsLastFetchTime = moment().toISOString()), !Array.isArray(data) || !data.length || !data[0] || typeof data[0] != "object" || !Array.isArray(data[0].values) || !data[0].values.length)
return [];
const now = moment(), items = data[0].values.filter((item) => moment(item.startTime).isAfter(now));
if (!items.length)
return items;
let { upcomingTime } = mergedConfig;
return upcomingTime && typeof upcomingTime != "number" && (upcomingTime = parseInt(upcomingTime, 10)), upcomingTime ? items.filter((item) => moment(item.startTime).diff(now, "seconds", !0) <= upcomingTime) : items;
}
let currentPlayingLastFetchTime;
async function getCurrentPlaying(server, mergedConfig) {
const url = `${utils.getApiUrl(server)}/schedule?subsystemExternalId=${server.subsystemExternalId}&activeItems=true`;
let headers;
mergedConfig.useIfModifiedSince !== !0 ? currentPlayingLastFetchTime = void 0 : currentPlayingLastFetchTime && (headers = {
"If-Modified-Since": currentPlayingLastFetchTime
});
const { data } = await axios.get(url, { headers });
if (mergedConfig.useIfModifiedSince === !0 && (currentPlayingLastFetchTime = moment().toISOString()), !Array.isArray(data) || !data.length || !data[0] || typeof data[0] != "object" || !Array.isArray(data[0].values) || !data[0].values.length)
return [];
const items = data[0].values;
let { upcomingCount } = mergedConfig;
return upcomingCount && typeof upcomingCount != "number" && (upcomingCount = parseInt(upcomingCount, 10)), upcomingCount ? items.slice(0, upcomingCount) : items;
}
module.exports = (RED) => {
function ScheduleNode(config) {
RED.nodes.createNode(this, config);
const node = this, server = RED.nodes.getNode(config.isaacConnection), tcpHost = new URL(server.ipAddress).hostname, tcpPort = 8099, didAddUsedConnection = utils.incrementConnectionUsage(node, server), setDisconnectedStatus = () => node.status({ fill: "red", shape: "ring", text: "disconnected" });
setDisconnectedStatus();
let preroll;
if (config.prerollWarning)
if (typeof config.prerollWarning == "string") {
const parsed = parseInt(config.prerollWarning, 10);
Number.isNaN(parsed) || (preroll = parsed);
} else typeof config.prerollWarning == "number" && (preroll = config.prerollWarning);
const client = new Client({
tcpHost,
tcpPort,
node,
subsystemExternalId: server.subsystemExternalId,
settings: {
preroll,
token: config.accessToken,
version: 1
},
onConnect: () => {
node.status({ fill: "green", shape: "dot", text: "connected" });
},
onDisconnect: () => {
setDisconnectedStatus();
},
onData: (line) => {
if (!line)
return;
let parsed;
try {
parsed = JSON.parse(line), console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] schedule data:`, parsed);
} catch (err) {
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] schedule data: (raw)`, line), node.error(err);
}
if (!parsed)
return;
if (parsed.error) {
node.error(new Error(parsed.error));
return;
}
if (parsed.result)
return;
let msg = {};
switch (parsed.params && (msg = {
displayName: parsed.params.displayName,
startTime: moment(parsed.params.startTime).toISOString(),
command: parsed.params.command
}, parsed.params.itemType === "PLAYABLE" && (msg.endTime = moment(parsed.params.endTime).toISOString())), parsed.method) {
case "schedule.item.start":
node.send({
topic: "item",
payload: msg
});
break;
case "schedule.item.end":
break;
case "schedule.item.preroll":
node.send({
topic: "preroll",
payload: msg
});
break;
default:
node.send({
topic: parsed.method,
payload: parsed.params
});
break;
}
}
}).connect();
this.on("input", async (msg) => {
try {
if (!server)
throw new Error("No ISAAC connection configured");
const mergedConfig = utils.mergeConfig({ config, msg });
if (!mergedConfig.command)
throw new Error("command must be set");
if (typeof mergedConfig.command != "string")
throw new Error("command must be a string");
if (mergedConfig.command === "getCurrentPlaying") {
const topic = "current";
try {
const items = await getCurrentPlaying(server, mergedConfig);
node.send({ ...msg, topic, payload: items });
} catch (e) {
if (mergedConfig.useIfModifiedSince === !0 && is304(e))
node.send({ ...msg, topic, payload: [], unchanged: !0 });
else
throw e;
}
return;
}
if (mergedConfig.command === "getUpcomingItems" || // Documentation mentions getUpcomingItems but getUpcoming used to be supported so we need to keep it
mergedConfig.command === "getUpcoming") {
const topic = "upcoming";
try {
const items = await getUpcomingItems(server, mergedConfig);
node.send({ ...msg, topic, payload: items });
} catch (e) {
if (mergedConfig.useIfModifiedSince === !0 && is304(e))
node.send({ ...msg, topic, payload: [], unchanged: !0 });
else
throw e;
}
return;
}
throw new Error(`"${mergedConfig.command}" is not a recognized command`);
} catch (e) {
const niceMessage = utils.getNiceAxiosErrorMessage(e);
if (niceMessage) {
const error = new Error(niceMessage);
node.error(error, msg);
return;
}
node.error(e, msg);
}
}), this.on("close", () => {
didAddUsedConnection && utils.decrementConnectionUsage(node, server);
try {
client && client.destroy();
} catch (e) {
node.error(e);
}
});
}
RED.nodes.registerType("isaac schedule", ScheduleNode);
};