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(); 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]; }