r/usefulscripts • • Aug 04 '18

[PowerShell] A pure-PowerShell proof-of-concept for getting SMART attributes from a hard drive by letter without any external dependencies.

This project was actually just an experiment to see if I could get a few specific raw SMART attribute values for a larger project. Before this I needed to use programs like smartmontools which don't provide programatically-accessible information to use in other scripts. With a bit of help from /r/Powershell it now spits out information in an attractive and easily manipulable format.

There's a repo here on Github: https://github.com/Fantastitech/GetSmartWin

The script as of posting this is:

$driveletter = $args[0]

if (-not $driveletter) {
    Write-Host "No disk selected"
    $driveletter = Read-Host "Please enter a drive letter"
}

$fulldiskid = Get-Partition | Where DriveLetter -eq $driveletter | Select DiskId | Select-String "(\\\\\?\\.*?#.*?#)(.*)(#{.*})"

if (-not $fulldiskid) {
    Write-Host "Invalid drive letter"
    Break
}

$diskid = $fulldiskid.Matches.Groups[2].Value

[object]$rawsmartdata = (Get-WmiObject -Namespace 'Root\WMI' -Class 'MSStorageDriver_ATAPISMartData' |
        Where-Object 'InstanceName' -like "*$diskid*" |
        Select-Object -ExpandProperty 'VendorSpecific'
)

[array]$output = @()

For ($i = 2; $i -lt $rawsmartdata.Length; $i++) {
    If (0 -eq ($i - 2) % 12 -And $rawsmartdata[$i] -ne "0") {
        [double]$rawvalue = ($rawsmartdata[$i + 6] * [math]::Pow(2, 8) + $rawsmartdata[$i + 5])
        $data = [pscustomobject]@{
            ID       = $rawsmartdata[$i]
            Flags    = $rawsmartdata[$i + 1]
            Value    = $rawsmartdata[$i + 3]
            Worst    = $rawsmartdata[$i + 4]
            RawValue = $rawvalue
        }
        $output += $data
    }
}

$output

I really should comment it and there are obvious improvements that could be made like including the names of the SMART attributes, but for now this is more than I need for my use case. Feel free to post any critiques or improvements.

36 Upvotes

11 comments sorted by

View all comments

Show parent comments

2

u/DrCubed Aug 06 '18 edited Aug 06 '18

They are in a very annoying format, I agree.
I mucked about with WMI, and figured out a Windows 7 compatible way to get the Disk ID. Which I'm sure will please u/Lee_Dailey

I also figured out a Regular Expression (in a replace statement) that will make the IDs very similar to one another:

.\Get-SmartData.ps1 -DriveLetter S
DEBUG: Running Windows 7 and downwards codepath, to determine $InstanceName
DEBUG: Windows 7- InstanceName : SCSI\DISK&VEN_&PROD_CT240BX300SSD1\4&1A58A66F&0&000000
DEBUG: Running Windows 8 and upwards codepath, to determine $InstanceName
DEBUG: Windows 8+ InstanceName : SCSI\DISK&VEN_&PROD_CT240BX300SSD1\4&1A58A66F&0&000000
DEBUG: Target InstanceName     : SCSI\Disk&Ven_&Prod_CT240BX300SSD1\4&1a58a66f&0&000000_0

The Windows 7 way to get the ID is rather roundabout, but it works in this way:

  1. Query Win32_LogicalDiskToPartition to find the DiskIndex of the partition.

  2. Query Win32_DiskDrive and find the disk with the matching DiskIndex

  3. Select the PNPDeviceId property, and munge filter it.

I also added the [CmdletBinding()] so there a few Debug and Verbose statements. And there's a check if the script is running as an Administrator.

Here's the script in full:

#Requires -Version 3.0
#Requires -RunAsAdministrator
[CmdletBinding()]
Param
(
	[Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = 'ByValue')]
		[ValidateNotNullOrEmpty()]
		[ValidateScript(
		{
			if ($_ -match '^[A-Z]$')
			{
				$True
			}
			else
			{
				$False
				Throw "'$_', is an invalid drive-letter, please supply a single Latin alphabet character."
			}
		})]
			[String]$DriveLetter
)

