294 lines
14 KiB
JavaScript
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();
|
|
});
|
|
|
|
|