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 @@ + diff --git a/plugin/app.js b/plugin/app.js index e6f8c1a..08a0561 100644 --- a/plugin/app.js +++ b/plugin/app.js @@ -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); } } diff --git a/property-inspector/modelrailway/inspector.html b/property-inspector/modelrailway/inspector.html new file mode 100644 index 0000000..56e9f66 --- /dev/null +++ b/property-inspector/modelrailway/inspector.html @@ -0,0 +1,25 @@ + + + + + + Model Railway + + + +
+
+
+
Server
+ +
+
+
+ + + + + + + + diff --git a/property-inspector/modelrailway/inspector.js b/property-inspector/modelrailway/inspector.js new file mode 100644 index 0000000..9e75ef6 --- /dev/null +++ b/property-inspector/modelrailway/inspector.js @@ -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); +}