railway over mqtt
change copilot to mqtt Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
@@ -1,32 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
+7
-1
@@ -29,6 +29,12 @@
|
|||||||
"Off": "Aus",
|
"Off": "Aus",
|
||||||
"Day": "Tag",
|
"Day": "Tag",
|
||||||
"Night": "Nacht",
|
"Night": "Nacht",
|
||||||
"Simulation": "Simulation"
|
"Simulation": "Simulation",
|
||||||
|
"MQTT URI": "MQTT URI",
|
||||||
|
"Username": "Benutzername",
|
||||||
|
"Password": "Passwort",
|
||||||
|
"Device ID": "Geräte-ID",
|
||||||
|
"Topic": "Topic",
|
||||||
|
"Not configured": "Nicht konfiguriert"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,12 @@
|
|||||||
"Off": "Off",
|
"Off": "Off",
|
||||||
"Day": "Day",
|
"Day": "Day",
|
||||||
"Night": "Night",
|
"Night": "Night",
|
||||||
"Simulation": "Simulation"
|
"Simulation": "Simulation",
|
||||||
|
"MQTT URI": "MQTT URI",
|
||||||
|
"Username": "Username",
|
||||||
|
"Password": "Password",
|
||||||
|
"Device ID": "Device ID",
|
||||||
|
"Topic": "Topic",
|
||||||
|
"Not configured": "Not configured"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+19
File diff suppressed because one or more lines are too long
+6
-6
@@ -65,16 +65,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "Model Railway",
|
"Name": "Model Railway",
|
||||||
"Icon": "assets/icons/modelrailway.svg",
|
"Icon": "assets/icons/railway.png",
|
||||||
"PropertyInspectorPath": "property-inspector/modelrailway/inspector.html",
|
"PropertyInspectorPath": "property-inspector/railway/inspector.html",
|
||||||
"States": [
|
"States": [
|
||||||
{
|
{
|
||||||
"Name": "Default",
|
"Name": "Default",
|
||||||
"Image": "assets/icons/modelrailway.svg"
|
"Image": "assets/icons/railway.png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Tooltip": "Shows light color, time and mode from an ESP32 model railway controller via WebSocket",
|
"Tooltip": "Shows light color, time and mode from an ESP32 model railway controller via MQTT",
|
||||||
"UUID": "dev.mars3142.ulanzideck.collection.modelrailway"
|
"UUID": "dev.mars3142.ulanzideck.collection.railway"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"OS": [
|
"OS": [
|
||||||
@@ -90,4 +90,4 @@
|
|||||||
"Software": {
|
"Software": {
|
||||||
"MinimumVersion": "2.1.18"
|
"MinimumVersion": "2.1.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,56 +1,104 @@
|
|||||||
class CopilotAction extends ActionBase {
|
class CopilotAction extends ActionBase {
|
||||||
constructor($UD, context) {
|
constructor($UD, context) {
|
||||||
super($UD, context);
|
super($UD, context);
|
||||||
this.config = {
|
this.config = { uri: '', username: '', password: '', topic: '' };
|
||||||
url: '',
|
this.mqttClient = null;
|
||||||
refreshRate: '4',
|
this.value = null;
|
||||||
};
|
this.isError = false;
|
||||||
|
|
||||||
|
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.topic}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
setActive() {}
|
setActive() {}
|
||||||
|
|
||||||
setParams(jsn) {
|
setParams(jsn) {
|
||||||
this.config = Object.assign(this.config, (jsn && jsn.param) || {});
|
this.config = Object.assign(this.config, (jsn && jsn.param) || {});
|
||||||
this.startTimer(() => this.fetchData(), this.config.refreshRate);
|
this.$UD.getSettings(this.context);
|
||||||
|
this.connectMqtt();
|
||||||
}
|
}
|
||||||
|
|
||||||
onRun() {
|
onRun() {
|
||||||
this.fetchData();
|
if (!this.mqttClient || !this.mqttClient.connected) {
|
||||||
}
|
this.connectMqtt();
|
||||||
|
}
|
||||||
fetchData() {
|
}
|
||||||
this.debounce(async () => {
|
|
||||||
if (!this.config.url) {
|
onClear() {
|
||||||
this.renderGauge(null, false);
|
super.onClear();
|
||||||
return;
|
this.disconnectMqtt();
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const response = await fetch(this.config.url);
|
disconnectMqtt() {
|
||||||
if (!response.ok) {
|
if (this.mqttClient) {
|
||||||
this.renderGauge(null, true);
|
this.mqttClient.end(true);
|
||||||
return;
|
this.mqttClient = null;
|
||||||
}
|
}
|
||||||
const text = await response.text();
|
}
|
||||||
const value = this.parsePrometheus(text);
|
|
||||||
this.renderGauge(value !== null ? parseFloat(value) : null, false);
|
connectMqtt() {
|
||||||
} catch (e) {
|
this.disconnectMqtt();
|
||||||
this.renderGauge(null, true);
|
if (!this.config.uri || !this.config.topic) {
|
||||||
}
|
this.renderGauge(null, false);
|
||||||
});
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
parsePrometheus(text) {
|
try {
|
||||||
for (const line of text.split('\n')) {
|
const opts = {
|
||||||
const trimmed = line.trim();
|
reconnectPeriod: 3000,
|
||||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
rejectUnauthorized: false,
|
||||||
if (trimmed.startsWith('github_copilot_usage_percentage')) {
|
};
|
||||||
const closingBrace = trimmed.indexOf('}');
|
if (this.config.username) opts.username = this.config.username;
|
||||||
if (closingBrace !== -1) return trimmed.slice(closingBrace + 1).trim();
|
if (this.config.password) opts.password = this.config.password;
|
||||||
const parts = trimmed.split(/\s+/);
|
|
||||||
if (parts.length >= 2) return parts[1];
|
this.mqttClient = mqtt.connect(this.config.uri, opts);
|
||||||
}
|
|
||||||
|
this.mqttClient.on('connect', () => {
|
||||||
|
this.isError = false;
|
||||||
|
this.mqttClient.subscribe(this.config.topic);
|
||||||
|
this.renderGauge(this.value, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mqttClient.on('message', (topic, payload) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(payload.toString());
|
||||||
|
if (msg.usage != null) {
|
||||||
|
this.value = parseFloat(msg.usage);
|
||||||
|
this.isError = false;
|
||||||
|
} else {
|
||||||
|
this.isError = true;
|
||||||
|
}
|
||||||
|
this.renderGauge(this.value, this.isError);
|
||||||
|
} catch (e) {
|
||||||
|
this.renderGauge(null, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mqttClient.on('reconnect', () => {
|
||||||
|
this.renderGauge(this.value, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mqttClient.on('error', () => {
|
||||||
|
this.isError = true;
|
||||||
|
this.renderGauge(null, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderGauge(this.value, false);
|
||||||
|
} catch (e) {
|
||||||
|
this.mqttClient = null;
|
||||||
|
this.renderGauge(null, true);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGauge(value, isError) {
|
renderGauge(value, isError) {
|
||||||
@@ -74,7 +122,9 @@ class CopilotAction extends ActionBase {
|
|||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
if (value !== null) {
|
const connected = this.mqttClient && this.mqttClient.connected;
|
||||||
|
|
||||||
|
if (value !== null && !isError && connected) {
|
||||||
const pct = Math.max(0, Math.min(100, value)) / 100;
|
const pct = Math.max(0, Math.min(100, value)) / 100;
|
||||||
const valueAngle = startAngle + pct * totalSweep;
|
const valueAngle = startAngle + pct * totalSweep;
|
||||||
|
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
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 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();
|
|
||||||
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 ? this.$UD.t('Connecting…') : this.$UD.t('No Host'), 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
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('(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -10,13 +10,14 @@
|
|||||||
<script src="../libs/js/timers.js"></script>
|
<script src="../libs/js/timers.js"></script>
|
||||||
<script src="../libs/js/utils.js"></script>
|
<script src="../libs/js/utils.js"></script>
|
||||||
<script src="../libs/js/ulanzideckApi.js"></script>
|
<script src="../libs/js/ulanzideckApi.js"></script>
|
||||||
|
<script src="../libs/js/mqtt.min.js"></script>
|
||||||
|
|
||||||
<script src="./ActionBase.js"></script>
|
<script src="./ActionBase.js"></script>
|
||||||
<script src="./actions/PetrolAction.js"></script>
|
<script src="./actions/PetrolAction.js"></script>
|
||||||
<script src="./actions/CopilotAction.js"></script>
|
<script src="./actions/CopilotAction.js"></script>
|
||||||
<script src="./actions/GiteaAction.js"></script>
|
<script src="./actions/GiteaAction.js"></script>
|
||||||
<script src="./actions/GiteaPRAction.js"></script>
|
<script src="./actions/GiteaPRAction.js"></script>
|
||||||
<script src="./actions/ModelRailwayAction.js"></script>
|
<script src="./actions/RailwayAction.js"></script>
|
||||||
<script src="./app.js"></script>
|
<script src="./app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+2
-2
@@ -22,8 +22,8 @@ $UD.onAdd(jsn => {
|
|||||||
ACTION_CACHES[context] = new GiteaAction($UD, context);
|
ACTION_CACHES[context] = new GiteaAction($UD, context);
|
||||||
} else if (name === 'giteapr') {
|
} else if (name === 'giteapr') {
|
||||||
ACTION_CACHES[context] = new GiteaPRAction($UD, context);
|
ACTION_CACHES[context] = new GiteaPRAction($UD, context);
|
||||||
} else if (name === 'modelrailway') {
|
} else if (name === 'railway') {
|
||||||
ACTION_CACHES[context] = new ModelRailwayAction($UD, context);
|
ACTION_CACHES[context] = new RailwayAction($UD, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,20 @@
|
|||||||
<div class="uspi-wrapper">
|
<div class="uspi-wrapper">
|
||||||
<form id="property-inspector">
|
<form id="property-inspector">
|
||||||
<div class="uspi-item">
|
<div class="uspi-item">
|
||||||
<div class="uspi-item-label" data-localize>URL</div>
|
<div class="uspi-item-label" data-localize>MQTT URI</div>
|
||||||
<input class="uspi-item-value" type="text" name="url" placeholder="https://..." />
|
<input class="uspi-item-value" type="text" name="uri" placeholder="wss://mqtt.example.com" />
|
||||||
</div>
|
</div>
|
||||||
<div class="uspi-item">
|
<div class="uspi-item">
|
||||||
<div class="uspi-item-label" data-localize>Refresh Rate</div>
|
<div class="uspi-item-label" data-localize>Username</div>
|
||||||
<select class="uspi-item-value" name="refreshRate">
|
<input class="uspi-item-value" type="text" name="username" />
|
||||||
<option value="0" data-localize>On Press</option>
|
</div>
|
||||||
<option value="1" data-localize>Every 1 min</option>
|
<div class="uspi-item">
|
||||||
<option value="2" data-localize>Every 2 min</option>
|
<div class="uspi-item-label" data-localize>Password</div>
|
||||||
<option value="3" data-localize>Every 5 min</option>
|
<input class="uspi-item-value" type="password" name="password" />
|
||||||
<option value="4" selected data-localize>Every 10 min</option>
|
</div>
|
||||||
<option value="5" data-localize>Every 30 min</option>
|
<div class="uspi-item">
|
||||||
<option value="6" data-localize>Every Hour</option>
|
<div class="uspi-item-label" data-localize>Topic</div>
|
||||||
</select>
|
<input class="uspi-item-value" type="text" name="topic" placeholder="copilot/usage" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<!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,37 @@
|
|||||||
|
<!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>MQTT URI</div>
|
||||||
|
<input class="uspi-item-value" type="text" name="uri" placeholder="wss://broker.example.com:8883" />
|
||||||
|
</div>
|
||||||
|
<div class="uspi-item">
|
||||||
|
<div class="uspi-item-label" data-localize>Username</div>
|
||||||
|
<input class="uspi-item-value" type="text" name="username" placeholder="user" />
|
||||||
|
</div>
|
||||||
|
<div class="uspi-item">
|
||||||
|
<div class="uspi-item-label" data-localize>Password</div>
|
||||||
|
<input class="uspi-item-value" type="password" name="password" />
|
||||||
|
</div>
|
||||||
|
<div class="uspi-item">
|
||||||
|
<div class="uspi-item-label" data-localize>Device ID</div>
|
||||||
|
<input class="uspi-item-value" type="text" name="deviceId" placeholder="DC:54:75:D6:09:AC" />
|
||||||
|
</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>
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
let ACTION_SETTING = {};
|
let ACTION_SETTING = {};
|
||||||
let form = '';
|
let form = '';
|
||||||
|
|
||||||
$UD.connect('dev.mars3142.ulanzideck.collection.modelrailway');
|
$UD.connect('dev.mars3142.ulanzideck.collection.railway');
|
||||||
|
|
||||||
$UD.onConnected(() => {
|
$UD.onConnected(() => {
|
||||||
form = document.querySelector('#property-inspector');
|
form = document.querySelector('#property-inspector');
|
||||||
Reference in New Issue
Block a user