Repository: giampaolo/psutil
Pull request #962 (2017-02-01 15:22:36)

371 temperatures


Patch:
diff --git a/README.rst b/README.rst
index 20c2f9b3..00fbc87e 100644
--- a/README.rst
+++ b/README.rst
@@ -181,6 +181,21 @@ Network
     {'eth0': snicstats(isup=True, duplex=<NicDuplex.NIC_DUPLEX_FULL: 2>, speed=100, mtu=1500),
      'lo': snicstats(isup=True, duplex=<NicDuplex.NIC_DUPLEX_UNKNOWN: 0>, speed=0, mtu=65536)}
 
+Sensors (Linux only)
+====================
+
+.. code-block:: python
+
+    >>> import psutil
+    >>> psutil.sensors_temperatures()
+    {'acpitz': [shwtemp(label='', current=47.0, high=103.0, critical=103.0)],
+     'asus': [shwtemp(label='', current=47.0, high=None, critical=None)],
+     'coretemp': [shwtemp(label='Physical id 0', current=52.0, high=100.0, critical=100.0),
+                  shwtemp(label='Core 0', current=45.0, high=100.0, critical=100.0),
+                  shwtemp(label='Core 1', current=52.0, high=100.0, critical=100.0),
+                  shwtemp(label='Core 2', current=45.0, high=100.0, critical=100.0),
+                  shwtemp(label='Core 3', current=47.0, high=100.0, critical=100.0)]}
+
 Other system info
 =================
 
diff --git a/docs/index.rst b/docs/index.rst
index 645df8df..87ebf734 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -617,6 +617,41 @@ Network
   .. versionadded:: 3.0.0
 
 
+Sensors
+-------
+
+.. function:: sensors_temperatures(fahrenheit=False)
+
+  Return hardware temperatures. Each entry is a namedtuple representing a
+  certain hardware sensor (it may be a CPU, an hard disk or something
+  else, depending on the OS and its configuration).
+  All temperatures are expressed in celsius unless *fahrenheit* is set to
+  ``True``. Example::
+
+    >>> import psutil
+    >>> psutil.sensors_temperatures()
+    {'acpitz': [shwtemp(label='', current=47.0, high=103.0, critical=103.0)],
+     'asus': [shwtemp(label='', current=47.0, high=None, critical=None)],
+     'coretemp': [shwtemp(label='Physical id 0', current=52.0, high=100.0, critical=100.0),
+                  shwtemp(label='Core 0', current=45.0, high=100.0, critical=100.0),
+                  shwtemp(label='Core 1', current=52.0, high=100.0, critical=100.0),
+                  shwtemp(label='Core 2', current=45.0, high=100.0, critical=100.0),
+                  shwtemp(label='Core 3', current=47.0, high=100.0, critical=100.0)]}
+
+  See also `sensors.py <https://github.com/giampaolo/psutil/blob/master/scripts/sensors.py>`__
+  for an example application.
+
+  .. warning::
+
+    This API is experimental. Backwards incompatible changes may occur if
+    deemed necessary.
+
+  Availability: Linux
+
+  .. versionadded:: 5.1.0
+
+
+
 Other system info
 -----------------
 
diff --git a/psutil/__init__.py b/psutil/__init__.py
index 647af0a6..e13866a3 100644
--- a/psutil/__init__.py
+++ b/psutil/__init__.py
@@ -185,6 +185,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
     "users", "boot_time",                                           # others
 ]
 __all__.extend(_psplatform.__extra__all__)
@@ -2180,6 +2181,50 @@ def net_if_stats():
     return _psplatform.net_if_stats()
 
 
