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

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

nsid

(str): NVMe 'Name Space Identifier' reported on self-test failure. Set to '-' if no namespace is defined.

sct

(str): NVMe 'Status Code Type' reported on self-test failure. Set to '-' if undefined.

code

(str): NVMe 'Status Code' reported on self-test failure. Set to '-' if undefined.

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

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)
22    def __init__(self, num: int, name, flags: int, value, worst, thresh, attr_type, updated, when_failed, raw):
23        self.num: int = num
24        """**(int):** Attribute's ID as a decimal value (1-255)."""
25        self.name: str = name
26        """
27        **(str):** Attribute's name, as reported by smartmontools' drive.db.
28        """
29        self.flags: int = flags
30        """**(int):** Attribute flags as a bit value (ie: 0x0032)."""
31        self._value: str = value
32        """**(str):** Attribute's current normalized value."""
33        self._worst: str = worst
34        """**(str):** Worst recorded normalized value for this attribute."""
35        self._thresh: str = thresh
36        """**(str):** Attribute's failure threshold."""
37        self.type: str = attr_type
38        """**(str):** Attribute's type, generally 'pre-fail' or 'old-age'."""
39        self.updated: str = updated
40        """
41        **(str):** When is this attribute updated? Generally 'Always' or
42        'Offline'
43        """
44        self.when_failed: str = when_failed
45        """
46        **(str):** When did this attribute cross below
47        `pySMART.attribute.Attribute.thresh`? Reads '-' when not failed.
48        Generally either 'FAILING_NOW' or 'In_the_Past' otherwise.
49        """
50        self.raw = raw
51        """**(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.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
53    @property
54    def value_str(self) -> str:
55        """Gets the attribute value
56
57        Returns:
58            str: The attribute value in string format
59        """
60        return self._value

Gets the attribute value

Returns: str: The attribute value in string format

value_int: int
62    @property
63    def value_int(self) -> int:
64        """Gets the attribute value
65
66        Returns:
67            int: The attribute value in integer format.
68        """
69        return int(self._value)

Gets the attribute value

Returns: int: The attribute value in integer format.

value: str
71    @property
72    def value(self) -> str:
73        """Gets the attribue value
74
75        Returns:
76            str: The attribute value in string format
77        """
78        return self.value_str

Gets the attribue value

Returns: str: The attribute value in string format

worst: int
80    @property
81    def worst(self) -> int:
82        """Gets the worst value
83
84        Returns:
85            int: The attribute worst field in integer format
86        """
87        return int(self._worst)

Gets the worst value

Returns: int: The attribute worst field in integer format

thresh: Optional[int]
89    @property
90    def thresh(self) -> Optional[int]:
91        """Gets the threshold value
92
93        Returns:
94            int: The attribute threshold field in integer format
95        """
96        return None if self._thresh == '---' else int(self._thresh)

Gets the threshold value

Returns: int: The attribute threshold field in integer format

raw_int: Optional[int]
 98    @property
 99    def raw_int(self) -> Optional[int]:
100        """Gets the raw value converted to int
101        NOTE: Some values may not be correctly converted!
102
103        Returns:
104            int: The attribute raw-value field in integer format.
105            None: In case the raw string failed to be parsed
106        """
107        try:
108            return int(re.search(r'\d+', self.raw).group())
109        except:
110            return None

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

Instantiates and optionally initializes the DeviceList.

Args: init (bool, optional): By default, pySMART.device_list.DeviceList.devices is populated with Device objects during instantiation. Setting init to False will skip initialization and create an empty pySMART.device_list.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. catch_errors (bool, optional): If True, individual device-parsing errors will be caught

devices: List[Device]

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

smartctl: pySMART.smartctl.Smartctl

The smartctl wrapper

def initialize(self, catch_errors: bool = False):
 90    def initialize(self, catch_errors: bool = False):
 91        """
 92        Scans system busses for attached devices and add them to the
 93        `DeviceList` as `Device` objects.
 94        If device list is already populated, it will be cleared first.
 95
 96        Args:
 97            catch_errors (bool, optional): If True, individual device-parsing errors will be caught
 98        """
 99
100        # Clear the list if it's already populated
101        if len(self.devices):
102            self.devices = []
103
104        # Scan for devices
105        for line in self.smartctl.scan():
106            if not ('failed:' in line or line == ''):
107                groups = re.compile(
108                    r'^(\S+)\s+-d\s+(\S+)').match(line).groups()
109                name = groups[0]
110                interface = groups[1]
111
112                try:
113                    # Add the device to the list
114                    self.devices.append(
115                        Device(name, interface=interface, smartctl=self.smartctl))
116
117                except Exception as e:
118                    if catch_errors:
119                        # Print the exception
120                        import logging
121
122                        logging.exception(f"Error parsing device {name}")
123
124                    else:
125                        # Reraise the exception
126                        raise e
127
128        # Remove duplicates and unwanted devices (optical, etc.) from the list
129        self._cleanup()
130        # Sort the list alphabetically by device name
131        self.devices.sort(key=lambda device: device.name)

Scans system busses for attached devices and add them to the DeviceList as Device objects. If device list is already populated, it will be cleared first.

Args: catch_errors (bool, optional): If True, individual device-parsing errors will be caught

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

Instantiates and initializes the pySMART.device.Device.

abridged
smartctl
name: str

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

family: Optional[str]

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

model: Optional[str]

(str): Device's model number (if any).

serial: Optional[str]

(str): Device's serial number (if any).

firmware: Optional[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: Optional[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: Optional[int]

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

test_capabilities

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

test_polling_time

**(dict): ** This dictionary contains key == 'Test Name' and value == int of approximate times to run each test type that this device is capable of.

tests: List[TestEntry]

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

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: Optional[int]

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

physical_sector_size: Optional[int]

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

if_attributes: Union[NoneType, pySMART.interface.ata.AtaAttributes, pySMART.interface.nvme.NvmeAttributes, pySMART.interface.scsi.SCSIAttributes]

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

attributes: List[Optional[Attribute]]
287    @property
288    def attributes(self) -> List[Optional[Attribute]]:
289        """Returns the SMART attributes of the device.
290        Note: This is only filled with ATA/SATA attributes. SCSI/SAS/NVMe devices will have empty lists!!
291        @deprecated: Use `if_attributes` instead.
292
293        Returns:
294            list of `Attribute`: The SMART attributes of the device.
295        """
296        if self.if_attributes is None or not isinstance(self.if_attributes, AtaAttributes):
297            return [None] * 256
298        else:
299            return self.if_attributes.legacyAttributes

Returns the SMART attributes of the device. Note: This is only filled with ATA/SATA attributes. SCSI/SAS/NVMe devices will have empty lists!! @deprecated: Use if_attributes instead.

Returns: list of Attribute: The SMART attributes of the device.

dev_interface: Optional[str]
301    @property
302    def dev_interface(self) -> Optional[str]:
303        """Returns the internal interface type of the device.
304           It may not be the same as the interface type as used by smartctl.
305
306        Returns:
307            str: The interface type of the device. (example: ata, scsi, nvme)
308                 None if the interface type could not be determined.
309        """
310        # Try to get the fine-tuned interface type
311        fineType = self._classify()
312
313        # If return still contains a megaraid, just asume it's type
314        if 'megaraid' in fineType:
315            # If any attributes is not None and has at least non None value, then it is a sat+megaraid device
316            if isinstance(self.if_attributes, AtaAttributes):
317                return 'ata'
318            else:
319                return 'sas'
320
321        return fineType

Returns the internal interface type of the device. It may not be the same as the interface type as used by smartctl.

Returns: str: The interface type of the device. (example: ata, scsi, nvme) None if the interface type could not be determined.

temperature: Optional[int]
323    @property
324    def temperature(self) -> Optional[int]:
325        """Returns the temperature of the device.
326
327        Returns:
328            int: The temperature of the device in Celsius.
329                 None if the temperature could not be determined.
330        """
331        if self.if_attributes is None:
332            return self._temperature
333        else:
334            return self.if_attributes.temperature or self._temperature

Returns the temperature of the device.

Returns: int: The temperature of the device in Celsius. None if the temperature could not be determined.

smartctl_interface: Optional[str]
336    @property
337    def smartctl_interface(self) -> Optional[str]:
338        """Returns the interface type of the device as it is used in smartctl.
339
340        Returns:
341            str: The interface type of the device. (example: ata, scsi, nvme)
342                 None if the interface type could not be determined.
343        """
344        return self._interface

Returns the interface type of the device as it is used in smartctl.

Returns: str: The interface type of the device. (example: ata, scsi, nvme) None if the interface type could not be determined.

interface: Optional[str]
346    @property
347    def interface(self) -> Optional[str]:
348        """Returns the interface type of the device as it is used in smartctl.
349
350        Returns:
351            str: The interface type of the device. (example: ata, scsi, nvme)
352                 None if the interface type could not be determined.
353        """
354        return self.smartctl_interface

Returns the interface type of the device as it is used in smartctl.

Returns: str: The interface type of the device. (example: ata, scsi, nvme) None if the interface type could not be determined.

dev_reference: str
356    @property
357    def dev_reference(self) -> str:
358        """The reference to the device as provided by smartctl.
359           - On unix-like systems, this is the path to the device. (example /dev/<name>)
360           - On MacOS, this is the name of the device. (example <name>)
361           - On Windows, this is the drive letter of the device. (example <drive letter>)
362
363        Returns:
364            str: The reference to the device as provided by smartctl.
365        """
366
367        # detect if we are on MacOS
368        if 'IOService' in self.name:
369            return self.name
370
371        # otherwise asume we are on unix-like systems
372        return os.path.join('/dev/', self.name)

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.

vendor: Optional[str]
374    @property
375    def vendor(self) -> Optional[str]:
376        """Returns the vendor of the device.
377
378        Returns:
379            str: The vendor of the device.
380        """
381        if self._vendor:
382            return self._vendor
383
384        # If family is present, try to stract from family. Skip anything but letters.
385        elif self.family:
386            filter = re.search(r'^[a-zA-Z]+', self.family.strip())
387            if filter:
388                return filter.group(0)
389
390        # If model is present, try to stract from model. Skip anything but letters.
391        elif self.model:
392            filter = re.search(r'^[a-zA-Z]+', self.model.strip())
393            if filter:
394                return filter.group(0)
395
396        # If all else fails, return None
397        return None

Returns the vendor of the device.

Returns: str: The vendor of the device.

capacity: Optional[str]
399    @property
400    def capacity(self) -> Optional[str]:
401        """Returns the capacity in the raw smartctl format.
402        This may be deprecated in the future and its only retained for compatibility.
403
404        Returns:
405            str: The capacity in the raw smartctl format
406        """
407        return self._capacity_human

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

diagnostics: Optional[pySMART.interface.scsi.diagnostics.Diagnostics]
409    @property
410    def diagnostics(self) -> Optional[Diagnostics]:
411        """Gets the old/deprecated version of SCSI/SAS diagnostics atribute.
412        """
413        if self.if_attributes is None or not isinstance(self.if_attributes, SCSIAttributes):
414            return None
415
416        else:
417            return self.if_attributes.diagnostics

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

diags: Dict[str, str]
419    @property
420    def diags(self) -> Dict[str, str]:
421        """Gets the old/deprecated version of SCSI/SAS diags atribute.
422        """
423        if self.if_attributes is None or not isinstance(self.if_attributes, SCSIAttributes):
424            # Return an empty dict if the device is not SCSI/SAS
425            return {}
426
427        else:
428            return self.if_attributes.diagnostics.get_classic_format()

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

size_raw: Optional[str]
430    @property
431    def size_raw(self) -> Optional[str]:
432        """Returns the capacity in the raw smartctl format.
433
434        Returns:
435            str: The capacity in the raw smartctl format
436        """
437        return self._capacity_human

Returns the capacity in the raw smartctl format.

Returns: str: The capacity in the raw smartctl format

size: int
439    @property
440    def size(self) -> int:
441        """Returns the capacity in bytes
442
443        Returns:
444            int: The capacity in bytes
445        """
446        import humanfriendly
447
448        if self._capacity is not None:
449            return self._capacity
450        elif self._capacity_human is not None:
451            return humanfriendly.parse_size(self._capacity_human)
452        else:
453            return 0

Returns the capacity in bytes

Returns: int: The capacity in bytes

sector_size: int
455    @property
456    def sector_size(self) -> int:
457        """Returns the sector size of the device.
458
459        Returns:
460            int: The sector size of the device in Bytes. If undefined, we'll assume 512B
461        """
462        if self.logical_sector_size is not None:
463            return self.logical_sector_size
464        elif self.physical_sector_size is not None:
465            return self.physical_sector_size
466        else:
467            return 512

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]]:
516    def smart_toggle(self, action: str) -> Tuple[bool, List[str]]:
517        """
518        A basic function to enable/disable SMART on device.
519
520        # Args:
521        * **action (str):** Can be either 'on'(for enabling) or 'off'(for disabling).
522
523        # Returns"
524        * **(bool):** Return True (if action succeded) else False
525        * **(List[str]):** None if option succeded else contains the error message.
526        """
527        # Lets make the action verb all lower case
528        if self._interface == 'nvme':
529            return False, ['NVME devices do not currently support toggling SMART enabled']
530        action_lower = action.lower()
531        if action_lower not in ['on', 'off']:
532            return False, ['Unsupported action {0}'.format(action)]
533        # Now lets check if the device's smart enabled status is already that of what
534        # the supplied action is intending it to be. If so then just return successfully
535        if self.smart_enabled:
536            if action_lower == 'on':
537                return True, []
538        else:
539            if action_lower == 'off':
540                return True, []
541        if self._interface is not None:
542            raw, returncode = self.smartctl.generic_call(
543                ['-s', action_lower, '-d', self._interface, self.dev_reference])
544        else:
545            raw, returncode = self.smartctl.generic_call(
546                ['-s', action_lower, self.dev_reference])
547
548        if returncode != 0:
549            return False, raw
550        # if everything worked out so far lets perform an update() and check the result
551        self.update()
552        if action_lower == 'off' and self.smart_enabled:
553            return False, ['Failed to turn SMART off.']
554        if action_lower == 'on' and not self.smart_enabled:
555            return False, ['Failed to turn SMART on.']
556        return True, []

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>):
558    def all_attributes(self, print_fn=print):
559        """
560        Prints the entire SMART attribute table, in a format similar to
561        the output of smartctl.
562        allows usage of custom print function via parameter print_fn by default uses print
563        """
564        header_printed = False
565        for attr in self.attributes:
566            if attr is not None:
567                if not header_printed:
568                    print_fn("{0:>3} {1:24}{2:4}{3:4}{4:4}{5:9}{6:8}{7:12}{8}"
569                             .format('ID#', 'ATTRIBUTE_NAME', 'CUR', 'WST', 'THR', 'TYPE', 'UPDATED', 'WHEN_FAIL',
570                                     'RAW'))
571                    header_printed = True
572                print_fn(attr)
573        if not header_printed:
574            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):
576    def all_selftests(self):
577        """
578        Prints the entire SMART self-test log, in a format similar to
579        the output of smartctl.
580        """
581        if self.tests:
582            all_tests = []
583            if smartctl_type(self._interface) == 'scsi':
584                header = "{0:3}{1:17}{2:23}{3:7}{4:14}{5:15}".format(
585                    'ID',
586                    'Test Description',
587                    'Status',
588                    'Hours',
589                    '1st_Error@LBA',
590                    '[SK  ASC  ASCQ]'
591                )
592            else:
593                header = ("{0:3}{1:17}{2:30}{3:5}{4:7}{5:17}".format(
594                    'ID',
595                    'Test_Description',
596                    'Status',
597                    'Left',
598                    'Hours',
599                    '1st_Error@LBA'))
600            all_tests.append(header)
601            for test in self.tests:
602                all_tests.append(str(test))
603
604            return all_tests
605        else:
606            no_tests = 'No self-tests have been logged for this device.'
607            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):
727    def get_selftest_result(self, output=None):
728        """
729        Refreshes a device's `pySMART.device.Device.tests` attribute to obtain
730        the latest test results. If a new test result is obtained, its content
731        is returned.
732
733        # Args:
734        * **output (str, optional):** If set to 'str', the string
735        representation of the most recent test result will be returned, instead
736        of a `Test_Entry` object.
737
738        # Returns:
739        * **(int):** Return status code. One of the following:
740            * 0 - Success. Object (or optionally, string rep) is attached.
741            * 1 - Self-test in progress. Must wait for it to finish.
742            * 2 - No new test results.
743            * 3 - The Self-test was Aborted by host
744        * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or
745        optionally it's string representation) if new data exists.  Status
746        message string on failure.
747        * **(int):** Estimate progress percantage of the running SMART selftest, if known.
748        Otherwise 'None'.
749        """
750        # SCSI self-test logs hold 20 entries while ATA logs hold 21
751        if smartctl_type(self._interface) == 'scsi':
752            maxlog = 20
753        else:
754            maxlog = 21
755        # If we looked only at the most recent test result we could be fooled
756        # by two short tests run close together (within the same hour)
757        # appearing identical. Comparing the length of the log adds some
758        # confidence until it maxes, as above. Comparing the least-recent test
759        # result greatly diminishes the chances that two sets of two tests each
760        # were run within an hour of themselves, but with 16-17 other tests run
761        # in between them.
762        if self.tests:
763            _first_entry = self.tests[0]
764            _len = len(self.tests)
765            _last_entry = self.tests[_len - 1]
766        else:
767            _len = 0
768        self.update()
769        # Since I have changed the update() parsing to DTRT to pickup currently
770        # running selftests we can now purely rely on that for self._test_running
771        # Thus check for that variable first and return if it is True with appropos message.
772        if self._test_running is True:
773            return 1, 'Self-test in progress. Please wait.', self._test_progress
774        # Check whether the list got longer (ie: new entry)
775        # If so return the newest test result
776        # If not, because it's max size already, check for new entries
777        if (
778                (len(self.tests) != _len) or
779                (
780                    _len == maxlog and
781                    (
782                        _first_entry.type != self.tests[0].type or
783                        _first_entry.hours != self.tests[0].hours or
784                        _last_entry.type != self.tests[len(self.tests) - 1].type or
785                        _last_entry.hours != self.tests[len(
786                            self.tests) - 1].hours
787                    )
788                )
789        ):
790            return (
791                0 if 'Aborted' not in self.tests[0].status else 3,
792                str(self.tests[0]) if output == 'str' else self.tests[0],
793                None
794            )
795        else:
796            return 2, 'No new self-test results found.', None

Refreshes a device's pySMART.device.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):
798    def abort_selftest(self):
799        """
800        Aborts non-captive SMART Self Tests.   Note that this command
801        will  abort the Offline Immediate Test routine only if your disk
802        has the "Abort Offline collection upon new command"  capability.
803
804        # Args: Nothing (just aborts directly)
805
806        # Returns:
807        * **(int):** The returncode of calling `smartctl -X device_path`
808        """
809        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'):
811    def run_selftest(self, test_type, ETA_type='date'):
812        """
813        Instructs a device to begin a SMART self-test. All tests are run in
814        'offline' / 'background' mode, allowing normal use of the device while
815        it is being tested.
816
817        # Args:
818        * **test_type (str):** The type of test to run. Accepts the following
819        (not case sensitive):
820            * **short** - Brief electo-mechanical functionality check.
821            Generally takes 2 minutes or less.
822            * **long** - Thorough electro-mechanical functionality check,
823            including complete recording media scan. Generally takes several
824            hours.
825            * **conveyance** - Brief test used to identify damage incurred in
826            shipping. Generally takes 5 minutes or less. **This test is not
827            supported by SAS or SCSI devices.**
828            * **offline** - Runs SMART Immediate Offline Test. The effects of
829            this test are visible only in that it updates the SMART Attribute
830            values, and if errors are found they will appear in the SMART error
831            log, visible with the '-l error' option to smartctl. **This test is
832            not supported by SAS or SCSI devices in pySMART use cli smartctl for
833            running 'offline' selftest (runs in foreground) on scsi devices.**
834            * **ETA_type** - Format to return the estimated completion time/date
835            in. Default is 'date'. One could otherwise specidy 'seconds'.
836            Again only for ATA devices.
837
838        # Returns:
839        * **(int):** Return status code.  One of the following:
840            * 0 - Self-test initiated successfully
841            * 1 - Previous self-test running. Must wait for it to finish.
842            * 2 - Unknown or unsupported (by the device) test type requested.
843            * 3 - Unspecified smartctl error. Self-test not initiated.
844        * **(str):** Return status message.
845        * **(str)/(float):** Estimated self-test completion time if a test is started.
846        The optional argument of 'ETA_type' (see above) controls the return type.
847        if 'ETA_type' == 'date' then a date string is returned else seconds(float)
848        is returned.
849        Note: The self-test completion time can only be obtained for ata devices.
850        Otherwise 'None'.
851        """
852        # Lets call get_selftest_result() here since it does an update() and
853        # checks for an existing selftest is running or not, this way the user
854        # can issue a test from the cli and this can still pick that up
855        # Also note that we do not need to obtain the results from this as the
856        # data is already stored in the Device class object's variables
857        self.get_selftest_result()
858        if self._test_running:
859            return 1, 'Self-test in progress. Please wait.', self._test_ECD
860        test_type = test_type.lower()
861        interface = smartctl_type(self._interface)
862        try:
863            if not self.test_capabilities[test_type]:
864                return (
865                    2,
866                    "Device {0} does not support the '{1}' test ".format(
867                        self.name, test_type),
868                    None
869                )
870        except KeyError:
871            return 2, "Unknown test type '{0}' requested.".format(test_type), None
872
873        raw, rc = self.smartctl.test_start(
874            interface, test_type, self.dev_reference)
875        _success = False
876        _running = False
877        for line in raw:
878            if 'has begun' in line:
879                _success = True
880                self._test_running = True
881            if 'aborting current test' in line:
882                _running = True
883                try:
884                    self._test_progress = 100 - \
885                        int(line.split('(')[-1].split('%')[0])
886                except ValueError:
887                    pass
888
889            if _success and 'complete after' in line:
890                self._test_ECD = line[25:].rstrip()
891                if ETA_type == 'seconds':
892                    self._test_ECD = mktime(
893                        strptime(self._test_ECD, '%a %b %d %H:%M:%S %Y')) - time()
894                self._test_progress = 0
895        if _success:
896            return 0, 'Self-test started successfully', self._test_ECD
897        else:
898            if _running:
899                return 1, 'Self-test already in progress. Please wait.', self._test_ECD
900            else:
901                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):
903    def run_selftest_and_wait(self, test_type, output=None, polling=5, progress_handler=None):
904        """
905        This is essentially a wrapper around run_selftest() such that we
906        call self.run_selftest() and wait on the running selftest till
907        it finished before returning.
908        The above holds true for all pySMART supported tests with the
909        exception of the 'offline' test (ATA only) as it immediately
910        returns, since the entire test only affects the smart error log
911        (if any errors found) and updates the SMART attributes. Other
912        than that it is not visibile anywhere else, so we start it and
913        simply return.
914        # Args:
915        * **test_type (str):** The type of test to run. Accepts the following
916        (not case sensitive):
917            * **short** - Brief electo-mechanical functionality check.
918            Generally takes 2 minutes or less.
919            * **long** - Thorough electro-mechanical functionality check,
920            including complete recording media scan. Generally takes several
921            hours.
922            * **conveyance** - Brief test used to identify damage incurred in
923            shipping. Generally takes 5 minutes or less. **This test is not
924            supported by SAS or SCSI devices.**
925            * **offline** - Runs SMART Immediate Offline Test. The effects of
926            this test are visible only in that it updates the SMART Attribute
927            values, and if errors are found they will appear in the SMART error
928            log, visible with the '-l error' option to smartctl. **This test is
929            not supported by SAS or SCSI devices in pySMART use cli smartctl for
930            running 'offline' selftest (runs in foreground) on scsi devices.**
931        * **output (str, optional):** If set to 'str', the string
932            representation of the most recent test result will be returned,
933            instead of a `Test_Entry` object.
934        * **polling (int, default=5):** The time duration to sleep for between
935            checking for test_results and progress.
936        * **progress_handler (function, optional):** This if provided is called
937            with self._test_progress as the supplied argument everytime a poll to
938            check the status of the selftest is done.
939        # Returns:
940        * **(int):** Return status code.  One of the following:
941            * 0 - Self-test executed and finished successfully
942            * 1 - Previous self-test running. Must wait for it to finish.
943            * 2 - Unknown or illegal test type requested.
944            * 3 - The Self-test was Aborted by host
945            * 4 - Unspecified smartctl error. Self-test not initiated.
946        * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or
947        optionally it's string representation) if new data exists.  Status
948        message string on failure.
949        """
950        test_initiation_result = self.run_selftest(test_type)
951        if test_initiation_result[0] != 0:
952            return test_initiation_result[:2]
953        if test_type == 'offline':
954            self._test_running = False
955        # if not then the test initiated correctly and we can start the polling.
956        # For now default 'polling' value is 5 seconds if not specified by the user
957
958        # Do an initial check, for good measure.
959        # In the probably impossible case that self._test_running is instantly False...
960        selftest_results = self.get_selftest_result(output=output)
961        while self._test_running:
962            if selftest_results[0] != 1:
963                # the selftest is run and finished lets return with the results
964                break
965            # Otherwise see if we are provided with the progress_handler to update progress
966            if progress_handler is not None:
967                progress_handler(
968                    selftest_results[2] if selftest_results[2] is not None else 50)
969            # Now sleep 'polling' seconds before checking the progress again
970            sleep(polling)
971
972            # Check after the sleep to ensure we return the right result, and not an old one.
973            selftest_results = self.get_selftest_result(output=output)
974
975        # Now if (selftes_results[0] == 2) i.e No new selftest (because the same
976        # selftest was run twice within the last hour) but we know for a fact that
977        # we just ran a new selftest then just return the latest entry in self.tests
978        if selftest_results[0] == 2:
979            selftest_return_value = 0 if 'Aborted' not in self.tests[0].status else 3
980            return selftest_return_value, str(self.tests[0]) if output == 'str' else self.tests[0]
981        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):
 983    def update(self):
 984        """
 985        Queries for device information using smartctl and updates all
 986        class members, including the SMART attribute table and self-test log.
 987        Can be called at any time to refresh the `pySMART.device.Device`
 988        object's data content.
 989        """
 990        # set temperature back to None so that if update() is called more than once
 991        # any logic that relies on self.temperature to be None to rescan it works.it
 992        self._temperature = None
 993        # same for temperatures
 994        self.temperatures = {}
 995        if self.abridged:
 996            interface = None
 997            raw = self.smartctl.info(self.dev_reference)
 998
 999        else:
1000            interface = smartctl_type(self._interface)
1001            raw = self.smartctl.all(
1002                self.dev_reference, interface)
1003
1004        canonical_interface = self.dev_interface
1005        if canonical_interface == 'nvme':
1006            self.smart_capable = True
1007            self.smart_enabled = True
1008            self.is_ssd = True
1009
1010        parse_self_tests = False
1011        parse_running_test = False
1012        parse_ascq = False
1013        polling_minute_type = None
1014        message = ''
1015        self.tests = []
1016        self._test_running = False
1017        self._test_progress = None
1018        # Lets skip the first couple of non-useful lines
1019        _stdout = raw[4:]
1020
1021        #######################################
1022        #           Encoding fixing           #
1023        #######################################
1024        # In some scenarios, smartctl returns some lines with a different/strange encoding
1025        # This is a workaround to fix that
1026        for i, line in enumerate(_stdout):
1027            # character ' ' (U+202F) should be removed
1028            _stdout[i] = line.replace('\u202f', '')
1029
1030        #######################################
1031        #   Dedicated interface attributes    #
1032        #######################################
1033        if AtaAttributes.has_compatible_data(iter(_stdout)):
1034            self.if_attributes = AtaAttributes(iter(_stdout))
1035
1036        elif self.dev_interface == 'nvme':
1037            self.if_attributes = NvmeAttributes(iter(_stdout))
1038
1039            # Get Tests
1040            for test in self.if_attributes.tests:
1041                self.tests.append(TestEntry('nvme', test.num, test.description, test.status, test.powerOnHours,
1042                                  test.failingLBA, nsid=test.nsid, segment=test.seg, sct=test.sct, code=test.code, remain=100-test.progress))
1043
1044            # Set running test
1045            if any(test.status == 'Running' for test in self.if_attributes.tests):
1046                self._test_running = True
1047                self._test_progress = self.if_attributes.tests[0].progress
1048            else:
1049                self._test_running = False
1050                self._test_progress = None
1051
1052        elif SCSIAttributes.has_compatible_data(iter(_stdout)):
1053            self.__get_smart_status(iter(_stdout))
1054            self.if_attributes = SCSIAttributes(iter(_stdout),
1055                                                abridged=self.abridged,
1056                                                smartEnabled=self.smart_enabled,
1057                                                sm=self.smartctl,
1058                                                dev_reference=self.dev_reference)
1059
1060            # Import (for now) the tests from if_attributes
1061            self.tests = self.if_attributes.tests
1062
1063        else:
1064            self.if_attributes = None
1065
1066        #######################################
1067        #    Global / generic  attributes     #
1068        #######################################
1069        stdout_iter = iter(_stdout)
1070        for line in stdout_iter:
1071            if line.strip() == '':  # Blank line stops sub-captures
1072                if parse_self_tests is True:
1073                    parse_self_tests = False
1074                if parse_ascq:
1075                    parse_ascq = False
1076                    self.messages.append(message)
1077            if parse_ascq:
1078                message += ' ' + line.lstrip().rstrip()
1079            if parse_self_tests:
1080                # Detect Test Format
1081
1082                ## SCSI/SAS FORMAT ##
1083                # Example smartctl output
1084                # SMART Self-test log
1085                # Num  Test              Status                 segment  LifeTime  LBA_first_err [SK ASC ASQ]
1086                #      Description                              number   (hours)
1087                # # 1  Background short  Completed                   -   33124                 - [-   -    -]
1088                format_scsi = re.compile(
1089                    r'^[#\s]*(\d+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s+\[([^\s]+)\s+([^\s]+)\s+([^\s]+)\]$').match(line)
1090
1091                ## ATA FORMAT ##
1092                # Example smartctl output:
1093                # SMART Self-test log structure revision number 1
1094                # Num  Test_Description    Status                  Remaining  LifeTime(hours)  LBA_of_first_error
1095                # # 1  Extended offline    Completed without error       00%     46660         -
1096                format_ata = re.compile(
1097                    r'^[#\s]*(\d+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{1,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])$').match(line)
1098
1099                if format_scsi is not None:
1100                    ## SCSI FORMAT ##
1101                    pass
1102
1103                elif format_ata is not None:
1104                    ## ATA FORMAT ##
1105                    format = 'ata'
1106                    parsed = format_ata.groups()
1107                    num = parsed[0]
1108                    test_type = parsed[1]
1109                    status = parsed[2]
1110                    remain = parsed[3]
1111                    hours = parsed[4]
1112                    lba = parsed[5]
1113
1114                    try:
1115                        num = int(num)
1116                    except:
1117                        num = None
1118
1119                    self.tests.append(
1120                        TestEntry(format, num, test_type, status,
1121                                  hours, lba, remain=remain)
1122                    )
1123                else:
1124                    pass
1125
1126            # Basic device information parsing
1127            if any_in(line, 'Device Model', 'Product', 'Model Number'):
1128                self.model = line.split(':')[1].lstrip().rstrip()
1129                self._guess_smart_type(line.lower())
1130                continue
1131
1132            if 'Model Family' in line:
1133                self.family = line.split(':')[1].strip()
1134                self._guess_smart_type(line.lower())
1135                continue
1136
1137            if 'LU WWN' in line:
1138                self._guess_smart_type(line.lower())
1139                continue
1140
1141            if any_in(line, 'Serial Number', 'Serial number'):
1142                try:
1143                    self.serial = line.split(':')[1].split()[0].rstrip()
1144                except IndexError:
1145                    # Serial reported empty
1146                    self.serial = ""
1147                continue
1148
1149            vendor = re.compile(r'^Vendor:\s+(\w+)').match(line)
1150            if vendor is not None:
1151                self._vendor = vendor.groups()[0]
1152
1153            if any_in(line, 'Firmware Version', 'Revision'):
1154                self.firmware = line.split(':')[1].strip()
1155
1156            if any_in(line, 'User Capacity', 'Total NVM Capacity', 'Namespace 1 Size/Capacity'):
1157                # TODO: support for multiple NVMe namespaces
1158                m = re.match(
1159                    r'.*:\s+([\d,. \u2019\u00a0]+)\s\D*\[?([^\]]+)?\]?', line.strip())
1160
1161                if m is not None:
1162                    tmp = m.groups()
1163                    if tmp[0] == ' ':
1164                        # This capacity is set to 0, skip it
1165                        continue
1166
1167                    self._capacity = int(
1168                        tmp[0].strip().replace(',', '').replace('.', '').replace(' ', '').replace('\u2019', '').replace('\u00a0', ''))
1169
1170                    if len(tmp) == 2 and tmp[1] is not None:
1171                        self._capacity_human = tmp[1].strip().replace(',', '.')
1172
1173            if 'SMART support' in line:
1174                # self.smart_capable = 'Available' in line
1175                # self.smart_enabled = 'Enabled' in line
1176                # Since this line repeats twice the above method is flawed
1177                # Lets try the following instead, it is a bit redundant but
1178                # more robust.
1179                if any_in(line, 'Unavailable', 'device lacks SMART capability'):
1180                    self.smart_capable = False
1181                    self.smart_enabled = False
1182                elif 'Enabled' in line:
1183                    self.smart_enabled = True
1184                elif 'Disabled' in line:
1185                    self.smart_enabled = False
1186                elif any_in(line, 'Available', 'device has SMART capability'):
1187                    self.smart_capable = True
1188                continue
1189
1190            if 'does not support SMART' in line:
1191                self.smart_capable = False
1192                self.smart_enabled = False
1193                continue
1194
1195            if 'Rotation Rate' in line:
1196                if 'Solid State Device' in line:
1197                    self.is_ssd = True
1198                elif 'rpm' in line:
1199                    self.is_ssd = False
1200                    try:
1201                        self.rotation_rate = int(
1202                            line.split(':')[1].lstrip().rstrip()[:-4])
1203                    except ValueError:
1204                        # Cannot parse the RPM? Assigning None instead
1205                        self.rotation_rate = None
1206                continue
1207
1208            if 'SMART overall-health self-assessment' in line:  # ATA devices
1209                if line.split(':')[1].strip() == 'PASSED':
1210                    self.assessment = 'PASS'
1211                else:
1212                    self.assessment = 'FAIL'
1213                continue
1214
1215            if 'SMART Health Status' in line:  # SCSI devices
1216                if line.split(':')[1].strip() == 'OK':
1217                    self.assessment = 'PASS'
1218                else:
1219                    self.assessment = 'FAIL'
1220                    parse_ascq = True  # Set flag to capture status message
1221                    message = line.split(':')[1].lstrip().rstrip()
1222                continue
1223
1224            # Parse SMART test capabilities (ATA only)
1225            # Note: SCSI does not list this but and allows for only 'offline', 'short' and 'long'
1226            if 'SMART execute Offline immediate' in line:
1227                self.test_capabilities['offline'] = 'No' not in line
1228                continue
1229
1230            if 'Conveyance Self-test supported' in line:
1231                self.test_capabilities['conveyance'] = 'No' not in line
1232                continue
1233
1234            if 'Selective Self-test supported' in line:
1235                self.test_capabilities['selective'] = 'No' not in line
1236                continue
1237
1238            if 'Self-test supported' in line:
1239                self.test_capabilities['short'] = 'No' not in line
1240                self.test_capabilities['long'] = 'No' not in line
1241                continue
1242
1243            # Parse SMART test capabilities (NVMe only)
1244            if 'Optional Admin Commands' in line:
1245                if 'Self_Test' in line:
1246                    self.test_capabilities['short'] = True
1247                    self.test_capabilities['long'] = True
1248
1249            if 'Short self-test routine' in line:
1250                polling_minute_type = 'short'
1251                continue
1252            if 'Extended self-test routine' in line:
1253                polling_minute_type = 'long'
1254                continue
1255            if 'Conveyance self-test routine' in line:
1256                polling_minute_type = 'conveyance'
1257                continue
1258            if 'recommended polling time:' in line:
1259                self.test_polling_time[polling_minute_type] = float(
1260                    re.sub("[^0-9]", "", line)
1261                )
1262                continue
1263
1264            # For some reason smartctl does not show a currently running test
1265            # for 'ATA' in the Test log so I just have to catch it this way i guess!
1266            # For 'scsi' I still do it since it is the only place I get % remaining in scsi
1267            if 'Self-test execution status' in line:
1268                if 'progress' in line:
1269                    self._test_running = True
1270                    # for ATA the "%" remaining is on the next line
1271                    # thus set the parse_running_test flag and move on
1272                    parse_running_test = True
1273                elif '%' in line:
1274                    # for scsi the progress is on the same line
1275                    # so we can just parse it and move on
1276                    self._test_running = True
1277                    try:
1278                        self._test_progress = 100 - \
1279                            int(line.split('%')[0][-3:].strip())
1280                    except ValueError:
1281                        pass
1282                continue
1283            if parse_running_test is True:
1284                try:
1285                    self._test_progress = 100 - \
1286                        int(line.split('%')[0][-3:].strip())
1287                except ValueError:
1288                    pass
1289                parse_running_test = False
1290
1291            if "Self-test log" in line:
1292                parse_self_tests = True  # Set flag to capture test entries
1293                continue
1294
1295            #######################################
1296            #              SCSI only              #
1297            #######################################
1298            #
1299            # Everything from here on is parsing SCSI information that takes
1300            # the place of similar ATA SMART information
1301
1302            if 'Current Drive Temperature' in line or ('Temperature:' in
1303                                                       line and interface == 'nvme'):
1304                try:
1305                    self._temperature = int(
1306                        line.split(':')[-1].strip().split()[0])
1307
1308                    if 'fahrenheit' in line.lower():
1309                        self._temperature = int(
1310                            (self.temperature - 32) * 5 / 9)
1311
1312                except ValueError:
1313                    pass
1314
1315                continue
1316
1317            if 'Temperature Sensor ' in line:
1318                try:
1319                    match = re.search(
1320                        r'Temperature\sSensor\s([0-9]+):\s+(-?[0-9]+)', line)
1321                    if match:
1322                        (tempsensor_number_s, tempsensor_value_s) = match.group(1, 2)
1323                        tempsensor_number = int(tempsensor_number_s)
1324                        tempsensor_value = int(tempsensor_value_s)
1325
1326                        if 'fahrenheit' in line.lower():
1327                            tempsensor_value = int(
1328                                (tempsensor_value - 32) * 5 / 9)
1329
1330                        self.temperatures[tempsensor_number] = tempsensor_value
1331                        if self.temperature is None or tempsensor_number == 0:
1332                            self._temperature = tempsensor_value
1333                except ValueError:
1334                    pass
1335
1336                continue
1337
1338            #######################################
1339            #            Common values            #
1340            #######################################
1341
1342            # Sector sizes
1343            if 'Sector Sizes' in line:  # ATA
1344                m = re.match(
1345                    r'.* (\d+) bytes logical,\s*(\d+) bytes physical', line)
1346                if m:
1347                    self.logical_sector_size = int(m.group(1))
1348                    self.physical_sector_size = int(m.group(2))
1349                continue
1350            if 'Logical block size:' in line:  # SCSI 1/2
1351                self.logical_sector_size = int(
1352                    line.split(':')[1].strip().split(' ')[0])
1353                continue
1354            if 'Physical block size:' in line:  # SCSI 2/2
1355                self.physical_sector_size = int(
1356                    line.split(':')[1].strip().split(' ')[0])
1357                continue
1358            if 'Namespace 1 Formatted LBA Size' in line:  # NVMe
1359                # Note: we will assume that there is only one namespace
1360                self.logical_sector_size = int(
1361                    line.split(':')[1].strip().split(' ')[0])
1362                continue
1363
1364        if not self.abridged:
1365            if not interface == 'scsi':
1366                # Parse the SMART table for below-threshold attributes and create
1367                # corresponding warnings for non-SCSI disks
1368                self._make_smart_warnings()
1369
1370        # Now that we have finished the update routine, if we did not find a runnning selftest
1371        # nuke the self._test_ECD and self._test_progress
1372        if self._test_running is False:
1373            self._test_ECD = None
1374            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.Device object's data content.

def smart_health_assement( disk_name: str, interface: Optional[str] = None, smartctl: pySMART.smartctl.Smartctl = <pySMART.smartctl.Smartctl object>) -> Optional[str]:
35def smart_health_assement(disk_name: str, interface: Optional[str] = None, smartctl: Smartctl = SMARTCTL) -> Optional[str]:
36    """
37    This function gets the SMART Health Status of the disk (IF the disk
38    is SMART capable and smart is enabled on it else returns None).
39    This function is to be used only in abridged mode and not otherwise,
40    since in non-abridged mode update gets this information anyways.
41
42    Args:
43        disk_name (str): name of the disk
44        interface (str, optional): interface type of the disk (e.g. 'sata', 'scsi', 'nvme',... Defaults to None.)
45
46    Returns:
47        str: SMART Health Status of the disk. Returns None if the disk is not SMART capable or smart is not enabled on it.
48             Possible values are 'PASS', 'FAIL' or None.
49    """
50    assessment = None
51    raw = smartctl.health(os.path.join(
52        '/dev/', disk_name.replace('nvd', 'nvme')), interface)
53    line = raw[4]  # We only need this line
54    if 'SMART overall-health self-assessment' in line:  # ATA devices
55        if line.split(':')[1].strip() == 'PASSED':
56            assessment = 'PASS'
57        else:
58            assessment = 'FAIL'
59    if 'SMART Health Status' in line:  # SCSI devices
60        if line.split(':')[1].strip() == 'OK':
61            assessment = 'PASS'
62        else:
63            assessment = 'FAIL'
64    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.

Args: disk_name (str): name of the disk interface (str, optional): interface type of the disk (e.g. 'sata', 'scsi', 'nvme',... Defaults to None.)

Returns: str: SMART Health Status of the disk. Returns None if the disk is not SMART capable or smart is not enabled on it. Possible values are 'PASS', 'FAIL' or None.