From 87ed286f7d009963ec49be1694459e82bf43bc0a Mon Sep 17 00:00:00 2001 From: Roland Date: Tue, 2 Jun 2026 13:50:47 -0600 Subject: [PATCH] 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) --- controller/lib/smart.py | 11 ++++++++++- controller/test/test_smart.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/controller/lib/smart.py b/controller/lib/smart.py index 10418e88..b7227e91 100644 --- a/controller/lib/smart.py +++ b/controller/lib/smart.py @@ -62,7 +62,16 @@ def _get_device_smart(device) -> dict: def _get_temperature(device) -> Optional[int]: - """Get drive temperature from SMART attributes.""" + """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") diff --git a/controller/test/test_smart.py b/controller/test/test_smart.py index fa6cc618..1a77871d 100644 --- a/controller/test/test_smart.py +++ b/controller/test/test_smart.py @@ -119,11 +119,47 @@ class TestGetTemperature: def test_returns_none_if_not_found(self): """Should return None if no temperature attribute.""" mock_device = mock.Mock() + mock_device.temperature = None mock_device.attributes = [] result = _get_temperature(mock_device) assert result is None + def test_prefers_pysmart_temperature_property(self): + """Real drives report raw values like '52 (Min/Max 16/57)' which + int() cannot parse; pySMART's temperature property handles them.""" + mock_attr = mock.Mock() + mock_attr.name = "Temperature_Celsius" + mock_attr.raw = "52 (Min/Max 16/57)" + + mock_device = mock.Mock() + mock_device.temperature = 52 + mock_device.attributes = [mock_attr] + + result = _get_temperature(mock_device) + assert result == 52 + + def test_pysmart_temperature_property_for_nvme(self): + """NVMe devices have no ATA attribute table, only the property.""" + mock_device = mock.Mock() + mock_device.temperature = 38 + mock_device.attributes = None + + result = _get_temperature(mock_device) + assert result == 38 + + def test_ignores_non_int_temperature_property(self): + """A missing/bogus temperature property falls back to attributes.""" + mock_attr = mock.Mock() + mock_attr.name = "Temperature_Celsius" + mock_attr.raw = "35" + + mock_device = mock.Mock() # .temperature is a Mock, not an int + mock_device.attributes = [mock_attr] + + result = _get_temperature(mock_device) + assert result == 35 + class TestGetAttribute: """Tests for _get_attribute function."""