feat: refactor MQTT from global config to multi-instance entity model
Lint & Test / test (push) Successful in 1m32s
Lint & Test / test (push) Successful in 1m32s
MQTT broker connections are now managed as entities (like HA sources) instead of a single global config. Each MQTTSource gets its own runtime with auto-reconnect, ref-counted via MQTTManager. Backend: - MQTTSource dataclass + MQTTSourceStore (SQLite) - MQTTRuntime (per-broker connection, refactored from MQTTService) - MQTTManager (ref-counted pool, same pattern as HAManager) - CRUD API at /api/v1/mqtt/sources + test + status endpoints - MQTTRule gains mqtt_source_id field for source selection - Automation engine acquires/releases MQTT runtimes automatically - Legacy MQTTService kept for backward compat during transition Frontend: - MQTT source cards in Streams > Integrations tab - Create/edit modal with test button - Dashboard integration cards with health-dot indicators - Removed MQTT tab from settings modal
This commit is contained in:
@@ -194,7 +194,7 @@ import {
|
||||
openSettingsModal, closeSettingsModal, switchSettingsTab,
|
||||
downloadBackup, handleRestoreFileSelected,
|
||||
saveAutoBackupSettings, triggerBackupNow, restoreSavedBackup, downloadSavedBackup, deleteSavedBackup,
|
||||
restartServer, saveMqttSettings,
|
||||
restartServer,
|
||||
loadApiKeysList,
|
||||
connectLogViewer, disconnectLogViewer, clearLogViewer, applyLogFilter,
|
||||
openLogOverlay, closeLogOverlay,
|
||||
@@ -562,7 +562,6 @@ Object.assign(window, {
|
||||
downloadSavedBackup,
|
||||
deleteSavedBackup,
|
||||
restartServer,
|
||||
saveMqttSettings,
|
||||
loadApiKeysList,
|
||||
connectLogViewer,
|
||||
disconnectLogViewer,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DataCache } from './cache.ts';
|
||||
import type {
|
||||
Device, OutputTarget, ColorStripSource, PatternTemplate,
|
||||
ValueSource, AudioSource, PictureSource, ScenePreset,
|
||||
SyncClock, WeatherSource, HomeAssistantSource, Asset, Automation, Display, FilterDef, EngineInfo,
|
||||
SyncClock, WeatherSource, HomeAssistantSource, MQTTSource, Asset, Automation, Display, FilterDef, EngineInfo,
|
||||
CaptureTemplate, PostprocessingTemplate, AudioTemplate,
|
||||
ColorStripProcessingTemplate, FilterInstance, KeyColorRectangle,
|
||||
GameIntegration, GameAdapterInfo,
|
||||
@@ -279,6 +279,14 @@ export const haSourcesCache = new DataCache<HomeAssistantSource[]>({
|
||||
});
|
||||
haSourcesCache.subscribe(v => { _cachedHASources = v; });
|
||||
|
||||
export let _cachedMQTTSources: MQTTSource[] = [];
|
||||
|
||||
export const mqttSourcesCache = new DataCache<MQTTSource[]>({
|
||||
endpoint: '/mqtt/sources',
|
||||
extractData: json => json.sources || [],
|
||||
});
|
||||
mqttSourcesCache.subscribe(v => { _cachedMQTTSources = v; });
|
||||
|
||||
export const assetsCache = new DataCache<Asset[]>({
|
||||
endpoint: '/assets',
|
||||
extractData: json => json.assets || [],
|
||||
|
||||
@@ -11,11 +11,12 @@ import { startAutoRefresh, updateTabBadge } from './tabs.ts';
|
||||
import {
|
||||
ICON_TARGET, ICON_AUTOMATION, ICON_CLOCK, ICON_WARNING, ICON_OK,
|
||||
ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE, ICON_HELP, ICON_SCENE,
|
||||
ICON_PLUG, ICON_HOME, ICON_RADIO,
|
||||
} from '../core/icons.ts';
|
||||
import { loadScenePresets, renderScenePresetsSection, initScenePresetDelegation } from './scene-presets.ts';
|
||||
import { cardColorStyle } from '../core/card-colors.ts';
|
||||
import { createFpsSparkline } from '../core/chart-utils.ts';
|
||||
import type { Device, OutputTarget, ColorStripSource, ScenePreset, SyncClock, Automation } from '../types.ts';
|
||||
import type { Device, OutputTarget, ColorStripSource, ScenePreset, SyncClock, Automation, HomeAssistantConnectionStatus, HomeAssistantStatusResponse, MQTTConnectionStatus, MQTTStatusResponse } from '../types.ts';
|
||||
|
||||
const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed';
|
||||
const MAX_FPS_SAMPLES = 120;
|
||||
@@ -255,6 +256,89 @@ function _updateSyncClocksInPlace(syncClocks: SyncClock[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
function _renderIntegrationCard(conn: HomeAssistantConnectionStatus): string {
|
||||
const healthClass = conn.connected ? 'health-online' : 'health-offline';
|
||||
const healthTitle = conn.connected
|
||||
? `${t('ha_source.connected')} — ${conn.entity_count} ${t('dashboard.integrations.entities')}`
|
||||
: t('ha_source.disconnected');
|
||||
const statusDot = `<span class="health-dot ${healthClass}" title="${escapeHtml(healthTitle)}" role="status"></span>`;
|
||||
const subtitle = conn.connected
|
||||
? `${conn.entity_count} ${t('dashboard.integrations.entities')}`
|
||||
: t('ha_source.disconnected');
|
||||
|
||||
return `<div class="dashboard-target dashboard-autostart dashboard-card-link" data-integration-id="${conn.source_id}" onclick="if(!event.target.closest('button')){navigateToCard('streams','home_assistant','ha-sources','data-id','${conn.source_id}')}">
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${ICON_HOME}</span>
|
||||
<div>
|
||||
<div class="dashboard-target-name"><span class="dashboard-target-name-text">${escapeHtml(conn.name)}</span>${statusDot}</div>
|
||||
<div class="dashboard-target-subtitle">${escapeHtml(subtitle)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _renderMQTTIntegrationCard(conn: MQTTConnectionStatus): string {
|
||||
const healthClass = conn.connected ? 'health-online' : 'health-offline';
|
||||
const healthTitle = conn.connected ? t('mqtt_source.connected') : t('mqtt_source.disconnected');
|
||||
const statusDot = `<span class="health-dot ${healthClass}" title="${escapeHtml(healthTitle)}" role="status"></span>`;
|
||||
const subtitle = conn.connected ? escapeHtml(conn.broker) : t('mqtt_source.disconnected');
|
||||
|
||||
return `<div class="dashboard-target dashboard-autostart dashboard-card-link" data-integration-id="${conn.source_id}" onclick="if(!event.target.closest('button')){navigateToCard('streams','mqtt','mqtt-sources','data-id','${conn.source_id}')}">
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${ICON_RADIO}</span>
|
||||
<div>
|
||||
<div class="dashboard-target-name"><span class="dashboard-target-name-text">${escapeHtml(conn.name)}</span>${statusDot}</div>
|
||||
<div class="dashboard-target-subtitle">${escapeHtml(subtitle)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _updateIntegrationsInPlace(haStatus: HomeAssistantStatusResponse, mqttStatus?: MQTTStatusResponse): void {
|
||||
// Update health dots and subtitles for each integration card
|
||||
for (const conn of haStatus.connections) {
|
||||
const card = document.querySelector(`[data-integration-id="${CSS.escape(conn.source_id)}"]`);
|
||||
if (!card) continue;
|
||||
const dot = card.querySelector('.health-dot');
|
||||
if (dot) {
|
||||
dot.className = `health-dot ${conn.connected ? 'health-online' : 'health-offline'}`;
|
||||
dot.setAttribute('title', conn.connected
|
||||
? `${t('ha_source.connected')} — ${conn.entity_count} ${t('dashboard.integrations.entities')}`
|
||||
: t('ha_source.disconnected'));
|
||||
}
|
||||
const subtitle = card.querySelector('.dashboard-target-subtitle');
|
||||
if (subtitle) {
|
||||
subtitle.textContent = conn.connected
|
||||
? `${conn.entity_count} ${t('dashboard.integrations.entities')}`
|
||||
: t('ha_source.disconnected');
|
||||
}
|
||||
}
|
||||
// Update MQTT integration cards
|
||||
if (mqttStatus) {
|
||||
for (const conn of mqttStatus.connections) {
|
||||
const card = document.querySelector(`[data-integration-id="${CSS.escape(conn.source_id)}"]`);
|
||||
if (!card) continue;
|
||||
const dot = card.querySelector('.health-dot');
|
||||
if (dot) {
|
||||
dot.className = `health-dot ${conn.connected ? 'health-online' : 'health-offline'}`;
|
||||
dot.setAttribute('title', conn.connected ? t('mqtt_source.connected') : t('mqtt_source.disconnected'));
|
||||
}
|
||||
const subtitle = card.querySelector('.dashboard-target-subtitle');
|
||||
if (subtitle) {
|
||||
subtitle.textContent = conn.connected ? conn.broker : t('mqtt_source.disconnected');
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update section count badge
|
||||
const totalSources = haStatus.total_sources + (mqttStatus?.total_sources || 0);
|
||||
const totalConnected = haStatus.connected_count + (mqttStatus?.connected_count || 0);
|
||||
const header = document.querySelector('[data-dashboard-section="integrations"]');
|
||||
if (header) {
|
||||
const countEl = header.querySelector('.dashboard-section-count');
|
||||
if (countEl) countEl.textContent = `${totalConnected}/${totalSources}`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDashboardSyncClock(clock: SyncClock): string {
|
||||
const toggleAction = clock.is_running
|
||||
? `dashboardPauseClock('${clock.id}')`
|
||||
@@ -379,7 +463,7 @@ export async function loadDashboard(forceFullRender: boolean = false): Promise<v
|
||||
|
||||
try {
|
||||
// Fire all requests in a single batch to avoid sequential RTTs
|
||||
const [targets, automationsResp, devicesArr, cssArr, batchStatesResp, batchMetricsResp, scenePresets, syncClocksResp] = await Promise.all([
|
||||
const [targets, automationsResp, devicesArr, cssArr, batchStatesResp, batchMetricsResp, scenePresets, syncClocksResp, haStatusResp, mqttStatusResp] = await Promise.all([
|
||||
outputTargetsCache.fetch().catch((): any[] => []),
|
||||
fetchWithAuth('/automations').catch(() => null),
|
||||
devicesCache.fetch().catch((): any[] => []),
|
||||
@@ -388,6 +472,8 @@ export async function loadDashboard(forceFullRender: boolean = false): Promise<v
|
||||
fetchWithAuth('/output-targets/batch/metrics').catch(() => null),
|
||||
loadScenePresets(),
|
||||
fetchWithAuth('/sync-clocks').catch(() => null),
|
||||
fetchWithAuth('/home-assistant/status').catch(() => null),
|
||||
fetchWithAuth('/mqtt/status').catch(() => null),
|
||||
]);
|
||||
|
||||
const automationsData = automationsResp && automationsResp.ok ? await automationsResp.json() : { automations: [] };
|
||||
@@ -398,6 +484,12 @@ export async function loadDashboard(forceFullRender: boolean = false): Promise<v
|
||||
for (const s of (cssArr || [])) { cssSourceMap[s.id] = s; }
|
||||
const syncClocksData = syncClocksResp && syncClocksResp.ok ? await syncClocksResp.json() : { clocks: [] };
|
||||
const syncClocks = syncClocksData.clocks || [];
|
||||
const haStatus: HomeAssistantStatusResponse = haStatusResp && haStatusResp.ok
|
||||
? await haStatusResp.json()
|
||||
: { connections: [], total_sources: 0, connected_count: 0 };
|
||||
const mqttStatus: MQTTStatusResponse = mqttStatusResp && mqttStatusResp.ok
|
||||
? await mqttStatusResp.json()
|
||||
: { connections: [], total_sources: 0, connected_count: 0 };
|
||||
|
||||
const allStates = batchStatesResp && batchStatesResp.ok ? (await batchStatesResp.json()).states : {};
|
||||
const allMetrics = batchMetricsResp && batchMetricsResp.ok ? (await batchMetricsResp.json()).metrics : {};
|
||||
@@ -405,7 +497,7 @@ export async function loadDashboard(forceFullRender: boolean = false): Promise<v
|
||||
// Build dynamic HTML (targets, automations)
|
||||
let dynamicHtml = '';
|
||||
let runningIds: any[] = [];
|
||||
if (targets.length === 0 && automations.length === 0 && scenePresets.length === 0 && syncClocks.length === 0) {
|
||||
if (targets.length === 0 && automations.length === 0 && scenePresets.length === 0 && syncClocks.length === 0 && haStatus.total_sources === 0 && mqttStatus.total_sources === 0) {
|
||||
dynamicHtml = `<div class="dashboard-no-targets">${t('dashboard.no_targets')}</div>`;
|
||||
} else {
|
||||
const enriched = targets.map(target => ({
|
||||
@@ -427,6 +519,7 @@ export async function loadDashboard(forceFullRender: boolean = false): Promise<v
|
||||
if (structureUnchanged && !forceFullRender && running.length > 0) {
|
||||
_updateRunningMetrics(running);
|
||||
_updateSyncClocksInPlace(syncClocks);
|
||||
_updateIntegrationsInPlace(haStatus, mqttStatus);
|
||||
_cacheUptimeElements();
|
||||
_startUptimeTimer();
|
||||
startPerfPolling();
|
||||
@@ -437,6 +530,7 @@ export async function loadDashboard(forceFullRender: boolean = false): Promise<v
|
||||
if (running.length > 0) _updateRunningMetrics(running);
|
||||
_updateAutomationsInPlace(automations);
|
||||
_updateSyncClocksInPlace(syncClocks);
|
||||
_updateIntegrationsInPlace(haStatus, mqttStatus);
|
||||
_cacheUptimeElements();
|
||||
_startUptimeTimer();
|
||||
startPerfPolling();
|
||||
@@ -444,6 +538,19 @@ export async function loadDashboard(forceFullRender: boolean = false): Promise<v
|
||||
return;
|
||||
}
|
||||
|
||||
// Integrations section (HA + MQTT sources)
|
||||
const totalIntSources = haStatus.total_sources + mqttStatus.total_sources;
|
||||
const totalIntConnected = haStatus.connected_count + mqttStatus.connected_count;
|
||||
if (totalIntSources > 0) {
|
||||
const haCards = haStatus.connections.map(c => _renderIntegrationCard(c)).join('');
|
||||
const mqttCards = mqttStatus.connections.map(c => _renderMQTTIntegrationCard(c)).join('');
|
||||
const intGrid = `<div class="dashboard-integrations-grid">${haCards}${mqttCards}</div>`;
|
||||
dynamicHtml += `<div class="dashboard-section">
|
||||
${_sectionHeader('integrations', t('dashboard.section.integrations'), `${totalIntConnected}/${totalIntSources}`)}
|
||||
${_sectionContent('integrations', intGrid)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (automations.length > 0) {
|
||||
const activeAutomations = automations.filter(a => a.is_active);
|
||||
const inactiveAutomations = automations.filter(a => !a.is_active);
|
||||
@@ -821,7 +928,7 @@ document.addEventListener('server:state_change', () => _debouncedDashboardReload
|
||||
document.addEventListener('server:automation_state_changed', () => _debouncedDashboardReload(true));
|
||||
document.addEventListener('server:device_health_changed', () => _debouncedDashboardReload());
|
||||
|
||||
const _DASHBOARD_ENTITY_TYPES = new Set(['output_target', 'automation', 'scene_preset', 'sync_clock', 'device']);
|
||||
const _DASHBOARD_ENTITY_TYPES = new Set(['output_target', 'automation', 'scene_preset', 'sync_clock', 'device', 'home_assistant_source']);
|
||||
document.addEventListener('server:entity_changed', (e: Event) => {
|
||||
const { entity_type } = (e as CustomEvent).detail || {};
|
||||
if (_DASHBOARD_ENTITY_TYPES.has(entity_type)) _debouncedDashboardReload(true);
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
/**
|
||||
* MQTT Sources — CRUD, test, cards.
|
||||
*/
|
||||
|
||||
import { mqttSourcesCache } from '../core/state.ts';
|
||||
import { fetchWithAuth, escapeHtml } from '../core/api.ts';
|
||||
import { t } from '../core/i18n.ts';
|
||||
import { Modal } from '../core/modal.ts';
|
||||
import { showToast, showConfirm } from '../core/ui.ts';
|
||||
import { ICON_CLONE, ICON_EDIT, ICON_TEST } from '../core/icons.ts';
|
||||
import * as P from '../core/icon-paths.ts';
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import type { MQTTSource } from '../types.ts';
|
||||
|
||||
const ICON_MQTT = `<svg class="icon" viewBox="0 0 24 24">${P.radio}</svg>`;
|
||||
|
||||
// ── Modal ──
|
||||
|
||||
let _mqttTagsInput: TagInput | null = null;
|
||||
|
||||
class MQTTSourceModal extends Modal {
|
||||
constructor() { super('mqtt-source-modal'); }
|
||||
|
||||
onForceClose() {
|
||||
if (_mqttTagsInput) { _mqttTagsInput.destroy(); _mqttTagsInput = null; }
|
||||
}
|
||||
|
||||
snapshotValues() {
|
||||
return {
|
||||
name: (document.getElementById('mqtt-source-name') as HTMLInputElement).value,
|
||||
host: (document.getElementById('mqtt-source-host') as HTMLInputElement).value,
|
||||
port: (document.getElementById('mqtt-source-port') as HTMLInputElement).value,
|
||||
username: (document.getElementById('mqtt-source-username') as HTMLInputElement).value,
|
||||
password: (document.getElementById('mqtt-source-password') as HTMLInputElement).value,
|
||||
client_id: (document.getElementById('mqtt-source-client-id') as HTMLInputElement).value,
|
||||
base_topic: (document.getElementById('mqtt-source-base-topic') as HTMLInputElement).value,
|
||||
description: (document.getElementById('mqtt-source-description') as HTMLInputElement).value,
|
||||
tags: JSON.stringify(_mqttTagsInput ? _mqttTagsInput.getValue() : []),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const mqttSourceModal = new MQTTSourceModal();
|
||||
|
||||
// ── Show / Close ──
|
||||
|
||||
export async function showMQTTSourceModal(editData: MQTTSource | null = null): Promise<void> {
|
||||
const isEdit = !!editData;
|
||||
const titleKey = isEdit ? 'mqtt_source.edit' : 'mqtt_source.add';
|
||||
document.getElementById('mqtt-source-modal-title')!.innerHTML = `${ICON_MQTT} ${t(titleKey)}`;
|
||||
(document.getElementById('mqtt-source-id') as HTMLInputElement).value = editData?.id || '';
|
||||
(document.getElementById('mqtt-source-error') as HTMLElement).style.display = 'none';
|
||||
|
||||
if (isEdit) {
|
||||
(document.getElementById('mqtt-source-name') as HTMLInputElement).value = editData.name || '';
|
||||
(document.getElementById('mqtt-source-host') as HTMLInputElement).value = editData.broker_host || '';
|
||||
(document.getElementById('mqtt-source-port') as HTMLInputElement).value = String(editData.broker_port ?? 1883);
|
||||
(document.getElementById('mqtt-source-username') as HTMLInputElement).value = editData.username || '';
|
||||
(document.getElementById('mqtt-source-password') as HTMLInputElement).value = ''; // never expose
|
||||
(document.getElementById('mqtt-source-client-id') as HTMLInputElement).value = editData.client_id || 'ledgrab';
|
||||
(document.getElementById('mqtt-source-base-topic') as HTMLInputElement).value = editData.base_topic || 'ledgrab';
|
||||
(document.getElementById('mqtt-source-description') as HTMLInputElement).value = editData.description || '';
|
||||
} else {
|
||||
(document.getElementById('mqtt-source-name') as HTMLInputElement).value = '';
|
||||
(document.getElementById('mqtt-source-host') as HTMLInputElement).value = '';
|
||||
(document.getElementById('mqtt-source-port') as HTMLInputElement).value = '1883';
|
||||
(document.getElementById('mqtt-source-username') as HTMLInputElement).value = '';
|
||||
(document.getElementById('mqtt-source-password') as HTMLInputElement).value = '';
|
||||
(document.getElementById('mqtt-source-client-id') as HTMLInputElement).value = 'ledgrab';
|
||||
(document.getElementById('mqtt-source-base-topic') as HTMLInputElement).value = 'ledgrab';
|
||||
(document.getElementById('mqtt-source-description') as HTMLInputElement).value = '';
|
||||
}
|
||||
|
||||
// Tags
|
||||
if (_mqttTagsInput) { _mqttTagsInput.destroy(); _mqttTagsInput = null; }
|
||||
_mqttTagsInput = new TagInput(document.getElementById('mqtt-source-tags-container'), { placeholder: t('tags.placeholder') });
|
||||
_mqttTagsInput.setValue(isEdit ? (editData.tags || []) : []);
|
||||
|
||||
// Show/hide test button based on edit mode
|
||||
const testBtn = document.getElementById('mqtt-source-test-btn');
|
||||
if (testBtn) testBtn.style.display = isEdit ? '' : 'none';
|
||||
|
||||
// Password hint
|
||||
const pwHint = document.getElementById('mqtt-source-password-hint');
|
||||
if (pwHint) pwHint.style.display = isEdit ? '' : 'none';
|
||||
|
||||
mqttSourceModal.open();
|
||||
mqttSourceModal.snapshot();
|
||||
}
|
||||
|
||||
export async function closeMQTTSourceModal(): Promise<void> {
|
||||
await mqttSourceModal.close();
|
||||
}
|
||||
|
||||
// ── Save ──
|
||||
|
||||
export async function saveMQTTSource(): Promise<void> {
|
||||
const id = (document.getElementById('mqtt-source-id') as HTMLInputElement).value;
|
||||
const name = (document.getElementById('mqtt-source-name') as HTMLInputElement).value.trim();
|
||||
const broker_host = (document.getElementById('mqtt-source-host') as HTMLInputElement).value.trim();
|
||||
const broker_port = parseInt((document.getElementById('mqtt-source-port') as HTMLInputElement).value, 10) || 1883;
|
||||
const username = (document.getElementById('mqtt-source-username') as HTMLInputElement).value.trim();
|
||||
const password = (document.getElementById('mqtt-source-password') as HTMLInputElement).value;
|
||||
const client_id = (document.getElementById('mqtt-source-client-id') as HTMLInputElement).value.trim() || 'ledgrab';
|
||||
const base_topic = (document.getElementById('mqtt-source-base-topic') as HTMLInputElement).value.trim() || 'ledgrab';
|
||||
const description = (document.getElementById('mqtt-source-description') as HTMLInputElement).value.trim() || null;
|
||||
|
||||
if (!name) {
|
||||
mqttSourceModal.showError(t('mqtt_source.error.name_required'));
|
||||
return;
|
||||
}
|
||||
if (!id && !broker_host) {
|
||||
mqttSourceModal.showError(t('mqtt_source.error.host_required'));
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Record<string, any> = {
|
||||
name, broker_port, username, client_id, base_topic, description,
|
||||
tags: _mqttTagsInput ? _mqttTagsInput.getValue() : [],
|
||||
};
|
||||
if (broker_host) payload.broker_host = broker_host;
|
||||
// Only send password if provided (edit mode may leave blank to keep)
|
||||
if (password) payload.password = password;
|
||||
|
||||
try {
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
const url = id ? `/mqtt/sources/${id}` : '/mqtt/sources';
|
||||
const resp = await fetchWithAuth(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.detail || `HTTP ${resp.status}`);
|
||||
}
|
||||
showToast(t(id ? 'mqtt_source.updated' : 'mqtt_source.created'), 'success');
|
||||
mqttSourceModal.forceClose();
|
||||
mqttSourcesCache.invalidate();
|
||||
} catch (e: any) {
|
||||
if (e.isAuth) return;
|
||||
mqttSourceModal.showError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Edit / Clone / Delete ──
|
||||
|
||||
export async function editMQTTSource(sourceId: string): Promise<void> {
|
||||
try {
|
||||
const resp = await fetchWithAuth(`/mqtt/sources/${sourceId}`);
|
||||
if (!resp.ok) throw new Error(t('mqtt_source.error.load'));
|
||||
const data = await resp.json();
|
||||
await showMQTTSourceModal(data);
|
||||
} catch (e: any) {
|
||||
if (e.isAuth) return;
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function cloneMQTTSource(sourceId: string): Promise<void> {
|
||||
try {
|
||||
const resp = await fetchWithAuth(`/mqtt/sources/${sourceId}`);
|
||||
if (!resp.ok) throw new Error(t('mqtt_source.error.load'));
|
||||
const data = await resp.json();
|
||||
delete data.id;
|
||||
data.name = data.name + ' (copy)';
|
||||
await showMQTTSourceModal(data);
|
||||
} catch (e: any) {
|
||||
if (e.isAuth) return;
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMQTTSource(sourceId: string): Promise<void> {
|
||||
const confirmed = await showConfirm(t('mqtt_source.delete.confirm'));
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
const resp = await fetchWithAuth(`/mqtt/sources/${sourceId}`, { method: 'DELETE' });
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.detail || `HTTP ${resp.status}`);
|
||||
}
|
||||
showToast(t('mqtt_source.deleted'), 'success');
|
||||
mqttSourcesCache.invalidate();
|
||||
} catch (e: any) {
|
||||
if (e.isAuth) return;
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test ──
|
||||
|
||||
export async function testMQTTSource(): Promise<void> {
|
||||
const id = (document.getElementById('mqtt-source-id') as HTMLInputElement).value;
|
||||
if (!id) return;
|
||||
|
||||
const testBtn = document.getElementById('mqtt-source-test-btn');
|
||||
if (testBtn) testBtn.classList.add('loading');
|
||||
|
||||
try {
|
||||
const resp = await fetchWithAuth(`/mqtt/sources/${id}/test`, { method: 'POST' });
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
showToast(t('mqtt_source.test.success'), 'success');
|
||||
} else {
|
||||
showToast(`${t('mqtt_source.test.failed')}: ${data.error}`, 'error');
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.isAuth) return;
|
||||
showToast(e.message, 'error');
|
||||
} finally {
|
||||
if (testBtn) testBtn.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
|
||||
async function _testMQTTSourceFromCard(sourceId: string): Promise<void> {
|
||||
try {
|
||||
const resp = await fetchWithAuth(`/mqtt/sources/${sourceId}/test`, { method: 'POST' });
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
showToast(t('mqtt_source.test.success'), 'success');
|
||||
} else {
|
||||
showToast(`${t('mqtt_source.test.failed')}: ${data.error}`, 'error');
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.isAuth) return;
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Card rendering ──
|
||||
|
||||
export function createMQTTSourceCard(source: MQTTSource) {
|
||||
let healthClass: string, healthTitle: string;
|
||||
if (source.connected) {
|
||||
healthClass = 'health-online';
|
||||
healthTitle = t('mqtt_source.connected');
|
||||
} else {
|
||||
healthClass = 'health-offline';
|
||||
healthTitle = t('mqtt_source.disconnected');
|
||||
}
|
||||
const statusDot = `<span class="health-dot ${healthClass}" title="${escapeHtml(healthTitle)}" role="status"></span>`;
|
||||
|
||||
return wrapCard({
|
||||
type: 'template-card',
|
||||
dataAttr: 'data-id',
|
||||
id: source.id,
|
||||
removeOnclick: `deleteMQTTSource('${source.id}')`,
|
||||
removeTitle: t('common.delete'),
|
||||
content: `
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">${ICON_MQTT} ${statusDot} ${escapeHtml(source.name)}</div>
|
||||
</div>
|
||||
<div class="stream-card-props">
|
||||
<span class="stream-card-prop">
|
||||
<svg class="icon" viewBox="0 0 24 24">${P.wifi}</svg> ${escapeHtml(source.broker_host)}:${source.broker_port}
|
||||
</span>
|
||||
<span class="stream-card-prop">
|
||||
<svg class="icon" viewBox="0 0 24 24">${P.hash}</svg> ${escapeHtml(source.base_topic)}
|
||||
</span>
|
||||
</div>
|
||||
${renderTagChips(source.tags)}
|
||||
${source.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(source.description)}</div>` : ''}`,
|
||||
actions: `
|
||||
<button class="btn btn-icon btn-secondary" data-action="test" title="${t('mqtt_source.test')}">${ICON_TEST}</button>
|
||||
<button class="btn btn-icon btn-secondary" data-action="clone" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||
<button class="btn btn-icon btn-secondary" data-action="edit" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Event delegation ──
|
||||
|
||||
const _mqttSourceActions: Record<string, (id: string) => void> = {
|
||||
test: (id) => _testMQTTSourceFromCard(id),
|
||||
clone: cloneMQTTSource,
|
||||
edit: editMQTTSource,
|
||||
};
|
||||
|
||||
export function initMQTTSourceDelegation(container: HTMLElement): void {
|
||||
container.addEventListener('click', (e: MouseEvent) => {
|
||||
const btn = (e.target as HTMLElement).closest<HTMLElement>('[data-action]');
|
||||
if (!btn) return;
|
||||
|
||||
const section = btn.closest<HTMLElement>('[data-card-section="mqtt-sources"]');
|
||||
if (!section) return;
|
||||
const card = btn.closest<HTMLElement>('[data-id]');
|
||||
if (!card) return;
|
||||
|
||||
const action = btn.dataset.action;
|
||||
const id = card.getAttribute('data-id');
|
||||
if (!action || !id) return;
|
||||
|
||||
const handler = _mqttSourceActions[action];
|
||||
if (handler) {
|
||||
e.stopPropagation();
|
||||
handler(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Expose to global scope for HTML template onclick handlers ──
|
||||
|
||||
window.showMQTTSourceModal = showMQTTSourceModal;
|
||||
window.closeMQTTSourceModal = closeMQTTSourceModal;
|
||||
window.saveMQTTSource = saveMQTTSource;
|
||||
window.editMQTTSource = editMQTTSource;
|
||||
window.cloneMQTTSource = cloneMQTTSource;
|
||||
window.deleteMQTTSource = deleteMQTTSource;
|
||||
window.testMQTTSource = testMQTTSource;
|
||||
@@ -290,7 +290,6 @@ export function openSettingsModal(): void {
|
||||
loadExternalUrl();
|
||||
loadAutoBackupSettings();
|
||||
loadBackupList();
|
||||
loadMqttSettings();
|
||||
loadLogLevel();
|
||||
}
|
||||
|
||||
@@ -629,56 +628,3 @@ export async function setLogLevel(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── MQTT settings ────────────────────────────────────────────
|
||||
|
||||
export async function loadMqttSettings(): Promise<void> {
|
||||
try {
|
||||
const resp = await fetchWithAuth('/system/mqtt/settings');
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
|
||||
(document.getElementById('mqtt-enabled') as HTMLInputElement).checked = data.enabled;
|
||||
(document.getElementById('mqtt-host') as HTMLInputElement).value = data.broker_host;
|
||||
(document.getElementById('mqtt-port') as HTMLInputElement).value = data.broker_port;
|
||||
(document.getElementById('mqtt-username') as HTMLInputElement).value = data.username;
|
||||
(document.getElementById('mqtt-password') as HTMLInputElement).value = '';
|
||||
(document.getElementById('mqtt-client-id') as HTMLInputElement).value = data.client_id;
|
||||
(document.getElementById('mqtt-base-topic') as HTMLInputElement).value = data.base_topic;
|
||||
|
||||
const hint = document.getElementById('mqtt-password-hint');
|
||||
if (hint) hint.style.display = data.password_set ? '' : 'none';
|
||||
} catch (err) {
|
||||
console.error('Failed to load MQTT settings:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveMqttSettings(): Promise<void> {
|
||||
const enabled = (document.getElementById('mqtt-enabled') as HTMLInputElement).checked;
|
||||
const broker_host = (document.getElementById('mqtt-host') as HTMLInputElement).value.trim();
|
||||
const broker_port = parseInt((document.getElementById('mqtt-port') as HTMLInputElement).value, 10);
|
||||
const username = (document.getElementById('mqtt-username') as HTMLInputElement).value;
|
||||
const password = (document.getElementById('mqtt-password') as HTMLInputElement).value;
|
||||
const client_id = (document.getElementById('mqtt-client-id') as HTMLInputElement).value.trim();
|
||||
const base_topic = (document.getElementById('mqtt-base-topic') as HTMLInputElement).value.trim();
|
||||
|
||||
if (!broker_host) {
|
||||
showToast(t('settings.mqtt.error_host_required'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetchWithAuth('/system/mqtt/settings', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ enabled, broker_host, broker_port, username, password, client_id, base_topic }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.detail || `HTTP ${resp.status}`);
|
||||
}
|
||||
showToast(t('settings.mqtt.saved'), 'success');
|
||||
loadMqttSettings();
|
||||
} catch (err) {
|
||||
console.error('Failed to save MQTT settings:', err);
|
||||
showToast(t('settings.mqtt.save_error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
_cachedSyncClocks,
|
||||
_cachedWeatherSources,
|
||||
_cachedHASources,
|
||||
_cachedMQTTSources, mqttSourcesCache,
|
||||
_cachedAudioTemplates,
|
||||
_cachedCSPTemplates,
|
||||
_csptModalFilters, set_csptModalFilters,
|
||||
@@ -54,6 +55,7 @@ import { createValueSourceCard } from './value-sources.ts';
|
||||
import { createSyncClockCard, initSyncClockDelegation } from './sync-clocks.ts';
|
||||
import { createWeatherSourceCard, initWeatherSourceDelegation } from './weather-sources.ts';
|
||||
import { createHASourceCard, initHASourceDelegation } from './home-assistant-sources.ts';
|
||||
import { createMQTTSourceCard, initMQTTSourceDelegation } from './mqtt-sources.ts';
|
||||
import { createAssetCard, initAssetDelegation } from './assets.ts';
|
||||
import { createColorStripCard } from './color-strips.ts';
|
||||
import { initAudioSourceDelegation } from './audio-sources.ts';
|
||||
@@ -105,6 +107,7 @@ const _valueSourceDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon
|
||||
const _syncClockDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('sync-clocks', syncClocksCache, 'sync_clock.deleted') }];
|
||||
const _weatherSourceDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('weather-sources', weatherSourcesCache, 'weather_source.deleted') }];
|
||||
const _haSourceDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('home-assistant/sources', haSourcesCache, 'ha_source.deleted') }];
|
||||
const _mqttSourceDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('mqtt/sources', mqttSourcesCache, 'mqtt_source.deleted') }];
|
||||
const _assetDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('assets', assetsCache, 'asset.deleted') }];
|
||||
const _csptDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('color-strip-processing-templates', csptCache, 'templates.deleted') }];
|
||||
|
||||
@@ -176,6 +179,7 @@ const csValueSources = new CardSection('value-sources', { titleKey: 'value_sourc
|
||||
const csSyncClocks = new CardSection('sync-clocks', { titleKey: 'sync_clock.group.title', gridClass: 'templates-grid', addCardOnclick: "showSyncClockModal()", keyAttr: 'data-id', emptyKey: 'section.empty.sync_clocks', bulkActions: _syncClockDeleteAction });
|
||||
const csWeatherSources = new CardSection('weather-sources', { titleKey: 'weather_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showWeatherSourceModal()", keyAttr: 'data-id', emptyKey: 'section.empty.weather_sources', bulkActions: _weatherSourceDeleteAction });
|
||||
const csHASources = new CardSection('ha-sources', { titleKey: 'ha_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showHASourceModal()", keyAttr: 'data-id', emptyKey: 'section.empty.ha_sources', bulkActions: _haSourceDeleteAction });
|
||||
const csMQTTSources = new CardSection('mqtt-sources', { titleKey: 'mqtt_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showMQTTSourceModal()", keyAttr: 'data-id', emptyKey: 'section.empty.mqtt_sources', bulkActions: _mqttSourceDeleteAction });
|
||||
const csAssets = new CardSection('assets', { titleKey: 'asset.group.title', gridClass: 'templates-grid', addCardOnclick: "showAssetUploadModal()", keyAttr: 'data-id', emptyKey: 'section.empty.assets', bulkActions: _assetDeleteAction });
|
||||
const csCSPTemplates = new CardSection('css-proc-templates', { titleKey: 'css_processing.title', gridClass: 'templates-grid', addCardOnclick: "showAddCSPTModal()", keyAttr: 'data-cspt-id', emptyKey: 'section.empty.cspt', bulkActions: _csptDeleteAction });
|
||||
const _gradientDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('gradients', gradientsCache, 'gradient.deleted') }];
|
||||
@@ -291,6 +295,7 @@ export async function loadPictureSources() {
|
||||
syncClocksCache.fetch(),
|
||||
weatherSourcesCache.fetch(),
|
||||
haSourcesCache.fetch(),
|
||||
mqttSourcesCache.fetch(),
|
||||
assetsCache.fetch(),
|
||||
audioTemplatesCache.fetch(),
|
||||
colorStripSourcesCache.fetch(),
|
||||
@@ -352,6 +357,7 @@ const _streamSectionMap = {
|
||||
sync: [csSyncClocks],
|
||||
weather: [csWeatherSources],
|
||||
home_assistant: [csHASources],
|
||||
mqtt: [csMQTTSources],
|
||||
game: [csGameIntegrations],
|
||||
};
|
||||
|
||||
@@ -580,6 +586,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
{ key: 'sync', icon: ICON_CLOCK, titleKey: 'streams.group.sync', count: _cachedSyncClocks.length },
|
||||
{ key: 'weather', icon: `<svg class="icon" viewBox="0 0 24 24">${P.cloudSun}</svg>`, titleKey: 'streams.group.weather', count: _cachedWeatherSources.length },
|
||||
{ key: 'home_assistant', icon: `<svg class="icon" viewBox="0 0 24 24">${P.home}</svg>`, titleKey: 'streams.group.home_assistant', count: _cachedHASources.length },
|
||||
{ key: 'mqtt', icon: `<svg class="icon" viewBox="0 0 24 24">${P.radio}</svg>`, titleKey: 'streams.group.mqtt', count: _cachedMQTTSources.length },
|
||||
{ key: 'assets', icon: ICON_ASSET, titleKey: 'streams.group.assets', count: _cachedAssets.length },
|
||||
{ key: 'game', icon: ICON_GAMEPAD, titleKey: 'streams.group.game', count: _cachedGameIntegrations.length },
|
||||
];
|
||||
@@ -634,6 +641,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
children: [
|
||||
{ key: 'weather', titleKey: 'streams.group.weather', icon: `<svg class="icon" viewBox="0 0 24 24">${P.cloudSun}</svg>`, count: _cachedWeatherSources.length },
|
||||
{ key: 'home_assistant', titleKey: 'streams.group.home_assistant', icon: `<svg class="icon" viewBox="0 0 24 24">${P.home}</svg>`, count: _cachedHASources.length },
|
||||
{ key: 'mqtt', titleKey: 'streams.group.mqtt', icon: `<svg class="icon" viewBox="0 0 24 24">${P.radio}</svg>`, count: _cachedMQTTSources.length },
|
||||
{ key: 'game', titleKey: 'streams.group.game', icon: ICON_GAMEPAD, count: _cachedGameIntegrations.length },
|
||||
]
|
||||
},
|
||||
@@ -804,6 +812,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
const syncClockItems = csSyncClocks.applySortOrder(_cachedSyncClocks.map(s => ({ key: s.id, html: createSyncClockCard(s) })));
|
||||
const weatherSourceItems = csWeatherSources.applySortOrder(_cachedWeatherSources.map(s => ({ key: s.id, html: createWeatherSourceCard(s) })));
|
||||
const haSourceItems = csHASources.applySortOrder(_cachedHASources.map(s => ({ key: s.id, html: createHASourceCard(s) })));
|
||||
const mqttSourceItems = csMQTTSources.applySortOrder(_cachedMQTTSources.map(s => ({ key: s.id, html: createMQTTSourceCard(s) })));
|
||||
const assetItems = csAssets.applySortOrder(_cachedAssets.map(a => ({ key: a.id, html: createAssetCard(a) })));
|
||||
const csptItems = csCSPTemplates.applySortOrder(csptTemplates.map(t => ({ key: t.id, html: renderCSPTCard(t) })));
|
||||
const gameIntegrationItems = csGameIntegrations.applySortOrder(_cachedGameIntegrations.map(g => ({ key: g.id, html: createGameIntegrationCard(g) })));
|
||||
@@ -826,6 +835,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
sync: _cachedSyncClocks.length,
|
||||
weather: _cachedWeatherSources.length,
|
||||
home_assistant: _cachedHASources.length,
|
||||
mqtt: _cachedMQTTSources.length,
|
||||
assets: _cachedAssets.length,
|
||||
game: _cachedGameIntegrations.length,
|
||||
});
|
||||
@@ -846,6 +856,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
csSyncClocks.reconcile(syncClockItems);
|
||||
csWeatherSources.reconcile(weatherSourceItems);
|
||||
csHASources.reconcile(haSourceItems);
|
||||
csMQTTSources.reconcile(mqttSourceItems);
|
||||
csAssets.reconcile(assetItems);
|
||||
csGameIntegrations.reconcile(gameIntegrationItems);
|
||||
} else {
|
||||
@@ -867,6 +878,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
else if (tab.key === 'sync') panelContent = csSyncClocks.render(syncClockItems);
|
||||
else if (tab.key === 'weather') panelContent = csWeatherSources.render(weatherSourceItems);
|
||||
else if (tab.key === 'home_assistant') panelContent = csHASources.render(haSourceItems);
|
||||
else if (tab.key === 'mqtt') panelContent = csMQTTSources.render(mqttSourceItems);
|
||||
else if (tab.key === 'assets') panelContent = csAssets.render(assetItems);
|
||||
else if (tab.key === 'game') panelContent = csGameIntegrations.render(gameIntegrationItems);
|
||||
else if (tab.key === 'video') panelContent = csVideoStreams.render(videoItems);
|
||||
@@ -875,12 +887,13 @@ function renderPictureSourcesList(streams: any) {
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = panels;
|
||||
CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csCSPTemplates, csColorStrips, csGradients, csAudioMulti, csAudioMono, csAudioBandExtract, csAudioTemplates, csStaticStreams, csVideoStreams, csValueSources, csSyncClocks, csWeatherSources, csHASources, csAssets, csGameIntegrations]);
|
||||
CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csCSPTemplates, csColorStrips, csGradients, csAudioMulti, csAudioMono, csAudioBandExtract, csAudioTemplates, csStaticStreams, csVideoStreams, csValueSources, csSyncClocks, csWeatherSources, csHASources, csMQTTSources, csAssets, csGameIntegrations]);
|
||||
|
||||
// Event delegation for card actions (replaces inline onclick handlers)
|
||||
initSyncClockDelegation(container);
|
||||
initWeatherSourceDelegation(container);
|
||||
initHASourceDelegation(container);
|
||||
initMQTTSourceDelegation(container);
|
||||
initAudioSourceDelegation(container);
|
||||
initAssetDelegation(container);
|
||||
|
||||
@@ -901,6 +914,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
'sync-clocks': 'sync',
|
||||
'weather-sources': 'weather',
|
||||
'ha-sources': 'home_assistant',
|
||||
'mqtt-sources': 'mqtt',
|
||||
'assets': 'assets',
|
||||
'game-integrations': 'game',
|
||||
});
|
||||
|
||||
@@ -371,7 +371,6 @@ startTargetOverlay: (...args: any[]) => any;
|
||||
downloadSavedBackup: (...args: any[]) => any;
|
||||
deleteSavedBackup: (...args: any[]) => any;
|
||||
restartServer: (...args: any[]) => any;
|
||||
saveMqttSettings: (...args: any[]) => any;
|
||||
loadApiKeysList: (...args: any[]) => any;
|
||||
connectLogViewer: (...args: any[]) => any;
|
||||
disconnectLogViewer: (...args: any[]) => any;
|
||||
|
||||
@@ -651,6 +651,55 @@ export interface HomeAssistantSourceListResponse {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface HomeAssistantConnectionStatus {
|
||||
source_id: string;
|
||||
name: string;
|
||||
connected: boolean;
|
||||
entity_count: number;
|
||||
}
|
||||
|
||||
export interface HomeAssistantStatusResponse {
|
||||
connections: HomeAssistantConnectionStatus[];
|
||||
total_sources: number;
|
||||
connected_count: number;
|
||||
}
|
||||
|
||||
// ── MQTT Source ────────────────────────────────────────────────
|
||||
|
||||
export interface MQTTSource {
|
||||
id: string;
|
||||
name: string;
|
||||
broker_host: string;
|
||||
broker_port: number;
|
||||
username: string;
|
||||
password_set: boolean;
|
||||
client_id: string;
|
||||
base_topic: string;
|
||||
connected: boolean;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface MQTTSourceListResponse {
|
||||
sources: MQTTSource[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface MQTTConnectionStatus {
|
||||
source_id: string;
|
||||
name: string;
|
||||
connected: boolean;
|
||||
broker: string;
|
||||
}
|
||||
|
||||
export interface MQTTStatusResponse {
|
||||
connections: MQTTConnectionStatus[];
|
||||
total_sources: number;
|
||||
connected_count: number;
|
||||
}
|
||||
|
||||
// ── Asset ────────────────────────────────────────────────────
|
||||
|
||||
export interface Asset {
|
||||
|
||||
@@ -544,6 +544,12 @@
|
||||
"filters.palette_quantization.desc": "Reduce colors to a limited palette",
|
||||
"filters.reverse": "Reverse",
|
||||
"filters.reverse.desc": "Reverse the LED order in the strip",
|
||||
"filters.hsl_shift": "HSL Shift",
|
||||
"filters.hsl_shift.desc": "Shift hue, saturation, and lightness values",
|
||||
"filters.contrast": "Contrast",
|
||||
"filters.contrast.desc": "Adjust image contrast around mid-gray",
|
||||
"filters.temporal_blur": "Temporal Blur",
|
||||
"filters.temporal_blur.desc": "Smooth color transitions over time",
|
||||
"postprocessing.description_label": "Description (optional):",
|
||||
"postprocessing.description_placeholder": "Describe this template...",
|
||||
"postprocessing.created": "Template created successfully",
|
||||
@@ -722,6 +728,9 @@
|
||||
"dashboard.section.sync_clocks": "Sync Clocks",
|
||||
"dashboard.targets": "Targets",
|
||||
"dashboard.section.performance": "System Performance",
|
||||
"dashboard.section.integrations": "Integrations",
|
||||
"dashboard.integrations.entities": "entities",
|
||||
"dashboard.integrations.no_sources": "No integration sources configured",
|
||||
"dashboard.perf.cpu": "CPU",
|
||||
"dashboard.perf.ram": "RAM",
|
||||
"dashboard.perf.gpu": "GPU",
|
||||
@@ -1877,6 +1886,37 @@
|
||||
"ha_source.deleted": "Home Assistant source deleted",
|
||||
"ha_source.delete.confirm": "Delete this Home Assistant connection?",
|
||||
"section.empty.ha_sources": "No Home Assistant sources yet. Click + to add one.",
|
||||
"streams.group.mqtt": "MQTT",
|
||||
"mqtt_source.group.title": "MQTT Sources",
|
||||
"mqtt_source.add": "Add MQTT Source",
|
||||
"mqtt_source.edit": "Edit MQTT Source",
|
||||
"mqtt_source.name": "Name:",
|
||||
"mqtt_source.name.placeholder": "My MQTT Broker",
|
||||
"mqtt_source.name.hint": "A descriptive name for this MQTT broker connection",
|
||||
"mqtt_source.broker_host": "Broker Host:",
|
||||
"mqtt_source.broker_host.hint": "MQTT broker hostname or IP address, e.g. 192.168.1.100",
|
||||
"mqtt_source.broker_port": "Port:",
|
||||
"mqtt_source.username": "Username (optional):",
|
||||
"mqtt_source.password": "Password (optional):",
|
||||
"mqtt_source.password.edit_hint": "Leave blank to keep the current password",
|
||||
"mqtt_source.client_id": "Client ID:",
|
||||
"mqtt_source.client_id.hint": "Unique MQTT client identifier. Change if running multiple instances.",
|
||||
"mqtt_source.base_topic": "Base Topic:",
|
||||
"mqtt_source.base_topic.hint": "Prefix for status and state topics, e.g. ledgrab/status",
|
||||
"mqtt_source.description": "Description (optional):",
|
||||
"mqtt_source.test": "Test Connection",
|
||||
"mqtt_source.test.success": "Connected to broker",
|
||||
"mqtt_source.test.failed": "Connection failed",
|
||||
"mqtt_source.connected": "Connected",
|
||||
"mqtt_source.disconnected": "Disconnected",
|
||||
"mqtt_source.error.name_required": "Name is required",
|
||||
"mqtt_source.error.host_required": "Broker host is required",
|
||||
"mqtt_source.error.load": "Failed to load MQTT source",
|
||||
"mqtt_source.created": "MQTT source created",
|
||||
"mqtt_source.updated": "MQTT source updated",
|
||||
"mqtt_source.deleted": "MQTT source deleted",
|
||||
"mqtt_source.delete.confirm": "Delete this MQTT broker connection?",
|
||||
"section.empty.mqtt_sources": "No MQTT sources yet. Click + to add one.",
|
||||
"ha_light.section.title": "Home Assistant",
|
||||
"ha_light.section.targets": "Light Targets",
|
||||
"ha_light.add": "Add HA Light Target",
|
||||
@@ -2145,6 +2185,7 @@
|
||||
"donation.message": "LedGrab is free & open-source. If it's useful to you, consider supporting development.",
|
||||
"donation.support": "Support the project",
|
||||
"donation.view_source": "View source code",
|
||||
"donation.about": "About",
|
||||
"donation.later": "Remind me later",
|
||||
"donation.dismiss": "Don't show again",
|
||||
"donation.about_title": "About LedGrab",
|
||||
|
||||
Reference in New Issue
Block a user