+# =====================================================================
+# --- sensors
+# =====================================================================
+
+
+if hasattr(_psplatform, "sensors_temperatures"):
+
+    def sensors_temperatures(fahrenheit=False):
+        """Return hardware temperatures. Each entry is a namedtuple
+        representing a certain hardware sensor (it may be a CPU, an
+        hard disk or something else, depending on the OS and its
+        configuration).
+        All temperatures are expressed in celsius unless *fahrenheit*
+        is set to True.
+        """
+        def to_fahrenheit(n):
+            return (float(n) * 9 / 5) + 32
+
+        ret = collections.defaultdict(list)
+        rawdict = _psplatform.sensors_temperatures()
+
+        for name, values in rawdict.items():
+            while values:
+                label, current, high, critical = values.pop(0)
+                if fahrenheit:
+                    current = to_fahrenheit(current)
+                    if high is not None:
+                        high = to_fahrenheit(high)
+                    if critical is not None:
+                        critical = to_fahrenheit(critical)
+
+                if high and not critical:
+                    critical = high
+                elif critical and not high:
+                    high = critical
+
+                ret[name].append(
+                    _common.shwtemp(label, current, high, critical))
+
+        return dict(ret)
+
+    __all__.append("sensors_temperatures")
+
+
 # =====================================================================
 # --- other system related functions
 # =====================================================================
diff --git a/psutil/_common.py b/psutil/_common.py
index 68134820..0d965ac1 100644
--- a/psutil/_common.py
+++ b/psutil/_common.py
@@ -158,6 +158,9 @@ scpustats = namedtuple(
     'scpustats', ['ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls'])
 # psutil.cpu_freq()
 scpufreq = namedtuple('scpufreq', ['current', 'min', 'max'])
+# psutil.sensors_temperatures()
+shwtemp = namedtuple(
+    'shwtemp', ['label', 'current', 'high', 'critical'])
 
 # --- for Process methods
 
diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py
index a9fbd8bc..16de79e5 100644
--- a/psutil/_pslinux.py
+++ b/psutil/_pslinux.py
@@ -7,6 +7,7 @@
 from __future__ import division
 
 import base64
+import collections
 import errno
 import functools
 import glob
