443 lines
30 KiB
JavaScript
443 lines
30 KiB
JavaScript
|
|
document.addEventListener('keydown', event => {
|
|
const target = event.target.closest?.('[data-temperature-kind]');
|
|
if (!target || event.target.matches('input')) return;
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
beginInlineTemperatureEdit(target);
|
|
}
|
|
});
|
|
|
|
document.addEventListener('click', async event => {
|
|
const inlineTarget = event.target.closest('[data-temperature-kind]');
|
|
if (inlineTarget && !event.target.closest('button')) { beginInlineTemperatureEdit(inlineTarget); return; }
|
|
const button = event.target.closest('button'); if (!button) return;
|
|
if (button.dataset.settingsTab) { setSettingsTab(button.dataset.settingsTab); return; }
|
|
if (button.dataset.debugFilter) { app.debugFilter = button.dataset.debugFilter; renderDebugOverlay(); return; }
|
|
if (button.dataset.dashboardTab) { setDashboardTab(button.dataset.dashboardTab); return; }
|
|
if (button.dataset.nav) {
|
|
if (button.dataset.nav === 'more') openDialog('moreDialog'); else showView(button.dataset.nav);
|
|
return;
|
|
}
|
|
if (button.dataset.go) { const more=$('#moreDialog'); if(more?.open) more.close(); showView(button.dataset.go); return; }
|
|
if (button.dataset.open) {
|
|
const form = document.getElementById(button.dataset.open.replace('Dialog','Form'));
|
|
if (form) form.reset();
|
|
if (button.dataset.open === 'zoneDialog') {
|
|
if (form) { form.dataset.editingZoneId = ''; form.dataset.separateHysteresisInitialized = 'false'; if (form.elements.id) form.elements.id.value = ''; }
|
|
updateZoneSensorFields();
|
|
updateZoneHysteresisFields();
|
|
}
|
|
if (button.dataset.open === 'groupDialog') {
|
|
if (form?.elements.id) form.elements.id.value = '';
|
|
renderGroupZoneChoices([]);
|
|
}
|
|
if (button.dataset.open === 'automationDialog') updateAutomationTargetFields();
|
|
if (button.dataset.open === 'scheduleDialog') updateSchedulePresetField();
|
|
openDialog(button.dataset.open);
|
|
return;
|
|
}
|
|
if (button.hasAttribute('data-close')) { requestDialogClose(button.closest('dialog')); return; }
|
|
if (button.dataset.chartLegend !== undefined) {
|
|
const id=button.dataset.chartId;
|
|
const runtime=chartRuntime.get(id);
|
|
const index=Number(button.dataset.chartLegend);
|
|
const item=runtime?.series?.[index];
|
|
if(item){
|
|
setChartSeriesHidden(id,item,index,!isChartSeriesHidden(id,item,index));
|
|
redrawHistoryChart(id);
|
|
}
|
|
return;
|
|
}
|
|
if (button.dataset.chartZoom) {
|
|
const id = button.dataset.chartId;
|
|
const current = clamp(Number(app.chartZooms[id] || 1), 1, MAX_CHART_ZOOM);
|
|
const next = button.dataset.chartZoom === 'in' ? Math.min(MAX_CHART_ZOOM, current * 1.5) : button.dataset.chartZoom === 'out' ? Math.max(1, current / 1.5) : 1;
|
|
setChartZoom(id,next);
|
|
return;
|
|
}
|
|
if (button.dataset.historyTab) { showHistoryTab(button.dataset.historyTab); return; }
|
|
if (button.dataset.historyAction) { await handleHistoryAction(button); return; }
|
|
if (button.dataset.scheduleTemplate) {
|
|
const zoneId = $('#schedulePresetZone')?.value; if (!zoneId) return toast(tr('schedules.chooseZone'), true);
|
|
if (!confirm(tr('schedules.replaceConfirm'))) return;
|
|
try {
|
|
button.disabled = true;
|
|
await api(`/api/zones/${encodeURIComponent(zoneId)}/schedule-template`, {method:'POST', body:{template:button.dataset.scheduleTemplate}});
|
|
await loadBootstrap(); toast(tr('schedules.templateApplied'));
|
|
} catch (error) { toast(error.message, true); } finally { button.disabled = false; }
|
|
return;
|
|
}
|
|
const action = button.dataset.action; if (!action) return;
|
|
const device = app.devices.find(v => v.id === button.dataset.device);
|
|
if (device && app.currentView !== 'devices' && deviceZoneDisabled(device.id) && ['power','temperature','mode','fan','toggle'].includes(action)) {
|
|
return toast(tr('devices.disabledZoneTechnicalOnly'), true);
|
|
}
|
|
if (action === 'power' && device) return sendDeviceCommand(device.id, current => ({power:!current?.power}));
|
|
if (action === 'temperature' && device) return sendDeviceCommand(device.id, current => ({target_temperature:clamp(Number(current?.target_temperature ?? device.target_temperature)+Number(button.dataset.delta),8,30)}));
|
|
if (action === 'mode' && device) return sendDeviceCommand(device.id, {mode:button.dataset.value, power:true});
|
|
if (action === 'fan' && device) return sendDeviceCommand(device.id, {fan_speed:Number(button.dataset.value)});
|
|
if (action === 'toggle' && device) return sendDeviceCommand(device.id, current => ({[button.dataset.field]:!current?.[button.dataset.field]}));
|
|
if (action === 'poll' && device) { try { button.disabled=true; updateDevice(await api(`/api/devices/${encodeURIComponent(device.id)}/poll`,{method:'POST'})); renderAll(); toast(tr('devices.readDone')); } catch(e){toast(e.message,true);} finally{button.disabled=false;} return; }
|
|
if (action === 'bind' && device) { try { button.disabled=true; updateDevice(await api(`/api/devices/${encodeURIComponent(device.id)}/bind`,{method:'POST'})); renderAll(); toast(tr('devices.bound')); } catch(e){toast(e.message,true);} finally{button.disabled=false;} return; }
|
|
if (action === 'rename-device' && device) return populateDeviceRename(device.id);
|
|
if (action === 'delete-device') return deleteEntity('devices', button.dataset.device, 'label.device');
|
|
if (action === 'house-mode') {
|
|
try {
|
|
await enqueueClimateControlTask(async () => {
|
|
await api('/api/house/control',{method:'POST', body:{mode:button.dataset.value}});
|
|
await loadBootstrap();
|
|
});
|
|
toast(tr('house.modeUpdated'));
|
|
} catch(error){ toast(error.message,true); }
|
|
return;
|
|
}
|
|
if (action === 'house-power') {
|
|
const power = button.dataset.value === 'true';
|
|
if (!power && app.settings?.house_power_enabled !== false && !confirm(tr('house.powerOffConfirm'))) return;
|
|
try {
|
|
button.disabled = true;
|
|
const result = await enqueueClimateControlTask(async () => {
|
|
const response = await api('/api/house/power',{method:'POST', body:{power}});
|
|
await loadBootstrap();
|
|
return response;
|
|
});
|
|
const failed = Array.isArray(result.failed) ? result.failed.length : 0;
|
|
if (failed) toast(tr('house.powerPartial', {count: failed}), true);
|
|
else toast(tr(power ? 'house.powerOnDone' : 'house.powerOffDone'));
|
|
} catch(error) { toast(error.message, true); }
|
|
finally { button.disabled = false; }
|
|
return;
|
|
}
|
|
if (action === 'house-preset') {
|
|
try {
|
|
const result = await enqueueClimateControlTask(async () => {
|
|
const response = await api('/api/house/preset',{method:'POST', body:{preset:button.dataset.value}});
|
|
await loadBootstrap();
|
|
return response;
|
|
});
|
|
const failed=Array.isArray(result.failed)?result.failed.length:0;
|
|
if(failed) toast(tr('house.powerPartial',{count:failed}),true); else toast(tr('house.presetUpdated'));
|
|
}
|
|
catch(error){ toast(error.message,true); }
|
|
return;
|
|
}
|
|
if (action === 'group-power') { const power=button.dataset.value==='true'; if(!power) delete app.groupCustomDrafts[button.dataset.id]; return sendGroupControl(button.dataset.id,{power}); }
|
|
if (action === 'group-mode') return sendGroupControl(button.dataset.id,{mode:button.dataset.value});
|
|
if (action === 'group-preset') { delete app.groupCustomDrafts[button.dataset.id]; return sendGroupControl(button.dataset.id,{preset:button.dataset.value}); }
|
|
if (action === 'group-custom-open') {
|
|
const editor=button.closest('.group-card')?.querySelector('[data-group-custom-editor]');
|
|
const input=editor?.querySelector('[data-group-custom-temperature]');
|
|
const value=parseDecimal(input?.value);
|
|
app.groupCustomDrafts[button.dataset.id]={open:true,value:Number.isFinite(value)?value:23};
|
|
if(editor){ editor.hidden=false; input?.focus(); input?.select(); }
|
|
return;
|
|
}
|
|
if (action === 'group-custom-cancel') {
|
|
delete app.groupCustomDrafts[button.dataset.id];
|
|
renderGroups();
|
|
return;
|
|
}
|
|
if (action === 'group-custom-temperature') {
|
|
const input=button.closest('.group-card')?.querySelector('[data-group-custom-temperature]');
|
|
const value=parseDecimal(input?.value);
|
|
if(!Number.isFinite(value) || value<8 || value>30) return toast(tr('groups.customTemperatureRange'),true);
|
|
return sendGroupControl(button.dataset.id,{preset:'custom',setpoint:Math.round(value*10)/10});
|
|
}
|
|
if (action === 'cancel-compressor-task') return cancelCompressorTask(button.dataset.id);
|
|
if (action === 'cancel-all-compressor-tasks') return cancelAllCompressorTasks();
|
|
if (action === 'zone-device-power') return sendZoneLocalPower(button.dataset.id, button.dataset.value === 'true');
|
|
if (action === 'zone-open-temporary') return populateTemporaryThermostat(button.dataset.id);
|
|
if (action === 'zone-temperature') { const zone=app.zones.find(v=>v.id===button.dataset.id); if(zone) { const base=Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint); queueZoneTemperature(zone, base+Number(button.dataset.delta)); } return; }
|
|
if (action === 'zone-mode') return sendZoneControl(button.dataset.id,{mode:button.dataset.value});
|
|
if (action === 'zone-preset') return sendZoneControl(button.dataset.id,{preset:button.dataset.value});
|
|
if (action === 'zone-enabled') return sendZoneControl(button.dataset.id,{enabled:button.dataset.value==='true'});
|
|
if (action === 'zone-resume-automation') {
|
|
const patch = button.dataset.resume === 'local'
|
|
? {clear_local_thermostat_override:true}
|
|
: {clear_device_manual_override:true};
|
|
return sendZoneControl(button.dataset.id, patch);
|
|
}
|
|
if (action === 'zone-go-control') {
|
|
showView('dashboard', {scroll:false});
|
|
setDashboardTab('thermostats', {scroll:false});
|
|
const target = [...document.querySelectorAll('[data-zone-card]')].find(card => card.dataset.zoneCard === button.dataset.id);
|
|
requestAnimationFrame(() => target?.scrollIntoView({behavior:'smooth', block:'center'}));
|
|
return;
|
|
}
|
|
if (action === 'debug-clear') { app.debugLines = []; renderDebugOverlay(); return; }
|
|
if (action === 'edit-group') return populateGroup(button.dataset.id);
|
|
if (action === 'delete-group') return deleteEntity('groups', button.dataset.id, 'groups.group');
|
|
if (action === 'edit-zone') return populateZone(button.dataset.id);
|
|
if (action === 'delete-zone') return deleteEntity('zones', button.dataset.id, 'label.zone');
|
|
if (action === 'edit-schedule') return populateSchedule(button.dataset.id);
|
|
if (action === 'delete-schedule') return deleteEntity('schedules', button.dataset.id, 'label.schedule');
|
|
if (action === 'edit-automation') return populateAutomation(button.dataset.id);
|
|
if (action === 'delete-automation') return deleteEntity('automations', button.dataset.id, 'label.automation');
|
|
if (action === 'revoke-access-token') {
|
|
if (!confirm(tr('confirm.revokeToken'))) return;
|
|
try {
|
|
await api(`/api/access-tokens/${encodeURIComponent(button.dataset.id)}`, {method:'DELETE'});
|
|
app.accessTokens = app.accessTokens.filter(item => item.id !== button.dataset.id);
|
|
renderAccessTokens();
|
|
toast(tr('toast.tokenRevoked'));
|
|
} catch (error) { toast(error.message, true); }
|
|
return;
|
|
}
|
|
});
|
|
|
|
$('#refreshButton').addEventListener('click', () => {
|
|
const form = activeDirtySettingsForm();
|
|
if (form && !confirmDiscardForm(form)) return;
|
|
loadBootstrap(true);
|
|
});
|
|
$('#connectionRetry')?.addEventListener('click', async event => {
|
|
const button = event.currentTarget;
|
|
button.disabled = true;
|
|
try {
|
|
if (app.ws) { try { app.ws.close(); } catch (_) {} app.ws = null; }
|
|
await loadBootstrap(false);
|
|
connectWebSocket();
|
|
} finally { button.disabled = false; }
|
|
});
|
|
$('#discoverButton').addEventListener('click', () => {
|
|
const form=$('#discoverForm'); form.reset();
|
|
form.protocol_version.value='0'; form.passes.value='3'; form.timeout_ms.value=String(Math.max(6000, Number(app.settings?.discovery_timeout_ms || 3000)));
|
|
openDialog('discoverDialog');
|
|
});
|
|
$('#historyRefresh').addEventListener('click', loadHistory);
|
|
$('#historyHours').addEventListener('change', () => { updateBrowserUrl(currentHistoryPath(), true); loadHistory(); });
|
|
$('#logsRefresh').addEventListener('click', loadLogs);
|
|
$('#languageSelect').addEventListener('change', event => setLanguage(event.target.value));
|
|
$('#themeSelect')?.addEventListener('change', event => setTheme(event.target.value));
|
|
$('#logLevelFilter')?.addEventListener('change', loadLogs); $('#logCategoryFilter')?.addEventListener('change', loadLogs);
|
|
$('#settingsForm [name=notifications_provider]')?.addEventListener('change', updateNotificationFields);
|
|
$('#settingsForm [name=compressor_protection_enabled]')?.addEventListener('change', updateCompressorProtectionFields);
|
|
$('#zoneForm [name=sensor_source]').addEventListener('change', updateZoneSensorFields);
|
|
document.addEventListener('input', event => {
|
|
const input=event.target.closest?.('[data-group-custom-temperature]');
|
|
if(!input) return;
|
|
const id=input.dataset.groupCustomTemperature;
|
|
if(!id) return;
|
|
app.groupCustomDrafts[id]={open:true,value:input.value};
|
|
});
|
|
|
|
document.addEventListener('keydown', event => {
|
|
const input=event.target.closest?.('[data-group-custom-temperature]');
|
|
if(!input || event.key!=='Enter') return;
|
|
event.preventDefault();
|
|
input.closest('.group-card')?.querySelector('[data-action="group-custom-temperature"]')?.click();
|
|
});
|
|
|
|
$('#zoneForm [name=separate_hysteresis]')?.addEventListener('change', () => updateZoneHysteresisFields({syncFromCommon:true}));
|
|
$('#scheduleForm [name=preset]').addEventListener('change', updateSchedulePresetField);
|
|
$('#automationForm [name=action_target_kind]')?.addEventListener('change', updateAutomationTargetFields);
|
|
$('#simulationScope')?.addEventListener('change', event => { app.simulationScope=event.target.value; app.simulationTarget='all'; renderSimulationPage(); updateSimulationUrl(); });
|
|
$('#simulationTarget')?.addEventListener('change', event => { app.simulationTarget=event.target.value || 'all'; renderSimulationPage(); updateSimulationUrl(); });
|
|
$('#simulationOpenTab')?.addEventListener('click', () => { const params=new URLSearchParams({standalone:'1'}); if(app.simulationScope!=='units')params.set('scope',app.simulationScope); if(app.simulationTarget!=='all')params.set('target',app.simulationTarget); window.open(`${withBase('/simulation')}?${params.toString()}`, '_blank', 'noopener'); });
|
|
$('#simulationFullscreen')?.addEventListener('click', async () => { try { if(document.fullscreenElement) await document.exitFullscreen(); else await document.documentElement.requestFullscreen(); } catch(error) { toast(error.message, true); } });
|
|
document.addEventListener('fullscreenchange', () => { const button=$('#simulationFullscreen'); if(button) button.textContent=tr(document.fullscreenElement?'simulation.exitFullscreen':'simulation.fullscreen'); });
|
|
|
|
$('#testNotifications')?.addEventListener('click', async () => { const f=$('#settingsForm'); const body={enabled:true,mode:f.notifications_mode.value,provider:f.notifications_provider.value,pushover_app_token:f.pushover_app_token.value.trim(),pushover_user_key:f.pushover_user_key.value.trim(),slack_webhook_url:f.slack_webhook_url.value.trim(),discord_webhook_url:f.discord_webhook_url.value.trim(),cooldown_seconds:Number(f.notification_cooldown_seconds.value||300),communication_failure_threshold:Number(f.notification_failure_threshold.value||3),target_timeout_minutes:Number(f.notification_target_timeout.value||60),alert_types:{stale_sensor:f.notification_alert_stale_sensor.checked,sensor_errors:f.notification_alert_sensor_errors.checked,communication:f.notification_alert_communication.checked,target_timeout:f.notification_alert_target_timeout.checked,automation:f.notification_alert_automation.checked,control_errors:f.notification_alert_control_errors.checked,important_events:f.notification_alert_important_events.checked,other:f.notification_alert_other.checked}}; try{await api('/api/integrations/notifications/test',{method:'POST',body});toast(tr('notifications.testSent'));}catch(e){toast(e.message,true);} });
|
|
|
|
$('#copyZoneSettings')?.addEventListener('click',()=>{
|
|
const f=$('#zoneForm');
|
|
const source=app.zones.find(zone=>zone.id===$('#copyZoneSource')?.value);
|
|
if(!source)return;
|
|
const targetId=f.dataset.editingZoneId || '';
|
|
if(targetId && source.id===targetId){ toast(tr('zones.copyDifferent'),true); return; }
|
|
|
|
// Identity and sensor assignment belong to the destination profile and must never
|
|
// be changed by copying thermostat tuning from another profile.
|
|
const protectedValues={
|
|
id:f.elements.id.value,
|
|
name:f.elements.name.value,
|
|
device_id:f.elements.device_id.value,
|
|
sensor_source:f.elements.sensor_source.value,
|
|
ha_entity_id:f.elements.ha_entity_id.value,
|
|
enabled:f.elements.enabled.checked,
|
|
};
|
|
const copyFields=['setpoint','cool_comfort_setpoint','cool_sleep_setpoint','cool_away_setpoint','heat_comfort_setpoint','heat_sleep_setpoint','heat_away_setpoint','hysteresis','cool_hysteresis','heat_hysteresis','min_on_seconds','min_off_seconds','min_adjust_seconds','standby_offset_c','external_sensor_weight_percent','max_sensor_difference','sensor_stale_after_seconds'];
|
|
copyFields.forEach(name=>{
|
|
const input=f.elements[name];
|
|
if(!input)return;
|
|
const value=name==='external_sensor_weight_percent'?(source.external_sensor_weight*100):source[name];
|
|
if(value!==undefined&&value!==null)input.value=String(value);
|
|
});
|
|
f.elements.smart_fan.checked=source.smart_fan!==false;
|
|
f.elements.separate_hysteresis.checked=source.separate_hysteresis===true;
|
|
updateZoneHysteresisFields();
|
|
if(f.elements.mode_policy) f.elements.mode_policy.value=source.inherit_house_mode===false?(source.mode||'cool'):'house';
|
|
|
|
f.elements.id.value=protectedValues.id;
|
|
f.elements.name.value=protectedValues.name;
|
|
f.elements.device_id.value=protectedValues.device_id;
|
|
f.elements.sensor_source.value=protectedValues.sensor_source;
|
|
f.elements.ha_entity_id.value=protectedValues.ha_entity_id;
|
|
f.elements.enabled.checked=protectedValues.enabled;
|
|
f.dataset.editingZoneId=targetId;
|
|
updateZoneSensorFields();
|
|
updateDirtyIndicator(f);
|
|
toast(tr('zones.copiedFrom',{name:source.name}));
|
|
});
|
|
|
|
$('#applyAutomationPreset')?.addEventListener('click',()=>{ const f=$('#automationForm'), p=$('#automationPreset')?.value; if(!p)return; const first=app.devices[0]?.id||''; const presets={hot:{name:tr('automations.presetHot'),trigger_kind:'temperature_above',threshold:'28',action_power:'true',action_mode:'cool',action_target_temperature:'23',cooldown_seconds:'900'},cold:{name:tr('automations.presetCold'),trigger_kind:'temperature_below',threshold:'17',action_power:'true',action_mode:'heat',action_target_temperature:'21',cooldown_seconds:'900'},morning:{name:tr('automations.presetMorning'),trigger_kind:'time',at_time:'07:00',action_power:'true',action_mode:'auto',action_target_temperature:'22',cooldown_seconds:'3600'},nightoff:{name:tr('automations.presetNightOff'),trigger_kind:'time',at_time:'23:30',action_power:'false',action_mode:'',action_target_temperature:'',cooldown_seconds:'3600'}}; const x=presets[p]; f.action_target_kind.value='device'; updateAutomationTargetFields(); Object.entries(x).forEach(([k,v])=>{if(f[k])f[k].value=v}); if(!f.trigger_device_id.value)f.trigger_device_id.value=first; if(!f.action_device_id.value)f.action_device_id.value=first; });
|
|
|
|
$('#tokenForm').addEventListener('submit', async event => {
|
|
event.preventDefault();
|
|
const form = event.currentTarget;
|
|
if (!validateForm(form) || form.dataset.submitting === 'true') return;
|
|
setFormBusy(form, true, 'auth.connecting');
|
|
app.token = new FormData(form).get('token').trim();
|
|
localStorage.setItem('gree_controller_token', app.token);
|
|
if (app.ws) app.ws.close();
|
|
try { await loadBootstrap(); }
|
|
finally { setFormBusy(form, false); }
|
|
});
|
|
|
|
$('#discoverForm').addEventListener('submit', async event => {
|
|
event.preventDefault(); const form=event.currentTarget, raw=Object.fromEntries(new FormData(form));
|
|
await runFormTask(form, async () => {
|
|
const result=await api('/api/discovery',{method:'POST',body:{protocol_version:Number(raw.protocol_version),passes:Number(raw.passes),timeout_ms:Number(raw.timeout_ms)}});
|
|
form.closest('dialog').close(); await loadBootstrap(); toast(tr('toast.found',{count:result.count})); showDiscoveryNames(result.new_device_ids || []);
|
|
}, {busyKey:'actions.discovering'});
|
|
});
|
|
|
|
$('#discoveryNamesForm').addEventListener('submit', async event => {
|
|
event.preventDefault();
|
|
const form = event.currentTarget;
|
|
const inputs = $$('input[data-device-id]', form);
|
|
await runFormTask(form, async () => {
|
|
const updated = await Promise.all(inputs.map(input => api(`/api/devices/${encodeURIComponent(input.dataset.deviceId)}`, {method:'PATCH', body:{name:input.value.trim()}})));
|
|
updated.forEach(updateDevice);
|
|
form.closest('dialog').close();
|
|
renderAll();
|
|
toast(tr('common.saved'));
|
|
});
|
|
});
|
|
|
|
$('#renameDeviceForm').addEventListener('submit', async event => {
|
|
event.preventDefault(); const form=event.currentTarget, raw=Object.fromEntries(new FormData(form));
|
|
await runFormTask(form, async () => {
|
|
const device=await api(`/api/devices/${encodeURIComponent(raw.id)}`,{method:'PATCH',body:{name:raw.name.trim(),protocol_version:Number(raw.protocol_version)}});
|
|
updateDevice(device); form.closest('dialog').close(); renderAll(); toast(tr('common.saved'));
|
|
});
|
|
});
|
|
|
|
$('#deviceForm').addEventListener('submit', async event => {
|
|
event.preventDefault(); const form=event.currentTarget, data=Object.fromEntries(new FormData(form));
|
|
data.port=Number(data.port); data.protocol_version=Number(data.protocol_version); data.simulated=form.simulated.checked;
|
|
await runFormTask(form, async () => {
|
|
await api('/api/devices',{method:'POST',body:data}); form.closest('dialog').close(); form.reset(); await loadBootstrap(); toast(tr('devices.added'));
|
|
});
|
|
});
|
|
|
|
$('#zoneForm').addEventListener('submit', async event => {
|
|
event.preventDefault(); const form=event.currentTarget, raw=Object.fromEntries(new FormData(form));
|
|
const id=form.dataset.editingZoneId || raw.id; const body={
|
|
name:raw.name,device_id:raw.device_id,enabled:form.enabled.checked,
|
|
mode:raw.mode_policy==='house'?'cool':raw.mode_policy,inherit_house_mode:raw.mode_policy==='house',setpoint:parseDecimal(raw.setpoint),
|
|
cool_comfort_setpoint:parseDecimal(raw.cool_comfort_setpoint),cool_sleep_setpoint:parseDecimal(raw.cool_sleep_setpoint),cool_away_setpoint:parseDecimal(raw.cool_away_setpoint),
|
|
heat_comfort_setpoint:parseDecimal(raw.heat_comfort_setpoint),heat_sleep_setpoint:parseDecimal(raw.heat_sleep_setpoint),heat_away_setpoint:parseDecimal(raw.heat_away_setpoint),
|
|
hysteresis:parseDecimal(raw.hysteresis),separate_hysteresis:form.separate_hysteresis.checked,cool_hysteresis:parseDecimal(raw.cool_hysteresis),heat_hysteresis:parseDecimal(raw.heat_hysteresis),min_on_seconds:Number(raw.min_on_seconds),min_off_seconds:Number(raw.min_off_seconds),
|
|
min_adjust_seconds:Number(raw.min_adjust_seconds),standby_offset_c:parseDecimal(raw.standby_offset_c),smart_fan:form.smart_fan.checked,
|
|
sensor_source:raw.sensor_source,ha_entity_id:raw.ha_entity_id||null,external_sensor_weight:Number(raw.external_sensor_weight_percent)/100,max_sensor_difference:parseDecimal(raw.max_sensor_difference),sensor_stale_after_seconds:Number(raw.sensor_stale_after_seconds||300),revision:raw.revision?Number(raw.revision):null
|
|
};
|
|
await runFormTask(form, async () => {
|
|
await api(id?`/api/zones/${encodeURIComponent(id)}`:'/api/zones',{method:id?'PUT':'POST',body}); form.closest('dialog').close(); form.reset(); await loadBootstrap(); toast(tr('common.saved'));
|
|
});
|
|
});
|
|
|
|
$('#temporaryFinishKind')?.addEventListener('change', updateTemporaryThermostatFields);
|
|
$('#temporaryStartKind')?.addEventListener('change', updateTemporaryThermostatFields);
|
|
$$('[data-temporary-start-delay]').forEach(button => button.addEventListener('click', () => {
|
|
const form = $('#temporaryThermostatForm');
|
|
form.start_delay_minutes.value = button.dataset.temporaryStartDelay;
|
|
}));
|
|
$$('[data-temporary-duration]').forEach(button => button.addEventListener('click', () => {
|
|
const form = $('#temporaryThermostatForm');
|
|
form.duration_minutes.value = button.dataset.temporaryDuration;
|
|
}));
|
|
|
|
$('#temporaryThermostatStop')?.addEventListener('click', async event => {
|
|
const id = event.currentTarget.dataset.id; if (!id) return;
|
|
try {
|
|
event.currentTarget.disabled = true;
|
|
const zone = await enqueueZoneControlRequest(id, {clear_temporary_quick_thermostat:true});
|
|
const index = app.zones.findIndex(item => item.id === zone.id); if (index >= 0) app.zones[index] = zone;
|
|
$('#temporaryThermostatDialog').close(); renderAll(); scheduleControlPlanLoad(); toast(tr('zones.temporaryStopped'));
|
|
} catch (error) { toast(error.message, true); }
|
|
finally { event.currentTarget.disabled = false; }
|
|
});
|
|
|
|
$('#temporaryThermostatForm')?.addEventListener('submit', async event => {
|
|
event.preventDefault();
|
|
const form = event.currentTarget, raw = Object.fromEntries(new FormData(form));
|
|
const activeSession = form.dataset.activeSession === 'true';
|
|
const zone = app.zones.find(item => item.id === raw.zone_id);
|
|
const existingSession = zone?.temporary_quick_thermostat;
|
|
const startKind = activeSession ? (existingSession?.start_kind || 'now') : raw.start_kind;
|
|
const request = {start_kind:startKind, finish_kind:raw.finish_kind, target_temperature:parseDecimal(raw.target_temperature)};
|
|
if (!activeSession && startKind === 'delay') request.start_delay_minutes = Number(raw.start_delay_minutes);
|
|
if (!activeSession && startKind === 'at') {
|
|
const startAt = new Date(raw.start_at);
|
|
if (!Number.isFinite(startAt.getTime())) return toast(tr('zones.temporaryInvalidStartAt'), true);
|
|
request.start_at = startAt.toISOString();
|
|
}
|
|
if (raw.finish_kind === 'duration') request.duration_minutes = Number(raw.duration_minutes);
|
|
if (raw.finish_kind === 'until') {
|
|
const until = new Date(raw.until);
|
|
if (!Number.isFinite(until.getTime())) return toast(tr('zones.temporaryInvalidUntil'), true);
|
|
const effectiveStart = activeSession ? new Date()
|
|
: startKind === 'delay' ? new Date(Date.now() + Number(raw.start_delay_minutes) * 60000)
|
|
: startKind === 'at' ? new Date(raw.start_at) : new Date();
|
|
if (Number.isFinite(effectiveStart.getTime()) && until <= effectiveStart) return toast(tr('zones.temporaryEndAfterStart'), true);
|
|
request.until = until.toISOString();
|
|
}
|
|
if (['temperature_reached','temperature_stable'].includes(raw.finish_kind)) {
|
|
request.temperature_operator = raw.temperature_operator;
|
|
request.tolerance_c = parseDecimal(raw.tolerance_c);
|
|
if (raw.finish_kind === 'temperature_stable') request.hold_minutes = Number(raw.hold_minutes);
|
|
const safety = Number(raw.max_duration_minutes);
|
|
if (Number.isFinite(safety) && safety > 0) request.max_duration_minutes = safety;
|
|
}
|
|
await runFormTask(form, async () => {
|
|
const updatedZone = await enqueueZoneControlRequest(raw.zone_id, {temporary_quick_thermostat:request});
|
|
const index = app.zones.findIndex(item => item.id === updatedZone.id); if (index >= 0) app.zones[index] = updatedZone;
|
|
form.closest('dialog').close(); renderAll(); scheduleControlPlanLoad(); toast(tr(activeSession || startKind === 'now' ? 'zones.temporaryStarted' : 'zones.temporaryScheduledToast'));
|
|
}, {busyKey: activeSession ? 'actions.saving' : 'zones.temporaryStarting'});
|
|
});
|
|
|
|
$('#groupForm')?.addEventListener('submit', async event => {
|
|
event.preventDefault();
|
|
const form=event.currentTarget, raw=Object.fromEntries(new FormData(form));
|
|
const id=raw.id || '';
|
|
const zone_ids=$$('input[name=zone_ids]:checked', form).map(input => input.value);
|
|
if (!zone_ids.length) return toast(tr('groups.chooseMember'), true);
|
|
const existing=id ? app.groups.find(group => group.id===id) : null;
|
|
const body={name:raw.name.trim(),zone_ids,power_enabled:existing?.power_enabled !== false};
|
|
await runFormTask(form, async () => {
|
|
await api(id?`/api/groups/${encodeURIComponent(id)}`:'/api/groups',{method:id?'PUT':'POST',body});
|
|
form.closest('dialog').close(); form.reset(); await loadBootstrap(); toast(tr('common.saved'));
|
|
});
|
|
});
|
|
|
|
$('#scheduleForm').addEventListener('submit', async event => {
|
|
event.preventDefault(); const form=event.currentTarget, raw=Object.fromEntries(new FormData(form));
|
|
const id=raw.id, weekdays=$$('[name=weekday]:checked',form).map(v=>Number(v.value));
|
|
const body={name:raw.name,zone_id:raw.zone_id,enabled:form.enabled.checked,weekdays,start_time:raw.start_time,end_time:raw.end_time,preset:raw.preset,setpoint:parseDecimal(raw.setpoint||23)};
|
|
await runFormTask(form, async () => {
|
|
await api(id?`/api/schedules/${encodeURIComponent(id)}`:'/api/schedules',{method:id?'PUT':'POST',body}); form.closest('dialog').close(); form.reset(); await loadBootstrap(); toast(tr('common.saved'));
|
|
});
|
|
});
|
|
|
|
$('#automationForm').addEventListener('submit', async event => {
|
|
event.preventDefault(); const form=event.currentTarget, raw=Object.fromEntries(new FormData(form)); const id=raw.id;
|
|
const groupTarget=raw.action_target_kind==='group';
|
|
const action={}; if(raw.action_power!=='') action.power=raw.action_power==='true'; if(raw.action_mode) action.mode=raw.action_mode; if(!groupTarget && raw.action_target_temperature!=='') action.target_temperature=parseDecimal(raw.action_target_temperature);
|
|
const body={name:raw.name,enabled:form.enabled.checked,trigger_kind:raw.trigger_kind,trigger_device_id:raw.trigger_device_id||null,threshold:raw.threshold===''?null:parseDecimal(raw.threshold),at_time:raw.at_time||null,action_device_id:groupTarget?'':raw.action_device_id,action_group_id:groupTarget?(raw.action_group_id||null):null,action_preset:groupTarget?(raw.action_preset||null):null,action,cooldown_seconds:Number(raw.cooldown_seconds)};
|
|
await runFormTask(form, async () => {
|
|
await api(id?`/api/automations/${encodeURIComponent(id)}`:'/api/automations',{method:id?'PUT':'POST',body}); form.closest('dialog').close(); form.reset(); await loadBootstrap(); toast(tr('common.saved'));
|
|
});
|
|
});
|