diff --git a/README.md b/README.md index 11b9d11..6c06012 100644 --- a/README.md +++ b/README.md @@ -92,20 +92,24 @@ This node can be used to record scenes and play them afterwards. #### Parameters: -| Parameter | Description | Possible Values | Default Value | Mandatory | -| --------- | ------------------------------------------------------------------------------ | --------------------- | ------------- | --------- | -| values | Interpret channel values as a percentage (0–100) or as raw DMX values (0–255). | `percent`, `absolute` | `percent` | yes | +| Parameter | Description | Possible Values | Default Value | Mandatory | +| -------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | ------------- | --------- | +| values | Interpret channel values as a percentage (0–100) or as raw DMX values (0–255). | `percent`, `absolute` | `percent` | yes | +| playMode | Whether a new `play` replaces the running look (`switch`) or stacks scenes additively, combined per channel by HTP (`add`) | `switch`, `add` | `switch` | yes | +| blackoutOnStop | Send a nulled universe once when the last active scene is stopped, so the output goes dark. | `true`, `false` | `false` | no | Recorded scenes are stored on disk (in a `sacn-scenes` directory inside the Node-RED user directory) and survive a restart. +In `add` mode multiple scenes can be active at once; every `play`, `stop` or `reset` recomputes the combined look (highest value wins per channel) and emits it once. The output only goes dark once the last active scene ends. + #### Expected input: -| Property | Description | Mandatory | -| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- | -| `action` | the action to be executed - `save` to save a preset, `play` to play a saved preset, `reset` to reset | yes | -| `scene` | for action | yes, for actions `save`, `play` | -| `universe` | if only one universe is handled, this parameter is mandatory and contains the used universe | only for a single universe | -| `payload` | contains the values to record. it might be an array (key 0-511) containing the values for a single universe,
an object (keys 1-512) containing the values for a single universe or
an object (any numeric keys) containing objects (keys 1-512) containing an universe each. | yes, for action `save` | +| Property | Description | Mandatory | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------- | +| `action` | the action to be executed - `save` to save a preset, `play` to play a saved preset, `stop` to stop a preset while keeping it stored, `reset` to reset | yes | +| `scene` | for action | yes, for actions `save`, `play`, `stop` | +| `universe` | if only one universe is handled, this parameter is mandatory and contains the used universe | only for a single universe | +| `payload` | contains the values to record. it might be an array (key 0-511) containing the values for a single universe,
an object (keys 1-512) containing the values for a single universe or
an object (any numeric keys) containing objects (keys 1-512) containing an universe each. | yes, for action `save` | #### Output for single universe: @@ -115,6 +119,7 @@ Recorded scenes are stored on disk (in a `sacn-scenes` directory inside the Node | `payload` | Array containing the dmx values as **percentage** by dmx channel. DMX-Channel `1` starts at key `1`, not `0`. (`Array`) | | `scene` | The scene that is played (`number`) | | `reset` | Identifies a reset message for action `reset`, otherwise it does not exist. (`true`) | +| `stopped` | Identifies the nulled message emitted for action `stop` (when `blackoutOnStop` is enabled), otherwise it does not exist. (`true`) | #### Output for multiple universes: @@ -124,6 +129,7 @@ Recorded scenes are stored on disk (in a `sacn-scenes` directory inside the Node | `payload` | Object containing one object per universe. DMX-Channel `1` starts at key `1`, not `0`. (`object>`) | | `scene` | The scene that is played (`number`) | | `reset` | Identifies a reset message for action `reset`, otherwise it does not exist. (`true`) | +| `stopped` | Identifies the nulled message emitted for action `stop` (when `blackoutOnStop` is enabled), otherwise it does not exist. (`true`) | ## Protocol notes and limitations diff --git a/dist/lib/dmx.js b/dist/lib/dmx.js index 598c8f3..287e24e 100644 --- a/dist/lib/dmx.js +++ b/dist/lib/dmx.js @@ -1,6 +1,12 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.MAX_ABSOLUTE = exports.MAX_PERCENT = exports.LAST_UNIVERSE = exports.FIRST_UNIVERSE = exports.LAST_CHANNEL = exports.FIRST_CHANNEL = void 0; +exports.MAX_ABSOLUTE = + exports.MAX_PERCENT = + exports.LAST_UNIVERSE = + exports.FIRST_UNIVERSE = + exports.LAST_CHANNEL = + exports.FIRST_CHANNEL = + void 0; exports.maxValue = maxValue; exports.fromPercent = fromPercent; exports.nulledUniverse = nulledUniverse; @@ -11,15 +17,15 @@ exports.LAST_UNIVERSE = 63999; exports.MAX_PERCENT = 100; exports.MAX_ABSOLUTE = 255; function maxValue(scale) { - return scale === "absolute" ? exports.MAX_ABSOLUTE : exports.MAX_PERCENT; + return scale === "absolute" ? exports.MAX_ABSOLUTE : exports.MAX_PERCENT; } function fromPercent(percent, scale) { - return scale === "absolute" ? Math.round(percent * 2.55) : percent; + return scale === "absolute" ? Math.round(percent * 2.55) : percent; } function nulledUniverse() { - const universe = {}; - for (let channel = exports.FIRST_CHANNEL; channel <= exports.LAST_CHANNEL; channel++) { - universe[channel] = 0; - } - return universe; + const universe = {}; + for (let channel = exports.FIRST_CHANNEL; channel <= exports.LAST_CHANNEL; channel++) { + universe[channel] = 0; + } + return universe; } diff --git a/dist/lib/interfaces.js b/dist/lib/interfaces.js index df0c2e9..bcde03f 100644 --- a/dist/lib/interfaces.js +++ b/dist/lib/interfaces.js @@ -4,24 +4,24 @@ exports.listInterfaces = listInterfaces; exports.registerInterfaceEndpoint = registerInterfaceEndpoint; const node_os_1 = require("node:os"); function listInterfaces() { - const result = []; - const interfaces = (0, node_os_1.networkInterfaces)(); - for (const name of Object.keys(interfaces)) { - for (const info of interfaces[name] ?? []) { - if (info.family === "IPv4") { - result.push({ name, address: info.address }); - } - } + const result = []; + const interfaces = (0, node_os_1.networkInterfaces)(); + for (const name of Object.keys(interfaces)) { + for (const info of interfaces[name] ?? []) { + if (info.family === "IPv4") { + result.push({ name, address: info.address }); + } } - return result; + } + return result; } let registered = false; function registerInterfaceEndpoint(RED) { - if (registered) { - return; - } - registered = true; - RED.httpAdmin.get("/sacn/interfaces", RED.auth.needsPermission("flows.read"), (_req, res) => { - res.json(listInterfaces()); - }); + if (registered) { + return; + } + registered = true; + RED.httpAdmin.get("/sacn/interfaces", RED.auth.needsPermission("flows.read"), (_req, res) => { + res.json(listInterfaces()); + }); } diff --git a/dist/lib/network.js b/dist/lib/network.js index e622fea..266b18a 100644 --- a/dist/lib/network.js +++ b/dist/lib/network.js @@ -5,11 +5,11 @@ exports.resolveNetworkOptions = resolveNetworkOptions; const node_net_1 = require("node:net"); exports.DEFAULT_PORT = 5568; function resolveNetworkOptions(config) { - const options = { - port: config.port !== undefined && config.port > 0 ? config.port : exports.DEFAULT_PORT, - }; - if (config.interface !== undefined && (0, node_net_1.isIP)(config.interface) === 4) { - options.iface = config.interface; - } - return options; + const options = { + port: config.port !== undefined && config.port > 0 ? config.port : exports.DEFAULT_PORT, + }; + if (config.interface !== undefined && (0, node_net_1.isIP)(config.interface) === 4) { + options.iface = config.interface; + } + return options; } diff --git a/dist/lib/scene-store.js b/dist/lib/scene-store.js index 70d7770..8ca7c56 100644 --- a/dist/lib/scene-store.js +++ b/dist/lib/scene-store.js @@ -4,25 +4,24 @@ exports.SceneStore = void 0; const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); class SceneStore { - file; - constructor(baseDir, nodeId) { - const dir = (0, node_path_1.join)(baseDir, "sacn-scenes"); - (0, node_fs_1.mkdirSync)(dir, { recursive: true }); - this.file = (0, node_path_1.join)(dir, `${nodeId}.json`); + file; + constructor(baseDir, nodeId) { + const dir = (0, node_path_1.join)(baseDir, "sacn-scenes"); + (0, node_fs_1.mkdirSync)(dir, { recursive: true }); + this.file = (0, node_path_1.join)(dir, `${nodeId}.json`); + } + load() { + if (!(0, node_fs_1.existsSync)(this.file)) { + return {}; } - load() { - if (!(0, node_fs_1.existsSync)(this.file)) { - return {}; - } - try { - return JSON.parse((0, node_fs_1.readFileSync)(this.file, "utf8")); - } - catch { - return {}; - } - } - save(scenes) { - (0, node_fs_1.writeFileSync)(this.file, JSON.stringify(scenes)); + try { + return JSON.parse((0, node_fs_1.readFileSync)(this.file, "utf8")); + } catch { + return {}; } + } + save(scenes) { + (0, node_fs_1.writeFileSync)(this.file, JSON.stringify(scenes)); + } } exports.SceneStore = SceneStore; diff --git a/dist/nodes/sacn-in/sacn-in.js b/dist/nodes/sacn-in/sacn-in.js index 9ca2791..8b35b96 100644 --- a/dist/nodes/sacn-in/sacn-in.js +++ b/dist/nodes/sacn-in/sacn-in.js @@ -5,183 +5,182 @@ const dmx_1 = require("../../lib/dmx"); const network_1 = require("../../lib/network"); const interfaces_1 = require("../../lib/interfaces"); class NodeHandler { - node; - config; - data = new Map(); - sACN; - currentUniverse; - trigger; - interval; - scale; - keepaliveTimer; - constructor(node, config) { - this.node = node; - this.config = config; - this.currentUniverse = config.universe; - this.scale = config.values ?? "percent"; - this.trigger = config.trigger ?? (config.mode === "passthrough" ? "always" : "changes"); - this.interval = config.interval !== undefined && config.interval > 0 ? config.interval : 1000; - const network = (0, network_1.resolveNetworkOptions)(config); - const options = { - universes: [config.universe], - reuseAddr: config.reuseAddress !== undefined ? config.reuseAddress : true, - port: network.port, - }; - if (network.iface !== undefined) { - options.iface = network.iface; - } - switch (config.mode) { - case "passthrough": - this.sACN = new sacn_1.Receiver(options); - break; - case "htp": - case "ltp": - options.mode = config.mode.toUpperCase(); - this.sACN = new sacn_1.unstable_MergingReceiver(options); - break; - default: - throw new Error("[node-red-sacn] None or invalid mode selected."); - } - this.sACN.on("error", (err) => { - this.node.error(err); - this.node.status({ fill: "red", shape: "dot", text: err.message || "receiver error" }); - }); - this.node.on("close", () => { - this.sACN.close(); - if (this.keepaliveTimer) { - clearTimeout(this.keepaliveTimer); - } - this.data = new Map(); - }); - if (config.mode === "passthrough") { - this.sACN.on("packet", (packet) => { - const { payload, changed } = this.applyFrame(packet.payload, packet.universe); - if (this.trigger === "always" || changed) { - this.sendData({ - universe: packet.universe, - payload, - sequence: packet.sequence, - source: packet.sourceAddress, - priority: packet.priority, - }); - } - }); + node; + config; + data = new Map(); + sACN; + currentUniverse; + trigger; + interval; + scale; + keepaliveTimer; + constructor(node, config) { + this.node = node; + this.config = config; + this.currentUniverse = config.universe; + this.scale = config.values ?? "percent"; + this.trigger = config.trigger ?? (config.mode === "passthrough" ? "always" : "changes"); + this.interval = config.interval !== undefined && config.interval > 0 ? config.interval : 1000; + const network = (0, network_1.resolveNetworkOptions)(config); + const options = { + universes: [config.universe], + reuseAddr: config.reuseAddress !== undefined ? config.reuseAddress : true, + port: network.port, + }; + if (network.iface !== undefined) { + options.iface = network.iface; + } + switch (config.mode) { + case "passthrough": + this.sACN = new sacn_1.Receiver(options); + break; + case "htp": + case "ltp": + options.mode = config.mode.toUpperCase(); + this.sACN = new sacn_1.unstable_MergingReceiver(options); + break; + default: + throw new Error("[node-red-sacn] None or invalid mode selected."); + } + this.sACN.on("error", (err) => { + this.node.error(err); + this.node.status({ fill: "red", shape: "dot", text: err.message || "receiver error" }); + }); + this.node.on("close", () => { + this.sACN.close(); + if (this.keepaliveTimer) { + clearTimeout(this.keepaliveTimer); + } + this.data = new Map(); + }); + if (config.mode === "passthrough") { + this.sACN.on("packet", (packet) => { + const { payload, changed } = this.applyFrame(packet.payload, packet.universe); + if (this.trigger === "always" || changed) { + this.sendData({ + universe: packet.universe, + payload, + sequence: packet.sequence, + source: packet.sourceAddress, + priority: packet.priority, + }); } - else { - this.sACN.on("changed", (data) => { - const { payload } = this.applyFrame(data.payload, data.universe); - if (this.trigger !== "always") { - this.sendData({ - universe: data.universe, - payload, - }); - } - }); - if (this.trigger === "always") { - this.sACN.on("packet", () => { - this.emitFull(this.currentUniverse); - }); - } + }); + } else { + this.sACN.on("changed", (data) => { + const { payload } = this.applyFrame(data.payload, data.universe); + if (this.trigger !== "always") { + this.sendData({ + universe: data.universe, + payload, + }); } - this.node.on("input", (msg) => { - this.handleUniverseChange(msg); + }); + if (this.trigger === "always") { + this.sACN.on("packet", () => { + this.emitFull(this.currentUniverse); }); - this.setStatus(); - this.resetKeepalive(); + } } - setStatus() { - this.node.status({ - fill: "green", - shape: "dot", - text: `Universe ${this.currentUniverse}`, - }); + this.node.on("input", (msg) => { + this.handleUniverseChange(msg); + }); + this.setStatus(); + this.resetKeepalive(); + } + setStatus() { + this.node.status({ + fill: "green", + shape: "dot", + text: `Universe ${this.currentUniverse}`, + }); + } + parseUniverse(value) { + const universe = typeof value === "string" ? parseInt(value, 10) : value; + if (typeof universe !== "number" || !Number.isInteger(universe) || universe < 1 || universe > 63999) { + return undefined; } - parseUniverse(value) { - const universe = typeof value === "string" ? parseInt(value, 10) : value; - if (typeof universe !== "number" || !Number.isInteger(universe) || universe < 1 || universe > 63999) { - return undefined; - } - return universe; + return universe; + } + handleUniverseChange(msg) { + const universe = this.parseUniverse(msg.universe); + if (universe === undefined) { + this.node.warn( + `The given "universe"-property "${msg.universe}" (${typeof msg.universe}) is invalid or not between 1 and 63999.`, + ); + return; } - handleUniverseChange(msg) { - const universe = this.parseUniverse(msg.universe); - if (universe === undefined) { - this.node.warn(`The given "universe"-property "${msg.universe}" (${typeof msg.universe}) is invalid or not between 1 and 63999.`); - return; - } - if (universe === this.currentUniverse) { - return; - } - this.sACN.removeUniverse(this.currentUniverse); - this.sACN.addUniverse(universe); - this.data?.delete(this.currentUniverse); - this.currentUniverse = universe; - this.setStatus(); - if (this.config.clearOnUniverseChange) { - this.data?.set(universe, (0, dmx_1.nulledUniverse)()); - this.emitFull(universe); - } - else { - this.resetKeepalive(); - } + if (universe === this.currentUniverse) { + return; } - applyFrame(incoming, universe) { - const previous = this.data?.get(universe); - const full = (0, dmx_1.nulledUniverse)(); - Object.keys(incoming).forEach((key) => { - const ch = parseInt(key, 10); - if (ch >= 1 && ch <= 512) { - full[ch] = (0, dmx_1.fromPercent)(incoming[ch], this.scale); - } - }); - let changed = false; - const changes = {}; - for (let ch = 1; ch <= 512; ch++) { - const before = previous ? previous[ch] : 0; - if (before !== full[ch]) { - changed = true; - changes[ch] = full[ch]; - } - } - this.data?.set(universe, full); - const payload = this.config.output === "changes" ? changes : full; - return { payload, changed }; + this.sACN.removeUniverse(this.currentUniverse); + this.sACN.addUniverse(universe); + this.data?.delete(this.currentUniverse); + this.currentUniverse = universe; + this.setStatus(); + if (this.config.clearOnUniverseChange) { + this.data?.set(universe, (0, dmx_1.nulledUniverse)()); + this.emitFull(universe); + } else { + this.resetKeepalive(); } - sendData(msg) { - if (msg.payload && typeof msg.payload === "object") { - msg = { ...msg, payload: { ...msg.payload } }; - } - this.node.send(msg); - this.resetKeepalive(); + } + applyFrame(incoming, universe) { + const previous = this.data?.get(universe); + const full = (0, dmx_1.nulledUniverse)(); + Object.keys(incoming).forEach((key) => { + const ch = parseInt(key, 10); + if (ch >= 1 && ch <= 512) { + full[ch] = (0, dmx_1.fromPercent)(incoming[ch], this.scale); + } + }); + let changed = false; + const changes = {}; + for (let ch = 1; ch <= 512; ch++) { + const before = previous ? previous[ch] : 0; + if (before !== full[ch]) { + changed = true; + changes[ch] = full[ch]; + } } - emitFull(universe) { - const full = this.data?.get(universe) ?? (0, dmx_1.nulledUniverse)(); - this.sendData({ universe, payload: full }); + this.data?.set(universe, full); + const payload = this.config.output === "changes" ? changes : full; + return { payload, changed }; + } + sendData(msg) { + if (msg.payload && typeof msg.payload === "object") { + msg = { ...msg, payload: { ...msg.payload } }; } - resetKeepalive() { - if (this.trigger !== "interval") { - return; - } - if (this.keepaliveTimer) { - clearTimeout(this.keepaliveTimer); - } - this.keepaliveTimer = setTimeout(() => { - this.keepaliveTick(); - }, this.interval); + this.node.send(msg); + this.resetKeepalive(); + } + emitFull(universe) { + const full = this.data?.get(universe) ?? (0, dmx_1.nulledUniverse)(); + this.sendData({ universe, payload: full }); + } + resetKeepalive() { + if (this.trigger !== "interval") { + return; } - keepaliveTick() { - if (this.data?.has(this.currentUniverse)) { - this.emitFull(this.currentUniverse); - } - else { - this.resetKeepalive(); - } + if (this.keepaliveTimer) { + clearTimeout(this.keepaliveTimer); + } + this.keepaliveTimer = setTimeout(() => { + this.keepaliveTick(); + }, this.interval); + } + keepaliveTick() { + if (this.data?.has(this.currentUniverse)) { + this.emitFull(this.currentUniverse); + } else { + this.resetKeepalive(); } + } } exports.default = (RED) => { - (0, interfaces_1.registerInterfaceEndpoint)(RED); - RED.nodes.registerType("sacn-in", function (config) { - RED.nodes.createNode(this, config); - new NodeHandler(this, config); - }); + (0, interfaces_1.registerInterfaceEndpoint)(RED); + RED.nodes.registerType("sacn-in", function (config) { + RED.nodes.createNode(this, config); + new NodeHandler(this, config); + }); }; diff --git a/dist/nodes/sacn-out/sacn-out.js b/dist/nodes/sacn-out/sacn-out.js index da96859..0db05b5 100644 --- a/dist/nodes/sacn-out/sacn-out.js +++ b/dist/nodes/sacn-out/sacn-out.js @@ -5,80 +5,79 @@ const dmx_1 = require("../../lib/dmx"); const network_1 = require("../../lib/network"); const interfaces_1 = require("../../lib/interfaces"); class NodeHandler { - node; - config; - sACN; - options; - constructor(node, config) { - this.node = node; - this.config = config; - const network = (0, network_1.resolveNetworkOptions)(config); - this.options = { - universe: config.universe, - reuseAddr: config.reuseAddress !== undefined ? config.reuseAddress : true, - minRefreshRate: config.speed !== undefined ? config.speed : 0, - port: network.port, - defaultPacketOptions: { - useRawDmxValues: (config.values ?? "percent") === "absolute", - }, - }; - if (network.iface !== undefined) { - this.options.iface = network.iface; - } - this.sACN = new sacn_1.Sender(this.options); - this.sACN.on("error", (err) => { - this.node.error(err); - this.node.status({ fill: "red", shape: "dot", text: err.message || "sender error" }); - }); - this.node.on("close", (done) => { - const shutdown = () => { - this.sACN.close(); - done(); - }; - if (this.config.blankOnClose) { - this.sACN - .send({ - payload: (0, dmx_1.nulledUniverse)(), - sourceName: config.sourceName, - priority: config.priority || 100, - }) - .then(shutdown) - .catch(shutdown); - } - else { - shutdown(); - } - }); - this.node.on("input", (msg) => { - this.sACN - .send({ - payload: msg.payload, - sourceName: config.sourceName, - priority: config.priority || 100, - }) - .then(() => { - this.setStatus(); - }) - .catch((err) => { - this.node.error(err, msg); - this.node.status({ fill: "red", shape: "dot", text: err.message || "send failed" }); - }); - }); - this.setStatus(); + node; + config; + sACN; + options; + constructor(node, config) { + this.node = node; + this.config = config; + const network = (0, network_1.resolveNetworkOptions)(config); + this.options = { + universe: config.universe, + reuseAddr: config.reuseAddress !== undefined ? config.reuseAddress : true, + minRefreshRate: config.speed !== undefined ? config.speed : 0, + port: network.port, + defaultPacketOptions: { + useRawDmxValues: (config.values ?? "percent") === "absolute", + }, + }; + if (network.iface !== undefined) { + this.options.iface = network.iface; } - setStatus() { - const rate = this.config.speed !== undefined && this.config.speed > 0 ? `${this.config.speed} Hz` : "once"; - this.node.status({ - fill: "green", - shape: "dot", - text: `Universe ${this.config.universe} · ${rate}`, + this.sACN = new sacn_1.Sender(this.options); + this.sACN.on("error", (err) => { + this.node.error(err); + this.node.status({ fill: "red", shape: "dot", text: err.message || "sender error" }); + }); + this.node.on("close", (done) => { + const shutdown = () => { + this.sACN.close(); + done(); + }; + if (this.config.blankOnClose) { + this.sACN + .send({ + payload: (0, dmx_1.nulledUniverse)(), + sourceName: config.sourceName, + priority: config.priority || 100, + }) + .then(shutdown) + .catch(shutdown); + } else { + shutdown(); + } + }); + this.node.on("input", (msg) => { + this.sACN + .send({ + payload: msg.payload, + sourceName: config.sourceName, + priority: config.priority || 100, + }) + .then(() => { + this.setStatus(); + }) + .catch((err) => { + this.node.error(err, msg); + this.node.status({ fill: "red", shape: "dot", text: err.message || "send failed" }); }); - } + }); + this.setStatus(); + } + setStatus() { + const rate = this.config.speed !== undefined && this.config.speed > 0 ? `${this.config.speed} Hz` : "once"; + this.node.status({ + fill: "green", + shape: "dot", + text: `Universe ${this.config.universe} · ${rate}`, + }); + } } exports.default = (RED) => { - (0, interfaces_1.registerInterfaceEndpoint)(RED); - RED.nodes.registerType("sacn-out", function (config) { - RED.nodes.createNode(this, config); - new NodeHandler(this, config); - }); + (0, interfaces_1.registerInterfaceEndpoint)(RED); + RED.nodes.registerType("sacn-out", function (config) { + RED.nodes.createNode(this, config); + new NodeHandler(this, config); + }); }; diff --git a/dist/nodes/scene-controller/locales/de/scene-controller.json b/dist/nodes/scene-controller/locales/de/scene-controller.json index dbd233b..d4702f5 100644 --- a/dist/nodes/scene-controller/locales/de/scene-controller.json +++ b/dist/nodes/scene-controller/locales/de/scene-controller.json @@ -3,7 +3,11 @@ "label": { "values": "Werte", "values_percent": "Prozent (0–100)", - "values_absolute": "Absolut (0–255)" + "values_absolute": "Absolut (0–255)", + "playMode": "Wiedergabe", + "playMode_switch": "Umschalten (neue Szene ersetzt)", + "playMode_add": "Addieren (Szenen stapeln, HTP)", + "blackoutOnStop": "Beim Stoppen Nullwerte senden" } } } \ No newline at end of file diff --git a/dist/nodes/scene-controller/locales/en-US/scene-controller.json b/dist/nodes/scene-controller/locales/en-US/scene-controller.json index 579358a..7d1498c 100644 --- a/dist/nodes/scene-controller/locales/en-US/scene-controller.json +++ b/dist/nodes/scene-controller/locales/en-US/scene-controller.json @@ -3,7 +3,11 @@ "label": { "values": "values", "values_percent": "percent (0–100)", - "values_absolute": "absolute (0–255)" + "values_absolute": "absolute (0–255)", + "playMode": "Playback", + "playMode_switch": "Switch (new scene replaces)", + "playMode_add": "Add (stack scenes, HTP)", + "blackoutOnStop": "Send zero values on stop" } } } \ No newline at end of file diff --git a/dist/nodes/scene-controller/scene-controller.html b/dist/nodes/scene-controller/scene-controller.html index 812dccd..d287419 100644 --- a/dist/nodes/scene-controller/scene-controller.html +++ b/dist/nodes/scene-controller/scene-controller.html @@ -16,6 +16,23 @@ +
+ + +
+
+ + +