Repository: giampaolo/psutil
Pull request #974 (2017-02-11 15:56:20)

Add new sensors_fans method
Add a new method to grab the fans speed.

The method is called sensors_fans():

```
>>> import psutil
>>> psutil.sensors_fans()
defaultdict(<type 'list'>, {'dell_smm': [('Processor Fan', 0)]})
```


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 05bbd936..457003df 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -2,6 +2,13 @@
 
 *2017-02-07*
 
+5.1.4
+=====
+
+**Enhancements**
+
+- 971_: [Linux] Add sensors_fans method.
+
 5.1.3
 =====
 
diff --git a/docs/index.rst b/docs/index.rst
index 1a0438a1..d8e1af80 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -653,6 +653,25 @@ Sensors
     This API is experimental. Backward incompatible changes may occur if
     deemed necessary.
 
+.. function:: sensors_fans()
+
+  Return hardware fans speed. Each entry is a named tuple representing a
+  certain hardware sensor.
+  All speed is expressed in RPM (round per minut). Example::
+
+    >>> import psutil
+    >>> psutil.sensors_fans()
+    defaultdict(<type 'list'>, {'dell_smm': [('Processor Fan', 3028)]})
+
+  Availability: Linux
+
+  .. versionadded:: 5.1.4
+
+  .. warning::
+
+    This API is experimental. Backward incompatible changes may occur if
+    deemed necessary.
+
 .. function:: sensors_battery()
 
   Return battery status information as a named tuple including the following
diff --git a/psutil/__init__.py b/psutil/__init__.py
index 7b539ef6..244e6fca 100644
--- a/psutil/__init__.py
+++ b/psutil/__init__.py
@@ -187,7 +187,7 @@ __all__ = [
     "net_io_counters", "net_connections", "net_if_addrs",           # network
     "net_if_stats",
     "disk_io_counters", "disk_partitions", "disk_usage",            # disk
-    # "sensors_temperatures", "sensors_battery",                    # sensors
+    # "sensors_temperatures", "sensors_battery", "sensors_fans"     # sensors
     "users", "boot_time",                                           # others
 ]
 __all__.extend(_psplatform.__extra__all__)
@@ -2234,6 +2234,19 @@ if hasattr(_psplatform, "sensors_temperatures"):
     __all__.append("sensors_temperatures")
 
 
+# Linux
+if hasattr(_psplatform, "sensors_fans"):
+
+    def sensors_fans():
+        """Return fans speed. Each entry is a namedtuple
+        representing a certain hardware sensor.
+        All speed are expressed in RPM (rounds per minute).
+        """
+        return _psplatform.sensors_fans()
+
+    __all__.append("sensors_fans")
+
+
 # Linux, Windows, FreeBSD
 if hasattr(_psplatform, "sensors_battery"):
 
diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py
index e019bbcf..ce20e8a0 100644
--- a/psutil/_pslinux.py
+++ b/psutil/_pslinux.py
@@ -1118,6 +1118,35 @@ def sensors_temperatures():
     return ret
 
 
+def sensors_fans():
+    """Return hardware (CPU and others) fans as a dict
+    including hardware label, current speed.
+
+    Implementation notes:
+    - /sys/class/hwmon looks like the most recent interface to
+      retrieve this info, and this implementation relies on it
+      only (old distros will probably use something else)
+    - lm-sensors on Ubuntu 16.04 relies on /sys/class/hwmon
+    """
+    ret = collections.defaultdict(list)
+    basenames = glob.glob('/sys/class/hwmon/hwmon*/fan*_*')
+    if not basenames:
+        # CentOS has an intermediate /device directory:
+        # https://github.com/giampaolo/psutil/issues/971
+        basenames = glob.glob('/sys/class/hwmon/hwmon*/device/fan*_*')
+
+    basenames = sorted(set([x.split('_')[0] for x in basenames]))
+    for base in basenames:
+        unit_name = cat(os.path.join(os.path.dirname(base), 'name'),
+                        binary=False)
+        label = cat(base + '_label', fallback='', binary=False)
+        current = int(cat(base + '_input'))
+
+        ret[unit_name].append((label, current))
+
+    return ret
+
+
 def sensors_battery():
     """Return battery information.
     Implementation note: it appears /sys/class/power_supply/BAT0/


Test patch:
diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py
index 80717564..e5fda9ad 100755
--- a/psutil/tests/test_linux.py
+++ b/psutil/tests/test_linux.py
@@ -1020,6 +1020,19 @@ class TestMisc(unittest.TestCase):
             t.stop()
 
 
+@unittest.skipUnless(LINUX, "LINUX only")
+@unittest.skipUnless(hasattr(psutil, "sensors_fans") and
+                     psutil.sensors_fans() is not None,
+                     "no fan")
+class TestSensorsFans(unittest.TestCase):
+
+    def test_current(self):
+        psutil_value = psutil.sensors_fans()
+        for fk in psutil_value:
+            # Fan speed should always be > 0
+            self.assertTrue(psutil_value[fk][0][1] >= 0)
+
+
 @unittest.skipUnless(LINUX, "LINUX only")
 @unittest.skipUnless(hasattr(psutil, "sensors_battery") and
                      psutil.sensors_battery() is not None,