v0.15.18
This commit is contained in:
1 parent
0148daf410
commit
f08613a254
23 files changed
+290
-70
No files matched your search
@@ -4895,6 +4895,90 @@ body.simulation-standalone [data-view="simulation"] {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
#controlPlan .plan-recent-trigger {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace {
|
||||
grid-column: 1 / -1;
|
||||
min-width: 0;
|
||||
margin-top: 2px;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace summary {
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace-list {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace-row {
|
||||
display: grid;
|
||||
grid-template-columns: 18px minmax(150px, 1fr) minmax(110px, .85fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 5px 7px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace-row>div {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace-row strong {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace-row small {
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: 9px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace-row code {
|
||||
overflow: hidden;
|
||||
color: var(--text-soft);
|
||||
font-size: 9px;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace-row.pass .plan-trigger-trace-mark {
|
||||
color: var(--accent-text);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace-row.fail .plan-trigger-trace-mark {
|
||||
color: var(--warning);
|
||||
background: var(--warning-soft);
|
||||
}
|
||||
|
||||
#controlPlan .plan-reason-alert {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
@@ -10939,4 +11023,13 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr
|
||||
grid-template-columns: 1fr;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace-row {
|
||||
grid-template-columns: 18px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
#controlPlan .plan-trigger-trace-row code {
|
||||
grid-column: 2;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,86 @@ function planZoneReason(zone) {
|
||||
return tr('plan.reasonWaiting');
|
||||
}
|
||||
|
||||
|
||||
function planFlowTraceNode(rule, item) {
|
||||
const flow = rule.flow_id ? (app.flows || []).find(candidate => candidate.id === rule.flow_id) : null;
|
||||
const current = flow?.nodes?.find(node => node.id === item.node_id);
|
||||
return {
|
||||
id: item.node_id || current?.id || '',
|
||||
kind: item.kind || current?.kind || '',
|
||||
config: item.config && typeof item.config === 'object' ? item.config : (current?.config || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function planFlowTraceSourceLabel(node) {
|
||||
const c = node?.config || {};
|
||||
if (!['rolling_stat', 'oscillates'].includes(node?.kind)) return '';
|
||||
if (c.source === 'outdoor_temperature') return tr('house.outdoor');
|
||||
if (c.source === 'device_temperature') return (app.devices || []).find(device => device.id === c.device_id)?.name || c.device_id || '';
|
||||
if (c.source === 'zone_temperature') return (app.zones || []).find(zone => zone.id === c.zone_id)?.name || c.zone_id || '';
|
||||
if (c.source === 'ha_numeric') return c.entity_id ? haSensorLabel(c.entity_id) : '';
|
||||
return c.source || '';
|
||||
}
|
||||
|
||||
function planFlowTraceScalar(node, value) {
|
||||
const formatted = flowSimpleSimulationActualText(node, value);
|
||||
if (formatted) return formatted;
|
||||
if (value === null || value === undefined) return '—';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function planFlowTraceActual(rule, item, node) {
|
||||
const actual = item?.actual;
|
||||
if (!actual || typeof actual !== 'object' || Array.isArray(actual)) return planFlowTraceScalar(node, actual);
|
||||
if (actual.error) return `${actual.error}${actual.entity_id ? ` · ${actual.entity_id}` : ''}`;
|
||||
if (node.kind === 'stable_for' || node.kind === 'delay') {
|
||||
const since = actual.since ? new Date(actual.since).toLocaleTimeString(locale(), { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '—';
|
||||
return `${actual.input ? tr('common.yes') : tr('common.no')} · ≥${flowDurationLabel(actual.seconds)} · ${since}`;
|
||||
}
|
||||
if (node.kind === 'state_duration') {
|
||||
const max = actual.max_seconds == null ? '∞' : flowDurationLabel(actual.max_seconds);
|
||||
return `${flowDurationLabel(actual.elapsed_seconds)} · ${flowDurationLabel(actual.min_seconds)}–${max}`;
|
||||
}
|
||||
if (node.kind === 'on_change') {
|
||||
const previous = actual.previous === null || actual.previous === undefined ? '—' : planFlowTraceScalar(node, actual.previous);
|
||||
const current = actual.current === null || actual.current === undefined ? '—' : planFlowTraceScalar(node, actual.current);
|
||||
return `${previous} → ${current}`;
|
||||
}
|
||||
if (node.kind === 'rate_limit') return `${Number(actual.used || 0)}/${Number(actual.max_count || 0)} · ${flowDurationLabel(actual.period_seconds)}`;
|
||||
if (node.kind === 'rolling_stat') {
|
||||
const sourceKind = node.config?.source;
|
||||
const tempSource = ['outdoor_temperature', 'device_temperature', 'zone_temperature'].includes(sourceKind);
|
||||
const aggregate = actual.aggregate == null ? '—' : tempSource ? fmtTemp(Number(actual.aggregate)) : Number(actual.aggregate).toFixed(2);
|
||||
const sample = actual.sample == null ? '—' : tempSource ? fmtTemp(Number(actual.sample)) : Number(actual.sample).toFixed(2);
|
||||
return `${aggregate} · x=${sample} · n=${Number(actual.samples || 0)}`;
|
||||
}
|
||||
if (node.kind === 'oscillates') {
|
||||
const span = actual.span == null ? '—' : Number(actual.span).toFixed(2);
|
||||
return `Δ=${span} · ↕=${Number(actual.direction_changes || 0)} · n=${Number(actual.samples || 0)}`;
|
||||
}
|
||||
if (node.kind === 'logic_and' || node.kind === 'logic_or') {
|
||||
return (Array.isArray(actual) ? actual : []).map(value => value ? tr('common.yes') : tr('common.no')).join(' · ') || '—';
|
||||
}
|
||||
if (node.kind === 'logic_not') return actual ? tr('common.yes') : tr('common.no');
|
||||
return JSON.stringify(actual);
|
||||
}
|
||||
|
||||
function planFlowTriggerTrace(rule, expanded = false) {
|
||||
const trace = Array.isArray(rule.last_trigger_trace) ? rule.last_trigger_trace : [];
|
||||
if (rule.trigger_kind !== 'flow' || !trace.length) return '';
|
||||
const rows = trace.map(item => {
|
||||
const node = planFlowTraceNode(rule, item);
|
||||
const meta = FLOW_NODE_META[node.kind] || { title: node.kind || item.node_id || 'Flow' };
|
||||
const source = planFlowTraceSourceLabel(node);
|
||||
const summary = [source, flowNodeSummary(node)].filter(Boolean).join(' · ');
|
||||
const actual = planFlowTraceActual(rule, item, node);
|
||||
const matched = item.matched === true;
|
||||
return `<div class="plan-trigger-trace-row ${matched ? 'pass' : 'fail'}"><span class="plan-trigger-trace-mark" aria-hidden="true">${matched ? '✓' : '×'}</span><div><strong>${esc(flowNodeTitle(meta))}</strong>${summary ? `<small>${esc(summary)}</small>` : ''}</div><code>${esc(actual)}</code></div>`;
|
||||
}).join('');
|
||||
return `<details class="plan-trigger-trace" ${expanded ? 'open' : ''}><summary>${esc(tr('plan.reasonTriggerDetails', { count: trace.length }))}</summary><div class="plan-trigger-trace-list">${rows}</div></details>`;
|
||||
}
|
||||
|
||||
function planReasonCard(plan) {
|
||||
const zones = (plan.zones || []).map(zone => {
|
||||
const reason = planZoneReason(zone);
|
||||
@@ -118,14 +198,15 @@ function planReasonCard(plan) {
|
||||
.filter(rule => rule.enabled && rule.last_fired_at)
|
||||
.sort((a, b) => new Date(b.last_fired_at) - new Date(a.last_fired_at))
|
||||
.slice(0, 4)
|
||||
.map(rule => {
|
||||
.map((rule, index) => {
|
||||
const when = new Date(rule.last_fired_at).toLocaleString(locale(), { weekday: 'short', hour: '2-digit', minute: '2-digit' });
|
||||
const triggerDevice = rule.trigger_device_id ? (app.devices || []).find(device => device.id === rule.trigger_device_id) : null;
|
||||
const current = triggerDevice?.current_temperature == null ? null : Number(triggerDevice.current_temperature);
|
||||
const trigger = Number.isFinite(current)
|
||||
? tr('plan.reasonTriggerReading', { trigger: automationTriggerLabel(rule), current: fmtTemp(current) })
|
||||
: automationTriggerLabel(rule);
|
||||
return `<li><div><strong>${esc(rule.name)}</strong><small>${esc(trigger)}</small></div><span>${esc(tr('plan.reasonLastTriggered', { when }))}</span></li>`;
|
||||
const trace = planFlowTriggerTrace(rule, index === 0);
|
||||
return `<li class="plan-recent-trigger"><div><strong>${esc(rule.name)}</strong><small>${esc(trigger)}</small></div><span>${esc(tr('plan.reasonLastTriggered', { when }))}</span>${trace}</li>`;
|
||||
}).join('');
|
||||
|
||||
const emergency = plan.emergency_stop_active
|
||||
|
||||
Reference in new issue
Block a user