v0.6.5
This commit is contained in:
+54
-25
@@ -43,6 +43,7 @@ const tr = (key, params = {}) => {
|
||||
const fmtTemp = value => value !== null && value !== undefined && value !== '' && Number.isFinite(Number(value)) ? `${Number(value).toFixed(1)}°C` : '--';
|
||||
const historyNumber = value => value === null || value === undefined || value === '' ? NaN : Number(value);
|
||||
const modeLabel = mode => tr(`mode.${mode}`) === `mode.${mode}` ? mode : tr(`mode.${mode}`);
|
||||
const houseModeLabel = mode => mode === 'off' ? tr('house.noControl') : modeLabel(mode);
|
||||
const fanLabel = value => ({0:'fan.auto',1:'fan.low',2:'fan.mediumLow',3:'fan.medium',4:'fan.mediumHigh',5:'fan.high'}[value] ? tr({0:'fan.auto',1:'fan.low',2:'fan.mediumLow',3:'fan.medium',4:'fan.mediumHigh',5:'fan.high'}[value]) : value);
|
||||
const dateTime = value => value ? new Intl.DateTimeFormat(locale(), {dateStyle:'short', timeStyle:'short'}).format(new Date(value)) : '—';
|
||||
const haSensorLabel = entity => app.sensorAliases?.[entity] || app.settings?.home_assistant?.sensor_aliases?.[entity] || entity;
|
||||
@@ -246,14 +247,20 @@ function renderSummary() {
|
||||
function renderHouseClimate() {
|
||||
const node = $('#houseClimate'); if (!node || !app.settings) return;
|
||||
const mode = app.settings.house_mode || 'cool';
|
||||
const zonePresets = (app.zones || []).map(zone => zone.manual_preset || 'auto');
|
||||
const preset = zonePresets.length && zonePresets.every(value => value === zonePresets[0]) ? zonePresets[0] : null;
|
||||
const outdoor = app.outdoorTemperature == null ? tr('common.unavailable') : fmtTemp(app.outdoorTemperature);
|
||||
const powerEnabled = app.settings.house_power_enabled !== false;
|
||||
const powerOn = $('#housePowerOn'), powerOff = $('#housePowerOff');
|
||||
if (powerOn) { powerOn.classList.toggle('active', powerEnabled); powerOn.setAttribute('aria-pressed', String(powerEnabled)); }
|
||||
if (powerOff) { powerOff.classList.toggle('active', !powerEnabled); powerOff.setAttribute('aria-pressed', String(!powerEnabled)); }
|
||||
node.innerHTML = `<div class="house-climate-head"><div><span class="eyebrow">${esc(tr('house.seasonMode'))}</span><h3>${esc(tr('house.smartThermostat'))}</h3><p>${esc(tr('house.setpointStrategy'))}</p></div><div class="outside-pill"><small>${esc(tr('house.outdoor'))}</small><strong>${esc(outdoor)}</strong></div></div>
|
||||
<div class="house-mode-row">
|
||||
<button class="${mode==='cool'?'active':''}" data-action="house-mode" data-value="cool" aria-pressed="${mode==='cool'}"><span class="mode-indicator"></span>${esc(tr('mode.cool'))}</button>
|
||||
<button class="${mode==='heat'?'active':''}" data-action="house-mode" data-value="heat" aria-pressed="${mode==='heat'}"><span class="mode-indicator"></span>${esc(tr('mode.heat'))}</button>
|
||||
<button class="${mode==='off'?'active':''}" data-action="house-mode" data-value="off" aria-pressed="${mode==='off'}"><span class="mode-indicator"></span>${esc(tr('mode.off'))}</button>
|
||||
<button class="${mode==='cool'?'active':''}" data-action="house-mode" data-value="cool" aria-pressed="${mode==='cool'}">${esc(tr('mode.cool'))}</button>
|
||||
<button class="${mode==='heat'?'active':''}" data-action="house-mode" data-value="heat" aria-pressed="${mode==='heat'}">${esc(tr('mode.heat'))}</button>
|
||||
<button class="${mode==='off'?'active':''}" data-action="house-mode" data-value="off" aria-pressed="${mode==='off'}">${esc(tr('house.noControl'))}</button>
|
||||
</div>
|
||||
<div class="preset-row house-preset-row">${['auto','comfort','sleep','away'].map(p=>`<button data-action="house-preset" data-value="${p}">${esc(p==='sleep'?tr('house.sleepAll'):p==='comfort'?tr('house.comfortAll'):p==='away'?tr('house.awayAll'):tr('house.autoAll'))}</button>`).join('')}</div>`;
|
||||
<div class="preset-row house-preset-row">${['auto','comfort','sleep','away'].map(p=>`<button class="${preset===p?'active':''}" data-action="house-preset" data-value="${p}" aria-pressed="${preset===p}">${esc(p==='sleep'?tr('house.sleepAll'):p==='comfort'?tr('house.comfortAll'):p==='away'?tr('house.awayAll'):tr('house.autoAll'))}</button>`).join('')}</div>`;
|
||||
}
|
||||
|
||||
function planEventMarkup(event) {
|
||||
@@ -276,11 +283,11 @@ function renderControlPlan() {
|
||||
return;
|
||||
}
|
||||
const houseEvents = (plan.next_events || []).slice(0, 5);
|
||||
const house = `<article class="panel plan-card plan-house"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.house'))}</span><h3>${esc(modeLabel(plan.house_mode || 'off'))}</h3></div><span class="badge active">${esc(plan.control_strategy || 'setpoint')}</span></div><p>${esc(tr('plan.houseSummary', {zones:(plan.zones || []).filter(zone=>zone.enabled).length, demand:(plan.zones || []).filter(zone=>zone.enabled && zone.demand).length}))}</p><ul class="plan-events">${houseEvents.length ? houseEvents.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
|
||||
const house = `<article class="panel plan-card plan-house"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.house'))}</span><h3>${esc(houseModeLabel(plan.house_mode || 'off'))}</h3></div><span class="badge active">${esc(plan.control_strategy || 'setpoint')}</span></div><p>${esc(tr('plan.houseSummary', {zones:(plan.zones || []).filter(zone=>zone.enabled).length, demand:(plan.zones || []).filter(zone=>zone.enabled && zone.demand).length}))}</p><ul class="plan-events">${houseEvents.length ? houseEvents.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
|
||||
const zones = (plan.zones || []).map(zone => {
|
||||
const events = (zone.next_events || []).slice(0, 3);
|
||||
const target = zone.target_temperature == null ? '--' : Number(zone.target_temperature).toFixed(1);
|
||||
return `<article class="panel plan-card ${zone.enabled ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(zone.device_name || tr('common.noDevice'))}</span><h3>${esc(zone.zone_name)}</h3></div><span class="badge ${zone.enabled && zone.demand ? 'active' : ''}">${esc(zone.enabled ? (zone.demand ? tr('zones.requesting') : tr('zones.satisfied')) : tr('common.disabled'))}</span></div><div class="plan-temp"><span>${fmtTemp(zone.current_temperature)}</span><b>→</b><strong>${esc(target)}<small>°C</small></strong></div><p>${esc(modeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}</p><ul class="plan-events">${events.length ? events.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
|
||||
return `<article class="panel plan-card ${zone.enabled ? '' : 'disabled'}"><div class="plan-card-head"><div><span class="eyebrow">${esc(zone.device_name || tr('common.noDevice'))}</span><h3>${esc(zone.zone_name)}</h3></div><span class="badge ${zone.enabled && zone.demand ? 'active' : ''}">${esc(zone.enabled ? (zone.demand ? tr('zones.requesting') : tr('zones.satisfied')) : tr('common.disabled'))}</span></div><div class="plan-temp"><span>${fmtTemp(zone.current_temperature)}</span><b>→</b><strong>${esc(target)}<small>°C</small></strong></div><p>${esc(houseModeLabel(zone.mode || 'off'))} · ${esc(zonePresetLabel(zone.preset))}${zone.current_schedule_name ? ` · ${esc(zone.current_schedule_name)}` : ''}</p><ul class="plan-events">${events.length ? events.map(planEventMarkup).join('') : `<li class="muted">${esc(tr('plan.noEvents'))}</li>`}</ul></article>`;
|
||||
}).join('');
|
||||
const rules = (plan.rules || []).filter(rule => rule.enabled);
|
||||
const ruleCard = rules.length ? `<article class="panel plan-card"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr('plan.ruleCount', {count:rules.length}))}</h3></div></div><ul class="plan-events">${rules.slice(0,5).map(rule=>`<li><time>${esc(automationTriggerLabel(rule))}</time><span>${esc(rule.name)} → ${esc(rule.action_device_name || '')}</span></li>`).join('')}</ul></article>` : '';
|
||||
@@ -330,7 +337,7 @@ function simulationSmartFanSpeed(mode, room, target, outdoor, demand) {
|
||||
function simulationStatusInfo(status) {
|
||||
const map = {
|
||||
disabled: {label: tr('common.disabled'), badge: ''},
|
||||
off: {label: tr('mode.off'), badge: ''},
|
||||
off: {label: tr('house.noControl'), badge: ''},
|
||||
waiting: {label: tr('simulation.waitingForData'), badge: ''},
|
||||
demand: {label: tr('simulation.stateDemand'), badge: 'active'},
|
||||
satisfied: {label: tr('simulation.stateSatisfied'), badge: ''},
|
||||
@@ -342,7 +349,7 @@ function buildZoneSimulation(zone, planZone) {
|
||||
const device = app.devices.find(item => item.id === (planZone?.device_id || zone?.device_id));
|
||||
const mode = planZone?.mode || zone?.effective_mode || zone?.mode || app.settings?.house_mode || 'off';
|
||||
const current = simulationNumeric(planZone?.current_temperature ?? zone?.current_temperature ?? zone?.device_temperature ?? device?.current_temperature);
|
||||
const target = simulationNumeric(planZone?.target_temperature ?? zone?.effective_setpoint ?? zone?.manual_setpoint ?? zone?.setpoint);
|
||||
const target = mode === 'off' ? null : simulationNumeric(planZone?.target_temperature ?? zone?.effective_setpoint ?? zone?.manual_setpoint ?? zone?.setpoint);
|
||||
const outdoor = simulationNumeric(app.outdoorTemperature ?? app.controlPlan?.outdoor_temperature);
|
||||
const hysteresis = Math.max(simulationNumeric(zone?.hysteresis) || 0.4, 0.1);
|
||||
const half = hysteresis / 2;
|
||||
@@ -370,15 +377,20 @@ function buildZoneSimulation(zone, planZone) {
|
||||
const desiredDeviceTarget = current == null || target == null ? null : simulationRoundDeviceSetpoint(mode, demand, demand ? activeTarget : standbyTarget);
|
||||
const nightActive = !!app.controlPlan?.night_mode_active;
|
||||
const nightMaxFan = clamp(Number(app.controlPlan?.night_mode_max_fan_speed || 1), 1, 5);
|
||||
let fanSpeed = zone?.smart_fan && current != null && target != null ? simulationSmartFanSpeed(mode, current, target, outdoor, demand) : simulationNumeric(device?.fan_speed);
|
||||
if (nightActive) {
|
||||
if (zone?.smart_fan) fanSpeed = fanSpeed === 0 ? 1 : Math.min(fanSpeed ?? nightMaxFan, nightMaxFan);
|
||||
else if (fanSpeed === 0 || fanSpeed == null || fanSpeed > nightMaxFan) fanSpeed = nightMaxFan;
|
||||
let fanSpeed = simulationNumeric(device?.fan_speed);
|
||||
if (mode !== 'off') {
|
||||
fanSpeed = zone?.smart_fan && current != null && target != null ? simulationSmartFanSpeed(mode, current, target, outdoor, demand) : fanSpeed;
|
||||
if (nightActive) {
|
||||
if (zone?.smart_fan) fanSpeed = fanSpeed === 0 ? 1 : Math.min(fanSpeed ?? nightMaxFan, nightMaxFan);
|
||||
else if (fanSpeed === 0 || fanSpeed == null || fanSpeed > nightMaxFan) fanSpeed = nightMaxFan;
|
||||
}
|
||||
}
|
||||
const quiet = nightActive && app.settings?.night_mode?.force_quiet
|
||||
? tr('simulation.quietIfSupported')
|
||||
: (zone?.smart_fan ? (!demand ? tr('simulation.quietIfSupported') : tr('common.off')) : tr('simulation.manual'));
|
||||
const nativeSleep = nightActive && app.settings?.night_mode?.use_native_sleep && device?.supports_sleep === true;
|
||||
const quiet = mode === 'off'
|
||||
? tr('simulation.manual')
|
||||
: nightActive && app.settings?.night_mode?.force_quiet
|
||||
? tr('simulation.quietIfSupported')
|
||||
: (zone?.smart_fan ? (!demand ? tr('simulation.quietIfSupported') : tr('common.off')) : tr('simulation.manual'));
|
||||
const nativeSleep = mode !== 'off' && nightActive && app.settings?.night_mode?.use_native_sleep && device?.supports_sleep === true;
|
||||
const reasoning = !zone?.enabled || planZone?.enabled === false
|
||||
? tr('simulation.reasonDisabled')
|
||||
: mode === 'off'
|
||||
@@ -416,7 +428,7 @@ function renderSimulationPage() {
|
||||
const demandingZones = enabledZones.filter(zone => zone.demand);
|
||||
const nightOn = !!plan.night_mode_active;
|
||||
summaryHost.innerHTML = [
|
||||
{label: tr('simulation.houseMode'), value: modeLabel(plan.house_mode || 'off'), note: tr('simulation.strategy', {strategy: plan.control_strategy || 'setpoint'})},
|
||||
{label: tr('simulation.houseMode'), value: houseModeLabel(plan.house_mode || 'off'), note: tr('simulation.strategy', {strategy: plan.control_strategy || 'setpoint'})},
|
||||
{label: tr('settings.nightMode'), value: nightOn ? tr('common.active') : (app.settings?.night_mode?.enabled ? tr('simulation.scheduled') : tr('common.disabled')), note: `${plan.night_mode_start || '22:00'} → ${plan.night_mode_end || '06:00'} · ${tr('simulation.fanMax')} ${fanLabel(plan.night_mode_max_fan_speed || 1)}`},
|
||||
{label: tr('simulation.outdoor'), value: plan.outdoor_temperature == null ? '—' : fmtTemp(plan.outdoor_temperature), note: tr('simulation.generatedAt', {time: dateTime(plan.generated_at)})},
|
||||
{label: tr('simulation.activeZones'), value: String(enabledZones.length), note: `${tr('simulation.requestingZones', {count: demandingZones.length})} · ${tr('simulation.rulesLabel')}: ${(plan.rules || []).filter(rule => rule.enabled).length}`},
|
||||
@@ -436,7 +448,7 @@ function renderSimulationPage() {
|
||||
const node = ({left, top, kind, eyebrow, title, value, meta='', badge='', badgeClass=''}) => `<article class="flow-node ${kind}" style="left:${left}px;top:${top}px;width:${nodeW}px;min-height:${nodeH}px"><div class="flow-node-top"><span>${esc(eyebrow)}</span>${badge ? `<b class="flow-node-badge ${badgeClass}">${esc(badge)}</b>` : ''}</div><h3>${esc(title)}</h3><strong>${esc(value)}</strong>${meta ? `<p>${esc(meta)}</p>` : ''}<i class="flow-port in"></i><i class="flow-port out"></i></article>`;
|
||||
|
||||
const globalHouseLeft = 305, globalNightLeft = 565, globalTop = 28;
|
||||
nodes.push(node({left:globalHouseLeft,top:globalTop,kind:'logic global',eyebrow:tr('simulation.globalInput'),title:tr('simulation.houseMode'),value:modeLabel(plan.house_mode || 'off'),meta:tr('simulation.strategy',{strategy:plan.control_strategy || 'setpoint'}),badge:tr('simulation.house')}));
|
||||
nodes.push(node({left:globalHouseLeft,top:globalTop,kind:'logic global',eyebrow:tr('simulation.globalInput'),title:tr('simulation.houseMode'),value:houseModeLabel(plan.house_mode || 'off'),meta:tr('simulation.strategy',{strategy:plan.control_strategy || 'setpoint'}),badge:tr('simulation.house')}));
|
||||
nodes.push(node({left:globalNightLeft,top:globalTop,kind:`logic global ${nightOn?'night-active':''}`,eyebrow:tr('simulation.globalInput'),title:tr('settings.nightMode'),value:nightOn?tr('common.active'):(app.settings?.night_mode?.enabled?tr('simulation.scheduled'):tr('common.disabled')),meta:`${plan.night_mode_start || '22:00'} → ${plan.night_mode_end || '06:00'} · ${fanLabel(plan.night_mode_max_fan_speed || 1)}`,badge:nightOn?'☾':'○',badgeClass:nightOn?'active':''}));
|
||||
|
||||
zones.forEach((planZone, index) => {
|
||||
@@ -458,10 +470,10 @@ function renderSimulationPage() {
|
||||
|
||||
nodes.push(`<div class="flow-lane-label" style="top:${y-27}px"><strong>${esc(planZone.zone_name)}</strong><span>${esc(planZone.device_name || '')}</span></div>`);
|
||||
nodes.push(node({left:x.sensor,top:y,kind:'input',eyebrow:tr('simulation.stepRoom'),title:sensorName,value:fmtTemp(sim.current),meta:source,badge:'●',badgeClass:sim.current == null?'':'active'}));
|
||||
nodes.push(node({left:x.thermostat,top:y,kind:'logic',eyebrow:tr('simulation.thermostat'),title:planZone.zone_name,value:sim.target == null?'—':fmtTemp(sim.target),meta:`${zonePresetLabel(planZone.preset)} · ${tr('simulation.hysteresis',{value:sim.hysteresis.toFixed(1)})}`,badge:modeLabel(sim.mode)}));
|
||||
nodes.push(node({left:x.thermostat,top:y,kind:'logic',eyebrow:tr('simulation.thermostat'),title:planZone.zone_name,value:sim.target == null?'—':fmtTemp(sim.target),meta:`${zonePresetLabel(planZone.preset)} · ${tr('simulation.hysteresis',{value:sim.hysteresis.toFixed(1)})}`,badge:houseModeLabel(sim.mode)}));
|
||||
nodes.push(node({left:x.decision,top:y,kind:`logic decision ${sim.demand?'demand':'satisfied'}`,eyebrow:tr('simulation.stepDecision'),title:status.label,value:sim.demand?tr('simulation.callForComfort'):tr('simulation.standby'),meta:sim.reasoning,badge:sim.demand?'▶':'✓',badgeClass:status.badge}));
|
||||
const sleepMeta = sim.nativeSleep ? ` · ${tr('devices.sleep')}: ${tr('common.on')}` : '';
|
||||
nodes.push(node({left:x.unit,top:y,kind:'action',eyebrow:tr('simulation.stepCommand'),title:planZone.device_name || tr('common.device'),value:command,meta:`${tr('devices.quiet')}: ${quietText}${sleepMeta}`,badge:sim.demand?'RUN':'IDLE',badgeClass:sim.demand?'active':''}));
|
||||
nodes.push(node({left:x.unit,top:y,kind:'action',eyebrow:tr('simulation.stepCommand'),title:planZone.device_name || tr('common.device'),value:command,meta:`${tr('devices.quiet')}: ${quietText}${sleepMeta}`,badge:sim.mode==='off'?tr('simulation.manual'):(sim.demand?'RUN':'IDLE'),badgeClass:sim.demand?'active':''}));
|
||||
nodes.push(node({left:x.event,top:y,kind:'event',eyebrow:tr('simulation.nextEvent'),title:eventValue,value:next?.preset ? zonePresetLabel(next.preset) : tr('simulation.schedule'),meta:eventMeta,badge:'›'}));
|
||||
|
||||
links.push(pathBetween(x.sensor+nodeW,mid,x.thermostat,mid,'input-link'));
|
||||
@@ -514,9 +526,10 @@ function deviceFeaturePanel(device) {
|
||||
function deviceCard(device, detailed = false) {
|
||||
const modes = ['auto','cool','dry','fan','heat'];
|
||||
const fans = [0,1,3,5];
|
||||
const error = device.last_error
|
||||
? `<small title="${esc(device.last_error)}">${esc(device.last_error)}</small>`
|
||||
: `<small>${esc(device.ip)} · ${device.simulated ? tr('devices.simulator') : device.protocol_version === 2 ? 'V2 GCM' : device.protocol_version === 1 ? 'V1 ECB' : tr('devices.protocolAuto')}</small>`;
|
||||
const hasResponseTime = device.response_time_ms !== null && device.response_time_ms !== undefined && Number.isFinite(Number(device.response_time_ms));
|
||||
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>`;
|
||||
return `<article class="device-card ${device.power ? '' : 'off'}" data-device-card="${esc(device.id)}">
|
||||
<div class="device-head">
|
||||
<div class="device-title"><h3>${esc(device.name)}</h3><p><span class="status ${device.online ? 'online' : ''}">${esc(tr(device.online ? 'status.online' : 'status.offline'))}</span> · ${esc(device.model || device.mac)}</p></div>
|
||||
@@ -536,7 +549,7 @@ function deviceCard(device, detailed = false) {
|
||||
${device.supports_turbo === false ? '' : `<button class="${device.turbo ? 'active' : ''}" data-action="toggle" data-field="turbo" data-device="${esc(device.id)}">${esc(tr('devices.turbo'))}</button>`}
|
||||
</div>
|
||||
${detailed ? deviceFeaturePanel(device) : ''}
|
||||
${detailed ? `<div class="card-footer">${error}<div class="card-menu"><button data-action="poll" data-device="${esc(device.id)}">${esc(tr('actions.read'))}</button><button data-action="rename-device" data-device="${esc(device.id)}">${esc(tr('devices.nameProtocol'))}</button>${device.simulated ? '' : `<button data-action="bind" data-device="${esc(device.id)}">${esc(tr('actions.bind'))}</button>`}<button class="danger" data-action="delete-device" data-device="${esc(device.id)}">${esc(tr('actions.delete'))}</button></div></div>` : ''}
|
||||
${detailed ? `<div class="card-footer">${networkInfo}<div class="card-menu"><button data-action="poll" data-device="${esc(device.id)}">${esc(tr('actions.read'))}</button><button data-action="rename-device" data-device="${esc(device.id)}">${esc(tr('devices.nameProtocol'))}</button>${device.simulated ? '' : `<button data-action="bind" data-device="${esc(device.id)}">${esc(tr('actions.bind'))}</button>`}<button class="danger" data-action="delete-device" data-device="${esc(device.id)}">${esc(tr('actions.delete'))}</button></div></div>` : ''}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
@@ -1410,8 +1423,23 @@ document.addEventListener('click', async event => {
|
||||
try { app.settings = await api('/api/house/control',{method:'POST',body:{mode:button.dataset.value}}); renderHouseClimate(); scheduleControlPlanLoad(); toast(tr('house.modeUpdated')); }
|
||||
catch(error){ toast(error.message,true); } return;
|
||||
}
|
||||
if (action === 'house-power') {
|
||||
const power = button.dataset.value === 'true';
|
||||
try {
|
||||
button.disabled = true;
|
||||
const result = await api('/api/house/power',{method:'POST',body:{power}});
|
||||
app.settings = result.settings || app.settings;
|
||||
app.devices = result.devices || app.devices;
|
||||
renderAll(); scheduleControlPlanLoad();
|
||||
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 api('/api/house/preset',{method:'POST',body:{preset:button.dataset.value}}); app.zones=result.zones||app.zones; renderZones(); scheduleControlPlanLoad(); toast(tr('house.presetUpdated')); }
|
||||
try { const result=await api('/api/house/preset',{method:'POST',body:{preset:button.dataset.value}}); app.zones=result.zones||app.zones; renderHouseClimate(); renderZones(); scheduleControlPlanLoad(); toast(tr('house.presetUpdated')); }
|
||||
catch(error){ toast(error.message,true); } return;
|
||||
}
|
||||
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; }
|
||||
@@ -1580,6 +1608,7 @@ function currentSettingsBody() {
|
||||
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),
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
</div>
|
||||
<div class="metrics" id="metrics"></div>
|
||||
<div class="panel house-climate" id="houseClimate"></div>
|
||||
<div class="panel house-power-panel">
|
||||
<div><span class="eyebrow" data-i18n="house.powerAllEyebrow">Global power</span><h3 data-i18n="house.powerAllTitle">All air conditioners</h3><p data-i18n="house.powerAllHint">Direct power control for all enabled units, independent from thermostat presets.</p></div>
|
||||
<div class="house-power-actions"><button id="housePowerOn" type="button" class="secondary" data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on</button><button id="housePowerOff" type="button" class="secondary" data-action="house-power" data-value="false" data-i18n="house.powerOffAll">Turn all off</button></div>
|
||||
</div>
|
||||
<div class="section-heading"><div><span class="eyebrow" data-i18n="plan.eyebrow">Control plan</span><h2 data-i18n="plan.title">What happens next</h2></div></div>
|
||||
<div class="automation-plan-grid" id="controlPlan"></div>
|
||||
<div class="section-heading"><div><span class="eyebrow" data-i18n="nav.zones">Zones</span><h2 data-i18n="dashboard.quickThermostats">Quick thermostats</h2></div></div>
|
||||
|
||||
+13
-6
@@ -144,12 +144,14 @@ h3 { margin-bottom: 10px; }
|
||||
.mode-row, .fan-row { display: flex; gap: 7px; overflow-x: auto; padding: 2px 0 10px; scrollbar-width: none; }
|
||||
.mode-row::-webkit-scrollbar, .fan-row::-webkit-scrollbar { display: none; }
|
||||
.mode-row button, .fan-row button { flex: 1 0 auto; min-width: 58px; padding: 9px 10px; color: var(--muted); background: var(--surface-muted); font-size: 12px; }
|
||||
.mode-row button.active, .fan-row button.active { color: var(--accent-text); background: var(--accent); font-weight: 800; }
|
||||
.mode-row button.active, .fan-row button.active, .house-mode-row button.active { color: var(--accent-text); background: var(--accent); font-weight: 800; }
|
||||
.device-toggles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 4px; }
|
||||
.device-toggles button { padding: 9px 5px; color: var(--muted); font-size: 11px; }
|
||||
.device-toggles button.active { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--surface)); }
|
||||
.card-footer { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
|
||||
.card-footer small { overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.device-network-info { display: grid; min-width: 0; gap: 2px; }
|
||||
.device-network-error { color: var(--danger) !important; }
|
||||
.card-menu { display: flex; gap: 5px; }
|
||||
.card-menu button { padding: 7px 9px; background: transparent; color: var(--muted); font-size: 12px; }
|
||||
.list-card { padding: 19px; }
|
||||
@@ -296,7 +298,7 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; }
|
||||
.mode-row, .fan-row, .preset-row { scrollbar-width: none; }
|
||||
.mode-row::-webkit-scrollbar, .fan-row::-webkit-scrollbar, .preset-row::-webkit-scrollbar { display: none; }
|
||||
|
||||
.house-climate { display: grid; gap: 16px; margin: -15px 0 34px; }
|
||||
.house-climate { display: grid; gap: 16px; margin: -15px 0 12px; }
|
||||
.house-climate-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
.house-climate-head h3 { margin: 2px 0 4px; font-size: 20px; }
|
||||
.house-climate-head p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; }
|
||||
@@ -322,10 +324,13 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; }
|
||||
.plan-house { border-color: color-mix(in srgb, var(--accent) 30%, var(--line)); }
|
||||
.plan-loading { grid-column: 1/-1; padding: 18px; color: var(--muted); }
|
||||
.house-mode-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||
.house-mode-row button { display: flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; border: 1px solid transparent; color: var(--muted); background: var(--surface-muted); }
|
||||
.house-mode-row .mode-indicator { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: var(--muted-2); transition: background .15s ease, box-shadow .15s ease, transform .15s ease; }
|
||||
.house-mode-row button.active { border-color: var(--accent); color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, var(--surface)); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent), 0 0 18px color-mix(in srgb, var(--accent) 28%, transparent), inset 0 0 14px color-mix(in srgb, var(--accent) 8%, transparent); font-weight: 800; }
|
||||
.house-mode-row button.active .mode-indicator { background: var(--accent); box-shadow: 0 0 4px var(--accent), 0 0 12px color-mix(in srgb, var(--accent) 82%, transparent); transform: scale(1.12); }
|
||||
.house-mode-row button { min-height: 44px; color: var(--muted); background: var(--surface-muted); }
|
||||
.house-power-panel { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin: 0 0 34px; }
|
||||
.house-power-panel h3 { margin: 2px 0 4px; font-size: 18px; }
|
||||
.house-power-panel p { margin: 0; max-width: 650px; color: var(--muted); font-size: 12px; line-height: 1.45; }
|
||||
.house-power-actions { display: flex; flex: 0 0 auto; gap: 8px; }
|
||||
.house-power-actions button { min-height: 40px; }
|
||||
.house-power-actions button.active { color: var(--accent-text); background: var(--accent); font-weight: 800; }
|
||||
|
||||
.zone-thermostat { overflow: hidden; }
|
||||
.zone-thermostat.demanding { border-color: color-mix(in srgb, var(--accent) 38%, var(--line)); }
|
||||
@@ -358,6 +363,8 @@ legend { padding: 0 5px; color: var(--muted); font-size: 11px; }
|
||||
.house-climate { margin-top: -20px; }
|
||||
.house-climate-head { align-items: flex-start; }
|
||||
.house-climate-head p { max-width: 220px; }
|
||||
.house-power-panel { align-items: stretch; flex-direction: column; }
|
||||
.house-power-actions { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.thermostat-main { grid-template-columns: 1fr auto 1fr; gap: 4px; }
|
||||
.thermostat-main .temperature-control.compact { gap: 9px; }
|
||||
.thermostat-main .target-temp.compact { min-width: 75px; font-size: 28px; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'gree-controller-v064';
|
||||
const CACHE = 'gree-controller-v065';
|
||||
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
|
||||
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/';
|
||||
const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json')];
|
||||
|
||||
Reference in New Issue
Block a user