pySMART

Copyright (C) 2014 Marc Herndon

pySMART is a simple Python wrapper for the smartctl component of smartmontools. It works under Linux and Windows, as long as smartctl is on the system path. Running with administrative (root) privilege is strongly recommended, as smartctl cannot accurately detect all device types or parse all SMART information without full permissions.

With only a device's name (ie: /dev/sda, pd0), the API will create a Device object, populated with all relevant information about that device. The documented API can then be used to query this object for information, initiate device self-tests, and perform other functions.

Usage

The most common way to use pySMART is to create a logical representation of the physical storage device that you would like to work with, as shown:

#!bash
>>> from pySMART import Device
>>> sda = Device('/dev/sda')
>>> sda
<SATA device on /dev/sda mod:WDC WD5000AAKS-60Z1A0 sn:WD-WCAWFxxxxxxx>

Device class members can be accessed directly, and a number of helper methods are provided to retrieve information in bulk. Some examples are shown below:

#!bash
>>> sda.assessment  # Query the SMART self-assessment
'PASS'
>>> sda.attributes[9]  # Query a single SMART attribute
<SMART Attribute 'Power_On_Hours' 068/000 raw:23644>
>>> sda.all_attributes()  # Print the entire SMART attribute table
ID# ATTRIBUTE_NAME          CUR WST THR TYPE     UPDATED WHEN_FAIL    RAW
  1 Raw_Read_Error_Rate     200 200 051 Pre-fail Always  -           0
  3 Spin_Up_Time            141 140 021 Pre-fail Always  -           3908
  4 Start_Stop_Count        098 098 000 Old_age  Always  -           2690
  5 Reallocated_Sector_Ct   200 200 140 Pre-fail Always  -           0
    ... # Edited for brevity
199 UDMA_CRC_Error_Count    200 200 000 Old_age  Always  -           0
200 Multi_Zone_Error_Rate   200 200 000 Old_age  Offline -           0
>>> sda.tests[0]  # Query the most recent self-test result
<SMART Self-test [Short offline|Completed without error] hrs:23734 lba:->
>>> sda.all_selftests()  # Print the entire self-test log
ID Test_Description Status                        Left Hours  1st_Error@lba
 1 Short offline    Completed without error       00%  23734  -
 2 Short offline    Completed without error       00%  23734  -
   ... # Edited for brevity
 7 Short offline    Completed without error       00%  23726  -
 8 Short offline    Completed without error       00%  1      -

Alternatively, the package provides a DeviceList class. When instantiated, this will auto-detect all local storage devices and create a list containing one Device object for each detected storage device.

#!bash
>>> from pySMART import DeviceList
>>> devlist = DeviceList()
>>> devlist
<DeviceList contents:
<SAT device on /dev/sdb mod:WDC WD20EADS-00R6B0 sn:WD-WCAVYxxxxxxx>
<SAT device on /dev/sdc mod:WDC WD20EADS-00S2B0 sn:WD-WCAVYxxxxxxx>
<CSMI device on /dev/csmi0,0 mod:WDC WD5000AAKS-60Z1A0 sn:WD-WCAWFxxxxxxx>
>
>>> devlist.devices[0].attributes[5]  # Access Device data as above
<SMART Attribute 'Reallocated_Sector_Ct' 173/140 raw:214>

In the above cases if a new DeviceList is empty or a specific Device reports an "UNKNOWN INTERFACE", you are likely running without administrative privileges. On POSIX systems, you can request smartctl is run as a superuser by setting the sudo attribute of the global SMARTCTL object to True. Note this may cause you to be prompted for a password.

#!bash
>>> from pySMART import DeviceList
>>> from pySMART import Device
>>> sda = Device('/dev/sda')
>>> sda
<UNKNOWN INTERFACE device on /dev/sda mod:None sn:None>
>>> devlist = DeviceList()
>>> devlist
<DeviceList contents:
>
>>> from pySMART import SMARTCTL
>>> SMARTCTL.sudo = True
>>> sda = Device('/dev/sda')
>>> sda
[sudo] password for user:
<SAT device on /dev/sda mod:ST10000DM0004-1ZC101 sn:ZA20VNPT>
>>> devlist = DeviceList()
>>> devlist
<DeviceList contents:
<NVME device on /dev/nvme0 mod:Sabrent Rocket 4.0 1TB sn:03850709185D88300410>
<NVME device on /dev/nvme1 mod:Samsung SSD 970 EVO Plus 2TB sn:S59CNM0RB05028D>
<NVME device on /dev/nvme2 mod:Samsung SSD 970 EVO Plus 2TB sn:S59CNM0RB05113H>
<SAT device on /dev/sda mod:ST10000DM0004-1ZC101 sn:ZA20VNPT>
<SAT device on /dev/sdb mod:ST10000DM0004-1ZC101 sn:ZA22W366>
<SAT device on /dev/sdc mod:ST10000DM0004-1ZC101 sn:ZA22SPLG>
<SAT device on /dev/sdd mod:ST10000DM0004-1ZC101 sn:ZA2215HL>
>

In general, it is recommended to run the base script with enough privileges to execute smartctl, but this is not possible in all cases, so this workaround is provided as a convenience. However, note that using sudo inside other non-terminal projects may cause dev-bugs/issues.

Using the pySMART wrapper, Python applications be be rapidly developed to take advantage of the powerful features of smartmontools.

Acknowledgements

I would like to thank the entire team behind smartmontools for creating and maintaining such a fantastic product.

In particular I want to thank Christian Franke, who maintains the Windows port of the software. For several years I have written Windows batch files that rely on smartctl.exe to automate evaluation and testing of large pools of storage devices under Windows. Without his work, my job would have been significantly more miserable. :)

Having recently migrated my development from Batch to Python for Linux portability, I thought a simple wrapper for smartctl would save time in the development of future automated test tools.

  1# Copyright (C) 2014 Marc Herndon
  2#
  3# This program is free software; you can redistribute it and/or
  4# modify it under the terms of the GNU General Public License,
  5# version 2, as published by the Free Software Foundation.
  6#
  7# This program is distributed in the hope that it will be useful,
  8# but WITHOUT ANY WARRANTY; without even the implied warranty of
  9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 10# GNU General Public License for more details.
 11#
 12# You should have received a copy of the GNU General Public License
 13# along with this program; if not, write to the Free Software
 14# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 15# MA  02110-1301, USA.
 16#
 17################################################################
 18"""
 19Copyright (C) 2014 Marc Herndon
 20
 21pySMART is a simple Python wrapper for the `smartctl` component of
 22`smartmontools`. It works under Linux and Windows, as long as smartctl is on
 23the system path. Running with administrative (root) privilege is strongly
 24recommended, as smartctl cannot accurately detect all device types or parse
 25all SMART information without full permissions.
 26
 27With only a device's name (ie: /dev/sda, pd0), the API will create a
 28`Device` object, populated with all relevant information about
 29that device. The documented API can then be used to query this object for
 30information, initiate device self-tests, and perform other functions.
 31
 32Usage
 33-----
 34The most common way to use pySMART is to create a logical representation of the
 35physical storage device that you would like to work with, as shown:
 36
 37    #!bash
 38    >>> from pySMART import Device
 39    >>> sda = Device('/dev/sda')
 40    >>> sda
 41    <SATA device on /dev/sda mod:WDC WD5000AAKS-60Z1A0 sn:WD-WCAWFxxxxxxx>
 42
 43`Device` class members can be accessed directly, and a number of helper methods
 44are provided to retrieve information in bulk.  Some examples are shown below:
 45
 46    #!bash
 47    >>> sda.assessment  # Query the SMART self-assessment
 48    'PASS'
 49    >>> sda.attributes[9]  # Query a single SMART attribute
 50    <SMART Attribute 'Power_On_Hours' 068/000 raw:23644>
 51    >>> sda.all_attributes()  # Print the entire SMART attribute table
 52    ID# ATTRIBUTE_NAME          CUR WST THR TYPE     UPDATED WHEN_FAIL    RAW
 53      1 Raw_Read_Error_Rate     200 200 051 Pre-fail Always  -           0
 54      3 Spin_Up_Time            141 140 021 Pre-fail Always  -           3908
 55      4 Start_Stop_Count        098 098 000 Old_age  Always  -           2690
 56      5 Reallocated_Sector_Ct   200 200 140 Pre-fail Always  -           0
 57        ... # Edited for brevity
 58    199 UDMA_CRC_Error_Count    200 200 000 Old_age  Always  -           0
 59    200 Multi_Zone_Error_Rate   200 200 000 Old_age  Offline -           0
 60    >>> sda.tests[0]  # Query the most recent self-test result
 61    <SMART Self-test [Short offline|Completed without error] hrs:23734 lba:->
 62    >>> sda.all_selftests()  # Print the entire self-test log
 63    ID Test_Description Status                        Left Hours  1st_Error@lba
 64     1 Short offline    Completed without error       00%  23734  -
 65     2 Short offline    Completed without error       00%  23734  -
 66       ... # Edited for brevity
 67     7 Short offline    Completed without error       00%  23726  -
 68     8 Short offline    Completed without error       00%  1      -
 69
 70Alternatively, the package provides a `DeviceList` class. When instantiated,
 71this will auto-detect all local storage devices and create a list containing
 72one `Device` object for each detected storage device.
 73
 74    #!bash
 75    >>> from pySMART import DeviceList
 76    >>> devlist = DeviceList()
 77    >>> devlist
 78    <DeviceList contents:
 79    <SAT device on /dev/sdb mod:WDC WD20EADS-00R6B0 sn:WD-WCAVYxxxxxxx>
 80    <SAT device on /dev/sdc mod:WDC WD20EADS-00S2B0 sn:WD-WCAVYxxxxxxx>
 81    <CSMI device on /dev/csmi0,0 mod:WDC WD5000AAKS-60Z1A0 sn:WD-WCAWFxxxxxxx>
 82    >
 83    >>> devlist.devices[0].attributes[5]  # Access Device data as above
 84    <SMART Attribute 'Reallocated_Sector_Ct' 173/140 raw:214>
 85
 86In the above cases if a new DeviceList is empty or a specific Device reports an
 87"UNKNOWN INTERFACE", you are likely running without administrative privileges.
 88On POSIX systems, you can request smartctl is run as a superuser by setting the
 89sudo attribute of the global SMARTCTL object to True. Note this may cause you
 90to be prompted for a password.
 91
 92    #!bash
 93    >>> from pySMART import DeviceList
 94    >>> from pySMART import Device
 95    >>> sda = Device('/dev/sda')
 96    >>> sda
 97    <UNKNOWN INTERFACE device on /dev/sda mod:None sn:None>
 98    >>> devlist = DeviceList()
 99    >>> devlist
100    <DeviceList contents:
101    >
102    >>> from pySMART import SMARTCTL
103    >>> SMARTCTL.sudo = True
104    >>> sda = Device('/dev/sda')
105    >>> sda
106    [sudo] password for user:
107    <SAT device on /dev/sda mod:ST10000DM0004-1ZC101 sn:ZA20VNPT>
108    >>> devlist = DeviceList()
109    >>> devlist
110    <DeviceList contents:
111    <NVME device on /dev/nvme0 mod:Sabrent Rocket 4.0 1TB sn:03850709185D88300410>
112    <NVME device on /dev/nvme1 mod:Samsung SSD 970 EVO Plus 2TB sn:S59CNM0RB05028D>
113    <NVME device on /dev/nvme2 mod:Samsung SSD 970 EVO Plus 2TB sn:S59CNM0RB05113H>
114    <SAT device on /dev/sda mod:ST10000DM0004-1ZC101 sn:ZA20VNPT>
115    <SAT device on /dev/sdb mod:ST10000DM0004-1ZC101 sn:ZA22W366>
116    <SAT device on /dev/sdc mod:ST10000DM0004-1ZC101 sn:ZA22SPLG>
117    <SAT device on /dev/sdd mod:ST10000DM0004-1ZC101 sn:ZA2215HL>
118    >
119
120In general, it is recommended to run the base script with enough privileges to
121execute smartctl, but this is not possible in all cases, so this workaround is
122provided as a convenience. However, note that using sudo inside other
123non-terminal projects may cause dev-bugs/issues.
124
125
126Using the pySMART wrapper, Python applications be be rapidly developed to take
127advantage of the powerful features of smartmontools.
128
129Acknowledgements
130----------------
131I would like to thank the entire team behind smartmontools for creating and
132maintaining such a fantastic product.
133
134In particular I want to thank Christian Franke, who maintains the Windows port
135of the software.  For several years I have written Windows batch files that
136rely on smartctl.exe to automate evaluation and testing of large pools of
137storage devices under Windows.  Without his work, my job would have been
138significantly more miserable. :)
139
140Having recently migrated my development from Batch to Python for Linux
141portability, I thought a simple wrapper for smartctl would save time in the
142development of future automated test tools.
143"""
144# autopep8: off
145from .testentry import TestEntry
146from .attribute import Attribute
147from . import utils
148utils.configure_trace_logging()
149from .smartctl import SMARTCTL
150from .device_list import DeviceList
151from .device import Device, smart_health_assement
152# autopep8: on
153
154
155__version__ = '1.2.1'
156__all__ = [
157    'TestEntry', 'Attribute', 'utils', 'SMARTCTL', 'DeviceList', 'Device',
158    'smart_health_assement'
159]
class TestEntry:
 25class TestEntry(object):
 26    """
 27    Contains all of the information associated with a single SMART Self-test
 28    log entry. This data is intended to exactly mirror that obtained through
 29    smartctl.
 30    """
 31
 32    def __init__(self, format, num: int, test_type, status, hours, lba, remain=None, segment=None, sense=None, asc=None,
 33                 ascq=None):
 34        self._format = format
 35        """
 36        **(str):** Indicates whether this entry was taken from an 'ata' or
 37        'scsi' self-test log. Used to display the content properly.
 38        """
 39        self.num: int = num
 40        """
 41        **(int):** Entry's position in the log from 1 (most recent) to 21
 42        (least recent).  ATA logs save the last 21 entries while SCSI logs
 43        only save the last 20.
 44        """
 45        self.type = test_type
 46        """
 47        **(str):** Type of test run.  Generally short, long (extended), or
 48        conveyance, plus offline (background) or captive (foreground).
 49        """
 50        self.status = status
 51        """
 52        **(str):** Self-test's status message, for example 'Completed without
 53        error' or 'Completed: read failure'.
 54        """
 55        self.hours = hours
 56        """
 57        **(str):** The device's power-on hours at the time the self-test
 58        was initiated.
 59        """
 60        self.LBA = lba
 61        """
 62        **(str):** Indicates the first LBA at which an error was encountered
 63        during this self-test. Presented as a decimal value for ATA/SATA
 64        devices and in hexadecimal notation for SAS/SCSI devices.
 65        """
 66        self.remain = remain
 67        """
 68        **(str):** Percentage value indicating how much of the self-test is
 69        left to perform. '00%' indicates a complete test, while any other
 70        value could indicate a test in progress or one that failed prior to
 71        completion. Only reported by ATA devices.
 72        """
 73        self.segment = segment
 74        """
 75        **(str):** A manufacturer-specific self-test segment number reported
 76        by SCSI devices on self-test failure. Set to '-' otherwise.
 77        """
 78        self.sense = sense
 79        """
 80        **(str):** SCSI sense key reported on self-test failure. Set to '-'
 81        otherwise.
 82        """
 83        self.ASC = asc
 84        """
 85        **(str):** SCSI 'Additonal Sense Code' reported on self-test failure.
 86        Set to '-' otherwise.
 87        """
 88        self.ASCQ = ascq
 89        """
 90        **(str):** SCSI 'Additonal Sense Code Quaifier' reported on self-test
 91        failure. Set to '-' otherwise.
 92        """
 93
 94    def __getstate__(self):
 95        try:
 96            num = int(self.num)
 97        except:
 98            num = None
 99        return {
100            'num': num,
101            'type': self.type,
102            'status': self.status,
103            'hours': self.hours,
104            'lba': self.LBA,
105            'remain': self.remain,
106            'segment': self.segment,
107            'sense': self.sense,
108            'asc': self.ASC,
109            'ascq': self.ASCQ
110        }
111
112    def __repr__(self):
113        """Define a basic representation of the class object."""
114        return "<SMART Self-test [%s|%s] hrs:%s LBA:%s>" % (
115            self.type, self.status, self.hours, self.LBA)
116
117    def __str__(self):
118        """
119        Define a formatted string representation of the object's content.
120        Looks nearly identical to the output of smartctl, without overflowing
121        80-character lines.
122        """
123        if self._format == 'ata':
124            return "{0:>2} {1:17}{2:30}{3:5}{4:7}{5:17}".format(
125                self.num, self.type, self.status, self.remain, self.hours,
126                self.LBA)
127        else:
128            # 'Segment' could not be fit on the 80-char line. It's of limited
129            # utility anyway due to it's manufacturer-proprietary nature...
130            return ("{0:>2} {1:17}{2:23}{3:7}{4:14}[{5:4}{6:5}{7:4}]".format(
131                self.num,
132                self.type,
133                self.status,
134                self.hours,
135                self.LBA,
136                self.sense,
137                self.ASC,
138                self.ASCQ
139            ))

