Hardware
banshee/hardware-collection/hardware/redfish/https/
thermal
The reading and health of each temperature sensor and fan of a server by Redfish.
by banshee1 installation1 version
Published on October 27, 2026. First version.
// The temperatures and fans of a server, by the Redfish service of its management card (DMTF Redfish): the
// reading of each temperature sensor and each fan, and its health, as the card judges it against its own
// thresholds. Any card that serves Redfish: Dell iDRAC, HPE iLO, Supermicro, Lenovo XCC, OpenBMC.
// A chassis is read by its Thermal resource, which the cards serve, deprecated since schema 2020.4; a chassis
// that serves only the ThermalSubsystem of the later schemas is read by it. Thermal is preferred when both
// are served: it gives each temperature sensor its own health.
need('protocol:http');
$address = need('host:address');
$user = need('protocol:http:user');
$password = need('protocol:http:password');
per('sensor', 'Sensors');
output('health', 'text', array('label' => 'Health', 'per' => 'sensor'));
output('temperature', 'number', array('label' => 'Temperature', 'unit' => '°C', 'per' => 'sensor'));
output('fan-speed', 'number', array('label' => 'Fan speed', 'unit' => 'rpm', 'per' => 'sensor'));
output('fan-percent', 'number', array('label' => 'Fan speed', 'unit' => '%', 'per' => 'sensor'));
watch.state('unhealthy', 'Sensor health degraded', 'health', array('warning' => array('warning'), 'critical' => array('critical')));
watch.above('hot', 'High temperature', 'temperature', array(
'help' => 'Raises above the temperature set on the probe. The card already judges each sensor against its own thresholds, in its health.',
'enabled' => false,
));
watch.below('fan-slow', 'Fan slow', 'fan-speed', array(
'help' => 'Raises below the speed set on the probe, in rpm.',
'enabled' => false,
));
watch.missing('missing', 'Sensor missing', 'health', array('enabled' => false));
// Returns one Redfish resource, read by its path (/redfish/v1/...), the account in Basic. The password only
// goes in the Basic header: the answer is not secret.
function redfish_get($address, $path, $user, $password) {
$answer = http.get('https://' . $address . $path, headers=array('Accept' => 'application/json'), user=$user, password=$password);
if ($answer['status'] == 401 || $answer['status'] == 403) {
fail('misconfigured', 'The Redfish service refuses the account, code ' . $answer['status'], $path);
}
if ($answer['status'] != 200) {
fail('unreachable', 'The Redfish service answers code ' . $answer['status'], $path);
}
return json.read($answer['body']);
}
// Returns the health of a resource, in lower case (ok, warning, critical): the health it rolls up from what
// is under it when it gives one, its own otherwise, unknown when it gives neither.
function health_of($resource) {
$status = list.get($resource, 'Status', array());
$health = list.get($status, 'HealthRollup', '');
if ($health == '') {
$health = list.get($status, 'Health', '');
}
if ($health == '') {
return 'unknown';
}
return text.lower($health);
}
// Returns the instance name of each item: its name, or, when another item bears the same name, its name
// followed by its id.
function instance_names($items) {
$counts = array();
foreach ($items as $item) {
$counts[$item['name']] = list.get($counts, $item['name'], 0) + 1;
}
$names = array();
foreach ($items as $key => $item) {
$names[$key] = $item['name'];
if ($item['name'] == '' || $counts[$item['name']] > 1) {
$names[$key] = text.trim($item['name'] . ' ' . $item['id']);
}
// A card that gives neither a name nor an id (both null, read as '').
if ($names[$key] == '') {
$names[$key] = 'unnamed ' . ($key + 1);
}
}
return $names;
}
// Each sensor, as array('name', 'id'), and what it reads: 'health', 'temperature', 'rpm', 'percent'.
$items = array();
$chassis_list = redfish_get($address, '/redfish/v1/Chassis', $user, $password);
foreach (list.get($chassis_list, 'Members', array()) as $member) {
$chassis = redfish_get($address, $member['@odata.id'], $user, $password);
$chassis_id = list.get($chassis, 'Id', $member['@odata.id']);
// Thermal: {"Temperatures": [{"MemberId": "0", "Name": "CPU1 Temp", "ReadingCelsius": 45, "Status":
// {"State": "Enabled", "Health": "OK"}}], "Fans": [{"MemberId": "0", "Name": "Fan 1", "Reading": 5880,
// "ReadingUnits": "RPM", "Status": {...}}]}. A fan is named FanName before Thermal 1.1. A sensor the
// card says absent is an empty slot.
if (list.has($chassis, 'Thermal')) {
$thermal = redfish_get($address, $chassis['Thermal']['@odata.id'], $user, $password);
foreach (list.get($thermal, 'Temperatures', array()) as $sensor) {
if (list.get(list.get($sensor, 'Status', array()), 'State', '') == 'Absent') {
continue;
}
$item = array('name' => list.get($sensor, 'Name', 'Temperature'), 'id' => list.get($sensor, 'MemberId', ''),
'health' => health_of($sensor));
if (list.get($sensor, 'ReadingCelsius', '') != '') {
$item['temperature'] = $sensor['ReadingCelsius'];
}
$items = list.add($items, $item);
}
foreach (list.get($thermal, 'Fans', array()) as $fan) {
if (list.get(list.get($fan, 'Status', array()), 'State', '') == 'Absent') {
continue;
}
$item = array('name' => list.get($fan, 'Name', list.get($fan, 'FanName', 'Fan')),
'id' => list.get($fan, 'MemberId', ''), 'health' => health_of($fan));
if (list.get($fan, 'Reading', '') != '') {
if (list.get($fan, 'ReadingUnits', 'RPM') == 'Percent') {
$item['percent'] = $fan['Reading'];
} else {
$item['rpm'] = $fan['Reading'];
}
}
$items = list.add($items, $item);
}
} elseif (list.has($chassis, 'ThermalSubsystem')) {
// ThermalSubsystem: {"Fans": {"@odata.id": ".../Fans"}, "ThermalMetrics": {"@odata.id": "..."},
// "Status": {...}}; each fan a resource of its own, {"Id": "1", "Name": "Fan 1", "SpeedPercent":
// {"Reading": 45, "SpeedRPM": 5880}, "Status": {...}}. ThermalMetrics gives the temperatures without a
// health of their own: {"TemperatureReadingsCelsius": [{"DeviceName": "CPU1", "Reading": 45,
// "DataSourceUri": "..."}]}; the subsystem's own health stands for them.
$subsystem = redfish_get($address, $chassis['ThermalSubsystem']['@odata.id'], $user, $password);
$items = list.add($items, array('name' => 'Thermal subsystem', 'id' => $chassis_id, 'health' => health_of($subsystem)));
if (list.has($subsystem, 'Fans')) {
$fans = redfish_get($address, $subsystem['Fans']['@odata.id'], $user, $password);
foreach (list.get($fans, 'Members', array()) as $link) {
$fan = redfish_get($address, $link['@odata.id'], $user, $password);
if (list.get(list.get($fan, 'Status', array()), 'State', '') == 'Absent') {
continue;
}
$item = array('name' => list.get($fan, 'Name', 'Fan'), 'id' => list.get($fan, 'Id', ''),
'health' => health_of($fan));
$speed = list.get($fan, 'SpeedPercent', array());
if (list.get($speed, 'Reading', '') != '') {
$item['percent'] = $speed['Reading'];
}
if (list.get($speed, 'SpeedRPM', '') != '') {
$item['rpm'] = $speed['SpeedRPM'];
}
$items = list.add($items, $item);
}
}
if (list.has($subsystem, 'ThermalMetrics')) {
$metrics = redfish_get($address, $subsystem['ThermalMetrics']['@odata.id'], $user, $password);
foreach (list.get($metrics, 'TemperatureReadingsCelsius', array()) as $sensor) {
if (list.get($sensor, 'Reading', '') == '') {
continue;
}
$items = list.add($items, array('name' => list.get($sensor, 'DeviceName', 'Temperature'),
'id' => list.get($sensor, 'DataSourceUri', ''), 'temperature' => $sensor['Reading']));
}
}
}
}
if (list.count($items) == 0) {
fail('misconfigured', 'The Redfish service serves no temperature and no fan',
'No chassis has a Thermal or a ThermalSubsystem resource.');
}
$names = instance_names($items);
foreach ($items as $key => $item) {
$name = $names[$key];
if (list.has($item, 'health')) {
value('health', $item['health'], $name);
}
if (list.has($item, 'temperature')) {
value('temperature', $item['temperature'], $name);
}
if (list.has($item, 'rpm')) {
value('fan-speed', $item['rpm'], $name);
}
if (list.has($item, 'percent')) {
value('fan-percent', $item['percent'], $name);
}
}