Files
gree-controller/web/js/settings.js
T
2026-09-01 13:05:44 +02:00

294 lines
14 KiB
JavaScript

function currentSettingsBody() {
const settings = app.settings || {};
const influx = settings.influxdb || {};
const ha = settings.home_assistant || {};
return {
controller_id: settings.controller_id || 'gree-controller',
simulator_enabled: !!settings.simulator_enabled,
poll_interval_seconds: Number(settings.poll_interval_seconds || 15),
zone_interval_seconds: Number(settings.zone_interval_seconds || 5),
discovery_timeout_ms: Number(settings.discovery_timeout_ms || 3000),
discovery_broadcast: settings.discovery_broadcast || '255.255.255.255:7000',
house_mode: settings.house_mode || 'cool',
house_power_enabled: settings.house_power_enabled !== false,
control_strategy: 'setpoint',
outdoor_assist_enabled: !!settings.outdoor_assist_enabled,
history_retention_days: Number(settings.history_retention_days || 30),
history_compaction_enabled: settings.history_compaction_enabled !== false,
event_log_retention_days: Number(settings.event_log_retention_days || 30),
suppress_device_beep: !!settings.suppress_device_beep,
compressor_protection_enabled: settings.compressor_protection_enabled !== false,
compressor_protection_seconds: Number(settings.compressor_protection_seconds || 180),
notifications: {
enabled: !!settings.notifications?.enabled, mode: settings.notifications?.mode || 'problems', provider: settings.notifications?.provider || 'pushover',
pushover_app_token: '', pushover_user_key: '', slack_webhook_url: '', discord_webhook_url: '',
cooldown_seconds: Number(settings.notifications?.cooldown_seconds || 300), communication_failure_threshold: Number(settings.notifications?.communication_failure_threshold || 3), target_timeout_minutes: Number(settings.notifications?.target_timeout_minutes || 60),
alert_types: {
stale_sensor: settings.notifications?.alert_types?.stale_sensor !== false,
sensor_errors: settings.notifications?.alert_types?.sensor_errors !== false,
communication: settings.notifications?.alert_types?.communication !== false,
target_timeout: settings.notifications?.alert_types?.target_timeout !== false,
automation: settings.notifications?.alert_types?.automation !== false,
control_errors: settings.notifications?.alert_types?.control_errors !== false,
important_events: settings.notifications?.alert_types?.important_events !== false,
other: settings.notifications?.alert_types?.other !== false,
},
},
night_mode: {
enabled: !!settings.night_mode?.enabled,
start_time: settings.night_mode?.start_time || '22:00',
end_time: settings.night_mode?.end_time || '06:00',
max_fan_speed: Number(settings.night_mode?.max_fan_speed || 1),
force_quiet: settings.night_mode?.force_quiet !== false,
use_native_sleep: settings.night_mode?.use_native_sleep !== false,
},
influxdb: {
enabled: !!influx.enabled,
version: String(influx.version || '2'),
url: influx.url || '',
database: influx.database || 'gree_controller',
username: influx.username || '',
password: '',
org: influx.org || '',
bucket: influx.bucket || 'gree_controller',
token: '',
history_threshold_days: Number(influx.history_threshold_days || 30),
},
debug: {
overlay_enabled: !!settings.debug?.overlay_enabled,
gree_frames: !!settings.debug?.gree_frames,
},
home_assistant: {
url: ha.url || '',
token: '',
default_entity_id: ha.default_entity_id || '',
outdoor_entity_id: ha.outdoor_entity_id || '',
sensor_stale_after_seconds: Number(ha.sensor_stale_after_seconds || 300),
allow_invalid_tls: !!ha.allow_invalid_tls,
sensor_aliases: {...(ha.sensor_aliases || {})},
},
};
}
function settingsBodyFromForm(form) {
const raw = Object.fromEntries(new FormData(form));
const body = currentSettingsBody();
body.controller_id = raw.controller_id;
body.simulator_enabled = form.simulator_enabled.checked;
body.poll_interval_seconds = Number(raw.poll_interval_seconds);
body.zone_interval_seconds = Number(raw.zone_interval_seconds);
body.discovery_timeout_ms = Number(raw.discovery_timeout_ms);
body.discovery_broadcast = raw.discovery_broadcast;
body.history_retention_days = Number(raw.history_retention_days);
body.history_compaction_enabled = form.history_compaction_enabled.checked;
body.event_log_retention_days = Number(raw.event_log_retention_days);
body.suppress_device_beep = form.suppress_device_beep.checked;
body.compressor_protection_enabled = form.compressor_protection_enabled.checked;
body.compressor_protection_seconds = Math.max(30, Math.min(1800, Math.round(Number(raw.compressor_protection_minutes || 3) * 60)));
body.influxdb = {
enabled: form.influx_enabled.checked, version: raw.influx_version, url: raw.influx_url,
database: raw.influx_database, username: raw.influx_username, password: raw.influx_password,
org: raw.influx_org, bucket: raw.influx_bucket, token: raw.influx_token,
history_threshold_days: Number(raw.influx_threshold_days),
};
body.debug = {overlay_enabled: form.debug_overlay_enabled.checked, gree_frames: form.debug_gree_frames.checked};
body.notifications = {
enabled: form.notifications_enabled.checked, mode: raw.notifications_mode, provider: raw.notifications_provider,
pushover_app_token: raw.pushover_app_token || '', pushover_user_key: raw.pushover_user_key || '',
slack_webhook_url: raw.slack_webhook_url || '', discord_webhook_url: raw.discord_webhook_url || '',
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,
control_errors: form.notification_alert_control_errors.checked,
important_events: form.notification_alert_important_events.checked,
other: form.notification_alert_other.checked,
},
};
return body;
}
function nightSettingsBodyFromForm(form) {
const raw = Object.fromEntries(new FormData(form));
const body = currentSettingsBody();
body.night_mode = {
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,
};
return body;
}
function homeAssistantSettingsBodyFromForm(form) {
const raw = Object.fromEntries(new FormData(form));
const body = currentSettingsBody();
body.outdoor_assist_enabled = form.outdoor_assist_enabled.checked;
body.home_assistant = {
url: raw.ha_url,
token: raw.ha_token,
default_entity_id: raw.ha_entity_id,
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,
sensor_aliases: {...(app.sensorAliases || {})},
};
return body;
}
async function saveRuntimeSettings(body, notify = true) {
app.settings = await api('/api/settings', {method:'PUT', body});
app.sensorAliases = {...(app.settings?.home_assistant?.sensor_aliases || {})};
renderSettings();
renderNightSettings();
renderHomeAssistantSettings();
renderHouseClimate();
renderSimulationModeBanner();
renderSystemInfo();
renderDebugOverlay();
scheduleControlPlanLoad();
if (app.settings?.debug?.overlay_enabled) loadDebugBacklog();
if (notify) toast(tr('common.saved'));
return app.settings;
}
$('#settingsForm').addEventListener('submit', async event => {
event.preventDefault(); const form = event.currentTarget;
await runFormTask(form, () => saveRuntimeSettings(settingsBodyFromForm(form), true));
});
$('#nightModeForm')?.addEventListener('submit', async event => {
event.preventDefault(); const form = event.currentTarget;
await runFormTask(form, () => saveRuntimeSettings(nightSettingsBodyFromForm(form), true));
});
$('#homeAssistantForm')?.addEventListener('submit', async event => {
event.preventDefault(); const form = event.currentTarget;
await runFormTask(form, () => saveRuntimeSettings(homeAssistantSettingsBodyFromForm(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 result = await api('/api/events/retention', {method:'PUT', body:{days}});
app.settings.event_log_retention_days = result.days;
$('#settingsForm').event_log_retention_days.value = result.days;
renderLogRetention(); await loadLogs(); toast(tr('logs.retentionSaved', {days: result.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'));
}
});
$('#haTest').addEventListener('click', async event => {
const form=$('#homeAssistantForm'), button=event.currentTarget;
if (!validateForm(form)) return;
button.disabled = true;
const idle = button.textContent;
button.textContent = tr('settings.testingHa');
try {
await saveRuntimeSettings(homeAssistantSettingsBodyFromForm(form), false);
const entity = form.ha_entity_id.value || form.ha_outdoor_entity_id.value || null;
const result=await api('/api/integrations/home-assistant/test',{method:'POST',body:{entity_id:entity}});
markFormClean(form);
toast(tr('toast.haTemperature',{temperature:result.temperature_c.toFixed(1)}));
} catch(error){presentFormError(form,error);}
finally { button.disabled=false; button.textContent=idle; }
});
$('#simulationRefresh')?.addEventListener('click', async () => {
try { await loadControlPlan(); toast(tr('common.updated')); }
catch (error) { toast(error.message, true); }
});
$('#exportSettings').addEventListener('click', async () => {
try {
const data = await api('/api/settings/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-settings-${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/settings/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.id==='historySensorSelect'){app.historySensor=target.value;updateBrowserUrl(currentHistoryPath());renderHistoryPage();}
else if(target.name==='influx_version') updateInfluxFields();
});