Contains all of the information associated with a single SMART Self-test log entry. This data is intended to exactly mirror that obtained through smartctl.

TestEntry( format, num: int, test_type, status, hours, lba, remain=None, segment=None, sense=None, asc=None, ascq=None)
32    def __init__(self, format, num: int, test_type, status, hours, lba, remain=None, segment=None, sense=None, asc=None,
33                 ascq=None):
34        self._format = format
35        """
36        **(str):** Indicates whether this entry was taken from an 'ata' or
37        'scsi' self-test log. Used to display the content properly.
38        """
39        self.num: int = num
40        """
41        **(int):** Entry's position in the log from 1 (most recent) to 21
42        (least recent).  ATA logs save the last 21 entries while SCSI logs
43        only save the last 20.
44        """
45        self.type = test_type
46        """
47        **(str):** Type of test run.  Generally short, long (extended), or
48        conveyance, plus offline (background) or captive (foreground).
49        """
50        self.status = status
51        """
52        **(str):** Self-test's status message, for example 'Completed without
53        error' or 'Completed: read failure'.
54        """
55        self.hours = hours
56        """
57        **(str):** The device's power-on hours at the time the self-test
58        was initiated.
59        """
60        self.LBA = lba
61        """
62        **(str):** Indicates the first LBA at which an error was encountered
63        during this self-test. Presented as a decimal value for ATA/SATA
64        devices and in hexadecimal notation for SAS/SCSI devices.
65        """
66        self.remain = remain
67        """
68        **(str):** Percentage value indicating how much of the self-test is
69        left to perform. '00%' indicates a complete test, while any other
70        value could indicate a test in progress or one that failed prior to
71        completion. Only reported by ATA devices.
72        """
73        self.segment = segment
74        """
75        **(str):** A manufacturer-specific self-test segment number reported
76        by SCSI devices on self-test failure. Set to '-' otherwise.
77        """
78        self.sense = sense
79        """
80        **(str):** SCSI sense key reported on self-test failure. Set to '-'
81        otherwise.
82        """
83        self.ASC = asc
84        """
85        **(str):** SCSI 'Additonal Sense Code' reported on self-test failure.
86        Set to '-' otherwise.
87        """
88        self.ASCQ = ascq
89        """
90        **(str):** SCSI 'Additonal Sense Code Quaifier' reported on self-test
91        failure. Set to '-' otherwise.
92        """
num: int

(int): Entry's position in the log from 1 (most recent) to 21 (least recent). ATA logs save the last 21 entries while SCSI logs only save the last 20.

type

(str): Type of test run. Generally short, long (extended), or conveyance, plus offline (background) or captive (foreground).

status

(str): Self-test's status message, for example 'Completed without error' or 'Completed: read failure'.

hours

(str): The device's power-on hours at the time the self-test was initiated.

LBA

(str): Indicates the first LBA at which an error was encountered during this self-test. Presented as a decimal value for ATA/SATA devices and in hexadecimal notation for SAS/SCSI devices.

remain

(str): Percentage value indicating how much of the self-test is left to perform. '00%' indicates a complete test, while any other value could indicate a test in progress or one that failed prior to completion. Only reported by ATA devices.

segment

(str): A manufacturer-specific self-test segment number reported by SCSI devices on self-test failure. Set to '-' otherwise.

sense

(str): SCSI sense key reported on self-test failure. Set to '-' otherwise.

ASC

(str): SCSI 'Additonal Sense Code' reported on self-test failure. Set to '-' otherwise.

ASCQ

(str): SCSI 'Additonal Sense Code Quaifier' reported on self-test failure. Set to '-' otherwise.

class Attribute:
 28class Attribute(object):
 29    """
 30    Contains all of the information associated with a single SMART attribute
 31    in a `Device`'s SMART table. This data is intended to exactly mirror that
 32    obtained through smartctl.
 33    """
 34
 35    def __init__(self, num: int, name, flags: int, value, worst, thresh, attr_type, updated, when_failed, raw):
 36        self.num: int = num
 37        """**(int):** Attribute's ID as a decimal value (1-255)."""
 38        self.name: str = name
 39        """
 40        **(str):** Attribute's name, as reported by smartmontools' drive.db.
 41        """
 42        self.flags: int = flags
 43        """**(int):** Attribute flags as a bit value (ie: 0x0032)."""
 44        self._value: str = value
 45        """**(str):** Attribute's current normalized value."""
 46        self._worst: str = worst
 47        """**(str):** Worst recorded normalized value for this attribute."""
 48        self._thresh: str = thresh
 49        """**(str):** Attribute's failure threshold."""
 50        self.type: str = attr_type
 51        """**(str):** Attribute's type, generally 'pre-fail' or 'old-age'."""
 52        self.updated: str = updated
 53        """
 54        **(str):** When is this attribute updated? Generally 'Always' or
 55        'Offline'
 56        """
 57        self.when_failed: str = when_failed
 58        """
 59        **(str):** When did this attribute cross below
 60        `pySMART.attribute.Attribute.thresh`? Reads '-' when not failed.
 61        Generally either 'FAILING_NOW' or 'In_the_Past' otherwise.
 62        """
 63        self.raw = raw
 64        """**(str):** Attribute's current raw (non-normalized) value."""
 65
 66    @property
 67    def value_str(self) -> str:
 68        """Gets the attribute value
 69
 70        Returns:
 71            str: The attribute value in string format
 72        """
 73        return self._value
 74
 75    @property
 76    def value_int(self) -> int:
 77        """Gets the attribute value
 78
 79        Returns:
 80            int: The attribute value in integer format.
 81        """
 82        return int(self._value)
 83
 84    @property
 85    def value(self) -> str:
 86        """Gets the attribue value
 87
 88        Returns:
 89            str: The attribute value in string format
 90        """
 91        return self.value_str
 92
 93    @property
 94    def worst(self) -> int:
 95        """Gets the worst value
 96
 97        Returns:
 98            int: The attribute worst field in integer format
 99        """
100        return int(self._worst)
101
102    @property
103    def thresh(self) -> Optional[int]:
104        """Gets the threshold value
105
106        Returns:
107            int: The attribute threshold field in integer format
108        """
109        return None if self._thresh == '---' else int(self._thresh)
110
111    @property
112    def raw_int(self) -> int:
113        """Gets the raw value converted to int
114        NOTE: Some values may not be correctly converted!
115
116        Returns:
117            int: The attribute raw-value field in integer format.
118            None: In case the raw string failed to be parsed
119        """
120        try:
121            return int(re.search(r'\d+', self.raw).group())
122        except:
123            return None
124
125    def __repr__(self):
126        """Define a basic representation of the class object."""
127        return "<SMART Attribute %r %s/%s raw:%s>" % (
128            self.name, self.value, self.thresh, self.raw)
129
130    def __str__(self):
131        """
132        Define a formatted string representation of the object's content.
133        In the interest of not overflowing 80-character lines this does not
134        print the value of `pySMART.attribute.Attribute.flags_hex`.
135        """
136        return "{0:>3} {1:23}{2:>4}{3:>4}{4:>4} {5:9}{6:8}{7:12}{8}".format(
137            self.num,
138            self.name,
139            self.value,
140            self.worst,
141            self.thresh,
142            self.type,
143            self.updated,
144            self.when_failed,
145            self.raw
146        )
147
148    def __getstate__(self):
149        return {
150            'num': self.num,
151            'flags': self.flags,
152            'raw': self.raw,
153            'value': self.value,
154            'worst': self.worst,
155            'threshold': self.thresh,
156            'type': self.type,
157            'updated': self.updated,
158            'when_failed': self.when_failed,
159        }

Contains all of the information associated with a single SMART attribute in a Device's SMART table. This data is intended to exactly mirror that obtained through smartctl.

Attribute( num: int, name, flags: int, value, worst, thresh, attr_type, updated, when_failed, raw)
35    def __init__(self, num: int, name, flags: int, value, worst, thresh, attr_type, updated, when_failed, raw):
36        self.num: int = num
37        """**(int):** Attribute's ID as a decimal value (1-255)."""
38        self.name: str = name
39        """
40        **(str):** Attribute's name, as reported by smartmontools' drive.db.
41        """
42        self.flags: int = flags
43        """**(int):** Attribute flags as a bit value (ie: 0x0032)."""
44        self._value: str = value
45        """**(str):** Attribute's current normalized value."""
46        self._worst: str = worst
47        """**(str):** Worst recorded normalized value for this attribute."""
48        self._thresh: str = thresh
49        """**(str):** Attribute's failure threshold."""
50        self.type: str = attr_type
51        """**(str):** Attribute's type, generally 'pre-fail' or 'old-age'."""
52        self.updated: str = updated
53        """
54        **(str):** When is this attribute updated? Generally 'Always' or
55        'Offline'
56        """
57        self.when_failed: str = when_failed
58        """
59        **(str):** When did this attribute cross below
60        `pySMART.attribute.Attribute.thresh`? Reads '-' when not failed.
61        Generally either 'FAILING_NOW' or 'In_the_Past' otherwise.
62        """
63        self.raw = raw
64        """**(str):** Attribute's current raw (non-normalized) value."""
num: int

(int): Attribute's ID as a decimal value (1-255).

name: str

(str): Attribute's name, as reported by smartmontools' drive.db.

flags: int

(int): Attribute flags as a bit value (ie: 0x0032).

type: str

(str): Attribute's type, generally 'pre-fail' or 'old-age'.

updated: str

(str): When is this attribute updated? Generally 'Always' or 'Offline'

when_failed: str

(str): When did this attribute cross below pySMART.Attribute.thresh? Reads '-' when not failed. Generally either 'FAILING_NOW' or 'In_the_Past' otherwise.

raw

(str): Attribute's current raw (non-normalized) value.

value_str: str

Gets the attribute value

Returns: str: The attribute value in string format

value_int: int

Gets the attribute value

Returns: int: The attribute value in integer format.

value: str

Gets the attribue value

Returns: str: The attribute value in string format

worst: int

Gets the worst value

Returns: int: The attribute worst field in integer format

thresh: Union[int, NoneType]

Gets the threshold value

Returns: int: The attribute threshold field in integer format

raw_int: int

Gets the raw value converted to int NOTE: Some values may not be correctly converted!

Returns: int: The attribute raw-value field in integer format. None: In case the raw string failed to be parsed

SMARTCTL = <pySMART.smartctl.Smartctl object>
class DeviceList:
 37class DeviceList(object):
 38    """
 39    Represents a list of all the storage devices connected to this computer.
 40    """
 41
 42    def __init__(self, init: bool = True, smartctl=SMARTCTL):
 43        """Instantiates and optionally initializes the `DeviceList`.
 44
 45        Args:
 46            init (bool, optional): By default, `pySMART.device_list.DeviceList.devices`
 47                is populated with `Device` objects during instantiation. Setting init
 48                to False will skip initialization and create an empty
 49                `pySMART.device_list.DeviceList` object instead. Defaults to True.
 50            smartctl ([type], optional): This stablish the smartctl wrapper.
 51                Defaults the global `SMARTCTL` object and should be only
 52                overwritten on tests.
 53        """
 54
 55        self.devices: List[Device] = []
 56        """
 57        **(list of `Device`):** Contains all storage devices detected during
 58        instantiation, as `Device` objects.
 59        """
 60        self.smartctl: Smartctl = smartctl
 61        """The smartctl wrapper
 62        """
 63        if init:
 64            self._initialize()
 65
 66    def __repr__(self):
 67        """Define a basic representation of the class object."""
 68        rep = "<DeviceList contents:\n"
 69        for device in self.devices:
 70            rep += str(device) + '\n'
 71        return rep + '>'
 72        # return "<DeviceList contents:%r>" % (self.devices)
 73
 74    def _cleanup(self):
 75        """
 76        Removes duplicate ATA devices that correspond to an existing CSMI
 77        device. Also removes any device with no capacity value, as this
 78        indicates removable storage, ie: CD/DVD-ROM, ZIP, etc.
 79        """
 80        # We can't operate directly on the list while we're iterating
 81        # over it, so we collect indeces to delete and remove them later
 82        to_delete = []
 83        # Enumerate the list to get tuples containing indeces and values
 84        for index, device in enumerate(self.devices):
 85            if device.interface == 'csmi':
 86                for otherindex, otherdevice in enumerate(self.devices):
 87                    if (otherdevice.interface == 'ata' or
 88                            otherdevice.interface == 'sata'):
 89                        if device.serial == otherdevice.serial:
 90                            to_delete.append(otherindex)
 91                            device._sd_name = otherdevice.name
 92            if device.capacity is None and index not in to_delete:
 93                to_delete.append(index)
 94        # Recreate the self.devices list without the marked indeces
 95        self.devices[:] = [v for i, v in enumerate(self.devices)
 96                           if i not in to_delete]
 97
 98    def _initialize(self):
 99        """
100        Scans system busses for attached devices and add them to the
101        `DeviceList` as `Device` objects.
102        """
103
104        for line in self.smartctl.scan():
105            if not ('failed:' in line or line == ''):
106                groups = re.compile(
107                    '^(\S+)\s+-d\s+(\S+)').match(line).groups()
108                name = groups[0]
109                interface = groups[1]
110                self.devices.append(
111                    Device(name, interface=interface, smartctl=self.smartctl))
112
113        # Remove duplicates and unwanted devices (optical, etc.) from the list
114        self._cleanup()
115        # Sort the list alphabetically by device name
116        self.devices.sort(key=lambda device: device.name)
117
118    def __getitem__(self, index: int) -> Device:
119        """Returns an element from self.devices
120
121        Args:
122            index (int): An index of self.devices
123
124        Returns:
125            Device: Returns a Device that is located on the asked index
126        """
127        return self.devices[index]

Represents a list of all the storage devices connected to this computer.

DeviceList(init: bool = True, smartctl=<pySMART.smartctl.Smartctl object>)
42    def __init__(self, init: bool = True, smartctl=SMARTCTL):
43        """Instantiates and optionally initializes the `DeviceList`.
44
45        Args:
46            init (bool, optional): By default, `pySMART.device_list.DeviceList.devices`
47                is populated with `Device` objects during instantiation. Setting init
48                to False will skip initialization and create an empty
49                `pySMART.device_list.DeviceList` object instead. Defaults to True.
50            smartctl ([type], optional): This stablish the smartctl wrapper.
51                Defaults the global `SMARTCTL` object and should be only
52                overwritten on tests.
53        """
54
55        self.devices: List[Device] = []
56        """
57        **(list of `Device`):** Contains all storage devices detected during
58        instantiation, as `Device` objects.
59        """
60        self.smartctl: Smartctl = smartctl
61        """The smartctl wrapper
62        """
63        if init:
64            self._initialize()

Instantiates and optionally initializes the DeviceList.

Args: init (bool, optional): By default, pySMART.DeviceList.devices is populated with Device objects during instantiation. Setting init to False will skip initialization and create an empty pySMART.DeviceList object instead. Defaults to True. smartctl ([type], optional): This stablish the smartctl wrapper. Defaults the global SMARTCTL object and should be only overwritten on tests.

devices: List[pySMART.Device]

(list of Device): Contains all storage devices detected during instantiation, as Device objects.

smartctl: pySMART.smartctl.Smartctl

