initialize generic rms-software repository
Add the reusable RMS core application (server, web UI, plugins, tests, tools) with generic defaults, GPL licensing, and maintainer context documentation so deployments can consume this repo as software source independent of station-specific overlays.
This commit is contained in:
158
plugins/rms.tx.control.native/index.js
Normal file
158
plugins/rms.tx.control.native/index.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
async function createPlugin(ctx) {
|
||||
return {
|
||||
async execute(action) {
|
||||
if (action === "enableTx") {
|
||||
return setTxState(ctx, true);
|
||||
}
|
||||
if (action === "disableTx") {
|
||||
return setTxState(ctx, false);
|
||||
}
|
||||
if (action === "getTxState") {
|
||||
return getTxState(ctx);
|
||||
}
|
||||
throw new Error(`Unknown action: ${action}`);
|
||||
},
|
||||
async getStatus() {
|
||||
return getTxState(ctx);
|
||||
},
|
||||
async health() {
|
||||
return { ok: true };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function setTxState(ctx, nextActive) {
|
||||
const stateFilePath = resolvePath(String(ctx.getSetting("stateFilePath", defaultStateFilePath(ctx))));
|
||||
const timeoutMs = Number(ctx.getSetting("timeoutMs", ctx.env.TX_CONTROL_TIMEOUT_MS || 20000));
|
||||
const settingValue = String(ctx.getSetting(nextActive ? "enableCommand" : "disableCommand", "")).trim();
|
||||
const envValue = String(nextActive ? (ctx.env.TX_ENABLE_CMD || defaultEnableCommand()) : (ctx.env.TX_DISABLE_CMD || defaultDisableCommand())).trim();
|
||||
const command = settingValue || envValue;
|
||||
|
||||
const simulate = typeof ctx.simulateHardware === "boolean"
|
||||
? ctx.simulateHardware
|
||||
: !(process.platform === "linux" || String(ctx.env.ALLOW_NON_LINUX_CMDS || "false") === "true");
|
||||
|
||||
if (!command && !simulate) {
|
||||
throw new Error(nextActive ? "TX enable command missing" : "TX disable command missing");
|
||||
}
|
||||
|
||||
const normalizedCommand = String(command || "").trim().toLowerCase();
|
||||
const commandIsNoopSuccess = normalizedCommand === "true" || normalizedCommand === ":";
|
||||
|
||||
if (command && !simulate && !commandIsNoopSuccess) {
|
||||
const result = await ctx.commandRunner(command, { timeoutMs });
|
||||
if (!result.ok) {
|
||||
throw new Error(result.stderr || result.error || `TX ${nextActive ? "enable" : "disable"} failed`);
|
||||
}
|
||||
}
|
||||
|
||||
await writeStateFile(stateFilePath, {
|
||||
txActive: nextActive,
|
||||
updatedAt: new Date().toISOString(),
|
||||
source: "rms.tx.control.native"
|
||||
});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
txActive: nextActive,
|
||||
updatedAt: new Date().toISOString(),
|
||||
source: "rms.tx.control.native",
|
||||
skipped: (simulate && Boolean(command)) || commandIsNoopSuccess,
|
||||
message: simulate ? `TX ${nextActive ? "ON" : "OFF"} simuliert (${ctx.execMode || "dev"})` : `TX ${nextActive ? "ON" : "OFF"}`
|
||||
};
|
||||
}
|
||||
|
||||
async function getTxState(ctx) {
|
||||
const statusCommand = String(ctx.getSetting("statusCommand", ctx.env.TX_STATUS_CMD || defaultStatusCommand())).trim();
|
||||
const stateFilePath = resolvePath(String(ctx.getSetting("stateFilePath", defaultStateFilePath(ctx))));
|
||||
const timeoutMs = Number(ctx.getSetting("timeoutMs", ctx.env.TX_CONTROL_TIMEOUT_MS || 20000));
|
||||
const simulate = typeof ctx.simulateHardware === "boolean"
|
||||
? ctx.simulateHardware
|
||||
: !(process.platform === "linux" || String(ctx.env.ALLOW_NON_LINUX_CMDS || "false") === "true");
|
||||
|
||||
if (statusCommand && !simulate) {
|
||||
const result = await ctx.commandRunner(statusCommand, { timeoutMs });
|
||||
if (result.ok) {
|
||||
const text = String(result.stdout || "").trim().toLowerCase();
|
||||
if (text === "1" || text === "true" || text === "on") {
|
||||
return {
|
||||
txActive: true,
|
||||
source: "rms.tx.control.native",
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
if (text === "0" || text === "false" || text === "off") {
|
||||
return {
|
||||
txActive: false,
|
||||
source: "rms.tx.control.native",
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = {
|
||||
txActive: false,
|
||||
source: "rms.tx.control.native",
|
||||
updatedAt: null
|
||||
};
|
||||
if (!stateFilePath || !fs.existsSync(stateFilePath)) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
const raw = fs.readFileSync(stateFilePath, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
txActive: Boolean(parsed && parsed.txActive),
|
||||
source: parsed && parsed.source ? parsed.source : "rms.tx.control.native",
|
||||
updatedAt: parsed && parsed.updatedAt ? parsed.updatedAt : null,
|
||||
details: parsed && typeof parsed === "object" ? parsed : null
|
||||
};
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeStateFile(filePath, state) {
|
||||
if (!filePath) return;
|
||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.promises.writeFile(filePath, JSON.stringify(state, null, 2));
|
||||
}
|
||||
|
||||
function defaultStateFilePath(ctx) {
|
||||
const explicit = String(ctx.env.TX_STATE_PATH || "").trim();
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
const dataDir = String(ctx.env.DATA_DIR || "").trim();
|
||||
if (dataDir) {
|
||||
return path.join(dataDir, "tx-state.json");
|
||||
}
|
||||
return "./data/tx-state.json";
|
||||
}
|
||||
|
||||
function defaultEnableCommand() {
|
||||
return "/opt/remotestation/bin/tx-control.sh enable";
|
||||
}
|
||||
|
||||
function defaultDisableCommand() {
|
||||
return "/opt/remotestation/bin/tx-control.sh disable";
|
||||
}
|
||||
|
||||
function defaultStatusCommand() {
|
||||
return "/opt/remotestation/bin/tx-control.sh status";
|
||||
}
|
||||
|
||||
function resolvePath(value) {
|
||||
const v = String(value || "").trim();
|
||||
if (!v) return "";
|
||||
if (path.isAbsolute(v)) return v;
|
||||
return path.resolve(process.cwd(), v);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createPlugin
|
||||
};
|
||||
59
plugins/rms.tx.control.native/manifest.json
Normal file
59
plugins/rms.tx.control.native/manifest.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"id": "rms.tx.control.native",
|
||||
"name": "TX Control Native",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "1.0",
|
||||
"capabilities": [
|
||||
"tx.control",
|
||||
"tx.state.read"
|
||||
],
|
||||
"settingsSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enableCommand": { "type": "string" },
|
||||
"disableCommand": { "type": "string" },
|
||||
"statusCommand": { "type": "string" },
|
||||
"stateFilePath": { "type": "string" },
|
||||
"timeoutMs": { "type": "integer", "minimum": 1000 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"uiControls": [
|
||||
{
|
||||
"controlId": "tx-control-native",
|
||||
"controlType": "switch.group",
|
||||
"title": "TX Steuerung",
|
||||
"capability": "tx.control",
|
||||
"actions": [
|
||||
{
|
||||
"name": "enableTx",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": { "type": "string", "default": "plugin-ui" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disableTx",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": { "type": "string", "default": "plugin-ui" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "getTxState",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user