From 6a297e8cd6753ffad3fbf477c3c76ece3a65f0d5 Mon Sep 17 00:00:00 2001 From: Simon Fairweather Date: Tue, 19 May 2026 12:09:12 +0100 Subject: [PATCH 1/7] feat(cpu): add legacy telemetry fallbacks --- .../info/cpu/cpu-topology.service.ts | 119 +++++++++++++++--- 1 file changed, 100 insertions(+), 19 deletions(-) diff --git a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts index 2c65c9749b..73c7bf3c13 100644 --- a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts +++ b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts @@ -79,29 +79,54 @@ export class CpuTopologyService { const label = (await readFile(join(path, 'name'), 'utf8')).trim(); if (/coretemp|k10temp|zenpower/i.test(label)) { const files = await readdir(path); + const packageTemps: number[] = []; + const coreTemps: number[] = []; + for (const f of files) { - if (f.startsWith('temp') && f.endsWith('_label')) { - const lbl = (await readFile(join(path, f), 'utf8')).trim().toLowerCase(); + if (!(f.startsWith('temp') && f.endsWith('_input'))) continue; + + const inputFile = join(path, f); + const labelFile = join(path, f.replace('_input', '_label')); + + try { + const raw = await readFile(inputFile, 'utf8'); + const parsed = parseInt(raw.trim(), 10); + + if (!Number.isFinite(parsed)) { + this.logger.warn(`Invalid temperature value: ${raw.trim()}`); + continue; + } + + const tempC = parsed / 1000; + let sensorLabel = ''; + + try { + sensorLabel = (await readFile(labelFile, 'utf8')).trim().toLowerCase(); + } catch { + // label file is optional + } + if ( - lbl.includes('package id') || - lbl.includes('tctl') || - lbl.includes('tdie') + sensorLabel.includes('package id') || + sensorLabel.includes('tctl') || + sensorLabel.includes('tdie') || + sensorLabel.includes('cpu temp') ) { - const inputFile = join(path, f.replace('_label', '_input')); - try { - const raw = await readFile(inputFile, 'utf8'); - const parsed = parseInt(raw.trim(), 10); - if (Number.isFinite(parsed)) { - temps.push(parsed / 1000); - } else { - this.logger.warn(`Invalid temperature value: ${raw.trim()}`); - } - } catch (err) { - this.logger.warn('Failed to read file', err); - } + packageTemps.push(tempC); + } else if (/^core\s+\d+$/i.test(sensorLabel)) { + coreTemps.push(tempC); } + } catch (err) { + this.logger.warn('Failed to read file', err); } } + + if (packageTemps.length > 0) { + temps.push(...packageTemps); + } else if (coreTemps.length > 0) { + // Legacy CPUs may expose only per-core readings. Use the hottest core as package temp. + temps.push(Math.max(...coreTemps)); + } } } catch (err) { this.logger.warn('Failed to read file', err); @@ -127,10 +152,10 @@ export class CpuTopologyService { } } } catch { - return {}; + return this.getPackagePowerFromHwmon(); } - if (!raplPaths.length) return {}; + if (!raplPaths.length) return this.getPackagePowerFromHwmon(); const readEnergy = async (p: string): Promise => { try { @@ -204,6 +229,62 @@ export class CpuTopologyService { domains['total'] = Math.round(total * 100) / 100; } + if (!Object.keys(results).length) { + return this.getPackagePowerFromHwmon(); + } + + return results; + } + + private async getPackagePowerFromHwmon(): Promise>> { + const results: Record> = {}; + + try { + const hwmons = await readdir('/sys/class/hwmon'); + for (const hwmon of hwmons) { + const path = join('/sys/class/hwmon', hwmon); + let chipName = ''; + + try { + chipName = (await readFile(join(path, 'name'), 'utf8')).trim(); + } catch { + continue; + } + + if (!/fam15h_power|zenpower|amd_energy|rapl/i.test(chipName)) { + continue; + } + + const files = await readdir(path); + for (const f of files) { + if (!(f.startsWith('power') && f.endsWith('_input'))) continue; + + try { + const raw = await readFile(join(path, f), 'utf8'); + const parsed = Number(raw.trim()); + if (!Number.isFinite(parsed) || parsed < 0) continue; + + const watts = parsed > 1000 ? parsed / 1_000_000 : parsed; + const rounded = Math.round(watts * 100) / 100; + + if (!Number.isFinite(rounded)) continue; + + if (!results[0]) results[0] = {}; + results[0][`${chipName}:${f}`] = rounded; + } catch (err) { + this.logger.warn('Failed to read file', err); + } + } + } + } catch { + return {}; + } + + for (const domains of Object.values(results)) { + const total = Object.values(domains).reduce((a, b) => a + b, 0); + domains['total'] = Math.round(total * 100) / 100; + } + return results; } From 04ea8378f2dee7ab9d0eea38ace143a3a7d342a0 Mon Sep 17 00:00:00 2001 From: Simon Fairweather Date: Tue, 19 May 2026 12:27:31 +0100 Subject: [PATCH 2/7] style(cpu): wrap sensor label read --- .../graph/resolvers/info/cpu/cpu-topology.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts index 73c7bf3c13..8eb4efaefa 100644 --- a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts +++ b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts @@ -101,7 +101,9 @@ export class CpuTopologyService { let sensorLabel = ''; try { - sensorLabel = (await readFile(labelFile, 'utf8')).trim().toLowerCase(); + sensorLabel = (await readFile(labelFile, 'utf8')) + .trim() + .toLowerCase(); } catch { // label file is optional } From 32b7af310ade2cc4befb6e1d4eba4dce87caf481 Mon Sep 17 00:00:00 2001 From: Simon Fairweather Date: Tue, 19 May 2026 12:44:09 +0100 Subject: [PATCH 3/7] fix(cpu): improve legacy package sensor mapping --- .../info/cpu/cpu-topology.service.ts | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts index 8eb4efaefa..1008c02c69 100644 --- a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts +++ b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts @@ -80,6 +80,8 @@ export class CpuTopologyService { if (/coretemp|k10temp|zenpower/i.test(label)) { const files = await readdir(path); const packageTemps: number[] = []; + const packageTdieTemps: number[] = []; + const packageTctlTemps: number[] = []; const coreTemps: number[] = []; for (const f of files) { @@ -110,11 +112,15 @@ export class CpuTopologyService { if ( sensorLabel.includes('package id') || - sensorLabel.includes('tctl') || - sensorLabel.includes('tdie') || sensorLabel.includes('cpu temp') ) { packageTemps.push(tempC); + } + + if (sensorLabel.includes('tdie')) { + packageTdieTemps.push(tempC); + } else if (sensorLabel.includes('tctl')) { + packageTctlTemps.push(tempC); } else if (/^core\s+\d+$/i.test(sensorLabel)) { coreTemps.push(tempC); } @@ -123,8 +129,12 @@ export class CpuTopologyService { } } - if (packageTemps.length > 0) { - temps.push(...packageTemps); + if (packageTdieTemps.length > 0) { + temps.push(Math.max(...packageTdieTemps)); + } else if (packageTctlTemps.length > 0) { + temps.push(Math.max(...packageTctlTemps)); + } else if (packageTemps.length > 0) { + temps.push(Math.max(...packageTemps)); } else if (coreTemps.length > 0) { // Legacy CPUs may expose only per-core readings. Use the hottest core as package temp. temps.push(Math.max(...coreTemps)); @@ -240,6 +250,7 @@ export class CpuTopologyService { private async getPackagePowerFromHwmon(): Promise>> { const results: Record> = {}; + let nextFallbackPackageIndex = 0; try { const hwmons = await readdir('/sys/class/hwmon'); @@ -258,6 +269,40 @@ export class CpuTopologyService { } const files = await readdir(path); + let packageIndex = Number.NaN; + + const chipPackageMatch = chipName.match(/package[-_\s:]?(\d+)/i); + if (chipPackageMatch) { + packageIndex = Number(chipPackageMatch[1]); + } + + if (!Number.isFinite(packageIndex)) { + for (const fileName of files) { + if (!fileName.endsWith('_label')) continue; + + try { + const labelValue = (await readFile(join(path, fileName), 'utf8')) + .trim() + .toLowerCase(); + const labelPackageMatch = labelValue.match(/package[-_\s:]?(\d+)/i); + + if (labelPackageMatch) { + packageIndex = Number(labelPackageMatch[1]); + break; + } + } catch { + // label file is optional + } + } + } + + if (!Number.isFinite(packageIndex)) { + packageIndex = nextFallbackPackageIndex; + nextFallbackPackageIndex += 1; + } else { + nextFallbackPackageIndex = Math.max(nextFallbackPackageIndex, packageIndex + 1); + } + for (const f of files) { if (!(f.startsWith('power') && f.endsWith('_input'))) continue; @@ -271,8 +316,8 @@ export class CpuTopologyService { if (!Number.isFinite(rounded)) continue; - if (!results[0]) results[0] = {}; - results[0][`${chipName}:${f}`] = rounded; + if (!results[packageIndex]) results[packageIndex] = {}; + results[packageIndex][`${chipName}:${f}`] = rounded; } catch (err) { this.logger.warn('Failed to read file', err); } From 2eff257a758fe8bf4122df21f326e6ae4f48c95d Mon Sep 17 00:00:00 2001 From: Simon Fairweather Date: Wed, 20 May 2026 16:36:10 +0100 Subject: [PATCH 4/7] fix(cpu): detect unlabeled k10temp package temp --- .../unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts index 1008c02c69..89e8008ff8 100644 --- a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts +++ b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts @@ -115,6 +115,8 @@ export class CpuTopologyService { sensorLabel.includes('cpu temp') ) { packageTemps.push(tempC); + } else if (!sensorLabel && /^temp1_input$/i.test(f) && /k10temp/i.test(label)) { + packageTemps.push(tempC); } if (sensorLabel.includes('tdie')) { From 369259732ed761168074a088539934d87d818f3b Mon Sep 17 00:00:00 2001 From: Simon Fairweather Date: Wed, 20 May 2026 17:04:37 +0100 Subject: [PATCH 5/7] style(cpu): format k10temp fallback condition --- .../graph/resolvers/info/cpu/cpu-topology.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts index 89e8008ff8..b705bad6ab 100644 --- a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts +++ b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts @@ -115,7 +115,11 @@ export class CpuTopologyService { sensorLabel.includes('cpu temp') ) { packageTemps.push(tempC); - } else if (!sensorLabel && /^temp1_input$/i.test(f) && /k10temp/i.test(label)) { + } else if ( + !sensorLabel && + /^temp1_input$/i.test(f) && + /k10temp/i.test(label) + ) { packageTemps.push(tempC); } From a36d8b436d45ece4be752a2d0c672974eb86c998 Mon Sep 17 00:00:00 2001 From: Simon Fairweather Date: Thu, 21 May 2026 08:15:29 +0100 Subject: [PATCH 6/7] fix(cpu): avoid package index gaps in hwmon fallback --- .../info/cpu/cpu-topology.service.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts index b705bad6ab..c44278e44b 100644 --- a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts +++ b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts @@ -270,19 +270,23 @@ export class CpuTopologyService { continue; } - if (!/fam15h_power|zenpower|amd_energy|rapl/i.test(chipName)) { + if (!/fam15h_power|zenpower|rapl/i.test(chipName)) { continue; } const files = await readdir(path); - let packageIndex = Number.NaN; + let packageIndex: number | null = null; + let wrotePower = false; const chipPackageMatch = chipName.match(/package[-_\s:]?(\d+)/i); if (chipPackageMatch) { - packageIndex = Number(chipPackageMatch[1]); + const parsedPackageIndex = Number(chipPackageMatch[1]); + if (Number.isFinite(parsedPackageIndex)) { + packageIndex = parsedPackageIndex; + } } - if (!Number.isFinite(packageIndex)) { + if (packageIndex === null) { for (const fileName of files) { if (!fileName.endsWith('_label')) continue; @@ -293,8 +297,11 @@ export class CpuTopologyService { const labelPackageMatch = labelValue.match(/package[-_\s:]?(\d+)/i); if (labelPackageMatch) { - packageIndex = Number(labelPackageMatch[1]); - break; + const parsedPackageIndex = Number(labelPackageMatch[1]); + if (Number.isFinite(parsedPackageIndex)) { + packageIndex = parsedPackageIndex; + break; + } } } catch { // label file is optional @@ -302,13 +309,6 @@ export class CpuTopologyService { } } - if (!Number.isFinite(packageIndex)) { - packageIndex = nextFallbackPackageIndex; - nextFallbackPackageIndex += 1; - } else { - nextFallbackPackageIndex = Math.max(nextFallbackPackageIndex, packageIndex + 1); - } - for (const f of files) { if (!(f.startsWith('power') && f.endsWith('_input'))) continue; @@ -322,12 +322,22 @@ export class CpuTopologyService { if (!Number.isFinite(rounded)) continue; + if (packageIndex === null) { + packageIndex = nextFallbackPackageIndex; + nextFallbackPackageIndex += 1; + } + if (!results[packageIndex]) results[packageIndex] = {}; results[packageIndex][`${chipName}:${f}`] = rounded; + wrotePower = true; } catch (err) { this.logger.warn('Failed to read file', err); } } + + if (wrotePower && packageIndex !== null) { + nextFallbackPackageIndex = Math.max(nextFallbackPackageIndex, packageIndex + 1); + } } } catch { return {}; From 090b3a962f549cdc158223b97cda27ca246b22ea Mon Sep 17 00:00:00 2001 From: Simon Fairweather Date: Thu, 21 May 2026 08:52:59 +0100 Subject: [PATCH 7/7] fix(cpu): always convert hwmon power from microwatts --- .../unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts index c44278e44b..2f99f2ee2d 100644 --- a/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts +++ b/api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts @@ -317,7 +317,7 @@ export class CpuTopologyService { const parsed = Number(raw.trim()); if (!Number.isFinite(parsed) || parsed < 0) continue; - const watts = parsed > 1000 ? parsed / 1_000_000 : parsed; + const watts = parsed / 1_000_000; const rounded = Math.round(watts * 100) / 100; if (!Number.isFinite(rounded)) continue;