The smartctl wrapper

class Device:
  73class Device(object):
  74    """
  75    Represents any device attached to an internal storage interface, such as a
  76    hard drive or DVD-ROM, and detected by smartmontools. Includes eSATA
  77    (considered SATA) but excludes other external devices (USB, Firewire).
  78    """
  79
  80    def __init__(self, name: str, interface: str = None, abridged: bool = False, smart_options: Union[str, List[str]] = None, smartctl: Smartctl = SMARTCTL):
  81        """Instantiates and initializes the `pySMART.device.Device`."""
  82        if not (
  83                interface is None or
  84                smartctl_isvalid_type(interface.lower())
  85        ):
  86            raise ValueError(
  87                'Unknown interface: {0} specified for {1}'.format(interface, name))
  88        self.abridged = abridged or interface == 'UNKNOWN INTERFACE'
  89        if smart_options is not None:
  90            if isinstance(smart_options,  str):
  91                smart_options = smart_options.split(' ')
  92            smartctl.add_options(smart_options)
  93        self.smartctl = smartctl
  94        """
  95        """
  96        self.name: str = name.replace('/dev/', '').replace('nvd', 'nvme')
  97        """
  98        **(str):** Device's hardware ID, without the '/dev/' prefix.
  99        (ie: sda (Linux), pd0 (Windows))
 100        """
 101        self.model: str = None
 102        """**(str):** Device's model number."""
 103        self.serial: str = None
 104        """**(str):** Device's serial number."""
 105        self.vendor: str = None
 106        """**(str):** Device's vendor (if any)."""
 107        self.interface: str = None if interface == 'UNKNOWN INTERFACE' else interface
 108        """
 109        **(str):** Device's interface type. Must be one of:
 110            * **ATA** - Advanced Technology Attachment
 111            * **SATA** - Serial ATA
 112            * **SCSI** - Small Computer Systems Interface
 113            * **SAS** - Serial Attached SCSI
 114            * **SAT** - SCSI-to-ATA Translation (SATA device plugged into a
 115            SAS port)
 116            * **CSMI** - Common Storage Management Interface (Intel ICH /
 117            Matrix RAID)
 118        Generally this should not be specified to allow auto-detection to
 119        occur. Otherwise, this value overrides the auto-detected type and could
 120        produce unexpected or no data.
 121        """
 122        self._capacity: str = None
 123        """**(str):** Device's user capacity as reported directly by smartctl (RAW)."""
 124        self.firmware: str = None
 125        """**(str):** Device's firmware version."""
 126        self.smart_capable: bool = 'nvme' in self.name
 127        """
 128        **(bool):** True if the device has SMART Support Available.
 129        False otherwise. This is useful for VMs amongst other things.
 130        """
 131        self.smart_enabled: bool = 'nvme' in self.name
 132        """
 133        **(bool):** True if the device supports SMART (or SCSI equivalent) and
 134        has the feature set enabled. False otherwise.
 135        """
 136        self.assessment: str = None
 137        """
 138        **(str):** SMART health self-assessment as reported by the device.
 139        """
 140        self.messages: List[str] = []
 141        """
 142        **(list of str):** Contains any SMART warnings or other error messages
 143        reported by the device (ie: ascq codes).
 144        """
 145        self.is_ssd: bool = True if 'nvme' in self.name else False
 146        """
 147        **(bool):** True if this device is a Solid State Drive.
 148        False otherwise.
 149        """
 150        self.rotation_rate: int = None
 151        """
 152        **(int):** The Roatation Rate of the Drive if it is not a SSD.
 153        The Metric is RPM.
 154        """
 155        self.attributes: List[Attribute] = [None] * 256
 156        """
 157        **(list of `Attribute`):** Contains the complete SMART table
 158        information for this device, as provided by smartctl. Indexed by
 159        attribute #, values are set to 'None' for attributes not suported by
 160        this device.
 161        """
 162        self.test_capabilities = {
 163            'offline': False,  # SMART execute Offline immediate (ATA only)
 164            'short': 'nvme' not in self.name,  # SMART short Self-test
 165            'long': 'nvme' not in self.name,  # SMART long Self-test
 166            'conveyance': False,  # SMART Conveyance Self-Test (ATA only)
 167            'selective': False,  # SMART Selective Self-Test (ATA only)
 168        }
 169        # Note have not included 'offline' test for scsi as it runs in the foregorund
 170        # mode. While this may be beneficial to us in someways it is against the
 171        # general layout and pattern that the other tests issued using pySMART are
 172        # followed hence not doing it currently
 173        """
 174        **(dict): ** This dictionary contains key == 'Test Name' and
 175        value == 'True/False' of self-tests that this device is capable of.
 176        """
 177        # Note: The above are just default values and can/will be changed
 178        # upon update() when the attributes and type of the disk is actually
 179        # determined.
 180        self.tests: List[TestEntry] = []
 181        """
 182        **(list of `TestEntry`):** Contains the complete SMART self-test log
 183        for this device, as provided by smartctl.
 184        """
 185        self._test_running = False
 186        """
 187        **(bool):** True if a self-test is currently being run.
 188        False otherwise.
 189        """
 190        self._test_ECD = None
 191        """
 192        **(str):** Estimated completion time of the running SMART selftest.
 193        Not provided by SAS/SCSI devices.
 194        """
 195        self._test_progress = None
 196        """
 197        **(int):** Estimate progress percantage of the running SMART selftest.
 198        """
 199        self.diagnostics: Diagnostics = Diagnostics()
 200        """
 201        **Diagnostics** Contains parsed and processed diagnostic information
 202        extracted from the SMART information. Currently only populated for
 203        SAS and SCSI devices, since ATA/SATA SMART attributes are manufacturer
 204        proprietary.
 205        """
 206        self.temperature: int = None
 207        """
 208        **(int or None): Since SCSI disks do not report attributes like ATA ones
 209        we need to grep/regex the shit outta the normal "smartctl -a" output.
 210        In case the device have more than one temperature sensor the first value
 211        will be stored here too.
 212        Note: Temperatures are always in Celsius (if possible).
 213        """
 214        self.temperatures: Dict[int, int] = {}
 215        """
 216        **(dict of int): NVMe disks usually report multiple temperatures, which
 217        will be stored here if available. Keys are sensor numbers as reported in
 218        output data.
 219        Note: Temperatures are always in Celsius (if possible).
 220        """
 221        self.logical_sector_size: int = None
 222        """
 223        **(int):** The logical sector size of the device (or LBA).
 224        """
 225        self.physical_sector_size: int = None
 226        """
 227        **(int):** The physical sector size of the device.
 228        """
 229        self.if_attributes: Union[None, NvmeAttributes] = None
 230        """
 231        **(NvmeAttributes):** This object may vary for each device interface attributes.
 232        It will store all data obtained from smartctl
 233        """
 234
 235        if self.name is None:
 236            warnings.warn(
 237                "\nDevice '{0}' does not exist! This object should be destroyed.".format(
 238                    name)
 239            )
 240            return
 241        # If no interface type was provided, scan for the device
 242        # Lets do this only for the non-abridged case
 243        # (we can work with no interface for abridged case)
 244        elif self.interface is None and not self.abridged:
 245            logger.trace(
 246                "Determining interface of disk: {0}".format(self.name))
 247            raw, returncode = self.smartctl.generic_call(
 248                ['-d', 'test', self.dev_reference])
 249
 250            if len(raw) > 0:
 251                # I do not like this parsing logic but it works for now!
 252                # just for reference _stdout.split('\n') gets us
 253                # something like
 254                # [
 255                #     ...copyright string...,
 256                #     '',
 257                #     "/dev/ada2: Device of type 'atacam' [ATA] detected",
 258                #     "/dev/ada2: Device of type 'atacam' [ATA] opened",
 259                #     ''
 260                # ]
 261                # The above example should be enough for anyone to understand the line below
 262                try:
 263                    self.interface = raw[-2].split("'")[1]
 264                    if self.interface == "nvme":  # if nvme set SMART to true
 265                        self.smart_capable = True
 266                        self.smart_enabled = True
 267                except:
 268                    # for whatever reason we could not get the interface type
 269                    # we should mark this as an `abbridged` case and move on
 270                    self.interface = None
 271                    self.abbridged = True
 272                # TODO: Uncomment the classify call if we ever find out that we need it
 273                # Disambiguate the generic interface to a specific type
 274                # self._classify()
 275            else:
 276                warnings.warn(
 277                    "\nDevice '{0}' does not exist! This object should be destroyed.".format(
 278                        name)
 279                )
 280                return
 281        # If a valid device was detected, populate its information
 282        # OR if in unabridged mode, then do it even without interface info
 283        if self.interface is not None or self.abridged:
 284            self.update()
 285
 286    @property
 287    def dev_reference(self) -> str:
 288        """The reference to the device as provided by smartctl.
 289           - On unix-like systems, this is the path to the device. (example /dev/<name>)
 290           - On MacOS, this is the name of the device. (example <name>)
 291           - On Windows, this is the drive letter of the device. (example <drive letter>)
 292
 293        Returns:
 294            str: The reference to the device as provided by smartctl.
 295        """
 296
 297        # detect if we are on MacOS
 298        if 'IOService' in self.name:
 299            return self.name
 300
 301        # otherwise asume we are on unix-like systems
 302        return os.path.join('/dev/', self.name)
 303
 304    @property
 305    def capacity(self) -> str:
 306        """Returns the capacity in the raw smartctl format.
 307        This may be deprecated in the future and its only retained for compatibility.
 308
 309        Returns:
 310            str: The capacity in the raw smartctl format
 311        """
 312        return self._capacity
 313
 314    @property
 315    def diags(self) -> Dict[str, str]:
 316        """Gets the old/deprecated version of SCSI/SAS diags atribute.
 317        """
 318        return self.diagnostics.get_classic_format()
 319
 320    @property
 321    def size_raw(self) -> str:
 322        """Returns the capacity in the raw smartctl format.
 323
 324        Returns:
 325            str: The capacity in the raw smartctl format
 326        """
 327        return self._capacity
 328
 329    @property
 330    def size(self) -> int:
 331        """Returns the capacity in bytes
 332
 333        Returns:
 334            int: The capacity in bytes
 335        """
 336        import humanfriendly
 337
 338        return humanfriendly.parse_size(self._capacity)
 339
 340    @property
 341    def sector_size(self) -> int:
 342        """Returns the sector size of the device.
 343
 344        Returns:
 345            int: The sector size of the device in Bytes. If undefined, we'll assume 512B
 346        """
 347        if self.logical_sector_size is not None:
 348            return self.logical_sector_size
 349        elif self.physical_sector_size is not None:
 350            return self.physical_sector_size
 351        else:
 352            return 512
 353
 354    def __repr__(self):
 355        """Define a basic representation of the class object."""
 356        return "<{0} device on /dev/{1} mod:{2} sn:{3}>".format(
 357            self.interface.upper() if self.interface else 'UNKNOWN INTERFACE',
 358            self.name,
 359            self.model,
 360            self.serial
 361        )
 362
 363    def __getstate__(self, all_info=True):
 364        """
 365        Allows us to send a pySMART Device object over a serializable
 366        medium which uses json (or the likes of json) payloads
 367        """
 368        state_dict = {
 369            'interface': self.interface if self.interface else 'UNKNOWN INTERFACE',
 370            'model': self.model,
 371            'firmware': self.firmware,
 372            'smart_capable': self.smart_capable,
 373            'smart_enabled': self.smart_enabled,
 374            'smart_status': self.assessment,
 375            'messages': self.messages,
 376            'test_capabilities': self.test_capabilities.copy(),
 377            'tests': [t.__getstate__() for t in self.tests] if self.tests else [],
 378            'diagnostics': self.diagnostics.__getstate__(),
 379            'temperature': self.temperature,
 380            'attributes': [attr.__getstate__() if attr else None for attr in self.attributes]
 381        }
 382        if all_info:
 383            state_dict.update({
 384                'name': self.name,
 385                'path': self.dev_reference,
 386                'serial': self.serial,
 387                'is_ssd': self.is_ssd,
 388                'rotation_rate': self.rotation_rate,
 389                'capacity': self._capacity
 390            })
 391        return state_dict
 392
 393    def __setstate__(self, state):
 394        state['assessment'] = state['smart_status']
 395        del state['smart_status']
 396        self.__dict__.update(state)
 397
 398    def smart_toggle(self, action: str) -> Tuple[bool, List[str]]:
 399        """
 400        A basic function to enable/disable SMART on device.
 401
 402        # Args:
 403        * **action (str):** Can be either 'on'(for enabling) or 'off'(for disabling).
 404
 405        # Returns"
 406        * **(bool):** Return True (if action succeded) else False
 407        * **(List[str]):** None if option succeded else contains the error message.
 408        """
 409        # Lets make the action verb all lower case
 410        if self.interface == 'nvme':
 411            return False, 'NVME devices do not currently support toggling SMART enabled'
 412        action_lower = action.lower()
 413        if action_lower not in ['on', 'off']:
 414            return False, 'Unsupported action {0}'.format(action)
 415        # Now lets check if the device's smart enabled status is already that of what
 416        # the supplied action is intending it to be. If so then just return successfully
 417        if self.smart_enabled:
 418            if action_lower == 'on':
 419                return True, None
 420        else:
 421            if action_lower == 'off':
 422                return True, None
 423        raw, returncode = self.smartctl.generic_call(
 424            ['-s', action_lower, self.dev_reference])
 425
 426        if returncode != 0:
 427            return False, raw
 428        # if everything worked out so far lets perform an update() and check the result
 429        self.update()
 430        if action_lower == 'off' and self.smart_enabled:
 431            return False, 'Failed to turn SMART off.'
 432        if action_lower == 'on' and not self.smart_enabled:
 433            return False, 'Failed to turn SMART on.'
 434        return True, None
 435
 436    def all_attributes(self, print_fn=print):
 437        """
 438        Prints the entire SMART attribute table, in a format similar to
 439        the output of smartctl.
 440        allows usage of custom print function via parameter print_fn by default uses print
 441        """
 442        header_printed = False
 443        for attr in self.attributes:
 444            if attr is not None:
 445                if not header_printed:
 446                    print_fn("{0:>3} {1:24}{2:4}{3:4}{4:4}{5:9}{6:8}{7:12}{8}"
 447                             .format('ID#', 'ATTRIBUTE_NAME', 'CUR', 'WST', 'THR', 'TYPE', 'UPDATED', 'WHEN_FAIL',
 448                                     'RAW'))
 449                    header_printed = True
 450                print_fn(attr)
 451        if not header_printed:
 452            print_fn('This device does not support SMART attributes.')
 453
 454    def all_selftests(self):
 455        """
 456        Prints the entire SMART self-test log, in a format similar to
 457        the output of smartctl.
 458        """
 459        if self.tests:
 460            all_tests = []
 461            if smartctl_type(self.interface) == 'scsi':
 462                header = "{0:3}{1:17}{2:23}{3:7}{4:14}{5:15}".format(
 463                    'ID',
 464                    'Test Description',
 465                    'Status',
 466                    'Hours',
 467                    '1st_Error@LBA',
 468                    '[SK  ASC  ASCQ]'
 469                )
 470            else:
 471                header = ("{0:3}{1:17}{2:30}{3:5}{4:7}{5:17}".format(
 472                    'ID',
 473                    'Test_Description',
 474                    'Status',
 475                    'Left',
 476                    'Hours',
 477                    '1st_Error@LBA'))
 478            all_tests.append(header)
 479            for test in self.tests:
 480                all_tests.append(str(test))
 481
 482            return all_tests
 483        else:
 484            no_tests = 'No self-tests have been logged for this device.'
 485            return no_tests
 486
 487    def _classify(self):
 488        """
 489        Disambiguates generic device types ATA and SCSI into more specific
 490        ATA, SATA, SAS, SAT and SCSI.
 491        """
 492        # SCSI devices might be SCSI, SAS or SAT
 493        # ATA device might be ATA or SATA
 494        if self.interface in ['scsi', 'ata']:
 495            test = 'sat' if self.interface == 'scsi' else 'sata'
 496            # Look for a SATA PHY to detect SAT and SATA
 497            raw, returncode = self.smartctl.generic_call([
 498                '-d',
 499                smartctl_type(test),
 500                '-l',
 501                'sataphy',
 502                self.dev_reference])
 503
 504            if 'GP Log 0x11' in raw[3]:
 505                self.interface = test
 506        # If device type is still SCSI (not changed to SAT above), then
 507        # check for a SAS PHY
 508        if self.interface == 'scsi':
 509            raw, returncode = self.smartctl.generic_call([
 510                '-d',
 511                'scsi',
 512                '-l',
 513                'sasphy',
 514                self.dev_reference])
 515            if 'SAS SSP' in raw[4]:
 516                self.interface = 'sas'
 517            # Some older SAS devices do not support the SAS PHY log command.
 518            # For these, see if smartmontools reports a transport protocol.
 519            else:
 520                raw = self.smartctl.all(
 521                    'scsi', self.dev_reference)
 522
 523                for line in raw:
 524                    if 'Transport protocol' in line and 'SAS' in line:
 525                        self.interface = 'sas'
 526
 527    def _guess_smart_type(self, line):
 528        """
 529        This function is not used in the generic wrapper, however the header
 530        is defined so that it can be monkey-patched by another application.
 531        """
 532        pass
 533
 534    def _make_smart_warnings(self):
 535        """
 536        Parses an ATA/SATA SMART table for attributes with the 'when_failed'
 537        value set. Generates an warning message for any such attributes and
 538        updates the self-assessment value if necessary.
 539        """
 540        if smartctl_type(self.interface) == 'scsi':
 541            return
 542        for attr in self.attributes:
 543            if attr is not None:
 544                if attr.when_failed == 'In_the_past':
 545                    warn_str = "{0} failed in the past with value {1}. [Threshold: {2}]".format(
 546                        attr.name, attr.worst, attr.thresh)
 547                    self.messages.append(warn_str)
 548                    if not self.assessment == 'FAIL':
 549                        self.assessment = 'WARN'
 550                elif attr.when_failed == 'FAILING_NOW':
 551                    warn_str = "{0} is failing now with value {1}. [Threshold: {2}]".format(
 552                        attr.name, attr.value, attr.thresh)
 553                    self.assessment = 'FAIL'
 554                    self.messages.append(warn_str)
 555                elif not attr.when_failed == '-':
 556                    warn_str = "{0} says it failed '{1}'. [V={2},W={3},T={4}]".format(
 557                        attr.name, attr.when_failed, attr.value, attr.worst, attr.thresh)
 558                    self.messages.append(warn_str)
 559                    if not self.assessment == 'FAIL':
 560                        self.assessment = 'WARN'
 561
 562    def get_selftest_result(self, output=None):
 563        """
 564        Refreshes a device's `pySMART.device.Device.tests` attribute to obtain
 565        the latest test results. If a new test result is obtained, its content
 566        is returned.
 567
 568        # Args:
 569        * **output (str, optional):** If set to 'str', the string
 570        representation of the most recent test result will be returned, instead
 571        of a `Test_Entry` object.
 572
 573        # Returns:
 574        * **(int):** Return status code. One of the following:
 575            * 0 - Success. Object (or optionally, string rep) is attached.
 576            * 1 - Self-test in progress. Must wait for it to finish.
 577            * 2 - No new test results.
 578            * 3 - The Self-test was Aborted by host
 579        * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or
 580        optionally it's string representation) if new data exists.  Status
 581        message string on failure.
 582        * **(int):** Estimate progress percantage of the running SMART selftest, if known.
 583        Otherwise 'None'.
 584        """
 585        # SCSI self-test logs hold 20 entries while ATA logs hold 21
 586        if smartctl_type(self.interface) == 'scsi':
 587            maxlog = 20
 588        else:
 589            maxlog = 21
 590        # If we looked only at the most recent test result we could be fooled
 591        # by two short tests run close together (within the same hour)
 592        # appearing identical. Comparing the length of the log adds some
 593        # confidence until it maxes, as above. Comparing the least-recent test
 594        # result greatly diminishes the chances that two sets of two tests each
 595        # were run within an hour of themselves, but with 16-17 other tests run
 596        # in between them.
 597        if self.tests:
 598            _first_entry = self.tests[0]
 599            _len = len(self.tests)
 600            _last_entry = self.tests[_len - 1]
 601        else:
 602            _len = 0
 603        self.update()
 604        # Since I have changed the update() parsing to DTRT to pickup currently
 605        # running selftests we can now purely rely on that for self._test_running
 606        # Thus check for that variable first and return if it is True with appropos message.
 607        if self._test_running is True:
 608            return 1, 'Self-test in progress. Please wait.', self._test_progress
 609        # Check whether the list got longer (ie: new entry)
 610        # If so return the newest test result
 611        # If not, because it's max size already, check for new entries
 612        if (
 613                (len(self.tests) != _len) or
 614                (
 615                    len == maxlog and
 616                    (
 617                        _first_entry.type != self.tests[0].type or
 618                        _first_entry.hours != self.tests[0].hours or
 619                        _last_entry.type != self.tests[len(self.tests) - 1].type or
 620                        _last_entry.hours != self.tests[len(
 621                            self.tests) - 1].hours
 622                    )
 623                )
 624        ):
 625            return (
 626                0 if 'Aborted' not in self.tests[0].status else 3,
 627                str(self.tests[0]) if output == 'str' else self.tests[0],
 628                None
 629            )
 630        else:
 631            return 2, 'No new self-test results found.', None
 632
 633    def abort_selftest(self):
 634        """
 635        Aborts non-captive SMART Self Tests.   Note that this command
 636        will  abort the Offline Immediate Test routine only if your disk
 637        has the "Abort Offline collection upon new command"  capability.
 638
 639        # Args: Nothing (just aborts directly)
 640
 641        # Returns:
 642        * **(int):** The returncode of calling `smartctl -X device_path`
 643        """
 644        return self.smartctl.test_stop(smartctl_type(self.interface), self.dev_reference)
 645
 646    def run_selftest(self, test_type, ETA_type='date'):
 647        """
 648        Instructs a device to begin a SMART self-test. All tests are run in
 649        'offline' / 'background' mode, allowing normal use of the device while
 650        it is being tested.
 651
 652        # Args:
 653        * **test_type (str):** The type of test to run. Accepts the following
 654        (not case sensitive):
 655            * **short** - Brief electo-mechanical functionality check.
 656            Generally takes 2 minutes or less.
 657            * **long** - Thorough electro-mechanical functionality check,
 658            including complete recording media scan. Generally takes several
 659            hours.
 660            * **conveyance** - Brief test used to identify damage incurred in
 661            shipping. Generally takes 5 minutes or less. **This test is not
 662            supported by SAS or SCSI devices.**
 663            * **offline** - Runs SMART Immediate Offline Test. The effects of
 664            this test are visible only in that it updates the SMART Attribute
 665            values, and if errors are found they will appear in the SMART error
 666            log, visible with the '-l error' option to smartctl. **This test is
 667            not supported by SAS or SCSI devices in pySMART use cli smartctl for
 668            running 'offline' selftest (runs in foreground) on scsi devices.**
 669            * **ETA_type** - Format to return the estimated completion time/date
 670            in. Default is 'date'. One could otherwise specidy 'seconds'.
 671            Again only for ATA devices.
 672
 673        # Returns:
 674        * **(int):** Return status code.  One of the following:
 675            * 0 - Self-test initiated successfully
 676            * 1 - Previous self-test running. Must wait for it to finish.
 677            * 2 - Unknown or unsupported (by the device) test type requested.
 678            * 3 - Unspecified smartctl error. Self-test not initiated.
 679        * **(str):** Return status message.
 680        * **(str)/(float):** Estimated self-test completion time if a test is started.
 681        The optional argument of 'ETA_type' (see above) controls the return type.
 682        if 'ETA_type' == 'date' then a date string is returned else seconds(float)
 683        is returned.
 684        Note: The self-test completion time can only be obtained for ata devices.
 685        Otherwise 'None'.
 686        """
 687        # Lets call get_selftest_result() here since it does an update() and
 688        # checks for an existing selftest is running or not, this way the user
 689        # can issue a test from the cli and this can still pick that up
 690        # Also note that we do not need to obtain the results from this as the
 691        # data is already stored in the Device class object's variables
 692        self.get_selftest_result()
 693        if self._test_running:
 694            return 1, 'Self-test in progress. Please wait.', self._test_ECD
 695        test_type = test_type.lower()
 696        interface = smartctl_type(self.interface)
 697        try:
 698            if not self.test_capabilities[test_type]:
 699                return (
 700                    2,
 701                    "Device {0} does not support the '{1}' test ".format(
 702                        self.name, test_type),
 703                    None
 704                )
 705        except KeyError:
 706            return 2, "Unknown test type '{0}' requested.".format(test_type), None
 707
 708        raw = self.smartctl.test_start(
 709            interface, test_type, self.dev_reference)
 710        _success = False
 711        _running = False
 712        for line in raw:
 713            if 'has begun' in line:
 714                _success = True
 715                self._test_running = True
 716            if 'aborting current test' in line:
 717                _running = True
 718                try:
 719                    self._test_progress = 100 - \
 720                        int(line.split('(')[-1].split('%')[0])
 721                except ValueError:
 722                    pass
 723
 724            if _success and 'complete after' in line:
 725                self._test_ECD = line[25:].rstrip()
 726                if ETA_type == 'seconds':
 727                    self._test_ECD = mktime(
 728                        strptime(self._test_ECD, '%a %b %d %H:%M:%S %Y')) - time()
 729                self._test_progress = 0
 730        if _success:
 731            return 0, 'Self-test started successfully', self._test_ECD
 732        else:
 733            if _running:
 734                return 1, 'Self-test already in progress. Please wait.', self._test_ECD
 735            else:
 736                return 3, 'Unspecified Error. Self-test not started.', None
 737
 738    def run_selftest_and_wait(self, test_type, output=None, polling=5, progress_handler=None):
 739        """
 740        This is essentially a wrapper around run_selftest() such that we
 741        call self.run_selftest() and wait on the running selftest till
 742        it finished before returning.
 743        The above holds true for all pySMART supported tests with the
 744        exception of the 'offline' test (ATA only) as it immediately
 745        returns, since the entire test only affects the smart error log
 746        (if any errors found) and updates the SMART attributes. Other
 747        than that it is not visibile anywhere else, so we start it and
 748        simply return.
 749        # Args:
 750        * **test_type (str):** The type of test to run. Accepts the following
 751        (not case sensitive):
 752            * **short** - Brief electo-mechanical functionality check.
 753            Generally takes 2 minutes or less.
 754            * **long** - Thorough electro-mechanical functionality check,
 755            including complete recording media scan. Generally takes several
 756            hours.
 757            * **conveyance** - Brief test used to identify damage incurred in
 758            shipping. Generally takes 5 minutes or less. **This test is not
 759            supported by SAS or SCSI devices.**
 760            * **offline** - Runs SMART Immediate Offline Test. The effects of
 761            this test are visible only in that it updates the SMART Attribute
 762            values, and if errors are found they will appear in the SMART error
 763            log, visible with the '-l error' option to smartctl. **This test is
 764            not supported by SAS or SCSI devices in pySMART use cli smartctl for
 765            running 'offline' selftest (runs in foreground) on scsi devices.**
 766        * **output (str, optional):** If set to 'str', the string
 767            representation of the most recent test result will be returned,
 768            instead of a `Test_Entry` object.
 769        * **polling (int, default=5):** The time duration to sleep for between
 770            checking for test_results and progress.
 771        * **progress_handler (function, optional):** This if provided is called
 772            with self._test_progress as the supplied argument everytime a poll to
 773            check the status of the selftest is done.
 774        # Returns:
 775        * **(int):** Return status code.  One of the following:
 776            * 0 - Self-test executed and finished successfully
 777            * 1 - Previous self-test running. Must wait for it to finish.
 778            * 2 - Unknown or illegal test type requested.
 779            * 3 - The Self-test was Aborted by host
 780            * 4 - Unspecified smartctl error. Self-test not initiated.
 781        * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or
 782        optionally it's string representation) if new data exists.  Status
 783        message string on failure.
 784        """
 785        test_initiation_result = self.run_selftest(test_type)
 786        if test_initiation_result[0] != 0:
 787            return test_initiation_result[:2]
 788        if test_type == 'offline':
 789            self._test_running = False
 790        # if not then the test initiated correctly and we can start the polling.
 791        # For now default 'polling' value is 5 seconds if not specified by the user
 792
 793        # Do an initial check, for good measure.
 794        # In the probably impossible case that self._test_running is instantly False...
 795        selftest_results = self.get_selftest_result(output=output)
 796        while self._test_running:
 797            if selftest_results[0] != 1:
 798                # the selftest is run and finished lets return with the results
 799                break
 800            # Otherwise see if we are provided with the progress_handler to update progress
 801            if progress_handler is not None:
 802                progress_handler(
 803                    selftest_results[2] if selftest_results[2] is not None else 50)
 804            # Now sleep 'polling' seconds before checking the progress again
 805            sleep(polling)
 806
 807            # Check after the sleep to ensure we return the right result, and not an old one.
 808            selftest_results = self.get_selftest_result(output=output)
 809
 810        # Now if (selftes_results[0] == 2) i.e No new selftest (because the same
 811        # selftest was run twice within the last hour) but we know for a fact that
 812        # we just ran a new selftest then just return the latest entry in self.tests
 813        if selftest_results[0] == 2:
 814            selftest_return_value = 0 if 'Aborted' not in self.tests[0].status else 3
 815            return selftest_return_value, str(self.tests[0]) if output == 'str' else self.tests[0]
 816        return selftest_results[:2]
 817
 818    def update(self):
 819        """
 820        Queries for device information using smartctl and updates all
 821        class members, including the SMART attribute table and self-test log.
 822        Can be called at any time to refresh the `pySMART.device.Device`
 823        object's data content.
 824        """
 825        # set temperature back to None so that if update() is called more than once
 826        # any logic that relies on self.temperature to be None to rescan it works.it
 827        self.temperature = None
 828        # same for temperatures
 829        self.temperatures = {}
 830        if self.abridged:
 831            interface = None
 832            raw = self.smartctl.info(self.dev_reference)
 833
 834        else:
 835            interface = smartctl_type(self.interface)
 836            raw = self.smartctl.all(
 837                interface, self.dev_reference)
 838
 839        parse_self_tests = False
 840        parse_running_test = False
 841        parse_ascq = False
 842        message = ''
 843        self.tests = []
 844        self._test_running = False
 845        self._test_progress = None
 846        # Lets skip the first couple of non-useful lines
 847        _stdout = raw[4:]
 848
 849        #######################################
 850        #   Dedicated interface attributes    #
 851        #######################################
 852
 853        if interface == 'nvme':
 854            self.if_attributes = NvmeAttributes(iter(_stdout))
 855        else:
 856            self.if_attributes = None
 857
 858        #######################################
 859        #    Global / generic  attributes     #
 860        #######################################
 861        stdout_iter = iter(_stdout)
 862        for line in stdout_iter:
 863            if line.strip() == '':  # Blank line stops sub-captures
 864                if parse_self_tests is True:
 865                    parse_self_tests = False
 866                if parse_ascq:
 867                    parse_ascq = False
 868                    self.messages.append(message)
 869            if parse_ascq:
 870                message += ' ' + line.lstrip().rstrip()
 871            if parse_self_tests:
 872                num = line[0:3]
 873                if '#' not in num:
 874                    continue
 875
 876                # Detect Test Format
 877
 878                ## SCSI/SAS FORMAT ##
 879                # Example smartctl output
 880                # SMART Self-test log
 881                # Num  Test              Status                 segment  LifeTime  LBA_first_err [SK ASC ASQ]
 882                #      Description                              number   (hours)
 883                # # 1  Background short  Completed                   -   33124                 - [-   -    -]
 884                format_scsi = re.compile(
 885                    r'^[#\s]*([^\s]+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s+\[([^\s]+)\s+([^\s]+)\s+([^\s]+)\]$').match(line)
 886
 887                if format_scsi is not None:
 888                    format = 'scsi'
 889                    parsed = format_scsi.groups()
 890                    num = int(parsed[0])
 891                    test_type = parsed[1]
 892                    status = parsed[2]
 893                    segment = parsed[3]
 894                    hours = parsed[4]
 895                    lba = parsed[5]
 896                    sense = parsed[6]
 897                    asc = parsed[7]
 898                    ascq = parsed[8]
 899                    self.tests.append(TestEntry(
 900                        format,
 901                        num,
 902                        test_type,
 903                        status,
 904                        hours,
 905                        lba,
 906                        segment=segment,
 907                        sense=sense,
 908                        asc=asc,
 909                        ascq=ascq
 910                    ))
 911                else:
 912                    ## ATA FORMAT ##
 913                    # Example smartctl output:
 914                    # SMART Self-test log structure revision number 1
 915                    # Num  Test_Description    Status                  Remaining  LifeTime(hours)  LBA_of_first_error
 916                    # # 1  Extended offline    Completed without error       00%     46660         -
 917                    format = 'ata'
 918                    parsed = re.compile(
 919                        r'^[#\s]*([^\s]+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{1,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])$').match(line).groups()
 920                    num = parsed[0]
 921                    test_type = parsed[1]
 922                    status = parsed[2]
 923                    remain = parsed[3]
 924                    hours = parsed[4]
 925                    lba = parsed[5]
 926                    self.tests.append(
 927                        TestEntry(format, num, test_type, status,
 928                                  hours, lba, remain=remain)
 929                    )
 930            # Basic device information parsing
 931            if any_in(line, 'Device Model', 'Product', 'Model Number'):
 932                self.model = line.split(':')[1].lstrip().rstrip()
 933                self._guess_smart_type(line.lower())
 934                continue
 935
 936            if 'Model Family' in line:
 937                self._guess_smart_type(line.lower())
 938                continue
 939
 940            if 'LU WWN' in line:
 941                self._guess_smart_type(line.lower())
 942                continue
 943
 944            if any_in(line, 'Serial Number', 'Serial number'):
 945                self.serial = line.split(':')[1].split()[0].rstrip()
 946                continue
 947
 948            vendor = re.compile(r'^Vendor:\s+(\w+)').match(line)
 949            if vendor is not None:
 950                self.vendor = vendor.groups()[0]
 951
 952            if any_in(line, 'Firmware Version', 'Revision'):
 953                self.firmware = line.split(':')[1].strip()
 954
 955            if any_in(line, 'User Capacity', 'Namespace 1 Size/Capacity'):
 956                # TODO: support for multiple NVMe namespaces
 957                self._capacity = line.replace(']', '[').split('[')[
 958                    1].strip().replace(',', '.')
 959
 960            if 'SMART support' in line:
 961                # self.smart_capable = 'Available' in line
 962                # self.smart_enabled = 'Enabled' in line
 963                # Since this line repeats twice the above method is flawed
 964                # Lets try the following instead, it is a bit redundant but
 965                # more robust.
 966                if any_in(line, 'Unavailable', 'device lacks SMART capability'):
 967                    self.smart_capable = False
 968                    self.smart_enabled = False
 969                elif 'Enabled' in line:
 970                    self.smart_enabled = True
 971                elif 'Disabled' in line:
 972                    self.smart_enabled = False
 973                elif any_in(line, 'Available', 'device has SMART capability'):
 974                    self.smart_capable = True
 975                continue
 976
 977            if 'does not support SMART' in line:
 978                self.smart_capable = False
 979                self.smart_enabled = False
 980                continue
 981
 982            if 'Rotation Rate' in line:
 983                if 'Solid State Device' in line:
 984                    self.is_ssd = True
 985                elif 'rpm' in line:
 986                    self.is_ssd = False
 987                    try:
 988                        self.rotation_rate = int(
 989                            line.split(':')[1].lstrip().rstrip()[:-4])
 990                    except ValueError:
 991                        # Cannot parse the RPM? Assigning None instead
 992                        self.rotation_rate = None
 993                continue
 994
 995            if 'SMART overall-health self-assessment' in line:  # ATA devices
 996                if line.split(':')[1].strip() == 'PASSED':
 997                    self.assessment = 'PASS'
 998                else:
 999                    self.assessment = 'FAIL'
