banshee is under active development: collectors will keep changing, and a version you publish will likely need adjustments. For stable use, wait for version 1.0.0.

Linux

banshee/linux-collection/linux/ssh/

temperatures

Temperatures of a Linux by SSH, from hwmon or thermal zones, with the sensors' own thresholds.

  • linux
  • ssh
  • temperature
  • hardware
  • alpine
  • debian
  • fedora

by banshee1 installation2 versions

v2Latest

Published on October 28, 2026. One naming convention across collections; documentation completed.

Keen 1.0.0 · 84 lines
// The temperatures of a Linux, by SSH: each sensor the kernel exposes, and the thresholds the sensor itself
// announces. A virtual machine has no sensor: the collector says so.
// Reads /sys/class/hwmon (the sensors of the drivers: CPU, motherboard, NVMe disks...) with grep -s -H, two
// options of BusyBox as of GNU grep; failing any sensor there, /sys/class/thermal (the thermal zones, the
// only ones on some ARM boards). The kernel writes both the same way on Alpine, Debian and Fedora. A file
// that a driver does not serve is silenced by grep -s, and its line is missing.
// Thermal zones are not read beside hwmon: they often repeat its sensors.
need('protocol:ssh');

per('sensor', 'Sensors');
output('temperature', 'number', array('label' => 'Temperature', 'unit' => '°C', 'per' => 'sensor'));

watch.above('hot', 'High temperature', 'temperature', array('warning' => 80, 'critical' => 95, 'for' => '3 runs'));
watch.custom('sensor-limit', 'Sensor threshold crossed', array(
    'help' => 'Warning beyond the high threshold the sensor announces (max); critical beyond its critical threshold (crit).',
));

// One line "/sys/class/hwmon/hwmon0/temp1_input:45000" per readable file, in thousandths of a degree;
// "name" names the chip, "temp1_label" the sensor when the driver names it.
$hwmon = ssh.run('grep -s -H . /sys/class/hwmon/hwmon*/name /sys/class/hwmon/hwmon*/temp*_input /sys/class/hwmon/hwmon*/temp*_label /sys/class/hwmon/hwmon*/temp*_max /sys/class/hwmon/hwmon*/temp*_crit');
$chips = array();
foreach (text.lines($hwmon['output']) as $line) {
    $m = text.match($line, '^/sys/class/hwmon/([^/]+)/([^:]+):(.*)$');
    if (list.count($m) > 0) {
        $chips[$m[1]][$m[2]] = text.trim($m[3]);
    }
}

// Two sensors of the same name each keep their own: the second carries its chip.
$seen = array();
foreach ($chips as $chip => $files) {
    foreach ($files as $file => $reading) {
        $t = text.match($file, '^temp([0-9]+)_input$');
        if (list.count($t) == 0 || !text.is_number($reading)) {
            continue;
        }
        $name = list.get($files, 'name', $chip) . ' ' . list.get($files, 'temp' . $t[1] . '_label', 'temp' . $t[1]);
        if (list.has($seen, $name)) {
            $name = $name . ' (' . $chip . ')';
        }
        $seen[$name] = true;
        $celsius = number.round($reading / 1000, 1);
        value('temperature', $celsius, $name);

        // A threshold missing, zero or unreadable judges nothing.
        $crit = list.get($files, 'temp' . $t[1] . '_crit', '');
        $max = list.get($files, 'temp' . $t[1] . '_max', '');
        if (text.is_number($crit) && $crit > 0 && $reading >= $crit) {
            watch.raise('sensor-limit', 'critical', $name . ' at ' . $celsius . ' °C, critical threshold ' . number.round($crit / 1000, 1) . ' °C', $name);
        }
        if (text.is_number($max) && $max > 0 && $reading >= $max) {
            watch.raise('sensor-limit', 'warning', $name . ' at ' . $celsius . ' °C, high threshold ' . number.round($max / 1000, 1) . ' °C', $name);
        }
    }
}

// Thermal zones: "/sys/class/thermal/thermal_zone0/type:cpu-thermal" and ".../temp:48312".
if (list.count($seen) == 0) {
    $thermal = ssh.run('grep -s -H . /sys/class/thermal/thermal_zone*/type /sys/class/thermal/thermal_zone*/temp');
    $zones = array();
    foreach (text.lines($thermal['output']) as $line) {
        $m = text.match($line, '^/sys/class/thermal/([^/]+)/(type|temp):(.*)$');
        if (list.count($m) > 0) {
            $zones[$m[1]][$m[2]] = text.trim($m[3]);
        }
    }
    foreach ($zones as $zone => $files) {
        $reading = list.get($files, 'temp', '');
        if (!text.is_number($reading)) {
            continue;
        }
        $name = list.get($files, 'type', $zone);
        if (list.has($seen, $name)) {
            $name = $name . ' (' . $zone . ')';
        }
        $seen[$name] = true;
        value('temperature', number.round($reading / 1000, 1), $name);
    }
}

if (list.count($seen) == 0) {
    fail('misconfigured', 'No temperature sensor found in /sys/class/hwmon or /sys/class/thermal',
        'A virtual machine exposes none.');
}