2026-05-12 23:47:53 +00:00
|
|
|
/**
|
|
|
|
|
* bf-power — POST /admin/kiosks/:id/power/(wake|standby) to wake or sleep
|
|
|
|
|
* the display attached to a kiosk. Server fans out to the kiosk over WS,
|
|
|
|
|
* the kiosk then runs CEC + DPMS sequentially.
|
|
|
|
|
*
|
|
|
|
|
* config.mode: "wake" | "standby" (can also be set via msg.mode)
|
|
|
|
|
* config.kiosk_id: numeric (can be overridden by msg.kiosk_id)
|
|
|
|
|
*/
|
|
|
|
|
module.exports = function (RED) {
|
|
|
|
|
function BfPowerNode(config) {
|
|
|
|
|
RED.nodes.createNode(this, config);
|
|
|
|
|
const node = this;
|
|
|
|
|
const cfg = RED.nodes.getNode(config.config);
|
|
|
|
|
|
|
|
|
|
node.on("input", async (msg, send, done) => {
|
|
|
|
|
if (!cfg || !cfg.server_url || !cfg.api_key) {
|
feat: restructure Node-RED nodes + server event emission
Renames:
- bf-config → bf-server-config (config node clarity)
- bf-event-in → bf-kiosk-camera-event (specific camera trigger)
New trigger nodes (input-only, under "BetterFrame Triggers"):
- bf-trigger-display-power, bf-trigger-layout-changed,
bf-trigger-kiosk-changed, bf-trigger-camera-changed
New flow nodes:
- bf-config-get: query state by type (displays/kiosks/cameras/layouts/
entities, or by-id)
- bf-config-set: mutate via typed setters (default-layout, enabled,
priority, name)
Server-side event emission:
- shared/strip-secrets.ts: recursive password scrub
- New JSON admin endpoints: GET/POST /api/admin/{displays,kiosks,
layouts,entities}[/:id]
- Coordinator-ws fires kiosk.changed on connect/disconnect/heartbeat
- Layout/power/camera routes call nodered.forward() on state change
2026-05-13 00:26:08 +00:00
|
|
|
node.status({ fill: "red", shape: "ring", text: "missing bf-server-config" });
|
|
|
|
|
return done(new Error("bf-server-config server_url + api_key required"));
|
2026-05-12 23:47:53 +00:00
|
|
|
}
|
|
|
|
|
const kioskId = msg.kiosk_id || config.kiosk_id;
|
|
|
|
|
const mode = (msg.mode || config.mode || "wake").toLowerCase();
|
|
|
|
|
if (!kioskId) {
|
|
|
|
|
node.status({ fill: "red", shape: "ring", text: "missing kiosk_id" });
|
|
|
|
|
return done(new Error("kiosk_id required"));
|
|
|
|
|
}
|
|
|
|
|
if (mode !== "wake" && mode !== "standby") {
|
|
|
|
|
node.status({ fill: "red", shape: "ring", text: "bad mode" });
|
|
|
|
|
return done(new Error("mode must be wake or standby"));
|
|
|
|
|
}
|
|
|
|
|
const url = cfg.server_url + "/admin/kiosks/" + encodeURIComponent(String(kioskId)) +
|
|
|
|
|
"/power/" + mode;
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch(url, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { authorization: "Bearer " + cfg.api_key },
|
|
|
|
|
redirect: "manual",
|
|
|
|
|
});
|
|
|
|
|
if (!r.ok && r.status !== 302) throw new Error("HTTP " + r.status);
|
|
|
|
|
node.status({ fill: "green", shape: "dot", text: mode + " " + kioskId });
|
|
|
|
|
msg.bf_result = { kiosk_id: Number(kioskId), mode, status: r.status };
|
|
|
|
|
send(msg);
|
|
|
|
|
done();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
node.status({ fill: "red", shape: "ring", text: err.message });
|
|
|
|
|
done(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
RED.nodes.registerType("bf-power", BfPowerNode);
|
|
|
|
|
};
|