v0.12.0
This commit is contained in:
Vendored
+4
-4
@@ -2,7 +2,7 @@ async function loadBootstrap(showMessage = false) {
|
||||
if (app.loading) return;
|
||||
app.loading = true;
|
||||
try {
|
||||
const data = await api('/api/bootstrap');
|
||||
const [data, sections] = await Promise.all([api('/api/bootstrap'), fetchSettingsSections()]);
|
||||
app.devices = data.devices || [];
|
||||
app.zones = data.zones || [];
|
||||
app.groups = data.groups || [];
|
||||
@@ -10,9 +10,9 @@ async function loadBootstrap(showMessage = false) {
|
||||
app.automations = data.automations || [];
|
||||
app.flows = data.flows || [];
|
||||
app.accessTokens = data.access_tokens || [];
|
||||
app.settings = data.settings || null;
|
||||
app.sensorAliases = { ...(app.settings?.home_assistant?.sensor_aliases || {}) };
|
||||
app.flowSharedInputs = JSON.parse(JSON.stringify(app.settings?.home_assistant?.flow_inputs || []));
|
||||
app.settings = aggregateSettingsSections(sections, data.house?.mode || 'cool');
|
||||
app.sensorAliases = { ...(app.settings.home_assistant?.sensor_aliases || {}) };
|
||||
app.flowSharedInputs = JSON.parse(JSON.stringify(app.settings.home_assistant?.flow_inputs || []));
|
||||
app.system = data.system || {};
|
||||
app.systemSnapshotAt = Date.now();
|
||||
app.outdoorTemperature = Number.isFinite(Number(data.outdoor_temperature)) ? Number(data.outdoor_temperature) : null;
|
||||
|
||||
@@ -469,7 +469,6 @@ async function sendGroupControl(id, patch) {
|
||||
if (index >= 0) app.zones[index] = zone; else app.zones.push(zone);
|
||||
});
|
||||
(result.devices || []).forEach(updateDevice);
|
||||
if (typeof result.master_power_enabled === 'boolean' && app.settings) app.settings.house_power_enabled = result.master_power_enabled;
|
||||
if (patch.preset === 'custom' && patch.setpoint != null) delete app.groupCustomDrafts[id];
|
||||
renderAll(); scheduleControlPlanLoad();
|
||||
const failed = Array.isArray(result.failed) ? result.failed.length : 0;
|
||||
|
||||
+48
-41
@@ -10,6 +10,52 @@ async function loadLogs() {
|
||||
} 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; app.devices = data.devices || []; app.zones = data.zones || []; app.groups = data.groups || []; app.schedules = data.schedules || []; app.automations = data.automations || [];
|
||||
app.flows = data.flows || []; await loadSettingsSections(data.house?.mode || app.settings?.house_mode || 'cool'); app.system = data.system || app.system; app.systemSnapshotAt = Date.now(); app.outdoorTemperature = Number.isFinite(Number(data.outdoor_temperature)) ? Number(data.outdoor_temperature) : app.outdoorTemperature; renderAll(); scheduleControlPlanLoad(); if (app.settings?.debug?.overlay_enabled) loadDebugBacklog(); return;
|
||||
}
|
||||
const data = message.data || {};
|
||||
if (['device.updated', 'device.created'].includes(message.event)) { updateDevice(data); renderSummary(); renderDevices(); renderGroups(); 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.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 (['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.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 === '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 === '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.startsWith('schedule.') || message.event.startsWith('automation.') || message.event.startsWith('flow.')) scheduleControlPlanLoad();
|
||||
} catch (_) { }
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
if (app.ws && [WebSocket.OPEN, WebSocket.CONNECTING].includes(app.ws.readyState)) return;
|
||||
clearTimeout(app.wsTimer);
|
||||
@@ -19,47 +65,8 @@ function connectWebSocket() {
|
||||
ws.onopen = () => updateConnectionIndicator('connected');
|
||||
ws.onclose = () => { updateConnectionIndicator('disconnected'); app.wsTimer = setTimeout(connectWebSocket, 3000); };
|
||||
ws.onerror = () => updateConnectionIndicator('connectionError');
|
||||
let messageQueue = Promise.resolve();
|
||||
ws.onmessage = event => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.event === 'bootstrap') {
|
||||
const data = message.data; app.devices = data.devices || []; app.zones = data.zones || []; app.groups = data.groups || []; app.schedules = data.schedules || []; app.automations = data.automations || [];
|
||||
app.flows = data.flows || []; app.settings = data.settings || app.settings; app.sensorAliases = { ...(app.settings?.home_assistant?.sensor_aliases || {}) }; app.flowSharedInputs = JSON.parse(JSON.stringify(app.settings?.home_assistant?.flow_inputs || [])); app.system = data.system || app.system; app.systemSnapshotAt = Date.now(); app.outdoorTemperature = Number.isFinite(Number(data.outdoor_temperature)) ? Number(data.outdoor_temperature) : app.outdoorTemperature; renderAll(); scheduleControlPlanLoad(); if (app.settings?.debug?.overlay_enabled) loadDebugBacklog(); return;
|
||||
}
|
||||
const data = message.data || {};
|
||||
if (['device.updated', 'device.created'].includes(message.event)) { updateDevice(data); renderSummary(); renderDevices(); renderGroups(); 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.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 (['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.updated') {
|
||||
const settingsDirty = isFormDirty($('#settingsForm')), nightDirty = isFormDirty($('#nightModeForm')), haDirty = isFormDirty($('#homeAssistantForm'));
|
||||
app.settings = data;
|
||||
if (!haDirty) { app.sensorAliases = { ...(app.settings?.home_assistant?.sensor_aliases || {}) }; app.flowSharedInputs = JSON.parse(JSON.stringify(app.settings?.home_assistant?.flow_inputs || [])); }
|
||||
if (!settingsDirty) renderSettings();
|
||||
if (!nightDirty) renderNightSettings();
|
||||
if (!haDirty) renderHomeAssistantSettings();
|
||||
renderHouseClimate(); renderSimulationModeBanner(); renderSystemInfo(); renderDebugOverlay(); if (app.settings?.debug?.overlay_enabled) loadDebugBacklog(); scheduleControlPlanLoad();
|
||||
}
|
||||
else if (message.event === 'house.power_changed') { renderHouseClimate(); renderGroups(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'debug.settings') { app.settings = app.settings || {}; app.settings.debug = data; if (!isFormDirty($('#settingsForm'))) renderSettings(); renderDebugOverlay(); if (data.overlay_enabled) loadDebugBacklog(); }
|
||||
else if (message.event === 'outdoor.updated') { app.outdoorTemperature = Number.isFinite(Number(data.temperature)) ? Number(data.temperature) : null; renderHouseClimate(); scheduleControlPlanLoad(); }
|
||||
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 === '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.startsWith('schedule.') || message.event.startsWith('automation.') || message.event.startsWith('flow.')) scheduleControlPlanLoad();
|
||||
} catch (_) { }
|
||||
messageQueue = messageQueue.then(() => handleWebSocketMessage(event)).catch(() => {});
|
||||
};
|
||||
}
|
||||
|
||||
+175
-115
@@ -1,103 +1,127 @@
|
||||
const SETTINGS_ENDPOINTS = {
|
||||
application: '/api/settings/application',
|
||||
gree: '/api/settings/gree',
|
||||
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 currentSettingsBody() {
|
||||
const settings = app.settings || {};
|
||||
const influx = settings.influxdb || {};
|
||||
const ha = settings.home_assistant || {};
|
||||
async function fetchSettingsSections() {
|
||||
const [application, gree, history, influxdb, notifications, night, homeAssistant, debug] = await Promise.all([
|
||||
api(SETTINGS_ENDPOINTS.application),
|
||||
api(SETTINGS_ENDPOINTS.gree),
|
||||
api(SETTINGS_ENDPOINTS.history),
|
||||
api(SETTINGS_ENDPOINTS.influxdb),
|
||||
api(SETTINGS_ENDPOINTS.notifications),
|
||||
api(SETTINGS_ENDPOINTS.night),
|
||||
api(SETTINGS_ENDPOINTS.homeAssistant),
|
||||
api(SETTINGS_ENDPOINTS.debug),
|
||||
]);
|
||||
return { application, gree, history, influxdb, notifications, night, homeAssistant, debug };
|
||||
}
|
||||
|
||||
function aggregateSettingsSections(sections, houseMode = app.settings?.house_mode || 'cool') {
|
||||
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 || {}) },
|
||||
flow_inputs: JSON.parse(JSON.stringify(ha.flow_inputs || [])),
|
||||
},
|
||||
simulator_enabled: !!sections.application.simulator_enabled,
|
||||
controller_id: sections.gree.controller_id,
|
||||
poll_interval_seconds: Number(sections.gree.poll_interval_seconds),
|
||||
zone_interval_seconds: Number(sections.gree.zone_interval_seconds),
|
||||
discovery_timeout_ms: Number(sections.gree.discovery_timeout_ms),
|
||||
discovery_broadcast: sections.gree.discovery_broadcast,
|
||||
suppress_device_beep: !!sections.gree.suppress_device_beep,
|
||||
compressor_protection_enabled: sections.gree.compressor_protection_enabled !== false,
|
||||
compressor_protection_seconds: Number(sections.gree.compressor_protection_seconds),
|
||||
history_retention_days: Number(sections.history.retention_days),
|
||||
history_compaction_enabled: sections.history.compaction_enabled !== false,
|
||||
event_log_retention_days: Number(sections.history.event_retention_days),
|
||||
influxdb: sections.influxdb,
|
||||
notifications: sections.notifications,
|
||||
night_mode: sections.night,
|
||||
home_assistant: sections.homeAssistant,
|
||||
outdoor_assist_enabled: !!sections.homeAssistant.outdoor_assist_enabled,
|
||||
debug: sections.debug,
|
||||
house_mode: houseMode || 'cool',
|
||||
};
|
||||
}
|
||||
|
||||
function settingsBodyFromForm(form) {
|
||||
async function loadSettingsSections(houseMode = app.settings?.house_mode || 'cool') {
|
||||
const sections = await fetchSettingsSections();
|
||||
app.settings = aggregateSettingsSections(sections, houseMode);
|
||||
app.sensorAliases = { ...(app.settings.home_assistant?.sensor_aliases || {}) };
|
||||
app.flowSharedInputs = JSON.parse(JSON.stringify(app.settings.home_assistant?.flow_inputs || []));
|
||||
return app.settings;
|
||||
}
|
||||
|
||||
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 === '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));
|
||||
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,
|
||||
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,
|
||||
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 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),
|
||||
};
|
||||
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 || '',
|
||||
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),
|
||||
@@ -112,13 +136,20 @@ function settingsBodyFromForm(form) {
|
||||
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 };
|
||||
}
|
||||
|
||||
function nightSettingsBodyFromForm(form) {
|
||||
const raw = Object.fromEntries(new FormData(form));
|
||||
const body = currentSettingsBody();
|
||||
body.night_mode = {
|
||||
return {
|
||||
enabled: form.night_mode_enabled.checked,
|
||||
start_time: raw.night_mode_start,
|
||||
end_time: raw.night_mode_end,
|
||||
@@ -126,30 +157,25 @@ function nightSettingsBodyFromForm(form) {
|
||||
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 = {
|
||||
const body = {
|
||||
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 || {}) },
|
||||
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;
|
||||
}
|
||||
|
||||
async function saveRuntimeSettings(body, notify = true) {
|
||||
app.settings = await api('/api/settings', { method: 'PUT', body });
|
||||
app.sensorAliases = { ...(app.settings?.home_assistant?.sensor_aliases || {}) };
|
||||
app.flowSharedInputs = JSON.parse(JSON.stringify(app.settings?.home_assistant?.flow_inputs || []));
|
||||
function refreshSettingsUi() {
|
||||
renderSettings();
|
||||
renderNightSettings();
|
||||
renderHomeAssistantSettings();
|
||||
@@ -160,23 +186,56 @@ async function saveRuntimeSettings(body, notify = true) {
|
||||
if (app.flowDraft) renderFlowEditor();
|
||||
scheduleControlPlanLoad();
|
||||
if (app.settings?.debug?.overlay_enabled) loadDebugBacklog();
|
||||
}
|
||||
|
||||
async function saveMainSettings(form, notify = true) {
|
||||
const [application, gree, 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.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('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 || []));
|
||||
refreshSettingsUi();
|
||||
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));
|
||||
await runFormTask(form, () => saveMainSettings(form, true));
|
||||
});
|
||||
|
||||
$('#nightModeForm')?.addEventListener('submit', async event => {
|
||||
event.preventDefault(); const form = event.currentTarget;
|
||||
await runFormTask(form, () => saveRuntimeSettings(nightSettingsBodyFromForm(form), true));
|
||||
await runFormTask(form, () => saveNightSettings(form, true));
|
||||
});
|
||||
|
||||
$('#homeAssistantForm')?.addEventListener('submit', async event => {
|
||||
event.preventDefault(); const form = event.currentTarget;
|
||||
await runFormTask(form, () => saveRuntimeSettings(homeAssistantSettingsBodyFromForm(form), true));
|
||||
await runFormTask(form, () => saveHomeAssistantSettings(form, true));
|
||||
});
|
||||
|
||||
$('#addSensorAlias')?.addEventListener('click', () => {
|
||||
@@ -206,10 +265,11 @@ document.addEventListener('click', event => {
|
||||
$('#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 }));
|
||||
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); }
|
||||
});
|
||||
|
||||
@@ -246,7 +306,7 @@ $('#haTest').addEventListener('click', async event => {
|
||||
const idle = button.textContent;
|
||||
button.textContent = tr('settings.testingHa');
|
||||
try {
|
||||
await saveRuntimeSettings(homeAssistantSettingsBodyFromForm(form), false);
|
||||
await saveHomeAssistantSettings(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);
|
||||
@@ -257,11 +317,11 @@ $('#haTest').addEventListener('click', async event => {
|
||||
|
||||
$('#exportSettings').addEventListener('click', async () => {
|
||||
try {
|
||||
const data = await api('/api/settings/export');
|
||||
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-settings-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
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); }
|
||||
@@ -273,7 +333,7 @@ $('#importSettingsFile').addEventListener('change', async event => {
|
||||
try {
|
||||
if (!confirm(tr('settings.importConfirm'))) return;
|
||||
const body = JSON.parse(await file.text());
|
||||
await api('/api/settings/import', { method: 'POST', body });
|
||||
await api('/api/configuration/import', { method: 'POST', body });
|
||||
app.debugBacklogLoaded = false;
|
||||
await loadBootstrap();
|
||||
toast(tr('toast.imported'));
|
||||
|
||||
Reference in New Issue
Block a user