pySMART

Copyright (C) 2014 Marc Herndon

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

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

Usage

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

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

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

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

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

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

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

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

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

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

Acknowledgements

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

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

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

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

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

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

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

name: str

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

flags: int

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

type: str

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

updated: str

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

when_failed: str

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

raw

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

value_str: str

Gets the attribute value

Returns: str: The attribute value in string format

value_int: int

Gets the attribute value

Returns: int: The attribute value in integer format.

value: str

Gets the attribue value

Returns: str: The attribute value in string format

worst: int

Gets the worst value

Returns: int: The attribute worst field in integer format

thresh: Optional[int]

Gets the threshold value

Returns: int: The attribute threshold field in integer format

raw_int: Optional[int]

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

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

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

Instantiates and optionally initializes the DeviceList.

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

Instantiates and initializes the 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.

diagnostics: pySMART.diagnostics.Diagnostics

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

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.nvme.NvmeAttributes, pySMART.interface.ata.AtaAttributes]

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

attributes: List[Optional[Attribute]]

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]

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]

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]

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]

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

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]

Returns the vendor of the device.

Returns: str: The vendor of the device.

capacity: Optional[str]

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

Returns: str: The capacity in the raw smartctl format

diags: Dict[str, str]

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

size_raw: Optional[str]

Returns the capacity in the raw smartctl format.

Returns: str: The capacity in the raw smartctl format

size: int

Returns the capacity in bytes

Returns: int: The capacity in bytes

sector_size: int

Returns the sector size of the device.

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

def smart_toggle(self, action: str) -> Tuple[bool, List[str]]:
522    def smart_toggle(self, action: str) -> Tuple[bool, List[str]]:
523        """
524        A basic function to enable/disable SMART on device.
525
526        # Args:
527        * **action (str):** Can be either 'on'(for enabling) or 'off'(for disabling).
528
529        # Returns"
530        * **(bool):** Return True (if action succeded) else False
531        * **(List[str]):** None if option succeded else contains the error message.
532        """
533        # Lets make the action verb all lower case
534        if self._interface == 'nvme':
535            return False, ['NVME devices do not currently support toggling SMART enabled']
536        action_lower = action.lower()
537        if action_lower not in ['on', 'off']:
538            return False, ['Unsupported action {0}'.format(action)]
539        # Now lets check if the device's smart enabled status is already that of what
540        # the supplied action is intending it to be. If so then just return successfully
541        if self.smart_enabled:
542            if action_lower == 'on':
543                return True, []
544        else:
545            if action_lower == 'off':
546                return True, []
547        if self._interface is not None:
548            raw, returncode = self.smartctl.generic_call(
549                ['-s', action_lower, '-d', self._interface, self.dev_reference])
550        else:
551            raw, returncode = self.smartctl.generic_call(
552                ['-s', action_lower, self.dev_reference])
553
554        if returncode != 0:
555            return False, raw
556        # if everything worked out so far lets perform an update() and check the result
557        self.update()
558        if action_lower == 'off' and self.smart_enabled:
559            return False, ['Failed to turn SMART off.']
560        if action_lower == 'on' and not self.smart_enabled:
561            return False, ['Failed to turn SMART on.']
562        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>):
564    def all_attributes(self, print_fn=print):
565        """
566        Prints the entire SMART attribute table, in a format similar to
567        the output of smartctl.
568        allows usage of custom print function via parameter print_fn by default uses print
569        """
570        header_printed = False
571        for attr in self.attributes:
572            if attr is not None:
573                if not header_printed:
574                    print_fn("{0:>3} {1:24}{2:4}{3:4}{4:4}{5:9}{6:8}{7:12}{8}"
575                             .format('ID#', 'ATTRIBUTE_NAME', 'CUR', 'WST', 'THR', 'TYPE', 'UPDATED', 'WHEN_FAIL',
576                                     'RAW'))
577                    header_printed = True
578                print_fn(attr)
579        if not header_printed:
580            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):
582    def all_selftests(self):
583        """
584        Prints the entire SMART self-test log, in a format similar to
585        the output of smartctl.
586        """
587        if self.tests:
588            all_tests = []
589            if smartctl_type(self._interface) == 'scsi':
590                header = "{0:3}{1:17}{2:23}{3:7}{4:14}{5:15}".format(
591                    'ID',
592                    'Test Description',
593                    'Status',
594                    'Hours',
595                    '1st_Error@LBA',
596                    '[SK  ASC  ASCQ]'
597                )
598            else:
599                header = ("{0:3}{1:17}{2:30}{3:5}{4:7}{5:17}".format(
600                    'ID',
601                    'Test_Description',
602                    'Status',
603                    'Left',
604                    'Hours',
605                    '1st_Error@LBA'))
606            all_tests.append(header)
607            for test in self.tests:
608                all_tests.append(str(test))
609
610            return all_tests
611        else:
612            no_tests = 'No self-tests have been logged for this device.'
613            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):
699    def get_selftest_result(self, output=None):
700        """
701        Refreshes a device's `pySMART.device.Device.tests` attribute to obtain
702        the latest test results. If a new test result is obtained, its content
703        is returned.
704
705        # Args:
706        * **output (str, optional):** If set to 'str', the string
707        representation of the most recent test result will be returned, instead
708        of a `Test_Entry` object.
709
710        # Returns:
711        * **(int):** Return status code. One of the following:
712            * 0 - Success. Object (or optionally, string rep) is attached.
713            * 1 - Self-test in progress. Must wait for it to finish.
714            * 2 - No new test results.
715            * 3 - The Self-test was Aborted by host
716        * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or
717        optionally it's string representation) if new data exists.  Status
718        message string on failure.
719        * **(int):** Estimate progress percantage of the running SMART selftest, if known.
720        Otherwise 'None'.
721        """
722        # SCSI self-test logs hold 20 entries while ATA logs hold 21
723        if smartctl_type(self._interface) == 'scsi':
724            maxlog = 20
725        else:
726            maxlog = 21
727        # If we looked only at the most recent test result we could be fooled
728        # by two short tests run close together (within the same hour)
729        # appearing identical. Comparing the length of the log adds some
730        # confidence until it maxes, as above. Comparing the least-recent test
731        # result greatly diminishes the chances that two sets of two tests each
732        # were run within an hour of themselves, but with 16-17 other tests run
733        # in between them.
734        if self.tests:
735            _first_entry = self.tests[0]
736            _len = len(self.tests)
737            _last_entry = self.tests[_len - 1]
738        else:
739            _len = 0
740        self.update()
741        # Since I have changed the update() parsing to DTRT to pickup currently
742        # running selftests we can now purely rely on that for self._test_running
743        # Thus check for that variable first and return if it is True with appropos message.
744        if self._test_running is True:
745            return 1, 'Self-test in progress. Please wait.', self._test_progress
746        # Check whether the list got longer (ie: new entry)
747        # If so return the newest test result
748        # If not, because it's max size already, check for new entries
749        if (
750                (len(self.tests) != _len) or
751                (
752                    len == maxlog and
753                    (
754                        _first_entry.type != self.tests[0].type or
755                        _first_entry.hours != self.tests[0].hours or
756                        _last_entry.type != self.tests[len(self.tests) - 1].type or
757                        _last_entry.hours != self.tests[len(
758                            self.tests) - 1].hours
759                    )
760                )
761        ):
762            return (
763                0 if 'Aborted' not in self.tests[0].status else 3,
764                str(self.tests[0]) if output == 'str' else self.tests[0],
765                None
766            )
767        else:
768            return 2, 'No new self-test results found.', None

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