Files
gree-controller/web/js/realtime.js
T
2026-09-16 11:09:38 +02:00

136 lines
13 KiB
JavaScript

function logNotificationBadge(item) {
const notification = item?.metadata?.notification;
if (notification?.status !== 'silent') return '';
const reasonKey = {
alert_type_disabled: 'logs.silentAlertTypeDisabled',
notifications_disabled: 'logs.silentNotificationsDisabled',
mode_filtered: 'logs.silentModeFiltered',
cooldown: 'logs.silentCooldown',
}[notification.reason] || 'logs.silentHint';
return `<span class="badge silent log-notification-badge" title="${esc(tr(reasonKey))}">${esc(tr('logs.silent'))}</span>`;
}
function logDisplayMessage(item) {
const message = String(item?.message || '');
if (!String(item?.kind || '').startsWith('ha.sensor_')) return message;
const metadata = item?.metadata || {};
const entityId = metadata.resolved_entity_id || metadata.configured_entity_id || '';
if (!entityId || message.includes(entityId)) return message;
const alias = haSensorLabel(entityId);
const sensor = alias && alias !== entityId ? `${alias} (${entityId})` : entityId;
const zoneName = metadata.zone_name || app.zones?.find(zone => zone.id === metadata.zone_id)?.name || '';
return `${message} · ${tr('history.haSensor')}: ${sensor}${zoneName ? ` · ${tr('common.zone')}: ${zoneName}` : ''}`;
}
async function loadLogs() {
renderLogRetention();
try {
const data = await api('/api/events?limit=150');
const level = $('#logLevelFilter')?.value || 'all', category = $('#logCategoryFilter')?.value || 'all';
const logs = (data.events || []).filter(item => (level === 'all' || item.level === level) && (category === 'all' || logCategory(item.kind) === category));
$('#logList').innerHTML = logs.length
? logs.map(item => `<div class="log-row ${esc(item.level)} category-${esc(logCategory(item.kind))}"><time>${esc(new Date(item.timestamp).toLocaleTimeString(locale()))}</time><span class="log-category">${esc(logCategory(item.kind))}</span><span class="kind">${esc(item.kind)}${logNotificationBadge(item)}</span><span class="message">${esc(logDisplayMessage(item))}</span></div>`).join('')
: `<div class="empty"><strong>${esc(tr('logs.emptyTitle'))}</strong>${esc(tr('logs.emptyText'))}</div>`;
} catch (error) { toast(error.message, true); }
}
async function handleWebSocketMessage(event) {
try {
const message = JSON.parse(event.data);
if (message.event === 'bootstrap') {
const data = message.data || {};
const hasControlPlan = applyBootstrapSnapshot(data);
app.controlPlanPushReady = hasControlPlan;
if (hasControlPlan) stopControlPlanFallback();
renderAll();
if (!hasControlPlan) scheduleControlPlanLoad(0);
if (app.settings?.debug?.overlay_enabled) loadDebugBacklog();
return;
}
const data = message.data || {};
if (message.event === 'control_plan.updated') { app.controlPlanPushReady = true; stopControlPlanFallback(); applyControlPlan(data.plan, data.revision); }
else if (message.event === 'device.updated') { updateDevice(data); renderSummary(); renderDevices(); renderGroups(); scheduleControlPlanLoad(); }
else if (message.event === 'device.created') { updateDevice(data); renderSummary(); renderDevices(); renderGroups(); fillSelects(); scheduleControlPlanLoad(); }
else if (message.event === 'device.deleted') { app.devices = app.devices.filter(v => v.id !== data.id); renderAll(); scheduleControlPlanLoad(); }
else if (message.event === 'devices.discovered') { (data.devices || []).forEach(updateDevice); renderAll(); scheduleControlPlanLoad(); }
else if (message.event === 'zone.updated') { const i = app.zones.findIndex(v => v.id === data.id); if (i >= 0) app.zones[i] = data; else app.zones.push(data); renderSummary(); renderHouseClimate(); renderZones(); renderDevices(); renderGroups(); scheduleControlPlanLoad(); }
else if (message.event === 'zone.created') { const i = app.zones.findIndex(v => v.id === data.id); if (i >= 0) app.zones[i] = data; else app.zones.push(data); renderSummary(); renderHouseClimate(); renderZones(); renderDevices(); renderGroups(); fillSelects(); scheduleControlPlanLoad(); }
else if (message.event === 'zone.deleted') { app.zones = app.zones.filter(v => v.id !== data.id); renderAll(); scheduleControlPlanLoad(); }
else if (['group.updated', 'group.created'].includes(message.event)) { const i = app.groups.findIndex(v => v.id === data.id); if (i >= 0) app.groups[i] = data; else app.groups.push(data); renderGroups(); fillSelects(); scheduleControlPlanLoad(); }
else if (message.event === 'group.deleted') { app.groups = app.groups.filter(v => v.id !== data.id); renderGroups(); fillSelects(); scheduleControlPlanLoad(); }
else if (['device_group.updated', 'device_group.created'].includes(message.event)) { const i = app.deviceGroups.findIndex(v => v.id === data.id); if (i >= 0) app.deviceGroups[i] = data; else app.deviceGroups.push(data); renderDevices(); renderHistoryNavigation(); }
else if (message.event === 'device_group.deleted') { app.deviceGroups = app.deviceGroups.filter(v => v.id !== data.id); renderDevices(); renderHistoryNavigation(); }
else if (message.event === 'energy.updated') {
const total = Number(data.total_kwh);
if (Number.isFinite(total)) {
const snapshot = { total_kwh: total, timestamp: data.timestamp || message.timestamp, source: data.source || '', origin: 'realtime' };
const selectedEnergySource = group => group.energy_source === 'gree_cloud' || (group.energy_source === 'auto' && !!group.energy_device_id)
? 'gree_cloud'
: (group.energy_source === 'home_assistant' || (group.energy_source === 'auto' && !!group.ha_energy_entity_id) ? 'home_assistant' : '');
if (String(data.target_id || '').startsWith('group:')) {
const groupId = String(data.target_id).slice(6);
const group = (app.deviceGroups || []).find(item => item.id === groupId);
if (group && selectedEnergySource(group) === snapshot.source) app.deviceGroupEnergy[groupId] = snapshot;
} else if (snapshot.source === 'gree_cloud') {
(app.deviceGroups || [])
.filter(group => group.energy_device_id === data.target_id && selectedEnergySource(group) === 'gree_cloud')
.forEach(group => { app.deviceGroupEnergy[group.id] = snapshot; });
}
renderDevices();
}
}
else if (['schedule.updated', 'schedule.created'].includes(message.event)) { const i = app.schedules.findIndex(v => v.id === data.id); if (i >= 0) app.schedules[i] = data; else app.schedules.push(data); renderSchedules(); fillSelects(); scheduleControlPlanLoad(); }
else if (message.event === 'schedule.deleted') { app.schedules = app.schedules.filter(v => v.id !== data.id); renderSchedules(); fillSelects(); scheduleControlPlanLoad(); }
else if (message.event === 'schedule.template_applied') { try { app.schedules = await api('/api/schedules'); renderSchedules(); fillSelects(); } catch (_) { } scheduleControlPlanLoad(); }
else if (['automation.updated', 'automation.created'].includes(message.event)) { const i = app.automations.findIndex(v => v.id === data.id); if (i >= 0) app.automations[i] = data; else app.automations.push(data); renderAutomations(); fillSelects(); scheduleControlPlanLoad(); }
else if (message.event === 'automation.deleted') { app.automations = app.automations.filter(v => v.id !== data.id); renderAutomations(); fillSelects(); scheduleControlPlanLoad(); }
else if (['flow.updated', 'flow.created'].includes(message.event)) { const i = app.flows.findIndex(v => v.id === data.id); if (i >= 0) app.flows[i] = data; else app.flows.push(data); renderFlows(); scheduleControlPlanLoad(); }
else if (message.event === 'flow.deleted') { app.flows = app.flows.filter(v => v.id !== data.id); renderFlows(); scheduleControlPlanLoad(); }
else if (message.event === 'settings.application.updated') { applySettingsSection('application', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); renderSimulationModeBanner(); renderSystemInfo(); }
else if (message.event === 'settings.gree.updated') { applySettingsSection('gree', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); scheduleControlPlanLoad(); }
else if (message.event === 'settings.gree_cloud.updated') { applySettingsSection('greeCloud', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); }
else if (message.event === 'settings.history.updated') { applySettingsSection('history', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); renderLogRetention(); }
else if (message.event === 'settings.influxdb.updated') { applySettingsSection('influxdb', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); }
else if (message.event === 'settings.notifications.updated') { applySettingsSection('notifications', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); }
else if (message.event === 'settings.debug.updated') { applySettingsSection('debug', data); if (!isFormDirty($('#settingsForm'))) renderSettings(); renderDebugOverlay(); if (data.overlay_enabled) loadDebugBacklog(); }
else if (message.event === 'settings.night.updated') { applySettingsSection('night', data); if (!isFormDirty($('#nightModeForm'))) renderNightSettings(); renderHouseClimate(); scheduleControlPlanLoad(); }
else if (message.event === 'settings.home_assistant.updated') {
applySettingsSection('homeAssistant', data);
if (!isFormDirty($('#homeAssistantForm'))) { app.sensorAliases = { ...(data.sensor_aliases || {}) }; app.flowSharedInputs = JSON.parse(JSON.stringify(data.flow_inputs || [])); renderHomeAssistantSettings(); }
renderHouseClimate(); if (app.flowDraft) renderFlowEditor(); scheduleControlPlanLoad();
}
else if (message.event === 'house.mode_changed') { app.settings = app.settings || {}; app.settings.house_mode = data.mode || 'cool'; renderHouseClimate(); renderGroups(); scheduleControlPlanLoad(); }
else if (message.event === 'outdoor.updated') { app.outdoorTemperature = Number.isFinite(Number(data.temperature)) ? Number(data.temperature) : null; renderHouseClimate(); scheduleControlPlanLoad(); }
else if (message.event === 'configuration.imported') { await loadBootstrap(); return; }
else if (message.event === 'gree.frame_received') {
app.system = app.system || {};
app.system.gree_received_frames = Number(data.total || 0);
app.system.gree_received_frames_by_device = { ...(app.system.gree_received_frames_by_device || {}) };
if (data.device_id) app.system.gree_received_frames_by_device[data.device_id] = Number(data.device_count || 0);
renderGreeFrameStats();
renderSystemInfo();
}
else if (message.event === 'gree.frame') { if (app.settings?.debug?.overlay_enabled) debugLine('GREE', `${data.direction || '?'} ${data.protocol_version || ''}`, data.device_name || data.device_id || data.target || '', message.timestamp, data.payload); }
else if (message.event === 'gree_cloud.request') { if (app.settings?.debug?.overlay_enabled) debugLine('CLOUD', `${data.operation || 'request'} · ${data.phase || '?'}`, `${data.device_name || data.device_id || data.transport || ''}${data.duration_ms == null ? '' : ` · ${data.duration_ms} ms`}`, message.timestamp, data); }
else if (message.event === 'gree_cloud.mqtt') { if (app.settings?.debug?.overlay_enabled) debugLine('MQTT', `${data.direction || '?'}`, `${data.topic || data.broker || ''}${data.device_id ? ` · ${data.device_id}` : ''}`, message.timestamp, data); }
else if (message.event === 'api.request') { if (app.settings?.debug?.overlay_enabled) debugLine('HTTP', `${data.method || '?'} ${data.status || ''}`, `${data.path || ''} · ${data.duration_ms ?? '?'} ms`, message.timestamp); }
else if (message.event === 'log.created') { if (app.settings?.debug?.overlay_enabled) debugLine('API', data.kind || data.level || 'log', data.message || '', message.timestamp, data.metadata); if (app.currentView === 'logs') loadLogs(); }
else if (message.event === 'log.updated') { if (app.currentView === 'logs') loadLogs(); }
} catch (_) { }
}
function connectWebSocket() {
if (app.ws && [WebSocket.OPEN, WebSocket.CONNECTING].includes(app.ws.readyState)) return;
clearTimeout(app.wsTimer);
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const query = app.token ? `?token=${encodeURIComponent(app.token)}` : '';
const ws = new WebSocket(`${protocol}//${location.host}${withBase('/ws')}${query}`); app.ws = ws;
ws.onopen = () => updateConnectionIndicator('connected');
ws.onclose = () => { app.controlPlanPushReady = false; updateConnectionIndicator('disconnected'); scheduleControlPlanLoad(0); app.wsTimer = setTimeout(connectWebSocket, 3000); };
ws.onerror = () => updateConnectionIndicator('connectionError');
let messageQueue = Promise.resolve();
ws.onmessage = event => {
messageQueue = messageQueue.then(() => handleWebSocketMessage(event)).catch(() => { });
};
}