Add host temperature telemetry and history

This commit is contained in:
2026-10-13 23:39:04 -04:00
parent 09396ab584
commit 171f657876
28 changed files with 1466 additions and 32 deletions
+92
View File
@@ -0,0 +1,92 @@
export const temperatureKinds = ['cpu-package', 'cpu-die', 'cpu-core', 'nvme', 'chipset'] as const;
export type TemperatureKind = (typeof temperatureKinds)[number];
export interface HostTemperatureSensor {
id: string;
device: string;
label: string;
kind: TemperatureKind;
celsius: number;
}
/** Optional telemetry must never make an otherwise valid heartbeat fail. */
export function validateTemperatures(value: unknown): HostTemperatureSensor[] {
if (!Array.isArray(value)) return [];
const seen = new Set<string>();
return value.slice(0, 128).flatMap((sensor) => {
if (
!sensor ||
typeof sensor !== 'object' ||
Array.isArray(sensor) ||
typeof sensor.id !== 'string' ||
!/^[a-zA-Z0-9_-]{1,100}$/.test(sensor.id) ||
seen.has(sensor.id) ||
!['device', 'label'].every(
(key) =>
typeof sensor[key] === 'string' &&
sensor[key].length > 0 &&
sensor[key].length <= 120
) ||
!temperatureKinds.includes(sensor.kind) ||
typeof sensor.celsius !== 'number' ||
!Number.isFinite(sensor.celsius) ||
sensor.celsius < -100 ||
sensor.celsius > 200
)
return [];
seen.add(sensor.id);
return [
{
id: sensor.id,
device: sensor.device,
label: sensor.label,
kind: sensor.kind,
celsius: sensor.celsius
}
];
});
}
export function cpuTemperature(
sensors: HostTemperatureSensor[] = []
): HostTemperatureSensor | null {
// Prefer physical package/die readings. Cores are a fallback, never mixed
// into an average that might hide a hot package on a multi-socket host.
for (const kind of ['cpu-package', 'cpu-die', 'cpu-core'] as const) {
const candidates = sensors.filter((sensor) => sensor.kind === kind);
if (candidates.length)
return candidates.reduce((hot, sensor) =>
sensor.celsius > hot.celsius ? sensor : hot
);
}
return null;
}
export function temperatureLabel(sensor: HostTemperatureSensor): string {
if (sensor.kind === 'cpu-package')
return `CPU package ${sensor.label.replace(/^Package id /, '')}`;
if (sensor.kind === 'cpu-core') return `CPU ${sensor.label.toLowerCase()}`;
if (sensor.kind === 'cpu-die') return `CPU die · ${sensor.label}`;
if (sensor.kind === 'chipset') return 'Chipset';
return `NVMe · ${sensor.label}`;
}
export function formatTemperature(celsius: number | null | undefined): string {
return typeof celsius === 'number' && Number.isFinite(celsius)
? `${Number(celsius.toFixed(1))} °C`
: 'Unavailable';
}
/** One CPU series per host, plus bounded device series for the host drill-down. */
export function historyTemperatures(
sensors: HostTemperatureSensor[] = []
): HostTemperatureSensor[] {
const cpu = cpuTemperature(sensors);
const devices = sensors
.filter(
(sensor) =>
sensor.kind === 'chipset' ||
(sensor.kind === 'nvme' && sensor.label.toLowerCase() === 'composite')
)
.slice(0, 16);
return [...(cpu ? [{ ...cpu, id: 'cpu', label: 'CPU temperature' }] : []), ...devices];
}