new local system-control plugin
Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<!-- Locomotive body -->
|
||||
<rect x="12" y="32" width="62" height="34" rx="5" fill="#ffffff"/>
|
||||
<!-- Cab section -->
|
||||
<rect x="12" y="22" width="28" height="13" rx="4" fill="#ffffff"/>
|
||||
<!-- Cab window -->
|
||||
<rect x="16" y="25" width="20" height="8" rx="2" fill="#87ceeb"/>
|
||||
<!-- Chimney -->
|
||||
<rect x="64" y="18" width="10" height="16" rx="3" fill="#ffffff"/>
|
||||
<!-- Smoke puff -->
|
||||
<circle cx="69" cy="14" r="5" fill="#cccccc" opacity="0.7"/>
|
||||
<circle cx="75" cy="11" r="3.5" fill="#cccccc" opacity="0.5"/>
|
||||
<!-- Front light -->
|
||||
<circle cx="74" cy="46" r="5" fill="#f0c040"/>
|
||||
<!-- Wheels -->
|
||||
<circle cx="27" cy="68" r="9" fill="#444444" stroke="#ffffff" stroke-width="2"/>
|
||||
<circle cx="27" cy="68" r="3" fill="#ffffff"/>
|
||||
<circle cx="52" cy="68" r="9" fill="#444444" stroke="#ffffff" stroke-width="2"/>
|
||||
<circle cx="52" cy="68" r="3" fill="#ffffff"/>
|
||||
<circle cx="72" cy="68" r="6" fill="#444444" stroke="#ffffff" stroke-width="2"/>
|
||||
<circle cx="72" cy="68" r="2" fill="#ffffff"/>
|
||||
<!-- Rails -->
|
||||
<rect x="5" y="77" width="90" height="4" rx="2" fill="#aaaaaa"/>
|
||||
<rect x="5" y="85" width="90" height="4" rx="2" fill="#aaaaaa"/>
|
||||
<!-- Sleepers -->
|
||||
<rect x="12" y="76" width="6" height="14" fill="#888888"/>
|
||||
<rect x="26" y="76" width="6" height="14" fill="#888888"/>
|
||||
<rect x="40" y="76" width="6" height="14" fill="#888888"/>
|
||||
<rect x="54" y="76" width="6" height="14" fill="#888888"/>
|
||||
<rect x="68" y="76" width="6" height="14" fill="#888888"/>
|
||||
<rect x="82" y="76" width="6" height="14" fill="#888888"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
+12
-1
@@ -18,6 +18,17 @@
|
||||
"PR Filter": "PR-Filter",
|
||||
"Assigned to me": "Mir zugewiesen",
|
||||
"Review requested": "Review angefordert",
|
||||
"Both": "Beides"
|
||||
"Both": "Beides",
|
||||
"Server": "Server",
|
||||
"Last Rised": "Letzter Anstieg",
|
||||
"Pull Requests": "Pull Requests",
|
||||
"API Error": "API-Fehler",
|
||||
"Offline": "Offline",
|
||||
"Connecting…": "Verbinde…",
|
||||
"No Host": "Kein Host",
|
||||
"Off": "Aus",
|
||||
"Day": "Tag",
|
||||
"Night": "Nacht",
|
||||
"Simulation": "Simulation"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,17 @@
|
||||
"PR Filter": "PR Filter",
|
||||
"Assigned to me": "Assigned to me",
|
||||
"Review requested": "Review requested",
|
||||
"Both": "Both"
|
||||
"Both": "Both",
|
||||
"Server": "Server",
|
||||
"Last Rised": "Last Rised",
|
||||
"Pull Requests": "Pull Requests",
|
||||
"API Error": "API Error",
|
||||
"Offline": "Offline",
|
||||
"Connecting…": "Connecting…",
|
||||
"No Host": "No Host",
|
||||
"Off": "Off",
|
||||
"Day": "Day",
|
||||
"Night": "Night",
|
||||
"Simulation": "Simulation"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,19 @@
|
||||
],
|
||||
"Tooltip": "Shows open pull requests assigned to you across all repositories",
|
||||
"UUID": "dev.mars3142.ulanzideck.collection.giteapr"
|
||||
},
|
||||
{
|
||||
"Name": "Model Railway",
|
||||
"Icon": "assets/icons/modelrailway.svg",
|
||||
"PropertyInspectorPath": "property-inspector/modelrailway/inspector.html",
|
||||
"States": [
|
||||
{
|
||||
"Name": "Default",
|
||||
"Image": "assets/icons/modelrailway.svg"
|
||||
}
|
||||
],
|
||||
"Tooltip": "Shows light color, time and mode from an ESP32 model railway controller via WebSocket",
|
||||
"UUID": "dev.mars3142.ulanzideck.collection.modelrailway"
|
||||
}
|
||||
],
|
||||
"OS": [
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
class ModelRailwayAction extends ActionBase {
|
||||
constructor($UD, context) {
|
||||
super($UD, context);
|
||||
this.config = { host: '' };
|
||||
this.ws = null;
|
||||
this.wsReconnectTimer = null;
|
||||
this.clockTimer = null;
|
||||
this.lightOn = false;
|
||||
this.mode = '';
|
||||
this.color = { r: 0, g: 0, b: 0 };
|
||||
this.simulationClock = null;
|
||||
|
||||
this.$UD.onDidReceiveSettings(jsn => {
|
||||
if (jsn.context !== this.context) return;
|
||||
const s = jsn.settings || {};
|
||||
const prevHost = this.config.host;
|
||||
this.config = Object.assign(this.config, s);
|
||||
if (this.config.host !== prevHost) {
|
||||
this.connectWebSocket();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setActive() {}
|
||||
|
||||
setParams(jsn) {
|
||||
this.config = Object.assign(this.config, (jsn && jsn.param) || {});
|
||||
this.$UD.getSettings(this.context);
|
||||
this.connectWebSocket();
|
||||
this.startClockTimer();
|
||||
}
|
||||
|
||||
onRun() {
|
||||
if (!this.config.host) return;
|
||||
fetch(`http://${this.config.host}/api/light/power`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ on: !this.lightOn }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
onClear() {
|
||||
super.onClear();
|
||||
this.stopClockTimer();
|
||||
this.disconnectWebSocket();
|
||||
}
|
||||
|
||||
startClockTimer() {
|
||||
this.stopClockTimer();
|
||||
this.clockTimer = setInterval(() => this.renderButton(), 1000);
|
||||
}
|
||||
|
||||
stopClockTimer() {
|
||||
if (this.clockTimer) {
|
||||
clearInterval(this.clockTimer);
|
||||
this.clockTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
disconnectWebSocket() {
|
||||
if (this.wsReconnectTimer) {
|
||||
clearTimeout(this.wsReconnectTimer);
|
||||
this.wsReconnectTimer = null;
|
||||
}
|
||||
if (this.ws) {
|
||||
this.ws.onclose = null;
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
connectWebSocket() {
|
||||
this.disconnectWebSocket();
|
||||
if (!this.config.host) {
|
||||
this.renderButton();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(`ws://${this.config.host}/ws`);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.ws.send(JSON.stringify({ type: 'getStatus' }));
|
||||
this.renderButton();
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'status') {
|
||||
this.lightOn = msg.on;
|
||||
this.mode = msg.mode || '';
|
||||
if (msg.color) this.color = msg.color;
|
||||
this.simulationClock = msg.clock || null;
|
||||
} else if (msg.type === 'color') {
|
||||
this.color = { r: msg.r, g: msg.g, b: msg.b };
|
||||
}
|
||||
this.renderButton();
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
this.renderButton();
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.ws = null;
|
||||
this.renderButton();
|
||||
this.wsReconnectTimer = setTimeout(() => this.connectWebSocket(), 3000);
|
||||
};
|
||||
} catch (e) {
|
||||
this.ws = null;
|
||||
this.renderButton();
|
||||
}
|
||||
}
|
||||
|
||||
formatTime() {
|
||||
const now = new Date();
|
||||
const h = String(now.getHours()).padStart(2, '0');
|
||||
const m = String(now.getMinutes()).padStart(2, '0');
|
||||
return `${h}:${m}`;
|
||||
}
|
||||
|
||||
modeLabel(mode) {
|
||||
switch (mode) {
|
||||
case 'day': return 'Tag';
|
||||
case 'night': return 'Nacht';
|
||||
case 'simulation': return 'Simulation';
|
||||
default: return mode || '---';
|
||||
}
|
||||
}
|
||||
|
||||
modeColor(mode) {
|
||||
switch (mode) {
|
||||
case 'day': return '#f0c040';
|
||||
case 'night': return '#7ec8e3';
|
||||
case 'simulation': return '#6bff6b';
|
||||
default: return '#888888';
|
||||
}
|
||||
}
|
||||
|
||||
renderButton() {
|
||||
const { canvas, ctx } = this.createCanvas();
|
||||
const connected = this.ws && this.ws.readyState === WebSocket.OPEN;
|
||||
|
||||
// Color strip at top
|
||||
const { r, g, b } = this.color;
|
||||
ctx.fillStyle = (connected && this.lightOn) ? `rgb(${r},${g},${b})` : '#2a2a2a';
|
||||
ctx.fillRect(0, 0, 196, 46);
|
||||
|
||||
// Time (center) — simulation clock from ESP32, otherwise local time
|
||||
const timeText = (this.mode === 'simulation' && this.simulationClock)
|
||||
? this.simulationClock
|
||||
: this.formatTime();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 54px "Source Han Sans SC"';
|
||||
ctx.fillText(timeText, 98, 110);
|
||||
|
||||
// Status / Mode (bottom)
|
||||
if (!connected) {
|
||||
ctx.fillStyle = '#ff6b6b';
|
||||
ctx.font = '22px "Source Han Sans SC"';
|
||||
ctx.fillText(this.config.host ? 'Verbinde…' : 'Kein Host', 98, 162);
|
||||
} else if (!this.lightOn) {
|
||||
ctx.fillStyle = '#888888';
|
||||
ctx.font = '26px "Source Han Sans SC"';
|
||||
ctx.fillText('Aus', 98, 162);
|
||||
} else {
|
||||
ctx.fillStyle = this.modeColor(this.mode);
|
||||
ctx.font = '24px "Source Han Sans SC"';
|
||||
ctx.fillText(this.modeLabel(this.mode), 98, 162);
|
||||
}
|
||||
|
||||
this.setIcon(canvas);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
<script src="./actions/CopilotAction.js"></script>
|
||||
<script src="./actions/GiteaAction.js"></script>
|
||||
<script src="./actions/GiteaPRAction.js"></script>
|
||||
<script src="./actions/ModelRailwayAction.js"></script>
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,6 +19,8 @@ $UD.onAdd(jsn => {
|
||||
ACTION_CACHES[context] = new GiteaAction($UD, context);
|
||||
} else if (name === 'giteapr') {
|
||||
ACTION_CACHES[context] = new GiteaPRAction($UD, context);
|
||||
} else if (name === 'modelrailway') {
|
||||
ACTION_CACHES[context] = new ModelRailwayAction($UD, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
|
||||
<title>Model Railway</title>
|
||||
<link rel="stylesheet" href="../../libs/css/uspi.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="uspi-wrapper">
|
||||
<form id="property-inspector">
|
||||
<div class="uspi-item">
|
||||
<div class="uspi-item-label" data-localize>Server</div>
|
||||
<input class="uspi-item-value" type="text" name="host" placeholder="system-control.local" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="../../libs/js/constants.js"></script>
|
||||
<script src="../../libs/js/eventEmitter.js"></script>
|
||||
<script src="../../libs/js/utils.js"></script>
|
||||
<script src="../../libs/js/ulanzideckApi.js"></script>
|
||||
<script src="./inspector.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
let ACTION_SETTING = {};
|
||||
let form = '';
|
||||
|
||||
$UD.connect('dev.mars3142.ulanzideck.collection.modelrailway');
|
||||
|
||||
$UD.onConnected(() => {
|
||||
form = document.querySelector('#property-inspector');
|
||||
|
||||
form.addEventListener('input', Utils.debounce(() => {
|
||||
const value = Utils.getFormValue(form);
|
||||
ACTION_SETTING = { ...ACTION_SETTING, ...value };
|
||||
$UD.sendParamFromPlugin(ACTION_SETTING);
|
||||
}));
|
||||
});
|
||||
|
||||
$UD.onAdd(jsn => {
|
||||
if (jsn && jsn.param) settingSaveParam(jsn.param);
|
||||
});
|
||||
|
||||
$UD.onParamFromApp(jsn => {
|
||||
settingSaveParam((jsn && jsn.param) || {});
|
||||
});
|
||||
|
||||
$UD.onParamFromPlugin(jsn => {
|
||||
settingSaveParam((jsn && jsn.param) || {});
|
||||
});
|
||||
|
||||
function settingSaveParam(params) {
|
||||
ACTION_SETTING = { ...ACTION_SETTING, ...params };
|
||||
if (form) Utils.setFormValue(ACTION_SETTING, form);
|
||||
}
|
||||
Reference in New Issue
Block a user