function renderAccessTokens() {
const list = $('#accessTokenList');
if (!list) return;
list.innerHTML = app.accessTokens.length ? app.accessTokens.map(item => `
${esc(item.name)}${esc(item.token_prefix)}${esc(tr('settings.created'))}: ${esc(dateTime(item.created_at))}
`).join('') : `${esc(tr('settings.noTokens'))}${esc(tr('settings.noTokensHint'))}
`;
}
function metricHaEntities() {
return new Set([
app.settings?.home_assistant?.outdoor_entity_id,
...app.zones.filter(zone => ['home_assistant', 'combined'].includes(zone.sensor_source)).map(zone => zone.ha_entity_id),
...app.historyData.sensors.map(row => row.entity_id),
].filter(Boolean));
}
function flowHaEntities() {
return new Set((app.flowSharedInputs || []).map(item => item?.config?.entity_id).filter(Boolean));
}
function knownHaEntities() {
return [...new Set([
...Object.keys(app.sensorAliases || {}),
...app.zones.map(zone => zone.ha_entity_id).filter(Boolean),
app.settings?.home_assistant?.default_entity_id,
app.settings?.home_assistant?.outdoor_entity_id,
...(app.flowSharedInputs || []).map(item => item?.config?.entity_id).filter(Boolean),
...app.historyData.sensors.map(row => row.entity_id),
].filter(Boolean))].sort();
}
function renderSensorAliases() {
const host = $('#sensorAliasList'); if (!host) return;
const entities = knownHaEntities();
const metricEntities = metricHaEntities(), flowEntities = flowHaEntities();
host.innerHTML = entities.length ? entities.map(entity => {
const badges = `${metricEntities.has(entity) ? `${esc(tr('settings.sensorMetricBadge'))}` : ''}${flowEntities.has(entity) ? `${esc(tr('settings.sensorFlowBadge'))}` : ''}`;
return ``;
}).join('') : `${esc(tr('settings.noSensorAliases'))}
`;
}
function flowSharedInputKinds() {
return [
['constant', 'flow.node.constant'], ['ha_state', 'flow.node.haState'], ['ha_numeric', 'flow.node.haNumeric'],
['ha_attribute', 'flow.node.haAttribute'], ['ha_available', 'flow.node.haAvailable'],
['outdoor_temperature', 'flow.node.outdoorTemperature'], ['device_temperature', 'flow.node.deviceTemperature'],
['zone_temperature', 'flow.node.zoneTemperature'], ['house_mode', 'flow.node.houseMode'],
['device_state', 'flow.node.deviceState'], ['zone_state', 'flow.node.zoneState'],
['group_state', 'flow.node.groupState'], ['night_mode', 'flow.node.nightMode'],
];
}
function flowSharedInputDefaultConfig(kind) {
if (kind === 'device_temperature') return { device_id: app.devices[0]?.id || '' };
if (kind === 'zone_temperature') return { zone_id: app.zones[0]?.id || '' };
if (kind === 'ha_state' || kind === 'ha_numeric' || kind === 'ha_available') return { entity_id: '' };
if (kind === 'ha_attribute') return { entity_id: '', attribute: '' };
if (kind === 'device_state') return { device_id: app.devices[0]?.id || '', field: 'online' };
if (kind === 'zone_state') return { zone_id: app.zones[0]?.id || '', field: 'demand' };
if (kind === 'group_state') return { group_id: app.groups[0]?.id || '', field: 'power_enabled' };
if (kind === 'constant') return { value: true };
return {};
}
function sharedFlowInputSourceSummary(item) {
if (!item) return '—';
const c = item.config || {};
if (item.kind === 'outdoor_temperature') return tr('flow.node.outdoorTemperature');
if (item.kind === 'device_temperature') return app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice');
if (item.kind === 'zone_temperature') return app.zones.find(value => value.id === c.zone_id)?.name || tr('common.noZone');
if (item.kind === 'ha_state' || item.kind === 'ha_numeric' || item.kind === 'ha_available') return c.entity_id || 'entity_id';
if (item.kind === 'ha_attribute') return `${c.entity_id || 'entity_id'}.${c.attribute || 'attribute'}`;
if (item.kind === 'house_mode') return tr('flow.houseMode');
if (item.kind === 'device_state') return `${app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice')} · ${c.field || 'state'}`;
if (item.kind === 'zone_state') return `${app.zones.find(value => value.id === c.zone_id)?.name || tr('common.noZone')} · ${c.field || 'state'}`;
if (item.kind === 'group_state') return `${app.groups.find(value => value.id === c.group_id)?.name || tr('groups.group')} · ${c.field || 'power_enabled'}`;
return flowNodeSummary({ kind: item.kind, config: c });
}
function isHaSharedInputKind(kind) { return ['ha_state', 'ha_numeric', 'ha_attribute', 'ha_available'].includes(kind); }
function flowSharedInputUsages(id) {
return (app.flows || []).filter(flow => (flow.nodes || []).some(node => node.kind === 'shared_input' && node.config?.input_id === id));
}
function renderFlowSharedInputs() {
const host = $('#flowSharedInputList'); if (!host) return;
const items = app.flowSharedInputs || [];
host.innerHTML = items.length ? items.map(item => {
const usages = flowSharedInputUsages(item.id);
const usageMarkup = usages.length
? `${esc(tr('flow.sharedInputUsedBy', { count: usages.length }))}${usages.slice(0, 4).map(flow => ``).join('')}${usages.length > 4 ? `+${usages.length - 4}` : ''}
`
: `${esc(tr('flow.sharedInputUnused'))}`;
return ``;
}).join('') : `${esc(tr('flow.sharedInputsEmpty'))}${esc(tr('flow.sharedInputsEmptyHint'))}
`;
const revision = $('#flowSharedInputsRevision');
if (revision) revision.value = JSON.stringify(items);
}
function sharedFlowOptions(items, selected, label = item => item.name) {
return items.map(item => ``).join('');
}
function renderFlowSharedInputFields(kind, config = {}) {
const host = $('#flowSharedInputFields'); if (!host) return;
const c = config || {};
let fields = '';
if (kind === 'constant') fields = ``;
else if (kind === 'ha_state') fields = ``;
else if (kind === 'ha_numeric') fields = ``;
else if (kind === 'ha_attribute') fields = ``;
else if (kind === 'ha_available') fields = `${esc(tr('flow.haAvailableHint'))}
`;
else if (kind === 'outdoor_temperature') fields = `${esc(tr('flow.sharedInputSourceOnlyHint'))}
`;
else if (kind === 'device_temperature') fields = ``;
else if (kind === 'zone_temperature') fields = ``;
else if (kind === 'house_mode') fields = `${esc(tr('flow.sharedInputSourceOnlyHint'))}
`;
else if (kind === 'device_state') fields = ``;
else if (kind === 'zone_state') fields = ``;
else if (kind === 'group_state') fields = ``;
else if (kind === 'night_mode') fields = `${esc(tr('flow.nightModeHint'))}
`;
host.innerHTML = fields;
const testPanel = $('#flowSharedInputTestPanel'), testResult = $('#flowSharedInputTestResult');
if (testPanel) testPanel.hidden = !isHaSharedInputKind(kind);
if (testResult) { testResult.hidden = true; testResult.innerHTML = ''; }
}
function openFlowSharedInputEditor(id = '') {
const form = $('#flowSharedInputForm'), dialog = $('#flowSharedInputDialog'); if (!form || !dialog) return;
const item = id ? (app.flowSharedInputs || []).find(value => value.id === id) : null;
const kindSelect = form.kind;
kindSelect.innerHTML = flowSharedInputKinds().map(([kind, key]) => ``).join('');
form.id.value = item?.id || '';
form.name.value = item?.name || '';
form.kind.value = item?.kind || 'constant';
form.dataset.editingId = item?.id || '';
renderFlowSharedInputFields(form.kind.value, item?.config || flowSharedInputDefaultConfig(form.kind.value));
dialog.showModal();
setTimeout(() => form.name.focus(), 0);
}
function collectFlowSharedInputConfig(kind) {
const config = {};
$$('[data-shared-config]', $('#flowSharedInputFields')).forEach(field => {
const key = field.dataset.sharedConfig;
let value = field.value;
if (kind === 'constant' && key === 'value') value = value === 'true';
config[key] = value;
});
return config;
}
function renderLogRetention() {
const select = $('#logRetentionDays'); if (!select || !app.settings) return;
[...select.options].forEach(option => { option.textContent = `${option.value} ${tr('common.days')}`; });
const days = String(app.settings.event_log_retention_days || 30);
if (![...select.options].some(option => option.value === days)) {
const option = document.createElement('option'); option.value = days; option.textContent = `${days} ${tr('common.days')}`; select.appendChild(option);
}
select.value = days;
}
function renderSettings() {
if (!app.settings) return;
const form = $('#settingsForm');
if (!form) return;
form.controller_id.value = app.settings.controller_id || '';
form.poll_interval_seconds.value = app.settings.poll_interval_seconds || 15;
form.zone_interval_seconds.value = app.settings.zone_interval_seconds || 5;
form.discovery_broadcast.value = app.settings.discovery_broadcast || '255.255.255.255:7000';
form.discovery_timeout_ms.value = app.settings.discovery_timeout_ms || 3000;
form.simulator_enabled.checked = !!app.settings.simulator_enabled;
form.history_retention_days.value = app.settings.history_retention_days || 30;
form.event_log_retention_days.value = app.settings.event_log_retention_days || 30;
form.history_compaction_enabled.checked = app.settings.history_compaction_enabled !== false;
form.suppress_device_beep.checked = !!app.settings.suppress_device_beep;
const cloud = app.settings.gree_cloud || {};
form.gree_cloud_enabled.checked = !!cloud.enabled;
form.gree_cloud_region.value = cloud.region || 'Europe';
form.gree_cloud_username.value = cloud.username || '';
form.gree_cloud_password.value = '';
form.gree_cloud_password.placeholder = cloud.password_configured ? tr('settings.secretSaved') : tr('settings.secretKeep');
form.gree_cloud_polling_interval_seconds.value = Number(cloud.polling_interval_seconds || 60);
const cloudInstance = $('#greeCloudInstanceId');
if (cloudInstance) cloudInstance.textContent = cloud.installation_id || '—';
const cloudLastContact = $('#greeCloudLastContact');
if (cloudLastContact) cloudLastContact.textContent = cloud.last_successful_contact ? dateTime(cloud.last_successful_contact) : tr('common.unavailable');
form.compressor_protection_enabled.checked = app.settings.compressor_protection_enabled !== false;
form.compressor_protection_minutes.value = (Number(app.settings.compressor_protection_seconds || 180) / 60).toFixed(1).replace(/\.0$/, '');
updateCompressorProtectionFields();
form.influx_enabled.checked = !!app.settings.influxdb?.enabled;
form.influx_version.value = String(app.settings.influxdb?.version || '2');
form.influx_threshold_days.value = app.settings.influxdb?.history_threshold_days || 30;
form.influx_url.value = app.settings.influxdb?.url || '';
form.influx_database.value = app.settings.influxdb?.database || 'gree_controller';
form.influx_username.value = app.settings.influxdb?.username || '';
form.influx_password.value = '';
form.influx_password.placeholder = app.settings.influxdb?.password_configured ? tr('settings.secretSaved') : tr('settings.secretKeep');
form.influx_org.value = app.settings.influxdb?.org || '';
form.influx_bucket.value = app.settings.influxdb?.bucket || 'gree_controller';
form.influx_token.value = '';
form.influx_token.placeholder = app.settings.influxdb?.token_configured ? tr('settings.secretSaved') : tr('settings.secretKeep');
form.debug_overlay_enabled.checked = !!app.settings.debug?.overlay_enabled;
form.debug_gree_frames.checked = !!app.settings.debug?.gree_frames;
form.debug_cloud_requests.checked = !!app.settings.debug?.cloud_requests;
form.debug_cloud_mqtt.checked = !!app.settings.debug?.cloud_mqtt;
const n = app.settings.notifications || {};
form.notifications_enabled.checked = !!n.enabled; form.notifications_mode.value = n.mode || 'problems'; form.notifications_provider.value = n.provider || 'pushover';
form.pushover_app_token.value = ''; form.pushover_user_key.value = ''; form.slack_webhook_url.value = ''; form.discord_webhook_url.value = '';
form.pushover_app_token.placeholder = n.pushover_configured ? tr('settings.secretSaved') : tr('notifications.applicationTokenPlaceholder');
form.pushover_user_key.placeholder = n.pushover_configured ? tr('settings.secretSaved') : tr('notifications.userKeyPlaceholder');
form.slack_webhook_url.placeholder = n.slack_configured ? tr('settings.secretSaved') : 'https://hooks.slack.com/services/…';
form.discord_webhook_url.placeholder = n.discord_configured ? tr('settings.secretSaved') : 'https://discord.com/api/webhooks/…';
form.notification_cooldown_seconds.value = n.cooldown_seconds || 300; form.notification_failure_threshold.value = n.communication_failure_threshold || 3; form.notification_target_timeout.value = n.target_timeout_minutes || 60;
const alerts = n.alert_types || {};
form.notification_alert_stale_sensor.checked = alerts.stale_sensor !== false;
form.notification_alert_sensor_errors.checked = alerts.sensor_errors !== false;
form.notification_alert_communication.checked = alerts.communication !== false;
form.notification_alert_target_timeout.checked = alerts.target_timeout !== false;
form.notification_alert_automation.checked = alerts.automation !== false;
form.notification_alert_sensor_discrepancy.checked = alerts.sensor_discrepancy !== false;
form.notification_alert_control_errors.checked = alerts.control_errors !== false;
form.notification_alert_important_events.checked = alerts.important_events !== false;
form.notification_alert_other.checked = alerts.other !== false;
updateNotificationFields();
updateInfluxFields();
renderLogRetention();
renderGreeFrameStats();
renderSystemInfo();
renderSimulationModeBanner();
setSettingsTab(app.settingsTab);
markFormClean(form);
void refreshGreeCloudRuntimeStatus();
}
function updateCompressorProtectionFields() {
const form = $('#settingsForm');
if (!form?.compressor_protection_enabled || !form?.compressor_protection_minutes) return;
form.compressor_protection_minutes.disabled = !form.compressor_protection_enabled.checked;
}
function renderSimulationModeBanner() {
const banner = $('#simulationModeBanner');
if (!banner) return;
const enabled = !!app.settings?.simulator_enabled;
banner.hidden = !enabled;
document.body.classList.toggle('simulation-mode-enabled', enabled);
}
function setSettingsTab(tab) {
app.settingsTab = ['gree', 'cloud'].includes(tab) ? tab : 'app';
$$('[data-settings-pane]').forEach(pane => { pane.hidden = pane.dataset.settingsPane !== app.settingsTab; });
$$('[data-settings-tab]').forEach(button => {
const active = button.dataset.settingsTab === app.settingsTab;
button.classList.toggle('active', active);
button.setAttribute('aria-selected', String(active));
});
}
function renderSystemInfo() {
const host = $('#systemInfo');
if (!host) return;
const elapsed = Math.max(0, Math.floor((Date.now() - Number(app.systemSnapshotAt || Date.now())) / 1000));
const uptime = Number(app.system?.uptime_seconds || 0) + elapsed;
const dbPath = String(app.system?.database || '');
const database = dbPath ? dbPath.split(/[\\/]/).filter(Boolean).pop() : '—';
const ready = !!app.system?.control_ready;
const connected = app.connectionStatus === 'connected';
const items = [
[tr('settings.version'), app.system?.version || '—', 'version'],
[tr('settings.uptime'), formatDuration(uptime), 'uptime'],
[tr('settings.controlEngine'), ready ? tr('settings.ready') : tr('settings.syncing'), ready ? 'ok' : 'warn'],
[tr('settings.websocket'), tr(`status.${app.connectionStatus}`), connected ? 'ok' : 'warn'],
[tr('settings.apiAuth'), app.system?.auth_required ? tr('settings.enabled') : tr('settings.disabled'), 'neutral'],
[tr('settings.httpBind'), app.system?.bind || '—', 'mono'],
[tr('settings.database'), database, 'mono'],
[tr('settings.basePath'), app.system?.base_path || APP_BASE || '/', 'mono'],
];
host.innerHTML = `${esc(tr('settings.systemState'))}${esc(tr('settings.systemStatusTitle'))}
${esc(tr('settings.systemStateHint'))}
${esc(ready && connected ? tr('settings.healthy') : tr('settings.attention'))}${items.map(([label, value, tone]) => `
${esc(label)}${esc(value)}
`).join('')}
`;
}
function renderGreeFrameStats() {
const host = $('#greeFrameStats');
if (!host) return;
const total = Number(app.system?.gree_received_frames || 0);
const byDevice = app.system?.gree_received_frames_by_device || {};
const deviceRows = app.devices.filter(device => !device.simulated).map(device => {
const count = Number(byDevice[device.id] || 0);
return `${esc(device.name || device.id)}${count.toLocaleString(locale())}
`;
}).join('');
host.innerHTML = `${esc(tr('settings.receivedFramesTotal'))}${total.toLocaleString(locale())}
${deviceRows || `${esc(tr('settings.receivedFramesDevices'))}0
`}`;
}
function renderNightSettings() {
if (!app.settings) return;
const form = $('#nightModeForm');
if (!form) return;
form.night_mode_enabled.checked = !!app.settings.night_mode?.enabled;
form.night_mode_start.value = app.settings.night_mode?.start_time || '22:00';
form.night_mode_end.value = app.settings.night_mode?.end_time || '06:00';
form.night_mode_max_fan_speed.value = String(app.settings.night_mode?.max_fan_speed || 1);
form.night_mode_force_quiet.checked = app.settings.night_mode?.force_quiet !== false;
form.night_mode_native_sleep.checked = app.settings.night_mode?.use_native_sleep !== false;
markFormClean(form);
}
function renderHomeAssistantSettings() {
if (!app.settings) return;
const form = $('#homeAssistantForm');
if (!form) return;
form.ha_url.value = app.settings.home_assistant?.url || '';
form.ha_token.value = '';
form.ha_token.placeholder = app.settings.home_assistant?.token_configured ? tr('settings.haTokenSaved') : tr('settings.haLongLivedToken');
form.ha_entity_id.value = app.settings.home_assistant?.default_entity_id || '';
form.ha_outdoor_entity_id.value = app.settings.home_assistant?.outdoor_entity_id || '';
form.ha_sensor_stale_after_minutes.value = String(Math.max(1, Math.round(Number(app.settings.home_assistant?.sensor_stale_after_seconds || 300) / 60)));
form.ha_allow_invalid_tls.checked = !!app.settings.home_assistant?.allow_invalid_tls;
form.outdoor_assist_enabled.checked = !!app.settings.outdoor_assist_enabled;
renderSensorAliases();
renderFlowSharedInputs();
renderAccessTokens();
markFormClean(form);
}
function updateInfluxFields() {
const version = $('#settingsForm [name=influx_version]')?.value || '2';
$$('[data-influx-fields]').forEach(node => { node.hidden = node.dataset.influxFields !== version; });
}
function updateNotificationFields() { const provider = $('#settingsForm [name=notifications_provider]')?.value || 'pushover'; $$('[data-notification-provider]').forEach(n => n.hidden = n.dataset.notificationProvider !== provider); }
function logCategory(kind = '') { const prefix = String(kind).split('.')[0]; return ['device', 'zone', 'automation', 'settings'].includes(prefix) ? prefix : (['home_assistant', 'influx', 'notification'].includes(prefix) ? 'integration' : 'system'); }
function debugLine(source, kind, message, timestamp = new Date().toISOString(), data = null) {
app.debugLines.push({ source, kind, message, timestamp, data });
if (app.debugLines.length > 160) app.debugLines.splice(0, app.debugLines.length - 160);
renderDebugOverlay();
}
function renderDebugOverlay() {
const overlay = $('#debugOverlay'); if (!overlay) return;
const enabled = !!app.settings?.debug?.overlay_enabled;
overlay.hidden = !enabled;
if (!enabled) return;
if (!['all', 'requests', 'gree', 'cloud', 'mqtt'].includes(app.debugFilter)) app.debugFilter = 'all';
const status = $('#debugOverlayStatus');
if (status) {
const enabledSources = [
app.settings?.debug?.gree_frames ? tr('debug.gree') : '',
app.settings?.debug?.cloud_requests ? tr('debug.cloud') : '',
app.settings?.debug?.cloud_mqtt ? tr('debug.mqtt') : '',
].filter(Boolean);
status.textContent = enabledSources.length ? `${tr('debug.liveSources')}: ${enabledSources.join(' · ')}` : tr('debug.apiOnly');
}
$$('[data-debug-filter]', overlay).forEach(button => {
const active = button.dataset.debugFilter === app.debugFilter;
button.classList.toggle('active', active);
button.setAttribute('aria-selected', String(active));
});
const host = $('#debugOverlayLines'); if (!host) return;
const visible = app.debugLines.filter(line => {
if (app.debugFilter === 'gree') return line.source === 'GREE';
if (app.debugFilter === 'requests') return line.source === 'HTTP';
if (app.debugFilter === 'cloud') return line.source === 'CLOUD';
if (app.debugFilter === 'mqtt') return line.source === 'MQTT';
return true;
}).slice(-120);
host.innerHTML = visible.length ? visible.map(line => {
const details = line.data == null ? '' : ` ${typeof line.data === 'string' ? line.data : JSON.stringify(line.data)}`;
const sourceClass = line.source === 'GREE' ? 'gree' : line.source === 'HTTP' ? 'request' : line.source === 'CLOUD' ? 'cloud' : line.source === 'MQTT' ? 'mqtt' : 'api';
return `${esc(line.source)}${esc(line.kind)}: ${esc(line.message || '')}${esc(details)}
`;
}).join('') : `${esc(tr(app.debugFilter === 'gree' ? 'debug.emptyGree' : app.debugFilter === 'requests' ? 'debug.emptyRequests' : app.debugFilter === 'cloud' ? 'debug.emptyCloud' : app.debugFilter === 'mqtt' ? 'debug.emptyMqtt' : 'debug.empty'))}
`;
host.scrollTop = host.scrollHeight;
}
async function loadDebugBacklog() {
if (app.debugBacklogLoaded || !app.settings?.debug?.overlay_enabled) return;
try {
const data = await api('/api/events?limit=60');
app.debugLines = (data.events || []).reverse().map(item => ({ source: 'API', kind: item.kind, message: item.message, timestamp: item.timestamp, data: item.metadata })).slice(-120);
app.debugBacklogLoaded = true;
renderDebugOverlay();
} catch (_) { }
}
function formatDuration(seconds) {
const days = Math.floor(seconds / 86400), hours = Math.floor((seconds % 86400) / 3600), minutes = Math.floor((seconds % 3600) / 60);
return `${days ? `${days}d ` : ''}${hours}h ${minutes}m`;
}
setInterval(() => { if (app.currentView === 'settings') renderSystemInfo(); }, 30000);
async function refreshGreeCloudRuntimeStatus() {
const account = $('#greeCloudAccountStatus');
const mqtt = $('#greeCloudMqttStatus');
if (!account || !mqtt) return;
const setText = (selector, value) => { const node = $(selector); if (node) node.textContent = value; };
try {
const status = await api('/api/integrations/gree-cloud/status');
const runtime = status.runtime || {};
account.textContent = String(status.account_status || 'unknown').replaceAll('_', ' ');
mqtt.textContent = String(status.mqtt_status || 'disconnected').replaceAll('_', ' ');
setText('#greeCloudDevicesOnline', `${Number(status.online_device_count || 0)} / ${Number(status.device_count || 0)}`);
setText('#greeCloudRestResponseTime', status.last_rest_response_time_ms != null && Number.isFinite(Number(status.last_rest_response_time_ms)) ? `${Number(status.last_rest_response_time_ms)} ms` : tr('common.unavailable'));
setText('#greeCloudResponseTime', runtime.last_response_time_ms != null && Number.isFinite(Number(runtime.last_response_time_ms)) ? `${Number(runtime.last_response_time_ms)} ms` : tr('common.unavailable'));
setText('#greeCloudLastDeviceResponse', runtime.last_device_response ? dateTime(runtime.last_device_response) : tr('common.unavailable'));
setText('#greeCloudLastMqttMessage', runtime.last_mqtt_message ? dateTime(runtime.last_mqtt_message) : tr('common.unavailable'));
setText('#greeCloudConnectedSince', runtime.mqtt_connected_since ? dateTime(runtime.mqtt_connected_since) : tr('common.unavailable'));
setText('#greeCloudBroker', runtime.broker_host || tr('common.unavailable'));
setText('#greeCloudTraffic', `${Number(runtime.requests_sent || 0)} / ${Number(runtime.responses_received || 0)} / ${Number(runtime.request_timeouts || 0)}`);
if (status.last_successful_contact) setText('#greeCloudLastContact', dateTime(status.last_successful_contact));
} catch (_) {
account.textContent = 'unknown';
mqtt.textContent = 'disconnected';
['#greeCloudDevicesOnline', '#greeCloudRestResponseTime', '#greeCloudResponseTime', '#greeCloudLastDeviceResponse', '#greeCloudLastMqttMessage', '#greeCloudConnectedSince', '#greeCloudBroker', '#greeCloudTraffic'].forEach(selector => setText(selector, tr('common.unavailable')));
}
}