wrolpi/controller/lib/smart.py
Roland ead47e916b
Fix SMART temperature always reporting null.
Real drives report attribute 194 raw values like "52 (Min/Max 16/57)"
which int() cannot parse, so every drive's temperature was null.
Prefer pySMART's `temperature` property, which parses these formats
and also works for NVMe devices that have no ATA attribute table.

Verified on real hardware (WD100EMAZ over USB-SAT): 51-53C now
reported where null was returned before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 19:46:31 -06:00

92 lines
2.5 KiB
Python

"""
SMART disk health monitoring for WROLPi Controller.
"""
from typing import Optional
# Import pySMART only if available
try:
from pySMART import DeviceList, Device
SMART_AVAILABLE = True
except ImportError:
SMART_AVAILABLE = False
from controller.lib.config import is_docker_mode
def is_smart_available() -> bool:
"""Check if SMART monitoring is available."""
if is_docker_mode():
return False
return SMART_AVAILABLE
def get_all_smart_status() -> list[dict]:
"""
Get SMART status for all drives.
Returns:
list of SMART status dicts
"""
if not is_smart_available():
return []
try:
devices = DeviceList()
results = []
for device in devices.devices:
results.append(_get_device_smart(device))
return results
except Exception:
return []
def _get_device_smart(device) -> dict:
"""Extract SMART data from a pySMART device."""
return {
"device": device.name,
"path": f"/dev/{device.name}",
"model": device.model,
"serial": device.serial,
"capacity": device.capacity,
"assessment": device.assessment, # PASS, FAIL, WARN
"temperature": _get_temperature(device),
"power_on_hours": _get_attribute(device, "Power_On_Hours"),
"reallocated_sectors": _get_attribute(device, "Reallocated_Sector_Ct"),
"pending_sectors": _get_attribute(device, "Current_Pending_Sector"),
"smart_enabled": device.smart_enabled,
}
def _get_temperature(device) -> Optional[int]:
"""Get drive temperature.
Prefer pySMART's `temperature` property: it parses real-world raw
values like "52 (Min/Max 16/57)" that int() cannot, and works for
NVMe devices which have no ATA attribute table. Fall back to the
plain attributes for devices where the property is unavailable.
"""
temp = getattr(device, "temperature", None)
if isinstance(temp, int):
return temp
temp = _get_attribute(device, "Temperature_Celsius")
if temp is None:
temp = _get_attribute(device, "Airflow_Temperature_Cel")
return temp
def _get_attribute(device, name: str) -> Optional[int]:
"""Get a specific SMART attribute value."""
if not hasattr(device, 'attributes') or device.attributes is None:
return None
for attr in device.attributes:
if attr and attr.name == name:
try:
return int(attr.raw)
except (ValueError, TypeError):
return None
return None