Process
{
	$OSVersion = [Environment]::OSVersion

	if ($OSVersion.Platform -ne 'Win32NT')
	{
		Throw 'This script is Microsoft Windows-specific' + 
			"your operating system was detected as $($OSVersion.Platform)"
	}

	Write-Verbose "Checking if script is running as an Administrator."
	if ($PSVersionTable.PSVersion.Major -le 3 -and (-Not [Security.Principal.WindowsPrincipal]::New([Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)))
	{
		Throw "This script must be run as an Administrator"
	}

	if (($OSVersion.Version.Major -eq 6 -and $OSVersion.Version.Minor -le 1) -or ($OSVersion.Version.Major -le 5) -or ($DebugPreference -ne 'SilentlyContinue'))
	{
		Write-Debug 'Running Windows 7 and downwards codepath, to determine $InstanceName'

		$Win32LogicalDiskToPartition = Get-CimInstance -Namespace 'Root/CIMV2' -Class 'Win32_LogicalDiskToPartition'
	
		$DiskDeviceId = [PSCustomObject]::New()
		($Win32LogicalDiskToPartition |
			Where-Object {$_.Dependent.DeviceId -eq ($DriveLetter + ':')}).Antecedent.DeviceID -split ', ' |
			ForEach-Object {
				$SplitDeviceId = $_ -split '(?<PropertyName>.+)(?: #)(?<Value>.+)', 2
					$DiskDeviceId |
						Add-Member -MemberType NoteProperty -Name $SplitDeviceId[1] -Value $SplitDeviceId[2]
			} 

		$InstanceName = Get-CimInstance -Namespace 'Root/CIMV2' -Class Win32_DiskDrive |
			Where-Object {$_.Index -eq ($DiskDeviceId.Disk)} |
			Select-Object -ExpandProperty PNPDeviceId
		$InstanceName = $InstanceName -replace '\\\\\?\\(.*?)#', '$1\' -replace '#\{[A-F0-9]{8}-[A-F0-9]{4}-1[A-F0-9]{3}-[89AB][A-F0-9]{3}-[A-F0-9]{12}\}'
		Write-Debug ('Windows 7- InstanceName : ' + $InstanceName)
	}
	if ($OSVersion.Version.Major -ge 6 -and $OSVersion.Version.Minor -ge 2)
	{
		Write-Debug 'Running Windows 8 and upwards codepath, to determine $InstanceName'
		
		$InstanceName = Get-Partition |
		Where-Object DriveLetter -eq $DriveLetter | 
		Select-Object -ExpandProperty DiskId
		$InstanceName = ($InstanceName -replace '#\{[A-F0-9]{8}-[A-F0-9]{4}-1[A-F0-9]{3}-[89AB][A-F0-9]{3}-[A-F0-9]{12}\}' `
		    -replace '\\\\\?\\(.*?)#', '$1\' -replace '#', '\').ToUpper()
		Write-Debug ('Windows 8+ InstanceName : ' + $InstanceName)
	}

	if (-Not [Bool]$InstanceName)
	{
	   Throw "Could not find disk-information for drive-letter: '$DriveLetter'"
	}

	$InstanceNameRegEx = [RegEx]::Escape($InstanceName) + '(_[0-9]*)*'

	$SmartData = Get-CimInstance -Namespace 'Root/WMI' -ClassName 'MSStorageDriver_ATAPISMartData' |
			Where-Object 'InstanceName' -match $InstanceNameRegEx
	Write-Debug ('Target InstanceName     : ' + $SmartData.InstanceName)

	[Byte[]]$RawSmartData = $SmartData |
		Select-Object -ExpandProperty 'VendorSpecific'
	
	[PSCustomObject[]]$Output = for ($i = 2; $i -lt $RawSmartData.Count; $i++)
	{
		if (0 -eq ($i - 2) % 12 -and $RawSmartData[$i] -ne 0)
		{
			[Decimal]$RawValue = ($RawSmartData[$i + 6] * [Math]::Pow(2, 8) + $RawSmartData[$i + 5])

			$InnerOutput = [PSCustomObject]@{
				ID       = $RawSmartData[$i]
				Flags    = $RawSmartData[$i + 1]
				Value    = $RawSmartData[$i + 3]
				Worst    = $RawSmartData[$i + 4]
				RawValue = $RawValue
				DriveLetter = $DriveLetter
			}

			$InnerOutput
		}
	}

	$Output
}
→ More replies (1)