1000                continue
1001
1002            if 'SMART Health Status' in line:  # SCSI devices
1003                if line.split(':')[1].strip() == 'OK':
1004                    self.assessment = 'PASS'
1005                else:
1006                    self.assessment = 'FAIL'
1007                    parse_ascq = True  # Set flag to capture status message
1008                    message = line.split(':')[1].lstrip().rstrip()
1009                continue
1010
1011            # Parse SMART test capabilities (ATA only)
1012            # Note: SCSI does not list this but and allows for only 'offline', 'short' and 'long'
1013            if 'SMART execute Offline immediate' in line:
1014                self.test_capabilities['offline'] = 'No' not in line
1015                continue
1016
1017            if 'Conveyance Self-test supported' in line:
1018                self.test_capabilities['conveyance'] = 'No' not in line
1019                continue
1020
1021            if 'Selective Self-test supported' in line:
1022                self.test_capabilities['selective'] = 'No' not in line
1023                continue
1024
1025            if 'Self-test supported' in line:
1026                self.test_capabilities['short'] = 'No' not in line
1027                self.test_capabilities['long'] = 'No' not in line
1028                continue
1029
1030            # SMART Attribute table parsing
1031            if all_in(line, '0x0', '_') and not interface == 'nvme':
1032                # Replace multiple space separators with a single space, then
1033                # tokenize the string on space delimiters
1034                line_ = ' '.join(line.split()).split(' ')
1035                if '' not in line_:
1036                    self.attributes[int(line_[0])] = Attribute(
1037                        int(line_[0]), line_[1], int(line[2], base=16), line_[3], line_[4], line_[5], line_[6], line_[7], line_[8], line_[9])
1038            # For some reason smartctl does not show a currently running test
1039            # for 'ATA' in the Test log so I just have to catch it this way i guess!
1040            # For 'scsi' I still do it since it is the only place I get % remaining in scsi
1041            if 'Self-test execution status' in line:
1042                if 'progress' in line:
1043                    self._test_running = True
1044                    # for ATA the "%" remaining is on the next line
1045                    # thus set the parse_running_test flag and move on
1046                    parse_running_test = True
1047                elif '%' in line:
1048                    # for scsi the progress is on the same line
1049                    # so we can just parse it and move on
1050                    self._test_running = True
1051                    try:
1052                        self._test_progress = 100 - \
1053                            int(line.split('%')[0][-3:].strip())
1054                    except ValueError:
1055                        pass
1056                continue
1057            if parse_running_test is True:
1058                try:
1059                    self._test_progress = 100 - \
1060                        int(line.split('%')[0][-3:].strip())
1061                except ValueError:
1062                    pass
1063                parse_running_test = False
1064
1065            if all_in(line, 'Description', '(hours)'):
1066                parse_self_tests = True  # Set flag to capture test entries
1067
1068            #######################################
1069            #              SCSI only              #
1070            #######################################
1071            #
1072            # Everything from here on is parsing SCSI information that takes
1073            # the place of similar ATA SMART information
1074            if 'used endurance' in line:
1075                pct = int(line.split(':')[1].strip()[:-1])
1076                self.diagnostics.Life_Left = 100 - pct
1077                continue
1078
1079            if 'Specified cycle count' in line:
1080                self.diagnostics.Start_Stop_Spec = int(
1081                    line.split(':')[1].strip())
1082                continue
1083
1084            if 'Accumulated start-stop cycles' in line:
1085                self.diagnostics.Start_Stop_Cycles = int(
1086                    line.split(':')[1].strip())
1087                if self.diagnostics.Start_Stop_Spec != 0:
1088                    self.diagnostics.Start_Stop_Pct_Left = int(round(
1089                        100 - (self.diagnostics.Start_Stop_Cycles /
1090                               self.diagnostics.Start_Stop_Spec), 0))
1091                continue
1092
1093            if 'Specified load-unload count' in line:
1094                self.diagnostics.Load_Cycle_Spec = int(
1095                    line.split(':')[1].strip())
1096                continue
1097
1098            if 'Accumulated load-unload cycles' in line:
1099                self.diagnostics.Load_Cycle_Count = int(
1100                    line.split(':')[1].strip())
1101                if self.diagnostics.Load_Cycle_Spec != 0:
1102                    self.diagnostics.Load_Cycle_Pct_Left = int(round(
1103                        100 - (self.diagnostics.Load_Cycle_Count /
1104                               self.diagnostics.Load_Cycle_Spec), 0))
1105                continue
1106
1107            if 'Elements in grown defect list' in line:
1108                self.diagnostics.Reallocated_Sector_Ct = int(
1109                    line.split(':')[1].strip())
1110                continue
1111
1112            if 'read:' in line:
1113                line_ = ' '.join(line.split()).split(' ')
1114                if line_[1] == '0' and line_[2] == '0' and line_[3] == '0' and line_[4] == '0':
1115                    self.diagnostics.Corrected_Reads = 0
1116                elif line_[4] == '0':
1117                    self.diagnostics.Corrected_Reads = int(
1118                        line_[1]) + int(line_[2]) + int(line_[3])
1119                else:
1120                    self.diagnostics.Corrected_Reads = int(line_[4])
1121                self.diagnostics._Reads_GB = float(line_[6].replace(',', '.'))
1122                self.diagnostics._Uncorrected_Reads = int(line_[7])
1123                continue
1124
1125            if 'write:' in line:
1126                line_ = ' '.join(line.split()).split(' ')
1127                if (line_[1] == '0' and line_[2] == '0' and
1128                        line_[3] == '0' and line_[4] == '0'):
1129                    self.diagnostics.Corrected_Writes = 0
1130                elif line_[4] == '0':
1131                    self.diagnostics.Corrected_Writes = int(
1132                        line_[1]) + int(line_[2]) + int(line_[3])
1133                else:
1134                    self.diagnostics.Corrected_Writes = int(line_[4])
1135                self.diagnostics._Writes_GB = float(line_[6].replace(',', '.'))
1136                self.diagnostics._Uncorrected_Writes = int(line_[7])
1137                continue
1138
1139            if 'verify:' in line:
1140                line_ = ' '.join(line.split()).split(' ')
1141                if (line_[1] == '0' and line_[2] == '0' and
1142                        line_[3] == '0' and line_[4] == '0'):
1143                    self.diagnostics.Corrected_Verifies = 0
1144                elif line_[4] == '0':
1145                    self.diagnostics.Corrected_Verifies = int(
1146                        line_[1]) + int(line_[2]) + int(line_[3])
1147                else:
1148                    self.diagnostics.Corrected_Verifies = int(line_[4])
1149                self.diagnostics._Verifies_GB = float(
1150                    line_[6].replace(',', '.'))
1151                self.diagnostics._Uncorrected_Verifies = int(line_[7])
1152                continue
1153
1154            if 'non-medium error count' in line:
1155                self.diagnostics.Non_Medium_Errors = int(
1156                    line.split(':')[1].strip())
1157                continue
1158
1159            if 'Accumulated power on time' in line:
1160                self.diagnostics.Power_On_Hours = int(
1161                    line.split(':')[1].split(' ')[1])
1162                continue
1163
1164            if 'Current Drive Temperature' in line or ('Temperature:' in
1165                                                       line and interface == 'nvme'):
1166                try:
1167                    self.temperature = int(
1168                        line.split(':')[-1].strip().split()[0])
1169
1170                    if 'fahrenheit' in line.lower():
1171                        self.temperature = int((self.temperature - 32) * 5 / 9)
1172
1173                except ValueError:
1174                    pass
1175
1176                continue
1177
1178            if 'Temperature Sensor ' in line:
1179                try:
1180                    match = re.search(
1181                        r'Temperature\sSensor\s([0-9]+):\s+(-?[0-9]+)', line)
1182                    if match:
1183                        (tempsensor_number_s, tempsensor_value_s) = match.group(1, 2)
1184                        tempsensor_number = int(tempsensor_number_s)
1185                        tempsensor_value = int(tempsensor_value_s)
1186
1187                        if 'fahrenheit' in line.lower():
1188                            tempsensor_value = int(
1189                                (tempsensor_value - 32) * 5 / 9)
1190
1191                        self.temperatures[tempsensor_number] = tempsensor_value
1192                        if self.temperature is None or tempsensor_number == 0:
1193                            self.temperature = tempsensor_value
1194                except ValueError:
1195                    pass
1196
1197                continue
1198
1199            #######################################
1200            #            Common values            #
1201            #######################################
1202
1203            # Sector sizes
1204            if 'Sector Sizes' in line:  # ATA
1205                m = re.match(
1206                    r'.* (\d+) bytes logical,\s*(\d+) bytes physical', line)
1207                if m:
1208                    self.logical_sector_size = int(m.group(1))
1209                    self.physical_sector_size = int(m.group(2))
1210                    # set diagnostics block size to physical sector size
1211                    self.diagnostics._block_size = self.physical_sector_size
1212                continue
1213            if 'Logical block size:' in line:  # SCSI 1/2
1214                self.logical_sector_size = int(
1215                    line.split(':')[1].strip().split(' ')[0])
1216                # set diagnostics block size to logical sector size
1217                self.diagnostics._block_size = self.logical_sector_size
1218                continue
1219            if 'Physical block size:' in line:  # SCSI 2/2
1220                self.physical_sector_size = int(
1221                    line.split(':')[1].strip().split(' ')[0])
1222                continue
1223            if 'Namespace 1 Formatted LBA Size' in line:  # NVMe
1224                # Note: we will assume that there is only one namespace
1225                self.logical_sector_size = int(
1226                    line.split(':')[1].strip().split(' ')[0])
1227                continue
1228
1229        if not self.abridged:
1230            if not interface == 'scsi':
1231                # Parse the SMART table for below-threshold attributes and create
1232                # corresponding warnings for non-SCSI disks
1233                self._make_smart_warnings()
1234            else:
1235                # If not obtained Power_On_Hours above, make a direct attempt to extract power on
1236                # hours from the background scan results log.
1237                if self.diagnostics.Power_On_Hours is None:
1238                    raw, returncode = self.smartctl.generic_call(
1239                        [
1240                            '-d',
1241                            'scsi',
1242                            '-l',
1243                            'background',
1244                            self.dev_reference
1245                        ])
1246
1247                    for line in raw:
1248                        if 'power on time' in line:
1249                            self.diagnostics.Power_On_Hours = int(
1250                                line.split(':')[1].split(' ')[1])
1251        # map temperature
1252        if self.temperature is None:
1253            # in this case the disk is probably ata
1254            try:
1255                # Some disks report temperature to attribute number 190 ('Airflow_Temperature_Cel')
1256                # see https://bugs.freenas.org/issues/20860
1257                temp_attr = self.attributes[194] or self.attributes[190]
1258                self.temperature = int(temp_attr.raw)
1259            except (ValueError, AttributeError):
1260                pass
1261        # Now that we have finished the update routine, if we did not find a runnning selftest
1262        # nuke the self._test_ECD and self._test_progress
1263        if self._test_running is False:
1264            self._test_ECD = None
1265            self._test_progress = None

