UNPKG

@isaac-platform/isaac-node-red

Version:

Set of Node-RED nodes to communicate with an ISAAC system

226 lines (225 loc) 9.72 kB
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); };