@@ -64,6 +65,7 @@ __extra__all__ = [
 
 HAS_SMAPS = os.path.exists('/proc/%s/smaps' % os.getpid())
 HAS_PRLIMIT = hasattr(cext, "linux_prlimit")
+_DEFAULT = object()
 
 # RLIMIT_* constants, not guaranteed to be present on all kernels
 if HAS_PRLIMIT:
@@ -1059,6 +1061,47 @@ def disk_partitions(all=False):
     return retlist
 
 
+# =====================================================================
+# --- sensors
+# =====================================================================
+
+
+if os.path.exists('/sys/class/hwmon'):
+
+    def sensors_temperatures():
+        """Return hardware (CPU and others) temperatures as a dict
+        including hardware name, label, current, max and critical
+        temperatures.
+
+        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
+        - /sys/class/thermal/thermal_zone* is another one but it's more
+          difficult to parse
+        """
+        ret = collections.defaultdict(list)
+        basenames = sorted(set(
+            [x.split('_')[0] for x in
+             glob.glob('/sys/class/hwmon/hwmon*/temp*_*')]))
+        for base in basenames:
+            unit_name = cat(os.path.join(os.path.dirname(base), 'name'))
+            label = cat(base + '_label', fallback='')
+            current = float(cat(base + '_input')) / 1000.0
+            high = cat(base + '_max', fallback=None)
+            critical = cat(base + '_crit', fallback=None)
+
+            if high is not None:
+                high = float(high) / 1000.0
+            if critical is not None:
+                critical = float(critical) / 1000.0
+
+            ret[unit_name].append((label, current, high, critical))
+
+        return ret
+
+
 # =====================================================================
 # --- other system functions
 # =====================================================================
diff --git a/scripts/sensors.py b/scripts/sensors.py
new file mode 100755
index 00000000..4b14180e
--- /dev/null
+++ b/scripts/sensors.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""
+A clone of 'sensors' utility on Linux printing hardware temperatures.
+
+$ python scripts/sensors.py
+asus
+    asus                 47.0 °C (high = None °C, critical = None °C)
+
+acpitz
+    acpitz               47.0 °C (high = 103.0 °C, critical = 103.0 °C)
+
+coretemp
+    Physical id 0        54.0 °C (high = 100.0 °C, critical = 100.0 °C)
+    Core 0               47.0 °C (high = 100.0 °C, critical = 100.0 °C)
+    Core 1               48.0 °C (high = 100.0 °C, critical = 100.0 °C)
+    Core 2               47.0 °C (high = 100.0 °C, critical = 100.0 °C)
+    Core 3               54.0 °C (high = 100.0 °C, critical = 100.0 °C)
+"""
+
+from __future__ import print_function
+import sys
+
+import psutil
+
+
+def main():
+    if not hasattr(psutil, "sensors_temperatures"):
+        sys.exit("platform not supported")
+    temps = psutil.sensors_temperatures()
+    for name, entries in temps.items():
+        print(name)
+        for entry in entries:
+            print("    %-20s %s °C (high = %s °C, critical = %s °C)" % (
+                entry.label or name, entry.current, entry.high,
+                entry.critical))
+        print()
+
+
+if __name__ == '__main__':
+    main()


Test patch:
diff --git a/psutil/tests/test_misc.py b/psutil/tests/test_misc.py
index 0b696f8c..8697bcff 100755
--- a/psutil/tests/test_misc.py
+++ b/psutil/tests/test_misc.py
@@ -388,7 +388,7 @@ class TestScripts(unittest.TestCase):
             src = f.read()
         ast.parse(src)
 
-    def test_check_presence(self):
+    def test_coverage(self):
         # make sure all example scripts have a test method defined
         meths = dir(self)
         for name in os.listdir(SCRIPTS_DIR):
@@ -469,6 +469,12 @@ class TestScripts(unittest.TestCase):
     def test_cpu_distribution(self):
         self.assert_syntax('cpu_distribution.py')
 
+    def test_sensors(self):
+        if hasattr(psutil, "sensors_temperatures"):
+            self.assert_stdout('sensors.py')
+        else:
+            self.assert_syntax('sensors.py')
+
 
 # ===================================================================
 # --- Unit tests for test utilities.
diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py
index db86290b..74ae9330 100755
--- a/psutil/tests/test_process.py
+++ b/psutil/tests/test_process.py
@@ -1808,10 +1808,10 @@ class TestFetchAllProcesses(unittest.TestCase):
             try:
                 st = os.stat(ret)
             except OSError as err:
-                # directory has been removed in mean time
                 if WINDOWS and err.errno in \
                         psutil._psplatform.ACCESS_DENIED_SET:
                     pass
+                # directory has been removed in mean time
                 elif err.errno != errno.ENOENT:
                     raise
             else:
diff --git a/psutil/tests/test_system.py b/psutil/tests/test_system.py
index 18a5ae2d..79ae6bba 100755
--- a/psutil/tests/test_system.py
+++ b/psutil/tests/test_system.py
@@ -29,6 +29,7 @@ from psutil import POSIX
 from psutil import SUNOS
 from psutil import WINDOWS
 from psutil._compat import long
+from psutil._compat import unicode
 from psutil.tests import AF_INET6
 from psutil.tests import APPVEYOR
 from psutil.tests import check_net_address
@@ -755,6 +756,21 @@ class TestSystemAPIs(unittest.TestCase):
         for name in names:
             self.assertIs(getattr(psutil, name), False, msg=name)
 
+    @unittest.skipUnless(hasattr(psutil, "sensors_temperatures"),
+                         "platform not suported")
+    def test_sensors_temperatures(self):
+        temps = psutil.sensors_temperatures()
+        for name, entries in temps.items():
+            self.assertIsInstance(name, (str, unicode))
+            for entry in entries:
+                self.assertIsInstance(entry.label, (str, unicode))
+                if entry.current is not None:
+                    self.assertGreaterEqual(entry.current, 0)
+                if entry.high is not None:
+                    self.assertGreaterEqual(entry.high, 0)
+                if entry.critical is not None:
+                    self.assertGreaterEqual(entry.critical, 0)
+
 
 if __name__ == '__main__':
     run_test_module_by_name(__file__)