Represents any device attached to an internal storage interface, such as a hard drive or DVD-ROM, and detected by smartmontools. Includes eSATA (considered SATA) but excludes other external devices (USB, Firewire).

Device( name: str, interface: str = None, abridged: bool = False, smart_options: Union[str, List[str]] = None, smartctl: pySMART.smartctl.Smartctl = <pySMART.smartctl.Smartctl object>)
 80    def __init__(self, name: str, interface: str = None, abridged: bool = False, smart_options: Union[str, List[str]] = None, smartctl: Smartctl = SMARTCTL):
 81        """Instantiates and initializes the `pySMART.device.Device`."""
 82        if not (
 83                interface is None or
 84                smartctl_isvalid_type(interface.lower())
 85        ):
 86            raise ValueError(
 87                'Unknown interface: {0} specified for {1}'.format(interface, name))
 88        self.abridged = abridged or interface == 'UNKNOWN INTERFACE'
 89        if smart_options is not None:
 90            if isinstance(smart_options,  str):
 91                smart_options = smart_options.split(' ')
 92            smartctl.add_options(smart_options)
 93        self.smartctl = smartctl
 94        """
 95        """
 96        self.name: str = name.replace('/dev/', '').replace('nvd', 'nvme')
 97        """
 98        **(str):** Device's hardware ID, without the '/dev/' prefix.
 99        (ie: sda (Linux), pd0 (Windows))
100        """
101        self.model: str = None
102        """**(str):** Device's model number."""
103        self.serial: str = None
104        """**(str):** Device's serial number."""
105        self.vendor: str = None
106        """**(str):** Device's vendor (if any)."""
107        self.interface: str = None if interface == 'UNKNOWN INTERFACE' else interface
108        """
109        **(str):** Device's interface type. Must be one of:
110            * **ATA** - Advanced Technology Attachment
111            * **SATA** - Serial ATA
112            * **SCSI** - Small Computer Systems Interface
113            * **SAS** - Serial Attached SCSI
114            * **SAT** - SCSI-to-ATA Translation (SATA device plugged into a
115            SAS port)
116            * **CSMI** - Common Storage Management Interface (Intel ICH /
117            Matrix RAID)
118        Generally this should not be specified to allow auto-detection to
119        occur. Otherwise, this value overrides the auto-detected type and could
120        produce unexpected or no data.
121        """
122        self._capacity: str = None
123        """**(str):** Device's user capacity as reported directly by smartctl (RAW)."""
124        self.firmware: str = None
125        """**(str):** Device's firmware version."""
126        self.smart_capable: bool = 'nvme' in self.name
127        """
128        **(bool):** True if the device has SMART Support Available.
129        False otherwise. This is useful for VMs amongst other things.
130        """
131        self.smart_enabled: bool = 'nvme' in self.name
132        """
133        **(bool):** True if the device supports SMART (or SCSI equivalent) and
134        has the feature set enabled. False otherwise.
135        """
136        self.assessment: str = None
137        """
138        **(str):** SMART health self-assessment as reported by the device.
139        """
140        self.messages: List[str] = []
141        """
142        **(list of str):** Contains any SMART warnings or other error messages
143        reported by the device (ie: ascq codes).
144        """
145        self.is_ssd: bool = True if 'nvme' in self.name else False
146        """
147        **(bool):** True if this device is a Solid State Drive.
148        False otherwise.
149        """
150        self.rotation_rate: int = None
151        """
152        **(int):** The Roatation Rate of the Drive if it is not a SSD.
153        The Metric is RPM.
154        """
155        self.attributes: List[Attribute] = [None] * 256
156        """
157        **(list of `Attribute`):** Contains the complete SMART table
158        information for this device, as provided by smartctl. Indexed by
159        attribute #, values are set to 'None' for attributes not suported by
160        this device.
161        """
162        self.test_capabilities = {
163            'offline': False,  # SMART execute Offline immediate (ATA only)
164            'short': 'nvme' not in self.name,  # SMART short Self-test
165            'long': 'nvme' not in self.name,  # SMART long Self-test
166            'conveyance': False,  # SMART Conveyance Self-Test (ATA only)
167            'selective': False,  # SMART Selective Self-Test (ATA only)
168        }
169        # Note have not included 'offline' test for scsi as it runs in the foregorund
170        # mode. While this may be beneficial to us in someways it is against the
171        # general layout and pattern that the other tests issued using pySMART are
172        # followed hence not doing it currently
173        """
174        **(dict): ** This dictionary contains key == 'Test Name' and
175        value == 'True/False' of self-tests that this device is capable of.
176        """
177        # Note: The above are just default values and can/will be changed
178        # upon update() when the attributes and type of the disk is actually
179        # determined.
180        self.tests: List[TestEntry] = []
181        """
182        **(list of `TestEntry`):** Contains the complete SMART self-test log
183        for this device, as provided by smartctl.
184        """
185        self._test_running = False
186        """
187        **(bool):** True if a self-test is currently being run.
188        False otherwise.
189        """
190        self._test_ECD = None
191        """
192        **(str):** Estimated completion time of the running SMART selftest.
193        Not provided by SAS/SCSI devices.
194        """
195        self._test_progress = None
196        """
197        **(int):** Estimate progress percantage of the running SMART selftest.
198        """
199        self.diagnostics: Diagnostics = Diagnostics()
200        """
201        **Diagnostics** Contains parsed and processed diagnostic information
202        extracted from the SMART information. Currently only populated for
203        SAS and SCSI devices, since ATA/SATA SMART attributes are manufacturer
204        proprietary.
205        """
206        self.temperature: int = None
207        """
208        **(int or None): Since SCSI disks do not report attributes like ATA ones
209        we need to grep/regex the shit outta the normal "smartctl -a" output.
210        In case the device have more than one temperature sensor the first value
211        will be stored here too.
212        Note: Temperatures are always in Celsius (if possible).
213        """
214        self.temperatures: Dict[int, int] = {}
215        """
216        **(dict of int): NVMe disks usually report multiple temperatures, which
217        will be stored here if available. Keys are sensor numbers as reported in
218        output data.
219        Note: Temperatures are always in Celsius (if possible).
220        """
221        self.logical_sector_size: int = None
222        """
223        **(int):** The logical sector size of the device (or LBA).
224        """
225        self.physical_sector_size: int = None
226        """
227        **(int):** The physical sector size of the device.
228        """
229        self.if_attributes: Union[None, NvmeAttributes] = None
230        """
231        **(NvmeAttributes):** This object may vary for each device interface attributes.
232        It will store all data obtained from smartctl
233        """
234
235        if self.name is None:
236            warnings.warn(
237                "\nDevice '{0}' does not exist! This object should be destroyed.".format(
238                    name)
239            )
240            return
241        # If no interface type was provided, scan for the device
242        # Lets do this only for the non-abridged case
243        # (we can work with no interface for abridged case)
244        elif self.interface is None and not self.abridged:
245            logger.trace(
246                "Determining interface of disk: {0}".format(self.name))
247            raw, returncode = self.smartctl.generic_call(
248                ['-d', 'test', self.dev_reference])
249
250            if len(raw) > 0:
251                # I do not like this parsing logic but it works for now!
252                # just for reference _stdout.split('\n') gets us
253                # something like
254                # [
255                #     ...copyright string...,
256                #     '',
257                #     "/dev/ada2: Device of type 'atacam' [ATA] detected",
258                #     "/dev/ada2: Device of type 'atacam' [ATA] opened",
259                #     ''
260                # ]
261                # The above example should be enough for anyone to understand the line below
262                try:
263                    self.interface = raw[-2].split("'")[1]
264                    if self.interface == "nvme":  # if nvme set SMART to true
265                        self.smart_capable = True
266                        self.smart_enabled = True
267                except:
268                    # for whatever reason we could not get the interface type
269                    # we should mark this as an `abbridged` case and move on
270                    self.interface = None
271                    self.abbridged = True
272                # TODO: Uncomment the classify call if we ever find out that we need it
273                # Disambiguate the generic interface to a specific type
274                # self._classify()
275            else:
276                warnings.warn(
277                    "\nDevice '{0}' does not exist! This object should be destroyed.".format(
278                        name)
279                )
280                return
281        # If a valid device was detected, populate its information
282        # OR if in unabridged mode, then do it even without interface info
283        if self.interface is not None or self.abridged:
284            self.update()

