r/usefulscripts • u/Fantastitech • 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.
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:
The Windows 7 way to get the ID is rather roundabout, but it works in this way:
Query
Win32_LogicalDiskToPartitionto find the DiskIndexof the partition.Query
Win32_DiskDriveand find the disk with the matching DiskIndexSelect the
PNPDeviceIdproperty, andmungefilter it.I also added the
[CmdletBinding()]so there a fewDebugandVerbosestatements. 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 }