Files
2026-04-21 08:15:39 +02:00

222 lines
6.2 KiB
JavaScript

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);
}
}