Instantiates and initializes the pySMART.Device.

name: str

(str): Device's hardware ID, without the '/dev/' prefix. (ie: sda (Linux), pd0 (Windows))

model: str

(str): Device's model number.

serial: str

(str): Device's serial number.

vendor: str

(str): Device's vendor (if any).

interface: str

(str): Device's interface type. Must be one of: * ATA - Advanced Technology Attachment * SATA - Serial ATA * SCSI - Small Computer Systems Interface * SAS - Serial Attached SCSI * SAT - SCSI-to-ATA Translation (SATA device plugged into a SAS port) * CSMI - Common Storage Management Interface (Intel ICH / Matrix RAID) Generally this should not be specified to allow auto-detection to occur. Otherwise, this value overrides the auto-detected type and could produce unexpected or no data.

firmware: str

(str): Device's firmware version.

smart_capable: bool

(bool): True if the device has SMART Support Available. False otherwise. This is useful for VMs amongst other things.

smart_enabled: bool

(bool): True if the device supports SMART (or SCSI equivalent) and has the feature set enabled. False otherwise.

assessment: str

(str): SMART health self-assessment as reported by the device.

messages: List[str]

(list of str): Contains any SMART warnings or other error messages reported by the device (ie: ascq codes).

is_ssd: bool

(bool): True if this device is a Solid State Drive. False otherwise.

rotation_rate: int

(int): The Roatation Rate of the Drive if it is not a SSD. The Metric is RPM.

attributes: List[pySMART.Attribute]

(list of Attribute): Contains the complete SMART table information for this device, as provided by smartctl. Indexed by attribute #, values are set to 'None' for attributes not suported by this device.

test_capabilities

*(dict): * This dictionary contains key == 'Test Name' and value == 'True/False' of self-tests that this device is capable of.

tests: List[pySMART.TestEntry]

(list of TestEntry): Contains the complete SMART self-test log for this device, as provided by smartctl.

diagnostics: pySMART.diagnostics.Diagnostics

Diagnostics Contains parsed and processed diagnostic information extracted from the SMART information. Currently only populated for SAS and SCSI devices, since ATA/SATA SMART attributes are manufacturer proprietary.

temperature: int

**(int or None): Since SCSI disks do not report attributes like ATA ones we need to grep/regex the shit outta the normal "smartctl -a" output. In case the device have more than one temperature sensor the first value will be stored here too. Note: Temperatures are always in Celsius (if possible).

temperatures: Dict[int, int]

**(dict of int): NVMe disks usually report multiple temperatures, which will be stored here if available. Keys are sensor numbers as reported in output data. Note: Temperatures are always in Celsius (if possible).

logical_sector_size: int

(int): The logical sector size of the device (or LBA).

physical_sector_size: int

(int): The physical sector size of the device.

if_attributes: Union[NoneType, pySMART.interface.nvme.NvmeAttributes]

(NvmeAttributes): This object may vary for each device interface attributes. It will store all data obtained from smartctl

dev_reference: str

The reference to the device as provided by smartctl.

  • On unix-like systems, this is the path to the device. (example /dev/)
  • On MacOS, this is the name of the device. (example )
  • On Windows, this is the drive letter of the device. (example )

Returns: str: The reference to the device as provided by smartctl.

capacity: str

Returns the capacity in the raw smartctl format. This may be deprecated in the future and its only retained for compatibility.

Returns: str: The capacity in the raw smartctl format

diags: Dict[str, str]

Gets the old/deprecated version of SCSI/SAS diags atribute.

size_raw: str

Returns the capacity in the raw smartctl format.

Returns: str: The capacity in the raw smartctl format

size: int

Returns the capacity in bytes

Returns: int: The capacity in bytes

sector_size: int

Returns the sector size of the device.

Returns: int: The sector size of the device in Bytes. If undefined, we'll assume 512B

def smart_toggle(self, action: str) -> Tuple[bool, List[str]]:
398    def smart_toggle(self, action: str) -> Tuple[bool, List[str]]:
399        """
400        A basic function to enable/disable SMART on device.
401
402        # Args:
403        * **action (str):** Can be either 'on'(for enabling) or 'off'(for disabling).
404
405        # Returns"
406        * **(bool):** Return True (if action succeded) else False
407        * **(List[str]):** None if option succeded else contains the error message.
408        """
409        # Lets make the action verb all lower case
410        if self.interface == 'nvme':
411            return False, 'NVME devices do not currently support toggling SMART enabled'
412        action_lower = action.lower()
413        if action_lower not in ['on', 'off']:
414            return False, 'Unsupported action {0}'.format(action)
415        # Now lets check if the device's smart enabled status is already that of what
416        # the supplied action is intending it to be. If so then just return successfully
417        if self.smart_enabled:
418            if action_lower == 'on':
419                return True, None
420        else:
421            if action_lower == 'off':
422                return True, None
423        raw, returncode = self.smartctl.generic_call(
424            ['-s', action_lower, self.dev_reference])
425
426        if returncode != 0:
427            return False, raw
428        # if everything worked out so far lets perform an update() and check the result
429        self.update()
430        if action_lower == 'off' and self.smart_enabled:
431            return False, 'Failed to turn SMART off.'
432        if action_lower == 'on' and not self.smart_enabled:
433            return False, 'Failed to turn SMART on.'
434        return True, None

A basic function to enable/disable SMART on device.

Args:

  • action (str): Can be either 'on'(for enabling) or 'off'(for disabling).

Returns"

  • (bool): Return True (if action succeded) else False
  • (List[str]): None if option succeded else contains the error message.
def all_attributes(self, print_fn=<built-in function print>):
436    def all_attributes(self, print_fn=print):
437        """
438        Prints the entire SMART attribute table, in a format similar to
439        the output of smartctl.
440        allows usage of custom print function via parameter print_fn by default uses print
441        """
442        header_printed = False
443        for attr in self.attributes:
444            if attr is not None:
445                if not header_printed:
446                    print_fn("{0:>3} {1:24}{2:4}{3:4}{4:4}{5:9}{6:8}{7:12}{8}"
447                             .format('ID#', 'ATTRIBUTE_NAME', 'CUR', 'WST', 'THR', 'TYPE', 'UPDATED', 'WHEN_FAIL',
448                                     'RAW'))
449                    header_printed = True
450                print_fn(attr)
451        if not header_printed:
452            print_fn('This device does not support SMART attributes.')

Prints the entire SMART attribute table, in a format similar to the output of smartctl. allows usage of custom print function via parameter print_fn by default uses print

def all_selftests(self):
454    def all_selftests(self):
455        """
456        Prints the entire SMART self-test log, in a format similar to
457        the output of smartctl.
458        """
459        if self.tests:
460            all_tests = []
461            if smartctl_type(self.interface) == 'scsi':
462                header = "{0:3}{1:17}{2:23}{3:7}{4:14}{5:15}".format(
463                    'ID',
464                    'Test Description',
465                    'Status',
466                    'Hours',
467                    '1st_Error@LBA',
468                    '[SK  ASC  ASCQ]'
469                )
470            else:
471                header = ("{0:3}{1:17}{2:30}{3:5}{4:7}{5:17}".format(
472                    'ID',
473                    'Test_Description',
474                    'Status',
475                    'Left',
476                    'Hours',
477                    '1st_Error@LBA'))
478            all_tests.append(header)
479            for test in self.tests:
480                all_tests.append(str(test))
481
482            return all_tests
483        else:
484            no_tests = 'No self-tests have been logged for this device.'
485            return no_tests

Prints the entire SMART self-test log, in a format similar to the output of smartctl.

def get_selftest_result(self, output=None):
562    def get_selftest_result(self, output=None):
563        """
564        Refreshes a device's `pySMART.device.Device.tests` attribute to obtain
565        the latest test results. If a new test result is obtained, its content
566        is returned.
567
568        # Args:
569        * **output (str, optional):** If set to 'str', the string
570        representation of the most recent test result will be returned, instead
571        of a `Test_Entry` object.
572
573        # Returns:
574        * **(int):** Return status code. One of the following:
575            * 0 - Success. Object (or optionally, string rep) is attached.
576            * 1 - Self-test in progress. Must wait for it to finish.
577            * 2 - No new test results.
578            * 3 - The Self-test was Aborted by host
579        * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or
580        optionally it's string representation) if new data exists.  Status
581        message string on failure.
582        * **(int):** Estimate progress percantage of the running SMART selftest, if known.
583        Otherwise 'None'.
584        """
585        # SCSI self-test logs hold 20 entries while ATA logs hold 21
586        if smartctl_type(self.interface) == 'scsi':
587            maxlog = 20
588        else:
589            maxlog = 21
590        # If we looked only at the most recent test result we could be fooled
591        # by two short tests run close together (within the same hour)
592        # appearing identical. Comparing the length of the log adds some
593        # confidence until it maxes, as above. Comparing the least-recent test
594        # result greatly diminishes the chances that two sets of two tests each
595        # were run within an hour of themselves, but with 16-17 other tests run
596        # in between them.
597        if self.tests:
598            _first_entry = self.tests[0]
599            _len = len(self.tests)
600            _last_entry = self.tests[_len - 1]
601        else:
602            _len = 0
603        self.update()
604        # Since I have changed the update() parsing to DTRT to pickup currently
605        # running selftests we can now purely rely on that for self._test_running
606        # Thus check for that variable first and return if it is True with appropos message.
607        if self._test_running is True:
608            return 1, 'Self-test in progress. Please wait.', self._test_progress
609        # Check whether the list got longer (ie: new entry)
610        # If so return the newest test result
611        # If not, because it's max size already, check for new entries
612        if (
613                (len(self.tests) != _len) or
614                (
615                    len == maxlog and
616                    (
617                        _first_entry.type != self.tests[0].type or
618                        _first_entry.hours != self.tests[0].hours or
619                        _last_entry.type != self.tests[len(self.tests) - 1].type or
620                        _last_entry.hours != self.tests[len(
621                            self.tests) - 1].hours
622                    )
623                )
624        ):
625            return (
626                0 if 'Aborted' not in self.tests[0].status else 3,
627                str(self.tests[0]) if output == 'str' else self.tests[0],
628                None
629            )
630        else:
631            return 2, 'No new self-test results found.', None

