v0.8.15
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
function setDashboardTab(tab, {scroll=true}={}) {
|
||||
const next = ['main','thermostats','manual'].includes(tab) ? tab : 'main';
|
||||
app.dashboardTab = next;
|
||||
$$('[data-dashboard-tab]').forEach(button => {
|
||||
const active = button.dataset.dashboardTab === next;
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-selected', String(active));
|
||||
});
|
||||
$$('[data-dashboard-panel]').forEach(panel => {
|
||||
const active = panel.dataset.dashboardPanel === next;
|
||||
panel.classList.toggle('active', active);
|
||||
panel.hidden = !active;
|
||||
});
|
||||
if (scroll && app.currentView === 'dashboard') window.scrollTo({top:0, behavior:'smooth'});
|
||||
}
|
||||
|
||||
function showView(name, {push=true, scroll=true}={}) {
|
||||
if (!canLeaveCurrentView(name)) return false;
|
||||
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' && ['groups','schedules','automations','simulation','night','homeassistant','settings','logs'].includes(name))));
|
||||
if (push) updateBrowserUrl(pathForView(name));
|
||||
if (scroll) window.scrollTo({top: 0, behavior: 'smooth'});
|
||||
if (name === 'dashboard') setDashboardTab(app.dashboardTab, {scroll:false});
|
||||
if (name === 'history') { renderHistoryNavigation(); loadHistory(); }
|
||||
if (name === 'logs') loadLogs();
|
||||
return true;
|
||||
}
|
||||
|
||||
function showHistoryTab(tab, {push=true, load=true}={}) {
|
||||
app.historyTab = HISTORY_TABS.includes(tab) ? tab : 'overview';
|
||||
renderHistoryNavigation();
|
||||
if (push) updateBrowserUrl(currentHistoryPath());
|
||||
if (load) loadHistory();
|
||||
}
|
||||
|
||||
async function sendDeviceCommand(id, command) {
|
||||
try {
|
||||
const device = await api(`/api/devices/${encodeURIComponent(id)}/command`, {method:'POST', body:command});
|
||||
updateDevice(device); renderAll();
|
||||
} catch (error) { toast(error.message, true); }
|
||||
}
|
||||
|
||||
function updateDevice(device) {
|
||||
const index = app.devices.findIndex(item => item.id === device.id);
|
||||
if (index >= 0) app.devices[index] = device; else app.devices.push(device);
|
||||
}
|
||||
|
||||
async function sendZoneLocalPower(id, power) {
|
||||
try {
|
||||
const zone = await api(`/api/zones/${encodeURIComponent(id)}/control`, {method:'POST', body:{power}});
|
||||
const index = app.zones.findIndex(item => item.id === zone.id);
|
||||
if (index >= 0) app.zones[index] = zone; else app.zones.push(zone);
|
||||
renderAll(); scheduleControlPlanLoad(); toast(tr('zones.localPowerUpdated'));
|
||||
} catch (error) { toast(error.message, true); }
|
||||
}
|
||||
|
||||
async function sendZoneControl(id, patch) {
|
||||
const sequence = (app.zoneControlSeq[id] || 0) + 1;
|
||||
app.zoneControlSeq[id] = sequence;
|
||||
try {
|
||||
const zone = await api(`/api/zones/${encodeURIComponent(id)}/control`, {method:'POST', body:patch});
|
||||
if (app.zoneControlSeq[id] !== sequence) return;
|
||||
const index = app.zones.findIndex(item => item.id === zone.id);
|
||||
if (index >= 0) app.zones[index] = zone; else app.zones.push(zone);
|
||||
renderSummary(); renderZones(); scheduleControlPlanLoad();
|
||||
} catch (error) {
|
||||
if (app.zoneControlSeq[id] === sequence) { await loadBootstrap(); toast(error.message, true); }
|
||||
}
|
||||
}
|
||||
|
||||
function queueZoneTemperature(zone, value) {
|
||||
const next = Math.round(clamp(value, 8, 30) * 2) / 2;
|
||||
zone.manual_setpoint = next;
|
||||
zone.setpoint = next;
|
||||
zone.effective_setpoint = next;
|
||||
renderZones();
|
||||
clearTimeout(app.zoneTemperatureTimers[zone.id]);
|
||||
app.zoneTemperatureTimers[zone.id] = setTimeout(() => sendZoneControl(zone.id, {setpoint: next}), 160);
|
||||
}
|
||||
|
||||
function showDiscoveryNames(ids) {
|
||||
const wanted = new Set(Array.isArray(ids) ? ids : []);
|
||||
const devices = app.devices.filter(device => wanted.has(device.id));
|
||||
if (!devices.length) return;
|
||||
const list = $('#discoveryNamesList');
|
||||
list.innerHTML = devices.map(device => `
|
||||
<label class="discovery-name-row">
|
||||
<span><strong>${esc(device.model || 'GREE')}</strong><small>${esc(device.ip)} · ${esc(device.mac)} · ${device.protocol_version === 2 ? 'V2 GCM' : 'V1 ECB'}</small></span>
|
||||
<input data-device-id="${esc(device.id)}" maxlength="80" required value="${esc(device.name)}">
|
||||
</label>`).join('');
|
||||
openDialog('discoveryNamesDialog');
|
||||
}
|
||||
|
||||
function populateDeviceRename(id) {
|
||||
const device = app.devices.find(item => item.id === id); if (!device) return;
|
||||
const form = $('#renameDeviceForm'); form.reset();
|
||||
form.id.value = device.id; form.name.value = device.name; form.protocol_version.value = String(device.protocol_version ?? 0);
|
||||
openDialog('renameDeviceDialog');
|
||||
}
|
||||
|
||||
async function deleteEntity(type, id, labelKey) {
|
||||
if (!confirm(tr('confirm.delete', {label: tr(labelKey)}))) return;
|
||||
try { await api(`/api/${type}/${encodeURIComponent(id)}`, {method:'DELETE'}); await loadBootstrap(); toast(tr('common.removed')); }
|
||||
catch (error) { toast(error.message, true); }
|
||||
}
|
||||
|
||||
function openDialog(id) {
|
||||
fillSelects();
|
||||
const dialog = document.getElementById(id);
|
||||
if (dialog && !dialog.open) {
|
||||
const form = $('form', dialog);
|
||||
if (form) { clearFormErrors(form); markFormClean(form); }
|
||||
dialog.showModal();
|
||||
}
|
||||
}
|
||||
|
||||
function updateZoneSensorFields() {
|
||||
const form = $('#zoneForm');
|
||||
if (!form) return;
|
||||
const external = form.sensor_source.value !== 'device';
|
||||
$('#externalSensorFields').hidden = !external;
|
||||
form.ha_entity_id.required = external;
|
||||
}
|
||||
|
||||
function updateSchedulePresetField() {
|
||||
const form = $('#scheduleForm'); if (!form) return;
|
||||
$('#scheduleSetpointField').hidden = form.preset.value !== 'custom';
|
||||
form.setpoint.required = form.preset.value === 'custom';
|
||||
}
|
||||
|
||||
function updateAutomationTargetFields() {
|
||||
const form = $('#automationForm'); if (!form) return;
|
||||
const groupTarget = form.action_target_kind.value === 'group';
|
||||
$('#automationDeviceTarget').hidden = groupTarget;
|
||||
$('#automationGroupTarget').hidden = !groupTarget;
|
||||
$('#automationGroupPreset').hidden = !groupTarget;
|
||||
$('#automationTargetTemperature').hidden = groupTarget;
|
||||
$('#automationGroupHint').hidden = !groupTarget;
|
||||
form.action_device_id.required = !groupTarget;
|
||||
form.action_group_id.required = groupTarget;
|
||||
form.action_target_temperature.disabled = groupTarget;
|
||||
if (groupTarget) form.action_target_temperature.value = '';
|
||||
[...form.action_mode.options].forEach(option => {
|
||||
if (!['dry','fan'].includes(option.value)) return;
|
||||
option.disabled = groupTarget;
|
||||
option.hidden = groupTarget;
|
||||
});
|
||||
if (groupTarget && ['dry','fan'].includes(form.action_mode.value)) form.action_mode.value = '';
|
||||
}
|
||||
|
||||
function populateZone(id) {
|
||||
const item = app.zones.find(v => v.id === id); if (!item) return;
|
||||
const form = $('#zoneForm');
|
||||
form.reset();
|
||||
form.dataset.editingZoneId = item.id;
|
||||
form.elements.id.value = item.id;
|
||||
fillSelects();
|
||||
Object.entries(item).forEach(([key,value]) => { if (key !== 'id' && form.elements[key] && value != null && typeof value !== 'object') form.elements[key].value = value; });
|
||||
form.mode_policy.value = item.inherit_house_mode ? 'house' : (item.mode || 'cool');
|
||||
if (Number(item.profile_version || 0) === 0) {
|
||||
if ((item.mode || 'cool') === 'heat') form.heat_comfort_setpoint.value = Number(item.setpoint ?? 21);
|
||||
else form.cool_comfort_setpoint.value = Number(item.setpoint ?? 23);
|
||||
}
|
||||
form.external_sensor_weight_percent.value = Math.round(Number(item.external_sensor_weight ?? 0.4) * 100);
|
||||
form.max_sensor_difference.value = Number(item.max_sensor_difference ?? 3);
|
||||
form.min_adjust_seconds.value = Number(item.min_adjust_seconds ?? 120);
|
||||
form.standby_offset_c.value = Number(item.standby_offset_c ?? 2);
|
||||
form.smart_fan.checked = item.smart_fan !== false;
|
||||
form.enabled.checked = item.enabled; updateZoneSensorFields(); openDialog('zoneDialog');
|
||||
}
|
||||
|
||||
const dateTimeLocalValue = value => {
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (!Number.isFinite(date.getTime())) return '';
|
||||
const pad = number => String(number).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
};
|
||||
|
||||
function updateTemporaryThermostatFields() {
|
||||
const form = $('#temporaryThermostatForm'); if (!form) return;
|
||||
const startKind = form.start_kind.value;
|
||||
const kind = form.finish_kind.value;
|
||||
const startDelay = $('#temporaryStartDelayFields');
|
||||
const startAt = $('#temporaryStartAtFields');
|
||||
const duration = $('#temporaryDurationFields');
|
||||
const until = $('#temporaryUntilFields');
|
||||
const temperature = $('#temporaryTemperatureFields');
|
||||
const schedule = $('#temporaryScheduleFields');
|
||||
startDelay.hidden = startKind !== 'delay';
|
||||
startAt.hidden = startKind !== 'at';
|
||||
duration.hidden = kind !== 'duration';
|
||||
until.hidden = kind !== 'until';
|
||||
temperature.hidden = !['temperature_reached','temperature_stable'].includes(kind);
|
||||
schedule.hidden = kind !== 'schedule_boundary';
|
||||
$('#temporaryHoldField').hidden = kind !== 'temperature_stable';
|
||||
form.start_delay_minutes.required = startKind === 'delay';
|
||||
form.start_at.required = startKind === 'at';
|
||||
form.duration_minutes.required = kind === 'duration';
|
||||
form.until.required = kind === 'until';
|
||||
form.hold_minutes.required = kind === 'temperature_stable';
|
||||
}
|
||||
|
||||
function populateTemporaryThermostat(id) {
|
||||
const zone = app.zones.find(item => item.id === id); if (!zone) return;
|
||||
const form = $('#temporaryThermostatForm');
|
||||
form.reset();
|
||||
form.elements.zone_id.value = zone.id;
|
||||
form.target_temperature.value = Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint ?? 23).toFixed(1);
|
||||
form.start_kind.value = 'now';
|
||||
form.start_delay_minutes.value = '30';
|
||||
form.start_at.value = dateTimeLocalValue(new Date(Date.now() + 60 * 60 * 1000));
|
||||
form.until.value = dateTimeLocalValue(new Date(Date.now() + 2 * 60 * 60 * 1000));
|
||||
const session = zone.temporary_quick_thermostat;
|
||||
const activeSession = !!session?.activated_at;
|
||||
form.dataset.activeSession = activeSession ? 'true' : 'false';
|
||||
if (session) {
|
||||
form.start_kind.value = session.start_kind || (new Date(session.started_at).getTime() > Date.now() ? 'at' : 'now');
|
||||
if (form.start_kind.value === 'delay' && session.started_at) {
|
||||
form.start_delay_minutes.value = Math.max(1, Math.ceil((new Date(session.started_at).getTime() - Date.now()) / 60000));
|
||||
}
|
||||
if (form.start_kind.value === 'at' && session.started_at) form.start_at.value = dateTimeLocalValue(session.started_at);
|
||||
form.finish_kind.value = session.finish_kind || 'duration';
|
||||
if (session.temperature_target != null) form.target_temperature.value = Number(session.temperature_target).toFixed(1);
|
||||
if (session.temperature_operator) form.temperature_operator.value = session.temperature_operator;
|
||||
form.tolerance_c.value = Number(session.tolerance_c ?? 0.3).toFixed(1);
|
||||
if (session.hold_seconds) form.hold_minutes.value = Math.max(1, Math.round(Number(session.hold_seconds) / 60));
|
||||
if (session.finish_kind === 'duration') {
|
||||
if (session.duration_seconds) {
|
||||
form.duration_minutes.value = Math.max(1, Math.round(Number(session.duration_seconds) / 60));
|
||||
} else if (session.expires_at) {
|
||||
const base = new Date(session.activated_at || session.started_at || Date.now()).getTime();
|
||||
form.duration_minutes.value = Math.max(1, Math.round((new Date(session.expires_at).getTime() - base) / 60000));
|
||||
}
|
||||
}
|
||||
if (session.expires_at && session.finish_kind !== 'duration') form.until.value = dateTimeLocalValue(session.expires_at);
|
||||
if (session.safety_duration_seconds) {
|
||||
form.max_duration_minutes.value = Math.max(1, Math.round(Number(session.safety_duration_seconds) / 60));
|
||||
} else if (session.safety_expires_at) {
|
||||
const base = new Date(session.activated_at || session.started_at || Date.now()).getTime();
|
||||
form.max_duration_minutes.value = Math.max(1, Math.round((new Date(session.safety_expires_at).getTime() - base) / 60000));
|
||||
} else if (['temperature_reached','temperature_stable'].includes(session.finish_kind)) {
|
||||
form.max_duration_minutes.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Once ownership has started, the historical start is informational. Editing changes only
|
||||
// target/finish rules; rescheduling requires stopping the session and creating a new one.
|
||||
form.start_kind.disabled = activeSession;
|
||||
form.start_delay_minutes.disabled = activeSession;
|
||||
form.start_at.disabled = activeSession;
|
||||
$$('[data-temporary-start-delay]').forEach(button => { button.disabled = activeSession; });
|
||||
|
||||
updateTemporaryThermostatFields();
|
||||
const active = $('#temporaryThermostatActive');
|
||||
active.hidden = !session;
|
||||
$('#temporaryThermostatStop').dataset.id = zone.id;
|
||||
$('#temporaryThermostatSubmit').textContent = tr(session ? 'zones.temporaryUpdate' : 'zones.temporaryStart');
|
||||
updateTemporaryThermostatCountdowns();
|
||||
openDialog('temporaryThermostatDialog');
|
||||
updateTemporaryThermostatCountdowns();
|
||||
}
|
||||
|
||||
function populateSchedule(id) {
|
||||
const item = app.schedules.find(v => v.id === id); if (!item) return;
|
||||
const form = $('#scheduleForm'); form.reset(); fillSelects();
|
||||
['id','name','zone_id','start_time','end_time','setpoint','preset'].forEach(key => { if (form.elements[key] && item[key] != null) form.elements[key].value = item[key]; });
|
||||
form.enabled.checked = item.enabled;
|
||||
$$('[name=weekday]', form).forEach(input => input.checked = item.weekdays.includes(Number(input.value)));
|
||||
updateSchedulePresetField(); openDialog('scheduleDialog');
|
||||
}
|
||||
|
||||
function populateAutomation(id) {
|
||||
const item = app.automations.find(v => v.id === id); if (!item) return;
|
||||
const form = $('#automationForm'); form.reset(); fillSelects();
|
||||
['id','name','trigger_kind','trigger_device_id','threshold','at_time','action_device_id','cooldown_seconds'].forEach(key => { if (form.elements[key] && item[key] != null) form.elements[key].value = item[key]; });
|
||||
const groupTarget = !!item.action_group_id;
|
||||
form.action_target_kind.value = groupTarget ? 'group' : 'device';
|
||||
if (groupTarget) form.action_group_id.value = item.action_group_id;
|
||||
form.action_power.value = item.action.power == null ? '' : String(item.action.power);
|
||||
form.action_mode.value = item.action.mode || '';
|
||||
form.action_preset.value = item.action_preset || '';
|
||||
form.action_target_temperature.value = item.action.target_temperature ?? '';
|
||||
form.enabled.checked = item.enabled;
|
||||
updateAutomationTargetFields();
|
||||
openDialog('automationDialog');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user