diff --git a/assets/icons/modelrailway.svg b/assets/icons/modelrailway.svg
new file mode 100644
index 0000000..52cf44b
--- /dev/null
+++ b/assets/icons/modelrailway.svg
@@ -0,0 +1,32 @@
+
diff --git a/de_DE.json b/de_DE.json
index 53c0771..be73a2a 100644
--- a/de_DE.json
+++ b/de_DE.json
@@ -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"
}
}
diff --git a/en.json b/en.json
index 93d1479..43b41f0 100644
--- a/en.json
+++ b/en.json
@@ -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"
}
}
diff --git a/manifest.json b/manifest.json
index 448d370..ace0ab8 100644
--- a/manifest.json
+++ b/manifest.json
@@ -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": [
diff --git a/plugin/actions/ModelRailwayAction.js b/plugin/actions/ModelRailwayAction.js
new file mode 100644
index 0000000..43ede2e
--- /dev/null
+++ b/plugin/actions/ModelRailwayAction.js
@@ -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);
+ }
+}
diff --git a/plugin/app.html b/plugin/app.html
index 91f8782..c79ad27 100644
--- a/plugin/app.html
+++ b/plugin/app.html
@@ -16,6 +16,7 @@
+