@mysterycode/node-red-sacn
Version:
Implementation of sACN (Streaming ACN) for Node-RED
292 lines (291 loc) • 10.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
class NodeHandler {
node;
config;
constructor(node, config) {
this.node = node;
this.config = config;
this.node.on("input", (msg) => {
const message = msg;
switch (message.action || "undefined") {
case "save":
if (this.validateSave(message)) {
this.handleSave(message);
}
break;
case "play":
if (this.validatePlay(message)) {
this.handlePlay(message);
}
break;
case "reset":
if (this.validateReset(message)) {
this.handleReset(message);
}
break;
default:
if (message.action === undefined) {
this.node.warn('Message-Object is missing the "action"-property.');
break;
}
this.node.warn(`The given "action"-property "${message.action}" (${typeof message.action}) is not supported.`);
break;
}
});
}
validateUniverse(universe) {
if (universe === undefined || isNaN(universe) || universe < 1 || universe > 65279) {
throw new Error(`The universe number '${universe}' (${typeof universe}) is invalid or not between 1 and 65279.`);
}
}
validateChannel(channel, universe, firstChannel = 1, lastChannel = 512) {
if (isNaN(channel) || channel < firstChannel || channel > lastChannel) {
throw new Error(`Channel number '${channel}' (${typeof channel}) of universe '${universe}' is invalid or not between ${firstChannel} and ${lastChannel}.`);
}
}
validateValue(value, channel, universe) {
if (isNaN(value) || value < 0 || value > 255) {
throw new Error(`Value '${value}' (${typeof value}) for channel '${channel}' of universe '${universe}' is invalid or not between 0 and 255.`);
}
}
parseUniverseObject(input, universe) {
const keys = Object.keys(input);
const output = {};
const startIndex = keys.includes("0") || keys.includes(0) ? 0 : 1;
const endIndex = startIndex === 0 ? 511 : 512;
if (keys.length !== 512) {
throw new Error(`Universe ${universe} contains ${keys.length} values, but not 512 values.`);
}
let channel;
for (channel in input) {
let value = input[channel];
if (typeof channel === "string") {
channel = parseInt(channel, 10);
}
this.validateChannel(channel, universe, startIndex, endIndex);
if (typeof value === "string") {
value = parseInt(value);
}
this.validateValue(value, channel, universe);
output[channel] = value;
}
return output;
}
parseUniverseArray(input, universe) {
const output = {};
if (input.length !== 512) {
throw new Error(`Universe ${universe} contains ${input.length} values, but not 512 values.`);
}
input.forEach((value, channel) => {
this.validateChannel(channel, universe, 0, 511);
if (typeof value === "string") {
value = parseInt(value, 10);
}
this.validateValue(value, channel, universe);
output[channel] = value;
});
return output;
}
parseUniverseData(input, universe) {
const output = {};
if (input === undefined || input === null || (typeof input !== "object" && !Array.isArray(input))) {
throw new Error(`Payload must be an object or an array. Type "${typeof input}" is not supported.`);
}
if (Array.isArray(input)) {
if (typeof universe === "string") {
universe = parseInt(universe, 10);
}
this.validateUniverse(universe);
output[universe] = this.parseUniverseArray(input, universe);
}
else if (typeof input === "object") {
let containsPossibleNumericValues = false;
let containsPossibleUniverses = false;
let containsDifferent = false;
for (const key in input) {
const item = input[key];
if (typeof item === "string" || !isNaN(item)) {
containsPossibleNumericValues = true;
}
else if (Array.isArray(item) || typeof item === "object") {
containsPossibleUniverses = true;
}
else {
containsDifferent = true;
}
}
if (containsDifferent) {
throw new Error("Payload contains invalid data.");
}
if (containsPossibleUniverses && containsPossibleNumericValues) {
throw new Error("Payload contains objects or arrays parallel to numeric values");
}
if (containsPossibleNumericValues) {
if (typeof universe === "string") {
universe = parseInt(universe, 10);
}
this.validateUniverse(universe);
output[universe] = this.parseUniverseObject(input, universe);
}
else if (containsPossibleUniverses) {
let key;
for (key in input) {
if (typeof key === "string") {
key = parseInt(key, 10);
}
this.validateUniverse(key);
const item = input[key];
if (Array.isArray(item)) {
output[key] = this.parseUniverseArray(item, key);
}
else if (typeof item === "object") {
output[key] = this.parseUniverseObject(item, key);
}
else {
throw new Error(`Data for universe ${key} is invalid.`);
}
}
}
}
return output;
}
validateScene(message) {
if (message.scene === undefined) {
this.node.warn('Message-Object is missing the "scene"-property.');
return false;
}
if (isNaN(message.scene)) {
this.node.warn(`The given "scene"-property "${message.scene}" (${typeof message.scene}) is not a positive number.`);
return false;
}
return true;
}
validateSave(message) {
if (!this.validateScene(message)) {
return false;
}
if (typeof message.payload !== "object" && !Array.isArray(message.payload)) {
this.node.warn("The given payload has to be an object or array.");
return false;
}
try {
message.payload = this.parseUniverseData(message.payload, message.universe);
}
catch (e) {
if (e instanceof Error) {
this.node.warn(e.message);
}
return false;
}
return true;
}
validatePlay(message) {
if (!this.validateScene(message)) {
return false;
}
return true;
}
validateReset(_) {
return true;
}
handleSave(message) {
const scene = {
scene: message.scene,
data: message.payload,
};
this.node.context().set(`scene-${message.scene}`, scene);
}
handlePlay(message) {
const data = this.node.context().get(`scene-${message.scene}`);
if (!data) {
this.node.warn(`Cannot play scene no. ${message.scene} since it has not been recorded yet.`);
return;
}
this.node.context().set("playingScene", message.scene);
let payload;
let universe = undefined;
const universes = Object.keys(data.data);
if (universes.length === 1) {
universe = parseInt(universes[0], 10);
payload = data.data[universe];
}
else {
payload = data.data;
}
this.node.send({
topic: message.topic ?? `Scene ${message.scene}`,
scene: message.scene,
payload: payload,
universe: universe,
});
this.node.status({
fill: "green",
shape: "dot",
text: message.topic ?? `Scene ${message.scene}`,
});
}
getNulledUniverse() {
const universe = {};
for (let ch = 1; ch <= 512; ch++) {
universe[ch] = 0;
}
return universe;
}
handleReset(message) {
const resetScene = (scene) => {
const data = this.node.context().get(`scene-${scene}`);
if (this.node.context().get("playingScene") === scene) {
this.node.context().set("playingScene", null);
this.node.status({
fill: "green",
shape: "ring",
text: "Standby",
});
if (data) {
const universes = Object.keys(data.data);
let universe = undefined;
let payload;
if (universes.length === 1) {
universe = parseInt(universes[0], 10);
payload = this.getNulledUniverse();
}
else {
payload = {};
universes.forEach((universe) => {
payload[parseInt(universe, 10)] = this.getNulledUniverse();
});
}
this.node.send({
topic: `Scene ${scene}`,
scene: scene,
payload: payload,
universe: universe,
reset: true,
});
}
}
this.node.context().set(`scene-${scene}`, undefined);
};
if (message.scene) {
resetScene(message.scene);
}
else {
this.node
.context()
.keys()
.forEach((key) => {
const matches = key.match(/^scene-(\d+)/);
if (matches !== null) {
resetScene(parseInt(matches[1], 10));
}
});
}
}
}
exports.default = (RED) => {
RED.nodes.registerType("scene-controller", function (config) {
RED.nodes.createNode(this, config);
new NodeHandler(this, config);
});
};