diff --git a/emhttp/plugins/dynamix/DashStats.page b/emhttp/plugins/dynamix/DashStats.page index 5522666aed..60fbc8f968 100755 --- a/emhttp/plugins/dynamix/DashStats.page +++ b/emhttp/plugins/dynamix/DashStats.page @@ -94,7 +94,7 @@ foreach ($devs as $disk) { } $array_percent = number_format(100*$array_used/($array_size ?: 1),1,$dot,''); -exec('cat /sys/devices/system/cpu/*/topology/thread_siblings_list|sort -nu', $cpus); +$cpus=get_cpu_packages(); $wg_up = $wireguard ? exec("wg show interfaces") : ''; $wg_up = $wg_up ? explode(' ',$wg_up) : []; $up = count($wg_up); @@ -351,8 +351,13 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout - + _(Total)_ _(Power)_: N/A + + + _(Temperature)_: N/A + + _(Load)_: 0% @@ -398,22 +403,29 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout - + "; - if ($is_intel_cpu && count($core_types) > 0) - $core_type = "({$core_types[$cpu1]})"; - else - $core_type = ""; - - if ($cpu2) - echo "CPU $cpu1 $core_type - HT $cpu2 0%
0%
"; - else - echo "CPU $cpu1 $core_type0%
"; - echo ""; - } + foreach ($cpus as $cpu_index=>$package) { + if (count($cpus) > 1) { + echo " "._("Physical")." CPU $cpu_index "._("Power").": N/A "; + if (count($cpus)>1) echo " "._("Temperature").": N/A"; + echo ""; + } + foreach ($package as $pair) { + [$cpu1, $cpu2] = my_preg_split('/[,-]/',$pair); + echo ""; + if ($is_intel_cpu && count($core_types) > 0) + $core_type = "({$core_types[$cpu1]})"; + else + $core_type = ""; + + if ($cpu2) + echo "CPU $cpu1 $core_type - HT $cpu2 0%
0%
"; + else + echo "CPU $cpu1 $core_type0%
"; + echo ""; + } + } ?> @@ -1441,6 +1453,7 @@ var startup = true; var stopgap = ''; var recall = null; var recover = null; +var tempunit=""; // Helper function to calculate millisPerPixel based on container width function getMillisPerPixel(timeInSeconds, containerId) { @@ -1695,6 +1708,39 @@ function addChartNet(rx, tx) { txTimeSeries.append(now, Math.floor(tx / 1000)); } +function updateCPUPower() { + if (!cpupower) return; + + // Update total power + const totalEl = document.getElementById('cpu-total-power'); + const totalPower = cpupower.totalPower ?? 0; + if (totalEl) { + totalEl.innerHTML = ` _(Total)_ _(Power)_: ${totalPower.toFixed(2)} W`; + } + + // Update each core's span + const cpuspower = cpupower.power ?? []; + cpuspower.forEach((power, index) => { + const coreEl = document.getElementById(`cpu-power${index}`); + if (coreEl) { + coreEl.innerHTML = `${power.toFixed(2)} W`; + } + }); + + const cpustemps = cpupower.temp ?? []; + cpustemps.forEach((temp, index) => { + const coreTempEl = document.getElementById(`cpu-temp${index}`); + if (coreTempEl) { + tempdisplay = temp.toFixed(0); + if (tempunit === "F") { + tempdisplay = ((temp.toFixed(0))* 9 / 5) + 32; + } + coreTempEl.innerHTML = Math.round(tempdisplay)+` °`+tempunit;; + } + }); + +} + // Cache for last values to avoid unnecessary DOM updates var lastCpuValues = { load: -1, @@ -2770,6 +2816,60 @@ $(function() { setTimeout(function() { // Charts initialized },500); + + + + // Start GraphQL CPU power subscription with retry logic + let cpuInitPWRAttempts = 0, cpuPWRRetryMs = 100; + function initPwrCpuSubscription() { + + + if (window.gql && window.apolloClient) { + // Define the subscription query when GraphQL is available + // corepower has the temps currently. + CPU_POWER_SUBSCRIPTION = window.gql(` + subscription SystemMetricsCpuTelemetry { + systemMetricsCpuTelemetry { + totalPower, + power, + temp, + } + } + `); + cpuPowerSubscription = window.apolloClient.subscribe({ + query: CPU_POWER_SUBSCRIPTION + }).subscribe({ + next: (result) => { + + + if (result.data?.systemMetricsCpuTelemetry){ + cpupower = result.data.systemMetricsCpuTelemetry; + + updateCPUPower(); + } + }, + error: (err) => { + console.error('CPU power subscription error:', err); + // Try to resubscribe with capped backoff + if (cpuPowerSubscription) { try { cpuPowerSubscription.unsubscribe(); } catch(e){} } + setTimeout(initPwrCpuSubscription, Math.min(cpuPWRRetryMs *= 2, 5000)); + } + }); + } else { + // Retry with capped backoff if GraphQL client not ready + cpuInitPWRAttempts++; + setTimeout(initPwrCpuSubscription, Math.min(cpuPWRRetryMs *= 2, 2000)); + } + } + initPwrCpuSubscription(); + // Cleanup GraphQL subscription on page unload + $(window).on('beforeunload', function() { + if (cpuPowerSubscription) { + cpuPowerSubscription.unsubscribe(); + } + }); + + // Cleanup GraphQL subscription on page unload $(window).on('beforeunload', function() { diff --git a/emhttp/plugins/dynamix/include/Helpers.php b/emhttp/plugins/dynamix/include/Helpers.php index 61a84cab9f..15009500f1 100644 --- a/emhttp/plugins/dynamix/include/Helpers.php +++ b/emhttp/plugins/dynamix/include/Helpers.php @@ -856,4 +856,21 @@ function display_deprecated_filesystem_warning($deprecated_disks, $type = 'array return $html; } + +function get_cpu_packages(string $separator = ','): array { + $packages = []; + foreach (glob("/sys/devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list") as $path) { + $pkg_id = (int)file_get_contents(dirname($path) . "/physical_package_id"); + $siblings = str_replace(",", $separator, trim(file_get_contents($path))); + if (!in_array($siblings, $packages[$pkg_id] ?? [])) { + $packages[$pkg_id][] = $siblings; + } + } + foreach ($packages as &$list) { + $keys = array_map(fn($s) => (int)explode($separator, $s)[0], $list); + array_multisort($keys, SORT_ASC, SORT_NUMERIC, $list); + } + unset($list); + return $packages; +} ?> diff --git a/emhttp/plugins/dynamix/include/cpulist.php b/emhttp/plugins/dynamix/include/cpulist.php new file mode 100644 index 0000000000..8d1db35733 --- /dev/null +++ b/emhttp/plugins/dynamix/include/cpulist.php @@ -0,0 +1,22 @@ + (int)explode($separator, $s)[0], $list); + array_multisort($keys, SORT_ASC, SORT_NUMERIC, $list); + } + unset($list); + + return $packages; +}