Refreshes a device's pySMART.Device.tests attribute to obtain the latest test results. If a new test result is obtained, its content is returned.

Args:

  • output (str, optional): If set to 'str', the string representation of the most recent test result will be returned, instead of a Test_Entry object.

Returns:

  • (int): Return status code. One of the following:
    • 0 - Success. Object (or optionally, string rep) is attached.
    • 1 - Self-test in progress. Must wait for it to finish.
    • 2 - No new test results.
    • 3 - The Self-test was Aborted by host
  • (Test_Entry or str): Most recent Test_Entry object (or optionally it's string representation) if new data exists. Status message string on failure.
  • (int): Estimate progress percantage of the running SMART selftest, if known. Otherwise 'None'.
def abort_selftest(self):
633    def abort_selftest(self):
634        """
635        Aborts non-captive SMART Self Tests.   Note that this command
636        will  abort the Offline Immediate Test routine only if your disk
637        has the "Abort Offline collection upon new command"  capability.
638
639        # Args: Nothing (just aborts directly)
640
641        # Returns:
642        * **(int):** The returncode of calling `smartctl -X device_path`
643        """
644        return self.smartctl.test_stop(smartctl_type(self.interface), self.dev_reference)

Aborts non-captive SMART Self Tests. Note that this command will abort the Offline Immediate Test routine only if your disk has the "Abort Offline collection upon new command" capability.

Args: Nothing (just aborts directly)

Returns:

  • (int): The returncode of calling smartctl -X device_path
def run_selftest(self, test_type, ETA_type='date'):
646    def run_selftest(self, test_type, ETA_type='date'):
647        """
648        Instructs a device to begin a SMART self-test. All tests are run in
649        'offline' / 'background' mode, allowing normal use of the device while
650        it is being tested.
651
652        # Args:
653        * **test_type (str):** The type of test to run. Accepts the following
654        (not case sensitive):
655            * **short** - Brief electo-mechanical functionality check.
656            Generally takes 2 minutes or less.
657            * **long** - Thorough electro-mechanical functionality check,
658            including complete recording media scan. Generally takes several
659            hours.
660            * **conveyance** - Brief test used to identify damage incurred in
661            shipping. Generally takes 5 minutes or less. **This test is not
662            supported by SAS or SCSI devices.**
663            * **offline** - Runs SMART Immediate Offline Test. The effects of
664            this test are visible only in that it updates the SMART Attribute
665            values, and if errors are found they will appear in the SMART error
666            log, visible with the '-l error' option to smartctl. **This test is
667            not supported by SAS or SCSI devices in pySMART use cli smartctl for
668            running 'offline' selftest (runs in foreground) on scsi devices.**
669            * **ETA_type** - Format to return the estimated completion time/date
670            in. Default is 'date'. One could otherwise specidy 'seconds'.
671            Again only for ATA devices.
672
673        # Returns:
674        * **(int):** Return status code.  One of the following:
675            * 0 - Self-test initiated successfully
676            * 1 - Previous self-test running. Must wait for it to finish.
677            * 2 - Unknown or unsupported (by the device) test type requested.
678            * 3 - Unspecified smartctl error. Self-test not initiated.
679        * **(str):** Return status message.
680        * **(str)/(float):** Estimated self-test completion time if a test is started.
681        The optional argument of 'ETA_type' (see above) controls the return type.
682        if 'ETA_type' == 'date' then a date string is returned else seconds(float)
683        is returned.
684        Note: The self-test completion time can only be obtained for ata devices.
685        Otherwise 'None'.
686        """
687        # Lets call get_selftest_result() here since it does an update() and
688        # checks for an existing selftest is running or not, this way the user
689        # can issue a test from the cli and this can still pick that up
690        # Also note that we do not need to obtain the results from this as the
691        # data is already stored in the Device class object's variables
692        self.get_selftest_result()
693        if self._test_running:
694            return 1, 'Self-test in progress. Please wait.', self._test_ECD
695        test_type = test_type.lower()
696        interface = smartctl_type(self.interface)
697        try:
698            if not self.test_capabilities[test_type]:
699                return (
700                    2,
701                    "Device {0} does not support the '{1}' test ".format(
702                        self.name, test_type),
703                    None
704                )
705        except KeyError:
706            return 2, "Unknown test type '{0}' requested.".format(test_type), None
707
708        raw = self.smartctl.test_start(
709            interface, test_type, self.dev_reference)
710        _success = False
711        _running = False
712        for line in raw:
713            if 'has begun' in line:
714                _success = True
715                self._test_running = True
716            if 'aborting current test' in line:
717                _running = True
718                try:
719                    self._test_progress = 100 - \
720                        int(line.split('(')[-1].split('%')[0])
721                except ValueError:
722                    pass
723
724            if _success and 'complete after' in line:
725                self._test_ECD = line[25:].rstrip()
726                if ETA_type == 'seconds':
727                    self._test_ECD = mktime(
728                        strptime(self._test_ECD, '%a %b %d %H:%M:%S %Y')) - time()
729                self._test_progress = 0
730        if _success:
731            return 0, 'Self-test started successfully', self._test_ECD
732        else:
733            if _running:
734                return 1, 'Self-test already in progress. Please wait.', self._test_ECD
735            else:
736                return 3, 'Unspecified Error. Self-test not started.', None

Instructs a device to begin a SMART self-test. All tests are run in 'offline' / 'background' mode, allowing normal use of the device while it is being tested.

Args:

  • test_type (str): The type of test to run. Accepts the following (not case sensitive):
    • short - Brief electo-mechanical functionality check. Generally takes 2 minutes or less.
    • long - Thorough electro-mechanical functionality check, including complete recording media scan. Generally takes several hours.
    • conveyance - Brief test used to identify damage incurred in shipping. Generally takes 5 minutes or less. This test is not supported by SAS or SCSI devices.
    • offline - Runs SMART Immediate Offline Test. The effects of this test are visible only in that it updates the SMART Attribute values, and if errors are found they will appear in the SMART error log, visible with the '-l error' option to smartctl. This test is not supported by SAS or SCSI devices in pySMART use cli smartctl for running 'offline' selftest (runs in foreground) on scsi devices.
    • ETA_type - Format to return the estimated completion time/date in. Default is 'date'. One could otherwise specidy 'seconds'. Again only for ATA devices.

Returns:

  • (int): Return status code. One of the following:
    • 0 - Self-test initiated successfully
    • 1 - Previous self-test running. Must wait for it to finish.
    • 2 - Unknown or unsupported (by the device) test type requested.
    • 3 - Unspecified smartctl error. Self-test not initiated.
  • (str): Return status message.
  • (str)/(float): Estimated self-test completion time if a test is started. The optional argument of 'ETA_type' (see above) controls the return type. if 'ETA_type' == 'date' then a date string is returned else seconds(float) is returned. Note: The self-test completion time can only be obtained for ata devices. Otherwise 'None'.
def run_selftest_and_wait(self, test_type, output=None, polling=5, progress_handler=None):
738    def run_selftest_and_wait(self, test_type, output=None, polling=5, progress_handler=None):
739        """
740        This is essentially a wrapper around run_selftest() such that we
741        call self.run_selftest() and wait on the running selftest till
742        it finished before returning.
743        The above holds true for all pySMART supported tests with the
744        exception of the 'offline' test (ATA only) as it immediately
745        returns, since the entire test only affects the smart error log
746        (if any errors found) and updates the SMART attributes. Other
747        than that it is not visibile anywhere else, so we start it and
748        simply return.
749        # Args:
750        * **test_type (str):** The type of test to run. Accepts the following
751        (not case sensitive):
752            * **short** - Brief electo-mechanical functionality check.
753            Generally takes 2 minutes or less.
754            * **long** - Thorough electro-mechanical functionality check,
755            including complete recording media scan. Generally takes several
756            hours.
757            * **conveyance** - Brief test used to identify damage incurred in
758            shipping. Generally takes 5 minutes or less. **This test is not
759            supported by SAS or SCSI devices.**
760            * **offline** - Runs SMART Immediate Offline Test. The effects of
761            this test are visible only in that it updates the SMART Attribute
762            values, and if errors are found they will appear in the SMART error
763            log, visible with the '-l error' option to smartctl. **This test is
764            not supported by SAS or SCSI devices in pySMART use cli smartctl for
765            running 'offline' selftest (runs in foreground) on scsi devices.**
766        * **output (str, optional):** If set to 'str', the string
767            representation of the most recent test result will be returned,
768            instead of a `Test_Entry` object.
769        * **polling (int, default=5):** The time duration to sleep for between
770            checking for test_results and progress.
771        * **progress_handler (function, optional):** This if provided is called
772            with self._test_progress as the supplied argument everytime a poll to
773            check the status of the selftest is done.
774        # Returns:
775        * **(int):** Return status code.  One of the following:
776            * 0 - Self-test executed and finished successfully
777            * 1 - Previous self-test running. Must wait for it to finish.
778            * 2 - Unknown or illegal test type requested.
779            * 3 - The Self-test was Aborted by host
780            * 4 - Unspecified smartctl error. Self-test not initiated.
781        * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or
782        optionally it's string representation) if new data exists.  Status
783        message string on failure.
784        """
785        test_initiation_result = self.run_selftest(test_type)
786        if test_initiation_result[0] != 0:
787            return test_initiation_result[:2]
788        if test_type == 'offline':
789            self._test_running = False
790        # if not then the test initiated correctly and we can start the polling.
791        # For now default 'polling' value is 5 seconds if not specified by the user
792
793        # Do an initial check, for good measure.
794        # In the probably impossible case that self._test_running is instantly False...
795        selftest_results = self.get_selftest_result(output=output)
796        while self._test_running:
797            if selftest_results[0] != 1:
798                # the selftest is run and finished lets return with the results
799                break
800            # Otherwise see if we are provided with the progress_handler to update progress
801            if progress_handler is not None:
802                progress_handler(
803                    selftest_results[2] if selftest_results[2] is not None else 50)
804            # Now sleep 'polling' seconds before checking the progress again
805            sleep(polling)
806
807            # Check after the sleep to ensure we return the right result, and not an old one.
808            selftest_results = self.get_selftest_result(output=output)
809
810        # Now if (selftes_results[0] == 2) i.e No new selftest (because the same
811        # selftest was run twice within the last hour) but we know for a fact that
812        # we just ran a new selftest then just return the latest entry in self.tests
813        if selftest_results[0] == 2:
814            selftest_return_value = 0 if 'Aborted' not in self.tests[0].status else 3
815            return selftest_return_value, str(self.tests[0]) if output == 'str' else self.tests[0]
816        return selftest_results[:2]

This is essentially a wrapper around run_selftest() such that we call self.run_selftest() and wait on the running selftest till it finished before returning. The above holds true for all pySMART supported tests with the exception of the 'offline' test (ATA only) as it immediately returns, since the entire test only affects the smart error log (if any errors found) and updates the SMART attributes. Other than that it is not visibile anywhere else, so we start it and simply return.

Args:

  • test_type (str): The type of test to run. Accepts the following (not case sensitive):
    • short - Brief electo-mechanical functionality check. Generally takes 2 minutes or less.
    • long - Thorough electro-mechanical functionality check, including complete recording media scan. Generally takes several hours.
    • conveyance - Brief test used to identify damage incurred in shipping. Generally takes 5 minutes or less. This test is not supported by SAS or SCSI devices.
    • offline - Runs SMART Immediate Offline Test. The effects of this test are visible only in that it updates the SMART Attribute values, and if errors are found they will appear in the SMART error log, visible with the '-l error' option to smartctl. This test is not supported by SAS or SCSI devices in pySMART use cli smartctl for running 'offline' selftest (runs in foreground) on scsi devices.
  • output (str, optional): If set to 'str', the string representation of the most recent test result will be returned, instead of a Test_Entry object.
  • polling (int, default=5): The time duration to sleep for between checking for test_results and progress.
  • progress_handler (function, optional): This if provided is called with self._test_progress as the supplied argument everytime a poll to check the status of the selftest is done.

    Returns:

  • (int): Return status code. One of the following:

    • 0 - Self-test executed and finished successfully
    • 1 - Previous self-test running. Must wait for it to finish.
    • 2 - Unknown or illegal test type requested.
    • 3 - The Self-test was Aborted by host
    • 4 - Unspecified smartctl error. Self-test not initiated.
  • (Test_Entry or str): Most recent Test_Entry object (or optionally it's string representation) if new data exists. Status message string on failure.
def update(self):
 818    def update(self):
 819        """
 820        Queries for device information using smartctl and updates all
 821        class members, including the SMART attribute table and self-test log.
 822        Can be called at any time to refresh the `pySMART.device.Device`
 823        object's data content.
 824        """
 825        # set temperature back to None so that if update() is called more than once
 826        # any logic that relies on self.temperature to be None to rescan it works.it
 827        self.temperature = None
 828        # same for temperatures
 829        self.temperatures = {}
 830        if self.abridged:
 831            interface = None
 832            raw = self.smartctl.info(self.dev_reference)
 833
 834        else:
 835            interface = smartctl_type(self.interface)
 836            raw = self.smartctl.all(
 837                interface, self.dev_reference)
 838
 839        parse_self_tests = False
 840        parse_running_test = False
 841        parse_ascq = False
 842        message = ''
 843        self.tests = []
 844        self._test_running = False
 845        self._test_progress = None
 846        # Lets skip the first couple of non-useful lines
 847        _stdout = raw[4:]
 848
 849        #######################################
 850        #   Dedicated interface attributes    #
 851        #######################################
 852
 853        if interface == 'nvme':
 854            self.if_attributes = NvmeAttributes(iter(_stdout))
 855        else:
 856            self.if_attributes = None
 857
 858        #######################################
 859        #    Global / generic  attributes     #
 860        #######################################
 861        stdout_iter = iter(_stdout)
 862        for line in stdout_iter:
 863            if line.strip() == '':  # Blank line stops sub-captures
 864                if parse_self_tests is True:
 865                    parse_self_tests = False
 866                if parse_ascq:
 867                    parse_ascq = False
 868                    self.messages.append(message)
 869            if parse_ascq:
 870                message += ' ' + line.lstrip().rstrip()
 871            if parse_self_tests:
 872                num = line[0:3]
 873                if '#' not in num:
 874                    continue
 875
 876                # Detect Test Format
 877
 878                ## SCSI/SAS FORMAT ##
 879                # Example smartctl output
 880                # SMART Self-test log
 881                # Num  Test              Status                 segment  LifeTime  LBA_first_err [SK ASC ASQ]
 882                #      Description                              number   (hours)
 883                # # 1  Background short  Completed                   -   33124                 - [-   -    -]
 884                format_scsi = re.compile(
 885                    r'^[#\s]*([^\s]+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s+\[([^\s]+)\s+([^\s]+)\s+([^\s]+)\]$').match(line)
 886
 887                if format_scsi is not None:
 888                    format = 'scsi'
 889                    parsed = format_scsi.groups()
 890                    num = int(parsed[0])
 891                    test_type = parsed[1]
 892                    status = parsed[2]
 893                    segment = parsed[3]
 894                    hours = parsed[4]
 895                    lba = parsed[5]
 896                    sense = parsed[6]
 897                    asc = parsed[7]
 898                    ascq = parsed[8]
 899                    self.tests.append(TestEntry(
 900                        format,
 901                        num,
 902                        test_type,
 903                        status,
 904                        hours,
 905                        lba,
 906                        segment=segment,
 907                        sense=sense,
 908                        asc=asc,
 909                        ascq=ascq
 910                    ))
 911                else:
 912                    ## ATA FORMAT ##
 913                    # Example smartctl output:
 914                    # SMART Self-test log structure revision number 1
 915                    # Num  Test_Description    Status                  Remaining  LifeTime(hours)  LBA_of_first_error
 916                    # # 1  Extended offline    Completed without error       00%     46660         -
 917                    format = 'ata'
 918                    parsed = re.compile(
 919                        r'^[#\s]*([^\s]+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{1,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])$').match(line).groups()
 920                    num = parsed[0]
 921                    test_type = parsed[1]
 922                    status = parsed[2]
 923                    remain = parsed[3]
 924                    hours = parsed[4]
 925                    lba = parsed[5]
 926                    self.tests.append(
 927                        TestEntry(format, num, test_type, status,
 928                                  hours, lba, remain=remain)
 929                    )
 930            # Basic device information parsing
 931            if any_in(line, 'Device Model', 'Product', 'Model Number'):
 932                self.model = line.split(':')[1].lstrip().rstrip()
 933                self._guess_smart_type(line.lower())
 934                continue
 935
 936            if 'Model Family' in line:
 937                self._guess_smart_type(line.lower())
 938                continue
 939
 940            if 'LU WWN' in line:
 941                self._guess_smart_type(line.lower())
 942                continue
 943
 944            if any_in(line, 'Serial Number', 'Serial number'):
 945                self.serial = line.split(':')[1].split()[0].rstrip()
 946                continue
 947
 948            vendor = re.compile(r'^Vendor:\s+(\w+)').match(line)
 949            if vendor is not None:
 950                self.vendor = vendor.groups()[0]
 951
 952            if any_in(line, 'Firmware Version', 'Revision'):
 953                self.firmware = line.split(':')[1].strip()
 954
 955            if any_in(line, 'User Capacity', 'Namespace 1 Size/Capacity'):
 956                # TODO: support for multiple NVMe namespaces
 957                self._capacity = line.replace(']', '[').split('[')[
 958                    1].strip().replace(',', '.')
 959
 960            if 'SMART support' in line:
 961                # self.smart_capable = 'Available' in line
 962                # self.smart_enabled = 'Enabled' in line
 963                # Since this line repeats twice the above method is flawed
 964                # Lets try the following instead, it is a bit redundant but
 965                # more robust.
 966                if any_in(line, 'Unavailable', 'device lacks SMART capability'):
 967                    self.smart_capable = False
 968                    self.smart_enabled = False
 969                elif 'Enabled' in line:
 970                    self.smart_enabled = True
 971                elif 'Disabled' in line:
 972                    self.smart_enabled = False
 973                elif any_in(line, 'Available', 'device has SMART capability'):
 974                    self.smart_capable = True
 975                continue
 976
 977            if 'does not support SMART' in line:
 978                self.smart_capable = False
 979                self.smart_enabled = False
 980                continue
 981
 982            if 'Rotation Rate' in line:
 983                if 'Solid State Device' in line:
 984                    self.is_ssd = True
 985                elif 'rpm' in line:
 986                    self.is_ssd = False
 987                    try:
 988                        self.rotation_rate = int(
 989                            line.split(':')[1].lstrip().rstrip()[:-4])
 990                    except ValueError:
 991                        # Cannot parse the RPM? Assigning None instead
 992                        self.rotation_rate = None
 993                continue
 994
 995            if 'SMART overall-health self-assessment' in line:  # ATA devices
 996                if line.split(':')[1].strip() == 'PASSED':
 997                    self.assessment = 'PASS'
 998                else:
 999                    self.assessment = 'FAIL'
1000                continue
1001
1002            if 'SMART Health Status' in line:  # SCSI devices
1003                if line.split(':')[1].strip() == 'OK':
1004                    self.assessment = 'PASS'
1005                else:
1006                    self.assessment = 'FAIL'
1007                    parse_ascq = True  # Set flag to capture status message
1008                    message = line.split(':')[1].lstrip().rstrip()
1009                continue
1010
1011            # Parse SMART test capabilities (ATA only)
1012            # Note: SCSI does not list this but and allows for only 'offline', 'short' and 'long'
1013            if 'SMART execute Offline immediate' in line:
1014                self.test_capabilities['offline'] = 'No' not in line
1015                continue
1016
1017            if 'Conveyance Self-test supported' in line:
1018                self.test_capabilities['conveyance'] = 'No' not in line
1019                continue
1020
1021            if 'Selective Self-test supported' in line:
1022                self.test_capabilities['selective'] = 'No' not in line
1023                continue
1024
1025            if 'Self-test supported' in line:
1026                self.test_capabilities['short'] = 'No' not in line
1027                self.test_capabilities['long'] = 'No' not in line
1028                continue
1029
1030            # SMART Attribute table parsing
1031            if all_in(line, '0x0', '_') and not interface == 'nvme':
1032                # Replace multiple space separators with a single space, then
1033                # tokenize the string on space delimiters
1034                line_ = ' '.join(line.split()).split(' ')
1035                if '' not in line_:
1036                    self.attributes[int(line_[0])] = Attribute(
1037                        int(line_[0]), line_[1], int(line[2], base=16), line_[3], line_[4], line_[5], line_[6], line_[7], line_[8], line_[9])
1038            # For some reason smartctl does not show a currently running test
1039            # for 'ATA' in the Test log so I just have to catch it this way i guess!
1040            # For 'scsi' I still do it since it is the only place I get % remaining in scsi
1041            if 'Self-test execution status' in line:
1042                if 'progress' in line:
1043                    self._test_running = True
1044                    # for ATA the "%" remaining is on the next line
1045                    # thus set the parse_running_test flag and move on
1046                    parse_running_test = True
1047                elif '%' in line:
1048                    # for scsi the progress is on the same line
1049                    # so we can just parse it and move on
1050                    self._test_running = True
1051                    try:
1052                        self._test_progress = 100 - \
1053                            int(line.split('%')[0][-3:].strip())
1054                    except ValueError:
1055                        pass
1056                continue
1057            if parse_running_test is True:
1058                try:
1059                    self._test_progress = 100 - \
1060                        int(line.split('%')[0][-3:].strip())
1061                except ValueError:
1062                    pass
1063                parse_running_test = False
1064
1065            if all_in(line, 'Description', '(hours)'):
1066                parse_self_tests = True  # Set flag to capture test entries
1067
1068            #######################################
1069            #              SCSI only              #
1070            #######################################
1071            #
1072            # Everything from here on is parsing SCSI information that takes
1073            # the place of similar ATA SMART information
1074            if 'used endurance' in line:
1075                pct = int(line.split(':')[1].strip()[:-1])
1076                self.diagnostics.Life_Left = 100 - pct
1077                continue
1078
1079            if 'Specified cycle count' in line:
1080                self.diagnostics.Start_Stop_Spec = int(
1081                    line.split(':')[1].strip())
1082                continue
1083
1084            if 'Accumulated start-stop cycles' in line:
1085                self.diagnostics.Start_Stop_Cycles = int(
1086                    line.split(':')[1].strip())
1087                if self.diagnostics.Start_Stop_Spec != 0:
1088                    self.diagnostics.Start_Stop_Pct_Left = int(round(
1089                        100 - (self.diagnostics.Start_Stop_Cycles /
1090                               self.diagnostics.Start_Stop_Spec), 0))
1091                continue
1092
1093            if 'Specified load-unload count' in line:
1094                self.diagnostics.Load_Cycle_Spec = int(
1095                    line.split(':')[1].strip())
1096                continue
1097
1098            if 'Accumulated load-unload cycles' in line:
1099                self.diagnostics.Load_Cycle_Count = int(
1100                    line.split(':')[1].strip())
1101                if self.diagnostics.Load_Cycle_Spec != 0:
1102                    self.diagnostics.Load_Cycle_Pct_Left = int(round(
1103                        100 - (self.diagnostics.Load_Cycle_Count /
1104                               self.diagnostics.Load_Cycle_Spec), 0))
1105                continue
1106
1107            if 'Elements in grown defect list' in line:
1108                self.diagnostics.Reallocated_Sector_Ct = int(
1109                    line.split(':')[1].strip())
1110                continue
1111
1112            if 'read:' in line:
1113                line_ = ' '.join(line.split()).split(' ')
1114                if line_[1] == '0' and line_[2] == '0' and line_[3] == '0' and line_[4] == '0':
1115                    self.diagnostics.Corrected_Reads = 0
1116                elif line_[4] == '0':
1117                    self.diagnostics.Corrected_Reads = int(
1118                        line_[1]) + int(line_[2]) + int(line_[3])
1119                else:
1120                    self.diagnostics.Corrected_Reads = int(line_[4])
1121                self.diagnostics._Reads_GB = float(line_[6].replace(',', '.'))
1122                self.diagnostics._Uncorrected_Reads = int(line_[7])
1123                continue
1124
1125            if 'write:' in line:
1126                line_ = ' '.join(line.split()).split(' ')
1127                if (line_[1] == '0' and line_[2] == '0' and
1128                        line_[3] == '0' and line_[4] == '0'):
1129                    self.diagnostics.Corrected_Writes = 0
1130                elif line_[4] == '0':
1131                    self.diagnostics.Corrected_Writes = int(
1132                        line_[1]) + int(line_[2]) + int(line_[3])
1133                else:
1134                    self.diagnostics.Corrected_Writes = int(line_[4])
1135                self.diagnostics._Writes_GB = float(line_[6].replace(',', '.'))
1136                self.diagnostics._Uncorrected_Writes = int(line_[7])
1137                continue
1138
1139            if 'verify:' in line:
1140                line_ = ' '.join(line.split()).split(' ')
1141                if (line_[1] == '0' and line_[2] == '0' and
1142                        line_[3] == '0' and line_[4] == '0'):
1143                    self.diagnostics.Corrected_Verifies = 0
1144                elif line_[4] == '0':
1145                    self.diagnostics.Corrected_Verifies = int(
1146                        line_[1]) + int(line_[2]) + int(line_[3])
1147                else:
1148                    self.diagnostics.Corrected_Verifies = int(line_[4])
1149                self.diagnostics._Verifies_GB = float(
1150                    line_[6].replace(',', '.'))
1151                self.diagnostics._Uncorrected_Verifies = int(line_[7])
1152                continue
1153
1154            if 'non-medium error count' in line:
1155                self.diagnostics.Non_Medium_Errors = int(
1156                    line.split(':')[1].strip())
1157                continue
1158
1159            if 'Accumulated power on time' in line:
1160                self.diagnostics.Power_On_Hours = int(
1161                    line.split(':')[1].split(' ')[1])
1162                continue
1163
1164            if 'Current Drive Temperature' in line or ('Temperature:' in
1165                                                       line and interface == 'nvme'):
1166                try:
1167                    self.temperature = int(
1168                        line.split(':')[-1].strip().split()[0])
1169
1170                    if 'fahrenheit' in line.lower():
1171                        self.temperature = int((self.temperature - 32) * 5 / 9)
1172
1173                except ValueError:
1174                    pass
1175
1176                continue
1177
1178            if 'Temperature Sensor ' in line:
1179                try:
1180                    match = re.search(
1181                        r'Temperature\sSensor\s([0-9]+):\s+(-?[0-9]+)', line)
1182                    if match:
1183                        (tempsensor_number_s, tempsensor_value_s) = match.group(1, 2)
1184                        tempsensor_number = int(tempsensor_number_s)
1185                        tempsensor_value = int(tempsensor_value_s)
1186
1187                        if 'fahrenheit' in line.lower():
1188                            tempsensor_value = int(
1189                                (tempsensor_value - 32) * 5 / 9)
1190
1191                        self.temperatures[tempsensor_number] = tempsensor_value
1192                        if self.temperature is None or tempsensor_number == 0:
1193                            self.temperature = tempsensor_value
1194                except ValueError:
1195                    pass
1196
1197                continue
1198
1199            #######################################
1200            #            Common values            #
1201            #######################################
1202
1203            # Sector sizes
1204            if 'Sector Sizes' in line:  # ATA
1205                m = re.match(
1206                    r'.* (\d+) bytes logical,\s*(\d+) bytes physical', line)
1207                if m:
1208                    self.logical_sector_size = int(m.group(1))
1209                    self.physical_sector_size = int(m.group(2))
1210                    # set diagnostics block size to physical sector size
1211                    self.diagnostics._block_size = self.physical_sector_size
1212                continue
1213            if 'Logical block size:' in line:  # SCSI 1/2
1214                self.logical_sector_size = int(
1215                    line.split(':')[1].strip().split(' ')[0])
1216                # set diagnostics block size to logical sector size
1217                self.diagnostics._block_size = self.logical_sector_size
1218                continue
1219            if 'Physical block size:' in line:  # SCSI 2/2
1220                self.physical_sector_size = int(
1221                    line.split(':')[1].strip().split(' ')[0])
1222                continue
1223            if 'Namespace 1 Formatted LBA Size' in line:  # NVMe
1224                # Note: we will assume that there is only one namespace
1225                self.logical_sector_size = int(
1226                    line.split(':')[1].strip().split(' ')[0])
1227                continue
1228
1229        if not self.abridged:
1230            if not interface == 'scsi':
1231                # Parse the SMART table for below-threshold attributes and create
1232                # corresponding warnings for non-SCSI disks
1233                self._make_smart_warnings()
1234            else:
1235                # If not obtained Power_On_Hours above, make a direct attempt to extract power on
1236                # hours from the background scan results log.
1237                if self.diagnostics.Power_On_Hours is None:
1238                    raw, returncode = self.smartctl.generic_call(
1239                        [
1240                            '-d',
1241                            'scsi',
1242                            '-l',
1243                            'background',
1244                            self.dev_reference
1245                        ])
1246
1247                    for line in raw:
1248                        if 'power on time' in line:
1249                            self.diagnostics.Power_On_Hours = int(
1250                                line.split(':')[1].split(' ')[1])
1251        # map temperature
1252        if self.temperature is None:
1253            # in this case the disk is probably ata
1254            try:
1255                # Some disks report temperature to attribute number 190 ('Airflow_Temperature_Cel')
1256                # see https://bugs.freenas.org/issues/20860
1257                temp_attr = self.attributes[194] or self.attributes[190]
1258                self.temperature = int(temp_attr.raw)
1259            except (ValueError, AttributeError):
1260                pass
1261        # Now that we have finished the update routine, if we did not find a runnning selftest
1262        # nuke the self._test_ECD and self._test_progress
1263        if self._test_running is False:
1264            self._test_ECD = None
1265            self._test_progress = None

Queries for device information using smartctl and updates all class members, including the SMART attribute table and self-test log. Can be called at any time to refresh the pySMART.Device object's data content.

def smart_health_assement( disk_name, smartctl: pySMART.smartctl.Smartctl = <pySMART.smartctl.Smartctl object>):
49def smart_health_assement(disk_name, smartctl: Smartctl = SMARTCTL):
50    """
51    This function gets the SMART Health Status of the disk (IF the disk
52    is SMART capable and smart is enabled on it else returns None).
53    This function is to be used only in abridged mode and not otherwise,
54    since in non-abridged mode update gets this information anyways.
55    """
56    assessment = None
57    raw = smartctl.health(os.path.join(
58        '/dev/', disk_name.replace('nvd', 'nvme')))
59    line = raw[4]  # We only need this line
60    if 'SMART overall-health self-assessment' in line:  # ATA devices
61        if line.split(':')[1].strip() == 'PASSED':
62            assessment = 'PASS'
63        else:
64            assessment = 'FAIL'
65    if 'SMART Health Status' in line:  # SCSI devices
66        if line.split(':')[1].strip() == 'OK':
67            assessment = 'PASS'
68        else:
69            assessment = 'FAIL'
70    return assessment

This function gets the SMART Health Status of the disk (IF the disk is SMART capable and smart is enabled on it else returns None). This function is to be used only in abridged mode and not otherwise, since in non-abridged mode update gets this information anyways.