class RailwayAction extends ActionBase { constructor($UD, context) { super($UD, context); this.config = { uri: '', username: '', password: '', deviceId: '' }; this.mqttClient = null; this.clockTimer = null; this.connected = false; this.lastError = 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 prevKey = this.connectionKey(); this.config = Object.assign(this.config, s); if (this.connectionKey() !== prevKey) { this.connectMqtt(); } }); } connectionKey() { return `${this.config.uri}|${this.config.username}|${this.config.deviceId}`; } buildTopic() { const last4 = this.config.deviceId.replace(/:/g, '').slice(-4).toLowerCase(); return `device/system_control/${last4}/status`; } setActive() {} setParams(jsn) { this.config = Object.assign(this.config, (jsn && jsn.param) || {}); this.$UD.getSettings(this.context); this.connectMqtt(); this.startClockTimer(); } onRun() {} onClear() { super.onClear(); this.stopClockTimer(); this.disconnectMqtt(); } startClockTimer() { this.stopClockTimer(); this.clockTimer = setInterval(() => this.renderButton(), 1000); } stopClockTimer() { if (this.clockTimer) { clearInterval(this.clockTimer); this.clockTimer = null; } } disconnectMqtt() { if (this.mqttClient) { this.mqttClient.end(true); this.mqttClient = null; } this.connected = false; } connectMqtt() { this.disconnectMqtt(); if (!this.config.uri || !this.config.deviceId) { this.renderButton(); return; } const log = (msg) => this.$UD.logMessage(`[Railway] ${msg}`, 'info'); if (typeof mqtt === 'undefined') { this.lastError = 'mqtt lib missing'; log('ERROR: mqtt.min.js not loaded'); this.renderButton(); return; } try { const opts = { reconnectPeriod: 3000, rejectUnauthorized: false, }; if (this.config.username) opts.username = this.config.username; if (this.config.password) opts.password = this.config.password; const topic = this.buildTopic(); log(`Connecting to ${this.config.uri}, topic: ${topic}`); this.mqttClient = mqtt.connect(this.config.uri, opts); this.mqttClient.on('connect', () => { this.connected = true; this.lastError = null; log(`Connected, subscribing to ${topic}`); this.mqttClient.subscribe(topic, (err) => { if (err) log(`Subscribe error: ${err.message}`); else log(`Subscribed to ${topic}`); }); this.renderButton(); }); this.mqttClient.on('message', (t, payload) => { try { const outer = JSON.parse(payload.toString()); const msg = outer.message || outer; this.lightOn = msg.on ?? this.lightOn; this.mode = msg.mode || this.mode; if (msg.color) this.color = msg.color; this.simulationClock = msg.clock || null; this.renderButton(); } catch (e) { log(`Parse error: ${e.message}`); } }); this.mqttClient.on('reconnect', () => { this.connected = false; log('Reconnecting…'); this.renderButton(); }); this.mqttClient.on('error', (err) => { this.connected = false; this.lastError = err ? err.message : 'unknown'; log(`Error: ${this.lastError}`); this.renderButton(); }); this.mqttClient.on('close', () => { this.connected = false; log('Connection closed'); this.renderButton(); }); this.renderButton(); } catch (e) { this.lastError = e.message; this.$UD.logMessage(`[Railway] Exception: ${e.message}`, 'error'); this.mqttClient = 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 this.$UD.t('Day'); case 'night': return this.$UD.t('Night'); case 'simulation': return this.$UD.t('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(); // Color strip at top const { r, g, b } = this.color; ctx.fillStyle = (this.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 (!this.connected) { ctx.fillStyle = '#ff6b6b'; const missing = !this.config.uri || !this.config.deviceId; if (this.lastError) { ctx.font = '16px "Source Han Sans SC"'; const short = this.lastError.length > 20 ? this.lastError.slice(0, 19) + '…' : this.lastError; ctx.fillText(short, 98, 155); ctx.font = '14px "Source Han Sans SC"'; ctx.fillStyle = '#888888'; ctx.fillText(this.$UD.t('See log'), 98, 175); } else { ctx.font = '22px "Source Han Sans SC"'; ctx.fillText(missing ? this.$UD.t('Not configured') : this.$UD.t('Connecting…'), 98, 162); } } else if (!this.lightOn) { ctx.fillStyle = '#888888'; ctx.font = '26px "Source Han Sans SC"'; ctx.fillText(this.$UD.t('Off'), 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); } }