This commit is contained in:
Mateusz Gruszczyński
2026-08-24 16:47:23 +02:00
parent 66a5d6e5e9
commit 4ab878d587
15 changed files with 284 additions and 101 deletions
+158 -38
View File
@@ -213,6 +213,8 @@ function renderAll() {
renderAccessTokens();
fillSelects();
renderSettings();
renderNightSettings();
renderHomeAssistantSettings();
renderDebugOverlay();
}
@@ -565,6 +567,7 @@ function zoneCard(zone, detailed = true) {
const sensorDetails = zone.sensor_source === 'device'
? `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature ?? device?.current_temperature)}`
: `${tr('zones.greeTemp')}: ${fmtTemp(zone.device_temperature)} · ${tr('zones.externalTemp')}: ${fmtTemp(zone.external_temperature)} · ${tr('zones.usedSource')}: ${zoneControlSourceLabel(zone.control_temperature_source)}`;
const roomTemperature = zone.current_temperature ?? zone.device_temperature ?? device?.current_temperature;
const target = Number(zone.effective_setpoint ?? zone.setpoint);
const manual = zone.manual_preset || 'auto';
const mode = zone.inherit_house_mode ? 'house' : zone.mode;
@@ -574,7 +577,7 @@ function zoneCard(zone, detailed = true) {
: `<button type="button" class="zone-enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}"><span>${zone.enabled ? '✓' : '○'}</span>${esc(state)}</button>`;
return `<article class="list-card zone-thermostat ${zone.demand ? 'demanding' : ''} ${zone.enabled ? '' : 'zone-disabled'}">
<div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(zone.active_preset))}</p></div>${enabledControl}</div>
<div class="thermostat-main"><div><small>${esc(tr('zones.measurement'))}</small><strong>${fmtTemp(zone.current_temperature)}</strong></div><div class="temperature-control compact"><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="-0.5"></button><div class="target-temp compact">${Number.isFinite(target)?target.toFixed(1):'--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">+</button></div><div><small>${esc(tr('zones.deviceTarget'))}</small><strong>${fmtTemp(zone.device_setpoint)}</strong></div></div>
<div class="thermostat-main"><div><small>${esc(tr('zones.measurement'))}</small><strong>${fmtTemp(roomTemperature)}</strong></div><div class="temperature-control compact"><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="-0.5"></button><div class="target-temp compact">${Number.isFinite(target)?target.toFixed(1):'--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">+</button></div><div><small>${esc(tr('zones.deviceTarget'))}</small><strong>${fmtTemp(zone.device_setpoint)}</strong></div></div>
<div class="preset-row">
${['auto','comfort','sleep','away'].map(preset=>`<button class="${manual===preset?'active':''}" data-action="zone-preset" data-id="${esc(zone.id)}" data-value="${preset}">${esc(preset==='sleep'?tr('zones.sleepNow'):zonePresetLabel(preset))}</button>`).join('')}
</div>
@@ -663,6 +666,7 @@ function renderLogRetention() {
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;
@@ -673,12 +677,6 @@ function renderSettings() {
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;
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;
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;
@@ -693,6 +691,27 @@ function renderSettings() {
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;
updateInfluxFields();
renderLogRetention();
$('#systemInfo').innerHTML = `<h3>${esc(tr('settings.systemState'))}</h3><div>${esc(tr('settings.version'))}: <strong>${esc(app.system.version || '—')}</strong></div><div>${esc(tr('settings.uptime'))}: <strong>${esc(formatDuration(app.system.uptime_seconds || 0))}</strong></div><div>${esc(tr('settings.apiAuth'))}: <strong>${esc(app.system.auth_required ? tr('settings.enabled') : tr('settings.disabled'))}</strong></div>`;
}
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;
}
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');
@@ -700,10 +719,8 @@ function renderSettings() {
form.ha_outdoor_entity_id.value = app.settings.home_assistant?.outdoor_entity_id || '';
form.ha_allow_invalid_tls.checked = !!app.settings.home_assistant?.allow_invalid_tls;
form.outdoor_assist_enabled.checked = !!app.settings.outdoor_assist_enabled;
updateInfluxFields();
renderSensorAliases();
renderLogRetention();
$('#systemInfo').innerHTML = `<h3>${esc(tr('settings.systemState'))}</h3><div>${esc(tr('settings.version'))}: <strong>${esc(app.system.version || '—')}</strong></div><div>${esc(tr('settings.uptime'))}: <strong>${esc(formatDuration(app.system.uptime_seconds || 0))}</strong></div><div>${esc(tr('settings.apiAuth'))}: <strong>${esc(app.system.auth_required ? tr('settings.enabled') : tr('settings.disabled'))}</strong></div>`;
renderAccessTokens();
}
function updateInfluxFields() {
@@ -747,7 +764,7 @@ function formatDuration(seconds) {
return `${days ? `${days}d ` : ''}${hours}h ${minutes}m`;
}
const VIEW_ROUTES = {dashboard:'/dashboard', devices:'/devices', zones:'/zones', schedules:'/schedules', automations:'/automations', simulation:'/simulation', settings:'/settings', logs:'/events'};
const VIEW_ROUTES = {dashboard:'/dashboard', devices:'/devices', zones:'/zones', schedules:'/schedules', automations:'/automations', simulation:'/simulation', night:'/night-mode', homeassistant:'/home-assistant', settings:'/settings', logs:'/events'};
const HISTORY_TABS = ['overview','zones','devices','sensors','custom'];
function currentHistoryPath() {
@@ -772,7 +789,7 @@ function updateBrowserUrl(path, replace=false) {
function showView(name, {push=true, scroll=true}={}) {
app.currentView = name;
$$('.view').forEach(view => view.classList.toggle('active', view.dataset.view === name));
$$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['schedules','automations','simulation','settings','logs'].includes(name))));
$$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['schedules','automations','simulation','night','homeassistant','settings','logs'].includes(name))));
if (push) updateBrowserUrl(name === 'history' ? currentHistoryPath() : (VIEW_ROUTES[name] || '/dashboard'));
if (scroll) window.scrollTo({top: 0, behavior: 'smooth'});
if (name === 'history') { renderHistoryNavigation(); loadHistory(); }
@@ -1302,7 +1319,7 @@ function connectWebSocket() {
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(); scheduleControlPlanLoad(); }
else if (message.event === 'settings.updated') { app.settings=data; app.sensorAliases={...(app.settings?.home_assistant?.sensor_aliases||{})}; renderSettings(); renderHouseClimate(); renderDebugOverlay(); if(app.settings?.debug?.overlay_enabled) loadDebugBacklog(); scheduleControlPlanLoad(); }
else if (message.event === 'settings.updated') { app.settings=data; app.sensorAliases={...(app.settings?.home_assistant?.sensor_aliases||{})}; renderSettings(); renderNightSettings(); renderHomeAssistantSettings(); renderHouseClimate(); renderDebugOverlay(); if(app.settings?.debug?.overlay_enabled) loadDebugBacklog(); scheduleControlPlanLoad(); }
else if (message.event === 'debug.settings') { app.settings = app.settings || {}; app.settings.debug = data; 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') { 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); }
@@ -1463,38 +1480,141 @@ $('#automationForm').addEventListener('submit', async event => {
catch(error){toast(error.message,true);}
});
function settingsBodyFromForm(form) {
const raw=Object.fromEntries(new FormData(form));
function currentSettingsBody() {
const settings = app.settings || {};
const influx = settings.influxdb || {};
const ha = settings.home_assistant || {};
return {
controller_id:raw.controller_id, simulator_enabled:form.simulator_enabled.checked,
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,
house_mode:app.settings?.house_mode||'cool', control_strategy:'setpoint', outdoor_assist_enabled:form.outdoor_assist_enabled.checked,
history_retention_days:Number(raw.history_retention_days), history_compaction_enabled:form.history_compaction_enabled.checked,
event_log_retention_days:Number(raw.event_log_retention_days),
suppress_device_beep:form.suppress_device_beep.checked,
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,
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',
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,
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: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),
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 || '',
allow_invalid_tls: !!ha.allow_invalid_tls,
sensor_aliases: {...(ha.sensor_aliases || {})},
},
debug:{overlay_enabled:form.debug_overlay_enabled.checked, gree_frames:form.debug_gree_frames.checked},
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,allow_invalid_tls:form.ha_allow_invalid_tls.checked,sensor_aliases:{...(app.sensorAliases||{})}},
};
}
async function saveSettingsForm(form, notify=true) {
app.settings=await api('/api/settings',{method:'PUT',body:settingsBodyFromForm(form)}); app.sensorAliases={...(app.settings?.home_assistant?.sensor_aliases||{})}; renderSettings(); renderHouseClimate(); renderDebugOverlay(); scheduleControlPlanLoad(); if(app.settings?.debug?.overlay_enabled) loadDebugBacklog(); if(notify) toast(tr('common.saved')); return app.settings;
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.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};
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,
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();
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(); try { await saveSettingsForm(event.currentTarget, true); } catch(error){toast(error.message,true);}
event.preventDefault();
try { await saveRuntimeSettings(settingsBodyFromForm(event.currentTarget), true); }
catch(error) { toast(error.message, true); }
});
$('#nightModeForm')?.addEventListener('submit', async event => {
event.preventDefault();
try { await saveRuntimeSettings(nightSettingsBodyFromForm(event.currentTarget), true); }
catch(error) { toast(error.message, true); }
});
$('#homeAssistantForm')?.addEventListener('submit', async event => {
event.preventDefault();
try { await saveRuntimeSettings(homeAssistantSettingsBodyFromForm(event.currentTarget), true); }
catch(error) { toast(error.message, true); }
});
$('#addSensorAlias')?.addEventListener('click', () => {
@@ -1555,9 +1675,9 @@ $('#copyAccessToken').addEventListener('click', async () => {
});
$('#haTest').addEventListener('click', async () => {
const form=$('#settingsForm');
const form=$('#homeAssistantForm');
try {
await saveSettingsForm(form, false);
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}});
toast(tr('toast.haTemperature',{temperature:result.temperature_c.toFixed(1)}));