538 lines
25 KiB
JavaScript
538 lines
25 KiB
JavaScript
const SETTINGS_ENDPOINTS = {
|
|
application: '/api/settings/application',
|
|
gree: '/api/settings/gree',
|
|
greeCloud: '/api/settings/gree-cloud',
|
|
history: '/api/settings/history',
|
|
influxdb: '/api/settings/influxdb',
|
|
notifications: '/api/settings/notifications',
|
|
night: '/api/settings/night',
|
|
homeAssistant: '/api/settings/home-assistant',
|
|
debug: '/api/settings/debug',
|
|
};
|
|
|
|
function applySettingsSection(section, data) {
|
|
app.settings = app.settings || {};
|
|
if (section === 'application') app.settings.simulator_enabled = !!data.simulator_enabled;
|
|
else if (section === 'gree') Object.assign(app.settings, data);
|
|
else if (section === 'greeCloud') app.settings.gree_cloud = data;
|
|
else if (section === 'history') {
|
|
app.settings.history_retention_days = Number(data.retention_days);
|
|
app.settings.history_compaction_enabled = data.compaction_enabled !== false;
|
|
app.settings.event_log_retention_days = Number(data.event_retention_days);
|
|
} else if (section === 'influxdb') app.settings.influxdb = data;
|
|
else if (section === 'notifications') app.settings.notifications = data;
|
|
else if (section === 'night') app.settings.night_mode = data;
|
|
else if (section === 'homeAssistant') {
|
|
app.settings.home_assistant = data;
|
|
app.settings.outdoor_assist_enabled = !!data.outdoor_assist_enabled;
|
|
} else if (section === 'debug') app.settings.debug = data;
|
|
}
|
|
|
|
function applicationSettingsBodyFromForm(form) {
|
|
return { simulator_enabled: form.simulator_enabled.checked };
|
|
}
|
|
|
|
function greeSettingsBodyFromForm(form) {
|
|
const raw = Object.fromEntries(new FormData(form));
|
|
return {
|
|
controller_id: raw.controller_id,
|
|
poll_interval_seconds: Number(raw.poll_interval_seconds),
|
|
zone_interval_seconds: Number(raw.zone_interval_seconds),
|
|
discovery_timeout_ms: Number(raw.discovery_timeout_ms),
|
|
discovery_broadcast: raw.discovery_broadcast,
|
|
ping_metrics_enabled: form.ping_metrics_enabled.checked,
|
|
ping_interval_seconds: Number(raw.ping_interval_seconds || 60),
|
|
ping_sample_count: Number(raw.ping_sample_count || 3),
|
|
suppress_device_beep: form.suppress_device_beep.checked,
|
|
compressor_protection_enabled: form.compressor_protection_enabled.checked,
|
|
compressor_protection_seconds: Math.max(30, Math.min(1800, Math.round(Number(raw.compressor_protection_minutes || 3) * 60))),
|
|
};
|
|
}
|
|
|
|
function greeCloudSettingsBodyFromForm(form) {
|
|
const raw = Object.fromEntries(new FormData(form));
|
|
const body = {
|
|
enabled: form.gree_cloud_enabled.checked,
|
|
region: raw.gree_cloud_region || 'Europe',
|
|
username: raw.gree_cloud_username || '',
|
|
polling_interval_seconds: Number(raw.gree_cloud_polling_interval_seconds || 60),
|
|
connectivity_metrics_enabled: form.gree_cloud_connectivity_metrics_enabled.checked,
|
|
connectivity_metrics_interval_seconds: Number(raw.gree_cloud_connectivity_metrics_interval_seconds || 300),
|
|
connectivity_metrics_sample_count: Number(raw.gree_cloud_connectivity_metrics_sample_count || 3),
|
|
};
|
|
if (raw.gree_cloud_password) body.password = raw.gree_cloud_password;
|
|
return body;
|
|
}
|
|
|
|
function historySettingsBodyFromForm(form, eventRetentionDays = null) {
|
|
const raw = Object.fromEntries(new FormData(form));
|
|
return {
|
|
retention_days: Number(raw.history_retention_days),
|
|
compaction_enabled: form.history_compaction_enabled.checked,
|
|
event_retention_days: eventRetentionDays == null ? Number(raw.event_log_retention_days) : Number(eventRetentionDays),
|
|
};
|
|
}
|
|
|
|
function influxDbSettingsBodyFromForm(form) {
|
|
const raw = Object.fromEntries(new FormData(form));
|
|
const body = {
|
|
enabled: form.influx_enabled.checked,
|
|
version: raw.influx_version,
|
|
url: raw.influx_url,
|
|
database: raw.influx_database,
|
|
username: raw.influx_username,
|
|
org: raw.influx_org,
|
|
bucket: raw.influx_bucket,
|
|
history_threshold_days: Number(raw.influx_threshold_days),
|
|
};
|
|
if (raw.influx_password) body.password = raw.influx_password;
|
|
if (raw.influx_token) body.token = raw.influx_token;
|
|
return body;
|
|
}
|
|
|
|
function notificationSettingsBodyFromForm(form) {
|
|
const raw = Object.fromEntries(new FormData(form));
|
|
const body = {
|
|
enabled: form.notifications_enabled.checked,
|
|
mode: raw.notifications_mode,
|
|
provider: raw.notifications_provider,
|
|
cooldown_seconds: Number(raw.notification_cooldown_seconds || 300),
|
|
communication_failure_threshold: Number(raw.notification_failure_threshold || 3),
|
|
target_timeout_minutes: Number(raw.notification_target_timeout || 60),
|
|
alert_types: {
|
|
stale_sensor: form.notification_alert_stale_sensor.checked,
|
|
sensor_errors: form.notification_alert_sensor_errors.checked,
|
|
communication: form.notification_alert_communication.checked,
|
|
target_timeout: form.notification_alert_target_timeout.checked,
|
|
automation: form.notification_alert_automation.checked,
|
|
sensor_discrepancy: form.notification_alert_sensor_discrepancy.checked,
|
|
control_errors: form.notification_alert_control_errors.checked,
|
|
important_events: form.notification_alert_important_events.checked,
|
|
other: form.notification_alert_other.checked,
|
|
},
|
|
};
|
|
if (raw.pushover_app_token) body.pushover_app_token = raw.pushover_app_token;
|
|
if (raw.pushover_user_key) body.pushover_user_key = raw.pushover_user_key;
|
|
if (raw.slack_webhook_url) body.slack_webhook_url = raw.slack_webhook_url;
|
|
if (raw.discord_webhook_url) body.discord_webhook_url = raw.discord_webhook_url;
|
|
return body;
|
|
}
|
|
|
|
function debugSettingsBodyFromForm(form) {
|
|
return {
|
|
overlay_enabled: form.debug_overlay_enabled.checked,
|
|
gree_frames: form.debug_gree_frames.checked,
|
|
cloud_requests: form.debug_cloud_requests.checked,
|
|
cloud_mqtt: form.debug_cloud_mqtt.checked,
|
|
};
|
|
}
|
|
|
|
function nightSettingsBodyFromForm(form) {
|
|
const raw = Object.fromEntries(new FormData(form));
|
|
return {
|
|
enabled: form.night_mode_enabled.checked,
|
|
start_time: raw.night_mode_start,
|
|
end_time: raw.night_mode_end,
|
|
max_fan_speed: Number(raw.night_mode_max_fan_speed),
|
|
force_quiet: form.night_mode_force_quiet.checked,
|
|
use_native_sleep: form.night_mode_native_sleep.checked,
|
|
};
|
|
}
|
|
|
|
function homeAssistantSettingsBodyFromForm(form) {
|
|
const raw = Object.fromEntries(new FormData(form));
|
|
const body = {
|
|
url: raw.ha_url,
|
|
outdoor_entity_id: raw.ha_outdoor_entity_id,
|
|
sensor_stale_after_seconds: Math.max(60, Math.min(86400, Math.round(Number(raw.ha_sensor_stale_after_minutes || 5) * 60))),
|
|
allow_invalid_tls: form.ha_allow_invalid_tls.checked,
|
|
manual_auth_override: form.dataset.haManualOverride === 'true',
|
|
sensor_aliases: { ...(app.sensorAliases || {}) },
|
|
flow_inputs: JSON.parse(JSON.stringify(app.flowSharedInputs || [])),
|
|
outdoor_assist_enabled: form.outdoor_assist_enabled.checked,
|
|
};
|
|
if (raw.ha_token) body.token = raw.ha_token;
|
|
return body;
|
|
}
|
|
|
|
function refreshSettingsUi() {
|
|
renderSettings();
|
|
renderNightSettings();
|
|
renderHomeAssistantSettings();
|
|
renderHouseClimate();
|
|
renderSimulationModeBanner();
|
|
renderSystemInfo();
|
|
renderDebugOverlay();
|
|
if (app.flowDraft) renderFlowEditor();
|
|
scheduleControlPlanLoad();
|
|
if (app.settings?.debug?.overlay_enabled) loadDebugBacklog();
|
|
}
|
|
|
|
async function saveMainSettings(form, notify = true) {
|
|
const [application, gree, greeCloud, history, influxdb, notifications, debug] = await Promise.all([
|
|
api(SETTINGS_ENDPOINTS.application, { method: 'PUT', body: applicationSettingsBodyFromForm(form) }),
|
|
api(SETTINGS_ENDPOINTS.gree, { method: 'PUT', body: greeSettingsBodyFromForm(form) }),
|
|
api(SETTINGS_ENDPOINTS.greeCloud, { method: 'PUT', body: greeCloudSettingsBodyFromForm(form) }),
|
|
api(SETTINGS_ENDPOINTS.history, { method: 'PUT', body: historySettingsBodyFromForm(form) }),
|
|
api(SETTINGS_ENDPOINTS.influxdb, { method: 'PUT', body: influxDbSettingsBodyFromForm(form) }),
|
|
api(SETTINGS_ENDPOINTS.notifications, { method: 'PUT', body: notificationSettingsBodyFromForm(form) }),
|
|
api(SETTINGS_ENDPOINTS.debug, { method: 'PUT', body: debugSettingsBodyFromForm(form) }),
|
|
]);
|
|
applySettingsSection('application', application);
|
|
applySettingsSection('gree', gree);
|
|
applySettingsSection('greeCloud', greeCloud);
|
|
applySettingsSection('history', history);
|
|
applySettingsSection('influxdb', influxdb);
|
|
applySettingsSection('notifications', notifications);
|
|
applySettingsSection('debug', debug);
|
|
refreshSettingsUi();
|
|
if (notify) toast(tr('common.saved'));
|
|
}
|
|
|
|
async function saveNightSettings(form, notify = true) {
|
|
const data = await api(SETTINGS_ENDPOINTS.night, { method: 'PUT', body: nightSettingsBodyFromForm(form) });
|
|
applySettingsSection('night', data);
|
|
refreshSettingsUi();
|
|
if (notify) toast(tr('common.saved'));
|
|
}
|
|
|
|
async function saveHomeAssistantSettings(form, notify = true) {
|
|
const data = await api(SETTINGS_ENDPOINTS.homeAssistant, { method: 'PUT', body: homeAssistantSettingsBodyFromForm(form) });
|
|
applySettingsSection('homeAssistant', data);
|
|
app.sensorAliases = { ...(data.sensor_aliases || {}) };
|
|
app.flowSharedInputs = JSON.parse(JSON.stringify(data.flow_inputs || []));
|
|
app.haEntityCatalogLoadedAt = 0;
|
|
refreshSettingsUi();
|
|
if (notify) toast(tr('common.saved'));
|
|
}
|
|
|
|
$('#settingsForm').addEventListener('submit', async event => {
|
|
event.preventDefault(); const form = event.currentTarget;
|
|
await runFormTask(form, () => saveMainSettings(form, true));
|
|
});
|
|
|
|
$('#nightModeForm')?.addEventListener('submit', async event => {
|
|
event.preventDefault(); const form = event.currentTarget;
|
|
await runFormTask(form, () => saveNightSettings(form, true));
|
|
});
|
|
|
|
$('#homeAssistantForm')?.addEventListener('submit', async event => {
|
|
event.preventDefault(); const form = event.currentTarget;
|
|
await runFormTask(form, () => saveHomeAssistantSettings(form, true));
|
|
});
|
|
|
|
$('#addSensorAlias')?.addEventListener('click', () => {
|
|
const entityInput = $('#sensorAliasEntity'), aliasInput = $('#sensorAliasName');
|
|
const entity = entityInput.value.trim(), alias = aliasInput.value.trim();
|
|
if (!entity || !alias) return toast(tr('settings.aliasRequired'), true);
|
|
app.sensorAliases[entity] = alias;
|
|
entityInput.value = ''; aliasInput.value = '';
|
|
renderSensorAliases(); renderHistoryNavigation();
|
|
updateDirtyIndicator($('#homeAssistantForm'));
|
|
});
|
|
|
|
document.addEventListener('input', event => {
|
|
const input = event.target.closest('[data-sensor-alias]'); if (!input) return;
|
|
const entity = input.dataset.sensorAlias, alias = input.value.trim();
|
|
if (alias) app.sensorAliases[entity] = alias; else delete app.sensorAliases[entity];
|
|
updateDirtyIndicator($('#homeAssistantForm'));
|
|
});
|
|
|
|
document.addEventListener('click', event => {
|
|
const button = event.target.closest('[data-clear-sensor-alias]'); if (!button) return;
|
|
delete app.sensorAliases[button.dataset.clearSensorAlias];
|
|
renderSensorAliases(); renderHistoryNavigation();
|
|
updateDirtyIndicator($('#homeAssistantForm'));
|
|
});
|
|
|
|
$('#saveLogRetention')?.addEventListener('click', async () => {
|
|
const days = Number($('#logRetentionDays')?.value || 30);
|
|
try {
|
|
const form = $('#settingsForm');
|
|
const result = await api(SETTINGS_ENDPOINTS.history, { method: 'PUT', body: historySettingsBodyFromForm(form, days) });
|
|
applySettingsSection('history', result);
|
|
form.event_log_retention_days.value = result.event_retention_days;
|
|
renderLogRetention(); await loadLogs(); toast(tr('logs.retentionSaved', { days: result.event_retention_days }));
|
|
} catch (error) { toast(error.message, true); }
|
|
});
|
|
|
|
$('#createAccessToken').addEventListener('click', async event => {
|
|
const button = event.currentTarget;
|
|
button.disabled = true;
|
|
try {
|
|
const result = await api('/api/access-tokens', { method: 'POST', body: { name: 'Home Assistant' } });
|
|
if (result.item) app.accessTokens.unshift(result.item);
|
|
renderAccessTokens();
|
|
$('#generatedAccessToken').value = result.token || '';
|
|
openDialog('generatedTokenDialog');
|
|
toast(tr('toast.tokenCreated'));
|
|
} catch (error) { toast(error.message, true); }
|
|
finally { button.disabled = false; }
|
|
});
|
|
|
|
$('#copyAccessToken').addEventListener('click', async () => {
|
|
const input = $('#generatedAccessToken');
|
|
try {
|
|
await navigator.clipboard.writeText(input.value);
|
|
toast(tr('toast.tokenCopied'));
|
|
} catch (_) {
|
|
input.select();
|
|
document.execCommand('copy');
|
|
toast(tr('toast.tokenCopied'));
|
|
}
|
|
});
|
|
|
|
function homeAssistantTestToast(result) {
|
|
const sample = result?.sample;
|
|
if (!sample?.entity_id) return tr('toast.haConnected');
|
|
const value = `${sample.state ?? '—'}${sample.unit ? ` ${sample.unit}` : ''}`;
|
|
const label = sample.name ? `${sample.name} (${sample.entity_id})` : sample.entity_id;
|
|
return tr('toast.haConnectedSample', { entity: label, value });
|
|
}
|
|
|
|
$('#haTest').addEventListener('click', async event => {
|
|
const form = $('#homeAssistantForm'), button = event.currentTarget;
|
|
clearFormErrors(form);
|
|
const ha = app.settings?.home_assistant || {};
|
|
const supervisorAuto = ha.supervisor_detected === true && form.dataset.haManualOverride !== 'true';
|
|
if (!supervisorAuto && !validateForm(form)) return;
|
|
button.disabled = true;
|
|
const idle = button.textContent;
|
|
button.textContent = tr('settings.testingHa');
|
|
try {
|
|
if (!supervisorAuto) await saveHomeAssistantSettings(form, false);
|
|
const result = await api('/api/integrations/home-assistant/test', { method: 'POST' });
|
|
app.haSupervisorTestState = { ok: true, sample: result.sample || null };
|
|
if (app.settings?.home_assistant?.supervisor_detected && app.settings.home_assistant.auth_mode === 'supervisor') {
|
|
app.haManualFallbackVisible = false;
|
|
}
|
|
app.haEntityCatalogLoadedAt = 0;
|
|
renderHomeAssistantAuthState();
|
|
markFormClean(form);
|
|
toast(homeAssistantTestToast(result));
|
|
} catch (error) {
|
|
if (supervisorAuto) {
|
|
app.haSupervisorTestState = { ok: false, message: error.message };
|
|
app.haManualFallbackVisible = true;
|
|
renderHomeAssistantAuthState();
|
|
toast(`${tr('settings.haSupervisorTestFailedTitle')}: ${error.message}`, true);
|
|
} else {
|
|
presentFormError(form, error);
|
|
}
|
|
} finally { button.disabled = false; button.textContent = idle; }
|
|
});
|
|
|
|
$('#haUseSupervisor')?.addEventListener('click', async event => {
|
|
const form = $('#homeAssistantForm'), button = event.currentTarget;
|
|
const idle = button.textContent;
|
|
button.disabled = true;
|
|
clearFormErrors(form);
|
|
try {
|
|
app.haManualFallbackVisible = false;
|
|
form.dataset.haManualOverride = 'false';
|
|
await saveHomeAssistantSettings(form, false);
|
|
const result = await api('/api/integrations/home-assistant/test', { method: 'POST' });
|
|
app.haSupervisorTestState = { ok: true, sample: result.sample || null };
|
|
app.haEntityCatalogLoadedAt = 0;
|
|
renderHomeAssistantSettings();
|
|
toast(homeAssistantTestToast(result));
|
|
} catch (error) {
|
|
app.haManualFallbackVisible = true;
|
|
app.haSupervisorTestState = { ok: false, message: error.message };
|
|
renderHomeAssistantAuthState();
|
|
toast(`${tr('settings.haSupervisorTestFailedTitle')}: ${error.message}`, true);
|
|
} finally { button.disabled = false; button.textContent = idle; }
|
|
});
|
|
|
|
$('#exportSettings').addEventListener('click', async () => {
|
|
try {
|
|
const data = await api('/api/configuration/export');
|
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = `gree-controller-configuration-${new Date().toISOString().slice(0, 10)}.json`;
|
|
document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(link.href);
|
|
toast(tr('toast.exported'));
|
|
} catch (error) { toast(error.message, true); }
|
|
});
|
|
|
|
$('#importSettings').addEventListener('click', () => $('#importSettingsFile').click());
|
|
$('#importSettingsFile').addEventListener('change', async event => {
|
|
const file = event.target.files?.[0]; if (!file) return;
|
|
try {
|
|
if (!confirm(tr('settings.importConfirm'))) return;
|
|
const body = JSON.parse(await file.text());
|
|
await api('/api/configuration/import', { method: 'POST', body });
|
|
app.debugBacklogLoaded = false;
|
|
await loadBootstrap();
|
|
toast(tr('toast.imported'));
|
|
} catch (error) { toast(error.message, true); }
|
|
finally { event.target.value = ''; }
|
|
});
|
|
|
|
document.addEventListener('change', event => {
|
|
const target = event.target;
|
|
if (target.id === 'historyZoneSelect') { app.historyZone = target.value; updateBrowserUrl(currentHistoryPath()); renderHistoryPage(); }
|
|
else if (target.id === 'historyDeviceSelect') { app.historyDevice = target.value; updateBrowserUrl(currentHistoryPath()); renderHistoryPage(); }
|
|
else if (target.matches?.('[data-history-energy-target]')) {
|
|
const checked = $$('[data-history-energy-target]:checked').map(input => input.dataset.historyEnergyTarget).filter(Boolean);
|
|
if (checked.length > 8) { target.checked = false; toast(tr('energy.maxTargets'), true); return; }
|
|
app.historyEnergyTargets = checked;
|
|
app.historyEnergyDevice = app.historyEnergyTargets[0] || '';
|
|
updateBrowserUrl(currentHistoryPath());
|
|
loadHistory();
|
|
}
|
|
else if (target.id === 'historyEnergyInterval') { app.historyEnergyInterval = target.value; updateBrowserUrl(currentHistoryPath()); loadHistory(); }
|
|
else if (target.id === 'historyEnergyCompare') { app.historyEnergyCompare = target.value; updateBrowserUrl(currentHistoryPath()); loadHistory(); }
|
|
else if (target.id === 'historyNetworkSelect') { app.historyNetworkTarget = target.value; updateBrowserUrl(currentHistoryPath()); loadHistory(); }
|
|
else if (target.id === 'historySensorSelect') { app.historySensor = target.value; updateBrowserUrl(currentHistoryPath()); renderHistoryPage(); }
|
|
else if (target.name === 'influx_version') updateInfluxFields();
|
|
});
|
|
|
|
|
|
|
|
$('#addFlowSharedInput')?.addEventListener('click', () => openFlowSharedInputEditor());
|
|
$('#flowSharedInputKind')?.addEventListener('change', event => {
|
|
renderFlowSharedInputFields(event.target.value, flowSharedInputDefaultConfig(event.target.value));
|
|
});
|
|
function evaluateFlowSharedHaTest(kind, config, result) {
|
|
if (kind === 'ha_available') return { actual: result.available === true, valid: true };
|
|
if (kind === 'ha_attribute') {
|
|
const actual = result.attributes?.[config.attribute];
|
|
return { actual, valid: actual !== undefined };
|
|
}
|
|
if (kind === 'ha_numeric') {
|
|
const actual = Number(result.state);
|
|
return { actual, valid: Number.isFinite(actual) };
|
|
}
|
|
return { actual: result.state, valid: true };
|
|
}
|
|
|
|
$('#flowSharedInputTest')?.addEventListener('click', async event => {
|
|
const form = $('#flowSharedInputForm'), resultHost = $('#flowSharedInputTestResult');
|
|
if (!form || !resultHost) return;
|
|
const kind = form.kind.value, config = collectFlowSharedInputConfig(kind);
|
|
const entityId = String(config.entity_id || '').trim();
|
|
if (!entityId) {
|
|
resultHost.hidden = false;
|
|
resultHost.classList.remove('pass');
|
|
resultHost.classList.add('fail');
|
|
resultHost.innerHTML = `<strong>${esc(tr('flow.sharedInputTestUnavailable'))}</strong><span>${esc(tr('flow.sharedInputTestEntityRequired'))}</span>`;
|
|
return;
|
|
}
|
|
const button = event.currentTarget, idle = button.textContent;
|
|
button.disabled = true; button.textContent = tr('flow.sharedInputTesting');
|
|
resultHost.hidden = false; resultHost.innerHTML = `<span>${esc(tr('flow.sharedInputTesting'))}</span>`;
|
|
try {
|
|
const result = await api('/api/integrations/home-assistant/entity', { method: 'POST', body: { entity_id: entityId } });
|
|
const evaluation = evaluateFlowSharedHaTest(kind, config, result);
|
|
const actual = evaluation.actual == null ? 'null' : (typeof evaluation.actual === 'object' ? JSON.stringify(evaluation.actual) : String(evaluation.actual));
|
|
const success = evaluation.valid;
|
|
resultHost.classList.toggle('pass', success);
|
|
resultHost.classList.toggle('fail', !success);
|
|
resultHost.innerHTML = `<div><strong>${esc(success ? tr('flow.sharedInputTestValueRead') : tr('flow.sharedInputTestUnavailable'))}</strong><span class="badge ${success ? 'active' : ''}">${esc(result.available ? tr('flow.sharedInputTestAvailable') : tr('flow.sharedInputTestUnavailable'))}</span></div><dl><div><dt>${esc(tr('flow.sharedInputTestCurrent'))}</dt><dd><code>${esc(actual)}</code></dd></div></dl><small>${esc(result.entity_id)}${result.last_updated ? ` · ${esc(dateTime(result.last_updated))}` : ''}</small>`;
|
|
} catch (error) {
|
|
resultHost.classList.remove('pass'); resultHost.classList.add('fail');
|
|
resultHost.innerHTML = `<strong>${esc(tr('flow.sharedInputTestUnavailable'))}</strong><span>${esc(error.message)}</span>`;
|
|
} finally { button.disabled = false; button.textContent = idle; }
|
|
});
|
|
$('#flowSharedInputForm')?.addEventListener('submit', event => {
|
|
event.preventDefault();
|
|
const form = event.currentTarget;
|
|
const name = form.name.value.trim(), kind = form.kind.value;
|
|
if (!name) return toast(tr('flow.sharedInputNameRequired'), true);
|
|
const editingId = form.dataset.editingId || '';
|
|
const id = editingId || newFlowId('shared');
|
|
const item = { id, name, kind, config: collectFlowSharedInputConfig(kind) };
|
|
const index = app.flowSharedInputs.findIndex(value => value.id === id);
|
|
if (index >= 0) app.flowSharedInputs[index] = item; else app.flowSharedInputs.push(item);
|
|
renderFlowSharedInputs();
|
|
updateDirtyIndicator($('#homeAssistantForm'));
|
|
$('#flowSharedInputDialog')?.close();
|
|
});
|
|
|
|
document.addEventListener('click', event => {
|
|
const edit = event.target.closest?.('[data-flow-shared-edit]');
|
|
if (edit) { openFlowSharedInputEditor(edit.dataset.flowSharedEdit); return; }
|
|
const openFlow = event.target.closest?.('[data-open-shared-flow]');
|
|
if (openFlow) {
|
|
if (showView('flows')) requestAnimationFrame(() => openFlowEditor(openFlow.dataset.openSharedFlow));
|
|
return;
|
|
}
|
|
const remove = event.target.closest?.('[data-flow-shared-delete]');
|
|
if (!remove) return;
|
|
const id = remove.dataset.flowSharedDelete;
|
|
const item = app.flowSharedInputs.find(value => value.id === id); if (!item) return;
|
|
const uses = app.flows.reduce((count, flow) => count + (flow.nodes || []).filter(node => node.kind === 'shared_input' && node.config?.input_id === id).length, 0);
|
|
const message = uses ? tr('flow.sharedInputDeleteUsed', { name: item.name, count: uses }) : tr('flow.sharedInputDeleteConfirm', { name: item.name });
|
|
if (!confirm(message)) return;
|
|
app.flowSharedInputs = app.flowSharedInputs.filter(value => value.id !== id);
|
|
renderFlowSharedInputs();
|
|
updateDirtyIndicator($('#homeAssistantForm'));
|
|
});
|
|
|
|
async function saveGreeCloudSettings(form = $('#settingsForm')) {
|
|
const data = await api(SETTINGS_ENDPOINTS.greeCloud, { method: 'PUT', body: greeCloudSettingsBodyFromForm(form) });
|
|
applySettingsSection('greeCloud', data);
|
|
return data;
|
|
}
|
|
|
|
function renderCloudDiscoveryDevices(devices) {
|
|
const list = $('#cloudDiscoveryList');
|
|
if (!list) return;
|
|
const items = Array.isArray(devices) ? devices : [];
|
|
list.innerHTML = items.length ? items.map(device => `
|
|
<div class="discovery-name-row cloud-discovery-row">
|
|
<span><strong>${esc(device.name || 'GREE')}</strong><small>${esc(device.model || 'GREE')} · ${esc(device.mac || device.id)} · ${esc(tr(device.online ? 'status.online' : 'status.offline'))}</small></span>
|
|
<button type="button" class="${device.already_added ? 'secondary' : 'primary'}" data-action="add-cloud-device" data-cloud-id="${esc(device.id)}" ${device.already_added ? 'disabled' : ''}>${esc(device.already_added ? tr('devices.cloudAlreadyAdded') : tr('actions.add'))}</button>
|
|
</div>`).join('') : `<div class="empty"><strong>${esc(tr('devices.cloudDiscoveryEmpty'))}</strong>${esc(tr('devices.cloudDiscoveryEmptyHint'))}</div>`;
|
|
}
|
|
|
|
async function loadCloudDiscovery({ open = true } = {}) {
|
|
const result = await api('/api/integrations/gree-cloud/devices');
|
|
renderCloudDiscoveryDevices(result.devices || []);
|
|
if (open) openDialog('cloudDiscoveryDialog');
|
|
return result;
|
|
}
|
|
|
|
$('#greeCloudTestButton')?.addEventListener('click', async event => {
|
|
const button = event.currentTarget;
|
|
const form = $('#settingsForm');
|
|
const resultBox = $('#greeCloudTestResult');
|
|
button.disabled = true;
|
|
if (resultBox) { resultBox.hidden = true; resultBox.classList.remove('success', 'error'); }
|
|
try {
|
|
await saveGreeCloudSettings(form);
|
|
const result = await api('/api/integrations/gree-cloud/test', { method: 'POST' });
|
|
if (resultBox) {
|
|
resultBox.hidden = false;
|
|
resultBox.classList.add(result.ok ? 'success' : 'error');
|
|
const message = result.ok ? `Connected. ${Number(result.device_count || 0)} device(s) found.` : (result.message || result.status || 'Connection failed');
|
|
resultBox.innerHTML = `<span>${esc(result.ok ? tr('status.online') : tr('devices.lastError'))}</span><strong>${esc(message)}</strong>`;
|
|
}
|
|
if (result.ok) {
|
|
const refreshed = await api(SETTINGS_ENDPOINTS.greeCloud);
|
|
applySettingsSection('greeCloud', refreshed);
|
|
renderSettings();
|
|
}
|
|
} catch (error) {
|
|
if (resultBox) { resultBox.hidden = false; resultBox.classList.add('error'); resultBox.innerHTML = `<span>${esc(tr('devices.lastError'))}</span><strong>${esc(error.message)}</strong>`; }
|
|
} finally { button.disabled = false; }
|
|
});
|
|
|
|
$('#greeCloudRefreshButton')?.addEventListener('click', async event => {
|
|
const button = event.currentTarget;
|
|
button.disabled = true;
|
|
try { await saveGreeCloudSettings($('#settingsForm')); await loadCloudDiscovery(); }
|
|
catch (error) { toast(error.message, true); }
|
|
finally { button.disabled = false; }
|
|
});
|
|
|
|
$('#cloudDiscoveryRefresh')?.addEventListener('click', async event => {
|
|
const button = event.currentTarget;
|
|
button.disabled = true;
|
|
try { await loadCloudDiscovery({ open: false }); }
|
|
catch (error) { toast(error.message, true); }
|
|
finally { button.disabled = false; }
|
|
});
|