v0.9.0
This commit is contained in:
+34
-2
@@ -14,6 +14,10 @@ function deviceZoneDisabled(deviceId) {
|
||||
return app.zones.some(zone => zone.device_id === deviceId && zone.enabled === false);
|
||||
}
|
||||
|
||||
function zoneForDevice(deviceId) {
|
||||
return app.zones.find(zone => zone.device_id === deviceId) || null;
|
||||
}
|
||||
|
||||
function deviceCard(device, detailed = false) {
|
||||
const modes = ['auto','cool','dry','fan','heat'];
|
||||
const fans = [0,1,3,5];
|
||||
@@ -21,6 +25,7 @@ function deviceCard(device, detailed = false) {
|
||||
const responseTime = hasResponseTime ? `${Math.max(0, Math.round(Number(device.response_time_ms)))} ms` : '— ms';
|
||||
const protocol = device.simulated ? tr('devices.simulator') : device.protocol_version === 2 ? 'V2 GCM' : device.protocol_version === 1 ? 'V1 ECB' : tr('devices.protocolAuto');
|
||||
const networkInfo = `<div class="device-network-info"><small>${esc(device.ip)} · ${esc(responseTime)} · ${esc(protocol)}</small>${device.last_error ? `<small class="device-network-error" title="${esc(device.last_error)}">${esc(device.last_error)}</small>` : ''}</div>`;
|
||||
const managedZone = app.zones.find(zone => zone.device_id === device.id && zone.compressor_pending_action) || zoneForDevice(device.id);
|
||||
const zoneLocked = !detailed && deviceZoneDisabled(device.id);
|
||||
const locked = zoneLocked ? ' disabled' : '';
|
||||
const quickClass = detailed ? '' : ' quick-control-card quick-device-control';
|
||||
@@ -36,6 +41,7 @@ function deviceCard(device, detailed = false) {
|
||||
<button data-action="temperature" data-delta="1" data-device="${esc(device.id)}"${locked}>+</button>
|
||||
</div>
|
||||
<div class="current-line">${esc(tr('devices.currentTemperature'))}: <strong>${fmtTemp(device.current_temperature)}</strong>${device.outdoor_temperature == null ? '' : ` · ${esc(tr('devices.outdoor'))} ${fmtTemp(device.outdoor_temperature)}`}</div>
|
||||
${compressorQueuePanel(managedZone)}
|
||||
${zoneLocked ? `<div class="field-note warning-note">${esc(tr('devices.disabledZoneTechnicalOnly'))}</div>` : ''}
|
||||
<div class="mode-row${quickRow}${detailed ? '' : ' quick-control-row-5'}">${modes.map(mode => `<button class="${device.mode === mode ? 'active' : ''}" data-action="mode" data-value="${mode}" data-device="${esc(device.id)}"${locked}>${esc(modeLabel(mode))}</button>`).join('')}</div>
|
||||
<div class="fan-row${quickRow}${detailed ? '' : ' quick-control-row-4'}">${fans.map(fan => `<button class="${Number(device.fan_speed) === fan ? 'active' : ''}" data-action="fan" data-value="${fan}" data-device="${esc(device.id)}"${locked}>${esc(fanLabel(fan))}</button>`).join('')}</div>
|
||||
@@ -102,6 +108,24 @@ function zoneLockoutActive(zone) {
|
||||
return Number.isFinite(until) && until>Date.now();
|
||||
}
|
||||
|
||||
function compressorPendingDescription(zone) {
|
||||
const action=String(zone?.compressor_pending_action || '');
|
||||
if(!action) return '';
|
||||
const [kind, mode, target] = action.split(':');
|
||||
const targetValue = Number(target);
|
||||
if(kind==='mode_change') return tr('zones.queuedModeChange', {mode:modeLabel(mode), temperature:Number.isFinite(targetValue)?targetValue.toFixed(1):'—'});
|
||||
if(kind==='power_on') return tr('zones.queuedPowerOn', {mode:modeLabel(mode), temperature:Number.isFinite(targetValue)?targetValue.toFixed(1):'—'});
|
||||
return tr('zones.queuedAction');
|
||||
}
|
||||
|
||||
function compressorQueuePanel(zone) {
|
||||
if(!zone?.compressor_pending_action) return '';
|
||||
const until=zone.compressor_pending_until || zone.lockout_until;
|
||||
const untilMs=until ? new Date(until).getTime() : NaN;
|
||||
const when=Number.isFinite(untilMs) && untilMs>Date.now() ? dateTime(until) : tr('zones.afterProtection');
|
||||
return `<div class="compressor-queue-panel"><div><strong>${esc(tr('zones.compressorQueueTitle'))}</strong><span>${esc(compressorPendingDescription(zone))}</span><small>${esc(tr('zones.compressorQueueUntil', {time:when}))}</small></div><button type="button" class="secondary" data-action="cancel-compressor-task" data-id="${esc(zone.id)}">${esc(tr('actions.cancel'))}</button></div>`;
|
||||
}
|
||||
|
||||
function zoneControlOwnerLabel(zone) {
|
||||
const owner=zone.control_owner || (zone.device_manual_override ? 'direct_manual' : (zone.local_thermostat_power != null ? 'local_thermostat' : 'automation'));
|
||||
const source=zone.control_source || '';
|
||||
@@ -134,7 +158,8 @@ function zoneRuntimeStatusLabel(zone, effectiveMode, device = null) {
|
||||
if (device?.enabled === false) return tr('zones.waitingDeviceDisabled');
|
||||
if (device && (!device.online || Number(device.communication_failures || 0) > 0)) return tr('zones.waitingOffline');
|
||||
if (zone.current_temperature == null) return tr('zones.noMeasurement');
|
||||
if (zone.demand && zoneLockoutActive(zone)) return tr('zones.waitingLockout');
|
||||
if (zone.compressor_pending_action && zoneLockoutActive(zone)) return tr('zones.waitingLockout');
|
||||
if (zone.compressor_cancelled_action && device && (!device.power || (['heat','cool'].includes(effectiveMode) && device.mode !== effectiveMode))) return tr('zones.queueCancelledStatus');
|
||||
if (zone.demand && device && !device.power) return tr('zones.waitingStart');
|
||||
if (zone.demand && device?.power && ['heat','cool'].includes(effectiveMode) && device.mode !== effectiveMode) return tr('zones.waitingMode');
|
||||
return zone.demand ? tr('zones.runningDemand') : tr('zones.satisfied');
|
||||
@@ -158,7 +183,7 @@ function zoneCard(zone, detailed = true) {
|
||||
const deviceManualOverride = zone.device_manual_override === true;
|
||||
const controlGroup = groupControlForZone(zone);
|
||||
const deviceUnavailable = !!device && (device.enabled === false || !device.online || Number(device.communication_failures || 0) > 0);
|
||||
const lockoutWaiting = zone.demand && zoneLockoutActive(zone);
|
||||
const lockoutWaiting = !!zone.compressor_pending_action && zoneLockoutActive(zone);
|
||||
const startWaiting = zone.demand && !!device && !deviceUnavailable && !device.power;
|
||||
const modeWaiting = zone.demand && !!device?.power && !deviceUnavailable && ['heat','cool'].includes(effectiveMode) && device.mode !== effectiveMode;
|
||||
const visualGroup = controlGroup;
|
||||
@@ -211,6 +236,7 @@ function zoneCard(zone, detailed = true) {
|
||||
<div><small>${esc(tr('zones.smartFan'))}</small><strong>${esc(tr(zone.smart_fan === false ? 'common.off' : 'common.on'))}</strong></div>
|
||||
</div>
|
||||
<div class="sensor-detail">${esc(sensorDetails)}</div>
|
||||
${compressorQueuePanel(zone)}
|
||||
${manualTakeover}
|
||||
<div class="card-footer"><small>${esc(tr('zones.controlOnDashboard'))}</small><div class="card-menu"><button class="primary" data-action="zone-go-control" data-id="${esc(zone.id)}">${esc(tr('zones.controlNow'))}</button><button data-action="edit-zone" data-id="${esc(zone.id)}">${esc(tr('actions.edit'))}</button><button class="danger" data-action="delete-zone" data-id="${esc(zone.id)}">${esc(tr('actions.delete'))}</button></div></div>
|
||||
</article>`;
|
||||
@@ -226,6 +252,7 @@ function zoneCard(zone, detailed = true) {
|
||||
<div class="mode-row zone-mode-row quick-control-row quick-control-row-3"><button class="${mode==='house'&&globalModeAvailable?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="house"${globalModeDisabled}>${esc(tr('zones.followHouseShort'))}</button><button class="${mode==='heat'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="heat">${esc(modeLabel('heat'))}</button><button class="${mode==='cool'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="cool">${esc(modeLabel('cool'))}</button></div>
|
||||
<div class="zone-state-line"><span class="zone-runtime-status" title="${esc(runtimeStatus)}">${esc(runtimeStatus)}${controlGroup ? ` <b class="group-control-tag">${esc(controlGroup.name)}</b>` : ''}</span><span>${esc(override)}</span></div>
|
||||
<div class="zone-state-line control-owner-line"><span><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}</span><span>${esc(zoneControlOwnerMeta(zone))}</span></div>
|
||||
${compressorQueuePanel(zone)}
|
||||
${manualTakeover}
|
||||
</article>`;
|
||||
}
|
||||
@@ -237,6 +264,11 @@ function renderZones() {
|
||||
if (dashboard) dashboard.innerHTML = app.zones.length ? app.zones.map(zone => zoneCard(zone, false)).join('') : empty;
|
||||
const dashboardCount = $('#dashboardZoneCount');
|
||||
if (dashboardCount) dashboardCount.textContent = String(app.zones.length);
|
||||
const pendingCount = app.zones.filter(zone => !!zone.compressor_pending_action).length;
|
||||
$$('[data-compressor-cancel-all]').forEach(cancelAll => {
|
||||
cancelAll.hidden = pendingCount === 0;
|
||||
cancelAll.textContent = pendingCount ? `${tr('zones.cancelAllQueued')} (${pendingCount})` : tr('zones.cancelAllQueued');
|
||||
});
|
||||
}
|
||||
|
||||
function groupZones(group) {
|
||||
|
||||
Reference in New Issue
Block a user