(function () { "use strict"; const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]; const SAMPLE_INTERVAL_MS = 1000; const MEASURED_STALE_MS = 15000; const HISTORY_LIMIT = 60; const MAX_DEVICE_POWER_W = 100000; const MAX_REPORTED_ENERGY_WH = 1000000000; const ENERGY_MONITORED_DEVICES = Object.freeze([ { id: "passenger", telemetryKeys: ["passenger", "passenger_seat"], name: "副驾座椅", icon: "armchair", phase: 1 }, { id: "driver", telemetryKeys: ["driver", "driver_seat"], name: "主驾座椅", icon: "armchair", phase: 2 }, { id: "climate", telemetryKeys: ["climate", "hvac"], name: "空调", icon: "snowflake", phase: 3 }, { id: "shade", telemetryKeys: ["shade", "roof", "sunshade"], name: "遮阳帘", icon: "panels-top-left", phase: 4 }, { id: "windows", telemetryKeys: ["windows", "window"], name: "车窗", icon: "gallery-vertical-end", phase: 5 }, { id: "reading", telemetryKeys: ["reading", "reading_light", "interior_light"], name: "阅读灯", icon: "book-open", phase: 6 }, { id: "ambient", telemetryKeys: ["ambient", "ambient_light"], name: "氛围灯", icon: "sparkles", phase: 7 }, ]); const SERIES = Object.freeze([ { id: "total", name: "总功率", devices: ENERGY_MONITORED_DEVICES.map(({ id }) => id) }, { id: "climate", name: "空调", devices: ["climate"] }, { id: "seats", name: "座椅", devices: ["passenger", "driver"] }, { id: "lights", name: "灯光", devices: ["reading", "ambient"] }, ]); const aliasToDevice = new Map(); ENERGY_MONITORED_DEVICES.forEach((device) => { device.telemetryKeys.forEach((key) => aliasToDevice.set(key, device.id)); }); const monitor = { mounted: false, open: false, activeSeries: "total", timer: null, startedAt: Date.now(), lastSampleAt: null, frames: [], latest: null, measured: new Map(), reportedEnergy: new Map(), energyWh: Object.fromEntries(ENERGY_MONITORED_DEVICES.map(({ id }) => [id, 0])), motionUntil: new Map(), controlRequests: new Map(), returnFocus: null, backgroundInert: [], }; function finiteNonNegative(value) { if (value === null || value === undefined || value === "") return null; const number = Number(value); return Number.isFinite(number) && number >= 0 ? number : null; } function finitePower(value) { const number = finiteNonNegative(value); return number !== null && number <= MAX_DEVICE_POWER_W ? number : null; } function finiteReportedEnergy(value) { const number = finiteNonNegative(value); return number !== null && number <= MAX_REPORTED_ENERGY_WH ? number : null; } function normalizeSource(value) { return String(value || "").trim().toLowerCase().replace(/[\s-]+/g, "_"); } function telemetrySource(telemetry, payload) { if (telemetry?.simulated === true || payload?.simulated === true) return "simulated"; if (telemetry?.estimated === true || payload?.estimated === true) return "estimated"; const declared = normalizeSource( telemetry?.source || telemetry?.data_source || payload?.source || payload?.data_source, ); if (/(^|_)(simulated|simulation|demo|mock|synthetic|virtual)(_|$)/.test(declared)) { return "simulated"; } if (/(^|_)(estimated|estimate|derived|inferred|calculated|projected|unknown)(_|$)/.test(declared)) { return "estimated"; } if (telemetry?.measured === true || payload?.measured === true) return "measured"; if (/(^|_)(measured|meter|sensor|hardware|physical|vehicle|can|mqtt)(_|$)/.test(declared)) { return "measured"; } // An unlabelled caller is not enough evidence to claim hardware measurement. return "estimated"; } function text(selector, fallback = "") { return $(selector)?.textContent?.trim() || fallback; } function stateFlag(value) { const normalized = String(value || "").trim(); if (/OFF|关闭|停止|未运行/i.test(normalized)) return false; if (/ON|ACTIVE|AMBIENT|INTERIOR|开启|运行|L[1-9]|高|中|低/i.test(normalized)) return true; return null; } function numericText(selector, fallback) { const match = text(selector).match(/-?\d+(?:\.\d+)?/); const value = match ? Number(match[0]) : NaN; return Number.isFinite(value) ? value : fallback; } function deterministicWave(now, phase, amplitude = 0.04) { const seconds = now / 1000; return 1 + Math.sin(seconds / 2.4 + phase * 1.37) * amplitude + Math.sin(seconds / 5.7 + phase * 0.71) * amplitude * 0.45; } function controlIsActive(deviceId, now) { if ((monitor.motionUntil.get(deviceId) || 0) > now) return true; const card = $(`.v2-synced-device[data-device="${deviceId}"]`); return /等待车机同步/.test(card?.querySelector("[data-device-control-apply]")?.textContent || ""); } function estimateDevicePower(device, now) { let watts = 0; let activity = "待机"; const moving = controlIsActive(device.id, now); if (device.id === "climate") { const power = stateFlag(text("#climatePower")); const fan = Math.max(0, Math.min(7, numericText("#fanLevel", 3))); const target = (numericText("#driverTemperature", 24) + numericText("#passengerTemperature", 24)) / 2; if (power === false) { watts = 3.2; activity = "待机"; } else { watts = 390 + fan * 48 + Math.abs(24 - target) * 32; activity = power === true ? `${fan} 档运行` : "状态估算"; } if (moving) { watts += 95; activity = "调节执行中"; } } else if (device.id === "passenger") { const card = $('.seat-card[data-seat="passenger"]'); const ventilation = stateFlag(card?.dataset.ventilationState); const heating = stateFlag(card?.dataset.heatingState); const ventilationLevel = Math.max(1, Math.min(3, Number(card?.dataset.ventilationLevel) || 1)); const heatingLevel = Math.max(1, Math.min(3, Number(card?.dataset.heatingLevel) || 1)); watts = 5.6 + (ventilation ? 7 + ventilationLevel * 7 : 0) + (heating ? 18 + heatingLevel * 16 : 0) + (moving ? 130 : 0); activity = moving ? "执行器动作" : ventilation || heating ? "舒适功能运行" : "待机"; } else if (device.id === "driver") { const card = $('.seat-card[data-seat="driver"]'); const ventilation = stateFlag(card?.dataset.ventilationState || text('.seat-card[data-seat="driver"] [data-function="ventilation"] b')); const heating = stateFlag(card?.dataset.heatingState || text('.seat-card[data-seat="driver"] [data-function="heating"] b')); const massage = stateFlag(card?.dataset.massageState || text('.seat-card[data-seat="driver"] [data-function="massage"] b')); const ventilationLevel = Math.max(1, Math.min(3, Number(card?.dataset.ventilationLevel) || 1)); const heatingLevel = Math.max(1, Math.min(3, Number(card?.dataset.heatingLevel) || 1)); watts = 6.4 + (ventilation ? 7 + ventilationLevel * 7 : 0) + (heating ? 18 + heatingLevel * 16 : 0) + (massage ? 31 : 0) + (moving ? 92 : 0); activity = moving ? "执行器动作" : ventilation || heating || massage ? "舒适功能运行" : "待机"; } else if (device.id === "windows") { watts = moving ? 118 : 2.8; activity = moving ? "车窗动作" : "待机"; } else if (device.id === "shade") { watts = moving ? 86 : 1.9; activity = moving ? "遮阳帘动作" : "待机"; } else if (device.id === "reading") { const power = stateFlag(text("#interiorLightPower")); watts = power === false ? 0.8 : power === true ? 18 : 7.5; activity = power === true ? "已开启" : power === false ? "已关闭" : "状态估算"; if (moving) { watts += 6; activity = "切换执行中"; } } else if (device.id === "ambient") { const power = stateFlag(text("#ambientPower")); const brightness = Math.max(1, Math.min(5, numericText("#ambientBrightness", 3))); watts = power === false ? 1.1 : power === true ? 8 + brightness * 6.2 : 18; activity = power === true ? `${brightness} 级亮度` : power === false ? "已关闭" : "状态估算"; if (moving) { watts += 5; activity = "调光执行中"; } } const amplitude = watts < 10 ? 0.025 : device.id === "climate" ? 0.055 : 0.04; return { watts: Math.max(0, watts * deterministicWave(now, device.phase, amplitude)), source: "estimated", activity, updatedAt: now, }; } function readingForDevice(device, now) { const measured = monitor.measured.get(device.id); const reportedEnergy = monitor.reportedEnergy.get(device.id); if (measured && now - measured.receivedAt <= MEASURED_STALE_MS) { const activity = measured.source === "measured" ? "车端功率通道" : measured.source === "simulated" ? "模拟功率通道" : "外部估算通道"; return { watts: measured.watts, source: measured.source, activity, updatedAt: measured.updatedAt || measured.receivedAt, reportedEnergyWh: reportedEnergy?.wattsHours ?? null, reportedEnergyLabel: reportedEnergy?.label || "", reportedEnergySource: reportedEnergy?.source || "", }; } return { ...estimateDevicePower(device, now), reportedEnergyWh: reportedEnergy?.wattsHours ?? null, reportedEnergyLabel: reportedEnergy?.label || "", reportedEnergySource: reportedEnergy?.source || "", }; } function frameSeriesValue(frame, seriesId) { const exactDevice = ENERGY_MONITORED_DEVICES.find(({ id }) => id === seriesId); const deviceIds = exactDevice ? [exactDevice.id] : SERIES.find(({ id }) => id === seriesId)?.devices || SERIES[0].devices; return deviceIds.reduce((sum, id) => sum + (frame.values[id] || 0), 0); } function publicSnapshot() { if (!monitor.latest) return null; return { timestamp: monitor.latest.timestamp, total_power_w: monitor.latest.total, total_energy_wh: Object.values(monitor.energyWh).reduce((sum, value) => sum + value, 0), source: monitor.latest.source, measured_count: monitor.latest.measuredCount, simulated_count: monitor.latest.simulatedCount, estimated_count: monitor.latest.estimatedCount, total_energy_scope: "monitor_power_integral_estimate", devices: Object.fromEntries(ENERGY_MONITORED_DEVICES.map((device) => { const reading = monitor.latest.readings[device.id]; return [device.id, { name: device.name, power_w: reading.watts, energy_wh: monitor.energyWh[device.id], reported_energy_wh: reading.reportedEnergyWh, reported_energy_label: reading.reportedEnergyLabel, reported_energy_source: reading.reportedEnergySource, source: reading.source, activity: reading.activity, }]; })), sample_count: monitor.frames.length, }; } function pruneExpiredControlRequests(now) { monitor.controlRequests.forEach((request, requestId) => { if (request.until <= now) monitor.controlRequests.delete(requestId); }); } function sampleNow({ now = Date.now(), integrate = true, render = true } = {}) { const timestamp = Number(now); if (!Number.isFinite(timestamp)) return publicSnapshot(); pruneExpiredControlRequests(timestamp); const readings = {}; const values = {}; let total = 0; let measuredCount = 0; let simulatedCount = 0; ENERGY_MONITORED_DEVICES.forEach((device) => { const reading = readingForDevice(device, timestamp); readings[device.id] = reading; values[device.id] = reading.watts; total += finitePower(reading.watts) ?? 0; if (reading.source === "measured") measuredCount += 1; if (reading.source === "simulated") simulatedCount += 1; }); const elapsedMs = monitor.lastSampleAt === null ? 0 : Math.max(0, timestamp - monitor.lastSampleAt); if (integrate && elapsedMs > 0) { ENERGY_MONITORED_DEVICES.forEach(({ id }) => { const previous = monitor.frames.at(-1)?.values?.[id] ?? values[id]; monitor.energyWh[id] += ((previous + values[id]) / 2) * elapsedMs / 3600000; }); } monitor.lastSampleAt = timestamp; monitor.frames.push({ timestamp, values, total }); if (monitor.frames.length > HISTORY_LIMIT) monitor.frames.splice(0, monitor.frames.length - HISTORY_LIMIT); const estimatedCount = ENERGY_MONITORED_DEVICES.length - measuredCount - simulatedCount; const source = measuredCount === ENERGY_MONITORED_DEVICES.length ? "measured" : simulatedCount === ENERGY_MONITORED_DEVICES.length ? "simulated" : estimatedCount === ENERGY_MONITORED_DEVICES.length ? "estimated" : measuredCount && simulatedCount && estimatedCount ? "measured_simulated_estimated" : measuredCount && simulatedCount ? "measured_simulated" : measuredCount ? "mixed" : "simulated_estimated"; monitor.latest = { timestamp, readings, total, measuredCount, simulatedCount, estimatedCount, source, }; if (render && monitor.mounted) renderMonitor(); window.dispatchEvent(new CustomEvent("cognitive-agent:energy-snapshot", { detail: publicSnapshot() })); return publicSnapshot(); } function ingestPowerTelemetry(payload, { sample = true } = {}) { const receivedAt = Date.now(); const updates = payload?.devices && typeof payload.devices === "object" ? Object.entries(payload.devices) : payload?.device ? [[payload.device, payload]] : []; updates.forEach(([rawKey, rawTelemetry]) => { const deviceId = aliasToDevice.get(String(rawKey || "").trim().toLowerCase()); if (!deviceId) return; const telemetry = rawTelemetry && typeof rawTelemetry === "object" ? rawTelemetry : { power_w: rawTelemetry }; const directEnergyWh = finiteReportedEnergy(telemetry.energy_wh ?? telemetry.total_energy_wh); const tripEnergyKwh = finiteNonNegative(telemetry.trip_energy_kwh); const todayEnergyKwh = finiteNonNegative(telemetry.today_energy_kwh); const tripEnergyWh = tripEnergyKwh === null ? null : finiteReportedEnergy(tripEnergyKwh * 1000); const todayEnergyWh = todayEnergyKwh === null ? null : finiteReportedEnergy(todayEnergyKwh * 1000); const reportedEnergyWh = directEnergyWh ?? tripEnergyWh ?? todayEnergyWh ?? null; const reportedEnergyLabel = directEnergyWh !== null ? String(telemetry.energy_label || "车端累计") : tripEnergyWh !== null ? "本次行程" : todayEnergyWh !== null ? "今日累计" : ""; const source = telemetrySource(telemetry, payload); if (reportedEnergyWh !== null) { monitor.reportedEnergy.set(deviceId, { wattsHours: reportedEnergyWh, label: reportedEnergyLabel, source, receivedAt, }); } const watts = finitePower( telemetry.power_w ?? telemetry.total_power_w ?? telemetry.current_power_w, ); if (watts === null) return; const parsedUpdatedAt = telemetry.updated_at ? new Date(telemetry.updated_at).getTime() : NaN; monitor.measured.set(deviceId, { watts, receivedAt, updatedAt: Number.isFinite(parsedUpdatedAt) ? parsedUpdatedAt : receivedAt, source, }); }); return sample ? sampleNow() : publicSnapshot(); } function formatPowerParts(watts) { const value = finiteNonNegative(watts) || 0; if (value >= 1000) return { value: (value / 1000).toFixed(value >= 10000 ? 1 : 2), unit: "kW" }; return { value: String(Math.round(value)), unit: "W" }; } function formatPower(watts, estimated = false) { const parts = formatPowerParts(watts); return `${estimated ? "≈ " : ""}${parts.value} ${parts.unit}`; } function formatEnergy(wh, estimated = false) { const value = finiteNonNegative(wh) || 0; const prefix = estimated ? "≈ " : ""; if (value >= 1000) return `${prefix}${(value / 1000).toFixed(3)} kWh`; return `${prefix}${value < 10 ? value.toFixed(2) : value.toFixed(1)} Wh`; } function sourceLabel(source) { return { measured: "车端实测", simulated: "模拟数据", simulated_estimated: "模拟 + 估算", measured_simulated: "实测 + 模拟", measured_simulated_estimated: "实测 + 模拟 + 估算", mixed: "实测 + 估算", estimated: "状态估算", }[source] || "状态估算"; } function sourceShortLabel(source) { return source === "measured" ? "实测" : source === "simulated" ? "模拟" : "估算"; } function totalPowerQualifier(source) { if (source === "measured") return "实测总功率"; if (source === "simulated") return "模拟总功率"; if (source === "estimated") return "估算总功率"; return "混合来源总功率"; } function niceMaximum(value) { const safe = Math.max(1, value); const magnitude = 10 ** Math.floor(Math.log10(safe)); const normalized = safe / magnitude; const nice = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10; return nice * magnitude; } function chartPoints(values, maxValue, width = 700, height = 238, padding = { left: 42, right: 14, top: 16, bottom: 28 }) { if (values.length < 2) return []; const innerWidth = width - padding.left - padding.right; const innerHeight = height - padding.top - padding.bottom; return values.map((value, index) => ({ x: padding.left + innerWidth * index / (values.length - 1), y: padding.top + innerHeight * (1 - Math.max(0, value) / maxValue), })); } function polylinePoints(values, maxValue, width, height, padding) { return chartPoints(values, maxValue, width, height, padding) .map(({ x, y }) => `${x.toFixed(1)},${y.toFixed(1)}`) .join(" "); } function renderMainChart() { const values = monitor.frames.map((frame) => frameSeriesValue(frame, monitor.activeSeries)); const maximum = niceMaximum(Math.max(...values, 1)); const points = chartPoints(values, maximum); const line = $("#energyMonitorChartLine"); const area = $("#energyMonitorChartArea"); const svg = $("#energyMonitorChart"); const baseline = 210; line?.setAttribute("points", points.map(({ x, y }) => `${x.toFixed(1)},${y.toFixed(1)}`).join(" ")); area?.setAttribute("d", points.length ? `M ${points[0].x.toFixed(1)} ${baseline} L ${points.map(({ x, y }) => `${x.toFixed(1)} ${y.toFixed(1)}`).join(" L ")} L ${points.at(-1).x.toFixed(1)} ${baseline} Z` : ""); const selectedDevice = ENERGY_MONITORED_DEVICES.find(({ id }) => id === monitor.activeSeries); const selectedSeries = SERIES.find(({ id }) => id === monitor.activeSeries); const label = selectedDevice?.name || selectedSeries?.name || "总功率"; const current = values.at(-1) || 0; const currentReading = selectedDevice ? monitor.latest?.readings?.[selectedDevice.id] : null; const estimated = currentReading ? currentReading.source !== "measured" : monitor.latest?.source !== "measured"; $("#energyMonitorTrendName").textContent = label; $("#energyMonitorTrendCurrent").textContent = formatPower(current, estimated); $("#energyMonitorChartMax").textContent = formatPower(maximum); $("#energyMonitorChartMid").textContent = formatPower(maximum / 2); const spanSeconds = Math.max(1, Math.round(((monitor.frames.at(-1)?.timestamp || 0) - (monitor.frames[0]?.timestamp || 0)) / 1000)); const windowSeconds = monitor.frames.length >= HISTORY_LIMIT ? 60 : Math.min(60, spanSeconds); $("#energyMonitorTrendWindow").textContent = `最近 ${windowSeconds} 秒`; $("#energyMonitorChartStart").textContent = `${windowSeconds} 秒前`; if (svg) svg.setAttribute("aria-label", `${label}最近 ${windowSeconds} 秒功率趋势,当前${formatPower(current, estimated)}`); $$('[data-energy-series]').forEach((button) => { const selected = button.dataset.energySeries === monitor.activeSeries; button.classList.toggle("active", selected); button.setAttribute("aria-pressed", String(selected)); }); $$('[data-energy-device]').forEach((button) => { const selected = button.dataset.energyDevice === monitor.activeSeries; button.classList.toggle("active", selected); button.setAttribute("aria-pressed", String(selected)); }); } function renderDeviceRows() { if (!monitor.latest) return; const total = Math.max(monitor.latest.total, 0.0001); ENERGY_MONITORED_DEVICES.forEach((device) => { const row = $(`[data-energy-device="${device.id}"]`); const reading = monitor.latest.readings[device.id]; if (!row || !reading) return; const estimated = reading.source !== "measured"; $("[data-energy-device-power]", row).textContent = formatPower(reading.watts, estimated); const hasReportedEnergy = reading.reportedEnergyWh !== null && reading.reportedEnergyWh !== undefined; const displayedEnergyWh = hasReportedEnergy ? reading.reportedEnergyWh : monitor.energyWh[device.id]; const reportedEnergyEstimated = !hasReportedEnergy || reading.reportedEnergySource !== "measured"; $("[data-energy-device-energy]", row).textContent = formatEnergy(displayedEnergyWh, reportedEnergyEstimated); $("[data-energy-device-energy]", row).title = hasReportedEnergy ? `${reading.reportedEnergyLabel || "车端累计"}能耗` : "本次监测功率积分估算"; $("[data-energy-device-source]", row).textContent = sourceShortLabel(reading.source); $("[data-energy-device-source]", row).dataset.source = reading.source; $("[data-energy-device-activity]", row).textContent = reading.activity; const share = Math.max(0, Math.min(100, reading.watts / total * 100)); $("[data-energy-device-share]", row).textContent = `${share.toFixed(1)}%`; row.style.setProperty("--energy-share", `${share}%`); const values = monitor.frames.map((frame) => frame.values[device.id] || 0); const maximum = Math.max(...values, 1); $("[data-energy-device-sparkline]", row)?.setAttribute( "points", polylinePoints(values, maximum, 92, 28, { left: 1, right: 1, top: 3, bottom: 3 }), ); const energyScope = hasReportedEnergy ? reading.reportedEnergyLabel || "车端累计" : "本次监测估算"; row.setAttribute("aria-label", `${device.name},当前${formatPower(reading.watts, estimated)},${energyScope}${formatEnergy(displayedEnergyWh, reportedEnergyEstimated)},点击查看趋势`); }); } function renderMonitor() { if (!monitor.latest) return; const totalParts = formatPowerParts(monitor.latest.total); const estimated = monitor.latest.source !== "measured"; $("#energyMonitorTotalPower").textContent = totalParts.value; $("#energyMonitorTotalUnit").textContent = totalParts.unit; $("#energyMonitorTotalQualifier").textContent = totalPowerQualifier(monitor.latest.source); $("#energyMonitorAccumulated").textContent = formatEnergy( Object.values(monitor.energyWh).reduce((sum, value) => sum + value, 0), true, ); const elapsedHours = Math.max(1 / 3600, (monitor.latest.timestamp - monitor.startedAt) / 3600000); $("#energyMonitorAverage").textContent = formatPower( Object.values(monitor.energyWh).reduce((sum, value) => sum + value, 0) / elapsedHours, true, ); $("#energyMonitorCoverage").textContent = `${ENERGY_MONITORED_DEVICES.length} / ${ENERGY_MONITORED_DEVICES.length}`; $("#energyMonitorSource").textContent = sourceLabel(monitor.latest.source); $("#energyMonitorSource").dataset.source = monitor.latest.source; const sourceBreakdown = [ monitor.latest.measuredCount ? `${monitor.latest.measuredCount} 路实测` : "", monitor.latest.simulatedCount ? `${monitor.latest.simulatedCount} 路模拟` : "", monitor.latest.estimatedCount ? `${monitor.latest.estimatedCount} 路估算` : "", ].filter(Boolean).join(" · "); $("#energyMonitorCoverageNote").textContent = sourceBreakdown || "等待设备采样"; $("#energyMonitorLiveLabelText").textContent = monitor.latest.measuredCount ? "实时监测" : monitor.latest.simulatedCount ? "动态模拟" : "状态估算"; $("#energyMonitorFreshness").textContent = `采样更新 ${new Date(monitor.latest.timestamp).toLocaleTimeString("zh-CN", { hour12: false })}`; $("#energyMonitorPage").dataset.revision = String(Number($("#energyMonitorPage").dataset.revision || 0) + 1); $("#energyMonitorPage").dataset.sampleCount = String(monitor.frames.length); renderMainChart(); renderDeviceRows(); } function deviceRowsMarkup() { return ENERGY_MONITORED_DEVICES.map((device) => ` `).join(""); } function createLayer() { const trigger = document.createElement("button"); trigger.id = "energyMonitorToggle"; trigger.className = "energy-monitor-toggle v2-panel-control-button"; trigger.type = "button"; trigger.setAttribute("aria-controls", "energyMonitorLayer"); trigger.setAttribute("aria-pressed", "false"); trigger.setAttribute("aria-expanded", "false"); trigger.setAttribute("aria-label", "打开整车能耗监测"); trigger.title = "打开整车能耗监测"; trigger.innerHTML = '整车能耗监测'; const layer = document.createElement("div"); layer.id = "energyMonitorLayer"; layer.className = "energy-monitor-layer"; layer.hidden = true; layer.inert = true; layer.setAttribute("role", "dialog"); layer.setAttribute("aria-modal", "true"); layer.setAttribute("aria-labelledby", "energyMonitorTitle"); layer.innerHTML = `

整车能耗监测

实时追踪各可控车端设备功率与累计能耗

状态估算 最后更新 --:--:-- 状态估算
实时总功率--W等待采样
本次监测累计能耗-- Wh按实时功率积分
本次监测平均功率-- W自页面加载开始
监测设备7 / 7等待设备采样

功率趋势

总功率 · 最近 24 秒

-- W
${SERIES.map((series) => ``).join("")}
-- W -- W 0
60 秒前现在
当前选中曲线

点击右侧设备可查看其独立趋势

设备实时功率

功率 · 累计能耗 · 占比

${ENERGY_MONITORED_DEVICES.length} 个可控设备
${deviceRowsMarkup()}
`; $("#v2PanelControlDock").append(trigger); document.body.append(layer); trigger.addEventListener("click", () => setOpen(!monitor.open)); $("#energyMonitorClose", layer).addEventListener("click", () => setOpen(false)); layer.addEventListener("click", (event) => { if (event.target === layer) setOpen(false); }); layer.addEventListener("click", (event) => { const seriesButton = event.target.closest("[data-energy-series]"); const deviceButton = event.target.closest("[data-energy-device]"); if (seriesButton) selectSeries(seriesButton.dataset.energySeries); if (deviceButton) selectSeries(deviceButton.dataset.energyDevice); }); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); } function focusableElements() { return $$('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', $("#energyMonitorLayer")) .filter((element) => !element.hidden && element.offsetParent !== null); } function setBackgroundInert(inert) { const layer = $("#energyMonitorLayer"); if (!layer || !document.body) return; if (inert) { monitor.backgroundInert = [...document.body.children] .filter((element) => element !== layer && !["SCRIPT", "STYLE", "LINK"].includes(element.tagName)) .map((element) => ({ element, wasInert: element.hasAttribute("inert") })); monitor.backgroundInert.forEach(({ element }) => { element.toggleAttribute("inert", true); }); return; } monitor.backgroundInert.forEach(({ element, wasInert }) => { element.toggleAttribute("inert", wasInert); }); monitor.backgroundInert = []; } function captureModalKeys(event) { if (!monitor.open) return; if (event.key === "Escape") { event.preventDefault(); event.stopImmediatePropagation(); setOpen(false); return; } if (event.key !== "Tab") return; const focusable = focusableElements(); if (!focusable.length) return; const first = focusable[0]; const last = focusable.at(-1); const layer = $("#energyMonitorLayer"); if (!layer?.contains(document.activeElement)) { event.preventDefault(); (event.shiftKey ? last : first).focus(); } else if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } } function setOpen(open) { const next = Boolean(open); const layer = $("#energyMonitorLayer"); const trigger = $("#energyMonitorToggle"); if (!layer || !trigger || monitor.open === next) return; monitor.open = next; trigger.classList.toggle("active", next); trigger.setAttribute("aria-pressed", String(next)); trigger.setAttribute("aria-expanded", String(next)); trigger.setAttribute("aria-label", next ? "关闭整车能耗监测" : "打开整车能耗监测"); trigger.title = next ? "关闭整车能耗监测" : "打开整车能耗监测"; if (next) { monitor.returnFocus = document.activeElement; layer.hidden = false; layer.inert = false; layer.dataset.open = "true"; setBackgroundInert(true); renderMonitor(); requestAnimationFrame(() => $("#energyMonitorClose", layer)?.focus()); } else { layer.dataset.open = "false"; layer.inert = true; layer.hidden = true; setBackgroundInert(false); const target = monitor.returnFocus instanceof HTMLElement && monitor.returnFocus.isConnected ? monitor.returnFocus : trigger; monitor.returnFocus = null; target.focus(); } } function selectSeries(seriesId) { const valid = SERIES.some(({ id }) => id === seriesId) || ENERGY_MONITORED_DEVICES.some(({ id }) => id === seriesId); if (!valid) return; monitor.activeSeries = seriesId; renderMainChart(); } function deviceIdsForControlDomain(domain, context = "") { const normalizedDomain = normalizeSource(domain).replace(/^native_/, ""); const normalizedContext = normalizeSource(context).replace(/[^a-z0-9\u4e00-\u9fff]+/g, "_"); if (normalizedDomain === "seat" || /(^|_)seat(_|$)/.test(normalizedContext)) { if (/(^|_)(driver|front_left|left_front)(_|$)|主驾/.test(normalizedContext)) return ["driver"]; if (/(^|_)(passenger|front_right|right_front)(_|$)|副驾/.test(normalizedContext)) return ["passenger"]; return ["passenger"]; } if (["window", "windows"].includes(normalizedDomain) || /(^|_)window(s)?(_|$)/.test(normalizedContext)) return ["windows"]; if (["roof", "shade", "sunshade"].includes(normalizedDomain) || /(^|_)(roof|shade|sunshade)(_|$)/.test(normalizedContext)) return ["shade"]; if (["climate", "hvac", "outlet"].includes(normalizedDomain) || /(^|_)(climate|hvac|outlet|ac)(_|$)/.test(normalizedContext)) return ["climate"]; if (["interior_light", "reading", "reading_light"].includes(normalizedDomain) || /(^|_)(interior|reading)_?light(_|$)/.test(normalizedContext)) return ["reading"]; if (["ambient", "ambient_light"].includes(normalizedDomain) || /(^|_)ambient_?light(_|$)/.test(normalizedContext)) return ["ambient"]; return []; } function controlActivityDevices(payload = {}) { const wrappedResult = payload?.result && typeof payload.result === "object"; const eventPayload = wrappedResult ? payload.result : payload; const status = String(eventPayload?.status || ""); if (payload?.error) return []; if (wrappedResult && !["succeeded", "partially_succeeded"].includes(status)) return []; if (!wrappedResult && status && !["processing", "succeeded", "partially_succeeded", "submitted"].includes(status)) return []; if (eventPayload?.preview_only === true || /_preview$/.test(String(eventPayload?.orchestration_route || ""))) { return []; } const deviceIds = new Set(); const directId = aliasToDevice.get(normalizeSource(eventPayload?.device_id)) || normalizeSource(eventPayload?.device_id); if (ENERGY_MONITORED_DEVICES.some(({ id }) => id === directId)) deviceIds.add(directId); const actions = [ ...(Array.isArray(eventPayload?.actions) ? eventPayload.actions : []), ...(Array.isArray(eventPayload?.validated_actions) ? eventPayload.validated_actions : []), ...(Array.isArray(eventPayload?.intent_ir?.spec?.actions) ? eventPayload.intent_ir.spec.actions : []), ]; actions.forEach((action) => { const capabilityDomain = String(action?.capability || "").split(".")[0]; const context = JSON.stringify({ capability: action?.capability, target: action?.target, }); deviceIdsForControlDomain(action?.domain || capabilityDomain, context).forEach((id) => deviceIds.add(id)); }); const functionConfigs = Object.entries(eventPayload?.function_configs || {}); if (eventPayload?.function_config && typeof eventPayload.function_config === "object") { functionConfigs.push([eventPayload.function_config?.spec?.domain || "function", eventPayload.function_config]); } functionConfigs.forEach(([configKey, config]) => { const domain = config?.spec?.domain || config?.domain || configKey; const operations = config?.spec?.operations || config?.operations || []; if (!operations.length) { deviceIdsForControlDomain(domain, configKey).forEach((id) => deviceIds.add(id)); return; } operations.forEach((operation) => { const context = JSON.stringify({ capability: operation?.capability, target: operation?.target, }); deviceIdsForControlDomain(operation?.domain || domain, context).forEach((id) => deviceIds.add(id)); }); }); Object.entries(eventPayload?.command_plans || {}).forEach(([planKey, plan]) => { const domain = plan?.spec?.domain || plan?.domain || planKey; const commands = plan?.spec?.commands || plan?.commands || []; const context = commands.length ? JSON.stringify(commands) : planKey; deviceIdsForControlDomain(domain, context).forEach((id) => deviceIds.add(id)); }); return [...deviceIds]; } function markControlActivity(payload) { const until = Date.now() + 4000; const requestId = controlRequestId(payload); const currentDeviceIds = controlActivityDevices(payload); const existing = requestId ? monitor.controlRequests.get(requestId) : null; const deviceIds = [...new Set([...(existing?.deviceIds || []), ...currentDeviceIds])]; deviceIds.forEach((deviceId) => { monitor.motionUntil.set(deviceId, Math.max(monitor.motionUntil.get(deviceId) || 0, until)); }); if (requestId && deviceIds.length) { monitor.controlRequests.set(requestId, { deviceIds, until }); } return currentDeviceIds; } function controlRequestId(payload = {}) { const eventPayload = payload?.result && typeof payload.result === "object" ? payload.result : payload; return String(eventPayload?.request_id || payload?.request_id || "").trim(); } function completeControlActivity(payload = {}) { const eventPayload = payload?.result && typeof payload.result === "object" ? payload.result : payload; const requestId = controlRequestId(payload); const tracked = requestId ? monitor.controlRequests.get(requestId) : null; const succeeded = !payload?.error && ["succeeded", "partially_succeeded"].includes(String(eventPayload?.status || "")); if (tracked) { monitor.controlRequests.delete(requestId); if (!succeeded) { const now = Date.now(); tracked.deviceIds.forEach((deviceId) => { if ((monitor.motionUntil.get(deviceId) || 0) > tracked.until) return; const remainingUntil = [...monitor.controlRequests.values()] .filter((request) => request.deviceIds.includes(deviceId)) .reduce((maximum, request) => Math.max(maximum, request.until), now); monitor.motionUntil.set(deviceId, remainingUntil); }); } return tracked.deviceIds; } return succeeded ? markControlActivity(payload) : []; } function seedHistory() { const now = Date.now(); for (let offset = 23; offset >= 1; offset -= 1) { sampleNow({ now: now - offset * SAMPLE_INTERVAL_MS, integrate: false, render: false }); } monitor.lastSampleAt = now - SAMPLE_INTERVAL_MS; sampleNow({ now, integrate: false, render: false }); monitor.startedAt = now; } function mount(attempt = 0) { if (monitor.mounted) return; if (!$("#v2PanelControlDock")) { if (attempt < 20) window.setTimeout(() => mount(attempt + 1), 50); return; } monitor.mounted = true; createLayer(); seedHistory(); renderMonitor(); monitor.timer = window.setInterval(() => sampleNow(), SAMPLE_INTERVAL_MS); document.addEventListener("keydown", captureModalKeys, true); window.addEventListener("cognitive-agent:power-telemetry", (event) => ingestPowerTelemetry(event.detail || {}, { sample: false })); const existingUpdater = window.updateDevicePowerTelemetry; if (typeof existingUpdater === "function" && !existingUpdater.energyMonitorLinked) { const linkedUpdater = (payload) => { const result = existingUpdater(payload); ingestPowerTelemetry(payload, { sample: false }); return result; }; linkedUpdater.energyMonitorLinked = true; window.updateDevicePowerTelemetry = linkedUpdater; } window.addEventListener("cognitive-agent:vehicle-can-control-request", (event) => markControlActivity(event.detail || {})); window.addEventListener("cognitive-agent:validated-can-commands", (event) => markControlActivity(event.detail || {})); window.addEventListener("cognitive-agent:vehicle-control-activity", (event) => markControlActivity(event.detail || {})); window.addEventListener("cognitive-agent:request-complete", (event) => completeControlActivity(event.detail || {})); } window.CognitiveAgentEnergyMonitor = { close: () => setOpen(false), getDevices: () => ENERGY_MONITORED_DEVICES.map(({ id, name, telemetryKeys }) => ({ id, name, telemetryKeys: [...telemetryKeys] })), getSnapshot: publicSnapshot, ingestTelemetry: ingestPowerTelemetry, isOpen: () => monitor.open, completeControlActivity, notifyControlActivity: markControlActivity, open: () => setOpen(true), sampleNow, selectSeries, }; document.addEventListener("DOMContentLoaded", () => mount()); }());